Files
NextEdu/src/shared/components/ui/stat-card.tsx
SpecialX 978d9a8309
Some checks failed
Security / deep-security-scan (push) Failing after 20m5s
DR Drill / dr-drill (push) Failing after 1m31s
CI / scheduled-backup (push) Failing after 1m31s
CI / backup-verify (push) Has been skipped
CI / weekly-dr-drill (push) Failing after 0s
CI / build-deploy (push) Has been cancelled
CI / security-scan (push) Has been cancelled
feat: 新增备课模块并修复全模块 P0/P1/P2 缺陷
主要变更:

- 新增 lesson-preparation 模块: 备课编辑器、节点编辑、AI 建议、知识点选择、版本历史、作业发布

- 新增 shared 通用组件: charts/question-bank-filters/schedule-list/ui (chip-nav/filter-bar/page-header/stat-card/stat-item)

- 新增 student/admin 端 loading.tsx 与 error.tsx, 优化加载与错误态体验

- 新增 teacher/lesson-plans 页面 (列表/新建/编辑)

- 新增 drizzle 迁移 0002_tiny_lionheart 及 snapshot

- 新增 textbooks/schema.ts 与 exams/utils/normalize-structure.ts

- 修复 Tiptap v3 SSR hydration 崩溃 (rich-text-block immediatelyRender: false)

- 重构多模块 data-access/actions/组件, 修复权限校验与类型规范

- 同步架构文档 004/005 反映新增模块、导出、依赖关系

- 归档 bugs/* 测试报告与 e2e 测试脚本 (admin/parent/student/teacher web_test)
2026-06-22 01:06:16 +08:00

96 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as React from "react"
import Link from "next/link"
import { cn } from "@/shared/lib/utils"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { Skeleton } from "@/shared/components/ui/skeleton"
/**
* 统计卡片:用于仪表盘、洞察页等场景的单张统计卡片。
*
* 覆盖以下重复模式:
* - TeacherStats / StudentStatsGrid带图标 + 描述 + 跳转)
* - AdminDashboardView KpiCard带图标无描述
* - insights / diagnostic / student-summary 页面内联卡片(无图标,带描述)
*/
interface StatCardProps {
/** 卡片标题(统计项名称) */
title: string
/** 统计数值 */
value: string | number
/** 图标组件Lucide 图标),传入组件类型而非实例 */
icon?: React.ComponentType<{ className?: string }>
/** 数值下方描述文本 */
description?: string
/** 图标颜色类名(如 "text-amber-500" */
color?: string
/** 是否高亮amber 边框 + 背景) */
highlight?: boolean
/** 点击跳转链接,传入则包裹 Link */
href?: string
/** 加载态,显示骨架屏 */
isLoading?: boolean
/** 数值自定义类名(如 "text-red-500" */
valueClassName?: string
}
function StatCardSkeleton() {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<Skeleton className="h-4 w-[100px]" />
<Skeleton className="h-4 w-4 rounded-full" />
</CardHeader>
<CardContent>
<Skeleton className="mb-2 h-8 w-[60px]" />
<Skeleton className="h-3 w-[140px]" />
</CardContent>
</Card>
)
}
export function StatCard({
title,
value,
icon: Icon,
description,
color,
highlight = false,
href,
isLoading = false,
valueClassName,
}: StatCardProps) {
if (isLoading) {
return <StatCardSkeleton />
}
const card = (
<Card
className={cn(
highlight && "border-amber-200 bg-amber-50/50 dark:border-amber-900 dark:bg-amber-950/20"
)}
>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
{Icon ? <Icon className={cn("h-4 w-4 text-muted-foreground", color)} /> : null}
</CardHeader>
<CardContent>
<div className={cn("text-2xl font-bold", valueClassName)}>{value}</div>
{description ? (
<p className="text-xs text-muted-foreground">{description}</p>
) : null}
</CardContent>
</Card>
)
if (href) {
return (
<Link href={href} className="block transition-transform hover:-translate-y-1">
{card}
</Link>
)
}
return card
}