bill 1 месяц назад
Родитель
Сommit
bbb8903496

+ 3 - 3
.vscode/settings.json

@@ -1,14 +1,14 @@
 {
   "i18n-ally.localesPaths": [
-    "src/lang"
+    "src/lang/locales"
   ],
   "i18n-ally.keystyle": "nested",
   "i18n-ally.sortKeys": true,
   "editor.tabSize": 2,
   "i18n-ally.namespace": true,
-  "i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}",
+  "i18n-ally.pathMatcher": "{locale}.{ext}",
   "i18n-ally.enabledParsers": [
-    "ts"
+    "json"
   ],
   "i18n-ally.sourceLanguage": "en",
   "i18n-ally.displayLanguage": "zh",

+ 8 - 5
package.json

@@ -4,11 +4,12 @@
   "version": "0.0.0",
   "type": "module",
   "scripts": {
-    "dev": "vite --host 0.0.0.0",
-    "dev-sdk": "vite --mode sdk --host 0.0.0.0",
-    "build": "vite build",
-    "build-test": "vite build --mode test",
-    "build-sdk": "vite build --mode sdk",
+    "translate": "node ./scripts/fetch-langs.mjs",
+    "dev": "yarn translate && vite --host 0.0.0.0",
+    "dev-sdk": "yarn translate && vite --mode sdk --host 0.0.0.0",
+    "build": "yarn translate && vite build",
+    "build-test": "yarn translate && vite build --mode test",
+    "build-sdk": "yarn translate && vite build --mode sdk",
     "preview": "vite preview"
   },
   "dependencies": {
@@ -26,10 +27,12 @@
     "js-base64": "^3.7.5",
     "jspdf": "^2.5.1",
     "mitt": "^3.0.0",
+    "node-fetch": "^3.3.2",
     "sass": "^1.62.0",
     "sass-loader": "^13.2.2",
     "stateshot": "^1.3.5",
     "swiper": "^8.3.0",
+    "unzipper": "^0.12.3",
     "vconsole": "^3.15.0",
     "vue": "^3.2.47",
     "vue-cropper": "1.0.2",

+ 132 - 0
pnpm-lock.yaml

@@ -18,12 +18,14 @@ specifiers:
   js-base64: ^3.7.5
   jspdf: ^2.5.1
   mitt: ^3.0.0
+  node-fetch: ^3.3.2
   sass: ^1.62.0
   sass-loader: ^13.2.2
   stateshot: ^1.3.5
   swiper: ^8.3.0
   terser: ^5.19.4
   typescript: ^4.9.3
+  unzipper: ^0.12.3
   vconsole: ^3.15.0
   vite: ^4.2.0
   vue: ^3.2.47
@@ -48,10 +50,12 @@ dependencies:
   js-base64: 3.7.5
   jspdf: 2.5.1
   mitt: 3.0.0
+  node-fetch: 3.3.2
   sass: 1.62.0
   sass-loader: 13.2.2_sass@1.62.0
   stateshot: 1.3.5
   swiper: 8.4.7
+  unzipper: 0.12.3
   vconsole: 3.15.1
   vue: 3.2.47
   vue-cropper: 1.0.2
@@ -1811,6 +1815,10 @@ packages:
     resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
     engines: {node: '>=8'}
 
+  /bluebird/3.7.2:
+    resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
+    dev: false
+
   /body-parser/1.20.1:
     resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
     engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
@@ -2024,6 +2032,10 @@ packages:
     requiresBuild: true
     dev: false
 
+  /core-util-is/1.0.3:
+    resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+    dev: false
+
   /css-line-break/2.1.0:
     resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
     dependencies:
@@ -2033,6 +2045,11 @@ packages:
   /csstype/2.6.21:
     resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==}
 
+  /data-uri-to-buffer/4.0.1:
+    resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
+    engines: {node: '>= 12'}
+    dev: false
+
   /de-indent/1.0.2:
     resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
     dev: true
@@ -2088,6 +2105,12 @@ packages:
     resolution: {integrity: sha512-bczjyKdX6XmFyCDkwtRmlaORDwfBk1xXmRO0CAe5VwNQTM98aWaG2LAIiIdTe53iV/B7W5lXlIy2xYtf0JRb7Q==}
     dev: false
 
+  /duplexer2/0.1.4:
+    resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==}
+    dependencies:
+      readable-stream: 2.3.8
+    dev: false
+
   /ee-first/1.1.1:
     resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
 
@@ -2202,6 +2225,14 @@ packages:
       - supports-color
     dev: true
 
+  /fetch-blob/3.2.0:
+    resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
+    engines: {node: ^12.20 || >= 14.13}
+    dependencies:
+      node-domexception: 1.0.0
+      web-streams-polyfill: 3.3.3
+    dev: false
+
   /fflate/0.4.8:
     resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
     dev: false
@@ -2246,6 +2277,13 @@ packages:
       mime-types: 2.1.35
     dev: false
 
+  /formdata-polyfill/4.0.10:
+    resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
+    engines: {node: '>=12.20.0'}
+    dependencies:
+      fetch-blob: 3.2.0
+    dev: false
+
   /forwarded/0.2.0:
     resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
     engines: {node: '>= 0.6'}
@@ -2256,6 +2294,15 @@ packages:
     engines: {node: '>= 0.6'}
     dev: true
 
+  /fs-extra/11.3.2:
+    resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==}
+    engines: {node: '>=14.14'}
+    dependencies:
+      graceful-fs: 4.2.11
+      jsonfile: 6.2.0
+      universalify: 2.0.1
+    dev: false
+
   /fsevents/2.3.2:
     resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -2289,6 +2336,10 @@ packages:
     engines: {node: '>=4'}
     dev: false
 
+  /graceful-fs/4.2.11:
+    resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+    dev: false
+
   /has-flag/3.0.0:
     resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
     engines: {node: '>=4'}
@@ -2374,6 +2425,10 @@ packages:
     resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
     engines: {node: '>=0.12.0'}
 
+  /isarray/1.0.0:
+    resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
+    dev: false
+
   /js-base64/3.7.5:
     resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==}
     dev: false
@@ -2399,6 +2454,14 @@ packages:
     hasBin: true
     dev: false
 
+  /jsonfile/6.2.0:
+    resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
+    dependencies:
+      universalify: 2.0.1
+    optionalDependencies:
+      graceful-fs: 4.2.11
+    dev: false
+
   /jspdf/2.5.1:
     resolution: {integrity: sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==}
     dependencies:
@@ -2517,6 +2580,25 @@ packages:
     resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
     dev: false
 
+  /node-domexception/1.0.0:
+    resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
+    engines: {node: '>=10.5.0'}
+    deprecated: Use your platform's native DOMException instead
+    dev: false
+
+  /node-fetch/3.3.2:
+    resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+    dependencies:
+      data-uri-to-buffer: 4.0.1
+      fetch-blob: 3.2.0
+      formdata-polyfill: 4.0.10
+    dev: false
+
+  /node-int64/0.4.0:
+    resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
+    dev: false
+
   /node-releases/2.0.13:
     resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
     dev: false
@@ -2565,6 +2647,10 @@ packages:
       picocolors: 1.0.0
       source-map-js: 1.0.2
 
+  /process-nextick-args/2.0.1:
+    resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
+    dev: false
+
   /proxy-addr/2.0.7:
     resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
     engines: {node: '>= 0.10'}
