23edb74bce
- 支付系统:微信支付 mock 自动完成、NATIVE 扫码支付、JSAPI 集成 - 运营助手:Tool Calling 架构,19 个可执行工具,AI 驱动操作 - 角色管理:表格布局 + Dialog 表单 + 权限勾选 - 配置统一:config.ts 单一数据源 - API 审计:补齐 status toggle / comments 端点 - 暗黑模式硬件编码颜色全部替换为 CSS 变量
428 lines
16 KiB
TypeScript
428 lines
16 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState, useCallback } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { API_BASE } from '@/lib/config';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import * as Dialog from '@/components/ui/dialog';
|
|
|
|
interface Role {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
permissions: string[];
|
|
status: string;
|
|
}
|
|
|
|
interface Permission {
|
|
key: string;
|
|
name: string;
|
|
category: string;
|
|
}
|
|
|
|
interface Admin {
|
|
id: number;
|
|
username: string;
|
|
nickname: string;
|
|
roleId: string | null;
|
|
role: { id: string; name: string } | null;
|
|
status: string;
|
|
}
|
|
|
|
function getAuthHeaders() {
|
|
const token = localStorage.getItem('adminToken');
|
|
return { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
|
|
}
|
|
|
|
export default function SettingsRolesPage() {
|
|
const router = useRouter();
|
|
const [roles, setRoles] = useState<Role[]>([]);
|
|
const [permissions, setPermissions] = useState<Permission[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [tab, setTab] = useState<'roles' | 'admins'>('roles');
|
|
|
|
const loadData = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const headers = getAuthHeaders();
|
|
const [rolesRes, permsRes] = await Promise.all([
|
|
fetch(`${API_BASE}/admin/settings/roles`, { headers }),
|
|
fetch(`${API_BASE}/admin/settings/permissions`, { headers }),
|
|
]);
|
|
if (rolesRes.ok) setRoles((await rolesRes.json()).items || []);
|
|
if (permsRes.ok) setPermissions((await permsRes.json()).items || []);
|
|
} catch (e) { console.error(e); }
|
|
setLoading(false);
|
|
}, []);
|
|
|
|
useEffect(() => { loadData(); }, [loadData]);
|
|
|
|
const categories = [...new Set(permissions.map(p => p.category))];
|
|
|
|
if (loading) {
|
|
return <div className="p-6 text-center text-muted-foreground">加载中...</div>;
|
|
}
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-foreground">角色权限管理</h1>
|
|
<p className="mt-2 text-muted-foreground">管理系统角色、权限和管理员</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-4 mb-6">
|
|
<Button variant={tab === 'roles' ? 'default' : 'outline'} onClick={() => setTab('roles')}>角色管理</Button>
|
|
<Button variant={tab === 'admins' ? 'default' : 'outline'} onClick={() => setTab('admins')}>管理员</Button>
|
|
</div>
|
|
|
|
{tab === 'roles' ? (
|
|
<RolesTab roles={roles} permissions={permissions} categories={categories} onReload={loadData} />
|
|
) : (
|
|
<AdminsTab onReload={loadData} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function RolesTab({ roles, permissions, categories, onReload }: {
|
|
roles: Role[]; permissions: Permission[]; categories: string[]; onReload: () => void;
|
|
}) {
|
|
const [editRole, setEditRole] = useState<Role | null>(null);
|
|
const [open, setOpen] = useState(false);
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex justify-end mb-4">
|
|
<Button onClick={() => { setEditRole(null); setOpen(true); }}>新建角色</Button>
|
|
</div>
|
|
|
|
<RoleDialog
|
|
role={editRole}
|
|
permissions={permissions}
|
|
categories={categories}
|
|
open={open}
|
|
onOpenChange={setOpen}
|
|
onSaved={() => { setOpen(false); onReload(); }}
|
|
/>
|
|
|
|
{roles.length === 0 ? (
|
|
<div className="text-center py-12 text-muted-foreground">暂无角色</div>
|
|
) : (
|
|
<div className="bg-card border border-border rounded-xl overflow-hidden">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-border bg-muted/50">
|
|
<th className="text-left px-4 py-3 font-medium text-foreground">角色名称</th>
|
|
<th className="text-left px-4 py-3 font-medium text-foreground">描述</th>
|
|
<th className="text-left px-4 py-3 font-medium text-foreground">权限</th>
|
|
<th className="text-right px-4 py-3 font-medium text-foreground">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{roles.map(role => (
|
|
<tr key={role.id} className="border-b border-border hover:bg-muted/30">
|
|
<td className="px-4 py-3 font-medium text-foreground">{role.name}</td>
|
|
<td className="px-4 py-3 text-muted-foreground">{role.description || '-'}</td>
|
|
<td className="px-4 py-3">
|
|
<div className="flex flex-wrap gap-1">
|
|
{(role.permissions || []).length === 0 ? (
|
|
<span className="text-xs text-muted-foreground">无权限</span>
|
|
) : (
|
|
role.permissions.map(p => (
|
|
<Badge key={p} variant="secondary" className="text-xs">
|
|
{permissions.find(perm => perm.key === p)?.name || p}
|
|
</Badge>
|
|
))
|
|
)}
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3 text-right">
|
|
<div className="flex justify-end gap-2">
|
|
<Button variant="ghost" size="sm" onClick={() => { setEditRole(role); setOpen(true); }}>编辑</Button>
|
|
<Button variant="ghost" size="sm" className="text-red-500 hover:text-red-700"
|
|
onClick={async () => {
|
|
if (!confirm('确定要删除此角色吗?')) return;
|
|
await fetch(`${API_BASE}/admin/settings/roles/${role.id}`, { method: 'DELETE', headers: getAuthHeaders() });
|
|
onReload();
|
|
}}
|
|
>删除</Button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function RoleDialog({ role, permissions, categories, open, onOpenChange, onSaved }: {
|
|
role: Role | null; permissions: Permission[]; categories: string[]; open: boolean; onOpenChange: (v: boolean) => void; onSaved: () => void;
|
|
}) {
|
|
const [name, setName] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [selectedPerms, setSelectedPerms] = useState<string[]>([]);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
setName(role?.name || '');
|
|
setDescription(role?.description || '');
|
|
setSelectedPerms(role?.permissions || []);
|
|
}
|
|
}, [open, role]);
|
|
|
|
async function handleSave() {
|
|
if (!name.trim()) return;
|
|
setSaving(true);
|
|
try {
|
|
const headers = getAuthHeaders();
|
|
if (role) {
|
|
await fetch(`${API_BASE}/admin/settings/roles/${role.id}`, {
|
|
method: 'PUT', headers,
|
|
body: JSON.stringify({ name, description, permissions: selectedPerms }),
|
|
});
|
|
} else {
|
|
await fetch(`${API_BASE}/admin/settings/roles`, {
|
|
method: 'POST', headers,
|
|
body: JSON.stringify({ name, description, permissions: selectedPerms }),
|
|
});
|
|
}
|
|
onSaved();
|
|
} catch (e) { console.error(e); }
|
|
setSaving(false);
|
|
}
|
|
|
|
return (
|
|
<Dialog.Dialog open={open} onOpenChange={onOpenChange}>
|
|
<Dialog.DialogContent>
|
|
<Dialog.DialogHeader>
|
|
<Dialog.DialogTitle>{role ? '编辑角色' : '新建角色'}</Dialog.DialogTitle>
|
|
</Dialog.DialogHeader>
|
|
<div className="space-y-4 py-2">
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground mb-1">角色名称</label>
|
|
<Input value={name} onChange={e => setName(e.target.value)} placeholder="请输入角色名称" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground mb-1">描述</label>
|
|
<textarea
|
|
className="flex w-full rounded-lg border border-border bg-transparent px-3 py-2 text-sm"
|
|
rows={2}
|
|
value={description}
|
|
onChange={e => setDescription(e.target.value)}
|
|
placeholder="请输入角色描述"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground mb-2">权限设置</label>
|
|
{categories.map(cat => (
|
|
<div key={cat} className="mb-2">
|
|
<div className="text-xs font-medium text-muted-foreground mb-1">{cat}</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{permissions.filter(p => p.category === cat).map(perm => (
|
|
<label key={perm.key} className="flex items-center gap-1.5 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedPerms.includes(perm.key)}
|
|
onChange={e => {
|
|
setSelectedPerms(prev =>
|
|
e.target.checked
|
|
? [...prev, perm.key]
|
|
: prev.filter(k => k !== perm.key)
|
|
);
|
|
}}
|
|
className="rounded border-border"
|
|
/>
|
|
<span className="text-sm text-foreground">{perm.name}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<Dialog.DialogFooter>
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
|
|
<Button onClick={handleSave} disabled={!name.trim() || saving}>
|
|
{saving ? '保存中...' : '保存'}
|
|
</Button>
|
|
</Dialog.DialogFooter>
|
|
</Dialog.DialogContent>
|
|
</Dialog.Dialog>
|
|
);
|
|
}
|
|
|
|
function AdminsTab({ onReload }: { onReload: () => void }) {
|
|
const [admins, setAdmins] = useState<Admin[]>([]);
|
|
const [roles, setRoles] = useState<Role[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [open, setOpen] = useState(false);
|
|
const [editAdmin, setEditAdmin] = useState<Admin | null>(null);
|
|
|
|
const loadData = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const headers = getAuthHeaders();
|
|
const [adminsRes, rolesRes] = await Promise.all([
|
|
fetch(`${API_BASE}/admin/settings/admins`, { headers }),
|
|
fetch(`${API_BASE}/admin/settings/roles`, { headers }),
|
|
]);
|
|
if (adminsRes.ok) setAdmins((await adminsRes.json()).items || []);
|
|
if (rolesRes.ok) setRoles((await rolesRes.json()).items || []);
|
|
} catch (e) { console.error(e); }
|
|
setLoading(false);
|
|
}, []);
|
|
|
|
useEffect(() => { loadData(); }, [loadData]);
|
|
|
|
if (loading) return <div className="text-center py-8 text-muted-foreground">加载中...</div>;
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex justify-end mb-4">
|
|
<Button onClick={() => { setEditAdmin(null); setOpen(true); }}>新建管理员</Button>
|
|
</div>
|
|
|
|
<AdminDialog
|
|
admin={editAdmin}
|
|
roles={roles}
|
|
open={open}
|
|
onOpenChange={setOpen}
|
|
onSaved={() => { setOpen(false); loadData(); onReload(); }}
|
|
/>
|
|
|
|
<div className="bg-card border border-border rounded-xl overflow-hidden">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-border bg-muted/50">
|
|
<th className="text-left px-4 py-3 font-medium text-foreground">用户名</th>
|
|
<th className="text-left px-4 py-3 font-medium text-foreground">昵称</th>
|
|
<th className="text-left px-4 py-3 font-medium text-foreground">角色</th>
|
|
<th className="text-right px-4 py-3 font-medium text-foreground">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{admins.map(admin => (
|
|
<tr key={admin.id} className="border-b border-border hover:bg-muted/30">
|
|
<td className="px-4 py-3 font-medium text-foreground">{admin.username}</td>
|
|
<td className="px-4 py-3 text-muted-foreground">{admin.nickname || '-'}</td>
|
|
<td className="px-4 py-3">
|
|
<Badge variant="secondary" className="text-xs">{admin.role?.name || '未分配'}</Badge>
|
|
</td>
|
|
<td className="px-4 py-3 text-right">
|
|
<div className="flex justify-end gap-2">
|
|
<Button variant="ghost" size="sm" onClick={() => { setEditAdmin(admin); setOpen(true); }}>编辑</Button>
|
|
<Button variant="ghost" size="sm" className="text-red-500 hover:text-red-700"
|
|
onClick={async () => {
|
|
if (!confirm('确定要禁用此管理员吗?')) return;
|
|
await fetch(`${API_BASE}/admin/settings/admins/${admin.id}`, {
|
|
method: 'PUT', headers: getAuthHeaders(),
|
|
body: JSON.stringify({ status: 'DISABLED' }),
|
|
});
|
|
loadData();
|
|
}}
|
|
>禁用</Button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
{admins.length === 0 && (
|
|
<tr><td colSpan={4} className="text-center py-8 text-muted-foreground">暂无管理员</td></tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function AdminDialog({ admin, roles, open, onOpenChange, onSaved }: {
|
|
admin: Admin | null; roles: Role[]; open: boolean; onOpenChange: (v: boolean) => void; onSaved: () => void;
|
|
}) {
|
|
const [username, setUsername] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [nickname, setNickname] = useState('');
|
|
const [roleId, setRoleId] = useState('');
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
setUsername(admin?.username || '');
|
|
setPassword('');
|
|
setNickname(admin?.nickname || '');
|
|
setRoleId(admin?.roleId || '');
|
|
}
|
|
}, [open, admin]);
|
|
|
|
async function handleSave() {
|
|
if (!username.trim()) return;
|
|
if (!admin && !password.trim()) return;
|
|
setSaving(true);
|
|
try {
|
|
const headers = getAuthHeaders();
|
|
if (admin) {
|
|
await fetch(`${API_BASE}/admin/settings/admins/${admin.id}`, {
|
|
method: 'PUT', headers,
|
|
body: JSON.stringify({ nickname, roleId: roleId || null }),
|
|
});
|
|
} else {
|
|
await fetch(`${API_BASE}/admin/settings/admins`, {
|
|
method: 'POST', headers,
|
|
body: JSON.stringify({ username, password, nickname, roleId: roleId || undefined }),
|
|
});
|
|
}
|
|
onSaved();
|
|
} catch (e) { console.error(e); }
|
|
setSaving(false);
|
|
}
|
|
|
|
return (
|
|
<Dialog.Dialog open={open} onOpenChange={onOpenChange}>
|
|
<Dialog.DialogContent>
|
|
<Dialog.DialogHeader>
|
|
<Dialog.DialogTitle>{admin ? '编辑管理员' : '新建管理员'}</Dialog.DialogTitle>
|
|
</Dialog.DialogHeader>
|
|
<div className="space-y-4 py-2">
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground mb-1">用户名</label>
|
|
<Input value={username} onChange={e => setUsername(e.target.value)} placeholder="登录用户名"
|
|
disabled={!!admin} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground mb-1">{admin ? '新密码(留空不修改)' : '密码'}</label>
|
|
<Input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder={admin ? '留空则不修改' : '请输入密码'} />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground mb-1">昵称</label>
|
|
<Input value={nickname} onChange={e => setNickname(e.target.value)} placeholder="可选" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground mb-1">角色</label>
|
|
<select
|
|
className="flex w-full rounded-lg border border-border bg-transparent px-3 py-2 text-sm"
|
|
value={roleId}
|
|
onChange={e => setRoleId(e.target.value)}
|
|
>
|
|
<option value="">无角色</option>
|
|
{roles.map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<Dialog.DialogFooter>
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
|
|
<Button onClick={handleSave} disabled={!username.trim() || (!admin && !password.trim()) || saving}>
|
|
{saving ? '保存中...' : '保存'}
|
|
</Button>
|
|
</Dialog.DialogFooter>
|
|
</Dialog.DialogContent>
|
|
</Dialog.Dialog>
|
|
);
|
|
}
|