MIF7 移动信息架构整改收口

这个提交包含在:
2026-05-13 00:02:21 +08:00
父节点 39a930399f
当前提交 c83dbb7376
共修改 10 个文件,包含 691 行新增和 206 行删除
+92 -26
查看文件
@@ -1,6 +1,6 @@
import { useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom";
import { Link, useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom";
import { toast } from "sonner";
import { mapApiErrorMessage } from "../api/errorMessages";
import { fetchLeaveRequests, type LeaveRequestItem } from "../api/leave";
@@ -22,6 +22,7 @@ import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission } from "../auth/permissions";
import { PageHeader } from "../components/common/PageHeader";
import { StatusBadge } from "../components/common/StatusBadge";
import { MobileDetailHeader } from "../components/mobile/MobileDetailHeader";
import { PageScaffold } from "../components/patterns/PageScaffold";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
@@ -47,6 +48,8 @@ import {
type MobileAction = "approve" | "reject" | "return" | "transfer";
type MobileTaskTab = "pending" | "completed";
type EmployeeProgressTab = "draft" | "reviewing" | "approved" | "returned";
type EmployeeProgressQueryStatus = "draft" | "reviewing" | "approved" | "rejected";
type EntityFilter = "all" | "leave" | "reimbursement" | "purchase_request" | "travel_request";
type ReplayClassification = "failed" | "conflicted";
@@ -83,6 +86,24 @@ const ENTITY_FILTER_OPTIONS: Array<{ value: EntityFilter; label: string }> = [
{ value: "travel_request", label: "差旅" },
];
const EMPLOYEE_PROGRESS_TABS: Array<{ value: EmployeeProgressTab; label: string }> = [
{ value: "draft", label: "草稿" },
{ value: "reviewing", label: "审批中" },
{ value: "approved", label: "已通过" },
{ value: "returned", label: "退回/驳回" },
];
function resolveProgressTab(value: string | null | undefined): EmployeeProgressTab {
if (value === "draft" || value === "reviewing" || value === "approved" || value === "returned") {
return value;
}
return "reviewing";
}
function progressStatusQuery(tab: EmployeeProgressTab): EmployeeProgressQueryStatus {
return tab === "returned" ? "rejected" : tab;
}
function entityLabel(entityType: string | undefined | null) {
if (entityType === "leave") {
return "请假";
@@ -320,14 +341,18 @@ function toEmployeeProgressCards(input: {
export function MobileApprovalsPage() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const location = useLocation();
const routeParams = useParams();
const { me, permissions, accessToken, offlineAccessMode } = useAuth();
const [searchParams] = useSearchParams();
const [searchParams, setSearchParams] = useSearchParams();
const deepLinkTaskId = routeParams.taskId?.trim() || searchParams.get("taskId")?.trim() || "";
const isPushDetail = Boolean(deepLinkTaskId);
const [selectedTaskId, setSelectedTaskId] = useState(deepLinkTaskId);
const [taskTab, setTaskTab] = useState<MobileTaskTab>("pending");
const [progressTab, setProgressTab] = useState<EmployeeProgressTab>(() =>
resolveProgressTab(searchParams.get("progress")),
);
const [entityFilter, setEntityFilter] = useState<EntityFilter>("all");
const [searchInput, setSearchInput] = useState("");
const deferredSearch = useDeferredValue(searchInput);
@@ -347,29 +372,35 @@ export function MobileApprovalsPage() {
const canAccessRemoteApprovals = hasAnyPermission(permissions, [...MOBILE_APPROVAL_PERMISSIONS]);
const canUseRemote = isOnline && Boolean(accessToken) && canAccessRemoteApprovals;
const remoteTaskStatus = taskTab === "completed" ? "completed" : "pending";
const currentMobileSource = `${location.pathname}${location.search}`;
const employeeProgressStatus = progressStatusQuery(progressTab);
const reimbursementProgressQuery = useQuery({
enabled: !canAccessRemoteApprovals,
queryKey: ["mobile", "approvals", "progress", "reimbursements"],
queryFn: () => fetchReimbursements({ scope: "own", page: 1, pageSize: 5 }),
queryKey: ["mobile", "approvals", "progress", "reimbursements", employeeProgressStatus],
queryFn: () => fetchReimbursements({ scope: "own", status: employeeProgressStatus, page: 1, pageSize: 8 }),
});
const leaveProgressQuery = useQuery({
enabled: !canAccessRemoteApprovals,
queryKey: ["mobile", "approvals", "progress", "leave"],
queryFn: () => fetchLeaveRequests({ scope: "own", page: 1, pageSize: 5 }),
queryKey: ["mobile", "approvals", "progress", "leave", employeeProgressStatus],
queryFn: () => fetchLeaveRequests({ scope: "own", status: employeeProgressStatus, page: 1, pageSize: 8 }),
});
const purchaseProgressQuery = useQuery({
enabled: !canAccessRemoteApprovals,
queryKey: ["mobile", "approvals", "progress", "purchase"],
queryFn: () => fetchPurchaseRequests({ mine: true, page: 1, pageSize: 5 }),
queryKey: ["mobile", "approvals", "progress", "purchase", employeeProgressStatus],
queryFn: () => fetchPurchaseRequests({ mine: true, status: employeeProgressStatus, page: 1, pageSize: 8 }),
});
useEffect(() => {
setSelectedTaskId(deepLinkTaskId);
}, [deepLinkTaskId]);
useEffect(() => {
setProgressTab(resolveProgressTab(searchParams.get("progress")));
}, [searchParams]);
useEffect(() => {
if (taskTab === "completed") {
setBatchMode(false);
@@ -379,7 +410,15 @@ export function MobileApprovalsPage() {
}, [taskTab]);
function handleTaskSelection(taskId: string) {
navigate(`/mobile/approvals/${encodeURIComponent(taskId)}`);
navigate(`/mobile/approvals/${encodeURIComponent(taskId)}`, { state: { from: currentMobileSource } });
}
function handleProgressTabChange(value: string) {
const nextTab = value as EmployeeProgressTab;
setProgressTab(nextTab);
const nextParams = new URLSearchParams(searchParams);
nextParams.set("progress", nextTab);
setSearchParams(nextParams, { replace: true });
}
async function invalidateWorkflowQueries(taskId: string | null = selectedTaskId) {
@@ -882,21 +921,30 @@ export function MobileApprovalsPage() {
});
}
const listTitle = taskTab === "pending" ? "待办任务" : "已处理任务";
const listTitle = taskTab === "pending" ? "待我审批" : "已处理";
const allVisibleSelected =
filteredTaskCards.length > 0 && filteredTaskCards.every((task) => selectedBatchTaskIds.includes(task.id));
return (
<PageScaffold className="space-y-4">
<PageHeader
title={isPushDetail ? "审批详情" : canAccessRemoteApprovals ? "审批" : "审批进度"}
subtitle={isPushDetail ? "查看并处理指定待办。" : canAccessRemoteApprovals ? "待我审批和已处理。" : "查看我发起的申请进度。"}
actions={isPushDetail ? (
<Button asChild size="sm" type="button" variant="secondary">
<Link to="/mobile/approvals">返回</Link>
</Button>
) : null}
/>
{isPushDetail ? (
<MobileDetailHeader
title="审批详情"
subtitle={selectedDetailTitle || "查看审批任务"}
status={
selectedTask ? (
<StatusBadge status={selectedTask.task.status} label={workflowStatusLabel(selectedTask.task.status)} />
) : null
}
fallbackTo="/mobile/approvals"
testId="mobile-approvals-detail-header"
/>
) : (
<PageHeader
title={canAccessRemoteApprovals ? "审批" : "审批进度"}
subtitle={canAccessRemoteApprovals ? "待我审批和已处理。" : "查看我发起的申请进度。"}
/>
)}
{!isOnline && canAccessRemoteApprovals ? (
<p className="rounded-lg border border-orange-200 bg-orange-50 px-3 py-2 text-sm text-orange-700">
@@ -916,12 +964,22 @@ export function MobileApprovalsPage() {
<section className="mobile-card space-y-3" data-testid="mobile-approvals-progress">
<div className="flex items-center justify-between gap-3">
<div>
<h3 className="text-base font-semibold text-zinc-900">我发起的</h3>
<h3 className="text-base font-semibold text-zinc-900">我发起的进度</h3>
<p className="mt-1 text-sm text-zinc-500">报销、请假、采购进度</p>
</div>
<span className="badge-neutral">{employeeProgressCards.length} 条</span>
</div>
<Tabs value={progressTab} onValueChange={handleProgressTabChange}>
<TabsList className="grid h-auto w-full grid-cols-4">
{EMPLOYEE_PROGRESS_TABS.map((item) => (
<TabsTrigger key={item.value} value={item.value} data-testid={`mobile-progress-tab-${item.value}`}>
{item.label}
</TabsTrigger>
))}
</TabsList>
</Tabs>
{employeeProgressLoading ? <div className="mobile-empty-state">加载中...</div> : null}
{employeeProgressError ? (
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
@@ -934,7 +992,13 @@ export function MobileApprovalsPage() {
<div className="space-y-2">
{employeeProgressCards.map((item) => (
<Link className="mobile-list-card block" data-testid="mobile-approval-progress-card" key={item.id} to={item.to}>
<Link
className="mobile-list-card block"
data-testid="mobile-approval-progress-card"
key={item.id}
to={item.to}
state={{ from: currentMobileSource }}
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-zinc-900">{item.title}</div>
@@ -960,7 +1024,7 @@ export function MobileApprovalsPage() {
<Tabs value={taskTab} onValueChange={(value) => setTaskTab(value as MobileTaskTab)}>
<TabsList className="flex w-full justify-start gap-1">
<TabsTrigger value="pending">待处理</TabsTrigger>
<TabsTrigger value="pending">待我审批</TabsTrigger>
<TabsTrigger value="completed" disabled={!canUseRemote}>已处理</TabsTrigger>
</TabsList>
</Tabs>
@@ -970,7 +1034,7 @@ export function MobileApprovalsPage() {
data-testid="mobile-approvals-search"
value={searchInput}
onChange={(event) => setSearchInput(event.target.value)}
placeholder="搜索待办"
placeholder="搜索审批"
/>
<details className="mobile-subcard bg-zinc-50/70 text-sm">
@@ -1086,10 +1150,10 @@ export function MobileApprovalsPage() {
{taskTab === "completed"
? "最近 30 天暂无符合筛选条件的已处理任务。"
: !canAccessRemoteApprovals && isOnline
? "暂无审批待办;收到企业微信审批消息后可从这里查看。"
? "暂无待我审批;收到企业微信审批消息后可从这里查看。"
: canUseRemote
? "暂无符合筛选条件的待办任务。"
: "暂无符合筛选条件的已缓存待办。"}
? "暂无符合筛选条件的审批任务。"
: "暂无符合筛选条件的已缓存审批。"}
</p>
) : null}
@@ -1324,6 +1388,7 @@ export function MobileApprovalsPage() {
</section>
) : null}
{!isPushDetail ? (
<section className="mobile-card space-y-3" data-testid="mobile-approvals-queue">
<div className="flex items-center justify-between gap-3">
<h3 className="text-base font-semibold text-zinc-900">离线草稿</h3>
@@ -1405,6 +1470,7 @@ export function MobileApprovalsPage() {
</div>
))}
</section>
) : null}
</PageScaffold>
);
}