Browse Source

第一次提交

龙三郎 1 year ago
commit
02770760f5
69 changed files with 4883 additions and 0 deletions
  1. 24 0
      .gitignore
  2. 18 0
      README.md
  3. 37 0
      components.d.ts
  4. 14 0
      index.html
  5. 30 0
      package.json
  6. BIN
      public/static/images/banner.png
  7. BIN
      public/static/images/bg-delete.png
  8. BIN
      public/static/images/bg-device.png
  9. BIN
      public/static/images/bg-home.png
  10. BIN
      public/static/images/bg-login.png
  11. BIN
      public/static/images/bg-register.png
  12. BIN
      public/static/images/bg-role.png
  13. BIN
      public/static/images/bg-role2.png
  14. BIN
      public/static/images/icon-back.png
  15. BIN
      public/static/images/icon-change-password.png
  16. BIN
      public/static/images/icon-check.png
  17. BIN
      public/static/images/icon-delete.png
  18. BIN
      public/static/images/icon-device-active.png
  19. BIN
      public/static/images/icon-device.png
  20. BIN
      public/static/images/icon-device2.png
  21. BIN
      public/static/images/icon-edit.png
  22. BIN
      public/static/images/icon-exit.png
  23. BIN
      public/static/images/icon-home-active.png
  24. BIN
      public/static/images/icon-home.png
  25. BIN
      public/static/images/icon-location.png
  26. BIN
      public/static/images/icon-location2.png
  27. BIN
      public/static/images/icon-log.png
  28. BIN
      public/static/images/icon-mine-active.png
  29. BIN
      public/static/images/icon-mine.png
  30. BIN
      public/static/images/icon-next.png
  31. BIN
      public/static/images/icon-search-black.png
  32. BIN
      public/static/images/icon-search-white.png
  33. BIN
      public/static/images/icon-type.png
  34. 1 0
      public/vite.svg
  35. 11 0
      src/App.vue
  36. 223 0
      src/api/model/index.ts
  37. 53 0
      src/api/request.ts
  38. 1 0
      src/assets/vue.svg
  39. 38 0
      src/components/HelloWorld.vue
  40. 114 0
      src/components/QR/Index.vue
  41. 50 0
      src/components/QR/interface.ts
  42. 18 0
      src/main.ts
  43. 85 0
      src/network/axios/index.ts
  44. 120 0
      src/network/websocket/index.ts
  45. 68 0
      src/pages/Index.vue
  46. 104 0
      src/pages/demo/Index.vue
  47. 62 0
      src/pages/demo/Index1.vue
  48. 122 0
      src/pages/device/Index.vue
  49. 13 0
      src/pages/docs/About.vue
  50. 13 0
      src/pages/docs/Feedback.vue
  51. 13 0
      src/pages/docs/Instructions.vue
  52. 13 0
      src/pages/docs/Questions.vue
  53. 208 0
      src/pages/home/Index.vue
  54. 122 0
      src/pages/mine/Index.vue
  55. 454 0
      src/pages/recycle/add/Index.vue
  56. 505 0
      src/pages/recycle/list/Index.vue
  57. 441 0
      src/pages/sales/add/Index.vue
  58. 505 0
      src/pages/sales/list/Index.vue
  59. 117 0
      src/pages/user/Login.vue
  60. 78 0
      src/router/index.ts
  61. 17 0
      src/store/index.ts
  62. 21 0
      src/store/userInfo.ts
  63. 100 0
      src/style.css
  64. 86 0
      src/utils/util.ts
  65. 1 0
      src/vite-env.d.ts
  66. 28 0
      tsconfig.json
  67. 10 0
      tsconfig.node.json
  68. 36 0
      vite.config.ts
  69. 909 0
      yarn.lock

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 18 - 0
README.md

@@ -0,0 +1,18 @@
+# Vue 3 + TypeScript + Vite
+
+This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+
+## Recommended IDE Setup
+
+- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
+
+## Type Support For `.vue` Imports in TS
+
+TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
+
+If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
+
+1. Disable the built-in TypeScript Extension
+   1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
+   2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
+2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.

+ 37 - 0
components.d.ts

@@ -0,0 +1,37 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// Generated by unplugin-vue-components
+// Read more: https://github.com/vuejs/core/pull/3399
+export {}
+
+declare module 'vue' {
+  export interface GlobalComponents {
+    HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
+    Index: typeof import('./src/components/QR/Index.vue')['default']
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
+    VanBackTop: typeof import('vant/es')['BackTop']
+    VanButton: typeof import('vant/es')['Button']
+    VanCell: typeof import('vant/es')['Cell']
+    VanCellGroup: typeof import('vant/es')['CellGroup']
+    VanCheckbox: typeof import('vant/es')['Checkbox']
+    VanDatePicker: typeof import('vant/es')['DatePicker']
+    VanDropdownItem: typeof import('vant/es')['DropdownItem']
+    VanDropdownMenu: typeof import('vant/es')['DropdownMenu']
+    VanField: typeof import('vant/es')['Field']
+    VanForm: typeof import('vant/es')['Form']
+    VanGrid: typeof import('vant/es')['Grid']
+    VanGridItem: typeof import('vant/es')['GridItem']
+    VanIcon: typeof import('vant/es')['Icon']
+    VanImage: typeof import('vant/es')['Image']
+    VanLoading: typeof import('vant/es')['Loading']
+    VanNavBar: typeof import('vant/es')['NavBar']
+    VanOverlay: typeof import('vant/es')['Overlay']
+    VanPullRefresh: typeof import('vant/es')['PullRefresh']
+    VanStepper: typeof import('vant/es')['Stepper']
+    VanSwipeCell: typeof import('vant/es')['SwipeCell']
+    VanTabbar: typeof import('vant/es')['Tabbar']
+    VanTabbarItem: typeof import('vant/es')['TabbarItem']
+  }
+}

+ 14 - 0
index.html

@@ -0,0 +1,14 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <link type="text/css" rel="stylesheet" href="//at.alicdn.com/t/c/font_3378860_jxulrbfimdf.css" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
+    <title>驼人物联网平台</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 30 - 0
package.json

@@ -0,0 +1,30 @@
+{
+  "name": "tuoren-iot-platform-webapp",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vue-tsc && vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@vant/area-data": "^1.5.0",
+    "axios": "^1.5.0",
+    "html5-qrcode": "^2.3.8",
+    "jsqr": "^1.4.0",
+    "pinia": "^2.1.6",
+    "pinia-use-persist": "^0.0.21",
+    "vant": "^4.6.8",
+    "vue": "^3.3.4",
+    "vue-router": "4"
+  },
+  "devDependencies": {
+    "@types/node": "^20.6.3",
+    "@vitejs/plugin-vue": "^4.2.3",
+    "typescript": "^5.0.2",
+    "unplugin-vue-components": "^0.25.2",
+    "vite": "^4.4.5",
+    "vue-tsc": "^1.8.5"
+  }
+}

BIN
public/static/images/banner.png


BIN
public/static/images/bg-delete.png


BIN
public/static/images/bg-device.png


BIN
public/static/images/bg-home.png


BIN
public/static/images/bg-login.png


BIN
public/static/images/bg-register.png


BIN
public/static/images/bg-role.png


BIN
public/static/images/bg-role2.png


BIN
public/static/images/icon-back.png


BIN
public/static/images/icon-change-password.png


BIN
public/static/images/icon-check.png


BIN
public/static/images/icon-delete.png


BIN
public/static/images/icon-device-active.png


BIN
public/static/images/icon-device.png


BIN
public/static/images/icon-device2.png


BIN
public/static/images/icon-edit.png


BIN
public/static/images/icon-exit.png


BIN
public/static/images/icon-home-active.png


BIN
public/static/images/icon-home.png


BIN
public/static/images/icon-location.png


BIN
public/static/images/icon-location2.png


BIN
public/static/images/icon-log.png


BIN
public/static/images/icon-mine-active.png


BIN
public/static/images/icon-mine.png


BIN
public/static/images/icon-next.png


BIN
public/static/images/icon-search-black.png


BIN
public/static/images/icon-search-white.png


BIN
public/static/images/icon-type.png


+ 1 - 0
public/vite.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

+ 11 - 0
src/App.vue

@@ -0,0 +1,11 @@
+<template>
+  <RouterView></RouterView>
+</template>
+
+<script setup lang="ts">
+
+</script>
+
+<style scoped>
+
+</style>

+ 223 - 0
src/api/model/index.ts

@@ -0,0 +1,223 @@
+// 登录参数
+export interface LoginParams{
+  username: string;
+  password: string;
+  captchaCode: string;
+  captchaKey: string;
+  isRememberMe: boolean;
+}
+
+// 注册参数
+export interface RegisterParams{
+  mobile: string;
+  username: string;
+  password: string;
+  veryfyCode: string;
+  isAgree: boolean;
+}
+
+
+
+// 设备
+export interface DeviceEntity{
+  id?: string;
+  address?: string;
+  createtime?: string;
+  description?: string;
+  latitude?: number;
+  longitude?: number;
+  mac?: string;
+  modifytime?: string;
+  name?: string;
+  open?: number;
+  tenantId?: string;
+  type?: string;
+  data?: any;
+  [x: string]: any;
+}
+// 设备查询参数
+export interface SearchDeviceParams{
+  size: number; // 每页数
+  page: number; // 页码
+  sort: string; // 排序字段
+  order: string; // 排序方式
+  keyWord: string; // 关键字
+  mac: string;
+}
+// 添加设备参数
+export interface AddDeviceParams{
+  id?: string;
+  mac: string;
+  name: string;
+  productId: string;
+  description?: string;
+}
+
+// 删除参数
+export interface DeleteParams{
+  id: string
+}
+
+
+// 产品
+export interface ProductEntity{
+  id?: string;
+  secret?: string;
+  createtime?: string;
+  description?: string;
+  latitude?: number;
+  longitude?: number;
+  code?: string;
+  modifytime?: string;
+  name?: string;
+  open?: number;
+  tenantId?: string;
+  type?: string;
+  data?: string;
+  [x: string]: any;
+}
+// 产品查询参数
+export interface SearchProductParams{
+  size: number; // 每页数
+  page: number; // 页码
+  sort: string; // 排序字段
+  order: string; // 排序方式
+  keyWord: string; // 关键字
+}
+// 添加产品参数
+export interface AddProductParams{
+  id?: string;
+  name: string;
+  description?: string;
+}
+
+
+
+// 产品模型
+export interface ProductModelEntity{
+  id?: string; // id
+  code: number | undefined; // 编号
+  productId?: string; // 产品编号
+  name: string; // 字段名称
+  title: string; // 标识
+  type: 'int' | 'float' | 'double' | 'enum' | 'string'; // 数据类型
+  define: string | {
+    isArray: boolean; // 是否为数组
+    range?: {
+      min?: number; // type为int | float
+      max?: number; // type为int | float
+      len?: number; // type为string
+      [propName: number]: string; // type为enum
+    }
+  }; // 数据定义
+  remark?: string; // 备注
+  tenantId?: string; // 租户Id
+  [propName: string]: any
+}
+// 产品模型查询参数
+export interface SearchProductModelParams{
+  size: number; // 每页数
+  page: number; // 页码
+  sort: string; // 排序字段
+  order: string; // 排序方式
+  keyWord: string; // 关键字
+  productId: string; // 产品id
+}
+// 添加产品模型参数
+export interface AddProductModelParams extends ProductModelEntity{
+  
+}
+
+
+
+
+
+// 设备日志
+export interface DeviceLogEntity{
+  id: string;
+  address: string;
+  createtime: string;
+  description: string;
+  latitude: number;
+  longitude: number;
+  mac: string;
+  modifytime: string;
+  name: string;
+  open: number;
+  tenantId: string;
+  type: string;
+  data: string;
+}
+// 设备日志查询参数
+export interface SearchDeviceLogParams{
+  size: number; // 每页数
+  page: number; // 页码
+  sort: string; // 排序字段
+  order: string; // 排序方式
+  keyWord: string; // 关键字
+  mac: string;
+}
+
+
+// 用户模型
+export interface UserEntity{
+  id?: string;
+  username: string;
+  mobile?: string;
+  email?: string;
+  code?: string;
+  password?: string;
+  token?: string;
+  roles?: string[];
+  roleIds?: string[];
+  isLogin?: boolean | undefined;
+  isRememberMe?: boolean;
+  [x: string]: any;
+}
+
+// 用户模型查询参数
+export interface SearchUserParams{
+  size: number; // 每页数
+  page: number; // 页码
+  sort: string; // 排序字段
+  order: string; // 排序方式
+  keyWord: string; // 关键字
+  [propName: string]: any
+}
+// 添加用户模型参数
+export interface AddUserParams{
+  id?: string;
+  username: string;
+  mobile?: string;
+  email?: string;
+  code?: string;
+  password?: string;
+  roles?: string[];
+  roleIds?: string[];
+}
+
+
+
+
+// 角色模型
+export interface RoleEntity{
+  id?: string;
+  name: string;
+  description: string;
+  [x: string]: any;
+}
+
+// 用户模型查询参数
+export interface SearchRoleParams{
+  size: number; // 每页数
+  page: number; // 页码
+  sort: string; // 排序字段
+  order: string; // 排序方式
+  keyWord: string; // 关键字
+  [propName: string]: any
+}
+// 添加用户模型参数
+export interface AddRoleParams extends RoleEntity{
+  
+}
+

+ 53 - 0
src/api/request.ts

@@ -0,0 +1,53 @@
+import axios from "axios";
+import { useLocalStore } from '@/store';
+const baseURL = 'https://www.huifutiancai.com/huishou-service';
+// const baseURL = 'http://192.168.0.104:8383/';
+
+const localStore = useLocalStore()
+
+// 创建axios对象
+const instance = axios.create({
+  baseURL,
+  timeout: 5000
+})
+
+
+// 请求拦截
+instance.interceptors.request.use(
+  function(config){
+    // 可以在这里统一加东西
+    if(localStore.token){
+      config.headers.accessToken = localStore.token;
+    }
+    return config;
+  }, 
+  function (error) {
+    // 对请求错误做些什么
+    return Promise.reject(error);
+  }
+)
+
+// 响应拦截器
+instance.interceptors.response.use(function (response) {
+  // 2xx 范围内的状态码都会触发该函数。
+  // 对响应数据做点什么
+  return response;
+}, function (error) {
+  // 超出 2xx 范围的状态码都会触发该函数。
+  // 对响应错误做点什么
+  return Promise.reject(error);
+});
+
+
+// get请求
+export const get = function(url: any, params: any){
+  return instance.get(url, {params})
+}
+// post请求
+export const post = function(url: any, data: any){
+  return instance.post(url, data)
+}
+
+export default instance
+
+

+ 1 - 0
src/assets/vue.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

+ 38 - 0
src/components/HelloWorld.vue

@@ -0,0 +1,38 @@
+<script setup lang="ts">
+import { ref } from 'vue'
+
+defineProps<{ msg: string }>()
+
+const count = ref(0)
+</script>
+
+<template>
+  <h1>{{ msg }}</h1>
+
+  <div class="card">
+    <button type="button" @click="count++">count is {{ count }}</button>
+    <p>
+      Edit
+      <code>components/HelloWorld.vue</code> to test HMR
+    </p>
+  </div>
+
+  <p>
+    Check out
+    <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
+      >create-vue</a
+    >, the official Vue + Vite starter
+  </p>
+  <p>
+    Install
+    <a href="https://github.com/vuejs/language-tools" target="_blank">Volar</a>
+    in your IDE for a better DX
+  </p>
+  <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
+</template>
+
+<style scoped>
+.read-the-docs {
+  color: #888;
+}
+</style>

+ 114 - 0
src/components/QR/Index.vue

@@ -0,0 +1,114 @@
+<template>
+  <div id="reader"></div>
+</template>
+
+<script setup lang="ts">
+  import { onMounted, ref, onUnmounted } from 'vue'
+  import { Html5Qrcode } from 'html5-qrcode'
+  // import { useRouter } from 'vue-router'
+  import { Html5QrcodeResult, CameraDevice } from './interface'
+
+  // 事件
+  const emit = defineEmits(['success']);
+
+  const cameraId = ref('')
+  const devicesInfo = ref<any>('')
+  const html5QrCode = ref<any>(null)
+  // const router = useRouter()
+
+  onMounted(() => {
+    // open()
+  })
+
+  onUnmounted(() => {
+    console.log('qr-onUnmounted')
+    stop()
+  })
+
+  const open = () => {
+    Html5Qrcode.getCameras()
+      .then((devices: CameraDevice[]) => {
+        console.log('摄像头信息', devices)
+        if (devices && devices.length) {
+          // 如果有2个摄像头,1为前置的
+          if (devices.length > 1) {
+            cameraId.value = devices[1].id
+          } else {
+            cameraId.value = devices[0].id
+          }
+          devicesInfo.value = devices
+          // start开始扫描
+          start()
+        }
+      })
+      .catch((err) => {
+        alert(err);
+        // handle err
+        console.log('获取设备信息失败', err) // 获取设备信息失败
+      })
+  }
+  const start = () => {
+    html5QrCode.value = new Html5Qrcode('reader')
+    console.log('html5QrCode', html5QrCode)
+
+    html5QrCode.value.start(
+      cameraId.value, // retreived in the previous step.
+      {
+        fps: 10, // 设置每秒多少帧
+        qrbox: { width: 250, height: 250 } // 设置取景范围
+        // scannable, rest shaded.
+      },
+      (decodedText: string, decodedResult: Html5QrcodeResult) => {
+        // do something when code is read. For example:
+        // if (qrCodeMessage) {
+        //   getCode(qrCodeMessage);
+        //   stop();
+        // }
+        console.log('扫描的结果', decodedText, decodedResult)
+        emit('success', decodedResult);
+        stop();
+        // if (decodedText) {
+        //   router.push('order');
+        // }
+      },
+      (errorMessage: any) => {
+        // parse error, ideally ignore it. For example:
+        // console.log(`QR Code no longer in front of camera.`);
+        console.log('暂无额扫描结果', errorMessage)
+      }
+    )
+      .catch((err: any) => {
+        // Start failed, handle it. For example,
+        console.log(`Unable to start scanning, error: ${err}`)
+      })
+  }
+  const stop = () => {
+    html5QrCode.value
+      .stop()
+      .then((ignore: any) => {
+        // QR Code scanning is stopped.
+        console.log('QR Code scanning stopped.', ignore)
+      })
+      .catch((err: any) => {
+        // Stop failed, handle it.
+        console.log('Unable to stop scanning.', err)
+      })
+  }
+  // 暴露方法
+  defineExpose({
+    open,
+    stop
+  })
+</script>
+
+<style scoped>
+#reader {
+  position: absolute;
+  width: 100%;
+  top: 50%;
+  left: 0;
+  transform: translateY(-50%);
+  /* border: 2px solid red; */
+  /* box-sizing: border-box; */
+}
+</style>

