feat(P25-2/P25-3/P26-2/P28-1/P24-2/P29-3): 落地内部OA够用版前端入口与运营页面

关联 Issue: P25-2, P25-3, P25-4, P26-2, P26-4, P28-1, P28-2, P28-3, P28-4, P24-2, P29-3
这个提交包含在:
2026-03-21 18:03:48 +08:00
父节点 e39e41287e
当前提交 9905c1c79d
共修改 44 个文件,包含 2029 行新增和 169 行删除
+78 -40
查看文件
@@ -28,6 +28,7 @@ import {
removeWorkflowActionDraft,
resolveWorkflowTaskReferenceSnapshot,
updateWorkflowActionDraftStatus,
type WorkflowDraftAction,
type WorkflowDraftQueueItem,
} from "../mobile/workflowActionQueue";
@@ -107,6 +108,7 @@ export function MobileApprovalsPage() {
const [comment, setComment] = useState("");
const [transferToApproverId, setTransferToApproverId] = useState("");
const [transferReason, setTransferReason] = useState("");
const [offlineDraftSubmitting, setOfflineDraftSubmitting] = useState(false);
const [isOnline, setIsOnline] = useState<boolean>(typeof navigator === "undefined" ? true : navigator.onLine);
const autoSyncRequestedRef = useRef(false);
@@ -192,14 +194,63 @@ export function MobileApprovalsPage() {
queryFn: () => fetchWorkflowApprovers({ page: 1, pageSize: 100 }),
});
async function refreshDraftQueueCache() {
if (!ownerEmployeeId) {
return [];
}
const drafts = await listWorkflowActionDrafts(ownerEmployeeId);
queryClient.setQueryData(["mobile", "workflow", "draft-queue", ownerEmployeeId], drafts);
return drafts;
}
const currentQueueItems = queueQuery.data ?? [];
const retryableQueueItems = useMemo(
() => currentQueueItems.filter((item) => item.status !== "conflicted"),
[currentQueueItems],
);
async function queueOfflineDraft(action: WorkflowDraftAction) {
try {
if (!selectedTaskId) {
throw new Error("请选择任务");
}
if (!selectedTask) {
throw new Error("当前待办尚未缓存,请联网加载后再操作");
}
if (!selectedTask.allowedActions.includes(action)) {
throw new Error("当前待办不允许该动作");
}
if (action === "reject" && !comment.trim()) {
throw new Error("驳回必须填写意见");
}
setOfflineDraftSubmitting(true);
await enqueueWorkflowActionDraft({
taskId: selectedTaskId,
action,
comment: action === "reject" ? comment.trim() : null,
ownerEmployeeId,
detail: selectedTask,
});
await refreshDraftQueueCache();
setComment("");
setError("");
setMessage(action === "approve" ? "当前离线,通过草稿已加入待同步队列。" : "当前离线,驳回草稿已加入待同步队列。");
toast.success(action === "approve" ? "已加入通过草稿队列" : "已加入驳回草稿队列");
} catch (actionError) {
const nextError = resolveErrorMessage(actionError);
setError(nextError);
setMessage("");
toast.error(nextError);
} finally {
setOfflineDraftSubmitting(false);
}
}
const actionMutation = useMutation({
mutationFn: async (action: MobileAction) => {
if (!canUseRemote) {
throw new Error("当前离线,请使用离线草稿动作");
}
if (!selectedTaskId) {
throw new Error("请选择任务");
}
@@ -210,28 +261,9 @@ export function MobileApprovalsPage() {
throw new Error("当前待办不允许该动作");
}
if (!canUseRemote) {
if (action === "return" || action === "transfer") {
throw new Error("离线仅支持通过 / 驳回草稿,退回与转审需联网执行");
}
if (action === "reject" && !comment.trim()) {
throw new Error("驳回必须填写意见");
}
await enqueueWorkflowActionDraft({
taskId: selectedTaskId,
action,
comment: action === "reject" ? comment.trim() : null,
ownerEmployeeId,
detail: selectedTask,
});
return { mode: "queued" as const, action };
}
if (action === "approve") {
await approveWorkflowTask(selectedTaskId);
return { mode: "submitted" as const, action };
return { action };
}
if (action === "reject") {
@@ -239,7 +271,7 @@ export function MobileApprovalsPage() {
throw new Error("驳回必须填写意见");
}
await rejectWorkflowTask(selectedTaskId, comment.trim());
return { mode: "submitted" as const, action };
return { action };
}
if (action === "return") {
@@ -247,7 +279,7 @@ export function MobileApprovalsPage() {
throw new Error("退回必须填写意见");
}
await returnWorkflowTask(selectedTaskId, comment.trim());
return { mode: "submitted" as const, action };
return { action };
}
if (!transferToApproverId) {
@@ -258,17 +290,9 @@ export function MobileApprovalsPage() {
}
await transferWorkflowTask(selectedTaskId, transferToApproverId, transferReason.trim());
return { mode: "submitted" as const, action };
return { action };
},
onSuccess: async (result) => {
if (result.mode === "queued") {
await queryClient.invalidateQueries({ queryKey: ["mobile", "workflow", "draft-queue", ownerEmployeeId] });
setError("");
setMessage(result.action === "approve" ? "当前离线,通过草稿已加入待同步队列。" : "当前离线,驳回草稿已加入待同步队列。");
toast.success(result.action === "approve" ? "已加入通过草稿队列" : "已加入驳回草稿队列");
return;
}
onSuccess: async () => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["mobile", "workflow", "tasks"] }),
queryClient.invalidateQueries({ queryKey: ["mobile", "workflow", "task-detail", selectedTaskId] }),
@@ -289,6 +313,8 @@ export function MobileApprovalsPage() {
},
});
const isActionSubmitting = actionMutation.isPending || offlineDraftSubmitting;
const syncMutation = useMutation({
mutationFn: async (items: WorkflowDraftQueueItem[]) => {
let succeeded = 0;
@@ -524,16 +550,28 @@ export function MobileApprovalsPage() {
<div className="flex flex-wrap gap-2">
<Button
data-testid="mobile-approvals-approve"
disabled={actionMutation.isPending || !selectedTask.allowedActions.includes("approve")}
onClick={() => actionMutation.mutate("approve")}
disabled={isActionSubmitting || !selectedTask.allowedActions.includes("approve")}
onClick={() => {
if (canUseRemote) {
actionMutation.mutate("approve");
return;
}
void queueOfflineDraft("approve");
}}
type="button"
>
{canUseRemote ? "通过" : "离线加入通过草稿"}
</Button>
<Button
data-testid="mobile-approvals-reject"
disabled={actionMutation.isPending || !selectedTask.allowedActions.includes("reject")}
onClick={() => actionMutation.mutate("reject")}
disabled={isActionSubmitting || !selectedTask.allowedActions.includes("reject")}
onClick={() => {
if (canUseRemote) {
actionMutation.mutate("reject");
return;
}
void queueOfflineDraft("reject");
}}
type="button"
variant="secondary"
>
@@ -541,7 +579,7 @@ export function MobileApprovalsPage() {
</Button>
<Button
data-testid="mobile-approvals-return"
disabled={actionMutation.isPending || !canUseRemote || !selectedTask.allowedActions.includes("return")}
disabled={isActionSubmitting || !canUseRemote || !selectedTask.allowedActions.includes("return")}
onClick={() => actionMutation.mutate("return")}
type="button"
variant="secondary"
@@ -577,7 +615,7 @@ export function MobileApprovalsPage() {
<Button
data-testid="mobile-approvals-transfer"
disabled={actionMutation.isPending || !selectedTask.allowedActions.includes("transfer")}
disabled={isActionSubmitting || !selectedTask.allowedActions.includes("transfer")}
onClick={() => actionMutation.mutate("transfer")}
type="button"
variant="secondary"
@@ -643,7 +681,7 @@ export function MobileApprovalsPage() {
status: "queued",
lastError: null,
});
await queryClient.invalidateQueries({ queryKey: ["mobile", "workflow", "draft-queue", ownerEmployeeId] });
await refreshDraftQueueCache();
if (canUseRemote) {
syncMutation.mutate([{ ...item, status: "queued", lastError: null }]);
}
@@ -658,7 +696,7 @@ export function MobileApprovalsPage() {
variant="secondary"
onClick={async () => {
await removeWorkflowActionDraft(item.taskId, ownerEmployeeId);
await queryClient.invalidateQueries({ queryKey: ["mobile", "workflow", "draft-queue", ownerEmployeeId] });
await refreshDraftQueueCache();
}}
>
丢弃草稿