diff --git a/.trellis/spec/frontend/components.md b/.trellis/spec/frontend/components.md index 09f5629..0679c8f 100644 --- a/.trellis/spec/frontend/components.md +++ b/.trellis/spec/frontend/components.md @@ -158,6 +158,48 @@ The project Dialog adapter must: - Browser validation should assert the panel center is within 2px of the viewport center on desktop and mobile. +### Static Module Data Contract + +Static or reserved module pages must not fabricate operational data. If a module does +not have a Drizzle-backed query/API and real persisted records, render a clear empty or +not-connected state instead of generated counters, fake workflows, sample records, or +mock priority/status rows. + +`modules/shared/components/ModulePage.tsx` is the shared placeholder for these modules. +Its props should stay descriptive only: + +```typescript +type ModulePageProps = { + title: string; + eyebrow: string; + description: string; + icon: LucideIcon; + phase: "待接入" | "二期预留" | "三期预留"; +}; +``` + +```typescript +// Bad: static pages pretending to have live business data. + + +// Good: no fabricated records until a real data source exists. + +``` + +When a module becomes real, replace the placeholder with a Server Component that loads +data from the server boundary and pass only persisted, permission-filtered data into +client components. + ### Use Proper Elements ```typescript diff --git a/app/(app)/app/alerts/page.tsx b/app/(app)/app/alerts/page.tsx index e9dfa74..972057c 100644 --- a/app/(app)/app/alerts/page.tsx +++ b/app/(app)/app/alerts/page.tsx @@ -7,15 +7,9 @@ export default function AlertsPage(): React.ReactElement { ); } diff --git a/app/(app)/app/care/page.tsx b/app/(app)/app/care/page.tsx index d1cb477..696029b 100644 --- a/app/(app)/app/care/page.tsx +++ b/app/(app)/app/care/page.tsx @@ -7,15 +7,9 @@ export default function CarePage(): React.ReactElement { ); } diff --git a/app/(app)/app/devices/page.tsx b/app/(app)/app/devices/page.tsx index 9b97542..3a7faa3 100644 --- a/app/(app)/app/devices/page.tsx +++ b/app/(app)/app/devices/page.tsx @@ -7,15 +7,9 @@ export default function DevicesPage(): React.ReactElement { ); } diff --git a/app/(app)/app/emergency/page.tsx b/app/(app)/app/emergency/page.tsx index 93c37cb..26bc86a 100644 --- a/app/(app)/app/emergency/page.tsx +++ b/app/(app)/app/emergency/page.tsx @@ -7,15 +7,9 @@ export default function EmergencyPage(): React.ReactElement { ); } diff --git a/app/(app)/app/family/page.tsx b/app/(app)/app/family/page.tsx index 733940d..d6d2389 100644 --- a/app/(app)/app/family/page.tsx +++ b/app/(app)/app/family/page.tsx @@ -7,15 +7,9 @@ export default function FamilyPage(): React.ReactElement { ); } diff --git a/app/(app)/app/health/page.tsx b/app/(app)/app/health/page.tsx index e8acb1a..3659057 100644 --- a/app/(app)/app/health/page.tsx +++ b/app/(app)/app/health/page.tsx @@ -7,15 +7,9 @@ export default function HealthPage(): React.ReactElement { ); } diff --git a/app/(app)/app/notices/page.tsx b/app/(app)/app/notices/page.tsx index 9944056..1603c5f 100644 --- a/app/(app)/app/notices/page.tsx +++ b/app/(app)/app/notices/page.tsx @@ -7,15 +7,9 @@ export default function NoticesPage(): React.ReactElement { ); } diff --git a/modules/shared/components/CmsModuleClient.tsx b/modules/shared/components/CmsModuleClient.tsx deleted file mode 100644 index d8d89dc..0000000 --- a/modules/shared/components/CmsModuleClient.tsx +++ /dev/null @@ -1,233 +0,0 @@ -"use client"; - -import { useMemo, useState } from "react"; -import { Eye, Filter, Search } from "lucide-react"; - -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Dialog } from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Select } from "@/components/ui/select"; -import { Table } from "@/components/ui/table"; - -export type CmsRecordTone = "success" | "warning" | "danger" | "secondary"; - -export type CmsRecord = { - id: string; - title: string; - owner: string; - status: string; - statusTone: CmsRecordTone; - category: string; - updatedAt: string; - priority: "高" | "中" | "低"; - summary: string; -}; - -type CmsModuleClientProps = { - records: CmsRecord[]; -}; - -const PAGE_SIZE = 5; -const ALL_STATUS = "全部状态"; - -function matchesRecord(record: CmsRecord, query: string, status: string): boolean { - const normalizedQuery = query.trim().toLowerCase(); - const matchesSearch = - normalizedQuery.length === 0 || - [record.title, record.owner, record.category, record.summary] - .join(" ") - .toLowerCase() - .includes(normalizedQuery); - const matchesStatus = status === ALL_STATUS || record.status === status; - - return matchesSearch && matchesStatus; -} - -export function CmsModuleClient({ records }: CmsModuleClientProps): React.ReactElement { - const [query, setQuery] = useState(""); - const [status, setStatus] = useState(ALL_STATUS); - const [page, setPage] = useState(1); - const [selectedRecord, setSelectedRecord] = useState(null); - - const statusOptions = useMemo(() => { - const uniqueStatuses = new Set(records.map((record) => record.status)); - return [ALL_STATUS, ...Array.from(uniqueStatuses)]; - }, [records]); - - const filteredRecords = useMemo( - () => records.filter((record) => matchesRecord(record, query, status)), - [query, records, status], - ); - const pageCount = Math.max(1, Math.ceil(filteredRecords.length / PAGE_SIZE)); - const safePage = Math.min(page, pageCount); - const pageStart = (safePage - 1) * PAGE_SIZE; - const visibleRecords = filteredRecords.slice(pageStart, pageStart + PAGE_SIZE); - const highPriorityCount = filteredRecords.filter((record) => record.priority === "高").length; - - function updateQuery(value: string): void { - setQuery(value); - setPage(1); - } - - function updateStatus(value: string): void { - setStatus(value); - setPage(1); - } - - return ( - - - - 数据列表 - - 共 {filteredRecords.length} 条记录,{highPriorityCount} 条高优先级已标记。 - - - - - - updateQuery(event.target.value)} - placeholder="搜索名称、负责人、分类" - value={query} - /> - - - - ({ value: option, label: option }))} - value={status} - /> - - - - - - - - - 记录 - 分类 - 负责人 - 状态 - 标记 - 操作 - - - - {visibleRecords.map((record) => ( - - - {record.title} - {record.summary} - - {record.category} - {record.owner} - - {record.status} - - - - {record.priority}优先级 - - - - - setSelectedRecord(record)} size="sm" type="button" variant="outline"> - - 详情 - - - - - ))} - {visibleRecords.length === 0 ? ( - - - 暂无匹配记录 - - - ) : null} - - - - - - - 第 {safePage} / {pageCount} 页 - - - setPage((current) => Math.max(1, current - 1))} - size="sm" - type="button" - variant="outline" - > - 上一页 - - setPage((current) => Math.min(pageCount, current + 1))} - size="sm" - type="button" - variant="outline" - > - 下一页 - - - - - setSelectedRecord(null)} - open={selectedRecord !== null} - title={selectedRecord?.title ?? "记录详情"} - > - {selectedRecord ? ( - - - 分类 - {selectedRecord.category} - - - 负责人 - {selectedRecord.owner} - - - 状态 - - {selectedRecord.status} - - - - 最近更新 - {selectedRecord.updatedAt} - - - 数据标记 - - - {selectedRecord.priority}优先级 - - - - - ) : null} - - - ); -} diff --git a/modules/shared/components/ModulePage.tsx b/modules/shared/components/ModulePage.tsx index c4b0c18..f1ff6f2 100644 --- a/modules/shared/components/ModulePage.tsx +++ b/modules/shared/components/ModulePage.tsx @@ -1,180 +1,84 @@ import type { LucideIcon } from "lucide-react"; -import { ArrowRight, BarChart3, CheckCircle2, Flag } from "lucide-react"; +import { Database, ExternalLink } from "lucide-react"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; +import { LinkButton } 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[]; + phase: "待接入" | "二期预留" | "三期预留"; }; -const STATUS_OPTIONS: Array<{ status: string; tone: CmsRecordTone }> = [ - { status: "进行中", tone: "warning" }, - { status: "已归档", tone: "success" }, - { status: "需复核", tone: "danger" }, - { status: "待处理", tone: "secondary" }, +const realWorkspaceLinks = [ + { href: "/app/dashboard", label: "运营看板" }, + { href: "/app/elders", label: "老人档案" }, + { href: "/app/beds", label: "床位房间" }, ]; -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; - +export function ModulePage({ title, eyebrow, description, icon: Icon, phase }: ModulePageProps): React.ReactElement { return ( - - - {phase} - - - - - - {eyebrow} - {title} + + + + + + + + {title} + {phase} + {eyebrow} + {description} - {description} - - - 筛选 - - 新增记录 - - - - {metrics.map((metric) => ( - - - {metric.label} - {metric.value} - - - {metric.detail} - - - ))} - + + + + + + 数据状态 + + 当前模块未接入真实业务数据源。 + + + + + 业务记录 + 暂不展示 + + + 统计指标 + 暂不展示 + + + 待办流程 + 待真实流程接入 + + + + - - - - - - - - 分析摘要 - 内置样例数据用于看板和列表联动。 - - - - - 列表记录 - {records.length} - - - - - 红色标记 - - {highPriorityCount} - - - {workflows.slice(0, 3).map((workflow) => ( - - - {workflow} - - ))} - - - - - + + + 已接入工作区 + 以下页面展示真实系统数据。 + + + {realWorkspaceLinks.map((link) => ( + + {link.label} + + + ))} + + );
- 共 {filteredRecords.length} 条记录,{highPriorityCount} 条高优先级已标记。 -
{record.title}
{record.summary}
- 第 {safePage} / {pageCount} 页 -
{eyebrow}
{description}
{metric.detail}
内置样例数据用于看板和列表联动。