131 lines
5.7 KiB
TypeScript
131 lines
5.7 KiB
TypeScript
'use client'
|
|
// src/app/admin/roles/page.tsx — 角色权限管理
|
|
import { useState, useEffect, useCallback } from 'react'
|
|
import { Shield, RotateCw, Save } from 'lucide-react'
|
|
|
|
interface RoleData { role: string; permissions: { key: string; enabled: boolean }[] }
|
|
|
|
export default function RolesPage() {
|
|
const [roles, setRoles] = useState<RoleData[]>([])
|
|
const [allPermissions, setAllPermissions] = useState<string[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [editing, setEditing] = useState<string | null>(null)
|
|
const [perms, setPerms] = useState<string[]>([])
|
|
const [saving, setSaving] = useState(false)
|
|
const [toast, setToast] = useState<{ type: 'ok' | 'err'; msg: string } | null>(null)
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true)
|
|
const res = await fetch('/api/admin/roles')
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
setRoles(data.roles)
|
|
setAllPermissions(data.allPermissions)
|
|
}
|
|
setLoading(false)
|
|
}, [])
|
|
|
|
useEffect(() => { load() }, [load])
|
|
|
|
const showToast = (type: 'ok' | 'err', msg: string) => {
|
|
setToast({ type, msg })
|
|
setTimeout(() => setToast(null), 3000)
|
|
}
|
|
|
|
const startEdit = (role: RoleData) => {
|
|
setEditing(role.role)
|
|
setPerms(role.permissions.filter(p => p.enabled).map(p => p.key))
|
|
}
|
|
|
|
const togglePerm = (key: string) => {
|
|
setPerms(prev => prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key])
|
|
}
|
|
|
|
const handleSave = async () => {
|
|
if (!editing) return
|
|
setSaving(true)
|
|
try {
|
|
const res = await fetch('/api/admin/roles', {
|
|
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ role: editing, permissions: perms }),
|
|
})
|
|
if (res.ok) { showToast('ok', `${editing} 角色权限已更新`); setEditing(null); load() }
|
|
else { const d = await res.json(); showToast('err', d.error || '保存失败') }
|
|
} catch { showToast('err', '网络错误') }
|
|
setSaving(false)
|
|
}
|
|
|
|
const permLabels: Record<string, string> = {
|
|
'dashboard:view': '查看仪表盘', 'services:view': '查看服务', 'services:manage': '管理服务',
|
|
'alerts:view': '查看告警', 'alerts:manage': '管理告警渠道', 'status_history:view': '查看状态历史',
|
|
'status_history:stats': '可用率统计', 'audit:view': '查看审计', 'users:view': '查看用户',
|
|
'users:manage': '管理用户', 'roles:manage': '管理角色',
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{toast && (
|
|
<div className={`fixed top-4 right-4 z-50 px-4 py-2 rounded-lg text-sm font-medium shadow-lg ${toast.type === 'ok' ? 'bg-green-600 text-white' : 'bg-red-600 text-white'}`}>
|
|
{toast.msg}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold font-[family-name:var(--font-display)]">角色权限管理</h1>
|
|
<p className="text-sm text-slate-400 mt-1">admin / editor / viewer</p>
|
|
</div>
|
|
<button onClick={load} className="px-4 py-2 text-sm border rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800 flex items-center gap-2">
|
|
<RotateCw size={14} /> 刷新
|
|
</button>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="text-center py-12 text-slate-400">加载中...</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{roles.map(role => (
|
|
<div key={role.role} className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="font-semibold capitalize flex items-center gap-2">
|
|
<Shield size={18} className={role.role === 'admin' ? 'text-indigo-600' : role.role === 'editor' ? 'text-blue-600' : 'text-slate-400'} />
|
|
{role.role}
|
|
</h2>
|
|
{editing === role.role ? (
|
|
<div className="flex gap-2">
|
|
<button onClick={handleSave} disabled={saving} className="flex items-center gap-2 px-3 py-1.5 text-sm bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50">
|
|
<Save size={14} /> {saving ? '保存中' : '保存'}
|
|
</button>
|
|
<button onClick={() => setEditing(null)} className="px-3 py-1.5 text-sm border rounded-lg">取消</button>
|
|
</div>
|
|
) : (
|
|
<button onClick={() => startEdit(role)} className="px-3 py-1.5 text-sm border rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800">编辑</button>
|
|
)}
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{role.role === 'admin' ? (
|
|
<span className="text-sm text-slate-400">admin 角色拥有全部 {allPermissions.length} 个权限</span>
|
|
) : editing === role.role ? (
|
|
allPermissions.map(p => (
|
|
<label key={p} className="flex items-center gap-2 text-sm py-1">
|
|
<input type="checkbox" checked={perms.includes(p)} onChange={() => togglePerm(p)}
|
|
className="rounded border-slate-300 text-indigo-600" />
|
|
{permLabels[p] || p}
|
|
</label>
|
|
))
|
|
) : (
|
|
role.permissions.filter(p => p.enabled).map(p => (
|
|
<span key={p.key} className="px-2 py-1 rounded bg-slate-50 dark:bg-slate-800 text-xs text-slate-600 dark:text-slate-400">
|
|
{permLabels[p.key] || p.key}
|
|
</span>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|