176 lines
8.2 KiB
React
176 lines
8.2 KiB
React
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="嵌入 URL(iframe嵌入用)" 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>
|
||
);
|
||
}
|