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:
attacker committed 2026-06-14 02:11:10 +08:00
1 parent 97c4207ff8
commit ff0b268984
39 files changed
+4283 -1878

No files matched your search

+319 -71
View File
@@ -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>
);
}