Просмотр исходного кода

feat(bx-mobile): add bxClient and retarget business APIs to shanxia2.0

Remove legacy bx user/role/menu/auth APIs. Wire auth store to portal
temporarily so the app still typechecks; IAM admin pages use local stubs
until Task 6 drops their routes.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 месяцев назад
Родитель
Сommit
3f1faeae34

+ 96 - 0
bx-mobile/src/api/bxClient.ts

@@ -0,0 +1,96 @@
+import axios, { type InternalAxiosRequestConfig } from 'axios'
+import { showToast } from 'vant'
+import { portalAuthApi } from './portalAuth'
+
+export class BizError extends Error {
+  code?: number
+  constructor(message: string, code?: number) {
+    super(message)
+    this.code = code
+    this.name = 'BizError'
+  }
+}
+
+const BX_COLLECTION_ROOT =
+  /^\/(reimburses|advances|payments|workflows|expense-items|invoices|invoice-wallets|invoice-ledgers)$/
+
+function normalizeBxUrl(url?: string): string | undefined {
+  if (!url || url.endsWith('/')) return url
+  return BX_COLLECTION_ROOT.test(url) ? `${url}/` : url
+}
+
+const bxClient = axios.create({
+  baseURL: import.meta.env.VITE_API_URL || '/api',
+  timeout: 0,
+  headers: { 'Content-Type': 'application/json' },
+})
+
+let refreshing: Promise<boolean> | null = null
+
+async function tryRefresh(): Promise<boolean> {
+  const rt = localStorage.getItem('refreshToken')
+  if (!rt) return false
+  try {
+    const res: any = await portalAuthApi.refreshToken(rt)
+    const token = res?.data?.token
+    const refreshToken = res?.data?.refreshToken
+    if (!token) return false
+    localStorage.setItem('accessToken', token)
+    if (refreshToken) localStorage.setItem('refreshToken', refreshToken)
+    return true
+  } catch {
+    return false
+  }
+}
+
+bxClient.interceptors.request.use((config: InternalAxiosRequestConfig) => {
+  if (config.url) config.url = normalizeBxUrl(config.url)
+  if (config.data instanceof FormData) {
+    delete (config.headers as any)['Content-Type']
+  }
+  const token = localStorage.getItem('accessToken')
+  if (token) config.headers.Authorization = `Bearer ${token}`
+  return config
+})
+
+bxClient.interceptors.response.use(
+  (res) => {
+    if (res.config.responseType === 'blob') return res.data
+    const body = res.data
+    if (body?.code !== undefined && body.code !== 0) {
+      showToast(body.msg || '请求失败')
+      return Promise.reject(new BizError(body.msg || '请求失败', body.code))
+    }
+    return body
+  },
+  async (err) => {
+    const original = err.config
+    if (err.response?.status === 401 && original && !(original as any)._retry) {
+      ;(original as any)._retry = true
+      if (!refreshing) {
+        refreshing = tryRefresh().finally(() => {
+          refreshing = null
+        })
+      }
+      const ok = await refreshing
+      if (ok) {
+        original.headers = original.headers || {}
+        original.headers.Authorization = `Bearer ${localStorage.getItem('accessToken')}`
+        return bxClient.request(original)
+      }
+      localStorage.removeItem('accessToken')
+      localStorage.removeItem('refreshToken')
+      showToast('登录已过期,请重新登录')
+      if (window.location.pathname !== '/login') {
+        window.location.href = '/login'
+      }
+    } else if (err.response?.status !== 401) {
+      const msg =
+        err.response?.data?.msg || err.response?.data?.detail || err.message || '网络异常'
+      showToast(msg)
+    }
+    return Promise.reject(err)
+  },
+)
+
+export default bxClient

+ 1 - 64
bx-mobile/src/api/client.ts

