feat: 修复仪表盘数据管线 — 补充应用发布种子数据 + 修复limit参数解析 + LDAP认证集成 + User类型完善
后端改进: - 修复 dashboard/recent-alerts 的 limit 参数解析 (原代码错误地将数字解析为日期) - 为 dashboard/recent-releases 添加 limit 参数解析支持 - 新增 generateAppReleases 种子数据生成函数 (10条应用发布记录) - 修复 AlertEvent 的 rule_name/rule_id/severity 数据一致性 - 添加 go-ldap/ldap/v3 依赖并集成 LDAP 认证处理器 - 添加 seed.go 自动种子数据初始化 (admin用户+LDAP配置) - User model 添加 AuthSource/LDAPConfig 字段 前端改进: - 修复 User 类型定义以匹配后端 API (real_name/is_admin/auth_source等) - 修复 Header.tsx 的 real_name 属性引用导致 TypeScript 编译错误 - 统一 API 服务层、暗色主题样式、组件联调优化 验证: - go build ./... 编译通过 - npm run build 构建通过 - 仪表盘发布历史、告警消息等区域数据正常展示 - 健康检查 /health 正常
This commit is contained in:
1 parent
97c4207ff8
commit
ff0b268984
39 files changed
+4283
-1878
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -51,20 +51,68 @@ import {
|
||||
LogOut,
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
import { users, roles, operationLogs } from '@/mocks/data';
|
||||
import type { User as UserType, Role, OperationLog } from '@/types';
|
||||
import { roles, operationLogs } from '@/mocks/data';
|
||||
import { userApi } from '@/services/api';
|
||||
import type { Role, OperationLog } from '@/types';
|
||||
|
||||
// 后端用户数据转换为前端兼容格式
|
||||
function mapUser(u: any) {
|
||||
return {
|
||||
...u,
|
||||
status: u.status === 1 || u.status === 'active' ? ('active' as const) : ('inactive' as const),
|
||||
roles: Array.isArray(u.roles) ? u.roles : u.role ? [u.role] : [],
|
||||
lastLoginAt: u.last_login ?? u.lastLoginAt,
|
||||
createdAt: u.created_at ?? u.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
const emptyForm = {
|
||||
username: '',
|
||||
nickname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
role: '',
|
||||
password: '',
|
||||
status: 1 as number,
|
||||
};
|
||||
|
||||
export function SystemManager() {
|
||||
const [activeTab, setActiveTab] = useState('users');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedUser, setSelectedUser] = useState<UserType | null>(null);
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedUser, setSelectedUser] = useState<any | null>(null);
|
||||
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
|
||||
|
||||
// 创建/编辑用户弹窗
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState<any | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [form, setForm] = useState({ ...emptyForm });
|
||||
|
||||
const loadUsers = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await userApi.getUsers({ page: 1, page_size: 100 });
|
||||
const rawUsers = res?.list ?? [];
|
||||
setUsers(rawUsers.map(mapUser));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch users:', error);
|
||||
setUsers([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadUsers();
|
||||
}, []);
|
||||
|
||||
const filteredUsers = users.filter(
|
||||
(u) =>
|
||||
u.username.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
u.nickname.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
u.email.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
(u.username || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(u.nickname || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(u.email || '').toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const filteredLogs = operationLogs.filter(
|
||||
@@ -74,7 +122,7 @@ export function SystemManager() {
|
||||
log.action.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const getUserStatusBadge = (status: UserType['status']) => {
|
||||
const getUserStatusBadge = (status: 'active' | 'inactive') => {
|
||||
return status === 'active' ? (
|
||||
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 bg-emerald-500/10">
|
||||
<CheckCircle className="h-3 w-3 mr-1" />
|
||||
@@ -100,6 +148,64 @@ export function SystemManager() {
|
||||
);
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
setEditingUser(null);
|
||||
setForm({ ...emptyForm });
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const openEdit = (user: any) => {
|
||||
setEditingUser(user);
|
||||
setForm({
|
||||
username: user.username || '',
|
||||
nickname: user.nickname || '',
|
||||
email: user.email || '',
|
||||
phone: user.phone || '',
|
||||
role: Array.isArray(user.roles) ? user.roles[0] || '' : user.role || '',
|
||||
password: '',
|
||||
status: user.status === 'active' ? 1 : 0,
|
||||
});
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload: any = {
|
||||
username: form.username,
|
||||
nickname: form.nickname,
|
||||
email: form.email,
|
||||
phone: form.phone,
|
||||
role: form.role,
|
||||
status: form.status,
|
||||
};
|
||||
if (form.password) {
|
||||
payload.password = form.password;
|
||||
}
|
||||
if (editingUser) {
|
||||
await userApi.updateUser(editingUser.id, payload);
|
||||
} else {
|
||||
await userApi.createUser(payload);
|
||||
}
|
||||
setDialogOpen(false);
|
||||
await loadUsers();
|
||||
} catch (error) {
|
||||
console.error('Failed to save user:', error);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (user: any) => {
|
||||
if (!window.confirm(`确认删除用户「${user.nickname || user.username}」吗?`)) return;
|
||||
try {
|
||||
await userApi.deleteUser(user.id);
|
||||
await loadUsers();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete user:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page Header */}
|
||||
@@ -143,7 +249,7 @@ export function SystemManager() {
|
||||
className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||
/>
|
||||
</div>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={openCreate}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建用户
|
||||
</Button>
|
||||
@@ -165,68 +271,94 @@ export function SystemManager() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredUsers.map((user) => (
|
||||
<TableRow key={user.id} className="border-slate-800 hover:bg-slate-800/50">
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-sm font-medium">
|
||||
{user.nickname.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-200">{user.nickname}</p>
|
||||
<p className="text-xs text-slate-500">@{user.username}</p>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1 text-sm text-slate-400">
|
||||
<Mail className="h-3.5 w-3.5" />
|
||||
{user.email}
|
||||
</div>
|
||||
{user.phone && (
|
||||
<div className="flex items-center gap-1 text-sm text-slate-400">
|
||||
<Phone className="h-3.5 w-3.5" />
|
||||
{user.phone}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{user.roles.map((role) => (
|
||||
<Badge key={role} variant="outline" className="border-slate-700 text-slate-400">
|
||||
{role}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{getUserStatusBadge(user.status)}</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-400">
|
||||
{user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString() : '--'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-slate-400 hover:text-slate-100"
|
||||
onClick={() => setSelectedUser(user)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-slate-100">
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-red-400">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
{loading ? (
|
||||
<TableRow className="border-slate-800">
|
||||
<TableCell colSpan={6} className="text-center py-12">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="h-6 w-6 border-2 border-slate-700 border-t-blue-500 rounded-full animate-spin" />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
) : filteredUsers.length === 0 ? (
|
||||
<TableRow className="border-slate-800">
|
||||
<TableCell colSpan={6} className="text-center py-12 text-sm text-slate-500">
|
||||
暂无用户数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredUsers.map((user) => (
|
||||
<TableRow key={user.id} className="border-slate-800 hover:bg-slate-800/50">
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-sm font-medium">
|
||||
{(user.nickname || user.username || '?').charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-200">{user.nickname || user.username}</p>
|
||||
<p className="text-xs text-slate-500">@{user.username}</p>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1 text-sm text-slate-400">
|
||||
<Mail className="h-3.5 w-3.5" />
|
||||
{user.email || '--'}
|
||||
</div>
|
||||
{user.phone && (
|
||||
<div className="flex items-center gap-1 text-sm text-slate-400">
|
||||
<Phone className="h-3.5 w-3.5" />
|
||||
{user.phone}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(user.roles || []).map((role: string) => (
|
||||
<Badge key={role} variant="outline" className="border-slate-700 text-slate-400">
|
||||
{role}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{getUserStatusBadge(user.status)}</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-400">
|
||||
{user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString() : '--'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-slate-400 hover:text-slate-100"
|
||||
onClick={() => setSelectedUser(user)}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-slate-400 hover:text-slate-100"
|
||||
onClick={() => openEdit(user)}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-slate-400 hover:text-red-400"
|
||||
onClick={() => handleDelete(user)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
@@ -536,7 +668,7 @@ export function SystemManager() {
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">创建时间</p>
|
||||
<p className="text-sm text-slate-200">
|
||||
{selectedUser && new Date(selectedUser.createdAt).toLocaleDateString()}
|
||||
{selectedUser && selectedUser.createdAt ? new Date(selectedUser.createdAt).toLocaleDateString() : '--'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -544,7 +676,7 @@ export function SystemManager() {
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-300 mb-2">角色</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedUser?.roles.map((role) => (
|
||||
{selectedUser?.roles.map((role: string) => (
|
||||
<Badge key={role} variant="outline" className="border-slate-700 text-slate-400">
|
||||
{role}
|
||||
</Badge>
|
||||
@@ -558,13 +690,129 @@ export function SystemManager() {
|
||||
<Key className="h-4 w-4 mr-2" />
|
||||
重置密码
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
|
||||
<Button
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={() => {
|
||||
if (selectedUser) {
|
||||
setSelectedUser(null);
|
||||
openEdit(selectedUser);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
编辑
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Create / Edit User Dialog */}
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<User className="h-5 w-5 text-blue-400" />
|
||||
{editingUser ? '编辑用户' : '新建用户'}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">
|
||||
{editingUser ? '修改用户信息' : '创建新的系统用户'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-slate-500">用户名</label>
|
||||
<Input
|
||||
value={form.username}
|
||||
onChange={(e) => setForm({ ...form, username: e.target.value })}
|
||||
disabled={!!editingUser}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||
placeholder="登录用户名"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-slate-500">昵称</label>
|
||||
<Input
|
||||
value={form.nickname}
|
||||
onChange={(e) => setForm({ ...form, nickname: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||
placeholder="显示昵称"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-slate-500">邮箱</label>
|
||||
<Input
|
||||
value={form.email}
|
||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||
placeholder="user@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-slate-500">手机号</label>
|
||||
<Input
|
||||
value={form.phone}
|
||||
onChange={(e) => setForm({ ...form, phone: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||
placeholder="可选"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-slate-500">角色</label>
|
||||
<Select value={form.role} onValueChange={(v) => setForm({ ...form, role: v })}>
|
||||
<SelectTrigger className="bg-slate-800/50 border-slate-700 text-slate-200">
|
||||
<SelectValue placeholder="选择角色" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
{roles.map((role) => (
|
||||
<SelectItem key={role.id} value={role.name}>
|
||||
{role.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-slate-500">状态</label>
|
||||
<Select
|
||||
value={String(form.status)}
|
||||
onValueChange={(v) => setForm({ ...form, status: Number(v) })}
|
||||
>
|
||||
<SelectTrigger className="bg-slate-800/50 border-slate-700 text-slate-200">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectItem value="1">正常</SelectItem>
|
||||
<SelectItem value="0">禁用</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs text-slate-500">
|
||||
密码{editingUser ? '(留空则不修改)' : ''}
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
className="bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
|
||||
placeholder={editingUser ? '留空则不修改' : '设置初始密码'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => setDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleSubmit} disabled={saving}>
|
||||
{saving ? '保存中...' : '保存'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user