FastAPI backend with auth, user, instance, ticket, query, dashboard APIs. React + TypeScript frontend with mock data UI (10 pages). SQLite + async SQLAlchemy, JWT auth, role-based access control. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
120 lines
6.6 KiB
TypeScript
120 lines
6.6 KiB
TypeScript
import { useState, useMemo } from 'react';
|
|
import { Plus, Search, Edit2, Lock, Trash2, } from 'lucide-react';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { users, roles } from '@/lib/mock';
|
|
// type User is used implicitly
|
|
|
|
const roleTagStyle: Record<string, string> = {
|
|
admin: 'bg-purple-500/15 text-purple-400',
|
|
dba: 'bg-blue-500/15 text-blue-400',
|
|
developer: 'bg-green-500/15 text-green-400',
|
|
guest: 'bg-gray-500/15 text-gray-400',
|
|
};
|
|
|
|
export default function UsersPage() {
|
|
const [search, setSearch] = useState('');
|
|
const [roleFilter, setRoleFilter] = useState('');
|
|
const [statusFilter, setStatusFilter] = useState('');
|
|
|
|
const filtered = useMemo(() => {
|
|
return users.filter(u => {
|
|
if (roleFilter && u.role !== roleFilter) return false;
|
|
if (statusFilter && u.status !== statusFilter) return false;
|
|
if (search && !u.username.includes(search) && !u.email.includes(search)) return false;
|
|
return true;
|
|
});
|
|
}, [search, roleFilter, statusFilter]);
|
|
|
|
return (
|
|
<div className="space-y-5">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold">用户管理</h1>
|
|
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">管理系统用户,分配角色和数据库访问权限</p>
|
|
</div>
|
|
<button className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg text-sm font-medium text-white hover:opacity-90 transition-opacity">
|
|
<Plus className="w-4 h-4" /> 新建用户
|
|
</button>
|
|
</div>
|
|
|
|
{/* 筛选 */}
|
|
<div className="flex items-center gap-3 p-4 dg-card flex-wrap">
|
|
<div className="relative flex-1 max-w-xs">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--dg-text-muted)]" />
|
|
<input className="w-full pl-10 pr-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm placeholder:text-[var(--dg-text-muted)] focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400/20" placeholder="搜索用户名/邮箱" value={search} onChange={e => setSearch(e.target.value)} />
|
|
</div>
|
|
<select className="px-3 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" value={roleFilter} onChange={e => setRoleFilter(e.target.value)}>
|
|
<option value="">全部角色</option>
|
|
{roles.map(r => <option key={r.id} value={r.id}>{r.name}</option>)}
|
|
</select>
|
|
<select className="px-3 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" value={statusFilter} onChange={e => setStatusFilter(e.target.value)}>
|
|
<option value="">全部状态</option>
|
|
<option value="active">启用</option>
|
|
<option value="disabled">禁用</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* 用户表格 */}
|
|
<Card className="dg-card">
|
|
<CardContent className="p-0">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]">
|
|
<th className="text-left px-5 py-3 font-semibold">用户</th>
|
|
<th className="text-left px-5 py-3 font-semibold">角色</th>
|
|
<th className="text-left px-5 py-3 font-semibold">部门</th>
|
|
<th className="text-left px-5 py-3 font-semibold">最后登录</th>
|
|
<th className="text-left px-5 py-3 font-semibold">状态</th>
|
|
<th className="text-left px-5 py-3 font-semibold">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filtered.map(user => (
|
|
<tr key={user.id} className="border-b border-[var(--dg-border-light)] hover:bg-[var(--dg-bg-hover)] transition-colors">
|
|
<td className="px-5 py-3.5">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-9 h-9 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-white text-sm font-semibold">
|
|
{user.avatar}
|
|
</div>
|
|
<div>
|
|
<div className="font-medium">{user.username}</div>
|
|
<div className="text-xs text-[var(--dg-text-muted)]">{user.email}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-5 py-3.5">
|
|
<span className={`inline-flex px-2.5 py-1 rounded-md text-xs font-medium ${(roleTagStyle as any)[user.role] || ''}`}>
|
|
{user.roleName}
|
|
</span>
|
|
</td>
|
|
<td className="px-5 py-3.5 text-[var(--dg-text-secondary)]">{user.department}</td>
|
|
<td className="px-5 py-3.5 text-[var(--dg-text-secondary)]">{user.lastLogin}</td>
|
|
<td className="px-5 py-3.5">
|
|
<button
|
|
className={`relative w-11 h-6 rounded-full transition-colors ${user.status === 'active' ? 'bg-green-500' : 'bg-[var(--dg-bg-hover)]'}`}
|
|
>
|
|
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white shadow transition-transform ${user.status === 'active' ? 'translate-x-5.5' : 'translate-x-0.5'}`} style={{ transform: user.status === 'active' ? 'translateX(20px)' : 'translateX(2px)' }} />
|
|
</button>
|
|
</td>
|
|
<td className="px-5 py-3.5">
|
|
<div className="flex gap-2">
|
|
<button className="text-blue-400 hover:text-blue-300 transition-colors"><Edit2 className="w-4 h-4" /></button>
|
|
<button className="text-purple-400 hover:text-purple-300 transition-colors"><Lock className="w-4 h-4" /></button>
|
|
<button className="text-red-400 hover:text-red-300 transition-colors"><Trash2 className="w-4 h-4" /></button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div className="flex items-center justify-between px-5 py-4 border-t border-[var(--dg-border)]">
|
|
<span className="text-sm text-[var(--dg-text-muted)]">共 {filtered.length} 条记录</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|