A2 收口统一审批中心入口与状态
这个提交包含在:
@@ -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<string, string> = {
|
||||
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 ? (
|
||||
<p className="rounded-lg border border-orange-200 bg-orange-50 px-3 py-2 text-sm text-orange-700">
|
||||
当前离线{offlineAccessMode ? ",已使用本地会话快照进入移动审批壳。" : "。"}仅支持查看已缓存待办,并起草通过 / 驳回草稿。
|
||||
当前离线{offlineAccessMode ? ",已使用本地会话快照进入移动审批壳。" : "。"}仅支持查看已缓存待办,并保存通过 / 驳回草稿;联网同步成功后才会正式提交。
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -857,7 +899,7 @@ export function MobileApprovalsPage() {
|
||||
disabled={!selectedBatchTaskIds.length || batchActionMutation.isPending}
|
||||
onClick={() => handleBatchAction("approve")}
|
||||
>
|
||||
{canUseRemote ? "批量通过" : "加入通过草稿"}
|
||||
{canUseRemote ? "批量通过" : "保存通过草稿"}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -867,7 +909,7 @@ export function MobileApprovalsPage() {
|
||||
disabled={!selectedBatchTaskIds.length || batchActionMutation.isPending || !batchRejectComment.trim()}
|
||||
onClick={() => handleBatchAction("reject")}
|
||||
>
|
||||
{canUseRemote ? "批量驳回" : "加入驳回草稿"}
|
||||
{canUseRemote ? "批量驳回" : "保存驳回草稿"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -940,6 +982,9 @@ export function MobileApprovalsPage() {
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">{task.subtitle}</div>
|
||||
{task.reference ? <div className="mt-1 text-xs text-zinc-500">业务单号:{task.reference}</div> : null}
|
||||
<div className="mt-1 text-xs text-zinc-500">
|
||||
申请人:{task.applicantName ?? "-"} · 金额/天数:{task.metric ?? "-"} · 业务状态:{businessStatusLabel(task.businessStatus)}
|
||||
</div>
|
||||
{task.isCached ? <div className="mt-1 text-xs text-zinc-500">来源:离线缓存</div> : null}
|
||||
</button>
|
||||
</div>
|
||||
@@ -962,6 +1007,9 @@ export function MobileApprovalsPage() {
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">{task.subtitle}</div>
|
||||
{task.reference ? <div className="mt-1 text-xs text-zinc-500">业务单号:{task.reference}</div> : null}
|
||||
<div className="mt-1 text-xs text-zinc-500">
|
||||
申请人:{task.applicantName ?? "-"} · 金额/天数:{task.metric ?? "-"} · 业务状态:{businessStatusLabel(task.businessStatus)}
|
||||
</div>
|
||||
{task.isCached ? <div className="mt-1 text-xs text-zinc-500">来源:离线缓存</div> : null}
|
||||
</button>
|
||||
);
|
||||
@@ -997,6 +1045,9 @@ export function MobileApprovalsPage() {
|
||||
<div>步骤:{selectedTask.task.stepName}</div>
|
||||
<div>对象类型:{entityLabel(selectedTask.workflowInstance?.entityType)}</div>
|
||||
<div>业务单号:{selectedDisplayReference ?? "-"}</div>
|
||||
<div>业务状态:{businessStatusLabel(selectedTaskCard?.businessStatus ?? selectedTask.workflowInstance?.currentStatus)}</div>
|
||||
<div>申请人:{selectedTaskCard?.applicantName ?? "-"}</div>
|
||||
<div>金额/天数:{selectedTaskCard?.metric ?? "-"}</div>
|
||||
<div>创建时间:{formatDateTime(selectedTask.task.createdAt)}</div>
|
||||
<div>完成时间:{formatDateTime(selectedTask.task.completedAt)}</div>
|
||||
{selectedTask.reimbursement ? (
|
||||
@@ -1046,7 +1097,7 @@ export function MobileApprovalsPage() {
|
||||
/>
|
||||
|
||||
{!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">
|
||||
@@ -1063,7 +1114,7 @@ export function MobileApprovalsPage() {
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{canUseRemote ? "通过" : "离线加入通过草稿"}
|
||||
{canUseRemote ? "通过" : "保存通过草稿"}
|
||||
</Button>
|
||||
<Button
|
||||
className="min-h-12 w-full"
|
||||
@@ -1079,7 +1130,7 @@ export function MobileApprovalsPage() {
|
||||
type="button"
|
||||
variant="secondary"
|
||||
>
|
||||
{canUseRemote ? "驳回" : "离线加入驳回草稿"}
|
||||
{canUseRemote ? "驳回" : "保存驳回草稿"}
|
||||
</Button>
|
||||
<Button
|
||||
className="min-h-12 w-full"
|
||||
|
||||
在新工单中引用
屏蔽一个用户