@@ -1,64 +1 @@
-import axios from 'axios'
-import { showToast } from 'vant'
-import { useAuthStore } from '@/stores/auth'
-
-const client = axios.create({
-  baseURL: '/api',
-  timeout: 0, // 0 = no timeout
-  headers: { 'Content-Type': 'application/json' },
-})
-
-client.interceptors.request.use((config) => {
-  const token = localStorage.getItem('token')
-  if (token) {
-    config.headers.Authorization = `Bearer ${token}`
-  }
-  return config
-})
-
-/**
- * Business error marker — page-level catch blocks should check this
- * to avoid double-toasting (interceptor already showed the toast).
- */
-export class BizError extends Error {
-  code?: number
-  constructor(message: string, code?: number) {
-    super(message)
-    this.code = code
-    this.name = 'BizError'
-  }
-}
-
-client.interceptors.response.use(
-  (res) => {
-    const body = res.data
-    if (body.code !== 0 && body.code !== undefined) {
-      showToast(body.msg || '请求失败')
-      return Promise.reject(new BizError(body.msg || '请求失败', body.code))
-    }
-    return body
-  },
-  (err) => {
-    if (err.response?.status === 401) {
-      const auth = useAuthStore()
-      auth.logout()
-      showToast('登录已过期,请重新登录')
-      if (window.location.pathname !== '/login') {
-        window.location.href = '/login'
-      }
-    } else if (err.response?.status === 403 && err.response?.data?.detail === '用户已被禁用') {
-      const auth = useAuthStore()
-      auth.logout()
-      showToast('账号已被禁用,请联系管理员')
-      if (window.location.pathname !== '/login') {
-        window.location.href = '/login'
-      }
-    } else {
-      const msg = err.response?.data?.msg || err.response?.data?.detail || err.message || '网络异常'
-      showToast(msg)
-    }
-    return Promise.reject(err)
-  },
-)
-
-export default client
+export { default, BizError } from './bxClient'

+ 76 - 116
bx-mobile/src/api/index.ts

@@ -1,122 +1,85 @@
-import client from './client'
+import bxClient from './bxClient'
 
-export const authApi = {
-  login: (data: { username: string; password: string }) =>
-    client.post('/auth/login', data),
-  register: (data: { username: string; password: string; realName: string }) =>
-    client.post('/auth/register', data),
-  me: () => client.get('/auth/me'),
-}
-
-export const userApi = {
-  list: (params?: Record<string, any>) => client.get('/users/', { params }),
-  dropdown: () => client.get('/users/dropdown'),
-  getById: (id: number) => client.get(`/users/${id}`),
-  create: (data: Record<string, any>) => client.post('/users/', data),
-  update: (id: number, data: Record<string, any>) => client.put(`/users/${id}`, data),
-  delete: (id: number) => client.delete(`/users/${id}`),
-  resetPassword: (id: number) => client.put(`/users/${id}/reset-password`),
-}
-
-export const roleApi = {
-  list: (params?: Record<string, any>) => client.get('/roles/', { params }),
-  getById: (id: number) => client.get(`/roles/${id}`),
-  create: (data: Record<string, any>) => client.post('/roles/', data),
-  update: (id: number, data: Record<string, any>) => client.put(`/roles/${id}`, data),
-  delete: (id: number) => client.delete(`/roles/${id}`),
-  getUsers: (roleId: number) => client.get(`/roles/${roleId}/users`),
-  assignUsers: (id: number, userIds: number[]) =>
-    client.put(`/roles/${id}/users`, { userIds }),
-  assignMenus: (id: number, menuIds: number[]) =>
-    client.put(`/roles/${id}/menus`, { menuIds }),
-}
-
-export const menuApi = {
-  mine: () => client.get('/menus/mine'),
-  tree: () => client.get('/menus/tree'),
-  getByRole: (roleId: number) => client.get(`/menus/role/${roleId}`),
-  getById: (id: number) => client.get(`/menus/${id}`),
-  create: (data: Record<string, any>) => client.post('/menus/', data),
-  update: (id: number, data: Record<string, any>) => client.put(`/menus/${id}`, data),
-  delete: (id: number) => client.delete(`/menus/${id}`),
-}
+export type BxId = string | number
 
 export const expenseItemApi = {
-  tree: () => client.get('/expense-items/tree'),
-  getById: (id: number) => client.get(`/expense-items/${id}`),
-  create: (data: Record<string, any>) => client.post('/expense-items/', data),
-  update: (id: number, data: Record<string, any>) => client.put(`/expense-items/${id}`, data),
-  delete: (id: number) => client.delete(`/expense-items/${id}`),
+  tree: () => bxClient.get('/expense-items/tree'),
+  getById: (id: BxId) => bxClient.get(`/expense-items/${id}`),
+  create: (data: Record<string, any>) => bxClient.post('/expense-items/', data),
+  update: (id: BxId, data: Record<string, any>) => bxClient.put(`/expense-items/${id}`, data),
+  delete: (id: BxId) => bxClient.delete(`/expense-items/${id}`),
 }
 
 export const workflowApi = {
-  list: (params?: Record<string, any>) => client.get('/workflows/', { params }),
-  getByBizType: (bizType: string) => client.get(`/workflows/biz/${bizType}`),
-  getById: (id: number) => client.get(`/workflows/${id}`),
-  create: (data: Record<string, any>) => client.post('/workflows/', data),
-  update: (id: number, data: Record<string, any>) => client.put(`/workflows/${id}`, data),
-  saveNodes: (id: number, nodes: Record<string, any>[]) =>
-    client.put(`/workflows/${id}/nodes`, nodes),
-  delete: (id: number) => client.delete(`/workflows/${id}`),
+  list: (params?: Record<string, any>) => bxClient.get('/workflows/', { params }),
+  getByBizType: (bizType: string) => bxClient.get(`/workflows/biz/${bizType}`),
+  getById: (id: BxId) => bxClient.get(`/workflows/${id}`),
+  create: (data: Record<string, any>) => bxClient.post('/workflows/', data),
+  update: (id: BxId, data: Record<string, any>) => bxClient.put(`/workflows/${id}`, data),
+  saveNodes: (id: BxId, nodes: Record<string, any>[]) =>
+    bxClient.put(`/workflows/${id}/nodes`, nodes),
+  delete: (id: BxId) => bxClient.delete(`/workflows/${id}`),
 }
 
 export const advanceApi = {
-  list: (params?: Record<string, any>) => client.get('/advances/', { params }),
-  my: (params?: Record<string, any>) => client.get('/advances/my', { params }),
-  myStats: () => client.get('/advances/my/stats'),
+  list: (params?: Record<string, any>) => bxClient.get('/advances/', { params }),
+  my: (params?: Record<string, any>) => bxClient.get('/advances/my', { params }),
+  myStats: () => bxClient.get('/advances/my/stats'),
   pendingApproval: (params?: Record<string, any>) =>
-    client.get('/advances/pending-approval', { params }),
+    bxClient.get('/advances/pending-approval', { params }),
   approvedByMe: (params?: Record<string, any>) =>
-    client.get('/advances/approved-by-me', { params }),
-  getById: (id: number) => client.get(`/advances/${id}`),
-  getApprovals: (id: number) => client.get(`/advances/${id}/approvals`),
-  create: (data: Record<string, any>) => client.post('/advances/', data),
-  update: (id: number, data: Record<string, any>) => client.put(`/advances/${id}`, data),
-  submit: (id: number) => client.post(`/advances/${id}/submit`),
-  approve: (id: number, data: { approvalOpinion?: string }) =>
-    client.post(`/advances/${id}/approve`, data),
-  reject: (id: number, data: { approvalOpinion?: string }) =>
-    client.post(`/advances/${id}/reject`, data),
-  delete: (id: number) => client.delete(`/advances/${id}`),
+    bxClient.get('/advances/approved-by-me', { params }),
+  getById: (id: BxId) => bxClient.get(`/advances/${id}`),
+  getApprovals: (id: BxId) => bxClient.get(`/advances/${id}/approvals`),
+  create: (data: Record<string, any>) => bxClient.post('/advances/', data),
+  update: (id: BxId, data: Record<string, any>) => bxClient.put(`/advances/${id}`, data),
+  submit: (id: BxId) => bxClient.post(`/advances/${id}/submit`),
+  approve: (id: BxId, data: { approvalOpinion?: string }) =>
+    bxClient.post(`/advances/${id}/approve`, data),
+  reject: (id: BxId, data: { approvalOpinion?: string }) =>
+    bxClient.post(`/advances/${id}/reject`, data),
+  delete: (id: BxId) => bxClient.delete(`/advances/${id}`),
 }
 
 export const reimburseApi = {
-  generateTitle: () => client.get('/reimburses/generate-title'),
-  list: (params?: Record<string, any>) => client.get('/reimburses/', { params }),
-  my: (params?: Record<string, any>) => client.get('/reimburses/my', { params }),
-  myStats: () => client.get('/reimburses/my/stats'),
+  generateTitle: () => bxClient.get('/reimburses/generate-title'),
+  list: (params?: Record<string, any>) => bxClient.get('/reimburses/', { params }),
+  my: (params?: Record<string, any>) => bxClient.get('/reimburses/my', { params }),
+  myStats: () => bxClient.get('/reimburses/my/stats'),
   pendingApproval: (params?: Record<string, any>) =>
-    client.get('/reimburses/pending-approval', { params }),
+    bxClient.get('/reimburses/pending-approval', { params }),
   approvedByMe: (params?: Record<string, any>) =>
-    client.get('/reimburses/approved-by-me', { params }),
-  getById: (id: number) => client.get(`/reimburses/${id}`),
-  getApprovals: (id: number) => client.get(`/reimburses/${id}/approvals`),
-  create: (data: Record<string, any>) => client.post('/reimburses/', data),
-  update: (id: number, data: Record<string, any>) => client.put(`/reimburses/${id}`, data),
-  submit: (id: number) => client.post(`/reimburses/${id}/submit`),
-  approve: (id: number, data: { approvalOpinion?: string }) =>
-    client.post(`/reimburses/${id}/approve`, data),
-  reject: (id: number, data: { approvalOpinion?: string }) =>
-    client.post(`/reimburses/${id}/reject`, data),
-  delete: (id: number) => client.delete(`/reimburses/${id}`),
+    bxClient.get('/reimburses/approved-by-me', { params }),
+  getById: (id: BxId) => bxClient.get(`/reimburses/${id}`),
+  getApprovals: (id: BxId) => bxClient.get(`/reimburses/${id}/approvals`),
+  create: (data: Record<string, any>) => bxClient.post('/reimburses/', data),
+  update: (id: BxId, data: Record<string, any>) => bxClient.put(`/reimburses/${id}`, data),
+  submit: (id: BxId) => bxClient.post(`/reimburses/${id}/submit`),
+  approve: (id: BxId, data: { approvalOpinion?: string }) =>
+    bxClient.post(`/reimburses/${id}/approve`, data),
+  reject: (id: BxId, data: { approvalOpinion?: string }) =>
+    bxClient.post(`/reimburses/${id}/reject`, data),
+  delete: (id: BxId) => bxClient.delete(`/reimburses/${id}`),
 }
 
 export const paymentApi = {
-  pending: (params?: Record<string, any>) => client.get('/payments/pending', { params }),
-  pay: (id: number, data?: { payRemark?: string; actualAmount?: number }) =>
-    client.post(`/payments/${id}/pay`, data),
-  offset: (id: number, data: { advanceId: number; offsetAmount: number; remark?: string }) =>
-    client.post(`/payments/${id}/offset`, data),
+  pending: (params?: Record<string, any>) => bxClient.get('/payments/pending', { params }),
+  pay: (id: BxId, data?: { payRemark?: string; actualAmount?: number }) =>
+    bxClient.post(`/payments/${id}/pay`, data),
+  offset: (
+    id: BxId,
+    data: { advanceId: BxId; offsetAmount: number; remark?: string },
+  ) => bxClient.post(`/payments/${id}/offset`, data),
   batchOffset: (
-    id: number,
+    id: BxId,
     data: {
-      items: Array<{ advanceId: number; offsetAmount: number }>
+      items: Array<{ advanceId: BxId; offsetAmount: number }>
       payAmount: number
       remark?: string
     },
-  ) => client.post(`/payments/${id}/batch-offset`, data),
+  ) => bxClient.post(`/payments/${id}/batch-offset`, data),
   advanceIssues: (params?: Record<string, any>) =>
