From aed5c6db5685c293eff16201ce44f6ee92ef24b4 Mon Sep 17 00:00:00 2001 From: TalexDreamSoul Date: Wed, 1 Jul 2026 06:49:56 -0700 Subject: [PATCH] feat: standardize cms data views --- components/ui/dialog.tsx | 58 ++ modules/elders/components/EldersClient.tsx | 549 ++++++++++++------ .../settings/components/SettingsOverview.tsx | 117 +++- modules/shared/components/CmsModuleClient.tsx | 235 ++++++++ modules/shared/components/ModulePage.tsx | 118 +++- 5 files changed, 875 insertions(+), 202 deletions(-) create mode 100644 components/ui/dialog.tsx create mode 100644 modules/shared/components/CmsModuleClient.tsx diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..4b8a64e --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,58 @@ +import * as React from "react"; +import { X } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +type DialogProps = { + open: boolean; + title: string; + description?: string; + children: React.ReactNode; + footer?: React.ReactNode; + onClose: () => void; + className?: string; +}; + +export function Dialog({ + open, + title, + description, + children, + footer, + onClose, + className, +}: DialogProps): React.ReactElement | null { + if (!open) { + return null; + } + + return ( +
+
+
+
+

{title}

+ {description ?

{description}

: null} +
+ +
+
{children}
+ {footer ?
{footer}
: null} +
+
+ ); +} diff --git a/modules/elders/components/EldersClient.tsx b/modules/elders/components/EldersClient.tsx index 65b3687..f8ff0e7 100644 --- a/modules/elders/components/EldersClient.tsx +++ b/modules/elders/components/EldersClient.tsx @@ -1,15 +1,16 @@ "use client"; import { FormEvent, useMemo, useState } from "react"; -import { Edit3, Plus, Trash2, X } from "lucide-react"; +import { Edit3, Filter, Plus, Search, Trash2 } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Dialog } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import type { ApiResult } from "@/modules/core/server/api"; import type { Permission } from "@/modules/core/types"; -import type { Elder, ElderInput } from "@/modules/elders/types"; +import type { CareLevel, Elder, ElderInput, ElderStatus } from "@/modules/elders/types"; import { CARE_LEVEL_LABELS, ELDER_STATUS_LABELS, GENDER_LABELS } from "@/modules/elders/types"; type EldersClientProps = { @@ -30,6 +31,10 @@ const emptyInput: ElderInput = { medicalNotes: "", }; +const PAGE_SIZE = 8; +const ALL_STATUS = "all"; +const ALL_CARE_LEVELS = "all"; + function elderToInput(elder: Elder): ElderInput { return { name: elder.name, @@ -45,12 +50,53 @@ function elderToInput(elder: Elder): ElderInput { }; } +function formatDateTime(value: string): string { + return new Date(value).toLocaleString("zh-CN"); +} + +function getRiskLabel(elder: Elder): { label: string; variant: "danger" | "warning" | "success" } { + if (elder.careLevel === "intensive") { + return { label: "红色重点", variant: "danger" }; + } + + if (elder.status === "pending" || elder.careLevel === "assisted") { + return { label: "黄色关注", variant: "warning" }; + } + + return { label: "绿色稳定", variant: "success" }; +} + +function matchesFilters( + elder: Elder, + query: string, + statusFilter: ElderStatus | typeof ALL_STATUS, + careLevelFilter: CareLevel | typeof ALL_CARE_LEVELS, +): boolean { + const normalizedQuery = query.trim().toLowerCase(); + const matchesSearch = + normalizedQuery.length === 0 || + [elder.name, elder.room, elder.bed, elder.primaryContact, elder.phone, elder.medicalNotes] + .join(" ") + .toLowerCase() + .includes(normalizedQuery); + const matchesStatus = statusFilter === ALL_STATUS || elder.status === statusFilter; + const matchesCareLevel = careLevelFilter === ALL_CARE_LEVELS || elder.careLevel === careLevelFilter; + + return matchesSearch && matchesStatus && matchesCareLevel; +} + export function EldersClient({ initialElders, permissions }: EldersClientProps): React.ReactElement { const [elders, setElders] = useState(initialElders); const [editingId, setEditingId] = useState(null); const [form, setForm] = useState(emptyInput); const [message, setMessage] = useState(""); const [isPending, setIsPending] = useState(false); + const [isFormOpen, setIsFormOpen] = useState(false); + const [deleteTarget, setDeleteTarget] = useState(null); + const [query, setQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState(ALL_STATUS); + const [careLevelFilter, setCareLevelFilter] = useState(ALL_CARE_LEVELS); + const [page, setPage] = useState(1); const canCreate = permissions.includes("elder:create"); const canUpdate = permissions.includes("elder:update"); @@ -62,9 +108,19 @@ export function EldersClient({ initialElders, permissions }: EldersClientProps): const activeCount = elders.filter((elder) => elder.status === "active").length; const pendingCount = elders.filter((elder) => elder.status === "pending").length; const intensiveCount = elders.filter((elder) => elder.careLevel === "intensive").length; - return { activeCount, pendingCount, intensiveCount }; + const warningCount = elders.filter((elder) => getRiskLabel(elder).variant !== "success").length; + return { activeCount, pendingCount, intensiveCount, warningCount }; }, [elders]); + const filteredElders = useMemo( + () => elders.filter((elder) => matchesFilters(elder, query, statusFilter, careLevelFilter)), + [careLevelFilter, elders, query, statusFilter], + ); + const pageCount = Math.max(1, Math.ceil(filteredElders.length / PAGE_SIZE)); + const safePage = Math.min(page, pageCount); + const pageStart = (safePage - 1) * PAGE_SIZE; + const visibleElders = filteredElders.slice(pageStart, pageStart + PAGE_SIZE); + function updateField(key: K, value: ElderInput[K]): void { setForm((current) => ({ ...current, [key]: value })); } @@ -73,12 +129,39 @@ export function EldersClient({ initialElders, permissions }: EldersClientProps): setEditingId(null); setForm(emptyInput); setMessage(""); + setIsFormOpen(true); } function beginEdit(elder: Elder): void { setEditingId(elder.id); setForm(elderToInput(elder)); setMessage(""); + setIsFormOpen(true); + } + + function closeForm(): void { + if (isPending) { + return; + } + + setIsFormOpen(false); + setEditingId(null); + setForm(emptyInput); + } + + function updateQuery(value: string): void { + setQuery(value); + setPage(1); + } + + function updateStatusFilter(value: string): void { + setStatusFilter(value as ElderStatus | typeof ALL_STATUS); + setPage(1); + } + + function updateCareLevelFilter(value: string): void { + setCareLevelFilter(value as CareLevel | typeof ALL_CARE_LEVELS); + setPage(1); } async function refreshElders(): Promise { @@ -115,10 +198,15 @@ export function EldersClient({ initialElders, permissions }: EldersClientProps): await refreshElders(); setEditingId(null); setForm(emptyInput); + setIsFormOpen(false); setMessage(result.reason); } - async function handleDelete(elder: Elder): Promise { + async function handleDelete(): Promise { + if (!deleteTarget) { + return; + } + if (!canDelete) { setMessage("当前角色无权删除老人档案"); return; @@ -127,7 +215,7 @@ export function EldersClient({ initialElders, permissions }: EldersClientProps): setIsPending(true); setMessage(""); - const response = await fetch(`/api/elders/${elder.id}`, { method: "DELETE" }); + const response = await fetch(`/api/elders/${deleteTarget.id}`, { method: "DELETE" }); const result = (await response.json()) as ApiResult<{ elder: Elder }>; setIsPending(false); @@ -137,6 +225,7 @@ export function EldersClient({ initialElders, permissions }: EldersClientProps): } await refreshElders(); + setDeleteTarget(null); setMessage(result.reason); } @@ -146,160 +235,116 @@ export function EldersClient({ initialElders, permissions }: EldersClientProps):
全栈 CRUD

老人档案

-

服务端持久化档案、权限校验和审计记录。

+

列表管理、筛选搜索、分页、弹窗维护和审计持久化。

-
-
-

在院老人

-

{metrics.activeCount}

-
-
-

待入住

-

{metrics.pendingCount}

-
-
-

重点照护

-

{metrics.intensiveCount}

-
+
+
-
+
- - {isEditing ? "编辑档案" : "新增档案"} - {isEditing ? ( - - ) : null} + + 在院老人 + {metrics.activeCount} - -
- -
- - -
-
- - -
-
- - -
- - -