chore: initialize Next.js project baseline

This commit is contained in:
2026-07-01 06:27:09 -07:00
commit 7f227c5f2a
53 changed files with 5894 additions and 0 deletions

13
.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
.data/
.next/
node_modules/
tsconfig.tsbuildinfo
*.log
.DS_Store
.playwright-mcp/
.serena/
.trellis/.runtime/
.trellis/.developer
__pycache__/

21
AGENTS.md Normal file
View File

@@ -0,0 +1,21 @@
<!-- TRELLIS:START -->
# Trellis Instructions
These instructions are for AI assistants working in this project.
This project is managed by Trellis. The working knowledge you need lives under `.trellis/`:
- `.trellis/workflow.md` — development phases, when to create tasks, skill routing
- `.trellis/spec/` — package- and layer-scoped coding guidelines (read before writing code in a given layer)
- `.trellis/workspace/` — per-developer journals and session traces
- `.trellis/tasks/` — active and archived tasks (PRDs, research, jsonl context)
If a Trellis command is available on your platform (e.g. `/trellis:finish-work`, `/trellis:continue`), prefer it over manual steps. Not every platform exposes every command.
If you're using Codex or another agent-capable tool, additional project-scoped helpers may live in:
- `.agents/skills/` — reusable Trellis skills
- `.codex/agents/` — optional custom subagents
Managed by Trellis. Edits outside this block are preserved; edits inside may be overwritten by a future `trellis update`.
<!-- TRELLIS:END -->

View File

@@ -0,0 +1,21 @@
import { Radio } from "lucide-react";
import { ModulePage } from "@/modules/shared/components/ModulePage";
export default function AlertsPage(): React.ReactElement {
return (
<ModulePage
title="规则预警"
eyebrow="规则中心与风险识别"
description="三期引入健康阈值、任务逾期、设备故障、应急事件和风险老人识别规则。"
icon={Radio}
phase="三期预留"
metrics={[
{ label: "规则类型", value: "5", detail: "健康、用药、护理、设备、应急" },
{ label: "预警状态", value: "4", detail: "生成、确认、处理、关闭" },
{ label: "风险维度", value: "3", detail: "健康异常、事件频次、护理强度" },
]}
workflows={["配置预警规则", "匹配业务来源数据", "生成预警记录", "确认处理并关闭"]}
/>
);
}

View File

@@ -0,0 +1,21 @@
import { BedDouble } from "lucide-react";
import { ModulePage } from "@/modules/shared/components/ModulePage";
export default function BedsPage(): React.ReactElement {
return (
<ModulePage
title="床位房间"
eyebrow="房间与床位状态"
description="维护楼栋、楼层、房间、床位编号和空闲、占用、维修状态流转。"
icon={BedDouble}
phase="一期"
metrics={[
{ label: "总床位", value: "154", detail: "护理区 96 张,康复区 58 张" },
{ label: "占用率", value: "82%", detail: "低于阈值时支持入住调度" },
{ label: "维修床位", value: "3", detail: "维修完成后恢复可用" },
]}
workflows={["维护楼栋与房间", "新增床位编号", "入住时占用床位", "退住或维修后释放床位"]}
/>
);
}

View File

@@ -0,0 +1,21 @@
import { ClipboardCheck } from "lucide-react";
import { ModulePage } from "@/modules/shared/components/ModulePage";
export default function CarePage(): React.ReactElement {
return (
<ModulePage
title="护理服务"
eyebrow="护理计划、任务与巡检"
description="按护理等级制定计划,生成任务并支持接收、执行、完成和逾期标记。"
icon={ClipboardCheck}
phase="一期"
metrics={[
{ label: "今日任务", value: "184", detail: "已完成 142 项" },
{ label: "逾期任务", value: "4", detail: "集中在午间巡检和复测任务" },
{ label: "巡检打卡", value: "91%", detail: "一楼护理区已全部完成" },
]}
workflows={["配置护理等级", "创建护理计划", "派发护理任务", "巡检打卡并提交记录"]}
/>
);
}

View File

@@ -0,0 +1,5 @@
import { DashboardHome } from "@/modules/dashboard/components/DashboardHome";
export default function DashboardPage(): React.ReactElement {
return <DashboardHome />;
}

View File

