Files
Edu/apps/admin-portal/src/components/user-form-modal.tsx
SpecialX b3511910d1 feat(admin-portal): 完整实现 admin-portal 管理端微前端
包含 src 全部实现、Dockerfile、配置文件等
2026-07-10 19:09:12 +08:00

217 lines
6.2 KiB
TypeScript

"use client";
import { type ReactNode, useState, type FormEvent } from "react";
import type { UserViewModel, RoleViewModel } from "@/types/view-models";
import { Button, Input, Select } from "./ui";
import { t } from "@/lib/i18n";
interface UserFormModalProps {
open: boolean;
user: UserViewModel | null;
roles: RoleViewModel[];
onSubmit: (data: UserFormData) => Promise<void>;
onCancel: () => void;
}
export interface UserFormData {
email: string;
name: string;
password?: string;
roleIds: string[];
dataScope: string;
organizationId?: string;
}
export function UserFormModal({
open,
user,
roles,
onSubmit,
onCancel,
}: UserFormModalProps): ReactNode {
const [email, setEmail] = useState(user?.email ?? "");
const [name, setName] = useState(user?.name ?? "");
const [password, setPassword] = useState("");
const [roleIds, setRoleIds] = useState<string[]>(
user?.roles.map((r) => r.id) ?? [],
);
const [dataScope, setDataScope] = useState<string>(
user?.dataScope ?? "SCHOOL",
);
const [loading, setLoading] = useState(false);
if (!open) return null;
const isEdit = user !== null;
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
try {
await onSubmit({
email,
name,
password: isEdit ? undefined : password,
roleIds,
dataScope,
});
} finally {
setLoading(false);
}
};
const toggleRole = (id: string) => {
setRoleIds((prev) =>
prev.includes(id) ? prev.filter((r) => r !== id) : [...prev, id],
);
};
return (
<div
role="dialog"
aria-modal="true"
style={{
position: "fixed",
inset: 0,
background: "rgba(0,0,0,0.4)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1000,
}}
>
<div
style={{
width: 440,
maxHeight: "80vh",
overflowY: "auto",
padding: 24,
background: "var(--bg-paper)",
borderRadius: 8,
border: "1px solid var(--color-rule)",
}}
>
<h2
style={{
fontFamily: "var(--font-serif)",
fontSize: 18,
color: "var(--color-ink)",
marginBottom: 16,
}}
>
{isEdit ? t("admin.users.edit") : t("admin.users.new")}
</h2>
<form
onSubmit={handleSubmit}
style={{ display: "flex", flexDirection: "column", gap: 12 }}
>
<label style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<span style={{ fontSize: 12, color: "var(--color-ink)" }}>
{t("admin.common.email")}
</span>
<Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={isEdit}
/>
</label>
<label style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<span style={{ fontSize: 12, color: "var(--color-ink)" }}>
{t("admin.common.name")}
</span>
<Input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</label>
{!isEdit && (
<label style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<span style={{ fontSize: 12, color: "var(--color-ink)" }}>
{t("admin.users.password")}
</span>
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={8}
/>
</label>
)}
<label style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<span style={{ fontSize: 12, color: "var(--color-ink)" }}>
{t("admin.common.dataScope")}
</span>
<Select
value={dataScope}
onChange={(e) => setDataScope(e.target.value)}
>
<option value="ALL">ALL - </option>
<option value="SCHOOL">SCHOOL - </option>
<option value="GRADE">GRADE - </option>
<option value="CLASS">CLASS - </option>
<option value="SUBJECT">SUBJECT - </option>
<option value="SELF">SELF - </option>
</Select>
</label>
<fieldset
style={{
border: "1px solid var(--color-rule)",
padding: 12,
borderRadius: 4,
}}
>
<legend style={{ fontSize: 12, color: "var(--color-ink)" }}>
{t("admin.users.roles")}
</legend>
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
{roles.map((role) => (
<label
key={role.id}
style={{
display: "flex",
alignItems: "center",
gap: 6,
fontSize: 13,
}}
>
<input
type="checkbox"
checked={roleIds.includes(role.id)}
onChange={() => toggleRole(role.id)}
/>
<span>{role.name}</span>
<span
style={{ color: "var(--color-ink-muted)", fontSize: 11 }}
>
({role.code})
</span>
</label>
))}
</div>
</fieldset>
<div
style={{
display: "flex",
justifyContent: "flex-end",
gap: 8,
marginTop: 16,
}}
>
<Button variant="secondary" type="button" onClick={onCancel}>
{t("admin.common.cancel")}
</Button>
<Button variant="primary" type="submit" disabled={loading}>
{loading ? t("admin.common.loading") : t("admin.common.save")}
</Button>
</div>
</form>
</div>
</div>
);
}