+ 50 - 0
src/components/QR/interface.ts

@@ -0,0 +1,50 @@
+/* eslint-disable no-unused-vars */
+export interface CameraDevice {
+  id: string;
+  label: string;
+}
+
+/**
+ * Code formats supported by this library.
+ */
+enum Html5QrcodeSupportedFormats {
+  QR_CODE = 0,
+  AZTEC,
+  CODABAR,
+  CODE_39,
+  CODE_93,
+  CODE_128,
+  DATA_MATRIX,
+  MAXICODE,
+  ITF,
+  EAN_13,
+  EAN_8,
+  PDF_417,
+  RSS_14,
+  RSS_EXPANDED,
+  UPC_A,
+  UPC_E,
+  UPC_EAN_EXTENSION,
+}
+
+class QrcodeResultFormat {
+  // @ts-ignore
+  public readonly format: Html5QrcodeSupportedFormats
+  // @ts-ignore
+  public readonly formatName: string
+}
+
+/** Detailed scan result. */
+export interface QrcodeResult {
+  /** Decoded text. */
+  text: string;
+
+  /** Format that was successfully scanned. */
+  format?: QrcodeResultFormat;
+}
+
+/** QrCode result object. */
+export interface Html5QrcodeResult {
+  decodedText: string;
+  result: QrcodeResult;
+}

+ 18 - 0
src/main.ts

@@ -0,0 +1,18 @@
+import { createApp } from 'vue'
+import './style.css'
+import App from './App.vue'
+const app = createApp(App);
+
+// 引入路由
+import router from './router'
+app.use(router)
+
+// 引入pinia和本地存储
+import { createPinia } from 'pinia'
+import { usePersist } from 'pinia-use-persist'
+const pinia = createPinia();
+pinia.use(usePersist);
+app.use(pinia)
+
+
+app.mount('#app')

+ 85 - 0
src/network/axios/index.ts

@@ -0,0 +1,85 @@
+import axios from 'axios';
+import { useStoreOfUserInfo } from '@/store/userInfo'
+// import { refreshCurrentRoute } from '@/router'
+
+const userInfo = useStoreOfUserInfo();
+
+export const instance = axios.create({
+  baseURL: '/forward-service',
+  timeout: 10000,
+});
+
+type RequestType = {
+  url: string,
+  data?: any
+}
+export const post = function(request: RequestType, success?: Function, failed?: Function){
+  const {url, data} = request;
+  instance.post(url, data, {
+    headers: {
+      token: userInfo.token
+    }
+  }).then((response: any) => {
+    // console.log('success', response)
+    const result = response.data;
+    // console.log(result)
+    if(result.code == 0){
+      if(success) success(result);
+    }else if(result.code == 3){
+      userInfo.isLogin = false;
+      // 路由跳转
+      // message.error(result.msg, 3, ()=>{
+      //   console.log(userInfo);
+      //   refreshCurrentRoute();
+      // });
+    }else{
+      if(failed) {
+        failed(result.msg);
+      }
+      else {
+        // message.error(result.msg);
+      }
+    }
+  }, (response: any) => {
+    // console.log('failed', response)
+    if(failed) {
+      failed(response.message);
+    }
+    else{
+      // message.error(response.message);
+    }
+  }).catch((reason:any) =>{
+    console.log(reason)
+  })
+}
+
+
+
+export const post_promise = function(request: RequestType): Promise<any>{
+  const {url, data} = request;
+  return instance.post(url, data, {
+    headers: {
+      token: userInfo.token
+    }
+  }).then((response: any) => {
+    // console.log('success', response)
+    const result = response.data;
+    // console.log(result)
+    if(result.code == 0){
+      return Promise.resolve(result)
+    }else if(result.code == 3){
+      // 路由跳转
+      // message.error(result.msg, 3, ()=>{
+      //   console.log(userInfo);
+      //   refreshCurrentRoute();
+      // });
+    }else{
+      return Promise.reject(result.msg)
+    }
+  }, (response: any) => {
+    // console.log('failed', response)
+    return Promise.reject(response.message)
+  })
+}
+
+

+ 120 - 0
src/network/websocket/index.ts

@@ -0,0 +1,120 @@
+const developmentUrl = 'ws://192.168.103.36:8080/wss';
+const productionUrl = 'ws://' + window.location.host + '/wss';
+const wssUrl = import.meta.env.MODE == 'development'?developmentUrl:productionUrl;
+
+(function(){
+  console.log(wssUrl)
+})()
+
+
+export interface Pump{
+  [x: string]: any
+}
+
+export type Message = {
+  messageId?: string,
+  messageType: 'SUBSCRIBE' | 'PUBLISH' | 'UNSUBSCRIBE' | 'SYSTEM',
+  topics?: string[],
+  content?: any,
+  date?: string
+}
+
+
+export type WebSocketManager = {
+  host: string,
+  ws: WebSocket | undefined,
+  reconnection: Boolean,
+  reconnectiontime: number,
+  open: (params?: Params) => Promise<WebSocketManager>,
+  send: (message: Message) => void,
+  close: () => void,
+  onmessage: undefined | ((event: MessageEvent) => any)
+} & Params
+
+type Params = {
+  host?: string,
+  reconnection?: Boolean,
+  reconnectiontime?: number,
+  subscription?: string[],
+}
+
+export function useWebSocket(): WebSocketManager{
+
+  const instance: WebSocketManager = {
+    host: wssUrl,
+    ws: undefined,
+    reconnection: true,
+    reconnectiontime: 10,
+    subscription: [],
+    open,
+    send,
+    close,
+    onmessage: undefined,
+  }
+
+  function open(params?: Params){
+    // console.log(params)
+    params?.host && (instance.host = params.host);
+    params?.reconnection && (instance.reconnection = params.reconnection);
+    params?.reconnectiontime && (instance.reconnectiontime = params.reconnectiontime);
+    params?.subscription && (instance.subscription = params.subscription);
+    // console.log(instance)
+    return new Promise<WebSocketManager>((resolve, reject) => {
+      !instance.ws && (instance.ws = new WebSocket(instance.host));
+      instance.ws && (instance.ws.onopen = function() { 
+        // console.log('ws连接成功', event, this, instance.ws === this, this.readyState);
+        // 连接成功后添加数据监听
+        instance.ws?.readyState == WebSocket.OPEN && (instance.ws.onmessage = function(event: MessageEvent) {
+          // console.log('ws收到数据', event, this, instance.ws === this, this.readyState);
+          // 收到数据后传出
+          instance.onmessage && instance.onmessage(event);
+        });
+        // 连接成功后添加关闭监听
+        instance.ws?.readyState == WebSocket.OPEN && (instance.ws.onclose = function(event: CloseEvent) {
+          console.log('ws连接关闭', event);
+          instance.ws = undefined;
+          // 重连
+          instance.reconnection && (setTimeout(instance.open, instance.reconnectiontime*1000));
+        });
+        // 订阅
+        instance.subscription?.length && (instance.send({
+          messageType: 'SUBSCRIBE',
+          topics: instance.subscription,
+        }));
+        resolve(instance);
+      });
+      
+      instance.ws && (instance.ws.onerror = function() {
+        // console.log('ws连接错误', event, this, instance.ws === this, this.readyState);
+        instance.ws = undefined;
+        // 重连
+        instance.reconnection && (setTimeout(instance.open, instance.reconnectiontime*1000));
+        reject(instance);
+      });
+    });
+  }
+
+  function send(message: Message){
+    !message.messageId && (message.messageId = new Date().getTime() + '');
+    const mes = JSON.stringify(message);
+    // console.log(mes)
+    instance.ws?.send(mes);
+  }
+
+  function close(){
+    instance.ws && (function(){
+      instance.reconnection = false;
+      instance.ws.close();
+    }());
+  }
+
+
+  return instance;
+}
+
+
+
+
+
+
+

+ 68 - 0
src/pages/Index.vue

@@ -0,0 +1,68 @@
+<template>
+  <div>
+    <RouterView></RouterView>
+  </div>
+  <van-tabbar class="jj" v-show="showBtn" active-color="#0047FF" v-model="active">
+    <van-tabbar-item name="/home" to="/">
+      <span>工作台</span>
+      <template #icon="props">
+        <img :src="props.active? '/static/images/icon-home-active.png': '/static/images/icon-home.png'">
+      </template>
+    </van-tabbar-item>
+    <van-tabbar-item name="/device" to="/device">
+      <span>设备</span>
+      <template #icon="props">
+        <img :src="props.active? '/static/images/icon-device-active.png': '/static/images/icon-device.png'">
+      </template>
+    </van-tabbar-item>
+    <van-tabbar-item name="/mine" to="/mine">
+      <span>我的</span>
+      <template #icon="props">
+        <img :src="props.active? '/static/images/icon-mine-active.png': '/static/images/icon-mine.png'">
+      </template>
+    </van-tabbar-item>
+  </van-tabbar>
+</template>
+
+<script setup lang="ts">
+  import { ref, onMounted } from 'vue';
+  import { useRouter } from 'vue-router'
+  const active = ref('/home')
+  const router = useRouter();
+
+  console.log(router.currentRoute.value.path)
+
+  // 处理软键盘破坏布局
+  const showBtn = ref(true)
+  const clientHeight = ref(document.documentElement.clientHeight)
+  onMounted(() => {
+    window.onresize = () => {
+      if(clientHeight.value > document.documentElement.clientHeight){
+        showBtn.value = false
+      }else{
+        showBtn.value = true
+      }
+    }
+    // 标签栏active
+    active.value = router.currentRoute.value.path
+  })
+
+</script>
+
+<style scoped>
+  .van-tabbar--fixed{
+    left: auto;
+    max-width: 475px;
+    height: 60px !important;
+  }
+  :deep(.van-tabbar-item__icon){
+    margin-bottom: 5px;
+  };
+  .van-tabbar{
+    height: 55px !important;
+  }
+  
+  
+
+
+</style>

+ 104 - 0
src/pages/demo/Index.vue

@@ -0,0 +1,104 @@
+<template>
+  
+  <div style="display: flex;flex-direction: column; height: 100vh;">
+
+    <!-- <div>
+      demo
+    </div> -->
+
+    
+
+    <div style="flex-grow: 1; background-color: aqua;">
+      <canvas style="background-color: #CCC; width: auto; height: auto" ref="canvasObj"></canvas>
+    </div>
+    <div style="padding: 10px 16px;">
+      <div>
+        <span>识别内容:</span>
+        <span>{{ scanResult }}</span>
+      </div>
+    </div>
+
+    <div style="padding: 16px;">
+      <van-button :block="true" type="primary" @click="openCamera">input拍照</van-button>
+      <input style="display: none;" ref="fileInput" type="file" 
+        accept="image/*" @change="fileInputChange">
+    </div>
+
+  </div>
+
+
+</template>
+
+<script setup lang="ts">
+  import { ref } from "vue";
+  import jsQR from "jsqr";
+
+  const fileInput = ref<HTMLInputElement>();
+  const canvasObj = ref<HTMLCanvasElement>();
+  const scanResult = ref('123');
+  
+
+
+  const openCamera = () => {
+    fileInput.value?.click();
+    console.log('%O', fileInput.value);
+    console.log(typeof(fileInput.value));
+  }
+  const fileInputChange = (e: Event) => {
+    const eTarget = e.target as HTMLInputElement;
+    console.log('fileInputChange');
+    console.log('%O', e.target);
+    if(eTarget.files){
+      const file = eTarget.files[0];
+      const reader = new FileReader();
+      reader.onload = (ev: Event) => {
+        const evTarget = ev.target as FileReader;
+        console.log(evTarget);
+        const base64Result = evTarget.result as string;
+        base64ToQR(base64Result);
+      }
+      reader.readAsDataURL(file);
+    }
+  }
+
+  // base64转imageData
+  const base64ToQR = (data: string) => {
+    let ctx = canvasObj.value?.getContext('2d');
+
+    let image = new Image();
+    image.src = data;
+
+    console.log('image');
+    console.log('%O', image);
+
+    image.onload = () => {
+      ctx?.drawImage(image, 0, 0, image.width, image.height); // 绘图
+
+      let imageData = ctx?.getImageData(0, 0, image.width, image.height);
+      console.log(imageData);
+      // QR解码
+      let code = jsQR(
+        imageData?.data as Uint8ClampedArray, 
+        imageData?.width as number, 
+        imageData?.height as number
+      );
+
+      console.log(code);
+      if(code){
+        scanResult.value = code.data
+      }else{
+        scanResult.value = '扫码错误'
+      }
+
+    }
+
+  }
+
+  
+
+
+</script>
+
+<style scoped>
+
+</style>

+ 62 - 0
src/pages/demo/Index1.vue

@@ -0,0 +1,62 @@
+<template>
+  
+  <div style="position: flex; height: 100%; width: 100%; background-color: #aaa;">
+
+    <div style="display: flex; width: 100%; height: 100%; align-items: center; justify-content: center;">
+
+      <div style="height: 60%; width: 60%; display: flex;flex-direction: column;">
+        <div style="height: 200px; background-color: red;">jjj</div>
+        <div style="overflow: auto; 
+          background-color: #ccc; flex-grow: 1;">
+
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>123456</div>
+          <div>666666</div>
+
+        </div>
+        <div style="height: 200px; background-color: red;"></div>
+
+      </div>
+
+    </div>
+
+  </div>
+
+</template>
+
+<script setup lang="ts">
+
+</script>
+
+<style scoped>
+
+</style>

+ 122 - 0
src/pages/device/Index.vue

@@ -0,0 +1,122 @@
+<template>
+  <div class="container">
+    <div style="padding: 40px 16px 40px 16px;background-color: #31c0a8;">
+      <div v-if="localStore.username" style="display: flex;justify-content: space-between;color: #f0ffff;">
+        <div style="display: flex;align-items: center;">
+          <van-image
+            round
+            width="60px"
+            height="60px"
+            src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
+          />
+          <div style="margin-left: 10px;">
+            <div style="font-size: 16px;">
+              <div>{{localStore.username}}</div>
+            </div>
+            <div style="margin-top: 3px;">
+              <!-- <div>欢迎您,亲爱的用户~~~</div> -->
+              <div>{{ localStore.token }}</div>
+            </div>
+          </div>
+        </div>
+        <div>
+          <div>编辑资料</div>
+          <van-icon name="edit" size="16px"/>
+        </div>
+      </div>
+
+      <div v-else style="display: flex;justify-content: space-between;color: #f0ffff;">
+        <div style="display: flex;align-items: center;">
+          <van-image
+            round
+            width="60px"
+            height="60px"
+            src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
+          />
+          <div style="margin-left: 10px;">
+            <div style="font-size: 16px;">
+              <div @click="clickLogin">设备列表</div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      
+
+    </div>
+
+    <div style="margin-top: 6px;">
+    
+      <van-cell title="常用功能" icon="apps-o">
+        <template #right-icon>
+          <van-icon name="arrow" />
+        </template>
+      </van-cell>
+
+      <van-grid clickable :border="false" :column-num="4">
+        <van-grid-item to="/user/appoint-list">
+          <div style="width: 100%;text-align: center;">
+            <div>
+              <van-icon name="clock-o" size="30px" color="#31c0a8" />
+            </div>
+            <div style="text-align: center;margin-top: 5px;">
+              <div>我的预约</div>
+            </div>
+          </div>
+        </van-grid-item>
+        <!-- <van-grid-item to="/pages/U/order/index">
+          <div style="width: 100%;text-align: center;">
+            <div>
+              <van-icon name="todo-list-o" size="30px" color="#31c0a8" />
+            </div>
+            <div style="text-align: center;margin-top: 5px;">
+              <div>敬请期待</div>
+            </div>
+          </div>
+        </van-grid-item> -->
+
+      </van-grid>
+    </div>
+
+    <div style="height: 10px; background-color: #eeeeee;"></div>
+
+    <div class="cell-list">
+      <!-- <van-cell title="合作交流" is-link icon="comment-o" /> -->
+      <van-cell title="使用说明" is-link icon="description" to="/docs/instructions" />
+      <van-cell title="常见问题" is-link icon="close" to="/docs/questions" />
+      <van-cell title="意见反馈" is-link icon="notes-o" to="/docs/feedback" />
+      <van-cell title="登录" is-link icon="friends-o" to="/login" />
+    </div>
+    
+
+  </div>
+
+
+  
+</template>
+
+<script setup lang="ts">
+  import { useRouter } from 'vue-router';
+  import { useLocalStore } from '@/store';
+  const router = useRouter()
+  const localStore = useLocalStore();
+
+  
+
+  const clickLogin = () => {
+    router.push('/login')
+  }
+
+</script>
+
+<style scoped>
+  .van-tabbar--fixed{
+    left: auto;
+    max-width: 475px;
+  }
+  .cell-list .van-cell{
+    padding: 14px 16px;
+  }
+  
+
+</style>

+ 13 - 0
src/pages/docs/About.vue

@@ -0,0 +1,13 @@
+<template>
+  <div>
+    关于我们
+  </div>
+</template>
+
+<script setup lang="ts">
+
+</script>
+
+<style scoped>
+
+</style>

+ 13 - 0
src/pages/docs/Feedback.vue

@@ -0,0 +1,13 @@
+<template>
+  <div>
+    关于我们
+  </div>
+</template>
+
+<script setup lang="ts">
+
+</script>
+
+<style scoped>
+
+</style>

+ 13 - 0
src/pages/docs/Instructions.vue

