优化构架
This commit is contained in:
@@ -16,20 +16,25 @@
|
||||
|
||||
## 目录与职责
|
||||
|
||||
- `src/views/`:路由级页面和业务编排;复杂、可复用的展示或交互下沉到组件。
|
||||
- `src/components/`:无路由依赖的复用组件,输入输出应有明确类型。
|
||||
- `src/layouts/`:应用壳层、导航和全局页面结构。
|
||||
- `src/router/`:路由、标题、登录保护和权限元数据;新增模块时同步维护导航与路由配置。
|
||||
- `src/stores/`:跨页面状态和业务动作;仅页面局部使用的状态留在组件内。
|
||||
- `src/api/`:HTTP 客户端和接口适配;页面与组件不得散落创建 Axios 实例。
|
||||
- `src/app/`:应用装配层,包含布局、根路由、全局守卫和启动入口;不承载具体业务或被业务模块反向依赖。
|
||||
- `src/core/`:认证会话、访问控制、导航配置、未保存工作保护和路由安全工具等全应用基础能力;可以被 `app` 和业务模块使用,但不得依赖业务模块。
|
||||
- `src/modules/`:按客户、仓库、头程订单、主数据、下载中心、IAM 等稳定业务领域组织页面、API、类型、组件和路由。新增业务页面必须进入对应模块,不再建立根级 `views/`。
|
||||
- `src/shared/`:与具体业务无关的后台公共 HTTP、组件和工具。模块专属能力先留在模块内部,只有被无关模块真实复用时才上移。
|
||||
- `src/prototype/`:未纳入当前迭代的原型页面和原型路由;正式模块不得依赖原型代码。
|
||||
- `src/modules/*/pages/`:路由级页面和业务编排;复杂展示、表单、抽屉和交互下沉到同模块组件或 composable。
|
||||
- `src/modules/*/routes.ts`:模块路由片段;由 `src/app/router/index.ts` 统一装配。新增模块时同步维护 `src/core/navigation/adminNavigation.ts`。
|
||||
- `src/core/*/` 与 `src/modules/*/stores/`:前者只放全应用基础状态,后者放领域状态;仅页面局部使用的状态留在页面或 composable。
|
||||
- `src/modules/*/api/`:领域接口适配;公共 Axios 客户端位于 `src/shared/api/http.ts`,页面与组件不得散落创建 Axios 实例。
|
||||
- `VITE_API_BASE_URL` 控制浏览器请求前缀,`API_PROXY_TARGET` 控制 Vite 开发代理上游;后者只由开发服务器读取,不进入客户端构建。新增或变更环境变量时同步更新 `.env.example`,不得提交真实密钥或环境专属凭据。
|
||||
- `src/types/`:后台专属类型;跨应用共享的稳定契约放到 `packages/shared`。
|
||||
- 模块专属类型放在对应模块;跨应用共享的稳定契约放到 `packages/shared`。
|
||||
- `src/styles/`:设计令牌和全局样式;优先复用现有 `--cc-*` 变量和组件样式。
|
||||
|
||||
依赖方向保持为 `app -> modules/core/shared`、`modules -> core/shared`、`core -> shared`、`shared -> 无上层依赖`。模块间确需调用时只能引用对方根入口公开的 API 或契约,不得引用其他模块的页面、内部组件或内部目录。
|
||||
|
||||
## 业务与安全约束
|
||||
|
||||
- 菜单隐藏和前端路由守卫只改善体验,不能替代服务端鉴权。新增操作必须使用稳定的权限编码,并与 API 权限保持一致。
|
||||
- 认证统一经过 `src/stores/auth.ts` 和 `src/api/http.ts`;不要在页面中复制令牌读写或手工拼装 `Authorization`。
|
||||
- 认证统一经过 `src/core/auth/auth.ts` 和 `src/shared/api/http.ts`;不要在页面中复制令牌读写或手工拼装 `Authorization`。
|
||||
- 当前登录和部分数据仍可能是模拟实现。接入 `services/api` 时删除对应模拟路径,不允许让生产逻辑静默回退到假数据。
|
||||
- 删除、停用、改权限等高风险操作必须有明确确认、处理中、成功和失败反馈;失败时保留用户上下文,避免误以为已生效。
|
||||
- 日志、提示和示例数据不得包含真实账号、令牌、客户隐私或生产数据。
|
||||
|
||||
@@ -23,9 +23,19 @@ pnpm build
|
||||
pnpm test
|
||||
```
|
||||
|
||||
## 源码结构
|
||||
|
||||
- `src/app/`:应用壳层、根路由和启动装配。
|
||||
- `src/core/`:认证、权限、导航、未保存工作保护等全应用基础能力。
|
||||
- `src/modules/`:按业务领域组织页面、接口、组件、类型和模块路由。
|
||||
- `src/shared/`:不依赖具体业务的 HTTP、基础组件和工具。
|
||||
- `src/prototype/`:尚未进入当前迭代的原型页面与路由,和正式业务代码隔离。
|
||||
|
||||
新增业务页面时,在对应 `src/modules/<domain>/` 中就近放置页面、测试和领域能力,并从模块 `routes.ts` 导出路由片段;根路由只负责装配,菜单仍由 `src/core/navigation/adminNavigation.ts` 集中维护。跨模块调用必须经过模块根入口公开的 API,不能引用对方内部目录。
|
||||
|
||||
依赖和 `pnpm-lock.yaml` 均保存在当前前端项目中,不需要在仓库根目录安装 Node 依赖。
|
||||
|
||||
工作台已从一期路由与菜单移除;登录后按当前角色进入第一个有权限的业务页面。一期 Admin 路由聚焦登录、客户及注册审核、平台收货仓、快递公司、往来单位、头程订单、内部用户、角色管理和菜单权限。
|
||||
工作台已从一期菜单和默认入口移除;登录后按当前角色进入第一个有权限的业务页面。历史工作台仍保留为不展示菜单的原型路由。一期 Admin 正式路由聚焦登录、客户及注册审核、平台收货仓、快递公司、往来单位、头程订单、内部用户、角色管理和菜单权限。
|
||||
|
||||
后台菜单仅展示已接入的一期模块;其余平台原型菜单保留为导航配置中的注释,待对应业务契约接入后再恢复展示。企业审核、仓库、头程订单、平台角色、菜单及按钮权限资源均接入真实接口;用户管理通过 IAM 统一维护平台账号和企业租户账号。运行时不再提供本地演示业务数据或静态角色预览。
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { adminNavigation, filterAdminNavigation, type AdminNavigationNode } from '@/navigation/adminNavigation'
|
||||
import { adminNavigation, filterAdminNavigation, type AdminNavigationNode } from '@/core/navigation/adminNavigation'
|
||||
import AdminSidebarMenu from './AdminSidebarMenu.vue'
|
||||
|
||||
const stubs = {
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { AdminNavigationNode } from '@/navigation/adminNavigation'
|
||||
import type { AdminNavigationNode } from '@/core/navigation/adminNavigation'
|
||||
import AdminSidebarMenuNode from './AdminSidebarMenuNode.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { AdminNavigationNode } from '@/navigation/adminNavigation'
|
||||
import type { AdminNavigationNode } from '@/core/navigation/adminNavigation'
|
||||
|
||||
defineOptions({ name: 'AdminSidebarMenuNode' })
|
||||
|
||||
@@ -3,7 +3,7 @@ import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import AppLayout from './AppLayout.vue'
|
||||
|
||||
vi.mock('element-plus', async (importOriginal) => {
|
||||
@@ -3,13 +3,13 @@ import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { LogOut, Menu } from '@lucide/vue'
|
||||
import { getApiErrorDetails } from '@/api/http'
|
||||
import { getApiErrorDetails } from '@/shared/api/http'
|
||||
import baiyueLogoWhite from '@/assets/brand/baiyue-logo-white.png'
|
||||
import baiyueMarkWhite from '@/assets/brand/baiyue-mark-white.png'
|
||||
import AdminSidebarMenu from '@/components/AdminSidebarMenu.vue'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useUnsavedWorkStore } from '@/stores/unsavedWork'
|
||||
import AdminSidebarMenu from '@/app/components/AdminSidebarMenu.vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -2,8 +2,8 @@
|
||||
import { computed } from 'vue'
|
||||
import { ShieldX } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { sanitizeInternalPath } from '@/router/accessDecision'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import { sanitizeInternalPath } from '@/core/routing/accessDecision'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -1,13 +1,14 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { adminNavigation, getNavigationLeaves } from '@/core/navigation/adminNavigation'
|
||||
import router from './index'
|
||||
|
||||
describe('admin router access metadata', () => {
|
||||
it('uses the root route only as a permission-aware entry point', () => {
|
||||
it('uses the root route only as a permission-aware entry point while retaining the dashboard prototype', () => {
|
||||
const home = router.getRoutes().find((route) => route.name === 'home')
|
||||
|
||||
expect(home?.path).toBe('/')
|
||||
expect(home?.meta.permission).toBeUndefined()
|
||||
expect(router.getRoutes().some((route) => route.name === 'dashboard')).toBe(false)
|
||||
expect(router.getRoutes().some((route) => route.name === 'dashboard')).toBe(true)
|
||||
})
|
||||
|
||||
it('protects warehouse management with its stable list permission', () => {
|
||||
@@ -53,4 +54,14 @@ describe('admin router access metadata', () => {
|
||||
expect(forbidden?.meta?.permission).toBeUndefined()
|
||||
expect(resolved.matched.some((record) => record.meta.requiresAuth)).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps visible navigation paths and permissions aligned with routes', () => {
|
||||
for (const item of getNavigationLeaves(adminNavigation)) {
|
||||
const resolved = router.resolve(item.path)
|
||||
const pageRoute = resolved.matched.at(-1)
|
||||
|
||||
expect(pageRoute, `missing route for ${item.path}`).toBeDefined()
|
||||
expect(pageRoute?.meta.permission, `permission mismatch for ${item.path}`).toBe(item.permission)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,85 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import AppLayout from '@/app/layouts/AppLayout.vue'
|
||||
import { decideRouteAccess } from '@/core/routing/accessDecision'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { downloadRoutes } from '@/modules/downloads/routes'
|
||||
import { firstLegOrderRoutes } from '@/modules/first-leg-orders/routes'
|
||||
import { iamRoutes } from '@/modules/iam/routes'
|
||||
import { masterDataRoutes } from '@/modules/master-data/routes'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import { customerRoutes } from '@/modules/customers/routes'
|
||||
import { warehouseRoutes } from '@/modules/warehouses/routes'
|
||||
import { prototypeModuleRoutes } from '@/prototype/routes'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('@/modules/auth/pages/LoginView.vue'),
|
||||
meta: { title: '登录' },
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: AppLayout,
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'home',
|
||||
component: () => import('@/app/pages/ModuleView.vue'),
|
||||
meta: { title: '进入管理后台', description: '正在进入当前角色可访问的业务页面' },
|
||||
},
|
||||
...prototypeModuleRoutes,
|
||||
...customerRoutes,
|
||||
...warehouseRoutes,
|
||||
...firstLegOrderRoutes,
|
||||
...masterDataRoutes,
|
||||
...downloadRoutes,
|
||||
...iamRoutes,
|
||||
{
|
||||
path: '403',
|
||||
name: 'forbidden',
|
||||
component: () => import('@/app/pages/ForbiddenView.vue'),
|
||||
meta: { title: '权限不足', description: '当前登录账号没有访问此页面的权限' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: '/:pathMatch(.*)*', redirect: '/' },
|
||||
],
|
||||
})
|
||||
|
||||
router.beforeEach((to) => {
|
||||
document.title = to.meta.title ? `${to.meta.title} - 柏悦物流管理后台` : '柏悦物流管理后台'
|
||||
const auth = useAuthStore()
|
||||
if (to.name === 'login' && auth.isAuthenticated) {
|
||||
return { name: 'home' }
|
||||
}
|
||||
if (!auth.isAuthenticated && !to.meta.requiresAuth) return true
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
if (auth.isAuthenticated) access.initializeAfterLogin()
|
||||
const decision = decideRouteAccess({
|
||||
requiresAuth: to.meta.requiresAuth,
|
||||
isAuthenticated: auth.isAuthenticated,
|
||||
fullPath: to.fullPath,
|
||||
permission: to.meta.permission,
|
||||
hasPermission: to.meta.permission ? access.can(to.meta.permission) : true,
|
||||
})
|
||||
|
||||
if (decision.type === 'login') {
|
||||
return { name: 'login', query: { redirect: decision.redirect } }
|
||||
}
|
||||
if (decision.type === 'forbidden') {
|
||||
return { name: 'forbidden', query: { from: decision.from }, replace: true }
|
||||
}
|
||||
if (to.name === 'home') {
|
||||
return access.firstAccessiblePath
|
||||
? { path: access.firstAccessiblePath, replace: true }
|
||||
: { name: 'forbidden', replace: true }
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
|
||||
describe('server access store', () => {
|
||||
beforeEach(() => setActivePinia(createPinia()))
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
adminNavigation,
|
||||
filterAdminNavigation,
|
||||
getFirstNavigationLeaf,
|
||||
} from '@/navigation/adminNavigation'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
} from '@/core/navigation/adminNavigation'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
|
||||
export const useAccessPreviewStore = defineStore('access-preview', () => {
|
||||
const auth = useAuthStore()
|
||||
@@ -7,7 +7,7 @@ const mocks = vi.hoisted(() => ({
|
||||
refreshHandler: null as null | (() => Promise<string>),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/http', () => ({
|
||||
vi.mock('@/shared/api/http', () => ({
|
||||
http: { post: mocks.post },
|
||||
registerAuthRefreshHandler: (handler: () => Promise<string>) => { mocks.refreshHandler = handler },
|
||||
unwrapApiResponse: <T>(response: { code: string; message: string; data: T }) => {
|
||||
@@ -20,7 +20,7 @@ vi.mock('@/api/http', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/platformIam', () => ({
|
||||
vi.mock('./platformAuthorization', () => ({
|
||||
listCurrentPlatformPermissionCodes: mocks.listCurrentPlatformPermissionCodes,
|
||||
}))
|
||||
|
||||
@@ -6,9 +6,10 @@ import {
|
||||
registerAuthRefreshHandler,
|
||||
unwrapApiResponse,
|
||||
type ApiEnvelope,
|
||||
} from '@/api/http'
|
||||
import { listCurrentPlatformPermissionCodes, type PlatformPermissionResource } from '@/api/platformIam'
|
||||
import { decodeJwtClaims } from '@/utils/jwt'
|
||||
} from '@/shared/api/http'
|
||||
import type { PlatformPermissionResource } from '@/shared/types/platformPermission'
|
||||
import { decodeJwtClaims } from './jwt'
|
||||
import { listCurrentPlatformPermissionCodes } from './platformAuthorization'
|
||||
|
||||
const TOKEN_KEY = 'cc-admin-token'
|
||||
const REFRESH_TOKEN_KEY = 'cc-admin-refresh-token'
|
||||
@@ -0,0 +1,19 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import { listCurrentPlatformPermissionCodes } from './platformAuthorization'
|
||||
|
||||
vi.mock('@/shared/api/http', () => ({
|
||||
http: { get: vi.fn() },
|
||||
unwrapApiResponse: <T>(response: { data: T }) => response.data,
|
||||
}))
|
||||
|
||||
describe('platformAuthorization', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('loads the current user permission snapshot', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: ['platform:warehouse:list'] })
|
||||
|
||||
await expect(listCurrentPlatformPermissionCodes()).resolves.toEqual(['platform:warehouse:list'])
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/v1/iam/authorization/permissions')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
|
||||
export async function listCurrentPlatformPermissionCodes() {
|
||||
const response = await http.get<ApiEnvelope<string[]>, ApiEnvelope<string[]>>(
|
||||
'/platform/v1/iam/authorization/permissions',
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import { relative, resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const sourceRoot = resolve(process.cwd(), 'src')
|
||||
|
||||
function sourceFiles(directory: string): string[] {
|
||||
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||
const path = resolve(directory, entry.name)
|
||||
if (entry.isDirectory()) return sourceFiles(path)
|
||||
return /\.(ts|vue)$/.test(entry.name) ? [path] : []
|
||||
})
|
||||
}
|
||||
|
||||
function aliasImports(path: string): string[] {
|
||||
const source = readFileSync(path, 'utf8')
|
||||
return [...source.matchAll(/['"](@\/[^'"]+)['"]/g)].map((match) => match[1])
|
||||
}
|
||||
|
||||
describe('source dependency boundaries', () => {
|
||||
it('keeps app, core, modules and shared dependencies pointing inward', () => {
|
||||
const violations: string[] = []
|
||||
|
||||
for (const path of sourceFiles(sourceRoot)) {
|
||||
const sourcePath = relative(sourceRoot, path).replaceAll('\\', '/')
|
||||
const sourceModule = sourcePath.match(/^modules\/([^/]+)\//)?.[1]
|
||||
|
||||
for (const dependency of aliasImports(path)) {
|
||||
if (sourcePath.startsWith('shared/') && /^@\/(app|core|modules|prototype)\//.test(dependency)) {
|
||||
violations.push(`${sourcePath} -> ${dependency}`)
|
||||
}
|
||||
if (sourcePath.startsWith('core/') && /^@\/(app|modules|prototype)\//.test(dependency)) {
|
||||
violations.push(`${sourcePath} -> ${dependency}`)
|
||||
}
|
||||
if (sourceModule && /^@\/(app|prototype)\//.test(dependency)) {
|
||||
violations.push(`${sourcePath} -> ${dependency}`)
|
||||
}
|
||||
|
||||
const targetModule = dependency.match(/^@\/modules\/([^/]+)(\/.*)?$/)
|
||||
if (sourceModule && targetModule && targetModule[1] !== sourceModule && targetModule[2]) {
|
||||
violations.push(`${sourcePath} -> ${dependency}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(violations).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useUnsavedWorkStore } from '@/stores/unsavedWork'
|
||||
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
|
||||
|
||||
describe('unsaved work store', () => {
|
||||
beforeEach(() => {
|
||||
+2
-2
@@ -2,8 +2,8 @@ import { computed, defineComponent, nextTick, reactive, ref } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
|
||||
import { useUnsavedWorkStore } from '@/stores/unsavedWork'
|
||||
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
|
||||
|
||||
interface HarnessModel {
|
||||
name: string
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { computed, onScopeDispose, ref, watch, type ComputedRef, type Ref } from 'vue'
|
||||
import { useUnsavedWorkStore } from '@/stores/unsavedWork'
|
||||
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
|
||||
|
||||
interface UnsavedWorkRegistrationOptions<T extends object> {
|
||||
key: string
|
||||
+6
-7
@@ -4,12 +4,12 @@ import { ElMessage } from 'element-plus'
|
||||
import 'element-plus/es/components/message/style/css'
|
||||
import 'element-plus/es/components/message-box/style/css'
|
||||
import App from './App.vue'
|
||||
import { registerAuthSessionExpiredHandler } from '@/api/http'
|
||||
import router from './router'
|
||||
import { sanitizeInternalPath } from '@/router/accessDecision'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useUnsavedWorkStore } from '@/stores/unsavedWork'
|
||||
import router from '@/app/router'
|
||||
import { sanitizeInternalPath } from '@/core/routing/accessDecision'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import { registerAuthSessionExpiredHandler } from '@/shared/api/http'
|
||||
import './styles/index.css'
|
||||
|
||||
const pinia = createPinia()
|
||||
@@ -32,4 +32,3 @@ registerAuthSessionExpiredHandler(async () => {
|
||||
})
|
||||
|
||||
createApp(App).use(pinia).use(router).mount('#app')
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import { resolve } from 'node:path'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import LoginView from './LoginView.vue'
|
||||
|
||||
const loginSource = readFileSync(resolve(process.cwd(), 'src/views/LoginView.vue'), 'utf8')
|
||||
const loginSource = readFileSync(resolve(process.cwd(), 'src/modules/auth/pages/LoginView.vue'), 'utf8')
|
||||
const formStub = {
|
||||
template: '<form><slot /></form>',
|
||||
setup(_: unknown, { expose }: { expose: (exposed: { validate: () => Promise<boolean> }) => void }) {
|
||||
@@ -3,12 +3,12 @@ import { reactive, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { ArrowRight, LockKeyhole, ShieldCheck, UserRound } from '@lucide/vue'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/api/http'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import baiyueLogoWhite from '@/assets/brand/baiyue-logo-white.png'
|
||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
||||
import { sanitizeInternalPath } from '@/router/accessDecision'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import { sanitizeInternalPath } from '@/core/routing/accessDecision'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -10,13 +10,13 @@ import {
|
||||
updatePlatformTenant,
|
||||
uploadPlatformTenantQualification,
|
||||
type PlatformTenantResource,
|
||||
} from '@/api/platformIam'
|
||||
import { createPlatformExportTask } from '@/api/platformExportTasks'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
} from '@/modules/iam'
|
||||
import { createPlatformExportTask } from '@/modules/downloads'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import CustomersView from './CustomersView.vue'
|
||||
|
||||
vi.mock('@/api/platformIam', () => ({
|
||||
vi.mock('@/modules/iam', () => ({
|
||||
changePlatformTenantStatus: vi.fn(),
|
||||
createPlatformTenant: vi.fn(),
|
||||
downloadTenantQualification: vi.fn(),
|
||||
@@ -30,7 +30,7 @@ vi.mock('@/api/platformIam', () => ({
|
||||
uploadPlatformTenantQualification: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/platformExportTasks', () => ({ createPlatformExportTask: vi.fn() }))
|
||||
vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
|
||||
|
||||
class ResizeObserverStub implements ResizeObserver {
|
||||
observe() {}
|
||||
@@ -2,8 +2,8 @@
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { Building2, CheckCircle2, Download, Eye, FileCheck2, FileText, FileUp, Pencil, Plus, Power } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
|
||||
import { createPlatformExportTask } from '@/api/platformExportTasks'
|
||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { createPlatformExportTask } from '@/modules/downloads'
|
||||
import {
|
||||
changePlatformTenantStatus,
|
||||
createPlatformTenant,
|
||||
@@ -21,11 +21,11 @@ import {
|
||||
type PlatformTenantQualification,
|
||||
type PlatformTenantResource,
|
||||
type PlatformTenantStatus,
|
||||
} from '@/api/platformIam'
|
||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
} from '@/modules/iam'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
|
||||
type CustomerFilters = QueryModel & { keyword: string; status: PlatformTenantStatus | '' }
|
||||
type QualificationDraft = {
|
||||
+4
-4
@@ -11,12 +11,12 @@ import {
|
||||
rejectEnterpriseReview,
|
||||
type EnterpriseReview,
|
||||
type PlatformTenantQualification,
|
||||
} from '@/api/platformIam'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
} from '@/modules/iam'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import EnterpriseReviewsView from './EnterpriseReviewsView.vue'
|
||||
|
||||
vi.mock('@/api/platformIam', () => ({
|
||||
vi.mock('@/modules/iam', () => ({
|
||||
listEnterpriseReviews: vi.fn(),
|
||||
listTenantQualifications: vi.fn(),
|
||||
downloadTenantQualification: vi.fn(),
|
||||
+6
-6
@@ -11,12 +11,12 @@ import {
|
||||
rejectEnterpriseReview,
|
||||
type EnterpriseReview,
|
||||
type PlatformTenantQualification,
|
||||
} from '@/api/platformIam'
|
||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
|
||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
} from '@/modules/iam'
|
||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const loading = ref(false)
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const customerRoutes = [
|
||||
{
|
||||
path: 'customers',
|
||||
name: 'customers',
|
||||
component: () => import('@/modules/customers/pages/CustomersView.vue'),
|
||||
meta: { title: '客户管理', description: '维护客户企业资料、合作状态和注册审核', permission: 'platform:customer:list' },
|
||||
},
|
||||
{
|
||||
path: 'customers/reviews',
|
||||
name: 'enterprise-reviews',
|
||||
component: () => import('@/modules/customers/pages/EnterpriseReviewsView.vue'),
|
||||
meta: { title: '企业审核', description: '核验企业资质并维护客户下单资格', permission: 'platform:customer:review' },
|
||||
},
|
||||
] satisfies RouteRecordRaw[]
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/api/http'
|
||||
import { http } from '@/shared/api/http'
|
||||
import {
|
||||
createPlatformExportTask,
|
||||
downloadPlatformExportTask,
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
retryPlatformExportTask,
|
||||
} from './platformExportTasks'
|
||||
|
||||
vi.mock('@/api/http', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/api/http')>('@/api/http')
|
||||
vi.mock('@/shared/api/http', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/shared/api/http')>('@/shared/api/http')
|
||||
return { ...actual, http: { get: vi.fn(), post: vi.fn() } }
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/api/http'
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
|
||||
export type ExportTaskStatus = 'PENDING' | 'RUNNING' | 'SUCCEEDED' | 'FAILED' | 'EXPIRED'
|
||||
export type ExportTaskType = 'CUSTOMER_LIST' | string
|
||||
@@ -0,0 +1 @@
|
||||
export * from './api/platformExportTasks'
|
||||
+21
-4
@@ -7,12 +7,12 @@ import {
|
||||
listPlatformExportTasks,
|
||||
retryPlatformExportTask,
|
||||
type PlatformExportTask,
|
||||
} from '@/api/platformExportTasks'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
} from '@/modules/downloads/api/platformExportTasks'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import DownloadCenterView from './DownloadCenterView.vue'
|
||||
|
||||
vi.mock('@/api/platformExportTasks', () => ({
|
||||
vi.mock('@/modules/downloads/api/platformExportTasks', () => ({
|
||||
downloadPlatformExportTask: vi.fn(),
|
||||
listPlatformExportTasks: vi.fn(),
|
||||
retryPlatformExportTask: vi.fn(),
|
||||
@@ -33,6 +33,7 @@ const task: PlatformExportTask = {
|
||||
}
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
afterEach(() => vi.useRealTimers())
|
||||
|
||||
describe('DownloadCenterView', () => {
|
||||
beforeEach(() => {
|
||||
@@ -62,6 +63,7 @@ describe('DownloadCenterView', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(document.body.textContent).toContain('客户列表')
|
||||
expect(document.querySelector('.failure-cell')?.classList.contains('failure-message')).toBe(false)
|
||||
const button = [...document.querySelectorAll<HTMLButtonElement>('button')].find((item) => item.textContent?.trim() === '下载')
|
||||
button?.click()
|
||||
await flushPromises()
|
||||
@@ -80,9 +82,24 @@ describe('DownloadCenterView', () => {
|
||||
mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(document.querySelector('.failure-cell')?.classList.contains('failure-message')).toBe(true)
|
||||
const button = [...document.querySelectorAll<HTMLButtonElement>('button')].find((item) => item.textContent?.trim() === '重试')
|
||||
button?.click()
|
||||
await flushPromises()
|
||||
expect(retryPlatformExportTask).toHaveBeenCalledWith('9')
|
||||
})
|
||||
|
||||
it('does not schedule another refresh after an in-flight load finishes on an unmounted page', async () => {
|
||||
vi.useFakeTimers()
|
||||
let resolveRequest!: (value: { records: PlatformExportTask[]; page: number; size: number; total: number; pages: number }) => void
|
||||
vi.mocked(listPlatformExportTasks).mockReturnValue(new Promise((resolve) => { resolveRequest = resolve }))
|
||||
const wrapper = mount(DownloadCenterView, { global: { plugins: [createPinia(), ElementPlus] } })
|
||||
|
||||
wrapper.unmount()
|
||||
resolveRequest({ records: [{ ...task, status: 'RUNNING' }], page: 1, size: 20, total: 1, pages: 1 })
|
||||
await flushPromises()
|
||||
await vi.advanceTimersByTimeAsync(5000)
|
||||
|
||||
expect(listPlatformExportTasks).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
+16
-11
@@ -2,18 +2,18 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { Download, RefreshCw, RotateCcw } from '@lucide/vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
|
||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import {
|
||||
downloadPlatformExportTask,
|
||||
listPlatformExportTasks,
|
||||
retryPlatformExportTask,
|
||||
type ExportTaskStatus,
|
||||
type PlatformExportTask,
|
||||
} from '@/api/platformExportTasks'
|
||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
} from '@/modules/downloads/api/platformExportTasks'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
|
||||
type DownloadFilters = QueryModel & {
|
||||
exportType: string
|
||||
@@ -31,6 +31,7 @@ const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const result = ref({ records: [] as PlatformExportTask[], total: 0, pages: 0 })
|
||||
const filters = ref<DownloadFilters>({ exportType: '', status: '', requestedRange: [] })
|
||||
let refreshTimer: number | undefined
|
||||
let disposed = false
|
||||
|
||||
const queryFields: QueryField[] = [
|
||||
{ key: 'exportType', type: 'select', placeholder: '全部任务类型', options: [
|
||||
@@ -67,7 +68,7 @@ function clearRefreshTimer() {
|
||||
|
||||
function scheduleRefresh() {
|
||||
clearRefreshTimer()
|
||||
if (hasActiveTasks.value && !loadError.value) refreshTimer = window.setTimeout(() => { void loadTasks(true) }, 5000)
|
||||
if (!disposed && hasActiveTasks.value && !loadError.value) refreshTimer = window.setTimeout(() => { void loadTasks(true) }, 5000)
|
||||
}
|
||||
|
||||
async function loadTasks(background = false) {
|
||||
@@ -167,7 +168,10 @@ function canRetry(task: PlatformExportTask) {
|
||||
|
||||
watch([page, pageSize], () => { void loadTasks() })
|
||||
onMounted(loadTasks)
|
||||
onBeforeUnmount(clearRefreshTimer)
|
||||
onBeforeUnmount(() => {
|
||||
disposed = true
|
||||
clearRefreshTimer()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -188,7 +192,7 @@ onBeforeUnmount(clearRefreshTimer)
|
||||
<template #actions="{ row }"><div class="table-actions">
|
||||
<el-button v-if="(row as PlatformExportTask).status === 'SUCCEEDED'" link type="primary" :loading="downloadingId === (row as PlatformExportTask).id" @click="downloadTask(row as PlatformExportTask)"><Download :size="15" />下载</el-button>
|
||||
<el-button v-if="canRetry(row as PlatformExportTask)" link type="primary" :loading="retryingId === (row as PlatformExportTask).id" @click="retryTask(row as PlatformExportTask)"><RotateCcw :size="15" />重试</el-button>
|
||||
<span v-if="!['SUCCEEDED', 'FAILED'].includes((row as PlatformExportTask).status)" class="action-placeholder">—</span>
|
||||
<span v-if="(row as PlatformExportTask).status !== 'SUCCEEDED' && !canRetry(row as PlatformExportTask)" class="action-placeholder">—</span>
|
||||
</div></template>
|
||||
<template #type="{ row }">{{ typeLabel((row as PlatformExportTask).exportType) }}</template>
|
||||
<template #status="{ row }"><el-tag :type="statusMeta((row as PlatformExportTask).status).type" effect="light">{{ statusMeta((row as PlatformExportTask).status).label }}</el-tag></template>
|
||||
@@ -197,7 +201,7 @@ onBeforeUnmount(clearRefreshTimer)
|
||||
<template #requestedAt="{ row }">{{ formatDate((row as PlatformExportTask).requestedAt) }}</template>
|
||||
<template #completedAt="{ row }">{{ formatDate((row as PlatformExportTask).completedAt) }}</template>
|
||||
<template #expiresAt="{ row }">{{ formatDate((row as PlatformExportTask).expiresAt) }}</template>
|
||||
<template #failure="{ row }"><span class="failure-message">{{ (row as PlatformExportTask).failureMessage || '—' }}</span></template>
|
||||
<template #failure="{ row }"><span class="failure-cell" :class="{ 'failure-message': Boolean((row as PlatformExportTask).failureMessage) }">{{ (row as PlatformExportTask).failureMessage || '—' }}</span></template>
|
||||
</DataTable>
|
||||
</section>
|
||||
</div>
|
||||
@@ -215,7 +219,8 @@ onBeforeUnmount(clearRefreshTimer)
|
||||
.table-actions { display: flex; min-height: 32px; align-items: center; gap: 8px; }
|
||||
.table-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.action-placeholder { color: var(--cc-muted); }
|
||||
.failure-message { display: block; overflow: hidden; color: var(--cc-danger); text-overflow: ellipsis; white-space: nowrap; }
|
||||
.failure-cell { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.failure-message { color: var(--cc-danger); }
|
||||
small { display: block; margin-top: 3px; color: var(--cc-muted); font-size: 11px; }
|
||||
@media (max-width: 720px) {
|
||||
.download-error, .download-table-header { align-items: stretch; flex-direction: column; }
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const downloadRoutes = [
|
||||
{
|
||||
path: 'downloads',
|
||||
name: 'download-center',
|
||||
component: () => import('@/modules/downloads/pages/DownloadCenterView.vue'),
|
||||
meta: { title: '下载中心', description: '查看异步导出任务并下载已生成文件', permission: 'platform:download:list' },
|
||||
},
|
||||
] satisfies RouteRecordRaw[]
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/api/http'
|
||||
import { http } from '@/shared/api/http'
|
||||
import {
|
||||
acceptPlatformFirstLegOrder,
|
||||
addPlatformFirstLegOrderNote,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
updatePlatformFirstLegOrderExpress,
|
||||
} from './platformFirstLegOrders'
|
||||
|
||||
vi.mock('@/api/http', () => ({
|
||||
vi.mock('@/shared/api/http', () => ({
|
||||
http: { get: vi.fn(), post: vi.fn(), put: vi.fn() },
|
||||
unwrapApiResponse: <T>(response: { data: T }) => response.data,
|
||||
}))
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/api/http'
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
|
||||
export type PlatformFirstLegOrderStatus =
|
||||
| 'DRAFT'
|
||||
+7
-7
@@ -17,13 +17,13 @@ import {
|
||||
type PlatformFirstLegOrderDetailResource,
|
||||
type PlatformFirstLegOrderResource,
|
||||
type PlatformFirstLegOrderStatus,
|
||||
} from '@/api/platformFirstLegOrders'
|
||||
import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/api/http'
|
||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
||||
import StatusBadge from '@/components/StatusBadge.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
} from '@/modules/first-leg-orders/api/platformFirstLegOrders'
|
||||
import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
|
||||
const PHASE_ONE_TENANT_ID = '10001'
|
||||
const route = useRoute()
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import PlatformFirstLegOrdersPanel from '@/modules/first-leg-orders/components/PlatformFirstLegOrdersPanel.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PlatformFirstLegOrdersPanel />
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const firstLegOrderRoutes = [
|
||||
{
|
||||
path: 'orders/first-leg',
|
||||
name: 'first-leg-orders',
|
||||
component: () => import('@/modules/first-leg-orders/pages/FirstLegOrdersView.vue'),
|
||||
meta: { title: '头程订单', description: '创建、受理和跟进客户提交的头程运输订单', permission: 'platform:order:list' },
|
||||
},
|
||||
] satisfies RouteRecordRaw[]
|
||||
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/api/http'
|
||||
import { http } from '@/shared/api/http'
|
||||
import {
|
||||
assignPlatformInternalUserRoles,
|
||||
changePlatformManagedUserStatus,
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
listPlatformTenants,
|
||||
listPlatformTenantOrderCounts,
|
||||
listTenantQualifications,
|
||||
listCurrentPlatformPermissionCodes,
|
||||
listPlatformPermissions,
|
||||
listPlatformRoles,
|
||||
listPlatformInternalUsers,
|
||||
@@ -40,7 +39,7 @@ import {
|
||||
type PlatformRoleResource,
|
||||
} from './platformIam'
|
||||
|
||||
vi.mock('@/api/http', () => ({
|
||||
vi.mock('@/shared/api/http', () => ({
|
||||
http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn() },
|
||||
unwrapApiResponse: <T>(response: { data: T }) => response.data,
|
||||
}))
|
||||
@@ -237,13 +236,6 @@ describe('platform IAM roles', () => {
|
||||
expect(http.put).toHaveBeenNthCalledWith(3, `/platform/v1/iam/users/${managedUser.membershipId}/password`, { newPassword: 'ChangeMe123' })
|
||||
})
|
||||
|
||||
it('loads the current users permission snapshot without calling the role-management endpoint', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: ['platform:warehouse:list'] })
|
||||
|
||||
await expect(listCurrentPlatformPermissionCodes()).resolves.toEqual(['platform:warehouse:list'])
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/v1/iam/authorization/permissions')
|
||||
})
|
||||
|
||||
it('loads qualifications for the selected tenant', async () => {
|
||||
const qualification = {
|
||||
id: 12,
|
||||
@@ -1,19 +1,7 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/api/http'
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type { PlatformPermissionResource } from '@/shared/types/platformPermission'
|
||||
|
||||
export interface PlatformPermissionResource {
|
||||
id: number | string
|
||||
tenantId: number | string
|
||||
appCode: 'PLATFORM'
|
||||
permissionType: 'MENU' | 'BUTTON'
|
||||
parentId: number | string
|
||||
permissionCode: string
|
||||
permissionName: string
|
||||
routePath: string | null
|
||||
httpMethod: string | null
|
||||
sortNo: number
|
||||
status: 'ACTIVE' | 'DISABLED' | 'INACTIVE'
|
||||
builtIn?: boolean
|
||||
}
|
||||
export type { PlatformPermissionResource } from '@/shared/types/platformPermission'
|
||||
|
||||
export interface EnterpriseReview {
|
||||
id: number
|
||||
@@ -262,10 +250,6 @@ export function listPlatformPermissions() {
|
||||
return getData<PlatformPermissionResource[]>('/platform/v1/iam/permissions')
|
||||
}
|
||||
|
||||
export function listCurrentPlatformPermissionCodes() {
|
||||
return getData<string[]>('/platform/v1/iam/authorization/permissions')
|
||||
}
|
||||
|
||||
export function listEnterpriseReviews() {
|
||||
return getData<EnterpriseReview[]>('/platform/v1/iam/reviews')
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './api/platformIam'
|
||||
+4
-4
@@ -6,12 +6,12 @@ import {
|
||||
getPlatformUserOptions,
|
||||
listPlatformManagedUsers,
|
||||
type PlatformManagedUser,
|
||||
} from '@/api/platformIam'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
} from '@/modules/iam/api/platformIam'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import InternalUsersView from './InternalUsersView.vue'
|
||||
|
||||
vi.mock('@/api/platformIam', () => ({
|
||||
vi.mock('@/modules/iam/api/platformIam', () => ({
|
||||
listPlatformManagedUsers: vi.fn(),
|
||||
getPlatformUserOptions: vi.fn(),
|
||||
createPlatformManagedUser: vi.fn(),
|
||||
@@ -11,14 +11,14 @@ import {
|
||||
updatePlatformManagedUser,
|
||||
type PlatformManagedUser,
|
||||
type PlatformUserOptions,
|
||||
} from '@/api/platformIam'
|
||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
|
||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
||||
import StatusBadge from '@/components/StatusBadge.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
} from '@/modules/iam/api/platformIam'
|
||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const auth = useAuthStore()
|
||||
@@ -1,11 +1,11 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { flushPromises, shallowMount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { listPlatformPermissions, type PlatformPermissionResource } from '@/api/platformIam'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { listPlatformPermissions, type PlatformPermissionResource } from '@/modules/iam/api/platformIam'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import PermissionsView from './PermissionsView.vue'
|
||||
|
||||
vi.mock('@/api/platformIam', () => ({
|
||||
vi.mock('@/modules/iam/api/platformIam', () => ({
|
||||
changePlatformPermissionStatus: vi.fn(),
|
||||
createPlatformPermission: vi.fn(),
|
||||
deletePlatformPermission: vi.fn(),
|
||||
@@ -2,23 +2,23 @@
|
||||
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { Braces, FolderTree, Pencil, Plus, ShieldCheck, Trash2 } from '@lucide/vue'
|
||||
import StatusBadge from '@/components/StatusBadge.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import {
|
||||
changePlatformPermissionStatus,
|
||||
createPlatformPermission,
|
||||
deletePlatformPermission,
|
||||
listPlatformPermissions,
|
||||
updatePlatformPermission,
|
||||
} from '@/api/platformIam'
|
||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
|
||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
||||
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import type { Permission, PermissionType } from '@/types/admin'
|
||||
import { buildPermissionTree, getDescendantIds, type PermissionTreeNode } from '@/utils/permissionTree'
|
||||
import { mapPlatformPermission } from '@/utils/platformPermission'
|
||||
} from '@/modules/iam/api/platformIam'
|
||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import type { Permission, PermissionType } from '@/modules/iam/types'
|
||||
import { buildPermissionTree, getDescendantIds, type PermissionTreeNode } from '@/modules/iam/utils/permissionTree'
|
||||
import { mapPlatformPermission } from '@/modules/iam/utils/platformPermission'
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const editorOpen = ref(false)
|
||||
@@ -1,11 +1,11 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { flushPromises, shallowMount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createPlatformRole, listPlatformInternalUsers, listPlatformPermissions, listPlatformRoles, type PlatformRoleResource } from '@/api/platformIam'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { createPlatformRole, listPlatformInternalUsers, listPlatformPermissions, listPlatformRoles, type PlatformRoleResource } from '@/modules/iam/api/platformIam'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import RolesView from './RolesView.vue'
|
||||
|
||||
vi.mock('@/api/platformIam', () => ({
|
||||
vi.mock('@/modules/iam/api/platformIam', () => ({
|
||||
createPlatformRole: vi.fn(),
|
||||
listPlatformInternalUsers: vi.fn(),
|
||||
listPlatformPermissions: vi.fn(),
|
||||
@@ -91,7 +91,7 @@ describe('RolesView', () => {
|
||||
|
||||
expect(listPlatformRoles).toHaveBeenCalledTimes(1)
|
||||
expect(listPlatformInternalUsers).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.text()).toContain('角色查询、新增、编辑、启停、删除及权限分配均使用 IAM 真实接口')
|
||||
expect(wrapper.text()).toContain('角色负责聚合菜单、操作和数据范围,内部用户可分配多个角色')
|
||||
expect(wrapper.find('[data-testid="permission-tree"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
@@ -12,17 +12,17 @@ import {
|
||||
replacePlatformRolePermissions,
|
||||
updatePlatformRole,
|
||||
type PlatformRoleResource,
|
||||
} from '@/api/platformIam'
|
||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
|
||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
||||
import StatusBadge from '@/components/StatusBadge.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import type { Permission, Role } from '@/types/admin'
|
||||
import { buildPermissionTree, summarizePermissionSelection } from '@/utils/permissionTree'
|
||||
import { mapPlatformPermission } from '@/utils/platformPermission'
|
||||
} from '@/modules/iam/api/platformIam'
|
||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import type { Permission, Role } from '@/modules/iam/types'
|
||||
import { buildPermissionTree, summarizePermissionSelection } from '@/modules/iam/utils/permissionTree'
|
||||
import { mapPlatformPermission } from '@/modules/iam/utils/platformPermission'
|
||||
|
||||
interface TreeController {
|
||||
getCheckedKeys: (leafOnly?: boolean) => Array<string | number>
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const iamRoutes = [
|
||||
{
|
||||
path: 'system/users',
|
||||
name: 'users',
|
||||
component: () => import('@/modules/iam/pages/InternalUsersView.vue'),
|
||||
meta: { title: '用户管理', description: '统一管理平台和企业租户用户、状态及角色', permission: 'platform:user:list' },
|
||||
},
|
||||
{
|
||||
path: 'system/roles',
|
||||
name: 'roles',
|
||||
component: () => import('@/modules/iam/pages/RolesView.vue'),
|
||||
meta: { title: '角色管理', description: '维护平台角色并配置菜单及操作权限', permission: 'platform:role:list' },
|
||||
},
|
||||
{
|
||||
path: 'system/permissions',
|
||||
name: 'permissions',
|
||||
component: () => import('@/modules/iam/pages/PermissionsView.vue'),
|
||||
meta: { title: '菜单权限', description: '维护菜单、页面、按钮和接口权限编码', permission: 'platform:permission:list' },
|
||||
},
|
||||
] satisfies RouteRecordRaw[]
|
||||
@@ -0,0 +1,42 @@
|
||||
export type RecordStatus = '正常' | '停用'
|
||||
export type RoleStatus = '启用' | '停用'
|
||||
export type PermissionType = '目录' | '菜单' | '按钮' | '接口'
|
||||
export type DataScope = '全部数据' | '本部门数据' | '本人数据' | '自定义'
|
||||
|
||||
export interface InternalUser {
|
||||
id: string
|
||||
account: string
|
||||
name: string
|
||||
department: string
|
||||
phone: string
|
||||
email: string
|
||||
roleIds: string[]
|
||||
status: RecordStatus
|
||||
lastLogin: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
description: string
|
||||
userCount: number
|
||||
dataScope: DataScope
|
||||
status: RoleStatus
|
||||
permissionIds: string[]
|
||||
builtIn?: boolean
|
||||
}
|
||||
|
||||
export interface Permission {
|
||||
id: string
|
||||
parentId: string | null
|
||||
name: string
|
||||
code: string
|
||||
type: PermissionType
|
||||
path: string
|
||||
method: string
|
||||
status: RoleStatus
|
||||
sort: number
|
||||
builtIn?: boolean
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { Permission } from '@/types/admin'
|
||||
import { buildPermissionTree, getDescendantIds, summarizePermissionSelection } from '@/utils/permissionTree'
|
||||
import type { Permission } from '@/modules/iam/types'
|
||||
import { buildPermissionTree, getDescendantIds, summarizePermissionSelection } from '@/modules/iam/utils/permissionTree'
|
||||
|
||||
const permissions: Permission[] = [
|
||||
{ id: 'action-b', parentId: 'menu', name: '操作 B', code: 'demo:b', type: '按钮', path: '', method: '-', status: '启用', sort: 20 },
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Permission } from '@/types/admin'
|
||||
import type { Permission } from '@/modules/iam/types'
|
||||
|
||||
export type PermissionTreeNode = Permission & { children?: PermissionTreeNode[] }
|
||||
export type PermissionMatcher = (permission: Permission) => boolean
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { mapPlatformPermission } from '@/utils/platformPermission'
|
||||
import { mapPlatformPermission } from '@/modules/iam/utils/platformPermission'
|
||||
|
||||
describe('mapPlatformPermission', () => {
|
||||
it('treats the string root parent as a root node and preserves long ids', () => {
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PlatformPermissionResource } from '@/api/platformIam'
|
||||
import type { Permission } from '@/types/admin'
|
||||
import type { PlatformPermissionResource } from '@/modules/iam/api/platformIam'
|
||||
import type { Permission } from '@/modules/iam/types'
|
||||
|
||||
export function mapPlatformPermission(item: PlatformPermissionResource): Permission {
|
||||
const parentId = String(item.parentId)
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/api/http'
|
||||
import { http } from '@/shared/api/http'
|
||||
import {
|
||||
changePlatformPartnerStatus,
|
||||
createPlatformPartner,
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type SavePlatformPartnerRequest,
|
||||
} from './platformPartners'
|
||||
|
||||
vi.mock('@/api/http', () => ({
|
||||
vi.mock('@/shared/api/http', () => ({
|
||||
http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn() },
|
||||
unwrapApiResponse: <T>(response: { data: T }) => response.data,
|
||||
}))
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/api/http'
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
|
||||
export type PlatformPartnerType = 'CUSTOMER' | 'SUPPLIER' | 'CARRIER' | 'CUSTOMS_BROKER' | 'SHIPPER' | 'CONSIGNEE'
|
||||
export type PlatformPartnerLegacyType = PlatformPartnerType | 'BOTH'
|
||||
+4
-4
@@ -5,12 +5,12 @@ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vites
|
||||
import {
|
||||
listPlatformPartners,
|
||||
type PlatformPartnerResource,
|
||||
} from '@/api/platformPartners'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
} from '@/modules/master-data/counterparties/api/platformPartners'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import PartnersView from './PartnersView.vue'
|
||||
|
||||
vi.mock('@/api/platformPartners', () => ({
|
||||
vi.mock('@/modules/master-data/counterparties/api/platformPartners', () => ({
|
||||
listPlatformPartners: vi.fn(),
|
||||
createPlatformPartner: vi.fn(),
|
||||
getPlatformPartner: vi.fn(),
|
||||
+7
-7
@@ -2,11 +2,11 @@
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules, type UploadFile } from 'element-plus'
|
||||
import { Building2, Download, Eye, MapPin, Pencil, Plus, Power, Upload } from '@lucide/vue'
|
||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import {
|
||||
changePlatformPartnerStatus,
|
||||
createPlatformPartner,
|
||||
@@ -21,8 +21,8 @@ import {
|
||||
type PlatformPartnerLegacyType,
|
||||
type PlatformPartnerType,
|
||||
type SavePlatformPartnerRequest,
|
||||
} from '@/api/platformPartners'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
} from '@/modules/master-data/counterparties/api/platformPartners'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
|
||||
const fixedTenantId = 10001
|
||||
const pageSize = ref(20)
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/api/http'
|
||||
import { http } from '@/shared/api/http'
|
||||
import {
|
||||
changePlatformExpressCompanyStatus,
|
||||
createPlatformExpressCompany,
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type SavePlatformExpressCompanyRequest,
|
||||
} from './platformExpressCompanies'
|
||||
|
||||
vi.mock('@/api/http', () => ({
|
||||
vi.mock('@/shared/api/http', () => ({
|
||||
http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn() },
|
||||
unwrapApiResponse: <T>(response: { data: T }) => response.data,
|
||||
}))
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/api/http'
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
|
||||
export type PlatformExpressCompanyStatus = 'ACTIVE' | 'INACTIVE'
|
||||
|
||||
+4
-4
@@ -2,12 +2,12 @@ import ElementPlus from 'element-plus'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { listPlatformExpressCompanies, type PlatformExpressCompanyResource } from '@/api/platformExpressCompanies'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { listPlatformExpressCompanies, type PlatformExpressCompanyResource } from '@/modules/master-data/express-companies/api/platformExpressCompanies'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import ExpressCompaniesView from './ExpressCompaniesView.vue'
|
||||
|
||||
vi.mock('@/api/platformExpressCompanies', () => ({
|
||||
vi.mock('@/modules/master-data/express-companies/api/platformExpressCompanies', () => ({
|
||||
listPlatformExpressCompanies: vi.fn(),
|
||||
createPlatformExpressCompany: vi.fn(),
|
||||
updatePlatformExpressCompany: vi.fn(),
|
||||
+7
-7
@@ -2,11 +2,11 @@
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { Download, ExternalLink, FileSpreadsheet, Pencil, Plus, Power, Truck, Upload } from '@lucide/vue'
|
||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import {
|
||||
changePlatformExpressCompanyStatus,
|
||||
createPlatformExpressCompany,
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
type PlatformExpressCompanyResource,
|
||||
type PlatformExpressCompanyStatus,
|
||||
type SavePlatformExpressCompanyRequest,
|
||||
} from '@/api/platformExpressCompanies'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
} from '@/modules/master-data/express-companies/api/platformExpressCompanies'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
|
||||
const fixedTenantId = 10001
|
||||
const access = useAccessPreviewStore()
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const masterDataRoutes = [
|
||||
{
|
||||
path: 'master-data/express-companies',
|
||||
name: 'express-companies',
|
||||
component: () => import('@/modules/master-data/express-companies/pages/ExpressCompaniesView.vue'),
|
||||
meta: { title: '快递公司', description: '查询平台可用的快递公司档案与公开联系信息', permission: 'platform:express:list' },
|
||||
},
|
||||
{
|
||||
path: 'master-data/counterparties',
|
||||
name: 'counterparties',
|
||||
component: () => import('@/modules/master-data/counterparties/pages/PartnersView.vue'),
|
||||
meta: { title: '往来单位', description: '维护客户、供应商及客户/供应商双重类型单位档案', permission: 'platform:counterparty:list' },
|
||||
},
|
||||
] satisfies RouteRecordRaw[]
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/api/http'
|
||||
import { http } from '@/shared/api/http'
|
||||
import {
|
||||
changePlatformWarehouseStatus,
|
||||
createPlatformWarehouse,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
type SavePlatformWarehouseRequest,
|
||||
} from './platformWarehouses'
|
||||
|
||||
vi.mock('@/api/http', () => ({
|
||||
vi.mock('@/shared/api/http', () => ({
|
||||
http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn() },
|
||||
unwrapApiResponse: <T>(response: { data: T }) => response.data,
|
||||
}))
|
||||
@@ -1,4 +1,4 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/api/http'
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
|
||||
export type PlatformWarehouseStatus = 'ACTIVE' | 'INACTIVE'
|
||||
|
||||
+4
-4
@@ -9,12 +9,12 @@ import {
|
||||
updatePlatformWarehouse,
|
||||
type PlatformWarehouseDetailResource,
|
||||
type PlatformWarehouseResource,
|
||||
} from '@/api/platformWarehouses'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
} from '@/modules/warehouses/api/platformWarehouses'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import WarehousesView from './WarehousesView.vue'
|
||||
|
||||
vi.mock('@/api/platformWarehouses', () => ({
|
||||
vi.mock('@/modules/warehouses/api/platformWarehouses', () => ({
|
||||
listPlatformWarehouses: vi.fn(),
|
||||
getPlatformWarehouse: vi.fn(),
|
||||
createPlatformWarehouse: vi.fn(),
|
||||
@@ -2,10 +2,10 @@
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { MapPin, Pencil, Plus, Power, Warehouse as WarehouseIcon } from '@lucide/vue'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||||
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/api/http'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import {
|
||||
changePlatformWarehouseStatus,
|
||||
createPlatformWarehouse,
|
||||
@@ -15,9 +15,9 @@ import {
|
||||
type PlatformWarehouseDetailResource,
|
||||
type PlatformWarehouseResource,
|
||||
type SavePlatformWarehouseRequest,
|
||||
} from '@/api/platformWarehouses'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import type { Warehouse } from '@/types/admin'
|
||||
} from '@/modules/warehouses/api/platformWarehouses'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import type { Warehouse } from '@/modules/warehouses/types'
|
||||
|
||||
const fixedTenantId = 10001
|
||||
const access = useAccessPreviewStore()
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const warehouseRoutes = [
|
||||
{
|
||||
path: 'warehouses',
|
||||
name: 'warehouses',
|
||||
component: () => import('@/modules/warehouses/pages/WarehousesView.vue'),
|
||||
meta: { title: '仓库管理', description: '维护客户头程下单可选择的平台收货仓', permission: 'platform:warehouse:list' },
|
||||
},
|
||||
] satisfies RouteRecordRaw[]
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { PlatformReceivingWarehouse } from '@chongchong/shared'
|
||||
|
||||
export type Warehouse = PlatformReceivingWarehouse
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
||||
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||
@@ -5,24 +5,24 @@ type PrototypeRoute = readonly [path: string, name: string, title: string]
|
||||
type ModuleView = () => Promise<Component>
|
||||
|
||||
const moduleViews = {
|
||||
dashboard: () => import('@/views/prototype/dashboard/ModuleView.vue'),
|
||||
customerManagement: () => import('@/views/prototype/customer-management/ModuleView.vue'),
|
||||
domesticWarehouse: () => import('@/views/prototype/domestic-warehouse/ModuleView.vue'),
|
||||
overseasDelivery: () => import('@/views/prototype/overseas-delivery/ModuleView.vue'),
|
||||
overseasInventory: () => import('@/views/prototype/overseas-inventory/ModuleView.vue'),
|
||||
serviceCenter: () => import('@/views/prototype/service-center/ModuleView.vue'),
|
||||
supplierCenter: () => import('@/views/prototype/supplier-center/ModuleView.vue'),
|
||||
containerManagement: () => import('@/views/prototype/container-management/ModuleView.vue'),
|
||||
overseasBusiness: () => import('@/views/prototype/overseas-business/ModuleView.vue'),
|
||||
overseasWarehouseOrders: () => import('@/views/prototype/overseas-warehouse-orders/ModuleView.vue'),
|
||||
fbaReturns: () => import('@/views/prototype/fba-returns/ModuleView.vue'),
|
||||
finance: () => import('@/views/prototype/finance/ModuleView.vue'),
|
||||
vehicleManagement: () => import('@/views/prototype/vehicle-management/ModuleView.vue'),
|
||||
reports: () => import('@/views/prototype/reports/ModuleView.vue'),
|
||||
general: () => import('@/views/prototype/general/ModuleView.vue'),
|
||||
sales: () => import('@/views/prototype/sales/ModuleView.vue'),
|
||||
messages: () => import('@/views/prototype/messages/ModuleView.vue'),
|
||||
systemManagement: () => import('@/views/prototype/system-management/ModuleView.vue'),
|
||||
dashboard: () => import('@/prototype/modules/dashboard/ModuleView.vue'),
|
||||
customerManagement: () => import('@/prototype/modules/customer-management/ModuleView.vue'),
|
||||
domesticWarehouse: () => import('@/prototype/modules/domestic-warehouse/ModuleView.vue'),
|
||||
overseasDelivery: () => import('@/prototype/modules/overseas-delivery/ModuleView.vue'),
|
||||
overseasInventory: () => import('@/prototype/modules/overseas-inventory/ModuleView.vue'),
|
||||
serviceCenter: () => import('@/prototype/modules/service-center/ModuleView.vue'),
|
||||
supplierCenter: () => import('@/prototype/modules/supplier-center/ModuleView.vue'),
|
||||
containerManagement: () => import('@/prototype/modules/container-management/ModuleView.vue'),
|
||||
overseasBusiness: () => import('@/prototype/modules/overseas-business/ModuleView.vue'),
|
||||
overseasWarehouseOrders: () => import('@/prototype/modules/overseas-warehouse-orders/ModuleView.vue'),
|
||||
fbaReturns: () => import('@/prototype/modules/fba-returns/ModuleView.vue'),
|
||||
finance: () => import('@/prototype/modules/finance/ModuleView.vue'),
|
||||
vehicleManagement: () => import('@/prototype/modules/vehicle-management/ModuleView.vue'),
|
||||
reports: () => import('@/prototype/modules/reports/ModuleView.vue'),
|
||||
general: () => import('@/prototype/modules/general/ModuleView.vue'),
|
||||
sales: () => import('@/prototype/modules/sales/ModuleView.vue'),
|
||||
messages: () => import('@/prototype/modules/messages/ModuleView.vue'),
|
||||
systemManagement: () => import('@/prototype/modules/system-management/ModuleView.vue'),
|
||||
} satisfies Record<string, ModuleView>
|
||||
|
||||
function createRoutes(view: ModuleView, routes: PrototypeRoute[]): RouteRecordRaw[] {
|
||||
@@ -1,133 +0,0 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import AppLayout from '@/layouts/AppLayout.vue'
|
||||
import { decideRouteAccess } from '@/router/accessDecision'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { prototypeModuleRoutes } from '@/router/prototypeModuleRoutes'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('@/views/LoginView.vue'),
|
||||
meta: { title: '登录' },
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: AppLayout,
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'home',
|
||||
component: () => import('@/views/ModuleView.vue'),
|
||||
meta: { title: '进入管理后台', description: '正在进入当前角色可访问的业务页面' },
|
||||
},
|
||||
...prototypeModuleRoutes,
|
||||
{
|
||||
path: 'customers',
|
||||
name: 'customers',
|
||||
component: () => import('@/views/CustomersView.vue'),
|
||||
meta: { title: '客户管理', description: '维护客户企业资料、合作状态和注册审核', permission: 'platform:customer:list' },
|
||||
},
|
||||
{
|
||||
path: 'customers/reviews',
|
||||
name: 'enterprise-reviews',
|
||||
component: () => import('@/views/EnterpriseReviewsView.vue'),
|
||||
meta: { title: '企业审核', description: '核验企业资质并维护客户下单资格', permission: 'platform:customer:review' },
|
||||
},
|
||||
{
|
||||
path: 'warehouses',
|
||||
name: 'warehouses',
|
||||
component: () => import('@/views/WarehousesView.vue'),
|
||||
meta: { title: '仓库管理', description: '维护客户头程下单可选择的平台收货仓', permission: 'platform:warehouse:list' },
|
||||
},
|
||||
{
|
||||
path: 'orders/first-leg',
|
||||
name: 'first-leg-orders',
|
||||
component: () => import('@/views/FirstLegOrdersView.vue'),
|
||||
meta: { title: '头程订单', description: '创建、受理和跟进客户提交的头程运输订单', permission: 'platform:order:list' },
|
||||
},
|
||||
{
|
||||
path: 'master-data/express-companies',
|
||||
name: 'express-companies',
|
||||
component: () => import('@/views/ExpressCompaniesView.vue'),
|
||||
meta: { title: '快递公司', description: '查询平台可用的快递公司档案与公开联系信息', permission: 'platform:express:list' },
|
||||
},
|
||||
{
|
||||
path: 'master-data/counterparties',
|
||||
name: 'counterparties',
|
||||
component: () => import('@/views/PartnersView.vue'),
|
||||
meta: { title: '往来单位', description: '维护客户、供应商及客户/供应商双重类型单位档案', permission: 'platform:counterparty:list' },
|
||||
},
|
||||
{
|
||||
path: 'downloads',
|
||||
name: 'download-center',
|
||||
component: () => import('@/views/DownloadCenterView.vue'),
|
||||
meta: { title: '下载中心', description: '查看异步导出任务并下载已生成文件', permission: 'platform:download:list' },
|
||||
},
|
||||
{
|
||||
path: 'system/users',
|
||||
name: 'users',
|
||||
component: () => import('@/views/InternalUsersView.vue'),
|
||||
meta: { title: '用户管理', description: '统一管理平台和企业租户用户、状态及角色', permission: 'platform:user:list' },
|
||||
},
|
||||
{
|
||||
path: 'system/roles',
|
||||
name: 'roles',
|
||||
component: () => import('@/views/RolesView.vue'),
|
||||
meta: { title: '角色管理', description: '维护平台角色并配置菜单及操作权限', permission: 'platform:role:list' },
|
||||
},
|
||||
{
|
||||
path: 'system/permissions',
|
||||
name: 'permissions',
|
||||
component: () => import('@/views/PermissionsView.vue'),
|
||||
meta: { title: '菜单权限', description: '维护菜单、页面、按钮和接口权限编码', permission: 'platform:permission:list' },
|
||||
},
|
||||
{
|
||||
path: '403',
|
||||
name: 'forbidden',
|
||||
component: () => import('@/views/ForbiddenView.vue'),
|
||||
meta: { title: '权限不足', description: '当前登录账号没有访问此页面的权限' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: '/:pathMatch(.*)*', redirect: '/' },
|
||||
],
|
||||
})
|
||||
|
||||
router.beforeEach((to) => {
|
||||
document.title = to.meta.title ? `${to.meta.title} - 柏悦物流管理后台` : '柏悦物流管理后台'
|
||||
const auth = useAuthStore()
|
||||
if (to.name === 'login' && auth.isAuthenticated) {
|
||||
return { name: 'home' }
|
||||
}
|
||||
if (!auth.isAuthenticated && !to.meta.requiresAuth) return true
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
if (auth.isAuthenticated) access.initializeAfterLogin()
|
||||
const decision = decideRouteAccess({
|
||||
requiresAuth: to.meta.requiresAuth,
|
||||
isAuthenticated: auth.isAuthenticated,
|
||||
fullPath: to.fullPath,
|
||||
permission: to.meta.permission,
|
||||
hasPermission: to.meta.permission ? access.can(to.meta.permission) : true,
|
||||
})
|
||||
|
||||
if (decision.type === 'login') {
|
||||
return { name: 'login', query: { redirect: decision.redirect } }
|
||||
}
|
||||
if (decision.type === 'forbidden') {
|
||||
return { name: 'forbidden', query: { from: decision.from }, replace: true }
|
||||
}
|
||||
if (to.name === 'home') {
|
||||
return access.firstAccessiblePath
|
||||
? { path: access.firstAccessiblePath, replace: true }
|
||||
: { name: 'forbidden', replace: true }
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
export default router
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user