feat: 完成 P1 全部功能 + 修复 proxy 导出 + 切换 MySQL 端口至 14013

## P1 功能(20 项)
- 站内消息系统、家长仪表盘、学生考勤管理
- Excel 导入导出、用户批量导入、成绩导出
- 排课规则+自动排课+课表调整
- 成绩趋势+对比分析、密码安全策略、速率限制
- 数据变更日志、文件预览+存储策略、全文检索
- 依赖审计集成 CI、数据库定时备份、E2E 测试完善
- 通知偏好管理

## 基础设施修复
- src/proxy.ts: 将 middleware 导出重命名为 proxy(Next.js 16 要求)
- .env: MySQL 端口从 13002 切换至 14013
- scripts/create-db.ts: 新增数据库初始化脚本

## 架构文档同步
- 004_architecture_impact_map.md 和 005_architecture_data.json
  完整记录所有新增表、模块、路由、权限、依赖关系
This commit is contained in:
SpecialX
2026-06-17 13:44:37 +08:00
parent 125f7ec54c
commit 3b6272c99d
195 changed files with 27274 additions and 416 deletions

View File

@@ -0,0 +1,79 @@
"use client"
import Link from "next/link"
import { useMemo } from "react"
import { Badge } from "@/shared/components/ui/badge"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { formatDate } from "@/shared/lib/utils"
import type { Announcement } from "../types"
const STATUS_LABEL: Record<Announcement["status"], string> = {
draft: "Draft",
published: "Published",
archived: "Archived",
}
const STATUS_VARIANT: Record<
Announcement["status"],
"default" | "secondary" | "outline"
> = {
draft: "secondary",
published: "default",
archived: "outline",
}
const TYPE_LABEL: Record<Announcement["type"], string> = {
school: "School",
grade: "Grade",
class: "Class",
}
export function AnnouncementCard({
announcement,
href,
}: {
announcement: Announcement
href?: string
}) {
const card = useMemo(
() => (
<Card className="h-full transition-colors hover:bg-accent/50">
<CardHeader className="flex flex-row items-start justify-between gap-2 space-y-0">
<CardTitle className="line-clamp-2 text-base">{announcement.title}</CardTitle>
<Badge variant={STATUS_VARIANT[announcement.status]} className="shrink-0">
{STATUS_LABEL[announcement.status]}
</Badge>
</CardHeader>
<CardContent className="space-y-2">
<p className="line-clamp-3 text-sm text-muted-foreground whitespace-pre-wrap">
{announcement.content}
</p>
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<Badge variant="outline" className="capitalize">
{TYPE_LABEL[announcement.type]}
</Badge>
<span>
{announcement.publishedAt
? `Published ${formatDate(announcement.publishedAt)}`
: `Updated ${formatDate(announcement.updatedAt)}`}
</span>
{announcement.authorName ? (
<span className="ml-auto">by {announcement.authorName}</span>
) : null}
</div>
</CardContent>
</Card>
),
[announcement]
)
if (href) {
return (
<Link href={href} className="block h-full">
{card}
</Link>
)
}
return card
}