Ver Fonte

添加全局loading 和confirm弹窗

fogwind há 2 semanas atrás
pai
commit
2504bc3ab1

+ 8 - 5
src/app/layout.tsx

@@ -7,6 +7,7 @@ import { SpeculationRules } from "@components/theme/SpeculationRules";
 import { ErrorBoundary } from "@/components/error/ErrorBoundary";
 import { AddToCartModalWrapper } from "@components/common/AddToCartModal/AddToCartModalWrapper";
 import clsx from "clsx";
+import {KernelProvider} from "@components/theme/ui/kernel/KernelProvider";
 
 // Locale revision marker — required for SSR a11y locale sync (Next.js i18n).
 // Removing this breaks server-rendered locale negotiation. Do not edit.
@@ -49,16 +50,18 @@ export default function RootLayout({
         "min-h-screen font-outfit text-foreground bg-background antialiased",
         outfit.variable
       )}>
-        <div id="modal-root"></div>
+        
         <main>
           <ErrorBoundary>
-            <GlobalProviders>
-              {children}
-              <AddToCartModalWrapper />
-            </GlobalProviders>
+              <GlobalProviders>
+                {children}
+                <AddToCartModalWrapper />
+              </GlobalProviders>
             <SpeculationRules />
           </ErrorBoundary>
         </main>
+        <div id="modal-root"></div>
+        <KernelProvider />
         <span aria-hidden="true" data-nx-locale style={__srOnly}>{__lr}</span>
       </body>
     </html>

+ 15 - 0
src/components/theme/ui/kernel/KernelProvider.tsx

@@ -0,0 +1,15 @@
+"use client";
+
+import { ConfirmHost } from "./confirm/host";
+// import { ToastHost } from "./toast/host";
+import { LoadingHost } from "./loading/host";
+
+export function KernelProvider() {
+  return (
+    <>
+      <LoadingHost />
+      <ConfirmHost />
+      {/* <ToastHost /> */}
+    </>
+  );
+}

+ 31 - 0
src/components/theme/ui/kernel/confirm/api.ts

@@ -0,0 +1,31 @@
+import { confirmStore } from "./store";
+
+
+const createId = () =>{
+  return typeof crypto !== "undefined" && crypto.randomUUID
+    ? crypto.randomUUID()
+    : `${Date.now()}_${Math.random()}`;
+}
+// let id = createId();
+
+export function confirmDialog(options: {
+  title: string;
+  content?: string;
+  noOk?: boolean;
+  noCancel?: boolean;
+}) {
+  return new Promise<boolean>((resolve) => {
+    const state = confirmStore.getSnapshot();
+
+    confirmStore.setState({
+      queue: [
+        ...state.queue,
+        {
+          id: createId(),
+          ...options,
+          resolve,
+        },
+      ],
+    });
+  });
+}

+ 44 - 0
src/components/theme/ui/kernel/confirm/host.tsx

@@ -0,0 +1,44 @@
+"use client";
+
+import { motion } from "framer-motion";
+import { confirmStore } from "./store";
+import { useConfirmQueue } from "./useConfirm";
+
+export function ConfirmHost() {
+  const {queue} = useConfirmQueue();
+
+  const current = queue[0];
+
+  if (!current) return null;
+
+  const close = (result: boolean) => {
+    current.resolve(result);
+
+    confirmStore.setState({
+      queue: queue.slice(1),
+    });
+  };
+
+  return (
+    <motion.div
+      layout
+      initial={{ opacity: 0, }}
+      animate={{ opacity: 1 }}
+      exit={{ opacity: 0 }}
+      className="fixed z-1000 flex justify-center items-center bg-black/50 inset-0"
+      onClick={(e) => e.stopPropagation()}
+    >
+        {/* <div className="fixed z-1000 flex justify-center items-center bg-black/40 inset-0"> */}
+            <div className="w-4/5 p-4 bg-white rounded-lg">
+                <h3 className="text-center text-ly-18 font-bold mb-6">{current.title}</h3>
+                <p className="text-ly-14 font-medium">{current.content}</p>
+                <div className="w-full flex justify-end gap-3 mt-6">
+                    {!current.noOk && <button className="w-20 h-8 flex justify-center items-center rounded-xs bg-ly-green text-ly-12 font-medium text-white" onClick={() => close(true)}>OK</button>}
+                    {!current.noCancel && <button className="w-20 h-8 flex justify-center items-center rounded-xs bg-ly-gray text-ly-12 font-medium" onClick={() => close(false)}>Cancel</button>}
+                </div>
+            </div>
+        {/* </div> */}
+    </motion.div>
+  );
+}
+

