feat(admin-portal): 完整实现 admin-portal 管理端微前端
包含 src 全部实现、Dockerfile、配置文件等
This commit is contained in:
216
apps/admin-portal/src/components/user-form-modal.tsx
Normal file
216
apps/admin-portal/src/components/user-form-modal.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user