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} 条高优先级已标记。 -

-
-
- -
- -