@@ -0,0 +1,21 @@
import { Wrench } from "lucide-react";
import { ModulePage } from "@/modules/shared/components/ModulePage";
export default function DevicesPage(): React.ReactElement {
return (
<ModulePage
title="设备运维"
eyebrow="设备台账与维修工单"
description="维护设备编号、类型、位置、状态和责任人,支持故障上报与维修归档。"
icon={Wrench}
phase="一期"
metrics={[
{ label: "设备总数", value: "312", detail: "床旁呼叫、监测设备、公共设施" },
{ label: "故障设备", value: "2", detail: "均已生成维修工单" },
{ label: "待派单", value: "1", detail: "需维修人员确认" },
]}
workflows={["登记设备台账", "上报设备故障", "派发维修工单", "记录维修结果并恢复状态"]}
/>
);
}

View File

@@ -0,0 +1,21 @@
import { ShieldAlert } from "lucide-react";
import { ModulePage } from "@/modules/shared/components/ModulePage";
export default function EmergencyPage(): React.ReactElement {
return (
<ModulePage
title="安全应急"
eyebrow="紧急呼叫与事件处理"
description="支持老人或护理人员提交紧急呼叫,形成接警、派发、处理、完成、归档闭环。"
icon={ShieldAlert}
phase="一期"
metrics={[
{ label: "今日呼叫", value: "6", detail: "平均响应 2 分 18 秒" },
{ label: "处理中", value: "1", detail: "已通知护理主管与医护人员" },
{ label: "已归档", value: "21", detail: "本周事件处理率 100%" },
]}
workflows={["提交紧急呼叫", "生成应急事件", "派发责任人员", "记录处理结果并通知相关人员"]}
/>
);
}

View File

@@ -0,0 +1,21 @@
import { TabletSmartphone } from "lucide-react";
import { ModulePage } from "@/modules/shared/components/ModulePage";
export default function FamilyPage(): React.ReactElement {
return (
<ModulePage
title="家属服务"
eyebrow="家属端与探访反馈"
description="二期补齐家属查看老人动态、健康记录、护理记录、探访预约和服务反馈能力。"
icon={TabletSmartphone}
phase="二期预留"
metrics={[
{ label: "绑定家属", value: "214", detail: "一个老人可绑定多个授权家属" },
{ label: "探访预约", value: "12", detail: "待审核 4 条" },
{ label: "服务反馈", value: "7", detail: "2 条需要机构回复" },
]}
workflows={["绑定老人和家属账号", "查看授权范围内数据", "提交探访预约", "反馈处理与归档"]}
/>
);
}

View File

@@ -0,0 +1,21 @@
import { HeartPulse } from "lucide-react";
import { ModulePage } from "@/modules/shared/components/ModulePage";
export default function HealthPage(): React.ReactElement {
return (
<ModulePage
title="健康照护"
eyebrow="健康档案与生命体征"
description="记录体温、血压、心率、血氧、慢病信息和异常复核建议。"
icon={HeartPulse}
phase="一期"
metrics={[
{ label: "今日记录", value: "236", detail: "覆盖重点老人 100%" },
{ label: "异常体征", value: "9", detail: "血压异常 5 条,血氧异常 1 条" },
{ label: "待医护复核", value: "3", detail: "需在 30 分钟内处理" },
]}
workflows={["录入生命体征", "维护健康摘要", "识别异常指标", "形成处理建议并归档"]}
/>
);
}

View File

@@ -0,0 +1,21 @@
import { Bell } from "lucide-react";
import { ModulePage } from "@/modules/shared/components/ModulePage";
export default function NoticesPage(): React.ReactElement {
return (
<ModulePage
title="公告通知"
eyebrow="公告发布与阅读状态"
description="按角色和范围发布公告,区分草稿、发布、未读和已读状态。"
icon={Bell}
phase="一期"
metrics={[
{ label: "已发布公告", value: "18", detail: "本月面向全员 5 条" },
{ label: "未读消息", value: "64", detail: "护理人员未读占比最高" },
{ label: "草稿", value: "3", detail: "草稿不会对目标用户可见" },
]}
workflows={["创建公告草稿", "选择目标角色与范围", "发布通知", "跟踪阅读状态"]}
/>
);
}

5
app/(app)/app/page.tsx Normal file
View File

@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function AppIndexPage(): never {
redirect("/app/dashboard");
}

View File

@@ -0,0 +1,5 @@
import { AuthPanel } from "@/modules/auth/components/AuthPanel";
export default function LoginPage(): React.ReactElement {
return <AuthPanel mode="login" />;
}

View File

@@ -0,0 +1,5 @@
import { AuthPanel } from "@/modules/auth/components/AuthPanel";
export default function RegisterPage(): React.ReactElement {
return <AuthPanel mode="register" />;
}

