Procházet zdrojové kódy

feat: 文案国际化

bill před 6 měsíci
rodič
revize
250adc6b4b
78 změnil soubory, kde provedl 1129 přidání a 476 odebrání
  1. 2 0
      package.json
  2. 113 0
      pnpm-lock.yaml
  3. 2 5
      src/api/constant.ts
  4. 4 3
      src/api/floder.ts
  5. 2 1
      src/api/folder-type.ts
  6. 3 2
      src/api/instance.ts
  7. 2 11
      src/api/scene.ts
  8. 2 1
      src/api/setup.ts
  9. 9 8
      src/api/tagging-style.ts
  10. 35 36
      src/components/bill-ui/components/cropper/cropper.vue
  11. 4 2
      src/components/bill-ui/components/cropper/index.js
  12. 40 38
      src/components/bill-ui/components/dialog/Alert.vue
  13. 54 53
      src/components/bill-ui/components/dialog/Confirm.vue
  14. 25 11
      src/components/bill-ui/components/input/file.vue
  15. 8 6
      src/components/bill-ui/components/input/state.js
  16. 13 14
      src/components/error/index.vue
  17. 21 19
      src/components/materials/index.vue
  18. 5 7
      src/components/tagging/sign-new.vue
  19. 5 5
      src/components/tagging/sign.vue
  20. 9 0
      src/lang/en.ts
  21. 91 0
      src/lang/index.ts
  22. 315 0
      src/lang/zh.ts
  23. 1 1
      src/layout/edit/fuse-edit.vue
  24. 2 2
      src/layout/edit/header/index.vue
  25. 14 21
      src/layout/edit/scene-select.vue
  26. 4 3
      src/layout/model-list/index.vue
  27. 3 3
      src/layout/model-list/sign.vue
  28. 7 6
      src/layout/pwd.vue
  29. 3 3
      src/layout/scene-list/index.vue
  30. 2 0
      src/main.ts
  31. 0 1
      src/model/app.vue
  32. 3 2
      src/model/index.ts
  33. 25 22
      src/router/constant.ts
  34. 0 1
      src/sdk/association/index.ts
  35. 2 1
      src/sdk/association/path.ts
  36. 2 1
      src/store/guide.ts
  37. 5 4
      src/store/measure.ts
  38. 3 2
      src/store/path.ts
  39. 3 2
      src/store/record.ts
  40. 3 2
      src/store/scene.ts
  41. 3 2
      src/store/sys.ts
  42. 3 2
      src/store/view.ts
  43. 89 0
      src/utils/store.ts
  44. 1 1
      src/views/error/index.vue
  45. 12 20
      src/views/folder/fire/index.vue
  46. 1 1
      src/views/folder/index.vue
  47. 8 7
      src/views/guide/guide/edit-paths.vue
  48. 1 1
      src/views/guide/guide/edit.vue
  49. 6 5
      src/views/guide/guide/sign.vue
  50. 5 4
      src/views/guide/index.vue
  51. 14 18
      src/views/guide/path/edit-path.vue
  52. 3 2
      src/views/guide/path/edit.vue
  53. 3 2
      src/views/guide/path/sign.vue
  54. 4 3
      src/views/guide/show.vue
  55. 1 1
      src/views/measure/index.vue
  56. 1 1
      src/views/measure/show.vue
  57. 4 3
      src/views/measure/sign.vue
  58. 9 8
      src/views/merge/index.vue
  59. 4 3
      src/views/proportion/index.vue
  60. 6 5
      src/views/record/index.vue
  61. 6 9
      src/views/record/shot.vue
  62. 7 6
      src/views/record/sign.vue
  63. 5 5
      src/views/registration/index.vue
  64. 10 9
      src/views/setting/index.vue
  65. 2 1
      src/views/sign-model/index.vue
  66. 4 3
      src/views/summary/index.vue
  67. 6 5
      src/views/tagging-position/index.vue
  68. 1 1
      src/views/tagging-position/sign-item.vue
  69. 10 10
      src/views/tagging-position/sign.vue
  70. 21 20
      src/views/tagging/edit.vue
  71. 3 3
      src/views/tagging/index.vue
  72. 1 1
      src/views/tagging/show.vue
  73. 5 4
      src/views/tagging/sign.vue
  74. 2 1
      src/views/tagging/style-type-select.vue
  75. 0 4
      src/views/tagging/styles.vue
  76. 2 2
      src/views/view/index.vue
  77. 4 3
      src/views/view/sign.vue
  78. 16 1
      src/vite-env.d.ts

+ 2 - 0
package.json

@@ -14,6 +14,7 @@
     "ant-design-vue": "^4.2.6",
     "axios": "^0.27.2",
     "coordtransform": "^2.1.2",
+    "i18n": "^0.15.1",
     "less": "^4.1.3",
     "mitt": "^3.0.0",
     "simaqcore": "^1.2.0",
@@ -21,6 +22,7 @@
     "vite-plugin-mkcert": "^1.10.1",
     "vue": "3.2.47",
     "vue-cropper": "1.0.2",
+    "vue-i18n": "^11.1.1",
     "vue-router": "^4.1.3",
     "vuedraggable": "^4.1.0"
   },

+ 113 - 0
pnpm-lock.yaml

@@ -7,6 +7,7 @@ specifiers:
   ant-design-vue: ^4.2.6
   axios: ^0.27.2
   coordtransform: ^2.1.2
+  i18n: ^0.15.1
   less: ^4.1.3
   mitt: ^3.0.0
   sass: ^1.54.3
@@ -17,6 +18,7 @@ specifiers:
   vite-plugin-mkcert: ^1.10.1
   vue: 3.2.47
   vue-cropper: 1.0.2
+  vue-i18n: ^11.1.1
   vue-router: ^4.1.3
   vue-tsc: ^0.38.4
   vuedraggable: ^4.1.0
@@ -26,6 +28,7 @@ dependencies:
   ant-design-vue: 4.2.6_vue@3.2.47
   axios: 0.27.2
   coordtransform: 2.1.2
+  i18n: 0.15.1
   less: 4.1.3
   mitt: 3.0.0
   simaqcore: 1.2.0
@@ -33,6 +36,7 @@ dependencies:
   vite-plugin-mkcert: 1.10.1_vite@3.0.4
   vue: 3.2.47
   vue-cropper: 1.0.2
+  vue-i18n: 11.1.1_vue@3.2.47
   vue-router: 4.1.3_vue@3.2.47
   vuedraggable: 4.1.0_vue@3.2.47
 
@@ -116,10 +120,62 @@ packages:
     requiresBuild: true
     optional: true
 
+  /@intlify/core-base/11.1.1:
+    resolution: {integrity: sha512-bb8gZvoeKExCI2r/NVCK9E4YyOkvYGaSCPxVZe8T0jz8aX+dHEOZWxK06Z/Y9mWRkJfBiCH4aOhDF1yr1t5J8Q==}
+    engines: {node: '>= 16'}
+    dependencies:
+      '@intlify/message-compiler': 11.1.1
+      '@intlify/shared': 11.1.1
+    dev: false
+
+  /@intlify/message-compiler/11.1.1:
+    resolution: {integrity: sha512-4iEsUZ3aF7jXY19CJFN5VP+pPyLITD9FVsjB13z9TU1UxaZLlFsmNhvRxlPDSOfHAP5RpNF2QKKdZ3DHVf4Yzw==}
+    engines: {node: '>= 16'}
+    dependencies:
+      '@intlify/shared': 11.1.1
+      source-map-js: 1.2.1
+    dev: false
+
+  /@intlify/shared/11.1.1:
+    resolution: {integrity: sha512-2kGiWoXaeV8HZlhU/Nml12oTbhv7j2ufsJ5vQaa0VTjzUmZVdd/nmKFRAOJ/FtjO90Qba5AnZDwsrY7ZND5udA==}
+    engines: {node: '>= 16'}
+    dev: false
+
   /@jridgewell/sourcemap-codec/1.5.0:
     resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
     dev: true
 
+  /@messageformat/core/3.4.0:
+    resolution: {integrity: sha512-NgCFubFFIdMWJGN5WuQhHCNmzk7QgiVfrViFxcS99j7F5dDS5EP6raR54I+2ydhe4+5/XTn/YIEppFaqqVWHsw==}
+    dependencies:
+      '@messageformat/date-skeleton': 1.1.0
+      '@messageformat/number-skeleton': 1.2.0
+      '@messageformat/parser': 5.1.1
+      '@messageformat/runtime': 3.0.1
+      make-plural: 7.4.0
+      safe-identifier: 0.4.2
+    dev: false
+
+  /@messageformat/date-skeleton/1.1.0:
+    resolution: {integrity: sha512-rmGAfB1tIPER+gh3p/RgA+PVeRE/gxuQ2w4snFWPF5xtb5mbWR7Cbw7wCOftcUypbD6HVoxrVdyyghPm3WzP5A==}
+    dev: false
+
+  /@messageformat/number-skeleton/1.2.0:
+    resolution: {integrity: sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg==}
+    dev: false
+
+  /@messageformat/parser/5.1.1:
+    resolution: {integrity: sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==}
+    dependencies:
+      moo: 0.5.2
+    dev: false
+
+  /@messageformat/runtime/3.0.1:
+    resolution: {integrity: sha512-6RU5ol2lDtO8bD9Yxe6CZkl0DArdv0qkuoZC+ZwowU+cdRlVE1157wjCmlA5Rsf1Xc/brACnsZa5PZpEDfTFFg==}
+    dependencies:
+      make-plural: 7.4.0
+    dev: false
+
   /@octokit/auth-token/3.0.2:
     resolution: {integrity: sha512-pq7CwIMV1kmzkFTimdwjAINCXKTajZErLB4wMLYapR2nuB/Jpr66+05wOTZMSCBXP6n4DdDWT2W19Bm17vU69Q==}
     engines: {node: '>= 14'}
@@ -364,6 +420,10 @@ packages:
     resolution: {integrity: sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ==}
     dev: false
 
+  /@vue/devtools-api/6.6.4:
+    resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
+    dev: false
+
   /@vue/reactivity-transform/3.2.47:
     resolution: {integrity: sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==}
     dependencies:
@@ -796,6 +856,11 @@ packages:
     resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
     dev: false
 
+  /fast-printf/1.6.10:
+    resolution: {integrity: sha512-GwTgG9O4FVIdShhbVF3JxOgSBY2+ePGsu2V/UONgoCPzF9VY6ZdBMKsHKCYQHZwNk3qNouUolRDsgVxcVA5G1w==}
+    engines: {node: '>=10.0'}
+    dev: false
+
   /fill-range/7.0.1:
     resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
     engines: {node: '>=8'}
@@ -850,6 +915,20 @@ packages:
     dependencies:
       function-bind: 1.1.1
 
+  /i18n/0.15.1:
+    resolution: {integrity: sha512-yue187t8MqUPMHdKjiZGrX+L+xcUsDClGO0Cz4loaKUOK9WrGw5pgan4bv130utOwX7fHE9w2iUeHFalVQWkXA==}
+    engines: {node: '>=10'}
+    dependencies:
+      '@messageformat/core': 3.4.0
+      debug: 4.3.4
+      fast-printf: 1.6.10
+      make-plural: 7.4.0
+      math-interval-parser: 2.0.1
+      mustache: 4.2.0
+    transitivePeerDependencies:
+      - supports-color
+    dev: false
+
   /iconv-lite/0.6.3:
     resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
     engines: {node: '>=0.10.0'}
@@ -963,6 +1042,15 @@ packages:
       semver: 5.7.1
     optional: true
 
+  /make-plural/7.4.0:
+    resolution: {integrity: sha512-4/gC9KVNTV6pvYg2gFeQYTW3mWaoJt7WZE5vrp1KnQDgW92JtYZnzmZT81oj/dUTqAIu0ufI2x3dkgu3bB1tYg==}
+    dev: false
+
+  /math-interval-parser/2.0.1:
+    resolution: {integrity: sha512-VmlAmb0UJwlvMyx8iPhXUDnVW1F9IrGEd9CIOmv+XL8AErCUUuozoDMrgImvnYt2A+53qVX/tPW6YJurMKYsvA==}
+    engines: {node: '>=0.10.0'}
+    dev: false
+
   /mime-db/1.52.0:
     resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
     engines: {node: '>= 0.6'}
@@ -986,6 +1074,10 @@ packages:
     resolution: {integrity: sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==}
     dev: false
 
+  /moo/0.5.2:
+    resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==}
+    dev: false
+
   /ms/2.1.2:
     resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
     dev: false
@@ -994,6 +1086,11 @@ packages:
     resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
     optional: true
 
+  /mustache/4.2.0:
+    resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
+    hasBin: true
+    dev: false
+
   /nanoid/3.3.8:
     resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
     engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -1116,6 +1213,10 @@ packages:
       tslib: 2.4.0
     dev: false
 
+  /safe-identifier/0.4.2:
+    resolution: {integrity: sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==}
+    dev: false
+
   /safer-buffer/2.1.2:
     resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
     optional: true
@@ -1264,6 +1365,18 @@ packages:
     resolution: {integrity: sha512-ZD1kl8OMMrDXJOS1ZRdnohh2BFfYjTeq+r7+yAahydQvrVOKbzXixx6f8LCoMjB+AgAf5BXnlWsZxmu964wJYA==}
     dev: false
 
+  /vue-i18n/11.1.1_vue@3.2.47:
+    resolution: {integrity: sha512-0P6DkKy96R4Wh2sIZJEHw8ivnlD1pnB6Ib/eldoF1SUpQutfKZv6aMqZwICS1gW0rwq24ZSXw7y3jW+PRVYqWA==}
+    engines: {node: '>= 16'}
+    peerDependencies:
+      vue: ^3.0.0
+    dependencies:
+      '@intlify/core-base': 11.1.1
+      '@intlify/shared': 11.1.1
+      '@vue/devtools-api': 6.6.4
+      vue: 3.2.47
+    dev: false
+
   /vue-router/4.1.3_vue@3.2.47:
     resolution: {integrity: sha512-XvK81bcYglKiayT7/vYAg/f36ExPC4t90R/HIpzrZ5x+17BOWptXLCrEPufGgZeuq68ww4ekSIMBZY1qdUdfjA==}
     peerDependencies:

+ 2 - 5
src/api/constant.ts

@@ -1,4 +1,5 @@
 import {namespace} from '@/env'
