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:
108
src/modules/announcements/components/announcement-list.tsx
Normal file
108
src/modules/announcements/components/announcement-list.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Plus } from "lucide-react"
|
||||
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { EmptyState } from "@/shared/components/ui/empty-state"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/components/ui/select"
|
||||
import { Megaphone } from "lucide-react"
|
||||
|
||||
import { AnnouncementCard } from "./announcement-card"
|
||||
import type { Announcement, AnnouncementStatus } from "../types"
|
||||
|
||||
type Filter = "all" | AnnouncementStatus
|
||||
|
||||
const FILTER_OPTIONS: { value: Filter; label: string }[] = [
|
||||
{ value: "all", label: "All" },
|
||||
{ value: "published", label: "Published" },
|
||||
{ value: "draft", label: "Draft" },
|
||||
{ value: "archived", label: "Archived" },
|
||||
]
|
||||
|
||||
export function AnnouncementList({
|
||||
announcements,
|
||||
canManage,
|
||||
createHref,
|
||||
detailHrefBuilder,
|
||||
initialStatus,
|
||||
}: {
|
||||
announcements: Announcement[]
|
||||
canManage?: boolean
|
||||
createHref?: string
|
||||
detailHrefBuilder?: (id: string) => string
|
||||
initialStatus?: Filter
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const [filter, setFilter] = useState<Filter>(initialStatus ?? "all")
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (filter === "all") return announcements
|
||||
return announcements.filter((a) => a.status === filter)
|
||||
}, [announcements, filter])
|
||||
|
||||
const handleFilterChange = (value: string) => {
|
||||
setFilter(value as Filter)
|
||||
const params = new URLSearchParams()
|
||||
if (value !== "all") params.set("status", value)
|
||||
const qs = params.toString()
|
||||
router.replace(qs ? `?${qs}` : "?")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<Select value={filter} onValueChange={handleFilterChange}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Filter by status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FILTER_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{canManage && createHref ? (
|
||||
<Button asChild>
|
||||
<a href={createHref}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Announcement
|
||||
</a>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<EmptyState
|
||||
title="No announcements"
|
||||
description={
|
||||
announcements.length === 0
|
||||
? "There are no announcements yet."
|
||||
: "No announcements match the current filter."
|
||||
}
|
||||
icon={Megaphone}
|
||||
className="h-auto border-none shadow-none"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{filtered.map((a) => (
|
||||
<AnnouncementCard
|
||||
key={a.id}
|
||||
announcement={a}
|
||||
href={detailHrefBuilder ? detailHrefBuilder(a.id) : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user