统一 React 管理后台,修复评论换行与帖子置顶

- /admin/* 全部由 React SPA 渲染,替代旧版 HTML 后台页面
- 新增仪表盘、帖子/评论/用户管理、系统设置与 JSON API
- 帖子详情页支持管理员置顶;评论换行显示修复

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
freefire
2026-06-15 23:06:44 +08:00
parent 9230f7272d
commit d0555de28e
63 changed files with 1289 additions and 284 deletions

View File

@@ -0,0 +1,110 @@
import { useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Spinner } from '@/components/ui/spinner';
import { notify } from '@/lib/notify';
import { api } from '../../api/client';
import { useAdminGuard } from '../../layouts/AdminLayout';
import type { User } from '../../api/types';
export default function AdminUsersPage() {
const { ready } = useAdminGuard();
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const load = (p = page) => {
setLoading(true);
api.adminUsers(p)
.then(d => {
setUsers(d.users ?? []);
setPage(d.page);
setTotalPages(d.total_pages);
})
.catch(e => notify.error(e.message))
.finally(() => setLoading(false));
};
useEffect(() => {
if (ready) load(1);
}, [ready]);
const toggleBan = async (user: User) => {
if (user.role === 'admin') {
notify.warning('不能禁言管理员');
return;
}
try {
const r = await api.adminBanUser(user.id, !user.banned);
notify.success(r.message);
load();
} catch (e: unknown) {
notify.error(e instanceof Error ? e.message : '操作失败');
}
};
if (!ready) return null;
return (
<div className="admin-page">
<div className="admin-page-head">
<h1></h1>
<p></p>
</div>
<div className="admin-card">
{loading ? (
<div className="flex justify-center py-12"><Spinner size="lg" /></div>
) : (
<>
<table className="admin-table">
<thead>
<tr>
<th>ID</th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{users.map(u => (
<tr key={u.id}>
<td>{u.id}</td>
<td>{u.username}</td>
<td>{u.nickname}</td>
<td>
{u.role === 'admin'
? <Badge variant="orange"></Badge>
: <Badge variant="secondary"></Badge>}
</td>
<td>{u.banned ? <Badge variant="destructive"></Badge> : '正常'}</td>
<td>{u.created_at ? new Date(u.created_at).toLocaleString('zh-CN') : '—'}</td>
<td>
{u.role !== 'admin' && (
<Button size="sm" variant="outline" onClick={() => toggleBan(u)}>
{u.banned ? '解除禁言' : '禁言'}
</Button>
)}
</td>
</tr>
))}
</tbody>
</table>
{users.length === 0 && <div className="admin-empty"></div>}
{totalPages > 1 && (
<div className="admin-pagination">
<Button size="sm" variant="outline" disabled={page <= 1} onClick={() => load(page - 1)}></Button>
<span> {page} / {totalPages} </span>
<Button size="sm" variant="outline" disabled={page >= totalPages} onClick={() => load(page + 1)}></Button>
</div>
)}
</>
)}
</div>
</div>
);
}