156 lines
6.8 KiB
React
156 lines
6.8 KiB
React
import React, { useCallback, useEffect, useState } from 'react';
|
||
import Grid from '@mui/material/Grid';
|
||
import Card from '@mui/material/Card';
|
||
import CardContent from '@mui/material/CardContent';
|
||
import Typography from '@mui/material/Typography';
|
||
import Button from '@mui/material/Button';
|
||
import Box from '@mui/material/Box';
|
||
import Chip from '@mui/material/Chip';
|
||
import IconButton from '@mui/material/IconButton';
|
||
import EditIcon from '@mui/icons-material/Edit';
|
||
import DeleteIcon from '@mui/icons-material/Delete';
|
||
import AddIcon from '@mui/icons-material/Add';
|
||
import Dialog from '@mui/material/Dialog';
|
||
import DialogTitle from '@mui/material/DialogTitle';
|
||
import DialogContent from '@mui/material/DialogContent';
|
||
import DialogActions from '@mui/material/DialogActions';
|
||
import TextField from '@mui/material/TextField';
|
||
import { listCategories, createCategory, updateCategory, deleteCategory, listPosts } from '../../api/forum.js';
|
||
import { showSnack } from '../snack.jsx';
|
||
import ConfirmDialog from '../ConfirmDialog.jsx';
|
||
|
||
const EMPTY = { name: '', description: '', announcement: '', sub_categories: '', sort_order: '0' };
|
||
|
||
/** 论坛管理:板块卡片(帖子数)+ 添加/编辑弹窗 + 删除(MUI 版,同前台 ForumManage API) */
|
||
export default function ForumManage() {
|
||
const [cats, setCats] = useState([]);
|
||
const [counts, setCounts] = useState({});
|
||
const [dialog, setDialog] = useState(false);
|
||
const [editingId, setEditingId] = useState(null);
|
||
const [form, setForm] = useState(EMPTY);
|
||
const [confirm, setConfirm] = useState(null);
|
||
const [saving, setSaving] = useState(false);
|
||
|
||
const load = useCallback(() => {
|
||
listCategories()
|
||
.then((cs) => setCats(cs || []))
|
||
.catch((e) => showSnack(e.message, 'error'));
|
||
listPosts()
|
||
.then((ps) => {
|
||
const c = {};
|
||
(ps || []).forEach((p) => { c[p.category_id] = (c[p.category_id] || 0) + 1; });
|
||
setCounts(c);
|
||
})
|
||
.catch(() => {});
|
||
}, []);
|
||
|
||
useEffect(() => { load(); }, [load]);
|
||
|
||
const openAdd = () => { setEditingId(null); setForm(EMPTY); setDialog(true); };
|
||
const openEdit = (c) => {
|
||
setEditingId(c.id);
|
||
setForm({
|
||
name: c.name,
|
||
description: c.description || '',
|
||
announcement: c.announcement || '',
|
||
sub_categories: c.sub_categories || '',
|
||
sort_order: String(c.sort_order || 0),
|
||
});
|
||
setDialog(true);
|
||
};
|
||
|
||
const save = async () => {
|
||
if (!form.name.trim()) { showSnack('名称不能为空', 'error'); return; }
|
||
setSaving(true);
|
||
const data = {
|
||
name: form.name.trim(),
|
||
description: form.description.trim(),
|
||
announcement: form.announcement.trim(),
|
||
sub_categories: form.sub_categories.trim(),
|
||
sort_order: parseInt(form.sort_order, 10) || 0,
|
||
};
|
||
try {
|
||
if (editingId) await updateCategory(editingId, data);
|
||
else await createCategory(data);
|
||
showSnack('保存成功');
|
||
setDialog(false);
|
||
load();
|
||
} catch (e) {
|
||
showSnack(e.message, 'error');
|
||
}
|
||
setSaving(false);
|
||
};
|
||
|
||
const doDelete = async () => {
|
||
if (!confirm) return;
|
||
try {
|
||
await deleteCategory(confirm.id);
|
||
showSnack('已删除');
|
||
setConfirm(null);
|
||
load();
|
||
} catch (e) {
|
||
showSnack(e.message, 'error');
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Box>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
||
<Typography variant="h5">论坛管理</Typography>
|
||
<Button variant="contained" startIcon={<AddIcon />} onClick={openAdd}>添加板块</Button>
|
||
</Box>
|
||
|
||
{cats.length === 0 ? (
|
||
<Typography variant="body2" color="text.secondary">暂无板块,点击"添加板块"创建</Typography>
|
||
) : (
|
||
<Grid container spacing={2}>
|
||
{cats.map((c) => (
|
||
<Grid item xs={12} sm={6} md={4} key={c.id}>
|
||
<Card>
|
||
<CardContent sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>{c.name}</Typography>
|
||
<Chip size="small" label={`排序 ${c.sort_order}`} variant="outlined" />
|
||
</Box>
|
||
<Typography variant="body2" color="text.secondary" sx={{ flex: 1, mb: 1 }}>{c.description || '无描述'}</Typography>
|
||
{c.announcement && (
|
||
<Typography variant="caption" color="text.secondary" noWrap sx={{ mb: 1 }}>📢 {c.announcement}</Typography>
|
||
)}
|
||
<Typography variant="caption" color="text.secondary" sx={{ mb: 1 }}>{counts[c.id] || 0} 个帖子</Typography>
|
||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 0.5, mt: 'auto' }}>
|
||
<IconButton size="small" onClick={() => openEdit(c)} title="编辑"><EditIcon fontSize="small" /></IconButton>
|
||
<IconButton size="small" color="error" onClick={() => setConfirm({ id: c.id, name: c.name })}><DeleteIcon fontSize="small" /></IconButton>
|
||
</Box>
|
||
</CardContent>
|
||
</Card>
|
||
</Grid>
|
||
))}
|
||
</Grid>
|
||
)}
|
||
|
||
<Dialog open={dialog} onClose={() => setDialog(false)} fullWidth maxWidth="sm">
|
||
<DialogTitle>{editingId ? '编辑板块' : '添加板块'}</DialogTitle>
|
||
<DialogContent>
|
||
<TextField fullWidth label="名称 *" value={form.name} onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))} margin="normal" />
|
||
<TextField fullWidth label="描述" value={form.description} onChange={(e) => setForm((p) => ({ ...p, description: e.target.value }))} margin="normal" />
|
||
<TextField fullWidth label="板块公告" multiline rows={2} value={form.announcement} onChange={(e) => setForm((p) => ({ ...p, announcement: e.target.value }))} margin="normal" />
|
||
<TextField fullWidth label="帖子分类(逗号分隔)" value={form.sub_categories} onChange={(e) => setForm((p) => ({ ...p, sub_categories: e.target.value }))} margin="normal" placeholder="例: 求助,分享,讨论,建议" />
|
||
<TextField label="排序" type="number" value={form.sort_order} onChange={(e) => setForm((p) => ({ ...p, sort_order: e.target.value }))} margin="normal" sx={{ maxWidth: 160 }} />
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button onClick={() => setDialog(false)}>取消</Button>
|
||
<Button variant="contained" onClick={save} disabled={saving}>保存</Button>
|
||
</DialogActions>
|
||
</Dialog>
|
||
|
||
<ConfirmDialog
|
||
open={!!confirm}
|
||
message={`确定删除板块「${confirm ? confirm.name : ''}」?板块下的帖子将一并删除`}
|
||
onClose={() => setConfirm(null)}
|
||
onConfirm={doDelete}
|
||
confirmText="确认删除"
|
||
/>
|
||
</Box>
|
||
);
|
||
}
|