update:Dialog替换

This commit is contained in:
xiaozhu
2026-08-26 16:48:19 +08:00
parent d7f3db1cc4
commit e326cc1a93
16 changed files with 738 additions and 675 deletions
@@ -0,0 +1,90 @@
import type { ReactNode } from "react"
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog"
import { cn } from "~/lib/utils"
/** 与系统管理弹窗 footer 一致的次要按钮样式 */
export const dialogSecondaryButtonClass =
"h-[37px] gap-[7px] rounded-md border-[#d8e1e9] bg-white px-[15px] text-xs font-[650] text-[#56687c]"
/** 与系统管理弹窗 footer 一致的主按钮样式 */
export const dialogPrimaryButtonClass =
"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 EYEBROW_CLASS =
"text-[9px] font-extrabold tracking-[0.12em] text-[#2d82d4]"
const TITLE_CLASS = "mt-1 text-[19px] font-normal text-[#1c2d42]"
type AppFormDialogProps = {
open: boolean
onOpenChange: (open: boolean) => void
eyebrow?: string
title?: ReactNode
titleId?: string
children: ReactNode
footer?: ReactNode
contentClassName?: string
bodyClassName?: string
showCloseButton?: boolean
size?: "compact" | "panel"
}
function AppFormDialog({
open,
onOpenChange,
eyebrow,
title,
titleId,
children,
footer,
contentClassName,
bodyClassName,
showCloseButton = true,
size = "compact",
}: AppFormDialogProps) {
const maxWidth =
size === "panel"
? "sm:max-w-[min(520px,calc(100vw-40px))]"
: "sm:max-w-[min(480px,calc(100vw-40px))]"
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
showCloseButton={showCloseButton}
className={cn(
"flex max-h-[80vh] flex-col gap-0 overflow-hidden rounded-[11px] border-[#dce4eb] bg-white p-0 text-[#1c2d42] shadow-[0_24px_70px_rgb(8_27_48/25%)] ring-0",
maxWidth,
contentClassName,
)}
>
{eyebrow || title ? (
<DialogHeader className="shrink-0 space-y-0 border-b border-[#e7ecf1] px-[22px] pb-4 pt-5 text-left">
{eyebrow ? <span className={EYEBROW_CLASS}>{eyebrow}</span> : null}
{title ? (
<DialogTitle id={titleId} className={TITLE_CLASS}>
{title}
</DialogTitle>
) : null}
</DialogHeader>
) : null}
<div className={cn("min-h-0 flex-1 overflow-y-auto", bodyClassName)}>
{children}
</div>
{footer ? (
<DialogFooter className="mt-0 shrink-0 flex-row justify-end gap-2 border-t border-[#e8edf2] bg-white px-[22px] py-3.5 sm:justify-end">
{footer}
</DialogFooter>
) : null}
</DialogContent>
</Dialog>
)
}
export { AppFormDialog }
@@ -0,0 +1,74 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "~/components/ui/alert-dialog"
import { cn } from "~/lib/utils"
import {
dialogPrimaryButtonClass,
dialogSecondaryButtonClass,
} from "./AppFormDialog"
type ConfirmDialogProps = {
open: boolean
onOpenChange: (open: boolean) => void
title: string
description: string
confirmLabel?: string
cancelLabel?: string
destructive?: boolean
onConfirm: () => void | Promise<void>
}
function ConfirmDialog({
open,
onOpenChange,
title,
description,
confirmLabel = "确定",
cancelLabel = "取消",
destructive = false,
onConfirm,
}: ConfirmDialogProps) {
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent className="rounded-[11px] border-[#dce4eb] bg-white">
<AlertDialogHeader>
<AlertDialogTitle className="text-[#1c2d42]">{title}</AlertDialogTitle>
<AlertDialogDescription className="text-[#56687c]">
{description}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel
variant="outline"
size="sm"
className={dialogSecondaryButtonClass}
>
{cancelLabel}
</AlertDialogCancel>
<AlertDialogAction
variant={destructive ? "destructive" : "default"}
size="sm"
className={cn(
destructive
? "h-[37px] gap-[7px] rounded-md border-0 bg-[#d75b5b] px-[15px] text-xs font-[650] text-white hover:bg-[#a94f4f]"
: dialogPrimaryButtonClass,
)}
onClick={() => void onConfirm()}
>
{confirmLabel}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
export { ConfirmDialog }
@@ -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>
);
}
@@ -1,19 +1,16 @@
import { type FormEvent } from "react";
import Icon from "../../components/common/Icon";
import {
AppFormDialog,
dialogPrimaryButtonClass,
dialogSecondaryButtonClass,
} from "~/components/common/AppFormDialog";
import { Button } from "~/components/ui/button";
import {
destinationChipClass,
formFieldClass,
formInputClass,
iconButtonClass,
modalBackdropClass,
modalCompactClass,
modalEyebrowClass,
modalFooterClass,
modalFormClass,
modalHeaderClass,
modalTitleClass,
primaryButtonClass,
secondaryButtonClass,
} from "./modalUi";
type CreateFolderModalProps = {
@@ -35,26 +32,16 @@ export function CreateFolderModal({
onSubmit,
onClose,
}: CreateFolderModalProps) {
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}>WORKSPACE</span>
<h2 className={modalTitleClass}></h2>
</div>
<button
className={iconButtonClass}
type="button"
aria-label="关闭"
onClick={onClose}
>
<Icon name="close" />
</button>
</div>
<form className={modalFormClass} onSubmit={onSubmit}>
<AppFormDialog
open={open}
onOpenChange={(nextOpen) => {
if (!nextOpen) onClose();
}}
eyebrow="WORKSPACE"
title="新建文件夹"
>
<form className={modalFormClass} onSubmit={onSubmit}>
<div className={destinationChipClass}>
<Icon name="folder" size={16} />
{parentPath || "个人根目录"}
@@ -70,27 +57,30 @@ export function CreateFolderModal({
onChange={(event) => onNameChange(event.target.value)}
/>
</label>
<div className={modalFooterClass}>
<button
className={secondaryButtonClass}
<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
className={primaryButtonClass}
</Button>
<Button
variant="default"
size="sm"
type="submit"
disabled={busy || !name.trim()}
className={dialogPrimaryButtonClass}
>
{busy
? <span className="button-spinner" />
: <Icon name="folder" size={16} />}
{busy ? "正在创建…" : "创建文件夹"}
</button>
</Button>
</div>
</form>
</section>
</div>
</AppFormDialog>
);
}
@@ -1,21 +1,18 @@
import { type FormEvent } from "react";
import Icon from "../../components/common/Icon";
import type { ScriptType, Visibility } from "../../services/api";
import {
AppFormDialog,
dialogPrimaryButtonClass,
dialogSecondaryButtonClass,
} from "~/components/common/AppFormDialog";
import { Button } from "~/components/ui/button";
import {
destinationChipClass,
formFieldClass,
formHintClass,
formInputClass,
iconButtonClass,
modalBackdropClass,
modalEyebrowClass,
modalFooterClass,
modalFormClass,
modalHeaderClass,
modalPanelClass,
modalTitleClass,
primaryButtonClass,
secondaryButtonClass,
} from "./modalUi";
type NewScriptForm = {
@@ -48,26 +45,17 @@ export function CreateScriptModal({
onSubmit,
onClose,
}: CreateScriptModalProps) {
if (!open) return null;
return (
<div className={modalBackdropClass} role="presentation">
<section className={modalPanelClass} role="dialog" aria-modal="true">
<div className={modalHeaderClass}>
<div>
<span className={modalEyebrowClass}></span>
<h2 className={modalTitleClass}></h2>
</div>
<button
className={iconButtonClass}
type="button"
aria-label="关闭"
onClick={onClose}
>
<Icon name="close" />
</button>
</div>
<form className={modalFormClass} onSubmit={onSubmit}>
<AppFormDialog
open={open}
onOpenChange={(nextOpen) => {
if (!nextOpen) onClose();
}}
eyebrow="工作副本"
title="新建构建脚本"
size="panel"
>
<form className={modalFormClass} onSubmit={onSubmit}>
<div className={destinationChipClass}>
<Icon name="folder" size={16} />
{form.parentPath || "个人根目录"}
@@ -173,25 +161,28 @@ export function CreateScriptModal({
</select>
</label>
<div className={modalFooterClass}>
<button
className={secondaryButtonClass}
<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
className={primaryButtonClass}
</Button>
<Button
variant="default"
size="sm"
type="submit"
disabled={creating || !form.name.trim()}
className={dialogPrimaryButtonClass}
>
{creating ? <span className="button-spinner" /> : <Icon name="plus" size={16} />}
{creating ? "正在创建…" : "创建脚本"}
</button>
</Button>
</div>
</form>
</section>
</div>
</AppFormDialog>
);
}
@@ -1,21 +1,18 @@
import { type FormEvent } from "react";
import Icon from "../../components/common/Icon";
import type { Visibility } from "../../services/api";
import {
AppFormDialog,
dialogPrimaryButtonClass,
dialogSecondaryButtonClass,
} from "~/components/common/AppFormDialog";
import { Button } from "~/components/ui/button";
import {
formFieldClass,
formHintClass,
formInputClass,
formTextareaClass,
iconButtonClass,
modalBackdropClass,
modalCompactClass,
modalEyebrowClass,
modalFooterClass,
modalFormClass,
modalHeaderClass,
modalTitleClass,
primaryButtonClass,
secondaryButtonClass,
} from "./modalUi";
type DataResourceUploadModalProps = {
@@ -51,113 +48,106 @@ export function DataResourceUploadModal({
onSubmit,
onClose,
}: DataResourceUploadModalProps) {
if (!open) return null;
const finalTarget = targetPath.trim();
const finalPath = finalTarget
? `${finalTarget}/${file?.name ?? ""}`
: file?.name ?? "";
return (
<div className={modalBackdropClass} role="presentation">
<section className={modalCompactClass} role="dialog" aria-modal="true">
<div className={modalHeaderClass}>
<div>
<span className={modalEyebrowClass}></span>
<h2 className={modalTitleClass}></h2>
<AppFormDialog
open={open}
onOpenChange={(nextOpen) => {
if (!nextOpen && !uploading) onClose();
}}
eyebrow="数据资源"
title="上传数据文件"
>
<form className={modalFormClass} onSubmit={onSubmit}>
<label className={formFieldClass}>
<span></span>
<div className="font-normal text-[#283a4e]">
{file ? file.name : "未选择文件"}
</div>
<button
className={iconButtonClass}
type="button"
aria-label="关闭"
onClick={onClose}
</label>
<label className={formFieldClass}>
<span></span>
<input
className={formInputClass}
autoFocus
maxLength={255}
placeholder="例如:训练数据"
value={resourceName}
onChange={(event) => onNameChange(event.target.value)}
required
/>
</label>
<label className={formFieldClass}>
<span></span>
<input
className={formInputClass}
type="text"
maxLength={1024}
placeholder="例如:train/v1(留空上传到当前目录)"
value={targetPath}
onChange={(event) => onTargetPathChange(event.target.value)}
/>
<small className={formHintClass}>
{parentPath
? `当前位于 ${parentPath || "根目录"} · 最终路径:${finalPath || "—"}`
: `最终路径:${finalPath || "—"}`}
</small>
</label>
<label className={formFieldClass}>
<span></span>
<select
className={formInputClass}
value={visibility}
onChange={(event) =>
onVisibilityChange(event.target.value as Visibility)
}
>
<Icon name="close" />
</button>
<option value="private"></option>
<option value="workspace"></option>
<option value="public"></option>
</select>
</label>
<label className={formFieldClass}>
<span></span>
<textarea
className={formTextareaClass}
rows={3}
placeholder="可选描述"
value={description}
onChange={(event) => onDescriptionChange(event.target.value)}
/>
</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="outline"
size="sm"
type="button"
onClick={onClose}
disabled={uploading}
className={dialogSecondaryButtonClass}
>
</Button>
<Button
variant="default"
size="sm"
type="submit"
disabled={uploading || !resourceName.trim()}
className={dialogPrimaryButtonClass}
>
{uploading ? (
<span className="button-spinner" />
) : (
<Icon name="upload" size={16} />
)}
{uploading ? "上传中…" : "上传"}
</Button>
</div>
<form className={modalFormClass} onSubmit={onSubmit}>
<label className={formFieldClass}>
<span></span>
<div className="font-normal text-[#283a4e]">
{file ? file.name : "未选择文件"}
</div>
</label>
<label className={formFieldClass}>
<span></span>
<input
className={formInputClass}
autoFocus
maxLength={255}
placeholder="例如:训练数据"
value={resourceName}
onChange={(event) => onNameChange(event.target.value)}
required
/>
</label>
<label className={formFieldClass}>
<span></span>
<input
className={formInputClass}
type="text"
maxLength={1024}
placeholder="例如:train/v1(留空上传到当前目录)"
value={targetPath}
onChange={(event) => onTargetPathChange(event.target.value)}
/>
<small className={formHintClass}>
{parentPath
? `当前位于 ${parentPath || "根目录"} · 最终路径:${finalPath || "—"}`
: `最终路径:${finalPath || "—"}`}
</small>
</label>
<label className={formFieldClass}>
<span></span>
<select
className={formInputClass}
value={visibility}
onChange={(event) =>
onVisibilityChange(event.target.value as Visibility)
}
>
<option value="private"></option>
<option value="workspace"></option>
<option value="public"></option>
</select>
</label>
<label className={formFieldClass}>
<span></span>
<textarea
className={formTextareaClass}
rows={3}
placeholder="可选描述"
value={description}
onChange={(event) => onDescriptionChange(event.target.value)}
/>
</label>
<div className={modalFooterClass}>
<button
className={secondaryButtonClass}
type="button"
onClick={onClose}
disabled={uploading}
>
</button>
<button
className={primaryButtonClass}
type="submit"
disabled={uploading || !resourceName.trim()}
>
{uploading ? (
<span className="button-spinner" />
) : (
<Icon name="upload" size={16} />
)}
{uploading ? "上传中…" : "上传"}
</button>
</div>
</form>
</section>
</div>
</form>
</AppFormDialog>
);
}
+30 -37
View File
@@ -2,21 +2,18 @@ import { type FormEvent } from "react";
import Icon from "../../components/common/Icon";
import type { ScriptItem, Visibility } from "../../services/api";
import { scriptIcon } from "../../features/platform/WorkspaceTree";
import {
AppFormDialog,
dialogPrimaryButtonClass,
dialogSecondaryButtonClass,
} from "~/components/common/AppFormDialog";
import { Button } from "~/components/ui/button";
import {
formFieldClass,
formHintClass,
formInputClass,
formTextareaClass,
iconButtonClass,
modalBackdropClass,
modalEyebrowClass,
modalFooterClass,
modalFormClass,
modalHeaderClass,
modalPanelClass,
modalTitleClass,
primaryButtonClass,
secondaryButtonClass,
} from "./modalUi";
type PublishModalProps = {
@@ -40,26 +37,18 @@ export function PublishModal({
onSubmit,
onClose,
}: PublishModalProps) {
if (!publishTarget) return null;
return (
<div className={modalBackdropClass} role="presentation">
<section className={modalPanelClass} role="dialog" aria-modal="true">
<div className={modalHeaderClass}>
<div>
<span className={modalEyebrowClass}></span>
<h2 className={modalTitleClass}></h2>
</div>
<button
className={iconButtonClass}
type="button"
aria-label="关闭"
onClick={onClose}
>
<Icon name="close" />
</button>
</div>
<form className={modalFormClass} onSubmit={onSubmit}>
<AppFormDialog
open={publishTarget !== null}
onOpenChange={(nextOpen) => {
if (!nextOpen) onClose();
}}
eyebrow="不可变制品"
title="发布稳定版本"
size="panel"
>
<form className={modalFormClass} onSubmit={onSubmit}>
{publishTarget ? (
<div className="mb-[17px] flex items-center gap-2.5 rounded-[7px] border border-[#dce6ef] bg-[#f8fbfd] px-3 py-[11px]">
<span
className={`grid size-[25px] shrink-0 place-items-center rounded-[5px] ${
@@ -79,6 +68,7 @@ export function PublishModal({
</small>
</span>
</div>
) : null}
<label className={formFieldClass}>
<span></span>
<textarea
@@ -105,27 +95,30 @@ export function PublishModal({
<option value="public"></option>
</select>
</label>
<div className={modalFooterClass}>
<button
className={secondaryButtonClass}
<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
className={primaryButtonClass}
</Button>
<Button
variant="default"
size="sm"
type="submit"
disabled={publishing}
className={dialogPrimaryButtonClass}
>
{publishing
? <span className="button-spinner" />
: <Icon name="release" size={16} />}
{publishing ? "正在发布…" : "确认发布"}
</button>
</Button>
</div>
</form>
</section>
</div>
</AppFormDialog>
);
}
@@ -2,11 +2,11 @@ import Icon from "../../components/common/Icon";
import type { StableVersion } from "../../services/api";
import { copyToClipboard } from "../../lib/clipboard";
import {
modalBackdropClass,
modalEyebrowClass,
modalPanelClass,
primaryButtonClass,
} from "./modalUi";
AppFormDialog,
dialogPrimaryButtonClass,
} from "~/components/common/AppFormDialog";
import { Button } from "~/components/ui/button";
import { modalEyebrowClass } from "./modalUi";
type VersionReceiptModalProps = {
publishedVersion: StableVersion | null;
@@ -19,50 +19,51 @@ export function VersionReceiptModal({
onClose,
onCopy,
}: VersionReceiptModalProps) {
if (!publishedVersion) return null;
return (
<div className={modalBackdropClass} role="presentation">
<section
className={`${modalPanelClass} w-[min(470px,calc(100vw-40px))] px-7 py-[30px] pb-[25px] text-center`}
role="dialog"
aria-modal="true"
>
<div className="mx-auto mb-3.5 grid size-[58px] place-items-center rounded-full bg-gradient-to-br from-[#24b67d] to-[#149566] text-white shadow-[0_9px_24px_rgb(25_157_104/25%)]">
<Icon name="check" size={28} />
</div>
<span className={modalEyebrowClass}>STABLE VERSION</span>
<h2 className="mb-2 mt-[5px] text-xl text-[#24374a]">
</h2>
<p className="mx-auto mb-5 max-w-[360px] text-[11px] leading-[1.65] text-[#718093]">
{publishedVersion.version_label}
versions_id
</p>
<div className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-[9px] rounded-[7px] border border-[#dce6ee] bg-[#f7fafc] px-3 py-[11px] text-left">
<span className="text-[9px] text-[#8795a4]">versions_id</span>
<code className="truncate text-[10px] text-[#275271]">
{publishedVersion.versions_id}
</code>
<button
type="button"
className="cursor-pointer rounded border border-[#bad3e8] bg-white px-2 py-1 text-[9px] text-[#2475b7]"
onClick={async () => {
await copyToClipboard(publishedVersion.versions_id);
onCopy("versions_id 已复制");
}}
>
</button>
</div>
<AppFormDialog
open={publishedVersion !== null}
onOpenChange={(nextOpen) => {
if (!nextOpen) onClose();
}}
showCloseButton={false}
contentClassName="sm:max-w-[min(470px,calc(100vw-40px))] px-7 py-[30px] pb-[25px] text-center"
bodyClassName="overflow-visible"
>
<div className="mx-auto mb-3.5 grid size-[58px] place-items-center rounded-full bg-gradient-to-br from-[#24b67d] to-[#149566] text-white shadow-[0_9px_24px_rgb(25_157_104/25%)]">
<Icon name="check" size={28} />
</div>
<span className={modalEyebrowClass}>STABLE VERSION</span>
<h2 className="mb-2 mt-[5px] text-xl text-[#24374a]">
</h2>
<p className="mx-auto mb-5 max-w-[360px] text-[11px] leading-[1.65] text-[#718093]">
{publishedVersion?.version_label}
versions_id
</p>
<div className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-[9px] rounded-[7px] border border-[#dce6ee] bg-[#f7fafc] px-3 py-[11px] text-left">
<span className="text-[9px] text-[#8795a4]">versions_id</span>
<code className="truncate text-[10px] text-[#275271]">
{publishedVersion?.versions_id}
</code>
<button
className={`${primaryButtonClass} mt-[17px] h-[39px] w-full border-0 bg-[#1976cf] shadow-none`}
type="button"
onClick={onClose}
className="cursor-pointer rounded border border-[#bad3e8] bg-white px-2 py-1 text-[9px] text-[#2475b7]"
onClick={async () => {
if (!publishedVersion) return;
await copyToClipboard(publishedVersion.versions_id);
onCopy("versions_id 已复制");
}}
>
</button>
</section>
</div>
</div>
<Button
className={`${dialogPrimaryButtonClass} mt-[17px] h-[39px] w-full border-0 bg-[#1976cf] shadow-none`}
type="button"
onClick={onClose}
>
</Button>
</AppFormDialog>
);
}
+1 -1
View File
@@ -20,7 +20,7 @@ export const modalTitleClass = "mt-1 text-[19px] text-[#1c2d42]";
export const modalFormClass = "flex-1 overflow-y-auto px-[22px] pt-[19px]";
export const modalFooterClass =
"mt-[3px] flex shrink-0 justify-end gap-2 border-t border-[#e8edf2] bg-[#fafbfc] rounded-b-[11px] px-[22px] py-3.5";
"mt-[3px] flex shrink-0 justify-end gap-2 border-t border-[#e8edf2] bg-white rounded-b-[11px] px-[22px] py-3.5";
export const iconButtonClass =
"grid size-[34px] cursor-pointer place-items-center rounded-[7px] border border-[#dfe6ed] bg-white hover:border-[#b9c9da] hover:bg-[#f7faff]";
@@ -31,14 +31,13 @@ import { useApi, useAuth } from "~/context/AuthContext";
import {
formFieldClass,
formInputClass,
modalBackdropClass,
modalCompactClass,
modalEyebrowClass,
modalFooterClass,
modalFormClass,
modalHeaderClass,
modalTitleClass,
} from "~/features/platform/modalUi";
import {
AppFormDialog,
dialogPrimaryButtonClass,
dialogSecondaryButtonClass,
} from "~/components/common/AppFormDialog";
import { NodeInspector } from "./NodeInspector";
import { RunHistory } from "./RunHistory";
import { ScheduleInspector } from "./ScheduleInspector";
@@ -762,34 +761,16 @@ export default function SchedulePage({
</div>
)}
{createDialogOpen && (
<div className={modalBackdropClass} role="presentation">
<section
className={modalCompactClass}
role="dialog"
aria-modal="true"
aria-labelledby="create-schedule-title"
>
<div className={modalHeaderClass}>
<div>
<span className={modalEyebrowClass}>SCHEDULE</span>
<h2 id="create-schedule-title" className={modalTitleClass}>
</h2>
</div>
<Button
className="size-[34px] rounded-[7px] border-[#dfe6ed] bg-white hover:border-[#b9c9da] hover:bg-[#f7faff]"
type="button"
variant="ghost"
size="icon-lg"
aria-label="关闭"
disabled={Boolean(busy)}
onClick={() => setCreateDialogOpen(false)}
>
<X size={14} />
</Button>
</div>
<form className={modalFormClass} onSubmit={handleCreateScheduleSubmit}>
<AppFormDialog
open={createDialogOpen}
onOpenChange={(nextOpen) => {
if (!nextOpen && !busy) setCreateDialogOpen(false);
}}
eyebrow="SCHEDULE"
title="新建调度方案"
titleId="create-schedule-title"
>
<form className={modalFormClass} onSubmit={handleCreateScheduleSubmit}>
<label className={formFieldClass}>
<span></span>
<input
@@ -802,19 +783,23 @@ export default function SchedulePage({
onFocus={(event) => event.currentTarget.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
type="button"
variant="secondary"
variant="outline"
size="sm"
disabled={Boolean(busy)}
onClick={() => setCreateDialogOpen(false)}
className={dialogSecondaryButtonClass}
>
</Button>
<Button
type="submit"
variant="default"
size="sm"
disabled={Boolean(busy) || !newScheduleName.trim()}
className={dialogPrimaryButtonClass}
>
{busy === "create-schedule"
? <span className="button-spinner" />
@@ -823,9 +808,7 @@ export default function SchedulePage({
</Button>
</div>
</form>
</section>
</div>
)}
</AppFormDialog>
{busy && (
<div