+import { lang, ui18n } from '@/lang';
 
 export enum ResCode {
   TOKEN_INVALID = 4008,
@@ -6,11 +7,7 @@ export enum ResCode {
   SUCCESS = 0
 }
 
-export const ResCodeDesc: { [key in ResCode]: string } = {
-  [ResCode.UN_AUTH] : '您没有访问权限',
-  [ResCode.TOKEN_INVALID]: 'token已失效',
-  [ResCode.SUCCESS]: '请求成功'
-}
+export const ResCodeDesc: { [key in ResCode]: string } = lang.resCode
 
 export const UPLOAD_HEADS = {
   'Content-Type': 'multipart/form-data'

+ 4 - 3
src/api/floder.ts

@@ -2,6 +2,7 @@ import { params } from '@/env'
 import { FLODER_LIST } from './constant'
 import { addUnsetResErrorURLS,axios } from './instance'
 import {namespace} from '@/env'
+import { ui18n } from '@/lang'
 
 export interface Floder {
   filesId:	number,
@@ -20,13 +21,13 @@ export const fetchFloders = async () => {
   const otherFloders = [{
     filesId: 88,
     filesTypeId: 100,
-    filesTitle: '勘验笔录',
+    filesTitle: ui18n.t('floder.record'),
     ex: `${namespace}/caseInquest/info`,
     bex: `${namespace}/caseInquest/downDocx`
   }, {
     filesId: 89,
     filesTypeId: 100,
-    filesTitle: '提取清单',
+    filesTitle: ui18n.t('floder.extractList'),
     ex: `${namespace}/caseExtractDetail/info`,
     bex: `${namespace}/caseExtractDetail/downDocx`
   }, ]
@@ -36,7 +37,7 @@ export const fetchFloders = async () => {
       floders.push({
         filesId: 100 + ndx,
         filesTypeId: -1,
-        filesTitle: '照片制卷图',
+        filesTitle: ui18n.t('floder.photo'),
         caseId: params.caseId.toString(),
         filesUrl: item.imgUrl
       })

+ 2 - 1
src/api/folder-type.ts

@@ -1,3 +1,4 @@
+import { ui18n } from "@/lang";
 import { FOLDER_TYPE_LIST } from "./constant";
 import axios from "./instance";
 
@@ -15,7 +16,7 @@ export const fetchFloderTypes = async () => {
   const types = await axios.get<FloderTypes>(FOLDER_TYPE_LIST);
   types.push({
     filesTypeId: -1,
-    filesTypeName: "照片制卷",
+    filesTypeName: ui18n.t('floder.photo'),
     flatShow: false,
     modalShow: false,
     parentId: 39,

+ 3 - 2
src/api/instance.ts

@@ -5,6 +5,7 @@ import * as URL from "./constant";
 import { ResCode, ResCodeDesc } from "./constant";
 import { baseURL, params } from "@/env";
 import GAxios from "axios";
+import { ui18n } from "@/lang";
 
 const instance = axiosFactory();
 
@@ -60,12 +61,12 @@ addResErrorHandler((response, data) => {
     if (data.code === ResCode.TOKEN_INVALID) {
       gotoLogin();
     } else if (data.code === ResCode.UN_AUTH) {
-      Dialog.alert({content: msg, okText: '我知道了'}).then(() => {
+      Dialog.alert({content: msg, okText: ui18n.t('sys.ok')}).then(() => {
         gotoLogin();
       })
       throw msg
     } else {
-      Message.error(msg || '服务出现异常,请稍后再试');
+      Message.error(msg || ui18n.t('sys.serviceErr'));
     }
   }
 });

+ 2 - 11
src/api/scene.ts

@@ -1,6 +1,7 @@
 import axios from './instance'
 import { MODEL_LIST, MODEL_SIGN, SCENE_LIST_ALL, SYNC_INFO } from './constant'
 import { params } from '@/env'
+import { lang } from '@/lang'
 
 export enum SceneStatus {
   DEL = -1,
@@ -21,17 +22,7 @@ export enum SceneType {
   SWYDMX = 6,
   DSFXJ = 7,
 }
-export const SceneTypeDesc: Record<SceneType, string>  = {
-  [SceneType.SWKK]: '四维看看/Mesh',
-  [SceneType.SWKJ]: '四维看见/Mesh',
-  [SceneType.SWSS]: '四维深时/点云',
-  [SceneType.SWMX]: '媒体库',
-  [SceneType.SWSSMX]: '四维深时/Mesh',
-  [SceneType.SWYDSS]: '四维深光/点云',
-  [SceneType.SWYDMX]: '四维深光/Mesh',
-  [SceneType.DSFXJ]: '圆周率/Mesh'
-}
-
+export const SceneTypeDesc: Record<SceneType, string>  = lang.scene.types
 
 
 export interface Scene {

+ 2 - 1
src/api/setup.ts

@@ -3,6 +3,7 @@ import { ResCode } from './constant'
 import { setOfflineAxios } from './offline'
 
 import type { AxiosResponse, AxiosRequestConfig } from 'axios'
+import { ui18n } from '@/lang'
 
 export type ResErrorHandler = <D, T extends ResData<D>>(response: AxiosResponse<T>, data?: T) => void
 export type ReqErrorHandler = <T>(err: Error, response: AxiosRequestConfig<T>) => void
@@ -134,7 +135,7 @@ export const axiosFactory = () => {
       if (!matchURL(axiosConfig.unTokenSet, config)) {
         if (!axiosConfig.token) {
           if (!offline && !matchURL(axiosConfig.unReqErrorSet, config)) {
-            const error = new Error('缺少token')
+            const error = new Error(ui18n.t('resCode.4008'))
             callErrorHandler('req', error, config)
             throw error
           }

+ 9 - 8
src/api/tagging-style.ts

@@ -7,6 +7,7 @@ import {
 } from "./constant";
 import { jsonToForm } from "@/utils";
 import { params } from "@/env";
+import { ui18n } from "@/lang";
 interface ServiceStyle {
   iconId: number;
   iconTitle: string;
@@ -17,23 +18,23 @@ interface ServiceStyle {
 
 export const defStyleType = {
   id: 8,
-  name: "其他",
+  name: ui18n.t('sys.other'),
 }
 export const styleTypes = [
   {
     id: 1,
-    name: "痕迹",
+    name: ui18n.t('tagging.type.1'),
     children: [
-      { id: 2, name: "手印" },
-      { id: 3, name: "足迹" },
-      { id: 4, name: "血迹 " },
-      { id: 5, name: "尸体" },
-      { id: 6, name: "其他" },
+      { id: 2, name: ui18n.t('tagging.type.2') },
+      { id: 3, name: ui18n.t('tagging.type.3') },
+      { id: 4, name: ui18n.t('tagging.type.4') },
+      { id: 5, name: ui18n.t('tagging.type.5') },
+      { id: 6, name: ui18n.t('tagging.type.6') },
     ],
   },
   {
     id: 7,
-    name: "物证",
+    name: ui18n.t('tagging.type.7'),
   },
   defStyleType
 ];

+ 35 - 36
src/components/bill-ui/components/cropper/cropper.vue

@@ -1,39 +1,37 @@
 <template>
-  <Confirm title="裁剪" :func="clickHandler">
+  <Confirm :title="$t('sys.crop')" :func="clickHandler">
     <template v-slot:content>
       <div class="cropper-layer" :style="style">
-        <VueCropper 
-        ref="vmRef" 
-        v-bind="option"
-        v-on="on" />
+        <VueCropper ref="vmRef" v-bind="option" v-on="on" />
       </div>
     </template>
   </Confirm>
 </template>
 
 <script setup>
-import { VueCropper } from 'vue-cropper'
-import Confirm from '../dialog/Confirm.vue'
-import { computed, ref } from 'vue'
-import 'vue-cropper/dist/index.css'
+import { VueCropper } from "vue-cropper";
+import Confirm from "../dialog/Confirm.vue";
+import { computed, ref } from "vue";
+import "vue-cropper/dist/index.css";
+import { ui18n } from "@/lang";
 
-const layerWidth = 500
+const layerWidth = 500;
 const props = defineProps({
   fixedNumber: {
     type: Array,
-    default: () => [1, 1]
+    default: () => [1, 1],
   },
   img: { type: String },
   cb: {
-    type: Function
-  }
-})
+    type: Function,
+  },
+});
 
-const fixedNumber = props.fixedNumber
-const getHeight = width => (fixedNumber[1] / fixedNumber[0]) * width
+const fixedNumber = props.fixedNumber;
+const getHeight = (width) => (fixedNumber[1] / fixedNumber[0]) * width;
 const option = {
   outputSize: 1,
-  outputType: 'png',
+  outputType: "png",
   info: false,
   full: true,
   fixed: true,
@@ -46,37 +44,36 @@ const option = {
   autoCropWidth: layerWidth / 2,
   autoCropHeight: getHeight(layerWidth / 2),
   centerBox: true,
-  mode: 'contain',
+  mode: "contain",
   maxImgSize: 400,
-  ...props
-}
+  ...props,
+};
 
 const style = computed(() => ({
-  width: layerWidth + 'px',
-  height: getHeight(layerWidth) + 'px'
-}))
+  width: layerWidth + "px",
+  height: getHeight(layerWidth) + "px",
+}));
 
-const vmRef = ref()
+const vmRef = ref();
 const on = {
   imgLoad(status) {
-    if (status !== 'success') {
-      props.cb('图片加载失败')
+    if (status !== "success") {
+      props.cb(ui18n.t("sys.imgLoadErr"));
     }
   },
-}
+};
 
 const clickHandler = async (status) => {
-  if (status === 'ok') {
+  if (status === "ok") {
     const data = await Promise.all([
-      new Promise(resolve => vmRef.value.getCropBlob(resolve)),
-      new Promise(resolve => vmRef.value.getCropData(resolve)),
-    ])
-    props.cb(null, data)
+      new Promise((resolve) => vmRef.value.getCropBlob(resolve)),
+      new Promise((resolve) => vmRef.value.getCropData(resolve)),
+    ]);
+    props.cb(null, data);
   } else {
-    props.cb()
+    props.cb();
   }
-  
-}
+};
 </script>
 
 <style>
@@ -92,4 +89,6 @@ const clickHandler = async (status) => {
 }
 </style>
 
-<script> export default { name: 'ui-cropper' } </script>
+<script>
+export default { name: "ui-cropper" };
+</script>

+ 4 - 2
src/components/bill-ui/components/cropper/index.js

@@ -1,18 +1,20 @@
 import Cropper from './cropper.vue'
 import { mount } from '../../utils/componentHelper'
 import { toRawType } from '../../utils/index'
+import { ui18n } from '@/lang'
 
 Cropper.use = function use(app) {
     const isCropper = false
     Cropper.open = function (config) {
         if (isCropper) {
-            return Promise.reject('正在裁剪')
+            return Promise.reject(ui18n.t('sys.cropIng'))
         }
         if (toRawType(config) === 'String') {
             config = { img: config }
         }
         if (!config || !config.img) {
-            return Promise.reject('请传入裁剪图片')
+            
+            return Promise.reject(ui18n.t('sys.tranCropImg'))
         }
 
         return new Promise((resolve, reject) => {

+ 40 - 38
src/components/bill-ui/components/dialog/Alert.vue

@@ -1,44 +1,46 @@
 <template>
-    <ui-dialog>
-        <template v-slot:header>
-            <span>{{ title }}</span>
-            <i class="iconfont icon-close fun-ctrl" @click="close"></i>
-        </template>
-        {{ content }}
-        <template v-slot:footer>
-            <ui-button type="submit" @click="close">{{ okText }}</ui-button>
-        </template>
-    </ui-dialog>
+  <ui-dialog>
+    <template v-slot:header>
+      <span>{{ title }}</span>
+      <i class="iconfont icon-close fun-ctrl" @click="close"></i>
+    </template>
+    {{ content }}
+    <template v-slot:footer>
+      <ui-button type="submit" @click="close">{{ okText }}</ui-button>
+    </template>
+  </ui-dialog>
 </template>
 <script>
-import { defineComponent } from 'vue'
-import { isFunction, omit } from '../../utils'
+import { defineComponent } from "vue";
+import { isFunction, omit } from "../../utils";
+import { ui18n } from "@/lang";
+
 export default defineComponent({
-    name: 'ui-alert',
-    props: {
-        title: {
-            type: String,
-            default: '提示',
-        },
-        okText: {
-            type: String,
-            default: '确定',
-        },
-        func: Function,
-        content: String,
-        destroy: Function,
+  name: "ui-alert",
+  props: {
+    title: {
+      type: String,
+      default: ui18n.t("sys.tip"),
     },
-    setup: function (props, ctx) {
-        const close = () => {
-            if (isFunction(props.func) && props.func() === false) {
-                return
-            }
-            isFunction(props.destroy) && props.destroy()
-        }
-        return {
-            ...omit(props, 'destroy', 'func'),
-            close,
-        }
+    okText: {
+      type: String,
+      default: ui18n.t("sys.enter"),
     },
-})
-</script>
+    func: Function,
+    content: String,
+    destroy: Function,
+  },
+  setup: function (props, ctx) {
+    const close = () => {
+      if (isFunction(props.func) && props.func() === false) {
+        return;
+      }
+      isFunction(props.destroy) && props.destroy();
+    };
+    return {
+      ...omit(props, "destroy", "func"),
+      close,
+    };
+  },
+});
+</script>

+ 54 - 53
src/components/bill-ui/components/dialog/Confirm.vue

@@ -1,62 +1,63 @@
 <template>
-    <ui-dialog>
-        <template #header>
-            <template v-if="!$slots.header">
-                <span>{{ title }}</span>
-                <i class="iconfont icon-close fun-ctrl" @click="close('no')" v-if="func"></i>
-            </template>
-            <span v-else>{{ title }}</span>
-        </template>
+  <ui-dialog>
+    <template #header>
+      <template v-if="!$slots.header">
+        <span>{{ title }}</span>
+        <i class="iconfont icon-close fun-ctrl" @click="close('no')" v-if="func"></i>
+      </template>
+      <span v-else>{{ title }}</span>
+    </template>
 
-        <template v-if="$slots.content">
-            <slot name="content" />
-        </template>
-        <template v-else>
-            <pre class="confirm-content">{{ content }}</pre>
-        </template>
+    <template v-if="$slots.content">
+      <slot name="content" />
+    </template>
+    <template v-else>
+      <pre class="confirm-content">{{ content }}</pre>
+    </template>
 
-        <template #footer>
-            <template v-if="!$slots.footer">
-                <ui-button type="submit" @click="close('no')">{{ noText }}</ui-button>
-                <ui-button type="primary" @click="close('ok')">{{ okText }}</ui-button>
-            </template>
-            <slot v-else name="footer" />
-        </template>
-    </ui-dialog>
+    <template #footer>
+      <template v-if="!$slots.footer">
+        <ui-button type="submit" @click="close('no')">{{ noText }}</ui-button>
+        <ui-button type="primary" @click="close('ok')">{{ okText }}</ui-button>
+      </template>
+      <slot v-else name="footer" />
+    </template>
+  </ui-dialog>
 </template>
 <script>
-import { defineComponent } from 'vue'
-import { isFunction, omit } from '../../utils'
+import { defineComponent } from "vue";
+import { isFunction, omit } from "../../utils";
+import { ui18n } from "@/lang";
 export default defineComponent({
-    name: 'ui-confirm',
-    props: {
-        title: {
-            type: String,
-            default: '提示',
-        },
-        okText: {
-            type: String,
-            default: '确定',
-        },
-        noText: {
-            type: String,
-            default: '取消',
-        },
-        func: Function,
-        content: String,
-        destroy: Function,
+  name: "ui-confirm",
+  props: {
+    title: {
+      type: String,
+      default: ui18n.t("sys.tip"),
     },
-    setup: function (props, ctx) {
-        const close = result => {
-            if (isFunction(props.func) && props.func(result) === false) {
-                return
-            }
-            isFunction(props.destroy) && props.destroy()
-        }
-        return {
-            ...omit(props, 'destroy', 'func'),
-            close,
-        }
+    okText: {
+      type: String,
+      default: ui18n.t("sys.enter"),
     },
-})
+    noText: {
+      type: String,
+      default: ui18n.t("sys.cancel"),
+    },
+    func: Function,
+    content: String,
+    destroy: Function,
+  },
+  setup: function (props, ctx) {
+    const close = (result) => {
+      if (isFunction(props.func) && props.func(result) === false) {
+        return;
+      }
+      isFunction(props.destroy) && props.destroy();
+    };
+    return {
+      ...omit(props, "destroy", "func"),
+      close,
+    };
+  },
+});
 </script>

+ 25 - 11
src/components/bill-ui/components/input/file.vue

@@ -24,12 +24,16 @@
           </template>
           <p class="bottom">
             <template v-if="!othPlaceholder">
-              <template v-if="accept">支持 {{ accept }} 等格式,</template>
-              <template v-if="normalizeScale">宽*高比例 {{ scale }},</template>
+              <template v-if="accept">{{ $t("sys.upload.accept", { accept }) }}</template>
+              <template v-if="normalizeScale">{{
+                $t("sys.upload.accept", { scale })
+              }}</template>
               <template v-if="maxSize"
-                >大小不超过 {{ sizeStr }}{{ maxLen ? "," : "" }}</template
-              >
-              <template v-if="maxLen">个数不超过 {{ maxLen }}个</template>
+                >{{ $t("sys.upload.accept", { sizeStr }) }}
+              </template>
+              <template v-if="maxLen"
+                >{{ $t("sys.upload.accept", { maxLen }) }}
+              </template>
             </template>
             <template v-else>
               {{ othPlaceholder }}
@@ -38,7 +42,7 @@
         </div>
 
         <span v-else v-if="!maxLen || maxLen > modelValue.length">
-          {{ multiple ? "继续添加" : "替换" }}
+          {{ multiple ? t("sys.upload.conAdd") : t("sys.upload.rep") }}
         </span>
         <span class="tj" v-if="maxLen && modelValue.length">
           <span>{{ modelValue.length || 0 }}</span> / {{ maxLen }}
@@ -56,6 +60,7 @@ import { filePropsDesc } from "./state";
 import { toRawType } from "../../utils";
 import Message from "../message";
 import { ref, computed } from "vue";
+import { ui18n } from "@/lang";
 
 const props = defineProps({
   ...filePropsDesc,
@@ -130,7 +135,7 @@ const calcScale = (w, h) => parseInt((w / h) * 1000);
 const selectFileHandler = async (ev) => {
   const fileEl = ev.target;
   const files = Array.from(fileEl.files);
-  const previewError = (e, msg = `预览加载失败!`) => {
+  const previewError = (e, msg = ui18n.t("sys.upload.previewErr")) => {
     console.error(e);
     Message.error(msg);
     fileEl.value = "";
@@ -141,7 +146,10 @@ const selectFileHandler = async (ev) => {
       const accepts = props.accept.split(",").map((atom) => atom.trim().toUpperCase());
       const hname = file.name.substr(file.name.lastIndexOf(".")).toUpperCase();
       if (!accepts.includes(hname)) {
-        return previewError("格式错误", `仅支持${props.accept}格式文件`);
+        return previewError(
+          ui18n.t("sys.upload.accErr"),
+          ui18n.t("sys.upload.accSuperErr", props)
+        );
       }
     }
   }
@@ -177,7 +185,10 @@ const selectFileHandler = async (ev) => {
       const scaleDiff = calcScale(...normalizeScale.value) - calcScale(w, h);
 
       if (Math.abs(scaleDiff) > 300) {
-        return previewError("error scale", `${file.name}的比例部位不为${props.scale}`);
+        return previewError(
+          "error scale",
+          ui18n.t("sys.upload.scaleErr", { name: file.name, scale: props.scale })
+        );
       }
     }
   }
@@ -185,7 +196,10 @@ const selectFileHandler = async (ev) => {
   if (props.maxSize) {
     for (const file of files) {
       if (file.size > props.maxSize) {
-        return previewError("error size", `${file.name}的大小超过${sizeStr.value}`);
+        return previewError(
+          "error size",
+          ui18n.t("sys.upload.sizeErr", { name: file.name, sizeStr: sizeStr.value })
+        );
       }
     }
   }
@@ -209,7 +223,7 @@ const selectFileHandler = async (ev) => {
     : files[0];
 
   if (Array.isArray(emitData) && props.maxLen && emitData.length > props.maxLen) {
-    return previewError("err len", `最多仅支持${props.maxLen}个文件!`);
+    return previewError("err len", ui18n.t("sys.upload.lenErr", props));
   }
 
   emit("update:modelValue", emitData);

+ 8 - 6
src/components/bill-ui/components/input/state.js

@@ -1,3 +1,5 @@
+import { ui18n } from "@/lang";
+
 const instalcePublic = {
     name: {
         type: String,
@@ -11,7 +13,7 @@ const instalcePublic = {
     },
     placeholder: {
         require: false,
-        default: '请输入',
+        default: ui18n.t('sys.placeInput'),
     },
 }
 
@@ -35,7 +37,7 @@ export const filePropsDesc = {
     },
     placeholder: {
         require: false,
-        default: '请选择',
+        default: ui18n.t('sys.placeSelect'),
     },
     othPlaceholder: {
         require: false,
@@ -93,7 +95,7 @@ export const textPropsDesc = {
     },
     placeholder: {
         type: String,
-        default: '请输入',
+        default: ui18n.t('sys.placeInput'),
     },
     readonly: {
         type: Boolean,
@@ -141,8 +143,8 @@ export const selectPropsDesc = {
         type: Boolean,
         require: false,
     },
-    placeholder: { ...textPropsDesc.placeholder, default: '请选择' },
-    unplaceholder: { ...textPropsDesc.placeholder, default: '暂无选项' },
+    placeholder: { ...textPropsDesc.placeholder, default: ui18n.t('sys.placeSelect'), },
+    unplaceholder: { ...textPropsDesc.placeholder, default: ui18n.t('sys.unSelect'), },
     options: {
         type: Array,
         default: () => [],
@@ -155,7 +157,7 @@ export const selectPropsDesc = {
 
 export const searchPropsDesc = {
     ...selectPropsDesc,
-    unplaceholder: { ...textPropsDesc.placeholder, default: '无搜索结果' },
+    unplaceholder: { ...textPropsDesc.placeholder, default: ui18n.t('sys.unSearch'), },
 }
 
 export const numberPropsDesc = {

+ 13 - 14
src/components/error/index.vue

@@ -1,27 +1,26 @@
 <template>
-    <div class="layout err-layout">
-        <div>
-            <img :src="errImg" />
-            <div class="content" v-html="'内存不足,请勿同时打开多个页面或应用程序,尝试重启浏览器后重新打开。'"></div>
-        </div>
+  <div class="layout err-layout">
+    <div>
+      <img :src="errImg" />
+      <div class="content" v-html="$t('sys.jsError')"></div>
     </div>
+  </div>
 </template>
 
 <script setup lang="ts">
-import errImg from './img/err.png'
-
+import errImg from "./img/err.png";
 </script>
 
 <style lang="scss" scoped src="./style.scss" />
 <style lang="scss">
 .err-layout {
-    .content {
-        font-weight: 400;
-        font-size: 16px;
-        color: rgba(100, 101, 102, 1);
-        p {
-            margin-top: 10px;
-        }
+  .content {
+    font-weight: 400;
+    font-size: 16px;
+    color: rgba(100, 101, 102, 1);
+    p {
+      margin-top: 10px;
     }
+  }
 }
 </style>

+ 21 - 19
src/components/materials/index.vue

@@ -1,19 +1,20 @@
 <template>
   <Modal
     width="800px"
-    title="媒体库"
+    :title="$t('material.name')"
     :open="visible"
     @ok="okHandler"
     :afterClose="afterClose"
     @cancel="emit('update:visible', false)"
-    okText="确定"
-    cancelText="取消"
+    :okText="$t('sys.enter')"
+    :cancelText="$t('sys.cancel')"
     class="model-table"
   >
     <div>
       <div className="model-header">
         <p class="header-desc">
-          已选择数据<span>( {{ rowSelection.selectedRowKeys.length }} )</span>
+          {{ $t("material.selectCount") }}
+          <span>( {{ rowSelection.selectedRowKeys.length }} )</span>
         </p>
         <div class="up-se">
           <span class="upload fun-ctrls">
@@ -26,7 +27,7 @@
             >
               <template v-slot:replace>
                 <Button type="primary" ghost>
-                  <ui-icon type="add" class="icon" />上传文件
+                  <ui-icon type="add" class="icon" />{{ $t("sys.upload.place1") }}
                 </Button>
               </template>
             </ui-input>
@@ -34,7 +35,7 @@
           <Search
             v-if="Object.keys(allData).length"
             className="content-header-search"
-            placeholder="输入名称搜索"
+            :placeholder="$t('material.search')"
             v-model:value="params.name"
             allow-clear
             style="width: 244px"
@@ -66,10 +67,10 @@
             <template v-if="column.key === 'status'">
               {{
                 record.status === 1
-                  ? "上传成功"
+                  ? $t('material.uploadSuccess')
                   : record.status === -1
-                  ? "上传失败"
-                  : "上传中"
+                  ? $t('material.uploadErr')
+                  : $t('material.uploadIng')
               }}
             </template>
             <template v-if="column.key === 'group'">
@@ -77,14 +78,14 @@
             </template>
             <template v-else-if="column.key === 'action'">
               <span>
-                <a @click="delHandler(record.id)">删除</a>
+                <a @click="delHandler(record.id)">{{ $t('sys.del') }}</a>
               </span>
             </template>
           </template>
         </Table>
         <div style="padding: 1px" v-else>
           <Empty
-            description="暂无结果"
+            :description="$t('sys.undata')"
             :image="Empty.PRESENTED_IMAGE_SIMPLE"
             className="ant-empty ant-empty-normal"
           />
@@ -110,6 +111,7 @@ import {
 } from "@/api/material";
 import Message from "bill/components/message/message.vue";
 import { Dialog } from "bill/expose-common";
+import { ui18n } from "@/lang";
 
 const props = defineProps<{
   uploadFormat?: string[];
@@ -158,7 +160,7 @@ const rowSelection: any = ref({
     if (typeof props.count !== "number" || props.count >= newKeys.length) {
       selectKeys.value = newKeys;
     } else {
-      Message.error(`最多选择${props.count}项`);
+      Message.error(ui18n.t('material.selectCount', props));
     }
   },
   getCheckboxProps: (record: Material) => {
@@ -172,31 +174,31 @@ const rowSelection: any = ref({
 });
 const cloumns = computed(() => [
   {
-    title: "名称",
+    title: ui18n.t('material.tabs.name'),
     dataIndex: "name",
     key: "name",
   },
   {
     width: "100px",
-    title: "格式",
+    title: ui18n.t('material.tabs.format'),
     dataIndex: "format",
     key: "format",
   },
   {
     width: "100px",
-    title: "大小",
+    title: ui18n.t('material.tabs.size'),
     dataIndex: "size",
     key: "size",
   },
   {
     width: "100px",
-    title: "状态",
+    title: ui18n.t('material.tabs.status'),
     dataIndex: "status",
     key: "status",
   },
   {
     width: "100px",
-    title: "分组",
+    title: ui18n.t('material.tabs.group'),
     dataIndex: "group",
     key: "group",
     filters: groups.value.map((g) => ({
@@ -206,7 +208,7 @@ const cloumns = computed(() => [
   },
   {
     width: "100px",
-    title: "操作",
+    title: ui18n.t('material.tabs.action'),
     key: "action",
   },
 ]);
@@ -240,7 +242,7 @@ const addHandler = async (file: File) => {
   refresh();
 };
 const delHandler = async (id: Material["id"]) => {
-  if (await Dialog.confirm("确定要删除此数据吗?")) {
+  if (await Dialog.confirm(ui18n.t('sys.delConfrm'))) {
     await delMaterial(id);
     const ndx = selectKeys.value.indexOf(id);
     console.log(selectKeys.value, id);

+ 5 - 7
src/components/tagging/sign-new.vue

@@ -27,13 +27,13 @@
         </h2>
         <div class="content">
           <div class="p">
-            <span v-if="defStyleType.id !== taggingStyle?.typeId"> 特征描述: </span>
+            <span v-if="defStyleType.id !== taggingStyle?.typeId"> {{$t('tagging.tabs.typeId')}}: </span>
             <div v-html="tagging.desc"></div>
           </div>
           <template v-if="defStyleType.id !== taggingStyle?.typeId">
-            <p><span>遗留部位:</span>{{ tagging.part }}</p>
-            <p><span>提取方法:</span>{{ tagging.method }}</p>
-            <p><span>提取人:</span>{{ tagging.principal }}</p>
+            <p><span>{{$t('tagging.tabs.part')}}:</span>{{ tagging.part }}</p>
+            <p><span>{{$t('tagging.tabs.method')}}:</span>{{ tagging.method }}</p>
+            <p><span>{{$t('tagging.tabs.principal')}}:</span>{{ tagging.principal }}</p>
           </template>
         </div>
         <Images
@@ -45,7 +45,7 @@
         <div class="edit-hot" v-if="showDelete">
           <span @click="$emit('delete')" class="fun-ctrl">
             <ui-icon type="del" />
-            删除
+            {{$t('sys.del')}}
           </span>
         </div>
       </UIBubble>
@@ -88,7 +88,6 @@ const emit = defineEmits<{
 
 const audio = ref();
 watchEffect(() => {
-  audio.value && console.error("准备好了!,");
   if (props.show && audio.value) {
     audio.value.play();
   }
@@ -158,7 +157,6 @@ tag.bus.on("changePosition", (data) => {
 watch(getPosition, (p) => {
   changeTimeout = setTimeout(() => {
     if (inRevise(p, currentPosition)) {
-      console.log("更改当前位置");
       tag.changePosition(p);
       currentPosition = p;
     }

+ 5 - 5
src/components/tagging/sign.vue

@@ -25,10 +25,10 @@
           ref="audio"
         />
         <div class="content">
-          <p><span>特征描述:</span>{{ tagging.desc }}</p>
-          <p><span>遗留部位:</span>{{ tagging.part }}</p>
-          <p><span>提取方法:</span>{{ tagging.method }}</p>
-          <p><span>提取人:</span>{{ tagging.principal }}</p>
+          <p><span>{{$t('tagging.tabs.typeId')}}:</span>{{ tagging.desc }}</p>
+          <p><span>{{$t('tagging.tabs.part')}}:</span>{{ tagging.part }}</p>
+          <p><span>{{$t('tagging.tabs.method')}}:</span>{{ tagging.method }}</p>
+          <p><span>{{$t('tagging.tabs.principal')}}:</span>{{ tagging.principal }}</p>
         </div>
         <Images
           :tagging="tagging"
@@ -41,7 +41,7 @@
         >
           <span @click="$emit('delete')" class="fun-ctrl">
             <ui-icon type="del" />
-            删除
+            {{ $t('sys.del') }}
           </span>
         </div>
       </UIBubble>

+ 9 - 0
src/lang/en.ts

@@ -0,0 +1,9 @@
+
+
+export default {
+  resCode: {
+    4008: "token已失效",
+    4010: "您没有访问权限",
+    0: "请求成功"
+  }
+}

+ 91 - 0
src/lang/index.ts

@@ -0,0 +1,91 @@
+import { createI18n, I18n as BaseI18n } from 'vue-i18n'
+import { App, WritableComputedRef } from 'vue'
+import { localGetFactory, localSetFactory } from '@/utils/store'
+import { paramsToStr, strToParams } from '@/utils'
+import zh from './zh'
+import en from './en'
+
+// 语言支持
+export enum langNameEum {
+  zh = 'zh',
+  en = 'en',
+}
+
+export const langNameDescs = {
+  [langNameEum.zh]: '中文',
+  [langNameEum.en]: 'English',
+}
+
+export const deflangName = langNameEum.zh
+export const langNames = [langNameEum.en, langNameEum.zh]
+
+type I18n = BaseI18n & {
+  global: {
+    t: I18nGlobalTranslation
+    changeLang(langName: langNameEum, reload?: boolean): void
+    locale: WritableComputedRef<langNameEum>
+  }
+}
+
+const localKey = 'lang'
+const local = {
+  get: localGetFactory(str => {
+    if (str) {
+      return str as langNameEum
+    } else {
+      const langs = Object.keys(langNameDescs)
+      const defLang = window?.navigator?.language || 'zh'
+      const navLang = langs.find(lang =>
+        new RegExp(`-?${lang}-?`).test(defLang)
+      )
+      return navLang || langNameEum.en
+    }
+  }),
+  set: localSetFactory((lang: langNameEum) => lang)
+}
+
+
+const params = strToParams(location.search)
+export const langKey = (params.lang || local.get(localKey)) as langNameEum
+if (langKey !== local.get(localKey)) {
+  local.set(localKey, langKey)
+}
+
+
+const i18n: I18n = createI18n({
+  legacy: false,
+  fallbackLocale: deflangName,
+  availableLocales: langNames,
+  locale: langKey,
+  sync: true,
+  silentTranslationWarn: true,
+  missingWarn: false,
+  silentFallbackWarn: true
+}) as I18n
+
+
+export const langs = {
+  [langNameEum.en]: en,
+  [langNameEum.zh]: zh,
+}
+export const lang = langs[langKey] as typeof zh
+
+i18n.global.setLocaleMessage(langNameEum.zh, zh)
+i18n.global.setLocaleMessage(langNameEum.en, en)
+i18n.global.changeLang = (lang: langNameEum, reload = true) => {
+  i18n.global.locale.value = lang
+  local.set(localKey, lang)
+  params.lang = lang
+  if (reload) {
+    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

+ 315 - 0
src/lang/zh.ts

@@ -0,0 +1,315 @@
+export default {
+  resCode: {
+    4008: "token已失效",
+    4010: "您没有访问权限",
+    0: "请求成功",
+  },
+  security: {
+    name: "安防",
+  },
+  floder: {
+    name: "卷宗",
+    record: "勘验笔录",
+    extractList: "提取清单",
+    photo: "照片制卷",
+  },
+  sys: {
+    search: '搜索',
+    all: '全部',
+    download: '下载',
+    create: '创建',
+    rename: '重命名',
+    edit: '编辑',
+    add: '新增',
+    unSaveMsg: "您有操作未保存,确定要退出吗?",
+    "404Page": "资源不存在或已删除",
+    errPage: "错误页面",
+    list: "数据列表",
+    delConfrm: "确定要删除此数据吗?",
+    del: "删除",
+    quit: "退出",
+    save: "保存",
+    undata: "暂无结果",
+    unSearchData: "暂无搜索结果",
+    addData: "添加数据",
+    ok: "我知道了",
+    serviceErr: "服务出现异常,请稍后再试",
+    other: "其他",
+    crop: "裁剪",
+    imgLoadErr: "图片加载失败",
+    tranCropImg: "请传入裁剪图片",
+    cropIng: "正在裁剪",
+    tip: "提示",
+    enter: "确定",
+    cancel: "取消",
+    upload: {
+      place1: "上传文件",
+      accept: "支持 {accept} 等格式,",
+      normalizeScale: "宽*高比例 {scale},",
+      maxSize: "大小不超过 {sizeStr},",
+      maxLen: "个数不超过 {maxLen}个",
+      conAdd: "继续添加",
+      rep: "替换",
+      previewErr: "预览加载失败!",
+      accErr: "格式错误",
+      accSuperErr: "仅支持{accept}格式文件",
+      scaleErr: "{name}的比例部位不为{scale}",
+      sizeErr: "{name}的大小超过{sizeStr}",
+      lenErr: "最多仅支持{maxLen}个文件!",
+    },
+    placeInput: "请输入",
+    placeSelect: "请选择",
+    unSelect: "暂无选项",
+    unSearch: "无搜索结果",
+    jsError:
+      "内存不足,请勿同时打开多个页面或应用程序,尝试重启浏览器后重新打开。",
+    viewPWD: "访问密码",
+    placPWD: "请输入密码!",
+    pwdErr: "密码错误,请重新输入。",
+  },
+  material: {
+    name: "媒体库",
+    selectCount: "已选择数据",
+    search: "输入名称搜索",
+    uploadSuccess: "上传成功",
+    uploadErr: "上传失败",
+    up: '+从媒体库上传',
+    uploadIng: "上传中",
+    selectErr: "最多选择{count}项",
+    tabs: {
+      name: "名称",
+      format: "格式",
+      size: "大小",
+      status: "状态",
+      group: "分组",
+      action: "操作",
+    },
+  },
+  view: {
+    name: "视图提取",
+    vName: "视图",
+    defName: "视图 {num}",
+    nameErr: "视图名称不可为空",
+    all: '全部视图',
+
+  },
+  guide: {
+    name: "路径",
+    modelErr: "路径所在模型被删除,无法播放",
+    defTitle: "路径{num}",
+    nameErr: "路径名称不可为空",
+    pErr: "路径点不可少于两个",
+
+
+    pathName: '路线({count})',
+    guideName: '导览({count})',
+
+    guide: {
+      name: '导览',
+      add: '添加视角',
+      time: '视频时长',
+      clear: '清空画面',
+      undata: '暂无导览',
+      unItems: '无法保存空路径导览!',
+      delConfirm: '确定要删除此画面吗?',
+      clearConfirm: '确定要清空画面吗?'
+    },
+    path: { 
+      name: '路线' ,
+      title: '路径名称',
+      lineWidth: '路径粗细',
+      lineColor: '路径颜色',
+      showDirection: '路径箭头',
+      reverseDirection: '箭头反向',
+      fontSize: '文字大小',
+      visibilityRange: '可见范围',
+      preview: '预览路径',
+      stop: '停止',
+      pointTitle: '编辑点',
+      pointDesc: '描述',
+      globalVisibility: '全部范围可视',
+      applyConfirm: '确定要将此属性应用到所有位置?'
+    },
+  },
+  measure: {
+    list: '测量列表',
+    name: "测量",
+    area: {
+      desc: "面积",
+      unit: "面积",
+    },
+    free: {
+      desc: "自由",
+      unit: "长度",
+    },
+    vertical: {
+      desc: "垂直",
+      unit: "长度",
+    },
+    nameErr: "测量名称不可为空",
+  },
+  setting: {
+    name: "设置",
+    initView: '初始画面',
+    back: '设置天空',
+    backs: [
+      '无',
+      '地图',
+      '蓝天白云',
+      '乌云密布',
+      '夜空',
+      '傍晚',
+    ]
+  },
+  record: {
+    name: "屏幕录制",
+    vName: "录屏",
+    defName: "讲解视频{num}",
+    nameErr: "视频名称不可为空",
+    start: '开始录制',
+    list: '全部视频',
+    delConfirm: '确定要删除视频吗?',
+    tag: '标签',
+    merge: '合并视频',
+    con: '继续录制',
+    sizeErr: '已超出限制大小无法继续录制,可保存后继续录制!',
+    backHandler: '后台正在处理',
+  },
+  case: {
+    name: "案件信息",
+    tmName: "勘验信息",
+    sn: "案件名称",
+    summary: "案件概要",
+    yv: '是',
+    nv: '否',
+    cols: {
+      caseTitle: "案件名称",
+      caseNum: "立案编号",
+      caseCategory: "案件类别",
+      crimeTime: "案发时间",
+      homicideCase: '是否命案',
+      criminalCase: '是否刑案',
+      caseRegion: '案发区域',
+      caseAddress: '案发地点',
+      latAndLong: '经纬度',
+    },
+    tmCols: {
+      commandTime: "指挥中心电话时间",
+      alarmTime: "报警时间",
+      alarmName: "报警人",
+      assignDept: "指派/报告单位",
+      inquestDept: "现场勘验单位",
+      assignType: "指派方式",
+      inquestAddress: "勘验地点",
+      times: "勘验时间"
+    }
+  },
+  tagging: {
+    list: '标签列表',
+    name: "标签",
+    tabs: {
+      typeId: "特征描述",
+      part: "遗留部位",
+      method: "提取方法",
+      principal: "提取人",
+    },
+    type: {
+      1: "痕迹",
+      2: "手印",
+      3: "足迹",
+      4: "血迹 ",
+      5: "尸体",
+      6: "其他",
+      7: "物证",
+    },
+    style: '图标样式',
+    plcTitle: '请输入标签标题',
+    titleFex: '标题常驻',
+    plcType: '描述:',
+    plcType1: '特征描述:',
+    plcPart: '遗留部位:',
+    plcMethod: '提取方法:',
+    plcPrincipal: '提取人:',
+    mic: '音乐',
+    micPlc: '支持 mp3/wav 格式,≤30MB',
+    media: '图片/视频',
+    pleMedia: '上传图片/视频',
+    plcMedia1: '支持JPG、PNG、MP4等格式,单个不超过100MB,最多支持上传10张。',
+    styleErr: '请选择图标样式!',
+    titleErr: '标签标题必须填写!',
+    pos: '放置',
+    plcPos: '放置位置',
+    posName: '位置',
+    applyConfirm: '确定要将此属性应用到所有位置?',
+    apply: '应用到全部',
+    posErr: '当前位置无法添加',
+    posTip: '请在模型上单击选择标签位置',
+
+    posTabs: {
+      type: '图标放置方式',
+      typeVal: ['悬浮', '贴地/墙'],
+      scale: '图标大小',
+      rotation: '旋转图标',
+      fontSize: '文字大小',
+      lineHeight: '引线高度',
+      visibilityRange: '可见范围',
+      globalVisibility: '全部范围可视',
+    }
+  },
+  scene: {
+    list: "场景列表",
+    manage: "场景管理",
+    add: "添加场景",
+    tabs: {
+      name: "名称",
+      type: "类型",
+      createTime: "拍摄/创建时间",
+    },
+    typeRaws: {
+      0: "Mesh场景",
+      1: "Mesh场景",
+      2: "点云场景",
+      4: "Mesh场景",
+      5: "点云场景",
+      6: "Mesh场景",
+      7: "Mesh场景",
+    },
+    types: {
+      0: "四维看看/Mesh",
+      1: "四维看见/Mesh",
+      2: "四维深时/点云",
+      3: "媒体库",
+      4: "四维深时/Mesh",
+      5: "四维深光/点云",
+      6: "四维深光/Mesh",
+      7: "圆周率/Mesh",
+    },
+  },
+  fuse: {
+    name: "多元融合",
+    label: '融合场景',
+    join: "拼接",
+    unModel: '模型不存在!',
+    model: "三维模型",
+    pano: "全景图",
+    dataQue: "数据来源",
+    dataSize: "数据大小",
+    dataTime: "拍摄时间",
+    sync: "同屏勘验",
+    syncErr: "带看仅支持{types}类型场景,请添加此类型场景。",
+    repScale: '等比缩放',
+    setScale: '设置比例',
+    opacity: '模型不透明度',
+    registration: '配准',
+    def: '恢复默认',
+    move: '移动',
+    flip: '旋转',
+    defConfirm: '确定恢复默认?此操作无法撤销',
+    len: '长度',
+    reSelect: '重新选点',
+    selectTip: '请选择两点标记一段已知长度,并输入真实长度',
+    vre: '请在当前窗口调整水平方向位置',
+    hre: '请在当前窗口调整垂直方向位置',
+    opacity1: '透明度'
+  },
+};

+ 1 - 1
src/layout/edit/fuse-edit.vue

@@ -9,7 +9,7 @@
 
     <SelectModel v-else>
       <ui-button type="primary" class="add-fuse-model">
-        <ui-icon type="add" />添加数据
+        <ui-icon type="add" />{{ $t('sys.addData') }} 
       </ui-button>
     </SelectModel>
   </template>

+ 2 - 2
src/layout/edit/header/index.vue

@@ -6,7 +6,7 @@
 
     <div class="control">
       <template v-if="isEdit">
-        <ui-button width="105px" @click="leave">退出</ui-button>
+        <ui-button width="105px" @click="leave">{{$t('sys.quit')}}</ui-button>
         <ui-button 
           width="105px" 
           type="primary" 
@@ -14,7 +14,7 @@
           v-if="isOld"
           @click="save"
         >
-          保存
+          {{$t('sys.save')}}
         </ui-button>
       </template>
     </div>

+ 14 - 21
src/layout/edit/scene-select.vue

@@ -1,22 +1,22 @@
 <template>
   <Modal
     width="800px"
-    title="添加场景"
+    :title="$t('scene.add')"
     :visible="visible"
     @ok="okHandler"
     @cancel="visible = false"
-    okText="确定"
-    cancelText="取消"
+    :okText="$t('sys.enter')"
+    :cancelText="$t('sys.cancel')"
     class="model-table"
   >
     <div>
       <div className="model-header">
         <p class="header-desc">
-          已选择数据<span>( {{ rowSelection.selectedRowKeys.length }} )</span>
+          {{$t('material.selectCount')}}<span>( {{ rowSelection.selectedRowKeys.length }} )</span>
         </p>
         <Search
           className="content-header-search"
-          placeholder="输入名称搜索"
+          :placeholder="$t('material.search')"
           v-model:value="keyword"
           allow-clear
           style="width: 244px"
@@ -35,7 +35,7 @@
             />
             <div style="padding: 1px" v-else>
               <Empty
-                description="暂无搜索结果"
+                :description="$t('sys.unSearchData')"
                 :image="Empty.PRESENTED_IMAGE_SIMPLE"
                 className="ant-empty ant-empty-normal"
               />
@@ -45,7 +45,7 @@
       </div>
       <div style="padding: 1px" v-else>
         <Empty
-          :description="keyword.length ? '暂无搜索结果' : '暂无结果'"
+          :description="keyword.length ? $t('sys.unSearchData') : $t('sys.undata')"
           :image="Empty.PRESENTED_IMAGE_SIMPLE"
           className="ant-empty ant-empty-normal"
         />
@@ -58,8 +58,8 @@
       <slot></slot>
       <template #overlay>
         <Menu>
-          <MenuItem @click="visible = true">场景管理</MenuItem>
-          <MenuItem @click="selectModel">媒体库</MenuItem>
+          <MenuItem @click="visible = true">{{ $t('scene.manage') }}</MenuItem>
+          <MenuItem @click="selectModel">{{ $t('material.name') }}</MenuItem>
         </Menu>
       </template>
     </Dropdown>
@@ -92,6 +92,7 @@ import {
 import { SceneType, uploadMaterialToModel, type Scene } from "@/api";
 import { getSceneModel } from "@/sdk";
 import { selectMaterials } from "@/components/materials/quisk";
+import { lang, ui18n } from "@/lang";
 
 type Key = Scene["modelId"];
 
@@ -100,15 +101,7 @@ const Search = Input.Search;
 const selectIds = computed(() => fuseModels.value.filter(item => item.type !== SceneType.SWMX).map((item) => item.modelId));
 const visible = ref(false);
 const keyword = ref("");
-const SceneGroupTypeDesc: any = {
-  [SceneType.SWKK]: 'Mesh场景',
-  [SceneType.SWKJ]: 'Mesh场景',
-  [SceneType.SWSS]: '点云场景',
-  [SceneType.SWSSMX]: 'Mesh场景',
-  [SceneType.SWYDSS]: '点云场景',
-  [SceneType.SWYDMX]: 'Mesh场景',
-  [SceneType.DSFXJ]: 'Mesh场景'
-}
+const SceneGroupTypeDesc: any = lang.scene.typeRaws 
 
 const origin = computed(() =>
   scenes.value.filter(scene => scene.type !== SceneType.SWMX).map((scene) => ({
@@ -153,17 +146,17 @@ const rowSelection: any = ref({
 const cloumns = [
   {
     width: "400px",
-    title: "名称",
+    title: ui18n.t('scene.tabs.name'),
     dataIndex: "name",
     key: "name",
   },
   {
-    title: "类型",
+    title: ui18n.t('scene.tabs.type'),
     dataIndex: "type",
     key: "type",
   },
   {
-    title: "拍摄/创建时间",
+    title: ui18n.t('scene.tabs.createTime'),
     dataIndex: "createTime",
     key: "createTime",
   },

+ 4 - 3
src/layout/model-list/index.vue

@@ -26,14 +26,14 @@
         @click="modelChangeSelect(panoModel, 'fuse')"
         :class="{ active: custom.showMode === 'fuse' }"
       >
-        <ui-icon type="show_3d_n" class="icon" ctrl tip="三维模型" tipV="top" />
+        <ui-icon type="show_3d_n" class="icon" ctrl :tip="$t('fuse.model')" tipV="top" />
       </div>
       <div
         class="mode-icon-layout"
         @click="modelChangeSelect(panoModel, 'pano')"
         :class="{ active: custom.showMode === 'pano' }"
       >
-        <ui-icon type="show_roaming_n" class="icon" ctrl tip="全景图" tipV="top" />
+        <ui-icon type="show_roaming_n" class="icon" ctrl :tip="$t('fuse.pano')" tipV="top" />
       </div>
     </div>
   </Teleport>
@@ -49,6 +49,7 @@ import { fuseModels, getFuseModelShowVariable } from "@/store";
 import type { FuseModel } from "@/store";
 import { currentModel, fuseModel, loadModel } from "@/model";
 import { sdk } from "@/sdk/sdk";
+import { ui18n } from "@/lang";
 
 export type ModelListProps = {
   title?: string;
@@ -56,7 +57,7 @@ export type ModelListProps = {
   showContent?: boolean;
 };
 withDefaults(defineProps<ModelListProps>(), {
-  title: "数据列表",
+  title: ui18n.t('sys.list'),
   change: false,
   showContent: true,
 });

+ 3 - 3
src/layout/model-list/sign.vue

@@ -32,11 +32,11 @@
       </div>
     </div>
     <div class="model-desc" v-if="active">
-      <p><span>数据来源:</span>{{ SceneTypeDesc[model.type] }}</p>
+      <p><span>{{$t('fuse.dataQue')}}:</span>{{ SceneTypeDesc[model.type] }}</p>
       <p v-if="![SceneType.SWSS, SceneType.SWYDSS].includes(model.type)">
-        <span>数据大小:</span>{{ model.size }}
+        <span>{{$t('fuse.dataSize')}}:</span>{{ model.size }}
       </p>
-      <p v-if="model.type !== SceneType.SWMX"><span>拍摄时间:</span>{{ model.time }}</p>
+      <p v-if="model.type !== SceneType.SWMX"><span>{{$t('fuse.dataTime')}}:</span>{{ model.time }}</p>
     </div>
   </div>
 </template>

+ 7 - 6
src/layout/pwd.vue

@@ -1,10 +1,10 @@
 <template>
-  <Modal width="400px" title="访问密码" :visible="visible" centered  class="model-table" :closable="false">
+  <Modal width="400px" :title="$t('sys.viewPWD')" :visible="visible" centered  class="model-table" :closable="false">
     <template #footer>
-      <Button key="submit" type="primary" @click="okHandler">确定</Button>
+      <Button key="submit" type="primary" @click="okHandler">{{$t('sys.enter')}}</Button>
     </template>
-    <FormItem label="访问密码" name="password">
-      <InputPassword v-model:value="password" placeholder="请输入" />
+    <FormItem :label="$t('sys.viewPWD')" name="password">
+      <InputPassword v-model:value="password" :placeholder="$t('sys.placeInput')" />
     </FormItem>
   </Modal>
 </template>
@@ -15,6 +15,7 @@ import { createLoadPack } from '@/utils'
 import { ref } from 'vue';
 import { authSharePassword } from '@/api';
 import { Message } from 'bill/index'
+import { ui18n } from '@/lang';
 
 const visible = ref(true)
 const password = ref('')
@@ -23,7 +24,7 @@ const emit = defineEmits<{ (e: 'close'): void }>()
 
 const okHandler = createLoadPack(async () => {
   if (!password.value) {
-    Message.error("请输入密码!")
+    Message.error(ui18n.t('sys.placPWD'))
     return;
   }
 
@@ -31,7 +32,7 @@ const okHandler = createLoadPack(async () => {
   if (pass) {
     emit('close')
   } else {
-    Message.error("密码错误,请重新输入。")
+    Message.error(ui18n.t('sys.pwdErr'))
   }
 })
 </script>

+ 3 - 3
src/layout/scene-list/index.vue

@@ -39,7 +39,7 @@
             {{ SceneTypeDesc[item.raw.type as SceneType] }}
           </p>
         </div>
-        <Button
+        <!-- <Button
           size="small"
           type="primary"
           ghost
@@ -47,8 +47,8 @@
           v-if="canSync(item as Scene) && !voffline && currentLayout === RoutesName.show"
           @click.stop="sync(item as Scene)"
         >
-          同屏勘验
-        </Button>
+          {{$t('fuse.sync')}}
+        </Button> -->
       </div>
     </template>
   </List>

+ 2 - 0
src/main.ts

@@ -9,10 +9,12 @@ import { currentLayout, RoutesName } from "./router";
 import * as URL from "@/api/constant";
 // import 'ant-design-vue/dist/reset.css';
 import "@/assets/style/global.less";
+import { setupI18n } from "./lang";
 
 const app = createApp(App);
 app.use(Components);
 app.use(router);
+setupI18n(app)
 app.mount("#app");
 
 if (import.meta.env.DEV) {

+ 0 - 1
src/model/app.vue

@@ -150,7 +150,6 @@ export const Model = defineComponent({
       ([type], oldType, onCleanup) => {
         if (type === false) {
           // 手动渲染融合场景
-          console.log("手动渲染!");
           setTimeout(() => {
             (window as any).viewer?.setDisplay(true);
           }, 1000);

+ 3 - 2
src/model/index.ts

@@ -5,6 +5,7 @@ import { reactive, ref } from 'vue'
 
 import type { Scene } from '@/store'
 import type { ModelExpose } from './platform'
+import { ui18n } from '@/lang'
 
 export type FuseModelType = typeof fuseModel
 export type SceneModelType = Pick<Scene, 'type' | 'num'>
@@ -17,7 +18,7 @@ export const currentModel = ref<ModelType>(fuseModel)
 export const modelProps: ModelProps = reactive({ type: currentModel, callback: null })
 export const getModelTypeDesc = (model: ModelType) => {
   if (model === fuseModel) {
-    return '融合场景'
+    return ui18n.t('fuse.label')
   } else {
     return SceneTypeDesc[model.type]
   }
@@ -25,7 +26,7 @@ export const getModelTypeDesc = (model: ModelType) => {
 export const getModelDesc = (model: ModelType) => {
   console.log(model)
   if (model === fuseModel) {
-    return '融合场景'
+    return ui18n.t('fuse.label')
   } else {
     return scenes.value.find(scene => scene.num === model.num && scene.type === model.type)?.name
   }

+ 25 - 22
src/router/constant.ts

@@ -1,3 +1,5 @@
+import { ui18n } from "@/lang";
+
 export enum RoutesName {
   // 编辑融合页面
   fuseEdit = "fuseEdit",
@@ -73,67 +75,68 @@ export const paths = {
 export const metas = {
   [RoutesName.merge]: {
     icon: "joint",
-    title: "拼接",
-    sysTitle: "多元融合",
+    title: ui18n.t('fuse.join'),
+    sysTitle: ui18n.t('fuse.name'),
   },
-  [RoutesName.proportion]: { full: true, sysTitle: "多元融合" },
-  [RoutesName.registration]: { full: true, sysTitle: "多元融合" },
+  [RoutesName.proportion]: { full: true, sysTitle: ui18n.t('fuse.name') },
+  [RoutesName.registration]: { full: true, sysTitle: ui18n.t('fuse.name') },
   [RoutesName.tagging]: {
     icon: "label",
-    title: "标签",
-    sysTitle: "多元融合",
+    title: ui18n.t('tagging.name'),
+    sysTitle: ui18n.t('fuse.name'),
   },
   [RoutesName.guide]: {
     icon: "path",
-    title: "路径",
-    sysTitle: "多元融合",
+    title: ui18n.t('guide.name'),
+    sysTitle: ui18n.t('fuse.name'),
   },
   [RoutesName.measure]: {
     icon: "nav-measure",
-    title: "测量",
-    sysTitle: "多元融合",
+    title: ui18n.t('measure.name') ,
+    sysTitle: ui18n.t('fuse.name'),
   },
   [RoutesName.setting]: {
     icon: "nav-setup",
-    title: "设置",
-    sysTitle: "多元融合",
+    title: ui18n.t('setting.name'),
+    sysTitle: ui18n.t('fuse.name'),
   },
 
   [RoutesName.view]: {
-    sysTitle: "视图提取",
+    sysTitle: ui18n.t('view.name'),
     icon: "nav-setup",
-    title: "视图提取",
+    title: ui18n.t('view.name'),
     left: 'scene-list'
   },
-  [RoutesName.record]: { sysTitle: "屏幕录制" },
+  [RoutesName.record]: { sysTitle: ui18n.t('record.name') },
   [RoutesName.show]: { sysTitle: "" },
 
   [RoutesName.summaryShow]: {
     icon: "list_s",
-    title: "场景列表",
+    title: ui18n.t('scene.list'),
   },
   [RoutesName.viewShow]: {
     icon: "list-scene",
-    title: "视图",
+    title: ui18n.t('view.vName'),
   },
   [RoutesName.fireInfo]: {
     icon: "message_c",
-    title: "案件信息",
+    title:ui18n.t('case.name'),
   },
   [RoutesName.recordShow]: {
     icon: "list-record",
-    title: "录屏",
+    title: ui18n.t('record.vName'),
   },
   [RoutesName.folderShow]: {
     icon: "list-file",
-    title: "卷宗",
+    title: ui18n.t('floder.name')
+    
   },
   [RoutesName.error]: {
-    title: "错误页面",
+    title: ui18n.t('sys.errPage'),
   },
   [RoutesName.security]: {
     icon: "path",
-    title: "安防",
+    title: ui18n.t('security.name'),
   },
 };
 export const ViewHome = RoutesName.merge;

+ 0 - 1
src/sdk/association/index.ts

@@ -24,7 +24,6 @@ export const getSupperPanoModel = () => {
       const data = sdk.canTurnToPanoMode();
       if (data?.model) {
         const smodel = getFuseModel(data.model)!
-        console.log('?优质')
         supperModel.value = smodel;
       } else {
         supperModel.value = null;

+ 2 - 1
src/sdk/association/path.ts

@@ -8,6 +8,7 @@ import { isScenePlayRun, pauseScene, playScene } from "@/utils/full";
 import { analysisPose, setPose } from ".";
 import { custom, showPathsStack, showPathStack } from "@/env";
 import { Message } from "bill/expose-common";
+import { ui18n } from "@/lang";
 
 // -----------------导览线关联--------------------
 export type PathNode = Path;
@@ -52,7 +53,7 @@ export const playScenePath = async (
   const node = getPathNode(path)
   if (!node) {
     console.error('un', path.id)
-    return Message.error('路径所在模型被删除,无法播放');
+    return Message.error(ui18n.t('guide.modelErr'));
   }
 
   showPathsStack.push(ref(false))

+ 2 - 1
src/store/guide.ts

@@ -26,6 +26,7 @@ import {
 } from './guide-path'
 
 import type { Guide as SGuide } from '@/api'
+import { ui18n } from '@/lang'
 
 export type Guide = LocalMode<SGuide, 'cover'>
 export type Guides = Guide[]
@@ -34,7 +35,7 @@ export const guides = ref<Guides>([])
 
 export const createGuide = (guide: Partial<Guide> = {}): Guide => ({
   id: createTemploraryID(),
-  title: `路径${guides.value.length + 1}`,
+  title: ui18n.t('guide.defTitle', {num: guides.value.length + 1}),
   cover: '',
   ...guide
 })

+ 5 - 4
src/store/measure.ts

@@ -21,14 +21,15 @@ import {
 
 import type { Measure as SMeasure } from '@/api'
 import { Message } from 'bill/index'
+import { ui18n } from '@/lang'
 
 export type Measure<T extends MeasureType = MeasureType> = SMeasure<T> & { selected?: boolean }
 export type Measures = Measure[]
 
 export const MeasureTypeMeta = {
-  [MeasureType.area]: { icon: 'h-r', desc: '面积', unitDesc: '面积', unit: 'm²' },
-  [MeasureType.free]: { icon: 'f-l', desc: '自由', unitDesc: '长度', unit: 'm' },
-  [MeasureType.vertical]: { icon: 'v-l', desc: '垂直', unitDesc: '长度', unit: 'm' }
+  [MeasureType.area]: { icon: 'h-r', desc: ui18n.t('measure.area.desc'), unitDesc: ui18n.t('measure.area.unit'), unit: 'm²' },
+  [MeasureType.free]: { icon: 'f-l', desc: ui18n.t('measure.free.desc'), unitDesc: ui18n.t('measure.free.unit'), unit: 'm' },
+  [MeasureType.vertical]: { icon: 'v-l', desc:ui18n.t('measure.vertical.desc'), unitDesc: ui18n.t('measure.vertical.unit'), unit: 'm' }
 }
 
 export const measures = ref<Measures>([])
@@ -85,7 +86,7 @@ export const autoSaveMeasures = autoSetModeCallback(measures, {
   recovery: recoverMeasures,
   save: async () => {
     if (!measures.value.every(record => record.title)) {
-      Message.warning('测量名称不可为空')
+      Message.warning(ui18n.t('measure.nameErr'))
       throw '测量名称不可为空'
     }
 

+ 3 - 2
src/store/path.ts

@@ -21,6 +21,7 @@ import {
 import type { Path } from '@/api'
 import { custom, params } from '@/env'
 import { Message } from 'bill/expose-common'
+import { ui18n } from '@/lang'
 
 export  type { Path } from '@/api'
 export type Paths = Path[]
@@ -99,10 +100,10 @@ export const autoSavePaths = autoSetModeCallback(paths, {
   recovery: recoverPaths,
   save: async () => {
     if (!paths.value.every(record => record.name)) {
-      Message.warning('路径名称不可为空')
+      Message.warning(ui18n.t('guide.nameErr'))
       throw '路径名称不可为空'
     } else if (paths.value.some(path => path.points.length <= 1)) {
-      Message.warning('路径点不可少于两个')
+      Message.warning(ui18n.t('guide.pErr'))
       throw '路径点不可少于两个'
     }
     await savePaths()

+ 3 - 2
src/store/record.ts

@@ -32,6 +32,7 @@ import {
 
 import type { Record as SRecord } from '@/api'
 import { Message } from "bill/index";
+import { ui18n } from "@/lang";
 
 export type Record = LocalMode<SRecord, 'cover'> 
 export type Records = Record[]
@@ -40,7 +41,7 @@ export const records = ref<Records>([])
 
 export const createRecord = (record: Partial<Record> = {}): Record => ({
   id: createTemploraryID(),
-  title: '讲解视频' + (records.value.length + 1),
+  title: ui18n.t('record.defName', {num: records.value.length + 1}),
   cover: '',
   url: '',
   status: RecordStatus.UN,
@@ -122,7 +123,7 @@ export const autoSaveRecords = autoSetModeCallback(
     recovery: togetherCallback([recoverRecordFragments, recoverRecords]),
     save: async () => {
       if (!records.value.every(record => record.title)) {
-        Message.warning('视频名称不可为空')
+        Message.warning(ui18n.t('record.nameErr'))
         throw '视频名称不可为空'
       }
 

+ 3 - 2
src/store/scene.ts

@@ -5,6 +5,7 @@ import { fetchStoreItems } from '@/utils'
 import type { Scene, Scenes } from '@/api'
 import { Dialog } from 'bill/expose-common'
 import { getUserInfo } from '@/api/user'
+import { ui18n } from '@/lang'
 export type { Scene, Scenes } from '@/api'
 
 
@@ -45,9 +46,9 @@ export const getSWKKSyncLink = async (scene: Scene) => {
 
   let msg: string | null = null;
   if (!kkScenes.length) {
-    msg = `带看仅支持${supportTypes
+    msg = ui18n.t('fuse.syncErr', {types: supportTypes
       .map((type) => SceneTypeDesc[type])
-      .join("、")}类型场景,请添加此类型场景。`;
+      .join("、")});
   }
   if (msg) {
     Dialog.alert(msg);

+ 3 - 2
src/store/sys.ts

@@ -5,6 +5,7 @@ import { useViewStack } from "@/hook";
 
 import type { UnwrapRef } from "vue";
 import { currentMeta } from "@/router";
+import { ui18n } from "@/lang";
 
 const Flags = {
   EDIT: 0b10,
@@ -22,7 +23,7 @@ export const isNow = computed(() => !!(mode.value & Flags.NOW));
 export const appEl = ref<HTMLDivElement | null>(null);
 export const prefix = ref("");
 
-export const defTitle = ref("案件信息");
+export const defTitle = ref(ui18n.t('case.name');
 export const title = computed(() => {
   console.error(currentMeta.value);
   const last =
@@ -82,7 +83,7 @@ export const save = async () => {
 export const leave = async () => {
   if (
     isOld.value &&
-    !(await Dialog.confirm("您有操作未保存,确定要退出吗?"))
+    !(await Dialog.confirm(ui18n.t('sys.unSaveMsg')))
   ) {
     return;
   }

+ 3 - 2
src/store/view.ts

@@ -21,6 +21,7 @@ import { fuseModel } from '@/model'
 import type { View as SView } from '@/api'
 import type { ModelType } from '@/model'
 import { Message } from "bill/expose-common";
+import { ui18n } from "@/lang";
 
 export type View = LocalMode<SView, 'cover'>
 export type Views = View[]
@@ -30,7 +31,7 @@ export const views = ref<Views>([])
 export const createView = (view: Partial<View> = {}): View => {
   const base = {
     id: createTemploraryID(),
-    title: '视图' + (views.value.length + 1),
+    title: ui18n.t('view.defName', {num: views.value.length + 1}),
     cover: 'https://4dkk.4dage.com/scene_view_data/KK-t-F8e5M46wcQ/images/floor_0.png?t=1659422513133?v=0&rnd=0.9219648338739086&x-oss-process=image/resize,m_fill,w_80,h_60/quality,q_70&rnd=0.25420557086595965',
     flyData: '',
     num: null,
@@ -104,7 +105,7 @@ export const autoSaveViews = autoSetModeCallback(views, {
   recovery: recoverViews,
   save: async () => {
     if (!views.value.every(view => view.title)) {
-      Message.warning('视图名称不可为空')
+      Message.warning(ui18n.t('view.nameErr'))
       throw '视图名称不可为空'
     }
     for (let i = 0; i < views.value.length; i++) {

+ 89 - 0
src/utils/store.ts

@@ -0,0 +1,89 @@
+type Store = typeof localStorage | typeof sessionStorage
+
+type SetTransform<T> = (args: T) => string
+type GetTransform<T> = (args: null | string) => T
+
+export function get(store: Store, name: string): string | null
+export function get<T>(
+  store: Store,
+  name: string,
+  transform: GetTransform<T>
+): T
+export function get<T>(
+  store: Store,
+  name: string,
+  transform?: GetTransform<T>
+) {
+  const value = store.getItem(name)
+  if (transform) {
+    return transform(value)
+  } else {
+    return value
+  }
+}
+
+export function set(store: Store, name: string, value: string): string
+export function set<T>(
+  store: Store,
+  name: string,
+  value: T,
+  transform: SetTransform<T>
+): string
+export function set<T>(
+  store: Store,
+  name: string,
+  value: T,
+  transform?: SetTransform<T>
+) {
+  store.setItem(name, transform ? transform(value) : (value as string))
+  return value
+}
+
+export function getFactory(store: Store): (name: string) => string | null
+export function getFactory<T>(
+  store: Store,
+  transform: GetTransform<T>
+): (name: string | null) => T
+export function getFactory<T>(store: Store, transform?: GetTransform<T>) {
+  return (name: string | null) =>
+    transform ? get(store, name!, transform) : get(store, name!)
+}
+
+export function setFactory(
+  store: Store
+): (name: string, value: string) => string
+export function setFactory<T>(
+  store: Store,
+  transform: SetTransform<T>
+): (name: string, value: T) => string
+export function setFactory<T>(store: Store, transform?: SetTransform<T>) {
+  return (name: string, value: T) =>
+    transform
+      ? set(store, name, transform(value))
+      : set(store, name, value as string)
+}
+
+export function localGetFactory(): (name: string) => string | null
+export function localGetFactory<T>(
+  transform: GetTransform<T>
+): (name: string | null) => T
+export function localGetFactory<T>(transform?: GetTransform<T>) {
+  return getFactory(localStorage, transform!)
+}
+
+export function localSetFactory(): (name: string, value: string) => string
+export function localSetFactory<T>(
+  transform: SetTransform<T>
+): (name: string, value: T) => string
+export function localSetFactory<T>(transform?: SetTransform<T>) {
+  return setFactory(localStorage, transform!)
+}
+export function localDel(key: string) {
+  localStorage.removeItem(key)
+}
+
+export const local = {
+  get: localGetFactory(),
+  set: localSetFactory(),
+  del: localDel
+}

+ 1 - 1
src/views/error/index.vue

@@ -2,7 +2,7 @@
   <div class="error">
     <div>
       <img src="@/assets/index.png" />
-      <p>资源不存在或已删除</p>
+      <p>{{$t('sys.404Page')}}</p>
     </div>
   </div>
 </template>

+ 12 - 20
src/views/folder/fire/index.vue

@@ -7,13 +7,13 @@
     :footer="null"
   >
     <Info
-      title="案件信息"
+      :title="$t('case.name')"
       :data="caseProject"
       :label-map="tmLabelMap1"
       v-if="caseProject"
     />
     <Info
-      title="勘验信息"
+      :title="$t('case.tmName')"
       :data="caseProject.tmProject"
       :label-map="tmLabelMap2"
       v-if="caseProject?.tmProject"
@@ -30,6 +30,7 @@ import router, { RoutesName } from "@/router";
 import { title } from "@/store";
 import { caseProject } from "@/store/case";
 import { ref } from "vue";
+import { lang } from "@/lang";
 
 defineProps<{ open: boolean }>();
 defineEmits<{ (e: "update:open", v: boolean): void }>();
@@ -37,32 +38,23 @@ defineEmits<{ (e: "update:open", v: boolean): void }>();
 type LabelMap = Record<string, string | [string, (v: any) => any]>;
 
 const tmLabelMap1 = {
-  caseTitle: "案件名称",
-  caseNum: "立案编号",
-  caseCategory: "案件类别",
-  crimeTime: "案发时间",
-  homicideCase: ["是否命案", (v: any) => (v === null ? "" : v ? "是" : "否")],
+  ...lang.case.cols,
+  homicideCase: [lang.case.cols.homicideCase, (v: any) => (v === null ? "" : v ? lang.case.yv : lang.case.nv)],
   criminalCase: [
-    "是否刑案",
+    lang.case.cols.criminalCase,
     (v: any) => {
       console.error(v);
-      return v === null ? "" : v ? "是" : "否";
+      return v === null ? "" : v ?lang.case.yv : lang.case.nv;
     },
   ],
-  caseRegion: ["案发区域", (v: string[]) => v.join("-")],
-  caseAddress: "案发地点",
-  latAndLong: ["经纬度", (v: any) => (v ? v.split(",").reverse().join(",") : "")],
+  caseRegion: [lang.case.cols.caseRegion, (v: string[]) => v.join("-")],
+  caseAddress: lang.case.cols.caseAddress,
+  latAndLong: [lang.case.cols.latAndLong, (v: any) => (v ? v.split(",").reverse().join(",") : "")],
 } as LabelMap;
 
 const tmLabelMap2 = {
-  commandTime: "指挥中心电话时间",
-  alarmTime: "报警时间",
-  alarmName: "报警人",
-  assignDept: "指派/报告单位",
-  inquestDept: "现场勘验单位",
-  assignType: "指派方式",
-  inquestAddress: "勘验地点",
-  times: ["勘验时间", (v: string[]) => v.join("到")],
+  ...lang.case.tmCols,
+  times: [lang.case.tmCols.times, (v: string[]) => v.join("-")],
 } as LabelMap;
 </script>
 

+ 1 - 1
src/views/folder/index.vue

@@ -2,7 +2,7 @@
   <LeftPano>
     <div class="types">
       <h2 @click="showInfo = true">
-        案件概要
+        {{ $t('case.summary') }}
         <ui-icon :type="`pull-${showInfo ? 'up' : 'down'}`" class="icon" ctrl />
       </h2>
     </div>

+ 8 - 7
src/views/guide/guide/edit-paths.vue

@@ -13,13 +13,13 @@
         width="200px"
         :class="{ disabled: isScenePlayIng }"
       >
-        添加视角
+        {{ $t('guide.guide.add') }}
       </ui-button>
     </div>
     <div class="info" v-if="paths.length">
       <div class="meta">
         <div class="length">
-          <span>视频时长</span>{{ paths.reduce((t, c) => t + c.time, 0).toFixed(1) }}s
+          <span>{{ $t('guide.guide.time') }}</span>{{ paths.reduce((t, c) => t + c.time, 0).toFixed(1) }}s
         </div>
         <div
           class="fun-ctrl clear"
@@ -27,7 +27,7 @@
           :class="{ disabled: isScenePlayIng }"
         >
           <ui-icon type="del" />
-          <span>清空画面</span>
+          <span>{{ $t('guide.guide.clear') }}</span>
         </div>
       </div>
 
@@ -76,7 +76,7 @@
         </template>
       </div>
     </div>
-    <p class="un-video" v-else>暂无导览</p>
+    <p class="un-video" v-else>{{ $t('guide.guide.undata') }}</p>
   </div>
 </template>
 
@@ -112,6 +112,7 @@ import {
 
 import type { Guide, GuidePaths, GuidePath } from "@/store";
 import type { CalcPathProps } from "@/sdk";
+import { ui18n } from "@/lang";
 
 const props = defineProps<{ data: Guide }>();
 const paths = ref<GuidePaths>(getGuidePaths(props.data));
@@ -136,7 +137,7 @@ useAutoSetMode(
   {
     save() {
       if (!paths.value.length) {
-        Dialog.alert("无法保存空路径导览!");
+        Dialog.alert(ui18n.t('guide.guide.unItems'));
         throw "无法保存空路径导览!";
       }
       const oldPaths = getGuidePaths(props.data);
@@ -175,7 +176,7 @@ const addPath = () => {
 };
 
 const deletePath = async (path: GuidePath, fore: boolean = false) => {
-  if (fore || (await Dialog.confirm("确定要删除此画面吗?"))) {
+  if (fore || (await Dialog.confirm(ui18n.t('guide.guide.delConfirm')))) {
     const index = paths.value.indexOf(path);
     if (~index) {
       paths.value.splice(index, 1);
@@ -187,7 +188,7 @@ const deletePath = async (path: GuidePath, fore: boolean = false) => {
 };
 
 const deleteAll = async () => {
-  if (await Dialog.confirm("确定要清空画面吗?")) {
+  if (await Dialog.confirm(ui18n.t('guide.guide.clearConfirm'))) {
     paths.value.length = 0;
     current.value = paths.value[0];
   }

+ 1 - 1
src/views/guide/guide/edit.vue

@@ -3,7 +3,7 @@
     <template #header>
       <ui-button @click="edit(createGuide())">
         <ui-icon type="add" />
-        新增
+        {{ $t('sys.add') }}
       </ui-button>
     </template>
   </ui-group>

+ 6 - 5
src/views/guide/guide/sign.vue

@@ -44,6 +44,7 @@ import { playSceneGuide, isScenePlayIng, pauseSceneGuide } from "@/sdk";
 import { VideoRecorder } from "simaqcore";
 import useFocus from "bill/hook/useFocus";
 import { Message } from "bill/expose-common";
+import { ui18n } from "@/lang";
 
 const props = withDefaults(defineProps<{ guide: Guide; edit?: boolean }>(), {
   edit: true,
@@ -54,7 +55,7 @@ const isEditTitle = useFocus(computed(() => inputRef.value?.vmRef.root));
 watchEffect(() => {
   if (!isEditTitle.value && !props.guide.title.length) {
     isEditTitle.value = true;
-    Message.warning("测量名称不可为空");
+    Message.warning(ui18n.t('measure.nameErr'));
   }
 });
 
@@ -66,9 +67,9 @@ const emit = defineEmits<{
 }>();
 
 const menus = [
-  { label: "重命名", value: "editTitle" },
-  { label: "编辑", value: "edit" },
-  { label: "删除", value: "delete" },
+  { label: ui18n.t('sys.rename'), value: "editTitle" },
+  { label: ui18n.t('sys.edit'), value: "edit" },
+  { label: ui18n.t('sys.del'), value: "delete" },
 ];
 const actions = {
   edit: () => emit("edit"),
@@ -107,7 +108,7 @@ const actions = {
 
     videoRecorder.on("record", (blob) => {
       saveAs(
-        new File([blob], "录屏.mp4", { type: "video/mp4; codecs=h264" }),
+        new File([blob], ui18n.t('record.vName') + ".mp4", { type: "video/mp4; codecs=h264" }),
         props.guide.title + ".mp4"
       );
     });

+ 5 - 4
src/views/guide/index.vue

@@ -23,15 +23,16 @@ import GuideEdit from "./guide/edit.vue";
 import PathEdit from "./path/edit.vue";
 import { reactive, ref, watchEffect } from "vue";
 import { guides, isEdit, paths } from "@/store";
+import { ui18n } from "@/lang";
 
 const current = ref("path");
 const tabs = reactive([
-  { key: "path", text: "路线()" },
-  { key: "guide", text: "导览()" },
+  { key: "path", text: ui18n.t('guide.pathName', {count: '0'})},
+  { key: "guide", text: ui18n.t('guide.guideName', {count:'0'}) },
 ]);
 watchEffect(() => {
-  tabs[1].text = `导览(${guides.value.length})`;
-  tabs[0].text = `路线(${paths.value.length})`;
+  tabs[0].text = ui18n.t('guide.pathName', {count: paths.value.length});
+  tabs[1].text = ui18n.t('guide.guideName',{count: guides.value.length});
 });
 </script>
 

+ 14 - 18
src/views/guide/path/edit-path.vue

@@ -3,11 +3,11 @@
     <RightFillPano class="edit-path-point">
       <div v-show="!~activePointNdx" :class="{ disabled: !data.points.length }">
         <ui-group
-          :title="`${isTemploraryID(data.id) ? '创建' : '编辑'}路线`"
+          :title="`${isTemploraryID(data.id) ? $t('sys.create') : $t('sys.edit')}${$t('guide.path.name')}`"
           borderBottom
         >
           <ui-group-option class="item">
-            <span class="label">路径名称</span>
+            <span class="label">{{$t('guide.path.title')}}</span>
             <span class="oper"> <Switch v-model:checked="data.showName" /> </span>
           </ui-group-option>
           <ui-group-option class="item">
@@ -16,14 +16,14 @@
               type="text"
               ref="nameInput"
               class="nameInput"
-              placeholder="路径名称"
+              :placeholder="$t('guide.path.title')"
               @keydown="keydownHandler"
               v-model="data.name"
               :maxlength="100"
             />
           </ui-group-option>
           <ui-group-option class="item">
-            <span class="label">路径粗细</span>
+            <span class="label">{{$t('guide.path.lineWidth')}}</span>
             <span class="oper">
               <InputNumber
                 style="width: 120px"
@@ -53,7 +53,7 @@
             </span>
           </ui-group-option>
           <ui-group-option class="item">
-            <span class="label">路径颜色</span>
+            <span class="label">{{$t('guide.path.lineColor')}}</span>
             <span class="oper">
               <ui-input
                 type="color"
@@ -64,26 +64,26 @@
             </span>
           </ui-group-option>
           <ui-group-option class="item">
-            <span class="label">路径箭头</span>
+            <span class="label">{{$t('guide.path.showDirection')}}</span>
             <span class="oper">
               <Switch v-model:checked="data.showDirection" />
             </span>
           </ui-group-option>
           <ui-group-option class="item" v-if="data.showDirection">
-            <span class="label">箭头反向</span>
+            <span class="label">{{$t('guide.path.reverseDirection')}}</span>
             <span class="oper"> <Switch v-model:checked="data.reverseDirection" /> </span>
           </ui-group-option>
         </ui-group>
         <ui-group borderBottom>
           <ui-group-option>
-            <span>文字大小</span>
+            <span>{{$t('guide.path.fontSize')}}</span>
             <Slider v-model:value="data.fontSize" :min="12" :max="60" :step="0.1" />
           </ui-group-option>
         </ui-group>
         <ui-group borderBottom>
           <ui-group-option>
             <SignItem
-              label="可见范围"
+              :label="$t('guide.path.visibilityRange')"
               v-if="!data.globalVisibility"
               @apply-global="$emit('applyGlobal', 'visibilityRange')"
             >
@@ -101,7 +101,7 @@
               <template v-slot:label>
                 <ui-input
                   type="checkbox"
-                  label="全部范围可视"
+                  :label="$t('guide.path.globalVisibility')"
                   :modelValue="!!data.globalVisibility"
                   @update:modelValue="(v: boolean) => data.globalVisibility = v"
                 />
@@ -117,26 +117,25 @@
           @click="switchPlay"
           v-if="data.points.length"
         >
-          {{ isScenePathPlayIng ? "停止" : "" }}预览路径
+          {{ isScenePathPlayIng ? $t('guide.path.stop') : "" }}{{$t('guide.path.preview')}}
         </Button>
       </div>
       <div v-if="~activePointNdx">
-        <ui-group title="编辑点" borderBottom>
-          <ui-group-option>描述:</ui-group-option>
+        <ui-group :title="$t('guide.path.pointTitle')" borderBottom>
+          <ui-group-option>{{$t('guide.path.pointDesc')}}:</ui-group-option>
           <ui-group-option>
             <ui-input
               class="input"
               width="100%"
               height="158px"
               v-model="data.points[activePointNdx].name"
-              placeholder="特征描述:"
               type="textarea"
               :maxlength="200"
             />
           </ui-group-option>
         </ui-group>
         <Button block danger type="primary" ghost size="large" @click="deletePoint">
-          删除
+          {{$t('sys.del')}}
         </Button>
       </div>
     </RightFillPano>
@@ -199,7 +198,6 @@ onUnmounted(() => {
 const nameInput = ref();
 const keydownHandler = (event: KeyboardEvent) => {
   if (event.key === "z" && (event.ctrlKey || event.composed)) {
-    console.log("组织");
     // 阻止默认的撤销行为
     event.preventDefault();
   }
@@ -259,9 +257,7 @@ watch(
     $node.changeEditMode(true);
     $node.bus.on("activePoint", handler);
 
-    console.log("监听");
     onCleanup(() => {
-      console.log("取消监听");
       $node.bus.off("activePoint", handler);
       $node.changeEditMode(false);
     });

+ 3 - 2
src/views/guide/path/edit.vue

@@ -3,7 +3,7 @@
     <template #header>
       <ui-button @click="edit()">
         <ui-icon type="add" />
-        新增
+        {{ $t('sys.add') }}
       </ui-button>
     </template>
   </ui-group>
@@ -39,6 +39,7 @@ import type { Path } from "@/store";
 import { Dialog } from "bill/expose-common";
 import { showPathsStack, showPathStack } from "@/env";
 import { asyncTimeout } from "@/utils";
+import { ui18n } from "@/lang";
 
 const currentPath = ref<Path | null>();
 const leaveEdit = () => {
@@ -66,7 +67,7 @@ const deletePath = (path: Path) => {
   paths.value.splice(index, 1);
 };
 const applyGlobal = async (keys: string | string[]) => {
-  if (!(await Dialog.confirm("确定要将此属性应用到所有位置?"))) return;
+  if (!(await Dialog.confirm(ui18n.t('guide.path.applyConfirm')))) return;
   keys = Array.isArray(keys) ? keys : [keys];
   for (const current of paths.value!) {
     let val: any = current;

+ 3 - 2
src/views/guide/path/sign.vue

@@ -39,6 +39,7 @@ import { Path } from "@/store";
 import { getPathNode, playScenePath } from "@/sdk/association/path";
 import { computed, ref, watch, watchEffect } from "vue";
 import { custom } from "@/env";
+import { ui18n } from "@/lang";
 
 const props = withDefaults(defineProps<{ path: Path; edit?: boolean }>(), {
   edit: true,
@@ -50,8 +51,8 @@ const emit = defineEmits<{
 }>();
 
 const menus = [
-  { label: "编辑", value: "edit" },
-  { label: "删除", value: "delete" },
+  { label: ui18n.t('sys.edit'), value: "edit" },
+  { label: ui18n.t('sys.del'), value: "delete" },
 ];
 const actions = {
   edit: () => emit("edit"),

+ 4 - 3
src/views/guide/show.vue

@@ -1,5 +1,5 @@
 <template>
-  <ui-group title="标签列表" class="show-taggings">
+  <ui-group :title="$t('tagging.list')" class="show-taggings">
     <template #header>
       <Dropdown placement="bottom">
         <span class="tab-span">
@@ -40,11 +40,12 @@ import { guides, paths } from "@/store";
 import { Menu, Dropdown } from "ant-design-vue";
 import { DownOutlined } from "@ant-design/icons-vue";
 import { custom } from "@/env";
+import { ui18n } from "@/lang";
 
 const currentKey = ref("path");
 const tabs = computed(() => [
-  { key: "path", text: "路线", count: paths.value.length },
-  { key: "guide", text: "导览", count: guides.value.length },
+  { key: "path", text: ui18n.t('guide.path.name'), count: paths.value.length },
+  { key: "guide", text: ui18n.t('guide.guide.name'), count: guides.value.length },
 ]);
 const current = computed(() => tabs.value.find((i) => i.key === currentKey.value)!);
 const items = computed(() => {

+ 1 - 1
src/views/measure/index.vue

@@ -7,7 +7,7 @@
         </template>
       </ui-group>
     </template>
-    <ui-group title="测量列表" class="measure-list">
+    <ui-group :title="$t('measure.list')" class="measure-list">
       <template #icon>
         <ui-icon
           ctrl

+ 1 - 1
src/views/measure/show.vue

@@ -1,5 +1,5 @@
 <template>
-  <ui-group title="测量列表" class="show-measures">
+  <ui-group :title="$t('measure.list')" class="show-measures">
     <template #icon>
       <ui-icon 
         ctrl

+ 4 - 3
src/views/measure/sign.vue

@@ -51,6 +51,7 @@ import type { Measure } from "@/store";
 import { computed, ref, watch, watchEffect } from "vue";
 import { Message } from "bill/index";
 import { custom } from "@/env";
+import { ui18n } from "@/lang";
 
 const props = withDefaults(defineProps<{ measure: Measure; edit?: boolean }>(), {
   edit: true,
@@ -63,8 +64,8 @@ const emit = defineEmits<{
 const inputRef = ref();
 const isEditTitle = useFocus(computed(() => inputRef.value?.vmRef.root));
 const menus = [
-  { label: "重命名", value: "rename" },
-  { label: "删除", value: "delete" },
+  { label: ui18n.t('sys.rename'), value: "rename" },
+  { label: ui18n.t('sys.del'), value: "delete" },
 ];
 const actions = {
   delete: () => emit("delete"),
@@ -74,7 +75,7 @@ const actions = {
 watchEffect(() => {
   if (!isEditTitle.value && !props.measure.title.length) {
     isEditTitle.value = true;
-    Message.warning("测量名称不可为空");
+    Message.warning(ui18n.t('measure.nameErr'));
   }
 });
 

+ 9 - 8
src/views/merge/index.vue

@@ -7,7 +7,7 @@
       <template #header>
         <Actions class="edit-header" :items="actionItems" v-model:current="currentItem" />
       </template>
-      <ui-group-option label="等比缩放">
+      <ui-group-option :label="$t('fuse.repScale')">
         <template #icon>
           <a
             class="set-prop"
@@ -16,7 +16,7 @@
               name: RoutesName.proportion, 
               params: { id: custom.currentModel!.id, save: '1' },
             })"
-            >设置比例</a
+            >{{$t('fuse.setScale')}}</a
           >
         </template>
         <ui-input
@@ -40,7 +40,7 @@
           <template #icon>m</template>
         </ui-input>
       </ui-group-option> -->
-      <ui-group-option label="模型不透明度">
+      <ui-group-option :label="$t('fuse.opacity')">
         <ui-input
           type="range"
           v-model="custom.currentModel.opacity"
@@ -59,11 +59,11 @@
             name: RoutesName.registration, 
             params: {id: custom.currentModel!.id, save: '1' } 
           })"
-          >配准</ui-button
+          >{{$t('fuse.registration')}}</ui-button
         >
       </ui-group-option>
       <ui-group-option>
-        <ui-button @click="reset">恢复默认</ui-button>
+        <ui-button @click="reset">{{$t('fuse.def')}}</ui-button>
       </ui-group-option>
     </ui-group>
   </RightPano>
@@ -88,12 +88,13 @@ import { Dialog } from "bill/expose-common";
 import Actions from "@/components/actions/index.vue";
 
 import type { ActionsProps, ActionsItem } from "@/components/actions/index.vue";
+import { ui18n } from "@/lang";
 
 const active = useActive();
 const actionItems: ActionsProps["items"] = [
   {
     icon: "move",
-    text: "移动",
+    text: ui18n.t('fuse.move'),
     action: () => {
       getSceneModel(custom.currentModel)?.enterMoveMode();
       return () => getSceneModel(custom.currentModel)?.leaveTransform();
@@ -101,7 +102,7 @@ const actionItems: ActionsProps["items"] = [
   },
   {
     icon: "flip",
-    text: "旋转",
+    text: ui18n.t('fuse.flip'),
     action: () => {
       getSceneModel(custom.currentModel)?.enterRotateMode();
       return () => {
@@ -127,7 +128,7 @@ watch(
 );
 
 const reset = async () => {
-  if (custom.currentModel && (await Dialog.confirm("确定恢复默认?此操作无法撤销"))) {
+  if (custom.currentModel && (await Dialog.confirm(ui18n.t('fuse.defConfirm')))) {
     const rotation = getSceneModel(custom.currentModel)!.getDefaultRotation();
     Object.assign(custom.currentModel, {
       ...defaultFuseModelAttrs,

+ 4 - 3
src/views/proportion/index.vue

@@ -1,6 +1,6 @@
 <template>
   <ui-editor-toolbar toolbar v-if="sceneModel">
-    <span>长度:</span>
+    <span>{{$t('fuse.len')}}:</span>
     <ui-input
       type="number"
       width="120px"
@@ -11,7 +11,7 @@
     >
       <template #icon>m</template>
     </ui-input>
-    <ui-button type="submit" width="160px" @click="resetMeasure">重新选点</ui-button>
+    <ui-button type="submit" width="160px" @click="resetMeasure">{{$t('fuse.reSelect')}}</ui-button>
   </ui-editor-toolbar>
 </template>
 
@@ -26,6 +26,7 @@ import { currentModelStack } from "@/env";
 
 import type { ScaleSet } from "@/sdk";
 import { round } from "@/utils";
+import { ui18n } from "@/lang";
 
 const isCurrent = computed(
   () => router.currentRoute.value.name === RoutesName.proportion
@@ -74,7 +75,7 @@ watchEffect((onCleanup) => {
 });
 
 useViewStack(() => {
-  const hide = Message.show({ msg: "请选择两点标记一段已知长度,并输入真实长度" });
+  const hide = Message.show({ msg:ui18n.t('fuse.selectTip')});
   return () => {
     hide();
     length.value = null;

+ 6 - 5
src/views/record/index.vue

@@ -2,11 +2,11 @@
   <RightFillPano>
     <template #header>
       <div class="btns header-btns">
-        <ui-button class="start" @click="start" type="primary">开始录制</ui-button>
+        <ui-button class="start" @click="start" type="primary">{{ $t('record.start') }}</ui-button>
       </div>
     </template>
 
-    <ui-group title="全部视频" class="tree">
+    <ui-group :title="$t('record.list')" class="tree">
       <Draggable :list="records" draggable=".sign" itemKey="id">
         <template #item="{ element: record }">
           <Sign
@@ -47,6 +47,7 @@ import { Dialog } from "bill/index";
 import { initialTaggings } from "@/store/tagging";
 import { initialMeasures } from "@/store/measure";
 import { initialGuides } from "@/store/guide";
+import { ui18n } from "@/lang";
 
 initialRecords();
 initialTaggingStyles();
@@ -58,7 +59,7 @@ initialPaths();
 const start = () => records.value.push(createRecord());
 const deleteRecord = async (record: Record) => {
   const isTemp = getRecordFragmentBlobs(record).length === 0 && isTemploraryID(record.id);
-  if (isTemp || (await Dialog.confirm("确定要删除视频吗?"))) {
+  if (isTemp || (await Dialog.confirm($t('record.delConfirm')))) {
     const index = records.value.indexOf(record);
     if (~index) {
       records.value.splice(index, 1);
@@ -72,8 +73,8 @@ const getSignRecord = (record: Record): RecordProcess => ({
 });
 
 const setOptions = [
-  { value: "tagging", label: "标签" },
-  { value: "measure", label: "测量" },
+  { value: "tagging", label: ui18n.t('record.tag') },
+  { value: "measure", label: ui18n.t('measure.name') },
 ] as const;
 
 type SetKey = typeof setOptions[number]["value"];

+ 6 - 9
src/views/record/shot.vue

@@ -1,18 +1,14 @@
 <template>
   <teleport :to="appEl" v-if="appEl">
-    <!-- <div class="countdown strengthen" v-if="!custom.showBottomBar && countdown">
-      <p class="title"><span>{{countdown}}</span>秒后开始录制</p>
-      <p>按ESC可暂停录制</p>
-    </div> -->
 
     <ui-editor-toolbar :toolbar="custom.showBottomBar" class="shot-ctrl">
-      <ui-button type="submit" class="btn" @click="close">取消</ui-button>
+      <ui-button type="submit" class="btn" @click="close">{{$t('sys.cancel')}}</ui-button>
       <ui-button
         type="primary"
         class="btn"
         @click="complete"
         :class="{ disabled: blobs.length === 0 }"
-        >合并视频</ui-button
+        >{{$t('record.merge')}}</ui-button
       >
       <div class="other" :style="{ bottom: barHeight }">
         <ui-icon
@@ -20,7 +16,7 @@
           type="video1"
           ctrl
           @click="start"
-          tip="继续录制"
+          :tip="$t('record.con')"
           tipV="top"
         />
       </div>
@@ -80,6 +76,7 @@ import { appEl } from "@/store";
 import { useViewStack } from "@/hook";
 import { currentModel } from "@/model";
 import { Message } from "bill/expose-common";
+import { ui18n } from "@/lang";
 
 export default defineComponent({
   props: {
@@ -115,7 +112,7 @@ export default defineComponent({
     let recordIng = ref(false);
     const start = () => {
       if (size.value > MAX_SIZE || pauseTime.value < 2000) {
-        return Message.warning("已超出限制大小无法继续录制,可保存后继续录制!");
+        return Message.warning(ui18n.t('record.sizeErr'));
       }
 
       showBottomBar.value = false;
@@ -168,7 +165,7 @@ export default defineComponent({
     videoRecorder.off("*");
     videoRecorder.on("record", (blob) => {
       if (recordIng.value) {
-        blobs.push(new File([blob], "录屏.mp4", { type: "video/mp4; codecs=h264" }));
+        blobs.push(new File([blob], ui18n.t('record.vName') + ".mp4", { type: "video/mp4; codecs=h264" }));
       }
     });
     videoRecorder.on("cancelRecord", pause);

+ 7 - 6
src/views/record/sign.vue

@@ -25,7 +25,7 @@
       />
       <div class="title" v-show="!isEditTitle">
         <p>{{ record.title }}</p>
-        <span v-if="record.status === RecordStatus.RUN">后台正在处理</span>
+        <span v-if="record.status === RecordStatus.RUN">{{$t('record.backHandler')}}</span>
       </div>
     </div>
     <div class="action" v-if="edit && record.status === RecordStatus.SUCCESS">
@@ -70,6 +70,7 @@ import { getResource } from "@/env";
 import Shot from "./shot.vue";
 import type { RecordProcess } from "./help";
 import { Message } from "bill/index";
+import { ui18n } from "@/lang";
 
 export default defineComponent({
   props: {
@@ -93,15 +94,15 @@ export default defineComponent({
       const base = [];
       if ([RecordStatus.SUCCESS, RecordStatus.UN].includes(props.record.status)) {
         base.push(
-          { label: "重命名", value: "rename" },
-          { label: "继续录制", value: "continue" }
+          { label: ui18n.t('sys.rename'), value: "rename" },
+          { label: ui18n.t('record.con'), value: "continue" }
         );
 
         if (props.record.status === RecordStatus.SUCCESS) {
-          base.push({ label: "下载", value: "download" });
+          base.push({ label: ui18n.t('sys.download'), value: "download" });
         }
       }
-      base.push({ label: "删除", value: "delete" });
+      base.push({ label: ui18n.t('sys.del'), value: "delete" });
       return base;
     });
 
@@ -112,7 +113,7 @@ export default defineComponent({
     watchEffect(() => {
       if (!isEditTitle.value && !props.record.title.length) {
         isEditTitle.value = true;
-        Message.warning("视频名称不可为空");
+        Message.warning(ui18n.t('record.nameErr'));
       }
     });
 

+ 5 - 5
src/views/registration/index.vue

@@ -61,8 +61,8 @@
       </div>
     </div> -->
 
-    <div class="ui-message tip-left">请在当前窗口调整水平方向位置</div>
-    <div class="ui-message tip-right">请在当前窗口调整垂直方向位置</div>
+    <div class="ui-message tip-left">{{$t('fuse.vre')}}</div>
+    <div class="ui-message tip-right">{{$t('fuse.hre')}}</div>
   </template>
 </template>
 
@@ -98,9 +98,9 @@ const model = computed(() => {
 
 const sceneModel = computed(() => model.value && getSceneModel(model.value));
 const options = [
-  { desc: "移动", icon: "move", key: "move" },
-  { desc: "旋转", icon: "flip", key: "rotate" },
-  { desc: "透明度", icon: "transparency", key: "opacity" },
+  { desc: $t('fuse.move'), icon: "move", key: "move" },
+  { desc: $t('fuse.flip'), icon: "flip", key: "rotate" },
+  { desc: $t('fuse.opacity1'), icon: "transparency", key: "opacity" },
 ];
 const selectOptions = ref<typeof options>([]);
 const selectExpose = ref<ControlExpose>();

+ 10 - 9
src/views/setting/index.vue

@@ -1,15 +1,15 @@
 <template>
   <RightFillPano>
-    <ui-group title="初始画面" borderBottom>
+    <ui-group :title="$t('setting.initView')" borderBottom>
       <ui-group-option>
         <div class="init-pic" :class="{ disabled: isEdit }">
           <img :src="getFileUrl(setting!.cover)" class="init-puc-cover" />
-          <div class="init-pic-set" @click="enterSetPic">设置</div>
+          <div class="init-pic-set" @click="enterSetPic">{{$t('setting.name')}}</div>
         </div>
       </ui-group-option>
     </ui-group>
 
-    <ui-group title="设置天空">
+    <ui-group :title="$t('setting.back')">
       <ui-group-option>
         <div class="back-layout">
           <div
@@ -37,31 +37,32 @@ import { ref, watchEffect } from "vue";
 import { togetherCallback, getFileUrl, loadPack } from "@/utils";
 import { showRightPanoStack, showRightCtrlPanoStack, custom } from "@/env";
 import { analysisPose, sdk, SettingResourceType } from "@/sdk";
+import { ui18n } from "@/lang";
 
 const backs = ref<{ label: string; type: string; image: string; value: string }[]>([]);
 watchEffect(async () => {
   backs.value = [
-    { label: "无", type: "icon", image: "icon-without", value: "none" },
+    { label: ui18n.t('setting.backs.0'), type: "icon", image: "icon-without", value: "none" },
     {
-      label: "地图",
+      label: ui18n.t('setting.backs.1'),
       type: "map",
       image: "/oss/fusion/default/images/map.png",
       value: "map",
     },
     {
-      label: "蓝天白云",
+      label: ui18n.t('setting.backs.2'),
       type: "img",
       image: "/oss/fusion/default/images/pic_ltby@2x.png",
       value: "/oss/fusion/default/images/蓝天白云.jpg",
     },
     {
-      label: "乌云密布",
+      label: ui18n.t('setting.backs.3'),
       type: "img",
       image: "/oss/fusion/default/images/pic_wymb@2x.png",
       value: "/oss/fusion/default/images/乌云密布.jpg",
     },
     {
-      label: "夜空",
+      label: ui18n.t('setting.backs.4'),
       type: "img",
       image: "/oss/fusion/default/images/pic_yk@2x.png",
       value: "/oss/fusion/default/images/夜空.jpg",
@@ -79,7 +80,7 @@ watchEffect(async () => {
     //   value: "/oss/fusion/default/images/道路.jpg",
     // },
     {
-      label: "傍晚",
+      label: ui18n.t('setting.backs.5'),
       type: "img",
       image: "/oss/fusion/default/images/pic_bw@2x.png",
       value: "/oss/fusion/default/images/傍晚.jpg",

+ 2 - 1
src/views/sign-model/index.vue

@@ -9,6 +9,7 @@ import { custom, params, showRightPanoStack, viewModeStack } from "@/env";
 import { Dialog } from "bill/index";
 import { useViewStack, useActive } from "@/hook";
 import { sdk } from "@/sdk";
+import { ui18n } from "@/lang";
 
 const active = useActive();
 let pop: () => void;
@@ -52,7 +53,7 @@ const loadSignModel = async () => {
   }
 
   if (!scene) {
-    return Dialog.alert(`模型不存在!`);
+    return Dialog.alert(ui18n.t('fuse.unModel'));
   }
   if (active.value) {
     defTitle.value = scene.title || scene.modelTitle;

+ 4 - 3
src/views/summary/index.vue

@@ -32,12 +32,13 @@ import SceneList from '@/layout/scene-list/index.vue'
 import Taggings from '@/views/tagging/show.vue'
 import Measures from '@/views/measure/show.vue'
 import Guides from '@/views/guide/show.vue'
+import { ui18n } from '@/lang'
 
 enum TabKey { tagging, measure, guide }
 const tabs = [
-  { key: TabKey.tagging, text: '标签' },
-  { key: TabKey.measure, text: '测量' },
-  { key: TabKey.guide, text: '路径' },
+  { key: TabKey.tagging, text: ui18n.t('tagging.name') },
+  { key: TabKey.measure, text: ui18n.t('material.name')  },
+  { key: TabKey.guide, text: ui18n.t('guide.name') },
 ]
 const current = ref(tabs[0].key)
 const showRightCtrl = ref(true)

+ 6 - 5
src/views/tagging-position/index.vue

@@ -1,12 +1,12 @@
 <template>
   <RightFillPano>
-    <h3>{{ tagging?.title }}放置位置</h3>
+    <h3>{{ tagging?.title }}{{$t('tagging.plcPos')}}</h3>
     <Collapse v-model:activeKey="showId" ghost accordion expandIconPosition="end">
       <template v-for="(position, i) in positions" :key="position.id">
         <PositionSign
           v-show="!(unKeepAdding && position.id !== showId)"
           :position="position"
-          :title="`位置${i + 1}`"
+          :title="`${$t('tagging.posName')}${i + 1}`"
           @applyGlobal="(keys) => applyGlobal(position, keys)"
           @delete="deletePosition(position)"
           @show="showId = position.id"
@@ -60,6 +60,7 @@ import { Collapse } from "ant-design-vue";
 
 import type { TaggingPosition } from "@/store";
 import { clickListener } from "@/utils/event";
+import { ui18n } from "@/lang";
 
 const showId = ref<TaggingPosition["id"]>();
 const tagging = computed(() => getTagging(router.currentRoute.value.params.id as string));
@@ -114,7 +115,7 @@ const deletePosition = (position: TaggingPosition) => {
 };
 
 const applyGlobal = async (position: TaggingPosition, keys: string | string[]) => {
-  if (!(await Dialog.confirm("确定要将此属性应用到所有位置?"))) return;
+  if (!(await Dialog.confirm(ui18n.t('tagging.applyConfirm')))) return;
   keys = Array.isArray(keys) ? keys : [keys];
   for (const current of positions.value!) {
     let val: any = current;
@@ -134,7 +135,7 @@ let unKeepAdding = shallowRef<() => void>();
 const keepAdding = () => {
   unKeepAdding.value && unKeepAdding.value();
   sdk.startAddSth();
-  const hide = Message.show({ msg: "请在模型上单击选择标签位置", type: "warning" });
+  const hide = Message.show({ msg: ui18n.t('tagging.posTip'), type: "warning" });
   showId.value = void 0;
 
   const removeListener = clickListener(sdk.layout, async (pos) => {
@@ -143,7 +144,7 @@ const keepAdding = () => {
     const position = sdk.getPositionByScreen(pos);
 
     if (!position) {
-      Message.error("当前位置无法添加");
+      Message.error(ui18n.t('tagging.posErr'));
     } else {
       const storePosition = createTaggingPosition({
         ...position,

+ 1 - 1
src/views/tagging-position/sign-item.vue

@@ -7,7 +7,7 @@
           {{ label }}
         </template>
       </span>
-      <span @click="$emit('applyGlobal')" class="apply">应用到全部</span>
+      <span @click="$emit('applyGlobal')" class="apply">{{ $t('tagging.apply') }}</span>
     </div>
     <div class="item-content" v-if="$slots.default"><slot /></div>
   </div>

+ 10 - 10
src/views/tagging-position/sign.vue

@@ -6,7 +6,7 @@
 
     <div class="sign-position">
       <SignItem
-        label="图标放置方式"
+        :label="$t('tagging.posTabs.type')"
         class="item"
         @apply-global="$emit('applyGlobal', 'type')"
       >
@@ -16,26 +16,26 @@
             :value="TaggingPositionType['2d']"
             size="middle"
           >
-            悬浮
+            {{$t('tagging.posTabs.typeVal[0]')}}
           </RadioButton>
           <RadioButton
             style="width: 50%; text-align: center"
             :value="TaggingPositionType['3d']"
             size="middle"
           >
-            贴地/墙
+            {{$t('tagging.posTabs.typeVal[1]')}}
           </RadioButton>
         </RadioGroup>
       </SignItem>
       <SignItem
-        label="图标大小"
+        :label="$t('tagging.posTabs.scale')"
         class="item"
         @apply-global="$emit('applyGlobal', ['mat', 'scale'])"
       >
         <Slider v-model:value="position.mat.scale" :min="0.5" :max="5" :step="0.1" />
       </SignItem>
       <SignItem
-        label="旋转图标"
+        :label="$t('tagging.posTabs.rotation')"
         class="item"
         v-if="TaggingPositionType['2d'] !== position.type"
         @apply-global="$emit('applyGlobal', ['mat', 'rotation'])"
@@ -43,21 +43,21 @@
         <Slider v-model:value="position.mat.rotation" :min="0" :max="360" :step="0.1" />
       </SignItem>
       <SignItem
-        label="文字大小"
+        :label="$t('tagging.posTabs.fontSize')"
         class="item"
         @apply-global="$emit('applyGlobal', 'fontSize')"
       >
         <Slider v-model:value="position.fontSize" :min="12" :max="60" :step="0.1" />
       </SignItem>
       <SignItem
-        label="引线高度"
+        :label="$t('tagging.posTabs.lineHeight')"
         class="item"
         @apply-global="$emit('applyGlobal', 'lineHeight')"
       >
         <Slider v-model:value="position.lineHeight" :min="0.5" :max="5" :step="0.1" />
       </SignItem>
       <SignItem
-        label="可见范围"
+        :label="$t('tagging.posTabs.visibilityRange')"
         class="item"
         v-if="!position.globalVisibility"
         @apply-global="$emit('applyGlobal', 'visibilityRange')"
@@ -73,14 +73,14 @@
         <template v-slot:label>
           <ui-input
             type="checkbox"
-            label="全部范围可视"
+            :label="$t('tagging.posTabs.globalVisibility')"
             :modelValue="!!position.globalVisibility"
             @update:modelValue="(v: boolean) => position.globalVisibility = v"
           />
         </template>
       </SignItem>
       <Button block type="primary" danger ghost size="large" @click="$emit('delete')">
-        删除
+        {{$t('sys.del')}}
       </Button>
     </div>
   </CollapsePanel>

+ 21 - 20
src/views/tagging/edit.vue

@@ -2,11 +2,11 @@
   <div class="edit-hot-layer">
     <div class="edit-hot-item">
       <h3 class="edit-title">
-        标签
+        {{$t('tagging.name')}}
         <ui-icon type="close" ctrl @click.stop="$emit('quit')" class="edit-close" />
       </h3>
       <div class="style-select">
-        <span>图标样式</span>
+        <span>{{$t('tagging.style')}}</span>
         <span>
           <StyleTypeSelect v-model:value="type" />
         </span>
@@ -23,20 +23,20 @@
         require
         class="input"
         width="100%"
-        placeholder="请输入标签标题"
+        :placeholder="$t('tagging.plcTitle')"
         type="text"
         v-model="tagging.title"
         maxlength="15"
       />
       <div class="input">
-        <ui-input type="checkbox" label="标题常驻" v-model="tagging.show3dTitle" />
+        <ui-input type="checkbox" :label="$t('tagging.titleFex')" v-model="tagging.show3dTitle" />
       </div>
 
       <ui-input
         class="input"
         width="100%"
         height="158px"
-        :placeholder="defStyleType.id === type ? '描述:' : '特征描述:'"
+        :placeholder="defStyleType.id === type ? $t('tagging.plcType') : $t('tagging.plcType1')"
         type="richtext"
         v-model="tagging.desc"
         :maxlength="200"
@@ -50,7 +50,7 @@
           v-model="tagging.part"
           :maxlength="60"
         >
-          <template #preIcon><span>遗留部位:</span></template>
+          <template #preIcon><span>{{$t('tagging.plcPart')}}</span></template>
         </ui-input>
         <ui-input
           class="input preplace"
@@ -60,7 +60,7 @@
           v-model="tagging.method"
           :maxlength="60"
         >
-          <template #preIcon><span>提取方法:</span></template>
+          <template #preIcon><span>{{$t('tagging.plcMethod')}}</span></template>
         </ui-input>
         <ui-input
           class="input preplace"
@@ -70,13 +70,13 @@
           v-model="tagging.principal"
           :maxlength="60"
         >
-          <template #preIcon><span>提取人:</span></template>
+          <template #preIcon><span>{{$t('tagging.plcPrincipal')}}</span></template>
         </ui-input>
       </template>
       <div class="input" style="padding-top: 10px">
         <div class="mat-select">
-          <span>音乐</span>
-          <span class="select" @click="musicSelect">+从媒体库上传</span>
+          <span>{{$t('tagging.mic')}}</span>
+          <span class="select" @click="musicSelect">{{ $t('material.up') }}</span>
         </div>
 
         <ui-input
@@ -85,7 +85,7 @@
           width="100%"
           height="40px"
           preview
-          othPlaceholder="支持 mp3/wav 格式,≤30MB"
+          :othPlaceholder="$t('tagging.micPlc')"
           :accept="audioFormat.map((u) => `.${u}`).join(',')"
           :disable="true"
           :multiple="false"
@@ -95,7 +95,7 @@
         >
           <template v-slot:replace>
             <p class="audio-tip" v-if="!tagging.audio">
-              <ui-icon type="add" /> 支持 mp3/wav 格式,≤30MB
+              <ui-icon type="add" /> {{$t('tagging.micPlc')}}
             </p>
             <p v-else class="rep-val">
               <span>
@@ -109,12 +109,12 @@
 
       <div class="input">
         <div class="mat-select">
-          <span>图片/视频</span>
+          <span>{{$t('tagging.media')}}</span>
           <span
             class="select"
             @click="imageSelect"
             v-if="imageCount - tagging.images.length > 0"
-            >+从媒体库上传</span
+            >{{ $t('material.up') }}</span
           >
         </div>
         <ui-input
@@ -122,8 +122,8 @@
           width="100%"
           height="225px"
           preview
-          placeholder="上传图片/视频"
-          othPlaceholder="支持JPG、PNG、MP4等格式,单个不超过100MB,最多支持上传10张。"
+          :placeholder="$t('tagging.pleMedia')"
+          :othPlaceholder="$t('tagging.plcMedia1')"
           :accept="imageFormat.map((u) => `.${u}`).join(',')"
           :disable="true"
           :multiple="true"
@@ -145,7 +145,7 @@
         <div class="edit-hot">
           <a @click="submitHandler">
             <ui-icon type="nav-edit" />
-            确定
+            {{$t('sys.enter')}}
           </a>
         </div>
       </div>
@@ -171,6 +171,7 @@ import {
 import { defStyleType, styleTypes } from "@/api";
 import { selectMaterials } from "@/components/materials/quisk";
 import { getFileName } from "@/utils";
+import { ui18n } from "@/lang";
 
 export type EditProps = {
   data: Tagging;
@@ -195,9 +196,9 @@ const type = ref(activeStyle.value ? activeStyle.value.typeId : defStyleType.id)
 
 const submitHandler = () => {
   if (activeStyle.value?.typeId !== type.value) {
-    Message.error("请选择图标样式!");
+    Message.error(ui18n.t('tagging.styleErr'));
   } else if (!tagging.value.title.trim()) {
-    Message.error("标签标题必须填写!");
+    Message.error(ui18n.t('tagging.titleErr'));
   }
   //  else if (!tagging.value.images.length) {
   //   Message.error("至少上传一张图片!");
@@ -289,7 +290,7 @@ const fileChange = (file: LocalImageFile | LocalImageFile[]) => {
 
 const delImageHandler = async (file: Tagging["images"][number]) => {
   const index = tagging.value.images.indexOf(file);
-  if (~index && (await Dialog.confirm(`确定要删除此数据吗?`))) {
+  if (~index && (await Dialog.confirm(ui18n.t('sys.delConfrm')))) {
     tagging.value.images.splice(index, 1);
   }
 };

+ 3 - 3
src/views/tagging/index.vue

@@ -5,12 +5,12 @@
         <template #header>
           <ui-button @click="editTagging = createTagging()">
             <ui-icon type="add" />
-            新增
+            {{$t('sys.add')}}
           </ui-button>
         </template>
       </ui-group>
     </template>
-    <ui-group title="标签列表" class="tagging-list">
+    <ui-group :title="$t('tagging.list')" class="tagging-list">
       <template #header>
         <StyleTypeSelect v-model:value="type" all count />
       </template>
@@ -29,7 +29,7 @@
         />
       </template>
       <ui-group-option v-if="showSearch">
-        <ui-input type="text" width="100%" placeholder="搜索" v-model="keyword">
+        <ui-input type="text" width="100%" :placeholder="$t('sys.save')" v-model="keyword">
           <template #preIcon>
             <ui-icon type="search" />
           </template>

+ 1 - 1
src/views/tagging/show.vue

@@ -1,5 +1,5 @@
 <template>
-  <ui-group title="标签列表" class="show-taggings">
+  <ui-group :title="$t('tagging.list')" class="show-taggings">
     <template #header>
       <StyleTypeSelect v-model:value="type" all count />
     </template>

+ 5 - 4
src/views/tagging/sign.vue

@@ -8,7 +8,7 @@
       <img :src="getResource(getFileUrl(findImage))" v-if="findImage" />
       <div>
         <p>{{ tagging.title }}</p>
-        <span>放置:{{ positions.length }}</span>
+        <span>{{$t('tagging.pos')}}:{{ positions.length }}</span>
       </div>
     </div>
     <div class="actions" @click.stop>
@@ -20,7 +20,7 @@
         :class="{ disabled: !getTaggingIsShow(tagging) }"
       />
       <template v-else>
-        <ui-icon type="pin1" ctrl @click.stop="$emit('fixed')" tip="放置" />
+        <ui-icon type="pin1" ctrl @click.stop="$emit('fixed')" :tip="{{$t('tagging.pos')}}" />
         <ui-more
           :options="menus"
           style="margin-left: 20px"
@@ -45,6 +45,7 @@ import {
 } from "@/store";
 
 import type { Tagging } from "@/store";
+import { ui18n } from "@/lang";
 
 const props = withDefaults(
   defineProps<{ tagging: Tagging; selected?: boolean; edit?: boolean }>(),
@@ -72,8 +73,8 @@ const findImage = computed(() => {
 });
 
 const menus = [
-  { label: "编辑", value: "edit" },
-  { label: "删除", value: "delete" },
+  { label: ui18n.t('sys.edit'), value: "edit" },
+  { label: ui18n.t('sys.del'), value: "delete" },
 ];
 const actions = {
   edit: () => emit("edit"),

+ 2 - 1
src/views/tagging/style-type-select.vue

@@ -25,10 +25,11 @@ import { computed, watchEffect } from "vue";
 import { Menu, Dropdown } from "ant-design-vue";
 import { DownOutlined } from "@ant-design/icons-vue";
 import { taggings, getTaggingStyle } from "@/store";
+import { ui18n } from "@/lang";
 
 const props = defineProps<{ value: number; all?: boolean; count?: boolean }>();
 const emit = defineEmits<{ (e: "update:value", v: number): void }>();
-const allType = { name: "全部", id: -1 };
+const allType = { name: ui18n.t('sys.all'), id: -1 };
 const getTypeCount = (item: any) => {
   if (item.id === allType.id) {
     return taggings.value.length;

+ 0 - 4
src/views/tagging/styles.vue

@@ -53,10 +53,6 @@
       </span>
     </div>
   </div>
-  <!-- <div class="buttons" v-if="props.all">
-    <ui-button type="submit" class="button" @click="emit('quitMore')">取消</ui-button>
-    <ui-button type="primary" class="button" @click="enterHandler">保存</ui-button>
-  </div> -->
 </template>
 
 <script setup lang="ts">

+ 2 - 2
src/views/view/index.vue

@@ -4,12 +4,12 @@
       <div class="btns header-btns">
         <ui-button class="start" @click="getView">
           <ui-icon type="add" />
-          视图提取
+          {{$t('view.name')}}
         </ui-button>
       </div>
     </template>
 
-    <ui-group title="全部视图" class="tree">
+    <ui-group :title="$t('view.all')" class="tree">
       <Draggable :list="views" draggable=".sign" itemKey="id">
         <template #item="{ element: view }">
           <Sign

+ 4 - 3
src/views/view/sign.vue

@@ -40,6 +40,7 @@ import { viewToModelType } from '@/store'
 
 import type { View } from '@/store'
 import { Message } from 'bill/expose-common'
+import { ui18n } from '@/lang'
 
 const props = withDefaults(
   defineProps<{ view: View, edit?: boolean }>(),
@@ -52,8 +53,8 @@ const emit = defineEmits<{
 }>()
 
 const menus = [
-  { label: '重命名', value: 'rename' },
-  { label: '删除', value: 'delete' },
+  { label: ui18n.t('sys.rename'), value: 'rename' },
+  { label: ui18n.t('sys.del'), value: 'delete' },
 ]
 
 const inputRef = ref()
@@ -62,7 +63,7 @@ const isEditTitle = useFocus(computed(() => inputRef.value?.vmRef.root))
 watchEffect(() => {
   if (!isEditTitle.value && !props.view.title.length) {
     isEditTitle.value = true
-    Message.warning('视图名称不可为空')
+    Message.warning(ui18n.t('view.nameErr'))
   }
 })
 

+ 16 - 1
src/vite-env.d.ts

@@ -6,6 +6,8 @@ declare module '*.vue' {
   export default component
 }
 
+
+
 interface ImportMetaEnv {
   readonly VITE_LASER_HOST: string
   readonly VITE_LASER_OSS: string
@@ -41,4 +43,17 @@ type PartialProps<T, U extends keyof T = keyof T> = {
     [P in keyof Omit<T, U>]: T[P];
 } & {
   [P in U]?: T[P];
-}
+}
+
+
+declare type I18nGlobalTranslation = {
+  (key: string): string
+  (key: string, locale: string): string
+  (key: string, locale: string, list: unknown[]): string
+  (key: string, locale: string, named: Record<string, unknown>): string
+  (key: string, list: unknown[]): string
+  (key: string, named: Record<string, unknown>): string
+}
+
+
+declare const $t: I18nGlobalTranslation