"use client" import { useCallback, useMemo, useState } from "react" import Link from "next/link" import { Mail, MailOpen, Plus, Send, Inbox, Search, Loader2, ChevronLeft, ChevronRight, Star } from "lucide-react" import { useTranslations } from "next-intl" import { toast } from "sonner" import { Badge } from "@/shared/components/ui/badge" import { Button } from "@/shared/components/ui/button" import { Card, CardContent, CardHeader } from "@/shared/components/ui/card" import { EmptyState } from "@/shared/components/ui/empty-state" import { Input } from "@/shared/components/ui/input" import { Tabs, TabsList, TabsTrigger } from "@/shared/components/ui/tabs" import { cn, formatDate } from "@/shared/lib/utils" import { usePermission } from "@/shared/hooks/use-permission" import { Permissions } from "@/shared/types/permissions" import { getMessagesAction, toggleMessageStarAction } from "../actions" import { useMessageSearch } from "../hooks/use-message-search" import type { Message, MessageType } from "../types" type Tab = "inbox" | "sent" /** 客户端分页大小 */ const PAGE_SIZE = 20 export function MessageList({ messages, currentUserId, initialType = "inbox", }: { messages: Message[] currentUserId: string initialType?: MessageType }) { const t = useTranslations("messages") const [tab, setTab] = useState(initialType === "sent" ? "sent" : "inbox") const [currentPage, setCurrentPage] = useState(1) const [starredOverride, setStarredOverride] = useState>({}) const [togglingStarId, setTogglingStarId] = useState(null) const { hasPermission } = usePermission() const canSend = hasPermission(Permissions.MESSAGE_SEND) const { keyword, setKeyword, results, searching, isUsingInitial } = useMessageSearch({ searchAction: getMessagesAction, tab, }) // 客户端过滤仅在初始数据(type=all)时需要,搜索结果已由服务端按 tab 过滤 const filtered = useMemo(() => { const displayMessages = isUsingInitial ? messages : (results ?? []) if (!isUsingInitial) return displayMessages if (tab === "inbox") return displayMessages.filter((m) => m.receiverId === currentUserId) return displayMessages.filter((m) => m.senderId === currentUserId) }, [messages, results, tab, currentUserId, isUsingInitial]) // 客户端分页:超过 PAGE_SIZE 条时显示分页 UI const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE)) const safePage = Math.min(currentPage, totalPages) const paged = filtered.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE) const showPagination = filtered.length > PAGE_SIZE // 切换 tab 或搜索时重置分页 const handleTabChange = (v: string): void => { setTab(v as Tab) setCurrentPage(1) } const handleKeywordChange = (v: string): void => { setKeyword(v) setCurrentPage(1) } const getIsStarred = (m: Message): boolean => { const override = starredOverride[m.id] return override === undefined ? m.isStarred : override } const handleToggleStar = useCallback( async (e: React.MouseEvent, messageId: string, currentStarred: boolean): Promise => { e.preventDefault() e.stopPropagation() setTogglingStarId(messageId) // 乐观更新 setStarredOverride((prev) => ({ ...prev, [messageId]: !currentStarred })) try { const res = await toggleMessageStarAction(messageId) if (res.success) { toast.success(t("messages.starToggled")) } else { // 回滚 setStarredOverride((prev) => ({ ...prev, [messageId]: currentStarred })) toast.error(res.message) } } catch { // 回滚 setStarredOverride((prev) => ({ ...prev, [messageId]: currentStarred })) toast.error(t("messages.sendFailed")) } finally { setTogglingStarId(null) } }, [t] ) return (
{t("tabs.inbox")} {t("tabs.sent")} {canSend ? ( ) : null}
{/* 搜索框 */}
{paged.length === 0 ? ( ) : ( <>
{paged.map((m) => { const isReceived = m.receiverId === currentUserId const counterpart = isReceived ? m.senderName : m.receiverName const unread = isReceived && !m.isRead const isStarred = getIsStarred(m) return (
{unread ? (

{isReceived ? t("meta.from") : t("meta.to")}: {counterpart ?? t("meta.unknown")}

{formatDate(m.createdAt)}

{m.content}

) })}
{showPagination ? (
{safePage} / {totalPages}
) : null} )}
) }