feat: 昵称+头衔+UID 全站显示(系统身份/自定义头衔双层,主要位恒显#id 评论位开关)+ 用户管理大弹窗编辑 + 用户主页视觉修复(Banner/tab/博客/QQ)+ 评论通知回退 admin + 全量评论列表 + 头像 bug 修复 + 站长帖判断改 author_username
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user