From 917d4d1d285420fabb766620a7ae576c36082595 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sun, 2 Aug 2026 22:18:28 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=BA=86=E5=85=AC=E5=8F=B8?= =?UTF-8?q?=E5=AE=9A=E5=88=B6=E5=86=85=E5=AE=B9=E9=9A=94=E7=A6=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/auth.ts | 6 +- src/api/request.ts | 8 +- src/components/RoleSwitcher.vue | 405 ++++++++++++++++++++++++++++++++ src/layouts/DefaultLayout.vue | 60 ++--- src/router/index.ts | 72 ++++-- src/stores/user.ts | 21 +- src/views/LegacyPage.vue | 55 +++++ 7 files changed, 564 insertions(+), 63 deletions(-) create mode 100644 src/components/RoleSwitcher.vue create mode 100644 src/views/LegacyPage.vue diff --git a/src/api/auth.ts b/src/api/auth.ts index 15f059c..d41b0c3 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -55,9 +55,9 @@ export const getUserRoles = (): Promise => { return get<{ roles: RoleItem[] }>('/Auth/GetUserRoles').then((res) => res.roles || []) } -/** 切换当前角色 */ -export const switchRole = (role: string): Promise => { - return post('/Auth/SwitchRole', { Role: role }) +/** 切换当前角色(后端契约:JSON body 传 RoleUUID) */ +export const switchRole = (role: RoleItem): Promise => { + return post('/Auth/SwitchRole', { RoleUUID: role.RoleUUID }) } /** 获取当前登录用户信息(silent=true 时失败不弹全局提示,用于登录页会话恢复) */ diff --git a/src/api/request.ts b/src/api/request.ts index a5ea874..4594308 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -8,6 +8,8 @@ import { ElMessage, ElMessageBox } from 'element-plus' import type { ApiResult } from '@/types/api' import { getServerScopeParams } from '@/utils/serverScope' import router from '@/router' +import { useUserStore } from '@/stores/user' +import { useTabsStore } from '@/stores/tabs' /** 后端响应统一格式 */ type BackendResponse = AxiosResponse> @@ -113,7 +115,11 @@ function handleLoginExpired(): void { confirmButtonText: '重新登录', }) .then(() => { - // 清除本地用户状态 + // 清除本地用户状态(内存态 + localStorage) + // 必须清内存态:登录页守卫依据 userStore.isLoggedIn 判断,若不清内存, + // 点"重新登录"会被守卫重定向回 /dashboard,表现为跳不到登录页 + useUserStore().reset() + useTabsStore().reset() localStorage.removeItem('gerp-user') // 使用 Vue Router 跳转登录页 // hash 路由模式下 window.location.href 会保留 #/path,导致 Vue Router 按 hash 误路由到业务页("直接显示后台") diff --git a/src/components/RoleSwitcher.vue b/src/components/RoleSwitcher.vue new file mode 100644 index 0000000..2a9b23d --- /dev/null +++ b/src/components/RoleSwitcher.vue @@ -0,0 +1,405 @@ + + + + + diff --git a/src/layouts/DefaultLayout.vue b/src/layouts/DefaultLayout.vue index 358bae3..19a3dd2 100644 --- a/src/layouts/DefaultLayout.vue +++ b/src/layouts/DefaultLayout.vue @@ -10,14 +10,14 @@ */ import { computed, onMounted, ref, watch } from 'vue' import { useRoute, useRouter } from 'vue-router' -import { ElMessage } from 'element-plus' import { useAppStore } from '@/stores/app' import { useUserStore } from '@/stores/user' import { useTabsStore } from '@/stores/tabs' -import { getAvatarUrl, type RoleItem } from '@/api/auth' +import { getAvatarUrl } from '@/api/auth' import TabBar from '@/components/TabBar.vue' import RouteProgress from '@/components/RouteProgress.vue' import PageMask from '@/components/PageMask.vue' +import RoleSwitcher from '@/components/RoleSwitcher.vue' const appStore = useAppStore() const userStore = useUserStore() @@ -42,12 +42,12 @@ const roleLabel = computed(() => { }) // === 身份切换 === -const roleSwitching = ref(false) const roles = computed(() => userStore.roles) +const roleSwitcherVisible = ref(false) onMounted(() => { appStore.initTheme() - // 获取角色列表(用于身份切换) + // 获取角色列表(用于切换身份对话框) if (userStore.isLoggedIn) { userStore.fetchRoles().catch(() => { // 获取角色失败静默处理 @@ -55,20 +55,13 @@ onMounted(() => { } }) -/** 切换身份/角色(role 为 RoleItem 对象) */ -async function handleSwitchRole(role: RoleItem): Promise { - if (role.RoleName === userStore.currentRole || roleSwitching.value) return - roleSwitching.value = true - try { - await userStore.switchRole(role) - tabsStore.reset() - ElMessage.success(`已切换到:${role.RoleName}`) - router.push('/dashboard') - } catch { - ElMessage.error('身份切换失败') - } finally { - roleSwitching.value = false - } +/** 打开切换角色对话框 */ +function openRoleSwitcher(): void { + // 打开前刷新角色列表,保证最新 + userStore.fetchRoles().catch(() => { + // 静默处理 + }) + roleSwitcherVisible.value = true } function handleMenuSelect(index: string): void { @@ -122,6 +115,9 @@ function handleUserCommand(cmd: string): void { case 'settings': router.push('/system/settings') break + case 'switchRole': + openRoleSwitcher() + break } } @@ -214,27 +210,10 @@ const isFaIcon = (icon: string | undefined | null): boolean => { - -
-
- - 切换身份 -
-
-
- - - {{ role.RoleName }} - -
-
-
+ + + 切换角色 + 个人中心 @@ -329,6 +308,9 @@ const isFaIcon = (icon: string | undefined | null): boolean => { + + + diff --git a/src/router/index.ts b/src/router/index.ts index a149f4f..c9d975d 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -34,13 +34,13 @@ export const staticRoutes: RouteRecordRaw[] = [ path: 'workorder/admin', name: 'WorkorderAdmin', component: () => import('@/views/workorder/Admin.vue'), - meta: { title: '工单管理', icon: 'Tickets', requiresAuth: true }, + meta: { title: '工单管理', icon: 'Tickets', requiresAuth: true, menuGuarded: true }, }, { path: 'workorder/my', name: 'WorkorderMy', component: () => import('@/views/workorder/My.vue'), - meta: { title: '我的工单', icon: 'Document', requiresAuth: true }, + meta: { title: '我的工单', icon: 'Document', requiresAuth: true, menuGuarded: true }, }, { path: 'system/roles', @@ -58,44 +58,44 @@ export const staticRoutes: RouteRecordRaw[] = [ path: 'inventory/products', name: 'InventoryProducts', component: () => import('@/views/inventory/Products.vue'), - meta: { title: '产品管理', icon: 'Goods', requiresAuth: true }, + meta: { title: '产品管理', icon: 'Goods', requiresAuth: true, menuGuarded: true }, }, { path: 'sales/orders', name: 'SalesOrders', component: () => import('@/views/sales/Orders.vue'), - meta: { title: '销售订单', icon: 'ShoppingCart', requiresAuth: true }, + meta: { title: '销售订单', icon: 'ShoppingCart', requiresAuth: true, menuGuarded: true }, }, // === 阶段 2:Company 列表页 === { path: 'company/tenant', name: 'CompanyTenant', component: () => import('@/views/company/Tenant.vue'), - meta: { title: '租户管理', icon: 'OfficeBuilding', requiresAuth: true }, + meta: { title: '租户管理', icon: 'OfficeBuilding', requiresAuth: true, menuGuarded: true }, }, { path: 'company/company', name: 'CompanyCompany', component: () => import('@/views/company/Company.vue'), - meta: { title: '公司管理', icon: 'Management', requiresAuth: true }, + meta: { title: '公司管理', icon: 'Management', requiresAuth: true, menuGuarded: true }, }, { path: 'company/department', name: 'CompanyDepartment', component: () => import('@/views/company/Department.vue'), - meta: { title: '部门管理', icon: 'Share', requiresAuth: true }, + meta: { title: '部门管理', icon: 'Share', requiresAuth: true, menuGuarded: true }, }, { path: 'company/workgroup', name: 'CompanyWorkgroup', component: () => import('@/views/company/Workgroup.vue'), - meta: { title: '工作组管理', icon: 'UserFilled', requiresAuth: true }, + meta: { title: '工作组管理', icon: 'UserFilled', requiresAuth: true, menuGuarded: true }, }, { path: 'company/role', name: 'CompanyRole', component: () => import('@/views/company/Role.vue'), - meta: { title: '公司角色', icon: 'Avatar', requiresAuth: true }, + meta: { title: '公司角色', icon: 'Avatar', requiresAuth: true, menuGuarded: true }, }, // === 阶段 3:详情页 === { @@ -158,19 +158,19 @@ export const staticRoutes: RouteRecordRaw[] = [ path: 'system/message', name: 'SystemMessage', component: () => import('@/views/system/Message.vue'), - meta: { title: '站内信', icon: 'ChatDotRound', requiresAuth: true }, + meta: { title: '站内信', icon: 'ChatDotRound', requiresAuth: true, menuGuarded: true }, }, { path: 'system/tenant-create', name: 'SystemTenantCreate', component: () => import('@/views/system/TenantCreate.vue'), - meta: { title: '租户入驻', requiresAuth: true }, + meta: { title: '租户入驻', requiresAuth: true, menuGuarded: true }, }, { path: 'system/tenant-review', name: 'SystemTenantReview', component: () => import('@/views/system/TenantReview.vue'), - meta: { title: '租户审核', requiresAuth: true }, + meta: { title: '租户审核', requiresAuth: true, menuGuarded: true }, }, { path: 'system/payment-config', @@ -189,13 +189,13 @@ export const staticRoutes: RouteRecordRaw[] = [ path: 'doc/editor', name: 'DocEditor', component: () => import('@/views/doc/Editor.vue'), - meta: { title: '文档编辑器', requiresAuth: true }, + meta: { title: '文档编辑器', requiresAuth: true, menuGuarded: true }, }, { path: 'assets/manager', name: 'AssetsManager', component: () => import('@/views/assets/Manager.vue'), - meta: { title: '资产管理', requiresAuth: true }, + meta: { title: '资产管理', requiresAuth: true, menuGuarded: true }, }, // === 阶段 7:管理员工具 === { @@ -214,13 +214,13 @@ export const staticRoutes: RouteRecordRaw[] = [ path: 'system/menu', name: 'SystemMenu', component: () => import('@/views/system/Menu.vue'), - meta: { title: '菜单管理', requiresAuth: true }, + meta: { title: '菜单管理', requiresAuth: true, menuGuarded: true }, }, { path: 'system/database', name: 'SystemDatabase', component: () => import('@/views/system/Database.vue'), - meta: { title: '数据库管理', requiresAuth: true }, + meta: { title: '数据库管理', requiresAuth: true, menuGuarded: true }, }, { path: 'system/dbtable', @@ -228,6 +228,37 @@ export const staticRoutes: RouteRecordRaw[] = [ component: () => import('@/views/system/DbTable.vue'), meta: { title: '表结构管理', requiresAuth: true }, }, + // === 枢机协律(CreateE):旧页面 iframe 直载(LegacyPage) === + { + path: 'private/CreateE/shop', + name: 'PrivateCreateEShop', + component: () => import('@/views/LegacyPage.vue'), + meta: { title: '商店', requiresAuth: true, menuGuarded: true, legacySrc: '/pages/private/CreateE/shop.html' }, + }, + { + path: 'private/CreateE/Recharge', + name: 'PrivateCreateERecharge', + component: () => import('@/views/LegacyPage.vue'), + meta: { title: '储值充值', requiresAuth: true, menuGuarded: true, legacySrc: '/pages/private/CreateE/Recharge.html' }, + }, + { + path: 'private/CreateE/wallet', + name: 'PrivateCreateEWallet', + component: () => import('@/views/LegacyPage.vue'), + meta: { title: '钱包', requiresAuth: true, menuGuarded: true, legacySrc: '/pages/private/CreateE/wallet.html' }, + }, + { + path: 'private/CreateE/enterprise', + name: 'PrivateCreateEEnterprise', + component: () => import('@/views/LegacyPage.vue'), + meta: { title: '企业管理', requiresAuth: true, legacySrc: '/pages/private/CreateE/enterprise.html' }, + }, + { + path: 'private/CreateE/StoreValue', + name: 'PrivateCreateEStoreValue', + component: () => import('@/views/LegacyPage.vue'), + meta: { title: '储值查看', requiresAuth: true, menuGuarded: true, legacySrc: '/pages/private/CreateE/StoreValue.html' }, + }, // === 阶段 8:SKU(移动端 C 端,待决策是否拆分独立工程) === { path: 'sku', @@ -322,6 +353,15 @@ router.beforeEach(async (to, _from, next) => { } } + // 菜单守卫:menuGuarded 路由(后端菜单驱动的业务页)必须出现在当前用户可见菜单树中 + // 用途:URL 直达私有页(如 #/private/CreateE/wallet)时,若菜单树不含该路径(当前角色 + // 公司无权),则拦截到 404,避免绕过菜单过滤直接访问公司私有业务 + if (to.meta.menuGuarded && !userStore.hasMenuPath(to.path)) { + tabsStore.removeTab(to.path) + next({ name: 'NotFound', replace: true }) + return + } + next() }) diff --git a/src/stores/user.ts b/src/stores/user.ts index dacd3c3..e02c258 100644 --- a/src/stores/user.ts +++ b/src/stores/user.ts @@ -69,6 +69,20 @@ export const useUserStore = defineStore('user', () => { return result }) + /** 目标路径是否在用户可见菜单树中(含子菜单;供路由守卫校验 URL 直达私有页) */ + function hasMenuPath(path: string): boolean { + if (!path) return false + const normalized = path.split('?')[0].replace(/\/+$/, '') + const walk = (items: MenuItem[]): boolean => { + for (const item of items) { + if (item.Path && item.Path === normalized) return true + if (item.Children?.length && walk(item.Children)) return true + } + return false + } + return walk(menus.value) + } + // === 动作 === /** 登录 */ async function login(params: LoginParams): Promise { @@ -135,10 +149,8 @@ export const useUserStore = defineStore('user', () => { } /** 切换当前角色(身份切换) */ - async function switchRole(role: RoleItem | string): Promise { - // 传对象时取 RoleName(后端 SwitchRole 期望角色名) - const roleName = typeof role === 'string' ? role : role.RoleName - await apiSwitchRole(roleName) + async function switchRole(role: RoleItem): Promise { + await apiSwitchRole(role) // 刷新用户信息和菜单(不同角色可能有不同菜单权限) await fetchUserInfo() await fetchMenus() @@ -198,6 +210,7 @@ export const useUserStore = defineStore('user', () => { avatar, displayName, flatMenus, + hasMenuPath, // 动作 login, logout, diff --git a/src/views/LegacyPage.vue b/src/views/LegacyPage.vue new file mode 100644 index 0000000..a94bb16 --- /dev/null +++ b/src/views/LegacyPage.vue @@ -0,0 +1,55 @@ + + +