Develop #41
@@ -196,10 +196,6 @@
|
|||||||
animation: modal-in 0.18s ease-out;
|
animation: modal-in 0.18s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-enter {
|
|
||||||
animation: toast-in 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notebook-md h1,
|
.notebook-md h1,
|
||||||
.notebook-md h2,
|
.notebook-md h2,
|
||||||
.notebook-md h3,
|
.notebook-md h3,
|
||||||
@@ -260,13 +256,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes toast-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-7px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import Icon from "./Icon";
|
|
||||||
|
|
||||||
type ToastState = {
|
|
||||||
tone: "success" | "error" | "info";
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ToastProps = {
|
|
||||||
toast: ToastState | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const toneIconClass: Record<ToastState["tone"], string> = {
|
|
||||||
success: "bg-[#e6f7f0] text-[#177b55]",
|
|
||||||
error: "bg-[#fff0f0] text-[#b94a4a]",
|
|
||||||
info: "bg-[#eaf4fd] text-[#2675bf]",
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Toast({ toast }: ToastProps) {
|
|
||||||
if (!toast) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="toast-enter fixed top-[82px] right-[21px] z-30 flex min-h-[46px] min-w-[260px] max-w-[410px] items-center gap-[9px] rounded-lg border border-[#cfdbe6] bg-white px-3.5 py-2.5 text-xs text-[#43566a] shadow-[0_12px_35px_rgb(20_42_66/14%)]"
|
|
||||||
role="status"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`grid size-[27px] shrink-0 place-items-center rounded-full ${toneIconClass[toast.tone]}`}
|
|
||||||
>
|
|
||||||
<Icon name={toast.tone === "success" ? "check" : "info"} size={17} />
|
|
||||||
</span>
|
|
||||||
{toast.message}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||||
|
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme="light"
|
||||||
|
className="toaster group"
|
||||||
|
icons={{
|
||||||
|
success: (
|
||||||
|
<CircleCheckIcon className="size-4" />
|
||||||
|
),
|
||||||
|
info: (
|
||||||
|
<InfoIcon className="size-4" />
|
||||||
|
),
|
||||||
|
warning: (
|
||||||
|
<TriangleAlertIcon className="size-4" />
|
||||||
|
),
|
||||||
|
error: (
|
||||||
|
<OctagonXIcon className="size-4" />
|
||||||
|
),
|
||||||
|
loading: (
|
||||||
|
<Loader2Icon className="size-4 animate-spin" />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--normal-bg": "var(--popover)",
|
||||||
|
"--normal-text": "var(--popover-foreground)",
|
||||||
|
"--normal-border": "var(--border)",
|
||||||
|
"--border-radius": "var(--radius)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
toastOptions={{
|
||||||
|
classNames: {
|
||||||
|
toast: "cn-toast",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toaster }
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Toast as ToastPrimitive } from "@base-ui/react/toast"
|
|
||||||
|
|
||||||
import { cn } from "~/lib/utils"
|
|
||||||
import { Button } from "~/components/ui/button"
|
|
||||||
import { XIcon, CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
|
|
||||||
|
|
||||||
const toast = ToastPrimitive.createToastManager()
|
|
||||||
|
|
||||||
function ToastProvider({ ...props }: ToastPrimitive.Provider.Props) {
|
|
||||||
return <ToastPrimitive.Provider {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function ToastPortal({ ...props }: ToastPrimitive.Portal.Props) {
|
|
||||||
return <ToastPrimitive.Portal data-slot="toast-portal" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function ToastViewport({ className, ...props }: ToastPrimitive.Viewport.Props) {
|
|
||||||
return (
|
|
||||||
<ToastPrimitive.Viewport
|
|
||||||
data-slot="toast-viewport"
|
|
||||||
className={cn(
|
|
||||||
"pointer-events-none fixed inset-x-4 bottom-4 z-50 mx-auto w-auto max-w-sm outline-none sm:right-4 sm:left-auto sm:mx-0 sm:w-full",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Toast({ className, ...props }: ToastPrimitive.Root.Props) {
|
|
||||||
return (
|
|
||||||
<ToastPrimitive.Root
|
|
||||||
data-slot="toast"
|
|
||||||
className={cn(
|
|
||||||
"group/toast pointer-events-auto absolute right-0 bottom-0 z-[calc(1000-var(--toast-index))] w-full origin-bottom rounded-2xl border bg-popover text-popover-foreground shadow-lg will-change-transform outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
|
||||||
"[--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))]",
|
|
||||||
"h-(--height) [transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] [transition:transform_500ms_cubic-bezier(0.22,1,0.36,1),opacity_500ms,height_150ms]",
|
|
||||||
"after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']",
|
|
||||||
"data-expanded:h-(--toast-height) data-expanded:[transform:translateX(var(--toast-swipe-movement-x))_translateY(var(--offset-y))]",
|
|
||||||
"data-limited:opacity-0 data-starting-style:[transform:translateY(150%)]",
|
|
||||||
"[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)]",
|
|
||||||
"data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]",
|
|
||||||
"data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]",
|
|
||||||
"data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]",
|
|
||||||
"data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]",
|
|
||||||
"data-expanded:data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]",
|
|
||||||
"data-expanded:data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]",
|
|
||||||
"data-expanded:data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]",
|
|
||||||
"data-expanded:data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ToastContent({ className, ...props }: ToastPrimitive.Content.Props) {
|
|
||||||
return (
|
|
||||||
<ToastPrimitive.Content
|
|
||||||
data-slot="toast-content"
|
|
||||||
className={cn(
|
|
||||||
"flex h-full items-center gap-3 overflow-hidden p-4 transition-opacity duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] data-behind:opacity-0 data-expanded:opacity-100",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ToastTitle({ className, ...props }: ToastPrimitive.Title.Props) {
|
|
||||||
return (
|
|
||||||
<ToastPrimitive.Title
|
|
||||||
data-slot="toast-title"
|
|
||||||
className={cn("text-sm font-medium", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ToastDescription({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ToastPrimitive.Description.Props) {
|
|
||||||
return (
|
|
||||||
<ToastPrimitive.Description
|
|
||||||
data-slot="toast-description"
|
|
||||||
className={cn("text-sm text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ToastAction({
|
|
||||||
className,
|
|
||||||
render = <Button variant="outline" size="sm" />,
|
|
||||||
...props
|
|
||||||
}: ToastPrimitive.Action.Props) {
|
|
||||||
return (
|
|
||||||
<ToastPrimitive.Action
|
|
||||||
data-slot="toast-action"
|
|
||||||
render={render}
|
|
||||||
className={cn("shrink-0", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ToastClose({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
render = <Button variant="ghost" size="icon-sm" />,
|
|
||||||
...props
|
|
||||||
}: ToastPrimitive.Close.Props) {
|
|
||||||
return (
|
|
||||||
<ToastPrimitive.Close
|
|
||||||
data-slot="toast-close"
|
|
||||||
aria-label="Close toast"
|
|
||||||
render={render}
|
|
||||||
className={cn(
|
|
||||||
"relative shrink-0 text-muted-foreground after:absolute after:-inset-2 after:content-[''] hover:text-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children ?? (
|
|
||||||
<XIcon aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
</ToastPrimitive.Close>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ToastIcon({ type }: { type: string | undefined }) {
|
|
||||||
let icon: React.ReactNode = null
|
|
||||||
|
|
||||||
if (type === "success") {
|
|
||||||
icon = (
|
|
||||||
<CircleCheckIcon aria-hidden="true" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "info") {
|
|
||||||
icon = (
|
|
||||||
<InfoIcon aria-hidden="true" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "warning") {
|
|
||||||
icon = (
|
|
||||||
<TriangleAlertIcon aria-hidden="true" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "error") {
|
|
||||||
icon = (
|
|
||||||
<OctagonXIcon className="text-destructive" aria-hidden="true" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "loading") {
|
|
||||||
icon = (
|
|
||||||
<Loader2Icon className="animate-spin" aria-hidden="true" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!icon) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
data-slot="toast-icon"
|
|
||||||
className="shrink-0 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4"
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ToastList() {
|
|
||||||
const { toasts } = ToastPrimitive.useToastManager()
|
|
||||||
|
|
||||||
return toasts.map((toastItem) => (
|
|
||||||
<Toast key={toastItem.id} toast={toastItem}>
|
|
||||||
<ToastContent>
|
|
||||||
<ToastIcon type={toastItem.type} />
|
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
||||||
<ToastTitle />
|
|
||||||
<ToastDescription />
|
|
||||||
</div>
|
|
||||||
<ToastAction />
|
|
||||||
<ToastClose />
|
|
||||||
</ToastContent>
|
|
||||||
</Toast>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
function Toaster({
|
|
||||||
children,
|
|
||||||
toastManager = toast,
|
|
||||||
...props
|
|
||||||
}: ToastPrimitive.Provider.Props) {
|
|
||||||
return (
|
|
||||||
<ToastProvider toastManager={toastManager} {...props}>
|
|
||||||
{children}
|
|
||||||
<ToastPortal>
|
|
||||||
<ToastViewport>
|
|
||||||
<ToastList />
|
|
||||||
</ToastViewport>
|
|
||||||
</ToastPortal>
|
|
||||||
</ToastProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const createToastManager = ToastPrimitive.createToastManager
|
|
||||||
const useToastManager = ToastPrimitive.useToastManager
|
|
||||||
|
|
||||||
export {
|
|
||||||
Toaster,
|
|
||||||
Toast,
|
|
||||||
ToastAction,
|
|
||||||
ToastClose,
|
|
||||||
ToastContent,
|
|
||||||
ToastDescription,
|
|
||||||
ToastPortal,
|
|
||||||
ToastProvider,
|
|
||||||
ToastTitle,
|
|
||||||
ToastViewport,
|
|
||||||
createToastManager,
|
|
||||||
toast,
|
|
||||||
useToastManager,
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from "../../../services/api";
|
} from "../../../services/api";
|
||||||
|
|
||||||
import { useScriptWorkspaceStore } from "../../platform/state/scriptWorkspaceStore";
|
import { useScriptWorkspaceStore } from "../../platform/state/scriptWorkspaceStore";
|
||||||
import { useUiStore } from "../../platform/state/uiStore";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
type Notice = {
|
type Notice = {
|
||||||
tone: "success" | "error" | "info";
|
tone: "success" | "error" | "info";
|
||||||
@@ -65,7 +65,9 @@ function requireApi(): WorkspaceBoundApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pushToast(notice: Notice) {
|
function pushToast(notice: Notice) {
|
||||||
useUiStore.getState().pushToast(notice);
|
if (notice.tone === "error") toast.error(notice.message);
|
||||||
|
else if (notice.tone === "info") toast.info(notice.message);
|
||||||
|
else toast.success(notice.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setApiOnline(online: boolean) {
|
function setApiOnline(online: boolean) {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const saveBtnClass =
|
|||||||
// 模块级变量存储刷新版本号,用于检测只读内容刷新
|
// 模块级变量存储刷新版本号,用于检测只读内容刷新
|
||||||
let _lastRefreshVersion = 0;
|
let _lastRefreshVersion = 0;
|
||||||
|
|
||||||
type ToastState = {
|
type Notice = {
|
||||||
tone: "success" | "error" | "info";
|
tone: "success" | "error" | "info";
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
@@ -71,7 +71,7 @@ type ScriptWorkspaceProps = {
|
|||||||
onSwitchTab: (scriptId: string) => void;
|
onSwitchTab: (scriptId: string) => void;
|
||||||
onNewTab: () => void;
|
onNewTab: () => void;
|
||||||
onPublish: () => void;
|
onPublish: () => void;
|
||||||
onInfo: (toast: ToastState) => void;
|
onInfo: (toast: Notice) => void;
|
||||||
|
|
||||||
pythonEditorBuffers: Record<string, PythonEditorBuffer>;
|
pythonEditorBuffers: Record<string, PythonEditorBuffer>;
|
||||||
onOpenPythonEditor: () => void;
|
onOpenPythonEditor: () => void;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { VersionReceiptModal } from "./VersionReceiptModal";
|
|||||||
|
|
||||||
import { getSessionCache, useScriptWorkspaceStore } from "./state/scriptWorkspaceStore";
|
import { getSessionCache, useScriptWorkspaceStore } from "./state/scriptWorkspaceStore";
|
||||||
import { useUiStore } from "./state/uiStore";
|
import { useUiStore } from "./state/uiStore";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { ScriptWorkspace } from "./ScriptWorkspace";
|
import { ScriptWorkspace } from "./ScriptWorkspace";
|
||||||
import { copyToClipboard } from "../../lib/clipboard";
|
import { copyToClipboard } from "../../lib/clipboard";
|
||||||
|
|
||||||
@@ -68,7 +69,11 @@ export default function ScriptsPage() {
|
|||||||
const refreshReadOnlyContent = useScriptWorkspaceStore((s) => s.refreshReadOnlyContent);
|
const refreshReadOnlyContent = useScriptWorkspaceStore((s) => s.refreshReadOnlyContent);
|
||||||
|
|
||||||
// ui store
|
// ui store
|
||||||
const pushToast = useUiStore((s) => s.pushToast);
|
const pushToast = (notice: { tone: "success" | "error" | "info"; message: string }) => {
|
||||||
|
if (notice.tone === "error") toast.error(notice.message);
|
||||||
|
else if (notice.tone === "info") toast.info(notice.message);
|
||||||
|
else toast.success(notice.message);
|
||||||
|
};
|
||||||
const createDialog = useUiStore((s) => s.createDialog);
|
const createDialog = useUiStore((s) => s.createDialog);
|
||||||
const setCreateForm = useUiStore((s) => s.setCreateForm);
|
const setCreateForm = useUiStore((s) => s.setCreateForm);
|
||||||
const closeCreateDialog = useUiStore((s) => s.closeCreateDialog);
|
const closeCreateDialog = useUiStore((s) => s.closeCreateDialog);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type {
|
|||||||
WorkspaceBoundApi,
|
WorkspaceBoundApi,
|
||||||
} from "~/services/api";
|
} from "~/services/api";
|
||||||
|
|
||||||
import { useUiStore } from "./uiStore";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
// 缓存的会话类型(多 iframe 共存方案)
|
// 缓存的会话类型(多 iframe 共存方案)
|
||||||
type CachedSession = {
|
type CachedSession = {
|
||||||
@@ -173,7 +173,9 @@ export function pushToast(
|
|||||||
tone: "success" | "error" | "info",
|
tone: "success" | "error" | "info",
|
||||||
message: string,
|
message: string,
|
||||||
): void {
|
): void {
|
||||||
useUiStore.getState().pushToast({ tone, message });
|
if (tone === "error") toast.error(message);
|
||||||
|
else if (tone === "info") toast.info(message);
|
||||||
|
else toast.success(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sha256Hex(
|
export async function sha256Hex(
|
||||||
|
|||||||
@@ -7,12 +7,6 @@ import type {
|
|||||||
Visibility,
|
Visibility,
|
||||||
} from "../../../services/api";
|
} from "../../../services/api";
|
||||||
|
|
||||||
export type ToastTone = "success" | "error" | "info";
|
|
||||||
export type ToastState = {
|
|
||||||
tone: ToastTone;
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type NewScriptForm = {
|
export type NewScriptForm = {
|
||||||
name: string;
|
name: string;
|
||||||
scriptType: ScriptType;
|
scriptType: ScriptType;
|
||||||
@@ -66,7 +60,6 @@ export const initialCreateForm: NewScriptForm = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type UiState = {
|
type UiState = {
|
||||||
toast: ToastState | null;
|
|
||||||
createDialog: {
|
createDialog: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
creating: boolean;
|
creating: boolean;
|
||||||
@@ -79,10 +72,6 @@ type UiState = {
|
|||||||
publish: PublishDialogState;
|
publish: PublishDialogState;
|
||||||
dataResourceDialog: DataResourceDialogState;
|
dataResourceDialog: DataResourceDialogState;
|
||||||
|
|
||||||
// toast
|
|
||||||
pushToast: (toast: ToastState) => void;
|
|
||||||
dismissToast: () => void;
|
|
||||||
|
|
||||||
// create dialog
|
// create dialog
|
||||||
openCreateDialog: (parentPath?: string, scriptType?: ScriptType) => void;
|
openCreateDialog: (parentPath?: string, scriptType?: ScriptType) => void;
|
||||||
closeCreateDialog: () => void;
|
closeCreateDialog: () => void;
|
||||||
@@ -129,7 +118,6 @@ type UiState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useUiStore = create<UiState>((set) => ({
|
export const useUiStore = create<UiState>((set) => ({
|
||||||
toast: null,
|
|
||||||
createDialog: {
|
createDialog: {
|
||||||
open: false,
|
open: false,
|
||||||
creating: false,
|
creating: false,
|
||||||
@@ -165,9 +153,6 @@ export const useUiStore = create<UiState>((set) => ({
|
|||||||
targetPath: "",
|
targetPath: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
pushToast: (toast) => set({ toast }),
|
|
||||||
dismissToast: () => set({ toast: null }),
|
|
||||||
|
|
||||||
openCreateDialog: (parentPath = "", scriptType = "notebook") =>
|
openCreateDialog: (parentPath = "", scriptType = "notebook") =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
contextMenu: null,
|
contextMenu: null,
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { useScriptWorkspaceStore } from "../platform/state/scriptWorkspaceStore";
|
import { useScriptWorkspaceStore } from "../platform/state/scriptWorkspaceStore";
|
||||||
import { useUiStore } from "../platform/state/uiStore";
|
import { toast } from "sonner";
|
||||||
import SchedulePage from "./SchedulePage";
|
import SchedulePage from "./SchedulePage";
|
||||||
|
|
||||||
export default function SchedulesPageRoute() {
|
export default function SchedulesPageRoute() {
|
||||||
const setApiOnline = useScriptWorkspaceStore((s) => s.setApiOnline);
|
const setApiOnline = useScriptWorkspaceStore((s) => s.setApiOnline);
|
||||||
const pushToast = useUiStore((s) => s.pushToast);
|
const pushToast = (notice: { tone: "success" | "error" | "info"; message: string }) => {
|
||||||
|
if (notice.tone === "error") toast.error(notice.message);
|
||||||
|
else if (notice.tone === "info") toast.info(notice.message);
|
||||||
|
else toast.success(notice.message);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SchedulePage
|
<SchedulePage
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { ApiRequestError, type Schedule, type ScheduleNode, type WorkspaceBoundApi } from "../../../services/api";
|
import { ApiRequestError, type Schedule, type ScheduleNode, type WorkspaceBoundApi } from "../../../services/api";
|
||||||
|
|
||||||
import { useScriptWorkspaceStore } from "../../platform/state/scriptWorkspaceStore";
|
import { useScriptWorkspaceStore } from "../../platform/state/scriptWorkspaceStore";
|
||||||
import { useUiStore } from "../../platform/state/uiStore";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import type { NodeForm, NodePositionDraft, Notice, ScheduleForm } from "./types";
|
import type { NodeForm, NodePositionDraft, Notice, ScheduleForm } from "./types";
|
||||||
import type { SchedulesStore } from "./useSchedulesStore";
|
import type { SchedulesStore } from "./useSchedulesStore";
|
||||||
@@ -22,7 +22,9 @@ export function requireApi(): WorkspaceBoundApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function notify(notice: Notice): void {
|
export function notify(notice: Notice): void {
|
||||||
useUiStore.getState().pushToast(notice);
|
if (notice.tone === "error") toast.error(notice.message);
|
||||||
|
else if (notice.tone === "info") toast.info(notice.message);
|
||||||
|
else toast.success(notice.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setApiOnline(online: boolean): void {
|
export function setApiOnline(online: boolean): void {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from "react-router";
|
} from "react-router";
|
||||||
|
|
||||||
import type { Route } from "./+types/root";
|
import type { Route } from "./+types/root";
|
||||||
|
import { Toaster } from "./components/ui/sonner";
|
||||||
import { AuthProvider } from "./context/AuthContext";
|
import { AuthProvider } from "./context/AuthContext";
|
||||||
// Monaco 离线加载配置:必须在任何 <Editor> 挂载前执行,否则 loader 会默认
|
// Monaco 离线加载配置:必须在任何 <Editor> 挂载前执行,否则 loader 会默认
|
||||||
// 从 jsDelivr CDN 拉取 monaco,离线环境下编辑器初始化失败、永远 loading。
|
// 从 jsDelivr CDN 拉取 monaco,离线环境下编辑器初始化失败、永远 loading。
|
||||||
@@ -29,6 +30,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{children}
|
{children}
|
||||||
|
<Toaster />
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
<Scripts />
|
<Scripts />
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import type { ActivePage, NavigationItem, SubPage } from "./platform.navigation"
|
|||||||
import { Collapsible } from "@base-ui/react/collapsible";
|
import { Collapsible } from "@base-ui/react/collapsible";
|
||||||
|
|
||||||
import type { Route } from "./+types/platform";
|
import type { Route } from "./+types/platform";
|
||||||
import { Toast } from "../components/common/Toast";
|
|
||||||
import { Topbar } from "../components/common/Topbar";
|
import { Topbar } from "../components/common/Topbar";
|
||||||
import Icon from "../components/common/Icon";
|
import Icon from "../components/common/Icon";
|
||||||
import {
|
import {
|
||||||
@@ -165,8 +164,6 @@ function AuthenticatedLayout() {
|
|||||||
const endEditing = useScriptWorkspaceStore((s) => s.endEditing);
|
const endEditing = useScriptWorkspaceStore((s) => s.endEditing);
|
||||||
const selectScript = useScriptWorkspaceStore((s) => s.selectScript);
|
const selectScript = useScriptWorkspaceStore((s) => s.selectScript);
|
||||||
|
|
||||||
const toast = useUiStore((s) => s.toast);
|
|
||||||
const dismissToast = useUiStore((s) => s.dismissToast);
|
|
||||||
const workspaceMenuOpen = useUiStore((s) => s.workspaceMenuOpen);
|
const workspaceMenuOpen = useUiStore((s) => s.workspaceMenuOpen);
|
||||||
const setWorkspaceMenuOpen = useUiStore((s) => s.setWorkspaceMenuOpen);
|
const setWorkspaceMenuOpen = useUiStore((s) => s.setWorkspaceMenuOpen);
|
||||||
|
|
||||||
@@ -199,13 +196,6 @@ function AuthenticatedLayout() {
|
|||||||
// 心跳 / cleanup / 切页结束编辑 / 卸载前释放
|
// 心跳 / cleanup / 切页结束编辑 / 卸载前释放
|
||||||
useEditSessionLifecycle({ activePage });
|
useEditSessionLifecycle({ activePage });
|
||||||
|
|
||||||
// toast 自动消失
|
|
||||||
useEffect(() => {
|
|
||||||
if (!toast) return;
|
|
||||||
const timer = window.setTimeout(dismissToast, 3200);
|
|
||||||
return () => window.clearTimeout(timer);
|
|
||||||
}, [toast, dismissToast]);
|
|
||||||
|
|
||||||
// 原 common/Sidebar 的编辑会话守卫上移到这里:切出 /scripts 时有活动编辑
|
// 原 common/Sidebar 的编辑会话守卫上移到这里:切出 /scripts 时有活动编辑
|
||||||
// 会话先 endEditing,回工作台时清空选中脚本;再按当前激活页导航。
|
// 会话先 endEditing,回工作台时清空选中脚本;再按当前激活页导航。
|
||||||
function guardedNavigate(page: ActivePage, sub?: SubPage) {
|
function guardedNavigate(page: ActivePage, sub?: SubPage) {
|
||||||
@@ -279,7 +269,6 @@ function AuthenticatedLayout() {
|
|||||||
<Outlet />
|
<Outlet />
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
|
|
||||||
<Toast toast={toast} />
|
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useAuth } from "~/context/AuthContext";
|
import { useAuth } from "~/context/AuthContext";
|
||||||
import { useScriptWorkspaceStore } from "~/features/platform/state/scriptWorkspaceStore";
|
import { useScriptWorkspaceStore } from "~/features/platform/state/scriptWorkspaceStore";
|
||||||
import { useUiStore } from "~/features/platform/state/uiStore";
|
import { toast } from "sonner";
|
||||||
import { ProjectManagementPage } from "~/features/admin/ProjectManagementPage";
|
import { ProjectManagementPage } from "~/features/admin/ProjectManagementPage";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,7 +11,11 @@ import { ProjectManagementPage } from "~/features/admin/ProjectManagementPage";
|
|||||||
export default function ProjectManagementRoute() {
|
export default function ProjectManagementRoute() {
|
||||||
const { user, currentWorkspace } = useAuth();
|
const { user, currentWorkspace } = useAuth();
|
||||||
const setApiOnline = useScriptWorkspaceStore((s) => s.setApiOnline);
|
const setApiOnline = useScriptWorkspaceStore((s) => s.setApiOnline);
|
||||||
const pushToast = useUiStore((s) => s.pushToast);
|
const pushToast = (notice: { tone: "success" | "error" | "info"; message: string }) => {
|
||||||
|
if (notice.tone === "error") toast.error(notice.message);
|
||||||
|
else if (notice.tone === "info") toast.info(notice.message);
|
||||||
|
else toast.success(notice.message);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectManagementPage
|
<ProjectManagementPage
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useAuth } from "~/context/AuthContext";
|
import { useAuth } from "~/context/AuthContext";
|
||||||
import { useScriptWorkspaceStore } from "~/features/platform/state/scriptWorkspaceStore";
|
import { useScriptWorkspaceStore } from "~/features/platform/state/scriptWorkspaceStore";
|
||||||
import { useUiStore } from "~/features/platform/state/uiStore";
|
import { toast } from "sonner";
|
||||||
import { RoleManagementPage } from "~/features/admin/RoleManagementPage";
|
import { RoleManagementPage } from "~/features/admin/RoleManagementPage";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,7 +11,11 @@ import { RoleManagementPage } from "~/features/admin/RoleManagementPage";
|
|||||||
export default function RoleManagementRoute() {
|
export default function RoleManagementRoute() {
|
||||||
const { user, currentWorkspace } = useAuth();
|
const { user, currentWorkspace } = useAuth();
|
||||||
const setApiOnline = useScriptWorkspaceStore((s) => s.setApiOnline);
|
const setApiOnline = useScriptWorkspaceStore((s) => s.setApiOnline);
|
||||||
const pushToast = useUiStore((s) => s.pushToast);
|
const pushToast = (notice: { tone: "success" | "error" | "info"; message: string }) => {
|
||||||
|
if (notice.tone === "error") toast.error(notice.message);
|
||||||
|
else if (notice.tone === "info") toast.info(notice.message);
|
||||||
|
else toast.success(notice.message);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RoleManagementPage
|
<RoleManagementPage
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useAuth } from "~/context/AuthContext";
|
import { useAuth } from "~/context/AuthContext";
|
||||||
import { useScriptWorkspaceStore } from "~/features/platform/state/scriptWorkspaceStore";
|
import { useScriptWorkspaceStore } from "~/features/platform/state/scriptWorkspaceStore";
|
||||||
import { useUiStore } from "~/features/platform/state/uiStore";
|
import { toast } from "sonner";
|
||||||
import { UserManagementPage } from "~/features/admin/UserManagementPage";
|
import { UserManagementPage } from "~/features/admin/UserManagementPage";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,7 +11,11 @@ import { UserManagementPage } from "~/features/admin/UserManagementPage";
|
|||||||
export default function UserManagementRoute() {
|
export default function UserManagementRoute() {
|
||||||
const { user, currentWorkspace } = useAuth();
|
const { user, currentWorkspace } = useAuth();
|
||||||
const setApiOnline = useScriptWorkspaceStore((s) => s.setApiOnline);
|
const setApiOnline = useScriptWorkspaceStore((s) => s.setApiOnline);
|
||||||
const pushToast = useUiStore((s) => s.pushToast);
|
const pushToast = (notice: { tone: "success" | "error" | "info"; message: string }) => {
|
||||||
|
if (notice.tone === "error") toast.error(notice.message);
|
||||||
|
else if (notice.tone === "info") toast.info(notice.message);
|
||||||
|
else toast.success(notice.message);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserManagementPage
|
<UserManagementPage
|
||||||
|
|||||||
@@ -23,10 +23,12 @@
|
|||||||
"isbot": "^5.1.36",
|
"isbot": "^5.1.36",
|
||||||
"lucide-react": "^1.33.0",
|
"lucide-react": "^1.33.0",
|
||||||
"monaco-editor": "^0.56.0",
|
"monaco-editor": "^0.56.0",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.7",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7",
|
||||||
"react-router": "^8",
|
"react-router": "^8",
|
||||||
"shadcn": "^4.19.0",
|
"shadcn": "^4.19.0",
|
||||||
|
"sonner": "^2.0.8",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"zustand": "^5.0.14"
|
"zustand": "^5.0.14"
|
||||||
|
|||||||
Generated
+34
@@ -41,6 +41,9 @@ importers:
|
|||||||
monaco-editor:
|
monaco-editor:
|
||||||
specifier: ^0.56.0
|
specifier: ^0.56.0
|
||||||
version: 0.56.0
|
version: 0.56.0
|
||||||
|
next-themes:
|
||||||
|
specifier: ^0.4.6
|
||||||
|
version: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||||
react:
|
react:
|
||||||
specifier: ^19.2.7
|
specifier: ^19.2.7
|
||||||
version: 19.2.8
|
version: 19.2.8
|
||||||
@@ -53,6 +56,9 @@ importers:
|
|||||||
shadcn:
|
shadcn:
|
||||||
specifier: ^4.19.0
|
specifier: ^4.19.0
|
||||||
version: 4.19.0(typescript@5.9.3)
|
version: 4.19.0(typescript@5.9.3)
|
||||||
|
sonner:
|
||||||
|
specifier: ^2.0.8
|
||||||
|
version: 2.0.8(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^3.6.0
|
specifier: ^3.6.0
|
||||||
version: 3.6.0
|
version: 3.6.0
|
||||||
@@ -1574,6 +1580,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
next-themes@0.4.6:
|
||||||
|
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
|
|
||||||
node-releases@2.0.51:
|
node-releases@2.0.51:
|
||||||
resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==}
|
resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1897,6 +1909,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==}
|
resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==}
|
||||||
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
|
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
|
||||||
|
|
||||||
|
sonner@2.0.8:
|
||||||
|
resolution: {integrity: sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': ^18.0.0 || ^19.0.0
|
||||||
|
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -3521,6 +3543,11 @@ snapshots:
|
|||||||
|
|
||||||
negotiator@1.0.0: {}
|
negotiator@1.0.0: {}
|
||||||
|
|
||||||
|
next-themes@0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.8
|
||||||
|
react-dom: 19.2.8(react@19.2.8)
|
||||||
|
|
||||||
node-releases@2.0.51: {}
|
node-releases@2.0.51: {}
|
||||||
|
|
||||||
npm-run-path@4.0.1:
|
npm-run-path@4.0.1:
|
||||||
@@ -3890,6 +3917,13 @@ snapshots:
|
|||||||
ip-address: 10.5.0
|
ip-address: 10.5.0
|
||||||
smart-buffer: 4.2.0
|
smart-buffer: 4.2.0
|
||||||
|
|
||||||
|
sonner@2.0.8(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.8
|
||||||
|
react-dom: 19.2.8(react@19.2.8)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.17
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
source-map-support@0.5.21:
|
source-map-support@0.5.21:
|
||||||
|
|||||||
Reference in New Issue
Block a user