@@ -0,0 +1,13 @@
+<template>
+  <div>
+    关于我们
+  </div>
+</template>
+
+<script setup lang="ts">
+
+</script>
+
+<style scoped>
+
+</style>

+ 13 - 0
src/pages/docs/Questions.vue

@@ -0,0 +1,13 @@
+<template>
+  <div>
+    关于我们
+  </div>
+</template>
+
+<script setup lang="ts">
+
+</script>
+
+<style scoped>
+
+</style>

+ 208 - 0
src/pages/home/Index.vue

@@ -0,0 +1,208 @@
+<template>
+  
+
+  <div style="height: 100vh; overflow: hidden; display: flex; flex-direction: column;">
+
+    <!-- 店铺名称 -->
+    <div style="padding: 10px 16px 0px 16px;
+      background-image: url('/static/images/bg-home.png');
+      background-repeat: no-repeat;
+      background-position-y: 0px;
+      background-size: 100% 120%;">
+      <div style="display: flex; 
+        justify-content:space-between; 
+        align-items:center;
+        color: #ffffff;">
+        <div style="font-size: 20px; font-weight: bold;">
+          <span>工作台</span>
+        </div>
+        <div style="line-height: 0px;">
+          <van-icon name="/static/images/icon-search-white.png" size="1.6rem" />
+        </div>
+      </div>
+      <div style="margin-top: 20px; display: flex; 
+        justify-content:space-between; 
+        align-items:center;
+        color: #ffffff;">
+        <div style="font-size: 16px;">
+          <span>Hi,</span>
+          <span>龙三郎</span>
+        </div>
+      </div>
+      <div style="margin-top: 10px; line-height: 0px;">
+        <van-image
+          width="100%"
+          fit="contain"
+          src="/static/images/banner.png"
+        />
+      </div>
+    </div>
+
+    <!-- 九宫格 -->
+    <div>
+      <van-grid :square="false" :clickable="false" :border="false" column-num="2" gutter="16">
+        <van-grid-item>
+          <div style="width: 100%; text-align: left;">
+            <div style="font-size: 1rem; color: #969799;">
+              <span>个人设备</span>
+            </div>
+            <div style="font-size: 1rem; display: flex;
+              justify-content: space-between;
+              align-items: baseline;">
+              <div style="display: flex; align-items: baseline;">
+                <div style="font-size: 2rem; font-weight: bold;">2640</div>
+                <div style="margin-left: 5px; font-size: 1rem; color: #969799;">台</div>
+              </div>
+              <div>
+                <van-icon name="/static/images/icon-device2.png" size="1.6rem" />
+              </div>
+            </div>
+          </div>
+        </van-grid-item>
+        <van-grid-item>
+          <div style="width: 100%; text-align: left;">
+            <div style="font-size: 1rem; color: #969799;">
+              <span>产品类型</span>
+            </div>
+            <div style="font-size: 1rem; display: flex;
+              justify-content: space-between;
+              align-items: baseline;">
+              <div style="display: flex; align-items: baseline;">
+                <div style="font-size: 2rem; font-weight: bold;">5</div>
+                <div style="margin-left: 5px; font-size: 1rem; color: #969799;">类</div>
+              </div>
+              <div>
+                <van-icon name="/static/images/icon-type.png" size="1.6rem" />
+              </div>
+            </div>
+          </div>
+        </van-grid-item>
+      </van-grid>
+    </div>
+
+
+    <!-- 新闻 -->
+    <div style="padding: 0px 20px;">
+      <div style="
+        display: flex; 
+        justify-content: space-between; 
+        border-radius: 2px;
+        align-items: center;
+      ">
+        <div>
+          <div style="font-weight: bold; display: flex; align-items: center;">
+            <van-icon name="label-o" size="1.1rem" />
+            <span style="margin-left: 4px;">新闻</span>
+          </div>
+        </div>
+        <div style="color: #0047FF; font-size: 0.9rem; display: flex; align-items: baseline;">
+          <span style="margin-right: 3px;">换一批</span>
+          <van-icon name="replay" size="1rem" />
+        </div>
+      </div>
+    </div>
+
+
+    <!-- 文章列表 -->
+    <div style="padding: 0px 0px 0px 0px; height: 0px; flex-grow: 1; overflow-y: auto;">
+
+      <van-cell v-for="item in 10" clickable>
+        <div style="height: 80px; display: flex;">
+          <div style="margin-right: 18px;">
+            <van-image
+              width="80"
+              height="80"
+              radius="6"
+              src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
+            />
+          </div>
+          <div style="width: 0px; flex-grow: 1; display: flex; flex-direction: column; justify-content: space-between;">
+            <div style="font-weight: bold; text-align: left;">
+              <span>{{ item }}、公务员考试笔试,政审都过了,单位叫我去上班我要去吗?</span>
+            </div>
+            <div style="display: flex; justify-content: space-between; color: #969799;">
+              <div>河南新闻</div>
+              <div>2024-06-11</div>
+            </div>
+          </div>
+        </div>
+      </van-cell>
+      
+    </div>
+
+
+
+      
+
+
+    <div style="height: 60px;"></div>
+
+  </div>
+
+  
+</template>
+
+<script setup lang="ts">
+  // import { onMounted, reactive } from 'vue';
+  // import { useRouter } from 'vue-router';
+  // import { showToast } from 'vant';
+  // import 'vant/es/toast/style';
+  // import { checkPhone } from '@/utils/util'
+
+  // const router = useRouter();
+  // const opt = reactive({
+  //   type: 1,
+  //   show: false,
+  //   name: '',
+  //   mobile: '',
+  //   isAnonymous: false,
+  // })
+
+  // 提交
+  // const confirm = () => {
+  //   console.log('confirm');
+  //   // 非匿名,需要验证姓名和手机号
+  //   if(!opt.isAnonymous){
+  //     if(!opt.name){
+  //       showToast('请输入姓名');
+  //       return;
+  //     }
+  //     if(!checkPhone(opt.mobile)){
+  //       showToast('请正确的手机号');
+  //       return;
+  //     }
+  //   }
+  //   let url = '';
+  //   const query = '?isAnonymous=' + opt.isAnonymous + '&name=' + opt.name + '&mobile=' + opt.mobile;
+  //   if(opt.type == 1){
+  //     url = '/sales/add';
+  //   }else if(opt.type == 2){
+  //     url = '/recycle/add';
+  //   }
+  //   url += query;
+    
+  //   // 路由跳转
+  //   router.push(url);
+
+  // }
+  
+  // 生命周期函数
+  // onMounted(() => {
+  //   console.log('onMounted')
+
+  //   // 阻止页面退出
+  //   history.pushState(null, 'null', document.URL);
+  //   window.addEventListener('popstate', function(){
+  //     history.pushState(null, 'null', document.URL);
+  //   },false);
+
+  // })
+  
+
+
+</script>
+
+<style scoped>
+
+
+</style>

+ 122 - 0
src/pages/mine/Index.vue

@@ -0,0 +1,122 @@
+<template>
+  <div class="container">
+    <div style="padding: 40px 16px 40px 16px;background-color: #31c0a8;">
+      <div v-if="localStore.username" style="display: flex;justify-content: space-between;color: #f0ffff;">
+        <div style="display: flex;align-items: center;">
+          <van-image
+            round
+            width="60px"
+            height="60px"
+            src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
+          />
+          <div style="margin-left: 10px;">
+            <div style="font-size: 16px;">
+              <div>{{localStore.username}}</div>
+            </div>
+            <div style="margin-top: 3px;">
+              <!-- <div>欢迎您,亲爱的用户~~~</div> -->
+              <div>{{ localStore.token }}</div>
+            </div>
+          </div>
+        </div>
+        <div>
+          <div>编辑资料</div>
+          <van-icon name="edit" size="16px"/>
+        </div>
+      </div>
+
+      <div v-else style="display: flex;justify-content: space-between;color: #f0ffff;">
+        <div style="display: flex;align-items: center;">
+          <van-image
+            round
+            width="60px"
+            height="60px"
+            src="https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg"
+          />
+          <div style="margin-left: 10px;">
+            <div style="font-size: 16px;">
+              <div @click="clickLogin">点击登录</div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      
+
+    </div>
+
+    <div style="margin-top: 6px;">
+    
+      <van-cell title="常用功能" icon="apps-o">
+        <template #right-icon>
+          <van-icon name="arrow" />
+        </template>
+      </van-cell>
+
+      <van-grid clickable :border="false" :column-num="4">
+        <van-grid-item to="/user/appoint-list">
+          <div style="width: 100%;text-align: center;">
+            <div>
+              <van-icon name="clock-o" size="30px" color="#31c0a8" />
+            </div>
+            <div style="text-align: center;margin-top: 5px;">
+              <div>我的预约</div>
+            </div>
+          </div>
+        </van-grid-item>
+        <!-- <van-grid-item to="/pages/U/order/index">
+          <div style="width: 100%;text-align: center;">
+            <div>
+              <van-icon name="todo-list-o" size="30px" color="#31c0a8" />
+            </div>
+            <div style="text-align: center;margin-top: 5px;">
+              <div>敬请期待</div>
+            </div>
+          </div>
+        </van-grid-item> -->
+
+      </van-grid>
+    </div>
+
+    <div style="height: 10px; background-color: #eeeeee;"></div>
+
+    <div class="cell-list">
+      <!-- <van-cell title="合作交流" is-link icon="comment-o" /> -->
+      <van-cell title="使用说明" is-link icon="description" to="/docs/instructions" />
+      <van-cell title="常见问题" is-link icon="close" to="/docs/questions" />
+      <van-cell title="意见反馈" is-link icon="notes-o" to="/docs/feedback" />
+      <van-cell title="登录" is-link icon="friends-o" to="/login" />
+    </div>
+    
+
+  </div>
+
+
+  
+</template>
+
+<script setup lang="ts">
+  import { useRouter } from 'vue-router';
+  import { useLocalStore } from '@/store';
+  const router = useRouter()
+  const localStore = useLocalStore();
+
+  
+
+  const clickLogin = () => {
+    router.push('/login')
+  }
+
+</script>
+
+<style scoped>
+  .van-tabbar--fixed{
+    left: auto;
+    max-width: 475px;
+  }
+  .cell-list .van-cell{
+    padding: 14px 16px;
+  }
+  
+
+</style>

+ 454 - 0
src/pages/recycle/add/Index.vue

@@ -0,0 +1,454 @@
+<template>
+  
+  <div style="display: flex; flex-direction: column;height: 100vh;">
+    <!-- 顾客信息 -->
+    <div style="border-bottom: 1px solid #eee; padding: 20px 16px 10px 16px;">
+      <div style="opacity: 0.5;">
+        <div>
+          <van-icon name="user-o" />
+          <span style="margin-left: 6px;">姓&emsp;名:&emsp;</span>
+          <span v-if="customer.isAnonymous" style="color: red;">匿名</span>
+          <span v-else>{{ customer.name }}</span>
+        </div>
+        <div>
+          <van-icon name="phone-o" />
+          <span style="margin-left: 6px;">手机号:&emsp;</span>
+          <span v-if="customer.isAnonymous" style="color: red;">匿名</span>
+          <span v-else>{{ customer.mobile }}</span>
+        </div>
+      </div>
+    </div>
+    <div style="height: 10px;"></div>
+
+    <!-- 扫描结果 -->
+    <!-- <div>
+      <div>{{ scan_content }}</div>
+    </div> -->
+
+
+    <!-- 扫码结果 -->
+    <div style="flex-grow: 1; overflow-y: auto;">
+      <div v-if="scan_list.length == 0">
+      <!-- <div wx:if="{{ 0 }}"> -->
+        <div style="color: #aaa; display: flex; align-items: center; 
+          justify-content: center; padding: 40px 0px 0px 0px;">
+          <van-icon name="warning-o" size="20" />
+          <span style="margin-left: 6px;">暂无扫码结果</span>
+        </div>
+      </div>
+      <div v-else class="scanlist">
+        <van-swipe-cell v-for="(item) in scan_list"
+          :name="item.id"
+          right-width="65">
+          <van-cell-group :border="false">
+            <van-cell @click="clickCell(item)">
+              <div style="text-align: left;">
+                <div style="display: flex; flex-direction: column; justify-content: space-between; flex-grow: 1; overflow-x: hidden;word-break:break-all;">
+                  <div style="width: 100%;">
+                    <div style="display: flex; justify-content: space-between;">
+                      <div style="font-size: 16px; color: #000; font-weight: bold;">{{ item.name }}</div>
+                      <div style="font-size: 16px;">{{ item.typeName }}</div>
+                    </div>
+                    <div>{{ item.register }}</div>
+                  </div>
+                  <div style="display: flex; justify-content: space-between;">
+                    <div>{{ item.registCode }}</div>
+                    <div style="color: blue;">
+                      <van-icon name="cross" size="10" />
+                      <span style="margin-left: 3px; font-weight: bold;">{{ item.amount }}</span>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </van-cell>
+          </van-cell-group>
+          <template #right>
+            <!-- <van-button square type="danger" text="删除" /> -->
+            <div style="background-color: red; height: 100%;
+              width: 65px; color: #ffffff; display: flex; align-items: center;
+              justify-content: center;" @click="deleteCell(item)">
+              <div>删除</div>
+            </div>
+          </template>
+          <!-- <div class="swipe-right" slot="right">
+            <span>删除</span>
+          </div> -->
+        </van-swipe-cell>
+      </div>
+    </div>
+
+    <div style="height: 0px;"></div>
+
+    <!-- 合计结果 -->
+    <div style="border-top: 1px solid #eee; padding: 10px 16px;" 
+      v-if="scan_list.length > 0">
+      <div style="padding: 0px 16px;display: flex;
+        justify-content:flex-end;align-items: center;">
+        <div>
+          <span style="margin-left: 3px;">总计:</span>
+        </div>
+        <span style="display: inline-block; 
+          text-align: right;margin-left: 6px;
+          color: red;font-size: 24px;
+          font-weight: bold;">{{ total }}</span>
+      </div>
+    </div>
+
+    <div style="height: 0px;"></div>
+
+    <div style="padding: 0px 16px;">
+      <van-button color="#4fc08d" type="primary" round  block 
+        size="large" icon="scan" @click="clickScan">扫描二维码</van-button>
+      <div style="height: 10px;"></div>
+      <van-button type="primary" round  block 
+        size="large" icon="guide-o" @click="submit">提交结果</van-button>
+      <div style="height: 10px;"></div>
+    </div>
+  </div>
+
+  <!-- 扫描二维码的界面-相机界面 -->
+  <div v-show="show" style="position: absolute; 
+    width: 100vw; height: 100vh; top: 0; left: 0; 
+    z-index: 99; background-color: #ccc;">
+    <QRScreen ref="scanQR" @success="scanSuccess"></QRScreen>
+  </div>
+
+  <!-- 扫描结果 -->
+  <van-overlay :lock-scroll="false" :show="scan_result_show">
+    <div style="height: 100%; width: 100%; display: flex; align-items: center; justify-content: center;">
+      <div style="height: 60%; width: 80%; background-color: #ffffff; display: flex; flex-direction: column;">
+        <div style="height: 16px;"></div>
+        <div style="flex-grow: 1; 
+          overflow-y: auto;
+          word-break:break-all;
+          padding: 10px 16px 16px 16px;">
+            
+            <div style="padding-right: 16px;">
+              <div>
+                <span style="font-weight: bold;">扫描结果: </span>
+                <span>{{ scan_content.scan_result }}</span>
+              </div>
+              <div v-if="isLoading" style="display: flex; align-items: center; justify-content: center; margin-top: 30px;">
+                <div style="margin-right: 6px;">
+                  <span style="">农药信息加载中</span>
+                </div>
+                <div>
+                  <van-loading type="spinner" color="#1989fa" />
+                </div>
+              </div>
+              <div v-else>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">单元识别码: </span>
+                  <span>{{ scan_content.id }}</span>
+                </div>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">登记证号: </span>
+                  <span>{{ scan_content.registCode }}</span>
+                </div>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">农药名称: </span>
+                  <span>{{ scan_content.name }}</span>
+                </div>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">农药类别: </span>
+                  <span>{{ scan_content.typeName }}</span>
+                </div>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">剂型: </span>
+                  <span>{{ scan_content.physicType }}</span>
+                </div>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">总含量: </span>
+                  <span>{{ scan_content.concent }}</span>
+                </div>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">有效期至: </span>
+                  <span>{{ scan_content.validDate }}</span>
+                </div>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">登记证持有人: </span>
+                  <span>{{ scan_content.register }}</span>
+                </div>
+              </div>
+              
+            </div>
+            
+        </div>
+        <div style="height: 10px;"></div>
+        <div style="border-top: 1px solid #eee; border-bottom: 1px solid #eee; padding: 8px 0px 6px 0px;">
+          <div style="display: flex; align-items: center;justify-content: flex-end;">
+            <div style="margin: 0px 10px 0px 16px;">
+              <span style="color: red;">*</span>
+              <span>&nbsp;&nbsp;数量:</span>
+            </div>
+            <div style="padding-right: 16px;">
+              <van-stepper integer v-model="scan_content.amount" 
+                input-width="60px" theme="round" 
+                :disabled="customer.isAnonymous"
+                :max="scan_content.maxAmount" />
+            </div>
+          </div>
+        </div>
+        <div style="height: 20px;"></div>
+        <div style="padding: 0px 16px;">
+          <van-button color="#e87f5e" type="danger" round  block size="normal" icon="close" @click="cancel">取消</van-button>
+          <div style="height: 10px;"></div>
+          <van-button color="#4fc08d" type="primary" round  block size="normal" icon="passed" @click="confirm">添加</van-button>
+          <div style="height: 10px;"></div>
+        </div>
+      </div>
+    </div>
+  </van-overlay>
+
+  
+  
+</template>
+
+<script setup lang="ts">
+  import { computed, onMounted, reactive, ref } from 'vue';
+  import QRScreen from '@/components/QR/Index.vue';
+  import { post } from '@/api/request';
+  // Dialog
+  import { showConfirmDialog } from 'vant';
+  import 'vant/es/dialog/style';
+  // Toast
+  import { showToast } from 'vant';
+  import 'vant/es/toast/style';
+  // 路由
+  import { useRouter } from 'vue-router'
+  const router = useRouter();
+
+  const show = ref(false);
+  const scanQR = ref();
+
+  const customer = reactive({
+    isAnonymous: true,
+    name: '',
+    mobile: '',
+  })
+  const scan_list = ref<any[]>([]);
+  const scan_result_show = ref(false);
+  const isLoading = ref(false);
+  const scan_content = reactive({
+    scan_result: '',
+    registCode: '',
+    register: '',
+    id: '',
+    physicType: '',
+    name: '',
+    typeName: '',
+    validDate: '',
+    concent: '',
+    amount: 1,
+    maxAmount: 1,
+  })
+
+  // 生命周期
+  onMounted(() => {
+    const query = router.currentRoute.value.query;
+    customer.isAnonymous = query.isAnonymous == 'true' ? true : false
+    customer.name = query.name as string
+    customer.mobile = query.mobile as string
+    console.log(router.currentRoute.value.query)
+  })
+
+  // 查看详细信息
+  const clickCell = (event: any) => {
+    console.log('clickCell', event.id);
+    let ind = -1;
+    for(let i = 0; i < scan_list.value.length; i++){
+      if(scan_list.value[i].id == event.id){
+        ind = i;
+        break;
+      }
+    }
+    console.log(ind)
+    if(ind >= 0){
+      Object.assign(scan_content, scan_list.value[ind]);
+      // 打开遮罩层
+      scan_result_show.value = true
+    }
+  }
+  // 删除
+  const deleteCell = (event: any) => {
+    console.log('deleteCell', event.id);
+    showConfirmDialog({
+      message: '确定删除吗?',
+    }).then(() => {
+      console.log(event.id)
+      let ind = -1;
+      for(let i = 0; i < scan_list.value.length; i++){
+        if(scan_list.value[i].id == event.id){
+          ind = i;
+          break;
+        }
+      }
+      console.log(ind)
+      if(ind >= 0){
+        scan_list.value.splice(ind, 1);
+      }
+    }, () => {
+      console.log('取消')
+    });
+  }
+  // 扫码二维码
+  const clickScan = () => {
+    console.log('clickScan');
+    show.value = true
+    scanQR.value.open();
+  }
+  // 提交结果
+  const submit = () => {
+    console.log('submit');
+    console.log('提交');
+    const formData:any[] = [];
+    for(let i = 0; i< scan_list.value.length; i++){
+      const item = scan_list.value[i]
+      const formItem:any = {}
+      formItem.info = item.scan_result
+      formItem.num = item.amount
+      formItem.pesticideName = item.name
+      formItem.pesticideRegistCode = item.registCode
+      formItem.clientName = customer.name;
+      formItem.clientPhone = customer.mobile;
+      formItem.realnameFlag = customer.isAnonymous?0:1;
+      formData[i] = formItem
+    }
+    
+    console.log(formData)
+    if(formData.length == 0){
+      showToast('列表不能为空')
+      return;
+    }
+    // 提交提示
+    showConfirmDialog({
+      message: '确定提交吗?',
+    }).then(() => {
+      post('/recycleRecord/batchAddRecord',formData)
+      .then(({data}) => {
+        console.log(data)
+        // 处理成功结果
+        if(data.code == 0){
+          showToast('提交成功');
+          // 跳转到首页
+          setTimeout(() => {
+            // 跳转到首页
+            router.push('/')
+          }, 500)
+        }else{
+          showToast(data.msg);
+        }
+      })
+    }, () => {
+      console.log('取消')
+    });
+  }
+
+  // 计算属性
+  const total = computed(() => {
+    let sum = 0;
+    for(let i = 0; i < scan_list.value.length; i++){
+      sum += scan_list.value[i].amount;
+    }
+    return sum;
+  })
+
+  // 扫码结果
+  const scanSuccess = (e: any) => {
+    show.value = false
+    console.log(e)
+    scan_content.scan_result = e.decodedText;
+    // 获取农药信息
+    getPesticideInfo(scan_content.scan_result);
+  }
+  // 根据扫码结果获取农药信息
+  const getPesticideInfo = (scan_result: string) => {
+    post('/recycleRecord/recycleAnalyse',{
+      realnameFlag: customer.isAnonymous?'0':'1',
+      clientPhone: customer.mobile,
+      info: scan_result,
+    })
+    .then(({data}) => {
+      console.log(data)
+      // 处理成功结果
+      if(data.code == 0){
+        const result = data.data;
+        Object.assign(scan_content, result)
+        let maxAmount = 1;
+        // 实名
+        if(!customer.isAnonymous){
+          maxAmount = data.data.saleAmount - data.data.recycleAmount;
+        }
+        // 打开遮罩层
+        scan_result_show.value = true
+        scan_content.amount = 1;
+        scan_content.maxAmount = maxAmount
+      }else{
+        showToast(data.msg);
+      }
+    })
+  }
+
+  // 取消
+  const cancel = () => {
+    console.log('取消')
+    // 关闭遮罩层
+    scan_result_show.value = false;
+  }
+  // 添加
+  const confirm = (event: any) => {
+    console.log('添加',  scan_content)
+    console.log(event)
+
+    let ind = -1;
+    for(let i = 0; i < scan_list.value.length; i++){
+      if(scan_list.value[i].id == scan_content.id){
+        ind = i;
+        break;
+      }
+    }
+
+    // 添加列表, 克隆对象
+    const item = Object.assign({}, scan_content);
+    if(ind >= 0){
+      scan_list.value[ind] = item
+    }else{
+      scan_list.value.push(item)
+    }
+    console.log(scan_list.value)
+    // 关闭遮罩层
+    scan_result_show.value = false;
+  }
+
+</script>
+
+<style scoped>
+
+  .my-scroll{
+    -webkit-overflow-scrolling: touch;
+  }
+  /*修改滚动条样式*/
+  .my-scroll::-webkit-scrollbar {
+    width: 15px;
+    height: 15px;
+  }
+  .my-scroll::-webkit-scrollbar-track {
+    background: #f1f1f1;
+  }
+  .my-scroll::-webkit-scrollbar-thumb {
+    background: #ddd;
+    border-radius: 7px;
+    height: 50px;
+    border: 4px solid #f1f1f1;
+  }
+  .my-scroll::-webkit-scrollbar-track-piece {
+    width: 10px;
+  }
+  .my-scroll::-webkit-scrollbar-thumb:hover {
+    background: #dddddd;
+  }
+  .my-scroll::-webkit-scrollbar-corner {
+    background: #dddddd;
+  }
+
+</style>

