R97 B1-B5 精修高频页面操作密度

这个提交包含在:
2026-05-06 17:56:57 +08:00
父节点 4cfac0ddf8
当前提交 705cf1e361
共修改 7 个文件,包含 226 行新增和 151 行删除
+5 -5
查看文件
@@ -177,9 +177,9 @@ export function InvoicePoolPage() {
if (previousStatus === "pending_ocr" && currentStatus && currentStatus !== "pending_ocr") {
void queryClient.invalidateQueries({ queryKey: ["invoice-pool"] });
if (currentStatus === "available") {
toast.success("票据 OCR 已完成,可继续绑定或验真");
toast.success("票据识别已完成,可继续绑定或验真");
} else if (currentStatus === "ocr_failed") {
toast.error(`票据 OCR 未完成:${formatInvoiceOcrErrorMessage(detailQuery.data?.ocrLastError)}`);
toast.error(`票据识别未完成:${formatInvoiceOcrErrorMessage(detailQuery.data?.ocrLastError)}`);
}
}
@@ -268,7 +268,7 @@ export function InvoicePoolPage() {
setError("");
const hasPending = result.successes.some((item) => item.data.status === "pending_ocr");
toast.success(hasPending ? `已上传 ${result.successes.length} 个,OCR 识别中` : `已上传 ${result.successes.length} 个,可直接处理`);
toast.success(hasPending ? `已上传 ${result.successes.length} 个,识别中` : `已上传 ${result.successes.length} 个,可直接处理`);
},
onSettled: () => setUploadProgress(null),
onError: (err) => handleError(err, "上传失败,请稍后重试"),
@@ -368,9 +368,9 @@ export function InvoicePoolPage() {
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["invoice-pool"] });
setError("");
toast.success("OCR 重试已触发");
toast.success("已开始重新识别");
},
onError: (err) => handleError(err, "OCR 重试失败,请稍后再试"),
onError: (err) => handleError(err, "重新识别失败,请稍后再试"),
});
const triggerVerificationMutation = useMutation({
+30 -23
查看文件
@@ -771,6 +771,19 @@ export function MobileApprovalsPage() {
</TabsList>
</Tabs>
<div className="space-y-3">
<Input
data-testid="mobile-approvals-search"
value={searchInput}
onChange={(event) => setSearchInput(event.target.value)}
placeholder="搜索待办"
/>
<details className="rounded-xl border border-zinc-200 bg-zinc-50/70 px-3 py-2 text-sm">
<summary className="cursor-pointer select-none font-medium text-zinc-700">
筛选与批量
</summary>
<div className="mt-3 space-y-3">
<Tabs value={entityFilter} onValueChange={(value) => setEntityFilter(value as EntityFilter)}>
<TabsList className="flex w-full flex-wrap justify-start gap-1">
{ENTITY_FILTER_OPTIONS.map((option) => (
@@ -781,14 +794,6 @@ export function MobileApprovalsPage() {
</TabsList>
</Tabs>
<div className="space-y-3">
<Input
data-testid="mobile-approvals-search"
value={searchInput}
onChange={(event) => setSearchInput(event.target.value)}
placeholder="搜索步骤名、业务类型、任务 ID 或业务实体 ID"
/>
{taskTab === "pending" ? (
<div className="grid gap-2 sm:flex sm:flex-wrap sm:items-center">
<Button
@@ -803,7 +808,7 @@ export function MobileApprovalsPage() {
setBatchRejectComment("");
}}
>
{batchMode ? "退出批量处理" : "批量处理"}
{batchMode ? "退出批量" : "批量"}
</Button>
{batchMode ? (
<Button
@@ -814,23 +819,25 @@ export function MobileApprovalsPage() {
disabled={!filteredTaskCards.length}
onClick={toggleSelectAllVisibleTasks}
>
{allVisibleSelected ? "清空选择" : "全选当前筛选结果"}
{allVisibleSelected ? "清空选择" : "全选当前"}
</Button>
) : null}
<span className="text-sm text-zinc-500 sm:ml-1">
当前结果 {filteredTaskCards.length} 条
{batchMode ? `,已选 ${selectedBatchTaskIds.length} 条` : ""}
当前 {filteredTaskCards.length} 条
{batchMode ? `,已选 ${selectedBatchTaskIds.length}` : ""}
</span>
</div>
) : (
<p className="text-sm text-zinc-500">已处理页仅展示最近 30 天内的 completed 任务。</p>
<p className="text-sm text-zinc-500">仅显示最近 30 天。</p>
)}
</div>
</details>
</div>
{batchMode ? (
<div className="rounded-2xl border border-zinc-200 bg-zinc-50 p-3 text-sm">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="text-zinc-700">已选择 {selectedBatchTaskIds.length} 条待办,可在线串行处理或离线逐条入队。</div>
<div className="text-zinc-700">已选 {selectedBatchTaskIds.length} 条</div>
<div className="grid w-full gap-2 sm:flex sm:w-auto sm:flex-wrap">
<Button
type="button"
@@ -839,7 +846,7 @@ export function MobileApprovalsPage() {
disabled={!selectedBatchTaskIds.length || batchActionMutation.isPending}
onClick={() => handleBatchAction("approve")}
>
{canUseRemote ? "批量通过" : "离线批量加入通过草稿"}
{canUseRemote ? "批量通过" : "加入通过草稿"}
</Button>
<Button
type="button"
@@ -849,7 +856,7 @@ export function MobileApprovalsPage() {
disabled={!selectedBatchTaskIds.length || batchActionMutation.isPending || !batchRejectComment.trim()}
onClick={() => handleBatchAction("reject")}
>
{canUseRemote ? "批量驳回" : "离线批量加入驳回草稿"}
{canUseRemote ? "批量驳回" : "加入驳回草稿"}
</Button>
</div>
</div>
@@ -955,7 +962,7 @@ export function MobileApprovalsPage() {
</section>
<section ref={detailSectionRef} className="card-std scroll-mt-4 space-y-3 p-4" data-testid="mobile-approvals-detail">
<h3 className="text-base font-semibold text-zinc-900">详情与处理</h3>
<h3 className="text-base font-semibold text-zinc-900">处理</h3>
{!selectedTaskId ? <p className="text-sm text-zinc-500">请先选择任务。</p> : null}
@@ -1025,13 +1032,13 @@ export function MobileApprovalsPage() {
<Textarea
data-testid="mobile-approvals-comment"
onChange={(event) => setComment(event.target.value)}
placeholder="驳回 / 退回意见;离线仅支持驳回意见入队。"
placeholder="意见(驳回/退回必填)"
rows={3}
value={comment}
/>
{!canUseRemote ? (
<p className="text-xs text-zinc-500">离线状态下仅支持通过 / 驳回草稿,退回与转审需联网执行。</p>
<p className="text-xs text-zinc-500">离线仅支持通过 / 驳回草稿。</p>
) : null}
<div className="grid gap-2 sm:grid-cols-3">
@@ -1123,8 +1130,8 @@ export function MobileApprovalsPage() {
<section className="card-std space-y-3 p-4" data-testid="mobile-approvals-queue">
<div className="flex items-center justify-between gap-3">
<h3 className="text-base font-semibold text-zinc-900">离线审批草稿队列</h3>
<span className="text-sm text-zinc-500">{currentQueueItems.length ? `待处理 ${currentQueueItems.length} 项` : "无待处理项"}</span>
<h3 className="text-base font-semibold text-zinc-900">离线草稿</h3>
<span className="text-sm text-zinc-500">{currentQueueItems.length ? `待同步 ${currentQueueItems.length}` : "无草稿"}</span>
</div>
{currentQueueItems.length ? (
@@ -1137,12 +1144,12 @@ export function MobileApprovalsPage() {
type="button"
variant="secondary"
>
{syncMutation.isPending ? "同步中..." : `同步草稿队列 (${retryableQueueItems.length})`}
{syncMutation.isPending ? "同步中..." : `同步草稿 (${retryableQueueItems.length})`}
</Button>
</div>
) : null}
{!currentQueueItems.length ? <p className="text-sm text-zinc-500">暂无待同步审批草稿。</p> : null}
{!currentQueueItems.length ? <p className="text-sm text-zinc-500">暂无待同步草稿。</p> : null}
{currentQueueItems.map((item) => (
<div key={item.id} className="rounded-lg border border-zinc-200 px-3 py-3 text-sm">
+20 -11
查看文件
@@ -341,7 +341,7 @@ export function ReimbursementsPage() {
<ListWorkspace>
<PageHeader
title="我的报销"
subtitle="报销列表、详情与提交/撤回/支付动作入口。"
subtitle="草稿、审批、支付。"
actions={
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
{showProxyOwnerSelector ? (
@@ -491,10 +491,10 @@ export function ReimbursementsPage() {
) : null}
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<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" ? "全范围" : "仅自己"}`} />
<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" ? "全范围" : "仅自己"} />
</section>
<FilterBar className="space-y-4">
@@ -566,7 +566,7 @@ export function ReimbursementsPage() {
{batchMode ? (
<div className="rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
当前只可勾选你本人可处理的 reviewing 报销待办。批量通过可不填意见,批量驳回必须填写统一理由。
仅勾选你可处理的审批中报销;驳回需填写理由。
</div>
) : null}
</FilterBar>
@@ -636,7 +636,7 @@ export function ReimbursementsPage() {
{item.pendingWorkflow?.task?.id ? (
<div className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700">
当前待办:{item.pendingWorkflow.task.stepName}
待办:{item.pendingWorkflow.task.stepName}
</div>
) : null}
@@ -653,7 +653,12 @@ export function ReimbursementsPage() {
选择
</label>
) : null}
<Button asChild className="min-h-10 flex-1" size="sm" variant="secondary">
<Button
asChild
className="min-h-11 flex-1"
size="sm"
variant={["draft", "rejected"].includes(item.status) ? "default" : "secondary"}
>
<Link
to={`/reimbursements/${item.id}`}
data-testid="reimbursement-mobile-detail-link"
@@ -720,7 +725,7 @@ export function ReimbursementsPage() {
) : null}
{item.pendingWorkflow?.task?.id ? (
<div className="text-xs text-amber-700">
当前待办:{item.pendingWorkflow.task.stepName} · task {item.pendingWorkflow.task.id}
待办:{item.pendingWorkflow.task.stepName}
</div>
) : null}
</td>
@@ -744,8 +749,12 @@ export function ReimbursementsPage() {
</td>
<td className="table-td">{item.reimbursementDate ?? "-"}</td>
<td className="table-td">
<Link to={`/reimbursements/${item.id}`} className="btn-ghost" data-testid="reimbursement-detail-link">
<span className="material-symbols-outlined text-[18px]">more_horiz</span>
<Link
to={`/reimbursements/${item.id}`}
className={["draft", "rejected"].includes(item.status) ? "btn-primary" : "btn-secondary"}
data-testid="reimbursement-detail-link"
>
{["draft", "rejected"].includes(item.status) ? "编辑" : "详情"}
</Link>
</td>
</tr>
+60 -7
查看文件
@@ -71,12 +71,13 @@ export function WorkflowTasksPage() {
});
const totalPages = Math.max(1, Math.ceil((query.data?.total ?? 0) / PAGE_SIZE));
const taskItems = query.data?.items ?? [];
return (
<div className="space-y-6">
<PageHeader title="待我处理" subtitle="审批待办列表,覆盖报销、请假、采购与差旅流程。" />
<div className="space-y-4 md:space-y-6">
<PageHeader title="待我处理" subtitle="待处理审批。" />
<div className="card-std p-6 space-y-4">
<div className="card-std space-y-4 p-4 md:p-5">
<Tabs
data-testid="workflow-tasks-status-tabs"
value={status}
@@ -116,7 +117,58 @@ export function WorkflowTasksPage() {
) : null}
{!query.isLoading && !query.isError ? (
<div className="rounded-lg border border-zinc-200">
<>
<div className="space-y-3 md:hidden" data-testid="workflow-tasks-mobile-list">
{taskItems.length ? (
taskItems.map((task) => (
<article className="rounded-xl border border-zinc-200 bg-white p-4 shadow-sm" key={task.id}>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-zinc-900">{task.stepName}</div>
<div className="mt-1 text-xs text-zinc-500">
{entityLabel(task.workflowInstance?.entityType)} · {new Date(task.createdAt).toLocaleString()}
</div>
</div>
<StatusBadge className="shrink-0" status={task.status} />
</div>
<div className="mt-3 truncate rounded-lg bg-zinc-50 px-3 py-2 font-mono text-xs text-zinc-500">
{task.workflowInstance?.entityId ?? task.id}
</div>
<div className="mt-3 grid grid-cols-2 gap-2">
{status === "pending" && (task.allowedActions ?? []).includes("approve") ? (
<Button
className="min-h-11"
size="sm"
type="button"
data-testid={TEST_IDS.workflow.quickApprove}
disabled={approveMutation.isPending && approveMutation.variables === task.id}
onClick={() => approveMutation.mutate(task.id)}
>
{approveMutation.isPending && approveMutation.variables === task.id ? "通过中..." : "通过"}
</Button>
) : null}
<Button
asChild
className={`min-h-11 ${
status === "pending" && (task.allowedActions ?? []).includes("approve") ? "" : "col-span-2"
}`}
size="sm"
variant="secondary"
data-testid={TEST_IDS.workflow.detailLink}
>
<Link to={`/workflow/tasks/${task.id}`}>详情</Link>
</Button>
</div>
</article>
))
) : (
<div className="rounded-xl border border-dashed border-zinc-300 px-4 py-8 text-center text-sm text-zinc-500">
暂无数据
</div>
)}
</div>
<div className="hidden rounded-lg border border-zinc-200 md:block">
<Table data-testid="workflow-tasks-table">
<TableHeader>
<tr>
@@ -128,8 +180,8 @@ export function WorkflowTasksPage() {
</tr>
</TableHeader>
<TableBody>
{(query.data?.items ?? []).length ? (
query.data?.items.map((task) => (
{taskItems.length ? (
taskItems.map((task) => (
<TableRow data-testid="workflow-task-row" key={task.id}>
<TableCell>
<div className="font-medium text-zinc-900">{task.stepName}</div>
@@ -147,7 +199,7 @@ export function WorkflowTasksPage() {
<TableCell>{new Date(task.createdAt).toLocaleString()}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-2">
{status === "pending" && task.allowedActions.includes("approve") ? (
{status === "pending" && (task.allowedActions ?? []).includes("approve") ? (
<Button
size="sm"
type="button"
@@ -180,6 +232,7 @@ export function WorkflowTasksPage() {
</TableBody>
</Table>
</div>
</>
) : null}
<Pagination
@@ -86,12 +86,12 @@ export function InvoiceDetailPanel({
}: InvoiceDetailPanelProps) {
return (
<>
<div className="card-std p-6 space-y-4" data-testid="invoice-detail-panel">
<h3 className="card-title">详情轮询</h3>
<div className="card-std space-y-4 p-4 md:p-5" data-testid="invoice-detail-panel">
<h3 className="card-title">票据详情</h3>
{detail ? (
<>
<div className="grid grid-cols-2 gap-y-3 pt-2 text-sm text-zinc-700">
<div><span className="text-zinc-500">票据ID:</span>{detail.id}</div>
<div><span className="text-zinc-500">票据 ID:</span>{detail.id}</div>
<div><span className="text-zinc-500">状态:</span>{detail.status}</div>
<div className="col-span-2">
<span className="text-zinc-500">发票号:</span>
@@ -120,7 +120,7 @@ export function InvoiceDetailPanel({
</div>
) : null}
</div>
{detail.ocrLastError ? <div className="col-span-2 text-red-600"><span className="text-zinc-500">OCR错误:</span>{formatInvoiceOcrErrorMessage(detail.ocrLastError)}</div> : null}
{detail.ocrLastError ? <div className="col-span-2 text-red-600"><span className="text-zinc-500">识别错误:</span>{formatInvoiceOcrErrorMessage(detail.ocrLastError)}</div> : null}
{detail.remarks ? <div className="col-span-2"><span className="text-zinc-500">备注:</span>{detail.remarks}</div> : null}
<div className="col-span-2">
<span className="text-zinc-500">判重例外:</span>
@@ -130,13 +130,13 @@ export function InvoiceDetailPanel({
{detail.status === "pending_ocr" ? (
<div className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-700" data-testid="invoice-ocr-status-banner">
OCR 正在处理中,当前页面会自动轮询;完成后可直接继续绑定、验真或查看详情。
正在识别,完成后自动刷新。
</div>
) : null}
{detail.status === "ocr_failed" ? (
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700" data-testid="invoice-ocr-status-banner">
{formatInvoiceOcrErrorMessage(detail.ocrLastError)} 可在下方直接重试 OCR,或补齐关键字段后标记为可用。
{formatInvoiceOcrErrorMessage(detail.ocrLastError)} 可重新识别,或人工修正后标记可用。
</div>
) : null}
@@ -187,7 +187,7 @@ export function InvoiceDetailPanel({
disabled={retryOcrMutation.isPending}
onClick={() => setConfirmAction({ type: "retryOcr", invoiceId: detail.id })}
>
重试 OCR
重新识别
</Button>
) : null}
@@ -431,12 +431,12 @@ export function InvoiceDetailPanel({
<ConfirmDialog
open={Boolean(confirmAction)}
title={
confirmAction?.type === "retryOcr" ? "确认重试 OCR" :
confirmAction?.type === "retryOcr" ? "确认重新识别" :
confirmAction?.type === "unbind" ? "确认解绑" :
confirmAction?.type === "archive" ? "确认归档" : "确认操作"
}
description={
confirmAction?.type === "retryOcr" ? "将重新进入 pending_ocr 并消耗一次尝试次数。" :
confirmAction?.type === "retryOcr" ? "票据会重新进入识别中,并消耗一次尝试次数。" :
confirmAction?.type === "unbind" ? "仅草稿/驳回报销允许解绑。" :
confirmAction?.type === "archive" ? "归档后将不再用于绑定与判重。" : ""
}
@@ -34,9 +34,9 @@ export function InvoiceListCard({
signedUrlMutation,
}: InvoiceListCardProps) {
return (
<div className="card-std p-6 space-y-4">
<div className="card-std space-y-4 p-4 md:p-5">
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<h3 className="card-title">筛选与列表</h3>
<h3 className="card-title">票据列表</h3>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<select
className="select-sm"
@@ -100,10 +100,10 @@ export function InvoiceListCard({
<div className="mt-3 rounded-lg bg-zinc-50 px-3 py-2 text-xs text-zinc-600">
{item.status === "pending_ocr"
? `OCR 识别中,重试 ${item.ocrAttempts} 次`
? `识别中,已尝试 ${item.ocrAttempts} 次`
: item.status === "ocr_failed"
? `OCR 失败:${formatInvoiceOcrErrorMessage(item.ocrLastError)}`
: "OCR 已完成或无需处理"}
? `识别失败:${formatInvoiceOcrErrorMessage(item.ocrLastError)}`
: "已识别"}
</div>
<div className="mt-3 grid grid-cols-2 gap-2">
@@ -113,7 +113,6 @@ export function InvoiceListCard({
onClick={() => setDetailInvoiceId(item.id)}
size="sm"
type="button"
variant="secondary"
>
查看详情
</Button>
@@ -145,7 +144,7 @@ export function InvoiceListCard({
<th className="table-th w-[280px]">票据流水号</th>
<th className="table-th w-[120px]">状态</th>
<th className="table-th w-[200px]">发票号码</th>
<th className="table-th">识别情况 (OCR)</th>
<th className="table-th">识别状态</th>
<th className="table-th w-[80px] text-right">操作</th>
</tr>
</thead>
@@ -172,7 +171,7 @@ export function InvoiceListCard({
识别失败 <span className="text-zinc-400 text-xs">({formatInvoiceOcrErrorMessage(item.ocrLastError)})</span>
</span>
) : (
<span className="text-zinc-400 text-xs">解析成功</span>
<span className="text-zinc-400 text-xs">已识别</span>
)}
</td>
<td className="table-td text-right">
@@ -185,7 +184,7 @@ export function InvoiceListCard({
查看详情
</DropdownMenuItem>
<DropdownMenuItem data-testid="invoice-preview-button" onSelect={() => signedUrlMutation.mutate(item.id)}>
下载 / 预览
预览
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -0,0 +1,7 @@
ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes
B1,R97 高频页面精修范围冻结,冻结本轮只处理报销列表 票据中心 移动审批 审批待办的页面密度和操作显著性,CSV 校验通过且不改后端 API 状态机 权限模型,命令 python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-05-06_17-37-53-r97-high-frequency-ui-polish.csv 业务复核本轮仅做 UI 精修,manual,DONE,DONE,DONE,issues/2026-05-06_17-37-53-r97-high-frequency-ui-polish.csv,[],已按范围冻结 回滚路径 删除本合同
B2,报销列表密度与草稿入口优化,优化报销列表首屏指标 筛选 草稿继续编辑入口和手机卡片可读性,手机和桌面都能快速找到草稿继续编辑 已提交单据状态和主操作清楚 无横向溢出,Playwright 跑 r92 手机报销草稿用例并补视觉截图,playwright|typescript,DONE,DONE,DONE,client/src/pages/ReimbursementsPage.tsx|client/tests/e2e/r92-mobile-invoice-usability.spec.ts,["B1"],r92 通过 不改报销业务状态 回滚路径 恢复报销列表改动
B3,票据中心列表详情减噪,优化票据中心上传 列表 绑定 详情区的主次层级和移动端文案,上传区更短 列表和详情主操作更明显 OCR 技术词减少 手机票据中心无出屏,Playwright 跑 r92 手机票据中心用例并检查上传与详情可见性,playwright|typescript,DONE,DONE,DONE,client/src/pages/InvoicePoolPage.tsx|client/src/pages/invoice-pool,["B1"],r92 通过 不改 OCR provider 和状态机 回滚路径 恢复票据页面改动
B4,移动审批操作区精简,优化移动审批的筛选 搜索 批量处理和详情动作展示,默认只显示待处理与搜索 高级筛选和批量处理收起 详情动作清楚可点,Playwright 跑 r89 移动审批路径并截图,playwright|typescript,DONE,DONE,DONE,client/src/pages/MobileApprovalsPage.tsx|client/tests/e2e/r89-mobile-login-navigation.spec.ts,["B1"],r89 通过 不改离线队列语义 回滚路径 恢复移动审批页面改动
B5,审批待办桌面列表降噪,优化审批待办桌面列表标题 文案和操作密度,桌面待办页标题短 页面不再说明书化 快捷审批入口保持可用,Playwright 或 typecheck/build 验证待办页可渲染,playwright|typescript,DONE,DONE,DONE,client/src/pages/WorkflowTasksPage.tsx,["B1"],visual-shell 通过 不改 workflow API 回滚路径 恢复审批待办页面文案
B6,R97 视觉回归与中文提交,补充本轮高频页面视觉回归报告并分批中文提交,typecheck build r89 r92 visual-shell 通过 视觉报告写入 artifacts/r97-ui git status 干净,命令 cd client && npm run typecheck && npm run build 加 r89 r92 visual-shell Playwright 回归,playwright|git,DONE,DONE,DONE,artifacts/r97-ui|client/tests/e2e,"[""B2"",""B3"",""B4"",""B5""]",typecheck build r89 r92 visual-shell 均通过 截图产物不纳入 git 回滚路径 删除报告并 revert 对应提交
无法渲染此文件,因为它包含了意外字符,其位于第 3 行和第 433 列。