feat: 前端按钮基于 permissions 显隐 — 导出/新建报告/下载/删除

三处修改:
- TicketList 导出按钮基于 tickets:export 权限显隐
- 报告列表页新建/批量下载/批量删除按钮及各状态行内操作基于 reports:create/reports:download 显隐
- 报告详情页生成文档/下载报告/重新生成按钮基于 permissions 显隐
This commit is contained in:
gitadmin 2026-05-14 17:05:14 +08:00
parent 152241e666
commit 2d74f0a05b
3 changed files with 87 additions and 37 deletions

View File

@ -31,6 +31,14 @@ export default function ReportDetailPage() {
const [reportData, setReportData] = useState<any>(null)
const [loading, setLoading] = useState(true)
const [generating, setGenerating] = useState(false)
const [permissions, setPermissions] = useState<string[]>([])
useEffect(() => {
fetch('/api/auth/me')
.then(r => r.json())
.then(u => { if (u.user?.permissions) setPermissions(u.user.permissions) })
.catch(() => {})
}, [])
const fetchReport = () => {
fetch(`/api/reports/${params.id}`)
@ -84,16 +92,18 @@ export default function ReportDetailPage() {
setGenerating(false)
}
const can = (perm: string) => permissions.includes('*') || permissions.includes(perm)
const renderRightButton = () => {
if (!report) return null
switch (report.status) {
case 'ready':
return (
return can('reports:create') ? (
<Button size="sm" onClick={handleGenerate} loading={generating} className="bg-emerald-600 hover:bg-emerald-700 text-white">
<FileText size={16} className="mr-1" />
</Button>
)
) : null
case 'generating':
return (
<Button size="sm" disabled className="bg-slate-400 text-white cursor-not-allowed">
@ -102,17 +112,17 @@ export default function ReportDetailPage() {
</Button>
)
case 'completed':
return (
return can('reports:download') ? (
<Button size="sm" onClick={() => window.open(`/api/reports/${report.id}/download`, '_blank')} className="bg-blue-600 hover:bg-blue-700 text-white">
<Download size={16} className="mr-1" />
</Button>
)
) : null
case 'failed':
return (
return can('reports:create') ? (
<Button size="sm" onClick={handleGenerate} loading={generating} className="bg-amber-500 hover:bg-amber-600 text-white">
<RefreshCw size={16} className="mr-1" />
</Button>
)
) : null
default:
return null
}

View File

@ -84,6 +84,14 @@ export default function ReportsPage() {
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set())
const [batchDeleteOpen, setBatchDeleteOpen] = useState(false)
const [generatingIds, setGeneratingIds] = useState<Set<number>>(new Set())
const [permissions, setPermissions] = useState<string[]>([])
useEffect(() => {
fetch('/api/auth/me')
.then(r => r.json())
.then(u => { if (u.user?.permissions) setPermissions(u.user.permissions) })
.catch(() => {})
}, [])
const toggleSelect = (id: number) => {
setSelectedIds(prev => {
@ -269,12 +277,16 @@ export default function ReportsPage() {
<Button variant="ghost" size="sm" onClick={() => router.push(`/reports/${r.id}`)}>
<Eye size={14} className="mr-0.5" />
</Button>
{can('reports:create') && (
<Button variant="ghost" size="sm" onClick={() => handleGenerate(r.id)} loading={isGenerating}>
</Button>
)}
{can('reports:create') && (
<Button variant="ghost" size="sm" onClick={() => setDeleteTarget(r)}>
<Trash2 size={14} className="text-red-500" />
</Button>
)}
</div>
)
case 'generating':
@ -291,12 +303,16 @@ export default function ReportsPage() {
<Button variant="ghost" size="sm" onClick={() => router.push(`/reports/${r.id}`)}>
<Eye size={14} className="mr-0.5" />
</Button>
{can('reports:download') && (
<Button variant="ghost" size="sm" onClick={() => window.open(`/api/reports/${r.id}/download`, '_blank')}>
<Download size={14} className="mr-0.5" />
</Button>
)}
{can('reports:create') && (
<Button variant="ghost" size="sm" onClick={() => setDeleteTarget(r)}>
<Trash2 size={14} className="text-red-500" />
</Button>
)}
</div>
)
case 'failed':
@ -305,25 +321,33 @@ export default function ReportsPage() {
<Button variant="ghost" size="sm" onClick={() => router.push(`/reports/${r.id}`)}>
<Eye size={14} className="mr-0.5" />
</Button>
{can('reports:create') && (
<Button variant="ghost" size="sm" onClick={() => handleGenerate(r.id)} loading={isGenerating}>
<RefreshCw size={14} className="mr-0.5" />
</Button>
)}
{can('reports:create') && (
<Button variant="ghost" size="sm" onClick={() => setDeleteTarget(r)}>
<Trash2 size={14} className="text-red-500" />
</Button>
)}
</div>
)
default:
return (
<div className="flex items-center gap-1">
{can('reports:create') && (
<Button variant="ghost" size="sm" onClick={() => setDeleteTarget(r)}>
<Trash2 size={14} className="text-red-500" />
</Button>
)}
</div>
)
}
}
const can = (perm: string) => permissions.includes('*') || permissions.includes(perm)
const renderStatusBadge = (status: string) => {
if (status === 'generating') {
return (
@ -356,17 +380,23 @@ export default function ReportsPage() {
<div className="flex items-center gap-2">
{selectedIds.size > 0 && (
<>
{can('reports:download') && (
<Button size="sm" onClick={handleBatchDownload}>
<Archive size={16} className="mr-1" /> ({selectedIds.size})
</Button>
)}
{can('reports:create') && (
<Button size="sm" variant="danger" onClick={() => setBatchDeleteOpen(true)}>
<Trash2 size={16} className="mr-1" /> ({selectedIds.size})
</Button>
)}
</>
)}
{can('reports:create') && (
<Button size="sm" onClick={() => setShowCreate(!showCreate)}>
<Plus size={16} className="mr-1" />
</Button>
)}
</div>
</div>

View File

@ -166,8 +166,16 @@ function TicketListInner({ onPaginationChange, defaultStatusFilter, showSlaColum
const [dateFilter, setDateFilter] = useState<Record<string, { start: string; end: string }>>({})
const [fieldOptions, setFieldOptions] = useState<Record<string, string[]>>({})
const [ticketNoFilter, setTicketNoFilter] = useState('')
const [permissions, setPermissions] = useState<string[]>([])
const filterDropRef = useRef<HTMLDivElement>(null)
useEffect(() => {
fetch('/api/auth/me')
.then(r => r.json())
.then(u => { if (u.user?.permissions) setPermissions(u.user.permissions) })
.catch(() => {})
}, [])
// 列宽拖拽调整
const [colWidths, setColWidths] = useState<Record<string, number>>({})
const [resizingCol, setResizingCol] = useState<string | null>(null)
@ -429,7 +437,9 @@ function TicketListInner({ onPaginationChange, defaultStatusFilter, showSlaColum
<Button variant="secondary" size="sm" onClick={() => { setPage(1); fetchTickets() }}></Button>
</div>
<div className="flex items-center gap-2">
{(permissions.includes('*') || permissions.includes('tickets:export')) && (
<Button variant="secondary" size="sm" onClick={handleExport}><Download size={14} /></Button>
)}
</div>
</div>