feat: 后台评论管理 + 评论设置开关 + 技术债清理

- 后台评论管理页(待审核 Tab + 通过/拒绝),侧栏入口
- 站点设置加评论审核模式/评论邮件通知开关
- Material Icons 自托管(本地 woff2)+ ssr 图标修复
- /admin* 路由收紧为显式两条
- 删除死代码 routes/links.js
- 密码箱改 PIN 增加数据丢失警告确认
This commit is contained in:
2026-08-07 02:14:07 +08:00
parent 3cf560f932
commit 632d19f49e
12 changed files with 226 additions and 46 deletions
+126
View File
@@ -0,0 +1,126 @@
import React, { useCallback, useEffect, useState } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import IconButton from '@mui/material/IconButton';
import RefreshIcon from '@mui/icons-material/Refresh';
import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close';
import { listPendingComments, approveComment, rejectComment } from '../../api/blog.js';
import { showSnack } from '../snack.jsx';
/** 时间格式化:yyyy-mm-dd hh:mm */
function fmtTime(t) {
if (!t) return '';
return String(t).replace('T', ' ').slice(0, 16);
}
/** 评论管理:待审核队列 + 全部评论占位(后端暂无全量评论列表接口) */
export default function CommentManage() {
const [tab, setTab] = useState(0);
const [pending, setPending] = useState([]);
const [busy, setBusy] = useState(false);
const load = useCallback(() => {
setBusy(true);
listPendingComments()
.then((list) => setPending(list || []))
.catch((e) => showSnack(e.message, 'error'))
.finally(() => setBusy(false));
}, []);
useEffect(() => { load(); }, [load]);
const act = async (id, fn, okMsg) => {
setBusy(true);
try {
await fn(id);
setPending((list) => list.filter((c) => c.id !== id));
showSnack(okMsg);
} catch (e) {
showSnack(e.message, 'error');
}
setBusy(false);
};
return (
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="h5">评论管理</Typography>
<IconButton onClick={load} title="刷新" disabled={busy}><RefreshIcon /></IconButton>
</Box>
<Paper sx={{ mb: 2 }}>
<Tabs value={tab} onChange={(e, v) => setTab(v)}>
<Tab label={`待审核${pending.length ? ` (${pending.length})` : ''}`} />
<Tab label="全部评论" />
</Tabs>
</Paper>
{tab === 0 ? (
pending.length === 0 ? (
<Paper sx={{ p: 4, textAlign: 'center' }}>
<Typography variant="body1" sx={{ mb: 0.5 }}>暂无待审核评论</Typography>
<Typography variant="body2" color="text.secondary">
开启评论审核模式新评论会先进这里等待审核
</Typography>
</Paper>
) : (
<Paper>
<List disablePadding>
{pending.map((c) => (
<ListItem
key={c.id}
divider
alignItems="flex-start"
sx={{ flexDirection: 'column', alignItems: 'stretch', gap: 1, py: 2, px: { xs: 2, md: 3 } }}
>
<Box sx={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
{c.author_name || '匿名'}
</Typography>
<Chip size="small" variant="outlined" label={`文章:${c.post_title || `#${c.post_id}`}`} />
<Typography variant="caption" color="text.secondary">{fmtTime(c.created_at)}</Typography>
</Box>
<Typography variant="body2" sx={{ color: 'text.primary', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
{c.content}
</Typography>
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
<Button
size="small"
variant="contained"
startIcon={<CheckIcon />}
disabled={busy}
onClick={() => act(c.id, approveComment, '已通过审核')}
>通过</Button>
<Button
size="small"
variant="outlined"
color="error"
startIcon={<CloseIcon />}
disabled={busy}
onClick={() => act(c.id, rejectComment, '已拒绝')}
>拒绝</Button>
</Box>
</ListItem>
))}
</List>
</Paper>
)
) : (
<Paper sx={{ p: 4, textAlign: 'center' }}>
<Typography variant="body1" sx={{ mb: 0.5 }}>暂未提供全量评论列表接口</Typography>
<Typography variant="body2" color="text.secondary">
当前后端仅提供待审核评论的管理接口已通过 / 已拒绝评论可在对应文章页查看
</Typography>
</Paper>
)}
</Box>
);
}