View File

@@ -0,0 +1,5 @@
import { AuthPanel } from "@/modules/auth/components/AuthPanel";
export default function SetupPage(): React.ReactElement {
return <AuthPanel mode="setup" />;
}

5
app/icon.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="14" fill="#2f7d52"/>
<path d="M32 15c7 0 13 5 13 12 0 12-13 22-13 22S19 39 19 27c0-7 6-12 13-12z" fill="#f4fff8"/>
<path d="M32 21v22M22 32h20" stroke="#2f7d52" stroke-width="5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 310 B

20
app/layout.tsx Normal file
View File

@@ -0,0 +1,20 @@
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "TeaTea 康护智慧养老系统",
description: "AI赋能智慧养老系统后台管理端初步框架",
};
type RootLayoutProps = {
children: React.ReactNode;
};
export default function RootLayout({ children }: RootLayoutProps): React.ReactElement {
return (
<html lang="zh-CN">
<body>{children}</body>
</html>
);
}

5
app/page.tsx Normal file
View File

@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function HomePage(): never {
redirect("/login");
}

36
auth-after-clear.md Normal file
View File

@@ -0,0 +1,36 @@
- generic [active] [ref=f2e1] [box=0,0,2056,1020]:
- alert [ref=f2e3] [box=-1,-1,1,1]
- main [ref=f2e4] [box=0,0,2056,1020]:
- generic [ref=f2e5] [box=0,0,977,1020]:
- link "TeaTea 康护" [ref=f2e6] [cursor=pointer] [box=40,40,896,40]:
- /url: /login
- img [ref=f2e8] [box=50,50,20,20]
- text: TeaTea 康护
- generic [ref=f2e12] [box=40,453,448,139]:
- generic [ref=f2e13] [box=40,453,66,26]: First run
- heading "初始化机构" [level=1] [ref=f2e14] [box=40,499,448,48]
- paragraph [ref=f2e15] [box=40,567,448,24]: 简洁、稳定、面向机构照护。
- generic [ref=f2e16] [box=40,964,896,16]:
- generic [ref=f2e17] [box=40,964,291,16]: 档案
- generic [ref=f2e18] [box=343,964,291,16]: 护理
- generic [ref=f2e19] [box=645,964,291,16]: 预警
- generic [ref=f2e22] [box=1324,235,384,550]:
- generic [ref=f2e23] [box=1349,260,334,96]:
- img [ref=f2e25] [box=1361,272,20,20]
- heading "初始化机构" [level=2] [ref=f2e29] [box=1349,324,334,32]
- generic [ref=f2e30] [box=1349,380,334,380]:
- generic [ref=f2e31] [box=1349,380,334,68]:
- text: 机构名称
- textbox "机构名称" [ref=f2e32] [box=1349,404,334,44]
- generic [ref=f2e33] [box=1349,464,334,68]:
- text: 姓名
- textbox "姓名" [ref=f2e34] [box=1349,488,334,44]
- generic [ref=f2e35] [box=1349,548,334,68]:
- text: 邮箱
- textbox "邮箱" [ref=f2e36] [box=1349,572,334,44]
- generic [ref=f2e37] [box=1349,632,334,68]:
- text: 密码
- textbox "密码" [ref=f2e38] [box=1349,656,334,44]
- button "完成初始化" [ref=f2e39] [box=1349,716,334,44]:
- text: 完成初始化
- img [ref=f2e40] [box=1547,730,16,16]

36
auth-setup-snapshot.md Normal file
View File

@@ -0,0 +1,36 @@
- generic [active] [ref=e1] [box=0,0,2056,1020]:
- main [ref=e2] [box=0,0,2056,1020]:
- generic [ref=e3] [box=0,0,977,1020]:
- link "TeaTea 康护" [ref=e4] [cursor=pointer] [box=40,40,896,40]:
- /url: /login
- img [ref=e6] [box=50,50,20,20]
- text: TeaTea 康护
- generic [ref=e10] [box=40,453,448,139]:
- generic [ref=e11] [box=40,453,66,26]: First run
- heading "初始化机构" [level=1] [ref=e12] [box=40,499,448,48]
- paragraph [ref=e13] [box=40,567,448,24]: 简洁、稳定、面向机构照护。
- generic [ref=e14] [box=40,964,896,16]:
- generic [ref=e15] [box=40,964,291,16]: 档案
- generic [ref=e16] [box=343,964,291,16]: 护理
- generic [ref=e17] [box=645,964,291,16]: 预警
- generic [ref=e20] [box=1324,235,384,550]:
- generic [ref=e21] [box=1349,260,334,96]:
- img [ref=e23] [box=1361,272,20,20]
- heading "初始化机构" [level=2] [ref=e27] [box=1349,324,334,32]
- generic [ref=e28] [box=1349,380,334,380]:
- generic [ref=e29] [box=1349,380,334,68]:
- text: 机构名称
- textbox "机构名称" [ref=e30] [box=1349,404,334,44]
- generic [ref=e31] [box=1349,464,334,68]:
- text: 姓名
- textbox "姓名" [ref=e32] [box=1349,488,334,44]
- generic [ref=e33] [box=1349,548,334,68]:
- text: 邮箱
- textbox "邮箱" [ref=e34] [box=1349,572,334,44]
- generic [ref=e35] [box=1349,632,334,68]:
- text: 密码
- textbox "密码" [ref=e36] [box=1349,656,334,44]
- button "完成初始化" [ref=e37] [box=1349,716,334,44]:
- text: 完成初始化
- img [ref=e38] [box=1547,730,16,16]
- alert [ref=e40] [box=-1,1019,1,1]

21
components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

31
components/ui/badge.tsx Normal file
View File

@@ -0,0 +1,31 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-1 text-xs font-medium transition-colors",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground",
secondary: "border-transparent bg-secondary text-secondary-foreground",
outline: "text-foreground",
success: "border-emerald-200 bg-emerald-50 text-emerald-700",
warning: "border-amber-200 bg-amber-50 text-amber-700",
danger: "border-red-200 bg-red-50 text-red-700",
},
},
defaultVariants: {
variant: "default",
},
},
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
export function Badge({ className, variant, ...props }: BadgeProps): React.ReactElement {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}

50
components/ui/button.tsx Normal file
View File

