212 lines
7.8 KiB
TypeScript
212 lines
7.8 KiB
TypeScript
'use client'
|
|
import { useState, useEffect } from 'react'
|
|
import Table, { Column } from '@/components/ui/Table'
|
|
import Button from '@/components/ui/Button'
|
|
import Input from '@/components/ui/Input'
|
|
import Modal from '@/components/ui/Modal'
|
|
import Badge from '@/components/ui/Badge'
|
|
import { Plus, Pencil, Trash2 } from 'lucide-react'
|
|
|
|
interface Role {
|
|
id: number
|
|
name: string
|
|
display_name: string
|
|
permissions: string
|
|
created_at: string
|
|
}
|
|
|
|
const allPermissions = [
|
|
{ key: 'assets:read', label: '查看资产' },
|
|
{ key: 'assets:create', label: '新增资产' },
|
|
{ key: 'assets:import', label: '导入资产' },
|
|
{ key: 'assets:update', label: '编辑资产' },
|
|
{ key: 'assets:delete', label: '删除资产' },
|
|
{ key: 'assets:export:selected', label: '导出选中资产' },
|
|
{ key: 'assets:export:all', label: '导出全部资产' },
|
|
{ key: 'users:read', label: '查看用户' },
|
|
{ key: 'users:write', label: '管理用户' },
|
|
{ key: 'roles:read', label: '查看角色' },
|
|
{ key: 'roles:write', label: '管理角色' },
|
|
{ key: 'api-keys:read', label: '查看API Key' },
|
|
{ key: 'api-keys:write', label: '管理API Key' },
|
|
]
|
|
|
|
const BUILTIN_ROLES = ['admin', 'editor', 'viewer']
|
|
|
|
const roleColor: Record<string, 'blue' | 'green' | 'gray' | 'yellow'> = {
|
|
admin: 'blue', editor: 'green', viewer: 'gray',
|
|
}
|
|
|
|
function formatPermissions(permStr: string): string {
|
|
try {
|
|
const perms: string[] = JSON.parse(permStr)
|
|
if (perms.includes('*')) return '全部权限'
|
|
return perms.map(p => {
|
|
const f = allPermissions.find(a => a.key === p)
|
|
return f ? f.label : p
|
|
}).join(', ') || '无权限'
|
|
} catch { return '无权限' }
|
|
}
|
|
|
|
export default function RolesPage() {
|
|
const [roles, setRoles] = useState<Role[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [modalOpen, setModalOpen] = useState(false)
|
|
const [editRole, setEditRole] = useState<Role | null>(null)
|
|
const [form, setForm] = useState({ name: '', display_name: '', permissions: [] as string[] })
|
|
const [error, setError] = useState('')
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
const fetchRoles = async () => {
|
|
try {
|
|
const res = await fetch('/api/roles')
|
|
const d = await res.json()
|
|
if (d.roles) setRoles(d.roles)
|
|
} catch { /* ignore */ }
|
|
setLoading(false)
|
|
}
|
|
|
|
useEffect(() => { fetchRoles() }, [])
|
|
|
|
const openCreate = () => {
|
|
setEditRole(null)
|
|
setForm({ name: '', display_name: '', permissions: [] })
|
|
setError('')
|
|
setModalOpen(true)
|
|
}
|
|
|
|
const openEdit = (r: Role) => {
|
|
setEditRole(r)
|
|
let perms: string[] = []
|
|
try { perms = JSON.parse(r.permissions) } catch { /* ignore */ }
|
|
setForm({ name: r.name, display_name: r.display_name, permissions: perms })
|
|
setError('')
|
|
setModalOpen(true)
|
|
}
|
|
|
|
const togglePermission = (key: string) => {
|
|
setForm(prev => ({
|
|
...prev,
|
|
permissions: prev.permissions.includes(key)
|
|
? prev.permissions.filter(p => p !== key)
|
|
: [...prev.permissions, key],
|
|
}))
|
|
}
|
|
|
|
const handleSave = async () => {
|
|
setError('')
|
|
setSaving(true)
|
|
try {
|
|
if (editRole) {
|
|
const res = await fetch(`/api/roles/${editRole.id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ display_name: form.display_name, permissions: form.permissions }),
|
|
})
|
|
if (!res.ok) { const d = await res.json(); setError(d.error || '更新失败'); return }
|
|
} else {
|
|
if (!form.name || !form.display_name) { setError('请填写必填项'); return }
|
|
const res = await fetch('/api/roles', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(form),
|
|
})
|
|
if (!res.ok) { const d = await res.json(); setError(d.error || '创建失败'); return }
|
|
}
|
|
setModalOpen(false)
|
|
fetchRoles()
|
|
} catch { setError('操作失败') }
|
|
finally { setSaving(false) }
|
|
}
|
|
|
|
const handleDelete = async (id: number) => {
|
|
if (!confirm('确定删除此角色?')) return
|
|
const res = await fetch(`/api/roles/${id}`, { method: 'DELETE' })
|
|
if (res.ok) fetchRoles()
|
|
else { const d = await res.json(); alert(d.error || '删除失败') }
|
|
}
|
|
|
|
const columns: Column<Role>[] = [
|
|
{ key: 'name', title: '角色名', render: (r) => (
|
|
<span className="font-medium text-slate-900 dark:text-slate-100">{r.name}</span>
|
|
)},
|
|
{ key: 'display_name', title: '显示名称', render: (r) => (
|
|
<div className="flex items-center gap-2">
|
|
<span>{r.display_name}</span>
|
|
{BUILTIN_ROLES.includes(r.name) && (
|
|
<Badge color={roleColor[r.name] || 'gray'}>内置</Badge>
|
|
)}
|
|
</div>
|
|
)},
|
|
{ key: 'permissions', title: '权限', render: (r) => (
|
|
<span className="text-sm text-slate-500 dark:text-slate-400">{formatPermissions(r.permissions)}</span>
|
|
)},
|
|
{ key: 'actions', title: '操作', width: '100px', render: (r) => (
|
|
<div className="flex items-center gap-1">
|
|
<Button variant="ghost" size="sm" onClick={() => openEdit(r)}><Pencil size={14} /></Button>
|
|
{!BUILTIN_ROLES.includes(r.name) && (
|
|
<Button variant="ghost" size="sm" onClick={() => handleDelete(r.id)}><Trash2 size={14} className="text-red-500" /></Button>
|
|
)}
|
|
</div>
|
|
)},
|
|
]
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">角色权限</h1>
|
|
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1">管理系统角色与权限配置</p>
|
|
</div>
|
|
<Button size="sm" onClick={openCreate}><Plus size={16} />新建角色</Button>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="py-12 text-center text-slate-500 dark:text-slate-400">加载中...</div>
|
|
) : (
|
|
<Table columns={columns as unknown as Column<Record<string, unknown>>[]} data={roles as unknown as Record<string, unknown>[]} rowKey={(r) => String(r.id)} />
|
|
)}
|
|
|
|
<Modal open={modalOpen} onClose={() => setModalOpen(false)} title={editRole ? '编辑角色' : '新建角色'}>
|
|
<div className="space-y-4">
|
|
{!editRole && (
|
|
<Input
|
|
label="角色名(英文)"
|
|
value={form.name}
|
|
onChange={e => setForm(p => ({ ...p, name: e.target.value }))}
|
|
placeholder="e.g. supervisor"
|
|
/>
|
|
)}
|
|
<Input
|
|
label="显示名称"
|
|
value={form.display_name}
|
|
onChange={e => setForm(p => ({ ...p, display_name: e.target.value }))}
|
|
/>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">权限</label>
|
|
<div className="space-y-2 max-h-64 overflow-y-auto">
|
|
{allPermissions.map(p => (
|
|
<label key={p.key} className="flex items-center gap-2 cursor-pointer py-1">
|
|
<input
|
|
type="checkbox"
|
|
checked={form.permissions.includes(p.key) || form.permissions.includes('*')}
|
|
onChange={() => togglePermission(p.key)}
|
|
className="rounded border-slate-300 dark:border-slate-600"
|
|
/>
|
|
<span className="text-sm text-slate-700 dark:text-slate-300">{p.label}</span>
|
|
<span className="text-xs text-slate-400">{p.key}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
{error && <p className="text-sm text-red-500">{error}</p>}
|
|
<div className="flex gap-3">
|
|
<Button onClick={handleSave} loading={saving}>{editRole ? '保存' : '创建'}</Button>
|
|
<Button variant="secondary" onClick={() => setModalOpen(false)}>取消</Button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
</div>
|
|
)
|
|
}
|