Files
teatea-pension/modules/shared/components/ModulePage.tsx

182 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { LucideIcon } from "lucide-react";
import { ArrowRight, BarChart3, CheckCircle2, Flag } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { CmsModuleClient, type CmsRecord, type CmsRecordTone } from "@/modules/shared/components/CmsModuleClient";
type ModuleMetric = {
label: string;
value: string;
detail: string;
};
type ModulePageProps = {
title: string;
eyebrow: string;
description: string;
icon: LucideIcon;
phase: "一期" | "二期预留" | "三期预留";
metrics: ModuleMetric[];
workflows: string[];
};
const STATUS_OPTIONS: Array<{ status: string; tone: CmsRecordTone }> = [
{ status: "进行中", tone: "warning" },
{ status: "已归档", tone: "success" },
{ status: "需复核", tone: "danger" },
{ status: "待处理", tone: "secondary" },
];
const PRIORITY_OPTIONS: CmsRecord["priority"][] = ["高", "中", "低"];
function pickStatus(index: number): { status: string; tone: CmsRecordTone } {
return STATUS_OPTIONS[index % STATUS_OPTIONS.length] ?? { status: "待处理", tone: "secondary" };
}
function pickPriority(index: number): CmsRecord["priority"] {
return PRIORITY_OPTIONS[index % PRIORITY_OPTIONS.length] ?? "低";
}
function buildCmsRecords(title: string, metrics: ModuleMetric[], workflows: string[]): CmsRecord[] {
const workflowRecords = workflows.map((workflow, index) => {
const status = pickStatus(index);
return {
id: `workflow-${index + 1}`,
title: workflow,
owner: index % 2 === 0 ? "运营主管" : "护理组长",
status: status.status,
statusTone: status.tone,
category: title,
updatedAt: `今日 ${9 + index}:30`,
priority: pickPriority(index),
summary: `${title}流程节点:${workflow},支持按状态筛选、搜索和详情查看。`,
};
});
const metricRecords = metrics.map((metric, index) => {
const status = pickStatus(index + workflows.length);
return {
id: `metric-${index + 1}`,
title: `${metric.label}数据巡检`,
owner: "数据管理员",
status: status.status,
statusTone: status.tone,
category: "指标分析",
updatedAt: `昨日 ${14 + index}:00`,
priority: metric.value.includes("%") ? "中" : pickPriority(index + 1),
summary: `${metric.label}当前值 ${metric.value}${metric.detail}`,
};
});
return [
...workflowRecords,
...metricRecords,
{
id: "system-red-flag",
title: `${title}红色标记复核`,
owner: "值班主管",
status: "需复核",
statusTone: "danger",
category: "风险标记",
updatedAt: "今日 08:45",
priority: "高",
summary: "内置样例风险标记,用于演示红色、黄色、绿色状态分层和待办优先级。",
},
];
}
export function ModulePage({
title,
eyebrow,
description,
icon: Icon,
phase,
metrics,
workflows,
}: ModulePageProps): React.ReactElement {
const records = buildCmsRecords(title, metrics, workflows);
const highPriorityCount = records.filter((record) => record.priority === "高").length;
return (
<div className="mx-auto flex w-full max-w-7xl flex-col gap-6">
<section className="flex flex-col justify-between gap-4 border-b pb-5 lg:flex-row lg:items-end">
<div className="max-w-3xl">
<Badge variant={phase === "一期" ? "success" : "secondary"}>{phase}</Badge>
<div className="mt-4 flex items-center gap-3">
<span className="inline-flex size-11 items-center justify-center rounded-md bg-secondary text-primary">
<Icon className="size-5" aria-hidden="true" />
</span>
<div>
<p className="text-xs font-medium text-muted-foreground">{eyebrow}</p>
<h1 className="text-2xl font-semibold tracking-normal md:text-3xl">{title}</h1>
</div>
</div>
<p className="mt-3 text-sm text-muted-foreground">{description}</p>
</div>
<div className="flex gap-2">
<Button variant="outline"></Button>
<Button>
<ArrowRight className="size-4" aria-hidden="true" />
</Button>
</div>
</section>
<section className="grid gap-4 md:grid-cols-3">
{metrics.map((metric) => (
<Card key={metric.label}>
<CardHeader className="p-4">
<CardDescription>{metric.label}</CardDescription>
<CardTitle className="text-2xl">{metric.value}</CardTitle>
</CardHeader>
<CardContent className="p-4 pt-0">
<p className="text-xs text-muted-foreground">{metric.detail}</p>
</CardContent>
</Card>
))}
</section>
<section className="grid gap-4 lg:grid-cols-[0.72fr_1.28fr]">
<div className="rounded-lg border bg-card p-5">
<div className="flex items-center gap-3">
<span className="inline-flex size-10 items-center justify-center rounded-md bg-secondary text-primary">
<BarChart3 className="size-5" aria-hidden="true" />
</span>
<div>
<h2 className="text-base font-semibold"></h2>
<p className="text-sm text-muted-foreground"></p>
</div>
</div>
<div className="mt-5 grid gap-3">
<div className="flex items-center justify-between rounded-md border p-3">
<span className="text-sm text-muted-foreground"></span>
<strong>{records.length}</strong>
</div>
<div className="flex items-center justify-between rounded-md border border-red-200 bg-red-50 p-3 text-red-700">
<span className="flex items-center gap-2 text-sm">
<Flag className="size-4" aria-hidden="true" />
</span>
<strong>{highPriorityCount}</strong>
</div>
<ol className="grid gap-2">
{workflows.slice(0, 3).map((workflow) => (
<li key={workflow} className="flex items-start gap-2 text-sm">
<CheckCircle2 className="mt-0.5 size-4 shrink-0 text-primary" aria-hidden="true" />
<span>{workflow}</span>
</li>
))}
</ol>
</div>
</div>
<CmsModuleClient records={records} />
</section>
</div>
);
}