diff --git a/apps/web/src/components/auth/RequireAuth.spec.tsx b/apps/web/src/components/auth/RequireAuth.spec.tsx index 93547a9..2e75c9b 100644 --- a/apps/web/src/components/auth/RequireAuth.spec.tsx +++ b/apps/web/src/components/auth/RequireAuth.spec.tsx @@ -28,7 +28,7 @@ function renderGuard(entry: string): void {
受保护页面
@@ -54,7 +54,7 @@ describe('登录守卫', () => { }); it('未登录时跳登录页并携带 redirect', async () => { - renderGuard('/users'); + renderGuard('/system/users'); expect(await screen.findByTestId('login-route')).toBeTruthy(); expect(screen.queryByText('受保护页面')).toBeNull(); }); @@ -70,7 +70,7 @@ describe('登录守卫', () => { permissions: ['user:view'], mustChangePassword: true, }); - renderGuard('/users'); + renderGuard('/system/users'); expect(await screen.findByTestId('login-route')).toBeTruthy(); expect(useAuthStore.getState().accessToken).toBeNull(); }); @@ -78,7 +78,7 @@ describe('登录守卫', () => { it('已登录(演示会话)时直接渲染受保护内容', () => { useAuthStore.getState().signInDemo(); expect(useAuthStore.getState().accessToken).not.toBeNull(); - renderGuard('/users'); + renderGuard('/system/users'); expect(screen.getByText('受保护页面')).toBeTruthy(); }); }); diff --git a/apps/web/src/components/auth/RequirePermission.spec.tsx b/apps/web/src/components/auth/RequirePermission.spec.tsx index e7de08f..7405316 100644 --- a/apps/web/src/components/auth/RequirePermission.spec.tsx +++ b/apps/web/src/components/auth/RequirePermission.spec.tsx @@ -16,10 +16,10 @@ function renderGuard(permissions: string[]): void { permissions, }); render( - +
用户页面
diff --git a/apps/web/src/layout/AppLayout.tsx b/apps/web/src/layout/AppLayout.tsx index 96c7b20..9fcd7a5 100644 --- a/apps/web/src/layout/AppLayout.tsx +++ b/apps/web/src/layout/AppLayout.tsx @@ -13,7 +13,7 @@ import { TopBar } from './TopBar'; /** 用户与组织为「满高主从布局」,由页面自己控制内部滚动 */ function isFlushRoute(pathname: string): boolean { - return pathname === ROUTES.users || pathname.startsWith(`${ROUTES.users}/`); + return pathname === ROUTES.system.users || pathname.startsWith(`${ROUTES.system.users}/`); } export function AppLayout(): ReactNode { diff --git a/apps/web/src/navigation/breadcrumbs.ts b/apps/web/src/navigation/breadcrumbs.ts index 795ecb3..63e1b7d 100644 --- a/apps/web/src/navigation/breadcrumbs.ts +++ b/apps/web/src/navigation/breadcrumbs.ts @@ -1,23 +1,25 @@ +import { ROUTES } from '@/routes'; + import type { BreadcrumbEntry } from './nav-config'; /** - * /users 下的子路由(新建 / 详情)不在 nav-config 的菜单项里, - * 这里补齐面包屑:工作台 / 平台 / 用户与组织 / 当前页。 + * 用户模块下的子路由(新建 / 详情)不在 nav-config 的菜单项里, + * 这里补齐面包屑:工作台 / 管理中心 / 用户与组织 / 当前页。 */ export function getUserSectionTrail(pathname: string): BreadcrumbEntry[] | null { - if (pathname === '/users/new') { + if (pathname === ROUTES.system.userNew) { return [ - { key: 'home', label: '工作台', path: '/' }, - { key: 'platform', label: '平台' }, - { key: 'users', label: '用户与组织', path: '/users' }, + { key: 'home', label: '工作台', path: ROUTES.home }, + { key: 'system', label: '管理中心' }, + { key: 'users', label: '用户与组织', path: ROUTES.system.users }, { key: 'user-new', label: '新建用户' }, ]; } - if (/^\/users\/[^/]+$/.test(pathname)) { + if (/^\/system\/users\/[^/]+$/.test(pathname)) { return [ - { key: 'home', label: '工作台', path: '/' }, - { key: 'platform', label: '平台' }, - { key: 'users', label: '用户与组织', path: '/users' }, + { key: 'home', label: '工作台', path: ROUTES.home }, + { key: 'system', label: '管理中心' }, + { key: 'users', label: '用户与组织', path: ROUTES.system.users }, { key: 'user-detail', label: '成员详情' }, ]; } diff --git a/apps/web/src/navigation/legacy-redirect.tsx b/apps/web/src/navigation/legacy-redirect.tsx new file mode 100644 index 0000000..c34b043 --- /dev/null +++ b/apps/web/src/navigation/legacy-redirect.tsx @@ -0,0 +1,31 @@ +import type { ReactNode } from 'react'; +import { Navigate, useLocation } from 'react-router'; + +import { ROUTES } from '@/routes'; + +function buildTarget( + pathname: string, + search: string, + hash: string, + from: string, + to: string, +): string { + const rest = pathname === from ? '' : pathname.slice(from.length); + return `${to}${rest}${search}${hash}`; +} + +/** 旧路径 `/users*` → `/system/users*`,保留一个版本后删除 */ +export function LegacyUsersRedirect(): ReactNode { + const { pathname, search, hash } = useLocation(); + return ( + + ); +} + +/** 旧路径 `/platform*` → `/system*`;`/platform` 落到用户列表 */ +export function LegacySystemRedirect(): ReactNode { + const { pathname, search, hash } = useLocation(); + const to = pathname === '/platform' ? ROUTES.system.users : ROUTES.system.root; + const from = pathname === '/platform' ? '/platform' : '/platform'; + return ; +} diff --git a/apps/web/src/navigation/nav-config.spec.ts b/apps/web/src/navigation/nav-config.spec.ts index 45aca21..625f7cf 100644 --- a/apps/web/src/navigation/nav-config.spec.ts +++ b/apps/web/src/navigation/nav-config.spec.ts @@ -16,9 +16,9 @@ describe('导航配置', () => { it('缺少权限点时过滤菜单,模块没有子菜单时整组隐藏', () => { const modules = filterNavByPermissions(NAV_MODULES, new Set(['script:read'])); - const script = modules.find((module) => module.key === 'script'); + const script = modules.find((module) => module.key === 'scripts'); expect(script?.items.map((item) => item.key)).toEqual(['library']); - expect(modules.some((module) => module.key === 'platform')).toBe(false); + expect(modules.some((module) => module.key === 'system')).toBe(false); }); it('只有无权限要求的菜单时仍然可见', () => { @@ -26,10 +26,10 @@ describe('导航配置', () => { expect(modules.map((module) => module.key)).toEqual(['dashboard']); }); - it('子路由(/users/new、/users/:id)也归属用户与组织', () => { - expect(findActiveNav(NAV_MODULES, '/users/new')?.item?.label).toBe('用户与组织'); - expect(findActiveNav(NAV_MODULES, '/users/01HZZZZZZZZZZZZZZZZZZZZZZZ')?.module.key).toBe( - 'platform', + it('子路由(/system/users/new、/system/users/:id)也归属用户与组织', () => { + expect(findActiveNav(NAV_MODULES, '/system/users/new')?.item?.label).toBe('用户与组织'); + expect(findActiveNav(NAV_MODULES, '/system/users/01HZZZZZZZZZZZZZZZZZZZZZZZ')?.module.key).toBe( + 'system', ); }); @@ -42,7 +42,7 @@ describe('导航配置', () => { it('面包屑从导航配置派生', () => { expect(getBreadcrumbs(NAV_MODULES, '/')).toEqual([{ key: 'home', label: '工作台', path: '/' }]); - const breadcrumbs = getBreadcrumbs(NAV_MODULES, '/users'); - expect(breadcrumbs.map((entry) => entry.label)).toEqual(['工作台', '平台', '用户与组织']); + const breadcrumbs = getBreadcrumbs(NAV_MODULES, '/system/users'); + expect(breadcrumbs.map((entry) => entry.label)).toEqual(['工作台', '管理中心', '用户与组织']); }); }); diff --git a/apps/web/src/navigation/nav-config.ts b/apps/web/src/navigation/nav-config.ts index 913d59e..c5b65af 100644 --- a/apps/web/src/navigation/nav-config.ts +++ b/apps/web/src/navigation/nav-config.ts @@ -68,7 +68,7 @@ export const NAV_MODULES: readonly NavModule[] = [ ], }, { - key: 'script', + key: 'scripts', label: '脚本', path: '/scripts/library', icon: IconScript, @@ -166,7 +166,7 @@ export const NAV_MODULES: readonly NavModule[] = [ ], }, { - key: 'asset', + key: 'assets', label: '资产', path: '/assets/library', icon: IconAsset, @@ -240,16 +240,16 @@ export const NAV_MODULES: readonly NavModule[] = [ ], }, { - key: 'platform', - label: '平台', - path: '/users', + key: 'system', + label: '管理中心', + path: '/system/users', icon: IconSettings, permission: Permission.USER_VIEW, items: [ { key: 'users', label: '用户与组织', - path: '/users', + path: '/system/users', icon: IconUser, permission: Permission.USER_VIEW, description: '账号、组织与登录状态', @@ -257,7 +257,7 @@ export const NAV_MODULES: readonly NavModule[] = [ { key: 'roles', label: '角色权限', - path: '/platform/roles', + path: '/system/roles', icon: IconShield, permission: Permission.ROLE_MANAGE, description: '角色、权限点与职责互斥', @@ -265,7 +265,7 @@ export const NAV_MODULES: readonly NavModule[] = [ { key: 'customers', label: '客户', - path: '/platform/customers', + path: '/system/customers', icon: IconGlobal, permission: Permission.CUSTOMER_MANAGE, description: '客户、行业归属与数据范围', @@ -273,7 +273,7 @@ export const NAV_MODULES: readonly NavModule[] = [ { key: 'industries', label: '行业树', - path: '/platform/industries', + path: '/system/industries', icon: IconStack, permission: Permission.INDUSTRY_MANAGE, description: '行业分类树与行业包绑定', @@ -281,7 +281,7 @@ export const NAV_MODULES: readonly NavModule[] = [ { key: 'dicts', label: '字典', - path: '/platform/dicts', + path: '/system/dicts', icon: IconDocs, permission: Permission.DICT_MANAGE, description: '核心字典与业务枚举', @@ -289,7 +289,7 @@ export const NAV_MODULES: readonly NavModule[] = [ { key: 'audit', label: '审计日志', - path: '/platform/audit', + path: '/system/audit-logs', icon: IconView, permission: Permission.AUDIT_READ, description: '关键操作留痕与导出记录', @@ -297,7 +297,7 @@ export const NAV_MODULES: readonly NavModule[] = [ { key: 'ai', label: '模型接入', - path: '/platform/ai', + path: '/system/ai', icon: IconRobot, permission: Permission.AI_CONFIGURE, description: '供应商、路由与预算阈值', @@ -305,7 +305,7 @@ export const NAV_MODULES: readonly NavModule[] = [ { key: 'settings', label: '系统配置', - path: '/platform/settings', + path: '/system/settings', icon: IconSettings, permission: Permission.SYSTEM_CONFIGURE, description: '阈值、上限与功能开关', @@ -313,7 +313,7 @@ export const NAV_MODULES: readonly NavModule[] = [ { key: 'industry-packs', label: '行业包', - path: '/platform/industry-packs', + path: '/system/industry-packs', icon: IconGrid, permission: Permission.INDUSTRY_MANAGE, description: '行业包版本、发布与灰度', @@ -321,7 +321,7 @@ export const NAV_MODULES: readonly NavModule[] = [ { key: 'forms', label: '表单设计器', - path: '/platform/forms', + path: '/system/forms', icon: IconGrid, permission: Permission.FORM_DESIGN, description: '受控字段白名单与表单布局', @@ -329,7 +329,7 @@ export const NAV_MODULES: readonly NavModule[] = [ { key: 'customer-fields', label: '客户字段', - path: '/platform/customer-fields', + path: '/system/customer-fields', icon: IconTask, permission: Permission.CUSTOMER_MANAGE, description: '客户级扩展字段与审批', @@ -337,7 +337,7 @@ export const NAV_MODULES: readonly NavModule[] = [ { key: 'compliance', label: '合规规则', - path: '/platform/compliance', + path: '/system/compliance', icon: IconShield, permission: Permission.SYSTEM_CONFIGURE, description: '禁忌词、极限词与合规规则集', diff --git a/apps/web/src/pages/home/HomePage.tsx b/apps/web/src/pages/dashboard/HomePage.tsx similarity index 100% rename from apps/web/src/pages/home/HomePage.tsx rename to apps/web/src/pages/dashboard/HomePage.tsx diff --git a/apps/web/src/pages/login/LoginPage.tsx b/apps/web/src/pages/login/LoginPage.tsx index e641dc1..5b839a0 100644 --- a/apps/web/src/pages/login/LoginPage.tsx +++ b/apps/web/src/pages/login/LoginPage.tsx @@ -24,7 +24,7 @@ export default function LoginPage(): ReactNode { const [searchParams] = useSearchParams(); const form = useLoginForm(); const clearSession = useAuthStore((state) => state.clearSession); - const target = sanitizeRedirect(searchParams.get('redirect')) ?? ROUTES.users; + const target = sanitizeRedirect(searchParams.get('redirect')) ?? ROUTES.system.users; const accountDisabled = searchParams.get('reason') === 'account-disabled'; useEffect(() => { diff --git a/apps/web/src/pages/login/use-login-form.ts b/apps/web/src/pages/login/use-login-form.ts index a463dc8..49a2d75 100644 --- a/apps/web/src/pages/login/use-login-form.ts +++ b/apps/web/src/pages/login/use-login-form.ts @@ -60,7 +60,7 @@ export function useLoginForm(): LoginFormState { const [successMessage, setSuccessMessage] = useState(null); const redirectTarget = (): string => - sanitizeRedirect(searchParams.get('redirect')) ?? ROUTES.users; + sanitizeRedirect(searchParams.get('redirect')) ?? ROUTES.system.users; const submit = async (): Promise => { if (submitting || lockedMessage !== null || mustChangePassword) { diff --git a/apps/web/src/pages/users/RoleChipsSelect.tsx b/apps/web/src/pages/system/users/RoleChipsSelect.tsx similarity index 100% rename from apps/web/src/pages/users/RoleChipsSelect.tsx rename to apps/web/src/pages/system/users/RoleChipsSelect.tsx diff --git a/apps/web/src/pages/users/UserDetailPanel.tsx b/apps/web/src/pages/system/users/UserDetailPanel.tsx similarity index 100% rename from apps/web/src/pages/users/UserDetailPanel.tsx rename to apps/web/src/pages/system/users/UserDetailPanel.tsx diff --git a/apps/web/src/pages/users/UserDetailTabs.tsx b/apps/web/src/pages/system/users/UserDetailTabs.tsx similarity index 100% rename from apps/web/src/pages/users/UserDetailTabs.tsx rename to apps/web/src/pages/system/users/UserDetailTabs.tsx diff --git a/apps/web/src/pages/users/UserListPanel.tsx b/apps/web/src/pages/system/users/UserListPanel.tsx similarity index 100% rename from apps/web/src/pages/users/UserListPanel.tsx rename to apps/web/src/pages/system/users/UserListPanel.tsx diff --git a/apps/web/src/pages/users/UserNewPage.tsx b/apps/web/src/pages/system/users/UserNewPage.tsx similarity index 97% rename from apps/web/src/pages/users/UserNewPage.tsx rename to apps/web/src/pages/system/users/UserNewPage.tsx index 5fa5dee..7f2981e 100644 --- a/apps/web/src/pages/users/UserNewPage.tsx +++ b/apps/web/src/pages/system/users/UserNewPage.tsx @@ -213,7 +213,9 @@ export function UserNewPage(): ReactNode { createUser.mutate(input, { onSuccess: (userId) => { notifySuccess('成员已创建,初始密码已生成'); - void navigate(userId === null ? ROUTES.users : userDetailPath(userId), { replace: true }); + void navigate(userId === null ? ROUTES.system.users : userDetailPath(userId), { + replace: true, + }); }, }); }; @@ -241,7 +243,7 @@ export function UserNewPage(): ReactNode {