WARCH2 增加企微审批历史只读查询

这个提交包含在:
2026-05-20 10:23:23 +08:00
父节点 7a32399ca9
当前提交 123ebc9c95
共修改 23 个文件,包含 1358 行新增和 36 行删除
+112
查看文件
@@ -0,0 +1,112 @@
import { apiRequestData } from "./http";
export type WecomApprovalArchiveScope = "self" | "team" | "all";
export type WecomApprovalArchiveType = "leave" | "missed_punch";
export type WecomApprovalArchiveStatus = "pending" | "approved" | "rejected" | "revoked" | "terminated" | "changed" | "unknown";
export type WecomApprovalArchiveFinding = "open" | "resolved" | "any";
export type WecomApprovalArchiveEmployee = {
id: string;
employeeNo: string | null;
name: string;
} | null;
export type WecomApprovalArchiveItem = {
id: string;
spNoPrefix: string | null;
templateIdPrefix: string | null;
templateType: string;
spName: string | null;
normalizedStatus: string;
applicant: WecomApprovalArchiveEmployee;
appliedAt: string | null;
submittedAt: string | null;
completedAt: string | null;
statusObservedAt: string | null;
lastSource: string | null;
lastCallbackAt: string | null;
lastCompensationAt: string | null;
readabilityStatus: string | null;
singleTapApproveRisk: boolean;
templateAdjustmentRequired: boolean;
participantCount: number;
actionCount: number;
fieldCount: number;
findingCount: number;
openFindingCount: number;
};
export type WecomApprovalArchiveDetail = WecomApprovalArchiveItem & {
participants: Array<{
id: string;
role: string;
nodeIndex: number;
employee: WecomApprovalArchiveEmployee;
isCurrent: boolean;
mappingStatus: string;
}>;
actions: Array<{
id: string;
actionType: string;
operator: WecomApprovalArchiveEmployee;
actionTime: string | null;
nodeIndex: number;
source: string;
}>;
fields: Array<{
id: string;
fieldKey: string;
fieldIndex: number;
fieldName: string | null;
fieldType: string | null;
sensitiveLevel: string;
displayAllowed: boolean;
}>;
findings: Array<{
id: string;
findingType: string;
status: string;
severity: string;
source: string;
detectedAt: string | null;
resolvedAt: string | null;
}>;
};
export type WecomApprovalArchiveListQuery = {
scope?: WecomApprovalArchiveScope;
type?: "" | WecomApprovalArchiveType;
status?: "" | WecomApprovalArchiveStatus;
finding?: "" | WecomApprovalArchiveFinding;
page?: number;
pageSize?: number;
};
export type WecomApprovalArchiveListResponse = {
scope: WecomApprovalArchiveScope;
requestedScope: WecomApprovalArchiveScope;
canViewAll: boolean;
page: number;
pageSize: number;
total: number;
status: "OK" | "PARTIAL_ARCHIVE_NEEDS_DATA";
items: WecomApprovalArchiveItem[];
};
function buildQuery(query: WecomApprovalArchiveListQuery = {}) {
const search = new URLSearchParams();
Object.entries(query).forEach(([key, value]) => {
if (value === undefined || value === null || value === "") return;
search.set(key, String(value));
});
const value = search.toString();
return value ? `?${value}` : "";
}
export function fetchWecomApprovalArchive(query: WecomApprovalArchiveListQuery = {}) {
return apiRequestData<WecomApprovalArchiveListResponse>(`/api/wecom/approval-archive${buildQuery(query)}`);
}
export function fetchWecomApprovalArchiveDetail(id: string) {
return apiRequestData<{ item: WecomApprovalArchiveDetail }>(`/api/wecom/approval-archive/${encodeURIComponent(id)}`);
}
+3 -1
查看文件
@@ -1,12 +1,13 @@
export const MAIN_NAV_ORDER = ["/portal", "/workflow/tasks", "/team", "/leave-requests", "/reimbursements", "/assets", "/weekly-reports", "/todos", "/announcements", "/projects"];
export const ADMIN_NAV_ORDER = ["/system/workspace", "/people", "/organization", "/system/workflow-definitions", "/system/settings", "/system/feature-flags", "/audit-logs"];
export const BACKUP_NAV_ORDER = ["/attendance", "/contracts-registry", "/purchase-requests", "/travel-requests", "/budgets", "/payment-batches", "/ap-operations", "/invoice-pool", "/inventory-checks", "/system/cutover", "/system/integrations/erp", "/dashboard", "/settings/profile"];
export const BACKUP_NAV_ORDER = ["/attendance", "/wecom-approval-archive", "/contracts-registry", "/purchase-requests", "/travel-requests", "/budgets", "/payment-batches", "/ap-operations", "/invoice-pool", "/inventory-checks", "/system/cutover", "/system/integrations/erp", "/dashboard", "/settings/profile"];
const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [
{ path: "/portal", title: "工作台", group: "工作台" },
{ path: "/announcements", title: "公告", group: "主导航" },
{ path: "/team", title: "团队", group: "主导航" },
{ path: "/attendance", title: "考勤", group: "备用模块" },
{ path: "/wecom-approval-archive", title: "企微审批历史", group: "备用模块" },
{ path: "/projects", title: "项目", group: "主导航" },
{ path: "/contracts-registry", title: "合同台账", group: "备用模块" },
{ path: "/purchase-requests", title: "采购申请", group: "备用模块" },
@@ -26,6 +27,7 @@ const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [
{ path: "/workflow/tasks", title: "审批待办", group: "主导航" },
{ path: "/mobile/assets/scan", title: "移动资产扫描", group: "移动工作台" },
{ path: "/mobile/approvals", title: "移动审批", group: "移动工作台" },
{ path: "/mobile/wecom-approval-archive", title: "移动企微审批历史", group: "移动工作台" },
{ path: "/mobile/capture", title: "移动 PDF 发票上传", group: "移动工作台" },
{ path: "/mobile/leave", title: "移动请假", group: "移动工作台" },
{ path: "/mobile/me", title: "移动我的", group: "移动工作台" },
+18 -4
查看文件
@@ -114,6 +114,13 @@ export function MobilePortalPage() {
const canAccessLeave = hasAnyPermission(permissions, [...MOBILE_LEAVE_PERMISSIONS]);
const canAccessPurchase = hasAnyPermission(permissions, [...MOBILE_PURCHASE_PERMISSIONS]);
const canAccessReimbursements = hasAnyPermission(permissions, [...MOBILE_REIMBURSEMENT_PERMISSIONS]);
const canAccessWecomApprovalArchive = hasAnyPermission(permissions, [
"leave.view_own",
"attendance.view_own",
"leave.view_all",
"attendance.view_all",
"*",
]);
const canCreateExpense = hasAnyPermission(permissions, ["expense.create"]);
const currentMobileSource = `${location.pathname}${location.search}`;
@@ -351,11 +358,18 @@ export function MobilePortalPage() {
<span className="badge-neutral">企业微信打卡</span>
</div>
<div className="text-sm leading-6 text-zinc-600">
日常打卡在企业微信完成;补卡、外出、加班和月度解释在 OA 处理。
日常打卡、请假和补卡在企业微信完成;OA 保留已归档历史和考勤汇总。
</div>
<div className="mt-3 grid gap-2">
{canAccessWecomApprovalArchive ? (
<Button asChild className="w-full" size="sm" type="button" variant="secondary">
<Link to="/mobile/wecom-approval-archive">查看企微审批历史</Link>
</Button>
) : null}
<Button asChild className="w-full" size="sm" type="button" variant="secondary">
<Link to="/attendance">查看考勤汇总</Link>
</Button>
</div>
<Button asChild className="mt-3 w-full" size="sm" type="button" variant="secondary">
<Link to="/attendance">查看考勤汇总</Link>
</Button>
</section>
<section className="mobile-card mobile-page-stack" data-testid="mobile-notifications">
@@ -0,0 +1,344 @@
import { useMemo, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import {
fetchWecomApprovalArchive,
fetchWecomApprovalArchiveDetail,
type WecomApprovalArchiveFinding,
type WecomApprovalArchiveItem,
type WecomApprovalArchiveScope,
type WecomApprovalArchiveStatus,
type WecomApprovalArchiveType,
} from "../api/wecomApprovalArchive";
import { useAuth } from "../auth/AuthContext";
import { PageHeader } from "../components/common/PageHeader";
import { StatusBadge } from "../components/common/StatusBadge";
import { TableSkeleton } from "../components/common/TableSkeleton";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { Button } from "../components/ui/button";
import { Label } from "../components/ui/label";
import { mapApiErrorMessage } from "../api/errorMessages";
import { isApiError } from "../api/types";
const TYPE_OPTIONS: Array<{ value: "" | WecomApprovalArchiveType; label: string }> = [
{ value: "", label: "全部类型" },
{ value: "leave", label: "请假" },
{ value: "missed_punch", label: "补卡" },
];
const STATUS_OPTIONS: Array<{ value: "" | WecomApprovalArchiveStatus; label: string }> = [
{ value: "", label: "全部状态" },
{ value: "approved", label: "已通过" },
{ value: "rejected", label: "已驳回" },
{ value: "revoked", label: "已撤销" },
{ value: "terminated", label: "已终止" },
{ value: "pending", label: "处理中" },
{ value: "unknown", label: "未知" },
];
const FINDING_OPTIONS: Array<{ value: "" | WecomApprovalArchiveFinding; label: string }> = [
{ value: "", label: "全部对账" },
{ value: "open", label: "仅未关闭 finding" },
{ value: "resolved", label: "已关闭 finding" },
{ value: "any", label: "有 finding" },
];
function resolveError(error: unknown, fallback = "读取企微审批历史失败") {
if (isApiError(error)) {
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
}
return error instanceof Error ? error.message : fallback;
}
function typeLabel(value: string) {
if (value === "leave") return "请假";
if (value === "missed_punch") return "补卡";
return value || "未知";
}
function sourceLabel(value: string | null) {
if (value === "callback") return "回调";
if (value === "compensation_pull") return "补偿拉取";
return value || "-";
}
function actionLabel(value: string) {
if (value === "approve") return "同意";
if (value === "reject") return "驳回";
if (value === "revoke") return "撤销";
if (value === "terminate") return "终止";
if (value === "pending") return "状态记录";
return value || "状态记录";
}
function roleLabel(value: string) {
if (value === "applicant") return "申请人";
if (value === "approver") return "审批人";
if (value === "notifyer") return "抄送人";
return value || "-";
}
function formatTime(value: string | null) {
if (!value) return "-";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "-";
return date.toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
}
function employeeLabel(employee: WecomApprovalArchiveItem["applicant"]) {
if (!employee) return "未映射";
return employee.employeeNo ? `${employee.name}(${employee.employeeNo})` : employee.name;
}
function canRequestAll(roleCodes: string[], permissions: string[]) {
return roleCodes.includes("hr") || roleCodes.includes("admin") || permissions.includes("*");
}
function ArchiveRow({
item,
active,
onSelect,
}: {
item: WecomApprovalArchiveItem;
active: boolean;
onSelect: () => void;
}) {
return (
<>
<tr className={active ? "bg-primary/5" : ""}>
<td className="px-3 py-3 align-top">
<div className="font-medium text-zinc-900">{typeLabel(item.templateType)}</div>
<div className="mt-1 text-xs text-zinc-500">{item.spName || "OA-POC 归档记录"}</div>
<div className="mt-1 text-xs text-zinc-500">单号前缀:{item.spNoPrefix || "-"}</div>
</td>
<td className="px-3 py-3 align-top text-sm text-zinc-700">{employeeLabel(item.applicant)}</td>
<td className="px-3 py-3 align-top"><StatusBadge status={item.normalizedStatus} /></td>
<td className="px-3 py-3 align-top text-sm text-zinc-600">{formatTime(item.statusObservedAt || item.completedAt || item.appliedAt)}</td>
<td className="px-3 py-3 align-top text-sm text-zinc-600">
<div>{sourceLabel(item.lastSource)}</div>
{item.openFindingCount ? <div className="mt-1 text-xs text-amber-700">finding {item.openFindingCount} 项</div> : null}
</td>
<td className="px-3 py-3 align-top text-right">
<Button type="button" variant="secondary" onClick={onSelect}>查看历史</Button>
</td>
</tr>
</>
);
}
export function WecomApprovalArchivePage() {
const { me, permissions } = useAuth();
const [scope, setScope] = useState<WecomApprovalArchiveScope>("self");
const [type, setType] = useState<"" | WecomApprovalArchiveType>("");
const [status, setStatus] = useState<"" | WecomApprovalArchiveStatus>("");
const [finding, setFinding] = useState<"" | WecomApprovalArchiveFinding>("");
const [selectedId, setSelectedId] = useState("");
const allAllowed = canRequestAll(me?.roleCodes ?? [], permissions);
const query = useMemo(() => ({ scope, type, status, finding, pageSize: 50 }), [scope, type, status, finding]);
const listQuery = useQuery({
queryKey: ["wecom-approval-archive", query],
queryFn: () => fetchWecomApprovalArchive(query),
});
const detailQuery = useQuery({
enabled: Boolean(selectedId),
queryKey: ["wecom-approval-archive", "detail", selectedId],
queryFn: () => fetchWecomApprovalArchiveDetail(selectedId),
});
const items = listQuery.data?.items ?? [];
const detail = detailQuery.data?.item ?? null;
return (
<div className="page-shell space-y-6" data-testid="wecom-approval-archive-page">
<PageHeader
eyebrow="企微审批历史"
title="企微审批历史"
subtitle="企业微信主办,OA 自建留存;仅已归档记录。这里用于历史查看、审计和补偿对账,不提供审批操作。"
meta={
<>
<span className="rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-medium text-emerald-700">企业微信主办,OA 自建留存</span>
<span className="rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-medium text-slate-700">仅已归档记录</span>
</>
}
/>
<SectionPanel
title="历史筛选"
description="范围只覆盖已入库的请假、补卡记录;完整单号和企微 userid 仅保存在数据库内。"
>
<div className="grid gap-3 md:grid-cols-4">
<div className="space-y-1.5">
<Label htmlFor="wecom-archive-scope">查看范围</Label>
<select
id="wecom-archive-scope"
className="input-std"
value={scope}
onChange={(event) => {
setScope(event.target.value as WecomApprovalArchiveScope);
setSelectedId("");
}}
>
<option value="self">与我相关</option>
<option value="team">直属团队</option>
{allAllowed ? <option value="all">HR/管理员全量</option> : null}
</select>
</div>
<div className="space-y-1.5">
<Label htmlFor="wecom-archive-type">类型</Label>
<select id="wecom-archive-type" className="input-std" value={type} onChange={(event) => setType(event.target.value as "" | WecomApprovalArchiveType)}>
{TYPE_OPTIONS.map((option) => <option key={option.value || "all"} value={option.value}>{option.label}</option>)}
</select>
</div>
<div className="space-y-1.5">
<Label htmlFor="wecom-archive-status">状态</Label>
<select id="wecom-archive-status" className="input-std" value={status} onChange={(event) => setStatus(event.target.value as "" | WecomApprovalArchiveStatus)}>
{STATUS_OPTIONS.map((option) => <option key={option.value || "all"} value={option.value}>{option.label}</option>)}
</select>
</div>
<div className="space-y-1.5">
<Label htmlFor="wecom-archive-finding">对账</Label>
<select id="wecom-archive-finding" className="input-std" value={finding} onChange={(event) => setFinding(event.target.value as "" | WecomApprovalArchiveFinding)}>
{FINDING_OPTIONS.map((option) => <option key={option.value || "all"} value={option.value}>{option.label}</option>)}
</select>
</div>
</div>
</SectionPanel>
<SectionPanel
title="归档记录"
description={`当前返回 ${items.length} 条,服务端总数 ${listQuery.data?.total ?? 0} 条。`}
>
{listQuery.isLoading ? <TableSkeleton columns={6} rows={5} /> : null}
{listQuery.isError ? <div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">{resolveError(listQuery.error)}</div> : null}
{!listQuery.isLoading && !listQuery.isError && !items.length ? (
<div className="rounded-md border border-dashed border-slate-300 bg-slate-50 px-4 py-8 text-center text-sm text-slate-500">
暂无已归档的企微审批历史。若公网 OA DB 还没有留存数据,验收记录为 PARTIAL_ARCHIVE_NEEDS_DATA。
</div>
) : null}
{!listQuery.isLoading && !listQuery.isError && items.length ? (
<>
<div className="hidden overflow-x-auto md:block">
<table className="min-w-full divide-y divide-slate-200 text-left">
<thead className="bg-slate-50 text-xs font-semibold uppercase text-slate-500">
<tr>
<th className="px-3 py-2">类型/单号</th>
<th className="px-3 py-2">申请人</th>
<th className="px-3 py-2">状态</th>
<th className="px-3 py-2">状态时间</th>
<th className="px-3 py-2">来源/对账</th>
<th className="px-3 py-2 text-right">详情</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{items.map((item) => (
<ArchiveRow key={item.id} item={item} active={selectedId === item.id} onSelect={() => setSelectedId(item.id)} />
))}
</tbody>
</table>
</div>
<div className="divide-y divide-slate-100 md:hidden">
{items.map((item) => (
<article className="py-3" key={item.id}>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="font-medium text-zinc-900">{typeLabel(item.templateType)}</div>
<div className="mt-1 text-xs text-zinc-500">单号前缀:{item.spNoPrefix || "-"}</div>
</div>
<StatusBadge status={item.normalizedStatus} />
</div>
<div className="mt-2 text-sm text-zinc-600">{employeeLabel(item.applicant)} · {formatTime(item.statusObservedAt || item.completedAt || item.appliedAt)}</div>
<div className="mt-2 flex items-center justify-between gap-3">
<span className="text-xs text-zinc-500">{sourceLabel(item.lastSource)}{item.openFindingCount ? ` · finding ${item.openFindingCount}` : ""}</span>
<Button type="button" variant="secondary" onClick={() => setSelectedId(item.id)}>查看历史</Button>
</div>
</article>
))}
</div>
</>
) : null}
</SectionPanel>
<SectionPanel
title="历史详情"
description="详情只展示字段名称、字段类型、动作来源和脱敏人员摘要;不展示表单原文、评论、附件或完整企微标识。"
>
{!selectedId ? <div className="text-sm text-zinc-500">请选择一条归档记录查看详情。</div> : null}
{selectedId && detailQuery.isLoading ? <TableSkeleton columns={1} rows={3} /> : null}
{selectedId && detailQuery.isError ? <div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">{resolveError(detailQuery.error)}</div> : null}
{detail ? (
<div className="grid gap-4 lg:grid-cols-3">
<div className="space-y-3 lg:col-span-1">
<div>
<div className="text-xs text-zinc-500">状态</div>
<div className="mt-1"><StatusBadge status={detail.normalizedStatus} /></div>
</div>
<div>
<div className="text-xs text-zinc-500">申请人</div>
<div className="mt-1 text-sm text-zinc-800">{employeeLabel(detail.applicant)}</div>
</div>
<div>
<div className="text-xs text-zinc-500">单号前缀</div>
<div className="mt-1 text-sm text-zinc-800">{detail.spNoPrefix || "-"}</div>
</div>
{detail.singleTapApproveRisk ? <div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">企微同意可能单击即通过,OA 仅留存动作来源和历史审计。</div> : null}
</div>
<div className="space-y-4 lg:col-span-2">
<div>
<h4 className="text-sm font-semibold text-zinc-900">字段摘要</h4>
<div className="mt-2 divide-y divide-slate-100 rounded-md border border-slate-200">
{detail.fields.length ? detail.fields.map((field) => (
<div className="grid gap-1 px-3 py-2 text-sm md:grid-cols-3" key={field.id}>
<span className="font-medium text-zinc-800">{field.fieldName || field.fieldKey}</span>
<span className="text-zinc-500">{field.fieldType || "-"}</span>
<span className="text-zinc-500">{field.sensitiveLevel === "restricted" ? "受限字段" : "普通字段"}</span>
</div>
)) : <div className="px-3 py-3 text-sm text-zinc-500">暂无字段摘要。</div>}
</div>
</div>
<div>
<h4 className="text-sm font-semibold text-zinc-900">参与人与动作</h4>
<div className="mt-2 grid gap-3 md:grid-cols-2">
<div className="rounded-md border border-slate-200">
{detail.participants.length ? detail.participants.map((item) => (
<div className="border-b border-slate-100 px-3 py-2 text-sm last:border-b-0" key={item.id}>
<span className="font-medium text-zinc-800">{roleLabel(item.role)}</span>
<span className="ml-2 text-zinc-600">{employeeLabel(item.employee)}</span>
</div>
)) : <div className="px-3 py-3 text-sm text-zinc-500">暂无映射参与人。</div>}
</div>
<div className="rounded-md border border-slate-200">
{detail.actions.length ? detail.actions.map((item) => (
<div className="border-b border-slate-100 px-3 py-2 text-sm last:border-b-0" key={item.id}>
<span className="font-medium text-zinc-800">{actionLabel(item.actionType)}</span>
<span className="ml-2 text-zinc-600">{employeeLabel(item.operator)}</span>
<div className="mt-1 text-xs text-zinc-500">{formatTime(item.actionTime)} · {sourceLabel(item.source)}</div>
</div>
)) : <div className="px-3 py-3 text-sm text-zinc-500">暂无动作历史。</div>}
</div>
</div>
</div>
<div>
<h4 className="text-sm font-semibold text-zinc-900">对账 finding</h4>
<div className="mt-2 divide-y divide-slate-100 rounded-md border border-slate-200">
{detail.findings.length ? detail.findings.map((item) => (
<div className="flex flex-wrap items-center justify-between gap-2 px-3 py-2 text-sm" key={item.id}>
<span className="font-medium text-zinc-800">{item.findingType}</span>
<span className="text-zinc-500">{item.status} · {item.severity} · {formatTime(item.detectedAt)}</span>
</div>
)) : <div className="px-3 py-3 text-sm text-zinc-500">暂无对账 finding。</div>}
</div>
</div>
</div>
</div>
) : null}
</SectionPanel>
</div>
);
}
+19
查看文件
@@ -64,6 +64,7 @@ import {
ReimbursementsPage,
TravelRequestsPage,
UserManagementPage,
WecomApprovalArchivePage,
WeeklyReportsPage,
WorkspaceSetupPage,
WorkflowDefinitionsPage,
@@ -155,6 +156,15 @@ export function AppRouter() {
element={withRouteFallback(<AttendancePage />)}
/>
<Route
path="wecom-approval-archive"
element={
<RequirePermission anyOf={["leave.view_own", "attendance.view_own", "leave.view_all", "attendance.view_all", "*"]}>
{withRouteFallback(<WecomApprovalArchivePage />)}
</RequirePermission>
}
/>
<Route
path="reimbursements"
element={
@@ -311,6 +321,15 @@ export function AppRouter() {
<Route path="mobile/notifications" element={withRouteFallback(<MobileNotificationsPage />)} />
<Route
path="mobile/wecom-approval-archive"
element={
<RequirePermission anyOf={["leave.view_own", "attendance.view_own", "leave.view_all", "attendance.view_all", "*"]}>
{withRouteFallback(<WecomApprovalArchivePage />)}
</RequirePermission>
}
/>
<Route
path="mobile/capture"
element={
@@ -0,0 +1 @@
export { WecomApprovalArchivePage as default } from "../../pages/WecomApprovalArchivePage";
+1
查看文件
@@ -39,6 +39,7 @@ export const OrganizationPage = lazy(() => import("./OrganizationPage"));
export const EmployeeProfilePage = lazy(() => import("./EmployeeProfilePage"));
export const TeamPresencePage = lazy(() => import("./TeamPresencePage"));
export const WeeklyReportsPage = lazy(() => import("./WeeklyReportsPage"));
export const WecomApprovalArchivePage = lazy(() => import("./WecomApprovalArchivePage"));
export const TodosPage = lazy(() => import("./TodosPage"));
export const WorkspaceSetupPage = lazy(() => import("./WorkspaceSetupPage"));
export const SystemSettingsPage = lazy(() => import("./SystemSettingsPage"));