+ 505 - 0
src/pages/recycle/list/Index.vue

@@ -0,0 +1,505 @@
+<template>
+
+  <!-- 时间选择 -->
+  <div v-if="date_picker_show" style="position: fixed; bottom: 0px; left: 0px; right: 0px; z-index: 999;">
+    <van-date-picker type="date" v-model="currentDate" :min-date="minDate" :max-date="maxDate" 
+      @confirm="datetime_picker_confirm" 
+      @cancel="datetime_picker_cancel"/>
+  </div>
+
+  <div style="display: flex; flex-direction:column;height: 100vh;">
+    <van-nav-bar
+      title="回收列表"
+      left-text="返回"
+      left-arrow
+      @click-left="onClickLeft">
+      <template #left>
+        <van-icon name="arrow-left" size="20" color="#000000" />
+      </template>
+    </van-nav-bar>
+
+    <!-- 下拉菜单 -->
+    <div style="border-bottom: 1px solid #fefefe;" class="my-dropdown-menu">
+      <van-dropdown-menu :close-on-click-overlay="false" :close-on-click-outside="false">
+        <van-dropdown-item ref="dropdown_1" id="item111" title="筛选条件">
+          <van-cell-group>
+            <van-field size="large" v-model="search.name" :border="true" placeholder="请输入姓名" clearable>
+              <template #label>
+                <span style="color: #646566;">姓&ensp;&ensp;&emsp;名</span>
+              </template>
+            </van-field>
+            <van-field clearable size="large" type="number" v-model="search.mobile" :border="true" placeholder="请输入手机号">
+              <template #label>
+                <span style="color: #646566;">手&ensp;机&ensp;号</span>
+              </template>
+            </van-field>
+            <van-field right-icon="clear" clearable readonly label="时间区间" size="large" type="text" 
+              v-model="search.startDate" :border="true" placeholder="请输入开始时间" 
+              @click-input="datetime_picker_start" 
+              @click-right-icon="clear_datetime_picker_start"/>
+            <van-field right-icon="clear" clearable readonly label=" " size="large" type="text" 
+              v-model="search.endDate" :border="true" placeholder="请输入结束时间" 
+              @click-input="datetime_picker_end" 
+              @click-right-icon="clear_datetime_picker_end"/>
+          </van-cell-group>
+
+          <div style="height: 20px;"></div>
+          <div style="padding: 0px 16px; display: flex;">
+            <div style="flex-grow: 1;">
+              <van-button type="warning" icon="close" size="small" block round @click="onCancel">取消</van-button>
+            </div>
+            <div style="width: 20px;"></div>
+            <div style="flex-grow: 1;">
+              <van-button type="primary" icon="search" size="small" block round @click="onConfirm">搜索</van-button>
+            </div>
+          </div>
+          <div style="height: 15px;"></div>
+        </van-dropdown-item>
+        <van-dropdown-item title-class="my-title-class" v-model="search.sort" :options="option2" @change="sort_change" />
+      </van-dropdown-menu>
+    </div>
+    
+
+    <div class="container" style="overflow-y: auto;flex: 1;">
+      <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
+        <div>
+          <van-swipe-cell v-for="(item) in dataList" right-width="0">
+            <!-- <template #left>
+              <van-button square type="primary" text="选择" />
+            </template> -->
+            <van-cell :border="true"  @click="clickCell(item)">
+              <div style="text-align: left;">
+                <div style="display: flex; flex-direction: column; justify-content: space-between; flex-grow: 1; overflow-x: hidden;word-break:break-all;">
+                  <div style="width: 100%;">
+                    <div style="display: flex; justify-content: space-between;">
+                      <div style="font-size: 16px; color: #000; font-weight: bold; display: flex;">
+                        <div>
+                          <span>{{ item.pesticideName }}</span>
+                        </div>
+                        <div>
+                          <span>(</span>
+                          <van-icon name="cross" size="14" />
+                          <span>{{ item.num }}</span>
+                          <span>)</span>
+                        </div>
+                      </div>
+                      <div style="font-size: 16px;">
+                        <span v-if="item.realnameFlag == '1'">
+                          <!-- <span style="color: blue;">实名</span> -->
+                          <span style="color: blue;">{{ item.clientName }}</span>
+                        </span>
+                        <span v-else style="color: red;">匿名</span>
+                      </div>
+                    </div>
+                    <div>{{ item.register }}</div>
+                  </div>
+                  <div style="display: flex; justify-content: space-between;">
+                    <div>{{ item.pesticideRegistCode }}</div>
+                    <div>
+                      <span style="margin-left: 3px; font-weight: bold;">{{ item.createtime }}</span>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </van-cell>
+            <template #right>
+              <van-button square type="danger" text="删除" />
+            </template>
+          </van-swipe-cell>
+        </div>
+
+        <!-- 数据提示 -->
+        <div>
+          <div v-if="refreshing"></div>
+          <div v-else-if="dataList.length == 0" style="text-align: center; padding: 20px; color: #ccc;">
+            <span>暂无数据</span>
+          </div>
+          <div v-else-if="isLoading" style="text-align: center; padding: 20px;">
+            <van-loading type="spinner" color="#1989fa" />
+          </div>
+          <div v-else-if="page.pages <= page.pageNum" style="text-align: center; padding: 20px; color: #ccc;">
+            <span>没有更多了</span>
+          </div>
+          <div v-else style="text-align: center; padding: 20px;">
+            <span @click="clickLoading">点击加载</span>
+          </div>
+        </div>
+
+      </van-pull-refresh>
+      <!-- <div style="height: 20px; "></div> -->
+      <!-- 返回顶部 -->
+      <van-back-top target=".container" right="5vw" bottom="20px" />
+    </div>
+  </div>
+
+
+  <!-- 详情 -->
+  <van-overlay :lock-scroll="false" :show="detail.show">
+    <div style="height: 100%; width: 100%; display: flex; align-items: center; justify-content: center;">
+      <div style="height: 60%; width: 80%; background-color: #ffffff; display: flex; flex-direction: column;">
+        <div style="height: 16px;"></div>
+        <div style="flex-grow: 1; word-break:break-all;overflow-y: auto;padding: 10px 0px 16px 16px;">
+            
+          <div style="padding-right: 16px;">
+            <!-- <div>
+              <span style="font-weight: bold;">扫描结果: </span>
+              <span>{{ detail.content.info }}</span>
+            </div> -->
+            <div v-if="detail.isLoading" style="display: flex; align-items: center; justify-content: center; margin-top: 30px;">
+              <div style="margin-right: 6px;">
+                <span style="">销售信息加载中</span>
+              </div>
+              <div>
+                <van-loading type="spinner" color="#1989fa" />
+              </div>
+            </div>
+            <div v-else>
+              <!-- <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">单元识别码: </span>
+                <span>{{ detail.content.id }}</span>
+              </div> -->
+              <div v-if="detail.content.realnameFlag == '1'">
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">销售方式: </span>
+                  <span style="font-weight: bold; color: green;">实名</span>
+                </div>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">顾客姓名: </span>
+                  <span>{{ detail.content.clientName }}</span>
+                </div>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">手机号: </span>
+                  <span>{{ detail.content.clientPhone }}</span>
+                </div>
+              </div>
+              <div v-else>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">销售方式: </span>
+                  <span style="font-weight: bold; color: red;">匿名</span>
+                </div>
+              </div>
+              <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">登记证号: </span>
+                <span>{{ detail.content.registCode }}</span>
+              </div>
+              <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">农药名称: </span>
+                <span>{{ detail.content.name }}</span>
+              </div>
+              <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">农药类别: </span>
+                <span>{{ detail.content.typeName }}</span>
+              </div>
+              <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">剂型: </span>
+                <span>{{ detail.content.physicType }}</span>
+              </div>
+              <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">总含量: </span>
+                <span>{{ detail.content.concent }}</span>
+              </div>
+              <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">有效期至: </span>
+                <span>{{ detail.content.validDate }}</span>
+              </div>
+              <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">登记证持有人: </span>
+                <span>{{ detail.content.register }}</span>
+              </div>
+              <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">数量: </span>
+                <span>{{ detail.content.num }}</span>
+              </div>
+              <!-- <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">回收对象: </span>
+                <span>{{ detail.content.register }}</span>
+              </div> -->
+            </div>
+            
+          </div>
+            
+        </div>
+        <div style="height: 20px;"></div>
+        <div style="padding: 0px 16px;">
+          <van-button color="#4fc08d" type="danger" round  block size="normal" icon="close" @click="cancel">确定</van-button>
+          <div style="height: 10px;"></div>
+        </div>
+      </div>
+    </div>
+  </van-overlay>
+
+  
+
+
+
+
+
+</template>
+
+<script setup lang="ts">
+  import { onMounted, reactive, ref } from 'vue';
+  import { useRouter } from 'vue-router'
+  import { post } from '@/api/request';
+  import { getDateTimeAgo } from '@/utils/util'
+  // Toast
+  import { showToast } from 'vant';
+  import 'vant/es/toast/style';
+  // 路由
+  const router = useRouter();
+
+  const dropdown_1 = ref<any>();
+
+  // 列表
+  const search = reactive({
+    name: '',
+    mobile: '',
+    startDate: '',
+    endDate: '',
+    sort: 'desc'
+  })
+  const page = reactive({
+    pageNum: 1,
+    pageSize: 10,
+    pages: 0,
+  })
+  const detail = reactive({
+    show: false,
+    isLoading: false,
+    refresherLoading: false,
+    content: {
+      clientName: '',
+      clientPhone: '',
+      realnameFlag: '1',
+      info: '',
+      registCode: '',
+      register: '',
+      id: '',
+      physicType: '',
+      name: '',
+      typeName: '',
+      validDate: '',
+      concent: '',
+      num: 1,
+    },
+  })
+  const dataList = ref<any[]>([]);
+  const whichOne = ref('start');
+  const currentDate = ref<string[]>([]);
+  const maxDate = ref<Date>(new Date());
+  const minDate = ref<Date>(getDateTimeAgo(6));
+  const date_picker_show = ref(false);
+  const option2 = [
+    { text: '时间降序', value: 'desc' },
+    { text: '时间升序', value: 'asc' },
+  ];
+  const refreshing = ref(false);
+  const isLoading = ref(false);
+  const finished = ref(false);
+
+  // 选择开始时间
+  const datetime_picker_start = () => {
+    console.log('start')
+    whichOne.value = 'start';
+    date_picker_show.value = true;
+  }
+  // 清除开始时间
+  const clear_datetime_picker_start = () => {
+    console.log('clear_start')
+    search.startDate = '';
+  }
+  // 选择结束时间
+  const datetime_picker_end = () => {
+    console.log('end')
+    whichOne.value = 'end';
+    date_picker_show.value = true;
+  }
+  // 清除开始时间
+  const clear_datetime_picker_end = () => {
+    console.log('clear_end')
+    search.endDate = '';
+  }
+  // 确定时间选择
+  const datetime_picker_confirm = (confirm: any) => {
+    console.log('confirm', confirm.selectedValues)
+    currentDate.value = confirm.selectedValues
+    const year = confirm.selectedValues[0]
+    const mouth = confirm.selectedValues[1]
+    const day = confirm.selectedValues[2]
+    const date = year + '-' + mouth + '-' + day;
+
+    if(whichOne.value == 'start'){
+      search.startDate = date;
+    }else{
+      search.endDate = date;
+    }
+    console.log(search)
+    date_picker_show.value = false;
+  }
+  // 取消时间选择
+  const datetime_picker_cancel = (cancel: any) => {
+    console.log('cancel', cancel)
+    date_picker_show.value = false
+  }
+
+  // 获取数据
+  const getData = () => {
+    console.log('onload.....')
+    const startTime = search.startDate? search.startDate + ' 00:00:00' : '';
+    const endTime = search.endDate? search.endDate + ' 23:59:59' : '';
+    post('/recycleRecord/searchUserRecord',{
+      clientName: search.name,
+      clientPhone: search.mobile,
+      startTime,
+      endTime,
+      pageNum: page.pageNum,
+      pageSize: page.pageSize,
+      sort: 'createTime',
+      order: search.sort,
+    })
+    .then(({data}) => {
+      console.log(data)
+      // 刷新结束
+      isLoading.value = false;
+      refreshing.value = false;
+      // 处理成功结果
+      if(data.code == 0){
+        // 下拉刷新时清空列表
+        const dataArr:any[] = data.data;
+        const pages = data.pages;
+        console.log(pages);
+        console.log(dataArr)
+        
+        if(page.pageNum == 1){
+          dataList.value.splice(0, dataList.value.length);
+        }
+        dataList.value.push(...dataArr);
+        page.pages = pages
+        console.log(dataList.value, pages)
+      }
+    })
+
+  };
+  const onRefresh = () => {
+    console.log('onrefresh...')
+    console.log(refreshing.value)
+    // 清空列表数据
+    finished.value = false;
+
+    // 重新加载数据
+    // 将 loading 设置为 true,表示处于加载状态
+    isLoading.value = true;
+    getData();
+  };
+
+  // 生命周期
+  onMounted(() => {
+    console.log('onMounted');
+    getData();
+  })
+
+  // 取消
+  const cancel = () => {
+    console.log('确定')
+    // 关闭遮罩层
+    detail.show = false;
+  }
+
+  // 升降序
+  const sort_change = (value: any) => {
+    console.log(value)
+    search.sort = value
+    getData();
+  }
+
+  // 点击加载
+  const clickLoading = () => {
+    console.log('clickLoading');
+    page.pageNum++
+    isLoading.value = true
+    getData();
+  }
+  // 关闭搜索
+  const closeSearch = () => {
+    dropdown_1.value.toggle(false)
+    date_picker_show.value = false;
+  }
+
+  // 取消
+  const onCancel = () => {
+    console.log('onCancel')
+    closeSearch();
+  }
+  // 确认
+  const onConfirm = () => {
+    console.log('onConfirm')
+
+    console.log(search)
+
+    closeSearch();
+    page.pageNum = 1;
+    // 刷新数据
+    getData();
+  }
+
+  // 点击查看详情
+  const clickCell = (event: any) => {
+    console.log(event)
+    const id = event.id;
+    console.log(id)
+    detail.show = true
+    detail.isLoading = true
+
+    post('/recycleRecord/getRecordDetail',{id})
+    .then(({data}) => {
+      console.log(data)
+      // 刷新结束
+      detail.isLoading = false
+      // 处理成功结果
+      if(data.code == 0){
+        console.log('success', data)
+        Object.assign(detail.content, data.data)
+        return;
+      }else{
+        showToast('获取数据失败')
+      }
+    })
+  }
+    
+  // 返回
+  const onClickLeft = () => {
+    console.log(history)
+    if(!history.state.back){
+      console.log('跳转到首页')
+      router.push('/')
+    }else{
+      history.back();
+    }
+  }
+
+</script>
+
+<style scoped>
+  .van-tabbar--fixed{
+    left: auto;
+    max-width: 475px;
+  }
+  :deep(.van-dropdown-menu__item){
+    /* justify-content: flex-start; */
+    padding-left: 10px;
+  }
+  :deep(.van-list__loading, .van-list__finished-text, .van-list__error-text){
+    line-height: 70px;
+  }
+  :deep(.van-list__finished-text){
+    line-height: 70px;
+  }
+
+  .my-dropdown-menu .van-dropdown-menu__title{
+    /* font-size: 16px; */
+  }
+
+  .my-dropdown-menu .van-dropdown-item__title{
+    /* font-size: 16px; */
+  }
+
+</style>