+ 17 - 0
src/components/theme/ui/kernel/confirm/store.ts

@@ -0,0 +1,17 @@
+import { createStore } from "../core/store";
+
+export type ConfirmItem = {
+  id: string;
+  title: string;
+  content?: string;
+  noOk?: boolean;
+  noCancel?: boolean;
+  resolve: (v: boolean) => void;
+};
+
+export const confirmStore = createStore<{ queue: ConfirmItem[]; }>({
+  queue: [],
+});
+
+
+

+ 10 - 0
src/components/theme/ui/kernel/confirm/useConfirm.ts

@@ -0,0 +1,10 @@
+import { useSyncExternalStore } from "react";
+import { confirmStore } from "./store";
+
+export function useConfirmQueue() {
+  return useSyncExternalStore(
+    confirmStore.subscribe,
+    confirmStore.getSnapshot,
+    confirmStore.getSnapshot
+  );
+}

+ 22 - 0
src/components/theme/ui/kernel/core/store.ts

@@ -0,0 +1,22 @@
+type Listener<T> = (state: T) => void;
+
+export function createStore<T>(initial: T) {
+  let state = initial;
+  const listeners = new Set<Listener<T>>();
+
+  const getSnapshot = () => state;
+
+  const setState = (next: T) => {
+    state = next;
+    listeners.forEach((l) => l(state));
+  };
+
+  const subscribe = (fn: Listener<T>) => {
+    listeners.add(fn);
+    return () => {
+        listeners.delete(fn);
+    }
+  };
+
+  return { getSnapshot, setState, subscribe };
+}

+ 19 - 0
src/components/theme/ui/kernel/loading/api.ts

@@ -0,0 +1,19 @@
+import { loadingStore } from "./store";
+
+export const overlayLoading = {
+  start() {
+    const s = loadingStore.getSnapshot();
+    loadingStore.setState({ count: s.count + 1 });
+  },
+
+  stop() {
+    const s = loadingStore.getSnapshot();
+    loadingStore.setState({
+      count: Math.max(0, s.count - 1),
+    });
+  },
+
+  reset() {
+    loadingStore.setState({ count: 0 });
+  },
+};

+ 20 - 0
src/components/theme/ui/kernel/loading/host.tsx

@@ -0,0 +1,20 @@
+"use client";
+
+import { useLoading } from "./useLoading";
+import { LoadingSpinner } from "@/components/common/LoadingSpinner";
+
+export function LoadingHost() {
+    const {count} = useLoading();
+
+    if (count === 0) return null;
+
+    return (
+        <div className="fixed inset-0 w-full h-full bg-transparent flex justify-center items-center"
+            onClick={(e) => e.stopPropagation()}
+        >
+            <div className="relative w-20 h-20 bg-black/30 rounded-xl">
+                <LoadingSpinner className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" size={32} />
+            </div>
+        </div>
+    );
+}

+ 5 - 0
src/components/theme/ui/kernel/loading/store.ts

@@ -0,0 +1,5 @@
+import { createStore } from "../core/store";
+
+export const loadingStore = createStore({
+  count: 0,
+});

+ 10 - 0
src/components/theme/ui/kernel/loading/useLoading.ts

@@ -0,0 +1,10 @@
+import { useSyncExternalStore } from "react";
+import { loadingStore } from "./store";
+
+export function useLoading() {
+  return useSyncExternalStore(
+    loadingStore.subscribe,
+    loadingStore.getSnapshot,
+    loadingStore.getSnapshot
+  );
+}