diff --git a/client/src/api/workflow.ts b/client/src/api/workflow.ts index 1f440d2c..f5f11f18 100644 --- a/client/src/api/workflow.ts +++ b/client/src/api/workflow.ts @@ -17,6 +17,14 @@ export type WorkflowTaskSummary = { entityId: string; currentStatus: string; } | null; + businessSummary: { + reference: string | null; + title: string | null; + applicantName: string | null; + amount: string | null; + duration: string | null; + businessStatus: string | null; + } | null; allowedActions: Array<"approve" | "reject" | "return" | "transfer">; }; diff --git a/client/src/pages/MobileApprovalsPage.tsx b/client/src/pages/MobileApprovalsPage.tsx index 1e2bd958..9dbe8acb 100644 --- a/client/src/pages/MobileApprovalsPage.tsx +++ b/client/src/pages/MobileApprovalsPage.tsx @@ -50,6 +50,9 @@ type VisibleTaskCard = { entityType: string | null; entityId: string | null; reference: string | null; + applicantName: string | null; + metric: string | null; + businessStatus: string | null; allowedActions: MobileAction[]; completedAt: string | null; isCached: boolean; @@ -99,6 +102,22 @@ function workflowStatusLabel(status: string) { return status; } +function businessStatusLabel(status: string | null | undefined) { + const labels: Record = { + draft: "草稿", + pending: "待处理", + reviewing: "审批中", + submitted: "审批中", + approved: "已通过", + rejected: "已驳回", + returned: "已退回", + cancelled: "已取消", + completed: "已完成", + paid: "已付款", + }; + return status ? labels[status] ?? status : "-"; +} + function isInternalId(value: string | null | undefined) { return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value ?? ""); } @@ -120,6 +139,10 @@ function resolveErrorMessage(error: unknown) { return "动作执行失败"; } +function isAlreadyProcessedTaskError(error: unknown) { + return isApiError(error) && error.details?.task === "already_processed"; +} + function classifyReplayError(error: unknown): ReplayClassification { if (!isApiError(error)) { return "failed"; @@ -181,17 +204,23 @@ function matchesEntityFilter(entityType: string | null, filter: EntityFilter) { function toVisibleRemoteTask(task: WorkflowTaskSummary): VisibleTaskCard { const entityType = task.workflowInstance?.entityType ?? null; + const summary = task.businessSummary; + const reference = publicReference(summary?.reference ?? task.workflowInstance?.entityId); + const metric = summary?.amount ?? summary?.duration ?? null; return { id: task.id, - title: task.stepName, + title: summary?.title ?? task.stepName, subtitle: task.status === "completed" - ? `业务:${entityLabel(entityType)} · 完成于 ${formatDateTime(task.completedAt)}` - : `业务:${entityLabel(entityType)}`, + ? `业务:${entityLabel(entityType)} · ${reference ?? "未生成业务编号"} · 完成于 ${formatDateTime(task.completedAt)}` + : `业务:${entityLabel(entityType)} · ${reference ?? "未生成业务编号"}`, status: task.status, entityType, entityId: task.workflowInstance?.entityId ?? null, - reference: publicReference(task.workflowInstance?.entityId), + reference, + applicantName: summary?.applicantName ?? null, + metric, + businessStatus: summary?.businessStatus ?? task.workflowInstance?.currentStatus ?? null, allowedActions: task.allowedActions, completedAt: task.completedAt, isCached: false, @@ -211,6 +240,9 @@ function matchesSearch(card: VisibleTaskCard, keyword: string) { card.id, card.entityId, card.reference, + card.applicantName, + card.metric, + card.businessStatus, ] .map((item) => normalizeSearchValue(item)) .join(" "); @@ -362,6 +394,9 @@ export function MobileApprovalsPage() { entityType: item.taskSnapshot.entityType ?? item.detail.workflowInstance?.entityType ?? null, entityId: item.taskSnapshot.entityId, reference: publicReference(item.taskSnapshot.reference), + applicantName: null, + metric: null, + businessStatus: item.detail.workflowInstance?.currentStatus ?? null, allowedActions: item.detail.allowedActions, completedAt: item.detail.task.completedAt, isCached: true, @@ -386,10 +421,14 @@ export function MobileApprovalsPage() { () => filteredTaskCards.find((task) => task.id === selectedTaskId)?.detail ?? null, [filteredTaskCards, selectedTaskId], ); + const selectedTaskCard = useMemo( + () => filteredTaskCards.find((task) => task.id === selectedTaskId) ?? null, + [filteredTaskCards, selectedTaskId], + ); const selectedTask = detailQuery.data ?? cachedDetailQuery.data?.detail ?? selectedCachedTaskDetail; const selectedReference = selectedTask ? resolveWorkflowTaskReferenceSnapshot(selectedTask) : null; - const selectedDisplayReference = publicReference(selectedReference?.reference); + const selectedDisplayReference = selectedTaskCard?.reference ?? publicReference(selectedReference?.reference); const selectedTaskAllowsActions = Boolean(taskTab === "pending" && selectedTask?.task.status === "pending"); const isOfflineUncachedDeepLink = Boolean(selectedTaskId) && !selectedTask && !canUseRemote; @@ -528,6 +567,9 @@ export function MobileApprovalsPage() { toast.success("操作成功"); }, onError: (actionError) => { + if (isAlreadyProcessedTaskError(actionError)) { + void invalidateWorkflowQueries(selectedTaskId); + } const nextError = resolveErrorMessage(actionError); setError(nextError); setMessage(""); @@ -754,7 +796,7 @@ export function MobileApprovalsPage() { {!isOnline ? (

- 当前离线{offlineAccessMode ? ",已使用本地会话快照进入移动审批壳。" : "。"}仅支持查看已缓存待办,并起草通过 / 驳回草稿。 + 当前离线{offlineAccessMode ? ",已使用本地会话快照进入移动审批壳。" : "。"}仅支持查看已缓存待办,并保存通过 / 驳回草稿;联网同步成功后才会正式提交。

) : null} @@ -857,7 +899,7 @@ export function MobileApprovalsPage() { disabled={!selectedBatchTaskIds.length || batchActionMutation.isPending} onClick={() => handleBatchAction("approve")} > - {canUseRemote ? "批量通过" : "加入通过草稿"} + {canUseRemote ? "批量通过" : "保存通过草稿"} @@ -940,6 +982,9 @@ export function MobileApprovalsPage() {
{task.subtitle}
{task.reference ?
业务单号:{task.reference}
: null} +
+ 申请人:{task.applicantName ?? "-"} · 金额/天数:{task.metric ?? "-"} · 业务状态:{businessStatusLabel(task.businessStatus)} +
{task.isCached ?
来源:离线缓存
: null} @@ -962,6 +1007,9 @@ export function MobileApprovalsPage() {
{task.subtitle}
{task.reference ?
业务单号:{task.reference}
: null} +
+ 申请人:{task.applicantName ?? "-"} · 金额/天数:{task.metric ?? "-"} · 业务状态:{businessStatusLabel(task.businessStatus)} +
{task.isCached ?
来源:离线缓存
: null} ); @@ -997,6 +1045,9 @@ export function MobileApprovalsPage() {
步骤:{selectedTask.task.stepName}
对象类型:{entityLabel(selectedTask.workflowInstance?.entityType)}
业务单号:{selectedDisplayReference ?? "-"}
+
业务状态:{businessStatusLabel(selectedTaskCard?.businessStatus ?? selectedTask.workflowInstance?.currentStatus)}
+
申请人:{selectedTaskCard?.applicantName ?? "-"}
+
金额/天数:{selectedTaskCard?.metric ?? "-"}
创建时间:{formatDateTime(selectedTask.task.createdAt)}
完成时间:{formatDateTime(selectedTask.task.completedAt)}
{selectedTask.reimbursement ? ( @@ -1046,7 +1097,7 @@ export function MobileApprovalsPage() { /> {!canUseRemote ? ( -

离线仅支持通过 / 驳回草稿。

+

离线仅保存通过 / 驳回草稿,联网同步成功后才会正式提交。

) : null}
@@ -1063,7 +1114,7 @@ export function MobileApprovalsPage() { }} type="button" > - {canUseRemote ? "通过" : "离线加入通过草稿"} + {canUseRemote ? "通过" : "保存通过草稿"}
可执行动作
-
{detail.allowedActions.length ? detail.allowedActions.join(" / ") : "-"}
+
{allowedActionLabels || "-"}
@@ -374,7 +420,7 @@ export function WorkflowTaskDetailPage() { - {canTransfer && !isLeaveTask ? ( + {canTransfer ? (

转审