Files
rainblogweb/frontend/src/admin/pages/Links.jsx
T

176 lines
8.2 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useCallback, useEffect, useState } from 'react';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
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 Chip from '@mui/material/Chip';
import IconButton from '@mui/material/IconButton';
import AddIcon from '@mui/icons-material/Add';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
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 FormControlLabel from '@mui/material/FormControlLabel';
import Switch from '@mui/material/Switch';
import { listAdminLinks, createAdminLink, updateAdminLink, deleteAdminLink } from '../../api/adminLinks.js';
import { showSnack } from '../snack.jsx';
import ConfirmDialog from '../ConfirmDialog.jsx';
const EMPTY = {
title: '', url: '', embed_url: '', use_proxy: false, description: '',
icon: '', category: '默认', version: '', sort_order: '0',
};
/** 面板链接管理:CRUD + 代理嵌入开关(迁移自 v1 面板链接卡片) */
export default function Links() {
const [links, setLinks] = useState(null);
const [dialog, setDialog] = useState(false);
const [editingId, setEditingId] = useState(null);
const [form, setForm] = useState(EMPTY);
const [confirm, setConfirm] = useState(null);
const load = useCallback(() => {
listAdminLinks()
.then((ls) => setLinks(ls || []))
.catch((e) => showSnack(e.message, 'error'));
}, []);
useEffect(() => { load(); }, [load]);
const openAdd = () => { setEditingId(null); setForm(EMPTY); setDialog(true); };
const openEdit = (l) => {
setEditingId(l.id);
setForm({
title: l.title,
url: l.url,
embed_url: l.embed_url || '',
use_proxy: !!l.use_proxy,
description: l.description || '',
icon: l.icon || '',
category: l.category || '默认',
version: l.version || '',
sort_order: String(l.sort_order || 0),
});
setDialog(true);
};
const save = async () => {
if (!form.title.trim() || !form.url.trim()) { showSnack('标题和链接不能为空', 'error'); return; }
const data = {
title: form.title.trim(),
url: form.url.trim(),
embed_url: form.embed_url.trim(),
use_proxy: form.use_proxy ? 1 : 0,
description: form.description.trim(),
icon: form.icon.trim(),
category: form.category.trim() || '默认',
version: form.version.trim(),
sort_order: parseInt(form.sort_order, 10) || 0,
};
try {
if (editingId) await updateAdminLink(editingId, data);
else await createAdminLink(data);
showSnack('保存成功');
setDialog(false);
load();
} catch (e) {
showSnack(e.message, 'error');
}
};
const doDelete = async () => {
if (!confirm) return;
try {
await deleteAdminLink(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>
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>排序</TableCell>
<TableCell>标题</TableCell>
<TableCell>版本</TableCell>
<TableCell>URL</TableCell>
<TableCell>嵌入URL</TableCell>
<TableCell>分类</TableCell>
<TableCell align="right">操作</TableCell>
</TableRow>
</TableHead>
<TableBody>
{links === null ? (
<TableRow><TableCell colSpan={7}>加载中...</TableCell></TableRow>
) : links.length === 0 ? (
<TableRow><TableCell colSpan={7}>暂无面板</TableCell></TableRow>
) : links.map((l) => (
<TableRow key={l.id}>
<TableCell>{l.sort_order}</TableCell>
<TableCell><Box sx={{ fontWeight: 600 }}>{l.title}</Box></TableCell>
<TableCell sx={{ color: 'text.secondary', fontSize: 13 }}>{l.version || '-'}</TableCell>
<TableCell sx={{ maxWidth: 180 }}>
<Box component="a" href={l.url} target="_blank" rel="noopener" sx={{ color: 'primary.main', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{l.url}</Box>
</TableCell>
<TableCell sx={{ maxWidth: 140, color: 'text.secondary', fontSize: 13, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{l.embed_url || '-'}</TableCell>
<TableCell><Chip size="small" label={l.category} variant="outlined" /></TableCell>
<TableCell align="right" sx={{ whiteSpace: 'nowrap' }}>
<IconButton size="small" onClick={() => openEdit(l)}><EditIcon fontSize="small" /></IconButton>
<IconButton size="small" color="error" onClick={() => setConfirm({ id: l.id, label: l.title })}><DeleteIcon fontSize="small" /></IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Dialog open={dialog} onClose={() => setDialog(false)} fullWidth maxWidth="sm">
<DialogTitle>{editingId ? '编辑面板' : '添加面板'}</DialogTitle>
<DialogContent>
<TextField fullWidth label="标题 *" value={form.title} onChange={(e) => setForm((p) => ({ ...p, title: e.target.value }))} margin="normal" />
<TextField fullWidth label="URL *" type="url" value={form.url} onChange={(e) => setForm((p) => ({ ...p, url: e.target.value }))} margin="normal" placeholder="https://..." />
<TextField fullWidth label="嵌入 URLiframe嵌入用)" type="url" value={form.embed_url} onChange={(e) => setForm((p) => ({ ...p, embed_url: e.target.value }))} margin="normal" placeholder="留空则新标签页打开" />
<FormControlLabel control={<Switch checked={form.use_proxy} onChange={(e) => setForm((p) => ({ ...p, use_proxy: e.target.checked }))} />} label="通过代理嵌入(绕过 X-Frame-Options 限制)" />
<TextField fullWidth label="描述" value={form.description} onChange={(e) => setForm((p) => ({ ...p, description: e.target.value }))} margin="normal" />
<TextField fullWidth label="图标 (Material图标名)" value={form.icon} onChange={(e) => setForm((p) => ({ ...p, icon: e.target.value }))} margin="normal" placeholder="settings, dashboard, ..." />
<TextField fullWidth label="分类" value={form.category} onChange={(e) => setForm((p) => ({ ...p, category: e.target.value }))} margin="normal" placeholder="默认" />
<TextField fullWidth label="版本号(可选)" value={form.version} onChange={(e) => setForm((p) => ({ ...p, version: e.target.value }))} margin="normal" placeholder="v2.1.0" />
<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}>保存</Button>
</DialogActions>
</Dialog>
<ConfirmDialog
open={!!confirm}
message={`确定要删除 "${confirm ? confirm.label : ''}" 吗?`}
onClose={() => setConfirm(null)}
onConfirm={doDelete}
confirmText="确认删除"
/>
</Box>
);
}