完成 Wave4-5 财务与管理页刷新(05-29-frontend-ux-wave4-5-finance-admin-refresh)
这个提交包含在:
@@ -61,6 +61,22 @@ export function ApprovalRulesPage() {
|
||||
});
|
||||
|
||||
const rules = listQuery.data?.items ?? [];
|
||||
const ruleStats = useMemo(() => {
|
||||
const enabledRules = rules.filter((rule) => rule.enabled);
|
||||
const disabledRules = rules.length - enabledRules.length;
|
||||
const fallbackRules = rules.filter((rule) => rule.ownerSelfFallbackRole).length;
|
||||
const topPriority = enabledRules.length
|
||||
? Math.min(...enabledRules.map((rule) => rule.priority))
|
||||
: null;
|
||||
|
||||
return {
|
||||
total: rules.length,
|
||||
enabled: enabledRules.length,
|
||||
disabled: disabledRules,
|
||||
fallback: fallbackRules,
|
||||
topPriority,
|
||||
};
|
||||
}, [rules]);
|
||||
|
||||
const createPayload = useMemo(() => {
|
||||
return {
|
||||
@@ -169,8 +185,41 @@ export function ApprovalRulesPage() {
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<h3 className="text-base font-semibold text-zinc-900">新建审批规则</h3>
|
||||
<div className="finance-status-strip" data-testid="approval-rules-summary">
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">规则总数</div>
|
||||
<div className="finance-metric-card__value">{ruleStats.total}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">启用规则</div>
|
||||
<div className="finance-metric-card__value">{ruleStats.enabled}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">停用规则</div>
|
||||
<div className="finance-metric-card__value">{ruleStats.disabled}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">最高优先级</div>
|
||||
<div className="finance-metric-card__value">{ruleStats.topPriority ?? "-"}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">本人兜底</div>
|
||||
<div className="finance-metric-card__value">{ruleStats.fallback}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="admin-command-panel space-y-4" data-testid="approval-rules-create-panel">
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-zinc-900">新建审批规则</h2>
|
||||
<p className="mt-1 text-sm leading-6 text-zinc-600">
|
||||
规则按优先级匹配,只影响新提交单据;历史审批实例不回写。
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs font-semibold text-slate-600">
|
||||
金额段 + 角色链
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="space-y-1">
|
||||
@@ -311,10 +360,20 @@ export function ApprovalRulesPage() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<h3 className="text-base font-semibold text-zinc-900">规则列表</h3>
|
||||
<section className="admin-command-panel space-y-4">
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-zinc-900">规则列表</h2>
|
||||
<p className="mt-1 text-sm leading-6 text-zinc-600">
|
||||
优先级越小越先匹配;启停动作会经过确认。
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs text-slate-500">
|
||||
当前 {ruleStats.enabled} 条启用
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{listQuery.isLoading ? <TableSkeleton columns={6} rows={5} /> : null}
|
||||
|
||||
@@ -403,7 +462,7 @@ export function ApprovalRulesPage() {
|
||||
</Table>
|
||||
</PrimaryTable>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ConfirmDialog
|
||||
open={confirmToggleRule !== null}
|
||||
|
||||
@@ -7,19 +7,26 @@ import {
|
||||
type OrganizationTreeNode,
|
||||
} from "../api/employees";
|
||||
import { EmployeeAvatar } from "../components/common/EmployeeAvatar";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { Button } from "../components/ui/button";
|
||||
|
||||
function countNodes(nodes: OrganizationTreeNode[]): number {
|
||||
return nodes.reduce((total, node) => total + 1 + countNodes(node.children), 0);
|
||||
}
|
||||
|
||||
function countExternalManagers(nodes: OrganizationTreeNode[]): number {
|
||||
return nodes.reduce(
|
||||
(total, node) => total + (node.externalManager ? 1 : 0) + countExternalManagers(node.children),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
function OrganizationNodeCard({ node, depth = 0 }: { node: OrganizationTreeNode; depth?: number }) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div
|
||||
className="rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm"
|
||||
style={{ marginLeft: `${depth * 24}px` }}
|
||||
className="finance-list-card"
|
||||
style={{ marginLeft: `${Math.min(depth, 4) * 18}px` }}
|
||||
>
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -78,20 +85,45 @@ export function OrganizationPage() {
|
||||
}, [organizationQuery.data?.items]);
|
||||
|
||||
if (organizationQuery.isLoading) {
|
||||
return <div className="card-std p-6 text-sm text-zinc-500">加载组织架构中...</div>;
|
||||
return (
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
eyebrow="管理"
|
||||
title="组织架构"
|
||||
subtitle="按部门查看当前 active 员工的汇报关系。"
|
||||
/>
|
||||
<div className="admin-command-panel text-sm text-zinc-500">加载组织架构中...</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
if (organizationQuery.isError) {
|
||||
return <div className="card-std p-6 text-sm text-red-600">加载组织架构失败</div>;
|
||||
return (
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
eyebrow="管理"
|
||||
title="组织架构"
|
||||
subtitle="按部门查看当前 active 员工的汇报关系。"
|
||||
/>
|
||||
<div className="admin-command-panel text-sm text-red-600">加载组织架构失败</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
const sections = organizationQuery.data?.items ?? [];
|
||||
const totalEmployees = sections.reduce((total, section) => total + countNodes(section.roots), 0);
|
||||
const externalManagerCount = sections.reduce((total, section) => total + countExternalManagers(section.roots), 0);
|
||||
const expandedCount = sections.filter((section) => {
|
||||
const key = section.department?.id ?? section.department?.name ?? "unknown";
|
||||
return expandedDepartments[key] ?? true;
|
||||
}).length;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
eyebrow="管理"
|
||||
title="组织架构"
|
||||
subtitle="按部门查看当前 active 员工的汇报关系,跨部门上级会在节点右侧单独标注。"
|
||||
subtitle="按部门查看 active 员工的汇报关系,跨部门上级单独标注,便于组织复核。"
|
||||
actions={
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
@@ -124,6 +156,29 @@ export function OrganizationPage() {
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="finance-status-strip" data-testid="organization-summary">
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">部门分组</div>
|
||||
<div className="finance-metric-card__value">{sections.length}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">展示员工</div>
|
||||
<div className="finance-metric-card__value">{totalEmployees}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">已展开</div>
|
||||
<div className="finance-metric-card__value">{expandedCount}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">跨部门汇报</div>
|
||||
<div className="finance-metric-card__value">{externalManagerCount}</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>
|
||||
|
||||
{sections.length ? (
|
||||
<div className="space-y-6">
|
||||
{sections.map((section: OrganizationDepartmentSection) => {
|
||||
@@ -132,11 +187,11 @@ export function OrganizationPage() {
|
||||
const totalNodes = countNodes(section.roots);
|
||||
|
||||
return (
|
||||
<section key={sectionKey} className="card-std p-6">
|
||||
<section key={sectionKey} className="admin-command-panel">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-zinc-900">{section.department?.name ?? "未分配部门"}</h3>
|
||||
<div className="mt-1 text-sm text-zinc-500">{totalNodes} 名员工</div>
|
||||
<h2 className="text-base font-semibold text-zinc-900">{section.department?.name ?? "未分配部门"}</h2>
|
||||
<div className="mt-1 text-sm text-zinc-500">{totalNodes} 名员工 · {expanded ? "已展开" : "已折叠"}</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -166,9 +221,9 @@ export function OrganizationPage() {
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="card-std p-6 text-sm text-zinc-500">当前没有可展示的 active 员工。</div>
|
||||
<div className="admin-command-panel text-sm text-zinc-500">当前没有可展示的 active 员工。</div>
|
||||
)}
|
||||
</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+169
-119
@@ -29,7 +29,7 @@ import { hasPermission } from "../auth/permissions";
|
||||
import { EmployeeAvatar } from "../components/common/EmployeeAvatar";
|
||||
import { ConfirmDialog } from "../components/common/ConfirmDialog";
|
||||
import { DateTextInput } from "../components/common/DateTextInput";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { Button } from "../components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -397,6 +397,13 @@ export function PeoplePage() {
|
||||
const wecomHrMasterError = wecomHrMasterRunsQuery.isError
|
||||
? mapApiMessage(wecomHrMasterRunsQuery.error, "企微 HR 主数据同步未就绪")
|
||||
: "";
|
||||
const peopleStats = {
|
||||
total: employeesQuery.data?.total ?? items.length,
|
||||
active: items.filter((item) => item.status === "active").length,
|
||||
archived: items.filter((item) => item.status === "archived").length,
|
||||
departments: new Set(items.map((item) => item.department?.id).filter(Boolean)).size,
|
||||
wecomAttention: items.filter((item) => shouldShowWecomRetry(item.wecomDirectory)).length,
|
||||
};
|
||||
|
||||
function toggleSelection(employeeId: string, checked: boolean) {
|
||||
setSelectedEmployeeIds((current) => {
|
||||
@@ -415,10 +422,11 @@ export function PeoplePage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
eyebrow="管理"
|
||||
title="人员管理"
|
||||
subtitle="按当前员工档案与账号状态做筛选、批量处理和统计查看。"
|
||||
subtitle="按员工档案、账号状态和企业微信镜像做筛选、批量处理和审计留痕。"
|
||||
actions={
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{canManageUsers ? (
|
||||
@@ -462,8 +470,31 @@ export function PeoplePage() {
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="finance-status-strip" data-testid="people-summary">
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">员工总数</div>
|
||||
<div className="finance-metric-card__value">{peopleStats.total}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">当前页生效</div>
|
||||
<div className="finance-metric-card__value">{peopleStats.active}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">当前页归档</div>
|
||||
<div className="finance-metric-card__value">{peopleStats.archived}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">涉及部门</div>
|
||||
<div className="finance-metric-card__value">{peopleStats.departments}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">企微待处理</div>
|
||||
<div className="finance-metric-card__value">{peopleStats.wecomAttention}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{canManageUsers ? (
|
||||
<div className="card-std p-4">
|
||||
<section className="admin-command-panel" data-testid="people-wecom-command-panel">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -507,10 +538,21 @@ export function PeoplePage() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<div className="card-std space-y-4 p-4">
|
||||
<section className="admin-command-panel space-y-4" data-testid="people-filter-panel">
|
||||
<div className="flex flex-col gap-1 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-zinc-900">筛选与批量范围</h2>
|
||||
<p className="mt-1 text-sm leading-6 text-zinc-600">
|
||||
先收窄部门、状态、上级和入职时间,再执行批量动作。
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs text-slate-500">
|
||||
已选 {selectedEmployeeIds.length} 人
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-3 xl:grid-cols-4">
|
||||
<Input
|
||||
placeholder="搜索姓名、工号、手机号、邮箱"
|
||||
@@ -582,10 +624,10 @@ export function PeoplePage() {
|
||||
<span>到</span>
|
||||
<span>{hireDateTo || "-"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{canManageUsers && selectedEmployeeIds.length ? (
|
||||
<div className="card-std flex flex-wrap items-center justify-between gap-3 p-4">
|
||||
<section className="admin-command-panel flex flex-wrap items-center justify-between gap-3" data-testid="people-batch-actions">
|
||||
<div className="text-sm text-zinc-700">
|
||||
已选 <span className="font-semibold text-zinc-900">{selectedEmployeeIds.length}</span> 名员工
|
||||
</div>
|
||||
@@ -622,10 +664,16 @@ export function PeoplePage() {
|
||||
清空选择
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<div className="card-std p-6">
|
||||
<section className="admin-command-panel space-y-4">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-zinc-900">人员列表</h2>
|
||||
<p className="mt-1 text-sm leading-6 text-zinc-600">
|
||||
查看档案进入个人详情;启停、离职和企微重试会保留操作结果。
|
||||
</p>
|
||||
</div>
|
||||
{exportError ? (
|
||||
<p className="mb-4 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{exportError}
|
||||
@@ -636,124 +684,126 @@ export function PeoplePage() {
|
||||
{mapApiMessage(employeesQuery.error, "加载人员列表失败")}
|
||||
</p>
|
||||
) : null}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{canManageUsers ? <TableHead>选择</TableHead> : null}
|
||||
<TableHead>员工</TableHead>
|
||||
<TableHead>部门 / 上级</TableHead>
|
||||
<TableHead>岗位 / 雇用状态</TableHead>
|
||||
<TableHead>角色</TableHead>
|
||||
<TableHead>账号状态</TableHead>
|
||||
<TableHead>企业微信</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.length ? (
|
||||
items.map((item) => {
|
||||
const wecomView = wecomDirectoryStatusView(item.wecomDirectory);
|
||||
return (
|
||||
<TableRow key={item.id}>
|
||||
{canManageUsers ? (
|
||||
<PrimaryTable>
|
||||
<Table data-testid="people-table">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{canManageUsers ? <TableHead>选择</TableHead> : null}
|
||||
<TableHead>员工</TableHead>
|
||||
<TableHead>部门 / 上级</TableHead>
|
||||
<TableHead>岗位 / 雇用状态</TableHead>
|
||||
<TableHead>角色</TableHead>
|
||||
<TableHead>账号状态</TableHead>
|
||||
<TableHead>企业微信</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.length ? (
|
||||
items.map((item) => {
|
||||
const wecomView = wecomDirectoryStatusView(item.wecomDirectory);
|
||||
return (
|
||||
<TableRow key={item.id}>
|
||||
{canManageUsers ? (
|
||||
<TableCell>
|
||||
<input
|
||||
aria-label={`选择员工 ${item.name}`}
|
||||
checked={selectedEmployeeIds.includes(item.id)}
|
||||
className="size-4 rounded border-zinc-300 text-zinc-900 focus:ring-zinc-400"
|
||||
disabled={batchPending}
|
||||
type="checkbox"
|
||||
onChange={(event) => toggleSelection(item.id, event.target.checked)}
|
||||
/>
|
||||
</TableCell>
|
||||
) : null}
|
||||
<TableCell>
|
||||
<input
|
||||
aria-label={`选择员工 ${item.name}`}
|
||||
checked={selectedEmployeeIds.includes(item.id)}
|
||||
className="size-4 rounded border-zinc-300 text-zinc-900 focus:ring-zinc-400"
|
||||
disabled={batchPending}
|
||||
type="checkbox"
|
||||
onChange={(event) => toggleSelection(item.id, event.target.checked)}
|
||||
/>
|
||||
</TableCell>
|
||||
) : null}
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<EmployeeAvatar name={item.name} src={item.avatarUrl} />
|
||||
<div>
|
||||
<div className="font-medium text-zinc-900">{item.name}</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
{item.employeeNo} · {item.email}
|
||||
<div className="flex items-center gap-3">
|
||||
<EmployeeAvatar name={item.name} src={item.avatarUrl} />
|
||||
<div>
|
||||
<div className="font-medium text-zinc-900">{item.name}</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
{item.employeeNo} · {item.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1 text-sm">
|
||||
<div className="text-zinc-900">{item.department?.name ?? "-"}</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
上级:{item.directManager ? `${item.directManager.name} · ${item.directManager.employeeNo}` : "-"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1 text-sm">
|
||||
<div className="text-zinc-900">{item.department?.name ?? "-"}</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
上级:{item.directManager ? `${item.directManager.name} · ${item.directManager.employeeNo}` : "-"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1 text-sm">
|
||||
<div className="text-zinc-900">{item.title || "-"}</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
{employmentStatusLabel(item.employmentStatus)} · 入职 {item.hireDate ?? "-"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1 text-sm">
|
||||
<div className="text-zinc-900">{item.title || "-"}</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
{employmentStatusLabel(item.employmentStatus)} · 入职 {item.hireDate ?? "-"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{item.roleCodes.join(" / ") || "-"}</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={item.status} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<StatusBadge status={wecomView.status} label={wecomView.label} />
|
||||
<div className="max-w-40 truncate text-xs text-zinc-500">{wecomView.hint}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button asChild size="sm" variant="secondary" type="button">
|
||||
<Link to={`/people/${item.id}`}>查看档案</Link>
|
||||
</Button>
|
||||
{canManageUsers ? (
|
||||
<>
|
||||
<Button size="sm" type="button" disabled={onboardingMutation.isPending} onClick={() => onboardingMutation.mutate(item.id)}>
|
||||
发起入职
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" type="button" disabled={regularizeMutation.isPending} onClick={() => regularizeMutation.mutate(item.id)}>
|
||||
转正
|
||||
</Button>
|
||||
{shouldShowWecomRetry(item.wecomDirectory) ? (
|
||||
</TableCell>
|
||||
<TableCell>{item.roleCodes.join(" / ") || "-"}</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={item.status} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<StatusBadge status={wecomView.status} label={wecomView.label} />
|
||||
<div className="max-w-40 truncate text-xs text-zinc-500">{wecomView.hint}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button asChild size="sm" variant="secondary" type="button">
|
||||
<Link to={`/people/${item.id}`}>查看档案</Link>
|
||||
</Button>
|
||||
{canManageUsers ? (
|
||||
<>
|
||||
<Button size="sm" type="button" disabled={onboardingMutation.isPending} onClick={() => onboardingMutation.mutate(item.id)}>
|
||||
发起入职
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" type="button" disabled={regularizeMutation.isPending} onClick={() => regularizeMutation.mutate(item.id)}>
|
||||
转正
|
||||
</Button>
|
||||
{shouldShowWecomRetry(item.wecomDirectory) ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
disabled={wecomDirectoryRetryMutation.isPending}
|
||||
onClick={() => wecomDirectoryRetryMutation.mutate(item.id)}
|
||||
>
|
||||
企微重试
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
disabled={wecomDirectoryRetryMutation.isPending}
|
||||
onClick={() => wecomDirectoryRetryMutation.mutate(item.id)}
|
||||
disabled={offboardingMutation.isPending}
|
||||
onClick={() => setOffboardingTarget(item)}
|
||||
>
|
||||
企微重试
|
||||
发起离职
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
disabled={offboardingMutation.isPending}
|
||||
onClick={() => setOffboardingTarget(item)}
|
||||
>
|
||||
发起离职
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell className="py-3 text-zinc-500" colSpan={tableColumnCount}>
|
||||
暂无匹配员工
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell className="py-3 text-zinc-500" colSpan={tableColumnCount}>
|
||||
暂无匹配员工
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</PrimaryTable>
|
||||
</section>
|
||||
|
||||
<Dialog open={transferDialogOpen} onOpenChange={setTransferDialogOpen}>
|
||||
<DialogContent className="max-w-lg">
|
||||
@@ -890,7 +940,7 @@ export function PeoplePage() {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { fetchSystemNavigation, fetchWorkspaceSettings, updateWorkspaceSettings } from "../api/workspace";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
||||
import { toast } from "sonner";
|
||||
@@ -46,6 +46,16 @@ const MODULE_LABELS: Record<string, string> = {
|
||||
profile: "个人设置",
|
||||
};
|
||||
|
||||
const VISIBILITY_OPTIONS = [
|
||||
{ value: "main", label: "主导航" },
|
||||
{ value: "admin", label: "管理区" },
|
||||
{ value: "hidden", label: "隐藏" },
|
||||
] as const;
|
||||
|
||||
function visibilityModeLabel(mode: string) {
|
||||
return VISIBILITY_OPTIONS.find((item) => item.value === mode)?.label ?? mode;
|
||||
}
|
||||
|
||||
export function WorkspaceSetupPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const workspaceQuery = useQuery({ queryKey: ["workspace-settings"], queryFn: fetchWorkspaceSettings });
|
||||
@@ -74,95 +84,139 @@ export function WorkspaceSetupPage() {
|
||||
const workspace = workspaceQuery.data;
|
||||
const rolePresets = navigationQuery.data?.recommendedRolePresets ?? [];
|
||||
const moduleEntries = useMemo(() => Object.entries(workspace?.moduleVisibility ?? {}), [workspace?.moduleVisibility]);
|
||||
const workspaceStats = useMemo(() => {
|
||||
const entries = Object.entries(workspace?.moduleVisibility ?? {});
|
||||
return {
|
||||
total: entries.length,
|
||||
main: entries.filter(([, mode]) => mode === "main").length,
|
||||
admin: entries.filter(([, mode]) => mode === "admin").length,
|
||||
hidden: entries.filter(([, mode]) => mode === "hidden").length,
|
||||
rolePresets: rolePresets.length,
|
||||
};
|
||||
}, [rolePresets.length, workspace?.moduleVisibility]);
|
||||
|
||||
if (workspaceQuery.isLoading || !workspace) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
eyebrow="管理"
|
||||
title="工作区设置"
|
||||
subtitle="按 10 人以下小团队模式收敛主导航与默认入口。"
|
||||
/>
|
||||
<div className="card-std p-6 text-sm text-zinc-500">加载工作区设置中...</div>
|
||||
</div>
|
||||
<div className="admin-command-panel text-sm text-zinc-500">加载工作区设置中...</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
eyebrow="管理"
|
||||
title="工作区设置"
|
||||
subtitle="按 10 人以下小团队模式收敛主导航与默认入口。"
|
||||
subtitle="按小团队运营模式收敛主导航、管理入口和备用模块。"
|
||||
/>
|
||||
|
||||
<div className="grid gap-4 xl:grid-cols-3">
|
||||
<div className="card-std p-6">
|
||||
<div className="text-sm text-zinc-500">产品模式</div>
|
||||
<div className="mt-2 text-xl font-semibold text-zinc-900">{workspace.profile}</div>
|
||||
<div className="mt-3 text-sm text-zinc-600">单公司、账号密码、站内通知、桌面优先。</div>
|
||||
<div className="finance-status-strip" data-testid="workspace-settings-summary">
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">模块总数</div>
|
||||
<div className="finance-metric-card__value">{workspaceStats.total}</div>
|
||||
</div>
|
||||
<div className="card-std p-6">
|
||||
<div className="text-sm text-zinc-500">初始化状态</div>
|
||||
<div className="mt-2 text-xl font-semibold text-zinc-900">{workspace.persisted ? "已完成" : "待初始化"}</div>
|
||||
<div className="mt-3 text-sm text-zinc-600">首次管理员进入此页后保存一次,即完成小团队工作区初始化。</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">主导航</div>
|
||||
<div className="finance-metric-card__value">{workspaceStats.main}</div>
|
||||
</div>
|
||||
<div className="card-std p-6">
|
||||
<div className="text-sm text-zinc-500">推荐角色预设</div>
|
||||
<div className="mt-3 space-y-2 text-sm">
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">管理区</div>
|
||||
<div className="finance-metric-card__value">{workspaceStats.admin}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">隐藏</div>
|
||||
<div className="finance-metric-card__value">{workspaceStats.hidden}</div>
|
||||
</div>
|
||||
<div className="finance-metric-card">
|
||||
<div className="finance-metric-card__label">初始化</div>
|
||||
<div className="finance-metric-card__value text-base">{workspace.persisted ? "已完成" : "待初始化"}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="admin-command-panel space-y-4" data-testid="workspace-profile-panel">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-zinc-900">工作区基线</h2>
|
||||
<p className="mt-1 text-sm leading-6 text-zinc-600">
|
||||
{workspace.profile} · 单公司 · 账号密码 · 站内通知 · 桌面优先。
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs text-slate-500">
|
||||
{workspaceStats.rolePresets} 个角色预设
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{rolePresets.length ? (
|
||||
<div className="admin-tile-grid">
|
||||
{rolePresets.map((item) => (
|
||||
<div key={item.key} className="rounded-lg border border-zinc-200 p-3">
|
||||
<div key={item.key} className="admin-tile-card">
|
||||
<div className="font-medium text-zinc-900">{item.label}</div>
|
||||
<div className="mt-1 text-zinc-600">{item.description}</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">角色:{item.roleCodes.join(" / ")}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed border-slate-300 px-4 py-5 text-sm text-slate-500">
|
||||
当前没有推荐角色预设。
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<div className="card-std p-6 space-y-4">
|
||||
<section className="admin-command-panel space-y-4">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-zinc-900">模块显隐</h3>
|
||||
<p className="mt-1 text-sm text-zinc-500">主导航只保留高频入口;备用模块保留代码与数据,但默认放在管理员备用区。</p>
|
||||
<p className="mt-1 text-sm leading-6 text-zinc-600">
|
||||
主导航只保留高频入口;备用模块保留代码与数据,低频功能默认收进管理区或隐藏。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>模块</TableHead>
|
||||
<TableHead>当前显隐</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{moduleEntries.map(([key, mode]) => (
|
||||
<TableRow key={key}>
|
||||
<TableCell>{MODULE_LABELS[key] ?? key}</TableCell>
|
||||
<TableCell>{mode}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(["main", "admin", "hidden"] as const).map((nextMode) => (
|
||||
<Button
|
||||
key={nextMode}
|
||||
size="sm"
|
||||
variant={mode === nextMode ? "default" : "secondary"}
|
||||
type="button"
|
||||
disabled={mutation.isPending}
|
||||
onClick={() => {
|
||||
const nextVisibility = { ...(workspace.moduleVisibility ?? {}), [key]: nextMode };
|
||||
mutation.mutate({ moduleVisibility: nextVisibility });
|
||||
}}
|
||||
>
|
||||
{nextMode}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<PrimaryTable>
|
||||
<Table data-testid="workspace-modules-table">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>模块</TableHead>
|
||||
<TableHead>当前显隐</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{moduleEntries.map(([key, mode]) => (
|
||||
<TableRow key={key}>
|
||||
<TableCell>{MODULE_LABELS[key] ?? key}</TableCell>
|
||||
<TableCell>{visibilityModeLabel(mode)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{VISIBILITY_OPTIONS.map((option) => (
|
||||
<Button
|
||||
key={option.value}
|
||||
size="sm"
|
||||
variant={mode === option.value ? "default" : "secondary"}
|
||||
type="button"
|
||||
disabled={mutation.isPending}
|
||||
onClick={() => {
|
||||
const nextVisibility = { ...(workspace.moduleVisibility ?? {}), [key]: option.value };
|
||||
mutation.mutate({ moduleVisibility: nextVisibility });
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</PrimaryTable>
|
||||
</section>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,11 @@ const allPermissions = [
|
||||
"risk.review.manage",
|
||||
"travel.payment.manage",
|
||||
"system.user.manage",
|
||||
"system.users",
|
||||
"system.settings",
|
||||
"system.audit.view",
|
||||
"masterdata.manage",
|
||||
"notification.channel.manage",
|
||||
];
|
||||
|
||||
const visualMe = {
|
||||
@@ -517,9 +520,13 @@ async function mockWave4Apis(page: Page, options: Wave4MockOptions = {}) {
|
||||
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"),
|
||||
@@ -532,6 +539,35 @@ async function mockWave4Apis(page: Page, options: Wave4MockOptions = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/system/workspace") {
|
||||
await fulfillJson(route, {
|
||||
id: "workspace-wave4",
|
||||
persisted: true,
|
||||
profile: "small_team_internal",
|
||||
orgMode: "single_company",
|
||||
primaryEntryMode: "password",
|
||||
notificationStrategy: "in_app",
|
||||
devicePriority: "desktop",
|
||||
moduleVisibility: {
|
||||
portal: "main",
|
||||
workflowTasks: "main",
|
||||
reimbursements: "main",
|
||||
paymentBatches: "admin",
|
||||
apOperations: "admin",
|
||||
invoicePool: "admin",
|
||||
people: "admin",
|
||||
organization: "admin",
|
||||
approvalRules: "admin",
|
||||
featureFlags: "admin",
|
||||
travelRequests: "hidden",
|
||||
},
|
||||
createdAt: null,
|
||||
updatedAt: "2026-05-29T08:00:00.000Z",
|
||||
updatedBy: visualMe.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/notifications") {
|
||||
await fulfillJson(route, { page: 1, pageSize: 5, total: 0, unreadOnly: false, unreadCount: 0, items: [] });
|
||||
return;
|
||||
@@ -803,6 +839,97 @@ async function mockWave4Apis(page: Page, options: Wave4MockOptions = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/employees/organization") {
|
||||
await fulfillJson(route, {
|
||||
items: [
|
||||
{
|
||||
department: departments[0],
|
||||
roots: [
|
||||
{
|
||||
id: visualMe.id,
|
||||
employeeNo: visualMe.employeeNo,
|
||||
name: visualMe.name,
|
||||
title: "管理员",
|
||||
status: "active",
|
||||
avatarUrl: null,
|
||||
department: departments[0],
|
||||
externalManager: null,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
department: departments[1],
|
||||
roots: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/employees/wecom-hr-master-sync/runs") {
|
||||
await fulfillJson(route, {
|
||||
items: [
|
||||
{
|
||||
id: "wecom-hr-run-wave4",
|
||||
mode: "dry_run",
|
||||
status: "success",
|
||||
tokenSource: null,
|
||||
requestedUserids: null,
|
||||
limit: 50,
|
||||
summary: { total: 1, created: 0, updated: 1, skipped: 0, conflicts: 0, failed: 0 },
|
||||
errorCode: null,
|
||||
errorMessage: null,
|
||||
triggeredBy: visualMe.id,
|
||||
startedAt: "2026-05-29T07:50:00.000Z",
|
||||
finishedAt: "2026-05-29T07:51:00.000Z",
|
||||
createdAt: "2026-05-29T07:50:00.000Z",
|
||||
updatedAt: "2026-05-29T07:51:00.000Z",
|
||||
itemCount: 1,
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/approval-rules") {
|
||||
await fulfillJson(route, {
|
||||
enabled: null,
|
||||
items: [
|
||||
{
|
||||
id: "approval-rule-wave5-1",
|
||||
name: "5000 以下经理审批",
|
||||
minAmount: "0.00",
|
||||
maxAmount: "5000.00",
|
||||
level1Role: "manager",
|
||||
level2Role: null,
|
||||
level3Role: null,
|
||||
ownerSelfFallbackRole: "finance_mgr",
|
||||
enabled: true,
|
||||
priority: 1,
|
||||
createdAt: "2026-05-29T08:00:00.000Z",
|
||||
updatedAt: "2026-05-29T08:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "approval-rule-wave5-2",
|
||||
name: "高额财务负责人复核",
|
||||
minAmount: "5000.01",
|
||||
maxAmount: null,
|
||||
level1Role: "manager",
|
||||
level2Role: "finance_mgr",
|
||||
level3Role: "ceo",
|
||||
ownerSelfFallbackRole: null,
|
||||
enabled: false,
|
||||
priority: 2,
|
||||
createdAt: "2026-05-29T08:00:00.000Z",
|
||||
updatedAt: "2026-05-29T08:00:00.000Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathName === "/api/master-data/legal-entities") {
|
||||
await fulfillJson(route, { includeArchived: false, items: legalEntities });
|
||||
return;
|
||||
@@ -886,16 +1013,20 @@ const pages = [
|
||||
{ path: "/payment-batches", summary: "payment-batches-summary", proof: "text=批次列表", screenshot: "01-payment-batches" },
|
||||
{ path: "/ap-operations", summary: "ap-operations-summary", proof: "text=异常付款", screenshot: "02-ap-operations" },
|
||||
{ path: "/invoice-pool", summary: "invoice-pool-summary", proof: "data-testid=invoice-pool-table", screenshot: "03-invoice-pool" },
|
||||
{ path: "/system/users", summary: "users-summary", proof: "data-testid=users-table", screenshot: "04-users" },
|
||||
{ path: "/system/master-data", summary: "master-data-summary", proof: "text=法体列表", screenshot: "05-master-data" },
|
||||
{ path: "/system/notification-channels", summary: "notification-channels-summary", proof: "data-testid=notification-channels-table", screenshot: "06-notification-channels" },
|
||||
{ path: "/system/settings", summary: "system-settings-summary", proof: "text=系统设置", screenshot: "07-system-settings" },
|
||||
{ path: "/system/feature-flags", summary: "feature-flags-summary", proof: "data-testid=feature-flags-table", screenshot: "08-feature-flags" },
|
||||
{ path: "/system/workflow-definitions", summary: "workflow-definitions-summary", proof: "text=版本列表", screenshot: "09-workflow-definitions" },
|
||||
{ path: "/audit-logs", summary: "audit-summary", proof: "text=PAYMENT_BATCH_REVIEW", screenshot: "10-audit-logs" },
|
||||
{ path: "/system/approval-rules", summary: "approval-rules-summary", proof: "data-testid=approval-rules-table", screenshot: "04-approval-rules" },
|
||||
{ path: "/people", summary: "people-summary", proof: "data-testid=people-table", screenshot: "05-people" },
|
||||
{ path: "/organization", summary: "organization-summary", proof: "text=研发部", screenshot: "06-organization" },
|
||||
{ path: "/system/workspace", summary: "workspace-settings-summary", proof: "data-testid=workspace-modules-table", screenshot: "07-workspace" },
|
||||
{ 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/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" },
|
||||
];
|
||||
|
||||
test("PC 财务与管理页 Wave4 视觉冒烟", async ({ page }) => {
|
||||
test("PC 财务与管理页 Wave4-5 视觉冒烟", async ({ page }) => {
|
||||
await mockWave4Apis(page);
|
||||
await page.setViewportSize({ width: 1440, height: 960 });
|
||||
await login(page);
|
||||
|
||||
在新工单中引用
屏蔽一个用户