@@ -2614,6 +2700,18 @@ packages:
       unpipe: 1.0.0
     dev: false
 
+  /readable-stream/2.3.8:
+    resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
+    dependencies:
+      core-util-is: 1.0.3
+      inherits: 2.0.4
+      isarray: 1.0.0
+      process-nextick-args: 2.0.1
+      safe-buffer: 5.1.2
+      string_decoder: 1.1.1
+      util-deprecate: 1.0.2
+    dev: false
+
   /readdirp/3.6.0:
     resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
     engines: {node: '>=8.10.0'}
@@ -2680,6 +2778,10 @@ packages:
     optionalDependencies:
       fsevents: 2.3.2
 
+  /safe-buffer/5.1.2:
+    resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+    dev: false
+
   /safe-buffer/5.2.1:
     resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
     dev: true
@@ -2807,6 +2909,12 @@ packages:
     engines: {node: '>=10.0.0'}
     dev: false
 
+  /string_decoder/1.1.1:
+    resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
+    dependencies:
+      safe-buffer: 5.1.2
+    dev: false
+
   /supports-color/5.5.0:
     resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
     engines: {node: '>=4'}
@@ -2902,10 +3010,25 @@ packages:
     engines: {node: '>=4'}
     dev: false
 
+  /universalify/2.0.1:
+    resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
+    engines: {node: '>= 10.0.0'}
+    dev: false
+
   /unpipe/1.0.0:
     resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
     engines: {node: '>= 0.8'}
 
+  /unzipper/0.12.3:
+    resolution: {integrity: sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==}
+    dependencies:
+      bluebird: 3.7.2
+      duplexer2: 0.1.4
+      fs-extra: 11.3.2
+      graceful-fs: 4.2.11
+      node-int64: 0.4.0
+    dev: false
+
   /update-browserslist-db/1.0.11_browserslist@4.21.10:
     resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==}
     hasBin: true
@@ -2917,6 +3040,10 @@ packages:
       picocolors: 1.0.0
     dev: false
 
+  /util-deprecate/1.0.2:
+    resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+    dev: false
+
   /utils-merge/1.0.1:
     resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
     engines: {node: '>= 0.4.0'}
@@ -3039,6 +3166,11 @@ packages:
       vue: 3.2.47
     dev: false
 
+  /web-streams-polyfill/3.3.3:
+    resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
+    engines: {node: '>= 8'}
+    dev: false
+
   /yallist/3.1.1:
     resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
     dev: false

+ 55 - 0
scripts/fetch-langs.mjs

@@ -0,0 +1,55 @@
+import fetch from "node-fetch";
+import fs from "fs";
+import path from "path";
+import unzipper from "unzipper";
+
+import { pipeline } from "node:stream";
+import { promisify } from "node:util";
+import { createReadStream, createWriteStream } from "node:fs";
+
+const streamPipeline = promisify(pipeline);
+
+/**
+ * 下载并解压离线包
+ * @param {string} url 下载地址
+ * @param {string} destDir 解压目标目录
+ */
+async function downloadAndExtract(url, destDir) {
+  const zipPath = path.join(destDir, "temp.zip");
+
+  console.log(`开始下载语言包: ${url}`);
+
+  const res = await fetch(url, {
+    method: "GET",
+    headers: {
+      //'X-API-Key':'tgpak_gm2f6ntnor2gy4ztnfuw65twoj2wu2tdovzwwztqoe4ts5q'
+    },
+  });
+
+  if (!res.ok) {
+    throw new Error(`下载语言包失败: ${res.status} ${res.statusText}`);
+  }
+
+  // zip文件保存到本地目录
+  await streamPipeline(res.body, createWriteStream(zipPath));
+
+  console.log("语言包下载完成,开始解压...");
+
+  // 解压到指定目录
+  await streamPipeline(
+    createReadStream(zipPath),
+    unzipper.Extract({ path: destDir })
+  );
+
+  console.log("语言包解压完成!");
+
+  // 删除临时压缩包
+  fs.unlinkSync(zipPath);
+}
+
+// 示例调用
+const url =
+  "http://192.168.0.211:9012/v2/projects/export?ak=tgpak_gqyv6y3ngbrtg2dtg5xw65llnazxim3bgnxwwodvmizhcny";
+const dest = "./src/lang/locales";
+
+await downloadAndExtract(url, dest);

+ 4 - 1
src/hook/custom/preset.ts

@@ -1,4 +1,4 @@
-import { Mode, PointInfo, Pos } from "@/sdk/types";
+import { Mode, PointInfo, Pos, Pos3D } from "@/sdk/types";
 import { stackFactory, Stack, fastStacksValue, os } from "@/utils/vue";
 import { Ref, ref } from "vue";
 import { FixPoint } from "@/store/fixPoint";
@@ -130,6 +130,9 @@ export const controlFullStack = stackFactory(ref<boolean>(false));
 export const trackMeasureModeStack = stackFactory(ref<boolean>(false));
 export const activeFixPointStack = stackFactory(ref<FixPoint>());
 export const activeBasePointStack = stackFactory(ref<BasePoint>());
+export const reshootPointStack = stackFactory(
+  ref<{ ing: boolean; pos?: Pos3D }>()
+);
 
 export const customMapStack = {
   [CustomCom.LaserMode]: laserModeStack,

+ 40 - 41
src/lang/index.ts

@@ -1,39 +1,38 @@
-import { App, WritableComputedRef } from 'vue'
-import { createI18n, I18n as BaseI18n } from 'vue-i18n'
-import { deflangName, langNameEum, langNames, langNameDescs } from './constant'
-import { localGetFactory, localSetFactory } from '@/utils/store'
-import { paramsToStr, strToParams } from '@/utils'
-import zh from './zh-entry'
+import { App, WritableComputedRef } from "vue";
+import { createI18n, I18n as BaseI18n } from "vue-i18n";
+import { deflangName, langNameEum, langNames, langNameDescs } from "./constant";
+import { localGetFactory, localSetFactory } from "@/utils/store";
+import { paramsToStr, strToParams } from "@/utils";
+import zh from "./locales/zh.json";
 
 type I18n = BaseI18n & {
   global: {
-    t: I18nGlobalTranslation
-    changeLang(langName: langNameEum, reload?: boolean): void
-    locale: WritableComputedRef<langNameEum>
-  }
-}
-
-const localKey = 'lang'
+    t: I18nGlobalTranslation;
+    changeLang(langName: langNameEum, reload?: boolean): void;
+    locale: WritableComputedRef<langNameEum>;
+  };
+};
+const localKey = "lang";
 const local = {
-  get: localGetFactory(str => {
+  get: localGetFactory((str) => {
     if (str) {
-      return str as langNameEum
+      return str as langNameEum;
     } else {
-      const langs = Object.keys(langNameDescs)
-      const defLang = window?.navigator?.language || 'en'
-      const navLang = langs.find(lang =>
+      const langs = Object.keys(langNameDescs);
+      const defLang = window?.navigator?.language || "en";
+      const navLang = langs.find((lang) =>
         new RegExp(`-?${lang}-?`).test(defLang)
-      )
-      return navLang || langNameEum.en
+      );
+      return navLang || langNameEum.en;
     }
   }),
-  set: localSetFactory((lang: langNameEum) => lang)
-}
+  set: localSetFactory((lang: langNameEum) => lang),
+};
 
-const params = strToParams(location.search)
-export const lang = (params.lang || local.get(localKey)) as langNameEum
+const params = strToParams(location.search);
+export const lang = (params.lang || local.get(localKey)) as langNameEum;
 if (lang !== local.get(localKey)) {
-  local.set(localKey, lang)
+  local.set(localKey, lang);
 }
 
 const i18n: I18n = createI18n({
@@ -44,30 +43,30 @@ const i18n: I18n = createI18n({
   sync: true,
   silentTranslationWarn: true,
   missingWarn: false,
-  silentFallbackWarn: true
-}) as I18n
+  silentFallbackWarn: true,
+}) as I18n;
 
 export const langs = {
   [langNameEum.zh]: zh,
-}
+};
 