+ 441 - 0
src/pages/sales/add/Index.vue

@@ -0,0 +1,441 @@
+<template>
+  
+  <div style="display: flex; flex-direction: column;height: 100vh;">
+    <!-- 顾客信息 -->
+    <div style="border-bottom: 1px solid #eee; padding: 20px 16px 10px 16px;">
+      <div style="opacity: 0.5;">
+        <div>
+          <van-icon name="user-o" />
+          <span style="margin-left: 6px;">姓&emsp;名:&emsp;</span>
+          <span v-if="customer.isAnonymous" style="color: red;">匿名</span>
+          <span v-else>{{ customer.name }}</span>
+        </div>
+        <div>
+          <van-icon name="phone-o" />
+          <span style="margin-left: 6px;">手机号:&emsp;</span>
+          <span v-if="customer.isAnonymous" style="color: red;">匿名</span>
+          <span v-else>{{ customer.mobile }}</span>
+        </div>
+      </div>
+    </div>
+    <div style="height: 10px;"></div>
+
+    <!-- 扫描结果 -->
+    <!-- <div>
+      <div>{{ scan_content }}</div>
+    </div> -->
+
+
+    <!-- 扫码结果 -->
+    <div style="flex-grow: 1; overflow-y: auto;">
+      <div v-if="scan_list.length == 0">
+      <!-- <div wx:if="{{ 0 }}"> -->
+        <div style="color: #aaa; display: flex; align-items: center; 
+          justify-content: center; padding: 40px 0px 0px 0px;">
+          <van-icon name="warning-o" size="20" />
+          <span style="margin-left: 6px;">暂无扫码结果</span>
+        </div>
+      </div>
+      <div v-else class="scanlist">
+        <van-swipe-cell v-for="(item) in scan_list"
+          :name="item.id"
+          right-width="65">
+          <van-cell-group :border="false">
+            <van-cell @click="clickCell(item)">
+              <div style="text-align: left;">
+                <div style="display: flex; flex-direction: column; justify-content: space-between; flex-grow: 1; overflow-x: hidden;word-break:break-all;">
+                  <div style="width: 100%;">
+                    <div style="display: flex; justify-content: space-between;">
+                      <div style="font-size: 16px; color: #000; font-weight: bold;">{{ item.name }}</div>
+                      <div style="font-size: 16px;">{{ item.typeName }}</div>
+                    </div>
+                    <div>{{ item.register }}</div>
+                  </div>
+                  <div style="display: flex; justify-content: space-between;">
+                    <div>{{ item.registCode }}</div>
+                    <div style="color: blue;">
+                      <van-icon name="cross" size="10" />
+                      <span style="margin-left: 3px; font-weight: bold;">{{ item.amount }}</span>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </van-cell>
+          </van-cell-group>
+          <template #right>
+            <!-- <van-button square type="danger" text="删除" /> -->
+            <div style="background-color: red; height: 100%;
+              width: 65px; color: #ffffff; display: flex; align-items: center;
+              justify-content: center;" @click="deleteCell(item)">
+              <div>删除</div>
+            </div>
+          </template>
+          <!-- <div class="swipe-right" slot="right">
+            <span>删除</span>
+          </div> -->
+        </van-swipe-cell>
+      </div>
+    </div>
+
+    <div style="height: 0px;"></div>
+
+    <!-- 合计结果 -->
+    <div style="border-top: 1px solid #eee; padding: 10px 16px;" 
+      v-if="scan_list.length > 0">
+      <div style="padding: 0px 16px;display: flex;
+        justify-content:flex-end;align-items: center;">
+        <div>
+          <span style="margin-left: 3px;">总计:</span>
+        </div>
+        <span style="display: inline-block; 
+          text-align: right;margin-left: 6px;
+          color: red;font-size: 24px;
+          font-weight: bold;">{{ total }}</span>
+      </div>
+    </div>
+
+    <div style="height: 0px;"></div>
+
+    <div style="padding: 0px 16px;">
+      <van-button color="#4fc08d" type="primary" round  block 
+        size="large" icon="scan" @click="clickScan">扫描二维码</van-button>
+      <div style="height: 10px;"></div>
+      <van-button type="primary" round  block 
+        size="large" icon="guide-o" @click="submit">提交结果</van-button>
+      <div style="height: 10px;"></div>
+    </div>
+  </div>
+
+  <!-- 扫描二维码的界面-相机界面 -->
+  <div v-show="show" style="position: absolute; 
+    width: 100vw; height: 100vh; top: 0; left: 0; 
+    z-index: 99; background-color: #ccc;">
+    <QRScreen ref="scanQR" @success="scanSuccess"></QRScreen>
+  </div>
+
+  <!-- 扫描结果 -->
+  <van-overlay :lock-scroll="false" :show="scan_result_show">
+    <div style="height: 100%; width: 100%; display: flex; align-items: center; justify-content: center;">
+      <div style="height: 60%; width: 80%; background-color: #ffffff; display: flex; flex-direction: column;">
+        <div style="height: 16px;"></div>
+        <div style="flex-grow: 1; 
+          overflow-y: auto;
+          word-break:break-all;
+          padding: 10px 16px 16px 16px;">
+            
+            <div style="padding-right: 16px;">
+              <div>
+                <span style="font-weight: bold;">扫描结果: </span>
+                <span>{{ scan_content.scan_result }}</span>
+              </div>
+              <div v-if="isLoading" style="display: flex; align-items: center; justify-content: center; margin-top: 30px;">
+                <div style="margin-right: 6px;">
+                  <span style="">农药信息加载中</span>
+                </div>
+                <div>
+                  <van-loading type="spinner" color="#1989fa" />
+                </div>
+              </div>
+              <div v-else>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">单元识别码: </span>
+                  <span>{{ scan_content.id }}</span>
+                </div>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">登记证号: </span>
+                  <span>{{ scan_content.registCode }}</span>
+                </div>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">农药名称: </span>
+                  <span>{{ scan_content.name }}</span>
+                </div>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">农药类别: </span>
+                  <span>{{ scan_content.typeName }}</span>
+                </div>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">剂型: </span>
+                  <span>{{ scan_content.physicType }}</span>
+                </div>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">总含量: </span>
+                  <span>{{ scan_content.concent }}</span>
+                </div>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">有效期至: </span>
+                  <span>{{ scan_content.validDate }}</span>
+                </div>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">登记证持有人: </span>
+                  <span>{{ scan_content.register }}</span>
+                </div>
+              </div>
+              
+            </div>
+            
+        </div>
+        <div style="height: 10px;"></div>
+        <div style="border-top: 1px solid #eee; border-bottom: 1px solid #eee; padding: 8px 0px 6px 0px;">
+          <div style="display: flex; align-items: center;justify-content: flex-end;">
+            <div style="margin: 0px 10px 0px 16px;">
+              <span style="color: red;">*</span>
+              <span>&nbsp;&nbsp;数量:</span>
+            </div>
+            <div style="padding-right: 16px;">
+              <van-stepper integer v-model="scan_content.amount" input-width="60px" theme="round" :disabled="customer.isAnonymous" />
+            </div>
+          </div>
+        </div>
+        <div style="height: 20px;"></div>
+        <div style="padding: 0px 16px;">
+          <van-button color="#e87f5e" type="danger" round  block size="normal" icon="close" @click="cancel">取消</van-button>
+          <div style="height: 10px;"></div>
+          <van-button color="#4fc08d" type="primary" round  block size="normal" icon="passed" @click="confirm">添加</van-button>
+          <div style="height: 10px;"></div>
+        </div>
+      </div>
+    </div>
+  </van-overlay>
+
+  
+  
+</template>
+
+<script setup lang="ts">
+  import { computed, onMounted, reactive, ref } from 'vue';
+  import QRScreen from '@/components/QR/Index.vue';
+  import { post } from '@/api/request';
+  // Dialog
+  import { showConfirmDialog } from 'vant';
+  import 'vant/es/dialog/style';
+  // Toast
+  import { showToast } from 'vant';
+  import 'vant/es/toast/style';
+  // 路由
+  import { useRouter } from 'vue-router'
+  const router = useRouter();
+
+  const show = ref(false);
+  const scanQR = ref();
+
+  const customer = reactive({
+    isAnonymous: true,
+    name: '',
+    mobile: '',
+  })
+  const scan_list = ref<any[]>([]);
+  const scan_result_show = ref(false);
+  const isLoading = ref(false);
+  const scan_content = reactive({
+    scan_result: '',
+    registCode: '',
+    register: '',
+    id: '',
+    physicType: '',
+    name: '',
+    typeName: '',
+    validDate: '',
+    concent: '',
+    amount: 1,
+  })
+
+  // 生命周期
+  onMounted(() => {
+    const query = router.currentRoute.value.query;
+    customer.isAnonymous = query.isAnonymous == 'true' ? true : false
+    customer.name = query.name as string
+    customer.mobile = query.mobile as string
+    console.log(router.currentRoute.value.query)
+  })
+
+  // 查看详细信息
+  const clickCell = (event: any) => {
+    console.log('clickCell', event.id);
+    let ind = -1;
+    for(let i = 0; i < scan_list.value.length; i++){
+      if(scan_list.value[i].id == event.id){
+        ind = i;
+        break;
+      }
+    }
+    console.log(ind)
+    if(ind >= 0){
+      Object.assign(scan_content, scan_list.value[ind]);
+      // 打开遮罩层
+      scan_result_show.value = true
+    }
+  }
+  // 删除
+  const deleteCell = (event: any) => {
+    console.log('deleteCell', event.id);
+    showConfirmDialog({
+      message: '确定删除吗?',
+    }).then(() => {
+      console.log(event.id)
+      let ind = -1;
+      for(let i = 0; i < scan_list.value.length; i++){
+        if(scan_list.value[i].id == event.id){
+          ind = i;
+          break;
+        }
+      }
+      console.log(ind)
+      if(ind >= 0){
+        scan_list.value.splice(ind, 1);
+      }
+    }, () => {
+      console.log('取消')
+    });
+  }
+  // 扫码二维码
+  const clickScan = () => {
+    console.log('clickScan');
+    show.value = true
+    scanQR.value.open();
+  }
+  // 提交结果
+  const submit = () => {
+    console.log('submit');
+    console.log('提交');
+    const formData:any[] = [];
+    for(let i = 0; i< scan_list.value.length; i++){
+      const item = scan_list.value[i]
+      const formItem:any = {}
+      formItem.info = item.scan_result
+      formItem.num = item.amount
+      formItem.pesticideName = item.name
+      formItem.pesticideRegistCode = item.registCode
+      formItem.clientName = customer.name;
+      formItem.clientPhone = customer.mobile;
+      formItem.realnameFlag = customer.isAnonymous?0:1;
+      formData[i] = formItem
+    }
+    
+    console.log(formData)
+    if(formData.length == 0){
+      showToast('列表不能为空')
+      return;
+    }
+    // 提交提示
+    showConfirmDialog({
+      message: '确定提交吗?',
+    }).then(() => {
+      post('/saleRecord/batchAddRecord',formData)
+      .then(({data}) => {
+        console.log(data)
+        // 处理成功结果
+        if(data.code == 0){
+          showToast('提交成功');
+          // 跳转到首页
+          setTimeout(() => {
+            // 跳转到首页
+            router.push('/')
+          }, 500)
+        }else{
+          showToast(data.msg);
+        }
+      })
+    }, () => {
+      console.log('取消')
+    });
+  }
+
+  // 计算属性
+  const total = computed(() => {
+    let sum = 0;
+    for(let i = 0; i < scan_list.value.length; i++){
+      sum += scan_list.value[i].amount;
+    }
+    return sum;
+  })
+
+  // 扫码结果
+  const scanSuccess = (e: any) => {
+    show.value = false
+    console.log(e)
+    scan_content.scan_result = e.decodedText;
+    // 获取农药信息
+    getPesticideInfo(scan_content.scan_result);
+  }
+  // 根据扫码结果获取农药信息
+  const getPesticideInfo = (scan_result: string) => {
+    post('/pesticide/getPesticideInfo',{
+      scanResult: scan_result
+    })
+    .then(({data}) => {
+      console.log(data)
+      // 处理成功结果
+      if(data.code == 0){
+        const result = data.data;
+        Object.assign(scan_content, result)
+        // 打开遮罩层
+        scan_result_show.value = true
+        scan_content.amount = 1;
+        return;
+      }
+    })
+  }
+
+  // 取消
+  const cancel = () => {
+    console.log('取消')
+    // 关闭遮罩层
+    scan_result_show.value = false;
+  }
+  // 添加
+  const confirm = (event: any) => {
+    console.log('添加',  scan_content)
+    console.log(event)
+
+    let ind = -1;
+    for(let i = 0; i < scan_list.value.length; i++){
+      if(scan_list.value[i].id == scan_content.id){
+        ind = i;
+        break;
+      }
+    }
+
+    // 添加列表, 克隆对象
+    const item = Object.assign({}, scan_content);
+    if(ind >= 0){
+      scan_list.value[ind] = item
+    }else{
+      scan_list.value.push(item)
+    }
+    console.log(scan_list.value)
+    // 关闭遮罩层
+    scan_result_show.value = false;
+  }
+
+</script>
+
+<style scoped>
+
+  .my-scroll{
+    -webkit-overflow-scrolling: touch;
+  }
+  /*修改滚动条样式*/
+  .my-scroll::-webkit-scrollbar {
+    width: 15px;
+    height: 15px;
+  }
+  .my-scroll::-webkit-scrollbar-track {
+    background: #f1f1f1;
+  }
+  .my-scroll::-webkit-scrollbar-thumb {
+    background: #ddd;
+    border-radius: 7px;
+    height: 50px;
+    border: 4px solid #f1f1f1;
+  }
+  .my-scroll::-webkit-scrollbar-track-piece {
+    width: 10px;
+  }
+  .my-scroll::-webkit-scrollbar-thumb:hover {
+    background: #dddddd;
+  }
+  .my-scroll::-webkit-scrollbar-corner {
+    background: #dddddd;
+  }
+
+</style>

+ 505 - 0
src/pages/sales/list/Index.vue

