优化PC核心工作流视觉 Wave2(05-27-pc-core-workflow-visual-wave2)

这个提交包含在:
2026-05-27 18:12:23 +08:00
父节点 8f17d6454d
当前提交 9ac73a3872
共修改 16 个文件,包含 771 行新增和 96 行删除
+4 -4
查看文件
@@ -61,7 +61,7 @@ export function ForgotPasswordPage() {
try {
await forgotPassword({ email: normalizedEmail });
setCodeRequested(true);
setMessage("如账号可用,验证码将发送到企业邮箱;请在有效期内完成重置,并查看免提醒邮件。");
setMessage("如账号可用,验证码将发送到企业邮箱;10 分钟内有效,请同时查看免提醒邮件。");
} catch (err) {
if (isApiError(err)) {
setError(mapApiErrorMessage(err.code, err.details, err.message));
@@ -140,7 +140,7 @@ export function ForgotPasswordPage() {
{!emailDeliveryReady ? (
<div
aria-live="polite"
className="flex gap-3 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm leading-6 text-amber-800"
className="flex gap-3 rounded-lg border border-amber-200 bg-amber-50 px-3.5 py-2.5 text-sm leading-6 text-amber-800"
data-testid="forgot-password-email-delivery-status"
>
<MailCheck aria-hidden="true" className="mt-0.5 h-4 w-4 flex-none" />
@@ -214,13 +214,13 @@ export function ForgotPasswordPage() {
) : null}
{message ? (
<div className="mt-4 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700" data-testid="forgot-password-message">
<div className="mt-4 rounded-lg border border-emerald-200 bg-emerald-50 px-3.5 py-2.5 text-sm text-emerald-700" data-testid="forgot-password-message">
{message}
</div>
) : null}
{error ? (
<div className="mt-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600" data-testid="forgot-password-error">
<div className="mt-4 rounded-lg border border-red-200 bg-red-50 px-3.5 py-2.5 text-sm text-red-600" data-testid="forgot-password-error">
{error}
</div>
) : null}
+13 -13
查看文件
@@ -166,7 +166,7 @@ export function LoginPage() {
await requestEmailLoginCode({ email });
setEmailCodeRequested(true);
setEmailCodeCooldown(60);
setEmailCodeMessage("如账号可用,验证码将发送到企业邮箱;请同时查看免提醒邮件。");
setEmailCodeMessage("如账号可用,验证码将发送到企业邮箱;10 分钟内有效,请同时查看免提醒邮件。");
} catch (err) {
if (isApiError(err)) {
setEmailCodeError(mapApiErrorMessage(err.code, err.details, err.message));
@@ -260,7 +260,7 @@ export function LoginPage() {
</div>
</div>
{error ? <div className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600">{error}</div> : null}
{error ? <div className="rounded-lg border border-red-200 bg-red-50 px-3.5 py-2.5 text-sm text-red-600">{error}</div> : null}
<Button data-testid="login-submit" className="w-full" disabled={submitting} type="submit">
{submitting ? "登录中..." : "登录"}
@@ -309,7 +309,7 @@ export function LoginPage() {
{!emailDeliveryReady ? (
<div
aria-live="polite"
className="flex gap-3 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm leading-6 text-amber-800"
className="flex gap-3 rounded-lg border border-amber-200 bg-amber-50 px-3.5 py-2.5 text-sm leading-6 text-amber-800"
data-testid="login-email-delivery-status"
>
<MailCheck aria-hidden="true" className="mt-0.5 h-4 w-4 flex-none" />
@@ -341,13 +341,13 @@ export function LoginPage() {
</div>
{emailCodeMessage ? (
<div className="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700" data-testid="login-email-code-message">
<div className="rounded-lg border border-emerald-200 bg-emerald-50 px-3.5 py-2.5 text-sm text-emerald-700" data-testid="login-email-code-message">
{emailCodeMessage}
</div>
) : null}
{emailCodeError ? (
<div className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-600" data-testid="login-email-code-error">
<div className="rounded-lg border border-red-200 bg-red-50 px-3.5 py-2.5 text-sm text-red-600" data-testid="login-email-code-error">
{emailCodeError}
</div>
) : null}
@@ -366,10 +366,10 @@ export function LoginPage() {
}
return (
<AuthPanel title="登录工作台" subtitle={showWecomPrimary ? "企业微信进入优先" : loginMode === "email_code" ? "邮箱验证码登录" : "账号密码登录"}>
<div className="space-y-5">
<AuthPanel title="登录工作台" subtitle={showWecomPrimary ? "企业微信优先入口" : "账号、邮箱或企业微信"}>
<div className="space-y-4">
{wecomNotice ? (
<div className={`rounded-xl border px-4 py-3 text-sm ${noticeClasses(wecomNotice.tone)}`} data-testid="login-wecom-notice">
<div className={`rounded-lg border px-3.5 py-2.5 text-sm ${noticeClasses(wecomNotice.tone)}`} data-testid="login-wecom-notice">
<div className="font-medium">{wecomNotice.title}</div>
<div className="mt-1 leading-6">{wecomNotice.description}</div>
</div>
@@ -381,7 +381,7 @@ export function LoginPage() {
{wecomConfigQuery.isLoading ? "准备中..." : "企业微信一键进入"}
</Button>
{wecomConfig && !wecomEntry ? (
<div className="rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-sm leading-6 text-amber-700">
<div className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm leading-6 text-amber-700">
企业微信入口不可用,请用账号密码。
</div>
) : null}
@@ -398,10 +398,10 @@ export function LoginPage() {
) : null}
{!showWecomPrimary ? (
<div className="grid grid-cols-2 gap-1 rounded-xl border border-slate-200 bg-slate-100/80 p-1" data-testid="login-mode-switch">
<div className="auth-mode-switch" data-testid="login-mode-switch">
<button
aria-pressed={loginMode === "password"}
className={`h-10 rounded-lg text-sm font-medium transition ${loginMode === "password" ? "bg-white text-primary shadow-sm" : "text-slate-500 hover:text-slate-900"}`}
className={`auth-mode-switch__item ${loginMode === "password" ? "auth-mode-switch__item--active" : "auth-mode-switch__item--idle"}`}
data-testid="login-mode-password"
type="button"
onClick={() => selectLoginMode("password")}
@@ -410,7 +410,7 @@ export function LoginPage() {
</button>
<button
aria-pressed={loginMode === "email_code"}
className={`h-10 rounded-lg text-sm font-medium transition ${loginMode === "email_code" ? "bg-white text-primary shadow-sm" : "text-slate-500 hover:text-slate-900"}`}
className={`auth-mode-switch__item ${loginMode === "email_code" ? "auth-mode-switch__item--active" : "auth-mode-switch__item--idle"}`}
data-testid="login-mode-email-code"
type="button"
onClick={() => selectLoginMode("email_code")}
@@ -436,7 +436,7 @@ export function LoginPage() {
{showBrowserWecomNotice ? (
<div
className="flex gap-3 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm leading-6 text-slate-600"
className="auth-side-entry flex gap-3 text-slate-600"
data-testid="wecom-entry-browser-notice"
>
<Building2 aria-hidden="true" className="mt-0.5 h-4 w-4 flex-none text-primary" />
+16 -17
查看文件
@@ -388,7 +388,7 @@ function QuickLinksPanel({ items }: { items: PortalHomePayload["quickLinks"] })
const safeItems = Array.isArray(items) ? items : [];
return (
<SectionPanel title="常用入口">
<SectionPanel title="入口">
<div className="grid gap-3 sm:grid-cols-2">
{safeItems.length ? safeItems.map((item) => (
<Link key={item.key} to={item.href} className="portal-list-card text-sm font-medium text-slate-700">
@@ -400,13 +400,13 @@ function QuickLinksPanel({ items }: { items: PortalHomePayload["quickLinks"] })
);
}
function SecondaryCardsPanel({ title, description, cards }: { title: string; description: string; cards: PortalHomePayload["cards"] }) {
function SecondaryCardsPanel({ title, cards }: { title: string; cards: PortalHomePayload["cards"] }) {
if (!cards.length) {
return <EmptyStateCard title={title} description="暂无统计项。" />;
}
return (
<SectionPanel title={title} description={description}>
<SectionPanel title={title}>
<div className="grid gap-3 sm:grid-cols-2">
{cards.map((card) => (
<MetricCard
@@ -589,7 +589,7 @@ function GettingStartedCard({
return (
<SectionPanel
title="快速上手"
description="完成基础资料。"
description="基础资料"
actions={(
<button
type="button"
@@ -740,7 +740,7 @@ function TodoAggregatePanel({ items }: { items: TodoAggregateItem[] }) {
{item.severity === "high" ? <span className="badge-error">高优先</span> : null}
{item.severity === "warning" ? <span className="badge-warning">关注</span> : null}
</div>
<div className="mt-2 text-sm text-primary">进入处理</div>
<div className="mt-1 text-sm text-primary">进入</div>
</div>
</div>
</Link>
@@ -748,7 +748,7 @@ function TodoAggregatePanel({ items }: { items: TodoAggregateItem[] }) {
</div>
{hiddenCount > 0 ? (
<div className="portal-panel-footer">
<span>还有 {hiddenCount} 项已收起。</span>
<span>另有 {hiddenCount} 项。</span>
<Link to="/todos" className="font-medium text-primary hover:underline">查看全部待办</Link>
</div>
) : null}
@@ -784,7 +784,7 @@ function TeamStatusBoardPanel({ items }: { items: PortalTeamStatusItem[] }) {
</div>
{hiddenCount > 0 ? (
<div className="portal-panel-footer">
<span>已收起 {hiddenCount} 位成员。</span>
<span>另有 {hiddenCount} 位。</span>
<Link to="/team" className="font-medium text-primary hover:underline">查看全部</Link>
</div>
) : null}
@@ -1053,7 +1053,7 @@ export function PortalHomePage() {
key: "approval",
icon: "fact_check",
label: "处理审批",
description: "进入审批中心查看待办",
description: "待办处理",
to: "/workflow/tasks",
});
}
@@ -1062,7 +1062,7 @@ export function PortalHomePage() {
key: "finance",
icon: "payments",
label: "财务处理",
description: "查看付款和风险待处理",
description: "付款 / 风险",
to: payload.financeSummary.href,
});
}
@@ -1071,7 +1071,7 @@ export function PortalHomePage() {
key: "system",
icon: "admin_panel_settings",
label: "系统管理",
description: "账号、配置和审计入口",
description: "账号 / 配置",
to: "/people",
});
}
@@ -1080,7 +1080,7 @@ export function PortalHomePage() {
key: "leave",
icon: "event_available",
label: "提交请假",
description: "直接进入请假申请页",
description: "请假申请",
to: "/leave-requests?action=create",
});
}
@@ -1089,7 +1089,7 @@ export function PortalHomePage() {
key: "expense",
icon: "request_quote",
label: "提交报销",
description: "进入报销工作台创建单据",
description: "报销单据",
to: "/reimbursements",
});
}
@@ -1097,7 +1097,7 @@ export function PortalHomePage() {
key: "attendance",
icon: "schedule",
label: "考勤汇总",
description: "查看企微打卡摘要和 OA 修正",
description: "企微摘要 / OA 修正",
to: "/attendance",
});
if (payload.weeklyReport || payload.weeklySummary) {
@@ -1105,7 +1105,7 @@ export function PortalHomePage() {
key: "weekly",
icon: "article",
label: "提交周报",
description: "进入周报页继续填写或查看团队缺口",
description: "填写 / 查看缺口",
to: "/weekly-reports",
});
}
@@ -1136,7 +1136,7 @@ export function PortalHomePage() {
}
/>
<div className="portal-metric-strip">
<div className="portal-metric-strip ops-status-strip">
{primaryCards.map((card) => (
<MetricCard
key={card.key}
@@ -1161,7 +1161,7 @@ export function PortalHomePage() {
fallback={(
<PortalSectionFallback
title="快速上手"
description="完成基础资料。"
description="基础资料"
/>
)}
>
@@ -1234,7 +1234,6 @@ export function PortalHomePage() {
<SecondaryCardsPanel
title="项目与组织"
description="项目、组织、风险"
cards={secondaryCards}
/>
</SecondaryWorkspacePanel>
+25 -16
查看文件
@@ -363,7 +363,7 @@ export function ReimbursementsPage() {
<ListWorkspace>
<WorkspaceHeader
title="我的报销"
subtitle="草稿、审批和付款状态。"
subtitle="单据、金额、状态。"
actions={
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
{canCreateExpense ? (
@@ -461,21 +461,21 @@ export function ReimbursementsPage() {
</p>
) : null}
<StatStrip columns={4}>
<StatStrip columns={4} className="ops-status-strip">
<MetricCard title="当前列表" value={stats.currentPageCount} note={`共 ${listQuery.data?.total ?? 0} 条`} />
<MetricCard title="当前金额" value={formatCurrency(stats.currentPageAmount)} note="当前页" />
<MetricCard title="审批中" value={stats.reviewing} note="待推进" tone={stats.reviewing > 0 ? "warning" : "neutral"} />
<MetricCard title="已支付" value={stats.paid} note={scope === "all" ? "全范围" : "仅自己"} />
</StatStrip>
<FilterBar className="space-y-4">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<FilterBar className="ops-command-bar space-y-3 !p-3 md:!p-3.5">
<div className="ops-command-bar__row">
<Tabs
data-testid="reimbursements-status-tabs"
value={status}
onValueChange={(value) => setSearchParams({ status: value, page: "1", scope })}
>
<TabsList className="flex w-full flex-wrap justify-start gap-1 md:w-auto">
<TabsList className="ops-command-bar__tabs !h-auto !justify-start !bg-slate-50">
{[
{ value: "draft", label: "草稿" },
{ value: "reviewing", label: "审批中" },
@@ -495,10 +495,10 @@ export function ReimbursementsPage() {
</TabsList>
</Tabs>
<div className="flex flex-wrap items-center gap-2">
<div className="ops-command-bar__actions">
{showProxyOwnerSelector ? (
<select
className="select-sm h-10"
className="select-sm"
data-testid="reimbursements-owner-select"
onChange={(event) => setProxyOwnerEmployeeId(event.target.value)}
value={proxyOwnerEmployeeId}
@@ -518,6 +518,7 @@ export function ReimbursementsPage() {
<>
<Button
data-testid="reimbursements-scope-own"
size="sm"
variant={scope === "own" ? "default" : "secondary"}
onClick={() => setSearchParams({ status, page: "1", scope: "own" })}
type="button"
@@ -526,13 +527,14 @@ export function ReimbursementsPage() {
</Button>
<Button
data-testid="reimbursements-scope-all"
size="sm"
variant={scope === "all" ? "default" : "secondary"}
onClick={() => setSearchParams({ status, page: "1", scope: "all" })}
type="button"
>
全范围
</Button>
<Button asChild variant="secondary" type="button">
<Button asChild size="sm" variant="secondary" type="button">
<Link to="/reimbursements/dashboard">统计分析</Link>
</Button>
</>
@@ -542,6 +544,7 @@ export function ReimbursementsPage() {
<DropdownMenuTrigger asChild>
<Button
data-testid="reimbursements-export"
size="sm"
variant="secondary"
type="button"
disabled={exportMutation.isPending}
@@ -567,6 +570,7 @@ export function ReimbursementsPage() {
{canShowBatchEntry ? (
<Button
size="sm"
variant={batchMode ? "default" : "secondary"}
onClick={() => {
setBatchMode((current) => !current);
@@ -584,7 +588,7 @@ export function ReimbursementsPage() {
</div>
{batchMode ? (
<div className="rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
<div className="ops-inline-alert border-amber-200 bg-amber-50 text-amber-800">
仅勾选你可处理的审批中报销;驳回需填写理由。
</div>
) : null}
@@ -710,7 +714,7 @@ export function ReimbursementsPage() {
)}
</div>
<PrimaryTable className="hidden md:block">
<PrimaryTable className="hidden md:block ops-table-focus">
<table className="w-full border-collapse text-left" data-testid="reimbursements-table">
<thead className="table-header">
<tr>
@@ -782,13 +786,18 @@ export function ReimbursementsPage() {
<td className="table-td">{item.reimbursementDate ?? "-"}</td>
<td className="table-td">
<div className="flex flex-wrap gap-2">
<Link
to={`/reimbursements/${item.id}`}
className={["draft", "rejected"].includes(item.status) ? "btn-primary" : "btn-secondary"}
data-testid="reimbursement-detail-link"
<Button
asChild
size="sm"
variant={["draft", "rejected"].includes(item.status) ? "default" : "secondary"}
>
{["draft", "rejected"].includes(item.status) ? "编辑" : "详情"}
</Link>
<Link
to={`/reimbursements/${item.id}`}
data-testid="reimbursement-detail-link"
>
{["draft", "rejected"].includes(item.status) ? "编辑" : "详情"}
</Link>
</Button>
{canDeleteDraft ? (
<Button
disabled={deleteMutation.isPending}
+10 -11
查看文件
@@ -15,7 +15,6 @@ import { ConfirmDialog } from "../components/common/ConfirmDialog";
import { StatusBadge } from "../components/common/StatusBadge";
import { TableSkeleton } from "../components/common/TableSkeleton";
import {
ActionToolbar,
FilterBar,
ListWorkspace,
PrimaryTable,
@@ -210,11 +209,11 @@ export function WorkflowTasksPage() {
<WorkspaceHeader
eyebrow="审批"
title="审批中心"
subtitle="待处理、已处理和业务类型。"
subtitle="任务、节点、业务状态。"
/>
<div data-testid="workflow-task-summary">
<StatStrip columns={4}>
<StatStrip columns={4} className="ops-status-strip">
<MetricCard title="待办总数" value={summaryQuery.data?.pendingTotal ?? "-"} />
<MetricCard title="超时待办" value={summaryQuery.data?.overdueTotal ?? "-"} tone="warning" />
<MetricCard title="最早待处理" value={formatSummaryTime(summaryQuery.data?.earliestPendingAt)} />
@@ -232,14 +231,14 @@ export function WorkflowTasksPage() {
</StatStrip>
</div>
<FilterBar>
<ActionToolbar className="items-start">
<FilterBar className="ops-command-bar !p-3 md:!p-3.5">
<div className="ops-command-bar__row">
<Tabs
data-testid="workflow-tasks-status-tabs"
value={status}
onValueChange={(value) => updateSearchParams({ status: value as WorkflowStatusFilter, page: 1 })}
>
<TabsList className="flex w-full flex-wrap justify-start gap-1 xl:w-auto">
<TabsList className="ops-command-bar__tabs !h-auto !justify-start !bg-slate-50">
{WORKFLOW_STATUS_OPTIONS.map((item) => (
<TabsTrigger
data-testid={`workflow-tasks-tab-${item.value}`}
@@ -252,13 +251,13 @@ export function WorkflowTasksPage() {
</TabsList>
</Tabs>
<div className="flex w-full items-center gap-2 sm:w-auto">
<label className="text-sm font-medium text-zinc-700" htmlFor="workflow-entity-type-filter">
<div className="ops-command-bar__actions">
<label className="ops-filter-label" htmlFor="workflow-entity-type-filter">
业务类型
</label>
<select
id="workflow-entity-type-filter"
className="select-std min-w-36 flex-1 sm:flex-none"
className="select-sm min-w-32"
data-testid="workflow-tasks-entity-filter"
value={entityType}
onChange={(event) => updateSearchParams({ entityType: event.target.value as WorkflowEntityFilter, page: 1 })}
@@ -270,7 +269,7 @@ export function WorkflowTasksPage() {
))}
</select>
</div>
</ActionToolbar>
</div>
</FilterBar>
{query.isLoading ? <TableSkeleton columns={7} rows={5} /> : null}
@@ -352,7 +351,7 @@ export function WorkflowTasksPage() {
)}
</div>
<PrimaryTable className="hidden md:block">
<PrimaryTable className="hidden md:block ops-table-focus">
<Table data-testid="workflow-tasks-table">
<TableHeader>
<tr>