104 lines
3.4 KiB
TypeScript
104 lines
3.4 KiB
TypeScript
import * as React from 'react'
|
|
|
|
import { AppDataTable, type AppDataTableColumn } from '@/components/app/AppDataTable'
|
|
import { AppEmptyState } from '@/components/app/AppEmptyState'
|
|
import { AppPageHeader } from '@/components/app/AppPageHeader'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Switch } from '@/components/ui/switch'
|
|
|
|
import { listMenus, updateMenu } from './api'
|
|
import type { SystemMenu } from './types'
|
|
|
|
export function SystemMenusPage() {
|
|
const [menus, setMenus] = React.useState<SystemMenu[]>([])
|
|
const [query, setQuery] = React.useState('')
|
|
const [error, setError] = React.useState('')
|
|
|
|
const load = React.useCallback(async () => {
|
|
try {
|
|
setMenus(await listMenus())
|
|
setError('')
|
|
} catch (reason) {
|
|
setError(reason instanceof Error ? reason.message : '加载失败')
|
|
}
|
|
}, [])
|
|
|
|
React.useEffect(() => {
|
|
void load()
|
|
}, [load])
|
|
|
|
const filteredMenus = menus.filter((menu) => {
|
|
const keyword = query.trim().toLowerCase()
|
|
if (!keyword) return true
|
|
return `${menu.code} ${menu.title} ${menu.path} ${menu.permission_code}`.toLowerCase().includes(keyword)
|
|
})
|
|
|
|
async function toggleVisible(menu: SystemMenu, checked: boolean) {
|
|
await updateMenu(menu.id, { is_visible: checked })
|
|
await load()
|
|
}
|
|
|
|
async function toggleEnabled(menu: SystemMenu, checked: boolean) {
|
|
await updateMenu(menu.id, { is_enabled: checked })
|
|
await load()
|
|
}
|
|
|
|
const columns: AppDataTableColumn<SystemMenu>[] = [
|
|
{
|
|
key: 'title',
|
|
header: '菜单',
|
|
cell: (menu) => (
|
|
<div>
|
|
<p className="font-medium text-foreground">{menu.title}</p>
|
|
<p className="mt-1 text-sm text-muted-foreground">{menu.code}</p>
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
key: 'path',
|
|
header: '路径',
|
|
cell: (menu) => <span className="text-muted-foreground">{menu.path || '-'}</span>
|
|
},
|
|
{
|
|
key: 'permission',
|
|
header: '权限码',
|
|
cell: (menu) => menu.permission_code ? <Badge variant="secondary">{menu.permission_code}</Badge> : '-'
|
|
},
|
|
{
|
|
key: 'visible',
|
|
header: '展示',
|
|
cell: (menu) => <Switch checked={menu.is_visible} onCheckedChange={(checked) => void toggleVisible(menu, checked)} />
|
|
},
|
|
{
|
|
key: 'enabled',
|
|
header: '启用',
|
|
cell: (menu) => <Switch checked={menu.is_enabled} onCheckedChange={(checked) => void toggleEnabled(menu, checked)} />
|
|
}
|
|
]
|
|
|
|
return (
|
|
<main className="min-h-screen bg-background p-6 text-foreground">
|
|
<div className="mx-auto flex max-w-7xl flex-col gap-4">
|
|
<AppPageHeader title="菜单权限" description="按后端菜单配置控制前端导航展示和可用状态。" />
|
|
|
|
<div className="rounded-lg border border-border bg-surface p-3">
|
|
<Input placeholder="搜索菜单、路径或权限码" value={query} onChange={(event) => setQuery(event.target.value)} />
|
|
</div>
|
|
|
|
{error ? <AppEmptyState title="无法加载菜单" description={error} /> : null}
|
|
{!error ? (
|
|
<AppDataTable
|
|
columns={columns}
|
|
rows={filteredMenus}
|
|
getRowKey={(menu) => menu.id}
|
|
emptyTitle="暂无菜单"
|
|
emptyDescription="执行治理初始化后,系统菜单会显示在这里。"
|
|
footer={<p className="text-sm text-muted-foreground">当前显示 {filteredMenus.length} 个菜单项</p>}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
</main>
|
|
)
|
|
}
|