@@ -0,0 +1,50 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex min-h-11 shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline: "border bg-background hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
},
size: {
default: "h-11 px-4 py-2",
sm: "h-9 min-h-9 px-3",
lg: "h-12 px-6",
icon: "h-11 w-11",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
export function Button({
className,
variant,
size,
asChild = false,
...props
}: ButtonProps): React.ReactElement {
const Comp = asChild ? Slot : "button";
return <Comp className={cn(buttonVariants({ variant, size, className }))} {...props} />;
}
export { buttonVariants };

43
components/ui/card.tsx Normal file
View File

@@ -0,0 +1,43 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export function Card({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>): React.ReactElement {
return (
<div
className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
{...props}
/>
);
}
export function CardHeader({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>): React.ReactElement {
return <div className={cn("flex flex-col gap-1.5 p-5", className)} {...props} />;
}
export function CardTitle({
className,
...props
}: React.HTMLAttributes<HTMLHeadingElement>): React.ReactElement {
return <h3 className={cn("text-base font-semibold leading-none", className)} {...props} />;
}
export function CardDescription({
className,
...props
}: React.HTMLAttributes<HTMLParagraphElement>): React.ReactElement {
return <p className={cn("text-sm text-muted-foreground", className)} {...props} />;
}
export function CardContent({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>): React.ReactElement {
return <div className={cn("p-5 pt-0", className)} {...props} />;
}

18
components/ui/input.tsx Normal file
View File

@@ -0,0 +1,18 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
export function Input({ className, type, ...props }: InputProps): React.ReactElement {
return (
<input
type={type}
className={cn(
"flex h-11 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
);
}

14
dashboard-after-setup.md Normal file
View File

@@ -0,0 +1,14 @@
- generic [active] [ref=f2e1] [box=0,0,2056,1020]:
- alert [ref=f2e3] [box=-1,-1,1,1]
- generic [ref=f2e42] [box=0,0,2056,1020]:
- complementary [ref=f2e43] [box=0,0,256,1020]:
- generic [ref=f2e44] [box=0,0,255,1020]:
- link "TeaTea 康护" [ref=f2e45] [cursor=pointer] [box=0,0,255,64]:
- /url: /app/dashboard
- navigation "主导航" [ref=f2e53] [box=0,64,255,956]
- generic [ref=f2e127] [box=256,0,1800,1020]:
- banner [ref=f2e128] [box=256,0,1800,56]:
- paragraph [ref=f2e131] [box=288,18,42,20]: 工作台
- generic [ref=f2e132] [box=1864,10,160,36]
- main [ref=f2e142] [box=256,56,1800,964]:
- generic [ref=f2e143] [box=516,76,1280,728]

View File

@@ -0,0 +1,8 @@
- generic [active] [ref=f2e1] [box=0,0,390,1886]:
- alert [ref=f2e3] [box=-1,-1,1,1]
- generic [ref=f2e419] [box=0,0,390,1886]:
- banner [ref=f2e420] [box=0,0,390,56]:
- generic [ref=f2e421] [box=16,8,94,40]
- generic [ref=f2e424] [box=250,10,124,36]
- main [ref=f2e434] [box=0,56,390,1830]:
- generic [ref=f2e435] [box=16,76,358,1790]

42
eslint.config.mjs Normal file
View File

@@ -0,0 +1,42 @@
import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";
import tseslint from "typescript-eslint";
const compat = new FlatCompat({
baseDirectory: import.meta.dirname,
recommendedConfig: js.configs.recommended,
});
const eslintConfig = [
{
ignores: [
".next/**",
"node_modules/**",
"out/**",
"next-env.d.ts",
".agents/**",
".claude/**",
".codex/**",
".cursor/**",
".pi/**",
".playwright-mcp/**",
".serena/**",
".spec-workflow/**",
".trae/**",
".trellis/**",
"output/**",
"tmp/**",
"teatea-dashboard-*.png",
"teatea-mobile-snapshot.md",
],
},
...compat.extends("next/core-web-vitals"),
...tseslint.configs.recommended,
{
rules: {
"@typescript-eslint/no-explicit-any": "error",
},
},
];
export default eslintConfig;

6
lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}

View File

@@ -0,0 +1,166 @@
import {
Activity,
AlertTriangle,
BedDouble,
CheckCircle2,
Clock3,
HeartPulse,
ShieldAlert,
Users,
} from "lucide-react";
import type { LucideIcon } 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";
type Metric = {
label: string;
value: string;
trend: string;
icon: LucideIcon;
};
const metrics: Metric[] = [
{ label: "在院", value: "126", trend: "+6 本周", icon: Users },
{ label: "床位", value: "82%", trend: "28 空闲", icon: BedDouble },
{ label: "任务", value: "184", trend: "142 完成", icon: CheckCircle2 },
{ label: "异常", value: "9", trend: "3 待复核", icon: HeartPulse },
];
const taskRows = [
{ name: "晨间生命体征记录", owner: "护理一组", status: "进行中", due: "09:30" },
{ name: "二楼失能老人巡检", owner: "护理二组", status: "待执行", due: "10:00" },
{ name: "高血压老人复测", owner: "医护组", status: "需复核", due: "10:30" },
{ name: "晚间护理计划生成", owner: "护理主管", status: "待派发", due: "16:00" },
];
const alerts = [
{ label: "任务逾期", count: "4", tone: "warning" },
{ label: "设备故障", count: "2", tone: "danger" },
{ label: "应急处理中", count: "1", tone: "danger" },
{ label: "未读通知", count: "18", tone: "success" },
] as const;
export function DashboardHome(): React.ReactElement {
return (
<div className="mx-auto flex w-full max-w-7xl flex-col gap-6">
<section className="grid gap-5 border-b pb-5 lg:grid-cols-[1.15fr_0.85fr]">
<div className="flex flex-col justify-end gap-4">
<Badge variant="success"></Badge>
<div>
<h1 className="text-3xl font-semibold tracking-normal md:text-4xl"></h1>
<p className="mt-2 text-sm text-muted-foreground"></p>
</div>
<div className="flex flex-wrap gap-2">
<Button>
<Users className="size-4" aria-hidden="true" />
</Button>
<Button variant="outline">
<ShieldAlert className="size-4" aria-hidden="true" />
</Button>
</div>
</div>
<div className="grid gap-3 rounded-lg border bg-white/72 p-4 shadow-sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium"></p>
</div>
<Activity className="size-5 text-primary" aria-hidden="true" />
</div>
<div className="space-y-3">
{["档案", "床位", "健康", "护理", "应急"].map((item, index) => (
<div key={item} className="flex items-center gap-3">
<span className="w-9 text-xs font-medium text-muted-foreground">{item}</span>
<div className="h-2 flex-1 rounded-full bg-muted">
<div
className="h-2 rounded-full bg-primary"
style={{ width: `${88 - index * 9}%` }}
/>
</div>
<span className="w-14 text-right text-xs text-muted-foreground">
{88 - index * 9}%
</span>
</div>
))}
</div>
</div>
</section>
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{metrics.map((metric) => (
<Card key={metric.label}>
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardDescription>{metric.label}</CardDescription>
<metric.icon className="size-4 text-primary" aria-hidden="true" />
</CardHeader>
<CardContent>
<CardTitle className="text-3xl">{metric.value}</CardTitle>
<p className="mt-2 text-xs text-muted-foreground">{metric.trend}</p>
</CardContent>
</Card>
))}
</section>
<section className="grid gap-4 xl:grid-cols-[1.25fr_0.75fr]">
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-hidden rounded-md border">
<table className="w-full text-sm">
<thead className="bg-secondary text-left text-xs text-muted-foreground">
<tr>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 text-right font-medium"></th>
</tr>
</thead>
<tbody className="divide-y bg-card">
{taskRows.map((task) => (
<tr key={task.name}>
<td className="px-4 py-3 font-medium">{task.name}</td>
<td className="px-4 py-3 text-muted-foreground">{task.owner}</td>
<td className="px-4 py-3">
<Badge variant={task.status === "需复核" ? "warning" : "secondary"}>
{task.status}
</Badge>
</td>
<td className="px-4 py-3 text-right text-muted-foreground">{task.due}</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{alerts.map((alert) => (
<div key={alert.label} className="flex items-center justify-between rounded-md border p-3">
<div className="flex items-center gap-3">
{alert.tone === "danger" ? (
<AlertTriangle className="size-4 text-red-600" aria-hidden="true" />
) : (
<Clock3 className="size-4 text-amber-600" aria-hidden="true" />
)}
<span className="text-sm font-medium">{alert.label}</span>
</div>
<Badge variant={alert.tone}>{alert.count}</Badge>
</div>
))}
</CardContent>
</Card>
</section>
</div>
);
}

View File

@@ -0,0 +1 @@
export { DashboardHome } from "./DashboardHome";

View File

@@ -0,0 +1,89 @@
import type { LucideIcon } from "lucide-react";
import { ArrowRight, CheckCircle2 } 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";
type ModuleMetric = {
label: string;
value: string;
detail: string;
};
type ModulePageProps = {
title: string;
eyebrow: string;
description: string;
icon: LucideIcon;
phase: "一期" | "二期预留" | "三期预留";
metrics: ModuleMetric[];
workflows: string[];
};
export function ModulePage({
title,
eyebrow,
icon: Icon,
phase,
metrics,
workflows,
}: ModulePageProps): React.ReactElement {
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>
</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>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<ol className="grid gap-3 md:grid-cols-2">
{workflows.map((workflow) => (
<li key={workflow} className="flex items-start gap-3 rounded-md border bg-secondary/30 p-3">
<CheckCircle2 className="mt-0.5 size-4 shrink-0 text-primary" aria-hidden="true" />
<span className="text-sm">{workflow}</span>
</li>
))}
</ol>
</CardContent>
</Card>
</section>
</div>
);
}

6
next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

7
next.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
outputFileTracingRoot: process.cwd(),
};
export default nextConfig;

File diff suppressed because it is too large Load Diff

35
package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "teatea-pension",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint app components lib modules next.config.ts eslint.config.mjs",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@radix-ui/react-slot": "^1.2.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.468.0",
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwind-merge": "^3.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.0.0",
"@eslint/js": "^9.0.0",
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"eslint": "^9.0.0",
"eslint-config-next": "^15.0.0",
"tailwindcss": "^4.0.0",
"typescript-eslint": "^8.0.0",
"typescript": "^5.0.0"
}
}

3893
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
packages:
- "."

7
postcss.config.mjs Normal file
View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

BIN
teatea-dashboard-mobile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

10
teatea-mobile-snapshot.md Normal file
View File

@@ -0,0 +1,10 @@
- generic [active] [ref=e1] [box=0,0,390,2174]:
- button "Open Next.js Dev Tools" [ref=e7] [cursor=pointer] [box=22,790,32,32]:
- img [ref=e8] [box=22,786,40,40]
- generic [ref=e119] [box=0,0,390,2174]:
- banner [ref=e120] [box=0,0,390,64]:
- generic [ref=e121] [box=16,12,146,40]
- generic [ref=e125] [box=162,14,212,36]
- main [ref=e133] [box=0,64,390,2110]:
- generic [ref=e134] [box=16,88,358,2062]
- alert [ref=e297] [box=-1,2173,1,1]

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

32
tsconfig.json Normal file
View File

@@ -0,0 +1,32 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"],
"@/components/*": ["./components/*"],
"@/lib/*": ["./lib/*"],
"@/modules/*": ["./modules/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}