-i18n.global.setLocaleMessage(langNameEum.zh, zh)
+i18n.global.setLocaleMessage(langNameEum.zh, zh);
 i18n.global.changeLang = (lang: langNameEum, reload = true) => {
-  i18n.global.locale.value = lang
-  local.set(localKey, lang)
-  params.lang = lang
+  i18n.global.locale.value = lang;
+  local.set(localKey, lang);
+  params.lang = lang;
   if (reload) {
-    location.search = paramsToStr(params)
+    location.search = paramsToStr(params);
   }
   // location.reload()
-}
+};
 
 export const setupI18n = (app: App) => {
-  app.config.globalProperties.$t = i18n.global.t
-  app.use(i18n)
-}
-export const changeLang = i18n.global.changeLang
-export const ui18n = i18n.global
-export const useI18n = () => ui18n
+  app.config.globalProperties.$t = i18n.global.t;
+  app.use(i18n);
+};
+export const changeLang = i18n.global.changeLang;
+export const ui18n = i18n.global;
+export const useI18n = () => ui18n;
 
-export * from './constant'
+export * from "./constant";

+ 587 - 0
src/lang/locales/zh.json

@@ -0,0 +1,587 @@
+{
+  "coord": {
+    "copy": "复制坐标",
+    "copySuccess": "坐标复制成功!",
+    "ctrls": "控制点",
+    "edit": {
+      "diff": "相差",
+      "dms": "度°分′秒″",
+      "getGis": "如何获取地理坐标?",
+      "ggmap": "谷歌地图",
+      "gis": "地理坐标",
+      "gisDataErr": "数据不正确,请检查地理坐标p1,p2格式是否正确,是否数值相同!",
+      "gisUpdateLocalUn": "P {index} 地理坐标已改,本地坐标未改",
+      "gmap": "高德地图",
+      "inputGis": "请输入该坐标系下,控制点的地理坐标",
+      "localDataErr": "数据不正确,请检查本地坐标p1,p2格式是否正确,是否数值相同!",
+      "localPoint": "P{index}本地坐标",
+      "localUpdateGisUn": "P {index} 本地坐标已改,地理坐标未改",
+      "map": "地图",
+      "movePoint": "P {index} 移动到这里",
+      "noRepeat": "地理坐标和本地坐标未重合,请检查是否输入正确。",
+      "noRepeatUpdate": "查看如何调整",
+      "num": "度°",
+      "placeholder": "请输入",
+      "placeholderD": "请输入度°",
+      "placeholderDMS": "请输入度°分′秒″",
+      "pointEqual": "请勿在P1、P2输入相同数值",
+      "setCtrls": "设置控制点",
+      "setPoint": "设为P {index}",
+      "trapLocalPoint": "请在场景中鼠标右键设置控制点的本地坐标",
+      "unsetCtrls": "请先设置控制点",
+      "userUseMouse": "自定义(场景中右键选择)",
+      "whySetCtrls": "为什么要设置控制点?",
+      "whyTrapLocalPoint": "为什么要设置本地坐标?"
+    },
+    "height": "高程",
+    "lat": "纬度",
+    "lng": "经度",
+    "manageTitle": "地理注册",
+    "name": "坐标",
+    "selectType": "选择坐标类型",
+    "title": "坐标系",
+    "types": {
+      "amap": "高德坐标",
+      "gis": "大地坐标",
+      "gmap": "谷歌坐标",
+      "local": "本地坐标",
+      "pro": "投影坐标",
+      "screen": "屏幕坐标",
+      "webMercator": "大地坐标",
+      "wgs84": "wgs84"
+    }
+  },
+  "crop": {
+    "calcBtn": "重算",
+    "calcConfirm": "确定计算?计算可能需要一定时间,请确保裁剪完成后再进行此操作。",
+    "clearConfirm": "确定清空所有裁剪框?该操作无法撤销。",
+    "needToDisConnect": "请选择一个点位,删除它与周围点位的连接",
+    "panoNotAllConnected": "检测到断开的点云,无法计算。",
+    "pointActions": {
+      "clear": "清空",
+      "exclude": "从框内减去",
+      "intersect": "仅保留框内",
+      "move": "移动",
+      "rotate": "旋转",
+      "scale": "缩放"
+    },
+    "reset": "恢复初始状态",
+    "resetConfirm": "恢复初始状态需要重新计算。\n添加的热点、测量、空间模型、裁剪效果、已合并/上传的数据集将被清除,请谨慎操作。",
+    "tip": "裁剪点云前,请查看 ",
+    "tipOper": "操作提示",
+    "title": "裁剪点云"
+  },
+  "dataset": {
+    "all": "全部数据集",
+    "backCalc": "后台计算中…",
+    "backSearch": "< 返回搜索结果",
+    "calc": "计算中",
+    "calibration": {
+      "gotoTip": "请校准数据集,使其在场景中正确拼接。"
+    },
+    "correctTitle": "数据集校准",
+    "deleteJoinDeleteTip": "【{sceneName}】被删除,您添加的数据集【{title}】已同步删除",
+    "deleteTip": "该数据集下的热点、测量结果也将一并删除,此操作不可撤销。",
+    "exists": "已添加",
+    "format": "格式",
+    "initial": "初始数据集",
+    "join": "合并",
+    "joinBtn": "立即合并",
+    "joinSBtn": "在线合并",
+    "joinTip": "选择场景后,其{dataset}将与当前场景合并",
+    "joinTitle": "合并数据集",
+    "manageTitle": "数据集管理",
+    "model": {
+      "addFloorBottom": "在底部添加楼层",
+      "addFloorTop": "在顶部添加楼层",
+      "area": "面积",
+      "construct": "建筑物",
+      "delConstructTip": "删除建筑物将删除其包含所有空间模型,该操作不删除数据集。",
+      "delNoconstructTip": "是否删除空间模型?",
+      "floor": "楼",
+      "height": "层高",
+      "name": "空间名称",
+      "resetTip": "将恢复默认形状,是否确定重置?",
+      "room": "房间",
+      "set": "编辑空间模型",
+      "showTitle": "空间数据",
+      "title": "空间模型",
+      "titleConfirm": "空间名称为空无法保存。",
+      "volume": "体积"
+    },
+    "pointNum": "点位数",
+    "recalcJoinDeleteTip": "【{sceneName}】被重算,您添加的数据集【{title}】已被删除",
+    "refer": "参考数据集",
+    "repeatUpload": "数据正在上传,请稍后再试",
+    "reset": {
+      "tip": "合并/上传的数据集以及其所在位置的热点、测量将会被一并删除。",
+      "title": "确认恢复默认场景?"
+    },
+    "setting": {
+      "lockTip": "该数据集已被控制点锁定",
+      "setName": "编辑数据集",
+      "subtle": "微调",
+      "subtleTip": "请在右侧面板中选中需要微调的数据集",
+      "viewCenter": "视图居中"
+    },
+    "title": "数据集",
+    "unJoinDatasets": "您账号下暂无可添加数据集。",
+    "unsetTitle": "未分配的数据集",
+    "upload": "上传",
+    "uploadCheck": "文件名请勿包含非法字符\" / : ?[<+=;,¥%&*和空格",
+    "uploadIng": "文件上传中",
+    "uploadName": "您上传的",
+    "uploadSBtn": "本地上传",
+    "uploadTitle": "上传的数据集"
+  },
+  "epoint": {
+    "calcConfirm": "确定计算?\n场景将恢复默认。点云场景已添加的热点、测量、空间模型、已合并/上传的数据集将被清除,同时,被裁剪的点云也将恢复初始状态。Obj 场景的模型将被重置。",
+    "calcConfirmKanKan": "确定计算?场景将恢复默认状态,添加的三维模型被清除。",
+    "closeRTK": "关闭 RTK 定位",
+    "closeRTKTip": "将使用当前位置定位",
+    "editTip": "未选中点云时,鼠标左键旋转视图,右键移动视图",
+    "needToDisConnect": "请选择一个点位,删除它与周围点位的连接",
+    "noEnter": "该场景无法进行点位校准,如需继续编辑,请联系客服:400-669-8025",
+    "openRTK": "开启 RTK 定位",
+    "openRTKTip": "将使用 RTK 参数定位",
+    "panoNotAllConnected": "检测到断开的点云,无法计算。",
+    "pointActions": {
+      "connect": "连线",
+      "disconnect": "删除连线",
+      "move": "移动",
+      "reset": "重置",
+      "rotate": "旋转",
+      "scale": "放大"
+    },
+    "resetConfirmKanKan": "重置后场景将恢复至上一次计算完成后的效果,确定重置? 该操作无法撤销。",
+    "title": "点位校准",
+    "un-select": "未选中点云,无法显示全景图"
+  },
+  "err": {
+    "disconnect": "网络错误,请重试",
+    "preset": "内存不足,请勿同时打开多个页面或应用程序,尝试重启浏览器后重新打开。",
+    "scene": {
+      "archive": "场景已封存",
+      "del": "场景已被删除",
+      "err": "场景计算失败,请重试",
+      "run": "场景正在计算中,请稍后...",
+      "un": "场景不存在,请检查场景码",
+      "webgl": "内存不足,请勿同时打开多个页面或应用程序,尝试重启浏览器后重新打开。"
+    },
+    "sdk": "激光场景打开失败,请关闭浏览器后重新打开",
+    "serve": {
+      "desc[0]": "为了让您更好的使用平台资源,我们正在对平台进行升级,升级期间暂时无法访问。",
+      "desc[1]": "给您带来的不便,敬请谅解。",
+      "title": "系统升级中"
+    }
+  },
+  "fire": {
+    "addTip": "点击右键或者 Esc 取消添加",
+    "deleteConfirm": "确定要删除此数据吗?",
+    "effect": {
+      "listTitle": "我添加的",
+      "title": "特效",
+      "types": {
+        "blast": "爆炸",
+        "fire": "火",
+        "smoke": "烟"
+      }
+    },
+    "model": "模型",
+    "title": "消防管理平台",
+    "video": {
+      "title": "导览"
+    }
+  },
+  "help": {
+    "edit": {
+      "coord[0]": "若您的场景有使用 RTK 或相关设备采集控制点,即可获取场景内任意位置的地理坐标,且支持多个坐标系转换。",
+      "coordinate[0]": "此功能将您的场景真实映射在高德地图上,并获取精准的地理坐标。您需要在场景拍摄时使用 RTK 或相关设备获取控制点,并手动录入参数。若您的相机已配置 RTK,系统将自动获取控制点数据无需手动录入。",
+      "data[0]": "右侧列表展示全部数据,进入点云模式后,点击勾选可以隐藏/显示对应点云。",
+      "download[0]": "可下载完整场景,也可对场景进行裁剪后下载。支持下载格式:las. ply. obj。点云格式下载可包含坐标。",
+      "epoint[0]": "当场景拍摄过程中出现点位位置错误时,可以在此模块进行调整。",
+      "floorpan[0]": "算法自动生成场景平面图,支持下载、替换或隐藏。",
+      "hotspot[0]": "右侧列表展示全部数据,进入点云模式后,点击勾选可以隐藏/显示对应点云。",
+      "measure[0]": "点击[开始测量]进行长度、面积测量。",
+      "query[0]": "查看模式可以控制界面显示内容,在右下角切换成点云模式后,可以对点云显示进行设置。",
+      "spaceDivision[0]": "当您的场景导入/上传了多个数据集,需要在此模块进行拼接,以便能够正常浏览场景。",
+      "spaceModel[0]": "此模块可以为您的场景划分区域,区域划分好后,搜索该区域可以定位,或按区域导航。"
+    },
+    "init": "欢迎使用四维深时编辑平台",
+    "link": "用户手册",
+    "mobile": {
+      "step1": {
+        "content": "点击任意方向可移动",
+        "title": "行走"
+      },
+      "step2": {
+        "content": "左右滑动屏幕",
+        "title": "旋转视角"
+      },
+      "step3": {
+        "content": "双指滑动放大或缩小视图",
+        "title": "缩放"
+      },
+      "step4": {
+        "content": "单击按钮切换全景图/点云",
+        "title": "切换全景图/点云"
+      }
+    },
+    "next": "下一步",
+    "prev": "上一步",
+    "query": {
+      "cloud[0]": "按住鼠标左键,可以旋转镜头方向",
+      "cloud[1]": "按住鼠标右键,可以平移视图",
+      "cloud[2]": "单击地面点位,可切换视角在场景中漫游",
+      "cloud[3]": "点击左下角按钮切换全景图/点云",
+      "pano[0]": "按住鼠标左键,可以旋转镜头方向",
+      "pano[1]": "单击地面点位,可切换视角在场景中漫游",
+      "pano[2]": "点击左下角按钮切换全景图/点云"
+    },
+    "tip": "下次可以在此处打开新手指引哦!",
+    "title": "新手指引",
+    "video": {
+      "coordinate": "https://docs.4dkankan.com/#/product/laser/zh-cn/setcontrolpoint",
+      "epoint": "https://docs.4dkankan.com/#/product/laser/zh-cn/calibrationpoint",
+      "kankanEpoint": "https://docs.4dkankan.com/#/product/laser/zh-cn/calibrationpoint",
+      "spaceDivision": "https://docs.4dkankan.com/#/product/laser/zh-cn/splicing",
+      "spaceModel": "https://docs.4dkankan.com/#/product/laser/zh-cn/createfloor"
+    },
+    "videoBtn": "视频教程"
+  },
+  "hotspot": {
+    "addMenu": "添加热点",
+    "addTip": "请在场景中右键点击“添加热点”",
+    "added": "已添加热点",
+    "all": "全部热点",
+    "deleteConfirm": "确定要删除此{type}吗?",
+    "edit": {
+      "addLink": "添加链接",
+      "maxContentLen": "请先清理出位置再添加链接!",
+      "placeholder": {
+        "addLinkContent": "请填写链接地址",
+        "addLinkTitle": "请填写链接文本",
+        "content": "请输入内容",
+        "title": "请输入热点标题"
+      },
+      "unTitle": "存在热点未填写标题"
+    },
+    "flyErr": "距离太远,操作失败。",
+    "meta": {
+      "audio": {
+        "desc": "支持MP3、WAV格式,不超过5MB",
+        "place": "上传音频",
+        "title": "音频"
+      },
+      "image": {
+        "desc": "支持JPG、PNG图片格式,单张不超过5MB,最多支持上传9张。",
+        "place": "上传图片",
+        "title": "图片"
+      },
+      "video": {
+        "desc": "支持MP4、MOV视频格式,码率小于2Mbps,不超过20MB",
+        "place": "上传视频",
+        "title": "视频"
+      },
+      "web": {
+        "place": "网页展示区",
+        "title": "链接"
+      }
+    },
+    "name": "热点",
+    "range": {
+      "max": "最大",
+      "min": "最小",
+      "set": "可视范围设置"
+    },
+    "show": "显示热点"
+  },
+  "measure": {
+    "area": "面积",
+    "copy": "链接复制成功",
+    "downloadName": "测量结果",
+    "export": "导出测量",
+    "exportPDF": "导出PDF",
+    "invalidPoint": "点云为空,换个位置试试",
+    "len": "长度",
+    "name": "测量",
+    "pageMark": "{length}页中的第{index}页",
+    "start": "开始测量",
+    "stop": "停止测量",
+    "titlePlac": "点击添加备注",
+    "toolTip": "取消选中测量工具可以切换点位。",
+    "unSave": "测量结果未保存,无法生成分享链接",
+    "unit": {
+      "inch": "英制(ft)",
+      "meter": "公制(米)"
+    },
+    "wxError": "微信浏览器无法导出pdf"
+  },
+  "resStatus": {
+    "200": "操作成功",
+    "2001": "obj下载失败",
+    "2002": "只能输入数字或字母大小写",
+    "2003": "无点云数据",
+    "2004": "校验成功",
+    "2005": "校验失败",
+    "2006": "文件格式有误, 只接收png图片",
+    "204": "系统正在处理,请勿重复提交",
+    "3001": "对象不存在",
+    "3002": "特殊点表坐标异常",
+    "3006": "fdToken校验失败",
+    "3007": "查无此任务",
+    "3008": "任务失败",
+    "3009": "登录失败,请稍后再试",
+    "3014": "账号或密码不正确。",
+    "3015": "该用户未注册",
+    "3021": "账号不存在,请核对后重新输入。",
+    "3024": "不能将场景协作给自己",
+    "304": "操作失败,您已退出登录",
+    "305": "该账号已在另一台设备登陆,本机账号自动退出。",
+    "3101": "正在计算中",
+    "3102": "场景不存在",
+    "3103": "调用计算失败",
+    "3104": "route不存在",
+    "3105": "控制点计算表结果不存在,尝试重算解决",
+    "400": "参数列表错误(缺少,格式不匹配",
+    "4001": "验证码发送错误",
+    "4002": "检测控制点坐标正在被修改,暂时无法操作",
+    "4003": "请输入正确地理坐标",
+    "4004": "场景校验失败",
+    "4007": "数据集添加失败",
+    "4008": "检测到该数据集正在进行合并,暂时无法操作",
+    "401": "未授权",
+    "402": "访问受限,授权过期",
+    "408": "登录状态过期,已自动退出",
+    "500": "系统内部错误",
+    "502": "上传文件需小于 5GB",
+    "5027": "文件夹不存在",
+    "5028": "无权修改该文件夹",
+    "503": "上传异常",
+    "5030": "同级文件夹名称不能重复",
+    "6000": "不存在数据集",
+    "6001": "转换模型错误,场景查询不到",
+    "6002": "转换模型错误,控制点查询不到",
+    "6003": "转换模型错误,未设置控制点",
+    "6004": "场景大小超出限制,合并下载失败。",
+    "6005": "info.json文件不存在, 请检查",
+    "6006": "上传图片尺寸跟原图不一致",
+    "6007": "上传平面图错误",
+    "6008": "算法生成平面图失败, info.json不存在",
+    "6009": "没有生成平面图文件,请检查平面图算法是否调用成功",
+    "6010": "vision.txt不存在",
+    "6011": "final_freespace文件不存在",
+    "8001": "恢复默认点云失败,没有找到对应点云数据",
+    "8002": "未获取到可编辑点云",
+    "8004": "后台正在处理,请勿重复上传",
+    "8005": "2022年3月以前的场景不支持重算。",
+    "accountErr": "该账号下未检测到当前场景,请更换账号重新登录。",
+    "loginErr": "登录失败,请稍后再试。"
+  },
+  "scene": {
+    "cloud": "点云",
+    "cloudTip": "点击切换为点云模式",
+    "download": {
+      "btn": "立即下载",
+      "cloud": "下载点云",
+      "cropCloud": "裁剪后下载",
+      "format": "下载格式",
+      "formatNotSupport": "格式不支持",
+      "nullCloud": "裁剪框内点云为空,无法下载。"
+    },
+    "floorpan": {
+      "customize": {
+        "steps[0]": "请先下载默认平面图,修改或替换后上传。",
+        "steps[1]": "上传时,需按照原始文件格式上传,不得修改其尺寸大小。",
+        "success": "平面图更新成功",
+        "title": "自定义",
+        "un": "请上传{title}的自定义图"
+      },
+      "default": "系统默认",
+      "title": "平面图",
+      "un": "未上传平面图"
+    },
+    "flyCurrent": "您已在该位置",
+    "flyUnImages": "该位置无全景图,请切换到点云模式再试。",
+    "getPointError": "空白区域无法获取点位,请移动到点云区域再试。",
+    "invalidRight": "空白区域无法使用右键菜单,请移动到点云区域再试。",
+    "nav": "导航",
+    "navEnd": "导航终点",
+    "navErr": "超出数据集范围,无法规划路线",
+    "navPath": "导航路线",
+    "navPlaceholder": "请确认",
+    "navStart": "导航起点",
+    "notice": {
+      "coordResetSuccess": "控制点坐标已被修改,系统即将刷新页面",
+      "handerIng": "场景已被锁定无法编辑,您可以继续浏览场景。",
+      "handerSuccess": "[ {mode} ] 已完成数据处理。",
+      "handerTitle": "后台处理完成",
+      "resetSuccess": "场景已重算,系统即将刷新页面",
+      "updateSuccess": "场景已被修改,系统即将刷新页面。",
+      "updateTitle": "提示"
+    },
+    "objTip": "Mesh 场景",
+    "pano": "全景图",
+    "pose": {
+      "unImage": "全景模式下不允许设置位置",
+      "unSid": "没有找到该panoSid"
+    },
+    "spaceModel": {
+      "defaultFloorTitle": "1楼",
+      "title": "空间模型"
+    }
+  },
+  "sys": {
+    "accountLoginTitle": "用户登录",
+    "add": "添加",
+    "all": "全部",
+    "calc": "计算",
+    "cancel": "取消",
+    "close": "关闭",
+    "codePlace": "请输入验证码",
+    "compatible": {
+      "chrome": "Chrome",
+      "edg": "Microsoft Edge",
+      "ff": "火狐",
+      "safari": "Safari",
+      "selectTip": "建议使用以下浏览器",
+      "tip": "无法打开页面,请升级或更换浏览器后重新打开"
+    },
+    "crop": "裁剪",
+    "delete": "删除",
+    "dialogTitle": "提示",
+    "download": "下载",
+    "downloadSuccess": "下载成功",
+    "edit": "编辑",
+    "enter": "确定",
+    "forceLeaveConfirm": "您有操作未保存,确定要退出吗?",
+    "forgetPwd": "忘记密码",
+    "getCode": "获取验证码",
+    "haveAccountLogin": "使用已有帐户登录",
+    "help": "帮助中心",
+    "hide": "隐藏",
+    "ignore": "忽略",
+    "inputPlc": "请输入",
+    "inputScenePwd": "输入场景密码",
+    "leave": "退出",
+    "login": "登录",
+    "logout": "退出",
+    "logoutConfirm": "确定要退出登录吗?",
+    "markPwd": "记住密码",
+    "ok": "我知道了",
+    "open": "开启",
+    "phonePlace": "请输入手机号码",
+    "pwdErr": "密码错误",
+    "pwdPlace": "请输入密码",
+    "pwdReg": "密码需要包含英文大小写、数字、长度8-16字符",
+    "qrLoginTitle": "相机登录",
+    "qrPlace": "打开四维看看app扫一扫登录",
+    "query": "查看",
+    "refer": "刷新",
+    "repeatLogin": {
+      "btn": "继续登录",
+      "content": "选择【继续登录】,另一台设备将退出登录,其操作不会被保存。",
+      "title": "检测到另一台设备已登录此账号,是否继续?"
+    },
+    "repeatPwdDiff": "两次输入的密码不一致",
+    "reset": "重置",
+    "resetCodeTime": "{count}s后重新发送",
+    "resetConfirm": "确定重置?该操作无法撤销。",
+    "retrievePwd": "找回密码",
+    "save": "保存",
+    "search": "搜索建筑物\\房间\\热点...",
+    "searchAll": "搜索",
+    "selectPic": "请选择",
+    "setPwdPlace": "设置密码",
+    "setRepeatPwdPlace": "重复密码",
+    "setting": {
+      "public": "公开",
+      "pwd": "加密",
+      "setName": "修改名称",
+      "setNameErr": "场景名称不能为空!",
+      "setNamePlace": "请输入标题",
+      "setOpen": "浏览设置",
+      "setOpenErr": "请输入加密密码!",
+      "setPic": "设置初始画面",
+      "setView": "可视设置"
+    },
+    "setup": "设置",
+    "show": "显示",
+    "submit": "提交",
+    "time": {
+      "about": "约",
+      "h": "小时",
+      "m": "分钟"
+    },
+    "title": "激光",
+    "unData": "暂无数据",
+    "unPhonePlace": "手机号码不能为空",
+    "unPwdPlace": "密码不能为空",
+    "unRepeatPwd": "确认密码不能为空",
+    "unSearchData": "未搜索到结果",
+    "unUpdate": "没有修改",
+    "unset": "未设置",
+    "update": "修改",
+    "updatePwdSuccess": "密码修改成功",
+    "upload": "上传",
+    "uploadAddText": "继续添加",
+    "uploadErr": {
+      "accept": "仅支持{accept}格式文件",
+      "len": "最多仅支持{len}个文件!",
+      "scale": "{name}的比例不为{scale}",
+      "size": "{name}的大小超过{size}"
+    },
+    "uploadReplaceText": "替换"
+  },
+  "tool": {
+    "area": "多边形",
+    "free": "自由",
+    "lfree": "水平",
+    "magnify": "放大镜",
+    "move": "移动",
+    "rect": "矩形",
+    "rotate": "旋转",
+    "series": "连续直线",
+    "vfree": "垂直"
+  },
+  "view": {
+    "cloudSeting": "点云设置",
+    "clound": "漫游视图",
+    "colorMode": {
+      "altitude": "海拔",
+      "full": "彩色",
+      "name": "色彩模式",
+      "translucent": "半透明"
+    },
+    "density": {
+      "high": "高",
+      "low": "低",
+      "middle": "中",
+      "name": "点云质量"
+    },
+    "detail": "细节",
+    "moreSetting": "高级设置",
+    "opacity": "不透明度",
+    "range": "范围",
+    "reset": "恢复默认",
+    "scene": "3D",
+    "seting": "视图设置",
+    "shape": {
+      "circular": "圆形",
+      "name": "点的形状",
+      "rectangle": "矩形"
+    },
+    "showFloorpan": "显示平面图",
+    "showMap": "显示高德地图",
+    "showMini": "显示迷你视角",
+    "showPano": "显示漫游点位",
+    "side": "侧视图",
+    "sideLeft": "侧视图(N-S)",
+    "sideRight": "侧视图(E-W)",
+    "size": "点的大小",
+    "strong": "强化边缘",
+    "switchMiniView": "{action}迷你视角",
+    "switchView": "切换视图",
+    "top": "顶视图"
+  }
+}

