feat: 支付闭环 + 运营助手 Tool Calling + 管理后台完善
- 支付系统:微信支付 mock 自动完成、NATIVE 扫码支付、JSAPI 集成 - 运营助手:Tool Calling 架构,19 个可执行工具,AI 驱动操作 - 角色管理:表格布局 + Dialog 表单 + 权限勾选 - 配置统一:config.ts 单一数据源 - API 审计:补齐 status toggle / comments 端点 - 暗黑模式硬件编码颜色全部替换为 CSS 变量
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
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;
|
||||
@@ -17,6 +22,20 @@ interface Permission {
|
||||
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[]>([]);
|
||||
@@ -24,213 +43,385 @@ export default function SettingsRolesPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [tab, setTab] = useState<'roles' | 'admins'>('roles');
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
async function loadData() {
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const headers = { Authorization: `Bearer ${token}` };
|
||||
const base = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||
|
||||
const headers = getAuthHeaders();
|
||||
const [rolesRes, permsRes] = await Promise.all([
|
||||
fetch(`${base}/api/v1/admin/settings/roles`, { headers }),
|
||||
fetch(`${base}/api/v1/admin/settings/permissions`, { headers }),
|
||||
fetch(`${API_BASE}/admin/settings/roles`, { headers }),
|
||||
fetch(`${API_BASE}/admin/settings/permissions`, { headers }),
|
||||
]);
|
||||
|
||||
if (rolesRes.ok) {
|
||||
const data = await rolesRes.json();
|
||||
setRoles(data.items || []);
|
||||
}
|
||||
if (permsRes.ok) {
|
||||
const data = await permsRes.json();
|
||||
setPermissions(data.items || []);
|
||||
}
|
||||
} catch {}
|
||||
if (rolesRes.ok) setRoles((await rolesRes.json()).items || []);
|
||||
if (permsRes.ok) setPermissions((await permsRes.json()).items || []);
|
||||
} catch (e) { console.error(e); }
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
async function createRole() {
|
||||
const name = prompt('请输入角色名称:');
|
||||
if (!name) return;
|
||||
const desc = prompt('请输入角色描述:') || '';
|
||||
const token = localStorage.getItem('adminToken');
|
||||
await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/api/v1/admin/settings/roles`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ name, description: desc }),
|
||||
});
|
||||
loadData();
|
||||
}
|
||||
|
||||
async function deleteRole(id: string) {
|
||||
if (!confirm('确定要删除这个角色吗?')) return;
|
||||
const token = localStorage.getItem('adminToken');
|
||||
await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/api/v1/admin/settings/roles/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
loadData();
|
||||
}
|
||||
useEffect(() => { loadData(); }, [loadData]);
|
||||
|
||||
const categories = [...new Set(permissions.map(p => p.category))];
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-6">加载中...</div>;
|
||||
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-2xl font-bold text-foreground">角色权限管理</h1>
|
||||
<p className="text-sm text-muted-foreground">管理系统角色和权限配置</p>
|
||||
<h1 className="text-3xl font-bold text-foreground">角色权限管理</h1>
|
||||
<p className="mt-2 text-muted-foreground">管理系统角色、权限和管理员</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={createRole}
|
||||
className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700"
|
||||
>
|
||||
新建角色
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 mb-6">
|
||||
<button
|
||||
onClick={() => setTab('roles')}
|
||||
className={`px-4 py-2 rounded-lg ${tab === 'roles' ? 'bg-brand-600 text-white' : 'bg-muted text-muted-foreground'}`}
|
||||
>
|
||||
角色管理
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab('admins')}
|
||||
className={`px-4 py-2 rounded-lg ${tab === 'admins' ? 'bg-brand-600 text-white' : 'bg-muted text-muted-foreground'}`}
|
||||
>
|
||||
管理员
|
||||
</button>
|
||||
<Button variant={tab === 'roles' ? 'default' : 'outline'} onClick={() => setTab('roles')}>角色管理</Button>
|
||||
<Button variant={tab === 'admins' ? 'default' : 'outline'} onClick={() => setTab('admins')}>管理员</Button>
|
||||
</div>
|
||||
|
||||
{tab === 'roles' ? (
|
||||
<div className="space-y-4">
|
||||
{roles.length === 0 ? (
|
||||
<div className="text-center py-12 text-muted-foreground">暂无角色</div>
|
||||
) : (
|
||||
roles.map(role => (
|
||||
<div key={role.id} className="bg-card border border-border rounded-xl p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{role.name}</div>
|
||||
<div className="text-sm text-muted-foreground">{role.description || '暂无描述'}</div>
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{(role.permissions || []).map((p: string) => (
|
||||
<span key={p} className="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded">
|
||||
{permissions.find(perm => perm.key === p)?.name || p}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => deleteRole(role.id)}
|
||||
className="text-red-500 hover:text-red-700 text-sm"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<RolesTab roles={roles} permissions={permissions} categories={categories} onReload={loadData} />
|
||||
) : (
|
||||
<AdminsList />
|
||||
<AdminsTab onReload={loadData} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AdminsList() {
|
||||
const [admins, setAdmins] = useState<any[]>([]);
|
||||
const [roles, setRoles] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
async function loadData() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const headers = { Authorization: `Bearer ${token}` };
|
||||
const base = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||
|
||||
const [adminsRes, rolesRes] = await Promise.all([
|
||||
fetch(`${base}/api/v1/admin/settings/admins`, { headers }),
|
||||
fetch(`${base}/api/v1/admin/settings/roles`, { headers }),
|
||||
]);
|
||||
|
||||
if (adminsRes.ok) {
|
||||
const data = await adminsRes.json();
|
||||
setAdmins(data.items || []);
|
||||
}
|
||||
if (rolesRes.ok) {
|
||||
const data = await rolesRes.json();
|
||||
setRoles(data.items || []);
|
||||
}
|
||||
} catch {}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function createAdmin() {
|
||||
const username = prompt('请输入管理员用户名:');
|
||||
if (!username) return;
|
||||
const password = prompt('请输入密码:');
|
||||
if (!password) return;
|
||||
const nickname = prompt('请输入昵称(可选):') || '';
|
||||
const token = localStorage.getItem('adminToken');
|
||||
await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/api/v1/admin/settings/admins`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ username, password, nickname }),
|
||||
});
|
||||
loadData();
|
||||
}
|
||||
|
||||
if (loading) return <div>加载中...</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>
|
||||
<button
|
||||
onClick={createAdmin}
|
||||
className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 mb-4"
|
||||
>
|
||||
新建管理员
|
||||
</button>
|
||||
<div className="space-y-3">
|
||||
{admins.map(admin => (
|
||||
<div key={admin.id} className="bg-card border border-border rounded-xl p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{admin.username}</div>
|
||||
<div className="text-sm text-muted-foreground">{admin.nickname || '暂无昵称'} · {admin.role?.name || '未分配角色'}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!confirm('确定要禁用这个管理员吗?')) return;
|
||||
const token = localStorage.getItem('adminToken');
|
||||
await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'}/api/v1/admin/settings/admins/${admin.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ status: 'DISABLED' }),
|
||||
});
|
||||
loadData();
|
||||
}}
|
||||
className="text-red-500 hover:text-red-700 text-sm"
|
||||
>
|
||||
禁用
|
||||
</button>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user