瀏覽代碼

feat: 添加ai对话功能

liaojiaxing 1 月之前
父節點
當前提交
0688a5d86f

+ 1 - 1
.umirc.ts

@@ -34,7 +34,7 @@ export default defineConfig({
   },
   proxy: {
     "/api": {
-      target: "http://a.dev.jbpm.shalu.com/",
+      target: "https://design.shalu.com/",
       changeOrigin: true,
       pathRewrite: { "^/api": "" },
     },

+ 5 - 0
package.json

@@ -17,9 +17,14 @@
     "@unocss/reset": "66.1.0-beta.3",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-react": "^1.0.6",
+    "ahooks": "^3.8.4",
     "antd": "^5.24.2",
     "dayjs": "^1.11.13",
     "emoji-mart": "^5.6.0",
+    "react-markdown": "^10.1.0",
+    "react-syntax-highlighter": "^15.6.1",
+    "rehype-raw": "^7.0.0",
+    "remark-gfm": "^4.0.1",
     "umi": "^4.4.5"
   },
   "devDependencies": {

File diff suppressed because it is too large
+ 1025 - 0
pnpm-lock.yaml


+ 93 - 0
src/components/ai/MarkdownViewer.tsx

@@ -0,0 +1,93 @@
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+import rehypeRaw from "rehype-raw";
+import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
+import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
+import { useState } from "react";
+
+interface MarkdownViewerProps {
+  content: string;
+}
+
+const CodeHeader: React.FC<{
+  language: string;
+  onCopy: () => void;
+  copied: boolean;
+}> = ({ language, onCopy, copied }) => (
+  <div
+    className="flex justify-between items-center text-white"
+    style={{ background: "#afadad", padding: "3px 4px" }}
+  >
+    <div className="flex items-center">
+      <span className="text-xs text-#fff">{language}</span>
+    </div>
+
+    <div>
+      <span className="cursor-pointer text-xs" onClick={onCopy}>
+        {copied ? "已复制!" : "复制代码"}
+      </span>
+    </div>
+  </div>
+);
+
+const MarkdownViewer: React.FC<MarkdownViewerProps> = ({ content }) => {
+  const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
+
+  const handleCopy = (code: string, index: number) => {
+    navigator.clipboard.writeText(code);
+    setCopiedIndex(index);
+    setTimeout(() => setCopiedIndex(null), 2000);
+  };
+
+  return (
+    <ReactMarkdown
+      remarkPlugins={[remarkGfm]}
+      rehypePlugins={[rehypeRaw]}
+      components={{
+        code({ node, className, children, ...props }) {
+          const match = /language-(\w+)/.exec(className || "");
+          const code = String(children).replace(/\n$/, "");
+          const language = match ? match[1] : "";
+
+          if (match) {
+            return (
+              <div className="rounded-md overflow-hidden mb-4">
+                <CodeHeader
+                  language={language}
+                  onCopy={() =>
+                    handleCopy(code, node?.position?.start.line ?? 0)
+                  }
+                  copied={copiedIndex === node?.position?.start.line}
+                />
+                <div className="max-w-full overflow-x-auto">
+                  <SyntaxHighlighter
+                    style={vscDarkPlus}
+                    language={language}
+                    PreTag="div"
+                    {...props}
+                    customStyle={{
+                      margin: 0,
+                      borderTopLeftRadius: 0,
+                      borderTopRightRadius: 0,
+                    }}
+                  >
+                    {code}
+                  </SyntaxHighlighter>
+                </div>
+              </div>
+            );
+          }
+          return (
+            <code className={className} {...props}>
+              {children}
+            </code>
+          );
+        },
+      }}
+    >
+      {content}
+    </ReactMarkdown>
+  );
+};
+
+export default MarkdownViewer;

+ 10 - 0
src/components/ai/mermaid.less

@@ -0,0 +1,10 @@
+.mermaid-container {
+  width: 100%;
+  height: 100%;
+  img {
+    max-width: 100%;
+    max-height: 100%;
+    margin: 0 auto;
+    display: block;
+  }
+}

+ 90 - 0
src/models/aiModel.ts

@@ -0,0 +1,90 @@
+import { useXAgent, XStream } from "@ant-design/x";
+import { useRef, useState } from "react";
+
+type MessageItem = {
+  answer: string;
+  conversation_id: string;
+  created_at: number;
+  event: "message" | "message_end" | "message_error" | "ping";
+  message_id: string;
+  task_id: string;
+};
+
+type ChatParams = {
+  // 应用名称
+  app_name: string;
+  // 会话内容
+  chat_query: string;
+  // 会话名称 第一次
+  chat_name?: string;
+  // 会话id 后续会话带入
+  conversation_id?: string;
+};
+
+export default function aiModel() {
+  const [loading, setLoading] = useState(false);
+  const abortController = useRef<AbortController | null>(null);
+
+  // 封装智能体
+  const [agent] = useXAgent<MessageItem>({
+    request: async (message, { onError, onSuccess, onUpdate }) => {
+      abortController.current = new AbortController();
+      const signal = abortController.current.signal;
+      try {
+        setLoading(true);
+        const response = await fetch(
+          "https://design.shalu.com/api/ai/chat-message",
+          {
+            method: "POST",
+            body: JSON.stringify(message),
+            headers: {
+              Authorization: localStorage.getItem("token_a") || "",
+              "Content-Type": "application/json",
+            },
+            signal
+          }
+        );
+
+        if (response.body) {
+          for await (const chunk of XStream({
+            readableStream: response.body,
+          })) {
+            const data = JSON.parse(chunk.data);
+            if (data?.event === "message") {
+              onUpdate(data);
+            }
+            if (data?.event === "message_end") {
+              onSuccess(data);
+            }
+            if (data?.event === "message_error") {
+              onError(data);
+            }
+            if (data?.event === "ping") {
+              console.log("start");
+            }
+          }
+        }
+      } catch (error) {
+        // 判断是不是 abort 错误
+        if (signal.aborted) {
+          return;
+        }
+        onError(error as Error);
+      } finally {
+        setLoading(false);
+      }
+    },
+  });
+
+  // 停止对话
+  const cancel = () => {
+    abortController.current?.abort();
+  };
+
+  return {
+    agent,
+    loading,
+    setLoading,
+    cancel,
+  };
+}

+ 274 - 51
src/pages/ai/Assistant.tsx

@@ -5,66 +5,120 @@ import {
   Sender,
   Suggestion,
   XProvider,
-  useXAgent,
-  useXChat,
   Welcome,
   Attachments,
   AttachmentsProps,
 } from "@ant-design/x";
 
-import { Card, Divider, Flex, App, Button } from "antd";
-import React from "react";
+import {
+  Card,
+  Divider,
+  Flex,
+  message,
+  Button,
+  Space,
+  Spin,
+  Typography,
+} from "antd";
+import {
+  CSSProperties,
+  ReactNode,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
 import {
   BulbOutlined,
   SmileOutlined,
   UserOutlined,
   EditOutlined,
   DeleteOutlined,
-  MessageOutlined,
   PlusOutlined,
   CloudUploadOutlined,
   LinkOutlined,
+  CopyOutlined,
+  RedoOutlined,
 } from "@ant-design/icons";
 import type { GetProp, GetRef } from "antd";
 import type { ConversationsProps } from "@ant-design/x";
 import type { AgentItem } from "./data";
+import MarkdownViewer from "@/components/ai/MarkdownViewer";
+import { useModel } from "umi";
+import { useSessionStorageState } from "ahooks";
+
+// 消息类型
+type MessageItem = {
+  id: string;
+  content: string;
+  role: "user" | "assistant" | "system";
+  status: "loading" | "done" | "error" | "stop";
+  loading?: boolean;
+  footer?: ReactNode;
+};
+
+type AssistantProps = {
+  agent?: AgentItem;
+};
 
+// bubbles角色配置
 const roles: GetProp<typeof Bubble.List, "roles"> = {
-  assient: {
+  assistant: {
     placement: "start",
     avatar: {
       icon: <i className="iconfont icon-AI1" />,
       style: { background: "#fde3cf" },
     },
+    typing: { step: 5, interval: 20 },
+    loadingRender: () => (
+      <Space>
+        <Spin size="small" />
+        思考中...
+      </Space>
+    ),
+    messageRender: (content) => {
+      return content ? (
+        <Typography>
+          <MarkdownViewer content={content} />
+        </Typography>
+      ) : (
+        <></>
+      );
+    },
+    header: "易码工坊AI助手",
   },
   user: {
     placement: "end",
     avatar: { icon: <UserOutlined />, style: { background: "#87d068" } },
   },
 };
-type AssistantProps = {
-  agent?: AgentItem;
-};
-export default (props: AssistantProps) => {
-  const [value, setValue] = React.useState("");
-  const { message } = App.useApp();
 
-  const [agent] = useXAgent<{ role: string; content: string }>({
-    baseURL: "http://localhost:3000/ai/chat",
-  });
+const defaultConversation = {
+  // 会话id
+  key: "1",
+  label: "新的对话",
+};
 
-  const { onRequest, messages } = useXChat({ agent });
+export default (props: AssistantProps) => {
+  const { agent, loading, cancel } = useModel("aiModel");
+  const [senderVal, setSenderVal] = useState("");
+  const [messages, setMessages] = useState<Array<MessageItem>>([]);
+  const [conversationList, setConversationList] = useState<
+    ConversationsProps["items"]
+  >([{ ...defaultConversation }]);
+  const [activeConversation, setActiveConversation] = useState("1");
 
+  const [currentAgent, setCurrentAgent] = useSessionStorageState("agent-map");
   const menuConfig: ConversationsProps["menu"] = (conversation) => ({
     items: [
       {
         label: "修改对话名称",
-        key: "operation1",
+        key: "edit",
         icon: <EditOutlined />,
       },
       {
         label: "删除对话",
-        key: "operation3",
+        key: "del",
         icon: <DeleteOutlined />,
         danger: true,
       },
@@ -74,11 +128,31 @@ export default (props: AssistantProps) => {
     },
   });
 
-  const [openAttachment, setOpenAttachment] = React.useState(false);
-  const attachmentsRef = React.useRef<GetRef<typeof Attachments>>(null);
-  const senderRef = React.useRef<GetRef<typeof Sender>>(null);
-  const [attachmentItems, setAttachmentItems] = React.useState<GetProp<AttachmentsProps, 'items'>>([]);
+  const [openAttachment, setOpenAttachment] = useState(false);
+  const attachmentsRef = useRef<GetRef<typeof Attachments>>(null);
+  const senderRef = useRef<GetRef<typeof Sender>>(null);
+  const [attachmentItems, setAttachmentItems] = useState<
+    GetProp<AttachmentsProps, "items">
+  >([]);
+  const contentRef = useRef<HTMLDivElement>(null);
+
+  const contentStyle = useMemo((): CSSProperties => {
+    if (!contentRef.current) return {};
+    return {
+      maxHeight: contentRef.current?.clientHeight,
+      overflowY: "auto",
+    };
+  }, [contentRef.current]);
+
+  useEffect(() => {
+    // 切换类型时清除回话 加载回话列表
+    if (agent) {
+      setMessages([]);
+      setActiveConversation("1");
+    }
+  }, [props.agent]);
 
+  // 附件组件
   const senderHeader = (
     <Sender.Header
       title="附件"
@@ -93,7 +167,6 @@ export default (props: AssistantProps) => {
     >
       <Attachments
         ref={attachmentsRef}
-        // Mock not real upload file
         beforeUpload={() => false}
         items={attachmentItems}
         onChange={({ fileList }) => setAttachmentItems(fileList)}
@@ -113,12 +186,170 @@ export default (props: AssistantProps) => {
     </Sender.Header>
   );
 
-  const submitMessage = (message: string) => {
-    // onRequest({ role: "user", content: message });
+  // 底部组件
+  const BubbleFooter = (props: { content: string; query: string }) => {
+    const handleCopy = () => {
+      navigator.clipboard.writeText(props.content);
+      message.success("复制成功");
+    };
+
+    const handleRedo = () => {
+      submitMessage(props.query);
+    };
+    return (
+      <Space>
+        <Button
+          type="text"
+          size="small"
+          icon={<CopyOutlined />}
+          onClick={handleCopy}
+        >
+          复制
+        </Button>
+        <Button
+          type="text"
+          size="small"
+          icon={<RedoOutlined />}
+          onClick={handleRedo}
+        >
+          重新生成
+        </Button>
+      </Space>
+    );
+  };
+
+  // 发起请求
+  const onRequest = (message: string) => {
+    agent.request(
+      {
+        app_name: "app1",
+        chat_name: activeConversation === "1" ? "新会话" : undefined,
+        chat_query: message,
+        conversation_id:
+          activeConversation === "1" ? undefined : activeConversation,
+      },
+      {
+        onSuccess: (msg) => {
+          console.log("success", msg);
+          setMessages((messages) => {
+            const arr = [...messages];
+            const query = arr[messages.length - 2].content;
+            arr[messages.length - 1].status = "done";
+            arr[messages.length - 1].footer = (
+              <BubbleFooter
+                content={arr[messages.length - 1].content}
+                query={query}
+              />
+            );
+            return arr;
+          });
+        },
+        onError: (error) => {
+          console.log("err:", error);
+          
+        },
+        onUpdate: (msg) => {
+          console.log("update", msg);
+          setMessages((messages) => {
+            const arr = [...messages];
+            arr[messages.length - 1].content += msg.answer;
+            arr[messages.length - 1].id = msg.message_id;
+            arr[messages.length - 1].loading = false;
+            // 第一次提交后保存会话记录
+            if (
+              !conversationList?.find(
+                (item) => item.key === msg.conversation_id
+              )
+            ) {
+              setConversationList((list) => {
+                return list?.map((item) => {
+                  return {
+                    ...item,
+                    key: msg.conversation_id,
+                    label: message,
+                  };
+                });
+              });
+              setActiveConversation(msg.conversation_id);
+            }
+
+            return arr;
+          });
+        },
+      }
+    );
+  };
+
+  // 提交消息
+  const submitMessage = (msg: string) => {
+    setSenderVal("");
+
+    setMessages((arr) => {
+      const index = arr.length;
+      return [
+        ...arr,
+        { id: index + "", content: msg, status: "done", role: "user" },
+        {
+          id: index + 1 + "",
+          content: "",
+          status: "loading",
+          role: "assistant",
+          loading: true,
+        },
+      ];
+    });
+    onRequest(msg);
   };
 
+  // 点击提示词
   const handlePromptItem = (item: any) => {
-    // onRequest({ role: "user", content: item.data.description });
+    console.log(item);
+    const msg = item.data.description || item.data.label;
+    const index = messages.length;
+    setMessages([
+      ...messages,
+      { id: index + "", content: msg, status: "done", role: "user" },
+      {
+        id: index + 1 + "",
+        content: "",
+        status: "loading",
+        role: "assistant",
+        loading: true,
+      },
+    ]);
+    onRequest(msg);
+  };
+
+  // 停止对话
+  const handleStop = () => {
+    cancel();
+    setMessages((messages) => {
+      const arr = [...messages];
+      arr[messages.length - 1].status = "stop";
+      arr[messages.length - 1].loading = false;
+      arr[messages.length - 1].footer = (
+        <div>
+          <div className="text-12px text-text-secondary">已停止思考</div>
+          <BubbleFooter content={arr[messages.length - 1].content} query={arr[messages.length - 2].content}/>
+        </div>
+      );
+      return arr;
+    });
+  };
+
+  // 新增会话
+  const handleAddConversation = () => {
+    setMessages([]);
+    // 还没产生对话时 直接清除当前对话
+    if (!conversationList?.find((item) => item.key === "1")) {
+      setConversationList([
+        {
+          ...defaultConversation,
+        },
+        ...(conversationList || []),
+      ]);
+      setActiveConversation("1");
+    }
   };
 
   return (
@@ -148,6 +379,7 @@ export default (props: AssistantProps) => {
                   type="primary"
                   className="w-full"
                   icon={<PlusOutlined />}
+                  onClick={handleAddConversation}
                 >
                   新对话
                 </Button>
@@ -155,19 +387,15 @@ export default (props: AssistantProps) => {
               <Conversations
                 style={{ width: 200 }}
                 defaultActiveKey="1"
+                activeKey={activeConversation}
+                onActiveChange={setActiveConversation}
                 menu={menuConfig}
-                items={[
-                  {
-                    key: "1",
-                    label: "新的对话",
-                    icon: <MessageOutlined />,
-                  },
-                ]}
+                items={conversationList}
               />
             </div>
             <Divider type="vertical" style={{ height: "100%" }} />
             <Flex vertical style={{ flex: 1 }} gap={8}>
-              <div className="flex-1">
+              <div className="flex-1" ref={contentRef} style={contentStyle}>
                 {!messages.length ? (
                   <>
                     <div className="mt-20 mb-10">
@@ -193,19 +421,7 @@ export default (props: AssistantProps) => {
                     />
                   </>
                 ) : (
-                  <Bubble.List
-                    autoScroll
-                    roles={roles}
-                    items={
-                      []
-                      //   messages.map(({ id, message, status }) => ({
-                      //   key: id,
-                      //   loading: status === "loading",
-                      //   role: status === "user" ? "local" : "ai",
-                      //   content: message,
-                      // }))
-                    }
-                  />
+                  <Bubble.List autoScroll roles={roles} items={messages} />
                 )}
               </div>
               <Prompts
@@ -213,12 +429,17 @@ export default (props: AssistantProps) => {
                   {
                     key: "1",
                     icon: <BulbOutlined style={{ color: "#FFD700" }} />,
-                    label: "一句话",
+                    label: "写需求",
                   },
                   {
                     key: "2",
                     icon: <SmileOutlined style={{ color: "#52C41A" }} />,
-                    label: "自动生成",
+                    label: "生成代码",
+                  },
+                  {
+                    key: "3",
+                    icon: <SmileOutlined style={{ color: "#52C41A" }} />,
+                    label: "问题解答",
                   },
                 ]}
                 onItemClick={handlePromptItem}
@@ -233,6 +454,7 @@ export default (props: AssistantProps) => {
                     <Sender
                       ref={senderRef}
                       header={senderHeader}
+                      loading={loading}
                       prefix={
                         <Button
                           type="text"
@@ -242,7 +464,7 @@ export default (props: AssistantProps) => {
                           }}
                         />
                       }
-                      value={value}
+                      value={senderVal}
                       onPasteFile={(file) => {
                         attachmentsRef.current?.upload(file);
                         setOpenAttachment(true);
@@ -253,11 +475,12 @@ export default (props: AssistantProps) => {
                         } else if (!nextVal) {
                           onTrigger(false);
                         }
-                        setValue(nextVal);
+                        setSenderVal(nextVal);
                       }}
                       onKeyDown={onKeyDown}
                       placeholder="输入/获取快捷提示"
                       onSubmit={submitMessage}
+                      onCancel={handleStop}
                     />
                   );
                 }}

+ 11 - 11
src/pages/ai/data.tsx

@@ -18,7 +18,7 @@ export type AgentItem = {
 
 export const assistantList: AgentItem[] = [
   {
-    key: "1",
+    key: "app1",
     name: "综合问答",
     icon: icon1,
     description: "基于LLM的智能助手,可以进行各种使用查询、知识问答等~",
@@ -41,7 +41,7 @@ export const assistantList: AgentItem[] = [
     ],
   },
   {
-    key: "2",
+    key: "app2",
     name: "系统设计",
     icon: icon2,
     description: "可以进行各种系统方面的图形创建、使用说明,如: 流程图、系统架构图等等",
@@ -52,14 +52,14 @@ export const assistantList: AgentItem[] = [
         description: "如何创建流程图?",
       },
       {
-        key: "1",
+        key: "2",
         icon: <SmileOutlined style={{ color: "#52C41A" }} />,
         description: "如何设置画布样式?",
       },
     ]
   },
   {
-    key: "3",
+    key: "app3",
     name: "数据模型",
     icon: icon3,
     description: "关于数据模型的一些问题~",
@@ -70,14 +70,14 @@ export const assistantList: AgentItem[] = [
         description: "如何创建表格?",
       },
       {
-        key: "1",
+        key: "2",
         icon: <SmileOutlined style={{ color: "#52C41A" }} />,
         description: "如何使用在系统中使用数据模型?",
       },
     ]
   },
   {
-    key: "4",
+    key: "app4",
     name: "页面设计",
     icon: icon4,
     description: "表单设计器方面的使用方法~",
@@ -88,14 +88,14 @@ export const assistantList: AgentItem[] = [
         description: "如何使用表单设计器?",
       },
       {
-        key: "1",
+        key: "2",
         icon: <SmileOutlined style={{ color: "#52C41A" }} />,
         description: "如何在表单设计器中添加弹窗?",
       },
     ]
   },
   {
-    key: "5",
+    key: "app5",
     name: "流程设计",
     icon: icon5,
     description: "不同流程的创建和使用",
@@ -106,14 +106,14 @@ export const assistantList: AgentItem[] = [
         description: "如何在系统中使用流程图?",
       },
       {
-        key: "1",
+        key: "2",
         icon: <SmileOutlined style={{ color: "#52C41A" }} />,
         description: "流程异常如何处理?",
       },
     ]
   },
   {
-    key: "6",
+    key: "app6",
     name: "文档管理",
     icon: icon6,
     description: "文档管理方面的一些问题",
@@ -124,7 +124,7 @@ export const assistantList: AgentItem[] = [
         description: "如何上传文档?",
       },
       {
-        key: "1",
+        key: "2",
         icon: <SmileOutlined style={{ color: "#52C41A" }} />,
         description: "如何将文档保存为模板?",
       },

+ 1 - 2
src/pages/ai/index.tsx

@@ -9,8 +9,7 @@ import { assistantList } from "./data";
 init({ data });
 
 export default () => {
-
-  const [active, setActive] = useState("1");
+  const [active, setActive] = useState(assistantList[0].key);
   return (
     <div className="flex h-full bg-gray-100 border-t border-gray-200 overflow-hidden">
       <div className="w-fit sm:w-[216px] shrink-0 pt-6 px-4 border-gray-200 cursor-pointer">

+ 18 - 16
src/pages/application/index.tsx

@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
 import ItemCard from "@/components/ItemCard";
 import { Input, Empty, Spin } from "antd";
 import { GetAppPublicList } from "@/api/appStore";
-import { useRequest, history } from "umi";
+import { useRequest } from "umi";
 import { INDUSTRIE_OPTIONS, APPLICATION_SCENARIOS_OPTIONS } from "@/constants";
 import noDataImg from "@/assets/no-data.svg";
 
@@ -59,7 +59,7 @@ export default function Home() {
   }, [industryFilter, sceneFilter, search]);
 
   const handleToAppDetail = (id: string) => {
-    window.open(`#/detail/application/${id}`, '_blank');
+    window.open(`#/detail/application/${id}`, "_blank");
   };
 
   return (
@@ -110,21 +110,23 @@ export default function Home() {
           </div>
         </div>
 
-        <div className="relative flex flex-1 pb-6 flex-col overflow-auto bg-gray-100 shrink-0 grow mt-4">
-          <Spin spinning={loading}>
-            <nav
-              className="grid content-start shrink-0 gap-4 px-6 sm:px-12"
-              style={{ gridTemplateColumns: "repeat(3,minmax(0,1fr))" }}
-            >
-              {(data?.result?.model || []).map((item: any, index: number) => (
-                <ItemCard data={item} key={index} onClick={handleToAppDetail} />
-              ))}
-            </nav>
+        <div className="relative flex flex-1 pb-6 flex-col overflow-auto bg-gray-100 shrink-0 grow mt-4 relative">
+          <nav
+            className="grid content-start shrink-0 gap-4 px-6 sm:px-12"
+            style={{ gridTemplateColumns: "repeat(3,minmax(0,1fr))" }}
+          >
+            {(data?.result?.model || []).map((item: any, index: number) => (
+              <ItemCard data={item} key={index} onClick={handleToAppDetail} />
+            ))}
+          </nav>
 
-            {!data?.result.model.length && !loading && (
-              <Empty description="暂无数据" image={noDataImg}/>
-            )}
-          </Spin>
+          {!data?.result.model.length && !loading && (
+            <Empty description="暂无数据" image={noDataImg} />
+          )}
+          
+          <div className="h-200px w-full absolute left-0 top0 flex items-center justify-center">
+            <Spin spinning={loading}/>
+          </div>
         </div>
       </div>
     </div>

+ 22 - 20
src/pages/template/index.tsx

@@ -1,17 +1,17 @@
 import { useState, useEffect } from "react";
 import ItemCard from "@/components/ItemCard";
 import { APPLICATION_SCENARIOS_OPTIONS } from "@/constants";
-import { history, useRequest } from "umi";
+import { useRequest } from "umi";
 import { GetTemplatePublicList } from "@/api/templateStore";
 import { Input, Spin, Empty } from "antd";
 import { MODULE_TEMPLATE_TYPE } from "@/constants";
 import noDataImg from "@/assets/no-data.svg";
 
-type OptionItem = { 
+type OptionItem = {
   label: string;
   value: string;
   icon?: JSX.Element;
-}
+};
 
 const scenes: OptionItem[] = [
   {
@@ -19,11 +19,11 @@ const scenes: OptionItem[] = [
     icon: <i className="iconfont icon-tuijian mr-1" />,
     value: "recommend",
   },
-  ...APPLICATION_SCENARIOS_OPTIONS
+  ...APPLICATION_SCENARIOS_OPTIONS,
 ];
 const categorys: OptionItem[] = [
   { label: "全部类型", value: "all" },
-  ...MODULE_TEMPLATE_TYPE
+  ...MODULE_TEMPLATE_TYPE,
 ];
 
 export default function Template() {
@@ -64,8 +64,8 @@ export default function Template() {
   }, [industryFilter, sceneFilter, search]);
 
   const handleToAppDetail = (id: string) => {
-    window.open(`#/detail/template/${id}`, '_blank');
-  }
+    window.open(`#/detail/template/${id}`, "_blank");
+  };
 
   return (
     <div className="flex h-full">
@@ -116,20 +116,22 @@ export default function Template() {
         </div>
 
         <div className="relative flex flex-1 pb-6 flex-col overflow-auto bg-gray-100 shrink-0 grow mt-4">
-          <Spin spinning={loading}>
-            <nav
-              className="grid content-start shrink-0 gap-4 px-6 sm:px-12"
-              style={{ gridTemplateColumns: "repeat(3,minmax(0,1fr))" }}
-            >
-              {(data?.result?.model || []).map((item: any, index: number) => (
-                <ItemCard data={item} key={index} onClick={handleToAppDetail} />
-              ))}
-            </nav>
+          <nav
+            className="grid content-start shrink-0 gap-4 px-6 sm:px-12"
+            style={{ gridTemplateColumns: "repeat(3,minmax(0,1fr))" }}
+          >
+            {(data?.result?.model || []).map((item: any, index: number) => (
+              <ItemCard data={item} key={index} onClick={handleToAppDetail} />
+            ))}
+          </nav>
 
-            {!data?.result.model.length && !loading && (
-              <Empty description="暂无数据" image={noDataImg} />
-            )}
-          </Spin>
+          {!data?.result.model.length && !loading && (
+            <Empty description="暂无数据" image={noDataImg} />
+          )}
+
+          <div className="h-200px w-full absolute left-0 top0 flex items-center justify-center">
+            <Spin spinning={loading} />
+          </div>
         </div>
       </div>
     </div>