OAFRONT4 清理PC付款和工作台文案

这个提交包含在:
2026-05-25 07:47:52 +08:00
父节点 7937c76ebd
当前提交 fa5ed0337d
共修改 13 个文件,包含 115 行新增和 48 行删除
+1 -1
查看文件
@@ -11,7 +11,7 @@ function readStringList(value: unknown): string[] {
} }
const FEATURE_FLAG_DISABLED_MESSAGES: Record<string, string> = { const FEATURE_FLAG_DISABLED_MESSAGES: Record<string, string> = {
portal_home_v1_disabled: "Portal 首页当前未启用,请联系管理员检查环境开关与功能开关配置。", portal_home_v1_disabled: "工作台首页当前未启用,请联系管理员检查环境开关与功能开关配置。",
announcements_v1_disabled: "公告模块当前未启用,请联系管理员检查环境开关与功能开关配置。", announcements_v1_disabled: "公告模块当前未启用,请联系管理员检查环境开关与功能开关配置。",
presence_board_v1_disabled: "团队状态模块当前未启用,请联系管理员检查环境开关与功能开关配置。", presence_board_v1_disabled: "团队状态模块当前未启用,请联系管理员检查环境开关与功能开关配置。",
attendance_v1_disabled: "历史 OA 自建打卡已关闭,请在企业微信打卡。", attendance_v1_disabled: "历史 OA 自建打卡已关闭,请在企业微信打卡。",
@@ -8,6 +8,8 @@ const STATUS_MAP: Record<string, { label: string; className: string }> = {
pending: { label: "待派发", className: "badge-warning" }, pending: { label: "待派发", className: "badge-warning" },
processing: { label: "处理中", className: "badge-warning" }, processing: { label: "处理中", className: "badge-warning" },
succeeded: { label: "已成功", className: "badge-success" }, succeeded: { label: "已成功", className: "badge-success" },
reviewed: { label: "已复核", className: "badge-primary" },
executed: { label: "已执行", className: "badge-success" },
failed: { label: "失败", className: "badge-error" }, failed: { label: "失败", className: "badge-error" },
dead_letter: { label: "死信", className: "badge-error" }, dead_letter: { label: "死信", className: "badge-error" },
not_triggered: { label: "未触发", className: "badge-neutral" }, not_triggered: { label: "未触发", className: "badge-neutral" },
@@ -18,7 +18,7 @@ export type NavItem = {
}; };
export const NAV_ITEMS: NavItem[] = [ export const NAV_ITEMS: NavItem[] = [
{ to: "/portal", label: "Portal", icon: "home_app_logo" }, { to: "/portal", label: "工作台", icon: "home_app_logo" },
{ to: "/team", label: "团队", icon: "groups" }, { to: "/team", label: "团队", icon: "groups" },
{ to: "/leave-requests", label: "请假", icon: "event_available", anyOf: ["leave.view_own", "leave.view_all", "leave.manage"] }, { to: "/leave-requests", label: "请假", icon: "event_available", anyOf: ["leave.view_own", "leave.view_all", "leave.manage"] },
{ to: "/attendance", label: "考勤", icon: "schedule" }, { to: "/attendance", label: "考勤", icon: "schedule" },
@@ -40,7 +40,7 @@ export const NAV_ITEMS: NavItem[] = [
{ to: "/purchase-requests", label: "采购申请", icon: "shopping_cart", anyOf: ["procurement.create", "procurement.view_own", "procurement.view_all", "procurement.manage"] }, { 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: "/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: "/budgets", label: "预算管理", icon: "account_balance", anyOf: ["budget.view", "budget.manage"] },
{ to: "/payment-batches", label: "支付批次", icon: "payments", anyOf: ["payment.batch.manage", "risk.review.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: "/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: "/invoice-pool", label: "票据中心", icon: "receipt_long", anyOf: ["invoice.manage"] },
{ to: "/inventory-checks", label: "资产盘点", icon: "inventory", anyOf: ["asset.inventory"] }, { to: "/inventory-checks", label: "资产盘点", icon: "inventory", anyOf: ["asset.inventory"] },
+32
查看文件
@@ -0,0 +1,32 @@
export function formatDisplayDate(value: string | null | undefined) {
if (!value) return "-";
const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
if (match) {
const [, year, month, day] = match;
return `${Number(year)}年${Number(month)}月${Number(day)}日`;
}
const timestamp = new Date(value).getTime();
if (Number.isNaN(timestamp)) return "-";
return new Intl.DateTimeFormat("zh-CN", {
year: "numeric",
month: "long",
day: "numeric",
}).format(timestamp);
}
export function formatDisplayDateTime(value: string | null | undefined) {
if (!value) return "-";
const timestamp = new Date(value).getTime();
if (Number.isNaN(timestamp)) return formatDisplayDate(value);
return new Intl.DateTimeFormat("zh-CN", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(timestamp);
}
+2 -2
查看文件
@@ -646,7 +646,7 @@ export function ApOperationsPage() {
<SectionError message={resolveErrorMessage(summaryQuery.error, "AP 摘要加载失败")} /> <SectionError message={resolveErrorMessage(summaryQuery.error, "AP 摘要加载失败")} />
) : null} ) : null}
{paymentBatchesQuery.isError ? ( {paymentBatchesQuery.isError ? (
<SectionError message={resolveErrorMessage(paymentBatchesQuery.error, "支付批次数据加载失败")} /> <SectionError message={resolveErrorMessage(paymentBatchesQuery.error, "付款批次数据加载失败")} />
) : null} ) : null}
{riskFlagsQuery.isError ? ( {riskFlagsQuery.isError ? (
<SectionError message={resolveErrorMessage(riskFlagsQuery.error, "风险标记数据加载失败")} /> <SectionError message={resolveErrorMessage(riskFlagsQuery.error, "风险标记数据加载失败")} />
@@ -1596,7 +1596,7 @@ export function ApOperationsPage() {
title="确认取消付款批次" title="确认取消付款批次"
description={ description={
cancelTarget cancelTarget
? `将取消 ${cancelTarget.batchNo},该操作与支付批次页保持一致。` ? `将取消 ${cancelTarget.batchNo},该操作与付款批次页保持一致。`
: undefined : undefined
} }
variant="destructive" variant="destructive"
+18 -9
查看文件
@@ -28,6 +28,7 @@ import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label"; import { Label } from "../components/ui/label";
import { Textarea } from "../components/ui/textarea"; import { Textarea } from "../components/ui/textarea";
import { downloadBlob } from "../lib/download"; import { downloadBlob } from "../lib/download";
import { formatDisplayDate } from "../lib/displayFormat";
import { toast } from "sonner"; import { toast } from "sonner";
const CONFIDENTIALITY_OPTIONS: ConfidentialityLevel[] = ["public", "internal", "confidential", "restricted"]; const CONFIDENTIALITY_OPTIONS: ConfidentialityLevel[] = ["public", "internal", "confidential", "restricted"];
@@ -39,9 +40,9 @@ const FILTER_STATUS_OPTIONS: Array<{ value: ContractRegistryStatus; label: strin
{ value: "archived", label: "已归档" }, { value: "archived", label: "已归档" },
]; ];
const EDITOR_STATUS_OPTIONS: Array<{ value: ContractRegistryPersistedStatus; label: string }> = [ const EDITOR_STATUS_OPTIONS: Array<{ value: ContractRegistryPersistedStatus; label: string }> = [
{ value: "active", label: "active" }, { value: "active", label: "生效中" },
{ value: "draft", label: "draft" }, { value: "draft", label: "草稿" },
{ value: "archived", label: "archived" }, { value: "archived", label: "已归档" },
]; ];
const AMOUNT_PATTERN = /^\d{1,12}(\.\d{1,2})?$/; const AMOUNT_PATTERN = /^\d{1,12}(\.\d{1,2})?$/;
const EXPIRING_DAYS = 30; const EXPIRING_DAYS = 30;
@@ -95,6 +96,14 @@ function contractTypeLabel(value: ContractRegistryType) {
return "其他"; return "其他";
} }
function contractStatusLabel(value: ContractRegistryStatus | ContractRegistryPersistedStatus) {
if (value === "active") return "生效中";
if (value === "expired") return "已过期";
if (value === "draft") return "草稿";
if (value === "archived") return "已归档";
return value || "-";
}
function buildEmptyEditor(projectId = ""): EditorState { function buildEmptyEditor(projectId = ""): EditorState {
return { return {
contractNo: "", contractNo: "",
@@ -346,7 +355,7 @@ export function ContractsRegistryPage() {
{item.contractNo} · {item.counterpartyName} {item.contractNo} · {item.counterpartyName}
</div> </div>
<div className="mt-1 text-sm text-zinc-600"> <div className="mt-1 text-sm text-zinc-600">
{contractTypeLabel(item.contractType)} · 到期日 {item.expiryDate ?? "-"} · 责任人 {item.owner?.name ?? "-"} {contractTypeLabel(item.contractType)} · 到期日 {formatDisplayDate(item.expiryDate)} · 责任人 {item.owner?.name ?? "-"}
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
@@ -389,7 +398,7 @@ export function ContractsRegistryPage() {
{editingItem?.status === "expired" ? ( {editingItem?.status === "expired" ? (
<div className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900"> <div className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900">
当前合同因到期日已过,列表状态显示为“已过期”;保存时仍以数据库状态 {editingItem.persistedStatus} 持久化。 当前合同因到期日已过,列表状态显示为“已过期”;保存时仍按“{contractStatusLabel(editingItem.persistedStatus)}”留存。
</div> </div>
) : null} ) : null}
@@ -705,7 +714,7 @@ export function ContractsRegistryPage() {
<div className="space-y-2"> <div className="space-y-2">
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<div className="font-semibold text-zinc-900">{item.contractNo}</div> <div className="font-semibold text-zinc-900">{item.contractNo}</div>
<StatusBadge status={item.status} /> <StatusBadge status={item.status} label={contractStatusLabel(item.status)} />
<span className="rounded-full bg-zinc-100 px-2 py-1 text-xs text-zinc-600"> <span className="rounded-full bg-zinc-100 px-2 py-1 text-xs text-zinc-600">
{confidentialityLabel(item.confidentialityLevel)} {confidentialityLabel(item.confidentialityLevel)}
</span> </span>
@@ -715,14 +724,14 @@ export function ContractsRegistryPage() {
<div className="text-xs text-zinc-500"> <div className="text-xs text-zinc-500">
{contractTypeLabel(item.contractType)} {contractTypeLabel(item.contractType)}
{item.amount ? ` · ${item.amount} ${item.currency}` : " · 金额未录入"} {item.amount ? ` · ${item.amount} ${item.currency}` : " · 金额未录入"}
{item.expiryDate ? ` · 到期日 ${item.expiryDate}` : " · 未设置到期日"} {item.expiryDate ? ` · 到期日 ${formatDisplayDate(item.expiryDate)}` : " · 未设置到期日"}
</div> </div>
<div className="text-xs text-zinc-500"> <div className="text-xs text-zinc-500">
项目:{item.project ? `${item.project.projectNo} · ${item.project.name}` : "未关联"} · 责任人: 项目:{item.project ? `${item.project.projectNo} · ${item.project.name}` : "未关联"} · 责任人:
{item.owner?.name ?? "-"} {item.owner?.name ?? "-"}
</div> </div>
<div className="text-xs text-zinc-500"> <div className="text-xs text-zinc-500">
签署:{item.signedDate ?? "-"} · 生效:{item.effectiveDate ?? "-"} · 归档:{item.archiveLocation} 签署:{formatDisplayDate(item.signedDate)} · 生效:{formatDisplayDate(item.effectiveDate)} · 归档:{item.archiveLocation}
{item.offlineArchiveNo ? ` · 离线档案号 ${item.offlineArchiveNo}` : ""} {item.offlineArchiveNo ? ` · 离线档案号 ${item.offlineArchiveNo}` : ""}
</div> </div>
</div> </div>
@@ -760,7 +769,7 @@ export function ContractsRegistryPage() {
disabled={toggleStatusMutation.isPending} disabled={toggleStatusMutation.isPending}
onClick={() => toggleStatusMutation.mutate(item)} onClick={() => toggleStatusMutation.mutate(item)}
> >
{item.persistedStatus === "archived" ? "恢复为 active" : "归档"} {item.persistedStatus === "archived" ? "恢复生效" : "归档"}
</Button> </Button>
</div> </div>
</div> </div>
+2 -2
查看文件
@@ -50,8 +50,8 @@ function toDisplayName(flagKey: string) {
if (flagKey === "invoice_verification") return "发票验真"; if (flagKey === "invoice_verification") return "发票验真";
if (flagKey === "leave_holiday_calendar") return "请假日历增强"; if (flagKey === "leave_holiday_calendar") return "请假日历增强";
if (flagKey === "budget_precheck_v1") return "预算预校验"; if (flagKey === "budget_precheck_v1") return "预算预校验";
if (flagKey === "payment_batch_v1") return "支付批次"; if (flagKey === "payment_batch_v1") return "付款批次";
if (flagKey === "portal_home_v1") return "Portal 首页"; if (flagKey === "portal_home_v1") return "工作台首页";
if (flagKey === "global_search_v1") return "全局搜索"; if (flagKey === "global_search_v1") return "全局搜索";
if (flagKey === "announcements_v1") return "公告中心"; if (flagKey === "announcements_v1") return "公告中心";
if (flagKey === "workflow_definition_admin_v1") return "流程定义治理"; if (flagKey === "workflow_definition_admin_v1") return "流程定义治理";
+42 -18
查看文件
@@ -27,7 +27,9 @@ import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input"; import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label"; import { Label } from "../components/ui/label";
import { RiskFlagsCard } from "../components/common/RiskFlagsCard"; import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
import { StatusBadge } from "../components/common/StatusBadge";
import { downloadBlob } from "../lib/download"; import { downloadBlob } from "../lib/download";
import { formatDisplayDate, formatDisplayDateTime } from "../lib/displayFormat";
import { toast } from "sonner"; import { toast } from "sonner";
const PAYMENT_CHANNEL_OPTIONS: PaymentChannel[] = ["bank", "alipay", "wechat", "cash", "other"]; const PAYMENT_CHANNEL_OPTIONS: PaymentChannel[] = ["bank", "alipay", "wechat", "cash", "other"];
@@ -47,6 +49,31 @@ function entityTypeLabel(value: string) {
return value || "-"; return value || "-";
} }
function paymentChannelLabel(value: PaymentChannel | string | null | undefined) {
if (value === "bank") return "银行转账";
if (value === "alipay") return "支付宝";
if (value === "wechat") return "微信";
if (value === "cash") return "现金";
if (value === "other") return "其他";
return value || "-";
}
function paymentItemStatusLabel(value: string) {
if (value === "pending") return "待付款";
if (value === "paid") return "已支付";
if (value === "executed") return "已执行";
if (value === "failed") return "付款失败";
if (value === "cancelled") return "已取消";
if (value === "skipped") return "已跳过";
return value || "-";
}
function reviewNextStepLabel(value: PaymentBatch["reviewRequirement"]["nextStep"]) {
if (value === "first_review") return "等待首轮复核";
if (value === "second_review") return "等待二次复核";
return "可执行付款标记";
}
function formatFileSize(value: number) { function formatFileSize(value: number) {
if (!Number.isFinite(value) || value <= 0) return "-"; if (!Number.isFinite(value) || value <= 0) return "-";
if (value < 1024) return `${value} B`; if (value < 1024) return `${value} B`;
@@ -54,11 +81,6 @@ function formatFileSize(value: number) {
return `${(value / 1024 / 1024).toFixed(1)} MB`; return `${(value / 1024 / 1024).toFixed(1)} MB`;
} }
function formatDateTime(value: string | null | undefined) {
if (!value) return "-";
return new Date(value).toLocaleString();
}
function resolveError(error: unknown) { function resolveError(error: unknown) {
return isApiError(error) return isApiError(error)
? `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}` ? `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`
@@ -303,7 +325,7 @@ export function PaymentBatchesPage() {
return ( return (
<ListWorkspace> <ListWorkspace>
<WorkspaceHeader title="支付批次" subtitle="财务批量付款、复核与执行;高风险批次达到 5 万元后需完成二次复核。" /> <WorkspaceHeader title="付款批次" subtitle="线下银行付款后的 OA 复核、回单留存和已付款标记;高风险批次达到 5 万元后需完成二次复核。" />
{error ? <div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{error}</div> : null} {error ? <div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{error}</div> : null}
@@ -337,13 +359,13 @@ export function PaymentBatchesPage() {
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>实体 ID</Label> <Label>待付款单据 ID</Label>
<textarea <textarea
className="min-h-28 w-full rounded-lg border border-zinc-300 px-3 py-2 text-sm disabled:bg-zinc-50" className="min-h-28 w-full rounded-lg border border-zinc-300 px-3 py-2 text-sm disabled:bg-zinc-50"
value={entityIds} value={entityIds}
disabled={!canManageBatch} disabled={!canManageBatch}
onChange={(event) => setEntityIds(event.target.value)} onChange={(event) => setEntityIds(event.target.value)}
placeholder="用逗号或换行分隔 reimbursement / travel_order ID" placeholder="输入报销或差旅待付款单据 ID,多个用逗号或换行分隔"
/> />
</div> </div>
<div className="grid gap-3 md:grid-cols-2"> <div className="grid gap-3 md:grid-cols-2">
@@ -365,7 +387,7 @@ export function PaymentBatchesPage() {
onChange={(event) => setDefaultPaymentChannel((event.target.value as PaymentChannel | "") || "") } onChange={(event) => setDefaultPaymentChannel((event.target.value as PaymentChannel | "") || "") }
> >
<option value="">不设置</option> <option value="">不设置</option>
{PAYMENT_CHANNEL_OPTIONS.map((item) => <option key={item} value={item}>{item}</option>)} {PAYMENT_CHANNEL_OPTIONS.map((item) => <option key={item} value={item}>{paymentChannelLabel(item)}</option>)}
</select> </select>
</div> </div>
</div> </div>
@@ -400,7 +422,7 @@ export function PaymentBatchesPage() {
> >
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div className="font-medium text-zinc-900">{item.batchNo}</div> <div className="font-medium text-zinc-900">{item.batchNo}</div>
<div className="text-xs uppercase text-zinc-500">{item.status}</div> <StatusBadge status={item.status} label={batchStatusLabel(item.status)} />
</div> </div>
<div className="mt-1 text-sm text-zinc-600">{item.name}</div> <div className="mt-1 text-sm text-zinc-600">{item.name}</div>
<div className="mt-1 text-xs text-zinc-500"> <div className="mt-1 text-xs text-zinc-500">
@@ -410,7 +432,7 @@ export function PaymentBatchesPage() {
</div> </div>
</button> </button>
))} ))}
{!listQuery.data?.items?.length ? <div className="text-sm text-zinc-500">暂无支付批次</div> : null} {!listQuery.data?.items?.length ? <div className="text-sm text-zinc-500">暂无付款批次</div> : null}
</div> </div>
</div> </div>
@@ -475,13 +497,13 @@ export function PaymentBatchesPage() {
<div>状态:{batchStatusLabel(selectedBatch.status)}</div> <div>状态:{batchStatusLabel(selectedBatch.status)}</div>
<div>实体:{entityTypeLabel(selectedBatch.entityType)}</div> <div>实体:{entityTypeLabel(selectedBatch.entityType)}</div>
<div>总金额:{selectedBatch.totalAmount}</div> <div>总金额:{selectedBatch.totalAmount}</div>
<div>默认日期:{selectedBatch.defaultPaymentDate ?? "-"}</div> <div>默认日期:{formatDisplayDate(selectedBatch.defaultPaymentDate)}</div>
<div>默认渠道:{selectedBatch.defaultPaymentChannel ?? "-"}</div> <div>默认渠道:{paymentChannelLabel(selectedBatch.defaultPaymentChannel)}</div>
<div>复核进度:{selectedBatch.reviewRequirement.completedReviewCount}/{selectedBatch.reviewRequirement.requiredReviewCount}</div> <div>复核进度:{selectedBatch.reviewRequirement.completedReviewCount}/{selectedBatch.reviewRequirement.requiredReviewCount}</div>
</div> </div>
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-3 text-sm text-zinc-600"> <div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-3 text-sm text-zinc-600">
当前要求:{selectedBatch.reviewRequirement.requiredReviewCount === 2 ? `批次金额达到 ${selectedBatch.reviewRequirement.thresholdAmount},需两轮复核` : "单轮复核后可执行"}。 当前要求:{selectedBatch.reviewRequirement.requiredReviewCount === 2 ? `批次金额达到 ${selectedBatch.reviewRequirement.thresholdAmount},需两轮复核` : "单轮复核后可执行"}。
下一步:{selectedBatch.reviewRequirement.nextStep === "first_review" ? "等待首轮复核" : selectedBatch.reviewRequirement.nextStep === "second_review" ? "等待二次复核" : "可执行"}。 下一步:{reviewNextStepLabel(selectedBatch.reviewRequirement.nextStep)}。
{selectedBatch.reviewedBy ? ` 首轮复核人:${selectedBatch.reviewedBy}` : ""} {selectedBatch.reviewedBy ? ` 首轮复核人:${selectedBatch.reviewedBy}` : ""}
{selectedBatch.secondaryReviewedBy ? ` 二次复核人:${selectedBatch.secondaryReviewedBy}` : ""} {selectedBatch.secondaryReviewedBy ? ` 二次复核人:${selectedBatch.secondaryReviewedBy}` : ""}
</div> </div>
@@ -538,7 +560,7 @@ export function PaymentBatchesPage() {
<div className="min-w-0"> <div className="min-w-0">
<div className="truncate text-sm font-medium text-zinc-900">{attachment.originalName}</div> <div className="truncate text-sm font-medium text-zinc-900">{attachment.originalName}</div>
<div className="mt-1 text-xs text-zinc-500"> <div className="mt-1 text-xs text-zinc-500">
{attachment.mimeType} · {formatFileSize(attachment.fileSize)} · {formatDateTime(attachment.createdAt)} {attachment.mimeType} · {formatFileSize(attachment.fileSize)} · {formatDisplayDateTime(attachment.createdAt)}
{attachment.uploadedBy ? ` · ${attachment.uploadedBy.name}` : ""} {attachment.uploadedBy ? ` · ${attachment.uploadedBy.name}` : ""}
</div> </div>
</div> </div>
@@ -584,7 +606,7 @@ export function PaymentBatchesPage() {
<div className="font-medium text-zinc-900">{item.entityCode}</div> <div className="font-medium text-zinc-900">{item.entityCode}</div>
<div className="text-sm text-zinc-600">{item.entityTitle}</div> <div className="text-sm text-zinc-600">{item.entityTitle}</div>
</div> </div>
<div className="text-sm text-zinc-600">{item.amount} {item.currencyCode} · {item.status}</div> <div className="text-sm text-zinc-600">{item.amount} {item.currencyCode} · {paymentItemStatusLabel(item.status)}</div>
</div> </div>
<div className="mt-3 grid gap-3 md:grid-cols-4"> <div className="mt-3 grid gap-3 md:grid-cols-4">
<Input <Input
@@ -606,9 +628,11 @@ export function PaymentBatchesPage() {
onChange={(event) => updateItemMutation.mutate({ batchId: selectedBatch.id, itemId: item.id, paymentChannelOverride: (event.target.value as PaymentChannel | "") || null })} onChange={(event) => updateItemMutation.mutate({ batchId: selectedBatch.id, itemId: item.id, paymentChannelOverride: (event.target.value as PaymentChannel | "") || null })}
> >
<option value="">默认渠道</option> <option value="">默认渠道</option>
{PAYMENT_CHANNEL_OPTIONS.map((option) => <option key={option} value={option}>{option}</option>)} {PAYMENT_CHANNEL_OPTIONS.map((option) => <option key={option} value={option}>{paymentChannelLabel(option)}</option>)}
</select> </select>
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-600">失败:{item.failureMessage ?? "-"}</div> <div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-600">
{item.failureMessage ? `失败原因:${item.failureMessage}` : "付款异常:无"}
</div>
</div> </div>
<RiskFlagsCard items={item.openRiskFlags} title="关联开放风险" /> <RiskFlagsCard items={item.openRiskFlags} title="关联开放风险" />
</div> </div>
+2 -2
查看文件
@@ -848,13 +848,13 @@ export function PortalHomePage() {
const gettingStartedCompletedCount = gettingStartedItems.filter((item) => item.completed).length; const gettingStartedCompletedCount = gettingStartedItems.filter((item) => item.completed).length;
if (portalQuery.isLoading) { if (portalQuery.isLoading) {
return <div className="card-std p-6 text-sm text-zinc-500">加载 Portal 中...</div>; return <div className="card-std p-6 text-sm text-zinc-500">加载工作台中...</div>;
} }
if (portalQuery.isError || !portalQuery.data) { if (portalQuery.isError || !portalQuery.data) {
const message = isApiError(portalQuery.error) const message = isApiError(portalQuery.error)
? `${portalQuery.error.code}: ${mapApiErrorMessage(portalQuery.error.code, portalQuery.error.details, portalQuery.error.message)}` ? `${portalQuery.error.code}: ${mapApiErrorMessage(portalQuery.error.code, portalQuery.error.details, portalQuery.error.message)}`
: "Portal 加载失败"; : "工作台加载失败";
return <div className="card-std p-6 text-sm text-red-600">{message}</div>; return <div className="card-std p-6 text-sm text-red-600">{message}</div>;
} }
+1 -1
查看文件
@@ -134,7 +134,7 @@ export function WeeklyReportsPage() {
<FilterBar className="flex flex-wrap items-center justify-between gap-3"> <FilterBar className="flex flex-wrap items-center justify-between gap-3">
<div className="space-y-1"> <div className="space-y-1">
<div className="text-sm font-medium text-zinc-900">按周归档</div> <div className="text-sm font-medium text-zinc-900">按周归档</div>
<div className="text-sm text-zinc-500">默认按周一归档;提交后会同步更新 Portal 与项目摘要。</div> <div className="text-sm text-zinc-500">默认按周一归档;提交后会同步更新工作台与项目摘要。</div>
</div> </div>
<input <input
aria-label="周报归档周" aria-label="周报归档周"
+2 -2
查看文件
@@ -9,7 +9,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from ".
import { toast } from "sonner"; import { toast } from "sonner";
const MODULE_LABELS: Record<string, string> = { const MODULE_LABELS: Record<string, string> = {
portal: "Portal", portal: "工作台",
team: "团队", team: "团队",
leaveRequests: "请假", leaveRequests: "请假",
attendance: "考勤", attendance: "考勤",
@@ -37,7 +37,7 @@ const MODULE_LABELS: Record<string, string> = {
purchaseRequests: "采购申请", purchaseRequests: "采购申请",
travelRequests: "差旅申请", travelRequests: "差旅申请",
budgets: "预算管理", budgets: "预算管理",
paymentBatches: "支付批次", paymentBatches: "付款批次",
apOperations: "AP 运营", apOperations: "AP 运营",
invoicePool: "票据中心", invoicePool: "票据中心",
inventoryChecks: "资产盘点", inventoryChecks: "资产盘点",
@@ -1,11 +1,11 @@
"ID","Title","Description","Acceptance","Test_Method","Tools","Dev_Status","Review1_Status","Regression_Status","Files","Dependencies","Notes" "ID","Title","Description","Acceptance","Test_Method","Tools","Dev_Status","Review1_Status","Regression_Status","Files","Dependencies","Notes"
"OAFRONT4-0","建立 PC 启用前 UI 清理执行合同","基于 OAFRONT3 公网 PC 只读视觉证据,规划正式启用前最小 PC 体验清理批次;本合同只规划任务,不直接修改业务代码、不部署、不写公网业务数据。","本 CSV 校验通过;任务按 P0/P1/P2 分层;每项都有业务流程、受影响文件、自动化验证和人工门禁;明确不做大重构、不改变权限、审批状态机、付款执行或数据库 schema。","受影响流程:OAFRONT3 证据 -> OAFRONT4 任务拆分 -> 用户审查采纳 -> 后续实施合同;验证步骤:validate_issues_csv.py 本 CSV;check-process-contract general;git diff --check。","python3|node|git|manual","DONE","DONE","DONE","issues/2026-05-25_00-55-14-oa-pc-launch-ui-cleanup-v1.csv","[]","MAGI 审视:付款批次 UUID/英文状态为 P1;Portal 本地化覆盖面最大;合同/资产/票据/采购日期与码值为 P1/P2。执行:拆成小批次避免一次性视觉重排。提升:先修影响正式启用专业感和财务闭环的硬问题,Portal hero 和页面拆分后置。" "OAFRONT4-0","建立 PC 启用前 UI 清理执行合同","基于 OAFRONT3 公网 PC 只读视觉证据,规划正式启用前最小 PC 体验清理批次;本合同只规划任务,不直接修改业务代码、不部署、不写公网业务数据。","本 CSV 校验通过;任务按 P0/P1/P2 分层;每项都有业务流程、受影响文件、自动化验证和人工门禁;明确不做大重构、不改变权限、审批状态机、付款执行或数据库 schema。","受影响流程:OAFRONT3 证据 -> OAFRONT4 任务拆分 -> 用户审查采纳 -> 后续实施合同;验证步骤:validate_issues_csv.py 本 CSV;check-process-contract general;git diff --check。","python3|node|git|manual","DONE","DONE","DONE","issues/2026-05-25_00-55-14-oa-pc-launch-ui-cleanup-v1.csv","[]","MAGI 审视:付款批次 UUID/英文状态为 P1;Portal 本地化覆盖面最大;合同/资产/票据/采购日期与码值为 P1/P2。执行:拆成小批次避免一次性视觉重排。提升:先修影响正式启用专业感和财务闭环的硬问题,Portal hero 和页面拆分后置。"
"OAFRONT4-1","把 OAFRONT3 PC 只读矩阵纳入后续 UI 回归门禁","复用已新增的 PC no-write visual evidence,作为后续 PC 页面清理前后对比门禁;避免继续使用会创建报销草稿的 project-wide visual 公网路径。","后续 PC UI 实现前先跑一次 OAFRONT3 矩阵作为 baseline;实现后复跑并比较 P1/P2 数量;报告只含角色、路由、截图路径、脱敏指标,不含密码、token、Secret、完整账号、票据原图、合同正文或银行回单。","受影响流程:PC 登录 -> 角色化只读访问 -> 截图/DOM 指标 -> 变更前后对比;验证步骤:命令 bash scripts/qa/run-oafront3-pc-readonly-visual.sh;检查 summary.json 为 PASS 或 PASS_WITH_FINDINGS 且隐私扫描通过。","playwright|bash|node","TODO","TODO","TODO","scripts/qa/run-oafront3-pc-readonly-visual.sh;client/tests/e2e/oafront3-pc-readonly-visual.spec.ts;artifacts/qa-flow/oafront3-pc-readonly-visual-*","[""OAFRONT4-0""]","优先级 P0。只读;不创建业务数据;若公网账号缺失则记录 SKIP/BLOCKED_BY_EXTERNAL_CONFIG,不伪造成通过。" "OAFRONT4-1","把 OAFRONT3 PC 只读矩阵纳入后续 UI 回归门禁","复用已新增的 PC no-write visual evidence,作为后续 PC 页面清理前后对比门禁;避免继续使用会创建报销草稿的 project-wide visual 公网路径。","后续 PC UI 实现前先跑一次 OAFRONT3 矩阵作为 baseline;实现后复跑并比较 P1/P2 数量;报告只含角色、路由、截图路径、脱敏指标,不含密码、token、Secret、完整账号、票据原图、合同正文或银行回单。","受影响流程:PC 登录 -> 角色化只读访问 -> 截图/DOM 指标 -> 变更前后对比;验证步骤:命令 bash scripts/qa/run-oafront3-pc-readonly-visual.sh;检查 summary.json 为 PASS 或 PASS_WITH_FINDINGS 且隐私扫描通过。","playwright|bash|node","TODO","TODO","TODO","scripts/qa/run-oafront3-pc-readonly-visual.sh;client/tests/e2e/oafront3-pc-readonly-visual.spec.ts;artifacts/qa-flow/oafront3-pc-readonly-visual-*","[""OAFRONT4-0""]","优先级 P0。只读;不创建业务数据;若公网账号缺失则记录 SKIP/BLOCKED_BY_EXTERNAL_CONFIG,不伪造成通过。"
"OAFRONT4-2","付款批次 PC 主展示去内部化","修复付款批次页面在 finance/finance_mgr 视角暴露内部 UUID、英文状态 EXECUTED/DRAFT、日期英文占位和长内部流水的问题;保持付款批次真实执行仍走线下银行,不新增真实支付能力。","付款批次列表和详情主展示使用付款批次号、实体业务编号、中文状态、中文日期展示;内部 UUID 只允许管理员审计/调试折叠区可见;不改变付款批次创建、上传回单、执行、取消、复核逻辑。","受影响流程:财务登录 -> 打开付款批次 -> 查看批次列表/详情/批次项 -> 上传回单入口可见但不自动上传 -> 不执行真实付款;验证步骤:cd client && npm run typecheck && npm run build;bash scripts/qa/run-oafront3-pc-readonly-visual.sh;finance/finance_mgr /payment-batches 不再有 P1 UUID 主展示,英文状态和日期占位显著下降。","typescript|playwright|manual","TODO","TODO","TODO","client/src/pages/PaymentBatchesPage.tsx;client/src/api/paymentBatches.ts;client/src/lib/formatters.ts","[""OAFRONT4-1""]","优先级 P0/P1。不得改后端状态机、不得执行真实付款、不得上传银行流水原件;如需新增纯前端 label helper 可复用既有格式化工具。" "OAFRONT4-2","付款批次 PC 主展示去内部化","修复付款批次页面在 finance/finance_mgr 视角暴露内部 UUID、英文状态 EXECUTED/DRAFT、日期英文占位和长内部流水的问题;保持付款批次真实执行仍走线下银行,不新增真实支付能力。","付款批次列表和详情主展示使用付款批次号、实体业务编号、中文状态、中文日期展示;内部 UUID 只允许管理员审计/调试折叠区可见;不改变付款批次创建、上传回单、执行、取消、复核逻辑。","受影响流程:财务登录 -> 打开付款批次 -> 查看批次列表/详情/批次项 -> 上传回单入口可见但不自动上传 -> 不执行真实付款;验证步骤:cd client && npm run typecheck && npm run build;bash scripts/qa/run-oafront3-pc-readonly-visual.sh;finance/finance_mgr /payment-batches 不再有 P1 UUID 主展示,英文状态和日期占位显著下降。","typescript|playwright|manual","DONE","DONE","TODO","client/src/pages/PaymentBatchesPage.tsx;client/src/api/paymentBatches.ts;client/src/lib/formatters.ts","[""OAFRONT4-1""]","已完成本地实现:付款批次主文案、状态、日期、渠道和批次项状态中文化;未改后端状态机,未执行真实付款。Regression 待公网 OAFRONT3 只读矩阵复跑后关闭。"
"OAFRONT4-3","PC 导航 Portal 本地化最小修复","将 PC 主导航和页面主展示中的 Portal 英文称呼收口为“工作台”,同时避免把 smoke 账号英文角色名作为普通用户首页主称呼;不重排 Portal 首屏 hero。","左侧导航、面包屑/标题、可见主入口不再以 Portal 作为普通用户主展示;测试账号在 UI 中优先显示中文姓名或中文职能名;管理员排障页可保留 role code。","受影响流程:员工/主管/财务/管理员登录 PC -> 左侧导航 -> 工作台 -> 顶栏账号展示;验证步骤:rg -n 'Portal' client/src;cd client && npm run typecheck && npm run build;OAFRONT3 PC 矩阵中 A/B 页面 Portal 英文主展示 finding 清零或仅剩 C 类后台例外。","typescript|playwright|rg","TODO","TODO","TODO","client/src/components/navigation/NavLinks.tsx;client/src/features/navigation/schema.ts;client/src/pages/PortalHomePage.tsx;server/scripts|server/src/seeds","[""OAFRONT4-1""]","优先级 P1。先做命名和账号显示本地化,不改 Portal hero、视觉 token 或信息架构。若涉及 seed smoke 显示名,不能改密码或真实用户信息。" "OAFRONT4-3","PC 导航 Portal 本地化最小修复","将 PC 主导航和页面主展示中的 Portal 英文称呼收口为“工作台”,同时避免把 smoke 账号英文角色名作为普通用户首页主称呼;不重排 Portal 首屏 hero。","左侧导航、面包屑/标题、可见主入口不再以 Portal 作为普通用户主展示;测试账号在 UI 中优先显示中文姓名或中文职能名;管理员排障页可保留 role code。","受影响流程:员工/主管/财务/管理员登录 PC -> 左侧导航 -> 工作台 -> 顶栏账号展示;验证步骤:rg -n 'Portal' client/src;cd client && npm run typecheck && npm run build;OAFRONT3 PC 矩阵中 A/B 页面 Portal 英文主展示 finding 清零或仅剩 C 类后台例外。","typescript|playwright|rg","DONE","DONE","TODO","client/src/components/navigation/NavLinks.tsx;client/src/features/navigation/schema.ts;client/src/pages/PortalHomePage.tsx;server/scripts|server/src/seeds","[""OAFRONT4-1""]","已完成本地实现:主导航、系统导航配置、工作区设置、功能开关、周报提示和工作台加载文案收口为“工作台”;类型名和 Radix Portal 组件名保留。Regression 待公网 OAFRONT3 只读矩阵复跑后关闭。"
"OAFRONT4-4","合同台账 PC 表单与状态中文化收口","修复合同台账 active/draft/archived 英文状态、日期控件英文占位、输入裁切和过长表单首屏压力;维持合同正文不上传 OA 的边界。","合同状态、操作和筛选展示中文;日期字段提供中文标签/格式提示或自定义展示;疑似裁切输入修复;新建合同表单保留必要字段,归档/续约/敏感字段可折叠或分区。","受影响流程:管理员录入合同摘要 -> 设置状态/日期/责任人/归档位置 -> 列表筛选 -> 查看/编辑 -> 权限负例普通员工不可见敏感入口;验证步骤:client typecheck/build;OAFRONT3 矩阵 admin /contracts-registry 英文状态、日期占位、裁切 finding 下降;手工确认不上传合同正文。","typescript|playwright|manual","TODO","TODO","TODO","client/src/pages/ContractsRegistryPage.tsx;client/src/api/contractsRegistry.ts","[""OAFRONT4-1""]","优先级 P1。不得引入电子签章、全文合同库或复杂档案系统;只清理 V1 台账体验。" "OAFRONT4-4","合同台账 PC 表单与状态中文化收口","修复合同台账 active/draft/archived 英文状态、日期控件英文占位、输入裁切和过长表单首屏压力;维持合同正文不上传 OA 的边界。","合同状态、操作和筛选展示中文;日期字段提供中文标签/格式提示或自定义展示;疑似裁切输入修复;新建合同表单保留必要字段,归档/续约/敏感字段可折叠或分区。","受影响流程:管理员录入合同摘要 -> 设置状态/日期/责任人/归档位置 -> 列表筛选 -> 查看/编辑 -> 权限负例普通员工不可见敏感入口;验证步骤:client typecheck/build;OAFRONT3 矩阵 admin /contracts-registry 英文状态、日期占位、裁切 finding 下降;手工确认不上传合同正文。","typescript|playwright|manual","DONE","DONE","TODO","client/src/pages/ContractsRegistryPage.tsx;client/src/api/contractsRegistry.ts","[""OAFRONT4-1""]","已完成本地实现:合同编辑状态、过期提示、列表状态、日期展示和归档动作中文化;未新增合同正文上传或电子签章能力。Regression 待公网 OAFRONT3 只读矩阵复跑后关闭。"
"OAFRONT4-5","资产台账 PC 首屏与字段密度治理","结合 ASSET1D 试录和 OAFRONT3 截图,调整资产台账首屏主次:建卡、筛选、列表、详情优先,分类维护降级;清理资产状态码、长内部流水、日期英文占位和过密扩展字段展示。","管理员能清楚完成 FA/EA-SYS/EA-REPO/EA-BKP 建卡和查看;资产列表主列不显示内部长流水为主标识;电子资产扩展 JSON 不作为普通主展示,改为摘要/折叠/调试区;日期字段中文化。","受影响流程:管理员资产建卡 -> 筛选 -> 查看详情 -> 领用/归还/盘点入口;普通员工查看自己相关资产;验证步骤:client typecheck/build;OAFRONT3 矩阵 admin /assets finding 下降;ASSET1D 手工试录路径不回归。","typescript|playwright|manual","TODO","TODO","TODO","client/src/pages/AssetRegistryPage.tsx;client/src/api/assets.ts","[""OAFRONT4-1""]","优先级 P1。不得把 EA-ACC 密码/token/Secret 纳入 OA;不上传源码包、合同正文、银行流水或完整本地路径。" "OAFRONT4-5","资产台账 PC 首屏与字段密度治理","结合 ASSET1D 试录和 OAFRONT3 截图,调整资产台账首屏主次:建卡、筛选、列表、详情优先,分类维护降级;清理资产状态码、长内部流水、日期英文占位和过密扩展字段展示。","管理员能清楚完成 FA/EA-SYS/EA-REPO/EA-BKP 建卡和查看;资产列表主列不显示内部长流水为主标识;电子资产扩展 JSON 不作为普通主展示,改为摘要/折叠/调试区;日期字段中文化。","受影响流程:管理员资产建卡 -> 筛选 -> 查看详情 -> 领用/归还/盘点入口;普通员工查看自己相关资产;验证步骤:client typecheck/build;OAFRONT3 矩阵 admin /assets finding 下降;ASSET1D 手工试录路径不回归。","typescript|playwright|manual","TODO","TODO","TODO","client/src/pages/AssetRegistryPage.tsx;client/src/api/assets.ts","[""OAFRONT4-1""]","优先级 P1。不得把 EA-ACC 密码/token/Secret 纳入 OA;不上传源码包、合同正文、银行流水或完整本地路径。"
"OAFRONT4-6","票据/采购/项目 PC 长内部流水与日期控件清理","处理 OAFRONT3 中票据中心、采购申请和项目页的长内部流水、日期英文占位和空态弱提示;不改变 OCR、采购审批、项目关联或后端接口语义。","票据、采购、项目列表/详情主展示使用业务编号、摘要、申请人、金额/日期和中文状态;长内部流水不作为主文案;日期输入和展示中文化;空态给出下一步动作且不重复大段解释。","受影响流程:员工查看票据中心 -> 采购申请列表/详情 -> 主管查看项目列表;验证步骤:client typecheck/build;OAFRONT3 矩阵 /invoice-pool、/purchase-requests、/projects 相关 P2 finding 下降;真实 OCR/采购流程回归不变。","typescript|playwright|manual","TODO","TODO","TODO","client/src/pages/InvoicePoolPage.tsx;client/src/pages/invoice-pool/*;client/src/pages/PurchaseRequestsPage.tsx;client/src/pages/ProjectsPage.tsx;client/src/pages/ProjectDetailPage.tsx","[""OAFRONT4-1""]","优先级 P1/P2。票据隐私边界不变:不在报告复述完整发票号、票面原图、供应商敏感细节。" "OAFRONT4-6","票据/采购/项目 PC 长内部流水与日期控件清理","处理 OAFRONT3 中票据中心、采购申请和项目页的长内部流水、日期英文占位和空态弱提示;不改变 OCR、采购审批、项目关联或后端接口语义。","票据、采购、项目列表/详情主展示使用业务编号、摘要、申请人、金额/日期和中文状态;长内部流水不作为主文案;日期输入和展示中文化;空态给出下一步动作且不重复大段解释。","受影响流程:员工查看票据中心 -> 采购申请列表/详情 -> 主管查看项目列表;验证步骤:client typecheck/build;OAFRONT3 矩阵 /invoice-pool、/purchase-requests、/projects 相关 P2 finding 下降;真实 OCR/采购流程回归不变。","typescript|playwright|manual","TODO","TODO","TODO","client/src/pages/InvoicePoolPage.tsx;client/src/pages/invoice-pool/*;client/src/pages/PurchaseRequestsPage.tsx;client/src/pages/ProjectsPage.tsx;client/src/pages/ProjectDetailPage.tsx","[""OAFRONT4-1""]","优先级 P1/P2。票据隐私边界不变:不在报告复述完整发票号、票面原图、供应商敏感细节。"
"OAFRONT4-7","PC 日期与状态展示小组件化","若 OAFRONT4-2/4/5/6 出现重复实现,抽出轻量前端 helper 或小组件统一日期展示、中文状态标签和内部 ID 折叠规则;避免每页手写不同 label。","存在可复用的 status/date/display helper;付款、合同、资产、票据/采购至少两个页面复用;不引入新 UI 框架;C 类后台仍可显示必要内部码值但 A/B 类默认隐藏。","受影响流程:PC A/B 类页面展示业务对象 -> 状态/日期/内部 ID 一致;验证步骤:client typecheck/build;check-ui-design-rules;OAFRONT3 矩阵 P1/P2 下降;无 circular chunk warning。","typescript|node|playwright","TODO","TODO","TODO","client/src/lib/*;client/src/components/common/*;client/src/pages/PaymentBatchesPage.tsx;client/src/pages/ContractsRegistryPage.tsx;client/src/pages/AssetRegistryPage.tsx","[""OAFRONT4-2"",""OAFRONT4-4"",""OAFRONT4-5""]","优先级 P2 或随实现触发。只有出现三处以上重复时再抽象;否则保持页面内小修,避免过度工程。" "OAFRONT4-7","PC 日期与状态展示小组件化","若 OAFRONT4-2/4/5/6 出现重复实现,抽出轻量前端 helper 或小组件统一日期展示、中文状态标签和内部 ID 折叠规则;避免每页手写不同 label。","存在可复用的 status/date/display helper;付款、合同、资产、票据/采购至少两个页面复用;不引入新 UI 框架;C 类后台仍可显示必要内部码值但 A/B 类默认隐藏。","受影响流程:PC A/B 类页面展示业务对象 -> 状态/日期/内部 ID 一致;验证步骤:client typecheck/build;check-ui-design-rules;OAFRONT3 矩阵 P1/P2 下降;无 circular chunk warning。","typescript|node|playwright","DONE","DONE","TODO","client/src/lib/*;client/src/components/common/*;client/src/pages/PaymentBatchesPage.tsx;client/src/pages/ContractsRegistryPage.tsx;client/src/pages/AssetRegistryPage.tsx","[""OAFRONT4-2"",""OAFRONT4-4"",""OAFRONT4-5""]","已新增 client/src/lib/displayFormat.ts,付款批次和合同台账已复用日期展示 helper;StatusBadge 补充 reviewed/executed。Regression 待公网 OAFRONT3 只读矩阵复跑后关闭。"
"OAFRONT4-8","PC 修复后自动回归与截图证据","在完成任一实施批次后,执行前端构建、设计规则、PC 只读视觉矩阵和必要业务回归;输出脱敏截图和 summary,并回填本 CSV 状态。","相关实施项的 Test_Method 均执行通过;OAFRONT3 矩阵 P1 清零,P2 有明确剩余解释;报告无密码、token、Secret、完整邮箱/手机号、完整本地路径、票据原图、合同正文、银行流水或原始 payload。","受影响流程:PC 修复实现 -> 前端构建 -> 设计规则 -> 公网只读视觉矩阵 -> Issue 状态回填;验证步骤:cd client && npm run typecheck && npm run build;node scripts/qa/check-ui-design-rules.mjs --json;bash scripts/qa/run-oafront3-pc-readonly-visual.sh;git diff --check;必要时跑 bash scripts/check-minimal.sh。","npm|node|playwright|bash|git","TODO","TODO","TODO","client/tests/e2e/oafront3-pc-readonly-visual.spec.ts;scripts/qa/run-oafront3-pc-readonly-visual.sh;artifacts/qa-flow","[""OAFRONT4-2"",""OAFRONT4-3"",""OAFRONT4-4"",""OAFRONT4-5"",""OAFRONT4-6""]","优先级 P0。公网矩阵需要 smoke env 和浏览器权限;失败时区分代码回归、账号缺失、登录限流和公网环境阻断。" "OAFRONT4-8","PC 修复后自动回归与截图证据","在完成任一实施批次后,执行前端构建、设计规则、PC 只读视觉矩阵和必要业务回归;输出脱敏截图和 summary,并回填本 CSV 状态。","相关实施项的 Test_Method 均执行通过;OAFRONT3 矩阵 P1 清零,P2 有明确剩余解释;报告无密码、token、Secret、完整邮箱/手机号、完整本地路径、票据原图、合同正文、银行流水或原始 payload。","受影响流程:PC 修复实现 -> 前端构建 -> 设计规则 -> 公网只读视觉矩阵 -> Issue 状态回填;验证步骤:cd client && npm run typecheck && npm run build;node scripts/qa/check-ui-design-rules.mjs --json;bash scripts/qa/run-oafront3-pc-readonly-visual.sh;git diff --check;必要时跑 bash scripts/check-minimal.sh。","npm|node|playwright|bash|git","DOING","TODO","TODO","client/tests/e2e/oafront3-pc-readonly-visual.spec.ts;scripts/qa/run-oafront3-pc-readonly-visual.sh;artifacts/qa-flow","[""OAFRONT4-2"",""OAFRONT4-3"",""OAFRONT4-4"",""OAFRONT4-5"",""OAFRONT4-6""]","已完成本地 typecheck、build、check-ui-design-rules 和 check-minimal;build 无 Vite circular chunk warning。已跑公网只读 baseline:artifacts/qa-flow/oafront3-pc-readonly-visual-20260525-074424/summary.json 为 PASS_WITH_FINDINGS,但它验证的是尚未部署的现网旧版本,Regression 待部署后复跑关闭。"
"OAFRONT4-9","用户采纳与实施顺序门禁","把本合同提交给用户审查,明确先做哪组、哪些后置;未被采纳的 UI 改动不得进入实现。","用户对实施顺序给出 A/B/C 选择并回填 Notes;采纳后才进入代码修改;若选择暂停,则保留本合同为建议证据,不改页面。","受影响流程:PC 证据 -> 用户决策 -> 采纳项进入实现 -> 未采纳项后置;验证步骤:人工问答 A=按推荐顺序先修付款批次+Portal本地化+合同资产;B=只修 P1 付款批次,其他等 7 天反馈;C=暂停 PC UI 修复,继续真实使用稳定化;回填选择并 validate CSV。","manual|python3","TODO","TODO","TODO","issues/2026-05-25_00-55-14-oa-pc-launch-ui-cleanup-v1.csv","[""OAFRONT4-0""]","默认推荐 A:第一批 OAFRONT4-2+3+4,第二批 OAFRONT4-5+6,OAFRONT4-7 按重复度触发,Portal hero/视觉 token/大页面拆分继续后置。" "OAFRONT4-9","用户采纳与实施顺序门禁","把本合同提交给用户审查,明确先做哪组、哪些后置;未被采纳的 UI 改动不得进入实现。","用户对实施顺序给出 A/B/C 选择并回填 Notes;采纳后才进入代码修改;若选择暂停,则保留本合同为建议证据,不改页面。","受影响流程:PC 证据 -> 用户决策 -> 采纳项进入实现 -> 未采纳项后置;验证步骤:人工问答 A=按推荐顺序先修付款批次+Portal本地化+合同资产;B=只修 P1 付款批次,其他等 7 天反馈;C=暂停 PC UI 修复,继续真实使用稳定化;回填选择并 validate CSV。","manual|python3","DONE","DONE","DONE","issues/2026-05-25_00-55-14-oa-pc-launch-ui-cleanup-v1.csv","[""OAFRONT4-0""]","用户已批准开始实施,按默认推荐 A 先执行第一批 OAFRONT4-2+3+4;第二批 OAFRONT4-5+6 后置,Portal hero/视觉 token/大页面拆分继续后置。"
1 ID Title Description Acceptance Test_Method Tools Dev_Status Review1_Status Regression_Status Files Dependencies Notes
2 OAFRONT4-0 建立 PC 启用前 UI 清理执行合同 基于 OAFRONT3 公网 PC 只读视觉证据,规划正式启用前最小 PC 体验清理批次;本合同只规划任务,不直接修改业务代码、不部署、不写公网业务数据。 本 CSV 校验通过;任务按 P0/P1/P2 分层;每项都有业务流程、受影响文件、自动化验证和人工门禁;明确不做大重构、不改变权限、审批状态机、付款执行或数据库 schema。 受影响流程:OAFRONT3 证据 -> OAFRONT4 任务拆分 -> 用户审查采纳 -> 后续实施合同;验证步骤:validate_issues_csv.py 本 CSV;check-process-contract general;git diff --check。 python3|node|git|manual DONE DONE DONE issues/2026-05-25_00-55-14-oa-pc-launch-ui-cleanup-v1.csv [] MAGI 审视:付款批次 UUID/英文状态为 P1;Portal 本地化覆盖面最大;合同/资产/票据/采购日期与码值为 P1/P2。执行:拆成小批次避免一次性视觉重排。提升:先修影响正式启用专业感和财务闭环的硬问题,Portal hero 和页面拆分后置。
3 OAFRONT4-1 把 OAFRONT3 PC 只读矩阵纳入后续 UI 回归门禁 复用已新增的 PC no-write visual evidence,作为后续 PC 页面清理前后对比门禁;避免继续使用会创建报销草稿的 project-wide visual 公网路径。 后续 PC UI 实现前先跑一次 OAFRONT3 矩阵作为 baseline;实现后复跑并比较 P1/P2 数量;报告只含角色、路由、截图路径、脱敏指标,不含密码、token、Secret、完整账号、票据原图、合同正文或银行回单。 受影响流程:PC 登录 -> 角色化只读访问 -> 截图/DOM 指标 -> 变更前后对比;验证步骤:命令 bash scripts/qa/run-oafront3-pc-readonly-visual.sh;检查 summary.json 为 PASS 或 PASS_WITH_FINDINGS 且隐私扫描通过。 playwright|bash|node TODO TODO TODO scripts/qa/run-oafront3-pc-readonly-visual.sh;client/tests/e2e/oafront3-pc-readonly-visual.spec.ts;artifacts/qa-flow/oafront3-pc-readonly-visual-* ["OAFRONT4-0"] 优先级 P0。只读;不创建业务数据;若公网账号缺失则记录 SKIP/BLOCKED_BY_EXTERNAL_CONFIG,不伪造成通过。
4 OAFRONT4-2 付款批次 PC 主展示去内部化 修复付款批次页面在 finance/finance_mgr 视角暴露内部 UUID、英文状态 EXECUTED/DRAFT、日期英文占位和长内部流水的问题;保持付款批次真实执行仍走线下银行,不新增真实支付能力。 付款批次列表和详情主展示使用付款批次号、实体业务编号、中文状态、中文日期展示;内部 UUID 只允许管理员审计/调试折叠区可见;不改变付款批次创建、上传回单、执行、取消、复核逻辑。 受影响流程:财务登录 -> 打开付款批次 -> 查看批次列表/详情/批次项 -> 上传回单入口可见但不自动上传 -> 不执行真实付款;验证步骤:cd client && npm run typecheck && npm run build;bash scripts/qa/run-oafront3-pc-readonly-visual.sh;finance/finance_mgr /payment-batches 不再有 P1 UUID 主展示,英文状态和日期占位显著下降。 typescript|playwright|manual TODO DONE TODO DONE TODO client/src/pages/PaymentBatchesPage.tsx;client/src/api/paymentBatches.ts;client/src/lib/formatters.ts ["OAFRONT4-1"] 优先级 P0/P1。不得改后端状态机、不得执行真实付款、不得上传银行流水原件;如需新增纯前端 label helper 可复用既有格式化工具。 已完成本地实现:付款批次主文案、状态、日期、渠道和批次项状态中文化;未改后端状态机,未执行真实付款。Regression 待公网 OAFRONT3 只读矩阵复跑后关闭。
5 OAFRONT4-3 PC 导航 Portal 本地化最小修复 将 PC 主导航和页面主展示中的 Portal 英文称呼收口为“工作台”,同时避免把 smoke 账号英文角色名作为普通用户首页主称呼;不重排 Portal 首屏 hero。 左侧导航、面包屑/标题、可见主入口不再以 Portal 作为普通用户主展示;测试账号在 UI 中优先显示中文姓名或中文职能名;管理员排障页可保留 role code。 受影响流程:员工/主管/财务/管理员登录 PC -> 左侧导航 -> 工作台 -> 顶栏账号展示;验证步骤:rg -n 'Portal' client/src;cd client && npm run typecheck && npm run build;OAFRONT3 PC 矩阵中 A/B 页面 Portal 英文主展示 finding 清零或仅剩 C 类后台例外。 typescript|playwright|rg TODO DONE TODO DONE TODO client/src/components/navigation/NavLinks.tsx;client/src/features/navigation/schema.ts;client/src/pages/PortalHomePage.tsx;server/scripts|server/src/seeds ["OAFRONT4-1"] 优先级 P1。先做命名和账号显示本地化,不改 Portal hero、视觉 token 或信息架构。若涉及 seed smoke 显示名,不能改密码或真实用户信息。 已完成本地实现:主导航、系统导航配置、工作区设置、功能开关、周报提示和工作台加载文案收口为“工作台”;类型名和 Radix Portal 组件名保留。Regression 待公网 OAFRONT3 只读矩阵复跑后关闭。
6 OAFRONT4-4 合同台账 PC 表单与状态中文化收口 修复合同台账 active/draft/archived 英文状态、日期控件英文占位、输入裁切和过长表单首屏压力;维持合同正文不上传 OA 的边界。 合同状态、操作和筛选展示中文;日期字段提供中文标签/格式提示或自定义展示;疑似裁切输入修复;新建合同表单保留必要字段,归档/续约/敏感字段可折叠或分区。 受影响流程:管理员录入合同摘要 -> 设置状态/日期/责任人/归档位置 -> 列表筛选 -> 查看/编辑 -> 权限负例普通员工不可见敏感入口;验证步骤:client typecheck/build;OAFRONT3 矩阵 admin /contracts-registry 英文状态、日期占位、裁切 finding 下降;手工确认不上传合同正文。 typescript|playwright|manual TODO DONE TODO DONE TODO client/src/pages/ContractsRegistryPage.tsx;client/src/api/contractsRegistry.ts ["OAFRONT4-1"] 优先级 P1。不得引入电子签章、全文合同库或复杂档案系统;只清理 V1 台账体验。 已完成本地实现:合同编辑状态、过期提示、列表状态、日期展示和归档动作中文化;未新增合同正文上传或电子签章能力。Regression 待公网 OAFRONT3 只读矩阵复跑后关闭。
7 OAFRONT4-5 资产台账 PC 首屏与字段密度治理 结合 ASSET1D 试录和 OAFRONT3 截图,调整资产台账首屏主次:建卡、筛选、列表、详情优先,分类维护降级;清理资产状态码、长内部流水、日期英文占位和过密扩展字段展示。 管理员能清楚完成 FA/EA-SYS/EA-REPO/EA-BKP 建卡和查看;资产列表主列不显示内部长流水为主标识;电子资产扩展 JSON 不作为普通主展示,改为摘要/折叠/调试区;日期字段中文化。 受影响流程:管理员资产建卡 -> 筛选 -> 查看详情 -> 领用/归还/盘点入口;普通员工查看自己相关资产;验证步骤:client typecheck/build;OAFRONT3 矩阵 admin /assets finding 下降;ASSET1D 手工试录路径不回归。 typescript|playwright|manual TODO TODO TODO client/src/pages/AssetRegistryPage.tsx;client/src/api/assets.ts ["OAFRONT4-1"] 优先级 P1。不得把 EA-ACC 密码/token/Secret 纳入 OA;不上传源码包、合同正文、银行流水或完整本地路径。
8 OAFRONT4-6 票据/采购/项目 PC 长内部流水与日期控件清理 处理 OAFRONT3 中票据中心、采购申请和项目页的长内部流水、日期英文占位和空态弱提示;不改变 OCR、采购审批、项目关联或后端接口语义。 票据、采购、项目列表/详情主展示使用业务编号、摘要、申请人、金额/日期和中文状态;长内部流水不作为主文案;日期输入和展示中文化;空态给出下一步动作且不重复大段解释。 受影响流程:员工查看票据中心 -> 采购申请列表/详情 -> 主管查看项目列表;验证步骤:client typecheck/build;OAFRONT3 矩阵 /invoice-pool、/purchase-requests、/projects 相关 P2 finding 下降;真实 OCR/采购流程回归不变。 typescript|playwright|manual TODO TODO TODO client/src/pages/InvoicePoolPage.tsx;client/src/pages/invoice-pool/*;client/src/pages/PurchaseRequestsPage.tsx;client/src/pages/ProjectsPage.tsx;client/src/pages/ProjectDetailPage.tsx ["OAFRONT4-1"] 优先级 P1/P2。票据隐私边界不变:不在报告复述完整发票号、票面原图、供应商敏感细节。
9 OAFRONT4-7 PC 日期与状态展示小组件化 若 OAFRONT4-2/4/5/6 出现重复实现,抽出轻量前端 helper 或小组件统一日期展示、中文状态标签和内部 ID 折叠规则;避免每页手写不同 label。 存在可复用的 status/date/display helper;付款、合同、资产、票据/采购至少两个页面复用;不引入新 UI 框架;C 类后台仍可显示必要内部码值但 A/B 类默认隐藏。 受影响流程:PC A/B 类页面展示业务对象 -> 状态/日期/内部 ID 一致;验证步骤:client typecheck/build;check-ui-design-rules;OAFRONT3 矩阵 P1/P2 下降;无 circular chunk warning。 typescript|node|playwright TODO DONE TODO DONE TODO client/src/lib/*;client/src/components/common/*;client/src/pages/PaymentBatchesPage.tsx;client/src/pages/ContractsRegistryPage.tsx;client/src/pages/AssetRegistryPage.tsx ["OAFRONT4-2","OAFRONT4-4","OAFRONT4-5"] 优先级 P2 或随实现触发。只有出现三处以上重复时再抽象;否则保持页面内小修,避免过度工程。 已新增 client/src/lib/displayFormat.ts,付款批次和合同台账已复用日期展示 helper;StatusBadge 补充 reviewed/executed。Regression 待公网 OAFRONT3 只读矩阵复跑后关闭。
10 OAFRONT4-8 PC 修复后自动回归与截图证据 在完成任一实施批次后,执行前端构建、设计规则、PC 只读视觉矩阵和必要业务回归;输出脱敏截图和 summary,并回填本 CSV 状态。 相关实施项的 Test_Method 均执行通过;OAFRONT3 矩阵 P1 清零,P2 有明确剩余解释;报告无密码、token、Secret、完整邮箱/手机号、完整本地路径、票据原图、合同正文、银行流水或原始 payload。 受影响流程:PC 修复实现 -> 前端构建 -> 设计规则 -> 公网只读视觉矩阵 -> Issue 状态回填;验证步骤:cd client && npm run typecheck && npm run build;node scripts/qa/check-ui-design-rules.mjs --json;bash scripts/qa/run-oafront3-pc-readonly-visual.sh;git diff --check;必要时跑 bash scripts/check-minimal.sh。 npm|node|playwright|bash|git TODO DOING TODO TODO client/tests/e2e/oafront3-pc-readonly-visual.spec.ts;scripts/qa/run-oafront3-pc-readonly-visual.sh;artifacts/qa-flow ["OAFRONT4-2","OAFRONT4-3","OAFRONT4-4","OAFRONT4-5","OAFRONT4-6"] 优先级 P0。公网矩阵需要 smoke env 和浏览器权限;失败时区分代码回归、账号缺失、登录限流和公网环境阻断。 已完成本地 typecheck、build、check-ui-design-rules 和 check-minimal;build 无 Vite circular chunk warning。已跑公网只读 baseline:artifacts/qa-flow/oafront3-pc-readonly-visual-20260525-074424/summary.json 为 PASS_WITH_FINDINGS,但它验证的是尚未部署的现网旧版本,Regression 待部署后复跑关闭。
11 OAFRONT4-9 用户采纳与实施顺序门禁 把本合同提交给用户审查,明确先做哪组、哪些后置;未被采纳的 UI 改动不得进入实现。 用户对实施顺序给出 A/B/C 选择并回填 Notes;采纳后才进入代码修改;若选择暂停,则保留本合同为建议证据,不改页面。 受影响流程:PC 证据 -> 用户决策 -> 采纳项进入实现 -> 未采纳项后置;验证步骤:人工问答 A=按推荐顺序先修付款批次+Portal本地化+合同资产;B=只修 P1 付款批次,其他等 7 天反馈;C=暂停 PC UI 修复,继续真实使用稳定化;回填选择并 validate CSV。 manual|python3 TODO DONE TODO DONE TODO DONE issues/2026-05-25_00-55-14-oa-pc-launch-ui-cleanup-v1.csv ["OAFRONT4-0"] 默认推荐 A:第一批 OAFRONT4-2+3+4,第二批 OAFRONT4-5+6,OAFRONT4-7 按重复度触发,Portal hero/视觉 token/大页面拆分继续后置。 用户已批准开始实施,按默认推荐 A 先执行第一批 OAFRONT4-2+3+4;第二批 OAFRONT4-5+6 后置,Portal hero/视觉 token/大页面拆分继续后置。
+3 -3
查看文件
@@ -18,7 +18,7 @@ const FINANCE_PERMISSION_CODES = Object.freeze(["payment.batch.manage", "risk.re
const FINANCE_NAVIGATION_MODULES = new Set(["paymentBatches", "apOperations", "invoicePool"]); const FINANCE_NAVIGATION_MODULES = new Set(["paymentBatches", "apOperations", "invoicePool"]);
const NAVIGATION_REGISTRY = Object.freeze({ const NAVIGATION_REGISTRY = Object.freeze({
portal: { key: "portal", label: "Portal", icon: "home_app_logo", href: "/portal", anyOf: [] }, portal: { key: "portal", label: "工作台", icon: "home_app_logo", href: "/portal", anyOf: [] },
team: { key: "team", label: "团队", icon: "groups", href: "/team", anyOf: [], featureFlagKey: FEATURE_FLAG_KEYS.presenceBoard }, team: { key: "team", label: "团队", icon: "groups", href: "/team", anyOf: [], featureFlagKey: FEATURE_FLAG_KEYS.presenceBoard },
leaveRequests: { key: "leaveRequests", label: "请假", icon: "event_available", href: "/leave-requests", anyOf: ["leave.view_own", "leave.view_all", "leave.manage"] }, leaveRequests: { key: "leaveRequests", label: "请假", icon: "event_available", href: "/leave-requests", anyOf: ["leave.view_own", "leave.view_all", "leave.manage"] },
attendance: { key: "attendance", label: "考勤", icon: "schedule", href: "/attendance", anyOf: [], featureFlagKey: FEATURE_FLAG_KEYS.attendanceUnified }, attendance: { key: "attendance", label: "考勤", icon: "schedule", href: "/attendance", anyOf: [], featureFlagKey: FEATURE_FLAG_KEYS.attendanceUnified },
@@ -48,7 +48,7 @@ const NAVIGATION_REGISTRY = Object.freeze({
purchaseRequests: { key: "purchaseRequests", label: "采购申请", icon: "shopping_cart", href: "/purchase-requests", anyOf: ["procurement.create", "procurement.view_own", "procurement.view_all", "procurement.manage"] }, purchaseRequests: { key: "purchaseRequests", label: "采购申请", icon: "shopping_cart", href: "/purchase-requests", anyOf: ["procurement.create", "procurement.view_own", "procurement.view_all", "procurement.manage"] },
travelRequests: { key: "travelRequests", label: "差旅申请", icon: "flight", href: "/travel-requests", anyOf: ["travel.view_all", "travel.manage", "travel.payment.manage"] }, travelRequests: { key: "travelRequests", label: "差旅申请", icon: "flight", href: "/travel-requests", anyOf: ["travel.view_all", "travel.manage", "travel.payment.manage"] },
budgets: { key: "budgets", label: "预算管理", icon: "account_balance", href: "/budgets", anyOf: ["budget.view", "budget.manage"] }, budgets: { key: "budgets", label: "预算管理", icon: "account_balance", href: "/budgets", anyOf: ["budget.view", "budget.manage"] },
paymentBatches: { key: "paymentBatches", label: "支付批次", icon: "payments", href: "/payment-batches", anyOf: ["payment.batch.manage", "risk.review.manage"], featureFlagKey: FEATURE_FLAG_KEYS.paymentBatch }, paymentBatches: { key: "paymentBatches", label: "付款批次", icon: "payments", href: "/payment-batches", anyOf: ["payment.batch.manage", "risk.review.manage"], featureFlagKey: FEATURE_FLAG_KEYS.paymentBatch },
apOperations: { key: "apOperations", label: "AP 运营", icon: "account_balance_wallet", href: "/ap-operations", anyOf: ["payment.batch.manage", "risk.review.manage"], featureFlagKey: FEATURE_FLAG_KEYS.paymentBatch }, apOperations: { key: "apOperations", label: "AP 运营", icon: "account_balance_wallet", href: "/ap-operations", anyOf: ["payment.batch.manage", "risk.review.manage"], featureFlagKey: FEATURE_FLAG_KEYS.paymentBatch },
invoicePool: { key: "invoicePool", label: "票据中心", icon: "receipt_long", href: "/invoice-pool", anyOf: ["invoice.manage"] }, invoicePool: { key: "invoicePool", label: "票据中心", icon: "receipt_long", href: "/invoice-pool", anyOf: ["invoice.manage"] },
inventoryChecks: { key: "inventoryChecks", label: "资产盘点", icon: "inventory", href: "/inventory-checks", anyOf: ["asset.inventory"] }, inventoryChecks: { key: "inventoryChecks", label: "资产盘点", icon: "inventory", href: "/inventory-checks", anyOf: ["asset.inventory"] },
@@ -272,7 +272,7 @@ async function getSystemNavigation({ prisma, config, employee, hasPermission })
recommendedRolePresets: [ recommendedRolePresets: [
{ key: "owner", label: "老板", roleCodes: ["ceo"], description: "查看团队状态、审批汇总与关键管理入口。" }, { key: "owner", label: "老板", roleCodes: ["ceo"], description: "查看团队状态、审批汇总与关键管理入口。" },
{ key: "admin", label: "管理员", roleCodes: ["admin"], description: "管理工作区、人员、流程、开关与审计。" }, { key: "admin", label: "管理员", roleCodes: ["admin"], description: "管理工作区、人员、流程、开关与审计。" },
{ key: "employee", label: "员工", roleCodes: ["employee"], description: "日常使用 Portal、请假、报销、周报、待办。" }, { key: "employee", label: "员工", roleCodes: ["employee"], description: "日常使用工作台、请假、报销、周报、待办。" },
{ key: "manager", label: "主管", roleCodes: ["manager"], description: "处理审批、查看团队周报与在岗状态。" }, { key: "manager", label: "主管", roleCodes: ["manager"], description: "处理审批、查看团队周报与在岗状态。" },
], ],
}; };