217 lines
6.2 KiB
TypeScript
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>
|
|
);
|
|
}
|