feat: AI预测分析接入真实后端数据 + 修复CloudBilling文件损坏

- AIPredictiveAnalytics: 替换3组硬编码mock为实时数据驱动预测
  - 指标预测: 基于dashboard metrics时序数据(cpu/memory/disk)线性回归计算斜率与R²
  - 容量规划: 基于日均增长率推算阈值触及时间(天),动态生成AI扩容建议
  - 异常预测: 基于告警规则阈值+当前趋势+事件频率计算异常触发概率
  - 概览卡片数据驱动化(置信度基于R²拟合度,高风险/容量预警计数实时计算)
- CloudBilling.tsx: 修复read_file导致的行号前缀损坏(501行全部含N|前缀)
This commit is contained in:
attacker committed 2026-06-14 21:13:49 +08:00
1 parent 0ebe22d142
commit 2da4f52209
17 files changed
+2299 -322

No files matched your search

+124 -24
View File
@@ -1,5 +1,7 @@
import { useState, useEffect } from 'react';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -85,6 +87,7 @@ const emptyForm = {
};
export function SystemManager() {
const confirm = useConfirm();
const [activeTab, setActiveTab] = useState('users');
const [searchQuery, setSearchQuery] = useState('');
const [users, setUsers] = useState<any[]>([]);
@@ -108,6 +111,8 @@ export function SystemManager() {
const [ldapUsers, setLdapUsers] = useState<any[]>([]);
const [ldapSelected, setLdapSelected] = useState<Set<string>>(new Set());
const [ldapLoading, setLdapLoading] = useState(false);
const [ldapImportOUs, setLdapImportOUs] = useState<any[]>([]);
const [ldapImportOUDN, setLdapImportOUDN] = useState('');
// LDAP 配置
const [ldapConfig, setLdapConfig] = useState<any>(null);
@@ -135,6 +140,8 @@ export function SystemManager() {
const [ldapTestPass, setLdapTestPass] = useState('');
const [ldapUserTesting, setLdapUserTesting] = useState(false);
const [ldapUserTestResult, setLdapUserTestResult] = useState<any>(null);
const [editLdapUser, setEditLdapUser] = useState<any>(null);
const [editLdapForm, setEditLdapForm] = useState({ cn: '', mail: '', phone: '', title: '' });
const loadUsers = async () => {
setLoading(true);
@@ -155,6 +162,14 @@ export function SystemManager() {
loadLdapConfig();
}, []);
// LDAP 导入 Dialog 打开时加载 OU 和用户
useEffect(() => {
if (importDialogOpen === 'ldap') {
ldapApi.listOUs().then(ous => setLdapImportOUs(Array.isArray(ous) ? ous : [])).catch(() => {});
loadLdapUsers('');
}
}, [importDialogOpen]);
const filteredUsers = users.filter(
(u) =>
(u.username || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -244,7 +259,8 @@ export function SystemManager() {
};
const handleDelete = async (user: any) => {
if (!window.confirm(`确认删除用户「${user.nickname || user.username}」吗?`)) return;
const ok = await confirm({ title: '删除用户', description: `确认删除用户「${user.nickname || user.username}」吗?`, confirmText: '删除', variant: 'destructive' });
if (!ok) return;
try {
await userApi.deleteUser(user.id);
await loadUsers();
@@ -304,10 +320,10 @@ export function SystemManager() {
};
// 加载 LDAP 用户列表
const loadLdapUsers = async () => {
const loadLdapUsers = async (ouDn?: string) => {
setLdapLoading(true);
try {
const res = await ldapApi.listUsers();
const res = await ldapApi.listUsers(ouDn);
setLdapUsers(Array.isArray(res) ? res : []);
setLdapSelected(new Set());
} catch {
@@ -425,9 +441,9 @@ export function SystemManager() {
} catch (e: any) {
const msg = e?.message || '';
if (msg.includes('Already Exists') || msg.includes('68')) {
alert(`OU "${ouForm.ou_name}" 已存在,请使用其他名称`);
toast.error(`OU "${ouForm.ou_name}" 已存在,请使用其他名称`);
} else {
alert('创建失败: ' + msg);
toast.error('创建失败: ' + msg);
}
} finally { setLdapSaving(false); }
};
@@ -439,16 +455,30 @@ export function SystemManager() {
setCreateUserOpen(false);
setNewUser({ username: '', password: '', display_name: '', email: '', ou_dn: '' });
await loadLdapOUUsers(selectedOUDN);
} catch (e: any) { alert(e?.message || '创建失败'); }
} catch (e: any) { toast.error(e?.message || '创建失败'); }
finally { setLdapSaving(false); }
};
const handleDeleteLDAPEntry = async (dn: string, silent = false) => {
if (!silent && !confirm(`确定删除 ${dn}?`)) return;
if (!silent) { const ok = await confirm({ title: '删除 LDAP 条目', description: `确定删除 ${dn}?`, confirmText: '删除', variant: 'destructive' }); if (!ok) return; }
try {
await ldapApi.deleteUser(dn);
if (!silent) await loadLdapOUUsers(selectedOUDN);
} catch (e: any) { if (!silent) alert(e?.message || '删除失败'); }
} catch (e: any) { if (!silent) toast.error(e?.message || '删除失败'); }
};
const openEditLdapUser = (user: any) => {
setEditLdapUser(user);
setEditLdapForm({ cn: user.cn || '', mail: user.mail || '', phone: user.phone || '', title: user.title || '' });
};
const handleUpdateLdapUser = async () => {
if (!editLdapUser) return;
try {
await ldapApi.updateUser({ dn: editLdapUser.dn, ...editLdapForm });
setEditLdapUser(null);
await loadLdapOUUsers(selectedOUDN);
} catch (e: any) { toast.error(e?.message || '修改失败'); }
};
const handleLdapUserTest = async () => {
@@ -508,7 +538,7 @@ export function SystemManager() {
<div className="flex items-center gap-2">
<Button variant="outline" className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800" onClick={() => setImportDialogOpen('csv')}>
<Upload className="h-4 w-4 mr-2" />
批量导入用户
批量导入
</Button>
<Button variant="outline" className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800" onClick={() => setImportDialogOpen('ldap')}>
<Network className="h-4 w-4 mr-2" />
@@ -1264,14 +1294,24 @@ export function SystemManager() {
<code className="text-xs text-slate-500">{user.dn}</code>
</TableCell>
<TableCell>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-red-400 hover:text-red-300 hover:bg-red-500/10"
onClick={() => handleDeleteLDAPEntry(user.dn)}
>
<Trash2 className="h-3 w-3" />
</Button>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-slate-400 hover:text-blue-400 hover:bg-blue-500/10"
onClick={() => openEditLdapUser(user)}
>
<Edit className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-red-400 hover:text-red-300 hover:bg-red-500/10"
onClick={() => handleDeleteLDAPEntry(user.dn)}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</TableCell>
</TableRow>
))
@@ -1286,6 +1326,49 @@ export function SystemManager() {
</TabsContent>
</Tabs>
{/* 编辑 LDAP 用户 Dialog */}
<Dialog open={!!editLdapUser} onOpenChange={(v) => !v && setEditLdapUser(null)}>
<DialogContent className="max-w-md bg-slate-900 border-slate-800">
<DialogHeader>
<DialogTitle className="text-slate-100 flex items-center gap-2">
<Edit className="h-5 w-5 text-blue-400" />
编辑 LDAP 用户
</DialogTitle>
<DialogDescription className="text-slate-500">
修改 {editLdapUser?.uid} 的属性
</DialogDescription>
</DialogHeader>
{editLdapUser && (
<div className="space-y-3 py-2">
<div>
<label className="text-xs text-slate-500 mb-1 block">用户名(只读)</label>
<Input value={editLdapUser.uid || ''} disabled className="bg-slate-800/50 border-slate-700 text-slate-500" />
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">显示名称 (cn)</label>
<Input value={editLdapForm.cn} onChange={(e) => setEditLdapForm({ ...editLdapForm, cn: e.target.value })} className="bg-slate-800/50 border-slate-700 text-slate-200" />
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">邮箱</label>
<Input value={editLdapForm.mail} onChange={(e) => setEditLdapForm({ ...editLdapForm, mail: e.target.value })} placeholder="user@example.com" className="bg-slate-800/50 border-slate-700 text-slate-200" />
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">电话</label>
<Input value={editLdapForm.phone} onChange={(e) => setEditLdapForm({ ...editLdapForm, phone: e.target.value })} placeholder="13800138000" className="bg-slate-800/50 border-slate-700 text-slate-200" />
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">职位/头衔</label>
<Input value={editLdapForm.title} onChange={(e) => setEditLdapForm({ ...editLdapForm, title: e.target.value })} placeholder="运维工程师" className="bg-slate-800/50 border-slate-700 text-slate-200" />
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => setEditLdapUser(null)}>取消</Button>
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={handleUpdateLdapUser}>保存</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* User Detail Dialog */}
<Dialog open={!!selectedUser} onOpenChange={() => setSelectedUser(null)}>
<DialogContent className="bg-slate-900 border-slate-800">
@@ -1476,7 +1559,7 @@ export function SystemManager() {
<DialogHeader>
<DialogTitle className="text-slate-100 flex items-center gap-2">
<Upload className="h-5 w-5 text-blue-400" />
批量导入用户
批量导入用户(CSV)
</DialogTitle>
<DialogDescription className="text-slate-500">
上传 CSV 文件批量创建用户。请先下载模板,按格式填写后上传。
@@ -1586,16 +1669,33 @@ export function SystemManager() {
{!importResult ? (
<>
<div className="space-y-3">
<div className="flex items-center justify-between">
<p className="text-xs text-slate-500">
{ldapUsers.length > 0 ? `找到 ${ldapUsers.length} 个用户` : '点击刷新从 LDAP 获取用户列表'}
</p>
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={loadLdapUsers} disabled={ldapLoading}>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500 whitespace-nowrap">选择 OU</span>
<select
className="bg-slate-800 border border-slate-700 text-slate-200 text-xs rounded-md px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
value={ldapImportOUDN}
onChange={(e) => {
setLdapImportOUDN(e.target.value);
loadLdapUsers(e.target.value);
}}
>
<option value="">根 (全部)</option>
{ldapImportOUs.map((ou) => (
<option key={ou.dn} value={ou.dn}>{ou.name}</option>
))}
</select>
</div>
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300 ml-auto" onClick={() => loadLdapUsers(ldapImportOUDN)} disabled={ldapLoading}>
{ldapLoading ? <Loader2 className="h-4 w-4 mr-1.5 animate-spin" /> : <RefreshCw className="h-4 w-4 mr-1.5" />}
刷新列表
刷新
</Button>
</div>
<p className="text-xs text-slate-500">
{ldapUsers.length > 0 ? `找到 ${ldapUsers.length} 个用户` : '选择 OU 或点击刷新获取用户列表'}
</p>
{ldapUsers.length > 0 && (
<div className="max-h-80 overflow-auto rounded-lg border border-slate-800">
<Table>