update:Dialog替换

This commit is contained in:
xiaozhu
2026-09-02 10:10:41 +08:00
committed by tao.chen
parent ed58344add
commit d0f0fd2922
16 changed files with 738 additions and 675 deletions
@@ -1,25 +1,16 @@
import { X } from "lucide-react";
import { type Employee, type Workspace } from "../../services/api";
import {
AppFormDialog,
dialogPrimaryButtonClass,
dialogSecondaryButtonClass,
} from "~/components/common/AppFormDialog";
import { Button } from "~/components/ui/button";
import {
formFieldClass,
modalBackdropClass,
modalCompactClass,
modalEyebrowClass,
modalFooterClass,
modalFormClass,
modalHeaderClass,
modalTitleClass,
} from "../platform/modalUi";
import { UserMultiSelect } from "./UserMultiSelect";
const CLOSE_BUTTON_CLASS =
"size-[34px] rounded-[7px] border border-[#dfe6ed] bg-white hover:bg-[#f7faff] hover:border-[#b9c9da]";
const SECONDARY_BUTTON_CLASS =
"h-[37px] gap-[7px] rounded-md border-[#d8e1e9] bg-white px-[15px] text-xs font-[650] text-[#56687c]";
const PRIMARY_BUTTON_CLASS =
"h-[37px] gap-[7px] rounded-md border border-[#126ac3] bg-gradient-to-br from-[#1881e7] to-[#1268c9] px-[15px] text-xs font-[650] text-white shadow-[0_6px_15px_rgb(20_111_202/18%)] enabled:hover:from-[#1175d8] enabled:hover:to-[#0e5bad]";
export function ImportMemberDialog({
open,
selectedProject,
@@ -27,7 +18,6 @@ export function ImportMemberDialog({
selectedUserIds,
existingMemberIds,
saving,
onNotify,
onChangeSelectedUserIds,
onSubmit,
onClose,
@@ -43,60 +33,49 @@ export function ImportMemberDialog({
onSubmit: () => Promise<void> | void;
onClose: () => void;
}) {
if (!open || !selectedProject) return null;
return (
<div className={modalBackdropClass} role="presentation">
<section className={modalCompactClass} role="dialog" aria-modal="true">
<div className={modalHeaderClass}>
<div>
<span className={modalEyebrowClass}>IMPORT MEMBER</span>
<h2 className={modalTitleClass}> {selectedProject?.workspace_name ?? "项目"}</h2>
</div>
<AppFormDialog
open={open && selectedProject !== null}
onOpenChange={(nextOpen) => {
if (!nextOpen) onClose();
}}
eyebrow="IMPORT MEMBER"
title={`导入成员到 ${selectedProject?.workspace_name ?? "项目"}`}
footer={
<>
<Button
variant="ghost"
size="icon-sm"
variant="outline"
size="sm"
type="button"
aria-label="关闭"
onClick={() => onClose()}
className={CLOSE_BUTTON_CLASS}
className={dialogSecondaryButtonClass}
>
<X size={16} />
</Button>
</div>
<form className={modalFormClass}>
<label className={`${formFieldClass} pb-[120px]`}>
<span><span className="text-[#e74c3c]">*</span></span>
<UserMultiSelect
users={availableUsers}
selectedUserIds={selectedUserIds}
onChange={onChangeSelectedUserIds}
existingMemberIds={existingMemberIds}
/>
</label>
<div className={modalFooterClass}>
<Button
variant="outline"
size="sm"
type="button"
onClick={() => onClose()}
className={SECONDARY_BUTTON_CLASS}
>
</Button>
<Button
variant="default"
size="sm"
type="button"
disabled={saving || selectedUserIds.length === 0}
onClick={() => void onSubmit()}
className={PRIMARY_BUTTON_CLASS}
>
{saving ? "添加中…" : `添加 (${selectedUserIds.length})`}
</Button>
</div>
</form>
</section>
</div>
<Button
variant="default"
size="sm"
type="button"
disabled={saving || selectedUserIds.length === 0}
onClick={() => void onSubmit()}
className={dialogPrimaryButtonClass}
>
{saving ? "添加中…" : `添加 (${selectedUserIds.length})`}
</Button>
</>
}
>
<div className={modalFormClass}>
<label className={`${formFieldClass} pb-[120px]`}>
<span><span className="text-[#e74c3c]">*</span></span>
<UserMultiSelect
users={availableUsers}
selectedUserIds={selectedUserIds}
onChange={onChangeSelectedUserIds}
existingMemberIds={existingMemberIds}
/>
</label>
</div>
</AppFormDialog>
);
}
@@ -1,26 +1,15 @@
import { useEffect, useState } from "react";
import { X } from "lucide-react";
import type { PlatformPermission, Role } from "../../services/api";
import { Button } from "~/components/ui/button";
import {
modalBackdropClass,
modalCompactClass,
modalEyebrowClass,
modalFooterClass,
modalFormClass,
modalHeaderClass,
modalTitleClass,
} from "../platform/modalUi";
AppFormDialog,
dialogPrimaryButtonClass,
dialogSecondaryButtonClass,
} from "~/components/common/AppFormDialog";
import { Button } from "~/components/ui/button";
import { modalFormClass } from "../platform/modalUi";
import { PermissionCheckboxList } from "./PermissionCheckboxList";
const CLOSE_BUTTON_CLASS =
"size-[34px] rounded-[7px] border border-[#dfe6ed] bg-white hover:bg-[#f7faff] hover:border-[#b9c9da]";
const SECONDARY_BUTTON_CLASS =
"h-[37px] gap-[7px] rounded-md border-[#d8e1e9] bg-white px-[15px] text-xs font-[650] text-[#56687c]";
const PRIMARY_BUTTON_CLASS =
"h-[37px] gap-[7px] rounded-md border border-[#126ac3] bg-gradient-to-br from-[#1881e7] to-[#1268c9] px-[15px] text-xs font-[650] text-white shadow-[0_6px_15px_rgb(20_111_202/18%)] enabled:hover:from-[#1175d8] enabled:hover:to-[#0e5bad]";
export function PermissionEditDialog({
open,
target,
@@ -41,7 +30,6 @@ export function PermissionEditDialog({
const [selectedCodes, setSelectedCodes] = useState<string[]>(() => {
if (!open || !target) return [];
const initial = [...currentCodes];
// admin 角色必须保留 system:view(后端守卫);前端锁定勾选兜底。
if (target.role_code === "admin" && !initial.includes("system:view")) {
initial.push("system:view");
}
@@ -69,45 +57,22 @@ export function PermissionEditDialog({
);
};
if (!open || !target) return null;
return (
<div className={modalBackdropClass} role="presentation">
<section className={modalCompactClass} role="dialog" aria-modal="true">
<div className={modalHeaderClass}>
<div>
<span className={modalEyebrowClass}>PERMISSIONS</span>
<h2 className={modalTitleClass}> · {target.role_name}</h2>
</div>
<Button
variant="ghost"
size="icon-sm"
type="button"
aria-label="关闭"
onClick={onClose}
className={CLOSE_BUTTON_CLASS}
>
<X size={16} />
</Button>
</div>
<div className={modalFormClass}>
{target.role_code === "admin" && (
<p className="role-error">admin system:view </p>
)}
<PermissionCheckboxList
permissions={permissions}
selected={selectedCodes}
targetRoleCode={target.role_code}
onToggle={togglePermission}
/>
</div>
<div className={modalFooterClass}>
<AppFormDialog
open={open && target !== null}
onOpenChange={(nextOpen) => {
if (!nextOpen) onClose();
}}
eyebrow="PERMISSIONS"
title={target ? `权限配置 · ${target.role_name}` : "权限配置"}
footer={
<>
<Button
variant="outline"
size="sm"
type="button"
onClick={onClose}
className={SECONDARY_BUTTON_CLASS}
className={dialogSecondaryButtonClass}
>
</Button>
@@ -117,12 +82,26 @@ export function PermissionEditDialog({
type="button"
disabled={saving}
onClick={() => onSubmit(selectedCodes)}
className={PRIMARY_BUTTON_CLASS}
className={dialogPrimaryButtonClass}
>
{saving ? "保存中…" : "保存权限"}
</Button>
</div>
</section>
</div>
</>
}
>
<div className={modalFormClass}>
{target?.role_code === "admin" && (
<p className="role-error">admin system:view </p>
)}
{target ? (
<PermissionCheckboxList
permissions={permissions}
selected={selectedCodes}
targetRoleCode={target.role_code}
onToggle={togglePermission}
/>
) : null}
</div>
</AppFormDialog>
);
}
@@ -1,25 +1,17 @@
import { X } from "lucide-react";
import { type Workspace } from "../../services/api";
import {
AppFormDialog,
dialogPrimaryButtonClass,
dialogSecondaryButtonClass,
} from "~/components/common/AppFormDialog";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Textarea } from "~/components/ui/textarea";
import {
formFieldClass,
modalBackdropClass,
modalCompactClass,
modalEyebrowClass,
modalFooterClass,
modalFormClass,
modalHeaderClass,
modalTitleClass,
} from "../platform/modalUi";
const CLOSE_BUTTON_CLASS =
"size-[34px] rounded-[7px] border border-[#dfe6ed] bg-white hover:bg-[#f7faff] hover:border-[#b9c9da]";
const SECONDARY_BUTTON_CLASS =
"h-[37px] gap-[7px] rounded-md border-[#d8e1e9] bg-white px-[15px] text-xs font-[650] text-[#56687c]";
const PRIMARY_BUTTON_CLASS =
"h-[37px] gap-[7px] rounded-md border border-[#126ac3] bg-gradient-to-br from-[#1881e7] to-[#1268c9] px-[15px] text-xs font-[650] text-white shadow-[0_6px_15px_rgb(20_111_202/18%)] enabled:hover:from-[#1175d8] enabled:hover:to-[#0e5bad]";
const FORM_INPUT_CLASS =
"h-[39px] w-full rounded-md border border-[#d6e0e9] bg-white px-[11px] text-xs text-[#283a4e] outline-none focus:border-[#5b9ddb] focus:shadow-[0_0_0_3px_rgb(38_125_207/8%)] disabled:cursor-not-allowed disabled:opacity-50";
const FORM_TEXTAREA_CLASS =
@@ -30,7 +22,6 @@ export function ProjectEditDialog({
editingProject,
projectForm,
saving,
onNotify,
onChangeForm,
onSubmit,
onClose,
@@ -44,91 +35,78 @@ export function ProjectEditDialog({
onSubmit: (event: React.FormEvent) => Promise<void> | void;
onClose: () => void;
}) {
if (!open) return null;
return (
<div className={modalBackdropClass} role="presentation">
<section className={modalCompactClass} role="dialog" aria-modal="true">
<div className={modalHeaderClass}>
<div>
<span className={modalEyebrowClass}>PROJECT</span>
<h2 className={modalTitleClass}>{editingProject ? "编辑项目" : "新建项目"}</h2>
</div>
<AppFormDialog
open={open}
onOpenChange={(nextOpen) => {
if (!nextOpen) onClose();
}}
eyebrow="PROJECT"
title={editingProject ? "编辑项目" : "新建项目"}
>
<form className={modalFormClass} onSubmit={(event) => void onSubmit(event)}>
{!editingProject && (
<label className={formFieldClass}>
<span><span className="text-[#e74c3c]">*</span></span>
<Input
className={FORM_INPUT_CLASS}
autoFocus
value={projectForm.workspace_code}
onChange={(event) => onChangeForm({ ...projectForm, workspace_code: event.target.value })}
placeholder="例如:model-development(小写字母、数字、连字符,3-32 字符)"
/>
</label>
)}
<label className={formFieldClass}>
<span><span className="text-[#e74c3c]">*</span></span>
<Input
className={FORM_INPUT_CLASS}
value={projectForm.workspace_name}
onChange={(event) => onChangeForm({ ...projectForm, workspace_name: event.target.value })}
placeholder="请输入项目名称"
/>
</label>
<label className={formFieldClass}>
<span></span>
<Input
className={FORM_INPUT_CLASS}
type="number"
value={projectForm.quota_bytes}
onChange={(event) => onChangeForm({ ...projectForm, quota_bytes: parseInt(event.target.value) || 0 })}
placeholder="0 表示无配额限制"
/>
</label>
<label className={formFieldClass}>
<span></span>
<Textarea
className={FORM_TEXTAREA_CLASS}
value={projectForm.description}
onChange={(event) => onChangeForm({ ...projectForm, description: event.target.value })}
placeholder="请输入项目描述(可选)"
rows={4}
/>
</label>
<div className="mt-[3px] flex shrink-0 justify-end gap-2 border-t border-[#e8edf2] bg-white -mx-[22px] px-[22px] py-3.5">
<Button
variant="ghost"
size="icon-sm"
variant="outline"
size="sm"
type="button"
aria-label="关闭"
onClick={() => onClose()}
className={CLOSE_BUTTON_CLASS}
className={dialogSecondaryButtonClass}
>
<X size={16} />
</Button>
<Button
variant="default"
size="sm"
type="submit"
disabled={saving}
className={dialogPrimaryButtonClass}
>
{saving ? "保存中…" : "保存"}
</Button>
</div>
<form className={modalFormClass} onSubmit={(event) => void onSubmit(event)}>
{!editingProject && (
<label className={formFieldClass}>
<span><span className="text-[#e74c3c]">*</span></span>
<Input
className={FORM_INPUT_CLASS}
autoFocus
value={projectForm.workspace_code}
onChange={(event) => onChangeForm({ ...projectForm, workspace_code: event.target.value })}
placeholder="例如:model-development(小写字母、数字、连字符,3-32 字符)"
/>
</label>
)}
<label className={formFieldClass}>
<span><span className="text-[#e74c3c]">*</span></span>
<Input
className={FORM_INPUT_CLASS}
value={projectForm.workspace_name}
onChange={(event) => onChangeForm({ ...projectForm, workspace_name: event.target.value })}
placeholder="请输入项目名称"
/>
</label>
<label className={formFieldClass}>
<span></span>
<Input
className={FORM_INPUT_CLASS}
type="number"
value={projectForm.quota_bytes}
onChange={(event) => onChangeForm({ ...projectForm, quota_bytes: parseInt(event.target.value) || 0 })}
placeholder="0 表示无配额限制"
/>
</label>
<label className={formFieldClass}>
<span></span>
<Textarea
className={FORM_TEXTAREA_CLASS}
value={projectForm.description}
onChange={(event) => onChangeForm({ ...projectForm, description: event.target.value })}
placeholder="请输入项目描述(可选)"
rows={4}
/>
</label>
<div className={modalFooterClass}>
<Button
variant="outline"
size="sm"
type="button"
onClick={() => onClose()}
className={SECONDARY_BUTTON_CLASS}
>
</Button>
<Button
variant="default"
size="sm"
type="submit"
disabled={saving}
className={PRIMARY_BUTTON_CLASS}
>
{saving ? "保存中…" : "保存"}
</Button>
</div>
</form>
</section>
</div>
</form>
</AppFormDialog>
);
}
@@ -4,6 +4,7 @@ import { Plus, Search } from "lucide-react";
import { ApiRequestError, type Employee, type Workspace, type WorkspaceMember } from "../../services/api";
import { useApi, useAuth } from "../../context/AuthContext";
import { Button } from "~/components/ui/button";
import { ConfirmDialog } from "~/components/common/ConfirmDialog";
import {
Table,
TableBody,
@@ -52,6 +53,7 @@ export function ProjectManagementPage({
const [membersDrawerOpen, setMembersDrawerOpen] = useState(false);
const [currentProjectMembers, setCurrentProjectMembers] = useState<WorkspaceMember[]>([]);
const [membersLoading, setMembersLoading] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<Workspace | null>(null);
const canManage = user?.role_code === "admin";
@@ -138,8 +140,7 @@ export function ProjectManagementPage({
}
};
const deleteProject = async (project: Workspace): Promise<void> => {
if (!window.confirm(`确定要删除项目"${project.workspace_name}"吗?此操作将级联软删所有成员。`)) return;
const executeDeleteProject = async (project: Workspace): Promise<void> => {
try {
await api.deleteWorkspace(project.workspace_id);
setProjects((current) => current.filter((p) => p.workspace_id !== project.workspace_id));
@@ -332,7 +333,7 @@ export function ProjectManagementPage({
className="is-danger"
disabled={!canManage || project.status === "disabled"}
title={project.status === "disabled" ? "已删除的项目不能操作" : "删除项目"}
onClick={() => void deleteProject(project)}
onClick={() => setDeleteTarget(project)}
>
</button>
@@ -382,6 +383,26 @@ export function ProjectManagementPage({
onRemoveMember={(userId) => void removeMember(userId)}
onNotify={onNotify}
/>
<ConfirmDialog
open={deleteTarget !== null}
onOpenChange={(nextOpen) => {
if (!nextOpen) setDeleteTarget(null);
}}
title="确定删除项目?"
description={
deleteTarget
? `确定要删除项目"${deleteTarget.workspace_name}"吗?此操作将级联软删所有成员。`
: ""
}
confirmLabel="删除"
destructive
onConfirm={async () => {
if (!deleteTarget) return;
await executeDeleteProject(deleteTarget);
setDeleteTarget(null);
}}
/>
</section>
);
}
+83 -112
View File
@@ -1,24 +1,22 @@
import { useEffect, useState } from "react";
import { X } from "lucide-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,
modalBackdropClass,
modalCompactClass,
modalEyebrowClass,
modalFooterClass,
modalFormClass,
modalHeaderClass,
modalTitleClass,
} from "../platform/modalUi";
import { PermissionCheckboxList } from "./PermissionCheckboxList";
@@ -39,7 +37,6 @@ const EMPTY_FORM: RoleFormState = {
permission_codes: [],
};
/** 实时校验 role_code:空值不报错,提交时由必填拦截兜底。 */
function validateRoleCode(value: string): string {
const code = value.trim();
if (!code) return "";
@@ -58,13 +55,6 @@ function validateRoleCode(value: string): string {
return "";
}
const CLOSE_BUTTON_CLASS =
"size-[34px] rounded-[7px] border border-[#dfe6ed] bg-white hover:bg-[#f7faff] hover:border-[#b9c9da]";
const SECONDARY_BUTTON_CLASS =
"h-[37px] gap-[7px] rounded-md border-[#d8e1e9] bg-white px-[15px] text-xs font-[650] text-[#56687c]";
const PRIMARY_BUTTON_CLASS =
"h-[37px] gap-[7px] rounded-md border border-[#126ac3] bg-gradient-to-br from-[#1881e7] to-[#1268c9] px-[15px] text-xs font-[650] text-white shadow-[0_6px_15px_rgb(20_111_202/18%)] enabled:hover:from-[#1175d8] enabled:hover:to-[#0e5bad]";
export function RoleEditDialog({
open,
editing,
@@ -117,7 +107,6 @@ export function RoleEditDialog({
};
const toggleCreatePermission = (code: string): void => {
// 新建角色为非 admin,system:* 由后端守卫禁止;此处防御性拦截
if (code.startsWith("system:")) return;
setForm((current) => ({
...current,
@@ -143,7 +132,6 @@ export function RoleEditDialog({
}
const payload: RoleCreatePayload | RoleUpdatePayload = editing
? {
// 内置角色 role_name 锁定;description 可改
...(editing.is_builtin ? {} : { role_name: form.role_name.trim() }),
description: form.description.trim() || null,
}
@@ -156,108 +144,91 @@ export function RoleEditDialog({
onSubmit(payload);
};
if (!open) return null;
return (
<div className={modalBackdropClass} role="presentation">
<section className={modalCompactClass} role="dialog" aria-modal="true">
<div className={modalHeaderClass}>
<div>
<span className={modalEyebrowClass}>ROLE</span>
<h2 className={modalTitleClass}>{editing ? "编辑角色" : "新建角色"}</h2>
</div>
<Button
variant="ghost"
size="icon-sm"
type="button"
aria-label="关闭"
onClick={onClose}
className={CLOSE_BUTTON_CLASS}
>
<X size={16} />
</Button>
</div>
<form className={`${modalFormClass} role-create-form`} 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-[#e74c3c]">*</span></span>
<input
className={formInputClass}
autoFocus
value={form.role_code}
onChange={handleRoleCodeChange}
placeholder="例如:qa_engineer"
/>
<span className="form-hint">
//线/2~64
</span>
{roleCodeError && <span className="role-error">{roleCodeError}</span>}
</label>
)}
<AppFormDialog
open={open}
onOpenChange={(nextOpen) => {
if (!nextOpen) onClose();
}}
eyebrow="ROLE"
title={editing ? "编辑角色" : "新建角色"}
>
<form className={`${modalFormClass} role-create-form`} onSubmit={(event) => void handleSubmit(event)}>
{editing ? (
<label className={formFieldClass}>
<span><span className="text-[#e74c3c]">*</span></span>
<span></span>
<input className={formInputClass} disabled value={form.role_code} />
</label>
) : (
<label className={formFieldClass}>
<span><span className="text-[#e74c3c]">*</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="请输入角色名称"
autoFocus
value={form.role_code}
onChange={handleRoleCodeChange}
placeholder="例如:qa_engineer"
/>
{editing !== null && editing.is_builtin && (
<span className="form-hint"></span>
)}
<span className="form-hint">
//线/2~64
</span>
{roleCodeError && <span className="role-error">{roleCodeError}</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="permission-groups">
<div className="permission-group__title"></div>
<PermissionCheckboxList
permissions={permissions}
selected={form.permission_codes}
targetRoleCode=""
onToggle={toggleCreatePermission}
/>
</div>
)}
<label className={formFieldClass}>
<span><span className="text-[#e74c3c]">*</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="form-hint"></span>
)}
<div className={modalFooterClass}>
<Button
variant="outline"
size="sm"
type="button"
onClick={onClose}
className={SECONDARY_BUTTON_CLASS}
>
</Button>
<Button
variant="default"
size="sm"
type="submit"
disabled={saving}
className={PRIMARY_BUTTON_CLASS}
>
{saving ? "保存中…" : "保存"}
</Button>
</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="permission-groups">
<div className="permission-group__title"></div>
<PermissionCheckboxList
permissions={permissions}
selected={form.permission_codes}
targetRoleCode=""
onToggle={toggleCreatePermission}
/>
</div>
</form>
</section>
</div>
)}
<div className="mt-[3px] flex shrink-0 justify-end gap-2 border-t border-[#e8edf2] 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>
);
}
@@ -10,6 +10,7 @@ import {
} from "../../services/api";
import { useApi, useAuth } from "../../context/AuthContext";
import { Button } from "~/components/ui/button";
import { ConfirmDialog } from "~/components/common/ConfirmDialog";
import { PermissionEditDialog } from "./PermissionEditDialog";
import { RoleEditDialog } from "./RoleEditDialog";
import {
@@ -50,6 +51,7 @@ export function RoleManagementPage({
// 权限配置弹窗
const [permissionDialogOpen, setPermissionDialogOpen] = useState(false);
const [permissionTarget, setPermissionTarget] = useState<Role | null>(null);
const [deleteTarget, setDeleteTarget] = useState<Role | null>(null);
const canManage = user?.role_code === "admin";
@@ -150,9 +152,7 @@ export function RoleManagementPage({
}
};
const removeRole = async (role: Role): Promise<void> => {
if (role.is_builtin) return;
if (!window.confirm(`确定删除角色"${role.role_name}"吗?`)) return;
const executeRemoveRole = async (role: Role): Promise<void> => {
try {
await api.deletePlatformRole(role.role_code);
setRoles((current) => current.filter(
@@ -271,7 +271,7 @@ export function RoleManagementPage({
type="button"
disabled={!canManage || role.is_builtin}
title={role.is_builtin ? "内置角色不可删除" : "删除角色"}
onClick={() => void removeRole(role)}
onClick={() => setDeleteTarget(role)}
className="h-6 gap-1 rounded-[4px] border-red-200 bg-white px-[9px] text-[11px] text-red-700 hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-[0.45]"
>
@@ -306,6 +306,26 @@ export function RoleManagementPage({
onClose={() => setPermissionDialogOpen(false)}
/>
)}
<ConfirmDialog
open={deleteTarget !== null}
onOpenChange={(nextOpen) => {
if (!nextOpen) setDeleteTarget(null);
}}
title="确定删除角色?"
description={
deleteTarget
? `确定删除角色"${deleteTarget.role_name}"吗?`
: ""
}
confirmLabel="删除"
destructive
onConfirm={async () => {
if (!deleteTarget || deleteTarget.is_builtin) return;
await executeRemoveRole(deleteTarget);
setDeleteTarget(null);
}}
/>
</section>
);
}
@@ -1,8 +1,14 @@
import { useEffect, useState } from "react";
import { Plus, Search, X } from "lucide-react";
import { Plus, Search } from "lucide-react";
import { ApiRequestError, type Employee, type Role } from "../../services/api";
import { useApi, useAuth } from "../../context/AuthContext";
import {
AppFormDialog,
dialogPrimaryButtonClass,
dialogSecondaryButtonClass,
} from "~/components/common/AppFormDialog";
import { ConfirmDialog } from "~/components/common/ConfirmDialog";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import {
@@ -17,13 +23,7 @@ import { AdminColgroup, USER_PROJECT_COL_WIDTHS } from "./AdminTable";
import {
formFieldClass,
formInputClass,
modalBackdropClass,
modalCompactClass,
modalEyebrowClass,
modalFooterClass,
modalFormClass,
modalHeaderClass,
modalTitleClass,
} from "../platform/modalUi";
import "../../styles/admin.css";
@@ -37,10 +37,6 @@ const EMPTY_FORM = {
status: "active" as "active" | "disabled" | "locked",
};
const CLOSE_BUTTON_CLASS =
"size-[34px] rounded-[7px] border border-[#dfe6ed] bg-white hover:bg-[#f7faff] hover:border-[#b9c9da]";
const SECONDARY_BUTTON_CLASS =
"h-[37px] gap-[7px] rounded-md border-[#d8e1e9] bg-white px-[15px] text-xs font-[650] text-[#56687c]";
const PRIMARY_BUTTON_CLASS =
"h-[37px] gap-[7px] rounded-md border border-[#126ac3] bg-gradient-to-br from-[#1881e7] to-[#1268c9] px-[15px] text-xs font-[650] text-white shadow-[0_6px_15px_rgb(20_111_202/18%)] enabled:hover:from-[#1175d8] enabled:hover:to-[#0e5bad]";
const FORM_INPUT_CLASS =
@@ -67,6 +63,7 @@ export function UserManagementPage({
const [dialogOpen, setDialogOpen] = useState(false);
const [form, setForm] = useState(EMPTY_FORM);
const [userSearchTerm, setUserSearchTerm] = useState("");
const [deleteTarget, setDeleteTarget] = useState<Employee | null>(null);
const canManage = user?.role_code === "admin";
@@ -179,8 +176,7 @@ export function UserManagementPage({
}
};
const remove = async (employee: Employee): Promise<void> => {
if (!window.confirm(`确定从平台删除用户"${employee.display_name}"吗?`)) return;
const executeRemove = async (employee: Employee): Promise<void> => {
try {
await api.deletePlatformEmployee(employee.user_id);
setEmployees((current) => current.filter((item) => item.user_id !== employee.user_id));
@@ -274,7 +270,7 @@ export function UserManagementPage({
type="button"
disabled={!canManage || isProtectedAdmin}
title={isProtectedAdmin ? "管理员账号不能删除" : "删除"}
onClick={() => void remove(employee)}
onClick={() => setDeleteTarget(employee)}
className={ROW_DANGER_BUTTON_CLASS}
>
@@ -287,26 +283,15 @@ export function UserManagementPage({
</TableBody>
</Table>
{dialogOpen && (
<div className={modalBackdropClass} role="presentation">
<section className={modalCompactClass} role="dialog" aria-modal="true">
<div className={modalHeaderClass}>
<div>
<span className={modalEyebrowClass}>EMPLOYEE</span>
<h2 className={modalTitleClass}>{editing ? "编辑用户" : "添加用户"}</h2>
</div>
<Button
variant="ghost"
size="icon-sm"
type="button"
aria-label="关闭"
onClick={() => setDialogOpen(false)}
className={CLOSE_BUTTON_CLASS}
>
<X size={16} />
</Button>
</div>
<form className={modalFormClass} onSubmit={(event) => void submit(event)}>
<AppFormDialog
open={dialogOpen}
onOpenChange={(nextOpen) => {
if (!nextOpen) setDialogOpen(false);
}}
eyebrow="EMPLOYEE"
title={editing ? "编辑用户" : "添加用户"}
>
<form className={modalFormClass} onSubmit={(event) => void submit(event)}>
<label className={formFieldClass}>
<span><span className="text-[#e74c3c]">*</span></span>
<Input
@@ -388,13 +373,13 @@ export function UserManagementPage({
</select>
</label>
)}
<div className={modalFooterClass}>
<div className="mt-[3px] flex shrink-0 justify-end gap-2 border-t border-[#e8edf2] bg-white -mx-[22px] px-[22px] py-3.5">
<Button
variant="outline"
size="sm"
type="button"
onClick={() => setDialogOpen(false)}
className={SECONDARY_BUTTON_CLASS}
className={dialogSecondaryButtonClass}
>
</Button>
@@ -403,15 +388,33 @@ export function UserManagementPage({
size="sm"
type="submit"
disabled={saving}
className={PRIMARY_BUTTON_CLASS}
className={dialogPrimaryButtonClass}
>
{saving ? "保存中…" : "保存"}
</Button>
</div>
</form>
</section>
</div>
)}
</AppFormDialog>
<ConfirmDialog
open={deleteTarget !== null}
onOpenChange={(nextOpen) => {
if (!nextOpen) setDeleteTarget(null);
}}
title="确定删除用户?"
description={
deleteTarget
? `确定从平台删除用户"${deleteTarget.display_name}"吗?`
: ""
}
confirmLabel="删除"
destructive
onConfirm={async () => {
if (!deleteTarget) return;
await executeRemove(deleteTarget);
setDeleteTarget(null);
}}
/>
</section>
);
}