Files
opsxsql/frontend/src/features/rbac/components/CreateUserModal.tsx
T
Logan.Li 48c8fddbe9 refactor(ui): unify all dialogs on radix base per guide ch.6/7
- base dialog: z raised to 300/301 (was below sidebar z-200 - dialogs
  appeared sunken under navigation), overlay bg-black/45 + 2px blur,
  unified close button with hover/focus-visible states, footer pt-4
- FormDialog normalized: width presets sm/md/lg/xl + optional
  description, single scrollable body (header/footer fixed), unified
  top-right close from base
- ModalOverlay rewritten on radix kernel, same props (+title/noScroll):
  portal/focus trap/scroll lock/ESC with proper nesting (fixes
  chain-close when ticket detail opens quick-review), 200ms animation,
  z-300 layering - was a bare div with no a11y, no animation, ESC
  via document listener that leaked to nested dialogs
- killed box-in-box: all 14 ModalOverlay callers stripped their inner
  card wrapper (double border/shadow/radius/padding gone) and duplicate
  scroll containers; EditInstanceDialog (4th standalone system) migrated
  to ModalOverlay, gains ESC/animation/portal
- visual unification per guide: dialog title text-base semibold
  (was 4 sizes), close button one form (was 5), footer buttons on
  dg-btn system (secondary/primary/danger, was 4 systems), primary
  color var(--dg-blue) everywhere (yellow/emerald/blue-500/600 mixed
  before), widths 4 presets (was 7 arbitrary values)
- dead code removed: 3 orphaned LDAP dialog components, leftover
  create-OU state/handler in BatchImportModal, unused icon imports

verified in browser: users create dialog shows single card (0 inner
cards), 12px radius, z-301/300, overlay blur, sidebar correctly
covered; tsc/build/audit gates green
2026-10-27 16:35:32 +08:00

128 lines
5.5 KiB
TypeScript

/**
* 用途:新建用户表单弹窗(用户名/显示名称/密码/邮箱/角色)
* 必需 props:onClose(关闭)、onCreated(创建成功后回调,页面负责关窗并刷新列表)
* 已知限制:成功 toast 沿用原有反馈点,由本组件内部触发
*/
import { useState } from 'react';
import { userApi } from '@/api/user';
import { toast } from '@/hooks/useToast';
import { ModalOverlay } from '@/components/ModalOverlay';
import { validateUserForm } from '../utils';
import type { FormErrors } from '../types';
interface CreateUserModalProps {
onClose: () => void;
onCreated: () => void;
}
export function CreateUserModal({ onClose, onCreated }: CreateUserModalProps) {
const [username, setUsername] = useState('');
const [displayName, setDisplayName] = useState('');
const [password, setPassword] = useState('');
const [email, setEmail] = useState('');
const [role, setRole] = useState('developer');
const [submitting, setSubmitting] = useState(false);
const [errors, setErrors] = useState<FormErrors>({});
const handleSubmit = async () => {
const validation = validateUserForm({ username, password, email });
setErrors(validation);
if (Object.keys(validation).length > 0) return;
setSubmitting(true);
try {
await userApi.create({ username, display_name: displayName, password, email: email || undefined, role });
toast('success', '用户创建成功');
onCreated();
} catch (e: any) {
toast('error', e?.response?.data?.detail || '创建失败');
} finally {
setSubmitting(false);
}
};
return (
<ModalOverlay open={true} onClose={onClose} width="md">
<div className="w-full">
<div className="flex items-center justify-between mb-5">
<h2 className="text-base font-semibold">新建用户</h2>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">用户名 *</label>
<input
value={username}
onChange={(e) => {
setUsername(e.target.value);
if (errors.username) setErrors((p) => ({ ...p, username: undefined }));
}}
className={`w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border rounded-lg text-sm focus:border-blue-400 focus:outline-none ${errors.username ? 'border-red-400' : 'border-[var(--dg-border)]'}`}
placeholder="3-30位字母、数字或下划线"
/>
{errors.username && <p className="text-xs text-red-400 mt-1">{errors.username}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-1">显示名称</label>
<input
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none"
placeholder="用户的显示名称"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">密码 *</label>
<input
type="password"
value={password}
onChange={(e) => {
setPassword(e.target.value);
if (errors.password) setErrors((p) => ({ ...p, password: undefined }));
}}
className={`w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border rounded-lg text-sm focus:border-blue-400 focus:outline-none ${errors.password ? 'border-red-400' : 'border-[var(--dg-border)]'}`}
placeholder="至少6位"
/>
{errors.password && <p className="text-xs text-red-400 mt-1">{errors.password}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-1">邮箱</label>
<input
type="email"
value={email}
onChange={(e) => {
setEmail(e.target.value);
if (errors.email) setErrors((p) => ({ ...p, email: undefined }));
}}
className={`w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border rounded-lg text-sm focus:border-blue-400 focus:outline-none ${errors.email ? 'border-red-400' : 'border-[var(--dg-border)]'}`}
placeholder="example@company.com"
/>
{errors.email && <p className="text-xs text-red-400 mt-1">{errors.email}</p>}
</div>
<div>
<label className="block text-sm font-medium mb-1">角色</label>
<select
value={role}
onChange={(e) => setRole(e.target.value)}
className="w-full px-3 py-2 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none"
>
<option value="admin">超级管理员</option>
<option value="dba">数据库管理员</option>
<option value="developer">开发人员</option>
<option value="auditor">审计员</option>
</select>
</div>
<div className="flex justify-end gap-3 pt-2">
<button onClick={onClose} className="dg-btn dg-btn-secondary dg-btn-sm">
取消
</button>
<button onClick={handleSubmit} disabled={submitting} className="dg-btn dg-btn-primary dg-btn-sm">
{submitting ? '创建中...' : '创建'}
</button>
</div>
</div>
</div>
</ModalOverlay>
);
}