67b97e4b38
看板(Recharts): - 概览页加 DAU/WAU/MAU、今日/本月新增 KPI,新增趋势/活跃趋势/月活/留存曲线 - GET /admin/analytics 内存计算,活跃口径=当天有记录/发帖/评论,排除 bot 内容安全(微信官方 UGC): - 文本 msg_sec_check 同步判、图片 media_check_async 异步查 - 帖子/评论加 pending/rejected 审核态,feed 只放 published - 图片结果回调 /api/wx/sec-callback,JSON/XML + 明文模式,签名校验 - 检测不了/未发布时一律转待审核,走后台手动审核 - 后台帖子/评论审核页:修好看不到图(补 attachPostImages)、 加状态筛选 + 通过/打回;新增评论审核状态接口 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
127 lines
5.0 KiB
TypeScript
127 lines
5.0 KiB
TypeScript
import { useState } from 'react'
|
|
import { api } from '@/lib/api'
|
|
import { usePaged } from '@/lib/usePaged'
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Card, CardContent } from '@/components/ui/card'
|
|
import { Pager } from '@/components/Pager'
|
|
|
|
const statusMap: Record<string, { label: string; variant: any }> = {
|
|
pending: { label: '待审核', variant: 'warning' },
|
|
published: { label: '正常', variant: 'success' },
|
|
rejected: { label: '已打回', variant: 'destructive' },
|
|
deleted: { label: '已删除', variant: 'destructive' },
|
|
}
|
|
|
|
const FILTERS = [
|
|
{ label: '待审核', status: 'pending' },
|
|
{ label: '正常', status: 'published' },
|
|
{ label: '全部', status: '' },
|
|
]
|
|
|
|
function isUrl(s: string) {
|
|
return typeof s === 'string' && s.indexOf('http') === 0
|
|
}
|
|
|
|
export default function Comments() {
|
|
const [status, setStatus] = useState('pending')
|
|
const { page, setPage, data, totalPages, reload } = usePaged<any>(api.comments, { status })
|
|
|
|
async function setStat(id: number, s: string) {
|
|
await api.setCommentStatus(id, s)
|
|
reload()
|
|
}
|
|
async function del(id: number) {
|
|
await api.deleteComment(id)
|
|
reload()
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-6 flex items-center justify-between">
|
|
<h1 className="text-2xl font-bold">评论审核</h1>
|
|
<div className="flex gap-1 rounded-lg border bg-card p-1">
|
|
{FILTERS.map((f) => (
|
|
<button
|
|
key={f.status}
|
|
onClick={() => { setStatus(f.status); setPage(1) }}
|
|
className={`rounded-md px-3 py-1 text-sm transition ${
|
|
status === f.status ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:text-foreground'
|
|
}`}
|
|
>
|
|
{f.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>作者</TableHead>
|
|
<TableHead>内容</TableHead>
|
|
<TableHead>配图</TableHead>
|
|
<TableHead>帖子</TableHead>
|
|
<TableHead>状态</TableHead>
|
|
<TableHead className="text-right">操作</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{data?.list?.map((c) => {
|
|
const imgs: string[] = Array.isArray(c.images) ? c.images.filter(isUrl) : []
|
|
return (
|
|
<TableRow key={c.id}>
|
|
<TableCell className="whitespace-nowrap align-top">{c.author_name}</TableCell>
|
|
<TableCell className="max-w-xs align-top">
|
|
<div className="line-clamp-3 whitespace-pre-wrap text-sm">{c.content || '—'}</div>
|
|
</TableCell>
|
|
<TableCell className="align-top">
|
|
{imgs.length ? (
|
|
<div className="flex flex-wrap gap-1" style={{ maxWidth: 180 }}>
|
|
{imgs.map((u, i) => (
|
|
<a key={i} href={u} target="_blank" rel="noreferrer">
|
|
<img src={u} className="h-14 w-14 rounded-md border object-cover" loading="lazy" />
|
|
</a>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<span className="text-xs text-muted-foreground">无</span>
|
|
)}
|
|
</TableCell>
|
|
<TableCell className="align-top text-xs text-muted-foreground">{c.post_id}</TableCell>
|
|
<TableCell className="align-top">
|
|
<Badge variant={statusMap[c.status]?.variant}>{statusMap[c.status]?.label ?? c.status}</Badge>
|
|
</TableCell>
|
|
<TableCell className="space-x-2 whitespace-nowrap text-right align-top">
|
|
{c.status !== 'published' && c.status !== 'deleted' && (
|
|
<Button variant="default" size="sm" onClick={() => setStat(c.id, 'published')}>
|
|
通过
|
|
</Button>
|
|
)}
|
|
{c.status !== 'deleted' && (
|
|
<Button variant="destructive" size="sm" onClick={() => del(c.id)}>
|
|
删除
|
|
</Button>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
)
|
|
})}
|
|
{!data?.list?.length && (
|
|
<TableRow>
|
|
<TableCell colSpan={6} className="py-10 text-center text-muted-foreground">
|
|
这个状态下暂无评论
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
<Pager page={page} totalPages={totalPages} total={data?.total ?? 0} onChange={setPage} />
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|