feat(08-05-interaction-pattern-unification): 统一前端交互与反馈模式

这个提交包含在:
2026-08-07 06:50:40 +08:00
父节点 d4e9597b37
当前提交 94f1bad336
共修改 97 个文件,包含 784 行新增和 836 行删除
+40 -19
查看文件
@@ -1,9 +1,9 @@
import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage";
import type { ReactNode } from "react";
import { useMemo, useState } from "react";
import { Link, Navigate, useLocation, useNavigate, useParams } from "react-router-dom";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { isApiError } from "../api/types";
import { mapApiErrorMessage } from "../api/errorMessages";
import {
approveWorkflowTask,
fetchWorkflowApprovers,
@@ -31,6 +31,7 @@ import { MetricCard } from "../components/patterns/MetricCard";
import { PageScaffold } from "../components/patterns/PageScaffold";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { Button } from "../components/ui/button";
import { Label } from "../components/ui/label";
import { Textarea } from "../components/ui/textarea";
import { approvalRoleLabel, resolvePurchaseDisplayCode, workflowActionLabel } from "../lib/approvalDisplay";
import { formatDisplayDateTime } from "../lib/displayFormat";
@@ -177,14 +178,7 @@ export function WorkflowTaskDetailPage() {
queryClient.invalidateQueries({ queryKey: ["workflow", "task", taskId] }),
]);
}
let msg: string;
if (error instanceof Error && !isApiError(error)) {
msg = error.message;
} else if (isApiError(error)) {
msg = `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
} else {
msg = "动作执行失败,请稍后重试";
}
const msg = resolveUserErrorMessage(error, "动作执行失败,请稍后重试");
setActionError(msg);
toast.error(msg);
},
@@ -238,13 +232,7 @@ export function WorkflowTaskDetailPage() {
}
if (detailQuery.isError || !detailQuery.data) {
const message = isApiError(detailQuery.error)
? `${detailQuery.error.code}: ${mapApiErrorMessage(
detailQuery.error.code,
detailQuery.error.details,
detailQuery.error.message,
)}`
: "加载失败,请返回列表重试";
const message = resolveUserErrorMessage(detailQuery.error, "加载失败,请返回列表重试");
return (
<div className="card-std space-y-3 p-5">
@@ -265,6 +253,11 @@ export function WorkflowTaskDetailPage() {
const canReturn = allowedActions.has("return");
const canReject = allowedActions.has("reject");
const canTransfer = allowedActions.has("transfer");
const transferDisabledReason = !selectedApprover
? "请先选择目标审批人。"
: transferReason.trim().length < 10
? `请填写转审理由,至少 10 个字(还差 ${10 - transferReason.trim().length} 个字)。`
: "转审理由已满足要求。";
const allowedActionLabels = detail.allowedActions.map(workflowActionLabel).join(" / ");
const businessStatusLabel = workflowInstanceStatusLabel(detail.workflowInstance?.currentStatus);
@@ -420,7 +413,11 @@ export function WorkflowTaskDetailPage() {
当前 ¥{item.amount} / 标准 ¥{item.maxAmount}/{item.perUnit}
</div>
<div>超出 ¥{item.exceededAmount} · {item.standardName}</div>
<div>管控模式:{item.controlMode === "block" ? "阻断" : "预警"}</div>
<div>
{item.controlMode === "block"
? "处理影响:当前单据不应继续通过,请申请人调整金额或补充合规处理依据。"
: "处理影响:可继续审批,但请核对超标原因并在审批意见中记录判断。"}
</div>
</div>
))}
</div>
@@ -510,6 +507,13 @@ export function WorkflowTaskDetailPage() {
)}
testId="workflow-task-action-panel"
>
{canReturn || canReject ? (
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-xs leading-5 text-zinc-600" data-testid="workflow-action-consequence-help">
<div><span className="font-medium text-zinc-800">退回:</span>原单回到申请人修改,保留单号和历史,修改后可重新提交。</div>
<div><span className="font-medium text-zinc-800">驳回:</span>本次流程终止或进入驳回态,申请人通常需要复制或重新发起。</div>
</div>
) : null}
{actionError ? (
<p
className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600"
@@ -521,8 +525,10 @@ export function WorkflowTaskDetailPage() {
{activeSecondaryAction === "reject" ? (
<div className="detail-action-card space-y-2">
<p className="text-sm font-medium text-zinc-800">驳回意见</p>
<Label className="text-sm font-medium text-zinc-800" htmlFor="workflow-task-reject-comment">驳回意见</Label>
<p className="text-xs leading-5 text-zinc-500">驳回会终止本次流程或进入驳回态,申请人通常需要重新发起。</p>
<Textarea
id="workflow-task-reject-comment"
data-testid="workflow-task-reject-comment"
onChange={(event) => setRejectComment(event.target.value)}
rows={3}
@@ -542,8 +548,10 @@ export function WorkflowTaskDetailPage() {
{activeSecondaryAction === "return" ? (
<div className="detail-action-card space-y-2">
<p className="text-sm font-medium text-zinc-800">退回意见</p>
<Label className="text-sm font-medium text-zinc-800" htmlFor="workflow-task-return-comment">退回意见</Label>
<p className="text-xs leading-5 text-zinc-500">退回后原单可修改并重新提交,单号和审批历史会保留。</p>
<Textarea
id="workflow-task-return-comment"
data-testid="workflow-task-return-comment"
onChange={(event) => setReturnComment(event.target.value)}
rows={3}
@@ -564,7 +572,9 @@ export function WorkflowTaskDetailPage() {
{activeSecondaryAction === "transfer" && canTransfer ? (
<div className="detail-action-card space-y-2">
<p className="text-sm font-medium text-zinc-800">转审</p>
<Label className="text-xs text-zinc-600" htmlFor="workflow-task-transfer-approver">目标审批人</Label>
<select
id="workflow-task-transfer-approver"
className="select-std"
data-testid="workflow-task-transfer-approver"
onChange={(event) => setSelectedApprover(event.target.value)}
@@ -577,13 +587,24 @@ export function WorkflowTaskDetailPage() {
</option>
))}
</select>
<Label className="text-xs text-zinc-600" htmlFor="workflow-task-transfer-reason">转审理由</Label>
<Textarea
id="workflow-task-transfer-reason"
aria-describedby="workflow-task-transfer-help"
data-testid="workflow-task-transfer-reason"
onChange={(event) => setTransferReason(event.target.value)}
rows={3}
value={transferReason}
/>
<p
id="workflow-task-transfer-help"
className={transferReason.trim().length >= 10 && selectedApprover ? "text-xs text-emerald-700" : "text-xs text-amber-700"}
data-testid="workflow-task-transfer-disabled-reason"
>
{transferDisabledReason}
</p>
<Button
aria-describedby="workflow-task-transfer-help"
data-testid="workflow-task-transfer"
variant="secondary"
disabled={