DG9 统一全站 UI 布局样板迁移
关联 Issue: DG9-0..DG9-12
这个提交包含在:
@@ -1,22 +1,104 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export function ListWorkspace({ className, children }: { className?: string; children: ReactNode }) {
|
||||
type WorkspaceChildrenProps = {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
type WorkspaceHeaderProps = {
|
||||
eyebrow?: string;
|
||||
title: string;
|
||||
subtitle?: ReactNode;
|
||||
meta?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function ListWorkspace({ className, children }: WorkspaceChildrenProps) {
|
||||
return <div className={cn("page-scaffold", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function FilterBar({ className, children }: { className?: string; children: ReactNode }) {
|
||||
export function WorkspaceHeader({ eyebrow, title, subtitle, meta, actions, className }: WorkspaceHeaderProps) {
|
||||
return (
|
||||
<header className={cn("flex flex-col gap-4 md:flex-row md:items-end md:justify-between", className)}>
|
||||
<div className="min-w-0 max-w-3xl">
|
||||
{eyebrow ? <div className="text-xs font-bold uppercase text-primary">{eyebrow}</div> : null}
|
||||
<h1 className="mt-1 text-2xl font-semibold text-[var(--color-text-primary)] md:text-3xl">{title}</h1>
|
||||
{subtitle ? <div className="mt-2 text-sm leading-6 text-[var(--color-text-secondary)]">{subtitle}</div> : null}
|
||||
{meta ? <div className="mt-3 flex flex-wrap gap-2">{meta}</div> : null}
|
||||
</div>
|
||||
{actions ? <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div> : null}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterBar({ className, children }: WorkspaceChildrenProps) {
|
||||
return <section className={cn("card-std p-4 md:p-5", className)}>{children}</section>;
|
||||
}
|
||||
|
||||
export function StatStrip({ className, children }: { className?: string; children: ReactNode }) {
|
||||
return <section className={cn("grid gap-4 md:grid-cols-3", className)}>{children}</section>;
|
||||
export function FilterGrid({ className, children }: WorkspaceChildrenProps) {
|
||||
return <div className={cn("grid gap-3 md:grid-cols-2 xl:grid-cols-4", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function ActionToolbar({ className, children }: { className?: string; children: ReactNode }) {
|
||||
export function StatStrip({
|
||||
className,
|
||||
columns = 3,
|
||||
children,
|
||||
}: WorkspaceChildrenProps & { columns?: 2 | 3 | 4 }) {
|
||||
const columnClass = {
|
||||
2: "md:grid-cols-2",
|
||||
3: "md:grid-cols-3",
|
||||
4: "md:grid-cols-2 xl:grid-cols-4",
|
||||
}[columns];
|
||||
return <section className={cn("grid gap-4", columnClass, className)}>{children}</section>;
|
||||
}
|
||||
|
||||
export function WorkspaceSection({
|
||||
title,
|
||||
description,
|
||||
actions,
|
||||
className,
|
||||
children,
|
||||
}: WorkspaceChildrenProps & {
|
||||
title?: string;
|
||||
description?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className={cn("card-std space-y-4 p-4 md:p-5", className)}>
|
||||
{title || description || actions ? (
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div className="min-w-0">
|
||||
{title ? <h2 className="text-base font-semibold text-[var(--color-text-primary)]">{title}</h2> : null}
|
||||
{description ? <div className="mt-1 text-sm leading-6 text-[var(--color-text-secondary)]">{description}</div> : null}
|
||||
</div>
|
||||
{actions ? <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div> : null}
|
||||
</div>
|
||||
) : null}
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function ActionToolbar({ className, children }: WorkspaceChildrenProps) {
|
||||
return <div className={cn("flex flex-wrap items-center justify-between gap-3", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function PrimaryTable({ className, children }: { className?: string; children: ReactNode }) {
|
||||
export function PrimaryTable({ className, children }: WorkspaceChildrenProps) {
|
||||
return <section className={cn("table-container p-0", className)}>{children}</section>;
|
||||
}
|
||||
|
||||
export function WorkspaceEmptyState({
|
||||
className,
|
||||
children = "暂无数据",
|
||||
}: {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("rounded-xl border border-dashed border-zinc-300 px-4 py-8 text-center text-sm text-zinc-500", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { ConfirmDialog } from "../components/common/ConfirmDialog";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { Button } from "../components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -605,12 +605,12 @@ export function ApOperationsPage() {
|
||||
};
|
||||
|
||||
if (summaryQuery.isLoading && paymentBatchesQuery.isLoading && riskFlagsQuery.isLoading && financeLedgerQuery.isLoading) {
|
||||
return <div className="card-std p-6 text-sm text-zinc-500">加载财务工作台...</div>;
|
||||
return <div className="card-std p-4 text-sm text-zinc-500 md:p-5">加载财务工作台...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
title="财务付款与对账"
|
||||
subtitle="集中处理待付款批次、异常付款、风险标记和对账异常。"
|
||||
actions={
|
||||
@@ -671,7 +671,7 @@ export function ApOperationsPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="card-std p-6">
|
||||
<div className="card-std p-4 md:p-5">
|
||||
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as OperationsTab)}>
|
||||
<TabsList className="flex w-full flex-wrap justify-start gap-1">
|
||||
<TabsTrigger value="ledger">财务台账 {financeLedgerQuery.data?.total ?? 0}</TabsTrigger>
|
||||
@@ -1630,6 +1630,6 @@ export function ApOperationsPage() {
|
||||
await executeMutation.mutateAsync(retryTarget.batchId);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type ApprovalRoleCode,
|
||||
type ApprovalRule,
|
||||
} from "../api/approvalRules";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../components/ui/dialog";
|
||||
import { Input } from "../components/ui/input";
|
||||
@@ -144,8 +144,8 @@ export function ApprovalRulesPage() {
|
||||
}, [editOpen, editRule]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
title="审批规则"
|
||||
subtitle="维护金额段与审批层级(仅影响新提交的报销单,历史流程不回写)。"
|
||||
actions={
|
||||
@@ -161,7 +161,7 @@ export function ApprovalRulesPage() {
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="card-std p-6 space-y-4">
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<h3 className="text-base font-semibold text-zinc-900">新建审批规则</h3>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||
@@ -286,7 +286,7 @@ export function ApprovalRulesPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card-std p-6 space-y-4">
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<h3 className="text-base font-semibold text-zinc-900">规则列表</h3>
|
||||
|
||||
{listQuery.isLoading ? <TableSkeleton columns={6} rows={5} /> : null}
|
||||
@@ -304,7 +304,7 @@ export function ApprovalRulesPage() {
|
||||
) : null}
|
||||
|
||||
{!listQuery.isLoading && !listQuery.isError ? (
|
||||
<div className="rounded-lg border border-zinc-200">
|
||||
<PrimaryTable>
|
||||
<Table data-testid="approval-rules-table">
|
||||
<TableHeader>
|
||||
<tr>
|
||||
@@ -371,7 +371,7 @@ export function ApprovalRulesPage() {
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</PrimaryTable>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -516,7 +516,6 @@ export function ApprovalRulesPage() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,10 +27,10 @@ import { fetchCostCenters, fetchDocumentCategories, fetchLegalEntities } from ".
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission, hasPermission } from "../auth/permissions";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { AiSuggestionCard } from "../components/ai/AiSuggestionCard";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
import { FilterBar, ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
@@ -578,8 +578,8 @@ export function AssetRegistryPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
title="资产台账"
|
||||
subtitle="维护资产分类与资产卡片,支持法体、成本中心和采购来源引用。"
|
||||
actions={
|
||||
@@ -597,7 +597,7 @@ export function AssetRegistryPage() {
|
||||
/>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[380px_minmax(0,1fr)]">
|
||||
<div className="card-std space-y-4 p-6">
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-base font-semibold text-zinc-900">资产分类</h3>
|
||||
{canManage ? (
|
||||
@@ -669,7 +669,7 @@ export function AssetRegistryPage() {
|
||||
<div className="rounded-lg border border-zinc-200 p-3 text-sm text-zinc-500">暂无资产分类</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="hidden rounded-lg border border-zinc-200 md:block">
|
||||
<PrimaryTable className="hidden md:block">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<tr>
|
||||
@@ -691,13 +691,13 @@ export function AssetRegistryPage() {
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</PrimaryTable>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="card-std space-y-4 p-6">
|
||||
<FilterBar className="space-y-4">
|
||||
<div className="grid gap-3 md:grid-cols-3 xl:grid-cols-6">
|
||||
<select className="select-std" value={statusFilter} onChange={(event) => setStatusFilter(event.target.value as AssetStatus | "") }>
|
||||
{assetStatusOptions().map((item) => (
|
||||
@@ -796,7 +796,7 @@ export function AssetRegistryPage() {
|
||||
<div className="rounded-lg border border-zinc-200 p-4 text-sm text-zinc-500">暂无资产卡片</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="hidden rounded-lg border border-zinc-200 md:block">
|
||||
<PrimaryTable className="hidden md:block">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<tr>
|
||||
@@ -830,12 +830,12 @@ export function AssetRegistryPage() {
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</PrimaryTable>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</FilterBar>
|
||||
|
||||
<div className="card-std space-y-4 p-6">
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
{!selectedAssetId && !isCreatingAsset ? (
|
||||
<div className="rounded-lg border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500">
|
||||
请先从列表选择资产卡片,或点击“新建资产卡片”。
|
||||
@@ -1128,6 +1128,6 @@ export function AssetRegistryPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import { hasPermission } from "../auth/permissions";
|
||||
import { toast } from "sonner";
|
||||
import { downloadBlob } from "../lib/download";
|
||||
import { formatInvoiceOcrErrorMessage } from "../lib/invoiceOcr";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../components/ui/dropdown-menu";
|
||||
import { InvoiceUploadCard } from "./invoice-pool/InvoiceUploadCard";
|
||||
@@ -446,8 +446,8 @@ export function InvoicePoolPage() {
|
||||
// --- Render ---
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
title="票据中心"
|
||||
subtitle="上传、识别、绑定。"
|
||||
actions={
|
||||
@@ -569,6 +569,6 @@ export function InvoicePoolPage() {
|
||||
confirmAction={confirmAction}
|
||||
setConfirmAction={setConfirmAction}
|
||||
/>
|
||||
</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,11 +20,10 @@ import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { CardSkeleton } from "../components/common/CardSkeleton";
|
||||
import { ConfirmDialog } from "../components/common/ConfirmDialog";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { Pagination } from "../components/common/Pagination";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
import { ListWorkspace, PrimaryTable } from "../components/patterns/ListWorkspace";
|
||||
import { ListWorkspace, PrimaryTable, StatStrip, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { MetricCard } from "../components/patterns/MetricCard";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { Button } from "../components/ui/button";
|
||||
@@ -780,12 +779,12 @@ export function LeaveRequestPage() {
|
||||
|
||||
return (
|
||||
<ListWorkspace>
|
||||
<PageHeader
|
||||
<WorkspaceHeader
|
||||
title="请假申请"
|
||||
subtitle="支持假种选择、半天粒度请假、额度冻结与撤回释放。"
|
||||
/>
|
||||
|
||||
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<StatStrip columns={4}>
|
||||
<MetricCard title="可用额度" value={balanceSummary.available.toFixed(1)} note="按当前启用假种汇总" />
|
||||
<MetricCard title="已冻结" value={balanceSummary.reserved.toFixed(1)} note="审批中申请会占用额度" tone={balanceSummary.reserved > 0 ? "warning" : "neutral"} />
|
||||
<MetricCard title="已使用" value={balanceSummary.used.toFixed(1)} note="通过后计入已使用" />
|
||||
@@ -795,7 +794,7 @@ export function LeaveRequestPage() {
|
||||
note="创建草稿前可先预览天数"
|
||||
tone={effectiveRequestedDaysPreview ? "warning" : "neutral"}
|
||||
/>
|
||||
</section>
|
||||
</StatStrip>
|
||||
|
||||
<div className="page-shell-grid">
|
||||
<SectionPanel title={formTitle} description={formDescription}>
|
||||
|
||||
@@ -22,9 +22,9 @@ import { hasAnyPermission, hasPermission } from "../auth/permissions";
|
||||
import { ConfirmDialog } from "../components/common/ConfirmDialog";
|
||||
import { HolidayCalendarTab } from "../components/master-data/HolidayCalendarTab";
|
||||
import { SupplierTab } from "../components/master-data/SupplierTab";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
import { FilterBar, ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
import { Label } from "../components/ui/label";
|
||||
@@ -292,16 +292,16 @@ export function MasterDataPage() {
|
||||
|
||||
if (!canView) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<PageHeader title="主数据" subtitle="你当前没有主数据权限,请联系管理员开通。" />
|
||||
<div className="card-std p-6 text-sm text-zinc-600">所需权限:masterdata.view 或 masterdata.manage</div>
|
||||
</div>
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader title="主数据" subtitle="你当前没有主数据权限,请联系管理员开通。" />
|
||||
<div className="card-std p-4 text-sm text-zinc-600 md:p-5">所需权限:masterdata.view 或 masterdata.manage</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
title="主数据"
|
||||
subtitle="集中维护法体、成本中心、单据分类、日历与供应商,为预算、采购、请假与资产扩展提供统一骨架。"
|
||||
actions={
|
||||
@@ -325,7 +325,7 @@ export function MasterDataPage() {
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="legal-entities" className="space-y-6">
|
||||
<div className="card-std p-6 space-y-4">
|
||||
<FilterBar className="space-y-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<h3 className="text-base font-semibold text-zinc-900">法体筛选</h3>
|
||||
<select
|
||||
@@ -338,10 +338,10 @@ export function MasterDataPage() {
|
||||
<option value="false">仅停用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</FilterBar>
|
||||
|
||||
{canManage ? (
|
||||
<div className="card-std p-6 space-y-4">
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<h3 className="text-base font-semibold text-zinc-900">新建法体</h3>
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<div className="space-y-1">
|
||||
@@ -377,10 +377,11 @@ export function MasterDataPage() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="card-std p-6 space-y-3">
|
||||
<div className="card-std space-y-3 p-4 md:p-5">
|
||||
<h3 className="text-base font-semibold text-zinc-900">法体列表</h3>
|
||||
{legalEntitiesQuery.isLoading ? <TableSkeleton columns={6} rows={4} /> : null}
|
||||
{!legalEntitiesQuery.isLoading ? (
|
||||
<PrimaryTable>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -431,12 +432,13 @@ export function MasterDataPage() {
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</PrimaryTable>
|
||||
) : null}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="cost-centers" className="space-y-6">
|
||||
<div className="card-std p-6 space-y-4">
|
||||
<FilterBar className="space-y-4">
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<select
|
||||
className="select-std"
|
||||
@@ -462,10 +464,10 @@ export function MasterDataPage() {
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</FilterBar>
|
||||
|
||||
{canManage ? (
|
||||
<div className="card-std p-6 space-y-4">
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<h3 className="text-base font-semibold text-zinc-900">新建成本中心</h3>
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
<div className="space-y-1 md:col-span-2">
|
||||
@@ -532,10 +534,11 @@ export function MasterDataPage() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="card-std p-6 space-y-3">
|
||||
<div className="card-std space-y-3 p-4 md:p-5">
|
||||
<h3 className="text-base font-semibold text-zinc-900">成本中心列表</h3>
|
||||
{costCentersQuery.isLoading ? <TableSkeleton columns={7} rows={4} /> : null}
|
||||
{!costCentersQuery.isLoading ? (
|
||||
<PrimaryTable>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -588,12 +591,13 @@ export function MasterDataPage() {
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</PrimaryTable>
|
||||
) : null}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="document-categories" className="space-y-6">
|
||||
<div className="card-std p-6 space-y-4">
|
||||
<FilterBar className="space-y-4">
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<select
|
||||
className="select-std"
|
||||
@@ -619,10 +623,10 @@ export function MasterDataPage() {
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</FilterBar>
|
||||
|
||||
{canManage ? (
|
||||
<div className="card-std p-6 space-y-4">
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<h3 className="text-base font-semibold text-zinc-900">新建单据分类</h3>
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
<div className="space-y-1">
|
||||
@@ -693,7 +697,7 @@ export function MasterDataPage() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="card-std p-6 space-y-3">
|
||||
<div className="card-std space-y-3 p-4 md:p-5">
|
||||
<h3 className="text-base font-semibold text-zinc-900">单据分类列表</h3>
|
||||
{documentCategoriesQuery.isLoading ? <TableSkeleton columns={7} rows={4} /> : null}
|
||||
{!documentCategoriesQuery.isLoading ? (
|
||||
@@ -790,6 +794,6 @@ export function MasterDataPage() {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -999,11 +999,11 @@ export function MobileApprovalsPage() {
|
||||
) : null}
|
||||
|
||||
{!isPushDetail && !canAccessRemoteApprovals ? (
|
||||
<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>
|
||||
<p className="mt-1 text-sm text-zinc-500">报销、请假、采购进度</p>
|
||||
<section className="mobile-card mobile-page-stack" data-testid="mobile-approvals-progress">
|
||||
<div className="mobile-section-header">
|
||||
<div className="min-w-0">
|
||||
<h3 className="mobile-section-title">我发起的进度</h3>
|
||||
<p className="mobile-list-card__subtitle mt-1">报销、请假、采购进度</p>
|
||||
</div>
|
||||
<span className="badge-neutral">{employeeProgressCards.length} 条</span>
|
||||
</div>
|
||||
@@ -1028,7 +1028,7 @@ export function MobileApprovalsPage() {
|
||||
<div className="mobile-empty-state">暂无申请进度</div>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="mobile-page-stack">
|
||||
{employeeProgressCards.map((item) => (
|
||||
<Link
|
||||
className="mobile-list-card block"
|
||||
@@ -1037,12 +1037,14 @@ export function MobileApprovalsPage() {
|
||||
to={item.to}
|
||||
state={{ from: currentMobileSource }}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="mobile-list-card__header">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-semibold text-zinc-900">{item.title}</div>
|
||||
{item.subtitle ? <div className="mt-1 truncate text-xs text-zinc-500">{item.subtitle}</div> : null}
|
||||
<div className="mobile-list-card__title">{item.title}</div>
|
||||
{item.subtitle ? <div className="mobile-list-card__subtitle mt-1">{item.subtitle}</div> : null}
|
||||
</div>
|
||||
<StatusBadge status={item.status} />
|
||||
<span className="mobile-status-slot">
|
||||
<StatusBadge status={item.status} />
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
@@ -1051,13 +1053,13 @@ export function MobileApprovalsPage() {
|
||||
) : null}
|
||||
|
||||
{!isPushDetail && canAccessRemoteApprovals ? (
|
||||
<section className="mobile-card space-y-4" data-testid="mobile-approvals-list">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-zinc-900">{listTitle}</h3>
|
||||
<p className="mt-1 text-sm text-zinc-500">{filteredTaskCards.length} 条</p>
|
||||
<section className="mobile-card mobile-page-stack" data-testid="mobile-approvals-list">
|
||||
<div className="mobile-section-header">
|
||||
<div className="min-w-0">
|
||||
<h3 className="mobile-section-title">{listTitle}</h3>
|
||||
<p className="mobile-list-card__subtitle mt-1">{filteredTaskCards.length} 条</p>
|
||||
</div>
|
||||
<span className={`text-sm ${isOnline ? "text-emerald-600" : "text-orange-600"}`}>{isOnline ? "在线" : "离线"}</span>
|
||||
<span className={`mobile-section-meta ${isOnline ? "text-emerald-600" : "text-orange-600"}`}>{isOnline ? "在线" : "离线"}</span>
|
||||
</div>
|
||||
|
||||
<Tabs value={taskTab} onValueChange={handleTaskTabChange}>
|
||||
@@ -1195,7 +1197,7 @@ export function MobileApprovalsPage() {
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="mobile-page-stack">
|
||||
{filteredTaskCards.map((task) => {
|
||||
const isSelected = selectedTaskId === task.id;
|
||||
const isBatchSelected = selectedBatchTaskIds.includes(task.id);
|
||||
@@ -1204,7 +1206,7 @@ export function MobileApprovalsPage() {
|
||||
return (
|
||||
<div
|
||||
key={task.id}
|
||||
className={`mobile-list-card flex gap-3 ${
|
||||
className={`mobile-list-card grid grid-cols-[auto_minmax(0,1fr)] gap-3 ${
|
||||
isBatchSelected ? "border-primary bg-primary-50" : "border-zinc-200 bg-white"
|
||||
}`}
|
||||
>
|
||||
@@ -1221,12 +1223,14 @@ export function MobileApprovalsPage() {
|
||||
data-testid={`mobile-approvals-task-${task.id}`}
|
||||
onClick={() => toggleBatchTask(task.id)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="font-medium text-zinc-900">{task.title}</div>
|
||||
<StatusBadge status={task.status} label={workflowStatusLabel(task.status)} />
|
||||
<div className="mobile-list-card__header">
|
||||
<div className="mobile-list-card__title">{task.title}</div>
|
||||
<span className="mobile-status-slot">
|
||||
<StatusBadge status={task.status} label={workflowStatusLabel(task.status)} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">{task.subtitle}</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">
|
||||
<div className="mobile-list-card__subtitle mt-1">{task.subtitle}</div>
|
||||
<div className="mt-1 min-w-0 truncate text-xs text-zinc-500">
|
||||
{joinBusinessMeta([task.applicantName ? `申请人:${task.applicantName}` : null, task.metric ? `金额/天数:${task.metric}` : null]) || "暂无摘要"}
|
||||
</div>
|
||||
{task.isCached ? <div className="mt-1 text-xs text-zinc-500">来源:离线缓存</div> : null}
|
||||
@@ -1245,12 +1249,14 @@ export function MobileApprovalsPage() {
|
||||
onClick={() => handleTaskSelection(task.id)}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="font-medium text-zinc-900">{task.title}</div>
|
||||
<StatusBadge status={task.status} label={workflowStatusLabel(task.status)} />
|
||||
<div className="mobile-list-card__header">
|
||||
<div className="mobile-list-card__title">{task.title}</div>
|
||||
<span className="mobile-status-slot">
|
||||
<StatusBadge status={task.status} label={workflowStatusLabel(task.status)} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">{task.subtitle}</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">
|
||||
<div className="mobile-list-card__subtitle mt-1">{task.subtitle}</div>
|
||||
<div className="mt-1 min-w-0 truncate text-xs text-zinc-500">
|
||||
{joinBusinessMeta([task.applicantName ? `申请人:${task.applicantName}` : null, task.metric ? `金额/天数:${task.metric}` : null]) || "暂无摘要"}
|
||||
</div>
|
||||
{task.isCached ? <div className="mt-1 text-xs text-zinc-500">来源:离线缓存</div> : null}
|
||||
@@ -1262,8 +1268,8 @@ export function MobileApprovalsPage() {
|
||||
) : null}
|
||||
|
||||
{isPushDetail ? (
|
||||
<section className="mobile-card scroll-mt-4 space-y-3" data-testid="mobile-approvals-detail">
|
||||
<h3 className="text-base font-semibold text-zinc-900">处理</h3>
|
||||
<section className="mobile-card mobile-page-stack scroll-mt-4" data-testid="mobile-approvals-detail">
|
||||
<h3 className="mobile-section-title">处理</h3>
|
||||
|
||||
{!selectedTaskId ? <p className="text-sm text-zinc-500">请先选择任务。</p> : null}
|
||||
|
||||
@@ -1286,12 +1292,14 @@ export function MobileApprovalsPage() {
|
||||
{selectedTask ? (
|
||||
<>
|
||||
<div className="mobile-list-card text-sm">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="mobile-list-card__header">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-semibold text-zinc-900">{selectedDetailTitle}</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">{joinBusinessMeta([`节点:${selectedTask.task.stepName}`, `创建:${formatDateTime(selectedTask.task.createdAt)}`])}</div>
|
||||
<div className="mobile-list-card__title">{selectedDetailTitle}</div>
|
||||
<div className="mobile-list-card__subtitle mt-1">{joinBusinessMeta([`节点:${selectedTask.task.stepName}`, `创建:${formatDateTime(selectedTask.task.createdAt)}`])}</div>
|
||||
</div>
|
||||
<StatusBadge status={selectedTask.task.status} label={workflowStatusLabel(selectedTask.task.status)} />
|
||||
<span className="mobile-status-slot">
|
||||
<StatusBadge status={selectedTask.task.status} label={workflowStatusLabel(selectedTask.task.status)} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="mobile-field-list mt-3">
|
||||
<div className="mobile-field-row">
|
||||
@@ -1343,10 +1351,10 @@ export function MobileApprovalsPage() {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2" data-testid="mobile-approvals-history">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h4 className="text-sm font-semibold text-zinc-900">处理历史</h4>
|
||||
<span className="text-xs text-zinc-500">{selectedTask.history.length} 条</span>
|
||||
<div className="mobile-page-stack" data-testid="mobile-approvals-history">
|
||||
<div className="mobile-section-header">
|
||||
<h4 className="mobile-section-title">处理历史</h4>
|
||||
<span className="mobile-section-meta">{selectedTask.history.length} 条</span>
|
||||
</div>
|
||||
{selectedTask.history.length ? (
|
||||
selectedTask.history.map((item) => (
|
||||
@@ -1381,7 +1389,7 @@ export function MobileApprovalsPage() {
|
||||
<p className="text-xs text-zinc-500">离线仅保存通过 / 驳回草稿,联网同步成功后才会正式提交。</p>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-2 min-[390px]:grid-cols-3">
|
||||
<div className="mobile-sticky-action-grid">
|
||||
<Button
|
||||
className="min-h-12 w-full"
|
||||
data-testid="mobile-approvals-approve"
|
||||
@@ -1470,10 +1478,10 @@ export function MobileApprovalsPage() {
|
||||
) : null}
|
||||
|
||||
{!isPushDetail && shouldShowDraftQueue ? (
|
||||
<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>
|
||||
<span className="text-sm text-zinc-500">{currentQueueItems.length ? `待同步 ${currentQueueItems.length}` : "无草稿"}</span>
|
||||
<section className="mobile-card mobile-page-stack" data-testid="mobile-approvals-queue">
|
||||
<div className="mobile-section-header">
|
||||
<h3 className="mobile-section-title">离线草稿</h3>
|
||||
<span className="mobile-section-meta">{currentQueueItems.length ? `待同步 ${currentQueueItems.length}` : "无草稿"}</span>
|
||||
</div>
|
||||
|
||||
{currentQueueItems.length ? (
|
||||
@@ -1495,13 +1503,27 @@ export function MobileApprovalsPage() {
|
||||
|
||||
{currentQueueItems.map((item) => (
|
||||
<div key={item.id} className="mobile-list-card text-sm">
|
||||
<div className="font-medium text-zinc-900">
|
||||
<div className="mobile-list-card__title">
|
||||
{publicReference(item.taskSnapshot.reference) ?? entityLabel(item.taskSnapshot.entityType)} · {item.action === "approve" ? "通过" : "驳回"}
|
||||
</div>
|
||||
<div className="text-zinc-500">业务:{entityLabel(item.taskSnapshot.entityType)} · 步骤:{item.taskSnapshot.stepName ?? "-"}</div>
|
||||
<div className="text-zinc-500">状态:{queueStatusLabel(item.status)} · 更新时间:{formatDateTime(item.updatedAt)}</div>
|
||||
<div className="text-zinc-500">意见:{item.comment ?? "-"}</div>
|
||||
<div className="text-zinc-500">最近错误:{item.lastError ?? "-"}</div>
|
||||
<div className="mobile-field-list mt-2">
|
||||
<div className="mobile-field-row">
|
||||
<div className="mobile-field-label">业务</div>
|
||||
<div className="mobile-field-value">{entityLabel(item.taskSnapshot.entityType)} · 步骤:{item.taskSnapshot.stepName ?? "-"}</div>
|
||||
</div>
|
||||
<div className="mobile-field-row">
|
||||
<div className="mobile-field-label">状态</div>
|
||||
<div className="mobile-field-value">{queueStatusLabel(item.status)} · 更新时间:{formatDateTime(item.updatedAt)}</div>
|
||||
</div>
|
||||
<div className="mobile-field-row">
|
||||
<div className="mobile-field-label">意见</div>
|
||||
<div className="mobile-field-value">{item.comment ?? "-"}</div>
|
||||
</div>
|
||||
<div className="mobile-field-row">
|
||||
<div className="mobile-field-label">最近错误</div>
|
||||
<div className="mobile-field-value">{item.lastError ?? "-"}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 grid gap-2 sm:flex sm:flex-wrap">
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -119,25 +119,25 @@ export function MobileNotificationsPage() {
|
||||
|
||||
return (
|
||||
<PageScaffold className="pb-4">
|
||||
<div className="space-y-3" data-testid="mobile-notifications-page">
|
||||
<PageHeader
|
||||
title="通知"
|
||||
meta={<span className="badge-neutral">未读 {listQuery.data?.unreadCount ?? 0}</span>}
|
||||
actions={
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
data-testid="mobile-notifications-mark-all-read"
|
||||
disabled={(listQuery.data?.unreadCount ?? 0) === 0 || markAllReadMutation.isPending}
|
||||
onClick={() => markAllReadMutation.mutate()}
|
||||
>
|
||||
全部已读
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div className="mobile-page-stack" data-testid="mobile-notifications-page">
|
||||
<PageHeader
|
||||
title="通知"
|
||||
meta={<span className="badge-neutral">未读 {listQuery.data?.unreadCount ?? 0}</span>}
|
||||
actions={
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
data-testid="mobile-notifications-mark-all-read"
|
||||
disabled={(listQuery.data?.unreadCount ?? 0) === 0 || markAllReadMutation.isPending}
|
||||
onClick={() => markAllReadMutation.mutate()}
|
||||
>
|
||||
全部已读
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="mobile-card space-y-4" data-testid="mobile-notifications-list">
|
||||
<div className="space-y-3">
|
||||
<section className="mobile-card mobile-page-stack" data-testid="mobile-notifications-list">
|
||||
<div className="mobile-page-stack">
|
||||
<Tabs
|
||||
data-testid="mobile-notifications-filter-tabs"
|
||||
value={unreadOnly ? "unread" : "all"}
|
||||
@@ -158,7 +158,7 @@ export function MobileNotificationsPage() {
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm text-zinc-600">
|
||||
<label className="mobile-field-row text-sm text-zinc-600">
|
||||
<span className="shrink-0">类型</span>
|
||||
<select
|
||||
data-testid="mobile-notifications-type-filter"
|
||||
@@ -194,32 +194,42 @@ export function MobileNotificationsPage() {
|
||||
<div className="mobile-empty-state">暂无通知</div>
|
||||
) : null}
|
||||
|
||||
{notificationItems.map((item) => {
|
||||
const targetLink = resolveMobileNotificationTargetLink(item);
|
||||
return (
|
||||
<button
|
||||
data-testid="mobile-notification-card"
|
||||
key={item.id}
|
||||
className="mobile-list-card w-full text-left"
|
||||
type="button"
|
||||
onClick={() => void openNotificationTarget(item, targetLink)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-medium text-primary">{notificationTypeLabel(item.type)}</div>
|
||||
<div className="text-sm font-semibold text-zinc-900">{item.title}</div>
|
||||
{item.body ? <div className="mt-1 text-xs leading-5 text-zinc-500">{item.body}</div> : null}
|
||||
<div className="mobile-page-stack">
|
||||
{notificationItems.map((item) => {
|
||||
const targetLink = resolveMobileNotificationTargetLink(item);
|
||||
return (
|
||||
<button
|
||||
data-testid="mobile-notification-card"
|
||||
key={item.id}
|
||||
className="mobile-list-card w-full text-left"
|
||||
type="button"
|
||||
onClick={() => void openNotificationTarget(item, targetLink)}
|
||||
>
|
||||
<div className="mobile-list-card__header">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-medium text-primary">{notificationTypeLabel(item.type)}</div>
|
||||
<div className="mobile-list-card__title mt-0.5">{item.title}</div>
|
||||
{item.body ? <div className="mt-1 line-clamp-2 text-xs leading-5 text-zinc-500">{item.body}</div> : null}
|
||||
</div>
|
||||
<span className={item.readAt ? "badge-neutral mobile-status-slot" : "badge-warning mobile-status-slot"}>
|
||||
{item.readAt ? "已读" : "未读"}
|
||||
</span>
|
||||
</div>
|
||||
<span className={item.readAt ? "badge-neutral shrink-0" : "badge-warning shrink-0"}>
|
||||
{item.readAt ? "已读" : "未读"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-xs text-zinc-500">{formatTime(item.createdAt)}</div>
|
||||
<div className="mt-3 text-xs text-primary">{targetLink ? "直达详情" : "暂无直达详情"}</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="mobile-field-list mt-3">
|
||||
<div className="mobile-field-row">
|
||||
<div className="mobile-field-label">时间</div>
|
||||
<div className="mobile-field-value">{formatTime(item.createdAt)}</div>
|
||||
</div>
|
||||
<div className="mobile-field-row">
|
||||
<div className="mobile-field-label">去向</div>
|
||||
<div className="mobile-field-value text-primary">{targetLink ? "直达详情" : "暂无直达详情"}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
page={page}
|
||||
|
||||
@@ -325,15 +325,15 @@ export function MobilePortalPage() {
|
||||
].filter((item): item is MobileAction => Boolean(item)).slice(0, 4);
|
||||
|
||||
return (
|
||||
<PageScaffold className="space-y-4">
|
||||
<PageScaffold className="mobile-page-stack">
|
||||
<PageHeader title="今天" />
|
||||
|
||||
{canAccessApprovals && pendingApprovalCount > 0 ? (
|
||||
<section className="mobile-card" data-testid="mobile-pending-summary">
|
||||
<div className="mb-3 flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-base font-semibold text-zinc-900">待处理</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">待我审批</div>
|
||||
<section className="mobile-card mobile-page-stack" data-testid="mobile-pending-summary">
|
||||
<div className="mobile-section-header">
|
||||
<div className="min-w-0">
|
||||
<div className="mobile-section-title">待处理</div>
|
||||
<div className="mobile-list-card__subtitle mt-1">待我审批</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-3xl font-semibold leading-none text-zinc-900" data-testid="mobile-pending-summary-count">
|
||||
@@ -345,7 +345,7 @@ export function MobilePortalPage() {
|
||||
</div>
|
||||
</div>
|
||||
{urgentPendingTask ? (
|
||||
<div className="space-y-2">
|
||||
<div className="mobile-page-stack">
|
||||
{(() => {
|
||||
const task = urgentPendingTask;
|
||||
const entityType = task.workflowInstance?.entityType;
|
||||
@@ -355,16 +355,16 @@ export function MobilePortalPage() {
|
||||
key={task.id}
|
||||
to={`/mobile/approvals/${encodeURIComponent(task.id)}`}
|
||||
state={{ from: currentMobileSource }}
|
||||
className="block rounded-lg border border-zinc-200 px-3 py-2 text-sm"
|
||||
className="mobile-list-card block text-sm"
|
||||
data-testid="mobile-pending-summary-primary"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0 truncate font-medium text-zinc-900">
|
||||
{reference ? `${businessEntityLabel(entityType, { noun: true })} ${reference}` : businessEntityLabel(entityType, { noun: true })}
|
||||
<div className="mobile-list-card__header">
|
||||
<div className="mobile-list-card__title">
|
||||
{reference ? `${businessEntityLabel(entityType, { noun: true })} ${reference}` : businessEntityLabel(entityType, { noun: true })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 truncate text-xs text-zinc-500">
|
||||
{joinBusinessMeta([task.businessSummary?.applicantName ? `申请人:${task.businessSummary.applicantName}` : null, task.businessSummary?.amount ?? task.businessSummary?.duration, `节点:${task.stepName}`])}
|
||||
<div className="mobile-list-card__subtitle mt-1">
|
||||
{joinBusinessMeta([task.businessSummary?.applicantName ? `申请人:${task.businessSummary.applicantName}` : null, task.businessSummary?.amount ?? task.businessSummary?.duration, `节点:${task.stepName}`])}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
@@ -378,9 +378,9 @@ export function MobilePortalPage() {
|
||||
) : null}
|
||||
|
||||
{canUseFinance ? (
|
||||
<section className="mobile-card" data-testid="mobile-finance-summary">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="text-base font-semibold text-zinc-900">财务</div>
|
||||
<section className="mobile-card mobile-page-stack" data-testid="mobile-finance-summary">
|
||||
<div className="mobile-section-header">
|
||||
<div className="mobile-section-title">财务</div>
|
||||
<span className="badge-neutral">复核与付款</span>
|
||||
</div>
|
||||
{portalHomeQuery.isLoading ? <p className="text-sm text-zinc-500">读取中...</p> : null}
|
||||
@@ -408,9 +408,9 @@ export function MobilePortalPage() {
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="mobile-card" data-testid="mobile-today-focus">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="text-base font-semibold text-zinc-900">常用</div>
|
||||
<section className="mobile-card mobile-page-stack" data-testid="mobile-today-focus">
|
||||
<div className="mobile-section-header">
|
||||
<div className="mobile-section-title">常用</div>
|
||||
</div>
|
||||
<div className="mobile-icon-grid" data-testid="mobile-action-grid">
|
||||
{actions.map((action) => (
|
||||
@@ -420,9 +420,9 @@ export function MobilePortalPage() {
|
||||
</section>
|
||||
|
||||
{!attendanceFeatureDisabled ? (
|
||||
<section className="mobile-card" data-testid="mobile-attendance-card">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="text-base font-semibold text-zinc-900">打卡</div>
|
||||
<section className="mobile-card mobile-page-stack" data-testid="mobile-attendance-card">
|
||||
<div className="mobile-section-header">
|
||||
<div className="mobile-section-title">打卡</div>
|
||||
<span className="badge-neutral">{attendanceToday?.workDate ?? "今日"}</span>
|
||||
</div>
|
||||
|
||||
@@ -464,9 +464,9 @@ export function MobilePortalPage() {
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="mobile-card" data-testid="mobile-notifications">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="text-base font-semibold text-zinc-900">通知</div>
|
||||
<section className="mobile-card mobile-page-stack" data-testid="mobile-notifications">
|
||||
<div className="mobile-section-header">
|
||||
<div className="mobile-section-title">通知</div>
|
||||
<Button asChild size="sm" type="button" variant="secondary">
|
||||
<Link to="/mobile/notifications">全部</Link>
|
||||
</Button>
|
||||
@@ -482,23 +482,23 @@ export function MobilePortalPage() {
|
||||
<p className="text-sm text-zinc-500">暂无通知。</p>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="mobile-page-stack">
|
||||
{homeNotifications.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
className="block w-full rounded-lg border border-zinc-200 px-3 py-2 text-left text-sm"
|
||||
className="mobile-list-card w-full text-left text-sm"
|
||||
onClick={() => void openHomeNotification(item)}
|
||||
data-testid="mobile-home-notification-card"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="mobile-list-card__header">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs text-zinc-500">{notificationTypeLabel(item.type)}</div>
|
||||
<div className="mt-0.5 truncate font-medium text-zinc-900">{item.title}</div>
|
||||
<div className="text-xs font-medium text-primary">{notificationTypeLabel(item.type)}</div>
|
||||
<div className="mobile-list-card__title mt-0.5">{item.title}</div>
|
||||
</div>
|
||||
{!item.readAt ? <span className="badge-warning shrink-0">新</span> : null}
|
||||
{!item.readAt ? <span className="badge-warning mobile-status-slot">新</span> : null}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">{formatDateTime(item.createdAt)}</div>
|
||||
<div className="mobile-list-card__subtitle mt-1">{formatDateTime(item.createdAt)}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1173,7 +1173,7 @@ export function MobileReimbursementDetailPage() {
|
||||
className="mobile-sticky-action-surface fixed inset-x-0 bottom-0 z-50 border-t border-zinc-200 bg-white/95 shadow-[0_-10px_24px_rgba(15,23,42,0.10)] backdrop-blur"
|
||||
data-testid="mobile-reimbursement-sticky-actions"
|
||||
>
|
||||
<div className={cn(canOpenInvoiceDialog && showSubmitAction ? "mobile-two-action-grid" : "grid grid-cols-1 gap-2")}>
|
||||
<div className="mobile-sticky-action-grid">
|
||||
{canOpenInvoiceDialog ? (
|
||||
<Button
|
||||
className="min-h-11 min-w-0 px-2 text-sm whitespace-nowrap"
|
||||
|
||||
@@ -16,7 +16,7 @@ import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
import { Label } from "../components/ui/label";
|
||||
@@ -199,13 +199,13 @@ export function PaymentBatchesPage() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader title="支付批次" subtitle="财务批量付款、复核与执行;高风险批次达到 5 万元后需完成二次复核。" />
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader title="支付批次" subtitle="财务批量付款、复核与执行;高风险批次达到 5 万元后需完成二次复核。" />
|
||||
|
||||
{error ? <div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{error}</div> : null}
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[360px,1fr]">
|
||||
<div className="card-std space-y-4 p-6">
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<h3 className="text-base font-semibold text-zinc-900">新建批次</h3>
|
||||
{!canManageBatch ? (
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-600">
|
||||
@@ -281,7 +281,7 @@ export function PaymentBatchesPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="card-std p-6">
|
||||
<div className="card-std p-4 md:p-5">
|
||||
<h3 className="mb-3 text-base font-semibold text-zinc-900">批次列表</h3>
|
||||
<div className="space-y-2">
|
||||
{(listQuery.data?.items ?? []).map((item) => (
|
||||
@@ -313,7 +313,7 @@ export function PaymentBatchesPage() {
|
||||
|
||||
{selectedBatch ? (
|
||||
<div className="space-y-6">
|
||||
<div className="card-std space-y-3 p-6">
|
||||
<div className="card-std space-y-3 p-4 md:p-5">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">{selectedBatch.batchNo}</div>
|
||||
@@ -372,7 +372,7 @@ export function PaymentBatchesPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card-std space-y-4 p-6">
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<h3 className="text-base font-semibold text-zinc-900">批次项</h3>
|
||||
<div className="space-y-4">
|
||||
{selectedBatch.items.map((item) => (
|
||||
@@ -417,6 +417,6 @@ export function PaymentBatchesPage() {
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,9 +12,8 @@ import {
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission } from "../auth/permissions";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { FilterBar, ListWorkspace } from "../components/patterns/ListWorkspace";
|
||||
import { FilterBar, ListWorkspace, StatStrip, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { MetricCard } from "../components/patterns/MetricCard";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { Button } from "../components/ui/button";
|
||||
@@ -161,7 +160,7 @@ export function ProjectsPage() {
|
||||
|
||||
return (
|
||||
<ListWorkspace>
|
||||
<PageHeader
|
||||
<WorkspaceHeader
|
||||
title="项目"
|
||||
subtitle="轻项目主档、成员、里程碑、任务与风险统一在 OA 内跟进。"
|
||||
actions={
|
||||
@@ -173,12 +172,12 @@ export function ProjectsPage() {
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<StatStrip columns={4}>
|
||||
<MetricCard title="当前项目" value={projectStats.total} note={mine ? "已切换到我的项目" : "当前筛选结果"} />
|
||||
<MetricCard title="进行中" value={projectStats.active} note="优先跟进活跃项目" />
|
||||
<MetricCard title="逾期里程碑" value={projectStats.overdueMilestones} note="延期事项待处理" tone={projectStats.overdueMilestones > 0 ? "warning" : "neutral"} />
|
||||
<MetricCard title="未关闭风险" value={projectStats.openRisks} note="问题留痕建议绑定责任人" tone={projectStats.openRisks > 0 ? "danger" : "neutral"} />
|
||||
</section>
|
||||
</StatStrip>
|
||||
|
||||
<div className="page-shell-grid">
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -36,7 +36,6 @@ import { fetchProjects } from "../api/projects";
|
||||
import { remindWorkflowTask } from "../api/workflow";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
|
||||
import { AiSuggestionCard } from "../components/ai/AiSuggestionCard";
|
||||
import { Pagination } from "../components/common/Pagination";
|
||||
@@ -46,6 +45,7 @@ import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
||||
import { Textarea } from "../components/ui/textarea";
|
||||
import { FilterBar, ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { buildPrecheckDialogMessage } from "../lib/precheck";
|
||||
import {
|
||||
@@ -720,6 +720,9 @@ export function PurchaseRequestsPage() {
|
||||
|
||||
const detail = detailQuery.data;
|
||||
const archiveOrder = archiveOrderQuery.data?.archiveOrder ?? detail?.archiveOrder ?? null;
|
||||
const detailAttachments = detail?.attachments ?? [];
|
||||
const detailApprovalHistory = detail?.approvalHistory ?? [];
|
||||
const detailRiskFlags = detail?.riskFlags ?? [];
|
||||
|
||||
const handleSubmitAction = async () => {
|
||||
if (!selectedId) {
|
||||
@@ -769,8 +772,8 @@ export function PurchaseRequestsPage() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
title="采购申请"
|
||||
subtitle="草稿、附件、审批。"
|
||||
actions={
|
||||
@@ -793,7 +796,7 @@ export function PurchaseRequestsPage() {
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="card-std space-y-4 p-6">
|
||||
<FilterBar className="space-y-4">
|
||||
<div className="grid gap-3 md:grid-cols-5">
|
||||
<select
|
||||
className="select-std"
|
||||
@@ -887,10 +890,10 @@ export function PurchaseRequestsPage() {
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</FilterBar>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[420px_minmax(0,1fr)]">
|
||||
<div className="card-std space-y-4 p-6">
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-base font-semibold text-zinc-900">采购单列表</h3>
|
||||
<span className="text-sm text-zinc-500">共 {listQuery.data?.total ?? 0} 条</span>
|
||||
@@ -952,7 +955,7 @@ export function PurchaseRequestsPage() {
|
||||
<div className="rounded-lg border border-zinc-200 p-4 text-sm text-zinc-500">暂无采购申请</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="hidden rounded-lg border border-zinc-200 md:block">
|
||||
<PrimaryTable className="hidden md:block">
|
||||
<Table data-testid="purchase-requests-table">
|
||||
<TableHeader>
|
||||
<tr>
|
||||
@@ -1002,7 +1005,7 @@ export function PurchaseRequestsPage() {
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</PrimaryTable>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -1014,7 +1017,7 @@ export function PurchaseRequestsPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="card-std space-y-5 p-6">
|
||||
<div className="card-std space-y-5 p-4 md:p-5">
|
||||
{!selectedId ? (
|
||||
<div className="rounded-lg border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500">
|
||||
请先在左侧选择采购单,或新建一张采购草稿。
|
||||
@@ -1096,7 +1099,7 @@ export function PurchaseRequestsPage() {
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3 text-sm">
|
||||
<div className="text-zinc-500">附件</div>
|
||||
<div className="mt-2 font-medium text-zinc-900">{detail.attachments.length} 个</div>
|
||||
<div className="mt-2 font-medium text-zinc-900">{detailAttachments.length} 个</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1116,12 +1119,12 @@ export function PurchaseRequestsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RiskFlagsCard items={detail.riskFlags} title="待处理风险项" />
|
||||
<RiskFlagsCard items={detailRiskFlags} title="待处理风险项" />
|
||||
|
||||
<SectionPanel title="审批记录" description="展示最近审批动作、退回或驳回说明,便于申请人按意见修改。">
|
||||
{detail.approvalHistory.length ? (
|
||||
{detailApprovalHistory.length ? (
|
||||
<div className="space-y-2" data-testid="purchase-approval-history">
|
||||
{detail.approvalHistory.map((item) => (
|
||||
{detailApprovalHistory.map((item) => (
|
||||
<div key={item.id} className="rounded-lg border border-zinc-200 px-3 py-2 text-sm">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<span className="font-medium text-zinc-900">
|
||||
@@ -1515,8 +1518,8 @@ export function PurchaseRequestsPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3 md:hidden">
|
||||
{detail.attachments.length ? (
|
||||
detail.attachments.map((attachment) => (
|
||||
{detailAttachments.length ? (
|
||||
detailAttachments.map((attachment) => (
|
||||
<div key={attachment.id} className="rounded-lg border border-zinc-200 bg-white p-4">
|
||||
<div className="font-medium text-zinc-900">{attachment.originalName}</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">{attachment.mimeType} / {attachment.fileSize} bytes</div>
|
||||
@@ -1563,8 +1566,8 @@ export function PurchaseRequestsPage() {
|
||||
</tr>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{detail.attachments.length ? (
|
||||
detail.attachments.map((attachment) => (
|
||||
{detailAttachments.length ? (
|
||||
detailAttachments.map((attachment) => (
|
||||
<TableRow key={attachment.id}>
|
||||
<TableCell>
|
||||
<div className="font-medium text-zinc-900">{attachment.originalName}</div>
|
||||
@@ -1754,6 +1757,6 @@ export function PurchaseRequestsPage() {
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
withdrawReimbursement,
|
||||
linkReimbursementProject,
|
||||
type ReimbursementItem,
|
||||
type ReimbursementStatus,
|
||||
} from "../api/reimbursements";
|
||||
import { remindWorkflowTask } from "../api/workflow";
|
||||
import {
|
||||
@@ -51,6 +50,8 @@ import { Input } from "../components/ui/input";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
||||
import { DetailSideMeta } from "../components/patterns/DetailSideMeta";
|
||||
import { EntityOverviewHero } from "../components/patterns/EntityOverviewHero";
|
||||
import { StatStrip } from "../components/patterns/ListWorkspace";
|
||||
import { MetricCard } from "../components/patterns/MetricCard";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import {
|
||||
@@ -73,15 +74,6 @@ import { buildPrecheckDialogMessage } from "../lib/precheck";
|
||||
type ItemEditorState = { mode: "create" } | { mode: "edit"; itemId: string } | null;
|
||||
type SelectedInvoiceMap = Record<string, string>;
|
||||
|
||||
const REIMBURSEMENT_STATUS_LABEL: Record<ReimbursementStatus, string> = {
|
||||
draft: "草稿",
|
||||
reviewing: "审批中",
|
||||
approved: "已通过",
|
||||
paid: "已支付",
|
||||
rejected: "已驳回",
|
||||
cancelled: "已取消",
|
||||
};
|
||||
|
||||
function formatDateTime(value: string | null) {
|
||||
if (!value) return "-";
|
||||
return value.slice(0, 19).replace("T", " ");
|
||||
@@ -99,10 +91,6 @@ function buildActionErrorMessage(error: unknown, fallbackMessage: string) {
|
||||
return fallbackMessage;
|
||||
}
|
||||
|
||||
function reimbursementStatusLabel(status: ReimbursementStatus) {
|
||||
return REIMBURSEMENT_STATUS_LABEL[status] ?? "未知状态";
|
||||
}
|
||||
|
||||
function workflowActionLabel(action: string) {
|
||||
const labels: Record<string, string> = {
|
||||
start: "提交",
|
||||
@@ -1009,27 +997,24 @@ export function ReimbursementDetailPage() {
|
||||
<div className="space-y-6">
|
||||
<SectionPanel title="概览与关联">
|
||||
<div className="space-y-6" data-testid="reimbursement-overview">
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="rounded-lg border border-zinc-200 px-4 py-3 text-sm">
|
||||
<div className="text-zinc-500">当前状态</div>
|
||||
<div className="mt-2"><StatusBadge status={detail.status} /></div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 px-4 py-3 text-sm">
|
||||
<div className="text-zinc-500">关联项目</div>
|
||||
<div className="mt-2 font-medium text-zinc-900">{detail.project ? `${detail.project.projectNo} · ${detail.project.name}` : "未关联"}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 px-4 py-3 text-sm">
|
||||
<div className="text-zinc-500">明细数</div>
|
||||
<div className="mt-2 font-medium text-zinc-900">{detail.items.length}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 px-4 py-3 text-sm">
|
||||
<div className="text-zinc-500">发票</div>
|
||||
<div className="mt-2 font-medium text-zinc-900">{detail.invoices.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
<StatStrip columns={4}>
|
||||
<MetricCard
|
||||
title="关联项目"
|
||||
value={
|
||||
<span className="text-base">
|
||||
{detail.project ? `${detail.project.projectNo} · ${detail.project.name}` : "未关联"}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<MetricCard title="明细数" value={detail.items.length} />
|
||||
<MetricCard title="发票" value={detail.invoices.length} />
|
||||
<MetricCard
|
||||
title="付款状态"
|
||||
value={<span className="text-base">{isPaidReimbursement ? "已付款" : "未付款"}</span>}
|
||||
/>
|
||||
</StatStrip>
|
||||
|
||||
<div className="rounded-lg border border-zinc-200 p-4 text-sm" data-testid="reimbursement-summary">
|
||||
<div>状态:{reimbursementStatusLabel(detail.status)}</div>
|
||||
<div>报销编号:{displayCode}</div>
|
||||
<div>报销日期:{detail.reimbursementDate ?? "-"}</div>
|
||||
<div>金额:{detail.totalAmount ?? "-"}</div>
|
||||
|
||||
@@ -25,11 +25,10 @@ import { hasPermission } from "../auth/permissions";
|
||||
import { toast } from "sonner";
|
||||
import { downloadBlob } from "../lib/download";
|
||||
import { resolveReimbursementDisplayCode } from "../lib/reimbursementDisplay";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { Pagination } from "../components/common/Pagination";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
import { FilterBar, ListWorkspace, PrimaryTable } from "../components/patterns/ListWorkspace";
|
||||
import { FilterBar, ListWorkspace, PrimaryTable, StatStrip, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { MetricCard } from "../components/patterns/MetricCard";
|
||||
import { Button } from "../components/ui/button";
|
||||
import {
|
||||
@@ -362,7 +361,7 @@ export function ReimbursementsPage() {
|
||||
|
||||
return (
|
||||
<ListWorkspace>
|
||||
<PageHeader
|
||||
<WorkspaceHeader
|
||||
title="我的报销"
|
||||
subtitle="草稿、审批、支付。"
|
||||
actions={
|
||||
@@ -513,12 +512,12 @@ export function ReimbursementsPage() {
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<StatStrip columns={4}>
|
||||
<MetricCard title="当前列表" value={stats.currentPageCount} note={`共 ${listQuery.data?.total ?? 0} 条`} />
|
||||
<MetricCard title="当前金额" value={formatCurrency(stats.currentPageAmount)} note="当前页" />
|
||||
<MetricCard title="审批中" value={stats.reviewing} note="待推进" tone={stats.reviewing > 0 ? "warning" : "neutral"} />
|
||||
<MetricCard title="已支付" value={stats.paid} note={scope === "all" ? "全范围" : "仅自己"} />
|
||||
</section>
|
||||
</StatStrip>
|
||||
|
||||
<FilterBar className="space-y-4">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
|
||||
const ITEMS = [
|
||||
{ to: "/system/workspace", title: "工作区设置", description: "调整小团队模式的导航与模块显隐。" },
|
||||
@@ -14,17 +14,17 @@ const ITEMS = [
|
||||
|
||||
export function SystemSettingsPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader title="系统设置" subtitle="小团队模式下的后台设置总入口。" />
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader title="系统设置" subtitle="小团队模式下的后台设置总入口。" />
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{ITEMS.map((item) => (
|
||||
<Link key={item.to} to={item.to} className="card-std p-6 hover:bg-zinc-50">
|
||||
<Link key={item.to} to={item.to} className="card-std p-4 hover:bg-zinc-50 md:p-5">
|
||||
<div className="text-base font-semibold text-zinc-900">{item.title}</div>
|
||||
<div className="mt-2 text-sm text-zinc-600">{item.description}</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
updateWorkflowDefinition,
|
||||
type WorkflowDefinitionEditorModel,
|
||||
} from "../api/workflowDefinitions";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
import { Label } from "../components/ui/label";
|
||||
@@ -79,10 +79,10 @@ export function WorkflowDefinitionsPage() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader title="流程定义" subtitle="线性流程的版本化治理;报销继续由审批规则投影。" />
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader title="流程定义" subtitle="线性流程的版本化治理;报销继续由审批规则投影。" />
|
||||
<div className="grid gap-6 xl:grid-cols-[220px,280px,1fr]">
|
||||
<div className="card-std space-y-3 p-6">
|
||||
<div className="card-std space-y-3 p-4 md:p-5">
|
||||
<Label>业务域</Label>
|
||||
<select className="select-std" value={entityType} onChange={(event) => setEntityType(event.target.value as WorkflowDefinitionEditorModel["entityType"])}>
|
||||
{ENTITY_TYPES.map((item) => <option key={item} value={item}>{item}</option>)}
|
||||
@@ -90,7 +90,7 @@ export function WorkflowDefinitionsPage() {
|
||||
{entityType !== "reimbursement" ? <Button type="button" onClick={() => createMutation.mutate()}>创建草稿</Button> : null}
|
||||
</div>
|
||||
|
||||
<div className="card-std space-y-3 p-6">
|
||||
<div className="card-std space-y-3 p-4 md:p-5">
|
||||
<h3 className="text-base font-semibold text-zinc-900">版本列表</h3>
|
||||
<div className="space-y-2">
|
||||
{scopedItems.map((item) => (
|
||||
@@ -103,7 +103,7 @@ export function WorkflowDefinitionsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card-std space-y-4 p-6">
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
{selected ? (
|
||||
<>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
@@ -163,6 +163,6 @@ export function WorkflowDefinitionsPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link, Navigate, useNavigate, useParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -19,6 +20,8 @@ import { CardSkeleton } from "../components/common/CardSkeleton";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { DetailSideMeta } from "../components/patterns/DetailSideMeta";
|
||||
import { EntityOverviewHero } from "../components/patterns/EntityOverviewHero";
|
||||
import { StatStrip } from "../components/patterns/ListWorkspace";
|
||||
import { MetricCard } from "../components/patterns/MetricCard";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { Button } from "../components/ui/button";
|
||||
@@ -48,22 +51,6 @@ function formatDateTime(value: string | null | undefined) {
|
||||
return value.slice(0, 19).replace("T", " ");
|
||||
}
|
||||
|
||||
function workflowStatusLabel(status: string | undefined) {
|
||||
if (!status) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
const labels: Record<string, string> = {
|
||||
pending: "待处理",
|
||||
completed: "已处理",
|
||||
rejected: "已驳回",
|
||||
cancelled: "已取消",
|
||||
skipped: "已跳过",
|
||||
};
|
||||
|
||||
return labels[status] ?? status;
|
||||
}
|
||||
|
||||
function workflowInstanceStatusLabel(status: string | undefined | null) {
|
||||
if (!status) {
|
||||
return "-";
|
||||
@@ -103,6 +90,15 @@ function workflowActionLabel(action: string | undefined | null) {
|
||||
return labels[action] ?? action;
|
||||
}
|
||||
|
||||
function DetailField({ label, value }: { label: string; value: ReactNode }) {
|
||||
return (
|
||||
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3 text-sm">
|
||||
<dt className="text-xs font-medium text-zinc-500">{label}</dt>
|
||||
<dd className="mt-1 break-words font-medium text-zinc-900">{value || "-"}</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function isAlreadyProcessedTaskError(error: unknown) {
|
||||
return isApiError(error) && error.details?.task === "already_processed";
|
||||
}
|
||||
@@ -240,7 +236,6 @@ export function WorkflowTaskDetailPage() {
|
||||
meta={(
|
||||
<>
|
||||
<StatusBadge status={detail.task.status} />
|
||||
<span className="info-chip">对象类型:{entityTitle(detail.workflowInstance?.entityType)}</span>
|
||||
<span className="info-chip">业务状态:{businessStatusLabel}</span>
|
||||
</>
|
||||
)}
|
||||
@@ -255,87 +250,78 @@ export function WorkflowTaskDetailPage() {
|
||||
<div className="space-y-6">
|
||||
<SectionPanel title="审批对象摘要">
|
||||
<div className="space-y-4" data-testid="workflow-task-overview">
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="rounded-lg border border-zinc-200 px-4 py-3 text-sm">
|
||||
<div className="text-zinc-500">当前状态</div>
|
||||
<div className="mt-2"><StatusBadge status={detail.task.status} /></div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 px-4 py-3 text-sm">
|
||||
<div className="text-zinc-500">当前节点</div>
|
||||
<div className="mt-2 font-medium text-zinc-900">{detail.task.stepName}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 px-4 py-3 text-sm">
|
||||
<div className="text-zinc-500">对象类型</div>
|
||||
<div className="mt-2 font-medium text-zinc-900">{entityTitle(detail.workflowInstance?.entityType)}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 px-4 py-3 text-sm">
|
||||
<div className="text-zinc-500">可执行动作</div>
|
||||
<div className="mt-2 font-medium text-zinc-900">{allowedActionLabels || "-"}</div>
|
||||
</div>
|
||||
</div>
|
||||
<StatStrip columns={4}>
|
||||
<MetricCard title="当前节点" value={<span className="text-base">{detail.task.stepName}</span>} />
|
||||
<MetricCard title="业务类型" value={<span className="text-base">{entityTitle(detail.workflowInstance?.entityType)}</span>} />
|
||||
<MetricCard title="可执行动作" value={<span className="text-base">{allowedActionLabels || "-"}</span>} />
|
||||
<MetricCard title="创建时间" value={<span className="text-base">{formatDateTime(detail.task.createdAt)}</span>} />
|
||||
</StatStrip>
|
||||
|
||||
<div className="rounded-lg border border-zinc-200 p-4 text-sm" data-testid="workflow-task-summary">
|
||||
<div>状态:{workflowStatusLabel(detail.task.status)}</div>
|
||||
<div>业务状态:{businessStatusLabel}</div>
|
||||
<div>对象类型:{entityTitle(detail.workflowInstance?.entityType)}</div>
|
||||
<dl className="grid gap-3 md:grid-cols-2" data-testid="workflow-task-summary">
|
||||
{detail.reimbursement ? (
|
||||
<>
|
||||
<div>报销单号:{detail.reimbursement.formNumber ?? "-"}</div>
|
||||
<div>报销金额:{detail.reimbursement.totalAmount ?? "-"}</div>
|
||||
<div>提交人:{detail.reimbursement.submitter?.name ?? "-"}</div>
|
||||
<div>报销人:{detail.reimbursement.owner?.name ?? "-"}</div>
|
||||
<DetailField label="报销单号" value={detail.reimbursement.formNumber ?? "-"} />
|
||||
<DetailField label="报销金额" value={detail.reimbursement.totalAmount ?? "-"} />
|
||||
<DetailField label="提交人" value={detail.reimbursement.submitter?.name ?? "-"} />
|
||||
<DetailField label="报销人" value={detail.reimbursement.owner?.name ?? "-"} />
|
||||
</>
|
||||
) : null}
|
||||
{detail.leaveRequest ? (
|
||||
<>
|
||||
<div>假期类型:{detail.leaveRequest.leaveType?.name ?? "-"}</div>
|
||||
<div>申请人:{detail.leaveRequest.owner?.name ?? "-"}</div>
|
||||
<div>
|
||||
时间段:
|
||||
<DetailField label="假期类型" value={detail.leaveRequest.leaveType?.name ?? "-"} />
|
||||
<DetailField label="申请人" value={detail.leaveRequest.owner?.name ?? "-"} />
|
||||
<DetailField
|
||||
label="时间段"
|
||||
value={
|
||||
<>
|
||||
{detail.leaveRequest.startDate ?? "-"} {detail.leaveRequest.startSession === "am" ? "上午" : "下午"}
|
||||
{" → "}
|
||||
{detail.leaveRequest.endDate ?? "-"} {detail.leaveRequest.endSession === "am" ? "上午" : "下午"}
|
||||
</div>
|
||||
<div>请假天数:{detail.leaveRequest.requestedDays}</div>
|
||||
<div>说明:{detail.leaveRequest.reason ?? "-"}</div>
|
||||
<div>附件:{detail.leaveRequest.attachment?.originalName ?? "-"}</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<DetailField label="请假天数" value={detail.leaveRequest.requestedDays} />
|
||||
<DetailField label="说明" value={detail.leaveRequest.reason ?? "-"} />
|
||||
<DetailField label="附件" value={detail.leaveRequest.attachment?.originalName ?? "-"} />
|
||||
</>
|
||||
) : null}
|
||||
{detail.purchaseRequest ? (
|
||||
<>
|
||||
<div>采购单号:{resolvePurchaseDisplayCode(detail.purchaseRequest)}</div>
|
||||
<div>标题:{detail.purchaseRequest.title || "-"}</div>
|
||||
<div>申请人:{detail.purchaseRequest.applicant?.name ?? "-"}</div>
|
||||
<div>供应商:{detail.purchaseRequest.supplier?.name ?? "-"}</div>
|
||||
<div>法体:{detail.purchaseRequest.legalEntity?.name ?? "-"}</div>
|
||||
<div>成本中心:{detail.purchaseRequest.costCenter?.name ?? "-"}</div>
|
||||
<div>单据分类:{detail.purchaseRequest.documentCategory?.name ?? "-"}</div>
|
||||
<div>金额:{detail.purchaseRequest.totalAmount ?? "-"} {detail.purchaseRequest.currencyCode}</div>
|
||||
<div>预计到货:{detail.purchaseRequest.expectedDeliveryDate ?? "-"}</div>
|
||||
<div>说明:{detail.purchaseRequest.reason ?? "-"}</div>
|
||||
<DetailField label="采购单号" value={resolvePurchaseDisplayCode(detail.purchaseRequest)} />
|
||||
<DetailField label="标题" value={detail.purchaseRequest.title || "-"} />
|
||||
<DetailField label="申请人" value={detail.purchaseRequest.applicant?.name ?? "-"} />
|
||||
<DetailField label="供应商" value={detail.purchaseRequest.supplier?.name ?? "-"} />
|
||||
<DetailField label="法体" value={detail.purchaseRequest.legalEntity?.name ?? "-"} />
|
||||
<DetailField label="成本中心" value={detail.purchaseRequest.costCenter?.name ?? "-"} />
|
||||
<DetailField label="单据分类" value={detail.purchaseRequest.documentCategory?.name ?? "-"} />
|
||||
<DetailField label="金额" value={`${detail.purchaseRequest.totalAmount ?? "-"} ${detail.purchaseRequest.currencyCode}`} />
|
||||
<DetailField label="预计到货" value={detail.purchaseRequest.expectedDeliveryDate ?? "-"} />
|
||||
<DetailField label="说明" value={detail.purchaseRequest.reason ?? "-"} />
|
||||
</>
|
||||
) : null}
|
||||
{detail.travelRequest ? (
|
||||
<>
|
||||
<div>差旅单号:{detail.travelRequest.requestNo ?? "暂未生成"}</div>
|
||||
<div>申请人:{detail.travelRequest.applicant?.name ?? "-"}</div>
|
||||
<div>出差类型:{detail.travelRequest.tripType === "international" ? "国际" : "国内"}</div>
|
||||
<div>法体:{detail.travelRequest.legalEntity?.name ?? "-"}</div>
|
||||
<div>成本中心:{detail.travelRequest.costCenter?.name ?? "-"}</div>
|
||||
<div>起止日期:{detail.travelRequest.startDate ?? "-"} → {detail.travelRequest.endDate ?? "-"}</div>
|
||||
<div>预估金额:{detail.travelRequest.estimatedAmount ?? "-"}</div>
|
||||
<div>出差目的:{detail.travelRequest.purpose || "-"}</div>
|
||||
<div>
|
||||
行程段:
|
||||
{detail.travelRequest.segments.length
|
||||
? detail.travelRequest.segments
|
||||
.map((item) => `${item.fromCity}→${item.toCity}(${item.segmentRole})`)
|
||||
.join(";")
|
||||
: "-"}
|
||||
</div>
|
||||
<DetailField label="差旅单号" value={detail.travelRequest.requestNo ?? "暂未生成"} />
|
||||
<DetailField label="申请人" value={detail.travelRequest.applicant?.name ?? "-"} />
|
||||
<DetailField label="出差类型" value={detail.travelRequest.tripType === "international" ? "国际" : "国内"} />
|
||||
<DetailField label="法体" value={detail.travelRequest.legalEntity?.name ?? "-"} />
|
||||
<DetailField label="成本中心" value={detail.travelRequest.costCenter?.name ?? "-"} />
|
||||
<DetailField label="起止日期" value={`${detail.travelRequest.startDate ?? "-"} → ${detail.travelRequest.endDate ?? "-"}`} />
|
||||
<DetailField label="预估金额" value={detail.travelRequest.estimatedAmount ?? "-"} />
|
||||
<DetailField label="出差目的" value={detail.travelRequest.purpose || "-"} />
|
||||
<DetailField
|
||||
label="行程段"
|
||||
value={
|
||||
detail.travelRequest.segments.length
|
||||
? detail.travelRequest.segments
|
||||
.map((item) => `${item.fromCity}→${item.toCity}(${item.segmentRole})`)
|
||||
.join(";")
|
||||
: "-"
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</SectionPanel>
|
||||
|
||||
|
||||
@@ -9,10 +9,19 @@ import {
|
||||
fetchWorkflowTaskSummary,
|
||||
type WorkflowTaskSummary,
|
||||
} from "../api/workflow";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { Pagination } from "../components/common/Pagination";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
import {
|
||||
ActionToolbar,
|
||||
FilterBar,
|
||||
ListWorkspace,
|
||||
PrimaryTable,
|
||||
StatStrip,
|
||||
WorkspaceEmptyState,
|
||||
WorkspaceHeader,
|
||||
} from "../components/patterns/ListWorkspace";
|
||||
import { MetricCard } from "../components/patterns/MetricCard";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
||||
import { Tabs, TabsList, TabsTrigger } from "../components/ui/tabs";
|
||||
@@ -187,35 +196,34 @@ export function WorkflowTasksPage() {
|
||||
const taskItems = query.data?.items ?? [];
|
||||
|
||||
return (
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
<PageHeader title="审批中心" subtitle="集中处理待办、查看已办和业务审批历史。" />
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
eyebrow="审批"
|
||||
title="审批中心"
|
||||
subtitle="集中处理待办、查看已办和业务审批历史。"
|
||||
/>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-4" data-testid="workflow-task-summary">
|
||||
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3">
|
||||
<div className="text-xs text-zinc-500">待办总数</div>
|
||||
<div className="mt-2 text-2xl font-semibold text-zinc-900">{summaryQuery.data?.pendingTotal ?? "-"}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3">
|
||||
<div className="text-xs text-zinc-500">超时待办</div>
|
||||
<div className="mt-2 text-2xl font-semibold text-amber-700">{summaryQuery.data?.overdueTotal ?? "-"}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3">
|
||||
<div className="text-xs text-zinc-500">最早待处理</div>
|
||||
<div className="mt-2 text-sm font-medium text-zinc-900">{formatSummaryTime(summaryQuery.data?.earliestPendingAt)}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3">
|
||||
<div className="text-xs text-zinc-500">业务分布</div>
|
||||
<div className="mt-2 truncate text-sm font-medium text-zinc-900">
|
||||
{(summaryQuery.data?.byEntityType ?? [])
|
||||
.filter((item) => item.pendingTotal > 0)
|
||||
.map((item) => `${entityLabel(item.key)} ${item.pendingTotal}`)
|
||||
.join(" / ") || "-"}
|
||||
</div>
|
||||
</div>
|
||||
<div data-testid="workflow-task-summary">
|
||||
<StatStrip columns={4}>
|
||||
<MetricCard title="待办总数" value={summaryQuery.data?.pendingTotal ?? "-"} />
|
||||
<MetricCard title="超时待办" value={summaryQuery.data?.overdueTotal ?? "-"} tone="warning" />
|
||||
<MetricCard title="最早待处理" value={formatSummaryTime(summaryQuery.data?.earliestPendingAt)} />
|
||||
<MetricCard
|
||||
title="业务分布"
|
||||
value={
|
||||
<span className="block truncate text-base">
|
||||
{(summaryQuery.data?.byEntityType ?? [])
|
||||
.filter((item) => item.pendingTotal > 0)
|
||||
.map((item) => `${entityLabel(item.key)} ${item.pendingTotal}`)
|
||||
.join(" / ") || "-"}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</StatStrip>
|
||||
</div>
|
||||
|
||||
<div className="card-std space-y-4 p-4 md:p-5">
|
||||
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
|
||||
<FilterBar>
|
||||
<ActionToolbar className="items-start">
|
||||
<Tabs
|
||||
data-testid="workflow-tasks-status-tabs"
|
||||
value={status}
|
||||
@@ -234,13 +242,13 @@ export function WorkflowTasksPage() {
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex w-full items-center gap-2 sm:w-auto">
|
||||
<label className="text-sm font-medium text-zinc-700" htmlFor="workflow-entity-type-filter">
|
||||
业务类型
|
||||
</label>
|
||||
<select
|
||||
id="workflow-entity-type-filter"
|
||||
className="select-std min-w-36"
|
||||
className="select-std min-w-36 flex-1 sm:flex-none"
|
||||
data-testid="workflow-tasks-entity-filter"
|
||||
value={entityType}
|
||||
onChange={(event) => updateSearchParams({ entityType: event.target.value as WorkflowEntityFilter, page: 1 })}
|
||||
@@ -252,7 +260,8 @@ export function WorkflowTasksPage() {
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</ActionToolbar>
|
||||
</FilterBar>
|
||||
|
||||
{query.isLoading ? <TableSkeleton columns={7} rows={5} /> : null}
|
||||
|
||||
@@ -270,24 +279,36 @@ export function WorkflowTasksPage() {
|
||||
|
||||
{!query.isLoading && !query.isError ? (
|
||||
<>
|
||||
<div className="space-y-3 md:hidden" data-testid="workflow-tasks-mobile-list">
|
||||
<div className="mobile-page-stack md:hidden" data-testid="workflow-tasks-mobile-list">
|
||||
{taskItems.length ? (
|
||||
taskItems.map((task) => (
|
||||
<article className="rounded-xl border border-zinc-200 bg-white p-4 shadow-sm" key={task.id}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<article className="mobile-list-card" key={task.id}>
|
||||
<div className="mobile-list-card__header">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-semibold text-zinc-900">{displayTitle(task)}</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">
|
||||
<div className="mobile-list-card__title">{displayTitle(task)}</div>
|
||||
<div className="mobile-list-card__subtitle">
|
||||
{entityLabel(task.workflowInstance?.entityType)} · {displayReference(task)}
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge className="shrink-0" status={task.status} />
|
||||
<StatusBadge className="mobile-status-slot" status={task.status} />
|
||||
</div>
|
||||
<div className="mt-3 grid gap-1 rounded-lg bg-zinc-50 px-3 py-2 text-xs text-zinc-600">
|
||||
<div>申请人:{displayApplicant(task)}</div>
|
||||
<div>金额/天数:{displayMetric(task)}</div>
|
||||
<div>当前节点:{task.stepName}</div>
|
||||
<div>业务状态:{businessStatusLabel(task.businessSummary?.businessStatus)}</div>
|
||||
<div className="mobile-field-list mt-3 rounded-lg bg-zinc-50 px-3 py-2">
|
||||
<div className="mobile-field-row">
|
||||
<span className="mobile-field-label">申请人</span>
|
||||
<span className="mobile-field-value">{displayApplicant(task)}</span>
|
||||
</div>
|
||||
<div className="mobile-field-row">
|
||||
<span className="mobile-field-label">金额/天数</span>
|
||||
<span className="mobile-field-value">{displayMetric(task)}</span>
|
||||
</div>
|
||||
<div className="mobile-field-row">
|
||||
<span className="mobile-field-label">当前节点</span>
|
||||
<span className="mobile-field-value">{task.stepName}</span>
|
||||
</div>
|
||||
<div className="mobile-field-row">
|
||||
<span className="mobile-field-label">业务状态</span>
|
||||
<span className="mobile-field-value">{businessStatusLabel(task.businessSummary?.businessStatus)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2">
|
||||
{status === "pending" && (task.allowedActions ?? []).includes("approve") ? (
|
||||
@@ -317,13 +338,11 @@ export function WorkflowTasksPage() {
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<div className="rounded-xl border border-dashed border-zinc-300 px-4 py-8 text-center text-sm text-zinc-500">
|
||||
暂无数据
|
||||
</div>
|
||||
<WorkspaceEmptyState />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="hidden rounded-lg border border-zinc-200 md:block">
|
||||
<PrimaryTable className="hidden md:block">
|
||||
<Table data-testid="workflow-tasks-table">
|
||||
<TableHeader>
|
||||
<tr>
|
||||
@@ -381,14 +400,14 @@ export function WorkflowTasksPage() {
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell className="py-3 text-zinc-500" colSpan={8}>
|
||||
<TableCell className="px-6 py-8 text-center text-zinc-500" colSpan={8}>
|
||||
暂无数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</PrimaryTable>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -398,7 +417,6 @@ export function WorkflowTasksPage() {
|
||||
onPageChange={(p) => updateSearchParams({ page: p })}
|
||||
testIdPrefix="workflow-tasks"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -222,6 +222,72 @@
|
||||
padding: var(--mobile-card-padding);
|
||||
}
|
||||
|
||||
.mobile-page-stack {
|
||||
display: grid;
|
||||
gap: var(--mobile-card-gap);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mobile-section-header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mobile-section-title {
|
||||
min-width: 0;
|
||||
color: rgb(24 24 27);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.35rem;
|
||||
}
|
||||
|
||||
.mobile-section-meta {
|
||||
flex: none;
|
||||
color: rgb(113 113 122);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.mobile-list-card__header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 0.75rem;
|
||||
align-items: start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mobile-list-card__title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: rgb(24 24 27);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.35rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-list-card__subtitle {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: rgb(113 113 122);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.05rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-status-slot {
|
||||
flex: none;
|
||||
max-width: 42%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-subcard {
|
||||
min-width: 0;
|
||||
border: 1px solid rgb(228 228 231);
|
||||
@@ -299,6 +365,17 @@
|
||||
padding: 0.75rem var(--mobile-page-x) max(0.75rem, var(--mobile-safe-area-bottom));
|
||||
}
|
||||
|
||||
.mobile-sticky-action-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 8rem), 1fr));
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mobile-sticky-action-grid > * {
|
||||
min-height: 46px;
|
||||
}
|
||||
|
||||
@media (max-width: 359px) {
|
||||
:root {
|
||||
--mobile-page-x: 8px;
|
||||
|
||||
在新工单中引用
屏蔽一个用户