Files
model-platform/frontend/app/features/admin/RoleEditDialog.tsx
T
2026-08-28 12:00:14 +08:00

241 lines
7.3 KiB
TypeScript

import { useEffect, useState } from "react";
import type {
PlatformPermission,
Role,
RoleCreatePayload,
RoleUpdatePayload,
} from "../../services/api";
import {
AppFormDialog,
dialogPrimaryButtonClass,
dialogSecondaryButtonClass,
} from "~/components/common/AppFormDialog";
import { Button } from "~/components/ui/button";
import {
formFieldClass,
formInputClass,
formTextareaClass,
modalFormClass,
} from "../platform/modalUi";
import { PermissionCheckboxList } from "./PermissionCheckboxList";
import {
formHintClass,
permissionGroupTitleClass,
permissionGroupsClass,
roleErrorClass,
} from "./adminUi";
const RESERVED_ROLE_CODES = new Set(["admin", "developer"]);
const ROLE_CODE_PATTERN = /^[a-z][a-z0-9_-]{1,63}$/;
type RoleFormState = {
role_code: string;
role_name: string;
description: string;
permission_codes: string[];
};
const EMPTY_FORM: RoleFormState = {
role_code: "",
role_name: "",
description: "",
permission_codes: [],
};
function validateRoleCode(value: string): string {
const code = value.trim();
if (!code) return "";
if (RESERVED_ROLE_CODES.has(code)) {
return `role_code "${code}" 是预留字,不可使用`;
}
if (!/^[a-z]/.test(code)) return "role_code 必须以小写字母开头";
if (!/^[a-z][a-z0-9_-]*$/.test(code)) {
return "role_code 仅含小写字母/数字/下划线/连字符";
}
if (code.length < 2) return "role_code 至少 2 个字符";
if (code.length > 64) return "role_code 最多 64 个字符";
if (!ROLE_CODE_PATTERN.test(code)) {
return "role_code 必须以小写字母开头,仅含字母/数字/下划线/连字符";
}
return "";
}
export function RoleEditDialog({
open,
editing,
permissions,
saving,
onNotify,
onSubmit,
onClose,
}: {
open: boolean;
editing: Role | null;
permissions: PlatformPermission[];
saving: boolean;
onNotify: (notice: { tone: "success" | "error" | "info"; message: string }) => void;
onSubmit: (payload: RoleCreatePayload | RoleUpdatePayload) => void;
onClose: () => void;
}) {
const [form, setForm] = useState<RoleFormState>(() =>
editing
? {
role_code: editing.role_code,
role_name: editing.role_name,
description: editing.description ?? "",
permission_codes: [...editing.permission_codes],
}
: EMPTY_FORM,
);
const [roleCodeError, setRoleCodeError] = useState("");
useEffect(() => {
setForm(
editing
? {
role_code: editing.role_code,
role_name: editing.role_name,
description: editing.description ?? "",
permission_codes: [...editing.permission_codes],
}
: EMPTY_FORM,
);
setRoleCodeError("");
}, [open, editing]);
const handleRoleCodeChange = (
event: React.ChangeEvent<HTMLInputElement>,
): void => {
const value = event.target.value;
setForm((current) => ({ ...current, role_code: value }));
setRoleCodeError(validateRoleCode(value));
};
const toggleCreatePermission = (code: string): void => {
if (code.startsWith("system:")) return;
setForm((current) => ({
...current,
permission_codes: current.permission_codes.includes(code)
? current.permission_codes.filter((item) => item !== code)
: [...current.permission_codes, code],
}));
};
const handleSubmit = (event: React.FormEvent): void => {
event.preventDefault();
if (!form.role_name.trim()) {
onNotify({ tone: "error", message: "请输入角色名称" });
return;
}
if (!editing) {
const codeError = validateRoleCode(form.role_code);
if (codeError) {
setRoleCodeError(codeError);
onNotify({ tone: "error", message: codeError });
return;
}
}
const payload: RoleCreatePayload | RoleUpdatePayload = editing
? {
...(editing.is_builtin ? {} : { role_name: form.role_name.trim() }),
description: form.description.trim() || null,
}
: {
role_code: form.role_code.trim(),
role_name: form.role_name.trim(),
description: form.description.trim() || undefined,
permission_codes: form.permission_codes,
};
onSubmit(payload);
};
return (
<AppFormDialog
open={open}
onOpenChange={(nextOpen) => {
if (!nextOpen) onClose();
}}
eyebrow="ROLE"
title={editing ? "编辑角色" : "新建角色"}
>
<form className={modalFormClass} onSubmit={(event) => void handleSubmit(event)}>
{editing ? (
<label className={formFieldClass}>
<span>角色编码</span>
<input className={formInputClass} disabled value={form.role_code} />
</label>
) : (
<label className={formFieldClass}>
<span>角色编码<span className="text-danger">*</span></span>
<input
className={formInputClass}
autoFocus
value={form.role_code}
onChange={handleRoleCodeChange}
placeholder="例如:qa_engineer"
/>
<span className={formHintClass}>
以小写字母开头,仅含小写字母/数字/下划线/连字符(2~64 字符)
</span>
{roleCodeError && <span className={roleErrorClass}>{roleCodeError}</span>}
</label>
)}
<label className={formFieldClass}>
<span>角色名称<span className="text-danger">*</span></span>
<input
className={formInputClass}
value={form.role_name}
disabled={editing !== null && editing.is_builtin}
onChange={(event) => setForm({ ...form, role_name: event.target.value })}
placeholder="请输入角色名称"
/>
{editing !== null && editing.is_builtin && (
<span className={formHintClass}>内置角色名称不可修改,描述可编辑</span>
)}
</label>
<label className={formFieldClass}>
<span>描述</span>
<textarea
className={formTextareaClass}
value={form.description}
onChange={(event) => setForm({ ...form, description: event.target.value })}
placeholder="请输入角色描述(可选)"
/>
</label>
{!editing && (
<div className={permissionGroupsClass}>
<div className={permissionGroupTitleClass}>初始权限</div>
<PermissionCheckboxList
permissions={permissions}
selected={form.permission_codes}
targetRoleCode=""
onToggle={toggleCreatePermission}
/>
</div>
)}
<div className="mt-[3px] flex shrink-0 justify-end gap-2 border-t border-line bg-white -mx-[22px] px-[22px] py-3.5">
<Button
variant="outline"
size="sm"
type="button"
onClick={onClose}
className={dialogSecondaryButtonClass}
>
取消
</Button>
<Button
variant="default"
size="sm"
type="submit"
disabled={saving}
className={dialogPrimaryButtonClass}
>
{saving ? "保存中…" : "保存"}
</Button>
</div>
</form>
</AppFormDialog>
);
}