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 { type Employee, type Workspace } from "../../services/api";
|
||||||
|
import {
|
||||||
|
AppFormDialog,
|
||||||
|
dialogPrimaryButtonClass,
|
||||||
|
dialogSecondaryButtonClass,
|
||||||
|
} from "~/components/common/AppFormDialog";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
formFieldClass,
|
formFieldClass,
|
||||||
modalBackdropClass,
|
|
||||||
modalCompactClass,
|
|
||||||
modalEyebrowClass,
|
|
||||||
modalFooterClass,
|
|
||||||
modalFormClass,
|
modalFormClass,
|
||||||
modalHeaderClass,
|
|
||||||
modalTitleClass,
|
|
||||||
} from "../platform/modalUi";
|
} from "../platform/modalUi";
|
||||||
import { UserMultiSelect } from "./UserMultiSelect";
|
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({
|
export function ImportMemberDialog({
|
||||||
open,
|
open,
|
||||||
selectedProject,
|
selectedProject,
|
||||||
@@ -27,7 +18,6 @@ export function ImportMemberDialog({
|
|||||||
selectedUserIds,
|
selectedUserIds,
|
||||||
existingMemberIds,
|
existingMemberIds,
|
||||||
saving,
|
saving,
|
||||||
onNotify,
|
|
||||||
onChangeSelectedUserIds,
|
onChangeSelectedUserIds,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -43,44 +33,22 @@ export function ImportMemberDialog({
|
|||||||
onSubmit: () => Promise<void> | void;
|
onSubmit: () => Promise<void> | void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
if (!open || !selectedProject) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={modalBackdropClass} role="presentation">
|
<AppFormDialog
|
||||||
<section className={modalCompactClass} role="dialog" aria-modal="true">
|
open={open && selectedProject !== null}
|
||||||
<div className={modalHeaderClass}>
|
onOpenChange={(nextOpen) => {
|
||||||
<div>
|
if (!nextOpen) onClose();
|
||||||
<span className={modalEyebrowClass}>IMPORT MEMBER</span>
|
}}
|
||||||
<h2 className={modalTitleClass}>导入成员到 {selectedProject?.workspace_name ?? "项目"}</h2>
|
eyebrow="IMPORT MEMBER"
|
||||||
</div>
|
title={`导入成员到 ${selectedProject?.workspace_name ?? "项目"}`}
|
||||||
<Button
|
footer={
|
||||||
variant="ghost"
|
<>
|
||||||
size="icon-sm"
|
|
||||||
type="button"
|
|
||||||
aria-label="关闭"
|
|
||||||
onClick={() => onClose()}
|
|
||||||
className={CLOSE_BUTTON_CLASS}
|
|
||||||
>
|
|
||||||
<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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onClose()}
|
onClick={() => onClose()}
|
||||||
className={SECONDARY_BUTTON_CLASS}
|
className={dialogSecondaryButtonClass}
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
@@ -90,13 +58,24 @@ export function ImportMemberDialog({
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={saving || selectedUserIds.length === 0}
|
disabled={saving || selectedUserIds.length === 0}
|
||||||
onClick={() => void onSubmit()}
|
onClick={() => void onSubmit()}
|
||||||
className={PRIMARY_BUTTON_CLASS}
|
className={dialogPrimaryButtonClass}
|
||||||
>
|
>
|
||||||
{saving ? "添加中…" : `添加 (${selectedUserIds.length})`}
|
{saving ? "添加中…" : `添加 (${selectedUserIds.length})`}
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
</form>
|
</AppFormDialog>
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,15 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { X } from "lucide-react";
|
|
||||||
import type { PlatformPermission, Role } from "../../services/api";
|
import type { PlatformPermission, Role } from "../../services/api";
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
modalBackdropClass,
|
AppFormDialog,
|
||||||
modalCompactClass,
|
dialogPrimaryButtonClass,
|
||||||
modalEyebrowClass,
|
dialogSecondaryButtonClass,
|
||||||
modalFooterClass,
|
} from "~/components/common/AppFormDialog";
|
||||||
modalFormClass,
|
import { Button } from "~/components/ui/button";
|
||||||
modalHeaderClass,
|
import { modalFormClass } from "../platform/modalUi";
|
||||||
modalTitleClass,
|
|
||||||
} from "../platform/modalUi";
|
|
||||||
import { PermissionCheckboxList } from "./PermissionCheckboxList";
|
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({
|
export function PermissionEditDialog({
|
||||||
open,
|
open,
|
||||||
target,
|
target,
|
||||||
@@ -41,7 +30,6 @@ export function PermissionEditDialog({
|
|||||||
const [selectedCodes, setSelectedCodes] = useState<string[]>(() => {
|
const [selectedCodes, setSelectedCodes] = useState<string[]>(() => {
|
||||||
if (!open || !target) return [];
|
if (!open || !target) return [];
|
||||||
const initial = [...currentCodes];
|
const initial = [...currentCodes];
|
||||||
// admin 角色必须保留 system:view(后端守卫);前端锁定勾选兜底。
|
|
||||||
if (target.role_code === "admin" && !initial.includes("system:view")) {
|
if (target.role_code === "admin" && !initial.includes("system:view")) {
|
||||||
initial.push("system:view");
|
initial.push("system:view");
|
||||||
}
|
}
|
||||||
@@ -69,45 +57,22 @@ export function PermissionEditDialog({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!open || !target) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={modalBackdropClass} role="presentation">
|
<AppFormDialog
|
||||||
<section className={modalCompactClass} role="dialog" aria-modal="true">
|
open={open && target !== null}
|
||||||
<div className={modalHeaderClass}>
|
onOpenChange={(nextOpen) => {
|
||||||
<div>
|
if (!nextOpen) onClose();
|
||||||
<span className={modalEyebrowClass}>PERMISSIONS</span>
|
}}
|
||||||
<h2 className={modalTitleClass}>权限配置 · {target.role_name}</h2>
|
eyebrow="PERMISSIONS"
|
||||||
</div>
|
title={target ? `权限配置 · ${target.role_name}` : "权限配置"}
|
||||||
<Button
|
footer={
|
||||||
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}>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className={SECONDARY_BUTTON_CLASS}
|
className={dialogSecondaryButtonClass}
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
@@ -117,12 +82,26 @@ export function PermissionEditDialog({
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
onClick={() => onSubmit(selectedCodes)}
|
onClick={() => onSubmit(selectedCodes)}
|
||||||
className={PRIMARY_BUTTON_CLASS}
|
className={dialogPrimaryButtonClass}
|
||||||
>
|
>
|
||||||
{saving ? "保存中…" : "保存权限"}
|
{saving ? "保存中…" : "保存权限"}
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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>
|
</div>
|
||||||
</section>
|
</AppFormDialog>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,17 @@
|
|||||||
import { X } from "lucide-react";
|
|
||||||
import { type Workspace } from "../../services/api";
|
import { type Workspace } from "../../services/api";
|
||||||
|
import {
|
||||||
|
AppFormDialog,
|
||||||
|
dialogPrimaryButtonClass,
|
||||||
|
dialogSecondaryButtonClass,
|
||||||
|
} from "~/components/common/AppFormDialog";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import { Textarea } from "~/components/ui/textarea";
|
import { Textarea } from "~/components/ui/textarea";
|
||||||
import {
|
import {
|
||||||
formFieldClass,
|
formFieldClass,
|
||||||
modalBackdropClass,
|
|
||||||
modalCompactClass,
|
|
||||||
modalEyebrowClass,
|
|
||||||
modalFooterClass,
|
|
||||||
modalFormClass,
|
modalFormClass,
|
||||||
modalHeaderClass,
|
|
||||||
modalTitleClass,
|
|
||||||
} from "../platform/modalUi";
|
} 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 =
|
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";
|
"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 =
|
const FORM_TEXTAREA_CLASS =
|
||||||
@@ -30,7 +22,6 @@ export function ProjectEditDialog({
|
|||||||
editingProject,
|
editingProject,
|
||||||
projectForm,
|
projectForm,
|
||||||
saving,
|
saving,
|
||||||
onNotify,
|
|
||||||
onChangeForm,
|
onChangeForm,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -44,27 +35,15 @@ export function ProjectEditDialog({
|
|||||||
onSubmit: (event: React.FormEvent) => Promise<void> | void;
|
onSubmit: (event: React.FormEvent) => Promise<void> | void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
if (!open) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={modalBackdropClass} role="presentation">
|
<AppFormDialog
|
||||||
<section className={modalCompactClass} role="dialog" aria-modal="true">
|
open={open}
|
||||||
<div className={modalHeaderClass}>
|
onOpenChange={(nextOpen) => {
|
||||||
<div>
|
if (!nextOpen) onClose();
|
||||||
<span className={modalEyebrowClass}>PROJECT</span>
|
}}
|
||||||
<h2 className={modalTitleClass}>{editingProject ? "编辑项目" : "新建项目"}</h2>
|
eyebrow="PROJECT"
|
||||||
</div>
|
title={editingProject ? "编辑项目" : "新建项目"}
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon-sm"
|
|
||||||
type="button"
|
|
||||||
aria-label="关闭"
|
|
||||||
onClick={() => onClose()}
|
|
||||||
className={CLOSE_BUTTON_CLASS}
|
|
||||||
>
|
>
|
||||||
<X size={16} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<form className={modalFormClass} onSubmit={(event) => void onSubmit(event)}>
|
<form className={modalFormClass} onSubmit={(event) => void onSubmit(event)}>
|
||||||
{!editingProject && (
|
{!editingProject && (
|
||||||
<label className={formFieldClass}>
|
<label className={formFieldClass}>
|
||||||
@@ -107,13 +86,13 @@ export function ProjectEditDialog({
|
|||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
</label>
|
</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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onClose()}
|
onClick={() => onClose()}
|
||||||
className={SECONDARY_BUTTON_CLASS}
|
className={dialogSecondaryButtonClass}
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
@@ -122,13 +101,12 @@ export function ProjectEditDialog({
|
|||||||
size="sm"
|
size="sm"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className={PRIMARY_BUTTON_CLASS}
|
className={dialogPrimaryButtonClass}
|
||||||
>
|
>
|
||||||
{saving ? "保存中…" : "保存"}
|
{saving ? "保存中…" : "保存"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</AppFormDialog>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Plus, Search } from "lucide-react";
|
|||||||
import { ApiRequestError, type Employee, type Workspace, type WorkspaceMember } from "../../services/api";
|
import { ApiRequestError, type Employee, type Workspace, type WorkspaceMember } from "../../services/api";
|
||||||
import { useApi, useAuth } from "../../context/AuthContext";
|
import { useApi, useAuth } from "../../context/AuthContext";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { ConfirmDialog } from "~/components/common/ConfirmDialog";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -52,6 +53,7 @@ export function ProjectManagementPage({
|
|||||||
const [membersDrawerOpen, setMembersDrawerOpen] = useState(false);
|
const [membersDrawerOpen, setMembersDrawerOpen] = useState(false);
|
||||||
const [currentProjectMembers, setCurrentProjectMembers] = useState<WorkspaceMember[]>([]);
|
const [currentProjectMembers, setCurrentProjectMembers] = useState<WorkspaceMember[]>([]);
|
||||||
const [membersLoading, setMembersLoading] = useState(false);
|
const [membersLoading, setMembersLoading] = useState(false);
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState<Workspace | null>(null);
|
||||||
|
|
||||||
const canManage = user?.role_code === "admin";
|
const canManage = user?.role_code === "admin";
|
||||||
|
|
||||||
@@ -138,8 +140,7 @@ export function ProjectManagementPage({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteProject = async (project: Workspace): Promise<void> => {
|
const executeDeleteProject = async (project: Workspace): Promise<void> => {
|
||||||
if (!window.confirm(`确定要删除项目"${project.workspace_name}"吗?此操作将级联软删所有成员。`)) return;
|
|
||||||
try {
|
try {
|
||||||
await api.deleteWorkspace(project.workspace_id);
|
await api.deleteWorkspace(project.workspace_id);
|
||||||
setProjects((current) => current.filter((p) => p.workspace_id !== project.workspace_id));
|
setProjects((current) => current.filter((p) => p.workspace_id !== project.workspace_id));
|
||||||
@@ -332,7 +333,7 @@ export function ProjectManagementPage({
|
|||||||
className="is-danger"
|
className="is-danger"
|
||||||
disabled={!canManage || project.status === "disabled"}
|
disabled={!canManage || project.status === "disabled"}
|
||||||
title={project.status === "disabled" ? "已删除的项目不能操作" : "删除项目"}
|
title={project.status === "disabled" ? "已删除的项目不能操作" : "删除项目"}
|
||||||
onClick={() => void deleteProject(project)}
|
onClick={() => setDeleteTarget(project)}
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</button>
|
</button>
|
||||||
@@ -382,6 +383,26 @@ export function ProjectManagementPage({
|
|||||||
onRemoveMember={(userId) => void removeMember(userId)}
|
onRemoveMember={(userId) => void removeMember(userId)}
|
||||||
onNotify={onNotify}
|
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>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { X } from "lucide-react";
|
|
||||||
import type {
|
import type {
|
||||||
PlatformPermission,
|
PlatformPermission,
|
||||||
Role,
|
Role,
|
||||||
RoleCreatePayload,
|
RoleCreatePayload,
|
||||||
RoleUpdatePayload,
|
RoleUpdatePayload,
|
||||||
} from "../../services/api";
|
} from "../../services/api";
|
||||||
|
import {
|
||||||
|
AppFormDialog,
|
||||||
|
dialogPrimaryButtonClass,
|
||||||
|
dialogSecondaryButtonClass,
|
||||||
|
} from "~/components/common/AppFormDialog";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
formFieldClass,
|
formFieldClass,
|
||||||
formInputClass,
|
formInputClass,
|
||||||
formTextareaClass,
|
formTextareaClass,
|
||||||
modalBackdropClass,
|
|
||||||
modalCompactClass,
|
|
||||||
modalEyebrowClass,
|
|
||||||
modalFooterClass,
|
|
||||||
modalFormClass,
|
modalFormClass,
|
||||||
modalHeaderClass,
|
|
||||||
modalTitleClass,
|
|
||||||
} from "../platform/modalUi";
|
} from "../platform/modalUi";
|
||||||
import { PermissionCheckboxList } from "./PermissionCheckboxList";
|
import { PermissionCheckboxList } from "./PermissionCheckboxList";
|
||||||
|
|
||||||
@@ -39,7 +37,6 @@ const EMPTY_FORM: RoleFormState = {
|
|||||||
permission_codes: [],
|
permission_codes: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 实时校验 role_code:空值不报错,提交时由必填拦截兜底。 */
|
|
||||||
function validateRoleCode(value: string): string {
|
function validateRoleCode(value: string): string {
|
||||||
const code = value.trim();
|
const code = value.trim();
|
||||||
if (!code) return "";
|
if (!code) return "";
|
||||||
@@ -58,13 +55,6 @@ function validateRoleCode(value: string): string {
|
|||||||
return "";
|
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({
|
export function RoleEditDialog({
|
||||||
open,
|
open,
|
||||||
editing,
|
editing,
|
||||||
@@ -117,7 +107,6 @@ export function RoleEditDialog({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleCreatePermission = (code: string): void => {
|
const toggleCreatePermission = (code: string): void => {
|
||||||
// 新建角色为非 admin,system:* 由后端守卫禁止;此处防御性拦截
|
|
||||||
if (code.startsWith("system:")) return;
|
if (code.startsWith("system:")) return;
|
||||||
setForm((current) => ({
|
setForm((current) => ({
|
||||||
...current,
|
...current,
|
||||||
@@ -143,7 +132,6 @@ export function RoleEditDialog({
|
|||||||
}
|
}
|
||||||
const payload: RoleCreatePayload | RoleUpdatePayload = editing
|
const payload: RoleCreatePayload | RoleUpdatePayload = editing
|
||||||
? {
|
? {
|
||||||
// 内置角色 role_name 锁定;description 可改
|
|
||||||
...(editing.is_builtin ? {} : { role_name: form.role_name.trim() }),
|
...(editing.is_builtin ? {} : { role_name: form.role_name.trim() }),
|
||||||
description: form.description.trim() || null,
|
description: form.description.trim() || null,
|
||||||
}
|
}
|
||||||
@@ -156,36 +144,20 @@ export function RoleEditDialog({
|
|||||||
onSubmit(payload);
|
onSubmit(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!open) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={modalBackdropClass} role="presentation">
|
<AppFormDialog
|
||||||
<section className={modalCompactClass} role="dialog" aria-modal="true">
|
open={open}
|
||||||
<div className={modalHeaderClass}>
|
onOpenChange={(nextOpen) => {
|
||||||
<div>
|
if (!nextOpen) onClose();
|
||||||
<span className={modalEyebrowClass}>ROLE</span>
|
}}
|
||||||
<h2 className={modalTitleClass}>{editing ? "编辑角色" : "新建角色"}</h2>
|
eyebrow="ROLE"
|
||||||
</div>
|
title={editing ? "编辑角色" : "新建角色"}
|
||||||
<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)}>
|
<form className={`${modalFormClass} role-create-form`} onSubmit={(event) => void handleSubmit(event)}>
|
||||||
{editing ? (
|
{editing ? (
|
||||||
<label className={formFieldClass}>
|
<label className={formFieldClass}>
|
||||||
<span>角色编码</span>
|
<span>角色编码</span>
|
||||||
<input
|
<input className={formInputClass} disabled value={form.role_code} />
|
||||||
className={formInputClass}
|
|
||||||
disabled
|
|
||||||
value={form.role_code}
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
) : (
|
) : (
|
||||||
<label className={formFieldClass}>
|
<label className={formFieldClass}>
|
||||||
@@ -236,13 +208,13 @@ export function RoleEditDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className={SECONDARY_BUTTON_CLASS}
|
className={dialogSecondaryButtonClass}
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
@@ -251,13 +223,12 @@ export function RoleEditDialog({
|
|||||||
size="sm"
|
size="sm"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className={PRIMARY_BUTTON_CLASS}
|
className={dialogPrimaryButtonClass}
|
||||||
>
|
>
|
||||||
{saving ? "保存中…" : "保存"}
|
{saving ? "保存中…" : "保存"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</AppFormDialog>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from "../../services/api";
|
} from "../../services/api";
|
||||||
import { useApi, useAuth } from "../../context/AuthContext";
|
import { useApi, useAuth } from "../../context/AuthContext";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { ConfirmDialog } from "~/components/common/ConfirmDialog";
|
||||||
import { PermissionEditDialog } from "./PermissionEditDialog";
|
import { PermissionEditDialog } from "./PermissionEditDialog";
|
||||||
import { RoleEditDialog } from "./RoleEditDialog";
|
import { RoleEditDialog } from "./RoleEditDialog";
|
||||||
import {
|
import {
|
||||||
@@ -50,6 +51,7 @@ export function RoleManagementPage({
|
|||||||
// 权限配置弹窗
|
// 权限配置弹窗
|
||||||
const [permissionDialogOpen, setPermissionDialogOpen] = useState(false);
|
const [permissionDialogOpen, setPermissionDialogOpen] = useState(false);
|
||||||
const [permissionTarget, setPermissionTarget] = useState<Role | null>(null);
|
const [permissionTarget, setPermissionTarget] = useState<Role | null>(null);
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState<Role | null>(null);
|
||||||
|
|
||||||
const canManage = user?.role_code === "admin";
|
const canManage = user?.role_code === "admin";
|
||||||
|
|
||||||
@@ -150,9 +152,7 @@ export function RoleManagementPage({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeRole = async (role: Role): Promise<void> => {
|
const executeRemoveRole = async (role: Role): Promise<void> => {
|
||||||
if (role.is_builtin) return;
|
|
||||||
if (!window.confirm(`确定删除角色"${role.role_name}"吗?`)) return;
|
|
||||||
try {
|
try {
|
||||||
await api.deletePlatformRole(role.role_code);
|
await api.deletePlatformRole(role.role_code);
|
||||||
setRoles((current) => current.filter(
|
setRoles((current) => current.filter(
|
||||||
@@ -271,7 +271,7 @@ export function RoleManagementPage({
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={!canManage || role.is_builtin}
|
disabled={!canManage || role.is_builtin}
|
||||||
title={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]"
|
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)}
|
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>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { useEffect, useState } from "react";
|
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 { ApiRequestError, type Employee, type Role } from "../../services/api";
|
||||||
import { useApi, useAuth } from "../../context/AuthContext";
|
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 { Button } from "~/components/ui/button";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import {
|
import {
|
||||||
@@ -17,13 +23,7 @@ import { AdminColgroup, USER_PROJECT_COL_WIDTHS } from "./AdminTable";
|
|||||||
import {
|
import {
|
||||||
formFieldClass,
|
formFieldClass,
|
||||||
formInputClass,
|
formInputClass,
|
||||||
modalBackdropClass,
|
|
||||||
modalCompactClass,
|
|
||||||
modalEyebrowClass,
|
|
||||||
modalFooterClass,
|
|
||||||
modalFormClass,
|
modalFormClass,
|
||||||
modalHeaderClass,
|
|
||||||
modalTitleClass,
|
|
||||||
} from "../platform/modalUi";
|
} from "../platform/modalUi";
|
||||||
|
|
||||||
import "../../styles/admin.css";
|
import "../../styles/admin.css";
|
||||||
@@ -37,10 +37,6 @@ const EMPTY_FORM = {
|
|||||||
status: "active" as "active" | "disabled" | "locked",
|
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 =
|
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]";
|
"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 =
|
const FORM_INPUT_CLASS =
|
||||||
@@ -67,6 +63,7 @@ export function UserManagementPage({
|
|||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [form, setForm] = useState(EMPTY_FORM);
|
const [form, setForm] = useState(EMPTY_FORM);
|
||||||
const [userSearchTerm, setUserSearchTerm] = useState("");
|
const [userSearchTerm, setUserSearchTerm] = useState("");
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState<Employee | null>(null);
|
||||||
|
|
||||||
const canManage = user?.role_code === "admin";
|
const canManage = user?.role_code === "admin";
|
||||||
|
|
||||||
@@ -179,8 +176,7 @@ export function UserManagementPage({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const remove = async (employee: Employee): Promise<void> => {
|
const executeRemove = async (employee: Employee): Promise<void> => {
|
||||||
if (!window.confirm(`确定从平台删除用户"${employee.display_name}"吗?`)) return;
|
|
||||||
try {
|
try {
|
||||||
await api.deletePlatformEmployee(employee.user_id);
|
await api.deletePlatformEmployee(employee.user_id);
|
||||||
setEmployees((current) => current.filter((item) => item.user_id !== employee.user_id));
|
setEmployees((current) => current.filter((item) => item.user_id !== employee.user_id));
|
||||||
@@ -274,7 +270,7 @@ export function UserManagementPage({
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={!canManage || isProtectedAdmin}
|
disabled={!canManage || isProtectedAdmin}
|
||||||
title={isProtectedAdmin ? "管理员账号不能删除" : "删除"}
|
title={isProtectedAdmin ? "管理员账号不能删除" : "删除"}
|
||||||
onClick={() => void remove(employee)}
|
onClick={() => setDeleteTarget(employee)}
|
||||||
className={ROW_DANGER_BUTTON_CLASS}
|
className={ROW_DANGER_BUTTON_CLASS}
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
@@ -287,25 +283,14 @@ export function UserManagementPage({
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
{dialogOpen && (
|
<AppFormDialog
|
||||||
<div className={modalBackdropClass} role="presentation">
|
open={dialogOpen}
|
||||||
<section className={modalCompactClass} role="dialog" aria-modal="true">
|
onOpenChange={(nextOpen) => {
|
||||||
<div className={modalHeaderClass}>
|
if (!nextOpen) setDialogOpen(false);
|
||||||
<div>
|
}}
|
||||||
<span className={modalEyebrowClass}>EMPLOYEE</span>
|
eyebrow="EMPLOYEE"
|
||||||
<h2 className={modalTitleClass}>{editing ? "编辑用户" : "添加用户"}</h2>
|
title={editing ? "编辑用户" : "添加用户"}
|
||||||
</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)}>
|
<form className={modalFormClass} onSubmit={(event) => void submit(event)}>
|
||||||
<label className={formFieldClass}>
|
<label className={formFieldClass}>
|
||||||
<span>姓名<span className="text-[#e74c3c]">*</span></span>
|
<span>姓名<span className="text-[#e74c3c]">*</span></span>
|
||||||
@@ -388,13 +373,13 @@ export function UserManagementPage({
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setDialogOpen(false)}
|
onClick={() => setDialogOpen(false)}
|
||||||
className={SECONDARY_BUTTON_CLASS}
|
className={dialogSecondaryButtonClass}
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
@@ -403,15 +388,33 @@ export function UserManagementPage({
|
|||||||
size="sm"
|
size="sm"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className={PRIMARY_BUTTON_CLASS}
|
className={dialogPrimaryButtonClass}
|
||||||
>
|
>
|
||||||
{saving ? "保存中…" : "保存"}
|
{saving ? "保存中…" : "保存"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</AppFormDialog>
|
||||||
</div>
|
|
||||||
)}
|
<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>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import { type FormEvent } from "react";
|
import { type FormEvent } from "react";
|
||||||
import Icon from "../../components/common/Icon";
|
import Icon from "../../components/common/Icon";
|
||||||
|
import {
|
||||||
|
AppFormDialog,
|
||||||
|
dialogPrimaryButtonClass,
|
||||||
|
dialogSecondaryButtonClass,
|
||||||
|
} from "~/components/common/AppFormDialog";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
destinationChipClass,
|
destinationChipClass,
|
||||||
formFieldClass,
|
formFieldClass,
|
||||||
formInputClass,
|
formInputClass,
|
||||||
iconButtonClass,
|
|
||||||
modalBackdropClass,
|
|
||||||
modalCompactClass,
|
|
||||||
modalEyebrowClass,
|
|
||||||
modalFooterClass,
|
|
||||||
modalFormClass,
|
modalFormClass,
|
||||||
modalHeaderClass,
|
|
||||||
modalTitleClass,
|
|
||||||
primaryButtonClass,
|
|
||||||
secondaryButtonClass,
|
|
||||||
} from "./modalUi";
|
} from "./modalUi";
|
||||||
|
|
||||||
type CreateFolderModalProps = {
|
type CreateFolderModalProps = {
|
||||||
@@ -35,25 +32,15 @@ export function CreateFolderModal({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
onClose,
|
onClose,
|
||||||
}: CreateFolderModalProps) {
|
}: CreateFolderModalProps) {
|
||||||
if (!open) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={modalBackdropClass} role="presentation">
|
<AppFormDialog
|
||||||
<section className={modalCompactClass} role="dialog" aria-modal="true">
|
open={open}
|
||||||
<div className={modalHeaderClass}>
|
onOpenChange={(nextOpen) => {
|
||||||
<div>
|
if (!nextOpen) onClose();
|
||||||
<span className={modalEyebrowClass}>WORKSPACE</span>
|
}}
|
||||||
<h2 className={modalTitleClass}>新建文件夹</h2>
|
eyebrow="WORKSPACE"
|
||||||
</div>
|
title="新建文件夹"
|
||||||
<button
|
|
||||||
className={iconButtonClass}
|
|
||||||
type="button"
|
|
||||||
aria-label="关闭"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
>
|
||||||
<Icon name="close" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<form className={modalFormClass} onSubmit={onSubmit}>
|
<form className={modalFormClass} onSubmit={onSubmit}>
|
||||||
<div className={destinationChipClass}>
|
<div className={destinationChipClass}>
|
||||||
<Icon name="folder" size={16} />
|
<Icon name="folder" size={16} />
|
||||||
@@ -70,27 +57,30 @@ export function CreateFolderModal({
|
|||||||
onChange={(event) => onNameChange(event.target.value)}
|
onChange={(event) => onNameChange(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</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
|
<Button
|
||||||
className={secondaryButtonClass}
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
className={dialogSecondaryButtonClass}
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
className={primaryButtonClass}
|
variant="default"
|
||||||
|
size="sm"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={busy || !name.trim()}
|
disabled={busy || !name.trim()}
|
||||||
|
className={dialogPrimaryButtonClass}
|
||||||
>
|
>
|
||||||
{busy
|
{busy
|
||||||
? <span className="button-spinner" />
|
? <span className="button-spinner" />
|
||||||
: <Icon name="folder" size={16} />}
|
: <Icon name="folder" size={16} />}
|
||||||
{busy ? "正在创建…" : "创建文件夹"}
|
{busy ? "正在创建…" : "创建文件夹"}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</AppFormDialog>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
import { type FormEvent } from "react";
|
import { type FormEvent } from "react";
|
||||||
import Icon from "../../components/common/Icon";
|
import Icon from "../../components/common/Icon";
|
||||||
import type { ScriptType, Visibility } from "../../services/api";
|
import type { ScriptType, Visibility } from "../../services/api";
|
||||||
|
import {
|
||||||
|
AppFormDialog,
|
||||||
|
dialogPrimaryButtonClass,
|
||||||
|
dialogSecondaryButtonClass,
|
||||||
|
} from "~/components/common/AppFormDialog";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
destinationChipClass,
|
destinationChipClass,
|
||||||
formFieldClass,
|
formFieldClass,
|
||||||
formHintClass,
|
formHintClass,
|
||||||
formInputClass,
|
formInputClass,
|
||||||
iconButtonClass,
|
|
||||||
modalBackdropClass,
|
|
||||||
modalEyebrowClass,
|
|
||||||
modalFooterClass,
|
|
||||||
modalFormClass,
|
modalFormClass,
|
||||||
modalHeaderClass,
|
|
||||||
modalPanelClass,
|
|
||||||
modalTitleClass,
|
|
||||||
primaryButtonClass,
|
|
||||||
secondaryButtonClass,
|
|
||||||
} from "./modalUi";
|
} from "./modalUi";
|
||||||
|
|
||||||
type NewScriptForm = {
|
type NewScriptForm = {
|
||||||
@@ -48,25 +45,16 @@ export function CreateScriptModal({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
onClose,
|
onClose,
|
||||||
}: CreateScriptModalProps) {
|
}: CreateScriptModalProps) {
|
||||||
if (!open) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={modalBackdropClass} role="presentation">
|
<AppFormDialog
|
||||||
<section className={modalPanelClass} role="dialog" aria-modal="true">
|
open={open}
|
||||||
<div className={modalHeaderClass}>
|
onOpenChange={(nextOpen) => {
|
||||||
<div>
|
if (!nextOpen) onClose();
|
||||||
<span className={modalEyebrowClass}>工作副本</span>
|
}}
|
||||||
<h2 className={modalTitleClass}>新建构建脚本</h2>
|
eyebrow="工作副本"
|
||||||
</div>
|
title="新建构建脚本"
|
||||||
<button
|
size="panel"
|
||||||
className={iconButtonClass}
|
|
||||||
type="button"
|
|
||||||
aria-label="关闭"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
>
|
||||||
<Icon name="close" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<form className={modalFormClass} onSubmit={onSubmit}>
|
<form className={modalFormClass} onSubmit={onSubmit}>
|
||||||
<div className={destinationChipClass}>
|
<div className={destinationChipClass}>
|
||||||
<Icon name="folder" size={16} />
|
<Icon name="folder" size={16} />
|
||||||
@@ -173,25 +161,28 @@ export function CreateScriptModal({
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</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
|
<Button
|
||||||
className={secondaryButtonClass}
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
className={dialogSecondaryButtonClass}
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
className={primaryButtonClass}
|
variant="default"
|
||||||
|
size="sm"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={creating || !form.name.trim()}
|
disabled={creating || !form.name.trim()}
|
||||||
|
className={dialogPrimaryButtonClass}
|
||||||
>
|
>
|
||||||
{creating ? <span className="button-spinner" /> : <Icon name="plus" size={16} />}
|
{creating ? <span className="button-spinner" /> : <Icon name="plus" size={16} />}
|
||||||
{creating ? "正在创建…" : "创建脚本"}
|
{creating ? "正在创建…" : "创建脚本"}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</AppFormDialog>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
import { type FormEvent } from "react";
|
import { type FormEvent } from "react";
|
||||||
import Icon from "../../components/common/Icon";
|
import Icon from "../../components/common/Icon";
|
||||||
import type { Visibility } from "../../services/api";
|
import type { Visibility } from "../../services/api";
|
||||||
|
import {
|
||||||
|
AppFormDialog,
|
||||||
|
dialogPrimaryButtonClass,
|
||||||
|
dialogSecondaryButtonClass,
|
||||||
|
} from "~/components/common/AppFormDialog";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
formFieldClass,
|
formFieldClass,
|
||||||
formHintClass,
|
formHintClass,
|
||||||
formInputClass,
|
formInputClass,
|
||||||
formTextareaClass,
|
formTextareaClass,
|
||||||
iconButtonClass,
|
|
||||||
modalBackdropClass,
|
|
||||||
modalCompactClass,
|
|
||||||
modalEyebrowClass,
|
|
||||||
modalFooterClass,
|
|
||||||
modalFormClass,
|
modalFormClass,
|
||||||
modalHeaderClass,
|
|
||||||
modalTitleClass,
|
|
||||||
primaryButtonClass,
|
|
||||||
secondaryButtonClass,
|
|
||||||
} from "./modalUi";
|
} from "./modalUi";
|
||||||
|
|
||||||
type DataResourceUploadModalProps = {
|
type DataResourceUploadModalProps = {
|
||||||
@@ -51,30 +48,20 @@ export function DataResourceUploadModal({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
onClose,
|
onClose,
|
||||||
}: DataResourceUploadModalProps) {
|
}: DataResourceUploadModalProps) {
|
||||||
if (!open) return null;
|
|
||||||
|
|
||||||
const finalTarget = targetPath.trim();
|
const finalTarget = targetPath.trim();
|
||||||
const finalPath = finalTarget
|
const finalPath = finalTarget
|
||||||
? `${finalTarget}/${file?.name ?? ""}`
|
? `${finalTarget}/${file?.name ?? ""}`
|
||||||
: file?.name ?? "";
|
: file?.name ?? "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={modalBackdropClass} role="presentation">
|
<AppFormDialog
|
||||||
<section className={modalCompactClass} role="dialog" aria-modal="true">
|
open={open}
|
||||||
<div className={modalHeaderClass}>
|
onOpenChange={(nextOpen) => {
|
||||||
<div>
|
if (!nextOpen && !uploading) onClose();
|
||||||
<span className={modalEyebrowClass}>数据资源</span>
|
}}
|
||||||
<h2 className={modalTitleClass}>上传数据文件</h2>
|
eyebrow="数据资源"
|
||||||
</div>
|
title="上传数据文件"
|
||||||
<button
|
|
||||||
className={iconButtonClass}
|
|
||||||
type="button"
|
|
||||||
aria-label="关闭"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
>
|
||||||
<Icon name="close" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<form className={modalFormClass} onSubmit={onSubmit}>
|
<form className={modalFormClass} onSubmit={onSubmit}>
|
||||||
<label className={formFieldClass}>
|
<label className={formFieldClass}>
|
||||||
<span>已选文件</span>
|
<span>已选文件</span>
|
||||||
@@ -134,19 +121,23 @@ export function DataResourceUploadModal({
|
|||||||
onChange={(event) => onDescriptionChange(event.target.value)}
|
onChange={(event) => onDescriptionChange(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</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
|
<Button
|
||||||
className={secondaryButtonClass}
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
disabled={uploading}
|
disabled={uploading}
|
||||||
|
className={dialogSecondaryButtonClass}
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
className={primaryButtonClass}
|
variant="default"
|
||||||
|
size="sm"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={uploading || !resourceName.trim()}
|
disabled={uploading || !resourceName.trim()}
|
||||||
|
className={dialogPrimaryButtonClass}
|
||||||
>
|
>
|
||||||
{uploading ? (
|
{uploading ? (
|
||||||
<span className="button-spinner" />
|
<span className="button-spinner" />
|
||||||
@@ -154,10 +145,9 @@ export function DataResourceUploadModal({
|
|||||||
<Icon name="upload" size={16} />
|
<Icon name="upload" size={16} />
|
||||||
)}
|
)}
|
||||||
{uploading ? "上传中…" : "上传"}
|
{uploading ? "上传中…" : "上传"}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</AppFormDialog>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,18 @@ import { type FormEvent } from "react";
|
|||||||
import Icon from "../../components/common/Icon";
|
import Icon from "../../components/common/Icon";
|
||||||
import type { ScriptItem, Visibility } from "../../services/api";
|
import type { ScriptItem, Visibility } from "../../services/api";
|
||||||
import { scriptIcon } from "../../features/platform/WorkspaceTree";
|
import { scriptIcon } from "../../features/platform/WorkspaceTree";
|
||||||
|
import {
|
||||||
|
AppFormDialog,
|
||||||
|
dialogPrimaryButtonClass,
|
||||||
|
dialogSecondaryButtonClass,
|
||||||
|
} from "~/components/common/AppFormDialog";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
import {
|
import {
|
||||||
formFieldClass,
|
formFieldClass,
|
||||||
formHintClass,
|
formHintClass,
|
||||||
formInputClass,
|
formInputClass,
|
||||||
formTextareaClass,
|
formTextareaClass,
|
||||||
iconButtonClass,
|
|
||||||
modalBackdropClass,
|
|
||||||
modalEyebrowClass,
|
|
||||||
modalFooterClass,
|
|
||||||
modalFormClass,
|
modalFormClass,
|
||||||
modalHeaderClass,
|
|
||||||
modalPanelClass,
|
|
||||||
modalTitleClass,
|
|
||||||
primaryButtonClass,
|
|
||||||
secondaryButtonClass,
|
|
||||||
} from "./modalUi";
|
} from "./modalUi";
|
||||||
|
|
||||||
type PublishModalProps = {
|
type PublishModalProps = {
|
||||||
@@ -40,26 +37,18 @@ export function PublishModal({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
onClose,
|
onClose,
|
||||||
}: PublishModalProps) {
|
}: PublishModalProps) {
|
||||||
if (!publishTarget) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={modalBackdropClass} role="presentation">
|
<AppFormDialog
|
||||||
<section className={modalPanelClass} role="dialog" aria-modal="true">
|
open={publishTarget !== null}
|
||||||
<div className={modalHeaderClass}>
|
onOpenChange={(nextOpen) => {
|
||||||
<div>
|
if (!nextOpen) onClose();
|
||||||
<span className={modalEyebrowClass}>不可变制品</span>
|
}}
|
||||||
<h2 className={modalTitleClass}>发布稳定版本</h2>
|
eyebrow="不可变制品"
|
||||||
</div>
|
title="发布稳定版本"
|
||||||
<button
|
size="panel"
|
||||||
className={iconButtonClass}
|
|
||||||
type="button"
|
|
||||||
aria-label="关闭"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
>
|
||||||
<Icon name="close" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<form className={modalFormClass} onSubmit={onSubmit}>
|
<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]">
|
<div className="mb-[17px] flex items-center gap-2.5 rounded-[7px] border border-[#dce6ef] bg-[#f8fbfd] px-3 py-[11px]">
|
||||||
<span
|
<span
|
||||||
className={`grid size-[25px] shrink-0 place-items-center rounded-[5px] ${
|
className={`grid size-[25px] shrink-0 place-items-center rounded-[5px] ${
|
||||||
@@ -79,6 +68,7 @@ export function PublishModal({
|
|||||||
</small>
|
</small>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
<label className={formFieldClass}>
|
<label className={formFieldClass}>
|
||||||
<span>发布说明</span>
|
<span>发布说明</span>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -105,27 +95,30 @@ export function PublishModal({
|
|||||||
<option value="public">公开</option>
|
<option value="public">公开</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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
|
<Button
|
||||||
className={secondaryButtonClass}
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
className={dialogSecondaryButtonClass}
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
className={primaryButtonClass}
|
variant="default"
|
||||||
|
size="sm"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={publishing}
|
disabled={publishing}
|
||||||
|
className={dialogPrimaryButtonClass}
|
||||||
>
|
>
|
||||||
{publishing
|
{publishing
|
||||||
? <span className="button-spinner" />
|
? <span className="button-spinner" />
|
||||||
: <Icon name="release" size={16} />}
|
: <Icon name="release" size={16} />}
|
||||||
{publishing ? "正在发布…" : "确认发布"}
|
{publishing ? "正在发布…" : "确认发布"}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</AppFormDialog>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import Icon from "../../components/common/Icon";
|
|||||||
import type { StableVersion } from "../../services/api";
|
import type { StableVersion } from "../../services/api";
|
||||||
import { copyToClipboard } from "../../lib/clipboard";
|
import { copyToClipboard } from "../../lib/clipboard";
|
||||||
import {
|
import {
|
||||||
modalBackdropClass,
|
AppFormDialog,
|
||||||
modalEyebrowClass,
|
dialogPrimaryButtonClass,
|
||||||
modalPanelClass,
|
} from "~/components/common/AppFormDialog";
|
||||||
primaryButtonClass,
|
import { Button } from "~/components/ui/button";
|
||||||
} from "./modalUi";
|
import { modalEyebrowClass } from "./modalUi";
|
||||||
|
|
||||||
type VersionReceiptModalProps = {
|
type VersionReceiptModalProps = {
|
||||||
publishedVersion: StableVersion | null;
|
publishedVersion: StableVersion | null;
|
||||||
@@ -19,14 +19,15 @@ export function VersionReceiptModal({
|
|||||||
onClose,
|
onClose,
|
||||||
onCopy,
|
onCopy,
|
||||||
}: VersionReceiptModalProps) {
|
}: VersionReceiptModalProps) {
|
||||||
if (!publishedVersion) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={modalBackdropClass} role="presentation">
|
<AppFormDialog
|
||||||
<section
|
open={publishedVersion !== null}
|
||||||
className={`${modalPanelClass} w-[min(470px,calc(100vw-40px))] px-7 py-[30px] pb-[25px] text-center`}
|
onOpenChange={(nextOpen) => {
|
||||||
role="dialog"
|
if (!nextOpen) onClose();
|
||||||
aria-modal="true"
|
}}
|
||||||
|
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%)]">
|
<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} />
|
<Icon name="check" size={28} />
|
||||||
@@ -36,18 +37,19 @@ export function VersionReceiptModal({
|
|||||||
稳定版本发布成功
|
稳定版本发布成功
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mx-auto mb-5 max-w-[360px] text-[11px] leading-[1.65] text-[#718093]">
|
<p className="mx-auto mb-5 max-w-[360px] text-[11px] leading-[1.65] text-[#718093]">
|
||||||
{publishedVersion.version_label} 已成为不可变制品,
|
{publishedVersion?.version_label} 已成为不可变制品,
|
||||||
后续调度将通过 versions_id 引用它。
|
后续调度将通过 versions_id 引用它。
|
||||||
</p>
|
</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">
|
<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>
|
<span className="text-[9px] text-[#8795a4]">versions_id</span>
|
||||||
<code className="truncate text-[10px] text-[#275271]">
|
<code className="truncate text-[10px] text-[#275271]">
|
||||||
{publishedVersion.versions_id}
|
{publishedVersion?.versions_id}
|
||||||
</code>
|
</code>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="cursor-pointer rounded border border-[#bad3e8] bg-white px-2 py-1 text-[9px] text-[#2475b7]"
|
className="cursor-pointer rounded border border-[#bad3e8] bg-white px-2 py-1 text-[9px] text-[#2475b7]"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
if (!publishedVersion) return;
|
||||||
await copyToClipboard(publishedVersion.versions_id);
|
await copyToClipboard(publishedVersion.versions_id);
|
||||||
onCopy("versions_id 已复制");
|
onCopy("versions_id 已复制");
|
||||||
}}
|
}}
|
||||||
@@ -55,14 +57,13 @@ export function VersionReceiptModal({
|
|||||||
复制
|
复制
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
className={`${primaryButtonClass} mt-[17px] h-[39px] w-full border-0 bg-[#1976cf] shadow-none`}
|
className={`${dialogPrimaryButtonClass} mt-[17px] h-[39px] w-full border-0 bg-[#1976cf] shadow-none`}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
完成
|
完成
|
||||||
</button>
|
</Button>
|
||||||
</section>
|
</AppFormDialog>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 modalFormClass = "flex-1 overflow-y-auto px-[22px] pt-[19px]";
|
||||||
|
|
||||||
export const modalFooterClass =
|
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 =
|
export const iconButtonClass =
|
||||||
"grid size-[34px] cursor-pointer place-items-center rounded-[7px] border border-[#dfe6ed] bg-white hover:border-[#b9c9da] hover:bg-[#f7faff]";
|
"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 {
|
import {
|
||||||
formFieldClass,
|
formFieldClass,
|
||||||
formInputClass,
|
formInputClass,
|
||||||
modalBackdropClass,
|
|
||||||
modalCompactClass,
|
|
||||||
modalEyebrowClass,
|
|
||||||
modalFooterClass,
|
|
||||||
modalFormClass,
|
modalFormClass,
|
||||||
modalHeaderClass,
|
|
||||||
modalTitleClass,
|
|
||||||
} from "~/features/platform/modalUi";
|
} from "~/features/platform/modalUi";
|
||||||
|
import {
|
||||||
|
AppFormDialog,
|
||||||
|
dialogPrimaryButtonClass,
|
||||||
|
dialogSecondaryButtonClass,
|
||||||
|
} from "~/components/common/AppFormDialog";
|
||||||
import { NodeInspector } from "./NodeInspector";
|
import { NodeInspector } from "./NodeInspector";
|
||||||
import { RunHistory } from "./RunHistory";
|
import { RunHistory } from "./RunHistory";
|
||||||
import { ScheduleInspector } from "./ScheduleInspector";
|
import { ScheduleInspector } from "./ScheduleInspector";
|
||||||
@@ -762,33 +761,15 @@ export default function SchedulePage({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{createDialogOpen && (
|
<AppFormDialog
|
||||||
<div className={modalBackdropClass} role="presentation">
|
open={createDialogOpen}
|
||||||
<section
|
onOpenChange={(nextOpen) => {
|
||||||
className={modalCompactClass}
|
if (!nextOpen && !busy) setCreateDialogOpen(false);
|
||||||
role="dialog"
|
}}
|
||||||
aria-modal="true"
|
eyebrow="SCHEDULE"
|
||||||
aria-labelledby="create-schedule-title"
|
title="新建调度方案"
|
||||||
|
titleId="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}>
|
<form className={modalFormClass} onSubmit={handleCreateScheduleSubmit}>
|
||||||
<label className={formFieldClass}>
|
<label className={formFieldClass}>
|
||||||
<span>调度方案名称</span>
|
<span>调度方案名称</span>
|
||||||
@@ -802,19 +783,23 @@ export default function SchedulePage({
|
|||||||
onFocus={(event) => event.currentTarget.select()}
|
onFocus={(event) => event.currentTarget.select()}
|
||||||
/>
|
/>
|
||||||
</label>
|
</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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
disabled={Boolean(busy)}
|
disabled={Boolean(busy)}
|
||||||
onClick={() => setCreateDialogOpen(false)}
|
onClick={() => setCreateDialogOpen(false)}
|
||||||
|
className={dialogSecondaryButtonClass}
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="default"
|
variant="default"
|
||||||
|
size="sm"
|
||||||
disabled={Boolean(busy) || !newScheduleName.trim()}
|
disabled={Boolean(busy) || !newScheduleName.trim()}
|
||||||
|
className={dialogPrimaryButtonClass}
|
||||||
>
|
>
|
||||||
{busy === "create-schedule"
|
{busy === "create-schedule"
|
||||||
? <span className="button-spinner" />
|
? <span className="button-spinner" />
|
||||||
@@ -823,9 +808,7 @@ export default function SchedulePage({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</AppFormDialog>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{busy && (
|
{busy && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user