P5: 后台独立应用(MUI)- 13 模块/布局/守卫/复用前台基建

This commit is contained in:
2026-08-06 23:10:09 +08:00
parent f7dd4163b2
commit c71f4f0ed6
21 changed files with 2936 additions and 5 deletions
+93
View File
@@ -0,0 +1,93 @@
import React, { useCallback, useEffect, useState } from 'react';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
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 IconButton from '@mui/material/IconButton';
import DeleteIcon from '@mui/icons-material/Delete';
import { listAttachments, deleteAttachment } from '../../api/upload.js';
import { showSnack } from '../snack.jsx';
import ConfirmDialog from '../ConfirmDialog.jsx';
/** 附件管理:列表/删除(迁移自 v1 附件卡片) */
export default function Uploads() {
const [list, setList] = useState(null);
const [confirm, setConfirm] = useState(null);
const load = useCallback(() => {
listAttachments()
.then((rows) => setList(rows || []))
.catch((e) => showSnack(e.message, 'error'));
}, []);
useEffect(() => { load(); }, [load]);
const doDelete = async () => {
if (!confirm) return;
try {
await deleteAttachment(confirm.id);
showSnack('已删除');
setConfirm(null);
load();
} catch (e) {
showSnack(e.message, 'error');
}
};
return (
<Box>
<Typography variant="h5" sx={{ mb: 2 }}>附件管理</Typography>
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>ID</TableCell>
<TableCell>文件名</TableCell>
<TableCell>原始名</TableCell>
<TableCell>大小</TableCell>
<TableCell>类型</TableCell>
<TableCell>上传者</TableCell>
<TableCell>时间</TableCell>
<TableCell align="right">操作</TableCell>
</TableRow>
</TableHead>
<TableBody>
{list === null ? (
<TableRow><TableCell colSpan={8}>加载中...</TableCell></TableRow>
) : list.length === 0 ? (
<TableRow><TableCell colSpan={8}>暂无附件</TableCell></TableRow>
) : list.map((f) => (
<TableRow key={f.id}>
<TableCell>{f.id}</TableCell>
<TableCell sx={{ maxWidth: 160 }}>
<Box component="a" href={`/uploads/${f.filename}`} target="_blank" rel="noopener" sx={{ color: 'primary.main', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{f.filename}</Box>
</TableCell>
<TableCell sx={{ maxWidth: 160, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{f.original_name}</TableCell>
<TableCell>{((f.size || 0) / 1024).toFixed(0)} KB</TableCell>
<TableCell sx={{ color: 'text.secondary', fontSize: 13 }}>{f.mime_type || '-'}</TableCell>
<TableCell sx={{ color: 'text.secondary' }}>UID {f.user_id}</TableCell>
<TableCell sx={{ color: 'text.secondary', fontSize: 13 }}>{f.created_at}</TableCell>
<TableCell align="right">
<IconButton size="small" color="error" onClick={() => setConfirm({ id: f.id, label: f.filename })}><DeleteIcon fontSize="small" /></IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<ConfirmDialog
open={!!confirm}
message={`确定要删除 "${confirm ? confirm.label : ''}" 吗?`}
onClose={() => setConfirm(null)}
onConfirm={doDelete}
confirmText="确认删除"
/>
</Box>
);
}