feat: 前端按钮基于 permissions 显隐 — 导出/新建报告/下载/删除
三处修改: - TicketList 导出按钮基于 tickets:export 权限显隐 - 报告列表页新建/批量下载/批量删除按钮及各状态行内操作基于 reports:create/reports:download 显隐 - 报告详情页生成文档/下载报告/重新生成按钮基于 permissions 显隐
This commit is contained in:
parent
152241e666
commit
2d74f0a05b
|
|
@ -31,6 +31,14 @@ export default function ReportDetailPage() {
|
||||||
const [reportData, setReportData] = useState<any>(null)
|
const [reportData, setReportData] = useState<any>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [generating, setGenerating] = useState(false)
|
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 = () => {
|
const fetchReport = () => {
|
||||||
fetch(`/api/reports/${params.id}`)
|
fetch(`/api/reports/${params.id}`)
|
||||||
|
|
@ -84,16 +92,18 @@ export default function ReportDetailPage() {
|
||||||
setGenerating(false)
|
setGenerating(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const can = (perm: string) => permissions.includes('*') || permissions.includes(perm)
|
||||||
|
|
||||||
const renderRightButton = () => {
|
const renderRightButton = () => {
|
||||||
if (!report) return null
|
if (!report) return null
|
||||||
|
|
||||||
switch (report.status) {
|
switch (report.status) {
|
||||||
case 'ready':
|
case 'ready':
|
||||||
return (
|
return can('reports:create') ? (
|
||||||
<Button size="sm" onClick={handleGenerate} loading={generating} className="bg-emerald-600 hover:bg-emerald-700 text-white">
|
<Button size="sm" onClick={handleGenerate} loading={generating} className="bg-emerald-600 hover:bg-emerald-700 text-white">
|
||||||
<FileText size={16} className="mr-1" />生成报告文档
|
<FileText size={16} className="mr-1" />生成报告文档
|
||||||
</Button>
|
</Button>
|
||||||
)
|
) : null
|
||||||
case 'generating':
|
case 'generating':
|
||||||
return (
|
return (
|
||||||
<Button size="sm" disabled className="bg-slate-400 text-white cursor-not-allowed">
|
<Button size="sm" disabled className="bg-slate-400 text-white cursor-not-allowed">
|
||||||
|
|
@ -102,17 +112,17 @@ export default function ReportDetailPage() {
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
case 'completed':
|
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">
|
<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" />下载报告
|
<Download size={16} className="mr-1" />下载报告
|
||||||
</Button>
|
</Button>
|
||||||
)
|
) : null
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return (
|
return can('reports:create') ? (
|
||||||
<Button size="sm" onClick={handleGenerate} loading={generating} className="bg-amber-500 hover:bg-amber-600 text-white">
|
<Button size="sm" onClick={handleGenerate} loading={generating} className="bg-amber-500 hover:bg-amber-600 text-white">
|
||||||
<RefreshCw size={16} className="mr-1" />重新生成
|
<RefreshCw size={16} className="mr-1" />重新生成
|
||||||
</Button>
|
</Button>
|
||||||
)
|
) : null
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,14 @@ export default function ReportsPage() {
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set())
|
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set())
|
||||||
const [batchDeleteOpen, setBatchDeleteOpen] = useState(false)
|
const [batchDeleteOpen, setBatchDeleteOpen] = useState(false)
|
||||||
const [generatingIds, setGeneratingIds] = useState<Set<number>>(new Set())
|
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) => {
|
const toggleSelect = (id: number) => {
|
||||||
setSelectedIds(prev => {
|
setSelectedIds(prev => {
|
||||||
|
|
@ -269,12 +277,16 @@ export default function ReportsPage() {
|
||||||
<Button variant="ghost" size="sm" onClick={() => router.push(`/reports/${r.id}`)}>
|
<Button variant="ghost" size="sm" onClick={() => router.push(`/reports/${r.id}`)}>
|
||||||
<Eye size={14} className="mr-0.5" />预览
|
<Eye size={14} className="mr-0.5" />预览
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="sm" onClick={() => handleGenerate(r.id)} loading={isGenerating}>
|
{can('reports:create') && (
|
||||||
生成文档
|
<Button variant="ghost" size="sm" onClick={() => handleGenerate(r.id)} loading={isGenerating}>
|
||||||
</Button>
|
生成文档
|
||||||
<Button variant="ghost" size="sm" onClick={() => setDeleteTarget(r)}>
|
</Button>
|
||||||
<Trash2 size={14} className="text-red-500" />
|
)}
|
||||||
</Button>
|
{can('reports:create') && (
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => setDeleteTarget(r)}>
|
||||||
|
<Trash2 size={14} className="text-red-500" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
case 'generating':
|
case 'generating':
|
||||||
|
|
@ -291,12 +303,16 @@ export default function ReportsPage() {
|
||||||
<Button variant="ghost" size="sm" onClick={() => router.push(`/reports/${r.id}`)}>
|
<Button variant="ghost" size="sm" onClick={() => router.push(`/reports/${r.id}`)}>
|
||||||
<Eye size={14} className="mr-0.5" />预览
|
<Eye size={14} className="mr-0.5" />预览
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="sm" onClick={() => window.open(`/api/reports/${r.id}/download`, '_blank')}>
|
{can('reports:download') && (
|
||||||
<Download size={14} className="mr-0.5" />下载
|
<Button variant="ghost" size="sm" onClick={() => window.open(`/api/reports/${r.id}/download`, '_blank')}>
|
||||||
</Button>
|
<Download size={14} className="mr-0.5" />下载
|
||||||
<Button variant="ghost" size="sm" onClick={() => setDeleteTarget(r)}>
|
</Button>
|
||||||
<Trash2 size={14} className="text-red-500" />
|
)}
|
||||||
</Button>
|
{can('reports:create') && (
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => setDeleteTarget(r)}>
|
||||||
|
<Trash2 size={14} className="text-red-500" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
case 'failed':
|
case 'failed':
|
||||||
|
|
@ -305,25 +321,33 @@ export default function ReportsPage() {
|
||||||
<Button variant="ghost" size="sm" onClick={() => router.push(`/reports/${r.id}`)}>
|
<Button variant="ghost" size="sm" onClick={() => router.push(`/reports/${r.id}`)}>
|
||||||
<Eye size={14} className="mr-0.5" />预览
|
<Eye size={14} className="mr-0.5" />预览
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="sm" onClick={() => handleGenerate(r.id)} loading={isGenerating}>
|
{can('reports:create') && (
|
||||||
<RefreshCw size={14} className="mr-0.5" />重试
|
<Button variant="ghost" size="sm" onClick={() => handleGenerate(r.id)} loading={isGenerating}>
|
||||||
</Button>
|
<RefreshCw size={14} className="mr-0.5" />重试
|
||||||
<Button variant="ghost" size="sm" onClick={() => setDeleteTarget(r)}>
|
</Button>
|
||||||
<Trash2 size={14} className="text-red-500" />
|
)}
|
||||||
</Button>
|
{can('reports:create') && (
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => setDeleteTarget(r)}>
|
||||||
|
<Trash2 size={14} className="text-red-500" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button variant="ghost" size="sm" onClick={() => setDeleteTarget(r)}>
|
{can('reports:create') && (
|
||||||
<Trash2 size={14} className="text-red-500" />
|
<Button variant="ghost" size="sm" onClick={() => setDeleteTarget(r)}>
|
||||||
</Button>
|
<Trash2 size={14} className="text-red-500" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const can = (perm: string) => permissions.includes('*') || permissions.includes(perm)
|
||||||
|
|
||||||
const renderStatusBadge = (status: string) => {
|
const renderStatusBadge = (status: string) => {
|
||||||
if (status === 'generating') {
|
if (status === 'generating') {
|
||||||
return (
|
return (
|
||||||
|
|
@ -356,17 +380,23 @@ export default function ReportsPage() {
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{selectedIds.size > 0 && (
|
{selectedIds.size > 0 && (
|
||||||
<>
|
<>
|
||||||
<Button size="sm" onClick={handleBatchDownload}>
|
{can('reports:download') && (
|
||||||
<Archive size={16} className="mr-1" />批量下载 ({selectedIds.size})
|
<Button size="sm" onClick={handleBatchDownload}>
|
||||||
</Button>
|
<Archive size={16} className="mr-1" />批量下载 ({selectedIds.size})
|
||||||
<Button size="sm" variant="danger" onClick={() => setBatchDeleteOpen(true)}>
|
</Button>
|
||||||
<Trash2 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>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Button size="sm" onClick={() => setShowCreate(!showCreate)}>
|
{can('reports:create') && (
|
||||||
<Plus size={16} className="mr-1" />新建报告
|
<Button size="sm" onClick={() => setShowCreate(!showCreate)}>
|
||||||
</Button>
|
<Plus size={16} className="mr-1" />新建报告
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -166,8 +166,16 @@ function TicketListInner({ onPaginationChange, defaultStatusFilter, showSlaColum
|
||||||
const [dateFilter, setDateFilter] = useState<Record<string, { start: string; end: string }>>({})
|
const [dateFilter, setDateFilter] = useState<Record<string, { start: string; end: string }>>({})
|
||||||
const [fieldOptions, setFieldOptions] = useState<Record<string, string[]>>({})
|
const [fieldOptions, setFieldOptions] = useState<Record<string, string[]>>({})
|
||||||
const [ticketNoFilter, setTicketNoFilter] = useState('')
|
const [ticketNoFilter, setTicketNoFilter] = useState('')
|
||||||
|
const [permissions, setPermissions] = useState<string[]>([])
|
||||||
const filterDropRef = useRef<HTMLDivElement>(null)
|
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 [colWidths, setColWidths] = useState<Record<string, number>>({})
|
||||||
const [resizingCol, setResizingCol] = useState<string | null>(null)
|
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>
|
<Button variant="secondary" size="sm" onClick={() => { setPage(1); fetchTickets() }}>搜索</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="secondary" size="sm" onClick={handleExport}><Download size={14} />导出</Button>
|
{(permissions.includes('*') || permissions.includes('tickets:export')) && (
|
||||||
|
<Button variant="secondary" size="sm" onClick={handleExport}><Download size={14} />导出</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue