refactor: 菜单栏角色化降噪(role-based-navigation-simplification)
这个提交包含在:
@@ -207,7 +207,7 @@ export function hasBusinessApproverRole(roleCodes: string[] | undefined, permiss
|
||||
}
|
||||
|
||||
export function hasFinanceRole(roleCodes: string[] | undefined, permissionCodes: string[]) {
|
||||
const hasRole = (roleCodes ?? []).some((roleCode) => (FINANCE_ROLE_CODES as readonly string[]).includes(roleCode));
|
||||
const hasRole = (roleCodes ?? []).some((roleCode) => (FINANCE_ROLE_CODES as readonly string[]).includes(roleCode) || roleCode === "admin");
|
||||
return hasRole && hasAnyPermission(permissionCodes, [...FINANCE_PORTAL_PERMISSIONS]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { NavLink, useLocation } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchSystemNavigation, type NavigationItem } from "../../api/workspace";
|
||||
import { useAuth } from "../../auth/AuthContext";
|
||||
@@ -19,67 +19,53 @@ export type NavItem = {
|
||||
|
||||
export const NAV_ITEMS: NavItem[] = [
|
||||
{ to: "/portal", label: "工作台", icon: "home_app_logo" },
|
||||
{ to: "/team", label: "团队", icon: "groups" },
|
||||
{ to: "/leave-requests", label: "请假", icon: "event_available", anyOf: ["leave.view_own", "leave.view_all", "leave.manage"] },
|
||||
{ to: "/team", label: "团队", icon: "groups", anyOf: ["report.dashboard.view", "project.view_all", "project.manage", "system.users"] },
|
||||
{ to: "/leave-requests", label: "请假记录", icon: "event_available", anyOf: ["leave.view_own", "leave.view_all", "leave.manage"] },
|
||||
{ to: "/attendance", label: "考勤", icon: "schedule" },
|
||||
{ to: "/reimbursements", label: "报销", icon: "request_quote", anyOf: ["expense.view_own", "expense.view_all"] },
|
||||
{ to: "/assets", label: "资产", icon: "inventory_2", anyOf: ["asset.view_own", "asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"] },
|
||||
{ to: "/weekly-reports", label: "周报", icon: "article" },
|
||||
{ to: "/todos", label: "待办", icon: "check_circle" },
|
||||
{ to: "/announcements", label: "公告", icon: "campaign" },
|
||||
{ to: "/projects", label: "项目", icon: "workspaces", anyOf: ["project.view_own", "project.view_all", "project.manage"] },
|
||||
{ to: "/system/workspace", label: "工作区设置", icon: "tune", anyOf: ["system.settings"] },
|
||||
{ to: "/people", label: "人员管理", icon: "group", anyOf: ["system.users", "report.dashboard.view"] },
|
||||
{ to: "/organization", label: "组织架构", icon: "account_tree", anyOf: ["system.users", "report.dashboard.view"] },
|
||||
{ to: "/system/workflow-definitions", label: "流程定义", icon: "schema", anyOf: ["system.settings"] },
|
||||
{ to: "/system/settings", label: "系统设置", icon: "settings", anyOf: ["system.settings", "system.users", "masterdata.manage", "notification.channel.manage", "integration.manage"] },
|
||||
{ to: "/system/feature-flags", label: "功能开关", icon: "toggle_on", anyOf: ["system.settings"] },
|
||||
{ to: "/audit-logs", label: "审计日志", icon: "manage_search", anyOf: ["system.audit.view"] },
|
||||
{ to: "/workflow/tasks", label: "审批待办", icon: "fact_check", anyOf: ["expense.approve", "leave.approve", "attendance.approve", "procurement.approve", "travel.approve"] },
|
||||
{ to: "/projects", label: "项目", icon: "workspaces", anyOf: ["project.view_all", "project.manage"] },
|
||||
{ to: "/system/settings", label: "管理", icon: "settings", anyOf: ["system.settings", "system.users", "masterdata.manage", "notification.channel.manage", "integration.manage", "system.audit.view"] },
|
||||
{ to: "/workflow/tasks", label: "待我处理", icon: "fact_check", anyOf: ["expense.approve", "leave.approve", "attendance.approve", "procurement.approve", "travel.approve"] },
|
||||
{ to: "/contracts-registry", label: "合同台账", icon: "description", anyOf: ["project.view_all", "project.manage", "system.settings"] },
|
||||
{ to: "/purchase-requests", label: "采购申请", icon: "shopping_cart", anyOf: ["procurement.create", "procurement.view_own", "procurement.view_all", "procurement.manage"] },
|
||||
{ to: "/travel-requests", label: "差旅申请", icon: "flight", anyOf: ["travel.view_all", "travel.manage", "travel.payment.manage"] },
|
||||
{ to: "/budgets", label: "预算管理", icon: "account_balance", anyOf: ["budget.view", "budget.manage"] },
|
||||
{ to: "/payment-batches", label: "付款批次", icon: "payments", anyOf: ["payment.batch.manage", "risk.review.manage"] },
|
||||
{ to: "/ap-operations", label: "AP 运营", icon: "account_balance_wallet", anyOf: ["payment.batch.manage", "risk.review.manage"] },
|
||||
{ to: "/invoice-pool", label: "票据中心", icon: "receipt_long", anyOf: ["invoice.manage"] },
|
||||
{ to: "/payment-batches", label: "付款", icon: "payments", anyOf: ["payment.batch.manage", "risk.review.manage"] },
|
||||
{ to: "/ap-operations", label: "应付账款", icon: "account_balance_wallet", anyOf: ["payment.batch.manage", "risk.review.manage"] },
|
||||
{ to: "/invoice-pool", label: "票据", icon: "receipt_long", anyOf: ["invoice.manage"] },
|
||||
{ to: "/inventory-checks", label: "资产盘点", icon: "inventory", anyOf: ["asset.inventory"] },
|
||||
{ to: "/system/cutover", label: "切换控制台", icon: "swap_calls", anyOf: ["system.settings"] },
|
||||
{ to: "/system/integrations/erp", label: "外部系统桥接", icon: "hub", anyOf: ["integration.manage"] },
|
||||
{ to: "/dashboard", label: "仪表盘", icon: "dashboard", anyOf: ["report.dashboard.view"] },
|
||||
{ to: "/settings/profile", label: "个人设置", icon: "manage_accounts" },
|
||||
{ to: "/dashboard", label: "数据看板", icon: "dashboard", anyOf: ["report.dashboard.view"] },
|
||||
];
|
||||
|
||||
const LEGACY_ACCESSIBLE_LABELS: Record<string, string> = {
|
||||
const ACCESSIBLE_LABELS: Record<string, string> = {
|
||||
"/reimbursements": "我的报销",
|
||||
"/workflow/tasks": "待我处理",
|
||||
"/assets": "资产台账",
|
||||
};
|
||||
|
||||
const WORK_NAV_ORDER = ["/portal", "/workflow/tasks", "/reimbursements", "/todos"];
|
||||
const BUSINESS_NAV_ORDER = [
|
||||
"/projects",
|
||||
"/contracts-registry",
|
||||
"/purchase-requests",
|
||||
"/assets",
|
||||
"/weekly-reports",
|
||||
"/team",
|
||||
"/announcements",
|
||||
"/leave-requests",
|
||||
"/attendance",
|
||||
"/wecom-approval-archive",
|
||||
];
|
||||
const FINANCE_NAV_ORDER = ["/payment-batches", "/ap-operations", "/invoice-pool", "/budgets", "/inventory-checks"];
|
||||
const MANAGEMENT_NAV_ORDER = [
|
||||
"/people",
|
||||
"/organization",
|
||||
const MANAGEMENT_ACTIVE_PATHS = [
|
||||
"/system/settings",
|
||||
"/system/workspace",
|
||||
"/system/workflow-definitions",
|
||||
"/system/settings",
|
||||
"/system/feature-flags",
|
||||
"/system/approval-rules",
|
||||
"/system/expense-standards",
|
||||
"/system/reimbursement-templates",
|
||||
"/system/master-data",
|
||||
"/system/notification-channels",
|
||||
"/system/integrations/erp",
|
||||
"/system/users",
|
||||
"/system/departments",
|
||||
"/system/cutover",
|
||||
"/people",
|
||||
"/people/stats",
|
||||
"/organization",
|
||||
"/audit-logs",
|
||||
];
|
||||
const MORE_NAV_ORDER = ["/travel-requests", "/dashboard", "/settings/profile", "/system/cutover", "/system/integrations/erp"];
|
||||
const TEAM_NAVIGATION_ROLE_CODES = ["manager", "ceo", "finance_mgr", "hr", "admin"];
|
||||
|
||||
type ResolvedNavItem = {
|
||||
key?: string;
|
||||
@@ -88,8 +74,16 @@ type ResolvedNavItem = {
|
||||
icon: string;
|
||||
};
|
||||
|
||||
function renderNavItem(item: ResolvedNavItem, onNavigate?: () => void) {
|
||||
const accessibleLabel = LEGACY_ACCESSIBLE_LABELS[item.href] ?? item.label;
|
||||
function isGroupedActive(item: ResolvedNavItem, pathname: string) {
|
||||
if (item.href === "/system/settings") {
|
||||
return MANAGEMENT_ACTIVE_PATHS.some((path) => pathname === path || pathname.startsWith(`${path}/`));
|
||||
}
|
||||
|
||||
return pathname === item.href || pathname.startsWith(`${item.href}/`);
|
||||
}
|
||||
|
||||
function renderNavItem(item: ResolvedNavItem, pathname: string, onNavigate?: () => void) {
|
||||
const accessibleLabel = ACCESSIBLE_LABELS[item.href] ?? item.label;
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
@@ -99,68 +93,65 @@ function renderNavItem(item: ResolvedNavItem, onNavigate?: () => void) {
|
||||
title={accessibleLabel}
|
||||
onClick={() => onNavigate?.()}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"group nav-link-item",
|
||||
isActive ? "nav-link-item--active" : "nav-link-item--idle",
|
||||
)
|
||||
cn("group nav-link-item", isActive || isGroupedActive(item, pathname) ? "nav-link-item--active" : "nav-link-item--idle")
|
||||
}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<span
|
||||
className={cn(
|
||||
"nav-link-item__icon",
|
||||
isActive ? "nav-link-item__icon--active" : "nav-link-item__icon--idle",
|
||||
)}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[20px]">{item.icon}</span>
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate font-medium">{item.label}</span>
|
||||
</>
|
||||
)}
|
||||
{({ isActive }) => {
|
||||
const active = isActive || isGroupedActive(item, pathname);
|
||||
return (
|
||||
<>
|
||||
<span className={cn("nav-link-item__icon", active ? "nav-link-item__icon--active" : "nav-link-item__icon--idle")}>
|
||||
<span className="material-symbols-outlined text-[20px]">{item.icon}</span>
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate font-medium">{item.label}</span>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSection(title: string, items: ResolvedNavItem[], onNavigate?: () => void) {
|
||||
function renderPrimarySection(items: ResolvedNavItem[], pathname: string, onNavigate?: () => void) {
|
||||
if (!items.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="nav-section">
|
||||
<div className="nav-section__label">{title}</div>
|
||||
<div className="space-y-1.5">{items.map((item) => renderNavItem(item, onNavigate))}</div>
|
||||
<div className="space-y-1.5">{items.map((item) => renderNavItem(item, pathname, onNavigate))}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function renderBackupSection(items: ResolvedNavItem[], onNavigate?: () => void) {
|
||||
function renderBackupSection(items: ResolvedNavItem[], pathname: string, onNavigate?: () => void) {
|
||||
if (!items.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<details className="rounded-2xl border border-slate-200 bg-slate-50/80 px-3 py-3">
|
||||
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 rounded-xl px-2 py-1.5 text-sm font-semibold text-[var(--color-text-primary)]">
|
||||
<div>
|
||||
<div>更多</div>
|
||||
<div className="mt-0.5 text-xs font-normal text-[var(--color-text-secondary)]">低频入口和历史模块</div>
|
||||
</div>
|
||||
<span className="material-symbols-outlined text-[18px] text-[var(--color-text-tertiary)]">expand_more</span>
|
||||
<details className="nav-more-section">
|
||||
<summary className="nav-more-section__summary">
|
||||
<span className="nav-link-item__icon nav-link-item__icon--idle">
|
||||
<span className="material-symbols-outlined text-[20px]">more_horiz</span>
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate font-medium">更多</span>
|
||||
<span className="material-symbols-outlined nav-more-section__chevron">expand_more</span>
|
||||
</summary>
|
||||
<div className="mt-3 space-y-1.5 pb-1">{items.map((item) => renderNavItem(item, onNavigate))}</div>
|
||||
<div className="mt-1.5 space-y-1.5 pb-1">{items.map((item) => renderNavItem(item, pathname, onNavigate))}</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
function buildFallbackItems(permissions: string[], roleCodes: string[]) {
|
||||
const financeOnlyPaths = new Set(["/payment-batches", "/ap-operations", "/invoice-pool"]);
|
||||
const backupOnlyPaths = new Set(BACKUP_NAV_ORDER);
|
||||
const mapItems = (paths: string[]) =>
|
||||
NAV_ITEMS.filter((item) => paths.includes(item.to))
|
||||
.filter((item) => item.to !== "/workflow/tasks" || hasBusinessApproverRole(roleCodes, permissions))
|
||||
.filter((item) => !financeOnlyPaths.has(item.to) || hasFinanceRole(roleCodes, permissions))
|
||||
.filter((item) => item.to !== "/team" || roleCodes.some((roleCode) => TEAM_NAVIGATION_ROLE_CODES.includes(roleCode)))
|
||||
.filter((item) => !financeOnlyPaths.has(item.to) || hasFinanceRole(roleCodes, permissions) || roleCodes.includes("admin"))
|
||||
.filter((item) => !item.anyOf || hasAnyPermission(permissions, item.anyOf))
|
||||
.filter((item) => paths === BACKUP_NAV_ORDER || !backupOnlyPaths.has(item.to))
|
||||
.map((item) => ({ key: item.to, href: item.to, label: item.label, icon: item.icon }));
|
||||
|
||||
return {
|
||||
@@ -170,77 +161,43 @@ function buildFallbackItems(permissions: string[], roleCodes: string[]) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSections(sections: { main: NavigationItem[]; admin: NavigationItem[]; backup: NavigationItem[] }) {
|
||||
const allItems = [...sections.main, ...sections.admin, ...sections.backup];
|
||||
function orderItems(items: ResolvedNavItem[], order: string[]) {
|
||||
const byHref = new Map<string, ResolvedNavItem>();
|
||||
|
||||
for (const item of allItems) {
|
||||
for (const item of items) {
|
||||
if (!byHref.has(item.href)) {
|
||||
byHref.set(item.href, item);
|
||||
}
|
||||
}
|
||||
|
||||
const used = new Set<string>();
|
||||
const take = (hrefs: string[]) =>
|
||||
hrefs
|
||||
.map((href) => {
|
||||
const item = byHref.get(href);
|
||||
if (item) {
|
||||
used.add(href);
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.filter((item): item is ResolvedNavItem => Boolean(item));
|
||||
const ordered = order
|
||||
.map((href) => {
|
||||
const item = byHref.get(href);
|
||||
if (item) {
|
||||
used.add(href);
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.filter((item): item is ResolvedNavItem => Boolean(item));
|
||||
|
||||
return {
|
||||
main: take(MAIN_NAV_ORDER),
|
||||
admin: take(ADMIN_NAV_ORDER),
|
||||
backup: [
|
||||
...take(BACKUP_NAV_ORDER),
|
||||
...allItems
|
||||
.filter((item) => !used.has(item.href))
|
||||
.map((item) => ({ key: item.key, href: item.href, label: item.label, icon: item.icon })),
|
||||
],
|
||||
};
|
||||
return [...ordered, ...items.filter((item) => !used.has(item.href))];
|
||||
}
|
||||
|
||||
function regroupSections(sections: { main: ResolvedNavItem[]; admin: ResolvedNavItem[]; backup: ResolvedNavItem[] }) {
|
||||
const allItems = [...sections.main, ...sections.admin, ...sections.backup];
|
||||
const byHref = new Map<string, ResolvedNavItem>();
|
||||
for (const item of allItems) {
|
||||
if (!byHref.has(item.href)) {
|
||||
byHref.set(item.href, item);
|
||||
}
|
||||
}
|
||||
|
||||
const used = new Set<string>();
|
||||
const take = (hrefs: string[]) =>
|
||||
hrefs
|
||||
.map((href) => {
|
||||
const item = byHref.get(href);
|
||||
if (item) {
|
||||
used.add(href);
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.filter((item): item is ResolvedNavItem => Boolean(item));
|
||||
function toResolved(items: NavigationItem[]) {
|
||||
return items.map((item) => ({ key: item.key, href: item.href, label: item.label, icon: item.icon }));
|
||||
}
|
||||
|
||||
function normalizeSections(sections: { main: NavigationItem[]; admin: NavigationItem[]; backup: NavigationItem[] }) {
|
||||
return {
|
||||
work: take(WORK_NAV_ORDER),
|
||||
business: take(BUSINESS_NAV_ORDER),
|
||||
finance: take(FINANCE_NAV_ORDER),
|
||||
management: take(MANAGEMENT_NAV_ORDER),
|
||||
more: [
|
||||
...take(MORE_NAV_ORDER),
|
||||
...allItems
|
||||
.filter((item) => !used.has(item.href))
|
||||
.map((item) => ({ key: item.key, href: item.href, label: item.label, icon: item.icon })),
|
||||
],
|
||||
main: orderItems(toResolved(sections.main), MAIN_NAV_ORDER),
|
||||
admin: orderItems(toResolved(sections.admin), ADMIN_NAV_ORDER),
|
||||
backup: orderItems(toResolved(sections.backup), BACKUP_NAV_ORDER),
|
||||
};
|
||||
}
|
||||
|
||||
export function NavLinks({ onNavigate }: { onNavigate?: () => void }) {
|
||||
const { me, permissions } = useAuth();
|
||||
const location = useLocation();
|
||||
const navigationQuery = useQuery({
|
||||
queryKey: ["system-navigation"],
|
||||
queryFn: fetchSystemNavigation,
|
||||
@@ -250,15 +207,12 @@ export function NavLinks({ onNavigate }: { onNavigate?: () => void }) {
|
||||
const sections = navigationQuery.data?.sections
|
||||
? normalizeSections(navigationQuery.data.sections)
|
||||
: buildFallbackItems(permissions, me?.roleCodes ?? []);
|
||||
const groupedSections = regroupSections(sections);
|
||||
const primaryItems = [...sections.main, ...sections.admin];
|
||||
|
||||
return (
|
||||
<nav className="space-y-5">
|
||||
{renderSection("工作", groupedSections.work, onNavigate)}
|
||||
{renderSection("业务", groupedSections.business, onNavigate)}
|
||||
{renderSection("财务", groupedSections.finance, onNavigate)}
|
||||
{renderSection("管理", groupedSections.management, onNavigate)}
|
||||
{renderBackupSection(groupedSections.more, onNavigate)}
|
||||
<nav className="space-y-3.5">
|
||||
{renderPrimarySection(primaryItems, location.pathname, onNavigate)}
|
||||
{renderBackupSection(sections.backup, location.pathname, onNavigate)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,13 +6,6 @@ export function Sidebar() {
|
||||
<aside className="app-shell-sidebar fixed inset-y-0 z-20 hidden w-72 overflow-hidden border-r border-slate-200 lg:flex lg:flex-col">
|
||||
<div className="border-b border-slate-200 px-6 py-5">
|
||||
<AppBrand />
|
||||
<div className="sidebar-focus-card mt-4 text-[var(--color-text-secondary)]">
|
||||
<div className="flex items-center gap-2 font-semibold text-[var(--color-text-primary)]">
|
||||
<span className="size-2 rounded-full bg-primary" />
|
||||
今日优先
|
||||
</div>
|
||||
<div className="mt-1.5 leading-6">待办、报销、项目和财务流程按优先级进入。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4 [scrollbar-width:thin]">
|
||||
|
||||
@@ -42,7 +42,7 @@ export type MobileNavItem = {
|
||||
|
||||
export const MOBILE_NAV_ITEMS: MobileNavItem[] = [
|
||||
{ key: "home", to: "/mobile", label: "首页", icon: "home" },
|
||||
{ key: "approvals", to: "/mobile/approvals", label: "审批", icon: "fact_check" },
|
||||
{ key: "approvals", to: "/mobile/approvals", label: "待办", icon: "fact_check" },
|
||||
{ key: "reimbursements", to: "/mobile/reimbursements", label: "报销", icon: "request_quote", anyOf: MOBILE_REIMBURSEMENT_PERMISSIONS },
|
||||
{ key: "me", to: "/mobile/me", label: "我的", icon: "person" },
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const MAIN_NAV_ORDER = ["/portal", "/workflow/tasks", "/team", "/leave-requests", "/reimbursements", "/assets", "/weekly-reports", "/todos", "/announcements", "/projects"];
|
||||
export const ADMIN_NAV_ORDER = ["/system/workspace", "/people", "/organization", "/system/workflow-definitions", "/system/settings", "/system/feature-flags", "/audit-logs"];
|
||||
export const BACKUP_NAV_ORDER = ["/attendance", "/wecom-approval-archive", "/contracts-registry", "/purchase-requests", "/travel-requests", "/budgets", "/payment-batches", "/ap-operations", "/invoice-pool", "/inventory-checks", "/system/cutover", "/system/integrations/erp", "/dashboard", "/settings/profile"];
|
||||
export const MAIN_NAV_ORDER = ["/portal", "/workflow/tasks", "/reimbursements", "/payment-batches", "/ap-operations", "/invoice-pool", "/projects", "/team", "/leave-requests"];
|
||||
export const ADMIN_NAV_ORDER = ["/system/settings"];
|
||||
export const BACKUP_NAV_ORDER = ["/assets", "/weekly-reports", "/announcements", "/purchase-requests", "/contracts-registry", "/budgets", "/attendance", "/wecom-approval-archive", "/travel-requests", "/inventory-checks", "/dashboard"];
|
||||
|
||||
const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [
|
||||
{ path: "/portal", title: "工作台", group: "工作台" },
|
||||
@@ -13,18 +13,18 @@ const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [
|
||||
{ path: "/purchase-requests", title: "采购申请", group: "业务" },
|
||||
{ path: "/travel-requests", title: "差旅申请", group: "更多" },
|
||||
{ path: "/budgets", title: "预算管理", group: "财务" },
|
||||
{ path: "/payment-batches", title: "付款批次", group: "财务" },
|
||||
{ path: "/ap-operations", title: "应付作业", group: "财务" },
|
||||
{ path: "/invoice-pool", title: "票据中心", group: "财务" },
|
||||
{ path: "/payment-batches", title: "付款", group: "财务" },
|
||||
{ path: "/ap-operations", title: "应付账款", group: "财务" },
|
||||
{ path: "/invoice-pool", title: "票据", group: "财务" },
|
||||
{ path: "/inventory-checks", title: "盘点任务", group: "财务" },
|
||||
{ path: "/weekly-reports", title: "周报", group: "业务" },
|
||||
{ path: "/todos", title: "待办", group: "工作" },
|
||||
{ path: "/reimbursements/", title: "报销详情", group: "工作" },
|
||||
{ path: "/reimbursements", title: "报销", group: "工作" },
|
||||
{ path: "/leave-requests", title: "请假", group: "业务" },
|
||||
{ path: "/leave-requests", title: "请假记录", group: "业务" },
|
||||
{ path: "/assets", title: "资产", group: "业务" },
|
||||
{ path: "/workflow/tasks/", title: "审批详情", group: "工作" },
|
||||
{ path: "/workflow/tasks", title: "审批待办", group: "工作" },
|
||||
{ path: "/workflow/tasks", title: "待我处理", group: "工作" },
|
||||
{ path: "/mobile/assets/scan", title: "移动资产扫描", group: "移动工作台" },
|
||||
{ path: "/mobile/approvals", title: "移动审批", group: "移动工作台" },
|
||||
{ path: "/mobile/wecom-approval-archive", title: "移动企微审批历史", group: "移动工作台" },
|
||||
@@ -41,10 +41,10 @@ const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [
|
||||
{ path: "/organization", title: "组织架构", group: "管理" },
|
||||
{ path: "/system/approval-rules", title: "审批规则", group: "管理" },
|
||||
{ path: "/system/workflow-definitions", title: "流程定义", group: "管理" },
|
||||
{ path: "/system/settings", title: "系统设置", group: "管理" },
|
||||
{ path: "/system/feature-flags", title: "功能开关", group: "管理" },
|
||||
{ path: "/system/settings", title: "管理", group: "管理" },
|
||||
{ path: "/system/feature-flags", title: "功能管理", group: "管理" },
|
||||
{ path: "/system/cutover", title: "切换控制台", group: "更多" },
|
||||
{ path: "/system/integrations/erp", title: "ERP 集成", group: "更多" },
|
||||
{ path: "/system/integrations/erp", title: "外部系统", group: "更多" },
|
||||
{ path: "/dashboard", title: "数据看板", group: "更多" },
|
||||
{ path: "/audit-logs", title: "审计日志", group: "管理" },
|
||||
];
|
||||
|
||||
@@ -152,6 +152,28 @@
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.nav-more-section {
|
||||
@apply space-y-1.5;
|
||||
}
|
||||
|
||||
.nav-more-section__summary {
|
||||
color: var(--color-text-secondary);
|
||||
@apply flex cursor-pointer list-none items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm transition duration-150 hover:bg-slate-50 hover:text-slate-950;
|
||||
}
|
||||
|
||||
.nav-more-section__summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-more-section__chevron {
|
||||
color: var(--color-text-tertiary);
|
||||
@apply text-[18px] transition-transform;
|
||||
}
|
||||
|
||||
.nav-more-section[open] .nav-more-section__chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.auth-shell {
|
||||
background:
|
||||
radial-gradient(circle at 20% 12%, rgba(0, 57, 138, 0.10), transparent 26rem),
|
||||
|
||||
@@ -262,7 +262,7 @@ export function ErpIntegrationPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="ERP 集成"
|
||||
title="外部系统"
|
||||
subtitle="主数据同步、业务队列和 ID 映射。"
|
||||
actions={
|
||||
tab === "master-data" ? (
|
||||
|
||||
@@ -81,9 +81,9 @@ function formatRolloutTypeLabel(value: FeatureFlagRolloutType) {
|
||||
}
|
||||
|
||||
function formatSectionLabel(value: FeatureFlagSection) {
|
||||
if (value === "main") return "主导航";
|
||||
if (value === "main") return "常用入口";
|
||||
if (value === "admin") return "管理入口";
|
||||
if (value === "backup") return "备用入口";
|
||||
if (value === "backup") return "更多";
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ export function FeatureFlagsPage() {
|
||||
return (
|
||||
<ListWorkspace className={batchMode ? "pb-32" : undefined}>
|
||||
<WorkspaceHeader
|
||||
title="功能开关"
|
||||
title="功能管理"
|
||||
subtitle="外部桥接、验真、灰度和试运行边界。"
|
||||
/>
|
||||
<WorkspaceNotice title="安全摘要" tone="warning">
|
||||
|
||||
@@ -1033,7 +1033,7 @@ export function MobileApprovalsPage() {
|
||||
/>
|
||||
) : (
|
||||
<PageHeader
|
||||
title={canAccessRemoteApprovals ? "审批" : "审批进度"}
|
||||
title={canAccessRemoteApprovals ? "待办" : "进度"}
|
||||
subtitle={canAccessRemoteApprovals ? "处理待办,查看已处理。" : "查看我发起的申请进度。"}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -128,7 +128,7 @@ export function MobileMePage() {
|
||||
<MobileActionLink to="/mobile/leave" label="请假" icon="event_available" />
|
||||
<MobileActionLink to="/mobile/reimbursements" label="报销" icon="request_quote" />
|
||||
{canAccessPurchase ? <MobileActionLink to="/mobile/purchase-requests" label="采购" icon="shopping_cart" /> : null}
|
||||
<MobileActionLink to="/mobile/approvals" label={canAccessApprovals ? "审批" : "审批进度"} icon="fact_check" />
|
||||
<MobileActionLink to="/mobile/approvals" label={canAccessApprovals ? "待办" : "进度"} icon="fact_check" />
|
||||
{canAccessFinance ? <MobileActionLink to="/mobile#finance" label="财务" description="审批与付款状态" icon="account_balance_wallet" /> : null}
|
||||
{canAccessAssets ? <MobileActionLink to="/mobile/assets/scan" label="资产扫码" icon="inventory_2" /> : null}
|
||||
{canAccessProjects ? <MobileActionLink to="/mobile" label="项目动态" icon="workspaces" /> : null}
|
||||
|
||||
@@ -75,7 +75,7 @@ const ROLE_OPTIONS = [
|
||||
] as const;
|
||||
|
||||
const DOMAIN_SCOPE_OPTIONS = [
|
||||
{ value: "workflow", label: "审批待办" },
|
||||
{ value: "workflow", label: "待我处理" },
|
||||
{ value: "reimbursement", label: "报销" },
|
||||
{ value: "leave", label: "请假" },
|
||||
{ value: "procurement", label: "采购" },
|
||||
|
||||
@@ -16,7 +16,7 @@ const PAGE_SIZE = 20;
|
||||
|
||||
const NOTIFICATION_TYPE_OPTIONS = [
|
||||
{ label: "全部", value: "" },
|
||||
{ label: "审批待办", value: "workflow.task_assigned" },
|
||||
{ label: "待我处理", value: "workflow.task_assigned" },
|
||||
{ label: "报销", value: "reimbursement.approved,reimbursement.rejected,reimbursement.returned,reimbursement.paid" },
|
||||
{ label: "请假", value: "leave.approved,leave.rejected" },
|
||||
{ label: "采购", value: "purchase_request.approved,purchase_request.rejected" },
|
||||
|
||||
@@ -163,8 +163,8 @@ function buildTodoAggregateItems(payload: PortalHomePayload) {
|
||||
const items: TodoAggregateItem[] = [];
|
||||
if (payload.pendingTasks.count > 0) {
|
||||
items.push({
|
||||
title: `审批待办 ${payload.pendingTasks.count} 条`,
|
||||
sourceLabel: "审批",
|
||||
title: `待我处理 ${payload.pendingTasks.count} 条`,
|
||||
sourceLabel: "待办",
|
||||
severity: "warning",
|
||||
href: payload.pendingTasks.href,
|
||||
icon: "fact_check",
|
||||
@@ -925,9 +925,9 @@ export function PortalHomePage() {
|
||||
? {
|
||||
key: "approval-pending",
|
||||
href: payload.pendingTasks.href,
|
||||
label: "审批待办",
|
||||
label: "待我处理",
|
||||
value: `${payload.pendingTasks.count} 条`,
|
||||
note: payload.pendingTasks.count > 0 ? "进入审批中心处理。" : "暂无待处理审批。",
|
||||
note: payload.pendingTasks.count > 0 ? "进入待我处理办理。" : "暂无待处理事项。",
|
||||
tone: payload.pendingTasks.count > 0 ? "warning" as const : "normal" as const,
|
||||
}
|
||||
: null,
|
||||
@@ -1051,7 +1051,7 @@ export function PortalHomePage() {
|
||||
? { label: "报销进度", to: "/reimbursements" }
|
||||
: null,
|
||||
payload.pendingTasks.count > 0 && approvalAction?.to
|
||||
? { label: "审批中心", to: approvalAction.to }
|
||||
? { label: "待我处理", to: approvalAction.to }
|
||||
: null,
|
||||
todoAggregateItems.length > 1
|
||||
? { label: "全部待办", to: "/todos" }
|
||||
|
||||
@@ -1,41 +1,90 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission } from "../auth/permissions";
|
||||
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
|
||||
const ITEMS = [
|
||||
{ to: "/system/workspace", icon: "dashboard_customize", title: "工作区设置", description: "导航与模块显隐。" },
|
||||
{ to: "/people", icon: "badge", title: "人员管理", description: "入转调离与档案。" },
|
||||
{ to: "/system/approval-rules", icon: "account_tree", title: "审批规则", description: "审批链与规则。" },
|
||||
{ to: "/system/expense-standards", icon: "rule_settings", title: "费用标准", description: "费用上限与管控。" },
|
||||
{ to: "/system/reimbursement-templates", icon: "receipt_long", title: "报销模板", description: "常见报销模板。" },
|
||||
{ to: "/system/master-data", icon: "database", title: "主数据", description: "法体、成本中心、供应商。" },
|
||||
{ to: "/system/notification-channels", icon: "notifications", title: "通知渠道", description: "IM / 邮件补充通知。" },
|
||||
{ to: "/system/integrations/erp", icon: "hub", title: "外部系统桥接", description: "财务出入口与桥接。" },
|
||||
type ManagementItem = {
|
||||
to: string;
|
||||
icon: string;
|
||||
title: string;
|
||||
description: string;
|
||||
group: "人员与组织" | "流程与规则" | "系统与审计" | "功能管理";
|
||||
anyOf: string[];
|
||||
};
|
||||
|
||||
const ITEMS: ManagementItem[] = [
|
||||
{ to: "/people", icon: "badge", title: "人员", description: "员工档案、入转调离和人员状态。", group: "人员与组织", anyOf: ["system.users", "report.dashboard.view"] },
|
||||
{ to: "/organization", icon: "account_tree", title: "组织", description: "组织结构、部门和汇报关系。", group: "人员与组织", anyOf: ["system.users", "report.dashboard.view"] },
|
||||
{ to: "/system/users", icon: "manage_accounts", title: "账号", description: "账号、角色和登录权限。", group: "人员与组织", anyOf: ["system.users"] },
|
||||
{ to: "/system/departments", icon: "account_tree", title: "部门", description: "部门基础数据维护。", group: "人员与组织", anyOf: ["system.users"] },
|
||||
{ to: "/system/approval-rules", icon: "rule", title: "审批规则", description: "金额段、审批层级和处理规则。", group: "流程与规则", anyOf: ["system.settings"] },
|
||||
{ to: "/system/workflow-definitions", icon: "schema", title: "流程定义", description: "流程模板和节点治理。", group: "流程与规则", anyOf: ["system.settings"] },
|
||||
{ to: "/system/expense-standards", icon: "rule_settings", title: "费用标准", description: "费用上限、类别和管控口径。", group: "流程与规则", anyOf: ["system.settings"] },
|
||||
{ to: "/system/reimbursement-templates", icon: "receipt_long", title: "报销模板", description: "常见报销场景模板。", group: "流程与规则", anyOf: ["system.settings"] },
|
||||
{ to: "/system/workspace", icon: "dashboard_customize", title: "工作区", description: "菜单入口、模块显隐和小团队基线。", group: "系统与审计", anyOf: ["system.settings"] },
|
||||
{ to: "/audit-logs", icon: "manage_search", title: "审计日志", description: "关键操作留痕和排障。", group: "系统与审计", anyOf: ["system.audit.view"] },
|
||||
{ to: "/system/master-data", icon: "dataset", title: "主数据", description: "法体、成本中心、供应商等基础数据。", group: "功能管理", anyOf: ["masterdata.view", "masterdata.manage"] },
|
||||
{ to: "/system/notification-channels", icon: "notifications_active", title: "通知渠道", description: "企业微信、邮件等补充通知。", group: "功能管理", anyOf: ["notification.channel.manage"] },
|
||||
{ to: "/system/feature-flags", icon: "toggle_on", title: "功能管理", description: "功能开关、灰度和试运行边界。", group: "功能管理", anyOf: ["system.settings"] },
|
||||
{ to: "/system/integrations/erp", icon: "hub", title: "外部系统", description: "财务、ERP 和外部系统连接。", group: "功能管理", anyOf: ["integration.manage"] },
|
||||
];
|
||||
|
||||
const GROUPS: ManagementItem["group"][] = ["人员与组织", "流程与规则", "系统与审计", "功能管理"];
|
||||
|
||||
export function SystemSettingsPage() {
|
||||
const { permissions } = useAuth();
|
||||
const visibleItems = ITEMS.filter((item) => hasAnyPermission(permissions, item.anyOf));
|
||||
const visibleGroups = GROUPS.map((group) => ({
|
||||
group,
|
||||
items: visibleItems.filter((item) => item.group === group),
|
||||
})).filter((entry) => entry.items.length > 0);
|
||||
|
||||
return (
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader title="系统设置" subtitle="后台能力入口按管理职责收拢。" />
|
||||
<WorkspaceHeader title="管理" subtitle="低频配置集中到这里,侧边栏只保留一个管理入口。" />
|
||||
<div className="finance-status-strip" data-testid="system-settings-summary">
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">入口总数</div><div className="finance-metric-card__value">{ITEMS.length}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">业务配置</div><div className="finance-metric-card__value">5</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">外部桥接</div><div className="finance-metric-card__value">2</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">人员相关</div><div className="finance-metric-card__value">1</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">模式</div><div className="finance-metric-card__value text-base">小团队</div></div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">可用入口</div>
|
||||
<div className="finance-metric-card__value">{visibleItems.length}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">人员与组织</div>
|
||||
<div className="finance-metric-card__value">{visibleItems.filter((item) => item.group === "人员与组织").length}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">流程与规则</div>
|
||||
<div className="finance-metric-card__value">{visibleItems.filter((item) => item.group === "流程与规则").length}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">系统与审计</div>
|
||||
<div className="finance-metric-card__value">{visibleItems.filter((item) => item.group === "系统与审计").length}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">模式</div>
|
||||
<div className="finance-metric-card__value text-base">小团队</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-tile-grid">
|
||||
{ITEMS.map((item) => (
|
||||
<Link key={item.to} to={item.to} className="admin-tile-card">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="admin-tile-card__icon">
|
||||
<span className="material-symbols-outlined text-[20px]">{item.icon}</span>
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-base font-semibold text-zinc-900">{item.title}</span>
|
||||
<span className="mt-1 block text-sm leading-5 text-zinc-600">{item.description}</span>
|
||||
</span>
|
||||
|
||||
<div className="space-y-5">
|
||||
{visibleGroups.map(({ group, items }) => (
|
||||
<section key={group} className="admin-command-panel space-y-3">
|
||||
<h2 className="text-base font-semibold text-zinc-900">{group}</h2>
|
||||
<div className="admin-tile-grid">
|
||||
{items.map((item) => (
|
||||
<Link key={item.to} to={item.to} className="admin-tile-card">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="admin-tile-card__icon">
|
||||
<span className="material-symbols-outlined text-[20px]">{item.icon}</span>
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-base font-semibold text-zinc-900">{item.title}</span>
|
||||
<span className="mt-1 block text-sm leading-5 text-zinc-600">{item.description}</span>
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</Link>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</ListWorkspace>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { toast } from "sonner";
|
||||
const MODULE_LABELS: Record<string, string> = {
|
||||
portal: "工作台",
|
||||
team: "团队",
|
||||
leaveRequests: "请假",
|
||||
leaveRequests: "请假记录",
|
||||
attendance: "考勤",
|
||||
reimbursements: "报销",
|
||||
assets: "资产",
|
||||
@@ -24,31 +24,31 @@ const MODULE_LABELS: Record<string, string> = {
|
||||
organization: "组织架构",
|
||||
contractsRegistry: "合同台账",
|
||||
workflowDefinitions: "流程定义",
|
||||
systemSettings: "系统设置",
|
||||
featureFlags: "功能开关",
|
||||
systemSettings: "管理",
|
||||
featureFlags: "功能管理",
|
||||
auditLogs: "审计日志",
|
||||
approvalRules: "审批规则",
|
||||
userManagement: "用户管理",
|
||||
departments: "部门管理",
|
||||
masterData: "主数据",
|
||||
notificationChannels: "通知渠道",
|
||||
workflowTasks: "审批待办",
|
||||
workflowTasks: "待我处理",
|
||||
dashboard: "仪表盘",
|
||||
purchaseRequests: "采购申请",
|
||||
travelRequests: "差旅申请",
|
||||
budgets: "预算管理",
|
||||
paymentBatches: "付款批次",
|
||||
apOperations: "AP 运营",
|
||||
invoicePool: "票据中心",
|
||||
paymentBatches: "付款",
|
||||
apOperations: "应付账款",
|
||||
invoicePool: "票据",
|
||||
inventoryChecks: "资产盘点",
|
||||
cutover: "切换控制台",
|
||||
erpIntegrations: "ERP 集成",
|
||||
erpIntegrations: "外部系统",
|
||||
profile: "个人设置",
|
||||
};
|
||||
|
||||
const VISIBILITY_OPTIONS = [
|
||||
{ value: "main", label: "主导航" },
|
||||
{ value: "admin", label: "管理区" },
|
||||
{ value: "main", label: "常用入口" },
|
||||
{ value: "admin", label: "管理与更多" },
|
||||
{ value: "hidden", label: "隐藏" },
|
||||
] as const;
|
||||
|
||||
@@ -101,7 +101,7 @@ export function WorkspaceSetupPage() {
|
||||
<WorkspaceHeader
|
||||
eyebrow="管理"
|
||||
title="工作区设置"
|
||||
subtitle="按 10 人以下小团队模式收敛主导航与默认入口。"
|
||||
subtitle="按 10 人以下小团队模式收敛常用入口与低频模块。"
|
||||
/>
|
||||
<div className="admin-command-panel text-sm text-zinc-500">加载工作区设置中...</div>
|
||||
</ListWorkspace>
|
||||
@@ -113,7 +113,7 @@ export function WorkspaceSetupPage() {
|
||||
<WorkspaceHeader
|
||||
eyebrow="管理"
|
||||
title="工作区设置"
|
||||
subtitle="按小团队运营模式收敛主导航、管理入口和备用模块。"
|
||||
subtitle="按小团队运营模式收敛常用入口、管理入口和低频模块。"
|
||||
/>
|
||||
|
||||
<div className="finance-status-strip" data-testid="workspace-settings-summary">
|
||||
@@ -122,11 +122,11 @@ export function WorkspaceSetupPage() {
|
||||
<div className="finance-metric-card__value">{workspaceStats.total}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">主导航</div>
|
||||
<div className="finance-metric-card__label">常用入口</div>
|
||||
<div className="finance-metric-card__value">{workspaceStats.main}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">管理区</div>
|
||||
<div className="finance-metric-card__label">管理与更多</div>
|
||||
<div className="finance-metric-card__value">{workspaceStats.admin}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
@@ -171,9 +171,9 @@ export function WorkspaceSetupPage() {
|
||||
|
||||
<section className="admin-command-panel space-y-4">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-zinc-900">模块显隐</h3>
|
||||
<h3 className="text-base font-semibold text-zinc-900">入口显隐</h3>
|
||||
<p className="mt-1 text-sm leading-6 text-zinc-600">
|
||||
主导航只保留高频入口;备用模块保留代码与数据,低频功能默认收进管理区或隐藏。
|
||||
常用入口只保留高频任务;低频能力保留代码与数据,默认收进管理、更多或隐藏。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ async function mockApis(page: Page, ledgerQueries: string[]) {
|
||||
sections: {
|
||||
main: [navigationItem("portal", "/portal", "工作台", "home_app_logo")],
|
||||
admin: [],
|
||||
backup: [navigationItem("ap", "/ap-operations", "AP 运营", "account_balance_wallet")],
|
||||
backup: [navigationItem("ap", "/ap-operations", "应付账款", "account_balance_wallet")],
|
||||
},
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -508,7 +508,7 @@ async function mockApis(page: Page, role: RolePreset, notifications: MockNotific
|
||||
main: [
|
||||
navigationItem("portal", "/portal", "工作台", "home"),
|
||||
navigationItem("expense", "/reimbursements", "报销", "request_quote"),
|
||||
navigationItem("leave", "/leave-requests", "请假", "event_available"),
|
||||
navigationItem("leave", "/leave-requests", "请假记录", "event_available"),
|
||||
],
|
||||
admin: [],
|
||||
backup: [],
|
||||
@@ -938,7 +938,7 @@ test.describe("MIF7 移动信息架构整改", () => {
|
||||
await loginForMobile(mobile.page, "manager");
|
||||
|
||||
await mobile.page.goto("/mobile");
|
||||
await expect(mobile.page.getByTestId("mobile-nav-approvals-label")).toHaveText("审批");
|
||||
await expect(mobile.page.getByTestId("mobile-nav-approvals-label")).toHaveText("待办");
|
||||
await expect(mobile.page.getByTestId("mobile-pending-summary")).toHaveCount(1);
|
||||
await expect(mobile.page.getByTestId("mobile-notifications")).not.toContainText("报销待审批");
|
||||
await expect(mobile.page.getByTestId("mobile-action-approvals")).toHaveCount(0);
|
||||
|
||||
@@ -31,7 +31,7 @@ export class AppShell {
|
||||
}
|
||||
|
||||
async assertMainNavVisible() {
|
||||
await expect(this.page.getByRole("link", { name: "工作台" }).or(this.page.getByRole("link", { name: "Portal" })).first()).toBeVisible();
|
||||
await expect(this.page.getByRole("link", { name: "工作台" }).first()).toBeVisible();
|
||||
await expect(this.page.getByRole("link", { name: /项目/ }).first()).toBeVisible();
|
||||
}
|
||||
|
||||
|
||||
@@ -23,11 +23,7 @@ type Envelope<T> = {
|
||||
|
||||
export async function assertPortalLoaded(page: Page) {
|
||||
await expect(page).toHaveURL(/\/portal/);
|
||||
await expect(
|
||||
page.getByRole("link", { name: "工作台", exact: true })
|
||||
.or(page.getByRole("link", { name: "Portal", exact: true }))
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "工作台", exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "今日任务" })).toBeVisible();
|
||||
}
|
||||
|
||||
@@ -37,11 +33,7 @@ export async function assertForbiddenPage(page: Page) {
|
||||
}
|
||||
|
||||
export async function assertMainNavVisible(page: Page) {
|
||||
await expect(
|
||||
page.getByRole("link", { name: "工作台", exact: true })
|
||||
.or(page.getByRole("link", { name: "Portal", exact: true }))
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "工作台", exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "团队" }).first()).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "项目" }).first()).toBeVisible();
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ test("E2E 轻交互 OCR:票据中心缺关键字段后可在当前页修正并
|
||||
|
||||
await login(page, EMPLOYEE);
|
||||
|
||||
await (await openNavLink(page, "票据中心")).click();
|
||||
await (await openNavLink(page, "票据")).click();
|
||||
await expect(page.getByTestId("invoice-pool-table")).toBeVisible();
|
||||
|
||||
await page.getByTestId("invoice-upload-file").setInputFiles({
|
||||
|
||||
@@ -81,7 +81,7 @@ test("E2E 请假编辑:草稿保存后保留原单并可继续提交", async (
|
||||
|
||||
try {
|
||||
await login(page, EMPLOYEE);
|
||||
await (await openNavLink(page, "请假")).click();
|
||||
await (await openNavLink(page, "请假记录")).click();
|
||||
await expect(page.getByRole("heading", { name: "请假申请" })).toBeVisible();
|
||||
|
||||
const leaveId = await createLeaveDraft(page, startDate, startDate, `Playwright 编辑请假 ${Date.now()}`);
|
||||
@@ -125,7 +125,7 @@ test("E2E 请假主链路:草稿 → 提交 → 撤回 + 提交 → 审批", a
|
||||
|
||||
try {
|
||||
await login(page, EMPLOYEE);
|
||||
await (await openNavLink(page, "请假")).click();
|
||||
await (await openNavLink(page, "请假记录")).click();
|
||||
await expect(page.getByRole("heading", { name: "请假申请" })).toBeVisible();
|
||||
|
||||
const firstLeaveId = await createLeaveDraft(page, firstDate, firstDate, `Playwright 撤回请假 ${Date.now()}`);
|
||||
@@ -177,7 +177,7 @@ test("E2E 请假退回说明:审批退回后员工可看到处理意见", asyn
|
||||
|
||||
try {
|
||||
await login(page, EMPLOYEE);
|
||||
await (await openNavLink(page, "请假")).click();
|
||||
await (await openNavLink(page, "请假记录")).click();
|
||||
await expect(page.getByRole("heading", { name: "请假申请" })).toBeVisible();
|
||||
|
||||
const leaveId = await createLeaveDraft(page, leaveDate, leaveDate, `Playwright 退回请假 ${Date.now()}`);
|
||||
|
||||
@@ -89,7 +89,7 @@ test.describe("live smoke", () => {
|
||||
await expect(page.getByRole("heading", { name: "工作区设置", exact: true })).toBeVisible();
|
||||
|
||||
await page.goto("/system/feature-flags");
|
||||
await expect(page.getByRole("heading", { name: "功能开关", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "功能管理", exact: true })).toBeVisible();
|
||||
|
||||
await page.goto("/audit-logs");
|
||||
await expect(page.getByRole("heading", { name: "审计工作台", exact: true })).toBeVisible();
|
||||
|
||||
@@ -181,43 +181,36 @@ function mockNavigation(page: import("@playwright/test").Page, mode: "employee"
|
||||
const sections = mode === "manager"
|
||||
? {
|
||||
main: [
|
||||
{ key: "portal", href: "/portal", label: "Portal", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "team", href: "/team", label: "团队", icon: "groups", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "leave", href: "/leave-requests", label: "请假", icon: "event_available", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "portal", href: "/portal", label: "工作台", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "workflow", href: "/workflow/tasks", label: "待我处理", icon: "fact_check", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "expense", href: "/reimbursements", label: "报销", icon: "request_quote", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "assets", href: "/assets", label: "资产", icon: "inventory_2", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "weekly", href: "/weekly-reports", label: "周报", icon: "article", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "todos", href: "/todos", label: "待办", icon: "check_circle", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "announcements", href: "/announcements", label: "公告", icon: "campaign", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "projects", href: "/projects", label: "项目", icon: "workspaces", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "team", href: "/team", label: "团队", icon: "groups", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "leave", href: "/leave-requests", label: "请假记录", icon: "event_available", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
],
|
||||
admin: [
|
||||
{ key: "workspace", href: "/system/workspace", label: "工作区设置", icon: "tune", placement: "admin", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "people", href: "/people", label: "人员管理", icon: "group", placement: "admin", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "workflow", href: "/system/workflow-definitions", label: "流程定义", icon: "schema", placement: "admin", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "settings", href: "/system/settings", label: "系统设置", icon: "settings", placement: "admin", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "feature-flags", href: "/system/feature-flags", label: "功能开关", icon: "toggle_on", placement: "admin", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "audit", href: "/audit-logs", label: "审计日志", icon: "manage_search", placement: "admin", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "settings", href: "/system/settings", label: "管理", icon: "settings", placement: "admin", permissionCodes: [], featureFlagKey: null },
|
||||
],
|
||||
backup: [
|
||||
{ key: "assets", href: "/assets", label: "资产", icon: "inventory_2", placement: "backup", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "weekly", href: "/weekly-reports", label: "周报", icon: "article", placement: "backup", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "announcements", href: "/announcements", label: "公告", icon: "campaign", placement: "backup", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "purchase", href: "/purchase-requests", label: "采购申请", icon: "shopping_cart", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "travel", href: "/travel-requests", label: "差旅申请", icon: "flight", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
],
|
||||
}
|
||||
: {
|
||||
main: [
|
||||
{ key: "portal", href: "/portal", label: "Portal", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "team", href: "/team", label: "团队", icon: "groups", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "leave", href: "/leave-requests", label: "请假", icon: "event_available", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "portal", href: "/portal", label: "工作台", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "expense", href: "/reimbursements", label: "报销", icon: "request_quote", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "assets", href: "/assets", label: "资产", icon: "inventory_2", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "weekly", href: "/weekly-reports", label: "周报", icon: "article", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "todos", href: "/todos", label: "待办", icon: "check_circle", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "announcements", href: "/announcements", label: "公告", icon: "campaign", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "projects", href: "/projects", label: "项目", icon: "workspaces", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "leave", href: "/leave-requests", label: "请假记录", icon: "event_available", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
],
|
||||
admin: [],
|
||||
backup: [],
|
||||
backup: [
|
||||
{ key: "assets", href: "/assets", label: "资产", icon: "inventory_2", placement: "backup", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "weekly", href: "/weekly-reports", label: "周报", icon: "article", placement: "backup", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "announcements", href: "/announcements", label: "公告", icon: "campaign", placement: "backup", permissionCodes: [], featureFlagKey: null },
|
||||
],
|
||||
};
|
||||
|
||||
await route.fulfill({
|
||||
@@ -266,7 +259,7 @@ function mockPortalHome(page: import("@playwright/test").Page, mode: "employee"
|
||||
workspace: { id: "workspace-1", persisted: true, profile: "small_team_internal" },
|
||||
greeting: { name: "Manager" },
|
||||
cards: [
|
||||
{ key: "pending_approvals", title: "审批待办", value: 3, href: "/workflow/tasks", severity: "warning" },
|
||||
{ key: "pending_approvals", title: "待我处理", value: 3, href: "/workflow/tasks", severity: "warning" },
|
||||
{ key: "team_presence", title: "今日团队状态", value: "6/8 在岗", href: "/team", severity: "warning" },
|
||||
{ key: "weekly_missing", title: "未提交周报", value: 2, href: "/weekly-reports", severity: "warning" },
|
||||
{ key: "weekly_help", title: "需协助员工", value: 1, href: "/weekly-reports", severity: "high" },
|
||||
@@ -281,7 +274,7 @@ function mockPortalHome(page: import("@playwright/test").Page, mode: "employee"
|
||||
{ domain: "leave", count: 0, href: "/leave-requests" },
|
||||
],
|
||||
riskSummary: { openCount: 1, href: "/ap-operations" },
|
||||
announcements: [{ id: "announcement-1", title: "试点公告", summary: "今天统一从 Portal 进入。", href: null }],
|
||||
announcements: [{ id: "announcement-1", title: "试点公告", summary: "今天统一从工作台进入。", href: null }],
|
||||
quickLinks: [{ key: "projects", title: "项目", href: "/projects" }],
|
||||
todayPresence: null,
|
||||
teamPresence: { date: "2026-03-22", href: "/team", office: 6, remote: 1, out: 0, travel: 1, leave: 1, unspecified: 1, total: 8 },
|
||||
|
||||
@@ -28,7 +28,7 @@ test("E2E 主链路:登录→报销→待办审批→票据OCR→支付→登
|
||||
await reimbursementDetail.assertLoaded();
|
||||
|
||||
// 2) employee: upload invoice and wait OCR
|
||||
await (await openNavLink(page, "票据中心")).click();
|
||||
await (await openNavLink(page, "票据")).click();
|
||||
const marker = `MOCK_E2E_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
||||
const invoiceFileName = `e2e-${Date.now()}.pdf`;
|
||||
const uploadResponsePromise = page.waitForResponse((response) => {
|
||||
|
||||
@@ -187,7 +187,7 @@ test("R70 自动辅助试运行:员工、经理、管理员路径巡检", asyn
|
||||
});
|
||||
|
||||
await runStep("employee", "请假提交并生成经理待办", async () => {
|
||||
await (await openNavLink(page, "请假")).click();
|
||||
await (await openNavLink(page, "请假记录")).click();
|
||||
await expect(page.getByRole("heading", { name: "请假申请" })).toBeVisible();
|
||||
const leave = await createAndSubmitLeaveForManager(page);
|
||||
await page.goto("/leave-requests");
|
||||
@@ -204,7 +204,7 @@ test("R70 自动辅助试运行:员工、经理、管理员路径巡检", asyn
|
||||
await runStep("employee", "周报与待办入口", async () => {
|
||||
await (await openNavLink(page, "周报")).click();
|
||||
await expect(page.getByRole("heading", { name: "周报", exact: true })).toBeVisible();
|
||||
await (await openNavLink(page, "待办")).click();
|
||||
await page.goto("/todos");
|
||||
await expect(page.getByRole("heading", { name: "待办", exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -245,18 +245,20 @@ test("R70 自动辅助试运行:员工、经理、管理员路径巡检", asyn
|
||||
await runStep("admin", "管理入口与人员组织", async () => {
|
||||
await logout(page, MANAGER.name);
|
||||
await login(page, ADMIN);
|
||||
await (await openNavLink(page, "人员管理")).click();
|
||||
await (await openNavLink(page, /^管理$/)).click();
|
||||
await expect(page.getByRole("heading", { name: "管理", exact: true })).toBeVisible();
|
||||
await page.getByRole("link", { name: /人员/ }).first().click();
|
||||
await expect(page.getByRole("heading", { name: "人员管理" })).toBeVisible();
|
||||
await (await openNavLink(page, "组织架构")).click();
|
||||
await page.goto("/organization");
|
||||
await expect(page.getByRole("heading", { name: "组织架构" })).toBeVisible();
|
||||
});
|
||||
|
||||
await runStep("admin", "系统设置、功能开关、审计日志", async () => {
|
||||
await (await openNavLink(page, "系统设置")).click();
|
||||
await expect(page.getByRole("heading", { name: "系统设置" })).toBeVisible();
|
||||
await (await openNavLink(page, "功能开关")).click();
|
||||
await expect(page.getByRole("heading", { name: "功能开关" })).toBeVisible();
|
||||
await (await openNavLink(page, "审计日志")).click();
|
||||
await runStep("admin", "管理、功能管理、审计日志", async () => {
|
||||
await (await openNavLink(page, /^管理$/)).click();
|
||||
await expect(page.getByRole("heading", { name: "管理", exact: true })).toBeVisible();
|
||||
await page.getByRole("link", { name: /功能管理/ }).first().click();
|
||||
await expect(page.getByRole("heading", { name: "功能管理" })).toBeVisible();
|
||||
await page.goto("/audit-logs");
|
||||
await expect(page.getByRole("heading", { name: "审计工作台" })).toBeVisible();
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -173,7 +173,7 @@ test("R70 深层自动辅助试运行:周报待办、公告团队状态、真
|
||||
await submitResponse;
|
||||
await expect(page.getByText("周报已提交")).toBeVisible();
|
||||
|
||||
await (await openNavLink(page, "待办")).click();
|
||||
await page.goto("/todos");
|
||||
const todoRow = page.getByTestId(TEST_IDS.todo.row).filter({ hasText: uniqueToken }).first();
|
||||
await expect(todoRow).toBeVisible({ timeout: 30_000 });
|
||||
await todoRow.getByTestId(TEST_IDS.todo.complete).click();
|
||||
|
||||
@@ -778,7 +778,7 @@ test(`${trialrunIssueId} 浏览器业务先行试运行:报销、审批、请
|
||||
const startDate = futureWeekday(uniqueFutureOffset(5200));
|
||||
const employee = await loginRole(page, "employee");
|
||||
try {
|
||||
await (await openNavLink(page, "请假")).click();
|
||||
await (await openNavLink(page, "请假记录")).click();
|
||||
await expect(page.getByRole("heading", { name: "请假申请" })).toBeVisible();
|
||||
|
||||
const leaveId = await createLeaveDraft(page, startDate, `${trialrunMarkerPrefix} 审批边界 ${marker}`);
|
||||
@@ -952,7 +952,7 @@ test(`${trialrunIssueId} 浏览器业务先行试运行:报销、审批、请
|
||||
const startDate = futureWeekday(uniqueFutureOffset(3200));
|
||||
const employee = await loginRole(page, "employee");
|
||||
try {
|
||||
await (await openNavLink(page, "请假")).click();
|
||||
await (await openNavLink(page, "请假记录")).click();
|
||||
await expect(page.getByRole("heading", { name: "请假申请" })).toBeVisible();
|
||||
|
||||
const leaveId = await createLeaveDraft(page, startDate, `${trialrunMarkerPrefix} 浏览器请假 ${marker}`);
|
||||
|
||||
@@ -178,7 +178,7 @@ async function mockWave2Apis(page: Page) {
|
||||
sections: {
|
||||
main: [
|
||||
navigationItem("portal", "/portal", "工作台", "home_app_logo"),
|
||||
navigationItem("workflow", "/workflow/tasks", "审批待办", "fact_check"),
|
||||
navigationItem("workflow", "/workflow/tasks", "待我处理", "fact_check"),
|
||||
navigationItem("expense", "/reimbursements", "报销", "request_quote"),
|
||||
],
|
||||
admin: [],
|
||||
@@ -206,7 +206,7 @@ async function mockWave2Apis(page: Page) {
|
||||
workspace: { id: "workspace-wave2", persisted: true, profile: "small_team_internal" },
|
||||
greeting: { name: visualMe.name },
|
||||
cards: [
|
||||
{ key: "pending_approvals", title: "审批待办", value: 3, href: "/workflow/tasks", severity: "warning" },
|
||||
{ key: "pending_approvals", title: "待我处理", value: 3, href: "/workflow/tasks", severity: "warning" },
|
||||
{ key: "team_presence", title: "团队状态", value: "7/9 在岗", href: "/team", severity: "normal" },
|
||||
{ key: "weekly_missing", title: "周报缺口", value: 1, href: "/weekly-reports", severity: "warning" },
|
||||
{ key: "weekly_help", title: "需协助", value: 1, href: "/weekly-reports", severity: "normal" },
|
||||
|
||||
@@ -583,7 +583,7 @@ async function mockWave3Apis(page: Page) {
|
||||
sections: {
|
||||
main: [
|
||||
navigationItem("portal", "/portal", "工作台", "home_app_logo"),
|
||||
navigationItem("workflow", "/workflow/tasks", "审批待办", "fact_check"),
|
||||
navigationItem("workflow", "/workflow/tasks", "待我处理", "fact_check"),
|
||||
navigationItem("expense", "/reimbursements", "报销", "request_quote"),
|
||||
navigationItem("notifications", "/notifications", "通知", "notifications"),
|
||||
],
|
||||
@@ -1120,7 +1120,7 @@ test("Wave3 移动关键页视觉截图", async ({ page }) => {
|
||||
await page.screenshot({ path: screenshotPath("06-mobile-reimbursement-detail.png"), fullPage: true });
|
||||
|
||||
await page.goto("/mobile/approvals");
|
||||
await expect(page.getByRole("heading", { name: "审批", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "待办", exact: true })).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-approvals-focus")).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-approvals-list")).toBeVisible();
|
||||
await expect(page.locator("table")).toHaveCount(0);
|
||||
|
||||
@@ -515,22 +515,12 @@ async function mockWave4Apis(page: Page, options: Wave4MockOptions = {}) {
|
||||
sections: {
|
||||
main: [
|
||||
navigationItem("portal", "/portal", "工作台", "home_app_logo"),
|
||||
navigationItem("payment-batches", "/payment-batches", "付款批次", "payments"),
|
||||
navigationItem("ap-operations", "/ap-operations", "AP 运营", "account_balance_wallet"),
|
||||
navigationItem("invoice-pool", "/invoice-pool", "票据中心", "receipt_long"),
|
||||
navigationItem("payment-batches", "/payment-batches", "付款", "payments"),
|
||||
navigationItem("ap-operations", "/ap-operations", "应付账款", "account_balance_wallet"),
|
||||
navigationItem("invoice-pool", "/invoice-pool", "票据", "receipt_long"),
|
||||
],
|
||||
admin: [
|
||||
navigationItem("people", "/people", "人员管理", "badge", "admin"),
|
||||
navigationItem("organization", "/organization", "组织架构", "account_tree", "admin"),
|
||||
navigationItem("workspace", "/system/workspace", "工作区设置", "dashboard_customize", "admin"),
|
||||
navigationItem("users", "/system/users", "用户管理", "group", "admin"),
|
||||
navigationItem("master-data", "/system/master-data", "主数据", "database", "admin"),
|
||||
navigationItem("notification-channels", "/system/notification-channels", "通知渠道", "notifications", "admin"),
|
||||
navigationItem("approval-rules", "/system/approval-rules", "审批规则", "account_tree", "admin"),
|
||||
navigationItem("settings", "/system/settings", "系统设置", "settings", "admin"),
|
||||
navigationItem("feature-flags", "/system/feature-flags", "功能开关", "toggle_on", "admin"),
|
||||
navigationItem("workflow-definitions", "/system/workflow-definitions", "流程定义", "schema", "admin"),
|
||||
navigationItem("audit-logs", "/audit-logs", "审计日志", "manage_search", "admin"),
|
||||
navigationItem("settings", "/system/settings", "管理", "settings", "admin"),
|
||||
],
|
||||
backup: [],
|
||||
},
|
||||
@@ -1020,7 +1010,7 @@ const pages = [
|
||||
{ path: "/system/users", summary: "users-summary", proof: "data-testid=users-table", screenshot: "08-users" },
|
||||
{ path: "/system/master-data", summary: "master-data-summary", proof: "text=法体列表", screenshot: "09-master-data" },
|
||||
{ path: "/system/notification-channels", summary: "notification-channels-summary", proof: "data-testid=notification-channels-table", screenshot: "10-notification-channels" },
|
||||
{ path: "/system/settings", summary: "system-settings-summary", proof: "text=系统设置", screenshot: "11-system-settings" },
|
||||
{ path: "/system/settings", summary: "system-settings-summary", proof: "text=管理", screenshot: "11-system-settings" },
|
||||
{ path: "/system/feature-flags", summary: "feature-flags-summary", proof: "data-testid=feature-flags-table", screenshot: "12-feature-flags" },
|
||||
{ path: "/system/workflow-definitions", summary: "workflow-definitions-summary", proof: "text=版本列表", screenshot: "13-workflow-definitions" },
|
||||
{ path: "/audit-logs", summary: "audit-summary", proof: "text=PAYMENT_BATCH_REVIEW", screenshot: "14-audit-logs" },
|
||||
|
||||
@@ -244,7 +244,7 @@ async function expectMobileApprovalEntry(browser: Browser, user: E2EUser, canApp
|
||||
const approvalTab = page.getByTestId("mobile-nav-approvals");
|
||||
if (canApprove) {
|
||||
await expect(approvalTab).toHaveAttribute("href", "/mobile/approvals");
|
||||
await expect(page.getByTestId("mobile-nav-approvals-label")).toHaveText("审批");
|
||||
await expect(page.getByTestId("mobile-nav-approvals-label")).toHaveText("待办");
|
||||
await expect(page.getByTestId("mobile-action-grid").locator('a[href="/mobile/approvals"]')).toHaveCount(0);
|
||||
await page.goto("/mobile/approvals");
|
||||
await expect(page).not.toHaveURL(/\/403/);
|
||||
|
||||
@@ -97,7 +97,7 @@ async function mockDesignApis(page: Page) {
|
||||
main: [
|
||||
navigationItem("portal", "/portal", "工作台", "home_app_logo"),
|
||||
navigationItem("expense", "/reimbursements", "报销", "request_quote"),
|
||||
navigationItem("leave", "/leave-requests", "请假", "event_available"),
|
||||
navigationItem("leave", "/leave-requests", "请假记录", "event_available"),
|
||||
navigationItem("invoice", "/invoice-pool", "票据", "receipt_long"),
|
||||
],
|
||||
admin: [],
|
||||
|
||||
@@ -165,7 +165,7 @@ async function mockMobileApis(page: Page) {
|
||||
main: [
|
||||
navigationItem("portal", "/portal", "工作台", "home_app_logo"),
|
||||
navigationItem("expense", "/reimbursements", "报销", "request_quote"),
|
||||
navigationItem("leave", "/leave-requests", "请假", "event_available"),
|
||||
navigationItem("leave", "/leave-requests", "请假记录", "event_available"),
|
||||
navigationItem("invoice", "/invoice-pool", "票据", "receipt_long"),
|
||||
],
|
||||
admin: [],
|
||||
|
||||
@@ -174,7 +174,7 @@ async function mockApis(page: Page) {
|
||||
main: [
|
||||
navigationItem("portal", "/portal", "工作台", "home_app_logo"),
|
||||
navigationItem("expense", "/reimbursements", "报销", "request_quote"),
|
||||
navigationItem("leave", "/leave-requests", "请假", "event_available"),
|
||||
navigationItem("leave", "/leave-requests", "请假记录", "event_available"),
|
||||
],
|
||||
admin: [],
|
||||
backup: [navigationItem("invoice", "/invoice-pool", "票据", "receipt_long")],
|
||||
|
||||
@@ -224,11 +224,11 @@ async function mockApis(page: Page) {
|
||||
main: [
|
||||
navigationItem("portal", "/portal", "工作台", "home_app_logo"),
|
||||
navigationItem("purchase", "/purchase-requests", "采购申请", "shopping_cart"),
|
||||
navigationItem("invoice", "/invoice-pool", "票据中心", "receipt_long"),
|
||||
navigationItem("invoice", "/invoice-pool", "票据", "receipt_long"),
|
||||
],
|
||||
admin: [],
|
||||
backup: [
|
||||
navigationItem("payment", "/payment-batches", "付款批次", "payments"),
|
||||
navigationItem("payment", "/payment-batches", "付款", "payments"),
|
||||
navigationItem("dashboard", "/dashboard", "数据看板", "monitoring"),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -524,7 +524,7 @@ test.describe("R124 企业微信移动入口矩阵", () => {
|
||||
|
||||
await mobile.page.getByTestId("mobile-nav-approvals").click();
|
||||
await expect(mobile.page).toHaveURL(/\/mobile\/approvals$/);
|
||||
await expect(mobile.page.getByText("审批").first()).toBeVisible();
|
||||
await expect(mobile.page.getByRole("heading", { name: "待办", exact: true })).toBeVisible();
|
||||
await expect(mobile.page.locator(".mobile-status-slot--approval").first()).toContainText("待处理");
|
||||
await expectMobilePageContract(mobile.page);
|
||||
|
||||
|
||||
@@ -538,7 +538,7 @@ test.describe("R6 移动响应式体系", () => {
|
||||
await expect(page.getByTestId("mobile-action-grid")).toBeVisible();
|
||||
const homeMetrics = await expectMobilePageStable(page, device.label, `${device.key}-mobile-home`);
|
||||
expect(homeMetrics.actionItems.length, `${device.label} home action count`).toBeGreaterThanOrEqual(2);
|
||||
const bottomTabTexts = new Set(["首页", "审批", "进度", "报销", "我的"]);
|
||||
const bottomTabTexts = new Set(["首页", "待办", "进度", "报销", "我的"]);
|
||||
for (const action of homeMetrics.actionItems) {
|
||||
expect(bottomTabTexts.has(action.text), `${device.label} ${action.text} not duplicate bottom tab`).toBe(false);
|
||||
expect(action.width, `${device.label} ${action.text} action width`).toBeGreaterThanOrEqual(44);
|
||||
|
||||
@@ -527,7 +527,7 @@ async function exerciseMobileRendering(page: Page, key: string, label: string) {
|
||||
await expect(page.getByTestId("mobile-action-grid")).toBeVisible();
|
||||
const homeMetrics = await expectRenderingStable(page, label, `${key}-mobile-home`);
|
||||
expect(homeMetrics.actionItems.length, `${label} action count`).toBeGreaterThanOrEqual(2);
|
||||
const bottomTabLabels = new Set(["首页", "审批", "进度", "报销", "我的"]);
|
||||
const bottomTabLabels = new Set(["首页", "待办", "进度", "报销", "我的"]);
|
||||
for (const action of homeMetrics.actionItems) {
|
||||
expect(action.visible, `${label} ${action.key} action visible`).toBe(true);
|
||||
expect(bottomTabLabels.has(action.key), `${label} ${action.key} not bottom tab key`).toBe(false);
|
||||
|
||||
@@ -387,7 +387,7 @@ test.describe("R75 自动化补盲", () => {
|
||||
|
||||
await login(page, ADMIN);
|
||||
await page.goto("/system/feature-flags");
|
||||
await expect(page.getByRole("heading", { name: "功能开关" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "功能管理" })).toBeVisible();
|
||||
|
||||
const row = page.getByTestId(`feature-flag-row-${flagKey}`);
|
||||
await expect(row).toBeVisible();
|
||||
|
||||
@@ -210,12 +210,12 @@ async function mockR89Apis(page: Page) {
|
||||
},
|
||||
sections: {
|
||||
main: [
|
||||
navigationItem("portal", "/portal", "Portal", "home_app_logo"),
|
||||
navigationItem("portal", "/portal", "工作台", "home_app_logo"),
|
||||
navigationItem("team", "/team", "团队", "groups"),
|
||||
navigationItem("leave", "/leave-requests", "请假", "event_available"),
|
||||
navigationItem("leave", "/leave-requests", "请假记录", "event_available"),
|
||||
navigationItem("expense", "/reimbursements", "报销", "request_quote"),
|
||||
navigationItem("workflow", "/workflow/tasks", "审批待办", "fact_check"),
|
||||
navigationItem("invoice", "/invoice-pool", "票据中心", "receipt_long"),
|
||||
navigationItem("workflow", "/workflow/tasks", "待我处理", "fact_check"),
|
||||
navigationItem("invoice", "/invoice-pool", "票据", "receipt_long"),
|
||||
],
|
||||
admin: [],
|
||||
backup: [navigationItem("settings", "/settings/profile", "个人设置", "manage_accounts")],
|
||||
|
||||
@@ -96,10 +96,10 @@ async function mockAuthenticatedShell(page: Page) {
|
||||
},
|
||||
sections: {
|
||||
main: [
|
||||
{ key: "portal", href: "/portal", label: "Portal", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "portal", href: "/portal", label: "工作台", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "mobile", href: "/mobile", label: "移动工作台", icon: "phone_iphone", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "expense", href: "/reimbursements", label: "我的报销", icon: "request_quote", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "invoice", href: "/invoice-pool", label: "票据中心", icon: "receipt_long", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
{ key: "invoice", href: "/invoice-pool", label: "票据", icon: "receipt_long", placement: "main", permissionCodes: [], featureFlagKey: null },
|
||||
],
|
||||
admin: [],
|
||||
backup: [],
|
||||
|
||||
@@ -2,25 +2,25 @@ import { test, expect } from "./helpers/fixtures";
|
||||
import { ADMIN, login } from "./helpers/auth";
|
||||
import { TEST_IDS } from "./helpers/testIds";
|
||||
|
||||
test("E2E 管理域 smoke:工作区设置、系统设置、功能开关、流程定义、审计日志", async ({ page, shell }) => {
|
||||
test("E2E 管理域 smoke:工作区设置、管理、功能管理、流程定义、审计日志", async ({ page, shell }) => {
|
||||
await login(page, ADMIN);
|
||||
await page.goto("/portal");
|
||||
await expect(shell.topbarProfileTrigger).toBeVisible();
|
||||
await shell.assertBackupModulesCollapsed();
|
||||
|
||||
await (await shell.navigate("工作区设置")).click();
|
||||
await (await shell.navigate(/^管理$/)).click();
|
||||
await expect(page).toHaveURL(/\/system\/settings/);
|
||||
await expect(page.getByRole("heading", { name: "管理", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: /工作区/ }).first()).toBeVisible();
|
||||
|
||||
await page.goto("/system/workspace");
|
||||
await expect(page).toHaveURL(/\/system\/workspace/);
|
||||
await expect(page.getByRole("heading", { name: "工作区设置" })).toBeVisible();
|
||||
await expect(page.getByText("产品模式")).toBeVisible();
|
||||
await expect(page.getByText("工作区基线")).toBeVisible();
|
||||
|
||||
await (await shell.navigate("系统设置")).click();
|
||||
await expect(page).toHaveURL(/\/system\/settings/);
|
||||
await expect(page.getByRole("heading", { name: "系统设置" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "工作区设置", exact: true }).first()).toBeVisible();
|
||||
|
||||
await (await shell.navigate("功能开关")).click();
|
||||
await page.goto("/system/feature-flags");
|
||||
await expect(page).toHaveURL(/\/system\/feature-flags/);
|
||||
await expect(page.getByRole("heading", { name: "功能开关" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "功能管理" })).toBeVisible();
|
||||
await expect(page.getByTestId("feature-flags-table")).toBeVisible();
|
||||
|
||||
await page.goto("/system/workflow-definitions");
|
||||
@@ -28,7 +28,7 @@ test("E2E 管理域 smoke:工作区设置、系统设置、功能开关、流
|
||||
await expect(page.getByRole("heading", { name: "流程定义" })).toBeVisible();
|
||||
await expect(page.getByText("版本列表")).toBeVisible();
|
||||
|
||||
await (await shell.navigate("审计日志")).click();
|
||||
await page.goto("/audit-logs");
|
||||
await expect(page).toHaveURL(/\/audit-logs/);
|
||||
await expect(page.getByRole("heading", { name: "审计工作台" })).toBeVisible();
|
||||
await expect(page.getByTestId(TEST_IDS.audit.export)).toBeVisible();
|
||||
|
||||
@@ -209,7 +209,7 @@ async function mockSystemActionApis(page: Page) {
|
||||
items: [
|
||||
navigationItem("portal", "/portal", "首页", "home"),
|
||||
navigationItem("reimbursements", "/reimbursements", "报销", "receipt"),
|
||||
navigationItem("payment-batches", "/payment-batches", "付款批次", "credit_card"),
|
||||
navigationItem("payment-batches", "/payment-batches", "付款", "credit_card"),
|
||||
],
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -107,8 +107,8 @@ function mockCommonAuth(page: import("@playwright/test").Page, options: {
|
||||
updatedBy: null,
|
||||
},
|
||||
sections: {
|
||||
main: [{ key: "portal", href: "/portal", label: "Portal", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null }],
|
||||
admin: [{ key: "users", href: "/system/users", label: "系统用户", icon: "manage_accounts", placement: "admin", permissionCodes: ["system.users"], featureFlagKey: null }],
|
||||
main: [{ key: "portal", href: "/portal", label: "工作台", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null }],
|
||||
admin: [{ key: "settings", href: "/system/settings", label: "管理", icon: "settings", placement: "admin", permissionCodes: ["system.users"], featureFlagKey: null }],
|
||||
backup: [],
|
||||
},
|
||||
recommendedRolePresets: [],
|
||||
|
||||
@@ -116,13 +116,12 @@ async function mockVisualApis(page: Page) {
|
||||
},
|
||||
sections: {
|
||||
main: [
|
||||
navigationItem("portal", "/portal", "Portal", "home_app_logo"),
|
||||
navigationItem("portal", "/portal", "工作台", "home_app_logo"),
|
||||
navigationItem("reimbursements", "/reimbursements", "报销", "request_quote"),
|
||||
navigationItem("approvals", "/workflow/tasks", "审批待办", "fact_check"),
|
||||
navigationItem("invoice", "/invoice-pool", "票据中心", "receipt_long"),
|
||||
navigationItem("approvals", "/workflow/tasks", "待我处理", "fact_check"),
|
||||
],
|
||||
admin: [],
|
||||
backup: [navigationItem("settings", "/settings/profile", "个人设置", "manage_accounts")],
|
||||
backup: [navigationItem("invoice", "/invoice-pool", "票据", "receipt_long")],
|
||||
},
|
||||
recommendedRolePresets: [],
|
||||
});
|
||||
@@ -323,7 +322,7 @@ test.describe("visual shell", () => {
|
||||
|
||||
await expect(page.locator("aside")).toBeVisible();
|
||||
await expect(page.getByTestId("topbar-profile-trigger")).toBeVisible();
|
||||
await expect(page.locator("aside").getByRole("link", { name: "Portal", exact: true })).toBeVisible();
|
||||
await expect(page.locator("aside").getByRole("link", { name: "工作台", exact: true })).toBeVisible();
|
||||
await expect(page.locator("summary").filter({ hasText: "更多" })).toBeVisible();
|
||||
|
||||
await attachScreenshot(page, testInfo, "app-shell.png");
|
||||
@@ -364,7 +363,7 @@ test.describe("visual shell", () => {
|
||||
await attachScreenshot(page, testInfo, "mobile-today.png");
|
||||
});
|
||||
|
||||
test("审批待办手机卡片布局基线", async ({ page }, testInfo) => {
|
||||
test("待我处理手机卡片布局基线", async ({ page }, testInfo) => {
|
||||
await mockVisualApis(page);
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await loginVisualUser(page);
|
||||
@@ -372,7 +371,7 @@ test.describe("visual shell", () => {
|
||||
|
||||
await expect(page.getByTestId("workflow-tasks-mobile-list")).toBeVisible();
|
||||
await expect(page.getByTestId("workflow-tasks-table")).toBeHidden();
|
||||
await expect(page.getByRole("link", { name: "详情" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "查看" })).toBeVisible();
|
||||
|
||||
await attachScreenshot(page, testInfo, "r97-workflow-tasks-mobile.png");
|
||||
});
|
||||
|
||||
@@ -123,7 +123,7 @@ test("E2E 企微首次绑定:未绑定回退到密码登录后可继续进入
|
||||
updatedBy: null,
|
||||
},
|
||||
sections: {
|
||||
main: [{ key: "portal", href: "/portal", label: "Portal", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null }],
|
||||
main: [{ key: "portal", href: "/portal", label: "工作台", icon: "home_app_logo", placement: "main", permissionCodes: [], featureFlagKey: null }],
|
||||
admin: [],
|
||||
backup: [],
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { test, expect } from "./helpers/fixtures";
|
||||
|
||||
import { EMPLOYEE, login, logout } from "./helpers/auth";
|
||||
import { openNavLink } from "./helpers/navigation";
|
||||
import { TEST_IDS } from "./helpers/testIds";
|
||||
|
||||
function formatWeekStart(offsetDays: number) {
|
||||
@@ -53,7 +52,7 @@ test("E2E 周报待办联动:保存 → 生成待办 → 提交 → 完成待
|
||||
await expect(page.getByText("周报已提交")).toBeVisible();
|
||||
await expect(page.getByText("已提交", { exact: true })).toBeVisible();
|
||||
|
||||
await (await openNavLink(page, "待办")).click();
|
||||
await page.goto("/todos");
|
||||
const todoRow = page.getByTestId(TEST_IDS.todo.row).filter({ hasText: uniqueToken }).first();
|
||||
await expect(todoRow).toBeVisible({ timeout: 30_000 });
|
||||
await todoRow.getByTestId(TEST_IDS.todo.complete).click();
|
||||
|
||||
@@ -187,8 +187,8 @@ async function mockWorkflowConfirmApis(page: Page) {
|
||||
},
|
||||
sections: {
|
||||
main: [
|
||||
navigationItem("portal", "/portal", "Portal", "home_app_logo"),
|
||||
navigationItem("workflow", "/workflow/tasks", "审批待办", "fact_check"),
|
||||
navigationItem("portal", "/portal", "工作台", "home_app_logo"),
|
||||
navigationItem("workflow", "/workflow/tasks", "待我处理", "fact_check"),
|
||||
],
|
||||
admin: [],
|
||||
backup: [],
|
||||
|
||||
@@ -73,7 +73,7 @@ test("E2E 审批列表快捷通过:经理可在待办列表行内直接通过
|
||||
|
||||
try {
|
||||
await login(page, EMPLOYEE);
|
||||
await (await openNavLink(page, "请假")).click();
|
||||
await (await openNavLink(page, "请假记录")).click();
|
||||
await expect(page.getByRole("heading", { name: "请假申请" })).toBeVisible();
|
||||
|
||||
const leaveRequestId = await createLeaveDraft(page, startDate, startDate, reason);
|
||||
|
||||
在新工单中引用
屏蔽一个用户