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> = {
portal_home_v1_disabled: "Portal 首页当前未启用,请联系管理员检查环境开关与功能开关配置。",
portal_home_v1_disabled: "工作台首页当前未启用,请联系管理员检查环境开关与功能开关配置。",
announcements_v1_disabled: "公告模块当前未启用,请联系管理员检查环境开关与功能开关配置。",
presence_board_v1_disabled: "团队状态模块当前未启用,请联系管理员检查环境开关与功能开关配置。",
attendance_v1_disabled: "历史 OA 自建打卡已关闭,请在企业微信打卡。",
@@ -8,6 +8,8 @@ const STATUS_MAP: Record<string, { label: string; className: string }> = {
pending: { label: "待派发", className: "badge-warning" },
processing: { label: "处理中", className: "badge-warning" },
succeeded: { label: "已成功", className: "badge-success" },
reviewed: { label: "已复核", className: "badge-primary" },
executed: { label: "已执行", className: "badge-success" },
failed: { label: "失败", className: "badge-error" },
dead_letter: { label: "死信", className: "badge-error" },
not_triggered: { label: "未触发", className: "badge-neutral" },
@@ -18,7 +18,7 @@ export type 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: "/leave-requests", label: "请假", icon: "event_available", anyOf: ["leave.view_own", "leave.view_all", "leave.manage"] },
{ 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: "/travel-requests", label: "差旅申请", icon: "flight", anyOf: ["travel.view_all", "travel.manage", "travel.payment.manage"] },
{ to: "/budgets", label: "预算管理", icon: "account_balance", anyOf: ["budget.view", "budget.manage"] },
{ to: "/payment-batches", label: "支付批次", icon: "payments", anyOf: ["payment.batch.manage", "risk.review.manage"] },
{ to: "/payment-batches", label: "付款批次", icon: "payments", anyOf: ["payment.batch.manage", "risk.review.manage"] },
{ to: "/ap-operations", label: "AP 运营", icon: "account_balance_wallet", anyOf: ["payment.batch.manage", "risk.review.manage"] },
{ to: "/invoice-pool", label: "票据中心", icon: "receipt_long", anyOf: ["invoice.manage"] },
{ to: "/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 摘要加载失败")} />
) : null}
{paymentBatchesQuery.isError ? (
<SectionError message={resolveErrorMessage(paymentBatchesQuery.error, "支付批次数据加载失败")} />
<SectionError message={resolveErrorMessage(paymentBatchesQuery.error, "付款批次数据加载失败")} />
) : null}
{riskFlagsQuery.isError ? (
<SectionError message={resolveErrorMessage(riskFlagsQuery.error, "风险标记数据加载失败")} />
@@ -1596,7 +1596,7 @@ export function ApOperationsPage() {
title="确认取消付款批次"
description={
cancelTarget
? `将取消 ${cancelTarget.batchNo},该操作与支付批次页保持一致。`
? `将取消 ${cancelTarget.batchNo},该操作与付款批次页保持一致。`
: undefined
}
variant="destructive"
+18 -9
查看文件
@@ -28,6 +28,7 @@ import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
import { Textarea } from "../components/ui/textarea";
import { downloadBlob } from "../lib/download";
import { formatDisplayDate } from "../lib/displayFormat";
import { toast } from "sonner";
const CONFIDENTIALITY_OPTIONS: ConfidentialityLevel[] = ["public", "internal", "confidential", "restricted"];
@@ -39,9 +40,9 @@ const FILTER_STATUS_OPTIONS: Array<{ value: ContractRegistryStatus; label: strin
{ value: "archived", label: "已归档" },
];
const EDITOR_STATUS_OPTIONS: Array<{ value: ContractRegistryPersistedStatus; label: string }> = [
{ value: "active", label: "active" },
{ value: "draft", label: "draft" },
{ value: "archived", label: "archived" },
{ value: "active", label: "生效中" },
{ value: "draft", label: "草稿" },
{ value: "archived", label: "已归档" },
];
const AMOUNT_PATTERN = /^\d{1,12}(\.\d{1,2})?$/;
const EXPIRING_DAYS = 30;
@@ -95,6 +96,14 @@ function contractTypeLabel(value: ContractRegistryType) {
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 {
return {
contractNo: "",
@@ -346,7 +355,7 @@ export function ContractsRegistryPage() {
{item.contractNo} · {item.counterpartyName}
</div>
<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 className="flex gap-2">
@@ -389,7 +398,7 @@ export function ContractsRegistryPage() {
{editingItem?.status === "expired" ? (
<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>
) : null}
@@ -705,7 +714,7 @@ export function ContractsRegistryPage() {
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-3">
<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">
{confidentialityLabel(item.confidentialityLevel)}
</span>
@@ -715,14 +724,14 @@ export function ContractsRegistryPage() {
<div className="text-xs text-zinc-500">
{contractTypeLabel(item.contractType)}
{item.amount ? ` · ${item.amount} ${item.currency}` : " · 金额未录入"}
{item.expiryDate ? ` · 到期日 ${item.expiryDate}` : " · 未设置到期日"}
{item.expiryDate ? ` · 到期日 ${formatDisplayDate(item.expiryDate)}` : " · 未设置到期日"}
</div>
<div className="text-xs text-zinc-500">
项目:{item.project ? `${item.project.projectNo} · ${item.project.name}` : "未关联"} · 责任人:
{item.owner?.name ?? "-"}
</div>
<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}` : ""}
</div>
</div>
@@ -760,7 +769,7 @@ export function ContractsRegistryPage() {
disabled={toggleStatusMutation.isPending}
onClick={() => toggleStatusMutation.mutate(item)}
>
{item.persistedStatus === "archived" ? "恢复为 active" : "归档"}
{item.persistedStatus === "archived" ? "恢复生效" : "归档"}
</Button>
</div>
</div>
+2 -2
查看文件
@@ -50,8 +50,8 @@ function toDisplayName(flagKey: string) {
if (flagKey === "invoice_verification") return "发票验真";
if (flagKey === "leave_holiday_calendar") return "请假日历增强";
if (flagKey === "budget_precheck_v1") return "预算预校验";
if (flagKey === "payment_batch_v1") return "支付批次";
if (flagKey === "portal_home_v1") return "Portal 首页";
if (flagKey === "payment_batch_v1") return "付款批次";
if (flagKey === "portal_home_v1") return "工作台首页";
if (flagKey === "global_search_v1") return "全局搜索";
if (flagKey === "announcements_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 { Label } from "../components/ui/label";
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
import { StatusBadge } from "../components/common/StatusBadge";
import { downloadBlob } from "../lib/download";
import { formatDisplayDate, formatDisplayDateTime } from "../lib/displayFormat";
import { toast } from "sonner";
const PAYMENT_CHANNEL_OPTIONS: PaymentChannel[] = ["bank", "alipay", "wechat", "cash", "other"];
@@ -47,6 +49,31 @@ function entityTypeLabel(value: string) {
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) {
if (!Number.isFinite(value) || value <= 0) return "-";
if (value < 1024) return `${value} B`;
@@ -54,11 +81,6 @@ function formatFileSize(value: number) {
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) {
return isApiError(error)
? `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`
@@ -303,7 +325,7 @@ export function PaymentBatchesPage() {
return (
<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}
@@ -337,13 +359,13 @@ export function PaymentBatchesPage() {
/>
</div>
<div className="space-y-2">
<Label>实体 ID</Label>
<Label>待付款单据 ID</Label>
<textarea
className="min-h-28 w-full rounded-lg border border-zinc-300 px-3 py-2 text-sm disabled:bg-zinc-50"
value={entityIds}
disabled={!canManageBatch}
onChange={(event) => setEntityIds(event.target.value)}
placeholder="用逗号或换行分隔 reimbursement / travel_order ID"
placeholder="输入报销或差旅待付款单据 ID,多个用逗号或换行分隔"
/>
</div>
<div className="grid gap-3 md:grid-cols-2">
@@ -365,7 +387,7 @@ export function PaymentBatchesPage() {
onChange={(event) => setDefaultPaymentChannel((event.target.value as PaymentChannel | "") || "") }
>
<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>
</div>
</div>
@@ -400,7 +422,7 @@ export function PaymentBatchesPage() {
>
<div className="flex items-center justify-between gap-3">
<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 className="mt-1 text-sm text-zinc-600">{item.name}</div>
<div className="mt-1 text-xs text-zinc-500">
@@ -410,7 +432,7 @@ export function PaymentBatchesPage() {
</div>
</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>
@@ -475,13 +497,13 @@ export function PaymentBatchesPage() {
<div>状态:{batchStatusLabel(selectedBatch.status)}</div>
<div>实体:{entityTypeLabel(selectedBatch.entityType)}</div>
<div>总金额:{selectedBatch.totalAmount}</div>
<div>默认日期:{selectedBatch.defaultPaymentDate ?? "-"}</div>
<div>默认渠道:{selectedBatch.defaultPaymentChannel ?? "-"}</div>
<div>默认日期:{formatDisplayDate(selectedBatch.defaultPaymentDate)}</div>
<div>默认渠道:{paymentChannelLabel(selectedBatch.defaultPaymentChannel)}</div>
<div>复核进度:{selectedBatch.reviewRequirement.completedReviewCount}/{selectedBatch.reviewRequirement.requiredReviewCount}</div>
</div>
<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.nextStep === "first_review" ? "等待首轮复核" : selectedBatch.reviewRequirement.nextStep === "second_review" ? "等待二次复核" : "可执行"}。
下一步:{reviewNextStepLabel(selectedBatch.reviewRequirement.nextStep)}。
{selectedBatch.reviewedBy ? ` 首轮复核人:${selectedBatch.reviewedBy}` : ""}
{selectedBatch.secondaryReviewedBy ? ` 二次复核人:${selectedBatch.secondaryReviewedBy}` : ""}
</div>
@@ -538,7 +560,7 @@ export function PaymentBatchesPage() {
<div className="min-w-0">
<div className="truncate text-sm font-medium text-zinc-900">{attachment.originalName}</div>
<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}` : ""}
</div>
</div>
@@ -584,7 +606,7 @@ export function PaymentBatchesPage() {
<div className="font-medium text-zinc-900">{item.entityCode}</div>
<div className="text-sm text-zinc-600">{item.entityTitle}</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 className="mt-3 grid gap-3 md:grid-cols-4">
<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 })}
>
<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>
<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>
<RiskFlagsCard items={item.openRiskFlags} title="关联开放风险" />
</div>
+2 -2
查看文件
@@ -848,13 +848,13 @@ export function PortalHomePage() {
const gettingStartedCompletedCount = gettingStartedItems.filter((item) => item.completed).length;
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) {
const message = isApiError(portalQuery.error)
? `${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>;
}
+1 -1
查看文件
@@ -134,7 +134,7 @@ export function WeeklyReportsPage() {
<FilterBar className="flex flex-wrap items-center justify-between gap-3">
<div className="space-y-1">
<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>
<input
aria-label="周报归档周"
+2 -2
查看文件
@@ -9,7 +9,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from ".
import { toast } from "sonner";
const MODULE_LABELS: Record<string, string> = {
portal: "Portal",
portal: "工作台",
team: "团队",
leaveRequests: "请假",
attendance: "考勤",
@@ -37,7 +37,7 @@ const MODULE_LABELS: Record<string, string> = {
purchaseRequests: "采购申请",
travelRequests: "差旅申请",
budgets: "预算管理",
paymentBatches: "支付批次",
paymentBatches: "付款批次",
apOperations: "AP 运营",
invoicePool: "票据中心",
inventoryChecks: "资产盘点",