feat(announcements): add tests, skeleton, pagination, and service context
- Add announcement-card test for component testing - Add is-announcement-visible test and schema test for logic testing - Add announcement-list-skeleton for loading states - Add announcement-pagination for list pagination - Add announcements-service-context and default-announcements-service for service layer
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Plus, Megaphone } from "lucide-react"
|
||||
@@ -16,6 +17,8 @@ import {
|
||||
} 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
|
||||
@@ -28,29 +31,59 @@ type Filter = "all" | AnnouncementStatus
|
||||
* - 父页面根据 `?status=` 查询并传入 `announcements` prop
|
||||
* - 组件不再做客户端二次过滤,避免双重过滤逻辑冗余
|
||||
*
|
||||
* 详情链接构建:
|
||||
* - `detailHrefPrefix`:推荐方式,适用于 Server Component(前缀 + id 拼接)
|
||||
* - `detailHrefBuilder`:仅适用于 Client Component 之间的调用
|
||||
* P2-4: 删除了死 prop `detailHrefBuilder`,仅保留 `detailHrefPrefix`(Server Component 安全)。
|
||||
* P1-2: 用户端(canManage=false)时调用 `getReadStatus` 批量获取已读状态,
|
||||
* 传入卡片做已读/未读视觉区分。
|
||||
* P2-6: 新增分页支持。传入 `pagination` prop 时在列表底部渲染 `AnnouncementPagination`,
|
||||
* 通过 `buildPageHref` 回调生成页码 URL(与 `?status=` 过滤查询参数合并)。
|
||||
*/
|
||||
export function AnnouncementList({
|
||||
announcements,
|
||||
canManage,
|
||||
createHref,
|
||||
detailHrefBuilder,
|
||||
detailHrefPrefix,
|
||||
initialStatus,
|
||||
pagination,
|
||||
}: {
|
||||
announcements: Announcement[]
|
||||
canManage?: boolean
|
||||
createHref?: string
|
||||
detailHrefBuilder?: (id: string) => string
|
||||
detailHrefPrefix?: string
|
||||
initialStatus?: Filter
|
||||
pagination?: {
|
||||
page: number
|
||||
pageSize: number
|
||||
total: number
|
||||
buildPageHref: (page: number) => 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") },
|
||||
@@ -65,14 +98,6 @@ export function AnnouncementList({
|
||||
router.replace(qs ? `?${qs}` : "?")
|
||||
}
|
||||
|
||||
// 构建详情链接:优先使用 detailHrefPrefix(Server Component 安全),
|
||||
// 其次使用 detailHrefBuilder(仅 Client Component 间调用)
|
||||
const buildDetailHref = (id: string): string | undefined => {
|
||||
if (detailHrefPrefix) return `${detailHrefPrefix}/${id}`
|
||||
if (detailHrefBuilder) return detailHrefBuilder(id)
|
||||
return undefined
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
@@ -110,15 +135,26 @@ export function AnnouncementList({
|
||||
className="h-auto border-none shadow-none"
|
||||
/>
|
||||
) : (
|
||||
<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={buildDetailHref(a.id)}
|
||||
canManage={canManage}
|
||||
<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}
|
||||
buildHref={pagination.buildPageHref}
|
||||
/>
|
||||
))}
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user