Browse Source

Merge branch 'develop'

sunxiao 10 months ago
parent
commit
c9e5f8e0ab
62 changed files with 296706 additions and 296 deletions
  1. 26 0
      deploy/config.js
  2. 78 0
      deploy/index.js
  3. 1 1
      index.html
  4. 545 20
      package-lock.json
  5. 9 2
      package.json
  6. BIN
      public/factory.glb
  7. 97282 0
      public/factory.obj
  8. 22 0
      public/texture.mtl
  9. 1 1
      src/App.vue
  10. 5 0
      src/api/chat.js
  11. BIN
      src/assets/images/chat/back-btn-active.png
  12. BIN
      src/assets/images/chat/back-btn.png
  13. BIN
      src/assets/images/home/home_bg.png
  14. BIN
      src/assets/images/home/home_bg2.png
  15. BIN
      src/assets/images/warn-text1.png
  16. 8 0
      src/assets/svgs/chat/icon-close-btn.svg
  17. 69 35
      src/components/Chat/ChatAnswer.vue
  18. 3 5
      src/components/Chat/ChatInput.vue
  19. 351 0
      src/components/Chat/ChatInputCopy.vue
  20. 4 4
      src/components/Chat/ChatText.vue
  21. 3 1
      src/components/Chat/index.js
  22. 21 23
      src/components/Dialog/editPassword.vue
  23. 44 30
      src/components/Layout/TheChatView.vue
  24. 12 4
      src/components/Layout/TheSubMenu.vue
  25. 3 3
      src/components/Layout/TheUserAvatar.vue
  26. 3 15
      src/components/Layout/userTop.vue
  27. 1 1
      src/components/RecodeSquareCardItem/index.vue
  28. 8 0
      src/composables/useChat.js
  29. 20 5
      src/composables/useFetchStream.js
  30. 22 9
      src/composables/useInfinite.js
  31. 1 3
      src/main.ts
  32. 32 0
      src/router/index.js
  33. 4 5
      src/utils/format.js
  34. 24 13
      src/utils/tools.js
  35. 20 11
      src/views/analyse/ForecastView.vue
  36. 16 5
      src/views/analyse/PymolView.vue
  37. 34 12
      src/views/analyse/WaterView.vue
  38. 30 18
      src/views/analyse/WorkOrder.vue
  39. 18 24
      src/views/analyse/config/echartOptions.js
  40. 75 9
      src/views/analyse/config/index.jsx
  41. 30 6
      src/views/answer/AnswerView.vue
  42. 1158 0
      src/views/count/all_book1.json
  43. 496 0
      src/views/count/all_book2.json
  44. 340 0
      src/views/count/all_book3.json
  45. 144 0
      src/views/count/index1.vue
  46. 144 0
      src/views/count/index2.vue
  47. 144 0
      src/views/count/index3.vue
  48. BIN
      src/views/screen/3d/factory.glb
  49. 97282 0
      src/views/screen/3d/factory.obj
  50. 185 0
      src/views/screen/3d/renderModel.js
  51. 22 0
      src/views/screen/3d/texture.mtl
  52. 0 4
      src/views/screen/ScreenView.vue
  53. 336 0
      src/views/screen/ScreenView2.vue
  54. 2 2
      src/views/screen/components/ControlHelper.vue
  55. 19 8
      src/views/screen/components/dataBox.vue
  56. 21 0
      src/views/screen/components/middleBox.vue
  57. 97282 0
      src/views/test/factory.obj
  58. 71 0
      src/views/test/index.vue
  59. 129 0
      src/views/test/renderModel.js
  60. 22 0
      src/views/test/texture.mtl
  61. 40 17
      src/views/work/WorkView.vue
  62. 44 0
      vite.config.ts.timestamp-1720486813773-2c95633e7e6a2.mjs

+ 26 - 0
deploy/config.js

@@ -0,0 +1,26 @@
+export const config = [
+  {
+    id: 0,
+    nodeEnv: "test",
+    name: "测试环境",
+    domain: "",
+    host: "10.0.0.28",
+    port: "22",
+    username: "root",
+    password: "hsmysql",
+    path: "/usr/share/nginx/html",
+    removepath: "/usr/share/nginx/html"
+  },
+  {
+    id: 1,
+    nodeEnv: "prod",
+    name: "生产环境",
+    domain: "",
+    host: "192.168.9.54",
+    port: "22",
+    username: "root",
+    password: "admin1,xxh",
+    path: "/usr/share/nginx/html",
+    removepath: "/usr/share/nginx/html"
+  },
+];

+ 78 - 0
deploy/index.js

@@ -0,0 +1,78 @@
+import Client from 'ssh2-sftp-client';
+import chalk from 'chalk';
+import ora from 'ora';
+import shell from 'shelljs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import { config } from './config.js';
+
+const rawArgv = process.argv.slice(2);
+const filterStage = rawArgv.includes("--prod") ? "prod" : "test";
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+// 构建
+const compileDist = async () => {
+  console.log(chalk.blue("项目开始构建"));
+  if (shell.exec(`npm run build:${filterStage}`).code === 0) {
+    console.log(chalk.blue("项目构建成功"));
+  }
+}
+
+// 获取配置
+const getConfig = () => {
+  const [ result ] = config.filter(({ nodeEnv }) => nodeEnv === filterStage);
+  return result;
+}
+
+// 部署
+const connectShell = async () => {
+  const sftp = new Client();
+  const item = getConfig();
+  let spinner = null;
+
+  // 打印信息
+  const printMsg = ({ color, text }) => {
+    console.log(chalk.red(`${item.host} --> `) + chalk[color](text));
+  }
+
+  printMsg({color: 'green', text: '服务器连接中'});
+
+  sftp.connect({
+    host: item.host,
+    port: item.port,
+    username: item.username,
+    password: item.password,
+  })
+  .then(() => {
+    printMsg({color: 'green', text: '服务器连接成功'});
+    printMsg({color: 'yellow', text: '执行删除文件中'});
+    return sftp.rmdir(item.path, true);
+  })
+  .then(() => {
+    printMsg({color: 'green', text: '执行删除文件成功'});
+    printMsg({color: 'green', text: '即将开始上传'});
+    spinner = ora().start();
+    spinner.text = '文件上传中,请等待'
+    return sftp.uploadDir(path.resolve(__dirname, "../dist"), item.path);
+  })
+  .then(() => {
+    spinner.info('文件上传结束')
+    spinner.stop();
+    printMsg({color: 'green', text: '上传完成,部署成功'});
+    sftp.end();
+  })
+  .catch((err) => {
+    console.error(
+      err,
+      chalk.red(`${item.host} -->`) + chalk.red(`上传失败`)
+    );
+    sftp.end();
+  });
+}
+
+async function runStart() {
+  await compileDist();
+  await connectShell();
+}
+
+runStart();

+ 1 - 1
index.html

@@ -5,7 +5,7 @@
   <meta charset="UTF-8">
   <link rel="icon" href="/favicon.ico">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
-  <link rel="stylesheet" href="https://static.fuxicarbon.com/bigModel/js/katex.min.css">
+  <!-- <link rel="stylesheet" href="https://static.fuxicarbon.com/bigModel/js/katex.min.css"> -->
   <script src="https://static.fuxicarbon.com/bigModel/js/katex.min.js"></script>
   <title>LibraAI人工智能运营体</title>
 </head>

+ 545 - 20
package-lock.json

@@ -24,6 +24,7 @@
         "pinia-plugin-persistedstate": "^3.2.1",
         "sass": "^1.77.1",
         "sass-loader": "^14.2.1",
+        "three": "^0.166.1",
         "vue": "^3.4.21",
         "vue-router": "^4.3.0"
       },
@@ -35,9 +36,13 @@
         "@vitejs/plugin-vue-jsx": "^3.1.0",
         "@vue/tsconfig": "^0.5.1",
         "autoprefixer": "^10.4.19",
+        "chalk": "^5.3.0",
         "dayjs": "^1.11.11",
         "npm-run-all2": "^6.1.2",
+        "ora": "^8.0.1",
         "postcss": "^8.4.38",
+        "shelljs": "^0.8.5",
+        "ssh2-sftp-client": "^10.0.3",
         "tailwindcss": "^3.4.4",
         "typescript": "~5.4.0",
         "vite": "^5.2.8",
@@ -1997,6 +2002,15 @@
         "ascii2mathml": "bin/index.js"
       }
     },
+    "node_modules/asn1": {
+      "version": "0.2.6",
+      "resolved": "https://registry.npmmirror.com/asn1/-/asn1-0.2.6.tgz",
+      "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
+      "dev": true,
+      "dependencies": {
+        "safer-buffer": "~2.1.0"
+      }
+    },
     "node_modules/assign-symbols": {
       "version": "1.0.0",
       "resolved": "https://registry.npmmirror.com/assign-symbols/-/assign-symbols-1.0.0.tgz",
@@ -2114,6 +2128,15 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/bcrypt-pbkdf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+      "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
+      "dev": true,
+      "dependencies": {
+        "tweetnacl": "^0.14.3"
+      }
+    },
     "node_modules/big.js": {
       "version": "5.2.2",
       "resolved": "https://registry.npmmirror.com/big.js/-/big.js-5.2.2.tgz",
@@ -2198,6 +2221,22 @@
         "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
       }
     },
+    "node_modules/buffer-from": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz",
+      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+      "dev": true
+    },
+    "node_modules/buildcheck": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmmirror.com/buildcheck/-/buildcheck-0.0.6.tgz",
+      "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==",
+      "dev": true,
+      "optional": true,
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
     "node_modules/bundle-name": {
       "version": "4.1.0",
       "resolved": "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz",
@@ -2294,28 +2333,15 @@
       ]
     },
     "node_modules/chalk": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmmirror.com/chalk/-/chalk-1.1.3.tgz",
-      "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==",
-      "dev": true,
-      "dependencies": {
-        "ansi-styles": "^2.2.1",
-        "escape-string-regexp": "^1.0.2",
-        "has-ansi": "^2.0.0",
-        "strip-ansi": "^3.0.0",
-        "supports-color": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/chalk/node_modules/supports-color": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-2.0.0.tgz",
-      "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==",
+      "version": "5.3.0",
+      "resolved": "https://registry.npmmirror.com/chalk/-/chalk-5.3.0.tgz",
+      "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
       "dev": true,
       "engines": {
-        "node": ">=0.8.0"
+        "node": "^12.17.0 || ^14.13 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
       }
     },
     "node_modules/chokidar": {
@@ -2384,6 +2410,33 @@
       "resolved": "https://registry.npmmirror.com/claygl/-/claygl-1.3.0.tgz",
       "integrity": "sha512-+gGtJjT6SSHD2l2yC3MCubW/sCV40tZuSs5opdtn79vFSGUgp/lH139RNEQ6Jy078/L0aV8odCw8RSrUcMfLaQ=="
     },
+    "node_modules/cli-cursor": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-4.0.0.tgz",
+      "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==",
+      "dev": true,
+      "dependencies": {
+        "restore-cursor": "^4.0.0"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/cli-spinners": {
+      "version": "2.9.2",
+      "resolved": "https://registry.npmmirror.com/cli-spinners/-/cli-spinners-2.9.2.tgz",
+      "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/clone": {
       "version": "2.1.2",
       "resolved": "https://registry.npmmirror.com/clone/-/clone-2.1.2.tgz",
@@ -2456,6 +2509,27 @@
       "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
       "dev": true
     },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true
+    },
+    "node_modules/concat-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/concat-stream/-/concat-stream-2.0.0.tgz",
+      "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
+      "dev": true,
+      "engines": [
+        "node >= 6.0"
+      ],
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^3.0.2",
+        "typedarray": "^0.0.6"
+      }
+    },
     "node_modules/convert-source-map": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -2484,6 +2558,21 @@
         "node": ">= 0.10"
       }
     },
+    "node_modules/cpu-features": {
+      "version": "0.0.10",
+      "resolved": "https://registry.npmmirror.com/cpu-features/-/cpu-features-0.0.10.tgz",
+      "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
+      "dev": true,
+      "hasInstallScript": true,
+      "optional": true,
+      "dependencies": {
+        "buildcheck": "~0.0.6",
+        "nan": "^2.19.0"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.3",
       "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -3006,6 +3095,12 @@
         "url": "https://github.com/fb55/entities?sponsor=1"
       }
     },
+    "node_modules/err-code": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/err-code/-/err-code-2.0.3.tgz",
+      "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
+      "dev": true
+    },
     "node_modules/error-stack-parser-es": {
       "version": "0.1.4",
       "resolved": "https://registry.npmmirror.com/error-stack-parser-es/-/error-stack-parser-es-0.1.4.tgz",
@@ -3488,6 +3583,12 @@
         "node": ">=12"
       }
     },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "dev": true
