feat: 昵称+头衔+UID 全站显示(系统身份/自定义头衔双层,主要位恒显#id 评论位开关)+ 用户管理大弹窗编辑 + 用户主页视觉修复(Banner/tab/博客/QQ)+ 评论通知回退 admin + 全量评论列表 + 头像 bug 修复 + 站长帖判断改 author_username

This commit is contained in:
2026-08-14 02:36:57 +08:00
parent 4f3a2cf9a8
commit ee0086b252
19 changed files with 875 additions and 132 deletions
+123 -10
View File
@@ -9,24 +9,53 @@ 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 Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
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 DeleteIcon from '@mui/icons-material/Delete';
import { listPendingComments, listAllComments, approveComment, rejectComment, deleteComment } from '../../api/blog.js';
import { showSnack } from '../snack.jsx';
const PAGE_SIZE = 20;
/** 时间格式化:yyyy-mm-dd hh:mm */
function fmtTime(t) {
if (!t) return '';
return String(t).replace('T', ' ').slice(0, 16);
}
/** 评论管理:待审核队列 + 全部评论占位(后端暂无全量评论列表接口) */
/** 内容截断(单行预览):换行折叠 + 超长省略 */
function clip(content, max = 60) {
const s = String(content || '').replace(/\s+/g, ' ').trim();
return s.length > max ? s.slice(0, max) + '…' : s;
}
/** 状态 chip */
function StatusChip({ status }) {
if (status === 'approved') return <Chip size="small" label="已通过" color="success" />;
if (status === 'rejected') return <Chip size="small" label="已拒绝" color="error" variant="outlined" />;
return <Chip size="small" label="待审核" color="warning" />;
}
/** 评论管理:待审核队列 + 全部评论分页列表(status=all */
export default function CommentManage() {
const [tab, setTab] = useState(0);
const [pending, setPending] = useState([]);
const [busy, setBusy] = useState(false);
// 全部评论(分页)
const [allList, setAllList] = useState([]);
const [allPage, setAllPage] = useState(1);
const [allTotal, setAllTotal] = useState(0);
const [allTotalPages, setAllTotalPages] = useState(0);
const [allLoading, setAllLoading] = useState(false);
const load = useCallback(() => {
setBusy(true);
listPendingComments()
@@ -37,6 +66,23 @@ export default function CommentManage() {
useEffect(() => { load(); }, [load]);
const loadAll = useCallback((page) => {
setAllLoading(true);
listAllComments({ status: 'all', page, pageSize: PAGE_SIZE })
.then((res) => {
setAllList((res && res.list) || []);
setAllTotal(res ? res.total || 0 : 0);
setAllTotalPages(res ? res.totalPages || 0 : 0);
setAllPage(page);
})
.catch((e) => showSnack(e.message, 'error'))
.finally(() => setAllLoading(false));
}, []);
useEffect(() => {
if (tab === 1) loadAll(1);
}, [tab, loadAll]);
const act = async (id, fn, okMsg) => {
setBusy(true);
try {
@@ -49,17 +95,34 @@ export default function CommentManage() {
setBusy(false);
};
const actAll = async (id, fn, okMsg, reloadPage = allPage) => {
setBusy(true);
try {
await fn(id);
showSnack(okMsg);
// 同步待审核徽标 + 刷新当前页;若删除后本页空且非第一页则回退一页
setPending((list) => list.filter((c) => c.id !== id));
const remaining = allList.filter((c) => c.id !== id).length;
loadAll(remaining === 0 && reloadPage > 1 ? reloadPage - 1 : reloadPage);
} catch (e) {
showSnack(e.message, 'error');
}
setBusy(false);
};
const totalPages = Math.max(1, allTotalPages);
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>
<IconButton onClick={() => (tab === 0 ? load() : loadAll(allPage))} title="刷新" disabled={busy || allLoading}><RefreshIcon /></IconButton>
</Box>
<Paper sx={{ mb: 2 }}>
<Tabs value={tab} onChange={(e, v) => setTab(v)}>
<Tab label={`待审核${pending.length ? ` (${pending.length})` : ''}`} />
<Tab label="全部评论" />
<Tab label={`全部评论${allTotal ? ` (${allTotal})` : ''}`} />
</Tabs>
</Paper>
@@ -114,12 +177,62 @@ export default function CommentManage() {
</Paper>
)
) : (
<Paper sx={{ p: 4, textAlign: 'center' }}>
<Typography variant="body1" sx={{ mb: 0.5 }}>暂未提供全量评论列表接口</Typography>
<Typography variant="body2" color="text.secondary">
当前后端仅提供待审核评论的管理接口已通过 / 已拒绝评论可在对应文章页查看
</Typography>
</Paper>
<>
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>作者</TableCell>
<TableCell>文章</TableCell>
<TableCell>内容</TableCell>
<TableCell>状态</TableCell>
<TableCell>时间</TableCell>
<TableCell align="right">操作</TableCell>
</TableRow>
</TableHead>
<TableBody>
{allLoading && allList.length === 0 ? (
<TableRow><TableCell colSpan={6}>加载中...</TableCell></TableRow>
) : allList.length === 0 ? (
<TableRow><TableCell colSpan={6}>暂无评论</TableCell></TableRow>
) : allList.map((c) => (
<TableRow key={c.id}>
<TableCell sx={{ whiteSpace: 'nowrap', fontWeight: 600 }}>
{c.author_name || '匿名'}
</TableCell>
<TableCell sx={{ maxWidth: 200, color: 'text.secondary', fontSize: 13, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{c.post_title || `#${c.post_id}`}
</TableCell>
<TableCell sx={{ maxWidth: 320, fontSize: 13, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{clip(c.content)}
</TableCell>
<TableCell><StatusChip status={c.status} /></TableCell>
<TableCell sx={{ color: 'text.secondary', fontSize: 13, whiteSpace: 'nowrap' }}>{fmtTime(c.created_at)}</TableCell>
<TableCell align="right" sx={{ whiteSpace: 'nowrap' }}>
{c.status === 'pending' && (
<>
<Button size="small" startIcon={<CheckIcon fontSize="small" />} disabled={busy} onClick={() => actAll(c.id, approveComment, '已通过审核')}>通过</Button>
<Button size="small" color="error" startIcon={<CloseIcon fontSize="small" />} disabled={busy} onClick={() => actAll(c.id, rejectComment, '已拒绝')}>拒绝</Button>
</>
)}
<IconButton size="small" color="error" title="删除评论" disabled={busy} onClick={() => actAll(c.id, deleteComment, '已删除')}>
<DeleteIcon fontSize="small" />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 2, mt: 2 }}>
<Button size="small" variant="outlined" disabled={allPage <= 1 || allLoading} onClick={() => loadAll(allPage - 1)}>上一页</Button>
<Typography variant="body2" color="text.secondary">
{allPage} / {totalPages} · {allTotal}
</Typography>
<Button size="small" variant="outlined" disabled={allPage >= totalPages || allLoading} onClick={() => loadAll(allPage + 1)}>下一页</Button>
</Box>
</>
)}
</Box>
);