-    client.get('/payments/advance-issues', {
+    bxClient.get('/payments/advance-issues', {
       params: params
         ? {
             page: params.page,
@@ -127,41 +90,38 @@ export const paymentApi = {
         : undefined,
     }),
   exportAdvanceIssues: (params?: Record<string, any>) =>
-    client.get('/payments/advance-issues/export', {
+    bxClient.get('/payments/advance-issues/export', {
       params: params
         ? { issue_status: params.issueStatus, keyword: params.keyword }
         : undefined,
       responseType: 'blob',
     }),
-  issueAdvance: (
-    advanceId: number,
-    data: { issueRemark?: string; actualAmount?: number },
-  ) => client.post(`/payments/advance/${advanceId}/issue`, data),
-  processed: (params?: Record<string, any>) =>
-    client.get('/payments/processed', { params }),
-  advanceOffsetRecords: (advanceId: number) =>
-    client.get(`/payments/advance/${advanceId}/offset-records`),
-  reimburseOffsetRecords: (reimburseId: number) =>
-    client.get(`/payments/${reimburseId}/offset-records`),
+  issueAdvance: (advanceId: BxId, data: { issueRemark?: string; actualAmount?: number }) =>
+    bxClient.post(`/payments/advance/${advanceId}/issue`, data),
+  processed: (params?: Record<string, any>) => bxClient.get('/payments/processed', { params }),
+  advanceOffsetRecords: (advanceId: BxId) =>
+    bxClient.get(`/payments/advance/${advanceId}/offset-records`),
+  reimburseOffsetRecords: (reimburseId: BxId) =>
+    bxClient.get(`/payments/${reimburseId}/offset-records`),
 }
 
 export const attachmentApi = {
-  upload: (file: File, bizType: string = '', bizId: number = 0) => {
+  upload: (file: File, bizType: string = '', bizId: BxId = 0) => {
     const form = new FormData()
     form.append('file', file)
-    return client.post('/attachments/upload', form, {
-      headers: { 'Content-Type': 'multipart/form-data' },
+    return bxClient.post('/attachments/upload', form, {
       params: { biz_type: bizType, biz_id: bizId },
     })
   },
-  download: (id: number) => client.get(`/attachments/download/${id}`, { responseType: 'blob' }),
-  preview: (id: number) => client.get(`/attachments/preview/${id}`),
-  delete: (id: number) => client.delete(`/attachments/${id}`),
-  listByBiz: (bizType: string, bizId: number) =>
-    client.get('/attachments/list', { params: { biz_type: bizType, biz_id: bizId } }),
-  bindBiz: (ids: number[], bizType: string, bizId: number) =>
-    client.put('/attachments/bind-biz', null, {
+  download: (id: BxId) =>
+    bxClient.get(`/attachments/download/${id}`, { responseType: 'blob' }),
+  preview: (id: BxId) => bxClient.get(`/attachments/preview/${id}`),
+  delete: (id: BxId) => bxClient.delete(`/attachments/${id}`),
+  listByBiz: (bizType: string, bizId: BxId) =>
+    bxClient.get('/attachments/list', { params: { biz_type: bizType, biz_id: bizId } }),
+  bindBiz: (ids: BxId[], bizType: string, bizId: BxId) =>
+    bxClient.put('/attachments/bind-biz', null, {
       params: { ids: ids.join(','), biz_type: bizType, biz_id: bizId },
     }),
-  downloadUrl: (id: number) => `/api/attachments/download/${id}`,
+  downloadUrl: (id: BxId) => `/api/attachments/download/${id}`,
 }

+ 99 - 46
bx-mobile/src/stores/auth.ts

@@ -1,17 +1,21 @@
 import { defineStore } from 'pinia'
 import { ref, computed } from 'vue'
-import { authApi, menuApi } from '@/api'
+import { portalAuthApi } from '@/api/portalAuth'
+import { filterVisibleEntries } from '@/utils/bxPermission'
+import type { BxMenuEntry } from '@/constants/bxMenuCatalog'
 
+/** 门户用户(id 为 UUID 字符串) */
 export interface UserInfo {
-  id: number
+  id: string
   username: string
   realName: string
   phone?: string
-  email?: string
-  avatar?: string
-  roleList: { id: number; roleName: string; roleCode: string }[]
+  role?: string
+  roles?: string[]
+  avatarUrl?: string | null
 }
 
+/** @deprecated 旧 bx 菜单树;首页请用 visibleEntries(Task 7) */
 export interface MenuItem {
   id: number
   parentId: number
@@ -25,74 +29,123 @@ export interface MenuItem {
 }
 
 export const useAuthStore = defineStore('auth', () => {
-  const token = ref(localStorage.getItem('token') || '')
+  const accessToken = ref(
+    localStorage.getItem('accessToken') || localStorage.getItem('token') || '',
+  )
+  const refreshToken = ref(localStorage.getItem('refreshToken') || '')
   const user = ref<UserInfo | null>(null)
+  const menuCodes = ref<string[]>([])
+  const isSuperAdmin = ref(false)
+  const allMenus = ref(false)
+  /** 兼容旧首页:Task 7 前保持空数组 */
   const menuList = ref<MenuItem[]>([])
 
-  const isLoggedIn = computed(() => !!token.value)
-  const realName = computed(() => user.value?.realName || '')
-  const roleCodes = computed(() => user.value?.roleList?.map((r) => r.roleCode) || [])
+  const isLoggedIn = computed(() => !!accessToken.value)
+  const realName = computed(() => user.value?.realName || user.value?.username || '')
 
-  const isAdmin = computed(() => roleCodes.value.includes('ADMIN'))
-  const isFinance = computed(() => roleCodes.value.includes('FINANCE'))
-  const isGeneralManager = computed(() => roleCodes.value.includes('GENERAL_MANAGER'))
+  const visibleEntries = computed((): BxMenuEntry[] =>
+    filterVisibleEntries({
+      isSuperAdmin: isSuperAdmin.value,
+      all: allMenus.value,
+      menuCodes: menuCodes.value,
+    }),
+  )
 
-  async function login(username: string, password: string) {
-    const res: any = await authApi.login({ username, password })
-    if (res.code === 0) {
-      token.value = res.data.accessToken
-      localStorage.setItem('token', res.data.accessToken)
-      await fetchUser()
-      return true
+  /** @deprecated 旧角色码;路由守卫将改为 menuCodes(Task 6) */
+  const roleCodes = computed(() => {
+    const roles = user.value?.roles || []
+    if (user.value?.role) return Array.from(new Set([user.value.role, ...roles]))
+    return roles
+  })
+  const isAdmin = computed(() => false)
+  const isFinance = computed(() => false)
+  const isGeneralManager = computed(() => false)
+
+  function persistTokens(token: string, rt?: string) {
+    accessToken.value = token
+    localStorage.setItem('accessToken', token)
+    localStorage.removeItem('token')
+    if (rt) {
+      refreshToken.value = rt
+      localStorage.setItem('refreshToken', rt)
     }
-    throw new Error(res.msg || '登录失败')
+  }
+
+  async function login(username: string, password: string) {
+    const res: any = await portalAuthApi.login(username, password)
+    const token = res?.data?.token
+    if (!token) throw new Error(res?.message || '登录失败')
+    persistTokens(token, res?.data?.refreshToken)
+    await fetchUser()
+    return true
   }
 
   async function fetchUser() {
     try {
-      const res: any = await authApi.me()
-      if (res.code === 0) {
-        user.value = res.data
-      }
-      await fetchMenus()
+      const res: any = await portalAuthApi.currentUser()
+      user.value = res.data
+      const perm: any = await portalAuthApi.menuCodes()
+      isSuperAdmin.value = !!perm.data?.isSuperAdmin
+      allMenus.value = !!perm.data?.all
+      menuCodes.value = perm.data?.menuCodes || []
     } catch {
       logout()
     }
   }
 
   async function fetchMenus() {
+    menuList.value = []
+  }
+
+  async function logout() {
     try {
-      const res: any = await menuApi.mine()
-      if (res.code === 0) {
-        menuList.value = res.data || []
-      }
+      await portalAuthApi.logout()
     } catch {
-      menuList.value = []
+      /* ignore */
     }
-  }
-
-  function logout() {
-    token.value = ''
+    accessToken.value = ''
+    refreshToken.value = ''
     user.value = null
+    menuCodes.value = []
+    isSuperAdmin.value = false
+    allMenus.value = false
     menuList.value = []
+    localStorage.removeItem('accessToken')
+    localStorage.removeItem('refreshToken')
     localStorage.removeItem('token')
   }
 
+  function can(menuCode: string) {
+    if (isSuperAdmin.value || allMenus.value) return true
+    return menuCodes.value.includes(menuCode)
+  }
+
   function hasMenu(code: string): boolean {
-    function check(items: MenuItem[]): boolean {
-      for (const item of items) {
-        if (item.menuCode === code) return true
-        if (item.children?.length && check(item.children)) return true
-      }
-      return false
-    }
-    return check(menuList.value)
+    return can(code)
   }
 
   return {
-    token, user, menuList,
-    isLoggedIn, realName, roleCodes,
-    isAdmin, isFinance, isGeneralManager,
-    login, fetchUser, fetchMenus, logout, hasMenu,
+    accessToken,
+    refreshToken,
+    /** @deprecated 使用 accessToken */
+    token: accessToken,
+    user,
+    menuList,
+    menuCodes,
+    isSuperAdmin,
+    allMenus,
+    isLoggedIn,
+    realName,
+    roleCodes,
+    isAdmin,
+    isFinance,
+    isGeneralManager,
+    visibleEntries,
+    login,
+    fetchUser,
+    fetchMenus,
+    logout,
+    can,
+    hasMenu,
   }
 })

+ 13 - 1
bx-mobile/src/views/admin/MenuManagePage.vue

@@ -151,7 +151,19 @@ import { ref, reactive, computed, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
 import { showToast, showDialog } from 'vant'
 import { DELETE_CONFIRM_BUTTON_COLOR } from '@/constants/dialog'
-import { menuApi } from '@/api'
+/** IAM 已迁门户;本页路由将在 Task 6 移除 */
+const menuApi: any = {
+  tree: async () => ({ code: 0, data: [] }),
+  create: async () => {
+    throw new Error('请使用门户系统管理菜单')
+  },
+  update: async () => {
+    throw new Error('请使用门户系统管理菜单')
+  },
+  delete: async () => {
+    throw new Error('请使用门户系统管理菜单')
+  },
+}
 import { menuTypeLabel, statusLabel } from '@/utils/format'
 import StatusTag from '@/components/mobile/StatusTag.vue'
 import MenuNode from '@/components/mobile/MenuNode.vue'

+ 28 - 1
bx-mobile/src/views/admin/RoleManagePage.vue

@@ -237,8 +237,35 @@ import { ref, reactive, computed, onMounted, h, defineComponent } from 'vue'
 import { useRouter } from 'vue-router'
 import { showToast, showConfirmDialog } from 'vant'
 import { DELETE_CONFIRM_BUTTON_COLOR } from '@/constants/dialog'
-import { roleApi, menuApi, userApi } from '@/api'
 import { BizError } from '@/api/client'
+
+/** IAM 已迁门户;本页路由将在 Task 6 移除 */
+const roleApi: any = {
+  list: async () => ({ code: 0, data: { rows: [], total: 0 } }),
+  create: async () => {
+    throw new Error('请使用门户系统管理角色')
+  },
+  update: async () => {
+    throw new Error('请使用门户系统管理角色')
+  },
+  delete: async () => {
+    throw new Error('请使用门户系统管理角色')
+  },
+  getUsers: async () => ({ code: 0, data: [] }),
+  assignUsers: async () => {
+    throw new Error('请使用门户系统管理角色')
+  },
+  assignMenus: async () => {
+    throw new Error('请使用门户系统管理角色')
+  },
+}
+const menuApi: any = {
+  tree: async () => ({ code: 0, data: [] }),
+  getByRole: async () => ({ code: 0, data: [] }),
+}
+const userApi: any = {
+  list: async () => ({ code: 0, data: { rows: [], total: 0 } }),
+}
 import { statusLabel } from '@/utils/format'
 import StatusTag from '@/components/mobile/StatusTag.vue'
 import { useKeywordSearch } from '@/hooks/useKeywordSearch'

+ 20 - 1
bx-mobile/src/views/admin/UserManagePage.vue

@@ -226,8 +226,27 @@ import { ref, reactive, computed, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
 import { showToast, showConfirmDialog } from 'vant'
 import { DELETE_CONFIRM_BUTTON_COLOR } from '@/constants/dialog'
-import { userApi, roleApi } from '@/api'
 import { BizError } from '@/api/client'
+
+/** IAM 已迁门户;本页路由将在 Task 6 移除,保留空实现以免编译失败 */
+const userApi: any = {
+  list: async () => ({ code: 0, data: { rows: [], total: 0 } }),
+  create: async () => {
+    throw new Error('请使用门户系统管理用户')
+  },
+  update: async () => {
+    throw new Error('请使用门户系统管理用户')
+  },
+  delete: async () => {
+    throw new Error('请使用门户系统管理用户')
+  },
+  resetPassword: async () => {
+    throw new Error('请使用门户系统管理用户')
+  },
+}
+const roleApi: any = {
+  list: async () => ({ code: 0, data: { rows: [], total: 0 } }),
+}
 import { statusLabel } from '@/utils/format'
 import StatusTag from '@/components/mobile/StatusTag.vue'
 import { useKeywordSearch } from '@/hooks/useKeywordSearch'

+ 3 - 16
bx-mobile/src/views/admin/WfAdvancePage.vue

@@ -128,7 +128,7 @@ import { ref, reactive, computed, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
 import { showToast, showDialog } from 'vant'
 import { DELETE_CONFIRM_BUTTON_COLOR } from '@/constants/dialog'
-import { workflowApi, userApi } from '@/api'
+import { workflowApi } from '@/api'
 import { statusLabel, statusTagClass } from '@/utils/format'
 import StatusTag from '@/components/mobile/StatusTag.vue'
 
@@ -165,21 +165,8 @@ function nodeKey(node: Record<string, any>): string {
 }
 
 async function loadUsers() {
-  try {
-    const res: any = await userApi.dropdown()
-    const data = res.data ?? res
-    userOptions.value = (data || []).map((u: any) => {
-      const name = u.realName || u.username
-      const roles = u.roles || ''
-      return {
-        id: u.id,
-        realName: name,
-        displayName: roles ? `${name}(${roles})` : name,
-      }
-    })
-  } catch {
-    userOptions.value = []
-  }
+  // 旧 bx userApi 已移除;P3 再接门户用户下拉
+  userOptions.value = []
 }
 
 async function loadWfDef() {

+ 3 - 16
bx-mobile/src/views/admin/WfReimbursePage.vue

@@ -128,7 +128,7 @@ import { ref, reactive, computed, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
 import { showToast, showDialog } from 'vant'
 import { DELETE_CONFIRM_BUTTON_COLOR } from '@/constants/dialog'
-import { workflowApi, userApi } from '@/api'
+import { workflowApi } from '@/api'
 import { statusLabel, statusTagClass } from '@/utils/format'
 import StatusTag from '@/components/mobile/StatusTag.vue'
 
@@ -165,21 +165,8 @@ function nodeKey(node: Record<string, any>): string {
 }
 
 async function loadUsers() {
-  try {
-    const res: any = await userApi.dropdown()
-    const data = res.data ?? res
-    userOptions.value = (data || []).map((u: any) => {
-      const name = u.realName || u.username
-      const roles = u.roles || ''
-      return {
-        id: u.id,
-        realName: name,
-        displayName: roles ? `${name}(${roles})` : name,
-      }
-    })
-  } catch {
-    userOptions.value = []
-  }
+  // 旧 bx userApi 已移除;P3 再接门户用户下拉
+  userOptions.value = []
 }
 
 async function loadWfDef() {