@@ -0,0 +1,505 @@
+<template>
+
+  <!-- 时间选择 -->
+  <div v-if="date_picker_show" style="position: fixed; bottom: 0px; left: 0px; right: 0px; z-index: 999;">
+    <van-date-picker type="date" v-model="currentDate" :min-date="minDate" :max-date="maxDate" 
+      @confirm="datetime_picker_confirm" 
+      @cancel="datetime_picker_cancel"/>
+  </div>
+
+  <div style="display: flex; flex-direction:column;height: 100vh;">
+    <van-nav-bar
+      title="回收列表"
+      left-text="返回"
+      left-arrow
+      @click-left="onClickLeft">
+      <template #left>
+        <van-icon name="arrow-left" size="20" color="#000000" />
+      </template>
+    </van-nav-bar>
+
+    <!-- 下拉菜单 -->
+    <div style="border-bottom: 1px solid #fefefe;" class="my-dropdown-menu">
+      <van-dropdown-menu :close-on-click-overlay="false" :close-on-click-outside="false">
+        <van-dropdown-item ref="dropdown_1" id="item111" title="筛选条件">
+          <van-cell-group>
+            <van-field size="large" v-model="search.name" :border="true" placeholder="请输入姓名" clearable>
+              <template #label>
+                <span style="color: #646566;">姓&ensp;&ensp;&emsp;名</span>
+              </template>
+            </van-field>
+            <van-field clearable size="large" type="number" v-model="search.mobile" :border="true" placeholder="请输入手机号">
+              <template #label>
+                <span style="color: #646566;">手&ensp;机&ensp;号</span>
+              </template>
+            </van-field>
+            <van-field right-icon="clear" clearable readonly label="时间区间" size="large" type="text" 
+              v-model="search.startDate" :border="true" placeholder="请输入开始时间" 
+              @click-input="datetime_picker_start" 
+              @click-right-icon="clear_datetime_picker_start"/>
+            <van-field right-icon="clear" clearable readonly label=" " size="large" type="text" 
+              v-model="search.endDate" :border="true" placeholder="请输入结束时间" 
+              @click-input="datetime_picker_end" 
+              @click-right-icon="clear_datetime_picker_end"/>
+          </van-cell-group>
+
+          <div style="height: 20px;"></div>
+          <div style="padding: 0px 16px; display: flex;">
+            <div style="flex-grow: 1;">
+              <van-button type="warning" icon="close" size="small" block round @click="onCancel">取消</van-button>
+            </div>
+            <div style="width: 20px;"></div>
+            <div style="flex-grow: 1;">
+              <van-button type="primary" icon="search" size="small" block round @click="onConfirm">搜索</van-button>
+            </div>
+          </div>
+          <div style="height: 15px;"></div>
+        </van-dropdown-item>
+        <van-dropdown-item title-class="my-title-class" v-model="search.sort" :options="option2" @change="sort_change" />
+      </van-dropdown-menu>
+    </div>
+    
+
+    <div class="container" style="overflow-y: auto;flex: 1;">
+      <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
+        <div>
+          <van-swipe-cell v-for="(item) in dataList" right-width="0">
+            <!-- <template #left>
+              <van-button square type="primary" text="选择" />
+            </template> -->
+            <van-cell :border="true"  @click="clickCell(item)">
+              <div style="text-align: left;">
+                <div style="display: flex; flex-direction: column; justify-content: space-between; flex-grow: 1; overflow-x: hidden;word-break:break-all;">
+                  <div style="width: 100%;">
+                    <div style="display: flex; justify-content: space-between;">
+                      <div style="font-size: 16px; color: #000; font-weight: bold; display: flex;">
+                        <div>
+                          <span>{{ item.pesticideName }}</span>
+                        </div>
+                        <div>
+                          <span>(</span>
+                          <van-icon name="cross" size="14" />
+                          <span>{{ item.num }}</span>
+                          <span>)</span>
+                        </div>
+                      </div>
+                      <div style="font-size: 16px;">
+                        <span v-if="item.realnameFlag == '1'">
+                          <!-- <span style="color: blue;">实名</span> -->
+                          <span style="color: blue;">{{ item.clientName }}</span>
+                        </span>
+                        <span v-else style="color: red;">匿名</span>
+                      </div>
+                    </div>
+                    <div>{{ item.register }}</div>
+                  </div>
+                  <div style="display: flex; justify-content: space-between;">
+                    <div>{{ item.pesticideRegistCode }}</div>
+                    <div>
+                      <span style="margin-left: 3px; font-weight: bold;">{{ item.createtime }}</span>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </van-cell>
+            <template #right>
+              <van-button square type="danger" text="删除" />
+            </template>
+          </van-swipe-cell>
+        </div>
+
+        <!-- 数据提示 -->
+        <div>
+          <div v-if="refreshing"></div>
+          <div v-else-if="dataList.length == 0" style="text-align: center; padding: 20px; color: #ccc;">
+            <span>暂无数据</span>
+          </div>
+          <div v-else-if="isLoading" style="text-align: center; padding: 20px;">
+            <van-loading type="spinner" color="#1989fa" />
+          </div>
+          <div v-else-if="page.pages <= page.pageNum" style="text-align: center; padding: 20px; color: #ccc;">
+            <span>没有更多了</span>
+          </div>
+          <div v-else style="text-align: center; padding: 20px;">
+            <span @click="clickLoading">点击加载</span>
+          </div>
+        </div>
+
+      </van-pull-refresh>
+      <!-- <div style="height: 20px; "></div> -->
+      <!-- 返回顶部 -->
+      <van-back-top target=".container" right="5vw" bottom="20px" />
+    </div>
+  </div>
+
+
+  <!-- 详情 -->
+  <van-overlay :lock-scroll="false" :show="detail.show">
+    <div style="height: 100%; width: 100%; display: flex; align-items: center; justify-content: center;">
+      <div style="height: 60%; width: 80%; background-color: #ffffff; display: flex; flex-direction: column;">
+        <div style="height: 16px;"></div>
+        <div style="flex-grow: 1; word-break:break-all;overflow-y: auto;padding: 10px 0px 16px 16px;">
+            
+          <div style="padding-right: 16px;">
+            <!-- <div>
+              <span style="font-weight: bold;">扫描结果: </span>
+              <span>{{ detail.content.info }}</span>
+            </div> -->
+            <div v-if="detail.isLoading" style="display: flex; align-items: center; justify-content: center; margin-top: 30px;">
+              <div style="margin-right: 6px;">
+                <span style="">销售信息加载中</span>
+              </div>
+              <div>
+                <van-loading type="spinner" color="#1989fa" />
+              </div>
+            </div>
+            <div v-else>
+              <!-- <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">单元识别码: </span>
+                <span>{{ detail.content.id }}</span>
+              </div> -->
+              <div v-if="detail.content.realnameFlag == '1'">
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">销售方式: </span>
+                  <span style="font-weight: bold; color: green;">实名</span>
+                </div>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">顾客姓名: </span>
+                  <span>{{ detail.content.clientName }}</span>
+                </div>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">手机号: </span>
+                  <span>{{ detail.content.clientPhone }}</span>
+                </div>
+              </div>
+              <div v-else>
+                <div style="margin-top: 10px;">
+                  <span style="font-weight: bold;">销售方式: </span>
+                  <span style="font-weight: bold; color: red;">匿名</span>
+                </div>
+              </div>
+              <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">登记证号: </span>
+                <span>{{ detail.content.registCode }}</span>
+              </div>
+              <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">农药名称: </span>
+                <span>{{ detail.content.name }}</span>
+              </div>
+              <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">农药类别: </span>
+                <span>{{ detail.content.typeName }}</span>
+              </div>
+              <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">剂型: </span>
+                <span>{{ detail.content.physicType }}</span>
+              </div>
+              <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">总含量: </span>
+                <span>{{ detail.content.concent }}</span>
+              </div>
+              <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">有效期至: </span>
+                <span>{{ detail.content.validDate }}</span>
+              </div>
+              <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">登记证持有人: </span>
+                <span>{{ detail.content.register }}</span>
+              </div>
+              <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">数量: </span>
+                <span>{{ detail.content.num }}</span>
+              </div>
+              <!-- <div style="margin-top: 10px;">
+                <span style="font-weight: bold;">回收对象: </span>
+                <span>{{ detail.content.register }}</span>
+              </div> -->
+            </div>
+            
+          </div>
+            
+        </div>
+        <div style="height: 20px;"></div>
+        <div style="padding: 0px 16px;">
+          <van-button color="#4fc08d" type="danger" round  block size="normal" icon="close" @click="cancel">确定</van-button>
+          <div style="height: 10px;"></div>
+        </div>
+      </div>
+    </div>
+  </van-overlay>
+
+  
+
+
+
+
+
+</template>
+
+<script setup lang="ts">
+  import { onMounted, reactive, ref } from 'vue';
+  import { useRouter } from 'vue-router'
+  import { post } from '@/api/request';
+  import { getDateTimeAgo } from '@/utils/util'
+  // Toast
+  import { showToast } from 'vant';
+  import 'vant/es/toast/style';
+  // 路由
+  const router = useRouter();
+
+  const dropdown_1 = ref<any>();
+
+  // 列表
+  const search = reactive({
+    name: '',
+    mobile: '',
+    startDate: '',
+    endDate: '',
+    sort: 'desc'
+  })
+  const page = reactive({
+    pageNum: 1,
+    pageSize: 10,
+    pages: 0,
+  })
+  const detail = reactive({
+    show: false,
+    isLoading: false,
+    refresherLoading: false,
+    content: {
+      clientName: '',
+      clientPhone: '',
+      realnameFlag: '1',
+      info: '',
+      registCode: '',
+      register: '',
+      id: '',
+      physicType: '',
+      name: '',
+      typeName: '',
+      validDate: '',
+      concent: '',
+      num: 1,
+    },
+  })
+  const dataList = ref<any[]>([]);
+  const whichOne = ref('start');
+  const currentDate = ref<string[]>([]);
+  const maxDate = ref<Date>(new Date());
+  const minDate = ref<Date>(getDateTimeAgo(6));
+  const date_picker_show = ref(false);
+  const option2 = [
+    { text: '时间降序', value: 'desc' },
+    { text: '时间升序', value: 'asc' },
+  ];
+  const refreshing = ref(false);
+  const isLoading = ref(false);
+  const finished = ref(false);
+
+  // 选择开始时间
+  const datetime_picker_start = () => {
+    console.log('start')
+    whichOne.value = 'start';
+    date_picker_show.value = true;
+  }
+  // 清除开始时间
+  const clear_datetime_picker_start = () => {
+    console.log('clear_start')
+    search.startDate = '';
+  }
+  // 选择结束时间
+  const datetime_picker_end = () => {
+    console.log('end')
+    whichOne.value = 'end';
+    date_picker_show.value = true;
+  }
+  // 清除开始时间
+  const clear_datetime_picker_end = () => {
+    console.log('clear_end')
+    search.endDate = '';
+  }
+  // 确定时间选择
+  const datetime_picker_confirm = (confirm: any) => {
+    console.log('confirm', confirm.selectedValues)
+    currentDate.value = confirm.selectedValues
+    const year = confirm.selectedValues[0]
+    const mouth = confirm.selectedValues[1]
+    const day = confirm.selectedValues[2]
+    const date = year + '-' + mouth + '-' + day;
+
+    if(whichOne.value == 'start'){
+      search.startDate = date;
+    }else{
+      search.endDate = date;
+    }
+    console.log(search)
+    date_picker_show.value = false;
+  }
+  // 取消时间选择
+  const datetime_picker_cancel = (cancel: any) => {
+    console.log('cancel', cancel)
+    date_picker_show.value = false
+  }
+
+  // 获取数据
+  const getData = () => {
+    console.log('onload.....')
+    const startTime = search.startDate? search.startDate + ' 00:00:00' : '';
+    const endTime = search.endDate? search.endDate + ' 23:59:59' : '';
+    post('/saleRecord/searchUserRecord',{
+      clientName: search.name,
+      clientPhone: search.mobile,
+      startTime,
+      endTime,
+      pageNum: page.pageNum,
+      pageSize: page.pageSize,
+      sort: 'createTime',
+      order: search.sort,
+    })
+    .then(({data}) => {
+      console.log(data)
+      // 刷新结束
+      isLoading.value = false;
+      refreshing.value = false;
+      // 处理成功结果
+      if(data.code == 0){
+        // 下拉刷新时清空列表
+        const dataArr:any[] = data.data;
+        const pages = data.pages;
+        console.log(pages);
+        console.log(dataArr)
+        
+        if(page.pageNum == 1){
+          dataList.value.splice(0, dataList.value.length);
+        }
+        dataList.value.push(...dataArr);
+        page.pages = pages
+        console.log(dataList.value, pages)
+      }
+    })
+
+  };
+  const onRefresh = () => {
+    console.log('onrefresh...')
+    console.log(refreshing.value)
+    // 清空列表数据
+    finished.value = false;
+
+    // 重新加载数据
+    // 将 loading 设置为 true,表示处于加载状态
+    isLoading.value = true;
+    getData();
+  };
+
+  // 生命周期
+  onMounted(() => {
+    console.log('onMounted');
+    getData();
+  })
+
+  // 取消
+  const cancel = () => {
+    console.log('确定')
+    // 关闭遮罩层
+    detail.show = false;
+  }
+
+  // 升降序
+  const sort_change = (value: any) => {
+    console.log(value)
+    search.sort = value
+    getData();
+  }
+
+  // 点击加载
+  const clickLoading = () => {
+    console.log('clickLoading');
+    page.pageNum++
+    isLoading.value = true
+    getData();
+  }
+  // 关闭搜索
+  const closeSearch = () => {
+    dropdown_1.value.toggle(false)
+    date_picker_show.value = false;
+  }
+
+  // 取消
+  const onCancel = () => {
+    console.log('onCancel')
+    closeSearch();
+  }
+  // 确认
+  const onConfirm = () => {
+    console.log('onConfirm')
+
+    console.log(search)
+
+    closeSearch();
+    page.pageNum = 1;
+    // 刷新数据
+    getData();
+  }
+
+  // 点击查看详情
+  const clickCell = (event: any) => {
+    console.log(event)
+    const id = event.id;
+    console.log(id)
+    detail.show = true
+    detail.isLoading = true
+
+    post('/saleRecord/getRecordDetail',{id})
+    .then(({data}) => {
+      console.log(data)
+      // 刷新结束
+      detail.isLoading = false
+      // 处理成功结果
+      if(data.code == 0){
+        console.log('success', data)
+        Object.assign(detail.content, data.data)
+        return;
+      }else{
+        showToast('获取数据失败')
+      }
+    })
+  }
+    
+  // 返回
+  const onClickLeft = () => {
+    console.log(history)
+    if(!history.state.back){
+      console.log('跳转到首页')
+      router.push('/')
+    }else{
+      history.back();
+    }
+  }
+
+</script>
+
+<style scoped>
+  .van-tabbar--fixed{
+    left: auto;
+    max-width: 475px;
+  }
+  :deep(.van-dropdown-menu__item){
+    /* justify-content: flex-start; */
+    padding-left: 10px;
+  }
+  :deep(.van-list__loading, .van-list__finished-text, .van-list__error-text){
+    line-height: 70px;
+  }
+  :deep(.van-list__finished-text){
+    line-height: 70px;
+  }
+
+  .my-dropdown-menu .van-dropdown-menu__title{
+    /* font-size: 16px; */
+  }
+
+  .my-dropdown-menu .van-dropdown-item__title{
+    /* font-size: 16px; */
+  }
+
+</style>

+ 117 - 0
src/pages/user/Login.vue

@@ -0,0 +1,117 @@
+<template>
+
+  <div style="height: 100%; display: flex; align-items: center;">
+    <div>
+      <van-form ref="formRef" @submit="onSubmit">
+        <van-cell-group inset>
+          <van-field
+            v-model="username"
+            name="username"
+            label="账号"
+            label-width="70"
+            placeholder="请输入账号"
+            :rules="[{ required: true, message: '请填写账号' }]"
+          />
+          <van-field
+            v-model="password"
+            name="password"
+            center
+            clearable
+            label="密码"
+            label-width="70"
+            type="password"
+            placeholder="请输入密码"
+            :rules="[{ required: true, message: '请输入密码' }]"
+          >
+          </van-field>
+          <div style="padding: 16px;">
+            <van-checkbox v-model="rememberPasswordChecked" shape="square">记住密码</van-checkbox>
+          </div>
+        </van-cell-group>
+        <div style="margin: 16px;">
+          <van-button color="#4fc08d" 
+            round block type="primary" size="large"
+            native-type="submit">登录</van-button>
+        </div>
+      </van-form>
+    </div>
+    
+  
+
+  </div>
+
+
+
+</template>
+
+<script setup lang="ts">
+  import { onMounted, ref } from 'vue'
+  import { useLocalStore } from '@/store';
+  import { useRouter } from 'vue-router'
+  import { post } from '@/api/request';
+  import type { FormInstance } from 'vant';
+  // 路由
+  const router = useRouter();
+  const localStore = useLocalStore();
+  // 表单实例
+  const formRef = ref<FormInstance>();
+  // 用户名和验证码
+  const username = ref('')
+  const password = ref('')
+  const rememberPasswordChecked = ref(true);
+  
+  // 生命周期
+  onMounted(() => {
+    console.log('onMounted');
+    rememberPasswordChecked.value = localStore.$state.rememberPassword;
+    if(rememberPasswordChecked.value){
+      username.value = localStore.$state.username
+      password.value = localStore.$state.password
+    }
+  })
+  // 登录提交
+  const onSubmit = () => {
+    formRef.value?.validate().then(() => {
+      // 记住密码
+      localStore.$patch({
+        rememberPassword: rememberPasswordChecked.value
+      })
+      // 记住账号密码
+      if(rememberPasswordChecked.value){
+        localStore.$patch({
+          username: username.value,
+          password: password.value
+        })
+      }
+      // 登录
+      post('/user/login',{
+        username: username.value,
+        password: password.value,
+      })
+      .then(({data}) => {
+        console.log(data)
+        if(data.code == 0){
+          // 存储token
+          localStore.$patch((state) => {
+            state.token = data.data.token
+          })
+          // 跳转到首页
+          router.push('/')
+        }
+      })
+    }).catch((ex) => {
+      console.log(ex)
+    })
+  }
+
+  
+
+</script>
+
+<style scoped>
+  .van-tabbar--fixed{
+    left: auto;
+    max-width: 475px;
+  }
+
+</style>

+ 78 - 0
src/router/index.ts

