feat: complete ops dashboard AI board
This commit is contained in:
@@ -1,23 +1,31 @@
|
||||
import Link from "next/link";
|
||||
import { Activity, AlertTriangle, BedDouble, CheckCircle2, HeartPulse, ListChecks, Users } from "lucide-react";
|
||||
import { Activity, AlertTriangle, BedDouble, BrainCircuit, CheckCircle2, HeartPulse, ListChecks, Megaphone, ShieldAlert, TabletSmartphone, Users } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Table } from "@/components/ui/table";
|
||||
import type { ElderAiAnalysisBoardItem, ElderAiRiskLevel } from "@/modules/ai/types";
|
||||
import type { AlertTriggerStatus } from "@/modules/alerts/types";
|
||||
import { ALERT_TRIGGER_STATUS_LABELS } from "@/modules/alerts/types";
|
||||
import type { AdmissionStatus, BedStatus, IncidentSeverity } from "@/modules/core/types";
|
||||
import type { CareTaskPriority, CareTaskStatus } from "@/modules/care/types";
|
||||
import { CARE_TASK_PRIORITY_LABELS, CARE_TASK_STATUS_LABELS } from "@/modules/care/types";
|
||||
import { BedOccupancyChart, type BedOccupancyDatum } from "@/modules/dashboard/components/BedOccupancyChart";
|
||||
import type { MaintenanceTicketPriority, MaintenanceTicketStatus } from "@/modules/devices/types";
|
||||
import { MAINTENANCE_TICKET_PRIORITY_LABELS, MAINTENANCE_TICKET_STATUS_LABELS } from "@/modules/devices/types";
|
||||
import { INCIDENT_SEVERITY_LABELS, INCIDENT_STATUS_LABELS } from "@/modules/emergency/types";
|
||||
import type { FamilyFeedbackStatus, FamilyVisitStatus } from "@/modules/family/types";
|
||||
import { FAMILY_FEEDBACK_STATUS_LABELS, FAMILY_VISIT_STATUS_LABELS } from "@/modules/family/types";
|
||||
import type { HealthReviewSeverity, HealthReviewStatus } from "@/modules/health/types";
|
||||
import { HEALTH_REVIEW_SEVERITY_LABELS, HEALTH_REVIEW_STATUS_LABELS } from "@/modules/health/types";
|
||||
|
||||
import type { NoticeStatus } from "@/modules/notices/types";
|
||||
import { NOTICE_STATUS_LABELS } from "@/modules/notices/types";
|
||||
export type DashboardMetric = {
|
||||
label: string;
|
||||
value: string;
|
||||
trend: string;
|
||||
icon: "admissions" | "beds" | "care" | "health" | "incidents" | "users";
|
||||
icon: "admissions" | "ai" | "alerts" | "beds" | "care" | "devices" | "family" | "health" | "incidents" | "notices" | "users";
|
||||
};
|
||||
|
||||
export type DashboardAdmission = {
|
||||
@@ -56,26 +64,82 @@ export type DashboardHealthReview = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type DashboardDeviceTicket = {
|
||||
id: string;
|
||||
deviceName: string;
|
||||
priority: MaintenanceTicketPriority;
|
||||
status: MaintenanceTicketStatus;
|
||||
title: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type DashboardNotice = {
|
||||
id: string;
|
||||
audience: string;
|
||||
status: NoticeStatus;
|
||||
title: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type DashboardAlertTrigger = {
|
||||
id: string;
|
||||
elderName: string;
|
||||
status: AlertTriggerStatus;
|
||||
title: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type DashboardFamilyVisit = {
|
||||
id: string;
|
||||
contactName: string;
|
||||
elderName: string;
|
||||
scheduledAt: string;
|
||||
status: FamilyVisitStatus;
|
||||
};
|
||||
|
||||
export type DashboardFamilyFeedback = {
|
||||
id: string;
|
||||
elderName: string;
|
||||
status: FamilyFeedbackStatus;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
type DashboardHomeProps = {
|
||||
alertTriggers?: DashboardAlertTrigger[];
|
||||
alertsHref: string;
|
||||
aiBoardItems?: ElderAiAnalysisBoardItem[];
|
||||
bedsHref: string;
|
||||
careHref: string;
|
||||
careTasks: DashboardCareTask[];
|
||||
careTasks?: DashboardCareTask[];
|
||||
deviceTickets?: DashboardDeviceTicket[];
|
||||
devicesHref: string;
|
||||
eldersHref: string;
|
||||
emergencyHref: string;
|
||||
emergencyIncidents: DashboardIncident[];
|
||||
emergencyIncidents?: DashboardIncident[];
|
||||
familyFeedback?: DashboardFamilyFeedback[];
|
||||
familyHref: string;
|
||||
familyVisits?: DashboardFamilyVisit[];
|
||||
healthHref: string;
|
||||
healthReviews: DashboardHealthReview[];
|
||||
healthReviews?: DashboardHealthReview[];
|
||||
metrics: DashboardMetric[];
|
||||
occupancyData: BedOccupancyDatum[];
|
||||
recentAdmissions: DashboardAdmission[];
|
||||
incidents: DashboardIncident[];
|
||||
notices?: DashboardNotice[];
|
||||
noticesHref: string;
|
||||
occupancyData?: BedOccupancyDatum[];
|
||||
recentAdmissions?: DashboardAdmission[];
|
||||
incidents?: DashboardIncident[];
|
||||
};
|
||||
|
||||
const metricIcons: Record<DashboardMetric["icon"], LucideIcon> = {
|
||||
admissions: CheckCircle2,
|
||||
ai: BrainCircuit,
|
||||
alerts: ShieldAlert,
|
||||
beds: BedDouble,
|
||||
care: ListChecks,
|
||||
devices: TabletSmartphone,
|
||||
family: Users,
|
||||
health: HeartPulse,
|
||||
incidents: HeartPulse,
|
||||
notices: Megaphone,
|
||||
users: Users,
|
||||
};
|
||||
|
||||
@@ -107,19 +171,72 @@ function incidentVariant(severity: IncidentSeverity): "secondary" | "warning" |
|
||||
}
|
||||
}
|
||||
|
||||
const analysisRiskLabels: Record<ElderAiRiskLevel, string> = {
|
||||
critical: "高危",
|
||||
high: "高风险",
|
||||
low: "低风险",
|
||||
medium: "中风险",
|
||||
unknown: "未知",
|
||||
};
|
||||
|
||||
function analysisRiskVariant(risk: ElderAiRiskLevel): "danger" | "secondary" | "success" | "warning" {
|
||||
switch (risk) {
|
||||
case "critical":
|
||||
return "danger";
|
||||
case "high":
|
||||
case "medium":
|
||||
return "warning";
|
||||
case "low":
|
||||
return "success";
|
||||
case "unknown":
|
||||
return "secondary";
|
||||
}
|
||||
}
|
||||
|
||||
export function DashboardHome({
|
||||
alertTriggers,
|
||||
alertsHref,
|
||||
aiBoardItems,
|
||||
bedsHref,
|
||||
careHref,
|
||||
careTasks,
|
||||
deviceTickets,
|
||||
devicesHref,
|
||||
eldersHref,
|
||||
emergencyHref,
|
||||
emergencyIncidents,
|
||||
familyFeedback,
|
||||
familyHref,
|
||||
familyVisits,
|
||||
healthHref,
|
||||
healthReviews,
|
||||
metrics,
|
||||
notices,
|
||||
noticesHref,
|
||||
occupancyData,
|
||||
recentAdmissions,
|
||||
incidents,
|
||||
}: DashboardHomeProps): React.ReactElement {
|
||||
const familyQueueItems: QueueItem[] = [
|
||||
...(familyVisits ?? []).map((visit) => ({
|
||||
id: `visit-${visit.id}`,
|
||||
title: `${visit.elderName} 探访`,
|
||||
detail: `${visit.contactName} / ${formatDateTime(visit.scheduledAt)}`,
|
||||
badge: FAMILY_VISIT_STATUS_LABELS[visit.status],
|
||||
variant: visit.status === "requested" ? "warning" as const : "secondary" as const,
|
||||
meta: "探访",
|
||||
})),
|
||||
...(familyFeedback ?? []).map((feedback) => ({
|
||||
id: `feedback-${feedback.id}`,
|
||||
title: `${feedback.elderName} 家属反馈`,
|
||||
detail: formatDateTime(feedback.updatedAt),
|
||||
badge: FAMILY_FEEDBACK_STATUS_LABELS[feedback.status],
|
||||
variant: feedback.status === "open" || feedback.status === "in_progress" ? "warning" as const : "secondary" as const,
|
||||
meta: "反馈",
|
||||
})),
|
||||
].slice(0, 6);
|
||||
const hasCoreQueues = careTasks !== undefined || healthReviews !== undefined || emergencyIncidents !== undefined;
|
||||
const hasCollaborationQueues = deviceTickets !== undefined || notices !== undefined || alertTriggers !== undefined || familyVisits !== undefined || familyFeedback !== undefined;
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-6">
|
||||
<section className="flex flex-col gap-4 border-b pb-5 lg:flex-row lg:items-end lg:justify-between">
|
||||
@@ -148,132 +265,247 @@ export function DashboardHome({
|
||||
})}
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 xl:grid-cols-3">
|
||||
<QueueCard
|
||||
emptyText="暂无待处理护理任务"
|
||||
href={careHref}
|
||||
items={careTasks.map((task) => ({
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
detail: `${task.elderName || "公共区域"} / ${task.assigneeLabel || "未分配"} / ${formatDateTime(task.scheduledAt)}`,
|
||||
badge: CARE_TASK_STATUS_LABELS[task.status],
|
||||
variant: task.priority === "urgent" ? "danger" : task.priority === "high" ? "warning" : "secondary",
|
||||
meta: CARE_TASK_PRIORITY_LABELS[task.priority],
|
||||
}))}
|
||||
title="护理待办"
|
||||
/>
|
||||
<QueueCard
|
||||
emptyText="暂无待复核健康异常"
|
||||
href={healthHref}
|
||||
items={healthReviews.map((review) => ({
|
||||
id: review.id,
|
||||
title: review.title,
|
||||
detail: review.elderName,
|
||||
badge: HEALTH_REVIEW_STATUS_LABELS[review.status],
|
||||
variant: review.severity === "critical" ? "danger" : "warning",
|
||||
meta: HEALTH_REVIEW_SEVERITY_LABELS[review.severity],
|
||||
}))}
|
||||
title="健康复核"
|
||||
/>
|
||||
<QueueCard
|
||||
emptyText="暂无待处理应急事件"
|
||||
href={emergencyHref}
|
||||
items={emergencyIncidents.map((incident) => ({
|
||||
id: incident.id,
|
||||
title: incident.title,
|
||||
detail: `${incident.source} / ${formatDateTime(incident.createdAt)}`,
|
||||
badge: INCIDENT_STATUS_LABELS[incident.status as keyof typeof INCIDENT_STATUS_LABELS],
|
||||
variant: incidentVariant(incident.severity),
|
||||
meta: INCIDENT_SEVERITY_LABELS[incident.severity],
|
||||
}))}
|
||||
title="安全应急"
|
||||
/>
|
||||
</section>
|
||||
{hasCoreQueues ? (
|
||||
<section className="grid gap-4 xl:grid-cols-3">
|
||||
{careTasks !== undefined ? (
|
||||
<QueueCard
|
||||
emptyText="暂无待处理护理任务"
|
||||
href={careHref}
|
||||
items={careTasks.map((task) => ({
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
detail: `${task.elderName || "公共区域"} / ${task.assigneeLabel || "未分配"} / ${formatDateTime(task.scheduledAt)}`,
|
||||
badge: CARE_TASK_STATUS_LABELS[task.status],
|
||||
variant: task.priority === "urgent" ? "danger" : task.priority === "high" ? "warning" : "secondary",
|
||||
meta: CARE_TASK_PRIORITY_LABELS[task.priority],
|
||||
}))}
|
||||
title="护理待办"
|
||||
/>
|
||||
) : null}
|
||||
{healthReviews !== undefined ? (
|
||||
<QueueCard
|
||||
emptyText="暂无待复核健康异常"
|
||||
href={healthHref}
|
||||
items={healthReviews.map((review) => ({
|
||||
id: review.id,
|
||||
title: review.title,
|
||||
detail: review.elderName,
|
||||
badge: HEALTH_REVIEW_STATUS_LABELS[review.status],
|
||||
variant: review.severity === "critical" ? "danger" : "warning",
|
||||
meta: HEALTH_REVIEW_SEVERITY_LABELS[review.severity],
|
||||
}))}
|
||||
title="健康复核"
|
||||
/>
|
||||
) : null}
|
||||
{emergencyIncidents !== undefined ? (
|
||||
<QueueCard
|
||||
emptyText="暂无待处理应急事件"
|
||||
href={emergencyHref}
|
||||
items={emergencyIncidents.map((incident) => ({
|
||||
id: incident.id,
|
||||
title: incident.title,
|
||||
detail: `${incident.source} / ${formatDateTime(incident.createdAt)}`,
|
||||
badge: INCIDENT_STATUS_LABELS[incident.status as keyof typeof INCIDENT_STATUS_LABELS],
|
||||
variant: incidentVariant(incident.severity),
|
||||
meta: INCIDENT_SEVERITY_LABELS[incident.severity],
|
||||
}))}
|
||||
title="安全应急"
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="grid gap-4 xl:grid-cols-[0.95fr_1.05fr]">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Activity className="size-5 text-primary" aria-hidden="true" />
|
||||
床位状态分布
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{occupancyData.map((item) => `${bedStatusLabels[item.label as BedStatus] ?? item.label} ${item.count}`).join(" / ")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Link className="block" href={bedsHref}>
|
||||
<BedOccupancyChart data={occupancyData} />
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{hasCollaborationQueues ? (
|
||||
<section className="grid gap-4 xl:grid-cols-4">
|
||||
{deviceTickets !== undefined ? (
|
||||
<QueueCard
|
||||
emptyText="暂无待处理维修工单"
|
||||
href={devicesHref}
|
||||
items={deviceTickets.map((ticket) => ({
|
||||
id: ticket.id,
|
||||
title: ticket.title,
|
||||
detail: `${ticket.deviceName} / ${formatDateTime(ticket.updatedAt)}`,
|
||||
badge: MAINTENANCE_TICKET_STATUS_LABELS[ticket.status],
|
||||
variant: ticket.priority === "urgent" ? "danger" : ticket.priority === "high" ? "warning" : "secondary",
|
||||
meta: MAINTENANCE_TICKET_PRIORITY_LABELS[ticket.priority],
|
||||
}))}
|
||||
title="设备工单"
|
||||
/>
|
||||
) : null}
|
||||
{notices !== undefined ? (
|
||||
<QueueCard
|
||||
emptyText="暂无公告"
|
||||
href={noticesHref}
|
||||
items={notices.map((notice) => ({
|
||||
id: notice.id,
|
||||
title: notice.title,
|
||||
detail: `${notice.audience} / ${formatDateTime(notice.updatedAt)}`,
|
||||
badge: NOTICE_STATUS_LABELS[notice.status],
|
||||
variant: notice.status === "published" ? "success" : "secondary",
|
||||
meta: "公告",
|
||||
}))}
|
||||
title="公告通知"
|
||||
/>
|
||||
) : null}
|
||||
{alertTriggers !== undefined ? (
|
||||
<QueueCard
|
||||
emptyText="暂无预警记录"
|
||||
href={alertsHref}
|
||||
items={alertTriggers.map((trigger) => ({
|
||||
id: trigger.id,
|
||||
title: trigger.title,
|
||||
detail: `${trigger.elderName || "公共区域"} / ${formatDateTime(trigger.updatedAt)}`,
|
||||
badge: ALERT_TRIGGER_STATUS_LABELS[trigger.status],
|
||||
variant: trigger.status === "open" ? "danger" : trigger.status === "acknowledged" ? "warning" : "secondary",
|
||||
meta: "预警",
|
||||
}))}
|
||||
title="规则预警"
|
||||
/>
|
||||
) : null}
|
||||
{familyVisits !== undefined || familyFeedback !== undefined ? (
|
||||
<QueueCard
|
||||
emptyText="暂无家属协同事项"
|
||||
href={familyHref}
|
||||
items={familyQueueItems}
|
||||
title="家属服务"
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>最近入住记录</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto rounded-md border">
|
||||
<Table className="w-full min-w-[620px] text-sm">
|
||||
<Table.Header className="bg-secondary text-left text-xs text-muted-foreground">
|
||||
<Table.Row>
|
||||
<Table.Head className="px-4 py-3 font-medium">老人</Table.Head>
|
||||
<Table.Head className="px-4 py-3 font-medium">床位</Table.Head>
|
||||
<Table.Head className="px-4 py-3 font-medium">状态</Table.Head>
|
||||
<Table.Head className="px-4 py-3 font-medium">时间</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body className="divide-y bg-card">
|
||||
{recentAdmissions.map((admission) => (
|
||||
<Table.Row key={admission.id}>
|
||||
<Table.Cell className="px-4 py-3 font-medium">{admission.elderName}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{admission.bedLabel}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<Badge variant={admission.status === "active" ? "success" : "secondary"}>
|
||||
{admissionStatusLabels[admission.status]}
|
||||
</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">
|
||||
{formatDateTime(admission.dischargedAt ?? admission.admittedAt)}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
{recentAdmissions.length === 0 ? (
|
||||
<Table.Row>
|
||||
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={4}>
|
||||
暂无入住记录
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
) : null}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
{occupancyData !== undefined || recentAdmissions !== undefined ? (
|
||||
<section className="grid gap-4 xl:grid-cols-[0.95fr_1.05fr]">
|
||||
{occupancyData !== undefined ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Activity className="size-5 text-primary" aria-hidden="true" />
|
||||
床位状态分布
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{occupancyData.map((item) => `${bedStatusLabels[item.label as BedStatus] ?? item.label} ${item.count}`).join(" / ")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Link className="block" href={bedsHref}>
|
||||
<BedOccupancyChart data={occupancyData} />
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>未关闭故障</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{incidents.map((incident) => (
|
||||
<div key={incident.id} className="flex items-start justify-between gap-3 rounded-md border p-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="size-4 shrink-0 text-amber-600" aria-hidden="true" />
|
||||
<p className="truncate text-sm font-medium">{incident.title}</p>
|
||||
{recentAdmissions !== undefined ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>最近入住记录</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto rounded-md border">
|
||||
<Table className="w-full min-w-[620px] text-sm">
|
||||
<Table.Header className="bg-secondary text-left text-xs text-muted-foreground">
|
||||
<Table.Row>
|
||||
<Table.Head className="px-4 py-3 font-medium">老人</Table.Head>
|
||||
<Table.Head className="px-4 py-3 font-medium">床位</Table.Head>
|
||||
<Table.Head className="px-4 py-3 font-medium">状态</Table.Head>
|
||||
<Table.Head className="px-4 py-3 font-medium">时间</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body className="divide-y bg-card">
|
||||
{recentAdmissions.map((admission) => (
|
||||
<Table.Row key={admission.id}>
|
||||
<Table.Cell className="px-4 py-3 font-medium">{admission.elderName}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{admission.bedLabel}</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3">
|
||||
<Badge variant={admission.status === "active" ? "success" : "secondary"}>
|
||||
{admissionStatusLabels[admission.status]}
|
||||
</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell className="px-4 py-3 text-muted-foreground">
|
||||
{formatDateTime(admission.dischargedAt ?? admission.admittedAt)}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
{recentAdmissions.length === 0 ? (
|
||||
<Table.Row>
|
||||
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={4}>
|
||||
暂无入住记录
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
) : null}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{incident.source} / {formatDateTime(incident.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={incidentVariant(incident.severity)}>{incident.status}</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{aiBoardItems !== undefined ? (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-start justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<BrainCircuit className="size-5 text-primary" aria-hidden="true" />
|
||||
智能分析
|
||||
</CardTitle>
|
||||
<CardDescription>来自已保存老人 AI 分析历史,按数据权限自动脱敏。</CardDescription>
|
||||
</div>
|
||||
))}
|
||||
{incidents.length === 0 ? <p className="text-sm text-muted-foreground">暂无未关闭故障</p> : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Link className="text-sm font-medium text-primary hover:underline" href={eldersHref}>
|
||||
老人档案
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{aiBoardItems.map((item) => (
|
||||
<Link className="rounded-md border p-3 hover:bg-secondary/60" href={eldersHref} key={item.id}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium">{item.elderName}</p>
|
||||
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
||||
{item.restricted ? "结果受限:缺少一个或多个数据范围权限" : item.result?.summary ?? item.errorReason ?? "暂无摘要"}
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
{item.dataScopes.join(" / ")} / {formatDateTime(item.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col items-end gap-2">
|
||||
<Badge variant={item.status === "failed" ? "danger" : "success"}>{item.status === "failed" ? "失败" : "已完成"}</Badge>
|
||||
{!item.restricted && item.result ? (
|
||||
<Badge variant={analysisRiskVariant(item.result.overallRiskLevel)}>{analysisRiskLabels[item.result.overallRiskLevel]}</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
{aiBoardItems.length === 0 ? <p className="text-sm text-muted-foreground">暂无智能分析记录</p> : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{incidents !== undefined ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>未关闭故障</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{incidents.map((incident) => (
|
||||
<div key={incident.id} className="flex items-start justify-between gap-3 rounded-md border p-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="size-4 shrink-0 text-amber-600" aria-hidden="true" />
|
||||
<p className="truncate text-sm font-medium">{incident.title}</p>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{incident.source} / {formatDateTime(incident.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={incidentVariant(incident.severity)}>{incident.status}</Badge>
|
||||
</div>
|
||||
))}
|
||||
{incidents.length === 0 ? <p className="text-sm text-muted-foreground">暂无未关闭故障</p> : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -284,7 +516,7 @@ type QueueItem = {
|
||||
detail: string;
|
||||
meta: string;
|
||||
title: string;
|
||||
variant: "danger" | "secondary" | "warning";
|
||||
variant: "danger" | "secondary" | "success" | "warning";
|
||||
};
|
||||
|
||||
function QueueCard({
|
||||
|
||||
Reference in New Issue
Block a user