Procházet zdrojové kódy

feat(bx-mobile): guard routes by portal menuCodes and drop bx admin IAM pages

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu před 2 měsíci
rodič
revize
a0eebfdbd3

+ 116 - 25
bx-mobile/src/router/index.ts

@@ -1,4 +1,5 @@
 import { createRouter, createWebHistory } from 'vue-router'
+import { showToast } from 'vant'
 import { useAuthStore } from '@/stores/auth'
 
 const routes = [
@@ -13,29 +14,110 @@ const routes = [
     component: () => import('@/views/home/HomeLayout.vue'),
     children: [
       { path: '', name: 'Home', component: () => import('@/views/home/HomePage.vue') },
-      { path: 'mine', name: 'Mine', component: () => import('@/views/mine/MinePage.vue') },
-      { path: 'approval', name: 'Approval', component: () => import('@/views/approval/ApprovalPage.vue') },
-      { path: 'approval/:bizType/:id', name: 'ApprovalDetail', component: () => import('@/views/approval/ApprovalDetailPage.vue') },
-      { path: 'advance/list', name: 'AdvanceList', component: () => import('@/views/advance/AdvanceListPage.vue') },
-      { path: 'advance/apply', name: 'AdvanceApply', component: () => import('@/views/advance/AdvanceApplyPage.vue') },
+      {
+        path: 'mine',
+        name: 'Mine',
+        component: () => import('@/views/mine/MinePage.vue'),
+        meta: { menuCodes: ['bx:viewMine:list'] },
+      },
+      {
+        path: 'approval',
+        name: 'Approval',
+        component: () => import('@/views/approval/ApprovalPage.vue'),
+        meta: { menuCodes: ['bx:viewTodo:list'] },
+      },
+      {
+        path: 'approval/:bizType/:id',
+        name: 'ApprovalDetail',
+        component: () => import('@/views/approval/ApprovalDetailPage.vue'),
+        meta: { menuCodes: ['bx:viewTodo:list', 'bx:advanceApprove:list', 'bx:reimburseApprove:list'] },
+      },
+      {
+        path: 'advance/list',
+        name: 'AdvanceList',
+        component: () => import('@/views/advance/AdvanceListPage.vue'),
+        meta: { menuCodes: ['bx:advanceApply:list'] },
+      },
+      {
+        path: 'advance/apply',
+        name: 'AdvanceApply',
+        component: () => import('@/views/advance/AdvanceApplyPage.vue'),
+        meta: { menuCodes: ['bx:advanceApply:list'] },
+      },
       /* 须放在 advance/:id 之前,否则 approve/issue 会被当成动态 id */
-      { path: 'advance/approve', name: 'AdvanceApprove', component: () => import('@/views/advance/AdvanceApprovePage.vue'), meta: { roles: ['ADMIN', 'FINANCE', 'GENERAL_MANAGER'] } },
-      /* 与 bx_menu_role 一致:ADMIN、FINANCE 均分配「备用金发放管理」 */
-      { path: 'advance/issue', name: 'AdvanceIssue', component: () => import('@/views/advance/AdvanceIssuePage.vue'), meta: { roles: ['ADMIN', 'FINANCE'] } },
-      { path: 'advance/:id', name: 'AdvanceDetail', component: () => import('@/views/advance/AdvanceDetailPage.vue') },
-      { path: 'reimburse/list', name: 'ReimburseList', component: () => import('@/views/reimburse/ReimburseListPage.vue') },
-      { path: 'reimburse/apply', name: 'ReimburseApply', component: () => import('@/views/reimburse/ReimburseApplyPage.vue') },
-      { path: 'reimburse/approve', name: 'ReimburseApprove', component: () => import('@/views/reimburse/ReimburseApprovePage.vue'), meta: { roles: ['ADMIN', 'FINANCE', 'GENERAL_MANAGER'] } },
-      { path: 'reimburse/:id', name: 'ReimburseDetail', component: () => import('@/views/reimburse/ReimburseDetailPage.vue') },
-      { path: 'payment', name: 'Payment', component: () => import('@/views/payment/PaymentPage.vue'), meta: { roles: ['ADMIN', 'FINANCE'] } },
-      { path: 'all', name: 'All', component: () => import('@/views/all/AllPage.vue'), meta: { roles: ['ADMIN', 'GENERAL_MANAGER'] } },
-      { path: 'admin', name: 'AdminIndex', component: () => import('@/views/admin/AdminIndexPage.vue'), meta: { roles: ['ADMIN'] } },
-      { path: 'admin/users', name: 'UserManage', component: () => import('@/views/admin/UserManagePage.vue'), meta: { roles: ['ADMIN'] } },
-      { path: 'admin/roles', name: 'RoleManage', component: () => import('@/views/admin/RoleManagePage.vue'), meta: { roles: ['ADMIN'] } },
-      { path: 'admin/menus', name: 'MenuManage', component: () => import('@/views/admin/MenuManagePage.vue'), meta: { roles: ['ADMIN'] } },
-      { path: 'admin/expenses', name: 'ExpenseProject', component: () => import('@/views/admin/ExpenseProjectPage.vue'), meta: { roles: ['ADMIN'] } },
-      { path: 'admin/wf-advance', name: 'WfAdvance', component: () => import('@/views/admin/WfAdvancePage.vue'), meta: { roles: ['ADMIN'] } },
-      { path: 'admin/wf-reimburse', name: 'WfReimburse', component: () => import('@/views/admin/WfReimbursePage.vue'), meta: { roles: ['ADMIN'] } },
+      {
+        path: 'advance/approve',
+        name: 'AdvanceApprove',
+        component: () => import('@/views/advance/AdvanceApprovePage.vue'),
+        meta: { menuCodes: ['bx:advanceApprove:list'] },
+      },
+      {
+        path: 'advance/issue',
+        name: 'AdvanceIssue',
+        component: () => import('@/views/advance/AdvanceIssuePage.vue'),
+        meta: { menuCodes: ['bx:advanceIssue:list'] },
+      },
+      {
+        path: 'advance/:id',
+        name: 'AdvanceDetail',
+        component: () => import('@/views/advance/AdvanceDetailPage.vue'),
+        meta: {
+          menuCodes: [
+            'bx:advanceApply:list',
+            'bx:advanceApprove:list',
+            'bx:advanceIssue:list',
+            'bx:viewMine:list',
+            'bx:viewTodo:list',
+            'bx:viewAll:list',
+          ],
+        },
+      },
+      {
+        path: 'reimburse/list',
+        name: 'ReimburseList',
+        component: () => import('@/views/reimburse/ReimburseListPage.vue'),
+        meta: { menuCodes: ['bx:reimburseApply:list'] },
+      },
+      {
+        path: 'reimburse/apply',
+        name: 'ReimburseApply',
+        component: () => import('@/views/reimburse/ReimburseApplyPage.vue'),
+        meta: { menuCodes: ['bx:reimburseApply:list'] },
+      },
+      {
+        path: 'reimburse/approve',
+        name: 'ReimburseApprove',
+        component: () => import('@/views/reimburse/ReimburseApprovePage.vue'),
+        meta: { menuCodes: ['bx:reimburseApprove:list'] },
+      },
+      {
+        path: 'reimburse/:id',
+        name: 'ReimburseDetail',
+        component: () => import('@/views/reimburse/ReimburseDetailPage.vue'),
+        meta: {
+          menuCodes: [
+            'bx:reimburseApply:list',
+            'bx:reimburseApprove:list',
+            'bx:payment:list',
+            'bx:viewMine:list',
+            'bx:viewTodo:list',
+            'bx:viewAll:list',
+          ],
+        },
+      },
+      {
+        path: 'payment',
+        name: 'Payment',
+        component: () => import('@/views/payment/PaymentPage.vue'),
+        meta: { menuCodes: ['bx:payment:list'] },
+      },
+      {
+        path: 'all',
+        name: 'All',
+        component: () => import('@/views/all/AllPage.vue'),
+        meta: { menuCodes: ['bx:viewAll:list'] },
+      },
+      /* P1:不注册 invoice/*、admin 配置与 IAM(users/roles/menus) */
       { path: 'profile', name: 'Profile', component: () => import('@/views/profile/ProfilePage.vue') },
     ],
   },
@@ -50,9 +132,18 @@ router.beforeEach(async (to, _from, next) => {
   const auth = useAuthStore()
   if (to.meta?.public) return next()
   if (!auth.isLoggedIn) return next('/login')
-  if (!auth.user) await auth.fetchUser()
-  const roles = to.meta?.roles as string[] | undefined
-  if (roles?.length && !roles.some((r: string) => auth.roleCodes.includes(r))) {
+  if (!auth.user) {
+    try {
+      await auth.fetchUser()
+    } catch {
+      return next('/login')
+    }
+  }
+  if (!auth.isLoggedIn) return next('/login')
+
+  const codes = to.meta?.menuCodes as string[] | undefined
+  if (codes?.length && !codes.some((c) => auth.can(c))) {
+    showToast('无权限')
     return next('/')
   }
   next()

+ 42 - 25
bx-mobile/src/views/profile/ProfilePage.vue

@@ -28,20 +28,37 @@
       <van-cell title="修改密码" icon="lock" is-link @click="showPasswordDialog = true" />
     </van-cell-group>
 
-    <!-- Finance / approver actions -->
-    <template v-if="isApprover">
-      <van-cell-group inset class="group-block" title="审批管理">
-        <van-cell title="备用金审批" icon="cash-back-record" is-link to="/advance/approve" />
-        <van-cell title="报销审批" icon="description" is-link to="/reimburse/approve" />
-        <van-cell title="发放管理" icon="balance-list-o" is-link to="/advance/issue" />
-        <van-cell title="全部记录" icon="records" is-link to="/all" />
-      </van-cell-group>
-    </template>
-
-    <!-- Admin section -->
-    <template v-if="auth.isAdmin">
-      <van-cell-group inset class="group-block" title="系统">
-        <van-cell title="系统管理" icon="setting-o" is-link to="/admin" />
+    <!-- 按门户 menuCode 显隐快捷入口 -->
+    <template v-if="showQuickNav">
+      <van-cell-group inset class="group-block" title="快捷入口">
+        <van-cell
+          v-if="auth.can('bx:advanceApprove:list')"
+          title="备用金审批"
+          icon="cash-back-record"
+          is-link
+          to="/advance/approve"
+        />
+        <van-cell
+          v-if="auth.can('bx:reimburseApprove:list')"
+          title="报销审批"
+          icon="description"
+          is-link
+          to="/reimburse/approve"
+        />
+        <van-cell
+          v-if="auth.can('bx:advanceIssue:list')"
+          title="发放管理"
+          icon="balance-list-o"
+          is-link
+          to="/advance/issue"
+        />
+        <van-cell
+          v-if="auth.can('bx:viewAll:list')"
+          title="全部记录"
+          icon="records"
+          is-link
+          to="/all"
+        />
       </van-cell-group>
     </template>
 
@@ -128,10 +145,12 @@ const passwordForm = reactive({
   confirmPassword: '',
 })
 
-const APPROVER_ROLES = ['ADMIN', 'FINANCE', 'GENERAL_MANAGER']
-
-const isApprover = computed(() =>
-  APPROVER_ROLES.some((r) => auth.roleCodes.includes(r)),
+const showQuickNav = computed(
+  () =>
+    auth.can('bx:advanceApprove:list') ||
+    auth.can('bx:reimburseApprove:list') ||
+    auth.can('bx:advanceIssue:list') ||
+    auth.can('bx:viewAll:list'),
 )
 
 const avatarChar = computed(() => {
@@ -140,13 +159,11 @@ const avatarChar = computed(() => {
 })
 
 const roleText = computed(() => {
-  const codes = auth.roleCodes
-  const parts: string[] = []
-  if (codes.includes('ADMIN')) parts.push('管理员')
-  if (codes.includes('GENERAL_MANAGER')) parts.push('总经理')
-  if (codes.includes('FINANCE')) parts.push('财务人员')
-  if (codes.includes('EMPLOYEE')) parts.push('员工')
-  return parts.join('、') || '未分配角色'
+  if (auth.isSuperAdmin || auth.allMenus) return '超级管理员'
+  const role = auth.user?.role
+  if (role) return role
+  const roles = auth.user?.roles || []
+  return roles.length ? roles.join('、') : '已登录'
 })
 
 async function handleLogout() {