+    },
     "node_modules/fsevents": {
       "version": "2.3.3",
       "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
@@ -3546,6 +3647,18 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/get-east-asian-width": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz",
+      "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==",
+      "dev": true,
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/get-intrinsic": {
       "version": "1.2.4",
       "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
@@ -3917,6 +4030,17 @@
       "resolved": "https://registry.npmmirror.com/immutable/-/immutable-4.3.6.tgz",
       "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ=="
     },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+      "dev": true,
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
     "node_modules/inherits": {
       "version": "2.0.4",
       "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
@@ -3937,6 +4061,15 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/interpret": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/interpret/-/interpret-1.4.0.tgz",
+      "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
     "node_modules/is-accessor-descriptor": {
       "version": "1.0.1",
       "resolved": "https://registry.npmmirror.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz",
@@ -4159,6 +4292,18 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/is-interactive": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/is-interactive/-/is-interactive-2.0.0.tgz",
+      "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/is-negative-zero": {
       "version": "2.0.3",
       "resolved": "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
@@ -4303,6 +4448,18 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-unicode-supported": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-2.0.0.tgz",
+      "integrity": "sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/is-weakref": {
       "version": "1.0.2",
       "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.0.2.tgz",
@@ -4534,6 +4691,34 @@
       "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
       "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
     },
+    "node_modules/log-symbols": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-6.0.0.tgz",
+      "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
+      "dev": true,
+      "dependencies": {
+        "chalk": "^5.3.0",
+        "is-unicode-supported": "^1.3.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/log-symbols/node_modules/is-unicode-supported": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
+      "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/lru-cache": {
       "version": "5.1.1",
       "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -4906,6 +5091,13 @@
         "vue": "^3.0.0"
       }
     },
+    "node_modules/nan": {
+      "version": "2.20.0",
+      "resolved": "https://registry.npmmirror.com/nan/-/nan-2.20.0.tgz",
+      "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==",
+      "dev": true,
+      "optional": true
+    },
     "node_modules/nanoid": {
       "version": "3.3.7",
       "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz",
@@ -5234,6 +5426,15 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dev": true,
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
     "node_modules/onetime": {
       "version": "6.0.0",
       "resolved": "https://registry.npmmirror.com/onetime/-/onetime-6.0.0.tgz",
@@ -5267,6 +5468,79 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/ora": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmmirror.com/ora/-/ora-8.0.1.tgz",
+      "integrity": "sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ==",
+      "dev": true,
+      "dependencies": {
+        "chalk": "^5.3.0",
+        "cli-cursor": "^4.0.0",
+        "cli-spinners": "^2.9.2",
+        "is-interactive": "^2.0.0",
+        "is-unicode-supported": "^2.0.0",
+        "log-symbols": "^6.0.0",
+        "stdin-discarder": "^0.2.1",
+        "string-width": "^7.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ora/node_modules/ansi-regex": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.0.1.tgz",
+      "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+      "dev": true,
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/ora/node_modules/emoji-regex": {
+      "version": "10.3.0",
+      "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.3.0.tgz",
+      "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
+      "dev": true
+    },
+    "node_modules/ora/node_modules/string-width": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz",
+      "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+      "dev": true,
+      "dependencies": {
+        "emoji-regex": "^10.3.0",
+        "get-east-asian-width": "^1.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ora/node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz",
+      "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+      "dev": true,
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
     "node_modules/pascalcase": {
       "version": "0.1.1",
       "resolved": "https://registry.npmmirror.com/pascalcase/-/pascalcase-0.1.1.tgz",
@@ -5282,6 +5556,15 @@
       "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
       "dev": true
     },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/path-key": {
       "version": "3.1.1",
       "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
@@ -5653,6 +5936,19 @@
         "posthtml-render": "^1.0.6"
       }
     },
+    "node_modules/promise-retry": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/promise-retry/-/promise-retry-2.0.1.tgz",
+      "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+      "dev": true,
+      "dependencies": {
+        "err-code": "^2.0.2",
+        "retry": "^0.12.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/proxy-from-env": {
       "version": "1.1.0",
       "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -5746,6 +6042,18 @@
         "node": ">=8.10.0"
       }
     },
+    "node_modules/rechoir": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmmirror.com/rechoir/-/rechoir-0.6.2.tgz",
+      "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==",
+      "dev": true,
+      "dependencies": {
+        "resolve": "^1.1.6"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
     "node_modules/regenerator-runtime": {
       "version": "0.14.1",
       "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
@@ -5849,6 +6157,52 @@
       "deprecated": "https://github.com/lydell/resolve-url#deprecated",
       "dev": true
     },
+    "node_modules/restore-cursor": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-4.0.0.tgz",
+      "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==",
+      "dev": true,
+      "dependencies": {
+        "onetime": "^5.1.0",
+        "signal-exit": "^3.0.2"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/restore-cursor/node_modules/mimic-fn": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz",
+      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/restore-cursor/node_modules/onetime": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz",
+      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+      "dev": true,
+      "dependencies": {
+        "mimic-fn": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/restore-cursor/node_modules/signal-exit": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz",
+      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+      "dev": true
+    },
     "node_modules/ret": {
       "version": "0.1.15",
       "resolved": "https://registry.npmmirror.com/ret/-/ret-0.1.15.tgz",
@@ -5858,6 +6212,15 @@
         "node": ">=0.12"
       }
     },
+    "node_modules/retry": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz",
+      "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+      "dev": true,
+      "engines": {
+        "node": ">= 4"
+      }
+    },
     "node_modules/reusify": {
       "version": "1.0.4",
       "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz",
@@ -6014,6 +6377,12 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "dev": true
+    },
     "node_modules/sass": {
       "version": "1.77.1",
       "resolved": "https://registry.npmmirror.com/sass/-/sass-1.77.1.tgz",
@@ -6160,6 +6529,66 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/shelljs": {
+      "version": "0.8.5",
+      "resolved": "https://registry.npmmirror.com/shelljs/-/shelljs-0.8.5.tgz",
+      "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==",
+      "dev": true,
+      "dependencies": {
+        "glob": "^7.0.0",
+        "interpret": "^1.0.0",
+        "rechoir": "^0.6.2"
+      },
+      "bin": {
+        "shjs": "bin/shjs"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/shelljs/node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/shelljs/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "dev": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/shelljs/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/side-channel": {
       "version": "1.0.6",
       "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.0.6.tgz",
@@ -6372,6 +6801,42 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/ssh2": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmmirror.com/ssh2/-/ssh2-1.15.0.tgz",
+      "integrity": "sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "dependencies": {
+        "asn1": "^0.2.6",
+        "bcrypt-pbkdf": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=10.16.0"
+      },
+      "optionalDependencies": {
+        "cpu-features": "~0.0.9",
+        "nan": "^2.18.0"
+      }
+    },
+    "node_modules/ssh2-sftp-client": {
+      "version": "10.0.3",
+      "resolved": "https://registry.npmmirror.com/ssh2-sftp-client/-/ssh2-sftp-client-10.0.3.tgz",
+      "integrity": "sha512-Wlhasz/OCgrlqC8IlBZhF19Uw/X/dHI8ug4sFQybPE+0sDztvgvDf7Om6o7LbRLe68E7XkFZf3qMnqAvqn1vkQ==",
+      "dev": true,
+      "dependencies": {
+        "concat-stream": "^2.0.0",
+        "promise-retry": "^2.0.1",
+        "ssh2": "^1.15.0"
+      },
+      "engines": {
+        "node": ">=16.20.2"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://square.link/u/4g7sPflL"
+      }
+    },
     "node_modules/stable": {
       "version": "0.1.8",
       "resolved": "https://registry.npmmirror.com/stable/-/stable-0.1.8.tgz",
@@ -6404,6 +6869,18 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/stdin-discarder": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmmirror.com/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
+      "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/strict-uri-encode": {
       "version": "1.1.0",
       "resolved": "https://registry.npmmirror.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
@@ -6673,6 +7150,31 @@
         "traverse": "^0.6.6"
       }
     },
+    "node_modules/svg-baker/node_modules/chalk": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmmirror.com/chalk/-/chalk-1.1.3.tgz",
+      "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==",
+      "dev": true,
+      "dependencies": {
+        "ansi-styles": "^2.2.1",
+        "escape-string-regexp": "^1.0.2",
+        "has-ansi": "^2.0.0",
+        "strip-ansi": "^3.0.0",
+        "supports-color": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/svg-baker/node_modules/chalk/node_modules/supports-color": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-2.0.0.tgz",
+      "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
     "node_modules/svg-baker/node_modules/postcss": {
       "version": "5.2.18",
       "resolved": "https://registry.npmmirror.com/postcss/-/postcss-5.2.18.tgz",
@@ -6798,6 +7300,11 @@
         "node": ">=0.8"
       }
     },
+    "node_modules/three": {
+      "version": "0.166.1",
+      "resolved": "https://registry.npmmirror.com/three/-/three-0.166.1.tgz",
+      "integrity": "sha512-LtuafkKHHzm61AQA1be2MAYIw1IjmhOUxhBa0prrLpEMWbV7ijvxCRHjSgHPGp2493wLBzwKV46tA9nivLEgKg=="
+    },
     "node_modules/to-fast-properties": {
       "version": "2.0.0",
       "resolved": "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -6945,6 +7452,12 @@
       "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
       "dev": true
     },
+    "node_modules/tweetnacl": {
+      "version": "0.14.5",
+      "resolved": "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-0.14.5.tgz",
+      "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
+      "dev": true
+    },
     "node_modules/typed-array-buffer": {
       "version": "1.0.2",
       "resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz",
@@ -7018,6 +7531,12 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+      "dev": true
+    },
     "node_modules/typedarray.prototype.slice": {
       "version": "1.0.3",
       "resolved": "https://registry.npmmirror.com/typedarray.prototype.slice/-/typedarray.prototype.slice-1.0.3.tgz",
@@ -7689,6 +8208,12 @@
         "url": "https://github.com/chalk/strip-ansi?sponsor=1"
       }
     },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "dev": true
+    },
     "node_modules/yallist": {
       "version": "3.1.1",
       "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",

+ 9 - 2
package.json

@@ -6,11 +6,13 @@
   "scripts": {
     "dev": "vite",
     "build:prod": "vite build",
-    "build:test": "vite build  --mode test",
+    "build:test": "vite build --mode test",
     "build-lint": "run-p type-check \"build-only {@}\" --",
     "preview": "vite preview",
     "build-only": "vite build",
-    "type-check": "vue-tsc --build --force"
+    "type-check": "vue-tsc --build --force",
+    "deploy:prod": "node deploy/index.js --prod",
+    "deploy:test": "node deploy/index.js --test"
   },
   "dependencies": {
     "@vueuse/core": "^10.9.0",
@@ -31,6 +33,7 @@
     "pinia-plugin-persistedstate": "^3.2.1",
     "sass": "^1.77.1",
     "sass-loader": "^14.2.1",
+    "three": "^0.166.1",
     "vue": "^3.4.21",
     "vue-router": "^4.3.0"
   },
@@ -42,9 +45,13 @@
     "@vitejs/plugin-vue-jsx": "^3.1.0",
     "@vue/tsconfig": "^0.5.1",
     "autoprefixer": "^10.4.19",
+    "chalk": "^5.3.0",
     "dayjs": "^1.11.11",
     "npm-run-all2": "^6.1.2",
+    "ora": "^8.0.1",
     "postcss": "^8.4.38",
+    "shelljs": "^0.8.5",
+    "ssh2-sftp-client": "^10.0.3",
     "tailwindcss": "^3.4.4",
     "typescript": "~5.4.0",
     "vite": "^5.2.8",

BIN
public/factory.glb


File diff suppressed because it is too large
+ 97282 - 0
public/factory.obj


+ 22 - 0
public/texture.mtl

@@ -0,0 +1,22 @@
+# Blender 4.1.1 MTL File: '水厂6.blend'
+# www.blender.org
+
+newmtl 材质.001
+Ns 250.000000
+Ka 1.000000 1.000000 1.000000
+Kd 0.156420 0.800416 0.155804
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.500000
+d 1.000000
+illum 2
+
+newmtl 材质.002
+Ns 360.000000
+Ka 1.000000 1.000000 1.000000
+Kd 0.800000 0.800000 0.800000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2

+ 1 - 1
src/App.vue

@@ -27,7 +27,7 @@ const themeOverrides: GlobalThemeOverrides = {
     arrowColorChildActive: primaryColor
   },
   Scrollbar: {
-    width: '0px',
+    width: '2px',
   },
   Select: {
     peers: {

+ 5 - 0
src/api/chat.js

@@ -40,4 +40,9 @@ export const chatApi = {
    * 点赞 or 取消点赞
    */
   putIsSatisfiedAnswer: data => http.put(`/front/bigModel/chat/isSatisfiedAnswer`, data),
+
+  /**
+   * 停止流数据
+   */
+  getStopChatStream: params => http.get('/front/bigModel/warning/stopChat/' + params)
 }

BIN
src/assets/images/chat/back-btn-active.png


BIN
src/assets/images/chat/back-btn.png


BIN
src/assets/images/home/home_bg.png


BIN
src/assets/images/home/home_bg2.png


BIN
src/assets/images/warn-text1.png


+ 8 - 0
src/assets/svgs/chat/icon-close-btn.svg

@@ -0,0 +1,8 @@
+
+<svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path d="M3.14905 3.14777L12.8508 12.8496" stroke="#838A95" stroke-linecap="round"></path>
+  <path d="M13.0016 2.99786L2.99725 13.0022" stroke="#838A95" stroke-linecap="round"></path>
+  <g opacity="0.01" style="mix-blend-mode: darken;">
+    <rect width="16" height="16" fill="white"></rect>
+  </g>
+</svg>

+ 69 - 35
src/components/Chat/ChatAnswer.vue

@@ -35,10 +35,18 @@ const props = defineProps({
   loadingText: {
     type: String,
     default: '内容生成中...'
+  },
+  isVisibleStopBtn: {
+    type: Boolean,
+    default: false
+  },
+  isVisibleResetBtn: {
+    type: Boolean,
+    default: false
   }
 })
 
-const emit = defineEmits(['on-click-icon']);
+const emit = defineEmits(['on-click-icon', 'on-click-stop', 'on-click-reset']);
 
 const message = useMessage();
 
@@ -53,9 +61,13 @@ const handlLeToggleLike = async (state) => {
 
   isSatisfied < 2 ? message.success('感谢您的反馈') : message.success('已取消反馈');
 
-  emit('on-click-icon', params)
+  emit('on-click-icon', params);
 }
 
+const handleChatStop = () => emit('on-click-stop');
+
+const handleChatReset = () => emit('on-click-reset');
+
 const handleCopy = () => {
   copy(props.content).then(() => {
     message.success('复制成功');
@@ -72,21 +84,27 @@ const handleCopy = () => {
       <ChatText :content="content"></ChatText>
     </template>
 
-
     <template #button>
-      <ul class="answer-btn-group" v-if="!loading && toggleVisibleIcons">
-        <li class="btn" @click="handleCopy">
-          <SvgIcon name="chat-icon-copy" size="16" />
-        </li>
-        <li class="line"></li>
-        <li :class="['btn', { btn_active: isSatisfied == 1 }]">
-          <SvgIcon name="chat-icon-yes" size="16" @click="handlLeToggleLike(1)" />
-        </li>
-        <li class="line"></li>
-        <li :class="['btn', { btn_active: isSatisfied == 0 }]">
-          <SvgIcon name="chat-icon-no" size="16" @click="handlLeToggleLike(0)" />
-        </li>
-      </ul>
+      <div class="footer-wrap">
+        <div class="chat-stop-btn">
+          <span @click="handleChatStop" v-if="!delayLoading && loading && isVisibleStopBtn">停止生成</span>
+          <span @click="handleChatReset" v-if="!delayLoading && !loading && isVisibleResetBtn">重新生成</span>
+        </div>
+        <ul class="answer-btn-group" v-if="!loading && toggleVisibleIcons">
+          <li class="btn" @click="handleCopy">
+            <SvgIcon name="chat-icon-copy" size="16" />
+          </li>
+          <li class="line"></li>
+          <li :class="['btn', { btn_active: isSatisfied == 1 }]">
+            <SvgIcon name="chat-icon-yes" size="16" @click="handlLeToggleLike(1)" />
+          </li>
+          <li class="line"></li>
+          <li :class="['btn', { btn_active: isSatisfied == 0 }]">
+            <SvgIcon name="chat-icon-no" size="16" @click="handlLeToggleLike(0)" />
+          </li>
+        </ul>
+      </div>
+      
     </template>
 
   </ChatBaseCard>
@@ -118,34 +136,50 @@ const handleCopy = () => {
 
   .answer-card {
     @include flex(x, start, start);
-    // padding: 20px 20px 4px 20px;
     border-radius: 8px;
     background: #fff;
   }
 
-  .answer-btn-group {
-    @include flex(x, center, end);
-    padding-top: 6px;
+  .footer-wrap {
+    @include flex(x, cetner, between);
+    padding: 6px 0 0 5px;
+    color: #2454FF;
+
+    .chat-stop-btn {
+      font-size: 14px;
+      line-height: 34px;
+      span {
+        cursor: pointer;
+        &:hover {
+          color: #1D43CC;
+        }
+      }
+    }
 
-    .btn {
-      @include flex(x, center, center);
-      @include layout(28px, 28px, 4px);
-      color: #89909B;
-      cursor: pointer;
+    .answer-btn-group {
+      @include flex(x, center, end);
 
-      &:hover,
-      &_active {
-        background: #DBEFFF;
-        color: #2454FF;
-      }
+      .btn {
+        @include flex(x, center, center);
+        @include layout(28px, 28px, 4px);
+        color: #89909B;
+        cursor: pointer;
 
-    }
+        &:hover,
+        &_active {
+          background: #DBEFFF;
+          color: #2454FF;
+        }
 
-    .line {
-      @include layout(1px, 12px, 0);
-      margin: 0 5px;
-      background: #D3D0E1;
+      }
+
+      .line {
+        @include layout(1px, 12px, 0);
+        margin: 0 5px;
+        background: #D3D0E1;
+      }
     }
   }
+
 }
 </style>

+ 3 - 5
src/components/Chat/ChatInput.vue

@@ -48,8 +48,8 @@ const commonEmitEvent = (eventName) => {
 
 const handleInpEnter = (event) => {
   if (event.key === 'Enter' && !event.shiftKey) {
-    event.preventDefault()
-    commonEmitEvent('onEnter')
+    event.preventDefault();
+    commonEmitEvent('onEnter');
   }
 }
 
@@ -97,9 +97,7 @@ defineExpose({
     <NSwitch size="small" v-model:value="switchStatus"></NSwitch>
     <span class="text-[12px] text-[#9E9E9E]">使用搜索增强</span>
   </div>
-  <div class="masking-inner">
-    
-  </div>
+  <div class="masking-inner text-center text-[#2454FF]"></div>
 </template>
 
 <style scoped lang="scss">

+ 351 - 0
src/components/Chat/ChatInputCopy.vue

@@ -0,0 +1,351 @@
+<script setup>
+import { ref, unref, onMounted, onUnmounted, computed, watch } from 'vue';
+import { useMessage, NInput, NSwitch, NPopover, NScrollbar } from 'naive-ui';
+import SvgIcon from '@/components/SvgIcon';
+
+import 'load-awesome/css/ball-running-dots.min.css';
+
+const props = defineProps({
+  options: {
+    type: Array,
+    default: []
+  },
+  activeItem: {
+    type: Object,
+    default: () => ({})
+  }
+});
+
+const emit = defineEmits(['onClick', 'onEnter']);
+
+const modelLoading = defineModel('loading');
+const switchStatus = defineModel('switch');
+
+const message = useMessage();
+
+const inpVal = ref('');
+const inpRef = ref(null);
+const isFocusState = ref(false);
+
+const isOpen = ref(false);
+const highlightedIndex = ref(0);
+const selectedOption = ref(null);
+
+const popoverTriggerRef = ref(null);
+const popoverInnerRef = ref(null);
+
+const agentOptions = computed(() => props.options.filter(({ tools }) => tools));
+
+const focusInput = _ => isFocusState.value = true;
+
+const blurInput = _ => isFocusState.value = false;
+
+watch(inpVal, (curVal) => {
+  if (curVal === "@" && curVal.length === 1) {
+    if ( !unref(agentOptions).length ) {
+      return message.warning('当前未配置智能体');
+    }
+    isOpen.value = true;
+  } else {
+    isOpen.value = false;
+  }
+  // isOpen.value = (curVal === "@" && curVal.length === 1);
+})
+
+watch(() => props.activeItem, (curVal) => {
+  selectedOption.value = curVal?.tools ? curVal :  null;
+})
+
+const handleInpFocus = () => {
+  inpRef.value?.focus();
+}
+
+const commonEmitEvent = (eventName) => {
+  const val = unref(inpVal);
+  const len = val.trim().length;
+
+  if ( !len ) {
+    return message.warning('请输入您的问题或需求');
+  }
+
+  if ( len > 2000 ) {
+    return message.warning('问题限制2000个字以内');
+  }
+
+  if ( modelLoading.value ) {
+    return message.warning('当前对话进行中');
+  }
+
+  // emit(eventName, val);
+  emit(eventName, {question: val, selectedOption: selectedOption.value || {}});
+
+  inpVal.value = '';
+}
+
+// 回车事件
+const handleInpEnter = (event) => {
+  if (event.key === 'Enter' && !event.shiftKey && inpVal.value) {
+    event.preventDefault();
+    commonEmitEvent('onEnter');
+  }
+}
+
+// 点击事件
+const handleBtnClick = () => {
+  commonEmitEvent("onClick");
+}
+
+const clearInpVal = () => {
+  inpVal.value = '';
+}
+
+// 键盘事件
+const handleKeyDown = (event) => {
+  const len = unref(agentOptions).length;
+
+  if ( !isOpen.value ) return;
+
+  switch (event.key) {
+    case 'ArrowUp':
+      event.preventDefault();
+      highlightedIndex.value = (unref(highlightedIndex) - 1 + len) % len;
+      break;
+    case 'ArrowDown':
+      event.preventDefault();
+      highlightedIndex.value = (unref(highlightedIndex) + 1) % len;
+      break;
+    case 'Enter':
+      event.preventDefault();
+      selectOption(unref(highlightedIndex));
+      break;
+    default:
+      break;
+  }
+}
+
+// 处理点击空白处关闭
+const closePopoverOutside =(event) => {
+  if (!isOpen.value) return; 
+  const triggerResult = popoverTriggerRef.value.contains(event.target);
+  const innerResult = popoverInnerRef.value.contains(event.target);
+  isOpen.value = triggerResult || innerResult;
+}
+
+// 选中选项
+const selectOption = (index) => {
+  selectedOption.value = agentOptions.value[index];
+  highlightedIndex.value = index;
+  isOpen.value = false;
+  clearInpVal();
+}
+
+onMounted(() => {
+  document.addEventListener('keydown', handleKeyDown);
+  document.addEventListener('click', closePopoverOutside);
+})
+
+onUnmounted(() => {
+  document.removeEventListener('keydown', handleKeyDown);
+  document.removeEventListener('click', closePopoverOutside);
+})
+
+defineExpose({
+  clearInpVal,
+  handleInpFocus,
+  inpVal,
+})
+
+</script>
+<template>
+  <NPopover
+    trigger="hover"
+    width="trigger"
+    display-directive="show"
+    content-style="padding: 0;"
+    :show-arrow="false"
+    :show="isOpen"
+  >
+    <template #trigger>
+      <div class="popover-trigger" ref="popoverTriggerRef">
+        <div class="chat-inp-outer border-[1px]" :class="[{ 'border-[#2454FF]': isFocusState }]">
+          <ul class="chat-tools-inner py-[10px] px-[10px] bg-[#fcfcfc]" v-show="selectedOption">
+            <li class="tools-tips space-x-[10px]">
+              <span>与</span>            
+              <p class="agent-name space-x-[5px]" @click="isOpen = true">
+                <img src="https://static.fuxicarbon.com/userupload/db77ffe0cef843278a23b0d2db9505fa.png" alt="">
+                <span>{{ selectedOption?.title }}</span>
+              </p>
+              <span>对话中</span>
+            </li>
+            <li class="tools-close" @click="selectedOption = null">
+              <SvgIcon name="chat-icon-close-btn"></SvgIcon>
+            </li>
+          </ul>
+          <div class="chat-inp-inner">
+            <div class="inp-wrapper flex-1" @click="handleInpFocus">
+              <NInput 
+                class="flex-1"
+                ref="inpRef" 
+                type="textarea" 
+                size="medium"
+                placeholder="输入@,召唤智能体"
+                v-model:value="inpVal" 
+                :autosize="{ minRows: 1, maxRows: 5 }"
+                @focus="focusInput"
+                @blur="blurInput"
+                @keypress="handleInpEnter"
+              />
+            </div>
+            <div class="submit-btn">
+              <button class="btn bg-[#1A2029] hover:bg-[#3C4148]" @click="handleBtnClick">
+                <SvgIcon name="tool-send-plane" size="22" v-show="!modelLoading"></SvgIcon>
+                <div style="color: #fff" class="la-ball-running-dots la-sm" v-show="modelLoading">
+                  <div v-for="item in 5" :key="item"></div>
+                </div>
+              </button>
+            </div>
+          </div>
+        </div>
+        <div class="switch-inner pt-[8px] space-x-[6px]">
+          <NSwitch size="small" v-model:value="switchStatus"></NSwitch>
+          <span class="text-[12px] text-[#9E9E9E]">使用搜索增强</span>
+        </div>
+        <div class="masking-inner text-center text-[#2454FF]"></div>
+      </div>
+    </template>
+  
+    <div class="popover-inner" ref="popoverInnerRef">
+      <div class="header">
+        <span>选择智能体</span>
+        <p class="tools-close" @click="isOpen = false">
+          <SvgIcon name="chat-icon-close-btn"></SvgIcon>
+        </p>
+      </div>
+      <NScrollbar style="max-height: 240px;">
+        <div class="item" v-for="item, index in agentOptions" :class="['item', { active: highlightedIndex === index }]" @click="selectOption(index)">
+          <p class="icon">
+            <img :src="item.banner" alt="">
+          </p>
+          <p class="ml-[10px] space-x-[5px] text">
+            <span class="text-[15px]">{{item.title}}</span>
+            <span class="text-[#888] text-[14px]">这里可以补充个描述</span>
+          </p>
+        </div>
+      </NScrollbar>
+    </div>
+  </NPopover>
+</template>
+
+<style scoped lang="scss">
+.chat-inp-outer {
+  border-radius: 8px;
+  overflow: hidden;
+  box-shadow: 0px 3px 12px 0px #97D3FF40;
+
+  .chat-tools-inner {
+    @include flex(x, center, between);
+
+    .tools-tips {
+      @include flex(x, center, start);
+      color: #666;
+      font-size: 14px;
+      
+      .agent-name {
+        @include flex(x, center, start);
+        font-weight: bold;
+        color: #333;
+        cursor: pointer;
+
+        img {
+          width: 14px;
+          height: 14px;
+        }
+      }
+    }
+  }
+
+  .chat-inp-inner {
+    position: relative;
+    @include flex(x, center, between);
+    background: #fff;
+
+    .inp-wrapper {
+      padding: 17px 0px 17px 34px;
+    }
+
+    .submit-btn {
+      @include flex(x, center, center);
+      width: 84px;
+
+      .btn {
+        @include flex(x, center, center);
+        width: 50px;
+        height: 32px;
+        border-radius: 32px;
+        transition: all .3s;
+      }
+    }
+  }
+}
+
+.popover-inner {
+  .header {
+    @include flex(x, center, between);
+    padding-bottom: 8px;
+    font-size: 14px;
+    color: #666;
+  }
+
+    .item {
+      @include flex(x, center, start);
+      padding: 8px 10px;
+      cursor: pointer;
+      &:hover {
+        background: #f0fafe;
+      }
+      .icon {
+        @include flex(x, center, center);
+        width: 24px;
+        height: 24px;
+        border-radius: 100%;
+        background: #e9eef8;
+        img {
+          width: 16px;
+          height: 16px;
+        }
+      }
+      .text {
+        text-align: left;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+      }
+    }
+    .active {
+      background: #f0fafe;
+    }
+  }
+
+.tools-close {
+  @include flex(x, center, center);
+  width: 28px;
+  height: 28px;
+  border-radius: 6px;
+  background: #fff;
+  cursor: pointer;
+  &:hover {
+    background: #e9eef8;
+  }
+}
+
+.masking-inner {
+  position: absolute;
+  top: -30px;
+  left: 0;
+  width: 100%;
+  height: 30px;
+  background: linear-gradient(180deg, rgba(232, 241, 250, 0) 0%, #E7F0FA 95%);
+}
+
+
+</style>

+ 4 - 4
src/components/Chat/ChatText.vue

@@ -23,10 +23,10 @@ const highlightBlock = (str, lang) => {
   `
 }
 const mdi = new MarkdownIt({
-  html: true,
-  linkify: true,
-  breaks: true,
-  typographer: true,
+  // html: true,
+  // linkify: true,
+  // breaks: true,
+  // typographer: true,
   highlight (code, language) {
     const validLang = !!(language && hljs.getLanguage(language))
     if (validLang) {

+ 3 - 1
src/components/Chat/index.js

@@ -2,11 +2,13 @@ import ChatAsk from './ChatAsk';
 import ChatAnswer from './ChatAnswer';
 import ChatInput from './ChatInput';
 import ChatBaseCard from './ChatBaseCard';
+import ChatInputCopy from './ChatInputCopy';
 
 
 export { 
   ChatAsk,
   ChatAnswer,
   ChatInput,
-  ChatBaseCard
+  ChatBaseCard,
+  ChatInputCopy
 };

+ 21 - 23
src/components/Dialog/editPassword.vue

@@ -84,30 +84,28 @@ const onSubmit = event => {
 </script>
 
 <template>
-  <div>
-    <n-modal v-model:show="userStore.dialogStatus" :mask-closable="false">
-      <div class="edit-passWord">
-        <div class="title">修改密码</div>
-        <div class="main">
-          <n-form ref="formRef" :model="model" :rules="rules" label-placement="left" label-align="left" :label-width="64" :show-require-mark="false">
-            <n-form-item label="旧密码" path="oldPassword" first>
-              <n-input v-model:value="model.oldPassword" class="input" placeholder="请输入登录密码" type="password" />
-            </n-form-item>
-            <n-form-item label="新密码" path="newPassword" first>
-              <n-input v-model:value="model.newPassword" class="input" placeholder="请输入8~16位数字,字母" type="password" />
-            </n-form-item>
-            <n-form-item label="确认密码" path="reenteredPassword" first>
-              <n-input v-model:value="model.reenteredPassword" class="input" placeholder="请再次输入密码" type="password" />
-            </n-form-item>
-          </n-form>
-        </div>
-        <div class="footer">
-          <n-button class="cencel btn" @click="onCancel">取消</n-button>
-          <n-button type="primary" class="ok btn" @click="onSubmit">确定</n-button>
-        </div>
+  <n-modal v-model:show="userStore.dialogStatus" :mask-closable="false">
+    <div class="edit-passWord">
+      <div class="title">修改密码</div>
+      <div class="main">
+        <n-form ref="formRef" :model="model" :rules="rules" label-placement="left" label-align="left" :label-width="64" :show-require-mark="false">
+          <n-form-item label="旧密码" path="oldPassword" first>
+            <n-input v-model:value="model.oldPassword" class="input" placeholder="请输入登录密码" type="password" />
+          </n-form-item>
+          <n-form-item label="新密码" path="newPassword" first>
+            <n-input v-model:value="model.newPassword" class="input" placeholder="请输入8~16位数字,字母" type="password" />
+          </n-form-item>
+          <n-form-item label="确认密码" path="reenteredPassword" first>
+            <n-input v-model:value="model.reenteredPassword" class="input" placeholder="请再次输入密码" type="password" />
+          </n-form-item>
+        </n-form>
       </div>
-    </n-modal>
-  </div>
+      <div class="footer">
+        <n-button class="cencel btn" @click="onCancel">取消</n-button>
+        <n-button type="primary" class="ok btn" @click="onSubmit">确定</n-button>
+      </div>
+    </div>
+  </n-modal>
 </template>
 <style lang="scss" scoped>
 .edit-passWord {

+ 44 - 30
src/components/Layout/TheChatView.vue

@@ -1,58 +1,47 @@
 <script setup>
-import { ref, unref, computed, onMounted } from 'vue';
-import { NSelect, NDropdown, NPopover } from 'naive-ui';
+import { ref, unref, computed } from 'vue';
 
-import userTop from './userTop.vue';
+import UserTop from './userTop.vue';
 
 defineProps({
   isFooter: {
     type: Boolean,
     default: true
+  },
+  isBackBtn: {
+    type: Boolean,
+    default: false
   }
 })
 
-const userInfo = ref({});
+const emit = defineEmits(['onClickBack'])
 
 const targetScrollDom = ref(null);
-const selectValue = ref('water');
 const voiceSwitchStatus = ref(false);
 
-const userMenuOptions = [
-  {
-    label: '个人中心',
-    key: "1"
-  },
-  {
-    label: '修改密码',
-    key: "2"
-  },
-  {
-    label: '退出登录',
-    key: "3"
-  },
-]
-
-const options = [
-  {
-    label: "信义污水厂",
-    value: 'water',
-  }
-]
-
 const voiceName = computed(() => unref(voiceSwitchStatus) ? 'tool-voice-close' : 'tool-voice-open')
 
 const changeVoiceStatus = () => {
   voiceSwitchStatus.value = !voiceSwitchStatus.value;
 }
 
+const handleClickBack = () => {
+  emit("onClickBack");
+}
+
 defineExpose({ targetScrollDom });
 </script>
 
 <template>
   <div class="flex-1 h-full chat-container">
     <div class="chat-wrapper w-full h-full flex flex-col rounded-[20px]">
-      <div class="chat-header flex items-center justify-end py-[24px] pr-[18px] space-x-[16px]">
-        <userTop></userTop>
+      <div class="chat-header flex items-center justify-between py-[24px] px-[18px] ">
+        <div class="left_inner" @click="handleClickBack">
+          <span v-if="isBackBtn" class="back-btn"></span>
+        </div>
+        <div class="right_inner flex items-center space-x-[16px]">
+          <UserTop></UserTop>
+        </div>
       </div>
       <main class="chat-main flex flex-1 flex-col justify-between">
         <div class="chat-scroll" ref="targetScrollDom">
@@ -73,6 +62,32 @@ defineExpose({ targetScrollDom });
   padding: 20px 20px 20px 0;
   overflow: hidden;
 
+  .chat-header {
+    .left_inner {
+      display: flex;
+      align-items: center;
+      padding: 6px;
+      border-radius: 8px;
+      cursor: pointer;
+
+      &:hover {
+        background: #dceffe;
+      }
+      .back-btn {
+        display: inline-block;
+        width: 30px;
+        height: 30px;
+        background: url("@/assets/images/chat/back-btn.png") no-repeat;
+        background-size: cover;
+
+        // &:hover {
+        //   background: url("@/assets/images/chat/back-btn-active.png") no-repeat;
+        //   background-size: cover;
+        // }
+      }
+    }
+  }
+
   .chat-wrapper {
     border: 1px solid #fff;
     background: linear-gradient(180deg, rgba(238, 253, 255, 0.5) 0%, rgba(231, 243, 252, 0.5) 100%);
@@ -82,7 +97,6 @@ defineExpose({ targetScrollDom });
       color: #1A2029;
 
       .chat-scroll {
-        // flex: 1;
         overflow-x: hidden;
         overflow-y: auto;
 

+ 12 - 4
src/components/Layout/TheSubMenu.vue

@@ -1,6 +1,6 @@
 <script setup lang="jsx">
 import { storeToRefs } from 'pinia';
-import { NScrollbar, NInfiniteScroll } from 'naive-ui';
+import { NInfiniteScroll } from 'naive-ui';
 import { useAppStore } from '@/stores/modules/appStore';
 import SvgIcon from '@/components/SvgIcon';
 
@@ -15,8 +15,6 @@ defineProps({
   }
 })
 
-// const modelLoading = defineModel('loading');
-
 const emits = defineEmits(['scrollToLower']);
 
 const appStore = useAppStore();
@@ -26,6 +24,16 @@ const { subMenuCollapse } = storeToRefs(appStore);
 const changeCollapse = () => appStore.toggleSubMenuCollapse();
 
 const handleLoadMore = () => emits('scrollToLower');
+
+const scrollToTop = () => {
+  document.querySelector('.scroll_container').parentNode.scrollTo({
+    top: 0,
+    left: 0
+  })
+};
+
+defineExpose({ scrollToTop });
+
 </script>
 
 <template>
@@ -41,7 +49,7 @@ const handleLoadMore = () => emits('scrollToLower');
     </div>
 
     <div class="sub-menu-main w-full h-full" >
-      <NInfiniteScroll class="h-full" :distance="10" @load="handleLoadMore">
+      <NInfiniteScroll class="h-full" :distance="10" @load="handleLoadMore" content-class="scroll_container">
         <slot></slot>
         <div class="footer-loading w-full h-[50px]" v-show="loading">
           加载更多

+ 3 - 3
src/components/Layout/TheUserAvatar.vue

@@ -1,5 +1,5 @@
 <script lang="jsx">
-import { computed, defineComponent, unref, ref } from 'vue';
+import { defineComponent } from 'vue';
 import { useRouter } from 'vue-router';
 import { NPopover } from 'naive-ui';
 import { userApi } from '@/api/user';
@@ -66,7 +66,7 @@ const RenderUserAvatar = ({ store }) => {
     ),
     trigger: () => (
       <div class="flex items-center cursor-pointer">
-        <img src={user.avatar} alt="" class="w-[32px] mr-[10px] rounded-[50%]" />
+        <img src={user.avatar} alt="" class="w-[32px] h-[32px] mr-[10px] rounded-[50%]" />
         <span class="text-[#272D35] text-[12px]">{user.nickName}</span>
       </div>
     )
@@ -84,7 +84,7 @@ const RenderUserAvatar = ({ store }) => {
 }
 
 export default defineComponent({
-  setup (props, context) {
+  setup () {
 
     const userStore = useUserStore();
 

+ 3 - 15
src/components/Layout/userTop.vue

@@ -1,6 +1,6 @@
 <script setup>
-import { ref, unref, computed, onMounted } from 'vue';
-import { NSelect, NDropdown, NPopover } from 'naive-ui';
+import { ref, unref, computed } from 'vue';
+import { NSelect } from 'naive-ui';
 import { SvgIcon, BasePopover, editPassword } from '@/components';
 import TheUserAvatar from './TheUserAvatar.vue';
 
@@ -11,14 +11,9 @@ defineProps({
   }
 })
 
-
-
 const selectValue = ref('water');
 const voiceSwitchStatus = ref(false);
 
-
-
-
 const options = [
   {
     label: "信义污水厂",
@@ -31,25 +26,18 @@ const voiceName = computed(() => unref(voiceSwitchStatus) ? 'tool-voice-close' :
 const changeVoiceStatus = () => {
   voiceSwitchStatus.value = !voiceSwitchStatus.value;
 }
-
-
 </script>
 
 <template>
-  <!-- 数字人设置 -->
   <BasePopover placement="bottom" content="数字人设置">
     <div class="avatar rounded-full w-[24px] h-[24px]">
       <img src="@/assets/images/chat/img-avatar.png" alt="" class="cursor-pointer">
     </div>
   </BasePopover>
-  <!-- 声音开关 -->
   <SvgIcon :name="voiceName" size="24" class="cursor-pointer" @click="changeVoiceStatus"></SvgIcon>
-  <!--分割线 -->
   <div class="h-[24px] border-r-[1px] border-color-[#D3D0E1]"></div>
-  <!-- 水厂select -->
-  <NSelect v-model:value="selectValue" placeholder="" :options="options" class="w-[114px]" size="medium"
+  <NSelect v-model:value="selectValue" class="w-[114px]" size="medium" :options="options"
     :consistent-menu-width="false" />
-  <!-- 用户头像 -->
   <TheUserAvatar></TheUserAvatar>
   <editPassword></editPassword>
 </template>

+ 1 - 1
src/components/RecodeSquareCardItem/index.vue

@@ -72,7 +72,7 @@ const dataSources = computed(() => {
   if (item.type == 1) {
     return [
       { label: '报警时间', value: item.time },
-      { label: '报警值', value: Number(item?.warningVal?.toFixed(2)), type: 'wraning', unit: 'mg/L' },
+      { label: '报警值', value: item.warningValStr, type: 'wraning' },
       { label: '报警次数', value: item.counts },
     ]
   }

+ 8 - 0
src/composables/useChat.js

@@ -17,6 +17,13 @@ export const useChat = () => {
     chatDataSource.value[index] = chat;
   }
 
+  const stopChat = () => {
+    const length = unref(chatDataSource).length;
+    const index = length ? length - 1 : length;
+    const lastItem = chatDataSource.value[index];
+    chatDataSource.value[index] = { ...lastItem, loading: false, delayLoading: false };
+  }
+
   const clearChat = () => {
     chatDataSource.value = [];
   }
@@ -31,6 +38,7 @@ export const useChat = () => {
     createChat,
     addChat,
     updateChat,
+    stopChat,
     clearChat,
     updateById
   }

+ 20 - 5
src/composables/useFetchStream.js

@@ -47,11 +47,16 @@ export function useFetchStream(url, options = {}, immediate = true) {
 
       const reader = response.body.getReader();
       const textDecoder = new TextDecoder();
+
       let result = true;
 
+
       while (result) {
+   
         const { done, value } = await reader.read();
 
+        const chunkText = textDecoder.decode(value);
+
         if (done) {
           console.log('Stream ended');
           doneHandler && doneHandler(done);
@@ -59,16 +64,26 @@ export function useFetchStream(url, options = {}, immediate = true) {
           break;
         }
 
-        const chunkText = textDecoder.decode(value);
-        completeData.value.push(chunkText);
-        streamData.value = chunkText;
-        successHandler && successHandler(chunkText);
+        if ( chunkText.includes("}{") ) {
+          const regex = /{[^{}]*}/g;
+          const result1 = chunkText.match(regex);
+          result1.forEach(item => {
+            completeData.value.push(item);
+            streamData.value = item;
+            successHandler && successHandler(item);
+          })
+        } else {
+          completeData.value.push(chunkText);
+          streamData.value = chunkText;
+          successHandler && successHandler(chunkText);
+        }
+
       }
 
       return completeData;
     } catch (err) {
       errorHandler && errorHandler();
-      console.log("fetch: cancel request success");
+      console.log("fetch: cancel request success", err);
       error.value = err;
     } finally {
       loading.value = false;

+ 22 - 9
src/composables/useInfinite.js

@@ -1,14 +1,18 @@
-import { ref, unref, onMounted, computed } from "vue";
+import { ref, unref, onMounted, computed, watch } from "vue";
 import { chatApi } from '@/api/chat';
 
 export const useInfinite = (path, props) => {
+
   const pageParams = ref({ pageNum: 1, pageSize: 20 });
+  const queryParams = ref(props);
 
   const recordList = ref([]);
   const isFetching = ref(false);
   const counter = ref(0);
   const noMore = ref(false);
 
+  // let isSwitchStatus = false;
+
   const isMore = computed(() => pageParams.value.pageNum * pageParams.value.pageSize < counter.value);
 
   const addHistoryRecord = record => {
@@ -16,8 +20,7 @@ export const useInfinite = (path, props) => {
   }
 
   const onScrolltolower = async () => {
-
-    if(unref(isFetching) || unref(noMore)) return;
+    if(unref(isFetching) || unref(noMore) ) return;
 
     isFetching.value = true;
 
@@ -26,7 +29,7 @@ export const useInfinite = (path, props) => {
     recordList.value.push(...rows);
 
     counter.value = total;
-
+  
     if (unref(isMore)) {
       pageParams.value.pageNum ++;
     } else {
@@ -38,23 +41,33 @@ export const useInfinite = (path, props) => {
 
   const onReset = async () => {
     const { rows, total } = await initRecordData({pageNum:1, pageSize: (pageParams.value.pageNum * pageParams.value.pageSize) + 1});
-
     recordList.value = rows;
     counter.value = total;
-    noMore.value = !unref(isMore);
+
+    noMore.value = !unref(isMore)
   }
 
+  // 重置请求
   const onRestore = async (params) => {
     pageParams.value = { pageNum: 1, pageSize: 20 };
-    const { rows, total } = await initRecordData(params);
+    queryParams.value = { ...unref(queryParams), ...params };
+      
+    const { rows, total } = await initRecordData();
+
+    // isSwitchStatus = true;  
+    
     recordList.value = rows;
     counter.value = total;
+
+    // setTimeout(() =>  isSwitchStatus = false, 100);
+
+    noMore.value = !isMore.value;
   }
 
   const initRecordData = async (params = {}) => {
     const reqParams = {
-      ...pageParams.value,
-      ...props,
+      ...unref(pageParams),
+      ...unref(queryParams),
       ...params
     }
     return await chatApi.getRecordFetch(path, reqParams);

+ 1 - 3
src/main.ts

@@ -2,7 +2,7 @@ import { createApp } from 'vue'
 
 import App from './App.vue'
 import router from './router'
-import pinia from './stores/index.js'
+import pinia from './stores'
 import './permission'
 
 import 'virtual:svg-icons-register'
@@ -16,5 +16,3 @@ app.use(pinia)
 app.use(router)
 
 app.mount('#app')
-
-

+ 32 - 0
src/router/index.js

@@ -9,6 +9,38 @@ const constantRouterMap = [
       title: "智慧总控"
     }
   },
+  // {
+  //   path: '/test',
+  //   name: 'TempTest',
+  //   component: () => import('@/views/screen/ScreenView2.vue'),
+  //   meta: {
+  //     title: "测试建模文件"
+  //   }
+  // },
+  {
+    path: '/count1',
+    name: 'count1',
+    component: () => import('@/views/count/index1.vue'),
+    meta: {
+      title: "临时统计1"
+    }
+  },
+  {
+    path: '/count2',
+    name: 'count2',
+    component: () => import('@/views/count/index2.vue'),
+    meta: {
+      title: "临时统计2"
+    }
+  },
+  {
+    path: '/count3',
+    name: 'count3',
+    component: () => import('@/views/count/index3.vue'),
+    meta: {
+      title: "临时统计3"
+    }
+  },
   {
     path: '/login',
     name: 'Login',

+ 4 - 5
src/utils/format.js

@@ -1,7 +1,7 @@
 import { ORDER_OPTION_ENUM } from "./enum";
 
 
-export const formatToData = (dataSource, warnKey) => {
+export const formatToData = ({ dataSource, warnKey, isNoUnit, statusVal }) => {
   const reuslt = {
     title: dataSource?.title,
     list: []
@@ -9,7 +9,8 @@ export const formatToData = (dataSource, warnKey) => {
   delete dataSource.title;
   reuslt.list = Object.entries(dataSource).map(([key, value]) => {
     if ( Number.isFinite(value) ) value = Number(value.toFixed(2));
-    if ( key.includes("值") ) value = value? value + 'mg/L' : '';
+    if ( key.includes("值") && !isNoUnit) value = value? value + 'mg/L' : '';
+    if ( key === '状态' ) value =  statusVal;
     return { label: key, value, isWarning: warnKey === key };
   });
   return reuslt;
@@ -66,8 +67,6 @@ export const colorToRgba = (color, alpha) => {
   return `rgba(${r}, ${g}, ${b}, ${alpha})`
 }
 
-
-// 判断是不是数字
 export const isNumberComprehensive = (value) => {
   return isFinite(value) && !isNaN(parseFloat(value));
-}
+}

+ 24 - 13
src/utils/tools.ts → src/utils/tools.js

@@ -1,12 +1,9 @@
 const baseUrl = import.meta.env.VITE_BASE_URL;
 const basePrefix = import.meta.env.VITE_BASE_PREFIX;
 import { screenApi } from "@/api/screen"
-interface IUrlParams {
-  [propsName: string]: any
-}
 
 /** 统一 - get和post请求书写方式 */
-export function tansParams(params: any): string {
+export function tansParams(params) {
   let result = ''
   for (const propName of Object.keys(params)) {
     const value = params[propName];
@@ -30,14 +27,14 @@ export function tansParams(params: any): string {
 
 /** localhost - methods */
 export class LocalCache {
-  static setCath(key: string, value: any) {
+  static setCath(key, value) {
     window.localStorage.setItem(key, JSON.stringify(value));
   }
-  static getCache(key: string) {
+  static getCache(key) {
     const value = window.localStorage.getItem(key);
     return value ? JSON.parse(value) : {};
   }
-  static deleteCatch(key: string) {
+  static deleteCatch(key) {
     window.localStorage.removeItem(key);
   }
   static clearCache() {
@@ -50,7 +47,7 @@ export const getPreviewPath = (fileObjectKey = '') => {
   return baseUrl + basePrefix + "/auth/t/queryFiles?fileName=" + new Date().getTime() + ".pdf&fileObjectKey=" + fileObjectKey;
 }
 
-export const getQueryParamsAsObject = (url?: string) => {
+export const getQueryParamsAsObject = (url) => {
   url = url || window.location.href;
 
   const queryString = url.split('?')[1];
@@ -61,7 +58,7 @@ export const getQueryParamsAsObject = (url?: string) => {
 
   const queryParams = queryString.split('&');
 
-  const paramsObj:IUrlParams = {};
+  const paramsObj = {};
 
   queryParams.forEach(function(param) {
     const parts = param.split('=');
@@ -77,10 +74,10 @@ export const getQueryParamsAsObject = (url?: string) => {
  * 复制文本
 * @param options
  */
-export function copyText(options: { text: string; origin?: boolean }) {
+export function copyText(options) {
   const props = { origin: true, ...options }
 
-  let input: HTMLInputElement | HTMLTextAreaElement
+  let input;
 
   if (props.origin) {
     input = document.createElement('textarea')
@@ -97,7 +94,7 @@ export function copyText(options: { text: string; origin?: boolean }) {
 }
 
 
-export const upLoadImageFun = async (targe:any) => {
+export const upLoadImageFun = async (targe) => {
   const file = targe.files[0]; // 获取上传的文件
   const allowedTypes = [
     "image/png",
@@ -130,4 +127,18 @@ export const upLoadImageFun = async (targe:any) => {
   } catch (error) {
     return "";
   }
-};
+};
+
+// 简易版防抖
+export const debounce = (func, wait) => {
+  let timeout = null;
+
+  return function() {
+    const context = this;
+    const args = arguments;
+    clearTimeout(timeout);
+    timeout = setTimeout(function() {
+      func.apply(context, args);
+    }, wait);
+  };
+}

+ 20 - 11
src/views/analyse/ForecastView.vue

@@ -11,7 +11,7 @@ import { ChatBaseCard, ChatAnswer } from '@/components/Chat';
 import { formatToData, replaceArray } from "@/utils/format";
 import { waterApi } from '@/api/water';
 import { useInfinite, useRecommend, useScroll } from '@/composables'
-import { columns } from './config';
+import { inColumns, outColumns } from './config';
 
 const { recommendList } = useRecommend({ type: 3 });
 const { scrollRef, scrollToTop } = useScroll();
@@ -24,18 +24,23 @@ const answerResult = ref("");
 const textDataSources = ref(null);
 const chartTitle = ref("");
 
-let chart = {};
+let chart = null;
 const echartRef = ref({});
+const subMenuRef = ref(null);
 
 // 进出水数据
 const jsTableData = ref([]);
 const csTableData = ref([]);
 
+const warningActive = ref(0);
+
 // 切换Tabs
 const onChangeTabs = warningStatus => {
   answerResult.value = '';
   textDataSources.value = '';
-  onRestore({ warningStatus })
+  onRestore({ warningStatus });
+  warningActive.value = warningStatus;
+  subMenuRef.value.scrollToTop();
 }
 
 // 打开详情
@@ -49,7 +54,11 @@ const handleOpenContent = async ({ id, reason: title }) => {
   chartTitle.value = chartsTitle;
 
   basic.title = title
-  textDataSources.value = formatToData(basic, '预测值');
+  textDataSources.value = formatToData({
+    dataSource: basic,
+    warnKey: '预测值',
+    statusVal: !!warningActive.value ? '已完成' : basic['状态']
+  });
 
   jsTableData.value = [jsData];
   csTableData.value = [csData];
@@ -60,6 +69,7 @@ const handleOpenContent = async ({ id, reason: title }) => {
 
 // 创建图表
 const createLineChat = (data) => {
+  
   const [xAxisData, yData] = data.reduce((acc, { time, val }) => {
     acc[0].push(time);
     acc[1].push(val);
@@ -79,13 +89,13 @@ const handleWelcomeRecommend = question => {
 }
 
 onMounted(() => {
-  chart = echarts.init(echartRef.value, 'light');
+  chart = echarts.init(echartRef.value, 'light', { width: 660, height: 200 });
 })
 </script>
 
 <template>
   <section class="flex items-start h-full" id="warning">
-    <TheSubMenu title="预测预警" @scrollToLower="onScrolltolower" :loading="isFetching">
+    <TheSubMenu title="预测预警" @scrollToLower="onScrolltolower" :loading="isFetching" ref="subMenuRef">
       <template #top>
         <div class="border-[#DAE5ED]">
           <n-tabs type="line" justify-content="space-evenly">
@@ -120,7 +130,7 @@ onMounted(() => {
                 <span>当前进水数据:</span>
               </div>
               <div class="main">
-                <BaseTable :columns="columns" :data="jsTableData"></BaseTable>
+                <BaseTable :columns="inColumns" :data="jsTableData"></BaseTable>
               </div>
             </div>
             <div class="warning-table">
@@ -128,7 +138,7 @@ onMounted(() => {
                 <span>当前出水数据:</span>
               </div>
               <div class="main">
-                <BaseTable :columns="columns" :data="csTableData"></BaseTable>
+                <BaseTable :columns="outColumns" :data="csTableData"></BaseTable>
               </div>
             </div>
           </div>
@@ -150,8 +160,7 @@ onMounted(() => {
 
       </ChatBaseCard>
 
-      <ChatAnswer :loading="false" :delay-loading="false" :toggleVisibleIcons="false" :content="answerResult"
-        v-if="answerResult" />
+      <ChatAnswer :loading="false" :delay-loading="false" :toggleVisibleIcons="false" :content="answerResult" v-if="answerResult" class="reset-chart"/>
 
     </TheChatView>
   </section>
@@ -167,4 +176,4 @@ onMounted(() => {
   padding: 50px 0 30px 0;
   border-radius: 8px;
 }
-</style>
+</style>

+ 16 - 5
src/views/analyse/PymolView.vue

@@ -8,7 +8,7 @@ import { ChatBaseCard, ChatAnswer } from '@/components/Chat';
 import { formatToData } from "@/utils/format";
 import { waterApi } from '@/api/water';
 import { useInfinite, useRecommend, useScroll } from '@/composables'
-import { columns } from './config';
+import { inColumns, outColumns } from './config';
 
 const { recommendList } = useRecommend({ type: 2 });
 const { scrollRef, scrollToTop } = useScroll();
@@ -19,16 +19,21 @@ const chatStore = useChatStore();
 
 const answerResult = ref("");
 const textDataSources = ref(null);
+const subMenuRef = ref(null);
 
 // 进出水数据
 const jsTableData = ref([]);
 const csTableData = ref([]);
 
+const warningActive = ref(0);
+
 // 切换Tabs
 const onChangeTabs = warningStatus => {
   answerResult.value = '';
   textDataSources.value = '';
   onRestore({ warningStatus })
+  warningActive.value = warningStatus;
+  subMenuRef.value.scrollToTop();
 }
 
 // 打开详情
@@ -40,7 +45,13 @@ const handleOpenContent = async ({ id, reason:title }) => {
   answerResult.value = data.answer;
 
   basic.title = title
-  textDataSources.value = formatToData(basic, '报警值');
+
+  textDataSources.value = formatToData({
+    dataSource: basic,
+    warnKey: '报警值',
+    isNoUnit: true,
+    statusVal: !!warningActive.value ? '系统关闭' : basic['状态']
+  });
 
   jsTableData.value = [jsData];
   csTableData.value = [csData];
@@ -57,7 +68,7 @@ const handleWelcomeRecommend = question => {
 
 <template>
   <section class="flex items-start h-full" id="warning">
-    <TheSubMenu title="生化报警" @scrollToLower="onScrolltolower" :loading="isFetching">
+    <TheSubMenu title="生化报警" @scrollToLower="onScrolltolower" :loading="isFetching" ref="subMenuRef">
       <template #top>
         <div class="border-[#DAE5ED]">
           <n-tabs type="line" justify-content="space-evenly">
@@ -98,7 +109,7 @@ const handleWelcomeRecommend = question => {
                 <span>当前进水数据:</span>
               </div>
               <div class="main">
-                <BaseTable :columns="columns" :data="jsTableData"></BaseTable>
+                <BaseTable :columns="inColumns" :data="jsTableData"></BaseTable>
               </div>
             </div>
             <div class="warning-table">
@@ -106,7 +117,7 @@ const handleWelcomeRecommend = question => {
                 <span>当前出水数据:</span>
               </div>
               <div class="main">
-                <BaseTable :columns="columns" :data="csTableData"></BaseTable>
+                <BaseTable :columns="outColumns" :data="csTableData"></BaseTable>
               </div>
             </div>
           </div>

+ 34 - 12
src/views/analyse/WaterView.vue

@@ -7,7 +7,7 @@ import { BaseTable, ChatWelcome, RecodeSquareCardItem, TheSubMenu, TheChatView }
 import { useInfinite, useRecommend, useFetchStream, useScroll } from '@/composables';
 import { ChatBaseCard, ChatAnswer } from '@/components/Chat';
 import { CustomModal } from "./components";
-import { columns } from './config';
+import { inColumns, outColumns } from './config';
 
 import { formatToData } from "@/utils/format";
 
@@ -16,6 +16,7 @@ import { waterApi } from '@/api/water';
 const { recommendList } = useRecommend({type: 1});
 const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
 const { refetch, cancelFetch } = useFetchStream("/grpc/decisionStream", { methdos: 'POST' }, false);
+
 const { recordList, isFetching, onScrolltolower, onRestore } = useInfinite('/front/bigModel/warning/pageList', { type: 0, warningStatus: 0 });
 
 const router = useRouter();
@@ -34,6 +35,7 @@ const flowParams = {
 const answerLoading = ref(false);
 const textDataSources = ref(null);
 const warningActive = ref(0);
+const subMenuRef = ref(null);
 
 // 进出水数据
 const jsTableData = ref([]);
@@ -100,7 +102,6 @@ const handleOpenContent = async ({ id, category, reason:title }) => {
           alertList.push(answerObjItem);
           break
         case "DECISION_SIMULATE":
-          console.log( "DECISION_SIMULATE", answerObjItem.message );
           if (warningActive.value === 1) return; 
           const { off, on, pred } = JSON.parse(answerObjItem.message);
           simulateObj = {
@@ -148,7 +149,6 @@ const handleOpenContent = async ({ id, category, reason:title }) => {
       loading: false,
       delayLoading: false
     })
-    console.log("error", error);
   }
 
   cancelFetch();
@@ -211,7 +211,11 @@ const handleOpenContent = async ({ id, category, reason:title }) => {
   // }
 
   basic.title = title;
-  textDataSources.value = formatToData(basic, '报警值');
+  textDataSources.value = formatToData({
+    dataSource: basic,
+    warnKey: '报警值',
+    statusVal: !!warningActive.value ? '系统关闭' : basic['状态']
+  });
 
   jsTableData.value = [jsData];
   csTableData.value = [csData];
@@ -222,6 +226,7 @@ const onChangeTabs = warningStatus => {
   resetConfiguration();
   warningActive.value = warningStatus;
   onRestore({ warningStatus });
+  subMenuRef.value.scrollToTop();
 }
 
 // 生成流数据
@@ -248,6 +253,7 @@ const onRegenerate = async () => {
     body: JSON.stringify({ ...flowParams, feedback: JSON.stringify(feedback) }),
     errorHandler: () => {},
     successHandler: data => {
+      
       const item = JSON.parse(data);
 
       answerLoading.value = false;
@@ -375,7 +381,7 @@ const handleWelcomeRecommend = question => {
 
 <template>
   <section class="flex items-start h-full" id="warning">
-    <TheSubMenu title="水质报警" @scrollToLower="onScrolltolower" :loading="isFetching">
+    <TheSubMenu title="水质报警" @scrollToLower="onScrolltolower" :loading="isFetching" ref="subMenuRef">
       <template #top>
         <div class="border-[#DAE5ED]">
           <n-tabs type="line" justify-content="space-evenly">
@@ -420,7 +426,7 @@ const handleWelcomeRecommend = question => {
                 <span>当前进水数据:</span>
               </div>
               <div class="main">
-                <BaseTable :columns="columns" :data="jsTableData"></BaseTable>
+                <BaseTable :columns="inColumns" :data="jsTableData"></BaseTable>
               </div>
             </div>
             <div class="warning-table">
@@ -428,7 +434,7 @@ const handleWelcomeRecommend = question => {
                 <span>当前出水数据:</span>
               </div>
               <div class="main">
-                <BaseTable :columns="columns" :data="csTableData"></BaseTable>
+                <BaseTable :columns="outColumns" :data="csTableData"></BaseTable>
               </div>
             </div>
           </div>
@@ -479,12 +485,13 @@ const handleWelcomeRecommend = question => {
             水质预测推演
           </button>
         </template>
-        
+
         <template v-if="item.biz === 'DECISION_TABLE'">
           <ChatAnswer
             :loading="item.loading"
             :delay-loading="item.delayLoading"
             :toggleVisibleIcons="false"
+            class="reset-chart"
           >
             <div class="markdown-body text-[15px] break-all">
               <strong class="block mb-[16px]">推荐指标调整:</strong>
@@ -508,8 +515,8 @@ const handleWelcomeRecommend = question => {
           </ChatAnswer>
           <button class="
             px-[30px] py-[10px] mb-[20px]
-            rounded-[8px] 
-            bg-white text-[13px] 
+            rounded-[8px]
+            bg-white text-[13px]
             text-[#5E5E5E] hover:text-[#2454FF]"
             :disabled="item.isDisable"
             @click="handleModelVisible"
@@ -524,7 +531,7 @@ const handleWelcomeRecommend = question => {
         :delay-loading="answerLoading"
         :toggleVisibleIcons="false"
         v-show="answerLoading"
-        loadingText="内容生成中,大概需要50秒..."
+        loadingText="内容生成中,大概需要1分钟..."
       ></ChatAnswer>
 
     </TheChatView>
@@ -534,4 +541,19 @@ const handleWelcomeRecommend = question => {
     :current-data="modalData"
     @on-submit="handleSendSimulate"
     ></CustomModal>
-</template>
+</template>
+
+<style lang="scss">
+  .reset-chart {
+  .markdown-body {
+    .custom-table-wrapper {
+      width: 100%;
+      overflow: hidden;
+      padding: 10px;
+      table td,  table th {
+        white-space: normal !important; 
+      }
+    }
+  }
+}
+</style>

+ 30 - 18
src/views/analyse/WorkOrder.vue

@@ -1,5 +1,5 @@
 <script setup>
-import { ref, unref, computed, onMounted, onUnmounted, h } from 'vue';
+import { ref, unref, computed, onUnmounted, h } from 'vue';
 import { useMessage, NDatePicker, NTabs, NTab, NRadioGroup, NRadio, NCheckboxGroup, NCheckbox, NDataTable } from 'naive-ui';
 import { BaseButton, RecodeCardItem, TheSubMenu, TheChatView, ChatWelcome, SvgIcon } from '@/components';
 import { ChatAsk, ChatAnswer } from '@/components/Chat';
@@ -18,6 +18,7 @@ const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
 const { chatDataSource, addChat, updateChat, clearChat, updateById } = useChat();
 
 let controller = new AbortController();
+let timer = null;
 
 const ANSWER_ID_KEY = '@@id@@';
 
@@ -54,6 +55,20 @@ const resetFormData = () => {
   }
 }
 
+const resetState = () => {
+  tabActive.value = 'daily';
+
+  currenSessionId.value = null;
+
+  recordActive.value = null;
+
+  reportDate.value = null;
+
+  resetFormData();
+
+  clearChat();
+}
+
 // 新建对话
 const handleCreateDialog = async () => {
   message.destroyAll();
@@ -65,16 +80,8 @@ const handleCreateDialog = async () => {
   if (!unref(chatDataSource).length) {
     return message.info('已切换最新会话');
   }
-  
-  tabActive.value = 'daily';
 
-  currenSessionId.value = null;
-
-  recordActive.value = null;
-
-  resetFormData();
-
-  clearChat();
+  resetState();
 }
 
 // 查询对话详情
@@ -110,6 +117,7 @@ const handleChatDetail = async ({ sessionId }) => {
   scrollToBottom();
 }
 
+// 请求
 const onRegenerate = async (question, options) => {
   controller = new AbortController();
 
@@ -202,7 +210,7 @@ const handleSubmit = async (question, params) => {
     innerLoading: true,
   });
 
-  setTimeout(() => {
+  timer = setTimeout(() => {
 
     updateChat({
       ...chatDataSourceItem.value,
@@ -226,7 +234,7 @@ const handleCreateOrder = async () => {
   const endDateTime = dayjs(timeEnd);
   let params = {};
 
-  let question = `请生成${reportDate.value}智慧工单分析报告`;
+  let question = `${reportDate.value}智慧工单分析报告`;
 
   if (tabActive.value === 'customDaily') {
     const thirtyDaysBeforeEndTime = endDateTime.subtract(31, 'day');
@@ -244,7 +252,7 @@ const handleCreateOrder = async () => {
     }
     checkGroup.forEach(key => params[key] = true);
 
-    question = `请成生${startDateTime.format("MM月DD日")}-${endDateTime.format("MM月DD日")}的在线仪表的日报工单`
+    question = `${startDateTime.format("MM月DD日")}-${endDateTime.format("MM月DD日")}的在线仪表的日报工单`
 
   } else {
 
@@ -320,6 +328,13 @@ const formatData = (data) => {
   }).filter(Boolean);
 }
 
+// 返回
+const handleback = async () => {
+  clearInterval(timer);
+  await chatApi.getStopChatStream(currenSessionId.value);
+  resetState();
+}
+
 // 删除历史对话
 const handeChatDelete = async (id) => {
   await chatApi.deleteHistory(id);
@@ -350,9 +365,6 @@ const dateEndDisabled = (timestamp) => {
   }
 }
 
-onMounted(() => {
-})
-
 onUnmounted(() => {
   controller.abort();
   Object.keys(chartInstance).forEach(key => chartInstance[key].clear());
@@ -378,7 +390,7 @@ onUnmounted(() => {
       </div>
     </TheSubMenu>
 
-    <TheChatView ref="scrollRef" :is-footer="false">
+    <TheChatView ref="scrollRef" :is-footer="false" :is-back-btn="!!chatDataSource.length" @on-click-back="handleback">
       <ChatWelcome title="您好,我是LibraAI智慧工单助手" :sub-title="[
         '基于大语言模型的智能工单分析助手,可以为您实现数据分析及数据解读',
         '选择日期并为您生成日报分析'
@@ -394,7 +406,7 @@ onUnmounted(() => {
             :delay-loading="item.delayLoading"
             :isSatisfied="item.isSatisfied"
             :toggleVisibleIcons="false"
-            loadingText="数据分析中..."
+            loadingText="数据分析中,由于数据查询需要一些时间,请您耐心等候..."
             @on-click-icon="params => updateById(params)"
           >
             <main v-show="!item.innerLoading">

+ 18 - 24
src/views/analyse/config/echartOptions.js

@@ -11,10 +11,8 @@ export const getAreaOptions = ({ xAxisData, seriesList }) => {
       symbolSize: 5,
       smooth: true,
       lineStyle: {
-        normal: {
-          width: 2,
-          color: i ? "rgba(25,163,223,1)" : "rgba(36,175,83,1)", // 线条颜色
-        },
+        width: 2,
+        color: i ? "rgba(25,163,223,1)" : "rgba(36,175,83,1)",
         borderColor: 'rgba(0,0,0,.4)',
       },
       itemStyle: {
@@ -25,19 +23,17 @@ export const getAreaOptions = ({ xAxisData, seriesList }) => {
         shadowBlur: 1
       },
       areaStyle: {
-        normal: {
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
-            offset: 0,
-            color: i ? "rgba(25,163,223,.3)" : "rgba(48, 209, 136,.3)"
-          },
-          {
-            offset: 1,
-            color: i ? "rgba(25,163,223, 0)" : "rgba(48, 209, 136, 0)"
-          }
-          ], false),
-          shadowColor: 'rgba(25,163,223, 0.5)',
-          shadowBlur: 20
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
+          offset: 0,
+          color: i ? "rgba(25,163,223,.3)" : "rgba(48, 209, 136,.3)"
+        },
+        {
+          offset: 1,
+          color: i ? "rgba(25,163,223, 0)" : "rgba(48, 209, 136, 0)"
         }
+        ], false),
+        shadowColor: 'rgba(25,163,223, 0.5)',
+        shadowBlur: 20
       },
       z: i ? 3 : 2,
       data
@@ -79,10 +75,8 @@ export const getAreaOptions = ({ xAxisData, seriesList }) => {
         },
       },
       axisLabel: {
-        textStyle: {
-          color: '#1F2328',
-          fontSize: 12,
-        },
+        color: '#1F2328',
+        fontSize: 12,
         formatter: function (data) {
           return data + ":00"
         }
@@ -120,10 +114,10 @@ export const getAreaOptions = ({ xAxisData, seriesList }) => {
       },
       axisLabel: {
         show: true,
-        textStyle: {
-          color: '#1F2328',
-          padding: 0
-        },
+        color: '#1F2328',
+        padding: 0,
+        // textStyle: {
+        // },
         formatter: function (value) {
           if (value === 0) {
             return value

+ 75 - 9
src/views/analyse/config/index.jsx

@@ -1,15 +1,24 @@
 import { h } from 'vue';
+import { NTooltip, NGradientText } from 'naive-ui';
 import { truncateDecimals } from "@/utils/format";
 
 export const renderRowDom = ({ row, key }) => {
   const { exceed, value } = row[key] || {};
-  const cls = exceed ? 'text-[#F44C49] font-bold' : 'text-[1A2029]'
-  return (<span class={ cls }>{truncateDecimals(value)} {exceed && <i>↑</i>}</span>)
+  const cls = exceed ? 'text-[#F44C49] font-bold' : 'text-[1A2029]';
+  const num = value ? (value.toFixed(2)) : "";
+  return (<span class={ cls }>{num} {exceed && <i>↑</i>}</span>);
 } 
 
-export const columns = [
+export const renderTooltip = (trigger, content) => {
+  return h(NTooltip, null, {
+    trigger: () => trigger,
+    default: () => content
+  })
+}
+
+export const inColumns = [
   {
-    title: '流量(m³/h)',
+    title: () => renderTooltip(h( 'span', '流量(m³/h)' ), '进水流量 | 在线仪表'),
     key: 'name',
     titleAlign: 'center',
     align: 'center',
@@ -18,7 +27,7 @@ export const columns = [
     render: (row) => renderRowDom({ row, key: '流量' })
   },
   {
-    title: 'COD(mg/L)',
+    title: () => renderTooltip(h( 'span', 'COD(mg/L)' ), '进水cod | 在线仪表'),
     key: 'small',
     titleAlign: 'center',
     align: 'center',
@@ -27,7 +36,7 @@ export const columns = [
     render: (row) => renderRowDom({ row, key: 'COD' })
   },
   {
-    title: 'TN(mg/L)',
+    title: () => renderTooltip(h( 'span', 'TN(mg/L)' ), '进水总氮 | 在线仪表'),
     key: 'address',
     titleAlign: 'center',
     align: 'center',
@@ -36,7 +45,7 @@ export const columns = [
     render: (row) => renderRowDom({ row, key: 'TN' })
   },
   {
-    title: 'NH3-N(mg/L)',
+    title: () => renderTooltip(h( 'span', 'NH₃-N(mg/L)' ), '进水氨氮 | 在线仪表'),
     key: 'tags',
     titleAlign: 'center',
     align: 'center',
@@ -45,7 +54,7 @@ export const columns = [
     render: (row) => renderRowDom({ row, key: 'NH3-N' })
   },
   {
-    title: 'TP(mg/L)',
+    title: () => renderTooltip(h( 'span', 'TP(mg/L)' ), '进水总磷 | 在线仪表'),
     key: 'COD',
     titleAlign: 'center',
     align: 'center',
@@ -54,7 +63,7 @@ export const columns = [
     render: (row) => renderRowDom({ row, key: 'TP' })
   },
   {
-    title: 'SS(mg/L)',
+    title: () => renderTooltip(h( 'span', 'SS(mg/L)' ), '进水SS | 在线仪表'),
     key: '流量',
     titleAlign: 'center',
     align: 'center',
@@ -62,4 +71,61 @@ export const columns = [
     width: '78px',
     render: (row) => renderRowDom({ row, key: 'SS' })
   }
+]
+
+export const outColumns = [
+  {
+    title: () => renderTooltip(h( 'span', 'COD(mg/L)' ), '出水cod | 在线仪表'),
+    key: 'COD',
+    titleAlign: 'center',
+    align: 'center',
+    className: 'small',
+    width: '80px',
+    render: (row) => renderRowDom({ row, key: 'COD' })
+  },
+  {
+    title: () => renderTooltip(h( 'span', '#1NO₃⁻(mg/L)' ), '1号好氧池硝酸盐 | 连续检测'),
+    key: 'HYC1',
+    titleAlign: 'center',
+    align: 'center',
+    className: 'small',
+    width: '80px',
+    render: (row) => renderRowDom({ row, key: 'HYC1' })
+  },
+  {
+    title: () => renderTooltip(h( 'span', '#2NO₃⁻(mg/L)' ), '2号好氧池硝酸盐 | 连续检测'),
+    key: 'HYC2',
+    titleAlign: 'center',
+    align: 'center',
+    className: 'small',
+    width: '80px',
+    render: (row) => renderRowDom({ row, key: 'HYC2' })
+  },
+  {
+    title: () => renderTooltip(h( 'span', 'NH₃-N(mg/L)' ), '出水氨氮 | 在线仪表'),
+    key: 'NH3-N',
+    titleAlign: 'center',
+    align: 'center',
+    className: 'small',
+    width: '80px',
+    render: (row) => renderRowDom({ row, key: 'NH3-N' })
+  },
+  {
+    title: () => renderTooltip(h( 'span', 'PO₄³⁻(mg/L)' ), '二沉池正磷酸盐 | 连续检测'),
+    key: 'RCC',
+    titleAlign: 'center',
+    align: 'center',
+    className: 'small',
+    width: '80px',
+    render: (row) => renderRowDom({ row, key: 'RCC' })
+  },
+  {
+    title: () => renderTooltip(h( 'span', 'SS(mg/L)' ), '出水SS | 在线仪表'),
+    key: 'SS',
+    titleAlign: 'center',
+    align: 'center',
+    className: 'small',
+    width: '78px',
+    render: (row) => renderRowDom({ row, key: 'SS' })
+  }
 ]

+ 30 - 6
src/views/answer/AnswerView.vue

@@ -79,13 +79,16 @@ const onRegenerate = async ({ question, realQuestion }) => {
       showVal: question,
       question: realQuestion || question,
       module: 0,
-      isStrong: Number(unref(switchActive))
+      isStrong: Number(unref(switchActive)),
+      // TODO: 后续大概率需要删除
+      topP: 0.9,
+      temperature: 0.7
     },
     signal: controller.signal,
     onDownloadProgress: ({ event }) => {
       const xhr = event.target;
       const { responseText } = xhr;
-      const [answer] = responseText.split(ANSWER_ID_KEY);
+      const [ answer ] = responseText.split(ANSWER_ID_KEY);
 
       updateChat({
         sessionId,
@@ -126,7 +129,6 @@ const onRegenerate = async ({ question, realQuestion }) => {
 // 提交问题
 const handleSubmit = async (question, realQuestion = '') => {
   // 用于模拟 - 内容生成前置等待状态
-
   if (unref(isExistInHistory)) {
     const { data: sessionId } = await chatApi.getChatSessionTag();
     currenSessionId.value = sessionId;
@@ -161,6 +163,17 @@ const handeChatDelete = async (id) => {
   message.success('删除成功');
 }
 
+// 停止问题生成
+const onStopChatStream  = async ({ sessionId }) => {
+  await chatApi.getStopChatStream(sessionId);
+  return message.warning('已停止对话生成');
+}
+
+// 重新生成问题
+const onChatResetStream = ({ question }) => {
+  handleSubmit(question);
+}
+
 onMounted(() => {
   const question = chatStore.chatQuestion;
   if (Object.keys(question).length) {
@@ -198,10 +211,21 @@ onUnmounted(() => {
       ]" :card-content="recommendList" v-if="!chatDataSource.length" @on-click="handleWelcomeRecommend" />
 
       <div class="conversation-item" v-if="chatDataSource.length">
-        <template v-for="item in chatDataSource" :key="item.id">
+        <template v-for="item, index in chatDataSource" :key="item.id">
           <ChatAsk :content="item.question" :sessionId="item.sessionId"></ChatAsk>
-          <ChatAnswer :id="item.id" :content="item.answer" :loading="item.loading" :delay-loading="item.delayLoading"
-            :isSatisfied="item.isSatisfied" @on-click-icon="params => updateById(params)"></ChatAnswer>
+          <ChatAnswer
+            :id="item.id"
+            :content="item.answer"
+            :loading="item.loading"
+            :delay-loading="item.delayLoading"
+            :isSatisfied="item.isSatisfied"
+            :isVisibleResetBtn="chatDataSource.length - 1 === index"
+            isVisibleStopBtn
+            @on-click-stop="onStopChatStream(item)"
+            @on-click-icon="params => updateById(params)"
+            @on-click-reset="onChatResetStream(item)"
+           >
+          </ChatAnswer>
         </template>
       </div>
 

File diff suppressed because it is too large
+ 1158 - 0
src/views/count/all_book1.json


File diff suppressed because it is too large
+ 496 - 0
src/views/count/all_book2.json


File diff suppressed because it is too large
+ 340 - 0
src/views/count/all_book3.json


+ 144 - 0
src/views/count/index1.vue

@@ -0,0 +1,144 @@
+<script setup>
+import { ref, onMounted, watch } from 'vue';
+import { NButton, NSpace, NDrawer, NDrawerContent, NCard } from 'naive-ui';
+import { ChatAsk, ChatAnswer } from '@/components/Chat';
+import allBookData from "./all_book1.json";
+
+const dataSource = ref([]);
+const errIds = ref([]);
+const visible = ref(false);
+const pageNum = ref(0);
+const pageSize = 10;
+
+const scrollRef = ref(null);
+
+const totalPages = Math.ceil(allBookData.length / pageSize);
+
+watch(pageNum, (num) => {
+  dataSource.value = allBookData.slice((num - 1) * pageSize, num * pageSize).map(item => ({
+    ...item,
+    isDisable: errIds.value.includes(item.id),
+    history: [
+      ...item.history,
+      [item.instruction, item.output]
+    ]
+  }))
+})
+
+const changeVisible = () => {
+  visible.value = !visible.value;
+}
+
+const handlePrePage = () => {
+  if ( pageNum.value === 1 ) {
+    return alert("已经是第一页了");
+  }
+  pageNum.value -= 1;
+  scroolToTop();
+  localStorage.setItem('pageNum', pageNum.value);
+}
+
+const handleNextPage = () => {
+  if ( pageNum.value === totalPages ) return alert("已经是最后一页了");
+  pageNum.value += 1;
+  scroolToTop();
+  localStorage.setItem('pageNum', pageNum.value);
+}
+
+const handleError = item => {
+  item.isDisable = !item.isDisable;
+  item.isDisable ? (errIds.value.push(item.id)) : (errIds.value = errIds.value.filter(id => item.id !== id));
+  localStorage.setItem('errIds', JSON.stringify( errIds.value ));
+  // console.log(errIds.value);
+}
+
+// 返回顶部
+const scroolToTop = () => {
+  scrollRef.value?.scrollTo({ top: 0, behavior: 'smooth' });
+}
+
+onMounted(() => {
+  const localIds = localStorage.getItem('errIds');
+  const localNum = localStorage.getItem('pageNum');
+  const ids = localIds ? JSON.parse(localIds) : [];
+  const num = localNum ? Number(localNum) : 1;
+
+  errIds.value = ids;
+  pageNum.value = num;
+})
+</script>
+
+<template>
+  <section class="page-container">
+    <header class="header flex items-center">
+      <NSpace>
+        <n-button @click="handlePrePage">上一页</n-button>
+        <n-button @click="handleNextPage">下一页</n-button>
+        <n-button @click="changeVisible">显示错误id</n-button>
+      </NSpace>
+      <p class="ml-[30px] space-x-[10px]">
+        <span>{{ pageNum }}</span>
+        <span> / </span>
+        <span>{{ totalPages }}</span>
+      </p>
+    </header>
+    <div class="main" ref="scrollRef">
+      <NCard :title="item.id" v-for="item in dataSource" :key="item.id" class="mb-[20px]">
+        <div v-for="arr, i in item.history" :key="i + item.id" class="pt-[40px]">
+          <ChatAsk :content="arr[0]"></ChatAsk>
+          <ChatAnswer :content="arr[1]" :toggleVisibleIcons="false"></ChatAnswer>
+          <hr>
+        </div>
+        <div class="answer-btns pt-[20px]">
+          <NSpace>
+            <!-- :disabled="item.isDisable" -->
+            <NButton :type="item.isDisable? '' : 'error'" @click="handleError(item)">
+              {{ item.isDisable ? '已审核' : '错' }}
+            </NButton>
+          </NSpace>
+        </div>
+      </NCard>
+    </div>
+  </section>
+  <NDrawer placement="right" v-model:show="visible" width="800" title="错误ID">
+    <n-drawer-content :title="'错误ID列表 -- ' + errIds.length + ' 个'">
+      <!-- <div class="py-[30px]">{{ errIds.join(',') }}</div> -->
+    <!-- <hr> -->
+    <ul class="grid grid-cols-5">
+      <li v-for="item in errIds" :key="item">{{ item }}</li>
+    </ul>
+    </n-drawer-content>
+  </NDrawer>
+</template>
+
+<style lang="scss" scoped>
+.page-container {
+  width: 100vw;
+  height: 100vh;
+  background: #edf7fc;
+
+  .header {
+    display: flex;
+    justify-content: center;
+    padding: 20px 0;
+    background: #fff;
+  }
+
+  .main {
+    width: 1000px;
+    height: calc(100% - 74px);
+    margin: 0 auto;
+    overflow-y: scroll;
+
+    .answer-block {
+      border-bottom: 1px solid #000000;
+      padding-bottom: 20px;
+    }
+
+    .answer-btns {
+      display: flex;
+      justify-content: flex-end;
+    }
+  }
+}
+</style>

+ 144 - 0
src/views/count/index2.vue

@@ -0,0 +1,144 @@
+<script setup>
+import { ref, onMounted, watch } from 'vue';
+import { NButton, NSpace, NDrawer, NDrawerContent, NCard } from 'naive-ui';
+import { ChatAsk, ChatAnswer } from '@/components/Chat';
+import allBookData from "./all_book2.json";
+
+const dataSource = ref([]);
+const errIds = ref([]);
+const visible = ref(false);
+const pageNum = ref(0);
+const pageSize = 10;
+
+const scrollRef = ref(null);
+
+const totalPages = Math.ceil(allBookData.length / pageSize);
+
+watch(pageNum, (num) => {
+  dataSource.value = allBookData.slice((num - 1) * pageSize, num * pageSize).map(item => ({
+    ...item,
+    isDisable: errIds.value.includes(item.id),
+    history: [
+      ...item.history,
+      [item.instruction, item.output]
+    ]
+  }))
+})
+
+const changeVisible = () => {
+  visible.value = !visible.value;
+}
+
+const handlePrePage = () => {
+  if ( pageNum.value === 1 ) {
+    return alert("已经是第一页了");
+  }
+  pageNum.value -= 1;
+  scroolToTop();
+  localStorage.setItem('pageNum', pageNum.value);
+}
+
+const handleNextPage = () => {
+  if ( pageNum.value === totalPages ) return alert("已经是最后一页了");
+  pageNum.value += 1;
+  scroolToTop();
+  localStorage.setItem('pageNum', pageNum.value);
+}
+
+const handleError = item => {
+  item.isDisable = !item.isDisable;
+  item.isDisable ? (errIds.value.push(item.id)) : (errIds.value = errIds.value.filter(id => item.id !== id));
+  localStorage.setItem('errIds', JSON.stringify( errIds.value ));
+  // console.log(errIds.value);
+}
+
+// 返回顶部
+const scroolToTop = () => {
+  scrollRef.value?.scrollTo({ top: 0, behavior: 'smooth' });
+}
+
+onMounted(() => {
+  const localIds = localStorage.getItem('errIds');
+  const localNum = localStorage.getItem('pageNum');
+  const ids = localIds ? JSON.parse(localIds) : [];
+  const num = localNum ? Number(localNum) : 1;
+
+  errIds.value = ids;
+  pageNum.value = num;
+})
+</script>
+
+<template>
+  <section class="page-container">
+    <header class="header flex items-center">
+      <NSpace>
+        <n-button @click="handlePrePage">上一页</n-button>
+        <n-button @click="handleNextPage">下一页</n-button>
+        <n-button @click="changeVisible">显示错误id</n-button>
+      </NSpace>
+      <p class="ml-[30px] space-x-[10px]">
+        <span>{{ pageNum }}</span>
+        <span> / </span>
+        <span>{{ totalPages }}</span>
+      </p>
+    </header>
+    <div class="main" ref="scrollRef">
+      <NCard :title="item.id" v-for="item in dataSource" :key="item.id" class="mb-[20px]">
+        <div v-for="arr, i in item.history" :key="i + item.id" class="pt-[40px]">
+          <ChatAsk :content="arr[0]"></ChatAsk>
+          <ChatAnswer :content="arr[1]" :toggleVisibleIcons="false"></ChatAnswer>
+          <hr>
+        </div>
+        <div class="answer-btns pt-[20px]">
+          <NSpace>
+            <!-- :disabled="item.isDisable" -->
+            <NButton :type="item.isDisable? '' : 'error'" @click="handleError(item)">
+              {{ item.isDisable ? '已审核' : '错' }}
+            </NButton>
+          </NSpace>
+        </div>
+      </NCard>
+    </div>
+  </section>
+  <NDrawer placement="right" v-model:show="visible" width="800" title="错误ID">
+    <n-drawer-content :title="'错误ID列表 -- ' + errIds.length + ' 个'">
+      <!-- <div class="py-[30px]">{{ errIds.join(',') }}</div> -->
+    <!-- <hr> -->
+    <ul class="grid grid-cols-5">
+      <li v-for="item in errIds" :key="item">{{ item }}</li>
+    </ul>
+    </n-drawer-content>
+  </NDrawer>
+</template>
+
+<style lang="scss" scoped>
+.page-container {
+  width: 100vw;
+  height: 100vh;
+  background: #edf7fc;
+
+  .header {
+    display: flex;
+    justify-content: center;
+    padding: 20px 0;
+    background: #fff;
+  }
+
+  .main {
+    width: 1000px;
+    height: calc(100% - 74px);
+    margin: 0 auto;
+    overflow-y: scroll;
+
+    .answer-block {
+      border-bottom: 1px solid #000000;
+      padding-bottom: 20px;
+    }
+
+    .answer-btns {
+      display: flex;
+      justify-content: flex-end;
+    }
+  }
+}
+</style>

+ 144 - 0
src/views/count/index3.vue

@@ -0,0 +1,144 @@
+<script setup>
+import { ref, onMounted, watch } from 'vue';
+import { NButton, NSpace, NDrawer, NDrawerContent, NCard } from 'naive-ui';
+import { ChatAsk, ChatAnswer } from '@/components/Chat';
+import allBookData from "./all_book3.json";
+
+const dataSource = ref([]);
+const errIds = ref([]);
+const visible = ref(false);
+const pageNum = ref(0);
+const pageSize = 10;
+
+const scrollRef = ref(null);
+
+const totalPages = Math.ceil(allBookData.length / pageSize);
+
+watch(pageNum, (num) => {
+  dataSource.value = allBookData.slice((num - 1) * pageSize, num * pageSize).map(item => ({
+    ...item,
+    isDisable: errIds.value.includes(item.id),
+    history: [
+      ...item.history,
+      [item.instruction, item.output]
+    ]
+  }))
+})
+
+const changeVisible = () => {
+  visible.value = !visible.value;
+}
+
+const handlePrePage = () => {
+  if ( pageNum.value === 1 ) {
+    return alert("已经是第一页了");
+  }
+  pageNum.value -= 1;
+  scroolToTop();
+  localStorage.setItem('pageNum', pageNum.value);
+}
+
+const handleNextPage = () => {
+  if ( pageNum.value === totalPages ) return alert("已经是最后一页了");
+  pageNum.value += 1;
+  scroolToTop();
+  localStorage.setItem('pageNum', pageNum.value);
+}
+
+const handleError = item => {
+  item.isDisable = !item.isDisable;
+  item.isDisable ? (errIds.value.push(item.id)) : (errIds.value = errIds.value.filter(id => item.id !== id));
+  localStorage.setItem('errIds', JSON.stringify( errIds.value ));
+  // console.log(errIds.value);
+}
+
+// 返回顶部
+const scroolToTop = () => {
+  scrollRef.value?.scrollTo({ top: 0, behavior: 'smooth' });
+}
+
+onMounted(() => {
+  const localIds = localStorage.getItem('errIds');
+  const localNum = localStorage.getItem('pageNum');
+  const ids = localIds ? JSON.parse(localIds) : [];
+  const num = localNum ? Number(localNum) : 1;
+
+  errIds.value = ids;
+  pageNum.value = num;
+})
+</script>
+
+<template>
+  <section class="page-container">
+    <header class="header flex items-center">
+      <NSpace>
+        <n-button @click="handlePrePage">上一页</n-button>
+        <n-button @click="handleNextPage">下一页</n-button>
+        <n-button @click="changeVisible">显示错误id</n-button>
+      </NSpace>
+      <p class="ml-[30px] space-x-[10px]">
+        <span>{{ pageNum }}</span>
+        <span> / </span>
+        <span>{{ totalPages }}</span>
+      </p>
+    </header>
+    <div class="main" ref="scrollRef">
+      <NCard :title="item.id" v-for="item in dataSource" :key="item.id" class="mb-[20px]">
+        <div v-for="arr, i in item.history" :key="i + item.id" class="pt-[40px]">
+          <ChatAsk :content="arr[0]"></ChatAsk>
+          <ChatAnswer :content="arr[1]" :toggleVisibleIcons="false"></ChatAnswer>
+          <hr>
+        </div>
+        <div class="answer-btns pt-[20px]">
+          <NSpace>
+            <!-- :disabled="item.isDisable" -->
+            <NButton :type="item.isDisable? '' : 'error'" @click="handleError(item)">
+              {{ item.isDisable ? '已审核' : '错' }}
+            </NButton>
+          </NSpace>
+        </div>
+      </NCard>
+    </div>
+  </section>
+  <NDrawer placement="right" v-model:show="visible" width="800" title="错误ID">
+    <n-drawer-content :title="'错误ID列表 -- ' + errIds.length + ' 个'">
+      <!-- <div class="py-[30px]">{{ errIds.join(',') }}</div> -->
+    <!-- <hr> -->
+    <ul class="grid grid-cols-5">
+      <li v-for="item in errIds" :key="item">{{ item }}</li>
+    </ul>
+    </n-drawer-content>
+  </NDrawer>
+</template>
+
+<style lang="scss" scoped>
+.page-container {
+  width: 100vw;
+  height: 100vh;
+  background: #edf7fc;
+
+  .header {
+    display: flex;
+    justify-content: center;
+    padding: 20px 0;
+    background: #fff;
+  }
+
+  .main {
+    width: 1000px;
+    height: calc(100% - 74px);
+    margin: 0 auto;
+    overflow-y: scroll;
+
+    .answer-block {
+      border-bottom: 1px solid #000000;
+      padding-bottom: 20px;
+    }
+
+    .answer-btns {
+      display: flex;
+      justify-content: flex-end;
+    }
+  }
+}
+</style>

BIN
src/views/screen/3d/factory.glb


File diff suppressed because it is too large
+ 97282 - 0
src/views/screen/3d/factory.obj


+ 185 - 0
src/views/screen/3d/renderModel.js

@@ -0,0 +1,185 @@
+import * as THREE from 'three'//导入three.js核心库
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' //导入轨道控制器
+import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
+import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
+import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
+import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader';
+
+class motor3d {
+  constructor(selector) {
+    this.container = document.querySelector(selector)
+    this.scene
+    this.camera
+    this.renderer
+    this.controls
+    this.init()
+    this.animate()
+  }
+
+  init() {
+    // 初始化场景
+    this.initScene()
+    // 初始化辅助轴
+    this.initAxesHelper()
+    // 初始化灯光
+    this.initLight()
+    // 初始化Mesh
+    this.initMesh()
+    // 初始化相机
+    this.initCamera()
+    // 初始化渲染器
+    this.initRender()
+    // 初始化轨道控制器
+    this.initControls()
+    // 监听场景大小改变,重新渲染尺寸
+    window.addEventListener('resize', this.onWindowResize.bind(this))
+  }
+
+  initScene() {
+    this.scene = new THREE.Scene()
+    // this.scene.background = new THREE.Color(0xa0a0a0)
+    this.scene.background = null;
+  }
+
+  initAxesHelper() {
+    const axesHelper = new THREE.AxesHelper(5)
+    this.scene.add(axesHelper)
+  }
+
+  initLight() {
+    const hesLight = new THREE.HemisphereLight(0xffffff, 0.5)
+    hesLight.intensity = 1
+    this.scene.add(hesLight)
+
+    const dirLight = new THREE.DirectionalLight()
+    dirLight.position.set(10, 10, 10).normalize();
+    this.scene.add(dirLight)
+  }
+  recordCameraChanges() {
+    // 记录位置
+    const position = this.camera.position;
+    console.log(`Position: (${position.x}, ${position.y}, ${position.z})`);
+  };
+  initMesh() {
+    this.addOBJFModel()
+  }
+  initCamera() {
+    this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100)
+    // this.camera.position.set(3.5, 5.5, 1.5)
+    // this.camera.position.set(28.07, 22.32, -12.92)
+    // this.camera.position.set(37.3642, 24.8315, -22.6927)
+    // this.camera.position.set(24.5564, 16.6944, -9.8434)
+    this.camera.position.set(20.47294749946306, 25.9193717746894, -10.426531907485199)
+
+  }
+
+  initRender() {
+    this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })//设置抗锯齿
+    // this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
+    // this.renderer.toneMappingExposure = 1; 
+    //设置屏幕像素比
+    this.renderer.setPixelRatio(window.devicePixelRatio)
+    //渲染的尺寸大小
+    this.renderer.setSize(this.container.offsetWidth, this.container.offsetHeight)
+    // 添加到容器
+    this.container.appendChild(this.renderer.domElement)
+    // 设置清除颜色的 Alpha 为 0
+    this.renderer.setClearColor(0xffffff, 0)
+
+    this.renderer.outputEncoding = THREE.sRGBEncoding;
+  }
+
+  animate() {
+    this.renderer.setAnimationLoop(this.render.bind(this))
+  }
+  render() {
+    this.renderer.render(this.scene, this.camera)
+  }
+  initControls() {
+    this.controls = new OrbitControls(this.camera, this.renderer.domElement)
+  }
+  // 加载模型
+  addOBJFModel(modelName) {
+    return new Promise((resolve, reject) => {
+      const mtlLoader = new MTLLoader()
+      const objLoader = new OBJLoader()
+      const gltfLoader = new GLTFLoader();
+      
+      gltfLoader.load('/factory.glb', (gltf) => {
+        const model = gltf.scene;
+        // console.log("glb 加载成功", gltf)
+
+        model.position.set(-4, 10, 0); // 设置模型位置
+        // model.rotation.set(0, Math.PI / 0, 0); // 设置模型旋转
+        // model.scale.set(1, 0.5, 1); // 设置模型缩放
+
+        this.scene.add(model);
+
+      },(xhr) => {
+        console.log(xhr);
+      }, (error) => {
+        console.log(error);
+      })
+
+      const rgbeLoader = new RGBELoader();
+      rgbeLoader.load('/clarens_night_02_8k.hdr', (texture) => {
+        texture.mapping = THREE.EquirectangularReflectionMapping;
+        texture.minFilter = THREE.LinearFilter;
+        texture.magFilter = THREE.LineasrFilter;
+        // texture.format = THREE.RGBEFormat;
+
+        this.scene.environment = texture;
+
+        // texture.mapping = THREE.EquirectangularReflectionMapping;
+        // texture.encoding = THREE.LinearEncoding;
+        // this.scene.environment = texture;
+      }, undefined, (error) => {
+          console.error('An error happened.', error);
+      });
+
+      // mtlLoader.load('./texture.mtl', (mtl) => {
+      //   mtl.preload();
+
+      //   objLoader.setMaterials(mtl);
+
+      //   objLoader.load('./factory.obj', (obj) => {
+      //     obj.position.set(0, 10, 0);
+      //     var material = new THREE.MeshStandardMaterial({ color: '#fff' });
+      //     obj.traverse(function (child) {
+      //       console.log(child.name);
+      //     //   if (child instanceof THREE.Mesh) {
+      //     //     for (let i = 0; i < child.material.length; i++) {
+      //     //       child.material[i].color.set(0xffffff); // 设置材质颜色为白色
+      //     //   }
+      //         // child.material = material;
+      //         // if (child.material instanceof THREE.MeshStandardMaterial ||
+      //         //   child.material instanceof THREE.MeshPhongMaterial) {
+      //         //     child.material.color.set('#fff');
+      //         // }
+      //     // }
+      //     })
+      //     this.scene.add(obj)
+      //     resolve('模型添加成功')
+      //   })
+      // })
+
+    })
+  }
+
+  onWindowResize() {
+    this.recordCameraChanges();
+    this.camera.aspect = this.container.offsetWidth / this.container.offsetHeight
+    this.camera.updateProjectionMatrix()//更新矩阵,将3d内容投射到2d画面上转换
+    this.renderer.setSize(this.container.offsetWidth, this.container.offsetHeight)
+  }
+
+  dispose() {
+    this.scene.dispose();
+    this.camera.dispose();
+    this.renderer.dispose();
+    this.controls.dispose();
+    window.removeEventListener('resize', this.onWindowResize);
+  }
+
+}
+export default motor3d;

+ 22 - 0
src/views/screen/3d/texture.mtl

@@ -0,0 +1,22 @@
+# Blender 4.1.1 MTL File: '水厂6.blend'
+# www.blender.org
+
+newmtl 材质.001
+Ns 250.000000
+Ka 1.000000 1.000000 1.000000
+Kd 0.156420 0.800416 0.155804
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.500000
+d 1.000000
+illum 2
+
+newmtl 材质.002
+Ns 360.000000
+Ka 1.000000 1.000000 1.000000
+Kd 0.800000 0.800000 0.800000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2

+ 0 - 4
src/views/screen/ScreenView.vue

@@ -104,7 +104,6 @@ onBeforeUnmount(() => {
       <RouterLink to="/work" class="item item4">智能办公</RouterLink>
     </div>
     <div class="screen-container">
-      <!-- <div class="water-work-inner"></div> -->
       <div class="screen-container-main">
         <div class="left">
           <shuizhi :screenData="screenData"></shuizhi>
@@ -113,9 +112,6 @@ onBeforeUnmount(() => {
         </div>
         <div class="middle">
           <middleBox :dataTime="dataTime"></middleBox>
-          <!-- <div class="img-card">
-            <img src="@/assets/images/home/water-work.png" alt="" />
-          </div> -->
         </div>
         <div class="right">
           <ControlHelper></ControlHelper>

+ 336 - 0
src/views/screen/ScreenView2.vue

@@ -0,0 +1,336 @@
+<script setup>
+import { ref, onBeforeUnmount, onMounted } from "vue";
+import { userTop } from '@/components';
+import shuizhi from "./components/shuizhi";
+import dataBox from "./components/dataBox";
+import liuliang from "./components/liuliang";
+import gongyi from "./components/gongyi";
+import middleBox from "./components/middleBox";
+import EchartBar from "./components/EchartBar";
+// import motor3d from './3d/renderModel.js';
+import ControlHelper from "./components/ControlHelper";
+import { screenApi } from "@/api/screen"
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+
+dayjs.locale('zh-cn'); // 设置day.js使用中文/ 导入中文语言包
+
+const gongyiData = ref([])
+const reportData = ref({})
+const screenData = ref({})
+const time = ref('')
+let timer = null
+let timeHour = null
+
+const dataTime = ref("");
+
+// 获取工艺管控 助手
+const getWarningList = () => {
+  screenApi.warningList().then(res => {
+    gongyiData.value = res.data || []
+  })
+}
+
+// 获取数据费助手
+const getLeastShortReport = () => {
+  screenApi.getLeastShortReport().then(res => {
+    reportData.value = res.data
+  })
+}
+
+// 获取大屏分析数据
+const getRealTimeData = () => {
+  screenApi.realTimeData().then(res => {
+    screenData.value = res.data
+    dataTime.value = `更新时间:${res.data.testHour}`
+    time.value = dayjs(res.data.timestamp).format('HH:mm:ss')
+    timeHour = setInterval(() => {
+      time.value = updateCurrentTime(res.data.timestamp)
+    }, 1000)
+  })
+}
+
+const updateCurrentTime = (timestamp) => {
+  // 计算从初始时间戳到现在经过的时间
+  const now = dayjs();
+  const initialTime = dayjs(timestamp);
+  const elapsedTime = now.diff(initialTime);
+
+  // 使用初始时间戳加上经过的时间来更新当前时间
+  const currentTime = initialTime.add(elapsedTime, 'millisecond');
+  return currentTime.format('HH:mm:ss');
+}
+const init = () => {
+  getRealTimeData()
+  getWarningList()
+  getLeastShortReport()
+}
+
+init()
+
+onMounted(() => {
+
+  //每5s刷新数据
+  timer = setInterval(() => {
+    init()
+  }, 1000 * 60 * 60)
+})
+
+onBeforeUnmount(() => {
+  clearInterval(timer)
+  clearInterval(timeHour)
+  timer = null;
+  timeHour = null
+})
+
+</script>
+<template>
+  <div class="screen-view" id="screen-view">
+    <div class="screen-view-top">
+      <div class="left space-x-[1rem]">
+        <span class="time">{{ time }}</span>
+        <span class="line"></span>
+        <div class="data">
+          <div>{{ dayjs(screenData.timestamp).format("dddd") }}</div>
+          <div class="text-[12px]">{{ dayjs(screenData.timestamp).format('YYYY-MM-DD') }}</div>
+        </div>
+      </div>
+      <div class="chat-header flex items-center justify-end pr-[18px] space-x-[16px]">
+        <userTop></userTop>
+      </div>
+    </div>
+    <div class="menu">
+      <RouterLink to="/" class="item item1">智慧总控</RouterLink>
+      <RouterLink to="/answer" class="item item2">专家问答</RouterLink>
+      <RouterLink to="/water-warn" class="item item3">工艺管控</RouterLink>
+      <RouterLink to="/work" class="item item4">智能办公</RouterLink>
+    </div>
+    <div class="screen-container">
+      <div class="screen-container-main">
+        <div class="left">
+          <shuizhi :screenData="screenData"></shuizhi>
+          <EchartBar :screenData="screenData"></EchartBar>
+          <liuliang :screenData="screenData"></liuliang>
+        </div>
+        <div class="middle">
+          <middleBox :dataTime="dataTime"></middleBox>
+        </div>
+        <div class="right">
+          <ControlHelper></ControlHelper>
+          <dataBox :reportData="reportData"></dataBox>
+          <gongyi :gongyiData="gongyiData"></gongyi>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.screen-view {
+  position: relative;
+  display: flex;
+  justify-content: space-around;
+  flex-flow: column;
+  width: 100vw;
+  height: 100vh;
+  background: url(@/assets/images/home/home_bg1.png) center center no-repeat;
+  background-size: 100% 100%;
+  overflow: hidden;
+
+  &-top {
+    display: flex;
+    align-items: end;
+    justify-content: space-between;
+    padding: 0 5rem;
+    height: 9rem;
+
+    .left {
+      display: flex;
+      align-items: center;
+      font-size: 1.6rem;
+    }
+
+    .time {
+      font-size: 3.6rem;
+      font-weight: bold;
+      color: #333;
+    }
+
+    .line {
+      margin: 0 1rem 0 1.5rem;
+      display: inline-block;
+      background: #767C82;
+      height: 2.8rem;
+      width: 0.2rem;
+    }
+
+    .data {
+      color: #415B73;
+    }
+  }
+
+  .menu {
+    display: flex;
+    align-content: center;
+    justify-content: center;
+    flex-shrink: 0;
+
+    .item {
+      width: 21.8rem;
+      height: 5.2rem;
+      display: inline-block;
+      font-size: 0;
+    }
+
+    .item1 {
+      background: url('@/assets/images/home/menu_1_hover.png') no-repeat;
+      background-size: 100% 100%;
+    }
+
+    .item2 {
+      background: url('@/assets/images/home/menu_2.png') no-repeat;
+      background-size: 100% 100%;
+      margin-right: 20rem;
+
+      &:hover {
+        background: url('@/assets/images/home/menu_2_hover.png') no-repeat;
+        background-size: 100% 100%;
+      }
+    }
+
+    .item3 {
+      background: url('@/assets/images/home/menu_3.png') no-repeat;
+      background-size: 100% 100%;
+
+      &:hover {
+        background: url('@/assets/images/home/menu_3_hover.png') no-repeat;
+        background-size: 100% 100%;
+      }
+    }
+
+    .item4 {
+      background: url('@/assets/images/home/menu_4.png') no-repeat;
+      background-size: 100% 100%;
+
+      &:hover {
+        background: url('@/assets/images/home/menu_4_hover.png') no-repeat;
+        background-size: 100% 100%;
+      }
+    }
+
+  }
+
+  .water-work-inner {
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    width: 150rem;
+    height: 80rem;
+    background: url("@assets/images/home/water-work.png") no-repeat;
+  }
+
+  .screen-container {
+    position: relative;
+    display: flex;
+    flex-flow: column;
+    height: 80rem;
+    padding: 0 6rem;
+    z-index: 2;
+
+
+
+    &-main {
+      position: relative;
+      height: 100%;
+      display: flex;
+      justify-content: space-between;
+      z-index: 2;
+
+      .left {
+        position: relative;
+        display: flex;
+        flex-flow: column;
+        z-index: 3;
+      }
+
+      .middle {
+        position: relative;
+        flex: 1;
+
+        // .send-ask {
+        //   position: relative;
+        //   z-index: 2;
+        // }
+      }
+      .right {
+        position: relative;
+        z-index: 3;
+      }
+    }
+  }
+
+  .img-card {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    width: 79rem;
+    height: 59rem;
+  }
+}
+
+@media screen and (max-width: 1440px) {
+  .screen-view {
+    justify-content: flex-start;
+    // background: url(@/assets/images/home/home_bg.png) center top no-repeat;
+    // background-size: contain;
+  }
+}
+</style>
+
+<style lang="scss">
+.home-box {
+  width: 54rem;
+  // width: 540px;
+  border-radius: 0.8rem;
+  // border: 1px solid #fff;
+  // margin-bottom: 2rem;
+
+  &-top {
+    padding: 0.7rem 2.4rem 0.7rem 1.2rem;
+    display: flex;
+    justify-content: space-between;
+    position: relative;
+    align-items: center;
+
+    &::after {
+      content: '';
+      height: 0.2rem;
+      background: url('@/assets/images/home/line.png') no-repeat;
+      background-size: 100% 100%;
+      width: 100%;
+      position: absolute;
+      left: 1.2rem;
+      bottom: 0;
+    }
+
+    .title {
+      font-size: 1.8rem;
+      font-weight: bold;
+      display: flex;
+      align-items: center;
+
+      &::before {
+        content: '';
+        width: 2rem;
+        height: 2rem;
+        background: url('@/assets/images/home/mark.png') no-repeat;
+        background-size: cover;
+        margin-right: 0.8rem;
+        display: inline-block;
+      }
+    }
+  }
+}
+</style>

+ 2 - 2
src/views/screen/components/ControlHelper.vue

@@ -1,5 +1,5 @@
 <script setup>
-import { ref, watch, onMounted, onUnmounted } from 'vue';
+import { ref, onMounted, onUnmounted } from 'vue';
 import { screenApi } from '@/api/screen';
 import * as echarts from 'echarts';
 import dayjs from 'dayjs';
@@ -10,7 +10,6 @@ let echart = null;
 
 const echartRef = ref(null);
 const legendData = ref([]);
-const dateTime = ref();
 const dataSource = ref({});
 
 const windowResize = () => echart.resize();
@@ -43,6 +42,7 @@ onMounted(async () => {
 
 onUnmounted(() => {
   window.removeEventListener("resize", windowResize);
+  echart && echart.dispose();
 })
 
 </script>

+ 19 - 8
src/views/screen/components/dataBox.vue

@@ -17,13 +17,16 @@ const content = '① 因房地产市场并不十分活跃和顺利运转,因
         <RouterLink to="/work-order" class="flex items-center space-x-[0.4rem] text-[1.6rem]">
           <span>更多</span>
           <svg width="1.5rem" height="2rem" viewBox="0 0 15 20" fill="none" xmlns="http://www.w3.org/2000/svg">
-            <path fill-rule="evenodd" clip-rule="evenodd" d="M14.5303 10L4.53033 0L2.84959 1.68074L11.1688 10L2.84959 18.3193L4.53033 20L14.5303 10ZM7.39954 10L2.1534 4.75386L0.472656 6.4346L4.03805 10L0.472656 13.5654L2.1534 15.2461L7.39954 10Z" fill="url(#paint0_linear_1909_2940)"/>
+            <path fill-rule="evenodd" clip-rule="evenodd"
+              d="M14.5303 10L4.53033 0L2.84959 1.68074L11.1688 10L2.84959 18.3193L4.53033 20L14.5303 10ZM7.39954 10L2.1534 4.75386L0.472656 6.4346L4.03805 10L0.472656 13.5654L2.1534 15.2461L7.39954 10Z"
+              fill="url(#paint0_linear_1909_2940)" />
             <defs>
-            <linearGradient id="paint0_linear_1909_2940" x1="14.5303" y1="10" x2="-0.817942" y2="10.5941" gradientUnits="userSpaceOnUse">
-            <stop stop-color="#1872D8"/>
-            <stop offset="0.43028" stop-color="#08AEF1"/>
-            <stop offset="1" stop-color="white"/>
-            </linearGradient>
+              <linearGradient id="paint0_linear_1909_2940" x1="14.5303" y1="10" x2="-0.817942" y2="10.5941"
+                gradientUnits="userSpaceOnUse">
+                <stop stop-color="#1872D8" />
+                <stop offset="0.43028" stop-color="#08AEF1" />
+                <stop offset="1" stop-color="white" />
+              </linearGradient>
             </defs>
           </svg>
         </RouterLink>
@@ -53,17 +56,19 @@ const content = '① 因房地产市场并不十分活跃和顺利运转,因
       .markdown-inner {
         height: 20rem;
         overflow-y: scroll;
+
         &::-webkit-scrollbar-track {
-          background-color: rgba(255,255,255,0.3);
+          background-color: rgba(255, 255, 255, 0.3);
         }
       }
+
       // .markdown-body{
       //   &::-webkit-scrollbar {
       //     width: 0px !important;
       //   }
       // }
       .title {
-        margin-bottom:1.6rem;
+        margin-bottom: 1.6rem;
         font-size: 1.6rem;
         font-weight: bold;
       }
@@ -94,4 +99,10 @@ const content = '① 因房地产市场并不十分活跃和顺利运转,因
     }
   }
 }
+</style>
+
+<style >
+.data-box .data-box-main .content .markdown-body .custom-table-wrapper {
+  width: 45rem !important;
+}
 </style>

+ 21 - 0
src/views/screen/components/middleBox.vue

@@ -1,11 +1,21 @@
 <script setup>
+import { onMounted } from "vue";
+// import motor3d from '../3d/renderModel.js';
+
+
 defineProps({
   dataTime: ''
 })
+
+onMounted(() => {
+  // new motor3d('#scene') 
+})
+
 </script>
 
 <template>
   <div class="send-ask">
+    <!-- <div id="scene" class="3d-scene"></div> -->
     <RouterLink class="send-ask-button" to="/answer" :data-time="dataTime">输入您的问题或需求</RouterLink>
   </div>
 </template>
@@ -15,6 +25,16 @@ defineProps({
   margin: 0 2rem;
   position: relative;
   height: 100%;
+  
+  #scene {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    width: 116rem;
+    height: calc(100% - 6rem);
+    z-index: 1;
+  }
 
   &-button {
     position: absolute;
@@ -30,6 +50,7 @@ defineProps({
     justify-content: space-between;
     padding: 0 1.7rem 0 3.4rem;
     bottom: 4.7rem;
+    z-index: 2;
 
     &::before {
       content: attr(data-time);

File diff suppressed because it is too large
+ 97282 - 0
src/views/test/factory.obj


+ 71 - 0
src/views/test/index.vue

@@ -0,0 +1,71 @@
+<script setup>
+import { ref, onMounted, onUnmounted } from 'vue';
+import * as THREE from 'three';
+import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
+// import motor3d from './renderModel';
+
+
+
+onMounted(() => {
+  // new motor3d('#scene') 
+
+//   const scene = new THREE.Scene();
+
+//   const fov = 75;
+//   const aspect = 2;  // 相机默认值
+//   const near = 0.1;
+//   const far = 5;
+
+//   const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 201, 2000);
+
+//   // camera.up.set(0, 0, 1);
+//   // camera.lookAt(0, 1000, 0);
+
+//   const renderer = new THREE.WebGLRenderer();
+//   renderer.setSize(window.innerWidth, window.innerHeight);
+//   document.getElementById('three').appendChild(renderer.domElement);
+
+//   const loader = new OBJLoader();
+//   loader.load('factor.obj', (object) => {
+//     console.log(object);
+//     // 这里可以对加载进来的模型做一些处理,比如旋转、缩放等
+//     scene.add(object);
+//   });
+
+
+//   const ambientLight = new THREE.AmbientLight(0x404040); // soft white light
+// scene.add(ambientLight);
+
+// const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
+// directionalLight.position.set(1, 1, 1);
+// scene.add(directionalLight);
+//   // camera.position.z = 2;
+
+//   const controls = new OrbitControls(camera, renderer.domElement);
+//   controls.enableDamping = true;
+
+//   function animate() {
+//     requestAnimationFrame(animate);
+//     renderer.render(scene, camera);
+//     controls.update();
+//   }
+
+//   animate();
+
+})
+
+</script>
+
+
+
+<template>
+  <div id="scene">    
+  </div>
+  <!-- <div id="three"></div> -->
+  <!-- <section id="model" class="w-[600vw] h-[600vh]">
+
+  </section> -->
+</template>
+
+<style lang="scss" scoped></style>

+ 129 - 0
src/views/test/renderModel.js

@@ -0,0 +1,129 @@
+import * as THREE from 'three'//导入three.js核心库
+import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' //导入轨道控制器
+import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
+import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
+
+class motor3d {
+  constructor(selector) {
+    this.container = document.querySelector(selector)
+    this.scene
+    this.camera
+    this.renderer
+    this.controls
+    this.init()
+    this.animate()
+  }
+
+  init() {
+    // 初始化场景
+    this.initScene()
+    // 初始化辅助轴
+    this.initAxesHelper()
+    // 初始化灯光
+    this.initLight()
+    // 初始化Mesh
+    this.initMesh()
+    // 初始化相机
+    this.initCamera()
+    // 初始化渲染器
+    this.initRender()
+    // 初始化轨道控制器
+    this.initControls()
+    // 监听场景大小改变,重新渲染尺寸
+    window.addEventListener('resize', this.onWindowResize.bind(this))
+  }
+
+  initScene() {
+    this.scene = new THREE.Scene()
+    // this.scene.background = new THREE.Color(0xa0a0a0)
+    this.scene.background = null;
+  }
+
+  initAxesHelper() {
+    const axesHelper = new THREE.AxesHelper(5)
+    this.scene.add(axesHelper)
+  }
+
+  initLight() {
+    const hesLight = new THREE.HemisphereLight(0xffffff, 0.5)
+    // hesLight.intensity = 0.6
+    this.scene.add(hesLight)
+
+    const dirLight = new THREE.DirectionalLight()
+    dirLight.position.set(10, 10, 10).normalize();
+    this.scene.add(dirLight)
+  }
+  recordCameraChanges () {
+    // 记录位置
+    const position = this.camera.position;
+    console.log(`Position: (${position.x}, ${position.y}, ${position.z})`);
+  };
+  initMesh() {
+    this.addOBJFModel()
+  }
+  initCamera() {
+    this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100)
+    // this.camera.position.set(3.5, 5.5, 1.5)
+    // this.camera.position.set(28.07, 22.32, -12.92)
+    this.camera.position.set(37.3642, 24.8315, -22.6927)
+  }
+
+  initRender() {
+    this.renderer = new THREE.WebGLRenderer({ antialias: true,  alpha: true })//设置抗锯齿
+    //设置屏幕像素比
+    this.renderer.setPixelRatio(window.devicePixelRatio)
+    //渲染的尺寸大小
+    this.renderer.setSize(window.innerWidth, window.innerHeight)
+    // 添加到容器
+    this.container.appendChild(this.renderer.domElement)
+    // 设置清除颜色的 Alpha 为 0
+    this.renderer.setClearColor(0xffffff, 0)
+
+    this.renderer.outputEncoding = THREE.sRGBEncoding;
+  }
+
+  animate() {
+    this.renderer.setAnimationLoop(this.render.bind(this))
+  }
+  render() {
+    this.renderer.render(this.scene, this.camera)
+  }
+  initControls() {
+    this.controls = new OrbitControls(this.camera, this.renderer.domElement)
+  }
+  // 加载模型
+  addOBJFModel(modelName) {
+    return new Promise((resolve, reject) => {
+      const mtlLoader = new MTLLoader()
+      const objLoader = new OBJLoader()
+
+      mtlLoader.load('./texture.mtl', (mtl) => {
+        mtl.preload();
+
+        objLoader.setMaterials(mtl);
+
+        objLoader.load('./factory.obj', (obj) => {
+          this.scene.add(obj)
+          resolve('模型添加成功')
+        })
+      })
+
+    })
+  }
+
+  onWindowResize() {
+    this.recordCameraChanges();
+    this.camera.aspect = window.innerWidth / window.innerHeight
+    this.camera.updateProjectionMatrix()//更新矩阵,将3d内容投射到2d画面上转换
+    this.renderer.setSize(window.innerWidth, window.innerHeight)
+  }
+
+  dispose() {
+    this.scene.dispose();
+    this.camera.dispose();
+    this.renderer.dispose();
+    this.controls.dispose();
+    window.removeEventListener('resize', this.onWindowResize);
+  }
+}
+export default motor3d;

+ 22 - 0
src/views/test/texture.mtl

@@ -0,0 +1,22 @@
+# Blender 4.1.1 MTL File: '水厂6.blend'
+# www.blender.org
+
+newmtl 材质.001
+Ns 250.000000
+Ka 1.000000 1.000000 1.000000
+Kd 0.156420 0.800416 0.155804
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.500000
+d 1.000000
+illum 2
+
+newmtl 材质.002
+Ns 360.000000
+Ka 1.000000 1.000000 1.000000
+Kd 0.800000 0.800000 0.800000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 2

+ 40 - 17
src/views/work/WorkView.vue

@@ -2,7 +2,7 @@
 import { ref, unref, computed, onMounted, onUnmounted } from 'vue';
 import { useMessage } from 'naive-ui';
 import { BaseButton, RecodeCardItem, TheSubMenu, TheChatView, ChatWelcome, SvgIcon } from '@/components';
-import { ChatAsk, ChatAnswer, ChatInput } from '@/components/Chat';
+import { ChatAsk, ChatAnswer, ChatInputCopy } from '@/components/Chat';
 import { chatApi } from '@/api/chat';
 import { helperApi } from '@/api/helper';
 
@@ -12,7 +12,7 @@ const ANSWER_ID_KEY = '@@id@@';
 
 let controller = new AbortController();
 
-const { recordList, isFetching, onScrolltolower, onReset, addHistoryRecord } = useInfinite('/front/bigModel/qa/pageList', { module: 2 });
+const { recordList, isFetching, onScrolltolower, onReset, } = useInfinite('/front/bigModel/qa/pageList', { module: 2 });
 const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
 const { chatDataSource, addChat, updateChat, clearChat, updateById } = useChat();
 
@@ -25,6 +25,8 @@ const isLoading = ref(false);
 const inputRef = ref(null);
 const recordActive = ref(null);
 
+const activeItem = ref({});
+
 const currenSessionId = ref(null);
 
 const isExistInHistory = computed(() => (recordList.value.findIndex(({ sessionId: sId }) => sId === unref(currenSessionId)) === -1));
@@ -63,23 +65,24 @@ const handleChatDetail = async ({ sessionId }) => {
   const { data } = await chatApi.getAnswerHistoryDetail({ sessionId });
 
   chatDataSource.value = data.map(item => ({ ...item, loading: false, }));
-  currenSessionId.value = sessionId;
-
 
+  currenSessionId.value = sessionId;
 
   scrollToBottom();
 }
 
-const onRegenerate = async ({ question, realQuestion }) => {
+const onRegenerate = async ({ question, tools }) => {
   controller = new AbortController();
 
   const sessionId = unref(currenSessionId);
+
   const params = {
     data: {
       sessionId,
       showVal: question,
-      question: realQuestion || question,
+      question: question,
       module: 2,
+      tools: activeItem.value.tools || tools,
       isStrong: Number(unref(switchActive))
     },
     signal: controller.signal,
@@ -125,8 +128,7 @@ const onRegenerate = async ({ question, realQuestion }) => {
   }
 }
 // 提交问题
-const handleSubmit = async (question, realQuestion = '') => {
-  // 用于模拟 - 内容生成前置等待状态
+const handleSubmit = async ({question, selectedOption}) => {
 
   if (unref(isExistInHistory)) {
     const { data: sessionId } = await chatApi.getChatSessionTag();
@@ -138,7 +140,6 @@ const handleSubmit = async (question, realQuestion = '') => {
   addChat({
     sessionId: unref(currenSessionId),
     question,
-    realQuestion,
     answer: '',
     loading: true,
     delayLoading: true
@@ -146,13 +147,13 @@ const handleSubmit = async (question, realQuestion = '') => {
 
   scrollToBottom();
 
-  setTimeout(() => onRegenerate({ question, realQuestion }), 2 * 1000);
+  setTimeout(() => onRegenerate({ question, tools: selectedOption?.tools || null }), 2 * 1000);
 }
 
 // 处理推荐问题
-const handleWelcomeRecommend = ({ content }) => {
-  console.log(inputRef.value)
-  inputRef.value.inpVal = content;
+const handleWelcomeRecommend = (item) => {
+  activeItem.value = item;
+  inputRef.value.inpVal = item.content;
   inputRef.value.handleInpFocus();
 }
 
@@ -164,6 +165,21 @@ const handeChatDelete = async (id) => {
   message.success('删除成功');
 }
 
+// 返回操作
+const handleback = async () => {
+
+  controller?.abort();
+  // await chatApi.getStopChatStream(currenSessionId.value);
+
+  inputRef.value.clearInpVal();
+
+  recordActive.value = null;
+
+  currenSessionId.value = null;
+
+  clearChat();
+}
+
 onMounted(async () => {
   const { data } = await helperApi.getHelperList();
   helperList.value = data;
@@ -190,8 +206,8 @@ onUnmounted(() => {
       </div>
     </TheSubMenu>
 
-    <TheChatView ref="scrollRef">
-      <div v-if="!chatDataSource.length">
+    <TheChatView ref="scrollRef" :is-back-btn="!!chatDataSource.length" @on-click-back="handleback">
+      <div v-show="!chatDataSource.length">
         <ChatWelcome title="您好,我是LibraAI智能助手" :sub-title="[
           'LibarAI智能助手模块提供撰写文章、生成报告等服务',
           '请替换问题中##的内容'
@@ -228,8 +244,15 @@ onUnmounted(() => {
       </div>
 
       <template #footer>
-        <ChatInput ref="inputRef" v-model:loading="isLoading" v-model:switch="switchActive" @on-click="handleSubmit"
-          @on-enter="handleSubmit"></ChatInput>
+        <ChatInputCopy
+          :options="helperList"
+          :active-item="activeItem"
+          ref="inputRef"
+          v-model:loading="isLoading"
+          v-model:switch="switchActive"
+          @on-click="handleSubmit"
+          @on-enter="handleSubmit"
+        ></ChatInputCopy>
       </template>
     </TheChatView>
   </section>

File diff suppressed because it is too large
+ 44 - 0
vite.config.ts.timestamp-1720486813773-2c95633e7e6a2.mjs


Some files were not shown because too many files changed in this diff