209 lines
5.8 KiB
TypeScript
Raw Permalink Normal View History

2026-06-22 14:43:46 +08:00
'use client';
import { useEffect, useState } from 'react';
import useSWR from 'swr';
import { CheckCircle2, CheckCheck, Loader2, Trash2 } from 'lucide-react';
import { AdminHeader } from '@/components/admin/AdminHeader';
import { PaginationTable, type Column } from '@/components/admin/PaginationTable';
import { TableToolbar } from '@/components/admin/TableToolbar';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { adminApi } from '@/lib/admin-services';
import { useAdminStore } from '@/store/adminStore';
import { formatDate } from '@/lib/utils';
import type { Message } from '@/lib/types';
export default function MessageAdminPage() {
const [hydrated, setHydrated] = useState(false);
const [page, setPage] = useState(1);
const [isRead, setIsRead] = useState<number | undefined>();
const [keyword, setKeyword] = useState('');
const [searchKw, setSearchKw] = useState('');
const [markingAll, setMarkingAll] = useState(false);
const isSuperAdmin = useAdminStore((s) => s.admin?.role) === 'super_admin';
useEffect(() => setHydrated(true), []);
const { data, isLoading, mutate } = useSWR(
hydrated ? ['/admin/message', page, isRead, searchKw] : null,
() =>
adminApi.messagePaginate({
page,
pageSize: 10,
isRead,
keyword: searchKw || undefined,
}),
);
const onMarkRead = async (id: number) => {
try {
await adminApi.messageMarkRead(id);
await mutate();
} catch (e) {
alert((e as Error).message);
}
};
const onDelete = async (id: number) => {
if (!confirm('确认删除该留言?')) return;
try {
await adminApi.messageDelete(id);
await mutate();
} catch (e) {
alert((e as Error).message);
}
};
const onMarkAllRead = async () => {
setMarkingAll(true);
try {
// 拉取全部未读(跨页)
const unread = await adminApi.messagePaginate({
isRead: 0,
page: 1,
pageSize: 999,
});
// 串行标记,单个失败即中断
for (const m of unread.list) {
await adminApi.messageMarkRead(m.id);
}
await mutate();
} catch (e) {
alert((e as Error).message);
} finally {
setMarkingAll(false);
}
};
const hasUnread = (data?.list ?? []).some((m) => m.isRead === 0);
const columns: Column<Message>[] = [
{ key: 'id', title: 'ID', width: 60 },
{ key: 'name', title: '姓名', width: 120 },
{ key: 'phone', title: '电话', width: 140 },
{ key: 'email', title: '邮箱', width: 180 },
{
key: 'content',
title: '留言内容',
render: (r) => (
<span className="line-clamp-2 max-w-md text-xs text-gray-700">
{r.content}
</span>
),
},
{
key: 'createdAt',
title: '提交时间',
width: 160,
render: (r) => formatDate(r.createdAt, 'YYYY-MM-DD HH:mm'),
},
{
key: 'isRead',
title: '状态',
width: 90,
render: (r) =>
r.isRead === 1 ? (
<Badge variant="outline"></Badge>
) : (
<Badge variant="warning"></Badge>
),
},
{
key: '_op',
title: '操作',
width: 150,
render: (r) => (
<div className="flex items-center gap-1.5 whitespace-nowrap">
{r.isRead === 0 && (
<Button size="sm" variant="outline" onClick={() => onMarkRead(r.id)}>
<CheckCircle2 className="h-3 w-3" />
</Button>
)}
{isSuperAdmin && (
<Button
size="sm"
variant="destructive"
onClick={() => onDelete(r.id)}
>
<Trash2 className="h-3 w-3" />
</Button>
)}
</div>
),
},
];
return (
<div>
<AdminHeader title="留言管理" description="查看访客提交的咨询留言" />
<TableToolbar
left={
<>
<select
className="h-10 rounded-md border border-gray-300 px-3 text-sm"
value={isRead ?? ''}
onChange={(e) => {
setIsRead(e.target.value === '' ? undefined : Number(e.target.value));
setPage(1);
}}
>
<option value=""></option>
<option value="0"></option>
<option value="1"></option>
</select>
<Input
placeholder="搜索 姓名/电话/内容"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
setSearchKw(keyword);
setPage(1);
}
}}
className="max-w-xs"
/>
<Button
variant="outline"
onClick={() => {
setSearchKw(keyword);
setPage(1);
}}
>
</Button>
</>
}
right={
<Button
variant="outline"
onClick={onMarkAllRead}
disabled={!hasUnread || markingAll}
>
{markingAll ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<CheckCheck className="h-4 w-4" />
)}
</Button>
}
/>
<PaginationTable<Message>
columns={columns}
rows={data?.list ?? []}
total={data?.total ?? 0}
page={page}
pageSize={10}
loading={isLoading}
onPageChange={setPage}
rowKey={(r) => String(r.id)}
/>
</div>
);
}