Browse Source

fix: 国际化支持

bill 1 year ago
parent
commit
f0fec1e2e5
72 changed files with 2007 additions and 270 deletions
  1. 1 1
      .vscode/extensions.json
  2. 1 1
      index.html
  3. 13 0
      lang.html
  4. 6 1
      package.json
  5. 71 0
      scripts/lang.js
  6. 5 3
      src/api/constant.ts
  7. 2 1
      src/api/instance.ts
  8. 8 7
      src/api/project.ts
  9. 10 9
      src/api/scene.ts
  10. 2 1
      src/api/setup.ts
  11. 5 4
      src/api/tagging.ts
  12. 4 2
      src/components/data-list/index.vue
  13. 9 6
      src/components/upload/index.vue
  14. 2 1
      src/hook/useProject.ts
  15. 251 0
      src/lang-editer/app.vue
  16. 52 0
      src/lang-editer/main.ts
  17. 16 0
      src/lang/constant.ts
  18. 10 0
      src/lang/en/index.ts
  19. 5 0
      src/lang/en/sys.ts
  20. 41 0
      src/lang/helper.ts
  21. 81 0
      src/lang/index.ts
  22. 10 0
      src/lang/ja/index.ts
  23. 5 0
      src/lang/ja/sys.ts
  24. 10 0
      src/lang/zh/index.ts
  25. 5 0
      src/lang/zh/log.ts
  26. 22 0
      src/lang/zh/material.ts
  27. 53 0
      src/lang/zh/project.ts
  28. 12 0
      src/lang/zh/role.ts
  29. 18 0
      src/lang/zh/scene.ts
  30. 66 0
      src/lang/zh/sys.ts
  31. 9 0
      src/lang/zh/tagging.ts
  32. 2 1
      src/layout/header.vue
  33. 4 0
      src/main.ts
  34. 11 9
      src/router/constant.ts
  35. 9 0
      src/setup.ts
  36. 3 1
      src/shared/copy.ts
  37. 3 5
      src/shared/test.ts
  38. 2 1
      src/store/user.ts
  39. 182 0
      src/utils/file-serve.ts
  40. 25 0
      src/utils/index.ts
  41. 89 0
      src/utils/store.ts
  42. 5 4
      src/views/material/columns.ts
  43. 15 9
      src/views/material/edit.vue
  44. 7 4
      src/views/material/list.vue
  45. 7 6
      src/views/member/columns.ts
  46. 33 18
      src/views/member/edit.vue
  47. 16 10
      src/views/member/list.vue
  48. 19 12
      src/views/personal/index.vue
  49. 9 8
      src/views/project/columns.ts
  50. 28 21
      src/views/project/detailed.vue
  51. 17 10
      src/views/project/edit.vue
  52. 23 12
      src/views/project/list.vue
  53. 6 5
      src/views/record/columns.ts
  54. 1 1
      src/views/record/list.vue
  55. 5 4
      src/views/role/columns.ts
  56. 12 10
      src/views/role/edit.vue
  57. 13 10
      src/views/role/list.vue
  58. 19 10
      src/views/scene/actions.vue
  59. 8 7
      src/views/scene/columns.ts
  60. 15 10
      src/views/scene/insert.vue
  61. 4 2
      src/views/scene/list.vue
  62. 1 1
      src/views/scene/select-scenes.vue
  63. 5 4
      src/views/scene/update.vue
  64. 3 3
      src/views/scene/upload-bim.vue
  65. 10 8
      src/views/system/login.vue
  66. 3 2
      src/views/system/shared/rules.ts
  67. 11 7
      src/views/taggings/columns.ts
  68. 7 6
      src/views/taggings/list.vue
  69. 11 0
      src/vite-env.d.ts
  70. 0 1
      tsconfig.json
  71. 22 1
      vite.config.ts
  72. 537 10
      yarn.lock

+ 1 - 1
.vscode/extensions.json

@@ -1,3 +1,3 @@
 {
-  "recommendations": ["Vue.volar"]
+  "recommendations": ["Vue.volar"],
 }

+ 1 - 1
index.html

@@ -8,6 +8,6 @@
   </head>
   <body>
     <div id="app"></div>
-    <script type="module" src="/src/main.ts"></script>
+    <script type="module" src="/src/setup.ts"></script>
   </body>
 </html>

+ 13 - 0
lang.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="//4dkk.4dage.com/FDKKIMG/icon/kankan_icon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>四维工地管家</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/lang-editer/main.ts"></script>
+  </body>
+</html>

+ 6 - 1
package.json