@@ -0,0 +1,78 @@
+import { createRouter,createWebHashHistory,RouteRecordRaw } from 'vue-router'
+
+const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    redirect: '/home'
+  },
+  {
+    path: '/home1',
+    component: ()=> import('@/pages/Index.vue'),
+    children: [
+      {
+        path: '/home',
+        component: ()=> import('@/pages/home/Index.vue')
+      },
+      {
+        path: '/device',
+        component: ()=> import('@/pages/device/Index.vue')
+      },
+      {
+        path: '/mine',
+        component: ()=> import('@/pages/mine/Index.vue')
+      }
+    ]
+  },
+  {
+    path: '/sales/add',
+    component: ()=> import('@/pages/sales/add/Index.vue')
+  },
+  {
+    path: '/sales/list',
+    component: ()=> import('@/pages/sales/list/Index.vue')
+  },
+  {
+    path: '/recycle/add',
+    component: ()=> import('@/pages/recycle/add/Index.vue')
+  },
+  {
+    path: '/recycle/list',
+    component: ()=> import('@/pages/recycle/list/Index.vue')
+  },
+  {
+    path: '/login',
+    component: () => import('@/pages/user/Login.vue')
+  },
+  {
+    path: '/docs/instructions',
+    component: () => import('@/pages/docs/Instructions.vue')
+  },
+  {
+    path: '/docs/questions',
+    component: () => import('@/pages/docs/Questions.vue')
+  },
+  {
+    path: '/docs/feedback',
+    component: () => import('@/pages/docs/Feedback.vue')
+  },
+  {
+    path: '/docs/about',
+    component: () => import('@/pages/docs/About.vue')
+  },
+  {
+    path: '/demo',
+    component: () => import('@/pages/demo/Index1.vue')
+  }
+]
+
+const router = createRouter({
+  // history: createWebHistory(),
+  history: createWebHashHistory(),
+  routes
+})
+
+export default router
+
+
+
+

+ 17 - 0
src/store/index.ts

@@ -0,0 +1,17 @@
+import { defineStore } from 'pinia'
+
+
+// 存储账号密码
+export const useLocalStore = defineStore('localStore', {
+  state: () => ({
+    username: '',
+    password: '',
+    rememberPassword: true,
+    token: '',
+  }),
+  persist: {
+    enabled: true,
+    encryptionKey: 'nfgj',
+    storage: localStorage,
+  }
+})

+ 21 - 0
src/store/userInfo.ts

@@ -0,0 +1,21 @@
+import { defineStore } from 'pinia'
+import { UserEntity } from '@/api/model'
+
+export const useStoreOfUserInfo = defineStore('userInfo', {
+  state: () => {
+    return <UserEntity>{
+      mobile: '',
+      username: '',
+      password: '',
+      token: '',
+      roles: [],
+      isLogin: false,
+      isRememberMe: false,
+    }
+  },
+  persist: {
+    enabled: true,
+    encryptionKey: 'userInfo',
+    storage: localStorage,
+  }
+});

+ 100 - 0
src/style.css

@@ -0,0 +1,100 @@
+:root {
+  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+  line-height: 1.5;
+  font-weight: 400;
+
+  color-scheme: light dark;
+  color: rgba(255, 255, 255, 0.87);
+  background-color: #242424;
+
+  font-synthesis: none;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-text-size-adjust: 100%;
+
+  
+
+}
+/* 全局配置 */
+:root:root {
+  --van-step-horizontal-title-font-size: 14px;
+  --van-cell-group-background: transparent;
+  --van-cell-background: transparent;
+  --van-cell-font-size: 1rem;
+  --van-font-size-sm: 1rem;
+  --van-cell-icon-size: 1rem;
+  --van-padding-base: 6px;
+}
+
+a {
+  font-weight: 500;
+  color: #646cff;
+  text-decoration: inherit;
+}
+a:hover {
+  color: #535bf2;
+}
+
+body {
+  margin: 0;
+  /* display: flex; */
+  /* place-items: center; */
+  min-width: 320px;
+  min-height: 100vh;
+  /* overflow: hidden; */
+}
+
+h1 {
+  font-size: 3.2em;
+  line-height: 1.1;
+}
+
+button {
+  border-radius: 8px;
+  border: 1px solid transparent;
+  padding: 0.6em 1.2em;
+  font-size: 1em;
+  font-weight: 500;
+  font-family: inherit;
+  background-color: #1a1a1a;
+  cursor: pointer;
+  transition: border-color 0.25s;
+}
+button:hover {
+  border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+  outline: 4px auto -webkit-focus-ring-color;
+}
+
+.card {
+  padding: 2em;
+}
+html{
+  font-size: 16px;
+}
+
+#app {
+  margin: 0 auto;
+  /* padding: 2rem; */
+  /* text-align: center; */
+  height: 100vh;
+  width: 100vw;
+  max-width: 475px;
+  /* font-size: 1.1rem !important; */
+}
+
+@media (prefers-color-scheme: light) {
+  :root {
+    color: #213547;
+    background-color: #ffffff;
+  }
+  a:hover {
+    color: #747bff;
+  }
+  button {
+    background-color: #f9f9f9;
+  }
+}

+ 86 - 0
src/utils/util.ts

@@ -0,0 +1,86 @@
+export const formatDate = (date: Date) => {
+  const year = date.getFullYear()
+  const month = date.getMonth() + 1
+  const day = date.getDate()
+
+  return (
+    [year, month, day].map(formatNumber).join('-')
+  )
+}
+
+// 格式化日期
+export const formatDateWithSplit = (date: Date, split: string) => {
+  const year = date.getFullYear()
+  const month = date.getMonth() + 1
+  const day = date.getDate()
+
+  return (
+    [year, month, day].map(formatNumber).join(split)
+  )
+}
+
+// 格式化日期
+export const formatDate_2 = (date: Date, split: string) => {
+  const month = date.getMonth() + 1
+  const day = date.getDate()
+  return (
+    [month, day].map(formatNumber).join(split)
+  )
+}
+
+// 获取n天前的日期
+export const getDateOfDayAgo = (date: Date, days: number) => {
+  return new Date(date.getTime() - days * 24 * 60 * 60 * 1000);
+}
+
+export const formatTime = (date: Date) => {
+  const year = date.getFullYear()
+  const month = date.getMonth() + 1
+  const day = date.getDate()
+  const hour = date.getHours()
+  const minute = date.getMinutes()
+  const second = date.getSeconds()
+
+  return (
+    [year, month, day].map(formatNumber).join('-') +
+    ' ' +
+    [hour, minute, second].map(formatNumber).join(':')
+  )
+}
+
+const formatNumber = (n: number) => {
+  const s = n.toString()
+  return s[1] ? s : '0' + s
+}
+
+/*
+  验证手机号
+ */
+export const checkPhone = function(phone: string){
+  console.log(phone)
+  if(/^1[3456789]\d{9}$/.test(phone)){
+      return true; 
+  }
+  return false;
+}
+
+
+// 获取当前时间n个月前的时间
+export const getDateTimeAgo = function(month_count: number){
+  const currentDate = new Date();
+  let currentYear = currentDate.getFullYear();
+  let currentMonth = currentDate.getMonth();
+  let currentDay = currentDate.getDate();
+  let year: number = 0;
+  let month: number = 0;
+  let y = Math.trunc(month_count / 12);
+  let m = month_count % 12;
+  if(m < currentMonth){
+    year =  currentYear - y;
+    month = currentMonth - m;
+  }else{
+    year =  currentYear - y - 1;
+    month  = currentMonth + 12 - m;
+  }
+  return new Date(year, month, currentDay);
+}

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

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 28 - 0
tsconfig.json

@@ -0,0 +1,28 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "Node",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "preserve",
+    "baseUrl": "./",
+    "paths": {
+      "@/*":["src/*"]
+    },
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
+  "references": [{ "path": "./tsconfig.node.json" }]
+}

+ 10 - 0
tsconfig.node.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "skipLibCheck": true,
+    "module": "ESNext",
+    "moduleResolution": "Node",
+    "allowSyntheticDefaultImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 36 - 0
vite.config.ts

@@ -0,0 +1,36 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import Components from 'unplugin-vue-components/vite'
+import { VantResolver } from 'unplugin-vue-components/resolvers'
+import path from 'path'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [
+    vue(),
+    Components({
+      resolvers: [VantResolver()]
+    })
+  ],
+  resolve: {
+    alias: {
+      "@": path.resolve(__dirname, './src')
+    }
+  },
+  server: {
+    host: '0.0.0.0',
+    port: 5173,
+    open: true,
+    proxy: {
+      '/forward-service': {
+        target: 'http://192.168.103.33:8585/',
+        // target: 'http://192.168.100.115:8585/', // 生产环境
+        changeOrigin: true,
+        rewrite: (path) => {
+          // console.log('path', path)
+          return path.replace(/^\/forward-service/, '')
+        }
+      }
+    }
+  }
+})

+ 909 - 0
yarn.lock

