优化构架

This commit is contained in:
2026-08-03 18:49:18 +08:00
parent 1241d11c01
commit 44c33a6280
133 changed files with 651 additions and 603 deletions
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import { adminNavigation, filterAdminNavigation, type AdminNavigationNode } from '@/navigation/adminNavigation'
import { adminNavigation, filterAdminNavigation, type AdminNavigationNode } from '@/core/navigation/adminNavigation'
import AdminSidebarMenu from './AdminSidebarMenu.vue'
const stubs = {
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { AdminNavigationNode } from '@/navigation/adminNavigation'
import type { AdminNavigationNode } from '@/core/navigation/adminNavigation'
import AdminSidebarMenuNode from './AdminSidebarMenuNode.vue'
const props = defineProps<{
@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { AdminNavigationNode } from '@/navigation/adminNavigation'
import type { AdminNavigationNode } from '@/core/navigation/adminNavigation'
defineOptions({ name: 'AdminSidebarMenuNode' })
@@ -3,7 +3,7 @@ import { flushPromises, mount } from '@vue/test-utils'
import { createMemoryHistory, createRouter } from 'vue-router'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ElMessageBox } from 'element-plus'
import { useAuthStore } from '@/stores/auth'
import { useAuthStore } from '@/core/auth/auth'
import AppLayout from './AppLayout.vue'
vi.mock('element-plus', async (importOriginal) => {
@@ -3,13 +3,13 @@ import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { LogOut, Menu } from '@lucide/vue'
import { getApiErrorDetails } from '@/api/http'
import { getApiErrorDetails } from '@/shared/api/http'
import baiyueLogoWhite from '@/assets/brand/baiyue-logo-white.png'
import baiyueMarkWhite from '@/assets/brand/baiyue-mark-white.png'
import AdminSidebarMenu from '@/components/AdminSidebarMenu.vue'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import { useAuthStore } from '@/stores/auth'
import { useUnsavedWorkStore } from '@/stores/unsavedWork'
import AdminSidebarMenu from '@/app/components/AdminSidebarMenu.vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
const route = useRoute()
const router = useRouter()
@@ -2,8 +2,8 @@
import { computed } from 'vue'
import { ShieldX } from '@lucide/vue'
import { useRoute, useRouter } from 'vue-router'
import { sanitizeInternalPath } from '@/router/accessDecision'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import { sanitizeInternalPath } from '@/core/routing/accessDecision'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
const route = useRoute()
const router = useRouter()
@@ -1,13 +1,14 @@
import { describe, expect, it } from 'vitest'
import { adminNavigation, getNavigationLeaves } from '@/core/navigation/adminNavigation'
import router from './index'
describe('admin router access metadata', () => {
it('uses the root route only as a permission-aware entry point', () => {
it('uses the root route only as a permission-aware entry point while retaining the dashboard prototype', () => {
const home = router.getRoutes().find((route) => route.name === 'home')
expect(home?.path).toBe('/')
expect(home?.meta.permission).toBeUndefined()
expect(router.getRoutes().some((route) => route.name === 'dashboard')).toBe(false)
expect(router.getRoutes().some((route) => route.name === 'dashboard')).toBe(true)
})
it('protects warehouse management with its stable list permission', () => {
@@ -53,4 +54,14 @@ describe('admin router access metadata', () => {
expect(forbidden?.meta?.permission).toBeUndefined()
expect(resolved.matched.some((record) => record.meta.requiresAuth)).toBe(true)
})
it('keeps visible navigation paths and permissions aligned with routes', () => {
for (const item of getNavigationLeaves(adminNavigation)) {
const resolved = router.resolve(item.path)
const pageRoute = resolved.matched.at(-1)
expect(pageRoute, `missing route for ${item.path}`).toBeDefined()
expect(pageRoute?.meta.permission, `permission mismatch for ${item.path}`).toBe(item.permission)
}
})
})
+85
View File
@@ -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
View File
@@ -1,7 +1,7 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import { useAuthStore } from '@/stores/auth'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
describe('server access store', () => {
beforeEach(() => setActivePinia(createPinia()))
@@ -4,8 +4,8 @@ import {
adminNavigation,
filterAdminNavigation,
getFirstNavigationLeaf,
} from '@/navigation/adminNavigation'
import { useAuthStore } from '@/stores/auth'
} from '@/core/navigation/adminNavigation'
import { useAuthStore } from '@/core/auth/auth'
export const useAccessPreviewStore = defineStore('access-preview', () => {
const auth = useAuthStore()
@@ -7,7 +7,7 @@ const mocks = vi.hoisted(() => ({
refreshHandler: null as null | (() => Promise<string>),
}))
vi.mock('@/api/http', () => ({
vi.mock('@/shared/api/http', () => ({
http: { post: mocks.post },
registerAuthRefreshHandler: (handler: () => Promise<string>) => { mocks.refreshHandler = handler },
unwrapApiResponse: <T>(response: { code: string; message: string; data: T }) => {
@@ -20,7 +20,7 @@ vi.mock('@/api/http', () => ({
}),
}))
vi.mock('@/api/platformIam', () => ({
vi.mock('./platformAuthorization', () => ({
listCurrentPlatformPermissionCodes: mocks.listCurrentPlatformPermissionCodes,
}))
+4 -3
View File
@@ -6,9 +6,10 @@ import {
registerAuthRefreshHandler,
unwrapApiResponse,
type ApiEnvelope,
} from '@/api/http'
import { listCurrentPlatformPermissionCodes, type PlatformPermissionResource } from '@/api/platformIam'
import { decodeJwtClaims } from '@/utils/jwt'
} from '@/shared/api/http'
import type { PlatformPermissionResource } from '@/shared/types/platformPermission'
import { decodeJwtClaims } from './jwt'
import { listCurrentPlatformPermissionCodes } from './platformAuthorization'
const TOKEN_KEY = 'cc-admin-token'
const REFRESH_TOKEN_KEY = 'cc-admin-refresh-token'
@@ -0,0 +1,19 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/shared/api/http'
import { listCurrentPlatformPermissionCodes } from './platformAuthorization'
vi.mock('@/shared/api/http', () => ({
http: { get: vi.fn() },
unwrapApiResponse: <T>(response: { data: T }) => response.data,
}))
describe('platformAuthorization', () => {
beforeEach(() => vi.clearAllMocks())
it('loads the current user permission snapshot', async () => {
vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: ['platform:warehouse:list'] })
await expect(listCurrentPlatformPermissionCodes()).resolves.toEqual(['platform:warehouse:list'])
expect(http.get).toHaveBeenCalledWith('/platform/v1/iam/authorization/permissions')
})
})
+8
View File
@@ -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)
}
+48
View File
@@ -0,0 +1,48 @@
import { readdirSync, readFileSync } from 'node:fs'
import { relative, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
const sourceRoot = resolve(process.cwd(), 'src')
function sourceFiles(directory: string): string[] {
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const path = resolve(directory, entry.name)
if (entry.isDirectory()) return sourceFiles(path)
return /\.(ts|vue)$/.test(entry.name) ? [path] : []
})
}
function aliasImports(path: string): string[] {
const source = readFileSync(path, 'utf8')
return [...source.matchAll(/['"](@\/[^'"]+)['"]/g)].map((match) => match[1])
}
describe('source dependency boundaries', () => {
it('keeps app, core, modules and shared dependencies pointing inward', () => {
const violations: string[] = []
for (const path of sourceFiles(sourceRoot)) {
const sourcePath = relative(sourceRoot, path).replaceAll('\\', '/')
const sourceModule = sourcePath.match(/^modules\/([^/]+)\//)?.[1]
for (const dependency of aliasImports(path)) {
if (sourcePath.startsWith('shared/') && /^@\/(app|core|modules|prototype)\//.test(dependency)) {
violations.push(`${sourcePath} -> ${dependency}`)
}
if (sourcePath.startsWith('core/') && /^@\/(app|modules|prototype)\//.test(dependency)) {
violations.push(`${sourcePath} -> ${dependency}`)
}
if (sourceModule && /^@\/(app|prototype)\//.test(dependency)) {
violations.push(`${sourcePath} -> ${dependency}`)
}
const targetModule = dependency.match(/^@\/modules\/([^/]+)(\/.*)?$/)
if (sourceModule && targetModule && targetModule[1] !== sourceModule && targetModule[2]) {
violations.push(`${sourcePath} -> ${dependency}`)
}
}
}
expect(violations).toEqual([])
})
})
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { useUnsavedWorkStore } from '@/stores/unsavedWork'
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
describe('unsaved work store', () => {
beforeEach(() => {
@@ -2,8 +2,8 @@ import { computed, defineComponent, nextTick, reactive, ref } from 'vue'
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
import { useUnsavedWorkStore } from '@/stores/unsavedWork'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
interface HarnessModel {
name: string
@@ -1,5 +1,5 @@
import { computed, onScopeDispose, ref, watch, type ComputedRef, type Ref } from 'vue'
import { useUnsavedWorkStore } from '@/stores/unsavedWork'
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
interface UnsavedWorkRegistrationOptions<T extends object> {
key: string
+6 -7
View File
@@ -4,12 +4,12 @@ import { ElMessage } from 'element-plus'
import 'element-plus/es/components/message/style/css'
import 'element-plus/es/components/message-box/style/css'
import App from './App.vue'
import { registerAuthSessionExpiredHandler } from '@/api/http'
import router from './router'
import { sanitizeInternalPath } from '@/router/accessDecision'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import { useAuthStore } from '@/stores/auth'
import { useUnsavedWorkStore } from '@/stores/unsavedWork'
import router from '@/app/router'
import { sanitizeInternalPath } from '@/core/routing/accessDecision'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
import { useAuthStore } from '@/core/auth/auth'
import { registerAuthSessionExpiredHandler } from '@/shared/api/http'
import './styles/index.css'
const pinia = createPinia()
@@ -32,4 +32,3 @@ registerAuthSessionExpiredHandler(async () => {
})
createApp(App).use(pinia).use(router).mount('#app')
@@ -4,11 +4,11 @@ import { resolve } from 'node:path'
import { flushPromises, mount } from '@vue/test-utils'
import { createMemoryHistory, createRouter } from 'vue-router'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import { useAuthStore } from '@/stores/auth'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
import LoginView from './LoginView.vue'
const loginSource = readFileSync(resolve(process.cwd(), 'src/views/LoginView.vue'), 'utf8')
const loginSource = readFileSync(resolve(process.cwd(), 'src/modules/auth/pages/LoginView.vue'), 'utf8')
const formStub = {
template: '<form><slot /></form>',
setup(_: unknown, { expose }: { expose: (exposed: { validate: () => Promise<boolean> }) => void }) {
@@ -3,12 +3,12 @@ import { reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { ArrowRight, LockKeyhole, ShieldCheck, UserRound } from '@lucide/vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/api/http'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import baiyueLogoWhite from '@/assets/brand/baiyue-logo-white.png'
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
import { sanitizeInternalPath } from '@/router/accessDecision'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import { useAuthStore } from '@/stores/auth'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import { sanitizeInternalPath } from '@/core/routing/accessDecision'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
const route = useRoute()
const router = useRouter()
@@ -10,13 +10,13 @@ import {
updatePlatformTenant,
uploadPlatformTenantQualification,
type PlatformTenantResource,
} from '@/api/platformIam'
import { createPlatformExportTask } from '@/api/platformExportTasks'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import { useAuthStore } from '@/stores/auth'
} from '@/modules/iam'
import { createPlatformExportTask } from '@/modules/downloads'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
import CustomersView from './CustomersView.vue'
vi.mock('@/api/platformIam', () => ({
vi.mock('@/modules/iam', () => ({
changePlatformTenantStatus: vi.fn(),
createPlatformTenant: vi.fn(),
downloadTenantQualification: vi.fn(),
@@ -30,7 +30,7 @@ vi.mock('@/api/platformIam', () => ({
uploadPlatformTenantQualification: vi.fn(),
}))
vi.mock('@/api/platformExportTasks', () => ({ createPlatformExportTask: vi.fn() }))
vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
class ResizeObserverStub implements ResizeObserver {
observe() {}
@@ -2,8 +2,8 @@
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { Building2, CheckCircle2, Download, Eye, FileCheck2, FileText, FileUp, Pencil, Plus, Power } from '@lucide/vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
import { createPlatformExportTask } from '@/api/platformExportTasks'
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
import { createPlatformExportTask } from '@/modules/downloads'
import {
changePlatformTenantStatus,
createPlatformTenant,
@@ -21,11 +21,11 @@ import {
type PlatformTenantQualification,
type PlatformTenantResource,
type PlatformTenantStatus,
} from '@/api/platformIam'
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
import { useAccessPreviewStore } from '@/stores/accessPreview'
} from '@/modules/iam'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
type CustomerFilters = QueryModel & { keyword: string; status: PlatformTenantStatus | '' }
type QualificationDraft = {
@@ -11,12 +11,12 @@ import {
rejectEnterpriseReview,
type EnterpriseReview,
type PlatformTenantQualification,
} from '@/api/platformIam'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import { useAuthStore } from '@/stores/auth'
} from '@/modules/iam'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
import EnterpriseReviewsView from './EnterpriseReviewsView.vue'
vi.mock('@/api/platformIam', () => ({
vi.mock('@/modules/iam', () => ({
listEnterpriseReviews: vi.fn(),
listTenantQualifications: vi.fn(),
downloadTenantQualification: vi.fn(),
@@ -11,12 +11,12 @@ import {
rejectEnterpriseReview,
type EnterpriseReview,
type PlatformTenantQualification,
} from '@/api/platformIam'
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
import { useAccessPreviewStore } from '@/stores/accessPreview'
} from '@/modules/iam'
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
const access = useAccessPreviewStore()
const loading = ref(false)
+16
View File
@@ -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[]
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/api/http'
import { http } from '@/shared/api/http'
import {
createPlatformExportTask,
downloadPlatformExportTask,
@@ -7,8 +7,8 @@ import {
retryPlatformExportTask,
} from './platformExportTasks'
vi.mock('@/api/http', async () => {
const actual = await vi.importActual<typeof import('@/api/http')>('@/api/http')
vi.mock('@/shared/api/http', async () => {
const actual = await vi.importActual<typeof import('@/shared/api/http')>('@/shared/api/http')
return { ...actual, http: { get: vi.fn(), post: vi.fn() } }
})
@@ -1,4 +1,4 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/api/http'
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
export type ExportTaskStatus = 'PENDING' | 'RUNNING' | 'SUCCEEDED' | 'FAILED' | 'EXPIRED'
export type ExportTaskType = 'CUSTOMER_LIST' | string
+1
View File
@@ -0,0 +1 @@
export * from './api/platformExportTasks'
@@ -7,12 +7,12 @@ import {
listPlatformExportTasks,
retryPlatformExportTask,
type PlatformExportTask,
} from '@/api/platformExportTasks'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import { useAuthStore } from '@/stores/auth'
} from '@/modules/downloads/api/platformExportTasks'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
import DownloadCenterView from './DownloadCenterView.vue'
vi.mock('@/api/platformExportTasks', () => ({
vi.mock('@/modules/downloads/api/platformExportTasks', () => ({
downloadPlatformExportTask: vi.fn(),
listPlatformExportTasks: vi.fn(),
retryPlatformExportTask: vi.fn(),
@@ -33,6 +33,7 @@ const task: PlatformExportTask = {
}
enableAutoUnmount(afterEach)
afterEach(() => vi.useRealTimers())
describe('DownloadCenterView', () => {
beforeEach(() => {
@@ -62,6 +63,7 @@ describe('DownloadCenterView', () => {
await flushPromises()
expect(document.body.textContent).toContain('客户列表')
expect(document.querySelector('.failure-cell')?.classList.contains('failure-message')).toBe(false)
const button = [...document.querySelectorAll<HTMLButtonElement>('button')].find((item) => item.textContent?.trim() === '下载')
button?.click()
await flushPromises()
@@ -80,9 +82,24 @@ describe('DownloadCenterView', () => {
mount(DownloadCenterView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
expect(document.querySelector('.failure-cell')?.classList.contains('failure-message')).toBe(true)
const button = [...document.querySelectorAll<HTMLButtonElement>('button')].find((item) => item.textContent?.trim() === '重试')
button?.click()
await flushPromises()
expect(retryPlatformExportTask).toHaveBeenCalledWith('9')
})
it('does not schedule another refresh after an in-flight load finishes on an unmounted page', async () => {
vi.useFakeTimers()
let resolveRequest!: (value: { records: PlatformExportTask[]; page: number; size: number; total: number; pages: number }) => void
vi.mocked(listPlatformExportTasks).mockReturnValue(new Promise((resolve) => { resolveRequest = resolve }))
const wrapper = mount(DownloadCenterView, { global: { plugins: [createPinia(), ElementPlus] } })
wrapper.unmount()
resolveRequest({ records: [{ ...task, status: 'RUNNING' }], page: 1, size: 20, total: 1, pages: 1 })
await flushPromises()
await vi.advanceTimersByTimeAsync(5000)
expect(listPlatformExportTasks).toHaveBeenCalledTimes(1)
})
})
@@ -2,18 +2,18 @@
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { Download, RefreshCw, RotateCcw } from '@lucide/vue'
import { ElMessage } from 'element-plus'
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
import {
downloadPlatformExportTask,
listPlatformExportTasks,
retryPlatformExportTask,
type ExportTaskStatus,
type PlatformExportTask,
} from '@/api/platformExportTasks'
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
import { useAccessPreviewStore } from '@/stores/accessPreview'
} from '@/modules/downloads/api/platformExportTasks'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
type DownloadFilters = QueryModel & {
exportType: string
@@ -31,6 +31,7 @@ const loadError = ref<ApiErrorDetails | null>(null)
const result = ref({ records: [] as PlatformExportTask[], total: 0, pages: 0 })
const filters = ref<DownloadFilters>({ exportType: '', status: '', requestedRange: [] })
let refreshTimer: number | undefined
let disposed = false
const queryFields: QueryField[] = [
{ key: 'exportType', type: 'select', placeholder: '全部任务类型', options: [
@@ -67,7 +68,7 @@ function clearRefreshTimer() {
function scheduleRefresh() {
clearRefreshTimer()
if (hasActiveTasks.value && !loadError.value) refreshTimer = window.setTimeout(() => { void loadTasks(true) }, 5000)
if (!disposed && hasActiveTasks.value && !loadError.value) refreshTimer = window.setTimeout(() => { void loadTasks(true) }, 5000)
}
async function loadTasks(background = false) {
@@ -167,7 +168,10 @@ function canRetry(task: PlatformExportTask) {
watch([page, pageSize], () => { void loadTasks() })
onMounted(loadTasks)
onBeforeUnmount(clearRefreshTimer)
onBeforeUnmount(() => {
disposed = true
clearRefreshTimer()
})
</script>
<template>
@@ -188,7 +192,7 @@ onBeforeUnmount(clearRefreshTimer)
<template #actions="{ row }"><div class="table-actions">
<el-button v-if="(row as PlatformExportTask).status === 'SUCCEEDED'" link type="primary" :loading="downloadingId === (row as PlatformExportTask).id" @click="downloadTask(row as PlatformExportTask)"><Download :size="15" />下载</el-button>
<el-button v-if="canRetry(row as PlatformExportTask)" link type="primary" :loading="retryingId === (row as PlatformExportTask).id" @click="retryTask(row as PlatformExportTask)"><RotateCcw :size="15" />重试</el-button>
<span v-if="!['SUCCEEDED', 'FAILED'].includes((row as PlatformExportTask).status)" class="action-placeholder">—</span>
<span v-if="(row as PlatformExportTask).status !== 'SUCCEEDED' && !canRetry(row as PlatformExportTask)" class="action-placeholder">—</span>
</div></template>
<template #type="{ row }">{{ typeLabel((row as PlatformExportTask).exportType) }}</template>
<template #status="{ row }"><el-tag :type="statusMeta((row as PlatformExportTask).status).type" effect="light">{{ statusMeta((row as PlatformExportTask).status).label }}</el-tag></template>
@@ -197,7 +201,7 @@ onBeforeUnmount(clearRefreshTimer)
<template #requestedAt="{ row }">{{ formatDate((row as PlatformExportTask).requestedAt) }}</template>
<template #completedAt="{ row }">{{ formatDate((row as PlatformExportTask).completedAt) }}</template>
<template #expiresAt="{ row }">{{ formatDate((row as PlatformExportTask).expiresAt) }}</template>
<template #failure="{ row }"><span class="failure-message">{{ (row as PlatformExportTask).failureMessage || '—' }}</span></template>
<template #failure="{ row }"><span class="failure-cell" :class="{ 'failure-message': Boolean((row as PlatformExportTask).failureMessage) }">{{ (row as PlatformExportTask).failureMessage || '—' }}</span></template>
</DataTable>
</section>
</div>
@@ -215,7 +219,8 @@ onBeforeUnmount(clearRefreshTimer)
.table-actions { display: flex; min-height: 32px; align-items: center; gap: 8px; }
.table-actions :deep(.el-button + .el-button) { margin-left: 0; }
.action-placeholder { color: var(--cc-muted); }
.failure-message { display: block; overflow: hidden; color: var(--cc-danger); text-overflow: ellipsis; white-space: nowrap; }
.failure-cell { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.failure-message { color: var(--cc-danger); }
small { display: block; margin-top: 3px; color: var(--cc-muted); font-size: 11px; }
@media (max-width: 720px) {
.download-error, .download-table-header { align-items: stretch; flex-direction: column; }
+10
View File
@@ -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[]
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/api/http'
import { http } from '@/shared/api/http'
import {
acceptPlatformFirstLegOrder,
addPlatformFirstLegOrderNote,
@@ -12,7 +12,7 @@ import {
updatePlatformFirstLegOrderExpress,
} from './platformFirstLegOrders'
vi.mock('@/api/http', () => ({
vi.mock('@/shared/api/http', () => ({
http: { get: vi.fn(), post: vi.fn(), put: vi.fn() },
unwrapApiResponse: <T>(response: { data: T }) => response.data,
}))
@@ -1,4 +1,4 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/api/http'
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
export type PlatformFirstLegOrderStatus =
| 'DRAFT'
@@ -17,13 +17,13 @@ import {
type PlatformFirstLegOrderDetailResource,
type PlatformFirstLegOrderResource,
type PlatformFirstLegOrderStatus,
} from '@/api/platformFirstLegOrders'
import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/api/http'
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
import StatusBadge from '@/components/StatusBadge.vue'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
import { useAccessPreviewStore } from '@/stores/accessPreview'
} from '@/modules/first-leg-orders/api/platformFirstLegOrders'
import { ApiResponseError, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
const PHASE_ONE_TENANT_ID = '10001'
const route = useRoute()
@@ -0,0 +1,7 @@
<script setup lang="ts">
import PlatformFirstLegOrdersPanel from '@/modules/first-leg-orders/components/PlatformFirstLegOrdersPanel.vue'
</script>
<template>
<PlatformFirstLegOrdersPanel />
</template>
+10
View File
@@ -0,0 +1,10 @@
import type { RouteRecordRaw } from 'vue-router'
export const firstLegOrderRoutes = [
{
path: 'orders/first-leg',
name: 'first-leg-orders',
component: () => import('@/modules/first-leg-orders/pages/FirstLegOrdersView.vue'),
meta: { title: '头程订单', description: '创建、受理和跟进客户提交的头程运输订单', permission: 'platform:order:list' },
},
] satisfies RouteRecordRaw[]
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/api/http'
import { http } from '@/shared/api/http'
import {
assignPlatformInternalUserRoles,
changePlatformManagedUserStatus,
@@ -17,7 +17,6 @@ import {
listPlatformTenants,
listPlatformTenantOrderCounts,
listTenantQualifications,
listCurrentPlatformPermissionCodes,
listPlatformPermissions,
listPlatformRoles,
listPlatformInternalUsers,
@@ -40,7 +39,7 @@ import {
type PlatformRoleResource,
} from './platformIam'
vi.mock('@/api/http', () => ({
vi.mock('@/shared/api/http', () => ({
http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn() },
unwrapApiResponse: <T>(response: { data: T }) => response.data,
}))
@@ -237,13 +236,6 @@ describe('platform IAM roles', () => {
expect(http.put).toHaveBeenNthCalledWith(3, `/platform/v1/iam/users/${managedUser.membershipId}/password`, { newPassword: 'ChangeMe123' })
})
it('loads the current users permission snapshot without calling the role-management endpoint', async () => {
vi.mocked(http.get).mockResolvedValue({ code: '200', message: 'success', data: ['platform:warehouse:list'] })
await expect(listCurrentPlatformPermissionCodes()).resolves.toEqual(['platform:warehouse:list'])
expect(http.get).toHaveBeenCalledWith('/platform/v1/iam/authorization/permissions')
})
it('loads qualifications for the selected tenant', async () => {
const qualification = {
id: 12,
@@ -1,19 +1,7 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/api/http'
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type { PlatformPermissionResource } from '@/shared/types/platformPermission'
export interface PlatformPermissionResource {
id: number | string
tenantId: number | string
appCode: 'PLATFORM'
permissionType: 'MENU' | 'BUTTON'
parentId: number | string
permissionCode: string
permissionName: string
routePath: string | null
httpMethod: string | null
sortNo: number
status: 'ACTIVE' | 'DISABLED' | 'INACTIVE'
builtIn?: boolean
}
export type { PlatformPermissionResource } from '@/shared/types/platformPermission'
export interface EnterpriseReview {
id: number
@@ -262,10 +250,6 @@ export function listPlatformPermissions() {
return getData<PlatformPermissionResource[]>('/platform/v1/iam/permissions')
}
export function listCurrentPlatformPermissionCodes() {
return getData<string[]>('/platform/v1/iam/authorization/permissions')
}
export function listEnterpriseReviews() {
return getData<EnterpriseReview[]>('/platform/v1/iam/reviews')
}
+1
View File
@@ -0,0 +1 @@
export * from './api/platformIam'
@@ -6,12 +6,12 @@ import {
getPlatformUserOptions,
listPlatformManagedUsers,
type PlatformManagedUser,
} from '@/api/platformIam'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import { useAuthStore } from '@/stores/auth'
} from '@/modules/iam/api/platformIam'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
import InternalUsersView from './InternalUsersView.vue'
vi.mock('@/api/platformIam', () => ({
vi.mock('@/modules/iam/api/platformIam', () => ({
listPlatformManagedUsers: vi.fn(),
getPlatformUserOptions: vi.fn(),
createPlatformManagedUser: vi.fn(),
@@ -11,14 +11,14 @@ import {
updatePlatformManagedUser,
type PlatformManagedUser,
type PlatformUserOptions,
} from '@/api/platformIam'
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
import StatusBadge from '@/components/StatusBadge.vue'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import { useAuthStore } from '@/stores/auth'
} from '@/modules/iam/api/platformIam'
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
const access = useAccessPreviewStore()
const auth = useAuthStore()
@@ -1,11 +1,11 @@
import { createPinia, setActivePinia } from 'pinia'
import { flushPromises, shallowMount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { listPlatformPermissions, type PlatformPermissionResource } from '@/api/platformIam'
import { useAuthStore } from '@/stores/auth'
import { listPlatformPermissions, type PlatformPermissionResource } from '@/modules/iam/api/platformIam'
import { useAuthStore } from '@/core/auth/auth'
import PermissionsView from './PermissionsView.vue'
vi.mock('@/api/platformIam', () => ({
vi.mock('@/modules/iam/api/platformIam', () => ({
changePlatformPermissionStatus: vi.fn(),
createPlatformPermission: vi.fn(),
deletePlatformPermission: vi.fn(),
@@ -2,23 +2,23 @@
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Braces, FolderTree, Pencil, Plus, ShieldCheck, Trash2 } from '@lucide/vue'
import StatusBadge from '@/components/StatusBadge.vue'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import {
changePlatformPermissionStatus,
createPlatformPermission,
deletePlatformPermission,
listPlatformPermissions,
updatePlatformPermission,
} from '@/api/platformIam'
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import type { Permission, PermissionType } from '@/types/admin'
import { buildPermissionTree, getDescendantIds, type PermissionTreeNode } from '@/utils/permissionTree'
import { mapPlatformPermission } from '@/utils/platformPermission'
} from '@/modules/iam/api/platformIam'
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import type { Permission, PermissionType } from '@/modules/iam/types'
import { buildPermissionTree, getDescendantIds, type PermissionTreeNode } from '@/modules/iam/utils/permissionTree'
import { mapPlatformPermission } from '@/modules/iam/utils/platformPermission'
const access = useAccessPreviewStore()
const editorOpen = ref(false)
@@ -1,11 +1,11 @@
import { createPinia, setActivePinia } from 'pinia'
import { flushPromises, shallowMount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createPlatformRole, listPlatformInternalUsers, listPlatformPermissions, listPlatformRoles, type PlatformRoleResource } from '@/api/platformIam'
import { useAuthStore } from '@/stores/auth'
import { createPlatformRole, listPlatformInternalUsers, listPlatformPermissions, listPlatformRoles, type PlatformRoleResource } from '@/modules/iam/api/platformIam'
import { useAuthStore } from '@/core/auth/auth'
import RolesView from './RolesView.vue'
vi.mock('@/api/platformIam', () => ({
vi.mock('@/modules/iam/api/platformIam', () => ({
createPlatformRole: vi.fn(),
listPlatformInternalUsers: vi.fn(),
listPlatformPermissions: vi.fn(),
@@ -91,7 +91,7 @@ describe('RolesView', () => {
expect(listPlatformRoles).toHaveBeenCalledTimes(1)
expect(listPlatformInternalUsers).toHaveBeenCalledTimes(1)
expect(wrapper.text()).toContain('角色查询、新增、编辑、启停、删除及权限分配均使用 IAM 真实接口')
expect(wrapper.text()).toContain('角色负责聚合菜单、操作和数据范围,内部用户可分配多个角色')
expect(wrapper.find('[data-testid="permission-tree"]').exists()).toBe(false)
})
@@ -12,17 +12,17 @@ import {
replacePlatformRolePermissions,
updatePlatformRole,
type PlatformRoleResource,
} from '@/api/platformIam'
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
import StatusBadge from '@/components/StatusBadge.vue'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import type { Permission, Role } from '@/types/admin'
import { buildPermissionTree, summarizePermissionSelection } from '@/utils/permissionTree'
import { mapPlatformPermission } from '@/utils/platformPermission'
} from '@/modules/iam/api/platformIam'
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import type { Permission, Role } from '@/modules/iam/types'
import { buildPermissionTree, summarizePermissionSelection } from '@/modules/iam/utils/permissionTree'
import { mapPlatformPermission } from '@/modules/iam/utils/platformPermission'
interface TreeController {
getCheckedKeys: (leafOnly?: boolean) => Array<string | number>
+22
View File
@@ -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[]
+42
View File
@@ -0,0 +1,42 @@
export type RecordStatus = '正常' | '停用'
export type RoleStatus = '启用' | '停用'
export type PermissionType = '目录' | '菜单' | '按钮' | '接口'
export type DataScope = '全部数据' | '本部门数据' | '本人数据' | '自定义'
export interface InternalUser {
id: string
account: string
name: string
department: string
phone: string
email: string
roleIds: string[]
status: RecordStatus
lastLogin: string
createdAt: string
}
export interface Role {
id: string
name: string
code: string
description: string
userCount: number
dataScope: DataScope
status: RoleStatus
permissionIds: string[]
builtIn?: boolean
}
export interface Permission {
id: string
parentId: string | null
name: string
code: string
type: PermissionType
path: string
method: string
status: RoleStatus
sort: number
builtIn?: boolean
}
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { Permission } from '@/types/admin'
import { buildPermissionTree, getDescendantIds, summarizePermissionSelection } from '@/utils/permissionTree'
import type { Permission } from '@/modules/iam/types'
import { buildPermissionTree, getDescendantIds, summarizePermissionSelection } from '@/modules/iam/utils/permissionTree'
const permissions: Permission[] = [
{ id: 'action-b', parentId: 'menu', name: '操作 B', code: 'demo:b', type: '按钮', path: '', method: '-', status: '启用', sort: 20 },
@@ -1,4 +1,4 @@
import type { Permission } from '@/types/admin'
import type { Permission } from '@/modules/iam/types'
export type PermissionTreeNode = Permission & { children?: PermissionTreeNode[] }
export type PermissionMatcher = (permission: Permission) => boolean
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { mapPlatformPermission } from '@/utils/platformPermission'
import { mapPlatformPermission } from '@/modules/iam/utils/platformPermission'
describe('mapPlatformPermission', () => {
it('treats the string root parent as a root node and preserves long ids', () => {
@@ -1,5 +1,5 @@
import type { PlatformPermissionResource } from '@/api/platformIam'
import type { Permission } from '@/types/admin'
import type { PlatformPermissionResource } from '@/modules/iam/api/platformIam'
import type { Permission } from '@/modules/iam/types'
export function mapPlatformPermission(item: PlatformPermissionResource): Permission {
const parentId = String(item.parentId)
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/api/http'
import { http } from '@/shared/api/http'
import {
changePlatformPartnerStatus,
createPlatformPartner,
@@ -11,7 +11,7 @@ import {
type SavePlatformPartnerRequest,
} from './platformPartners'
vi.mock('@/api/http', () => ({
vi.mock('@/shared/api/http', () => ({
http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn() },
unwrapApiResponse: <T>(response: { data: T }) => response.data,
}))
@@ -1,4 +1,4 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/api/http'
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
export type PlatformPartnerType = 'CUSTOMER' | 'SUPPLIER' | 'CARRIER' | 'CUSTOMS_BROKER' | 'SHIPPER' | 'CONSIGNEE'
export type PlatformPartnerLegacyType = PlatformPartnerType | 'BOTH'
@@ -5,12 +5,12 @@ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vites
import {
listPlatformPartners,
type PlatformPartnerResource,
} from '@/api/platformPartners'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import { useAuthStore } from '@/stores/auth'
} from '@/modules/master-data/counterparties/api/platformPartners'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
import PartnersView from './PartnersView.vue'
vi.mock('@/api/platformPartners', () => ({
vi.mock('@/modules/master-data/counterparties/api/platformPartners', () => ({
listPlatformPartners: vi.fn(),
createPlatformPartner: vi.fn(),
getPlatformPartner: vi.fn(),
@@ -2,11 +2,11 @@
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules, type UploadFile } from 'element-plus'
import { Building2, Download, Eye, MapPin, Pencil, Plus, Power, Upload } from '@lucide/vue'
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
import { getApiErrorDetails, type ApiErrorDetails } from '@/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import {
changePlatformPartnerStatus,
createPlatformPartner,
@@ -21,8 +21,8 @@ import {
type PlatformPartnerLegacyType,
type PlatformPartnerType,
type SavePlatformPartnerRequest,
} from '@/api/platformPartners'
import { useAccessPreviewStore } from '@/stores/accessPreview'
} from '@/modules/master-data/counterparties/api/platformPartners'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
const fixedTenantId = 10001
const pageSize = ref(20)
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/api/http'
import { http } from '@/shared/api/http'
import {
changePlatformExpressCompanyStatus,
createPlatformExpressCompany,
@@ -11,7 +11,7 @@ import {
type SavePlatformExpressCompanyRequest,
} from './platformExpressCompanies'
vi.mock('@/api/http', () => ({
vi.mock('@/shared/api/http', () => ({
http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn() },
unwrapApiResponse: <T>(response: { data: T }) => response.data,
}))
@@ -1,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'
@@ -2,12 +2,12 @@ import ElementPlus from 'element-plus'
import { createPinia, setActivePinia } from 'pinia'
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { listPlatformExpressCompanies, type PlatformExpressCompanyResource } from '@/api/platformExpressCompanies'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import { useAuthStore } from '@/stores/auth'
import { listPlatformExpressCompanies, type PlatformExpressCompanyResource } from '@/modules/master-data/express-companies/api/platformExpressCompanies'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
import ExpressCompaniesView from './ExpressCompaniesView.vue'
vi.mock('@/api/platformExpressCompanies', () => ({
vi.mock('@/modules/master-data/express-companies/api/platformExpressCompanies', () => ({
listPlatformExpressCompanies: vi.fn(),
createPlatformExpressCompany: vi.fn(),
updatePlatformExpressCompany: vi.fn(),
@@ -2,11 +2,11 @@
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Download, ExternalLink, FileSpreadsheet, Pencil, Plus, Power, Truck, Upload } from '@lucide/vue'
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
import { getApiErrorDetails, type ApiErrorDetails } from '@/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import {
changePlatformExpressCompanyStatus,
createPlatformExpressCompany,
@@ -19,8 +19,8 @@ import {
type PlatformExpressCompanyResource,
type PlatformExpressCompanyStatus,
type SavePlatformExpressCompanyRequest,
} from '@/api/platformExpressCompanies'
import { useAccessPreviewStore } from '@/stores/accessPreview'
} from '@/modules/master-data/express-companies/api/platformExpressCompanies'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
const fixedTenantId = 10001
const access = useAccessPreviewStore()
+16
View File
@@ -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[]
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { http } from '@/api/http'
import { http } from '@/shared/api/http'
import {
changePlatformWarehouseStatus,
createPlatformWarehouse,
@@ -9,7 +9,7 @@ import {
type SavePlatformWarehouseRequest,
} from './platformWarehouses'
vi.mock('@/api/http', () => ({
vi.mock('@/shared/api/http', () => ({
http: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn() },
unwrapApiResponse: <T>(response: { data: T }) => response.data,
}))
@@ -1,4 +1,4 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/api/http'
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
export type PlatformWarehouseStatus = 'ACTIVE' | 'INACTIVE'
@@ -9,12 +9,12 @@ import {
updatePlatformWarehouse,
type PlatformWarehouseDetailResource,
type PlatformWarehouseResource,
} from '@/api/platformWarehouses'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import { useAuthStore } from '@/stores/auth'
} from '@/modules/warehouses/api/platformWarehouses'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
import WarehousesView from './WarehousesView.vue'
vi.mock('@/api/platformWarehouses', () => ({
vi.mock('@/modules/warehouses/api/platformWarehouses', () => ({
listPlatformWarehouses: vi.fn(),
getPlatformWarehouse: vi.fn(),
createPlatformWarehouse: vi.fn(),
@@ -2,10 +2,10 @@
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { MapPin, Pencil, Plus, Power, Warehouse as WarehouseIcon } from '@lucide/vue'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
import { getApiErrorDetails, type ApiErrorDetails } from '@/api/http'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import {
changePlatformWarehouseStatus,
createPlatformWarehouse,
@@ -15,9 +15,9 @@ import {
type PlatformWarehouseDetailResource,
type PlatformWarehouseResource,
type SavePlatformWarehouseRequest,
} from '@/api/platformWarehouses'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import type { Warehouse } from '@/types/admin'
} from '@/modules/warehouses/api/platformWarehouses'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import type { Warehouse } from '@/modules/warehouses/types'
const fixedTenantId = 10001
const access = useAccessPreviewStore()
+10
View File
@@ -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[]
+3
View File
@@ -0,0 +1,3 @@
import type { PlatformReceivingWarehouse } from '@chongchong/shared'
export type Warehouse = PlatformReceivingWarehouse
@@ -1,5 +1,5 @@
<script setup lang="ts">
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
defineProps<{ title: string; description: string }>()
</script>
<template><PrototypeModuleView v-bind="$props" /></template>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
defineProps<{ title: string; description: string }>()
</script>
<template><PrototypeModuleView v-bind="$props" /></template>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
defineProps<{ title: string; description: string }>()
</script>
<template><PrototypeModuleView v-bind="$props" /></template>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import PrototypeModuleView from '@/views/PrototypeModuleView.vue'
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
defineProps<{ title: string; description: string }>()
</script>
<template><PrototypeModuleView v-bind="$props" /></template>
@@ -0,0 +1,5 @@
<script setup lang="ts">
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
defineProps<{ title: string; description: string }>()
</script>
<template><PrototypeModuleView v-bind="$props" /></template>
@@ -0,0 +1,5 @@
<script setup lang="ts">
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
defineProps<{ title: string; description: string }>()
</script>
<template><PrototypeModuleView v-bind="$props" /></template>
@@ -0,0 +1,5 @@
<script setup lang="ts">
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
defineProps<{ title: string; description: string }>()
</script>
<template><PrototypeModuleView v-bind="$props" /></template>
@@ -0,0 +1,5 @@
<script setup lang="ts">
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
defineProps<{ title: string; description: string }>()
</script>
<template><PrototypeModuleView v-bind="$props" /></template>
@@ -0,0 +1,5 @@
<script setup lang="ts">
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
defineProps<{ title: string; description: string }>()
</script>
<template><PrototypeModuleView v-bind="$props" /></template>
@@ -0,0 +1,5 @@
<script setup lang="ts">
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
defineProps<{ title: string; description: string }>()
</script>
<template><PrototypeModuleView v-bind="$props" /></template>
@@ -0,0 +1,5 @@
<script setup lang="ts">
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
defineProps<{ title: string; description: string }>()
</script>
<template><PrototypeModuleView v-bind="$props" /></template>
@@ -0,0 +1,5 @@
<script setup lang="ts">
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
defineProps<{ title: string; description: string }>()
</script>
<template><PrototypeModuleView v-bind="$props" /></template>
@@ -0,0 +1,5 @@
<script setup lang="ts">
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
defineProps<{ title: string; description: string }>()
</script>
<template><PrototypeModuleView v-bind="$props" /></template>
@@ -0,0 +1,5 @@
<script setup lang="ts">
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
defineProps<{ title: string; description: string }>()
</script>
<template><PrototypeModuleView v-bind="$props" /></template>
@@ -0,0 +1,5 @@
<script setup lang="ts">
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
defineProps<{ title: string; description: string }>()
</script>
<template><PrototypeModuleView v-bind="$props" /></template>
@@ -0,0 +1,5 @@
<script setup lang="ts">
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
defineProps<{ title: string; description: string }>()
</script>
<template><PrototypeModuleView v-bind="$props" /></template>
@@ -0,0 +1,5 @@
<script setup lang="ts">
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
defineProps<{ title: string; description: string }>()
</script>
<template><PrototypeModuleView v-bind="$props" /></template>
@@ -0,0 +1,5 @@
<script setup lang="ts">
import PrototypeModuleView from '@/prototype/components/PrototypeModuleView.vue'
defineProps<{ title: string; description: string }>()
</script>
<template><PrototypeModuleView v-bind="$props" /></template>
@@ -5,24 +5,24 @@ type PrototypeRoute = readonly [path: string, name: string, title: string]
type ModuleView = () => Promise<Component>
const moduleViews = {
dashboard: () => import('@/views/prototype/dashboard/ModuleView.vue'),
customerManagement: () => import('@/views/prototype/customer-management/ModuleView.vue'),
domesticWarehouse: () => import('@/views/prototype/domestic-warehouse/ModuleView.vue'),
overseasDelivery: () => import('@/views/prototype/overseas-delivery/ModuleView.vue'),
overseasInventory: () => import('@/views/prototype/overseas-inventory/ModuleView.vue'),
serviceCenter: () => import('@/views/prototype/service-center/ModuleView.vue'),
supplierCenter: () => import('@/views/prototype/supplier-center/ModuleView.vue'),
containerManagement: () => import('@/views/prototype/container-management/ModuleView.vue'),
overseasBusiness: () => import('@/views/prototype/overseas-business/ModuleView.vue'),
overseasWarehouseOrders: () => import('@/views/prototype/overseas-warehouse-orders/ModuleView.vue'),
fbaReturns: () => import('@/views/prototype/fba-returns/ModuleView.vue'),
finance: () => import('@/views/prototype/finance/ModuleView.vue'),
vehicleManagement: () => import('@/views/prototype/vehicle-management/ModuleView.vue'),
reports: () => import('@/views/prototype/reports/ModuleView.vue'),
general: () => import('@/views/prototype/general/ModuleView.vue'),
sales: () => import('@/views/prototype/sales/ModuleView.vue'),
messages: () => import('@/views/prototype/messages/ModuleView.vue'),
systemManagement: () => import('@/views/prototype/system-management/ModuleView.vue'),
dashboard: () => import('@/prototype/modules/dashboard/ModuleView.vue'),
customerManagement: () => import('@/prototype/modules/customer-management/ModuleView.vue'),
domesticWarehouse: () => import('@/prototype/modules/domestic-warehouse/ModuleView.vue'),
overseasDelivery: () => import('@/prototype/modules/overseas-delivery/ModuleView.vue'),
overseasInventory: () => import('@/prototype/modules/overseas-inventory/ModuleView.vue'),
serviceCenter: () => import('@/prototype/modules/service-center/ModuleView.vue'),
supplierCenter: () => import('@/prototype/modules/supplier-center/ModuleView.vue'),
containerManagement: () => import('@/prototype/modules/container-management/ModuleView.vue'),
overseasBusiness: () => import('@/prototype/modules/overseas-business/ModuleView.vue'),
overseasWarehouseOrders: () => import('@/prototype/modules/overseas-warehouse-orders/ModuleView.vue'),
fbaReturns: () => import('@/prototype/modules/fba-returns/ModuleView.vue'),
finance: () => import('@/prototype/modules/finance/ModuleView.vue'),
vehicleManagement: () => import('@/prototype/modules/vehicle-management/ModuleView.vue'),
reports: () => import('@/prototype/modules/reports/ModuleView.vue'),
general: () => import('@/prototype/modules/general/ModuleView.vue'),
sales: () => import('@/prototype/modules/sales/ModuleView.vue'),
messages: () => import('@/prototype/modules/messages/ModuleView.vue'),
systemManagement: () => import('@/prototype/modules/system-management/ModuleView.vue'),
} satisfies Record<string, ModuleView>
function createRoutes(view: ModuleView, routes: PrototypeRoute[]): RouteRecordRaw[] {
-133
View File
@@ -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