Files
NextEdu/src/modules/announcements/components/announcement-list.tsx
SpecialX 98429e87eb feat(business): update audit, auth, course-plans, announcements, exams
- audit: update retention.ts, types.ts, audit-retention-settings.tsx

- auth: update actions.ts and types.ts

- course-plans: update actions.ts, course-plan-detail.tsx, template-picker-dialog.tsx,

  add lib/track-event.ts

- announcements: update page.tsx, announcement-list.tsx, announcement-pagination.tsx

- exams: update actions.ts
2026-07-04 10:23:03 +08:00

166 lines
5.2 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.
"use client"
import { useEffect, useState } from "react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { Plus, Megaphone } from "lucide-react"
import { useTranslations } from "next-intl"
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 { AnnouncementCard } from "./announcement-card"
import { AnnouncementPagination } from "./announcement-pagination"
import { useAnnouncementsService } from "./announcements-service-context"
import type { Announcement, AnnouncementStatus } from "../types"
type Filter = "all" | AnnouncementStatus
/**
* 公告列表组件。
*
* 过滤模式:纯服务端过滤。
* - Select 切换时更新 URL `?status=`,触发 RSC 重新渲染
* - 父页面根据 `?status=` 查询并传入 `announcements` prop
* - 组件不再做客户端二次过滤,避免双重过滤逻辑冗余
*
* P2-4: 删除了死 prop `detailHrefBuilder`,仅保留 `detailHrefPrefix`Server Component 安全)。
* P1-2: 用户端canManage=false时调用 `getReadStatus` 批量获取已读状态,
* 传入卡片做已读/未读视觉区分。
* P2-6: 新增分页支持。传入 `pagination` prop 时在列表底部渲染 `AnnouncementPagination`。
* v3: pagination 不再接收 `buildPageHref` 函数Server Component 不能传函数),
* 改为传 `basePath` + `statusFilter`,由 AnnouncementPagination 自行构建 URL。
*/
export function AnnouncementList({
announcements,
canManage,
createHref,
detailHrefPrefix,
initialStatus,
pagination,
}: {
announcements: Announcement[]
canManage?: boolean
createHref?: string
detailHrefPrefix?: string
initialStatus?: Filter
pagination?: {
page: number
pageSize: number
total: number
basePath: string
statusFilter?: string
}
}) {
const t = useTranslations("announcements")
const router = useRouter()
const service = useAnnouncementsService()
const filter: Filter = initialStatus ?? "all"
// P1-2: 用户端批量获取已读状态
const [readStatus, setReadStatus] = useState<Record<string, boolean>>({})
useEffect(() => {
if (canManage) return
if (announcements.length === 0) return
let cancelled = false
void service
.getReadStatus(announcements.map((a) => a.id))
.then((res) => {
if (cancelled) return
if (res.success && res.data) {
setReadStatus(res.data)
}
})
.catch(() => {
// 静默处理,已读状态不影响主流程
})
return () => {
cancelled = true
}
}, [announcements, canManage, service])
const filterOptions: { value: Filter; label: string }[] = [
{ value: "all", label: t("filter.all") },
{ value: "published", label: t("filter.published") },
{ value: "draft", label: t("filter.draft") },
{ value: "archived", label: t("filter.archived") },
]
const handleFilterChange = (value: string): void => {
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={t("filter.placeholder")} />
</SelectTrigger>
<SelectContent>
{filterOptions.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
{canManage && createHref ? (
<Button asChild>
<Link href={createHref}>
<Plus className="mr-2 h-4 w-4" />
{t("actions.new")}
</Link>
</Button>
) : null}
</div>
{announcements.length === 0 ? (
<EmptyState
title={t("empty.noAnnouncements")}
description={
filter === "all"
? t("empty.noAnnouncementsDesc")
: t("empty.noMatch")
}
icon={Megaphone}
className="h-auto border-none shadow-none"
/>
) : (
<div className="space-y-6">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{announcements.map((a) => (
<AnnouncementCard
key={a.id}
announcement={a}
href={detailHrefPrefix ? `${detailHrefPrefix}/${a.id}` : undefined}
canManage={canManage}
isRead={canManage ? undefined : readStatus[a.id]}
/>
))}
</div>
{pagination ? (
<AnnouncementPagination
page={pagination.page}
pageSize={pagination.pageSize}
total={pagination.total}
basePath={pagination.basePath}
statusFilter={pagination.statusFilter}
/>
) : null}
</div>
)}
</div>
)
}