- Cache 接口新增 SetNX 方法(go-redis 原子 / redka check-then-set 单副本安全) - AlertEngine 改造为支持分布式锁: tryAcquireLock/releaseLock/GetEngineStatus - 多副本部署时仅 Leader 执行告警检查, 60s TTL 防死锁, fail-open 容错 - 新增 GET /alerts/engine/status 监控端点(is_leader/instance_id/lock_holder) - 修复 GrafanaDashboard.tsx Panel.options 类型不匹配导致构建中断 - 同步未提交的前端类型安全迁移(140+ 文件) - ROADMAP/TDD_PLAN/PROJECT_EVALUATION 三文档同步更新 验证: go build 零错误, Docker 部署 healthy, 引擎状态端点正常, 锁竞争测试通过(非 Leader 跳过检查), 5/5 核心 API 回归通过
187 lines
6.4 KiB
TypeScript
187 lines
6.4 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { User } from 'lucide-react';
|
|
import { userApi } from '@/api';
|
|
import type { JsonData } from '@/types/common';
|
|
|
|
export interface UserFormData {
|
|
username: string;
|
|
nickname: string;
|
|
email: string;
|
|
phone: string;
|
|
role: string;
|
|
password: string;
|
|
status: number;
|
|
}
|
|
|
|
interface UserFormDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
editingUser: JsonData | null;
|
|
form: UserFormData;
|
|
setForm: React.Dispatch<React.SetStateAction<UserFormData>>;
|
|
saving: boolean;
|
|
onSubmit: () => void;
|
|
}
|
|
|
|
export function UserFormDialog({
|
|
open,
|
|
onOpenChange,
|
|
editingUser,
|
|
form,
|
|
setForm,
|
|
saving,
|
|
onSubmit,
|
|
}: UserFormDialogProps) {
|
|
const [roles, setRoles] = useState<{ id: string; name: string }[]>([]);
|
|
|
|
// 对话框打开时拉取角色列表 // Logan.Li
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
let cancelled = false;
|
|
(async () => {
|
|
try {
|
|
const resp = await userApi.getRoles();
|
|
if (cancelled) return;
|
|
const list = resp?.list ?? [];
|
|
setRoles(
|
|
list.map((r: JsonData) => ({
|
|
id: String(r.id ?? ''),
|
|
name: r.name ?? r.code ?? '',
|
|
}))
|
|
);
|
|
} catch {
|
|
if (!cancelled) setRoles([]);
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [open]);
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<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={() => onOpenChange(false)}>
|
|
取消
|
|
</Button>
|
|
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={onSubmit} disabled={saving}>
|
|
{saving ? '保存中...' : '保存'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|