@@ -5,6 +5,7 @@
   "type": "module",
   "scripts": {
     "dev": "vite",
+    "lang": "vite ./ lang",
     "build": "vue-tsc --noEmit && vite build",
     "preview": "vite preview",
     "format": "prettier --write ./**/*.{vue,ts,tsx,js,jsx,css,less,scss,json,md}",
@@ -21,13 +22,17 @@
     "@types/node": "^18.7.18",
     "ant-design-vue": "3.3.0-beta.3",
     "axios": "^0.27.2",
+    "body-parser": "^1.20.2",
     "dayjs": "^1.11.5",
+    "express": "^4.18.2",
     "js-base64": "^3.7.2",
     "less": "^4.1.3",
     "pinia": "^2.0.22",
     "sass": "^1.54.9",
     "vue": "^3.2.37",
-    "vue-router": "4"
+    "vue-i18n": "^9.9.0",
+    "vue-router": "4",
+    "xlsx": "^0.18.5"
   },
   "devDependencies": {
     "@typescript-eslint/eslint-plugin": "^5.36.2",

+ 71 - 0
scripts/lang.js

@@ -0,0 +1,71 @@
+import express from 'express'
+import path from 'path'
+import bodyParser from 'body-parser'
+import * as fs from 'fs'
+import { fileURLToPath } from 'url'
+
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = path.dirname(__filename)
+const startup = false
+
+const enter = `
+import { modulesParse } from '../helper'
+
+const { ctx, promise } = modulesParse(
+  import.meta.glob(['./*.ts', '!./index.ts'])
+)
+
+export default ctx
+export { promise }
+
+`
+const writeFile = (path, content) =>
+  new Promise((resolve, reject) => {
+    fs.writeFile(path, content, err => {
+      if (err) {
+        reject(err)
+      } else {
+        resolve()
+      }
+    })
+  })
+
+const langDir = path.join(__dirname, '../src/lang')
+const writeLang = (name, lang) => {
+  const dir = path.join(langDir, name)
+  if (!fs.existsSync(dir)) {
+    fs.mkdirSync(dir)
+  }
+
+  return Promise.all([
+    writeFile(path.join(dir, 'index.ts'), enter),
+    ...Object.entries(lang).map(([name, content]) =>
+      writeFile(
+        path.join(dir, `${name}.ts`),
+        `export default ${JSON.stringify(content, null, 4)}`
+      )
+    )
+  ])
+}
+
+export async function createServer(port) {
+  if (startup) {
+    return
+  }
+
+  const app = express()
+  app.use('/dev', bodyParser.json())
+  app.use('/dev', bodyParser.urlencoded({ extended: false }))
+
+  app.post('/dev/langs', async function (req, res) {
+    const langs = req.body
+    const handlers = Object.entries(langs).map(([name, lang]) =>
+      writeLang(name, lang)
+    )
+    Promise.all(handlers)
+      .then(() => res.json({ ok: true }))
+      .catch(() => res.json({ ok: false }))
+  })
+  app.listen(port)
+  console.log('语言编辑已开启')
+}

+ 5 - 3
src/api/constant.ts

@@ -1,3 +1,5 @@
+import { ui18n } from '@/lang'
+
 export enum ResCode {
   TOKEN_INVALID = 4008,
   NO_ACCESS = 4010,
@@ -8,9 +10,9 @@ export const all = '___all___'
 export type All = typeof all
 
 export const ResCodeDesc: { [key in ResCode]: string } = {
-  [ResCode.TOKEN_INVALID]: 'token已失效',
-  [ResCode.NO_ACCESS]: '无权访问',
-  [ResCode.SUCCESS]: '请求成功'
+  [ResCode.TOKEN_INVALID]: ui18n.t('sys.TOKEN_INVALID'),
+  [ResCode.NO_ACCESS]: ui18n.t('sys.NO_ACCESS'),
+  [ResCode.SUCCESS]: ui18n.t('sys.SUCCESS')
 }
 
 // 上传文件

+ 2 - 1
src/api/instance.ts

@@ -10,6 +10,7 @@ import {
 } from './constant'
 import { baseURL } from '@/env'
 import { router, RoutesName } from '@/router'
+import { ui18n } from '@/lang'
 
 const instance = axiosFactory()
 
@@ -41,7 +42,7 @@ addReqErrorHandler(err => {
 
 addResErrorHandler((response, data) => {
   if (response && response.status && response.status !== 200) {
-    message.error('服务错误,请稍后再试')
+    message.error(ui18n.t('sys.ERROR'))
   } else if (data) {
     const msg =
       data.code && ResCodeDesc[data.code]

+ 8 - 7
src/api/project.ts

@@ -16,6 +16,7 @@ import {
 } from './constant'
 
 import type { PageResult, PageRequest, Scenes, Scene } from './'
+import { ui18n } from '@/lang'
 
 export enum ProjectStatus {
   undone,
@@ -23,8 +24,8 @@ export enum ProjectStatus {
 }
 
 export const ProjectStatusDesc = {
-  [ProjectStatus.done]: '已完成',
-  [ProjectStatus.undone]: '未完成'
+  [ProjectStatus.done]: ui18n.t('project.desc.done'),
+  [ProjectStatus.undone]: ui18n.t('project.desc.undone')
 }
 
 export interface SimpleProject {
@@ -49,11 +50,11 @@ export enum BimStatus {
 }
 
 export const BimStatusDesc = {
-  [BimStatus.error]: '错误',
-  [BimStatus.done]: '完成',
-  [BimStatus.upload]: '上传中',
-  [BimStatus.transfrom]: '转换中',
-  [BimStatus.offline]: '离线包生成中'
+  [BimStatus.error]: ui18n.t('project.bimDesc.error'),
+  [BimStatus.done]: ui18n.t('project.bimDesc.done'),
+  [BimStatus.upload]: ui18n.t('project.bimDesc.upload'),
+  [BimStatus.transfrom]: ui18n.t('project.bimDesc.transfrom'),
+  [BimStatus.offline]: ui18n.t('project.bimDesc.offline')
 }
 
 export type Bim = {

+ 10 - 9
src/api/scene.ts

@@ -1,6 +1,7 @@
 import axios from './instance'
 import { GET_SCENE_LIST } from './constant'
 import type { PageResult, PageRequest } from './'
+import { ui18n } from '@/lang'
 
 export enum SceneStatus {
   DEL = -1,
@@ -11,12 +12,12 @@ export enum SceneStatus {
   RERUN = 4
 }
 export const sceneStatusDesc = {
-  [SceneStatus.DEL]: '场景被删',
-  [SceneStatus.RUN]: '计算中',
-  [SceneStatus.ERR]: '计算失败',
-  [SceneStatus.SUCCESS]: '计算成功',
-  [SceneStatus.ARCHIVE]: '封存',
-  [SceneStatus.RERUN]: '重新计算中'
+  [SceneStatus.DEL]: ui18n.t('project.sceneDesc.DEL'),
+  [SceneStatus.RUN]: ui18n.t('project.sceneDesc.RUN'),
+  [SceneStatus.ERR]: ui18n.t('project.sceneDesc.ERR'),
+  [SceneStatus.SUCCESS]: ui18n.t('project.sceneDesc.SUCCESS'),
+  [SceneStatus.ARCHIVE]: ui18n.t('project.sceneDesc.ARCHIVE'),
+  [SceneStatus.RERUN]: ui18n.t('project.sceneDesc.RERUN')
 }
 
 export enum SceneType {
@@ -25,9 +26,9 @@ export enum SceneType {
   SWSS
 }
 export const SceneTypeDesc = {
-  [SceneType.SWKK]: '四维看看',
-  [SceneType.SWKJ]: '四维看见',
-  [SceneType.SWSS]: '四维深时'
+  [SceneType.SWKK]: ui18n.t('project.sceneTypeDesc.SWKK'),
+  [SceneType.SWKJ]: ui18n.t('project.sceneTypeDesc.SWKJ'),
+  [SceneType.SWSS]: ui18n.t('project.sceneTypeDesc.SWSS')
 }
 
 export interface Scene {

+ 2 - 1
src/api/setup.ts

@@ -2,6 +2,7 @@ import Axios from 'axios'
 import { ResCode } from './constant'
 
 import type { AxiosResponse, AxiosRequestConfig } from 'axios'
+import { ui18n } from '@/lang'
 
 export type ResErrorHandler = <D, T extends ResData<D>>(
   response: AxiosResponse<T>,
@@ -138,7 +139,7 @@ export const axiosFactory = () => {
     if (!matchURL(axiosConfig.unTokenSet, config)) {
       if (!axiosConfig.token) {
         if (!matchURL(axiosConfig.unReqErrorSet, config)) {
-          const error = new Error('缺少token')
+          const error = new Error(ui18n.t('sys.NO_ACCESS'))
           callErrorHandler('req', error, config)
           throw error
         }

+ 5 - 4
src/api/tagging.ts

@@ -1,6 +1,7 @@
 import axios from './instance'
 import { GET_TAGGING_LIST, GET_TAGGING_TOTAL } from './constant'
 import type { PageResult, PageRequest, Project } from './'
+import { ui18n } from '@/lang'
 
 export enum TaggingStatus {
   pending = 1,
@@ -10,10 +11,10 @@ export enum TaggingStatus {
 }
 
 export const TaggingStatusDesc = {
-  [TaggingStatus.pending]: '待处理',
-  [TaggingStatus.progress]: '进行中',
-  [TaggingStatus.unsolved]: '未解决',
-  [TaggingStatus.solved]: '已解决'
+  [TaggingStatus.pending]: ui18n.t('project.taggingStatusDesc.pending'),
+  [TaggingStatus.progress]: ui18n.t('project.taggingStatusDesc.progress'),
+  [TaggingStatus.unsolved]: ui18n.t('project.taggingStatusDesc.unsolved'),
+  [TaggingStatus.solved]: ui18n.t('project.taggingStatusDesc.solved')
 }
 
 export interface Tagging {

+ 4 - 2
src/components/data-list/index.vue

@@ -2,8 +2,10 @@
   <slot v-if="dataSource.length"></slot>
   <div v-else class="un-data">
     <img :src="unDataPng" class="icon" />
-    <p v-if="keyword">未搜索到结果,</p>
-    <p>您还没有{{ name || '数据' }},请先创建{{ name || '数据' }}~</p>
+    <p v-if="keyword">{{ $t('sys.undata') }}</p>
+    <p>
+      {{ $t('sys.undataDesc', { name: name || $t('sys.data') } }}
+    </p>
     <div class="undata">
       <slot name="undata"></slot>
     </div>

+ 9 - 6
src/components/upload/index.vue

@@ -7,12 +7,12 @@
   >
     <a-button type="primary" :disabled="disabled || !!file">
       <upload-outlined></upload-outlined>
-      上传
+      {{ $t('sys.uploadBtn') }}
     </a-button>
   </a-upload>
   <ol class="desc">
-    <li>支持{{ extxTip }}文件格式;</li>
-    <li>最大支持上传{{ maxSizeTip }};</li>
+    <li>{{ $t('sys.uploadDesc[0]', { extxTip }) }}</li>
+    <li>{{ $t('sys.uploadDesc[1]', { maxSizeTip }) }}</li>
     <template v-if="tips">
       <li v-for="tip in tips" :key="tip">{{ tip }}</li>
     </template>
@@ -33,6 +33,7 @@ import { getExtname } from '@/shared'
 import { computed } from 'vue'
 
 import type { UploadProps } from 'ant-design-vue'
+import { ui18n } from '@/lang'
 
 export type BUploadProps = {
   disabled?: boolean
@@ -60,16 +61,18 @@ const extnames = computed(() =>
 const extxTip = computed(
   () =>
     extnames.value.map(ext => `.${ext}`).join('、') +
-    (extnames.value.length > 1 ? '等' : '')
+    (extnames.value.length > 1 ? ui18n.t('sys.more') : '')
 )
 
 const onBeforeUpload: UploadProps['beforeUpload'] = file => {
   const ext = getExtname(file.name)?.toLocaleLowerCase()
 
   if (!ext || !extnames.value.includes(ext)) {
-    message.error(`仅支持${extxTip.value}文件格式`)
+    message.error(ui18n.t('sys.noUploadDesc[0]', { extxTip: extxTip.value }))
   } else if (file.size > maxMB.value) {
-    message.error(`最大支持上传${maxSizeTip.value}`)
+    message.error(
+      ui18n.t('sys.noUploadDesc[1]', { maxSizeTip: maxSizeTip.value })
+    )
   } else {
     emit('update:file', file)
   }

+ 2 - 1
src/hook/useProject.ts

@@ -3,6 +3,7 @@ import { router } from '@/router'
 import { useRealtime } from './useRealtime'
 
 import type { Project } from '@/api'
+import { ui18n } from '@/lang'
 
 export const useProject = (
   id: Project['projectId'],
@@ -14,7 +15,7 @@ export const useCurrentProject = () => {
   const back = () => router.back()
   if (!id || id < 0) {
     back()
-    throw '错误页面'
+    throw ui18n.t('sys.404')
   }
   return useProject(id, back)
 }

+ 251 - 0
src/lang-editer/app.vue

@@ -0,0 +1,251 @@
+<template>
+  <p class="desc">被 {} 包裹的为占位符,在使用时会被实际参数占用,不可去除</p>
+  <div>
+    <RadioGroup v-model:value="currentLang" button-style="solid">
+      <RadioButton v-for="(_, name) in trees" :key="name" :value="name">{{
+        name
+      }}</RadioButton>
+    </RadioGroup>
+  </div>
+
+  <div :key="importCount" class="langs">
+    <template v-for="(tree, name) in trees">
+      <div v-if="name === currentLang" :key="name">
+        <Upload
+          :file-list="[]"
+          :multiple="false"
+          :before-upload="file => onBeforeUpload(name, file)"
+          accept=".xls"
+        >
+          <a>导入</a>
+        </Upload>
+        <a @click="exportFile(name)">导出</a>
+        <Tree :tree-data="tree" default-expand-all>
+          <template #title="{ dataRef }">
+            <template v-if="!dataRef.children">
+              {{ dataRef.label }} :
+              <Textarea
+                v-model:value="dataRef.title"
+                :placeholder="dataRef.label"
+                style="width: 300px"
+              />
+            </template>
+            <template v-else> {{ dataRef.title }} </template>
+          </template>
+        </Tree>
+      </div>
+    </template>
+  </div>
+
+  <Button block size="large" type="primary" class="btn" @click="emit('submit')"
+    >保存</Button
+  >
+</template>
+
+<script setup lang="ts">
+import 'ant-design-vue/dist/antd.css'
+import * as XLSX from 'xlsx'
+import { Lang } from './main'
+import {
+  Tree,
+  Textarea,
+  Button,
+  Upload,
+  message,
+  RadioGroup,
+  RadioButton
+} from 'ant-design-vue'
+import { reactive, ref } from 'vue'
+import { saveAs } from '@/utils/file-serve'
+
+// console.log(utils)
+const props = defineProps<{ langs: Record<string, Lang> }>()
+const emit = defineEmits<{ (e: 'submit'): void }>()
+
+const normalLangTree = (lang: Lang, prefix = '') => {
+  return Object.entries(lang)
+    .map(([name, value]) => {
+      const key = prefix + name
+      if (typeof value === 'string') {
+        return reactive({
+          key,
+          label: name,
+          get title() {
+            return lang[name]
+          },
+          set title(value) {
+            lang[name] = value
+          }
+        })
+      } else {
+        return {
+          key,
+          label: '',
+          title: name,
+          children: normalLangTree(value, key)
+        }
+      }
+    })
+    .sort((a, b) => a.key.localeCompare(b.key))
+}
+
+const trees = Object.entries(props.langs).reduce((t, [name, lang]) => {
+  t[name] = normalLangTree(lang)
+  return t
+}, {})
+const currentLang = ref('zh')
+
+const onBeforeUpload = (name: string, file: File) => {
+  const index = file.name.lastIndexOf('.')
+  const ext = ~index ? file.name.substring(index + 1).toUpperCase() : null
+
+  if (!ext || ext !== 'XLS') {
+    message.error(`仅支持xls文件格式`)
+  } else {
+    let reader = new FileReader()
+    //读入file
+    reader.readAsBinaryString(file)
+    reader.onload = e => {
+      importFile(props.langs[name], e.target.result)
+    }
+  }
+  return false
+}
+
+const importCount = ref(0)
+function importFile(lang: Lang, buffer: string | ArrayBuffer) {
+  const workbook = XLSX.read(buffer, { type: 'binary' })
+  const data = workbook.Sheets[workbook.SheetNames[0]]
+  const keyKeys = Object.keys(data).filter(
+    k => k.charAt(0) === 'A' && Number(k.substring(1)) > 1
+  )
+
+  for (const keyKey of keyKeys) {
+    const valueKey = `B${keyKey.substring(1)}`
+    const value = data[valueKey]
+    const key = data[keyKey]
+    const langKey: string = key?.v
+    const langValue: string = value?.v
+
+    if (!langKey || !langValue) {
+      continue
+    }
+
+    const paths = langKey.split('.')
+    for (let i = 0, target = lang; i < paths.length; i++) {
+      const key = paths[i]
+      if (key in target) {
+        if (i === paths.length - 1) {
+          target[key] = langValue
+        } else if (typeof target[key] !== 'string') {
+          target = target[key] as Lang
+        }
+      } else {
+        break
+      }
+    }
+  }
+
+  importCount.value++
+}
+
+const getSheetData = (lang: Lang, prev = '') => {
+  const result = []
+  for (const [key, value] of Object.entries(lang)) {
+    const label = (prev ? `${prev}.` : '') + key
+    if (typeof value === 'string') {
+      result.push({
+        key: label,
+        value
+      })
+    } else {
+      result.push(...getSheetData(value, label))
+    }
+  }
+  return result
+}
+
+function exportFile(name) {
+  const sheetData = getSheetData(props.langs[name])
+  // 将由对象组成的数组转化成sheet
+  const sheet = XLSX.utils.json_to_sheet(sheetData)
+  // 百分数和数字更改为数字类型
+  Object.keys(sheet).forEach(key => {
+    if (sheet[key].v) {
+      const val = sheet[key].v
+      if (!isNaN(val)) {
+        sheet[key].t = 'n'
+      }
+      if (val.lastIndexOf('%') === val.length - 1) {
+        sheet[key].t = 'n'
+        sheet[key].z = '0.00%'
+        sheet[key].v = Number(val.substring(0, val.length - 1)) / 100
+      }
+    }
+  })
+  // 创建虚拟的workbook
+  const wb = XLSX.utils.book_new()
+  // 把sheet添加到workbook中
+  XLSX.utils.book_append_sheet(wb, sheet, name)
+  const workbookBlob = workbook2blob(wb)
+  saveAs(workbookBlob, `${name}.xls`)
+}
+
+function workbook2blob(workbook) {
+  // 生成excel的配置项
+  const wopts = {
+    // 要生成的文件类型
+    bookType: 'xlsx',
+    // // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
+    bookSST: false,
+    type: 'binary'
+  }
+  const wbout = XLSX.write(workbook, wopts as any)
+  // 将字符串转ArrayBuffer
+  function s2ab(s: string) {
+    const buf = new ArrayBuffer(s.length)
+    const view = new Uint8Array(buf)
+    for (let i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
+    return buf
+  }
+  const blob = new Blob([s2ab(wbout)], {
+    type: 'application/octet-stream'
+  })
+  return blob
+}
+</script>
+
+<style scoped lang="scss">
+.desc {
+  text-align: center;
+  font-size: 1.5em;
+}
+
+h1 {
+  text-align: center;
+}
+
+.langs {
+  display: flex;
+  padding-bottom: 80px;
+}
+
+.lang {
+  flex: 1;
+  padding-left: 30px;
+
+  h2 {
+    text-align: center;
+    position: fixed;
+    top: 50%;
+    margin-left: -30px;
+    display: block;
+  }
+}
+
+.btn {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+}
+</style>

+ 52 - 0
src/lang-editer/main.ts

@@ -0,0 +1,52 @@
+import { computed, createApp, watchEffect } from 'vue'
+import App from './app.vue'
+import axios from 'axios'
+import { langs, deflangName, loaded } from '@/lang'
+
+export type Lang = {
+  [key in string]: string | Lang
+}
+
+document.title = '国际化文件配置'
+
+const deflang = langs[deflangName]
+const normalLangs = (lang: any, standard: any) => {
+  lang = { ...lang }
+  for (const key in standard) {
+    const exists1 = key in lang
+    const exists2 = key in standard
+    const type1 = typeof lang[key]
+    const type2 = typeof standard[key]
+
+    if (exists1 !== exists2 || type1 !== type2) {
+      lang[key] = standard[key]
+    } else if (type1 !== 'string') {
+      lang[key] = normalLangs(lang[key], standard[key])
+    }
+  }
+  return lang
+}
+
+const stopWatch = watchEffect(() => {
+  if (loaded.value) {
+    const nlangs = Object.entries(langs)
+      .map(([name, value]) => [
+        name,
+        deflang === value ? value : normalLangs(value, deflang)
+      ])
+      .reduce((t, [name, lang]) => {
+        ;(t as any)[name] = lang
+        return t
+      }, {})
+
+    const app = createApp(App, {
+      langs: nlangs,
+      onSubmit() {
+        axios.post('/dev/langs', nlangs)
+        console.log(nlangs)
+      }
+    })
+    app.mount('#app')
+    stopWatch()
+  }
+})

+ 16 - 0
src/lang/constant.ts

@@ -0,0 +1,16 @@
+// 语言支持
+export enum langNameEum {
+  zh = 'zh',
+  en = 'en',
+  jp = 'ja'
+}
+
+export const langNameDescs = {
+  [langNameEum.zh]: '中文',
+  [langNameEum.en]: 'English',
+  [langNameEum.jp]: '日本語'
+}
+
+export const deflangName = langNameEum.zh
+
+export const langNames = [langNameEum.en, langNameEum.zh, langNameEum.jp]

+ 10 - 0
src/lang/en/index.ts

@@ -0,0 +1,10 @@
+
+import { modulesParse } from '../helper'
+
+const { ctx, promise } = modulesParse(
+  import.meta.glob(['./*.ts', '!./index.ts'])
+)
+
+export default ctx
+export { promise }
+

+ 5 - 0
src/lang/en/sys.ts

@@ -0,0 +1,5 @@
+export default {
+  TOKEN_INVALID: 'token已失效',
+  NO_ACCESS: '无权访问',
+  SUCCESS: '请求成功'
+}

+ 41 - 0
src/lang/helper.ts

@@ -0,0 +1,41 @@
+import { reactive } from 'vue'
+
+export const modulesParse = (modules: Record<string, () => Promise<any>>) => {
+  const ctx: any = reactive({})
+  const requests = Promise.all(Object.values(modules).map(fn => fn()))
+
+  return {
+    ctx,
+    promise: requests.then((cts: any[]) => {
+      const keys = Object.keys(modules)
+      const lang = Object.fromEntries(keys.map((key, i) => [key, cts[i]]))
+      langMessageFactory(lang, 'lang', ctx)
+    })
+  }
+}
+
+export function langMessageFactory(
+  requireModules: { [key in string]: any },
+  prefix = 'lang',
+  lang: { [key in string]: any } = {}
+) {
+  const filenames = Object.keys(requireModules)
+
+  for (let filename of filenames) {
+    const langAtom = requireModules[filename].default
+
+    filename = filename.replace(`./${prefix}/`, '').replace(/^\.\//, '')
+    filename = filename.substring(0, filename.lastIndexOf('.'))
+
+    const locals = filename.split('/')
+    const langname = locals.pop()
+    let current = lang
+
+    for (const name of locals) {
+      lang[name] = current = lang[name] || {}
+    }
+    current[langname!] = langAtom
+  }
+
+  return lang
+}

+ 81 - 0
src/lang/index.ts

@@ -0,0 +1,81 @@
+import { App, WritableComputedRef, ref } from 'vue'
+import { createI18n, I18n as BaseI18n } from 'vue-i18n'
+import { deflangName, langNameEum, langNames, langNameDescs } from './constant'
+import { localGetFactory, localSetFactory } from '@/utils/store'
+import { paramsToStr, strToParams } from '@/utils'
+import zh, { promise as zhPromise } from './zh'
+import en, { promise as enPromise } from './en'
+import jp, { promise as jpPromise } from './ja'
+
+type I18n = BaseI18n & {
+  global: {
+    t: I18nGlobalTranslation
+    changeLang(langName: langNameEum, reload?: boolean): void
+    locale: WritableComputedRef<langNameEum>
+  }
+}
+
+export const loaded = ref(false)
+Promise.all([zhPromise, enPromise, jpPromise]).then(() => (loaded.value = true))
+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 || 'en'
+      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 lang = (params.lang || local.get(localKey)) as langNameEum
+if (lang !== local.get(localKey)) {
+  local.set(localKey, lang)
+}
+
+const i18n: I18n = createI18n({
+  legacy: false,
+  fallbackLocale: deflangName,
+  availableLocales: langNames,
+  locale: lang,
+  sync: true,
+  silentTranslationWarn: true,
+  missingWarn: false,
+  silentFallbackWarn: true
+}) as I18n
+
+export const langs = {
+  [langNameEum.en]: en,
+  [langNameEum.zh]: zh,
+  [langNameEum.jp]: jp
+}
+
+i18n.global.setLocaleMessage(langNameEum.zh, zh)
+i18n.global.setLocaleMessage(langNameEum.en, en)
+i18n.global.setLocaleMessage(langNameEum.jp, jp)
+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
+
+export * from './constant'

+ 10 - 0
src/lang/ja/index.ts

@@ -0,0 +1,10 @@
+
+import { modulesParse } from '../helper'
+
+const { ctx, promise } = modulesParse(
+  import.meta.glob(['./*.ts', '!./index.ts'])
+)
+
+export default ctx
+export { promise }
+

+ 5 - 0
src/lang/ja/sys.ts

@@ -0,0 +1,5 @@
+export default {
+  TOKEN_INVALID: 'token已失效',
+  NO_ACCESS: '无权访问',
+  SUCCESS: '请求成功'
+}

+ 10 - 0
src/lang/zh/index.ts

@@ -0,0 +1,10 @@
+
+import { modulesParse } from '../helper'
+
+const { ctx, promise } = modulesParse(
+  import.meta.glob(['./*.ts', '!./index.ts'])
+)
+
+export default ctx
+export { promise }
+

+ 5 - 0
src/lang/zh/log.ts

@@ -0,0 +1,5 @@
+export default {
+  logMsgLabel: '备注',
+  userNameLabel: '操作人',
+  createTimeLabel: '操作时间'
+}

+ 22 - 0
src/lang/zh/material.ts

@@ -0,0 +1,22 @@
+export default {
+  nickNameLabel: '成员名称',
+  userNameLabel: '账号',
+  remarkLabel: '备注',
+  roleLabel: '项目角色',
+  bindAccountLabel: '绑定手机号',
+
+  createTimeLabel: '添加时间',
+  name: '项目成员',
+  ruleUserName: '请输入用户账号',
+  ruleNickName: '请输入成员名称',
+  ruleNickName1: '成员名称最多50字',
+  ruleRemark: '请输入备注',
+  ruleRemark1: '备注最多{max}字',
+  ruleRole: '请选择项目角色',
+  rulebindAccount: '请输入用户手机号',
+
+  addMertial: '新建备注',
+  filterName: '请输入笔记名称',
+  add: '新增成员',
+  delTip: '确定要删除此用户?'
+}

+ 53 - 0
src/lang/zh/project.ts

@@ -0,0 +1,53 @@
+export default {
+  desc: {
+    done: '已完成',
+    undone: '未完成'
+  },
+  bimDesc: {
+    error: '错误',
+    done: '完成',
+    upload: '上传中',
+    transfrom: '转换中',
+    offline: '离线包生成中'
+  },
+  sceneDesc: {
+    DEL: '场景被删',
+    RUN: '计算中',
+    ERR: '计算失败',
+    SUCCESS: '计算成功',
+    ARCHIVE: '封存',
+    RERUN: '重新计算中'
+  },
+  sceneTypeDesc: {
+    SWKK: '四维看看',
+    SWKJ: '四维看见',
+    SWSS: '四维深时'
+  },
+  taggingStatusDesc: {
+    pending: '待处理',
+    progress: '进行中',
+    unsolved: '未解决',
+    solved: '已解决'
+  },
+  name: '项目',
+  projectImgLabel: '封面',
+  projectBimLabel: 'BIM文件',
+  projectImgTip: '推荐大小:500 * 500 像素',
+  projectNameLabel: '项目名称',
+
+  projectMsgLabel: '项目描述',
+  projectCreaterLabel: '创建人',
+  createTimeLabel: '创建时间',
+  updateTimeLabel: '更新时间',
+  projectStatusLabel: '项目状态',
+  addTitle: '新建项目',
+  delTitle: '删除项目',
+  updateTitle: '修改项目',
+  finshTitle: '完成项目',
+  projectNameRule: '请输入名称最多{max}字',
+  projectMsgRule: '请输入项目描述最多{max}字',
+  projectNamePleac: '请输入项目名称',
+  projectCreaterPleac: '请输入项目创建人',
+  dayPleac: '选择项目创建日期',
+  manageList: '管理列表'
+}

+ 12 - 0
src/lang/zh/role.ts

@@ -0,0 +1,12 @@
+export default {
+  roleNameLabel: '角色名称',
+  sroleNameLabel: '所属项目',
+  remarkLabel: '备注',
+  createTimeLabel: '操作时间',
+  name: '项目角色',
+  roleNameRule: '请输入角色名称',
+  remarkRule: '请输入备注,最多50字',
+  roleMenusLabel: '菜单分配',
+  add: '新增角色',
+  delMsg: '确定要删除此角色'
+}

+ 18 - 0
src/lang/zh/scene.ts

@@ -0,0 +1,18 @@
+export default {
+  delTip: '确定要删除此场景?',
+  nameLabel: '名称',
+  phoneLabel: '创建人',
+  numLabel: '场景码',
+  typeLabel: '类型',
+  statusLabel: '状态',
+  createTimeLabel: '拍摄时间',
+  name: '场景',
+  selected: '已选择{len}个场景',
+  nameRule: '请输入场景名称',
+  fileRule: '请上传BIM文件',
+  create: '创建场景',
+  nameLabel1: '场景名称',
+  nameLabel1Rule: '请输入名称最多{max}字',
+  fileLabel: 'BIM文件',
+  updateFile: '修改BIM'
+}

+ 66 - 0
src/lang/zh/sys.ts

@@ -0,0 +1,66 @@
+export default {
+  name: '四维工地管家',
+  TOKEN_INVALID: 'token已失效',
+  NO_ACCESS: '无权访问',
+  SUCCESS: '请求成功',
+  ERROR: '服务错误,请稍后再试',
+  undata: '未搜索到结果,',
+  data: '数据',
+  undataDesc: '您还没有{name},请先创建{name}~',
+  uploadBtn: '上传',
+  uploadDesc: ['支持{ extxTip }文件格式;', '最大支持上传{ maxSizeTip };'],
+  noUploadDesc: [`仅支持{extxTip}文件格式`, `最大支持上传{maxSizeTip}`],
+  more: '等',
+  404: '错误页面',
+  logout: '退出登录',
+  router: {
+    login: '登录',
+    personal: '个人信息',
+    projects: '项目管理',
+    projectScenes: '场景管理',
+    projectMaterial: '项目资料',
+    projectMembers: '成员管理',
+    projectTaggings: '项目标注',
+    projectRoles: '项目角色',
+    project: '项目'
+  },
+  copyAuth: '请授予写入粘贴板权限!',
+  unLoginName: '游客',
+  add: '新增',
+  update: '修改',
+  cancel: '取消',
+  save: '保存',
+  query: '查看',
+  oper: '操作',
+  del: '删除',
+  tipTitle: '系统提示',
+  good: '好',
+  time: ['早上', '中午', '下午', '晚上'],
+  projectCount: '项目数',
+  projectFileCount: '项目文件数',
+  projectSceneCount: '项目场景数',
+  projectOverCount: '已完成项目数',
+  operLog: '操作记录',
+  updateInfo: '操作记录',
+  updatePwd: '修改密码',
+  delTip: '删除后无法恢复,是否确认?',
+  selectTime: '选择日期',
+  search: '搜索',
+  reset: '重置',
+  all: '全部',
+  edit: '编辑',
+  sync: '同步',
+  select: '选择',
+  create: '创建',
+  loginh1: '欢迎登录',
+  loginh2: '账号登录',
+  phoneRule: '请输入账号',
+  passwordRule: '请输入账号',
+  phoneRul1: '请输入正确账号',
+  passwordRule1: '手机号或密码有误',
+  rememberLabel: '记住密码',
+  login: '登录',
+  forget: '忘记密码',
+  register: '注册',
+  un: '未知'
+}

+ 9 - 0
src/lang/zh/tagging.ts

@@ -0,0 +1,9 @@
+export default {
+  markingTitleLabel: '场景标注',
+  createByLabel: '创建人',
+  usersLabel: '涉及的成员',
+  statusLabel: '状态',
+  lastUpdateByLabel: '最后修改人',
+  updateTimeLabel: '最后修改时间',
+  filterNamePlace: '请输入标注关键字'
+}

+ 2 - 1
src/layout/header.vue

@@ -39,6 +39,7 @@ import { useUserStore } from '@/store'
 import { gotoLogin } from '@/api'
 import { router, getRouteTreePaths, routesMetas, RoutesName } from '@/router'
 import { computed } from 'vue'
+import { ui18n } from '@/lang'
 
 defineOptions({ name: 'LayoutHeader' })
 
@@ -58,7 +59,7 @@ const routeChange = (routeName: RoutesName) => {
   })
 }
 
-const menus = [{ label: '退出登录', key: 'logout' }]
+const menus = [{ label: ui18n.t('sys.logout'), key: 'logout' }]
 const handlerMenuClick: MenuProps['onClick'] = async e => {
   if (e.key === 'logout') {
     await userStore.logout()

+ 4 - 0
src/main.ts

@@ -3,12 +3,14 @@ import '@/assets/iconfont/iconfont.css'
 import 'ant-design-vue/lib/message/style/index.less'
 import './style.scss'
 import App from './App.vue'
+import { setupI18n, ui18n } from '@/lang'
 import { router, enableRouteNames } from './router'
 import { pinia, useProject, useUserStore } from './store'
 
 export const app = createApp(App)
 app.use(router)
 app.use(pinia)
+setupI18n(app)
 app.mount('#app')
 
 const userStore = useUserStore(pinia)
@@ -22,3 +24,5 @@ watch(
   },
   { flush: 'sync', immediate: true }
 )
+
+document.title = ui18n.t('sys.name')

+ 11 - 9
src/router/constant.ts

@@ -1,3 +1,5 @@
+import { ui18n } from '@/lang'
+
 export enum RoutesName {
   login = 'login',
   personal = 'personal',
@@ -25,33 +27,33 @@ export const routesPaths = {
 
 export const routesMetas = {
   [RoutesName.login]: {
-    title: '登录'
+    title: ui18n.t('sys.router.login')
   },
   [RoutesName.personal]: {
-    title: '个人信息',
+    title: ui18n.t('sys.router.login'),
     icon: ''
   },
   [RoutesName.projects]: {
-    title: '项目管理',
+    title: ui18n.t('sys.router.projects'),
     icon: ''
   },
   [RoutesName.projectScenes]: {
-    title: '场景管理'
+    title: ui18n.t('sys.router.projectScenes')
   },
   [RoutesName.projectMaterial]: {
-    title: '项目资料'
+    title: ui18n.t('sys.router.projectMaterial')
   },
   [RoutesName.projectMembers]: {
-    title: '成员管理'
+    title: ui18n.t('sys.router.projectMembers')
   },
   [RoutesName.projectTaggings]: {
-    title: '项目标注'
+    title: ui18n.t('sys.router.projectTaggings')
   },
   [RoutesName.projectRoles]: {
-    title: '项目角色'
+    title: ui18n.t('sys.router.projectRoles')
   },
   [RoutesName.project]: {
-    title: '项目'
+    title: ui18n.t('sys.router.project')
   }
 }
 

+ 9 - 0
src/setup.ts

@@ -0,0 +1,9 @@
+import { loaded } from '@/lang'
+import { watchEffect } from 'vue'
+
+const stopWatch = watchEffect(() => {
+  if (loaded.value) {
+    import('./main')
+    stopWatch()
+  }
+})

+ 3 - 1
src/shared/copy.ts

@@ -1,3 +1,5 @@
+import { ui18n } from '@/lang'
+
 export const copyText = async (
   text: string,
   fallback?: boolean
@@ -8,7 +10,7 @@ export const copyText = async (
 
     if (permiss && permiss.state === 'denied') {
       console.error(permiss)
-      throw new Error('请授予写入粘贴板权限!')
+      throw new Error(ui18n.t('sys.copyAuth'))
     } else {
       try {
         await navigator.clipboard.writeText(text)

+ 3 - 5
src/shared/test.ts

@@ -1,4 +1,3 @@
-
 export const arr = [
   {
     id: 2,
@@ -73,25 +72,24 @@ const toTrees = (items: Item[]) => {
     rootTreeAddNode(itemTree)
   }
 
-
   return {
     roots,
     addItem(item: Item) {
       if (map.has(item.id)) {
-        return;
+        return
       }
       const itemTree = normalItem(item)
       map.set(item.id, itemTree)
       if (rootTreeAddNode(itemTree)) {
         const oldRoots = roots.slice()
-        roots.length = 0;
+        roots.length = 0
         oldRoots.forEach(rootTreeAddNode)
       }
     },
     delItem(item: Item) {
       const itemTree = map.get(item.id)
       if (!itemTree) {
-        return;
+        return
       }
       const sameTrees = map.get(itemTree.parentId)?.children || roots
       const index = sameTrees.indexOf(itemTree)

+ 2 - 1
src/store/user.ts

@@ -12,12 +12,13 @@ import { defineStore } from 'pinia'
 
 import type { User, UserMeta, LoginState } from '@/api'
 import { RoutesName } from '@/router'
+import { ui18n } from '@/lang'
 
 export const LeastMenus = [RoutesName.projects, RoutesName.login]
 
 const defState = {
   current: {
-    nickname: '游客',
+    nickname: ui18n.t('sys.unLoginName'),
     phone: '',
     email: '',
     avatar: 'https://4dkk.4dage.com/head/18819272208/head_1662022947583.png'

+ 182 - 0
src/utils/file-serve.ts

@@ -0,0 +1,182 @@
+function bom(blob, opts) {
+  if (typeof opts === 'undefined') opts = { autoBom: false }
+  else if (typeof opts !== 'object') {
+    console.warn('Deprecated: Expected third argument to be a object')
+    opts = { autoBom: !opts }
+  }
+
+  if (
+    opts.autoBom &&
+    /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(
+      blob.type
+    )
+  ) {
+    return new Blob([String.fromCharCode(0xfeff), blob], { type: blob.type })
+  }
+  return blob
+}
+
+function download(url, name, opts, onprogress): Promise<void> {
+  return new Promise((resolve, reject) => {
+    const xhr = new XMLHttpRequest()
+    xhr.open('GET', url)
+    xhr.responseType = 'blob'
+    xhr.onload = function () {
+      saveAs(xhr.response, name, opts).then(resolve)
+    }
+    if (onprogress) {
+      xhr.onprogress = ev => {
+        if (ev.lengthComputable) {
+          onprogress(ev.loaded / ev.total)
+        }
+      }
+    }
+    xhr.onerror = function () {
+      reject('could not download file')
+    }
+    xhr.send()
+  })
+}
+
+function corsEnabled(url) {
+  const xhr = new XMLHttpRequest()
+  // use sync to avoid popup blocker
+  xhr.open('HEAD', url, false)
+  try {
+    xhr.send()
+  } catch (e) {}
+  return xhr.status >= 200 && xhr.status <= 299
+}
+
+function click(node) {
+  return new Promise<void>(resolve => {
+    setTimeout(() => {
+      try {
+        node.dispatchEvent(new MouseEvent('click'))
+      } catch (e) {
+        const evt = document.createEvent('MouseEvents')
+        evt.initMouseEvent(
+          'click',
+          true,
+          true,
+          window,
+          0,
+          0,
+          0,
+          80,
+          20,
+          false,
+          false,
+          false,
+          false,
+          0,
+          null
+        )
+        node.dispatchEvent(evt)
+      }
+      resolve()
+    }, 0)
+  })
+}
+
+const isMacOSWebView =
+  navigator &&
+  /Macintosh/.test(navigator.userAgent) &&
+  /AppleWebKit/.test(navigator.userAgent) &&
+  !/Safari/.test(navigator.userAgent)
+
+type SaveAs = (
+  blob: Blob | string,
+  name?: string,
+  opts?: { autoBom: boolean },
+  onprogress?: (progress: number) => void
+) => Promise<void>
+;(window as any).getFileName = () => lastName
+let lastName = ''
+
+const global = window
+
+export const saveAs: SaveAs =
+  'download' in HTMLAnchorElement.prototype && !isMacOSWebView
+    ? (blob, name = 'download', opts, onprogress) => {
+        lastName = name
+        const URL = global.URL || global.webkitURL
+        const a = document.createElement('a')
+
+        a.download = name
+        a.rel = 'noopener'
+
+        if (typeof blob === 'string') {
+          a.href = blob
+          if (a.origin !== location.origin) {
+            if (corsEnabled(a.href)) {
+              return download(blob, name, opts, onprogress)
+            }
+            a.target = '_blank'
+          }
+          return click(a)
+        } else {
+          a.href = URL.createObjectURL(blob)
+          setTimeout(function () {
+            URL.revokeObjectURL(a.href)
+          }, 4e4) // 40s
+          return click(a)
+        }
+      }
+    : 'msSaveOrOpenBlob' in navigator
+    ? (blob, name = 'download', opts, onprogress) => {
+        if (typeof blob === 'string') {
+          if (corsEnabled(blob)) {
+            return download(blob, name, opts, onprogress)
+          } else {
+            const a = document.createElement('a')
+            a.href = blob
+            a.target = '_blank'
+            return click(a)
+          }
+        } else {
+          return (navigator as any).msSaveOrOpenBlob(bom(blob, opts), name)
+            ? Promise.resolve()
+            : Promise.reject('unknown')
+        }
+      }
+    : (blob, name, opts, onprogress) => {
+        if (typeof blob === 'string')
+          return download(blob, name, opts, onprogress)
+
+        const force = blob.type === 'application/octet-stream'
+        const isSafari =
+          /constructor/i.test(HTMLElement.toString()) || (global as any).safari
+        const isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent)
+
+        if (
+          (isChromeIOS || (force && isSafari) || isMacOSWebView) &&
+          typeof FileReader !== 'undefined'
+        ) {
+          return new Promise<void>((resolve, reject) => {
+            const reader = new FileReader()
+            reader.onloadend = function () {
+              let url = reader.result as string
+              url = isChromeIOS
+                ? url
+                : url.replace(/^data:[^;]*;/, 'data:attachment/file;')
+              location.href = url
+              resolve()
+            }
+            reader.onerror = function () {
+              reject()
+            }
+            reader.readAsDataURL(blob)
+          })
+        } else {
+          const URL = global.URL || global.webkitURL
+          const url = URL.createObjectURL(blob)
+          location.href = url
+          setTimeout(function () {
+            URL.revokeObjectURL(url)
+          }, 4e4) // 40s
+          return Promise.resolve()
+        }
+      }
+
+export default saveAs

+ 25 - 0
src/utils/index.ts

@@ -0,0 +1,25 @@
+// 字符串转params对象
+export const strToParams = (str: string) => {
+  if (str[0] === '?') {
+    str = str.substr(1)
+  }
+
+  const result: { [key: string]: string } = {}
+  const splitRG = /([^=&]+)(?:=([^&]*))?&?/
+
+  let rgRet: any
+  while ((rgRet = str.match(splitRG))) {
+    result[rgRet[1]] = rgRet[2] === undefined ? true : rgRet[2]
+    str = str.substr(rgRet[0].length)
+  }
+
+  return result
+}
+
+// 对象转params
+export const paramsToStr = (params: { [key: string]: string | boolean }) =>
+  '?' +
+  Object.keys(params)
+    .filter(key => params[key] !== undefined)
+    .map(key => `${key}${params[key] == false ? '' : `=${params[key]}`}`)
+    .join('&')

+ 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
+}

+ 5 - 4
src/views/material/columns.ts

@@ -1,4 +1,5 @@
 import type { Member } from '@/api'
+import { ui18n } from '@/lang'
 import type { ColumnsType } from 'ant-design-vue/es/table'
 
 export const materialColumns: ColumnsType<Member> = [
@@ -9,22 +10,22 @@ export const materialColumns: ColumnsType<Member> = [
   //   customRender: ({ index }) => index + 1
   // },
   {
-    title: '成员名称',
+    title: ui18n.t('material.nickNameLabel'),
     dataIndex: 'userName',
     key: 'userName'
   },
   {
-    title: '账号',
+    title: ui18n.t('material.userNameLabel'),
     dataIndex: 'userName',
     key: 'userName'
   },
   {
-    title: '备注',
+    title: ui18n.t('material.remarkLabel'),
     key: 'remark',
     dataIndex: 'remark'
   },
   {
-    title: '添加时间',
+    title: ui18n.t('material.createTimeLabel'),
     dataIndex: 'createTime',
     key: 'createTime'
   }

+ 15 - 9
src/views/material/edit.vue

@@ -1,14 +1,16 @@
 <template>
   <a-modal
     v-model:visible="visible"
-    :title="`${editMember.teamId ? '修改' : '新增'}项目成员`"
+    :title="`${editMember.teamId ? $t('sys.update') : $t('sys.add')}${$t(
+      'material.name'
+    )}`"
     width="480px"
     :after-close="onCancel"
     @ok="saveHandler"
   >
     <template #footer>
       <a-button class="action-bottom" size="middle" @click="visible = false">
-        取消
+        {{ $t('sys.cancel') }}
       </a-button>
       <a-button
         class="action-bottom"
@@ -16,7 +18,7 @@
         size="middle"
         @click="saveHandler"
       >
-        保存
+        {{ $t('sys.save') }}
       </a-button>
     </template>
 
@@ -29,21 +31,24 @@
     >
       <a-form-item
         name="userName"
-        label="账号"
-        :rules="[{ required: true, message: '请输入用户账号' }]"
+        :label="$t('material.nickNameLabel')"
+        :rules="[{ required: true, message: ui18n.t('material.ruleUserName') }]"
       >
         <a-input
           v-model:value="editMember.userName"
           :disabled="!!editMember.teamId"
-          placeholder="请输入用户账号"
+          :placeholder="$t('material.ruleUserName')"
         />
       </a-form-item>
       <a-form-item
         name="remark"
-        label="备注"
-        :rules="[{ required: true, message: '请输入备注' }]"
+        :label="$t('material.remarkLabel')"
+        :rules="[{ required: true, message: $t('material.ruleRemark') }]"
       >
-        <a-input v-model:value="editMember.remark" placeholder="请输入备注" />
+        <a-input
+          v-model:value="editMember.remark"
+          :placeholder="$t('material.ruleRemark')"
+        />
       </a-form-item>
     </a-form>
   </a-modal>
@@ -54,6 +59,7 @@ import { ref, defineProps, toRaw } from 'vue'
 
 import type { Member, SetMemberProps } from '@/api'
 import type { FormInstance } from 'ant-design-vue'
+import { ui18n } from '@/lang'
 
 export type EditMember = PartialPart<SetMemberProps, 'projectId'>
 

+ 7 - 4
src/views/material/list.vue

@@ -1,11 +1,13 @@
 <template>
   <BodyPanlHeader>
-    <a-button type="primary" @click="addMaterial()">新建备注</a-button>
+    <a-button type="primary" @click="addMaterial()">{{
+      $t('material.addMertial')
+    }}</a-button>
     <div>
       <a-input-search
         v-model:value="filterName"
         style="width: 280px"
-        placeholder="请输入笔记名称"
+        :placeholder="$t('material.filterName')"
         @search="updateMaterials"
       />
     </div>
@@ -20,7 +22,7 @@
       <template #bodyCell="{ column }">
         <template v-if="column.key === 'action'">
           <div class="table-actions">
-            <a>查看</a>
+            <a>{{ $t('sys.query') }}</a>
           </div>
         </template>
       </template>
@@ -45,11 +47,12 @@ import {
 } from '@/api'
 
 import type { Member } from '@/api'
+import { ui18n } from '@/lang'
 
 const materialColumns = [
   ...baseColumns,
   {
-    title: '操作',
+    title: ui18n.t('sys.oper'),
     dataIndex: 'action',
     key: 'action'
   }

+ 7 - 6
src/views/member/columns.ts

@@ -1,4 +1,5 @@
 import type { Member } from '@/api'
+import { ui18n } from '@/lang'
 import type { ColumnsType } from 'ant-design-vue/es/table'
 import { h } from 'vue'
 
@@ -10,17 +11,17 @@ export const memberColumns: ColumnsType<Member> = [
   //   customRender: ({ index }) => index + 1
   // },
   {
-    title: '成员名称',
+    title: ui18n.t('material.nickNameLabel'),
     dataIndex: 'nickName',
     key: 'nickName'
   },
   {
-    title: '账号',
+    title: ui18n.t('material.userNameLabel'),
     dataIndex: 'userName',
     key: 'userName'
   },
   {
-    title: '项目角色',
+    title: ui18n.t('material.roleLabel'),
     dataIndex: 'roles',
     key: 'roles',
     customRender(data) {
@@ -32,17 +33,17 @@ export const memberColumns: ColumnsType<Member> = [
     }
   },
   {
-    title: '绑定手机号',
+    title: ui18n.t('material.bindAccountLabel'),
     dataIndex: 'bindAccount',
     key: 'bindAccount'
   },
   {
-    title: '备注',
+    title: ui18n.t('material.remarkLabel'),
     key: 'remark',
     dataIndex: 'remark'
   },
   {
-    title: '添加时间',
+    title: ui18n.t('material.createTimeLabel'),
     dataIndex: 'createTime',
     key: 'createTime'
   }

+ 33 - 18
src/views/member/edit.vue

@@ -1,14 +1,16 @@
 <template>
   <a-modal
     v-model:visible="visible"
-    :title="`${editMember.teamId ? '修改' : '新增'}项目成员`"
+    :title="`${editMember.teamId ? $t('sys.add') : $t('sys.update')}${$t(
+      'material.name'
+    )}`"
     width="480px"
     :after-close="onCancel"
     @ok="saveHandler"
   >
     <template #footer>
       <a-button class="action-bottom" size="middle" @click="visible = false">
-        取消
+        {{ $t('sys.cancel') }}
       </a-button>
       <a-button
         class="action-bottom"
@@ -16,7 +18,7 @@
         size="middle"
         @click="saveHandler"
       >
-        保存
+        {{ $t('sys.save') }}
       </a-button>
     </template>
 
@@ -29,33 +31,36 @@
     >
       <a-form-item
         name="userName"
-        label="账号"
-        :rules="[{ required: true, message: '请输入用户账号' }]"
+        :label="$t('material.userNameLabel')"
+        :rules="[{ required: true, message: $t('material.ruleUserName') }]"
       >
         <a-input
           v-model:value="editMember.userName"
           :disabled="!!editMember.teamId"
-          placeholder="请输入用户账号"
+          :placeholder="$t('material.ruleUserName')"
         />
       </a-form-item>
       <a-form-item
         name="nickName"
-        label="成员名称"
+        :label="$t('material.nickNameLabel')"
         :rules="[
-          { required: true, message: '请输入成员名称' },
-          { max: 50, message: '成员名称最多50字' }
+          { required: true, message: $t('material.ruleNickName') },
+          { max: 50, message: $t('material.ruleNickName1') }
         ]"
       >
         <a-input
           v-model:value="editMember.nickName"
-          placeholder="请输入成员名称"
+          :placeholder="$t('material.ruleNickName')"
         />
       </a-form-item>
       <a-form-item
-        label="项目角色"
-        :rules="[{ required: true, message: '请选择项目角色' }]"
+        :label="$t('material.roleLabel')"
+        :rules="[{ required: true, message: $t('material.ruleRole') }]"
       >
-        <a-select v-model:value="currentRole" placeholder="请选择项目角色">
+        <a-select
+          v-model:value="currentRole"
+          :placeholder="$t('material.ruleRole')"
+        >
           <a-select-option
             v-for="role in roleOptions"
             :key="role.roleId"
@@ -64,22 +69,32 @@
           >
         </a-select>
       </a-form-item>
-      <a-form-item name="bindAccount" label="绑定手机号" :rules="[phoneRule]">
+      <a-form-item
+        name="bindAccount"
+        :label="$t('material.bindAccountLabel')"
+        :rules="[phoneRule]"
+      >
         <a-input
           v-model:value="editMember.bindAccount"
-          placeholder="请输入用户手机号"
+          :placeholder="$t('material.rulebindAccount')"
         />
       </a-form-item>
       <a-form-item
         name="remark"
-        label="备注"
-        :rules="[{ required: false, max: 200, message: '备注最多200字' }]"
+        :label="$t('material.remarkLabel')"
+        :rules="[
+          {
+            required: false,
+            max: 200,
+            message: $t('material.ruleRemark1', 200)
+          }
+        ]"
       >
         <a-textarea
           v-model:value.trim="editMember.remark"
           :resize="false"
           style="height: 104px; resize: none"
-          placeholder="请输入备注,最多200字"
+          :placeholder="$t('material.ruleRemark1', 200)"
         />
       </a-form-item>
     </a-form>

+ 16 - 10
src/views/member/list.vue

@@ -1,11 +1,13 @@
 <template>
   <BodyPanlHeader>
-    <a-button type="primary" @click="setMember()">新增成员</a-button>
+    <a-button type="primary" @click="setMember()">{{
+      $t('material.add')
+    }}</a-button>
     <div>
       <a-input-search
         v-model:value="params.userName"
         style="width: 280px"
-        placeholder="请输入成员名称"
+        :placeholder="$t('material.ruleNickName')"
         allow-clear
         @search="updateList"
       />
@@ -22,9 +24,11 @@
       <template #bodyCell="{ column, record }">
         <template v-if="column.key === 'action'">
           <div class="table-actions">
-            <a @click="setMember(record)">修改</a>
+            <a @click="setMember(record)">{{ $t('sys.update') }}</a>
             <!-- <a>查看</a> -->
-            <a class="warn" @click="delMemberHandler(record)">删除</a>
+            <a class="warn" @click="delMemberHandler(record)">{{
+              $t('sys.del')
+            }}</a>
           </div>
         </template>
       </template>
@@ -34,7 +38,7 @@
 
 <script lang="ts" setup>
 import EditMember from './edit.vue'
-import { computed, onDeactivated, reactive, toRaw, watch } from "vue";
+import { computed, onDeactivated, reactive, toRaw, watch } from 'vue'
 import { BodyPanlHeader, BodyPanlBody } from '@/layout/panl'
 import { usePaging } from '@/hook'
 import { router } from '@/router'
@@ -50,11 +54,13 @@ import {
 } from '@/api'
 
 import type { Member } from '@/api'
+import { ui18n } from '@/lang'
+import sys from '@/lang/en/sys'
 
 const memberColumns = [
   ...baseColumns,
   {
-    title: '操作',
+    title: ui18n.t('sys.oper'),
     dataIndex: 'action',
     key: 'action'
   }
@@ -93,12 +99,12 @@ const setMember = (member?: Member) => {
 }
 const delMemberHandler = (member: Member) => {
   Modal.confirm({
-    content: '确定要删除此用户?',
-    title: '系统提示',
+    content: ui18n.t('material.delTip'),
+    title: ui18n.t('sys.tipTitle'),
     width: '400px',
-    okText: '删除',
+    okText: ui18n.t('sys.del'),
     icon: null,
-    cancelText: '取消',
+    cancelText: ui18n.t('sys.cancel'),
     onOk: async () => {
       try {
         await deleteMember(member.teamId)

+ 19 - 12
src/views/personal/index.vue

@@ -3,21 +3,21 @@
     <div class="user">
       <img :src="user.avatar" />
       <div class="info">
-        <h4>{{ getGreet() }},{{ user.nickname }}</h4>
+        <h4>{{ getGreet() }}{{ $t('sys.good') }},{{ user.nickname }}</h4>
         <div class="account">
           <span>{{ user.phone }}</span>
           <span>{{ user.email }}</span>
         </div>
         <div class="account-manage">
-          <a :href="userInfoLink">修改信息 ></a>
-          <a :href="userInfoLink">修改密码 ></a>
+          <a :href="userInfoLink">{{ $t('sys.updateInfo') }} ></a>
+          <a :href="userInfoLink">{{ $t('sys.updatePwd') }} ></a>
         </div>
       </div>
     </div>
     <Simples :data="simples" />
   </HeadPanl>
 
-  <BodyPanl title="操作记录">
+  <BodyPanl :title="$t('sys.operLog')">
     <RecordList />
   </BodyPanl>
 </template>
@@ -30,23 +30,30 @@ import { toRefs } from 'vue'
 import { computed } from 'vue'
 import { useUserStore } from '@/store'
 import { userInfoLink } from '@/env'
+import { ui18n } from '@/lang'
 
 const { current: user, meta } = toRefs(useUserStore().$state)
 const simples = computed(() => [
-  { label: '项目数', value: meta.value.projectCount },
-  { label: '项目文件数', value: meta.value.projectFileCount },
-  { label: '项目场景数', value: meta.value.projectSceneCount },
-  { label: '已完成项目数', value: meta.value.projectOverCount }
+  { label: ui18n.t('sys.projectCount'), value: meta.value.projectCount },
+  {
+    label: ui18n.t('sys.projectFileCount'),
+    value: meta.value.projectFileCount
+  },
+  {
+    label: ui18n.t('sys.projectSceneCount'),
+    value: meta.value.projectSceneCount
+  },
+  { label: ui18n.t('sys.projectOverCount'), value: meta.value.projectOverCount }
 ])
 const getGreet = () => {
   const hours = new Date().getHours()
   return hours > 5 && hours < 11
-    ? '早上'
+    ? ui18n.t('sys.time[0]')
     : hours >= 11 && hours < 15
-    ? '中午'
+    ? ui18n.t('sys.time[1]')
     : hours >= 15 && hours <= 18
-    ? '下午'
-    : '晚上'
+    ? ui18n.t('sys.time[2]')
+    : ui18n.t('sys.time[3]')
 }
 </script>
 

+ 9 - 8
src/views/project/columns.ts

@@ -6,6 +6,7 @@ import unProjectPic from '@/assets/images/un-project-pic.png'
 
 import type { SimpleProject } from '@/api'
 import type { ColumnsType } from 'ant-design-vue/es/table'
+import { ui18n } from '@/lang'
 
 const projectStore = useProject(pinia)
 
@@ -26,7 +27,7 @@ export const projectColumns: ColumnsType<SimpleProject> = [
   //   customRender: ({ index }) => index + 1
   // },
   {
-    title: '封面',
+    title: ui18n.t('project.projectImgLabel'),
     dataIndex: 'projectImg',
     key: 'projectImg',
     customRender({ record }) {
@@ -38,33 +39,33 @@ export const projectColumns: ColumnsType<SimpleProject> = [
     }
   },
   {
-    title: '项目名称',
+    title: ui18n.t('project.projectNameLabel'),
     dataIndex: 'projectName',
     key: 'projectName'
   },
   {
-    title: '创建人',
+    title: ui18n.t('project.projectCreaterLabel'),
     dataIndex: 'projectCreater',
     key: 'projectCreater'
   },
   {
-    title: '创建时间',
+    title: ui18n.t('project.projectCreaterLabel'),
     dataIndex: 'createTime',
     key: 'createTime'
   },
   {
-    title: '更新时间',
+    title: ui18n.t('project.updateTimeLabel'),
     dataIndex: 'updateTime',
     key: 'updateTime'
   },
   {
-    title: '项目状态',
+    title: ui18n.t('project.projectStatusLabel'),
     dataIndex: 'projectStatus',
     key: 'projectStatus',
     customRender: ({ record }) => ProjectStatusComp(record)
   },
   {
-    title: '操作',
+    title: ui18n.t('sys.oper'),
     dataIndex: 'projectId',
     key: 'projectId',
     customRender({ record }) {
@@ -76,7 +77,7 @@ export const projectColumns: ColumnsType<SimpleProject> = [
         })
       }
 
-      const renderManage = h('a', { onClick: onManage }, '查看')
+      const renderManage = h('a', { onClick: onManage }, ui18n.t('sys.query'))
 
       return h(
         'div',

+ 28 - 21
src/views/project/detailed.vue

@@ -7,28 +7,42 @@
           v-if="project?.projectStatus === ProjectStatus.undone"
           class="actions"
         >
-          <a-button @click="updateProject">修改项目</a-button>
-          <a-button @click="deleteProject">删除项目</a-button>
-          <a-button type="primary" @click="finishProject"> 完成项目 </a-button>
+          <a-button @click="updateProject">{{
+            $t('project.updateTitle')
+          }}</a-button>
+          <a-button @click="deleteProject">{{
+            $t('project.delTitle')
+          }}</a-button>
+          <a-button type="primary" @click="finishProject">
+            {{ $t('project.finshTitle') }}
+          </a-button>
         </div>
       </div>
       <div class="body">
         <div class="info">
           <div class="meta">
-            <p><span>创建人</span>{{ project?.projectCreater }}</p>
-            <p><span>创建时间</span>{{ project?.createTime }}</p>
-            <p><span>更新时间</span>{{ project?.updateTime }}</p>
+            <p>
+              <span>{{ $t('project.projectCreaterLabel') }}</span
+              >{{ project?.projectCreater }}
+            </p>
+            <p>
+              <span>{{ $t('project.createTimeLabel') }}</span
+              >{{ project?.createTime }}
+            </p>
+            <p>
+              <span>{{ $t('project.updateTimeLabel') }}</span
+              >{{ project?.updateTime }}
+            </p>
           </div>
           <p class="desc">{{ project?.projectMsg }}</p>
         </div>
-        <!-- <Simples :data="simples" /> -->
       </div>
 
       <a-tabs :active-key="activeTabName" @change="changeTab">
         <a-tab-pane
           v-for="option in tabOptions"
           :key="option.key"
-          :tab="option.label as any"
+          :tab="option.label"
         />
       </a-tabs>
     </div>
@@ -53,14 +67,7 @@ import { useProject, ProjectStatus } from '@/store'
 import { useRealtime } from '@/hook'
 import { renderModal } from '@/helper'
 import { uploadFile } from '@/api'
-
-const simples = computed(() => [
-  { label: '信息', value: 2 },
-  { label: '待处理', value: 2 },
-  { label: '已解决', value: 2 },
-  { label: '未解决', value: 2 },
-  { label: '进行中', value: 2 }
-])
+import { ui18n } from '@/lang'
 
 const allOptions = [
   // RoutesName.projectMaterial,
@@ -98,7 +105,7 @@ useRealtime(() => {
   const back = () => router.back()
   if (!id || id < 0) {
     // back()
-    throw '错误页面'
+    throw ui18n.t('sys.404')
   }
   return projectStore.setCurrent(id)
 })
@@ -126,12 +133,12 @@ onDeactivated(() => {
 
 const deleteProject = () => {
   Modal.confirm({
-    content: '删除后无法恢复,是否确认?',
-    title: '删除项目',
+    content: ui18n.t('sys.delTip'),
+    title: ui18n.t('project.delTitle'),
     width: '400px',
-    okText: '删除',
+    okText: ui18n.t('sys.del'),
     icon: null,
-    cancelText: '取消',
+    cancelText: ui18n.t('sys.cancel'),
     onOk: async () => {
       await projectStore.delete()
       router.replace({ name: RoutesName.projects })

+ 17 - 10
src/views/project/edit.vue

@@ -1,14 +1,16 @@
 <template>
   <a-modal
     v-model:visible="visible"
-    :title="`${project.projectId ? '修改' : '新建'}项目`"
+    :title="`${project.projectId ? $t('sys.update') : $t('sys.add')}${$t(
+      'project.name'
+    )}`"
     width="660px"
     :after-close="onCancel"
     @ok="saveHandler"
   >
     <template #footer>
       <a-button class="action-bottom" size="middle" @click="visible = false">
-        取消
+        {{ $t('sys.cancel') }}
       </a-button>
       <a-button
         class="action-bottom"
@@ -16,7 +18,7 @@
         size="middle"
         @click="saveHandler"
       >
-        {{ project.projectId ? '修改' : '添加' }}
+        {{ project.projectId ? $t('sys.update') : $t('sys.add') }}
       </a-button>
     </template>
 
@@ -29,7 +31,7 @@
     >
       <a-form-item
         name="projectName"
-        label="项目名称"
+        :label="$t('project.projectNameLabel')"
         :rules="[rules.projectName]"
       >
         <a-input
@@ -40,7 +42,7 @@
       </a-form-item>
       <a-form-item
         name="projectMsg"
-        label="项目描述"
+        :label="$t('project.projectMsgLabel')"
         :rules="[rules.projectMsg]"
       >
         <a-textarea
@@ -51,15 +53,19 @@
           :placeholder="rules.projectMsg.message"
         />
       </a-form-item>
-      <a-form-item name="projectImg" label="项目封面">
+      <a-form-item name="projectImg" :label="$t('project.projectImgLabel')">
         <Upload
           v-model:file="project.projectImg"
           :max-size="10"
           :extnames="['png', 'jpg', 'gif']"
-          :tips="['推荐大小:500 * 500 像素']"
+          :tips="[$t('project.projectImgTip')]"
         />
       </a-form-item>
-      <a-form-item v-if="!project.projectId" name="projectImg" label="BIM文件">
+      <a-form-item
+        v-if="!project.projectId"
+        name="projectImg"
+        :label="$t('project.projectBimLabel')"
+      >
         <Upload
           v-model:file="project.bimFile"
           :max-size="5 * 1024"
@@ -76,6 +82,7 @@ import Upload from '@/components/upload/index.vue'
 
 import type { InsertProjectData, Project } from '@/api'
 import type { FormInstance } from 'ant-design-vue'
+import { ui18n } from '@/lang'
 
 export type IProject = Omit<InsertProjectData, 'projectImg'> & {
   projectId?: Project['projectId']
@@ -106,13 +113,13 @@ const rules = {
     required: true,
     max: 40,
     min: 1,
-    message: '请输入名称最多40字'
+    message: ui18n.t('project.projectNameRule', { max: 40 })
   },
   projectMsg: {
     required: true,
     max: 200,
     min: 1,
-    message: '请输入项目描述最多200字'
+    message: ui18n.t('project.projectMsgRule', { max: 200 })
   }
 }
 const fromRef = ref<FormInstance>()

+ 23 - 12
src/views/project/list.vue

@@ -9,29 +9,35 @@
       autocomplete="off"
       @finish="updateList"
     >
-      <a-form-item label="项目名称" name="projectName">
+      <a-form-item :label="$t('project.projectNameLabel')" name="projectName">
         <a-input
           v-model:value="filterState.projectName"
-          placeholder="请输入项目名称"
+          :placeholder="$t('project.projectNamePleac')"
           allow-clear
         />
       </a-form-item>
-      <a-form-item label="创建人" name="projectCreater">
+      <a-form-item
+        :label="$t('project.projectCreaterLabel')"
+        name="projectCreater"
+      >
         <a-input
           v-model:value="filterState.projectCreater"
-          placeholder="请输入项目创建人"
+          :placeholder="$t('project.projectCreaterPleac')"
           allow-clear
         />
       </a-form-item>
-      <a-form-item label="选择日期" name="day">
+      <a-form-item :label="$t('sys.selectTime')" name="day">
         <a-date-picker
           v-model:value="filterState.day"
           style="width: 100%"
-          placeholder="选择项目创建日期"
+          :placeholder="$t('project.dayPleac')"
           allow-clear
         />
       </a-form-item>
-      <a-form-item label="项目状态" name="projectStatus">
+      <a-form-item
+        :label="$t('project.projectStatusLabel')"
+        name="projectStatus"
+      >
         <a-select
           ref="select"
           :value="filterState.projectStatus"
@@ -49,16 +55,20 @@
       </a-form-item>
 
       <a-form-item class="actions">
-        <a-button type="primary" html-type="submit">搜索</a-button>
+        <a-button type="primary" html-type="submit">{{
+          $t('sys.search')
+        }}</a-button>
         <a-button style="margin-left: 10px" @click="resetFilter">
-          重置
+          {{ $t('sys.reset') }}
         </a-button>
       </a-form-item>
     </a-form>
   </HeadPanl>
-  <BodyPanl title="管理列表">
+  <BodyPanl :title="$t('project.manageList')">
     <template #action>
-      <a-button type="primary" @click="createProject">新建项目</a-button>
+      <a-button type="primary" @click="createProject">{{
+        $t('project.addTitle')
+      }}</a-button>
     </template>
     <a-table
       :scroll="{ x: '100%', y: 510 }"
@@ -89,9 +99,10 @@ import {
 import type { All, InsertProjectData } from '@/api'
 import type { FormInstance } from 'ant-design-vue'
 import type { Dayjs } from 'dayjs'
+import { ui18n } from '@/lang'
 
 const statusOptions = [
-  { label: '全部', value: all },
+  { label: ui18n.t('sys.all'), value: all },
   { label: ProjectStatusDesc[ProjectStatus.done], value: ProjectStatus.done },
   {
     label: ProjectStatusDesc[ProjectStatus.undone],

+ 6 - 5
src/views/record/columns.ts

@@ -3,6 +3,7 @@ import { h } from 'vue'
 
 import type { Record } from '@/api'
 import type { ColumnsType } from 'ant-design-vue/es/table'
+import { ui18n } from '@/lang'
 
 export const recordColumns: ColumnsType<Record> = [
   // {
@@ -12,12 +13,12 @@ export const recordColumns: ColumnsType<Record> = [
   //   customRender: ({ index }) => index + 1
   // },
   {
-    title: '项目名称',
+    title: ui18n.t('project.projectNameLabel'),
     dataIndex: 'projectName',
     key: 'projectName'
   },
   {
-    title: '备注',
+    title: ui18n.t('log.logMsgLabel'),
     dataIndex: 'logMsg',
     key: 'logMsg',
     customRender({ record }) {
@@ -25,12 +26,12 @@ export const recordColumns: ColumnsType<Record> = [
     }
   },
   {
-    title: '操作人',
+    title: ui18n.t('log.userNameLabel'),
     dataIndex: 'userName',
     key: 'userName'
   },
   {
-    title: '操作时间',
+    title: ui18n.t('log.createTimeLabel'),
     dataIndex: 'createTime',
     key: 'createTime',
     customRender({ record }) {
@@ -39,7 +40,7 @@ export const recordColumns: ColumnsType<Record> = [
   },
   {
     width: '60px',
-    title: '操作',
+    title: ui18n.t('sys.oper'),
     dataIndex: 'action',
     key: 'action'
   }

+ 1 - 1
src/views/record/list.vue

@@ -8,7 +8,7 @@
     <template #bodyCell="{ column, record }">
       <template v-if="column.key === 'action'">
         <div class="table-actions">
-          <a @click="gotoProject(record)">查看</a>
+          <a @click="gotoProject(record)">{{ $t('sys.query') }}</a>
         </div>
       </template>
     </template>

+ 5 - 4
src/views/role/columns.ts

@@ -1,4 +1,5 @@
 import type { Role } from '@/api'
+import { ui18n } from '@/lang'
 import type { ColumnsType } from 'ant-design-vue/es/table'
 
 export const roleColumns: ColumnsType<Role> = [
@@ -9,22 +10,22 @@ export const roleColumns: ColumnsType<Role> = [
   //   customRender: ({ index }) => index + 1
   // },
   {
-    title: '角色名称',
+    title: ui18n.t('role.roleNameLabel'),
     dataIndex: 'roleName',
     key: 'roleName'
   },
   {
-    title: '所属项目',
+    title: ui18n.t('role.sroleNameLabel'),
     dataIndex: 'roleName',
     key: 'roleName'
   },
   {
-    title: '备注',
+    title: ui18n.t('role.remarkLabel'),
     dataIndex: 'remark',
     key: 'remark'
   },
   {
-    title: '创建时间',
+    title: ui18n.t('role.createTimeLabel'),
     dataIndex: 'createTime',
     key: 'createTime'
   }

+ 12 - 10
src/views/role/edit.vue

@@ -1,14 +1,16 @@
 <template>
   <a-modal
     v-model:visible="visible"
-    :title="`${role?.roleId ? '修改' : '新增'}项目角色`"
+    :title="`${role?.roleId ? $t('sys.update') : $t('sys.add')}${$t(
+      'role.name'
+    )}`"
     width="480px"
     :after-close="onCancel"
     @ok="saveHandler"
   >
     <template #footer>
       <a-button class="action-bottom" size="middle" @click="visible = false">
-        取消
+        {{ $t('sys.cancel') }}
       </a-button>
       <a-button
         class="action-bottom"
@@ -16,7 +18,7 @@
         size="middle"
         @click="saveHandler"
       >
-        保存
+        {{ $t('sys.save') }}
       </a-button>
     </template>
 
@@ -29,27 +31,27 @@
     >
       <a-form-item
         name="roleName"
-        label="角色名称"
-        :rules="[{ required: true, message: '请输入角色名称' }]"
+        :label="$t('role.roleNameLabel')"
+        :rules="[{ required: true, message: $t('role.roleNameRule') }]"
       >
         <a-input
           v-model:value="editRole.roleName"
-          placeholder="请输入角色名称"
+          :placeholder="$t('role.roleNameRule')"
         />
       </a-form-item>
       <a-form-item
         name="remark"
-        label="备注"
-        :rules="[{ required: false, max: 50, message: '备注最多50字' }]"
+        :label="$t('role.remarkLabel')"
+        :rules="[{ required: false, max: 50, message: $t('role.remarkRule') }]"
       >
         <a-textarea
           v-model:value.trim="editRole.remark"
           :resize="false"
           style="height: 104px; resize: none"
-          placeholder="请输入备注,最多50字"
+          :placeholder="$t('role.remarkRule')"
         />
       </a-form-item>
-      <a-form-item name="roleMenus" label="菜单分配">
+      <a-form-item name="roleMenus" :label="$t('role.roleMenusLabel')">
         <div class="menu-layer ant-input">
           <a-tree
             v-if="menuTree.length"

+ 13 - 10
src/views/role/list.vue

@@ -1,11 +1,11 @@
 <template>
   <BodyPanlHeader>
-    <a-button type="primary" @click="setRole()">新增角色</a-button>
+    <a-button type="primary" @click="setRole()">{{ $t('role.add') }}</a-button>
     <div>
       <a-input-search
         v-model:value="params.roleName"
         style="width: 280px"
-        placeholder="请输入角色名称"
+        :placeholder="$t('role.roleNameRule')"
         allow-clear
         @search="updateList"
       />
@@ -22,8 +22,10 @@
       <template #bodyCell="{ column, record }">
         <template v-if="column.key === 'action'">
           <div v-if="!record.defaultRole" class="table-actions">
-            <a @click="setRole(record)">编辑</a>
-            <a class="warn" @click="delRoleHandler(record)">删除</a>
+            <a @click="setRole(record)">{{ $t('sys.edit') }}</a>
+            <a class="warn" @click="delRoleHandler(record)">{{
+              $t('sys.del')
+            }}</a>
           </div>
         </template>
       </template>
@@ -33,7 +35,7 @@
 
 <script lang="ts" setup>
 import EditRole from './edit.vue'
-import { computed, onDeactivated, reactive, watch } from "vue";
+import { computed, onDeactivated, reactive, watch } from 'vue'
 import { BodyPanlHeader, BodyPanlBody } from '@/layout/panl'
 import { usePaging } from '@/hook'
 import { router } from '@/router'
@@ -43,11 +45,12 @@ import { Modal } from 'ant-design-vue'
 import { fetchRoles, addRole, updateRole, deleteRole } from '@/api'
 
 import type { Role } from '@/api'
+import { ui18n } from '@/lang'
 
 const roleColumns = [
   ...baseColumns,
   {
-    title: '操作',
+    title: ui18n.t('sys.oper'),
     dataIndex: 'action',
     key: 'action'
   }
@@ -84,12 +87,12 @@ watch(
 )
 const delRoleHandler = (role: Role) => {
   Modal.confirm({
-    content: '确定要删除此角色',
-    title: '系统提示',
+    content: ui18n.t('role.delMsg'),
+    title: ui18n.t('sys.tipTitle'),
     width: '400px',
-    okText: '删除',
+    okText: ui18n.t('sys.del'),
     icon: null,
-    cancelText: '取消',
+    cancelText: ui18n.t('sys.cancel'),
     onOk: async () => {
       try {
         await deleteRole(role.roleId, params.projectId)

+ 19 - 10
src/views/scene/actions.vue

@@ -1,14 +1,18 @@
 <template>
   <div class="table-actions">
     <template v-if="bimDone">
-      <a @click="updateBimName">修改</a>
-      <a @click="syncScene"> {{ store.current!.panos ? '修改' : '' }}同步 </a>
+      <a @click="updateBimName">{{ $t('sys.update') }}</a>
+      <a @click="syncScene">
+        {{ store.current!.panos ? $t('sys.update') : '' }}{{ $t('sys.sync') }}
+      </a>
     </template>
     <template v-if="done">
-      <a @click="queryScene">查看</a>
+      <a @click="queryScene">{{ $t('sys.query') }}</a>
       <!-- <a>分享</a> -->
     </template>
-    <a v-if="lastStep" class="warn" @click="deleteScene"> 删除 </a>
+    <a v-if="lastStep" class="warn" @click="deleteScene">
+      {{ $t('sys.del') }}
+    </a>
   </div>
 </template>
 
@@ -20,6 +24,7 @@ import { renderModal } from '@/helper'
 import { projectManage } from '@/env'
 import UploadBim from './update.vue'
 import type { ProjectScene } from '@/store'
+import { lang, ui18n } from '@/lang'
 
 const props = defineProps<{ scene: ProjectScene }>()
 const store = useProject()
@@ -39,12 +44,12 @@ const lastStep = computed(
 
 const deleteScene = () => {
   Modal.confirm({
-    content: '确定要删除此场景?',
-    title: '系统提示',
+    content: ui18n.t('scene.delTip'),
+    title: ui18n.t('sys.tipTitle'),
     width: '400px',
-    okText: '删除',
+    okText: ui18n.t('sys.del'),
     icon: null,
-    cancelText: '取消',
+    cancelText: ui18n.t('sys.cancel'),
     onOk: async () => {
       if (props.scene.type === BinType) {
         await store.deleteBim()
@@ -56,7 +61,9 @@ const deleteScene = () => {
 }
 
 const queryScene = () => {
-  const base = `${projectManage}?projectId=${store.current!.projectId}`
+  const base = `${projectManage}?projectId=${
+    store.current!.projectId
+  }&lang=${lang}`
   if ('num' in props.scene) {
     window.open(`${base}&m=${props.scene.num}`)
   } else {
@@ -65,7 +72,9 @@ const queryScene = () => {
 }
 
 const syncScene = () => {
-  window.open(`${projectManage}?projectId=${store.current!.projectId}&adjust`)
+  window.open(
+    `${projectManage}?projectId=${store.current!.projectId}&adjust&lang=${lang}`
+  )
 }
 
 const updateBimName = () => {

+ 8 - 7
src/views/scene/columns.ts

@@ -12,6 +12,7 @@ import {
 
 import type { ProjectScene } from '@/store'
 import type { ColumnsType } from 'ant-design-vue/es/table'
+import { ui18n } from '@/lang'
 
 export const sceneColumns: ColumnsType<ProjectScene> = [
   // {
@@ -21,30 +22,30 @@ export const sceneColumns: ColumnsType<ProjectScene> = [
   //   customRender: ({ index }) => index + 1
   // },
   {
-    title: '名称',
+    title: ui18n.t('scene.nameLabel'),
     dataIndex: 'name',
     width: '400px',
     key: 'name'
   },
   {
-    title: '创建人',
+    title: ui18n.t('scene.phoneLabel'),
     dataIndex: 'phone',
     key: 'phone'
   },
   {
     key: 'num',
     dataIndex: 'num',
-    title: '场景码'
+    title: ui18n.t('scene.numLabel')
   },
   {
-    title: '类型',
+    title: ui18n.t('scene.typeLabel'),
     dataIndex: 'type',
     key: 'type',
     customRender: ({ record }) =>
       record.type === BinType ? binTypeDesc : SceneTypeDesc[record.type]
   },
   {
-    title: '状态',
+    title: ui18n.t('scene.statusLabel'),
     dataIndex: 'status',
     key: 'status',
     customRender: ({ record }) => {
@@ -67,12 +68,12 @@ export const sceneColumns: ColumnsType<ProjectScene> = [
     }
   },
   {
-    title: '拍摄时间',
+    title: ui18n.t('scene.createTimeLabel'),
     dataIndex: 'createTime',
     key: 'createTime'
   },
   {
-    title: '操作',
+    title: ui18n.t('sys.oper'),
     dataIndex: 'action',
     key: 'action',
     customRender: ({ record }) => h(SceneActions, { scene: record })

+ 15 - 10
src/views/scene/insert.vue

@@ -1,7 +1,9 @@
 <template>
   <a-modal
     v-model:visible="visible"
-    :title="`${currentType === Type.bim ? '创建' : '选择'}场景`"
+    :title="`${
+      currentType === Type.bim ? $t('sys.create') : $t('sys.select')
+    }${$t('scene.name')}`"
     :width="`${currentType === Type.bim ? 480 : 660}px`"
     :after-close="onCancel"
     @ok="saveHandler"
@@ -10,9 +12,11 @@
       <div class="footer">
         <p>
           <template v-if="currentType === Type.scene">
-            已选择
-            {{ Object.values(typeNums).reduce((t, c) => t + c.length, 0) }}
-            个场景
+            {{
+              $t('scene.selected', {
+                len: Object.values(typeNums).reduce((t, c) => t + c.length, 0)
+              })
+            }}
           </template>
         </p>
         <div>
@@ -21,7 +25,7 @@
             size="middle"
             @click="visible = false"
           >
-            取消
+            {{ $t('sys.cancel') }}
           </a-button>
           <a-button
             class="action-bottom"
@@ -29,14 +33,14 @@
             size="middle"
             @click="saveHandler"
           >
-            保存
+            {{ $t('sys.save') }}
           </a-button>
         </div>
       </div>
     </template>
 
     <a-form name="basic" label-align="left" autocomplete="off">
-      <a-form-item v-if="showBim" label="场景类型" required>
+      <a-form-item v-if="showBim" :label="$t('scene.typeLabel')" required>
         <a-radio-group v-model:value="currentType" name="radioGroup">
           <a-radio :value="Type.scene">{{ Type.scene }}</a-radio>
           <a-radio :value="Type.bim">{{ Type.bim }}</a-radio>
@@ -60,6 +64,7 @@ import UploadBim from './upload-bim.vue'
 
 import type { UploadData } from './upload-bim.vue'
 import type { Project, BimUploadData, SelectTypeScenes } from '@/store'
+import { ui18n } from '@/lang'
 
 export type SaveData =
   | {
@@ -81,7 +86,7 @@ const props = defineProps<{
 }>()
 
 enum Type {
-  scene = '场景',
+  scene = ui18n.t('scene.name') as any,
   bim = 'BIM'
 }
 const currentType = ref(Type.scene)
@@ -107,9 +112,9 @@ const saveHandler = async () => {
   let data: SaveData
   if (currentType.value === Type.bim) {
     if (!bim.value.name) {
-      return message.error('请输入场景名称')
+      return message.error(ui18n.t('scene.nameRule'))
     } else if (!bim.value.file) {
-      return message.error('请上传BIM文件')
+      return message.error(ui18n.t('scene.fileRule'))
     }
     data = { payload: toRaw(bim.value) as BimUploadData, type: 'bim' }
   } else {

+ 4 - 2
src/views/scene/list.vue

@@ -1,11 +1,13 @@
 <template>
   <BodyPanlHeader>
-    <a-button type="primary" @click="insertScene">创建场景</a-button>
+    <a-button type="primary" @click="insertScene">{{
+      $t('scene.create')
+    }}</a-button>
     <div>
       <a-input-search
         v-model:value="filterName"
         style="width: 280px"
-        placeholder="请输入场景名称"
+        :placeholder="$t('scene.nameRule')"
         allow-clear
       />
     </div>

+ 1 - 1
src/views/scene/select-scenes.vue

@@ -9,7 +9,7 @@
   <a-input-search
     v-model:value="sceneName"
     allow-clear
-    placeholder="请输入场景名称"
+    :placeholder="$t('scene.nameRule')"
     @search="params.sceneName = sceneName"
   />
   <a-table

+ 5 - 4
src/views/scene/update.vue

@@ -1,14 +1,14 @@
 <template>
   <a-modal
     v-model:visible="visible"
-    title="修改BIM"
+    :title="$t('scene.updateFile')"
     width="480px"
     :after-close="onCancel"
     @ok="saveHandler"
   >
     <template #footer>
       <a-button class="action-bottom" size="middle" @click="visible = false">
-        取消
+        {{ $t('sys.cancel') }}
       </a-button>
       <a-button
         class="action-bottom"
@@ -16,7 +16,7 @@
         size="middle"
         @click="saveHandler"
       >
-        保存
+        {{ $t('sys.save') }}
       </a-button>
     </template>
 
@@ -32,6 +32,7 @@ import { message } from 'ant-design-vue'
 import UploadBim from './upload-bim.vue'
 
 import type { Bim } from '@/store'
+import { ui18n } from '@/lang'
 
 export type SaveData = Pick<Bim, 'bimName'> & { bimPath: string }
 
@@ -48,7 +49,7 @@ const visible = ref(true)
 
 const saveHandler = async () => {
   if (!bim.value.name) {
-    return message.error('请输入场景名称')
+    return message.error(ui18n.t('scene.nameRule'))
   } else {
     await props.onSave({
       bimName: bim.value.name,

+ 3 - 3
src/views/scene/upload-bim.vue

@@ -1,14 +1,14 @@
 <template>
-  <a-form-item label="场景名称" required>
+  <a-form-item :label="$t('scene.nameLabel1')" required>
     <a-input
       :value="data.name"
       name="radioGroup"
-      placeholder="请输入名称最多40字"
+      :placeholder="$t('scene.nameLabel1Rule', { max: 40 })"
       :maxlength="40"
       @update:value="name => $emit('update:data', { ...data, name })"
     />
   </a-form-item>
-  <a-form-item label="BIM文件" required>
+  <a-form-item :label="$t('scene.fileLabel')" required>
     <Upload
       :disabled="update"
       :file="data.file"

+ 10 - 8
src/views/system/login.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="header">
-    <h2>欢迎登录</h2>
-    <p>账号登录</p>
+    <h2>{{ $t('sys.loginh1') }}</h2>
+    <p>{{ $t('sys.loginh2') }}</p>
   </div>
 
   <a-form :model="loginState" class="form" @finish="login">
@@ -9,19 +9,21 @@
       <a-input
         v-model:value="loginState.phone"
         style="height: 50px"
-        placeholder="请输入账号"
+        :placeholder="$t('sys.phoneRule')"
       />
     </a-form-item>
     <a-form-item name="password" :rules="[passwordRule]">
       <a-input-password
         v-model:value="loginState.password"
-        placeholder="请输入密码"
+        :placeholder="$t('sys.passwordRule')"
         style="height: 50px"
       />
     </a-form-item>
     <a-form-item>
       <a-form-item name="remember" no-style>
-        <a-checkbox v-model:checked="loginState.remember">记住密码</a-checkbox>
+        <a-checkbox v-model:checked="loginState.remember">{{
+          $t('sys.rememberLabel')
+        }}</a-checkbox>
       </a-form-item>
     </a-form-item>
     <a-form-item>
@@ -32,13 +34,13 @@
         class="login-form-button"
         style="height: 50px"
       >
-        登录
+        {{ $t('sys.login') }}
       </a-button>
     </a-form-item>
   </a-form>
   <div class="footer">
-    <a :href="forgetLink">忘记密码</a>
-    <a :href="registerLink">账号注册</a>
+    <a :href="forgetLink">{{ $t('sys.forget') }}</a>
+    <a :href="registerLink">{{ $t('sys.register') }}</a>
   </div>
 </template>
 

+ 3 - 2
src/views/system/shared/rules.ts

@@ -1,3 +1,4 @@
+import { ui18n } from '@/lang'
 import type { Rule } from 'ant-design-vue/es/form'
 
 export const phonePattern = /^1[3|4|5|7|8][0-9]\d{8}$/
@@ -7,13 +8,13 @@ export const namePattern = /^[\d|\w]{1,16}$/
 export const phoneRule: Rule = {
   required: true,
   pattern: namePattern,
-  message: '请输入正确账号'
+  message: ui18n.t('sys.phoneRul1')
 }
 
 export const passwordRule: Rule = {
   required: true,
   pattern: passwordPattern,
-  message: '手机号或密码有误'
+  message: ui18n.t('sys.passwordRule1')
 }
 
 export const rules = {

+ 11 - 7
src/views/taggings/columns.ts

@@ -2,6 +2,7 @@ import type { Tagging } from '@/api'
 import type { ColumnsType } from 'ant-design-vue/es/table'
 import { h } from 'vue'
 import { TaggingStatusDesc } from '@/api'
+import { ui18n } from '@/lang'
 
 export const taggingColumns: ColumnsType<Tagging> = [
   // {
@@ -11,17 +12,17 @@ export const taggingColumns: ColumnsType<Tagging> = [
   //   customRender: ({ index }) => index + 1
   // },
   {
-    title: '场景标注',
+    title: ui18n.t('tagging.markingTitleLabel'),
     key: 'markingTitle',
     dataIndex: 'markingTitle'
   },
   {
-    title: '创建人',
+    title: ui18n.t('tagging.createByLabel'),
     dataIndex: 'createBy',
     key: 'createBy'
   },
   {
-    title: '涉及的成员',
+    title: ui18n.t('tagging.usersLabel'),
     dataIndex: 'users',
     key: 'users',
     customRender(data) {
@@ -29,20 +30,23 @@ export const taggingColumns: ColumnsType<Tagging> = [
     }
   },
   {
-    title: '状态',
+    title: ui18n.t('tagging.statusLabel'),
     dataIndex: 'status',
     key: 'status',
     customRender(data) {
-      return h('span', TaggingStatusDesc[data.record.markingStatus] || '未知')
+      return h(
+        'span',
+        TaggingStatusDesc[data.record.markingStatus] || ui18n.t('sys.un')
+      )
     }
   },
   {
-    title: '最后修改人',
+    title: ui18n.t('tagging.lastUpdateByLabel'),
     dataIndex: 'lastUpdateBy',
     key: 'lastUpdateBy'
   },
   {
-    title: '最后修改时间',
+    title: ui18n.t('tagging.updateTimeLabel'),
     dataIndex: 'updateTime',
     key: 'updateTime'
   }

+ 7 - 6
src/views/taggings/list.vue

@@ -3,14 +3,14 @@
     <div class="project-detail-body">
       <a-radio-group v-model:value="type">
         <a-badge :count="Object.values(totals).reduce((t, c) => t + c, 0)">
-          <a-radio-button value="all">所有</a-radio-button>
+          <a-radio-button value="all">{{ $t('sys.all') }}</a-radio-button>
         </a-badge>
         <a-badge
           v-for="(option, key) in types"
           :key="key"
           :count="totals[option]"
         >
-          <a-radio-button :key="option" :value="option as any">
+          <a-radio-button :key="option" :value="option">
             {{ TaggingStatusDesc[option] }}
           </a-radio-button>
         </a-badge>
@@ -19,7 +19,7 @@
     <div>
       <a-input-search
         v-model:value="filterName"
-        placeholder="请输入标注关键字"
+        :placeholder="$t('tagging.filterNamePlace')"
         style="width: 280px"
         @search="updateMaterials"
       />
@@ -37,9 +37,9 @@
         <template v-if="column.key === 'action'">
           <div class="table-actions">
             <a
-              :href="`smart-viewer.html?projectId=${record.projectId}&m=${record.num}`"
+              :href="`smart-viewer.html?projectId=${record.projectId}&lang=${lang}&m=${record.num}`"
               target="_blank"
-              >查看</a
+              >{{ $t('sys.query') }}</a
             >
           </div>
         </template>
@@ -69,11 +69,12 @@ import {
   TaggingStatus,
   TaggingStatusDesc
 } from '@/api'
+import { lang, ui18n } from '@/lang'
 
 const materialColumns = [
   ...baseColumns,
   {
-    title: '操作',
+    title: ui18n.t('sys.oper'),
     dataIndex: 'action',
     key: 'action'
   }

+ 11 - 0
src/vite-env.d.ts

@@ -22,3 +22,14 @@ interface ImportMeta {
 
 type PartialPart<T extends {}, K extends keyof T> = Omit<T, K> &
   Partial<Pick<T, K>>
+
+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

+ 0 - 1
tsconfig.json

@@ -4,7 +4,6 @@
     "useDefineForClassFields": true,
     "module": "ESNext",
     "moduleResolution": "Node",
-    "strict": true,
     "jsx": "preserve",
     "sourceMap": true,
     "resolveJsonModule": true,

+ 22 - 1
vite.config.ts

@@ -3,13 +3,20 @@ import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
 import vue from '@vitejs/plugin-vue'
 import ViteComponents from 'unplugin-vue-components/vite'
 import DefineOptions from 'unplugin-vue-define-options/vite'
+import { createServer as createLangServer } from './scripts/lang'
 
 import { resolve } from 'path'
 
 // https://vitejs.dev/config/
-export default ({ mode }) => {
+export default async ({ mode }) => {
   const env = loadEnv(mode, process.cwd())
+  const langProt = 9091
   const proxy = {
+    '/dev': {
+      target: `http://localhost:${langProt}`,
+      changeOrigin: true,
+      rewrite: path => path.replace(/^\/dev/, '/dev')
+    },
     '/api': {
       target: 'https://www.4dkankan.com',
       changeOrigin: true,
@@ -21,8 +28,22 @@ export default ({ mode }) => {
       rewrite: path => path.replace(/^\/local/, '')
     }
   }
+  const input: { [key in string]: string } = {}
+
+  if (process.argv.includes('lang')) {
+    await createLangServer(langProt)
+    input.lang = resolve(__dirname, 'lang.html')
+  } else {
+    input.main = resolve(__dirname, 'index.html')
+  }
 
+  console.log(input)
   return defineConfig({
+    build: {
+      rollupOptions: {
+        input
+      }
+    },
     base: './',
     plugins: [
       vue(),

+ 537 - 10
yarn.lock

@@ -114,6 +114,27 @@
   resolved "http://192.168.0.47:4873/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
   integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
 
+"@intlify/core-base@9.9.0":
+  version "9.9.0"
+  resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.9.0.tgz#edc55a5e3dbbf8dbbbf656529ed27832c4c4f522"
+  integrity sha512-C7UXPymDIOlMGSNjAhNLtKgzITc/8BjINK5gNKXg8GiWCTwL6n3MWr55czksxn8RM5wTMz0qcLOFT+adtaVQaA==
+  dependencies:
+    "@intlify/message-compiler" "9.9.0"
+    "@intlify/shared" "9.9.0"
+
+"@intlify/message-compiler@9.9.0":
+  version "9.9.0"
+  resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.9.0.tgz#7952759329e7af0388afbce7a984820bbeff82eb"
+  integrity sha512-yDU/jdUm9KuhEzYfS+wuyja209yXgdl1XFhMlKtXEgSFTxz4COZQCRXXbbH8JrAjMsaJ7bdoPSLsKlY6mXG2iA==
+  dependencies:
+    "@intlify/shared" "9.9.0"
+    source-map-js "^1.0.2"
+
+"@intlify/shared@9.9.0":
+  version "9.9.0"
+  resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.9.0.tgz#56907633c0f7b2d50f53269d31e88e7b24d39187"
+  integrity sha512-1ECUyAHRrzOJbOizyGufYP2yukqGrWXtkmTu4PcswVnWbkcjzk3YQGmJ0bLkM7JZ0ZYAaohLGdYvBYnTOGYJ9g==
+
 "@nodelib/fs.scandir@2.1.5":
   version "2.1.5"
   resolved "http://192.168.0.47:4873/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -346,6 +367,11 @@
   resolved "http://192.168.0.47:4873/@vue/devtools-api/-/devtools-api-6.2.1.tgz#6f2948ff002ec46df01420dfeff91de16c5b4092"
   integrity sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ==
 
+"@vue/devtools-api@^6.5.0":
+  version "6.5.1"
+  resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.5.1.tgz#7f71f31e40973eeee65b9a64382b13593fdbd697"
+  integrity sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==
+
 "@vue/reactivity-transform@3.2.39":
   version "3.2.39"
   resolved "http://192.168.0.47:4873/@vue/reactivity-transform/-/reactivity-transform-3.2.39.tgz#da6ae6c8fd77791b9ae21976720d116591e1c4aa"
@@ -406,6 +432,14 @@
   resolved "http://192.168.0.47:4873/@vue/shared/-/shared-3.2.39.tgz#302df167559a1a5156da162d8cc6760cef67f8e3"
   integrity sha512-D3dl2ZB9qE6mTuWPk9RlhDeP1dgNRUKC3NJxji74A4yL8M2MwlhLKUC/49WHjrNzSPug58fWx/yFbaTzGAQSBw==
 
+accepts@~1.3.8:
+  version "1.3.8"
+  resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
+  integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
+  dependencies:
+    mime-types "~2.1.34"
+    negotiator "0.6.3"
+
 acorn-jsx@^5.3.2:
   version "5.3.2"
   resolved "http://192.168.0.47:4873/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@@ -416,6 +450,11 @@ acorn@^8.8.0:
   resolved "http://192.168.0.47:4873/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
   integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
 
+adler-32@~1.3.0:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.3.1.tgz#1dbf0b36dda0012189a32b3679061932df1821e2"
+  integrity sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==
+
 aggregate-error@^3.0.0:
   version "3.1.0"
   resolved "http://192.168.0.47:4873/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
@@ -499,6 +538,11 @@ argparse@^2.0.1:
   resolved "http://192.168.0.47:4873/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
   integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
 
+array-flatten@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
+  integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
+
 array-tree-filter@^2.1.0:
   version "2.1.0"
   resolved "http://192.168.0.47:4873/array-tree-filter/-/array-tree-filter-2.1.0.tgz#873ac00fec83749f255ac8dd083814b4f6329190"
@@ -550,6 +594,42 @@ binary-extensions@^2.0.0:
   resolved "http://192.168.0.47:4873/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
   integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
 
+body-parser@1.20.1:
+  version "1.20.1"
+  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
+  integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==
+  dependencies:
+    bytes "3.1.2"
+    content-type "~1.0.4"
+    debug "2.6.9"
+    depd "2.0.0"
+    destroy "1.2.0"
+    http-errors "2.0.0"
+    iconv-lite "0.4.24"
+    on-finished "2.4.1"
+    qs "6.11.0"
+    raw-body "2.5.1"
+    type-is "~1.6.18"
+    unpipe "1.0.0"
+
+body-parser@^1.20.2:
+  version "1.20.2"
+  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
+  integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
+  dependencies:
+    bytes "3.1.2"
+    content-type "~1.0.5"
+    debug "2.6.9"
+    depd "2.0.0"
+    destroy "1.2.0"
+    http-errors "2.0.0"
+    iconv-lite "0.4.24"
+    on-finished "2.4.1"
+    qs "6.11.0"
+    raw-body "2.5.2"
+    type-is "~1.6.18"
+    unpipe "1.0.0"
+
 boolbase@^1.0.0:
   version "1.0.0"
   resolved "http://192.168.0.47:4873/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
@@ -577,11 +657,33 @@ braces@^3.0.2, braces@~3.0.2:
   dependencies:
     fill-range "^7.0.1"
 
+bytes@3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
+  integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
+
+call-bind@^1.0.0:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513"
+  integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==
+  dependencies:
+    function-bind "^1.1.2"
+    get-intrinsic "^1.2.1"
+    set-function-length "^1.1.1"
+
 callsites@^3.0.0:
   version "3.1.0"
   resolved "http://192.168.0.47:4873/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
   integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
 
+cfb@~1.2.1:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.2.2.tgz#94e687628c700e5155436dac05f74e08df23bc44"
+  integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==
+  dependencies:
+    adler-32 "~1.3.0"
+    crc-32 "~1.2.0"
+
 chalk@^4.0.0:
   version "4.1.2"
   resolved "http://192.168.0.47:4873/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
@@ -638,6 +740,11 @@ cli-truncate@^3.1.0:
     slice-ansi "^5.0.0"
     string-width "^5.0.0"
 
+codepage@~1.15.0:
+  version "1.15.0"
+  resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.15.0.tgz#2e00519024b39424ec66eeb3ec07227e692618ab"
+  integrity sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==
+
 color-convert@^2.0.1:
   version "2.0.1"
   resolved "http://192.168.0.47:4873/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
@@ -677,6 +784,28 @@ concat-map@0.0.1:
   resolved "http://192.168.0.47:4873/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
   integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
 
+content-disposition@0.5.4:
+  version "0.5.4"
+  resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
+  integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
+  dependencies:
+    safe-buffer "5.2.1"
+
+content-type@~1.0.4, content-type@~1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
+  integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
+
+cookie-signature@1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
+  integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
+
+cookie@0.5.0:
+  version "0.5.0"
+  resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
+  integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
+
 copy-anything@^2.0.1:
   version "2.0.6"
   resolved "http://192.168.0.47:4873/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480"
@@ -689,6 +818,11 @@ core-js@^3.15.1:
   resolved "http://192.168.0.47:4873/core-js/-/core-js-3.25.2.tgz#2d3670c1455432b53fa780300a6fc1bd8304932c"
   integrity sha512-YB4IAT1bjEfxTJ1XYy11hJAKskO+qmhuDBM8/guIfMz4JvdsAQAqvyb97zXX7JgSrfPLG5mRGFWJwJD39ruq2A==
 
+crc-32@~1.2.0, crc-32@~1.2.1:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff"
+  integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==
+
 cross-spawn@^7.0.2, cross-spawn@^7.0.3:
   version "7.0.3"
   resolved "http://192.168.0.47:4873/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -708,15 +842,17 @@ csstype@^2.6.8:
   resolved "http://192.168.0.47:4873/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e"
   integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==
 
-dayjs@^1.10.5:
+dayjs@^1.10.5, dayjs@^1.11.5:
   version "1.11.5"
   resolved "http://192.168.0.47:4873/dayjs/-/dayjs-1.11.5.tgz#00e8cc627f231f9499c19b38af49f56dc0ac5e93"
   integrity sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==
 
-dayjs@^1.11.5:
-  version "1.11.5"
-  resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.5.tgz#00e8cc627f231f9499c19b38af49f56dc0ac5e93"
-  integrity sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==
+debug@2.6.9:
+  version "2.6.9"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+  integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+  dependencies:
+    ms "2.0.0"
 
 debug@^3.2.6:
   version "3.2.7"
@@ -737,11 +873,30 @@ deep-is@^0.1.3:
   resolved "http://192.168.0.47:4873/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
   integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
 
+define-data-property@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3"
+  integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==
+  dependencies:
+    get-intrinsic "^1.2.1"
+    gopd "^1.0.1"
+    has-property-descriptors "^1.0.0"
+
 delayed-stream@~1.0.0:
   version "1.0.0"
   resolved "http://192.168.0.47:4873/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
   integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
 
+depd@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
+  integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+
+destroy@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
+  integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
+
 dir-glob@^3.0.1:
   version "3.0.1"
   resolved "http://192.168.0.47:4873/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@@ -771,6 +926,11 @@ eastasianwidth@^0.2.0:
   resolved "http://192.168.0.47:4873/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
   integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
 
+ee-first@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+  integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
+
 emoji-regex@^8.0.0:
   version "8.0.0"
   resolved "http://192.168.0.47:4873/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
@@ -781,6 +941,11 @@ emoji-regex@^9.2.2:
   resolved "http://192.168.0.47:4873/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
   integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
 
+encodeurl@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+  integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
+
 errno@^0.1.1:
   version "0.1.8"
   resolved "http://192.168.0.47:4873/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
@@ -923,6 +1088,11 @@ esbuild@^0.15.6:
     esbuild-windows-64 "0.15.8"
     esbuild-windows-arm64 "0.15.8"
 
+escape-html@~1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+  integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
+
 escape-string-regexp@^4.0.0:
   version "4.0.0"
   resolved "http://192.168.0.47:4873/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
@@ -1079,6 +1249,11 @@ esutils@^2.0.2:
   resolved "http://192.168.0.47:4873/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
   integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
 
+etag@~1.8.1:
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
+  integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
+
 execa@^6.1.0:
   version "6.1.0"
   resolved "http://192.168.0.47:4873/execa/-/execa-6.1.0.tgz#cea16dee211ff011246556388effa0818394fb20"
@@ -1094,6 +1269,43 @@ execa@^6.1.0:
     signal-exit "^3.0.7"
     strip-final-newline "^3.0.0"
 
+express@^4.18.2:
+  version "4.18.2"
+  resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59"
+  integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==
+  dependencies:
+    accepts "~1.3.8"
+    array-flatten "1.1.1"
+    body-parser "1.20.1"
+    content-disposition "0.5.4"
+    content-type "~1.0.4"
+    cookie "0.5.0"
+    cookie-signature "1.0.6"
+    debug "2.6.9"
+    depd "2.0.0"
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    etag "~1.8.1"
+    finalhandler "1.2.0"
+    fresh "0.5.2"
+    http-errors "2.0.0"
+    merge-descriptors "1.0.1"
+    methods "~1.1.2"
+    on-finished "2.4.1"
+    parseurl "~1.3.3"
+    path-to-regexp "0.1.7"
+    proxy-addr "~2.0.7"
+    qs "6.11.0"
+    range-parser "~1.2.1"
+    safe-buffer "5.2.1"
+    send "0.18.0"
+    serve-static "1.15.0"
+    setprototypeof "1.2.0"
+    statuses "2.0.1"
+    type-is "~1.6.18"
+    utils-merge "1.0.1"
+    vary "~1.1.2"
+
 fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
   version "3.1.3"
   resolved "http://192.168.0.47:4873/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@@ -1146,6 +1358,19 @@ fill-range@^7.0.1:
   dependencies:
     to-regex-range "^5.0.1"
 
+finalhandler@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32"
+  integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==
+  dependencies:
+    debug "2.6.9"
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    on-finished "2.4.1"
+    parseurl "~1.3.3"
+    statuses "2.0.1"
+    unpipe "~1.0.0"
+
 find-up@^5.0.0:
   version "5.0.0"
   resolved "http://192.168.0.47:4873/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
@@ -1181,6 +1406,21 @@ form-data@^4.0.0:
     combined-stream "^1.0.8"
     mime-types "^2.1.12"
 
+forwarded@0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
+  integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
+
+frac@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b"
+  integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==
+
+fresh@0.5.2:
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
+  integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
+
 fs.realpath@^1.0.0:
   version "1.0.0"
   resolved "http://192.168.0.47:4873/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -1196,11 +1436,26 @@ function-bind@^1.1.1:
   resolved "http://192.168.0.47:4873/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
   integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
 
+function-bind@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+  integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
+
 functional-red-black-tree@^1.0.1:
   version "1.0.1"
   resolved "http://192.168.0.47:4873/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
   integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==
 
+get-intrinsic@^1.0.2, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b"
+  integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==
+  dependencies:
+    function-bind "^1.1.2"
+    has-proto "^1.0.1"
+    has-symbols "^1.0.3"
+    hasown "^2.0.0"
+
 get-stream@^6.0.1:
   version "6.0.1"
   resolved "http://192.168.0.47:4873/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
@@ -1251,6 +1506,13 @@ globby@^11.1.0:
     merge2 "^1.4.1"
     slash "^3.0.0"
 
+gopd@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
+  integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
+  dependencies:
+    get-intrinsic "^1.1.3"
+
 graceful-fs@^4.1.2:
   version "4.2.10"
   resolved "http://192.168.0.47:4873/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
@@ -1266,6 +1528,23 @@ has-flag@^4.0.0:
   resolved "http://192.168.0.47:4873/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
   integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
 
+has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340"
+  integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==
+  dependencies:
+    get-intrinsic "^1.2.2"
+
+has-proto@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0"
+  integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==
+
+has-symbols@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
+  integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
+
 has@^1.0.3:
   version "1.0.3"
   resolved "http://192.168.0.47:4873/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
@@ -1273,6 +1552,24 @@ has@^1.0.3:
   dependencies:
     function-bind "^1.1.1"
 
+hasown@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c"
+  integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==
+  dependencies:
+    function-bind "^1.1.2"
+
+http-errors@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
+  integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
+  dependencies:
+    depd "2.0.0"
+    inherits "2.0.4"
+    setprototypeof "1.2.0"
+    statuses "2.0.1"
+    toidentifier "1.0.1"
+
 human-signals@^3.0.1:
   version "3.0.1"
   resolved "http://192.168.0.47:4873/human-signals/-/human-signals-3.0.1.tgz#c740920859dafa50e5a3222da9d3bf4bb0e5eef5"
@@ -1283,6 +1580,13 @@ husky@^8.0.1:
   resolved "http://192.168.0.47:4873/husky/-/husky-8.0.1.tgz#511cb3e57de3e3190514ae49ed50f6bc3f50b3e9"
   integrity sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==
 
+iconv-lite@0.4.24:
+  version "0.4.24"
+  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+  integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+  dependencies:
+    safer-buffer ">= 2.1.2 < 3"
+
 iconv-lite@^0.6.3:
   version "0.6.3"
   resolved "http://192.168.0.47:4873/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
@@ -1331,11 +1635,16 @@ inflight@^1.0.4:
     once "^1.3.0"
     wrappy "1"
 
-inherits@2:
+inherits@2, inherits@2.0.4:
   version "2.0.4"
   resolved "http://192.168.0.47:4873/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
 
+ipaddr.js@1.9.1:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
+  integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
+
 is-binary-path@~2.1.0:
   version "2.1.0"
   resolved "http://192.168.0.47:4873/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
@@ -1570,6 +1879,16 @@ make-dir@^2.1.0:
     pify "^4.0.1"
     semver "^5.6.0"
 
+media-typer@0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+  integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
+
+merge-descriptors@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+  integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
+
 merge-stream@^2.0.0:
   version "2.0.0"
   resolved "http://192.168.0.47:4873/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@@ -1580,6 +1899,11 @@ merge2@^1.3.0, merge2@^1.4.1:
   resolved "http://192.168.0.47:4873/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
   integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
 
+methods@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+  integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
+
 micromatch@^4.0.4, micromatch@^4.0.5:
   version "4.0.5"
   resolved "http://192.168.0.47:4873/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
@@ -1593,14 +1917,14 @@ mime-db@1.52.0:
   resolved "http://192.168.0.47:4873/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
   integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
 
-mime-types@^2.1.12:
+mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34:
   version "2.1.35"
   resolved "http://192.168.0.47:4873/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
   integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
   dependencies:
     mime-db "1.52.0"
 
-mime@^1.4.1:
+mime@1.6.0, mime@^1.4.1:
   version "1.6.0"
   resolved "http://192.168.0.47:4873/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
   integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
@@ -1644,12 +1968,17 @@ mlly@^0.5.14, mlly@^0.5.7:
     pkg-types "^0.3.4"
     ufo "^0.8.5"
 
+ms@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+  integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
+
 ms@2.1.2:
   version "2.1.2"
   resolved "http://192.168.0.47:4873/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
 
-ms@^2.1.1:
+ms@2.1.3, ms@^2.1.1:
   version "2.1.3"
   resolved "http://192.168.0.47:4873/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
   integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@@ -1678,6 +2007,11 @@ needle@^3.1.0:
     iconv-lite "^0.6.3"
     sax "^1.2.4"
 
+negotiator@0.6.3:
+  version "0.6.3"
+  resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
+  integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
+
 normalize-path@^3.0.0, normalize-path@~3.0.0:
   version "3.0.0"
   resolved "http://192.168.0.47:4873/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
@@ -1702,6 +2036,18 @@ object-inspect@^1.12.2:
   resolved "http://192.168.0.47:4873/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"
   integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==
 
+object-inspect@^1.9.0:
+  version "1.13.1"
+  resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2"
+  integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==
+
+on-finished@2.4.1:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
+  integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
+  dependencies:
+    ee-first "1.1.1"
+
 once@^1.3.0:
   version "1.4.0"
   resolved "http://192.168.0.47:4873/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -1768,6 +2114,11 @@ parse-node-version@^1.0.1:
   resolved "http://192.168.0.47:4873/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b"
   integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==
 
+parseurl@~1.3.3:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
+  integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+
 path-exists@^4.0.0:
   version "4.0.0"
   resolved "http://192.168.0.47:4873/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@@ -1793,6 +2144,11 @@ path-parse@^1.0.7:
   resolved "http://192.168.0.47:4873/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
   integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
 
+path-to-regexp@0.1.7:
+  version "0.1.7"
+  resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
+  integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
+
 path-type@^4.0.0:
   version "4.0.0"
   resolved "http://192.168.0.47:4873/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -1874,6 +2230,14 @@ prettier@^2.7.1:
   resolved "http://192.168.0.47:4873/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
   integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
 
+proxy-addr@~2.0.7:
+  version "2.0.7"
+  resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
+  integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
+  dependencies:
+    forwarded "0.2.0"
+    ipaddr.js "1.9.1"
+
 prr@~1.0.1:
   version "1.0.1"
   resolved "http://192.168.0.47:4873/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
@@ -1884,11 +2248,43 @@ punycode@^2.1.0:
   resolved "http://192.168.0.47:4873/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
 
+qs@6.11.0:
+  version "6.11.0"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
+  integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
+  dependencies:
+    side-channel "^1.0.4"
+
 queue-microtask@^1.2.2:
   version "1.2.3"
   resolved "http://192.168.0.47:4873/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
   integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
 
+range-parser@~1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
+  integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
+
+raw-body@2.5.1:
+  version "2.5.1"
+  resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
+  integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
+  dependencies:
+    bytes "3.1.2"
+    http-errors "2.0.0"
+    iconv-lite "0.4.24"
+    unpipe "1.0.0"
+
+raw-body@2.5.2:
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
+  integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
+  dependencies:
+    bytes "3.1.2"
+    http-errors "2.0.0"
+    iconv-lite "0.4.24"
+    unpipe "1.0.0"
+
 readdirp@~3.6.0:
   version "3.6.0"
   resolved "http://192.168.0.47:4873/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
@@ -1971,7 +2367,12 @@ rxjs@^7.5.5:
   dependencies:
     tslib "^2.1.0"
 
-"safer-buffer@>= 2.1.2 < 3.0.0":
+safe-buffer@5.2.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0":
   version "2.1.2"
   resolved "http://192.168.0.47:4873/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
@@ -2014,6 +2415,51 @@ semver@^7.3.5, semver@^7.3.6, semver@^7.3.7:
   dependencies:
     lru-cache "^6.0.0"
 
+send@0.18.0:
+  version "0.18.0"
+  resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
+  integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==
+  dependencies:
+    debug "2.6.9"
+    depd "2.0.0"
+    destroy "1.2.0"
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    etag "~1.8.1"
+    fresh "0.5.2"
+    http-errors "2.0.0"
+    mime "1.6.0"
+    ms "2.1.3"
+    on-finished "2.4.1"
+    range-parser "~1.2.1"
+    statuses "2.0.1"
+
+serve-static@1.15.0:
+  version "1.15.0"
+  resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540"
+  integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==
+  dependencies:
+    encodeurl "~1.0.2"
+    escape-html "~1.0.3"
+    parseurl "~1.3.3"
+    send "0.18.0"
+
+set-function-length@^1.1.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.0.tgz#2f81dc6c16c7059bda5ab7c82c11f03a515ed8e1"
+  integrity sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==
+  dependencies:
+    define-data-property "^1.1.1"
+    function-bind "^1.1.2"
+    get-intrinsic "^1.2.2"
+    gopd "^1.0.1"
+    has-property-descriptors "^1.0.1"
+
+setprototypeof@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
+  integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
+
 shallow-equal@^1.0.0:
   version "1.2.1"
   resolved "http://192.168.0.47:4873/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da"
@@ -2031,6 +2477,15 @@ shebang-regex@^3.0.0:
   resolved "http://192.168.0.47:4873/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
   integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
 
+side-channel@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
+  integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
+  dependencies:
+    call-bind "^1.0.0"
+    get-intrinsic "^1.0.2"
+    object-inspect "^1.9.0"
+
 signal-exit@^3.0.2, signal-exit@^3.0.7:
   version "3.0.7"
   resolved "http://192.168.0.47:4873/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
@@ -2082,6 +2537,18 @@ sourcemap-codec@^1.4.8:
   resolved "http://192.168.0.47:4873/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
   integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
 
+ssf@~0.11.2:
+  version "0.11.2"
+  resolved "https://registry.yarnpkg.com/ssf/-/ssf-0.11.2.tgz#0b99698b237548d088fc43cdf2b70c1a7512c06c"
+  integrity sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==
+  dependencies:
+    frac "~1.1.2"
+
+statuses@2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
+  integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
+
 string-argv@^0.3.1:
   version "0.3.1"
   resolved "http://192.168.0.47:4873/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
@@ -2170,6 +2637,11 @@ to-regex-range@^5.0.1:
   dependencies:
     is-number "^7.0.0"
 
+toidentifier@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
+  integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
+
 tslib@^1.8.1:
   version "1.14.1"
   resolved "http://192.168.0.47:4873/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
@@ -2204,6 +2676,14 @@ type-fest@^0.21.3:
   resolved "http://192.168.0.47:4873/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
   integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
 
+type-is@~1.6.18:
+  version "1.6.18"
+  resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
+  integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
+  dependencies:
+    media-typer "0.3.0"
+    mime-types "~2.1.24"
+
 typescript@^4.6.4:
   version "4.8.3"
   resolved "http://192.168.0.47:4873/typescript/-/typescript-4.8.3.tgz#d59344522c4bc464a65a730ac695007fdb66dd88"
@@ -2230,6 +2710,11 @@ unimport@^0.6.7:
     strip-literal "^0.4.0"
     unplugin "^0.9.0"
 
+unpipe@1.0.0, unpipe@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+  integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
+
 unplugin-auto-import@^0.11.2:
   version "0.11.2"
   resolved "http://192.168.0.47:4873/unplugin-auto-import/-/unplugin-auto-import-0.11.2.tgz#9b541f65e2800f298b13d73bca8e4eb333f94de7"
@@ -2290,6 +2775,16 @@ util-deprecate@^1.0.2:
   resolved "http://192.168.0.47:4873/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
   integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
 
+utils-merge@1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
+  integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
+
+vary@~1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+  integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
+
 vite@^3.1.0:
   version "3.1.2"
   resolved "http://192.168.0.47:4873/vite/-/vite-3.1.2.tgz#6b080f928490b1a46465ed3cbb4815f31f1a5376"
@@ -2320,6 +2815,15 @@ vue-eslint-parser@^9.0.1, vue-eslint-parser@^9.1.0:
     lodash "^4.17.21"
     semver "^7.3.6"
 
+vue-i18n@^9.9.0:
+  version "9.9.0"
+  resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.9.0.tgz#20d348fa7e37fc88e4c84f69781b2f1215c7769f"
+  integrity sha512-xQ5SxszUAqK5n84N+uUyHH/PiQl9xZ24FOxyAaNonmOQgXeN+rD9z/6DStOpOxNFQn4Cgcquot05gZc+CdOujA==
+  dependencies:
+    "@intlify/core-base" "9.9.0"
+    "@intlify/shared" "9.9.0"
+    "@vue/devtools-api" "^6.5.0"
+
 vue-router@4:
   version "4.1.5"
   resolved "http://192.168.0.47:4873/vue-router/-/vue-router-4.1.5.tgz#256f597e3f5a281a23352a6193aa6e342c8d9f9a"
@@ -2377,11 +2881,21 @@ which@^2.0.1:
   dependencies:
     isexe "^2.0.0"
 
+wmf@~1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wmf/-/wmf-1.0.2.tgz#7d19d621071a08c2bdc6b7e688a9c435298cc2da"
+  integrity sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==
+
 word-wrap@^1.2.3:
   version "1.2.3"
   resolved "http://192.168.0.47:4873/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
   integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
 
+word@~0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/word/-/word-0.3.0.tgz#8542157e4f8e849f4a363a288992d47612db9961"
+  integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==
+
 wrap-ansi@^6.2.0:
   version "6.2.0"
   resolved "http://192.168.0.47:4873/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
@@ -2405,6 +2919,19 @@ wrappy@1:
   resolved "http://192.168.0.47:4873/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
 
+xlsx@^0.18.5:
+  version "0.18.5"
+  resolved "https://registry.yarnpkg.com/xlsx/-/xlsx-0.18.5.tgz#16711b9113c848076b8a177022799ad356eba7d0"
+  integrity sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==
+  dependencies:
+    adler-32 "~1.3.0"
+    cfb "~1.2.1"
+    codepage "~1.15.0"
+    crc-32 "~1.2.1"
+    ssf "~0.11.2"
+    wmf "~1.0.1"
+    word "~0.3.0"
+
 xml-name-validator@^4.0.0:
   version "4.0.0"
   resolved "http://192.168.0.47:4873/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"