@@ -0,0 +1,909 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@antfu/utils@^0.7.5":
+  version "0.7.6"
+  resolved "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.6.tgz#30a046419b9e1ecd276e53d41ab68fb6c558c04d"
+  integrity sha512-pvFiLP2BeOKA/ZOS6jxx4XhKzdVLHDhGlFEaZ2flWWYf2xOqVniqpk38I04DFRyz+L0ASggl7SkItTc+ZLju4w==
+
+"@babel/parser@^7.21.3":
+  version "7.22.16"
+  resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.22.16.tgz#180aead7f247305cce6551bea2720934e2fa2c95"
+  integrity sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==
+
+"@babel/parser@^7.24.4":
+  version "7.24.7"
+  resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85"
+  integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==
+
+"@esbuild/android-arm64@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622"
+  integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==
+
+"@esbuild/android-arm@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682"
+  integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==
+
+"@esbuild/android-x64@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2"
+  integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==
+
+"@esbuild/darwin-arm64@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1"
+  integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==
+
+"@esbuild/darwin-x64@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d"
+  integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==
+
+"@esbuild/freebsd-arm64@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54"
+  integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==
+
+"@esbuild/freebsd-x64@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e"
+  integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==
+
+"@esbuild/linux-arm64@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0"
+  integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==
+
+"@esbuild/linux-arm@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0"
+  integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==
+
+"@esbuild/linux-ia32@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7"
+  integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==
+
+"@esbuild/linux-loong64@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d"
+  integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==
+
+"@esbuild/linux-mips64el@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231"
+  integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==
+
+"@esbuild/linux-ppc64@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb"
+  integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==
+
+"@esbuild/linux-riscv64@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6"
+  integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==
+
+"@esbuild/linux-s390x@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071"
+  integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==
+
+"@esbuild/linux-x64@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338"
+  integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==
+
+"@esbuild/netbsd-x64@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1"
+  integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==
+
+"@esbuild/openbsd-x64@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae"
+  integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==
+
+"@esbuild/sunos-x64@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d"
+  integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==
+
+"@esbuild/win32-arm64@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9"
+  integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==
+
+"@esbuild/win32-ia32@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102"
+  integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==
+
+"@esbuild/win32-x64@0.18.20":
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d"
+  integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==
+
+"@jridgewell/sourcemap-codec@^1.4.15":
+  version "1.4.15"
+  resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
+  integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+
+"@nodelib/fs.scandir@2.1.5":
+  version "2.1.5"
+  resolved "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
+  integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+  dependencies:
+    "@nodelib/fs.stat" "2.0.5"
+    run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+  version "2.0.5"
+  resolved "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
+  integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3":
+  version "1.2.8"
+  resolved "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
+  integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+  dependencies:
+    "@nodelib/fs.scandir" "2.1.5"
+    fastq "^1.6.0"
+
+"@rollup/pluginutils@^5.0.2":
+  version "5.0.4"
+  resolved "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.0.4.tgz#74f808f9053d33bafec0cc98e7b835c9667d32ba"
+  integrity sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==
+  dependencies:
+    "@types/estree" "^1.0.0"
+    estree-walker "^2.0.2"
+    picomatch "^2.3.1"
+
+"@types/estree@^1.0.0":
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
+  integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
+
+"@types/node@^20.6.3":
+  version "20.6.3"
+  resolved "https://registry.npmmirror.com/@types/node/-/node-20.6.3.tgz#5b763b321cd3b80f6b8dde7a37e1a77ff9358dd9"
+  integrity sha512-HksnYH4Ljr4VQgEy2lTStbCKv/P590tmPe5HqOnv9Gprffgv5WXAY+Y5Gqniu0GGqeTCUdBnzC3QSrzPkBkAMA==
+
+"@vant/area-data@^1.5.0":
+  version "1.5.0"
+  resolved "https://registry.npmmirror.com/@vant/area-data/-/area-data-1.5.0.tgz#0627f8a6cc67ebc137b43a1867a28d455d21a21c"
+  integrity sha512-SWmDhYmWiOgtAgtJqcW7N4XyGgrg/7l6t1+XSgt8BkPp2oOKO1ZUn8+46brLpT/gzRe/v8BtyTLmdBwMamrmQw==
+
+"@vant/popperjs@^1.3.0":
+  version "1.3.0"
+  resolved "https://registry.npmmirror.com/@vant/popperjs/-/popperjs-1.3.0.tgz#e0eff017124b5b2352ef3b36a6df06277f4400f2"
+  integrity sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw==
+
+"@vant/use@^1.6.0":
+  version "1.6.0"
+  resolved "https://registry.npmmirror.com/@vant/use/-/use-1.6.0.tgz#237df3091617255519552ca311ffdfea9de59001"
+  integrity sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==
+
+"@vitejs/plugin-vue@^4.2.3":
+  version "4.3.4"
+  resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.3.4.tgz#a289dff38e01949fe7be581d5542cabaeb961dec"
+  integrity sha512-ciXNIHKPriERBisHFBvnTbfKa6r9SAesOYXeGDzgegcvy9Q4xdScSHAmKbNT0M3O0S9LKhIf5/G+UYG4NnnzYw==
+
+"@volar/language-core@1.10.1", "@volar/language-core@~1.10.0":
+  version "1.10.1"
+  resolved "https://registry.npmmirror.com/@volar/language-core/-/language-core-1.10.1.tgz#76789c5b0c214eeff8add29cbff0333d89b6fc4a"
+  integrity sha512-JnsM1mIPdfGPxmoOcK1c7HYAsL6YOv0TCJ4aW3AXPZN/Jb4R77epDyMZIVudSGjWMbvv/JfUa+rQ+dGKTmgwBA==
+  dependencies:
+    "@volar/source-map" "1.10.1"
+
+"@volar/source-map@1.10.1", "@volar/source-map@~1.10.0":
+  version "1.10.1"
+  resolved "https://registry.npmmirror.com/@volar/source-map/-/source-map-1.10.1.tgz#b806845782cc615f2beba94624ff34a700f302f5"
+  integrity sha512-3/S6KQbqa7pGC8CxPrg69qHLpOvkiPHGJtWPkI/1AXCsktkJ6gIk/5z4hyuMp8Anvs6eS/Kvp/GZa3ut3votKA==
+  dependencies:
+    muggle-string "^0.3.1"
+
+"@volar/typescript@~1.10.0":
+  version "1.10.1"
+  resolved "https://registry.npmmirror.com/@volar/typescript/-/typescript-1.10.1.tgz#b20341c1cc5785b4de0669ea645e1619c97a4764"
+  integrity sha512-+iiO9yUSRHIYjlteT+QcdRq8b44qH19/eiUZtjNtuh6D9ailYM7DVR0zO2sEgJlvCaunw/CF9Ov2KooQBpR4VQ==
+  dependencies:
+    "@volar/language-core" "1.10.1"
+
+"@vue/compiler-core@3.3.4":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.4.tgz#7fbf591c1c19e1acd28ffd284526e98b4f581128"
+  integrity sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==
+  dependencies:
+    "@babel/parser" "^7.21.3"
+    "@vue/shared" "3.3.4"
+    estree-walker "^2.0.2"
+    source-map-js "^1.0.2"
+
+"@vue/compiler-core@3.4.27":
+  version "3.4.27"
+  resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.4.27.tgz#e69060f4b61429fe57976aa5872cfa21389e4d91"
+  integrity sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==
+  dependencies:
+    "@babel/parser" "^7.24.4"
+    "@vue/shared" "3.4.27"
+    entities "^4.5.0"
+    estree-walker "^2.0.2"
+    source-map-js "^1.2.0"
+
+"@vue/compiler-dom@3.4.27":
+  version "3.4.27"
+  resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.4.27.tgz#d51d35f40d00ce235d7afc6ad8b09dfd92b1cc1c"
+  integrity sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==
+  dependencies:
+    "@vue/compiler-core" "3.4.27"
+    "@vue/shared" "3.4.27"
+
+"@vue/compiler-dom@^3.3.0":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz#f56e09b5f4d7dc350f981784de9713d823341151"
+  integrity sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==
+  dependencies:
+    "@vue/compiler-core" "3.3.4"
+    "@vue/shared" "3.3.4"
+
+"@vue/compiler-sfc@3.4.27":
+  version "3.4.27"
+  resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.4.27.tgz#399cac1b75c6737bf5440dc9cf3c385bb2959701"
+  integrity sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==
+  dependencies:
+    "@babel/parser" "^7.24.4"
+    "@vue/compiler-core" "3.4.27"
+    "@vue/compiler-dom" "3.4.27"
+    "@vue/compiler-ssr" "3.4.27"
+    "@vue/shared" "3.4.27"
+    estree-walker "^2.0.2"
+    magic-string "^0.30.10"
+    postcss "^8.4.38"
+    source-map-js "^1.2.0"
+
+"@vue/compiler-ssr@3.4.27":
+  version "3.4.27"
+  resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.4.27.tgz#2a8ecfef1cf448b09be633901a9c020360472e3d"
+  integrity sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==
+  dependencies:
+    "@vue/compiler-dom" "3.4.27"
+    "@vue/shared" "3.4.27"
+
+"@vue/devtools-api@^6.5.0":
+  version "6.5.0"
+  resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz#98b99425edee70b4c992692628fa1ea2c1e57d07"
+  integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==
+
+"@vue/language-core@1.8.13":
+  version "1.8.13"
+  resolved "https://registry.npmmirror.com/@vue/language-core/-/language-core-1.8.13.tgz#fc05f8f9034c04013000da5ed6aa7a6afe6ccc2f"
+  integrity sha512-nata2fYBZAkl4QJrU+IcArJCMTHt1VP8ePL/Z7eUPC2AF+Cm7Qgo9ksNCPBzZRh1LYjCaSaqV7njqNogwpsMVg==
+  dependencies:
+    "@volar/language-core" "~1.10.0"
+    "@volar/source-map" "~1.10.0"
+    "@vue/compiler-dom" "^3.3.0"
+    "@vue/reactivity" "^3.3.0"
+    "@vue/shared" "^3.3.0"
+    minimatch "^9.0.0"
+    muggle-string "^0.3.1"
+    vue-template-compiler "^2.7.14"
+
+"@vue/reactivity@3.4.27":
+  version "3.4.27"
+  resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.4.27.tgz#6ece72331bf719953f5eaa95ec60b2b8d49e3791"
+  integrity sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==
+  dependencies:
+    "@vue/shared" "3.4.27"
+
+"@vue/reactivity@^3.3.0":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.4.tgz#a27a29c6cd17faba5a0e99fbb86ee951653e2253"
+  integrity sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==
+  dependencies:
+    "@vue/shared" "3.3.4"
+
+"@vue/runtime-core@3.4.27":
+  version "3.4.27"
+  resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.4.27.tgz#1b6e1d71e4604ba7442dd25ed22e4a1fc6adbbda"
+  integrity sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==
+  dependencies:
+    "@vue/reactivity" "3.4.27"
+    "@vue/shared" "3.4.27"
+
+"@vue/runtime-dom@3.4.27":
+  version "3.4.27"
+  resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.4.27.tgz#fe8d1ce9bbe8921d5dd0ad5c10df0e04ef7a5ee7"
+  integrity sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==
+  dependencies:
+    "@vue/runtime-core" "3.4.27"
+    "@vue/shared" "3.4.27"
+    csstype "^3.1.3"
+
+"@vue/server-renderer@3.4.27":
+  version "3.4.27"
+  resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.4.27.tgz#3306176f37e648ba665f97dda3ce705687be63d2"
+  integrity sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==
+  dependencies:
+    "@vue/compiler-ssr" "3.4.27"
+    "@vue/shared" "3.4.27"
+
+"@vue/shared@3.3.4", "@vue/shared@^3.0.0", "@vue/shared@^3.3.0":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.4.tgz#06e83c5027f464eef861c329be81454bc8b70780"
+  integrity sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==
+
+"@vue/shared@3.4.27":
+  version "3.4.27"
+  resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.4.27.tgz#f05e3cd107d157354bb4ae7a7b5fc9cf73c63b50"
+  integrity sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==
+
+"@vue/typescript@1.8.13":
+  version "1.8.13"
+  resolved "https://registry.npmmirror.com/@vue/typescript/-/typescript-1.8.13.tgz#0663f165778ba43d9691e617c5836496a6bc4f7a"
+  integrity sha512-ALJjHFqQ3dgZVCI/ogAS/dZ7JEhIi1N0Em5I7uwabY1p9RDRK3odLsycMHyxZRjm5dLI15c07eeBloHiD2Otlg==
+  dependencies:
+    "@volar/typescript" "~1.10.0"
+    "@vue/language-core" "1.8.13"
+
+acorn@^8.10.0:
+  version "8.10.0"
+  resolved "https://registry.npmmirror.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
+  integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
+
+anymatch@~3.1.2:
+  version "3.1.3"
+  resolved "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
+  integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+  integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
+axios@^1.5.0:
+  version "1.7.2"
+  resolved "https://registry.npmmirror.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621"
+  integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==
+  dependencies:
+    follow-redirects "^1.15.6"
+    form-data "^4.0.0"
+    proxy-from-env "^1.1.0"
+
+balanced-match@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+  integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+binary-extensions@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+  integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
+brace-expansion@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
+  integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
+  dependencies:
+    balanced-match "^1.0.0"
+
+braces@^3.0.2, braces@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.npmmirror.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
+chokidar@^3.5.3:
+  version "3.5.3"
+  resolved "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+  integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+  dependencies:
+    anymatch "~3.1.2"
+    braces "~3.0.2"
+    glob-parent "~5.1.2"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.6.0"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+combined-stream@^1.0.8:
+  version "1.0.8"
+  resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+  dependencies:
+    delayed-stream "~1.0.0"
+
+crypto-js@^4.1.1:
+  version "4.2.0"
+  resolved "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
+  integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
+
+csstype@^3.1.3:
+  version "3.1.3"
+  resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
+  integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
+
+de-indent@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
+  integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
+
+debug@^4.3.4:
+  version "4.3.4"
+  resolved "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+  dependencies:
+    ms "2.1.2"
+
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+  integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
+entities@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
+  integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
+
+esbuild@^0.18.10:
+  version "0.18.20"
+  resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6"
+  integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==
+  optionalDependencies:
+    "@esbuild/android-arm" "0.18.20"
+    "@esbuild/android-arm64" "0.18.20"
+    "@esbuild/android-x64" "0.18.20"
+    "@esbuild/darwin-arm64" "0.18.20"
+    "@esbuild/darwin-x64" "0.18.20"
+    "@esbuild/freebsd-arm64" "0.18.20"
+    "@esbuild/freebsd-x64" "0.18.20"
+    "@esbuild/linux-arm" "0.18.20"
+    "@esbuild/linux-arm64" "0.18.20"
+    "@esbuild/linux-ia32" "0.18.20"
+    "@esbuild/linux-loong64" "0.18.20"
+    "@esbuild/linux-mips64el" "0.18.20"
+    "@esbuild/linux-ppc64" "0.18.20"
+    "@esbuild/linux-riscv64" "0.18.20"
+    "@esbuild/linux-s390x" "0.18.20"
+    "@esbuild/linux-x64" "0.18.20"
+    "@esbuild/netbsd-x64" "0.18.20"
+    "@esbuild/openbsd-x64" "0.18.20"
+    "@esbuild/sunos-x64" "0.18.20"
+    "@esbuild/win32-arm64" "0.18.20"
+    "@esbuild/win32-ia32" "0.18.20"
+    "@esbuild/win32-x64" "0.18.20"
+
+estree-walker@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
+  integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
+
+fast-glob@^3.3.0:
+  version "3.3.1"
+  resolved "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
+  integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
+  dependencies:
+    "@nodelib/fs.stat" "^2.0.2"
+    "@nodelib/fs.walk" "^1.2.3"
+    glob-parent "^5.1.2"
+    merge2 "^1.3.0"
+    micromatch "^4.0.4"
+
+fastq@^1.6.0:
+  version "1.15.0"
+  resolved "https://registry.npmmirror.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
+  integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==
+  dependencies:
+    reusify "^1.0.4"
+
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+follow-redirects@^1.15.6:
+  version "1.15.6"
+  resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
+  integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
+
+form-data@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+  integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
+fsevents@~2.3.2:
+  version "2.3.3"
+  resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+  integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+glob-parent@^5.1.2, glob-parent@~5.1.2:
+  version "5.1.2"
+  resolved "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+  dependencies:
+    is-glob "^4.0.1"
+
+has@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+  dependencies:
+    function-bind "^1.1.1"
+
+he@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
+  integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+
+html5-qrcode@^2.3.8:
+  version "2.3.8"
+  resolved "https://registry.npmmirror.com/html5-qrcode/-/html5-qrcode-2.3.8.tgz#0b0cdf7a9926cfd4be530e13a51db47592adfa0d"
+  integrity sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==
+
+is-binary-path@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
+is-core-module@^2.13.0:
+  version "2.13.0"
+  resolved "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db"
+  integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==
+  dependencies:
+    has "^1.0.3"
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-glob@^4.0.1, is-glob@~4.0.1:
+  version "4.0.3"
+  resolved "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+jsqr@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.npmmirror.com/jsqr/-/jsqr-1.4.0.tgz#8efb8d0a7cc6863cb6d95116b9069123ce9eb2d1"
+  integrity sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==
+
+local-pkg@^0.4.3:
+  version "0.4.3"
+  resolved "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.4.3.tgz#0ff361ab3ae7f1c19113d9bb97b98b905dbc4963"
+  integrity sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==
+
+lru-cache@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+  dependencies:
+    yallist "^4.0.0"
+
+magic-string@^0.30.1:
+  version "0.30.3"
+  resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.3.tgz#403755dfd9d6b398dfa40635d52e96c5ac095b85"
+  integrity sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==
+  dependencies:
+    "@jridgewell/sourcemap-codec" "^1.4.15"
+
+magic-string@^0.30.10:
+  version "0.30.10"
+  resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e"
+  integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==
+  dependencies:
+    "@jridgewell/sourcemap-codec" "^1.4.15"
+
+merge2@^1.3.0:
+  version "1.4.1"
+  resolved "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
+  integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+micromatch@^4.0.4:
+  version "4.0.5"
+  resolved "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
+  integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+  dependencies:
+    braces "^3.0.2"
+    picomatch "^2.3.1"
+
+mime-db@1.52.0:
+  version "1.52.0"
+  resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+  integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.12:
+  version "2.1.35"
+  resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+  integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+  dependencies:
+    mime-db "1.52.0"
+
+minimatch@^9.0.0, minimatch@^9.0.3:
+  version "9.0.3"
+  resolved "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825"
+  integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==
+  dependencies:
+    brace-expansion "^2.0.1"
+
+ms@2.1.2:
+  version "2.1.2"
+  resolved "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+muggle-string@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.3.1.tgz#e524312eb1728c63dd0b2ac49e3282e6ed85963a"
+  integrity sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==
+
+nanoid@^3.3.6:
+  version "3.3.6"
+  resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
+  integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
+
+nanoid@^3.3.7:
+  version "3.3.7"
+  resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
+  integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+path-parse@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+  integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+picocolors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+pinia-use-persist@^0.0.21:
+  version "0.0.21"
+  resolved "https://registry.npmmirror.com/pinia-use-persist/-/pinia-use-persist-0.0.21.tgz#3b351f5f2143d2e8932bc88137493b05625a6595"
+  integrity sha512-l48Tiq536tnImvqz898iGIG6YfP0Fp8hj2skdNkFv5fTdf4iFC9/g28bMi17JR/aUQZZkg8fm6maZAGNP2BICg==
+  dependencies:
+    crypto-js "^4.1.1"
+    pinia "^2.0.14"
+
+pinia@^2.0.14, pinia@^2.1.6:
+  version "2.1.7"
+  resolved "https://registry.npmmirror.com/pinia/-/pinia-2.1.7.tgz#4cf5420d9324ca00b7b4984d3fbf693222115bbc"
+  integrity sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==
+  dependencies:
+    "@vue/devtools-api" "^6.5.0"
+    vue-demi ">=0.14.5"
+
+postcss@^8.4.27:
+  version "8.4.30"
+  resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.30.tgz#0e0648d551a606ef2192a26da4cabafcc09c1aa7"
+  integrity sha512-7ZEao1g4kd68l97aWG/etQKPKq07us0ieSZ2TnFDk11i0ZfDW2AwKHYU8qv4MZKqN2fdBfg+7q0ES06UA73C1g==
+  dependencies:
+    nanoid "^3.3.6"
+    picocolors "^1.0.0"
+    source-map-js "^1.0.2"
+
+postcss@^8.4.38:
+  version "8.4.38"
+  resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
+  integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
+  dependencies:
+    nanoid "^3.3.7"
+    picocolors "^1.0.0"
+    source-map-js "^1.2.0"
+
+proxy-from-env@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+  integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
+queue-microtask@^1.2.2:
+  version "1.2.3"
+  resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+  integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+readdirp@~3.6.0:
+  version "3.6.0"
+  resolved "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+  dependencies:
+    picomatch "^2.2.1"
+
+resolve@^1.22.2:
+  version "1.22.6"
+  resolved "https://registry.npmmirror.com/resolve/-/resolve-1.22.6.tgz#dd209739eca3aef739c626fea1b4f3c506195362"
+  integrity sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==
+  dependencies:
+    is-core-module "^2.13.0"
+    path-parse "^1.0.7"
+    supports-preserve-symlinks-flag "^1.0.0"
+
+reusify@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
+  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
+rollup@^3.27.1:
+  version "3.29.2"
+  resolved "https://registry.npmmirror.com/rollup/-/rollup-3.29.2.tgz#cbc76cd5b03b9f9e93be991d23a1dff9c6d5b740"
+  integrity sha512-CJouHoZ27v6siztc21eEQGo0kIcE5D1gVPA571ez0mMYb25LGYGKnVNXpEj5MGlepmDWGXNjDB5q7uNiPHC11A==
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+run-parallel@^1.1.9:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
+  integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+  dependencies:
+    queue-microtask "^1.2.2"
+
+semver@^7.3.8:
+  version "7.5.4"
+  resolved "https://registry.npmmirror.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
+  integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
+  dependencies:
+    lru-cache "^6.0.0"
+
+source-map-js@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
+  integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+
+source-map-js@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
+  integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
+
+supports-preserve-symlinks-flag@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+  integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
+typescript@^5.0.2:
+  version "5.2.2"
+  resolved "https://registry.npmmirror.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
+  integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
+
+unplugin-vue-components@^0.25.2:
+  version "0.25.2"
+  resolved "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-0.25.2.tgz#99d9d02a4066a24e720edbe74a82a4ee6ff86153"
+  integrity sha512-OVmLFqILH6w+eM8fyt/d/eoJT9A6WO51NZLf1vC5c1FZ4rmq2bbGxTy8WP2Jm7xwFdukaIdv819+UI7RClPyCA==
+  dependencies:
+    "@antfu/utils" "^0.7.5"
+    "@rollup/pluginutils" "^5.0.2"
+    chokidar "^3.5.3"
+    debug "^4.3.4"
+    fast-glob "^3.3.0"
+    local-pkg "^0.4.3"
+    magic-string "^0.30.1"
+    minimatch "^9.0.3"
+    resolve "^1.22.2"
+    unplugin "^1.4.0"
+
+unplugin@^1.4.0:
+  version "1.5.0"
+  resolved "https://registry.npmmirror.com/unplugin/-/unplugin-1.5.0.tgz#8938ae84defe62afc7757df9ca05d27160f6c20c"
+  integrity sha512-9ZdRwbh/4gcm1JTOkp9lAkIDrtOyOxgHmY7cjuwI8L/2RTikMcVG25GsZwNAgRuap3iDw2jeq7eoqtAsz5rW3A==
+  dependencies:
+    acorn "^8.10.0"
+    chokidar "^3.5.3"
+    webpack-sources "^3.2.3"
+    webpack-virtual-modules "^0.5.0"
+
+vant@^4.6.8:
+  version "4.6.8"
+  resolved "https://registry.npmmirror.com/vant/-/vant-4.6.8.tgz#4a257d512b88e810c53a8df1f1aa2d48ced4dd7b"
+  integrity sha512-kWkv4kQJWOyF8qQzLD07yDscVBtx281yYdSXJ6RnXVMYje0hZMIikE06zM/NoVYUZTnStVTO89h0zAxLuuDd/A==
+  dependencies:
+    "@vant/popperjs" "^1.3.0"
+    "@vant/use" "^1.6.0"
+    "@vue/shared" "^3.0.0"
+
+vite@^4.4.5:
+  version "4.4.9"
+  resolved "https://registry.npmmirror.com/vite/-/vite-4.4.9.tgz#1402423f1a2f8d66fd8d15e351127c7236d29d3d"
+  integrity sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==
+  dependencies:
+    esbuild "^0.18.10"
+    postcss "^8.4.27"
+    rollup "^3.27.1"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+vue-demi@>=0.14.5:
+  version "0.14.6"
+  resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.6.tgz#dc706582851dc1cdc17a0054f4fec2eb6df74c92"
+  integrity sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==
+
+vue-router@4:
+  version "4.2.4"
+  resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.2.4.tgz#382467a7e2923e6a85f015d081e1508052c191b9"
+  integrity sha512-9PISkmaCO02OzPVOMq2w82ilty6+xJmQrarYZDkjZBfl4RvYAlt4PKnEX21oW4KTtWfa9OuO/b3qk1Od3AEdCQ==
+  dependencies:
+    "@vue/devtools-api" "^6.5.0"
+
+vue-template-compiler@^2.7.14:
+  version "2.7.14"
+  resolved "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz#4545b7dfb88090744c1577ae5ac3f964e61634b1"
+  integrity sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==
+  dependencies:
+    de-indent "^1.0.2"
+    he "^1.2.0"
+
+vue-tsc@^1.8.5:
+  version "1.8.13"
+  resolved "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-1.8.13.tgz#979b515016a74ff975447e364d45c54e986b6101"
+  integrity sha512-Hl8zUXPVK2KzPtbXeMCN0CSFkwvD96YOtYt9KvJPG9W8QGcNpGk9KHwPuGMxA8blWXSIli7gtsoC+clICEVdVg==
+  dependencies:
+    "@vue/language-core" "1.8.13"
+    "@vue/typescript" "1.8.13"
+    semver "^7.3.8"
+
+vue@^3.3.4:
+  version "3.4.27"
+  resolved "https://registry.npmmirror.com/vue/-/vue-3.4.27.tgz#40b7d929d3e53f427f7f5945386234d2854cc2a1"
+  integrity sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==
+  dependencies:
+    "@vue/compiler-dom" "3.4.27"
+    "@vue/compiler-sfc" "3.4.27"
+    "@vue/runtime-dom" "3.4.27"
+    "@vue/server-renderer" "3.4.27"
+    "@vue/shared" "3.4.27"
+
+webpack-sources@^3.2.3:
+  version "3.2.3"
+  resolved "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
+  integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
+
+webpack-virtual-modules@^0.5.0:
+  version "0.5.0"
+  resolved "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz#362f14738a56dae107937ab98ea7062e8bdd3b6c"
+  integrity sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==
+
+yallist@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==