update:Dialog替换
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user