修正企微主办边界与工作台摘要(portal-task-first-redesign)

这个提交包含在:
2026-05-29 23:30:30 +08:00
父节点 86f887afa5
当前提交 8bd184f0b3
共修改 9 个文件,包含 296 行新增和 264 行删除
+30 -4
查看文件
@@ -28,6 +28,7 @@ type WorkItem = {
severity: "normal" | "warning" | "high";
icon: string;
priority: number;
count: number;
};
type EmptyAction = {
@@ -107,6 +108,7 @@ function buildTodayActionItems(payload: PortalHomePayload, roleCapabilities: Rol
severity: "warning",
icon: "fact_check",
priority: 100,
count: payload.pendingTasks.count,
});
}
@@ -125,6 +127,7 @@ function buildTodayActionItems(payload: PortalHomePayload, roleCapabilities: Rol
severity: highPriorityCount > 0 ? "high" : "normal",
icon: "check_circle",
priority: 90,
count: openTodoCount,
});
}
@@ -139,6 +142,7 @@ function buildTodayActionItems(payload: PortalHomePayload, roleCapabilities: Rol
severity: payload.weeklyReport.needsHelp ? "high" : "warning",
icon: "article",
priority: 72,
count: 1,
});
}
@@ -154,6 +158,7 @@ function buildTodayActionItems(payload: PortalHomePayload, roleCapabilities: Rol
severity: "high",
icon: "priority_high",
priority: 96,
count: payload.financeSummary.failedItemCount,
});
}
@@ -168,6 +173,7 @@ function buildTodayActionItems(payload: PortalHomePayload, roleCapabilities: Rol
severity: "high",
icon: "shield_person",
priority: 94,
count: payload.financeSummary.riskCount,
});
}
@@ -182,6 +188,7 @@ function buildTodayActionItems(payload: PortalHomePayload, roleCapabilities: Rol
severity: "warning",
icon: "payments",
priority: 86,
count: payload.financeSummary.pendingBatchCount,
});
}
}
@@ -197,6 +204,7 @@ function buildTodayActionItems(payload: PortalHomePayload, roleCapabilities: Rol
severity: "warning",
icon: "workspaces",
priority: 64,
count: payload.projectSummary.highPriorityTaskCount,
});
}
@@ -211,6 +219,7 @@ function buildTodayActionItems(payload: PortalHomePayload, roleCapabilities: Rol
severity: "warning",
icon: "event_upcoming",
priority: 62,
count: payload.projectSummary.followUpDueTodayCount,
});
}
@@ -231,6 +240,7 @@ function buildProgressItems(payload: PortalHomePayload) {
severity: "normal",
icon: "done_all",
priority: 30,
count: 1,
});
}
@@ -255,6 +265,7 @@ function buildTeamBlockers(payload: PortalHomePayload, roleCapabilities: RoleCap
severity: "warning",
icon: "article",
priority: 78,
count: payload.weeklySummary?.missingCount ?? 0,
});
}
@@ -269,6 +280,7 @@ function buildTeamBlockers(payload: PortalHomePayload, roleCapabilities: RoleCap
severity: "high",
icon: "support_agent",
priority: 82,
count: payload.weeklySummary?.needsHelpCount ?? 0,
});
}
@@ -283,6 +295,7 @@ function buildTeamBlockers(payload: PortalHomePayload, roleCapabilities: RoleCap
severity: "warning",
icon: "groups",
priority: 58,
count: payload.teamPresence?.unspecified ?? 0,
});
}
@@ -297,6 +310,7 @@ function buildTeamBlockers(payload: PortalHomePayload, roleCapabilities: RoleCap
severity: "warning",
icon: "flag",
priority: 74,
count: payload.projectSummary?.overdueMilestonesCount ?? 0,
});
}
@@ -311,6 +325,7 @@ function buildTeamBlockers(payload: PortalHomePayload, roleCapabilities: RoleCap
severity: "high",
icon: "warning",
priority: 76,
count: payload.projectSummary?.openRiskCount ?? 0,
});
}
@@ -325,6 +340,7 @@ function buildTeamBlockers(payload: PortalHomePayload, roleCapabilities: RoleCap
severity: "warning",
icon: "inventory_2",
priority: 60,
count: (payload.peopleLifecycle?.offboardingCount ?? 0) + (payload.peopleLifecycle?.assetReturnPendingCount ?? 0),
});
}
@@ -353,6 +369,7 @@ function buildManagementReminders(
severity: "warning",
icon: "settings",
priority: 88,
count: 1,
});
}
@@ -370,6 +387,7 @@ function buildManagementReminders(
severity: "warning",
icon: "manage_accounts",
priority: 52,
count: inactiveLoginCount,
});
}
@@ -391,6 +409,10 @@ function buildEmptyActions(permissions: string[], roleCapabilities: RoleCapabili
return actions.slice(0, roleCapabilities.canManageSystem && actions.length > 2 ? 3 : 2);
}
function sumWorkItemCount(items: WorkItem[]) {
return items.reduce((total, item) => total + Math.max(1, item.count), 0);
}
function SummaryStrip({
userName,
actionCount,
@@ -561,6 +583,10 @@ export function PortalHomePage() {
const managementReminders = buildManagementReminders(payload, roleCapabilities, canSetupWorkspace);
const emptyActions = buildEmptyActions(permissions, roleCapabilities);
const primaryItem = todayActionItems[0] ?? null;
const actionCount = sumWorkItemCount(todayActionItems);
const progressCount = sumWorkItemCount(progressItems);
const blockerCount = sumWorkItemCount(teamBlockers);
const reminderCount = sumWorkItemCount(managementReminders);
function handleCloseWelcome() {
clearWelcomeDialogFlag();
@@ -575,10 +601,10 @@ export function PortalHomePage() {
<SummaryStrip
userName={payload.greeting.name}
actionCount={todayActionItems.length}
progressCount={progressItems.length}
blockerCount={teamBlockers.length}
reminderCount={managementReminders.length}
actionCount={actionCount}
progressCount={progressCount}
blockerCount={blockerCount}
reminderCount={reminderCount}
primaryItem={primaryItem}
emptyActions={emptyActions}
/>
+9 -11
查看文件
@@ -587,17 +587,6 @@ export function ReimbursementsPage() {
</div>
<div className="ops-command-bar__actions">
{canApproveExpense ? (
<Button
data-testid="reimbursements-actionable"
size="sm"
variant={status === "reviewing" && scope === "all" ? "default" : "secondary"}
onClick={() => setSearchParams({ status: "reviewing", page: "1", scope: "all" })}
type="button"
>
待我处理
</Button>
) : null}
{canShowLedgerEntry ? (
<Button
size="sm"
@@ -698,6 +687,15 @@ export function ReimbursementsPage() {
{canViewAll ? (
<>
<Button
data-testid="reimbursements-reviewing-ledger"
size="sm"
variant={status === "reviewing" && scope === "all" ? "default" : "secondary"}
onClick={() => setSearchParams({ status: "reviewing", page: "1", scope: "all" })}
type="button"
>
审批中台账
</Button>
<Button
data-testid="reimbursements-scope-own"
size="sm"
+2 -127
查看文件
@@ -1,17 +1,14 @@
import { useState } from "react";
import { Link, useSearchParams } from "react-router-dom";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { useQuery } from "@tanstack/react-query";
import { isApiError } from "../api/types";
import { mapApiErrorMessage } from "../api/errorMessages";
import {
approveWorkflowTask,
fetchWorkflowTasks,
fetchWorkflowTaskSummary,
type WorkflowTaskSummary,
} from "../api/workflow";
import { Pagination } from "../components/common/Pagination";
import { ConfirmDialog } from "../components/common/ConfirmDialog";
import { StatusBadge } from "../components/common/StatusBadge";
import { TableSkeleton } from "../components/common/TableSkeleton";
import {
@@ -28,7 +25,6 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from ".
import { Tabs, TabsList, TabsTrigger } from "../components/ui/tabs";
import { formatDisplayDateTime } from "../lib/displayFormat";
import { TEST_IDS } from "../lib/testIds";
import { getWorkflowConfirmCopy } from "../lib/workflowActionConfirm";
const PAGE_SIZE = 20;
const WORKFLOW_DEFAULT_DUE_MS = 24 * 60 * 60 * 1000;
@@ -191,18 +187,6 @@ function isTaskDefaultOverdue(task: WorkflowTaskSummary) {
return task.status === "pending" && !isWithinDefaultDueWindow(task.createdAt);
}
function parseDurationDays(value: string | null | undefined) {
if (!value) {
return null;
}
const match = /([0-9]+(?:\.[0-9]+)?)/.exec(value);
if (!match) {
return null;
}
const days = Number(match[1]);
return Number.isFinite(days) ? days : null;
}
function workflowTaskStatusLabel(status: string) {
const match = WORKFLOW_STATUS_OPTIONS.find((item) => item.value === status);
return match?.label ?? status;
@@ -221,37 +205,12 @@ function taskSecondaryMeta(task: WorkflowTaskSummary) {
return [formatReceivedTime(task.createdAt), displayReference(task)].join(" · ");
}
function isLowRiskQuickApproveTask(task: WorkflowTaskSummary) {
if (task.status !== "pending" || !(task.allowedActions ?? []).includes("approve")) {
return false;
}
if (!displayTitle(task) || displayApplicant(task) === "-") {
return false;
}
const entityType = task.workflowInstance?.entityType;
if (entityType === "leave") {
const days = parseDurationDays(task.businessSummary?.duration);
if (days === null || days > 1) {
return false;
}
} else if (entityType !== "attendance_correction") {
return false;
}
return isWithinDefaultDueWindow(task.createdAt);
}
function isAlreadyProcessedTaskError(error: unknown) {
return isApiError(error) && error.details?.task === "already_processed";
}
export function WorkflowTasksPage() {
const [searchParams, setSearchParams] = useSearchParams();
const status = normalizeStatusFilter(searchParams.get("status"));
const entityType = normalizeEntityFilter(searchParams.get("entityType"));
const page = Number(searchParams.get("page") ?? "1") || 1;
const [confirmApproveTaskId, setConfirmApproveTaskId] = useState<string | null>(null);
const [showAdvanced, setShowAdvanced] = useState(() => status !== "pending" || entityType !== "all");
const queryClient = useQueryClient();
function updateSearchParams(next: {
status?: WorkflowStatusFilter;
@@ -286,35 +245,8 @@ export function WorkflowTasksPage() {
queryFn: () => fetchWorkflowTaskSummary(),
});
const approveMutation = useMutation({
mutationFn: (taskId: string) => approveWorkflowTask(taskId),
onSuccess: async () => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["workflow", "tasks"] }),
queryClient.invalidateQueries({ queryKey: ["portal-home"] }),
queryClient.invalidateQueries({ queryKey: ["notifications"] }),
]);
toast.success("审批已通过");
},
onError: (error) => {
if (isAlreadyProcessedTaskError(error)) {
void queryClient.invalidateQueries({ queryKey: ["workflow", "tasks"] });
}
const message = isApiError(error)
? `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`
: error instanceof Error
? error.message
: "审批失败,请稍后重试";
toast.error(message);
},
});
const totalPages = Math.max(1, Math.ceil((query.data?.total ?? 0) / PAGE_SIZE));
const taskItems = query.data?.items ?? [];
const confirmCopy = confirmApproveTaskId ? getWorkflowConfirmCopy("approve") : null;
const confirmApproveTask = confirmApproveTaskId
? taskItems.find((task) => task.id === confirmApproveTaskId) ?? null
: null;
const isDefaultTaskView = status === "pending" && entityType === "all";
const advancedVisible = showAdvanced || !isDefaultTaskView;
const pendingTotal = summaryQuery.data?.pendingTotal ?? "-";
@@ -339,7 +271,6 @@ export function WorkflowTasksPage() {
}
return (
<>
<ListWorkspace>
<WorkspaceHeader
eyebrow="审批"
@@ -485,7 +416,7 @@ export function WorkflowTasksPage() {
<div className="mt-3 grid grid-cols-2 gap-2">
<Button
asChild
className={`min-h-11 ${isLowRiskQuickApproveTask(task) ? "" : "col-span-2"}`}
className="col-span-2 min-h-11"
size="sm"
data-testid={TEST_IDS.workflow.detailLink}
>
@@ -493,19 +424,6 @@ export function WorkflowTasksPage() {
{task.status === "pending" ? "查看处理" : "查看详情"}
</Link>
</Button>
{isLowRiskQuickApproveTask(task) ? (
<Button
className="min-h-11"
size="sm"
variant="secondary"
type="button"
data-testid={TEST_IDS.workflow.quickApprove}
disabled={approveMutation.isPending && approveMutation.variables === task.id}
onClick={() => setConfirmApproveTaskId(task.id)}
>
{approveMutation.isPending && approveMutation.variables === task.id ? "通过中..." : "通过"}
</Button>
) : null}
</div>
</article>
))
@@ -567,18 +485,6 @@ export function WorkflowTasksPage() {
{task.status === "pending" ? "查看处理" : "查看详情"}
</Link>
</Button>
{isLowRiskQuickApproveTask(task) ? (
<Button
size="sm"
variant="secondary"
type="button"
data-testid={TEST_IDS.workflow.quickApprove}
disabled={approveMutation.isPending && approveMutation.variables === task.id}
onClick={() => setConfirmApproveTaskId(task.id)}
>
{approveMutation.isPending && approveMutation.variables === task.id ? "通过中..." : "通过"}
</Button>
) : null}
</div>
</TableCell>
</TableRow>
@@ -603,36 +509,5 @@ export function WorkflowTasksPage() {
testIdPrefix="workflow-tasks"
/>
</ListWorkspace>
<ConfirmDialog
open={Boolean(confirmCopy)}
onOpenChange={(open) => {
if (!open) {
setConfirmApproveTaskId(null);
}
}}
title={confirmCopy?.title ?? ""}
description={confirmCopy ? (
<>
<span>{confirmCopy.description}</span>
{confirmApproveTask ? (
<span className="mt-2 block rounded-lg bg-slate-50 px-3 py-2 text-xs leading-5 text-slate-600">
当前事项:{displayTaskTitle(confirmApproveTask)} · {taskLeadMeta(confirmApproveTask)}
</span>
) : null}
</>
) : undefined}
confirmLabel={confirmCopy?.confirmLabel}
variant={confirmCopy?.variant}
testId="workflow-action-confirm-dialog"
confirmTestId="workflow-action-confirm-submit"
cancelTestId="workflow-action-confirm-cancel"
onConfirm={async () => {
if (!confirmApproveTaskId) {
return;
}
await approveMutation.mutateAsync(confirmApproveTaskId);
}}
/>
</>
);
}