+ 2 - 0
src/sdk/types/sdk.ts

@@ -550,6 +550,8 @@ export type LaserSDK = {
   // 创建导航对象
   createNavigation: () => Navigation;
 
+  getCurrentPano: () => any;
+
   // 坐标转换
   coordTransform: <T extends CoordType, K extends CoordType>(
     originType: T,

+ 33 - 1
src/store/sync.ts

@@ -6,7 +6,13 @@ import { fixPoints } from "@/store/fixPoint";
 import { photos } from "@/store/photos";
 import { accidentPhotos } from "@/store/accidentPhotos";
 import { roadPhotos } from "@/store/roadPhotos";
-import { base64ToBlob, blobToBase64, debounce, getId } from "@/utils";
+import {
+  asyncTimeout,
+  base64ToBlob,
+  blobToBase64,
+  debounce,
+  getId,
+} from "@/utils";
 import { watch } from "vue";
 import { genUseLoading, params } from "@/hook";
 import router, { writeRouteName } from "@/router";
@@ -16,6 +22,7 @@ import { imageRotate } from "@/utils/image-rotate";
 import { sceneSeting } from "./sceneSeting";
 import { tables } from "./tables";
 import { drawSetting } from "./drawSetting";
+import { Pos3D } from "@/sdk";
 
 const global = window as any;
 
@@ -125,8 +132,31 @@ export const api = !global.android
       async closePage() {
         return router.push({ name: writeRouteName.scene });
       },
+      async reshoot(pos: Pos3D) {
+        await asyncTimeout(5000);
+        return "";
+      },
+      async delPano() {},
     }
   : {
+      delPano(data: any) {
+        return new Promise((resolve) => {
+          global.delPanoCallback = (data) => {
+            delete global.delPanoCallback;
+            resolve(data);
+          };
+          global.android.reshoot(JSON.stringify(data), "delPanoCallback");
+        });
+      },
+      reshoot(pos: Pos3D) {
+        return new Promise((resolve) => {
+          global.reshootCallback = (data) => {
+            delete global.reshootCallback;
+            resolve(data);
+          };
+          global.android.reshoot(JSON.stringify(pos), "reshootCallback");
+        });
+      },
       shareImage(filename: string) {
         return new Promise((resolve) => {
           global.shareImageCallback = (data) => {
@@ -297,6 +327,8 @@ export const uploadImage = (blob: Blob, name = `${getId()}.jpg`) => {
   return api.uploadImage(file);
 };
 
+export const reshoot = (pos: Pos3D) => api.reshoot(pos);
+
 export const downloadImage = async (
   data: Blob | string,
   name = `${getId()}.jpg`

+ 19 - 17
src/views/scene/covers/fixPoint.vue

@@ -27,6 +27,7 @@ import { ref, watchEffect } from "vue";
 const props = defineProps<{
   data: FixPoint;
   active: boolean;
+  fd?: boolean;
 }>();
 const emit = defineEmits<{
   (m: "changePos", pos: Pos3D): void;
@@ -38,25 +39,26 @@ const emit = defineEmits<{
 
 const isPure = !("type" in props.data) || props.data.type === FixType.POINT;
 const focus3d = ref(false);
+if (!props.fd) {
+  setTimeout(() => {
+    const fix3d = getFix3d(props.data);
 
-setTimeout(() => {
-  const fix3d = getFix3d(props.data);
+    if (fix3d && (!isPure || props.data.measure)) {
+      fix3d.bus.on("selectMeasure", (select) => {
+        console.error("selectMeasure", select);
+        select ? emit("focusMeasure") : emit("blurMeasure");
+      });
+      fix3d.bus.on("selectGraph", (select) => {
+        console.error("selectGraph", select);
+        select ? emit("focus", true) : emit("blur", true);
+      });
 
-  if (fix3d && (!isPure || props.data.measure)) {
-    fix3d.bus.on("selectMeasure", (select) => {
-      console.error("selectMeasure", select);
-      select ? emit("focusMeasure") : emit("blurMeasure");
-    });
-    fix3d.bus.on("selectGraph", (select) => {
-      console.error("selectGraph", select);
-      select ? emit("focus", true) : emit("blur", true);
-    });
-
-    // fix3d.bus.on("selectGraph", (select) => {
-    //   focus3d.value = select;
-    // });
-  }
-});
+      // fix3d.bus.on("selectGraph", (select) => {
+      //   focus3d.value = select;
+      // });
+    }
+  });
+}
 </script>
 
 <style scoped lang="scss">

+ 53 - 0
src/views/scene/covers/reshoot.vue

@@ -0,0 +1,53 @@
+<template>
+  <FixPoint
+    :data="data"
+    v-if="data"
+    :active="true"
+    fd
+    @changePos="(pos) => (reshootPointStack.current.value.value.pos = pos)"
+  />
+  <Confirm
+    v-if="reshootPointStack.current.value.value.ing"
+    :ok="okHandler"
+    :cancel="cancelHandler"
+  />
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue';
+import FixPoint from './fixPoint.vue';
+import { customMap, genUseLoading, reshootPointStack } from '@/hook';
+import { type FixPoint as FixPointT } from '@/store/fixPoint';
+import Confirm from '@/views/graphic/confirm.vue';
+import { reshoot } from '@/store/sync';
+import Message from "@/components/base/components/message/message.vue";
+import { Mode } from '@/sdk';
+
+
+const point = computed(() => reshootPointStack.current.value.value)
+
+const data = computed(() => {
+  if (!point.value?.pos) return;
+  const data: FixPointT = {
+    id: 'reshootPoint',
+    text: '补拍点',
+    pos: point.value.pos,
+    measure: false,
+    lines: []
+  }
+  return data
+})
+
+const okHandler = genUseLoading(async () => {
+  if (!reshootPointStack.current.value.value.pos) {
+    Message.error({ msg: "请选择补拍点位置", time: 2000 });
+  }
+  const data = await reshoot(reshootPointStack.current.value.value.pos)
+  cancelHandler()
+})
+
+const cancelHandler = () => {
+  reshootPointStack.current.value.value = undefined
+  customMap.mode = Mode.pano;
+}
+</script>

+ 5 - 1
src/views/scene/index.vue

@@ -25,7 +25,7 @@
                 class="back fun-ctrl"
                 :size="viewStatus ? 64 : 48"
                 @click="onScale"
-                v-if="!childPage"
+                v-if="!childPage && !reshootPointStack.current.value.value"
               >
                 <ui-icon :type="viewStatus ? 'screen_c' : 'screen_f'" class="icon" />
               </ButtonPane>
@@ -109,6 +109,7 @@ import ButtonPane from "@/components/button-pane/index.vue";
 import { types, accidentPhotos } from "@/store/accidentPhotos";
 import { debounce, getQueryString } from "@/utils";
 import { tables } from "@/store/tables";
+import { reshootPointStack } from "@/hook";
 
 const layoutRef = ref(null);
 const accodentSortPhotos = computed(() => {
@@ -134,6 +135,9 @@ const viewStatus = ref(false);
 const onScale = () => {
   viewStatus.value = !viewStatus.value;
 };
+if (import.meta.env.DEV) {
+  viewStatus.value = true;
+}
 
 const list = ref([
   {

+ 60 - 2
src/views/scene/menus/actions.ts

@@ -12,16 +12,18 @@ import {
 import { list, MeasureAtom, MeasureType } from "@/store/measure";
 import { baseLines } from "@/store/baseLine";
 import { basePoints } from "@/store/basePoint";
-import { nextTick, reactive, ref, Ref, watch } from "vue";
+import { nextTick, reactive, ref, Ref, watch, watchEffect } from "vue";
 import {
   activeBasePointStack,
   activeFixPointStack,
   customMap,
+  genUseLoading,
+  reshootPointStack,
   useConfirm,
   useSDK,
 } from "@/hook";
 import { getCoverPos } from "../linkage/cover";
-import { Pos3D, TypeEmu } from "@/sdk";
+import { Mode, Pos3D, TypeEmu } from "@/sdk";
 import { FixPoint, fixPoints, FixType } from "@/store/fixPoint";
 import Message from "@/components/base/components/message/message.vue";
 import { getId } from "@/utils";
@@ -35,6 +37,7 @@ import {
   trackBaseIng,
 } from "../fixManage";
 import { disableCover } from "../sceneStatus";
+import { api } from "@/store/sync";
 
 const trackPosMenuAction = (
   onComplete: () => void,
@@ -335,6 +338,61 @@ const menuActions = {
     }
     onComplete();
   },
+
+  [menuEnum.ADD_RESHOOT]: (_, onComplete) => {
+    reshootPointStack.current.value.value = { ing: true };
+    watchEffect(() => {
+      if (
+        !reshootPointStack.current.value.value ||
+        reshootPointStack.current.value.value.pos
+      ) {
+        onComplete();
+      }
+    });
+    customMap.mode = Mode.cloud;
+    const add = () => {
+      hide = Message.success({ msg: "请单击选择补拍点位置" });
+      disableCover.value = true;
+      onDestroy = trackPosMenuAction(
+        () => {
+          hide && hide();
+          onComplete();
+          trackBaseIng.value = false;
+          disableCover.value = false;
+        },
+        (pos) => {
+          reshootPointStack.current.value.value!.pos = pos;
+
+          if (hide) {
+            hide();
+            hide = null;
+          }
+          trackBaseIng.value = false;
+          disableCover.value = false;
+        },
+        false
+      );
+    };
+
+    let onDestroy;
+    let hide;
+    let stop;
+    add();
+    return () => {
+      onDestroy && onDestroy();
+      hide && hide();
+      stop && stop();
+      disableCover.value = false;
+      trackBaseIng.value = false;
+    };
+  },
+  [menuEnum.DEL_RESHOOT]: genUseLoading(async () => {
+    const pano = useSDK().getCurrentPano();
+    if (!pano) {
+      return Message.success({ msg: "当前位置不存在全景图", time: 2000 });
+    }
+    await api.delPano(pano);
+  }),
 };
 
 export const joinActions = (activeKey: Ref<string>) => {

+ 19 - 2
src/views/scene/menus/menus.ts

@@ -2,7 +2,7 @@ import {
   findMenuByAttr,
   generateMixMenus as generateMixMenusRaw,
 } from "@/utils/menus";
-import { Ref, computed, nextTick, ref } from "vue";
+import { Ref, computed, nextTick, reactive, ref } from "vue";
 import { useAsyncSDK, useSDK } from "@/hook";
 import { laserModeStack, modeDisabled } from "@/hook/custom/index";
 import { Mode } from "@/sdk";
@@ -44,6 +44,8 @@ export enum menuEnum {
   FIX_MEASURE = "fixMeasure",
   BASE_LINE = "baseLine",
   BASE_POINT = "basePoint",
+  ADD_RESHOOT = "add-reshoot",
+  DEL_RESHOOT = "del-reshoot",
 }
 
 const vView = ref(false);
@@ -53,7 +55,7 @@ useAsyncSDK().then((sdk) => {
   });
 });
 
-export const menus: MenuRaw[] = [
+export const pubMenus: MenuRaw[] = [
   // {
   //   icon: "save",
   //   text: "保存",
@@ -199,6 +201,21 @@ export const menus: MenuRaw[] = [
   },
 ];
 
+export const reshootMenus: MenuRaw[] = [
+  {
+    icon: "line_d",
+    text: "全景",
+    key: menuEnum.ADD_RESHOOT,
+  },
+  {
+    icon: "del",
+    text: "删除",
+    key: menuEnum.DEL_RESHOOT,
+  },
+];
+
+export const menus: MenuRaw[] = reactive([...pubMenus]);
+
 export const generateMixMenus = <T extends {}, K extends keyof MenuRaw>(
   childKey: K,
   generateFn: (men: MenuRaw) => T,

+ 20 - 9
src/views/scene/mode.vue

@@ -12,19 +12,25 @@
 <script lang="ts" setup>
 import GroupButton from "@/components/group-button/index.vue";
 import { Mode } from "@/sdk";
-import { computed, ref, watch, watchEffect } from "vue";
+import { computed, reactive, ref, watch, watchEffect } from "vue";
 import { customMap, disabledMap } from "@/hook/custom/index";
 import { params } from "@/hook";
 
 defineProps<{ size: number }>();
+const emit = defineEmits<{ (e: "reshoot", bol: boolean): void }>();
 
-const tabs = [
+const tabs = reactive([
+  {
+    mode: Mode.pano,
+    icon: "panorama_f",
+    activeIcon: "panorama_t",
+  },
   {
     mode: Mode.cloud,
     icon: "point_c_f",
     activeIcon: "point_c_t",
   },
-];
+]);
 
 const key = params.m + "model";
 
@@ -32,16 +38,21 @@ const menus = computed(() =>
   tabs.map((tab) => ({
     icon: tab.mode === customMap.mode ? tab.activeIcon : tab.icon,
     key: tab.mode,
-    onClick: () => (customMap.mode = tab.mode === Mode.pano ? Mode.cloud : Mode.pano),
+    onClick: () => {
+      if (tab.mode === Mode.pano) {
+        customMap.mode = Mode.cloud;
+      } else {
+        customMap.mode = Mode.pano;
+      }
+      if (params.temp) {
+        console.log("reshoot", customMap.mode === Mode.pano);
+        emit("reshoot", customMap.mode === Mode.pano);
+      }
+    },
   }))
 );
 
 if (!params.temp) {
-  tabs.unshift({
-    mode: Mode.pano,
-    icon: "panorama_f",
-    activeIcon: "panorama_t",
-  });
   customMap.mode = Number(localStorage.getItem(key)) || Mode.pano;
 } else {
   customMap.mode = Number(localStorage.getItem(key)) || Mode.cloud;

+ 35 - 11
src/views/scene/scene.vue

@@ -1,22 +1,25 @@
 <template>
   <Container @loaded="loaded = true" :viewStatus="viewStatus" :class="{ disableCover }" />
   <template v-if="loaded && !trackMode">
-    <Menus
-      v-if="viewStatus"
-      @active="(data) => (activeMenuKeys = data)"
-      @enter-child="childPage = true"
-      @leave-child="childPage = false"
-    />
+    <div :class="{ dismenus: reshootPointStack.current.value.value }">
+      <Menus
+        v-if="viewStatus && showMenu"
+        @active="(data) => (activeMenuKeys = data)"
+        @enter-child="childPage = true"
+        @leave-child="childPage = false"
+      />
+      <Photo :size="viewStatus ? 88 : 64" />
+      <Mode :size="viewStatus ? 64 : 48" @reshoot="reshootHandler" />
+    </div>
+    <Reshoot v-if="reshootPointStack.current.value.value" />
     <BasePoints />
     <FixPoints />
     <Measures />
-    <Photo :size="viewStatus ? 88 : 64" />
     <Range
       v-if="activeMenuKeys[0] === 'range'"
       :rangeKey="activeMenuKeys.slice(1).join(':')"
     />
     <slot :childPage="childPage" />
-    <Mode :size="viewStatus ? 64 : 48" />
   </template>
 </template>
 
@@ -25,16 +28,18 @@ import Container from "./container.vue";
 import Mode from "./mode.vue";
 import Menus from "./menus/pane.vue";
 import BasePoints from "@/views/scene/covers/basePoints.vue";
+import Reshoot from "@/views/scene/covers/reshoot.vue";
 import FixPoints from "@/views/scene/covers/fixPoints.vue";
 import Measures from "@/views/scene/covers/measures.vue";
 import Photo from "./photo.vue";
 import Range from "./covers/range.vue";
 import { disabledMap, useSDK } from "@/hook";
-import customSetup from "../../hook/custom";
-import { ref, watchEffect } from "vue";
+import customSetup, { reshootPointStack } from "../../hook/custom";
+import { nextTick, ref, watchEffect } from "vue";
 import { trackMode } from "@/views/scene/trackMeasureWidth";
-import { showMenus } from "./menus/menus";
+import { menus, pubMenus, reshootMenus, showMenus } from "./menus/menus";
 import { disableCover } from "./sceneStatus";
+import { tempMeasures } from "@/store/measure";
 
 withDefaults(defineProps<{ viewStatus: boolean }>(), { viewStatus: true });
 defineEmits<{ (e: "update:viewStatus", v: boolean): void }>();
@@ -56,4 +61,23 @@ if (import.meta.env.VITE_APP_SDK === "true") {
     });
   });
 }
+
+const showMenu = ref(true);
+const reshootHandler = (bol: boolean) => {
+  menus.length = 0;
+  if (bol) {
+    menus.push(...reshootMenus);
+  } else {
+    menus.push(...pubMenus);
+  }
+  showMenu.value = false;
+  nextTick(() => (showMenu.value = true));
+};
 </script>
+
+<style lang="scss" scoped>
+.dismenus {
+  visibility: hidden;
+  pointer-events: none;
+}
+</style>