优化构架
This commit is contained in:
@@ -16,20 +16,25 @@
|
|||||||
|
|
||||||
## 目录与职责
|
## 目录与职责
|
||||||
|
|
||||||
- `src/views/`:路由级页面和业务编排;复杂、可复用的展示或交互下沉到组件。
|
- `src/app/`:应用装配层,包含布局、根路由、全局守卫和启动入口;不承载具体业务或被业务模块反向依赖。
|
||||||
- `src/components/`:无路由依赖的复用组件,输入输出应有明确类型。
|
- `src/core/`:认证会话、访问控制、导航配置、未保存工作保护和路由安全工具等全应用基础能力;可以被 `app` 和业务模块使用,但不得依赖业务模块。
|
||||||
- `src/layouts/`:应用壳层、导航和全局页面结构。
|
- `src/modules/`:按客户、仓库、头程订单、主数据、下载中心、IAM 等稳定业务领域组织页面、API、类型、组件和路由。新增业务页面必须进入对应模块,不再建立根级 `views/`。
|
||||||
- `src/router/`:路由、标题、登录保护和权限元数据;新增模块时同步维护导航与路由配置。
|
- `src/shared/`:与具体业务无关的后台公共 HTTP、组件和工具。模块专属能力先留在模块内部,只有被无关模块真实复用时才上移。
|
||||||
- `src/stores/`:跨页面状态和业务动作;仅页面局部使用的状态留在组件内。
|
- `src/prototype/`:未纳入当前迭代的原型页面和原型路由;正式模块不得依赖原型代码。
|
||||||
- `src/api/`:HTTP 客户端和接口适配;页面与组件不得散落创建 Axios 实例。
|
- `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`,不得提交真实密钥或环境专属凭据。
|
- `VITE_API_BASE_URL` 控制浏览器请求前缀,`API_PROXY_TARGET` 控制 Vite 开发代理上游;后者只由开发服务器读取,不进入客户端构建。新增或变更环境变量时同步更新 `.env.example`,不得提交真实密钥或环境专属凭据。
|
||||||
- `src/types/`:后台专属类型;跨应用共享的稳定契约放到 `packages/shared`。
|
- 模块专属类型放在对应模块;跨应用共享的稳定契约放到 `packages/shared`。
|
||||||
- `src/styles/`:设计令牌和全局样式;优先复用现有 `--cc-*` 变量和组件样式。
|
- `src/styles/`:设计令牌和全局样式;优先复用现有 `--cc-*` 变量和组件样式。
|
||||||
|
|
||||||
|
依赖方向保持为 `app -> modules/core/shared`、`modules -> core/shared`、`core -> shared`、`shared -> 无上层依赖`。模块间确需调用时只能引用对方根入口公开的 API 或契约,不得引用其他模块的页面、内部组件或内部目录。
|
||||||
|
|
||||||
## 业务与安全约束
|
## 业务与安全约束
|
||||||
|
|
||||||
- 菜单隐藏和前端路由守卫只改善体验,不能替代服务端鉴权。新增操作必须使用稳定的权限编码,并与 API 权限保持一致。
|
- 菜单隐藏和前端路由守卫只改善体验,不能替代服务端鉴权。新增操作必须使用稳定的权限编码,并与 API 权限保持一致。
|
||||||
- 认证统一经过 `src/stores/auth.ts` 和 `src/api/http.ts`;不要在页面中复制令牌读写或手工拼装 `Authorization`。
|
- 认证统一经过 `src/core/auth/auth.ts` 和 `src/shared/api/http.ts`;不要在页面中复制令牌读写或手工拼装 `Authorization`。
|
||||||
- 当前登录和部分数据仍可能是模拟实现。接入 `services/api` 时删除对应模拟路径,不允许让生产逻辑静默回退到假数据。
|
- 当前登录和部分数据仍可能是模拟实现。接入 `services/api` 时删除对应模拟路径,不允许让生产逻辑静默回退到假数据。
|
||||||
- 删除、停用、改权限等高风险操作必须有明确确认、处理中、成功和失败反馈;失败时保留用户上下文,避免误以为已生效。
|
- 删除、停用、改权限等高风险操作必须有明确确认、处理中、成功和失败反馈;失败时保留用户上下文,避免误以为已生效。
|
||||||
- 日志、提示和示例数据不得包含真实账号、令牌、客户隐私或生产数据。
|
- 日志、提示和示例数据不得包含真实账号、令牌、客户隐私或生产数据。
|
||||||
|
|||||||
@@ -23,9 +23,19 @@ pnpm build
|
|||||||
pnpm test
|
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 依赖。
|
依赖和 `pnpm-lock.yaml` 均保存在当前前端项目中,不需要在仓库根目录安装 Node 依赖。
|
||||||
|
|
||||||
工作台已从一期路由与菜单移除;登录后按当前角色进入第一个有权限的业务页面。一期 Admin 路由聚焦登录、客户及注册审核、平台收货仓、快递公司、往来单位、头程订单、内部用户、角色管理和菜单权限。
|
工作台已从一期菜单和默认入口移除;登录后按当前角色进入第一个有权限的业务页面。历史工作台仍保留为不展示菜单的原型路由。一期 Admin 正式路由聚焦登录、客户及注册审核、平台收货仓、快递公司、往来单位、头程订单、内部用户、角色管理和菜单权限。
|
||||||
|
|
||||||
后台菜单仅展示已接入的一期模块;其余平台原型菜单保留为导航配置中的注释,待对应业务契约接入后再恢复展示。企业审核、仓库、头程订单、平台角色、菜单及按钮权限资源均接入真实接口;用户管理通过 IAM 统一维护平台账号和企业租户账号。运行时不再提供本地演示业务数据或静态角色预览。
|
后台菜单仅展示已接入的一期模块;其余平台原型菜单保留为导航配置中的注释,待对应业务契约接入后再恢复展示。企业审核、仓库、头程订单、平台角色、菜单及按钮权限资源均接入真实接口;用户管理通过 IAM 统一维护平台账号和企业租户账号。运行时不再提供本地演示业务数据或静态角色预览。
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import { describe, expect, it } from 'vitest'
|
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'
|
import AdminSidebarMenu from './AdminSidebarMenu.vue'
|
||||||
|
|
||||||
const stubs = {
|
const stubs = {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import type { AdminNavigationNode } from '@/navigation/adminNavigation'
|
import type { AdminNavigationNode } from '@/core/navigation/adminNavigation'
|
||||||
import AdminSidebarMenuNode from './AdminSidebarMenuNode.vue'
|
import AdminSidebarMenuNode from './AdminSidebarMenuNode.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { AdminNavigationNode } from '@/navigation/adminNavigation'
|
import type { AdminNavigationNode } from '@/core/navigation/adminNavigation'
|
||||||
|
|
||||||
defineOptions({ name: 'AdminSidebarMenuNode' })
|
defineOptions({ name: 'AdminSidebarMenuNode' })
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ import { flushPromises, mount } from '@vue/test-utils'
|
|||||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { ElMessageBox } from 'element-plus'
|
import { ElMessageBox } from 'element-plus'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/core/auth/auth'
|
||||||
import AppLayout from './AppLayout.vue'
|
import AppLayout from './AppLayout.vue'
|
||||||
|
|
||||||
vi.mock('element-plus', async (importOriginal) => {
|
vi.mock('element-plus', async (importOriginal) => {
|
||||||
@@ -3,13 +3,13 @@ import { computed, ref, watch } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { LogOut, Menu } from '@lucide/vue'
|
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 baiyueLogoWhite from '@/assets/brand/baiyue-logo-white.png'
|
||||||
import baiyueMarkWhite from '@/assets/brand/baiyue-mark-white.png'
|
import baiyueMarkWhite from '@/assets/brand/baiyue-mark-white.png'
|
||||||
import AdminSidebarMenu from '@/components/AdminSidebarMenu.vue'
|
import AdminSidebarMenu from '@/app/components/AdminSidebarMenu.vue'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/core/auth/auth'
|
||||||
import { useUnsavedWorkStore } from '@/stores/unsavedWork'
|
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { ShieldX } from '@lucide/vue'
|
import { ShieldX } from '@lucide/vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { sanitizeInternalPath } from '@/router/accessDecision'
|
import { sanitizeInternalPath } from '@/core/routing/accessDecision'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { adminNavigation, getNavigationLeaves } from '@/core/navigation/adminNavigation'
|
||||||
import router from './index'
|
import router from './index'
|
||||||
|
|
||||||
describe('admin router access metadata', () => {
|
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')
|
const home = router.getRoutes().find((route) => route.name === 'home')
|
||||||
|
|
||||||
expect(home?.path).toBe('/')
|
expect(home?.path).toBe('/')
|
||||||
expect(home?.meta.permission).toBeUndefined()
|
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', () => {
|
it('protects warehouse management with its stable list permission', () => {
|
||||||
@@ -53,4 +54,14 @@ describe('admin router access metadata', () => {
|
|||||||
expect(forbidden?.meta?.permission).toBeUndefined()
|
expect(forbidden?.meta?.permission).toBeUndefined()
|
||||||
expect(resolved.matched.some((record) => record.meta.requiresAuth)).toBe(true)
|
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 { createPinia, setActivePinia } from 'pinia'
|
||||||
import { beforeEach, describe, expect, it } from 'vitest'
|
import { beforeEach, describe, expect, it } from 'vitest'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/core/auth/auth'
|
||||||
|
|
||||||
describe('server access store', () => {
|
describe('server access store', () => {
|
||||||
beforeEach(() => setActivePinia(createPinia()))
|
beforeEach(() => setActivePinia(createPinia()))
|
||||||
@@ -4,8 +4,8 @@ import {
|
|||||||
adminNavigation,
|
adminNavigation,
|
||||||
filterAdminNavigation,
|
filterAdminNavigation,
|
||||||
getFirstNavigationLeaf,
|
getFirstNavigationLeaf,
|
||||||
} from '@/navigation/adminNavigation'
|
} from '@/core/navigation/adminNavigation'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/core/auth/auth'
|
||||||
|
|
||||||
export const useAccessPreviewStore = defineStore('access-preview', () => {
|
export const useAccessPreviewStore = defineStore('access-preview', () => {
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
@@ -7,7 +7,7 @@ const mocks = vi.hoisted(() => ({
|
|||||||
refreshHandler: null as null | (() => Promise<string>),
|
refreshHandler: null as null | (() => Promise<string>),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/api/http', () => ({
|
vi.mock('@/shared/api/http', () => ({
|
||||||
http: { post: mocks.post },
|
http: { post: mocks.post },
|
||||||
registerAuthRefreshHandler: (handler: () => Promise<string>) => { mocks.refreshHandler = handler },
|
registerAuthRefreshHandler: (handler: () => Promise<string>) => { mocks.refreshHandler = handler },
|
||||||
unwrapApiResponse: <T>(response: { code: string; message: string; data: T }) => {
|
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,
|
listCurrentPlatformPermissionCodes: mocks.listCurrentPlatformPermissionCodes,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -6,9 +6,10 @@ import {
|
|||||||
registerAuthRefreshHandler,
|
registerAuthRefreshHandler,
|
||||||
unwrapApiResponse,
|
unwrapApiResponse,
|
||||||
type ApiEnvelope,
|
type ApiEnvelope,
|
||||||
} from '@/api/http'
|
} from '@/shared/api/http'
|
||||||
import { listCurrentPlatformPermissionCodes, type PlatformPermissionResource } from '@/api/platformIam'
|
import type { PlatformPermissionResource } from '@/shared/types/platformPermission'
|
||||||
import { decodeJwtClaims } from '@/utils/jwt'
|
import { decodeJwtClaims } from './jwt'
|
||||||
|
import { listCurrentPlatformPermissionCodes } from './platformAuthorization'
|
||||||
|
|
||||||
const TOKEN_KEY = 'cc-admin-token'
|
const TOKEN_KEY = 'cc-admin-token'
|
||||||
const REFRESH_TOKEN_KEY = 'cc-admin-refresh-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 { beforeEach, describe, expect, it } from 'vitest'
|
||||||
import { createPinia, setActivePinia } from 'pinia'
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
import { useUnsavedWorkStore } from '@/stores/unsavedWork'
|
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
|
||||||
|
|
||||||
describe('unsaved work store', () => {
|
describe('unsaved work store', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
+2
-2
@@ -2,8 +2,8 @@ import { computed, defineComponent, nextTick, reactive, ref } from 'vue'
|
|||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import { beforeEach, describe, expect, it } from 'vitest'
|
import { beforeEach, describe, expect, it } from 'vitest'
|
||||||
import { createPinia, setActivePinia } from 'pinia'
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
|
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||||
import { useUnsavedWorkStore } from '@/stores/unsavedWork'
|
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
|
||||||
|
|
||||||
interface HarnessModel {
|
interface HarnessModel {
|
||||||
name: string
|
name: string
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { computed, onScopeDispose, ref, watch, type ComputedRef, type Ref } from 'vue'
|
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> {
|
interface UnsavedWorkRegistrationOptions<T extends object> {
|
||||||
key: string
|
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/style/css'
|
||||||
import 'element-plus/es/components/message-box/style/css'
|
import 'element-plus/es/components/message-box/style/css'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import { registerAuthSessionExpiredHandler } from '@/api/http'
|
import router from '@/app/router'
|
||||||
import router from './router'
|
import { sanitizeInternalPath } from '@/core/routing/accessDecision'
|
||||||
import { sanitizeInternalPath } from '@/router/accessDecision'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/core/auth/auth'
|
||||||
import { useUnsavedWorkStore } from '@/stores/unsavedWork'
|
import { registerAuthSessionExpiredHandler } from '@/shared/api/http'
|
||||||
import './styles/index.css'
|
import './styles/index.css'
|
||||||
|
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
@@ -32,4 +32,3 @@ registerAuthSessionExpiredHandler(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
createApp(App).use(pinia).use(router).mount('#app')
|
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 { flushPromises, mount } from '@vue/test-utils'
|
||||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/core/auth/auth'
|
||||||
import LoginView from './LoginView.vue'
|
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 = {
|
const formStub = {
|
||||||
template: '<form><slot /></form>',
|
template: '<form><slot /></form>',
|
||||||
setup(_: unknown, { expose }: { expose: (exposed: { validate: () => Promise<boolean> }) => void }) {
|
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 { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||||
import { ArrowRight, LockKeyhole, ShieldCheck, UserRound } from '@lucide/vue'
|
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 baiyueLogoWhite from '@/assets/brand/baiyue-logo-white.png'
|
||||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||||
import { sanitizeInternalPath } from '@/router/accessDecision'
|
import { sanitizeInternalPath } from '@/core/routing/accessDecision'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/core/auth/auth'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -10,13 +10,13 @@ import {
|
|||||||
updatePlatformTenant,
|
updatePlatformTenant,
|
||||||
uploadPlatformTenantQualification,
|
uploadPlatformTenantQualification,
|
||||||
type PlatformTenantResource,
|
type PlatformTenantResource,
|
||||||
} from '@/api/platformIam'
|
} from '@/modules/iam'
|
||||||
import { createPlatformExportTask } from '@/api/platformExportTasks'
|
import { createPlatformExportTask } from '@/modules/downloads'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/core/auth/auth'
|
||||||
import CustomersView from './CustomersView.vue'
|
import CustomersView from './CustomersView.vue'
|
||||||
|
|
||||||
vi.mock('@/api/platformIam', () => ({
|
vi.mock('@/modules/iam', () => ({
|
||||||
changePlatformTenantStatus: vi.fn(),
|
changePlatformTenantStatus: vi.fn(),
|
||||||
createPlatformTenant: vi.fn(),
|
createPlatformTenant: vi.fn(),
|
||||||
downloadTenantQualification: vi.fn(),
|
downloadTenantQualification: vi.fn(),
|
||||||
@@ -30,7 +30,7 @@ vi.mock('@/api/platformIam', () => ({
|
|||||||
uploadPlatformTenantQualification: vi.fn(),
|
uploadPlatformTenantQualification: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/api/platformExportTasks', () => ({ createPlatformExportTask: vi.fn() }))
|
vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
|
||||||
|
|
||||||
class ResizeObserverStub implements ResizeObserver {
|
class ResizeObserverStub implements ResizeObserver {
|
||||||
observe() {}
|
observe() {}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { Building2, CheckCircle2, Download, Eye, FileCheck2, FileText, FileUp, Pencil, Plus, Power } from '@lucide/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 { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
|
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
||||||
import { createPlatformExportTask } from '@/api/platformExportTasks'
|
import { createPlatformExportTask } from '@/modules/downloads'
|
||||||
import {
|
import {
|
||||||
changePlatformTenantStatus,
|
changePlatformTenantStatus,
|
||||||
createPlatformTenant,
|
createPlatformTenant,
|
||||||
@@ -21,11 +21,11 @@ import {
|
|||||||
type PlatformTenantQualification,
|
type PlatformTenantQualification,
|
||||||
type PlatformTenantResource,
|
type PlatformTenantResource,
|
||||||
type PlatformTenantStatus,
|
type PlatformTenantStatus,
|
||||||
} from '@/api/platformIam'
|
} from '@/modules/iam'
|
||||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
|
|
||||||
type CustomerFilters = QueryModel & { keyword: string; status: PlatformTenantStatus | '' }
|
type CustomerFilters = QueryModel & { keyword: string; status: PlatformTenantStatus | '' }
|
||||||
type QualificationDraft = {
|
type QualificationDraft = {
|
||||||
+4
-4
@@ -11,12 +11,12 @@ import {
|
|||||||
rejectEnterpriseReview,
|
rejectEnterpriseReview,
|
||||||
type EnterpriseReview,
|
type EnterpriseReview,
|
||||||
type PlatformTenantQualification,
|
type PlatformTenantQualification,
|
||||||
} from '@/api/platformIam'
|
} from '@/modules/iam'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/core/auth/auth'
|
||||||
import EnterpriseReviewsView from './EnterpriseReviewsView.vue'
|
import EnterpriseReviewsView from './EnterpriseReviewsView.vue'
|
||||||
|
|
||||||
vi.mock('@/api/platformIam', () => ({
|
vi.mock('@/modules/iam', () => ({
|
||||||
listEnterpriseReviews: vi.fn(),
|
listEnterpriseReviews: vi.fn(),
|
||||||
listTenantQualifications: vi.fn(),
|
listTenantQualifications: vi.fn(),
|
||||||
downloadTenantQualification: vi.fn(),
|
downloadTenantQualification: vi.fn(),
|
||||||
+6
-6
@@ -11,12 +11,12 @@ import {
|
|||||||
rejectEnterpriseReview,
|
rejectEnterpriseReview,
|
||||||
type EnterpriseReview,
|
type EnterpriseReview,
|
||||||
type PlatformTenantQualification,
|
type PlatformTenantQualification,
|
||||||
} from '@/api/platformIam'
|
} from '@/modules/iam'
|
||||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
|
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
||||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
|
|
||||||
const access = useAccessPreviewStore()
|
const access = useAccessPreviewStore()
|
||||||
const loading = ref(false)
|
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 { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { http } from '@/api/http'
|
import { http } from '@/shared/api/http'
|
||||||
import {
|
import {
|
||||||
createPlatformExportTask,
|
createPlatformExportTask,
|
||||||
downloadPlatformExportTask,
|
downloadPlatformExportTask,
|
||||||
@@ -7,8 +7,8 @@ import {
|
|||||||
retryPlatformExportTask,
|
retryPlatformExportTask,
|
||||||
} from './platformExportTasks'
|
} from './platformExportTasks'
|
||||||
|
|
||||||
vi.mock('@/api/http', async () => {
|
vi.mock('@/shared/api/http', async () => {
|
||||||
const actual = await vi.importActual<typeof import('@/api/http')>('@/api/http')
|
const actual = await vi.importActual<typeof import('@/shared/api/http')>('@/shared/api/http')
|
||||||
return { ...actual, http: { get: vi.fn(), post: vi.fn() } }
|
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 ExportTaskStatus = 'PENDING' | 'RUNNING' | 'SUCCEEDED' | 'FAILED' | 'EXPIRED'
|
||||||
export type ExportTaskType = 'CUSTOMER_LIST' | string
|
export type ExportTaskType = 'CUSTOMER_LIST' | string
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './api/platformExportTasks'
|
||||||
+21
-4
@@ -7,12 +7,12 @@ import {
|
|||||||
listPlatformExportTasks,
|
listPlatformExportTasks,
|
||||||
retryPlatformExportTask,
|
retryPlatformExportTask,
|
||||||
type PlatformExportTask,
|
type PlatformExportTask,
|
||||||
} from '@/api/platformExportTasks'
|
} from '@/modules/downloads/api/platformExportTasks'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/core/auth/auth'
|
||||||
import DownloadCenterView from './DownloadCenterView.vue'
|
import DownloadCenterView from './DownloadCenterView.vue'
|
||||||
|
|
||||||
vi.mock('@/api/platformExportTasks', () => ({
|
vi.mock('@/modules/downloads/api/platformExportTasks', () => ({
|
||||||
downloadPlatformExportTask: vi.fn(),
|
downloadPlatformExportTask: vi.fn(),
|
||||||
listPlatformExportTasks: vi.fn(),
|
listPlatformExportTasks: vi.fn(),
|
||||||
retryPlatformExportTask: vi.fn(),
|
retryPlatformExportTask: vi.fn(),
|
||||||
@@ -33,6 +33,7 @@ const task: PlatformExportTask = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enableAutoUnmount(afterEach)
|
enableAutoUnmount(afterEach)
|
||||||
|
afterEach(() => vi.useRealTimers())
|
||||||
|
|
||||||
describe('DownloadCenterView', () => {
|
describe('DownloadCenterView', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -62,6 +63,7 @@ describe('DownloadCenterView', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(document.body.textContent).toContain('客户列表')
|
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() === '下载')
|
const button = [...document.querySelectorAll<HTMLButtonElement>('button')].find((item) => item.textContent?.trim() === '下载')
|
||||||
button?.click()
|
button?.click()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
@@ -80,9 +82,24 @@ describe('DownloadCenterView', () => {
|
|||||||
mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(document.querySelector('.failure-cell')?.classList.contains('failure-message')).toBe(true)
|
||||||
const button = [...document.querySelectorAll<HTMLButtonElement>('button')].find((item) => item.textContent?.trim() === '重试')
|
const button = [...document.querySelectorAll<HTMLButtonElement>('button')].find((item) => item.textContent?.trim() === '重试')
|
||||||
button?.click()
|
button?.click()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(retryPlatformExportTask).toHaveBeenCalledWith('9')
|
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 { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { Download, RefreshCw, RotateCcw } from '@lucide/vue'
|
import { Download, RefreshCw, RotateCcw } from '@lucide/vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
|
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
||||||
import {
|
import {
|
||||||
downloadPlatformExportTask,
|
downloadPlatformExportTask,
|
||||||
listPlatformExportTasks,
|
listPlatformExportTasks,
|
||||||
retryPlatformExportTask,
|
retryPlatformExportTask,
|
||||||
type ExportTaskStatus,
|
type ExportTaskStatus,
|
||||||
type PlatformExportTask,
|
type PlatformExportTask,
|
||||||
} from '@/api/platformExportTasks'
|
} from '@/modules/downloads/api/platformExportTasks'
|
||||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
|
|
||||||
type DownloadFilters = QueryModel & {
|
type DownloadFilters = QueryModel & {
|
||||||
exportType: string
|
exportType: string
|
||||||
@@ -31,6 +31,7 @@ const loadError = ref<ApiErrorDetails | null>(null)
|
|||||||
const result = ref({ records: [] as PlatformExportTask[], total: 0, pages: 0 })
|
const result = ref({ records: [] as PlatformExportTask[], total: 0, pages: 0 })
|
||||||
const filters = ref<DownloadFilters>({ exportType: '', status: '', requestedRange: [] })
|
const filters = ref<DownloadFilters>({ exportType: '', status: '', requestedRange: [] })
|
||||||
let refreshTimer: number | undefined
|
let refreshTimer: number | undefined
|
||||||
|
let disposed = false
|
||||||
|
|
||||||
const queryFields: QueryField[] = [
|
const queryFields: QueryField[] = [
|
||||||
{ key: 'exportType', type: 'select', placeholder: '全部任务类型', options: [
|
{ key: 'exportType', type: 'select', placeholder: '全部任务类型', options: [
|
||||||
@@ -67,7 +68,7 @@ function clearRefreshTimer() {
|
|||||||
|
|
||||||
function scheduleRefresh() {
|
function scheduleRefresh() {
|
||||||
clearRefreshTimer()
|
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) {
|
async function loadTasks(background = false) {
|
||||||
@@ -167,7 +168,10 @@ function canRetry(task: PlatformExportTask) {
|
|||||||
|
|
||||||
watch([page, pageSize], () => { void loadTasks() })
|
watch([page, pageSize], () => { void loadTasks() })
|
||||||
onMounted(loadTasks)
|
onMounted(loadTasks)
|
||||||
onBeforeUnmount(clearRefreshTimer)
|
onBeforeUnmount(() => {
|
||||||
|
disposed = true
|
||||||
|
clearRefreshTimer()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -188,7 +192,7 @@ onBeforeUnmount(clearRefreshTimer)
|
|||||||
<template #actions="{ row }"><div class="table-actions">
|
<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="(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>
|
<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>
|
</div></template>
|
||||||
<template #type="{ row }">{{ typeLabel((row as PlatformExportTask).exportType) }}</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>
|
<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 #requestedAt="{ row }">{{ formatDate((row as PlatformExportTask).requestedAt) }}</template>
|
||||||
<template #completedAt="{ row }">{{ formatDate((row as PlatformExportTask).completedAt) }}</template>
|
<template #completedAt="{ row }">{{ formatDate((row as PlatformExportTask).completedAt) }}</template>
|
||||||
<template #expiresAt="{ row }">{{ formatDate((row as PlatformExportTask).expiresAt) }}</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>
|
</DataTable>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,7 +219,8 @@ onBeforeUnmount(clearRefreshTimer)
|
|||||||
.table-actions { display: flex; min-height: 32px; align-items: center; gap: 8px; }
|
.table-actions { display: flex; min-height: 32px; align-items: center; gap: 8px; }
|
||||||
.table-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
.table-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||||
.action-placeholder { color: var(--cc-muted); }
|
.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; }
|
small { display: block; margin-top: 3px; color: var(--cc-muted); font-size: 11px; }
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.download-error, .download-table-header { align-items: stretch; flex-direction: column; }
|
.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 { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { http } from '@/api/http'
|
import { http } from '@/shared/api/http'
|
||||||
import {
|
import {
|
||||||
acceptPlatformFirstLegOrder,
|
acceptPlatformFirstLegOrder,
|
||||||
addPlatformFirstLegOrderNote,
|
addPlatformFirstLegOrderNote,
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
updatePlatformFirstLegOrderExpress,
|
updatePlatformFirstLegOrderExpress,
|
||||||
} from './platformFirstLegOrders'
|
} from './platformFirstLegOrders'
|
||||||
|
|
||||||
vi.mock('@/api/http', () => ({
|
vi.mock('@/shared/api/http', () => ({
|
||||||
http: { get: vi.fn(), post: vi.fn(), put: vi.fn() },
|
http: { get: vi.fn(), post: vi.fn(), put: vi.fn() },
|
||||||
unwrapApiResponse: <T>(response: { data: T }) => response.data,
|
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 =
|
export type PlatformFirstLegOrderStatus =
|
||||||
| 'DRAFT'
|
| 'DRAFT'
|
||||||
+7
-7
@@ -17,13 +17,13 @@ import {
|
|||||||
type PlatformFirstLegOrderDetailResource,
|
type PlatformFirstLegOrderDetailResource,
|
||||||
type PlatformFirstLegOrderResource,
|
type PlatformFirstLegOrderResource,
|
||||||
type PlatformFirstLegOrderStatus,
|
type PlatformFirstLegOrderStatus,
|
||||||
} from '@/api/platformFirstLegOrders'
|
} from '@/modules/first-leg-orders/api/platformFirstLegOrders'
|
||||||
import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/api/http'
|
import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||||
import StatusBadge from '@/components/StatusBadge.vue'
|
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
|
|
||||||
const PHASE_ONE_TENANT_ID = '10001'
|
const PHASE_ONE_TENANT_ID = '10001'
|
||||||
const route = useRoute()
|
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 { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { http } from '@/api/http'
|
import { http } from '@/shared/api/http'
|
||||||
import {
|
import {
|
||||||
assignPlatformInternalUserRoles,
|
assignPlatformInternalUserRoles,
|
||||||
changePlatformManagedUserStatus,
|
changePlatformManagedUserStatus,
|
||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
listPlatformTenants,
|
listPlatformTenants,
|
||||||
listPlatformTenantOrderCounts,
|
listPlatformTenantOrderCounts,
|
||||||
listTenantQualifications,
|
listTenantQualifications,
|
||||||
listCurrentPlatformPermissionCodes,
|
|
||||||
listPlatformPermissions,
|
listPlatformPermissions,
|
||||||
listPlatformRoles,
|
listPlatformRoles,
|
||||||
listPlatformInternalUsers,
|
listPlatformInternalUsers,
|
||||||
@@ -40,7 +39,7 @@ import {
|
|||||||
type PlatformRoleResource,
|
type PlatformRoleResource,
|
||||||
} from './platformIam'
|
} from './platformIam'
|
||||||
|
|
||||||
vi.mock('@/api/http', () => ({
|
vi.mock('@/shared/api/http', () => ({
|
||||||
http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn() },
|
http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn() },
|
||||||
unwrapApiResponse: <T>(response: { data: T }) => response.data,
|
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' })
|
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 () => {
|
it('loads qualifications for the selected tenant', async () => {
|
||||||
const qualification = {
|
const qualification = {
|
||||||
id: 12,
|
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 {
|
export type { PlatformPermissionResource } from '@/shared/types/platformPermission'
|
||||||
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 interface EnterpriseReview {
|
export interface EnterpriseReview {
|
||||||
id: number
|
id: number
|
||||||
@@ -262,10 +250,6 @@ export function listPlatformPermissions() {
|
|||||||
return getData<PlatformPermissionResource[]>('/platform/v1/iam/permissions')
|
return getData<PlatformPermissionResource[]>('/platform/v1/iam/permissions')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listCurrentPlatformPermissionCodes() {
|
|
||||||
return getData<string[]>('/platform/v1/iam/authorization/permissions')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function listEnterpriseReviews() {
|
export function listEnterpriseReviews() {
|
||||||
return getData<EnterpriseReview[]>('/platform/v1/iam/reviews')
|
return getData<EnterpriseReview[]>('/platform/v1/iam/reviews')
|
||||||
}
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './api/platformIam'
|
||||||
+4
-4
@@ -6,12 +6,12 @@ import {
|
|||||||
getPlatformUserOptions,
|
getPlatformUserOptions,
|
||||||
listPlatformManagedUsers,
|
listPlatformManagedUsers,
|
||||||
type PlatformManagedUser,
|
type PlatformManagedUser,
|
||||||
} from '@/api/platformIam'
|
} from '@/modules/iam/api/platformIam'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/core/auth/auth'
|
||||||
import InternalUsersView from './InternalUsersView.vue'
|
import InternalUsersView from './InternalUsersView.vue'
|
||||||
|
|
||||||
vi.mock('@/api/platformIam', () => ({
|
vi.mock('@/modules/iam/api/platformIam', () => ({
|
||||||
listPlatformManagedUsers: vi.fn(),
|
listPlatformManagedUsers: vi.fn(),
|
||||||
getPlatformUserOptions: vi.fn(),
|
getPlatformUserOptions: vi.fn(),
|
||||||
createPlatformManagedUser: vi.fn(),
|
createPlatformManagedUser: vi.fn(),
|
||||||
@@ -11,14 +11,14 @@ import {
|
|||||||
updatePlatformManagedUser,
|
updatePlatformManagedUser,
|
||||||
type PlatformManagedUser,
|
type PlatformManagedUser,
|
||||||
type PlatformUserOptions,
|
type PlatformUserOptions,
|
||||||
} from '@/api/platformIam'
|
} from '@/modules/iam/api/platformIam'
|
||||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
|
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
||||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||||
import StatusBadge from '@/components/StatusBadge.vue'
|
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||||
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
|
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/core/auth/auth'
|
||||||
|
|
||||||
const access = useAccessPreviewStore()
|
const access = useAccessPreviewStore()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { createPinia, setActivePinia } from 'pinia'
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
import { flushPromises, shallowMount } from '@vue/test-utils'
|
import { flushPromises, shallowMount } from '@vue/test-utils'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { listPlatformPermissions, type PlatformPermissionResource } from '@/api/platformIam'
|
import { listPlatformPermissions, type PlatformPermissionResource } from '@/modules/iam/api/platformIam'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/core/auth/auth'
|
||||||
import PermissionsView from './PermissionsView.vue'
|
import PermissionsView from './PermissionsView.vue'
|
||||||
|
|
||||||
vi.mock('@/api/platformIam', () => ({
|
vi.mock('@/modules/iam/api/platformIam', () => ({
|
||||||
changePlatformPermissionStatus: vi.fn(),
|
changePlatformPermissionStatus: vi.fn(),
|
||||||
createPlatformPermission: vi.fn(),
|
createPlatformPermission: vi.fn(),
|
||||||
deletePlatformPermission: vi.fn(),
|
deletePlatformPermission: vi.fn(),
|
||||||
@@ -2,23 +2,23 @@
|
|||||||
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
||||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||||
import { Braces, FolderTree, Pencil, Plus, ShieldCheck, Trash2 } from '@lucide/vue'
|
import { Braces, FolderTree, Pencil, Plus, ShieldCheck, Trash2 } from '@lucide/vue'
|
||||||
import StatusBadge from '@/components/StatusBadge.vue'
|
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||||
import {
|
import {
|
||||||
changePlatformPermissionStatus,
|
changePlatformPermissionStatus,
|
||||||
createPlatformPermission,
|
createPlatformPermission,
|
||||||
deletePlatformPermission,
|
deletePlatformPermission,
|
||||||
listPlatformPermissions,
|
listPlatformPermissions,
|
||||||
updatePlatformPermission,
|
updatePlatformPermission,
|
||||||
} from '@/api/platformIam'
|
} from '@/modules/iam/api/platformIam'
|
||||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
|
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
||||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||||
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
|
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import type { Permission, PermissionType } from '@/types/admin'
|
import type { Permission, PermissionType } from '@/modules/iam/types'
|
||||||
import { buildPermissionTree, getDescendantIds, type PermissionTreeNode } from '@/utils/permissionTree'
|
import { buildPermissionTree, getDescendantIds, type PermissionTreeNode } from '@/modules/iam/utils/permissionTree'
|
||||||
import { mapPlatformPermission } from '@/utils/platformPermission'
|
import { mapPlatformPermission } from '@/modules/iam/utils/platformPermission'
|
||||||
|
|
||||||
const access = useAccessPreviewStore()
|
const access = useAccessPreviewStore()
|
||||||
const editorOpen = ref(false)
|
const editorOpen = ref(false)
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { createPinia, setActivePinia } from 'pinia'
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
import { flushPromises, shallowMount } from '@vue/test-utils'
|
import { flushPromises, shallowMount } from '@vue/test-utils'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { createPlatformRole, listPlatformInternalUsers, listPlatformPermissions, listPlatformRoles, type PlatformRoleResource } from '@/api/platformIam'
|
import { createPlatformRole, listPlatformInternalUsers, listPlatformPermissions, listPlatformRoles, type PlatformRoleResource } from '@/modules/iam/api/platformIam'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/core/auth/auth'
|
||||||
import RolesView from './RolesView.vue'
|
import RolesView from './RolesView.vue'
|
||||||
|
|
||||||
vi.mock('@/api/platformIam', () => ({
|
vi.mock('@/modules/iam/api/platformIam', () => ({
|
||||||
createPlatformRole: vi.fn(),
|
createPlatformRole: vi.fn(),
|
||||||
listPlatformInternalUsers: vi.fn(),
|
listPlatformInternalUsers: vi.fn(),
|
||||||
listPlatformPermissions: vi.fn(),
|
listPlatformPermissions: vi.fn(),
|
||||||
@@ -91,7 +91,7 @@ describe('RolesView', () => {
|
|||||||
|
|
||||||
expect(listPlatformRoles).toHaveBeenCalledTimes(1)
|
expect(listPlatformRoles).toHaveBeenCalledTimes(1)
|
||||||
expect(listPlatformInternalUsers).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)
|
expect(wrapper.find('[data-testid="permission-tree"]').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -12,17 +12,17 @@ import {
|
|||||||
replacePlatformRolePermissions,
|
replacePlatformRolePermissions,
|
||||||
updatePlatformRole,
|
updatePlatformRole,
|
||||||
type PlatformRoleResource,
|
type PlatformRoleResource,
|
||||||
} from '@/api/platformIam'
|
} from '@/modules/iam/api/platformIam'
|
||||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
|
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
||||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||||
import StatusBadge from '@/components/StatusBadge.vue'
|
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||||
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
|
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import type { Permission, Role } from '@/types/admin'
|
import type { Permission, Role } from '@/modules/iam/types'
|
||||||
import { buildPermissionTree, summarizePermissionSelection } from '@/utils/permissionTree'
|
import { buildPermissionTree, summarizePermissionSelection } from '@/modules/iam/utils/permissionTree'
|
||||||
import { mapPlatformPermission } from '@/utils/platformPermission'
|
import { mapPlatformPermission } from '@/modules/iam/utils/platformPermission'
|
||||||
|
|
||||||
interface TreeController {
|
interface TreeController {
|
||||||
getCheckedKeys: (leafOnly?: boolean) => Array<string | number>
|
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 { describe, expect, it } from 'vitest'
|
||||||
import type { Permission } from '@/types/admin'
|
import type { Permission } from '@/modules/iam/types'
|
||||||
import { buildPermissionTree, getDescendantIds, summarizePermissionSelection } from '@/utils/permissionTree'
|
import { buildPermissionTree, getDescendantIds, summarizePermissionSelection } from '@/modules/iam/utils/permissionTree'
|
||||||
|
|
||||||
const permissions: Permission[] = [
|
const permissions: Permission[] = [
|
||||||
{ id: 'action-b', parentId: 'menu', name: '操作 B', code: 'demo:b', type: '按钮', path: '', method: '-', status: '启用', sort: 20 },
|
{ 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 PermissionTreeNode = Permission & { children?: PermissionTreeNode[] }
|
||||||
export type PermissionMatcher = (permission: Permission) => boolean
|
export type PermissionMatcher = (permission: Permission) => boolean
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { mapPlatformPermission } from '@/utils/platformPermission'
|
import { mapPlatformPermission } from '@/modules/iam/utils/platformPermission'
|
||||||
|
|
||||||
describe('mapPlatformPermission', () => {
|
describe('mapPlatformPermission', () => {
|
||||||
it('treats the string root parent as a root node and preserves long ids', () => {
|
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 { PlatformPermissionResource } from '@/modules/iam/api/platformIam'
|
||||||
import type { Permission } from '@/types/admin'
|
import type { Permission } from '@/modules/iam/types'
|
||||||
|
|
||||||
export function mapPlatformPermission(item: PlatformPermissionResource): Permission {
|
export function mapPlatformPermission(item: PlatformPermissionResource): Permission {
|
||||||
const parentId = String(item.parentId)
|
const parentId = String(item.parentId)
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { http } from '@/api/http'
|
import { http } from '@/shared/api/http'
|
||||||
import {
|
import {
|
||||||
changePlatformPartnerStatus,
|
changePlatformPartnerStatus,
|
||||||
createPlatformPartner,
|
createPlatformPartner,
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
type SavePlatformPartnerRequest,
|
type SavePlatformPartnerRequest,
|
||||||
} from './platformPartners'
|
} from './platformPartners'
|
||||||
|
|
||||||
vi.mock('@/api/http', () => ({
|
vi.mock('@/shared/api/http', () => ({
|
||||||
http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn() },
|
http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn() },
|
||||||
unwrapApiResponse: <T>(response: { data: T }) => response.data,
|
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 PlatformPartnerType = 'CUSTOMER' | 'SUPPLIER' | 'CARRIER' | 'CUSTOMS_BROKER' | 'SHIPPER' | 'CONSIGNEE'
|
||||||
export type PlatformPartnerLegacyType = PlatformPartnerType | 'BOTH'
|
export type PlatformPartnerLegacyType = PlatformPartnerType | 'BOTH'
|
||||||
+4
-4
@@ -5,12 +5,12 @@ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vites
|
|||||||
import {
|
import {
|
||||||
listPlatformPartners,
|
listPlatformPartners,
|
||||||
type PlatformPartnerResource,
|
type PlatformPartnerResource,
|
||||||
} from '@/api/platformPartners'
|
} from '@/modules/master-data/counterparties/api/platformPartners'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/core/auth/auth'
|
||||||
import PartnersView from './PartnersView.vue'
|
import PartnersView from './PartnersView.vue'
|
||||||
|
|
||||||
vi.mock('@/api/platformPartners', () => ({
|
vi.mock('@/modules/master-data/counterparties/api/platformPartners', () => ({
|
||||||
listPlatformPartners: vi.fn(),
|
listPlatformPartners: vi.fn(),
|
||||||
createPlatformPartner: vi.fn(),
|
createPlatformPartner: vi.fn(),
|
||||||
getPlatformPartner: vi.fn(),
|
getPlatformPartner: vi.fn(),
|
||||||
+7
-7
@@ -2,11 +2,11 @@
|
|||||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules, type UploadFile } from 'element-plus'
|
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 { Building2, Download, Eye, MapPin, Pencil, Plus, Power, Upload } from '@lucide/vue'
|
||||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||||
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
|
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/api/http'
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||||
import {
|
import {
|
||||||
changePlatformPartnerStatus,
|
changePlatformPartnerStatus,
|
||||||
createPlatformPartner,
|
createPlatformPartner,
|
||||||
@@ -21,8 +21,8 @@ import {
|
|||||||
type PlatformPartnerLegacyType,
|
type PlatformPartnerLegacyType,
|
||||||
type PlatformPartnerType,
|
type PlatformPartnerType,
|
||||||
type SavePlatformPartnerRequest,
|
type SavePlatformPartnerRequest,
|
||||||
} from '@/api/platformPartners'
|
} from '@/modules/master-data/counterparties/api/platformPartners'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
|
|
||||||
const fixedTenantId = 10001
|
const fixedTenantId = 10001
|
||||||
const pageSize = ref(20)
|
const pageSize = ref(20)
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { http } from '@/api/http'
|
import { http } from '@/shared/api/http'
|
||||||
import {
|
import {
|
||||||
changePlatformExpressCompanyStatus,
|
changePlatformExpressCompanyStatus,
|
||||||
createPlatformExpressCompany,
|
createPlatformExpressCompany,
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
type SavePlatformExpressCompanyRequest,
|
type SavePlatformExpressCompanyRequest,
|
||||||
} from './platformExpressCompanies'
|
} from './platformExpressCompanies'
|
||||||
|
|
||||||
vi.mock('@/api/http', () => ({
|
vi.mock('@/shared/api/http', () => ({
|
||||||
http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn() },
|
http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn() },
|
||||||
unwrapApiResponse: <T>(response: { data: T }) => response.data,
|
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'
|
export type PlatformExpressCompanyStatus = 'ACTIVE' | 'INACTIVE'
|
||||||
|
|
||||||
+4
-4
@@ -2,12 +2,12 @@ import ElementPlus from 'element-plus'
|
|||||||
import { createPinia, setActivePinia } from 'pinia'
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { listPlatformExpressCompanies, type PlatformExpressCompanyResource } from '@/api/platformExpressCompanies'
|
import { listPlatformExpressCompanies, type PlatformExpressCompanyResource } from '@/modules/master-data/express-companies/api/platformExpressCompanies'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/core/auth/auth'
|
||||||
import ExpressCompaniesView from './ExpressCompaniesView.vue'
|
import ExpressCompaniesView from './ExpressCompaniesView.vue'
|
||||||
|
|
||||||
vi.mock('@/api/platformExpressCompanies', () => ({
|
vi.mock('@/modules/master-data/express-companies/api/platformExpressCompanies', () => ({
|
||||||
listPlatformExpressCompanies: vi.fn(),
|
listPlatformExpressCompanies: vi.fn(),
|
||||||
createPlatformExpressCompany: vi.fn(),
|
createPlatformExpressCompany: vi.fn(),
|
||||||
updatePlatformExpressCompany: vi.fn(),
|
updatePlatformExpressCompany: vi.fn(),
|
||||||
+7
-7
@@ -2,11 +2,11 @@
|
|||||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||||
import { Download, ExternalLink, FileSpreadsheet, Pencil, Plus, Power, Truck, Upload } from '@lucide/vue'
|
import { Download, ExternalLink, FileSpreadsheet, Pencil, Plus, Power, Truck, Upload } from '@lucide/vue'
|
||||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||||
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
|
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/api/http'
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||||
import {
|
import {
|
||||||
changePlatformExpressCompanyStatus,
|
changePlatformExpressCompanyStatus,
|
||||||
createPlatformExpressCompany,
|
createPlatformExpressCompany,
|
||||||
@@ -19,8 +19,8 @@ import {
|
|||||||
type PlatformExpressCompanyResource,
|
type PlatformExpressCompanyResource,
|
||||||
type PlatformExpressCompanyStatus,
|
type PlatformExpressCompanyStatus,
|
||||||
type SavePlatformExpressCompanyRequest,
|
type SavePlatformExpressCompanyRequest,
|
||||||
} from '@/api/platformExpressCompanies'
|
} from '@/modules/master-data/express-companies/api/platformExpressCompanies'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
|
|
||||||
const fixedTenantId = 10001
|
const fixedTenantId = 10001
|
||||||
const access = useAccessPreviewStore()
|
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 { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { http } from '@/api/http'
|
import { http } from '@/shared/api/http'
|
||||||
import {
|
import {
|
||||||
changePlatformWarehouseStatus,
|
changePlatformWarehouseStatus,
|
||||||
createPlatformWarehouse,
|
createPlatformWarehouse,
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
type SavePlatformWarehouseRequest,
|
type SavePlatformWarehouseRequest,
|
||||||
} from './platformWarehouses'
|
} from './platformWarehouses'
|
||||||
|
|
||||||
vi.mock('@/api/http', () => ({
|
vi.mock('@/shared/api/http', () => ({
|
||||||
http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn() },
|
http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn() },
|
||||||
unwrapApiResponse: <T>(response: { data: T }) => response.data,
|
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'
|
export type PlatformWarehouseStatus = 'ACTIVE' | 'INACTIVE'
|
||||||
|
|
||||||
+4
-4
@@ -9,12 +9,12 @@ import {
|
|||||||
updatePlatformWarehouse,
|
updatePlatformWarehouse,
|
||||||
type PlatformWarehouseDetailResource,
|
type PlatformWarehouseDetailResource,
|
||||||
type PlatformWarehouseResource,
|
type PlatformWarehouseResource,
|
||||||
} from '@/api/platformWarehouses'
|
} from '@/modules/warehouses/api/platformWarehouses'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/core/auth/auth'
|
||||||
import WarehousesView from './WarehousesView.vue'
|
import WarehousesView from './WarehousesView.vue'
|
||||||
|
|
||||||
vi.mock('@/api/platformWarehouses', () => ({
|
vi.mock('@/modules/warehouses/api/platformWarehouses', () => ({
|
||||||
listPlatformWarehouses: vi.fn(),
|
listPlatformWarehouses: vi.fn(),
|
||||||
getPlatformWarehouse: vi.fn(),
|
getPlatformWarehouse: vi.fn(),
|
||||||
createPlatformWarehouse: vi.fn(),
|
createPlatformWarehouse: vi.fn(),
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { MapPin, Pencil, Plus, Power, Warehouse as WarehouseIcon } from '@lucide/vue'
|
import { MapPin, Pencil, Plus, Power, Warehouse as WarehouseIcon } from '@lucide/vue'
|
||||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||||
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
|
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/api/http'
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||||
import {
|
import {
|
||||||
changePlatformWarehouseStatus,
|
changePlatformWarehouseStatus,
|
||||||
createPlatformWarehouse,
|
createPlatformWarehouse,
|
||||||
@@ -15,9 +15,9 @@ import {
|
|||||||
type PlatformWarehouseDetailResource,
|
type PlatformWarehouseDetailResource,
|
||||||
type PlatformWarehouseResource,
|
type PlatformWarehouseResource,
|
||||||
type SavePlatformWarehouseRequest,
|
type SavePlatformWarehouseRequest,
|
||||||
} from '@/api/platformWarehouses'
|
} from '@/modules/warehouses/api/platformWarehouses'
|
||||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||||
import type { Warehouse } from '@/types/admin'
|
import type { Warehouse } from '@/modules/warehouses/types'
|
||||||
|
|
||||||
const fixedTenantId = 10001
|
const fixedTenantId = 10001
|
||||||
const access = useAccessPreviewStore()
|
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">
|
<script setup lang="ts">
|
||||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||||
defineProps<{ title: string; description: string }>()
|
defineProps<{ title: string; description: string }>()
|
||||||
</script>
|
</script>
|
||||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||||
defineProps<{ title: string; description: string }>()
|
defineProps<{ title: string; description: string }>()
|
||||||
</script>
|
</script>
|
||||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||||
defineProps<{ title: string; description: string }>()
|
defineProps<{ title: string; description: string }>()
|
||||||
</script>
|
</script>
|
||||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
<template><PrototypeModuleView v-bind="$props" /></template>
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
|
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
|
||||||
defineProps<{ title: string; description: string }>()
|
defineProps<{ title: string; description: string }>()
|
||||||
</script>
|
</script>
|
||||||
<template><PrototypeModuleView v-bind="$props" /></template>
|
<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>
|
type ModuleView = () => Promise<Component>
|
||||||
|
|
||||||
const moduleViews = {
|
const moduleViews = {
|
||||||
dashboard: () => import('@/views/prototype/dashboard/ModuleView.vue'),
|
dashboard: () => import('@/prototype/modules/dashboard/ModuleView.vue'),
|
||||||
customerManagement: () => import('@/views/prototype/customer-management/ModuleView.vue'),
|
customerManagement: () => import('@/prototype/modules/customer-management/ModuleView.vue'),
|
||||||
domesticWarehouse: () => import('@/views/prototype/domestic-warehouse/ModuleView.vue'),
|
domesticWarehouse: () => import('@/prototype/modules/domestic-warehouse/ModuleView.vue'),
|
||||||
overseasDelivery: () => import('@/views/prototype/overseas-delivery/ModuleView.vue'),
|
overseasDelivery: () => import('@/prototype/modules/overseas-delivery/ModuleView.vue'),
|
||||||
overseasInventory: () => import('@/views/prototype/overseas-inventory/ModuleView.vue'),
|
overseasInventory: () => import('@/prototype/modules/overseas-inventory/ModuleView.vue'),
|
||||||
serviceCenter: () => import('@/views/prototype/service-center/ModuleView.vue'),
|
serviceCenter: () => import('@/prototype/modules/service-center/ModuleView.vue'),
|
||||||
supplierCenter: () => import('@/views/prototype/supplier-center/ModuleView.vue'),
|
supplierCenter: () => import('@/prototype/modules/supplier-center/ModuleView.vue'),
|
||||||
containerManagement: () => import('@/views/prototype/container-management/ModuleView.vue'),
|
containerManagement: () => import('@/prototype/modules/container-management/ModuleView.vue'),
|
||||||
overseasBusiness: () => import('@/views/prototype/overseas-business/ModuleView.vue'),
|
overseasBusiness: () => import('@/prototype/modules/overseas-business/ModuleView.vue'),
|
||||||
overseasWarehouseOrders: () => import('@/views/prototype/overseas-warehouse-orders/ModuleView.vue'),
|
overseasWarehouseOrders: () => import('@/prototype/modules/overseas-warehouse-orders/ModuleView.vue'),
|
||||||
fbaReturns: () => import('@/views/prototype/fba-returns/ModuleView.vue'),
|
fbaReturns: () => import('@/prototype/modules/fba-returns/ModuleView.vue'),
|
||||||
finance: () => import('@/views/prototype/finance/ModuleView.vue'),
|
finance: () => import('@/prototype/modules/finance/ModuleView.vue'),
|
||||||
vehicleManagement: () => import('@/views/prototype/vehicle-management/ModuleView.vue'),
|
vehicleManagement: () => import('@/prototype/modules/vehicle-management/ModuleView.vue'),
|
||||||
reports: () => import('@/views/prototype/reports/ModuleView.vue'),
|
reports: () => import('@/prototype/modules/reports/ModuleView.vue'),
|
||||||
general: () => import('@/views/prototype/general/ModuleView.vue'),
|
general: () => import('@/prototype/modules/general/ModuleView.vue'),
|
||||||
sales: () => import('@/views/prototype/sales/ModuleView.vue'),
|
sales: () => import('@/prototype/modules/sales/ModuleView.vue'),
|
||||||
messages: () => import('@/views/prototype/messages/ModuleView.vue'),
|
messages: () => import('@/prototype/modules/messages/ModuleView.vue'),
|
||||||
systemManagement: () => import('@/views/prototype/system-management/ModuleView.vue'),
|
systemManagement: () => import('@/prototype/modules/system-management/ModuleView.vue'),
|
||||||
} satisfies Record<string, ModuleView>
|
} satisfies Record<string, ModuleView>
|
||||||
|
|
||||||
function createRoutes(view: ModuleView, routes: PrototypeRoute[]): RouteRecordRaw[] {
|
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