feat: 支付闭环 + 运营助手 Tool Calling + 管理后台完善

- 支付系统:微信支付 mock 自动完成、NATIVE 扫码支付、JSAPI 集成
- 运营助手:Tool Calling 架构,19 个可执行工具,AI 驱动操作
- 角色管理:表格布局 + Dialog 表单 + 权限勾选
- 配置统一:config.ts 单一数据源
- API 审计:补齐 status toggle / comments 端点
- 暗黑模式硬件编码颜色全部替换为 CSS 变量
This commit is contained in:
yuzhiran-dev
2026-05-20 18:29:00 +08:00
parent 728edc59ef
commit 23edb74bce
76 changed files with 1589 additions and 473 deletions
+366 -175
View File
@@ -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>
);
}