tangning 2 years ago
parent
commit
1cabbd1e8d
100 changed files with 12481 additions and 0 deletions
  1. 4 0
      .env
  2. 3 0
      .env.development
  3. 3 0
      .env.production
  4. 0 0
      .env.test
  5. 15 0
      .eslintignore
  6. 72 0
      .eslintrc.js
  7. 25 0
      .gitignore
  8. 7 0
      .husky/pre-commit
  9. 9 0
      .prettierignore
  10. 3 0
      .stylelintignore
  11. 3 0
      .vscode/extensions.json
  12. 17 0
      .vscode/settings.json
  13. 6 0
      Dockerfile
  14. 9 0
      License
  15. 9 0
      build/constant.ts
  16. 27 0
      build/utils.ts
  17. 22 0
      build/vite/plugins/autoImport.ts
  18. 22 0
      build/vite/plugins/component.ts
  19. 19 0
      build/vite/plugins/compress.ts
  20. 10 0
      build/vite/plugins/eruda.ts
  21. 37 0
      build/vite/plugins/imagemin.ts
  22. 74 0
      build/vite/plugins/index.ts
  23. 19 0
      build/vite/plugins/mock.ts
  24. 14 0
      build/vite/plugins/pages.ts
  25. 11 0
      build/vite/plugins/progress.ts
  26. 12 0
      build/vite/plugins/restart.ts
  27. 22 0
      build/vite/plugins/styleImport.ts
  28. 17 0
      build/vite/plugins/svgIcons.ts
  29. 19 0
      build/vite/plugins/visualizer.ts
  30. 20 0
      build/vite/proxy.ts
  31. 45 0
      index.html
  32. 22 0
      mock/index.ts
  33. 47 0
      nginx.conf
  34. 117 0
      package.json
  35. 8292 0
      pnpm-lock.yaml
  36. 30 0
      postcss.config.js
  37. 10 0
      prettier.config.js
  38. BIN
      public/favicon.ico
  39. 14 0
      src/App.vue
  40. 12 0
      src/api/index.ts
  41. 11 0
      src/assets/app.css
  42. BIN
      src/assets/button/en-us/confirm.png
  43. BIN
      src/assets/button/zh-cn/confirm.png
  44. 536 0
      src/assets/font/demo.css
  45. 37 0
      src/assets/font/iconfont.css
  46. 51 0
      src/assets/font/iconfont.json
  47. BIN
      src/assets/font/iconfont.ttf
  48. BIN
      src/assets/font/iconfont.woff
  49. BIN
      src/assets/font/iconfont.woff2
  50. BIN
      src/assets/image/case1.jpg
  51. BIN
      src/assets/image/tips/kj.png
  52. BIN
      src/assets/image/tips/kk.png
  53. BIN
      src/assets/image/tips/l-sh.png
  54. BIN
      src/assets/image/tips/ss.png
  55. BIN
      src/assets/image/tips/xf.png
  56. BIN
      src/assets/logo.png
  57. 53 0
      src/components/TitleBar/index.vue
  58. 27 0
      src/i18n/index.ts
  59. 20 0
      src/i18n/lang/en-us.ts
  60. 18 0
      src/i18n/lang/lang-base.ts
  61. 20 0
      src/i18n/lang/zh-cn.ts
  62. 69 0
      src/layout/basic/index.vue
  63. 30 0
      src/main.ts
  64. 71 0
      src/plugins/nutUI.ts
  65. 13 0
      src/router/index.ts
  66. 99 0
      src/router/routes.ts
  67. 16 0
      src/store/index.ts
  68. 57 0
      src/store/modules/home.ts
  69. 45 0
      src/store/modules/user.ts
  70. 3 0
      src/styles/index.scss
  71. 23 0
      src/styles/mixin.scss
  72. 16 0
      src/utils/index.ts
  73. 72 0
      src/utils/useAxiosApi.ts
  74. 41 0
      src/utils/useFetchApi.ts
  75. 63 0
      src/views/demo/index.vue
  76. 56 0
      src/views/detail/evaluate.vue
  77. 339 0
      src/views/detail/index.vue
  78. 136 0
      src/views/detail/invoice.vue
  79. 55 0
      src/views/detail/payment.vue
  80. 128 0
      src/views/detail/repair.vue
  81. 93 0
      src/views/home/index.vue
  82. 110 0
      src/views/home/list.vue
  83. 188 0
      src/views/home/submit.vue
  84. 17 0
      src/views/list/index.vue
  85. 60 0
      src/views/login/index.vue
  86. 60 0
      src/views/member/index.vue
  87. 89 0
      stylelint.config.js
  88. 44 0
      tsconfig.json
  89. 73 0
      types/auto-imports.d.ts
  90. 53 0
      types/axios.d.ts
  91. 30 0
      types/components.d.ts
  92. 162 0
      types/config.d.ts
  93. 76 0
      types/global.d.ts
  94. 27 0
      types/index.d.ts
  95. 16 0
      types/module.d.ts
  96. 48 0
      types/store.d.ts
  97. 5 0
      types/utils.d.ts
  98. 45 0
      types/vue-router.d.ts
  99. 61 0
      vite.config.ts
  100. 0 0
      yarn.lock

+ 4 - 0
.env

@@ -0,0 +1,4 @@
+VITE_TOKEN_KEY=tokenKey
+
+VITE_URL_PREFIX=/api
+

+ 3 - 0
.env.development

@@ -0,0 +1,3 @@
+VITE_USE_MOCK=true
+
+VITE_USE_ERUDA=true

+ 3 - 0
.env.production

@@ -0,0 +1,3 @@
+VITE_USE_MOCK=false
+
+VITE_USE_ERUDA=false

+ 0 - 0
.env.test


+ 15 - 0
.eslintignore

@@ -0,0 +1,15 @@
+
+*.sh
+node_modules
+*.md
+*.woff
+*.ttf
+.vscode
+.idea
+dist
+/public
+/docs
+.husky
+.local
+/bin
+Dockerfile

+ 72 - 0
.eslintrc.js

@@ -0,0 +1,72 @@
+module.exports = {
+  root: true,
+  env: {
+    browser: true,
+    node: true,
+    es6: true,
+  },
+  parser: 'vue-eslint-parser',
+  parserOptions: {
+    parser: '@typescript-eslint/parser',
+    ecmaVersion: 2020,
+    sourceType: 'module',
+    jsxPragma: 'React',
+    ecmaFeatures: {
+      jsx: true,
+    },
+  },
+  extends: ['plugin:vue/vue3-recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
+  rules: {
+    'vue/script-setup-uses-vars': 'error',
+    '@typescript-eslint/ban-ts-ignore': 'off',
+    '@typescript-eslint/explicit-function-return-type': 'off',
+    '@typescript-eslint/no-explicit-any': 'off',
+    '@typescript-eslint/no-var-requires': 'off',
+    '@typescript-eslint/no-empty-function': 'off',
+    'vue/custom-event-name-casing': 'off',
+    'no-use-before-define': 'off',
+    '@typescript-eslint/no-use-before-define': 'off',
+    '@typescript-eslint/ban-ts-comment': 'off',
+    '@typescript-eslint/ban-types': 'off',
+    '@typescript-eslint/no-non-null-assertion': 'off',
+    '@typescript-eslint/explicit-module-boundary-types': 'off',
+    '@typescript-eslint/no-unused-vars': [
+      'error',
+      {
+        argsIgnorePattern: '^_',
+        varsIgnorePattern: '^_',
+      },
+    ],
+    'no-unused-vars': [
+      'error',
+      {
+        argsIgnorePattern: '^_',
+        varsIgnorePattern: '^_',
+      },
+    ],
+    'space-before-function-paren': 'off',
+
+    'vue/attributes-order': 'off',
+    'vue/one-component-per-file': 'off',
+    'vue/html-closing-bracket-newline': 'off',
+    'vue/max-attributes-per-line': 'off',
+    'vue/multiline-html-element-content-newline': 'off',
+    'vue/singleline-html-element-content-newline': 'off',
+    'vue/attribute-hyphenation': 'off',
+    'vue/require-default-prop': 'off',
+    'vue/require-explicit-emits': 'off',
+    'vue/html-self-closing': [
+      'error',
+      {
+        html: {
+          void: 'always',
+          normal: 'never',
+          component: 'always',
+        },
+        svg: 'always',
+        math: 'always',
+      },
+    ],
+    'vue/multi-word-component-names': 'off',
+  },
+};

+ 25 - 0
.gitignore

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

+ 7 - 0
.husky/pre-commit

@@ -0,0 +1,7 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+[ -n "$CI" ] && exit 0
+
+# Format and submit code according to lintstagedrc.js configuration
+npm run lint:lint-staged

+ 9 - 0
.prettierignore

@@ -0,0 +1,9 @@
+/dist/*
+.local
+.output.js
+/node_modules/**
+
+**/*.svg
+**/*.sh
+
+/public/*

+ 3 - 0
.stylelintignore

@@ -0,0 +1,3 @@
+/dist/*
+/public/*
+public/*

+ 3 - 0
.vscode/extensions.json

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

+ 17 - 0
.vscode/settings.json

@@ -0,0 +1,17 @@
+{
+  "i18n-ally.localesPaths": ["src/i18n", "src/i18n/lang"],
+  "cSpell.words": [
+    "browserslist",
+    "consola",
+    "eruda",
+    "mockjs",
+    "nutui",
+    "pinia",
+    "stylelint",
+    "unplugin",
+    "vant",
+    "vite",
+    "vitejs",
+    "vueuse"
+  ]
+}

+ 6 - 0
Dockerfile

@@ -0,0 +1,6 @@
+
+FROM nginx
+# 将dist文件中的内容复制到 /usr/share/nginx/html/ 这个目录下面
+COPY dist/ /usr/share/nginx/html/dist/
+# 用本地的 nginx.conf 配置来替换nginx镜像里的默认配置
+COPY nginx.conf /etc/nginx/nginx.conf

+ 9 - 0
License

@@ -0,0 +1,9 @@
+MIT License
+
+Copyright (c) 2020-present, vue-h5-template
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 9 - 0
build/constant.ts

@@ -0,0 +1,9 @@
+export const IsReport = process.env.REPORT;
+
+export const API_BASE_URL = '';
+
+export const MOCK_API_BASE_URL = '';
+
+export const API_TARGET_URL = '';
+
+export const MOCK_API_TARGET_URL = '';

+ 27 - 0
build/utils.ts

@@ -0,0 +1,27 @@
+// Read all environment variable configuration files to process.env
+export function wrapperEnv(envConf: Recordable): ViteEnv {
+  const ret: any = {};
+
+  for (const envName of Object.keys(envConf)) {
+    let realName = envConf[envName].replace(/\\n/g, '\n');
+    realName = realName === 'true' ? true : realName === 'false' ? false : realName;
+
+    if (envName === 'VITE_PORT') {
+      realName = Number(realName);
+    }
+    if (envName === 'VITE_PROXY' && realName) {
+      try {
+        realName = JSON.parse(realName.replace(/'/g, '"'));
+      } catch (error) {
+        realName = '';
+      }
+    }
+    ret[envName] = realName;
+    if (typeof realName === 'string') {
+      process.env[envName] = realName;
+    } else if (typeof realName === 'object') {
+      process.env[envName] = JSON.stringify(realName);
+    }
+  }
+  return ret;
+}

+ 22 - 0
build/vite/plugins/autoImport.ts

@@ -0,0 +1,22 @@
+/**
+ * @name AutoImportDeps
+ * @description 按需加载,自动引入
+ */
+
+import AutoImport from 'unplugin-auto-import/vite';
+import { VarletUIResolver, VantResolver } from 'unplugin-vue-components/resolvers';
+
+export const AutoImportDeps = () => {
+  return AutoImport({
+    dts: 'types/auto-imports.d.ts',
+    imports: [
+      'vue',
+      'pinia',
+      'vue-router',
+      {
+        '@vueuse/core': [],
+      },
+    ],
+    resolvers: [VarletUIResolver(), VantResolver()],
+  });
+};

+ 22 - 0
build/vite/plugins/component.ts

@@ -0,0 +1,22 @@
+/**
+ * @name  AutoRegistryComponents
+ * @description 按需加载,自动引入组件
+ */
+
+import Components from 'unplugin-vue-components/vite';
+import { VueUseComponentsResolver, VantResolver, VarletUIResolver } from 'unplugin-vue-components/resolvers';
+
+export const AutoRegistryComponents = () => {
+  return Components({
+    // dirs: ['src/components'],
+    extensions: ['vue', 'md'],
+    deep: true,
+    dts: 'types/components.d.ts',
+    directoryAsNamespace: false,
+    globalNamespaces: [],
+    directives: true,
+    include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
+    exclude: [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/, /[\\/]\.nuxt[\\/]/],
+    resolvers: [VueUseComponentsResolver(), VantResolver(), VarletUIResolver()],
+  });
+};

+ 19 - 0
build/vite/plugins/compress.ts

@@ -0,0 +1,19 @@
+/**
+ * @name ConfigCompressPlugin
+ * @description 开启.gz压缩
+ */
+
+import viteCompression from 'vite-plugin-compression';
+
+export const ConfigCompressPlugin = () => {
+  return viteCompression({
+    verbose: true, // 默认即可
+    disable: false, //开启压缩(不禁用),默认即可
+    deleteOriginFile: false, //删除源文件
+    threshold: 10240, //压缩前最小文件大小
+    algorithm: 'gzip', //压缩算法
+    ext: '.gz', //文件类型
+  });
+
+  return [];
+};

+ 10 - 0
build/vite/plugins/eruda.ts

@@ -0,0 +1,10 @@
+/**
+ * @name ConfigEruda
+ * @description 控制台,方便移动端调试
+ */
+
+import eruda from 'vite-plugin-eruda';
+
+export const ConfigEruda = () => {
+  return eruda();
+};

+ 37 - 0
build/vite/plugins/imagemin.ts

@@ -0,0 +1,37 @@
+/**
+ * @name ConfigImageminPlugin
+ * @description 图片压缩
+ */
+
+import viteImagemin from 'vite-plugin-imagemin';
+
+export function ConfigImageminPlugin() {
+  const plugin = viteImagemin({
+    gifsicle: {
+      optimizationLevel: 7,
+      interlaced: false,
+    },
+    mozjpeg: {
+      quality: 20,
+    },
+    optipng: {
+      optimizationLevel: 7,
+    },
+    pngquant: {
+      quality: [0.8, 0.9],
+      speed: 4,
+    },
+    svgo: {
+      plugins: [
+        {
+          name: 'removeViewBox',
+        },
+        {
+          name: 'removeEmptyAttrs',
+          active: false,
+        },
+      ],
+    },
+  });
+  return plugin;
+}

+ 74 - 0
build/vite/plugins/index.ts

@@ -0,0 +1,74 @@
+/**
+ * @name createVitePlugins
+ * @description 封装plugins数组统一调用
+ */
+
+import type { PluginOption } from 'vite';
+import vue from '@vitejs/plugin-vue';
+import vueJsx from '@vitejs/plugin-vue-jsx';
+import vueSetupExtend from 'vite-plugin-vue-setup-extend';
+import { ConfigSvgIconsPlugin } from './svgIcons';
+import { AutoRegistryComponents } from './component';
+import { AutoImportDeps } from './autoImport';
+import { ConfigMockPlugin } from './mock';
+import { ConfigCompressPlugin } from './compress';
+import { ConfigPagesPlugin } from './pages';
+import { ConfigRestartPlugin } from './restart';
+import { ConfigProgressPlugin } from './progress';
+import { ConfigEruda } from './eruda';
+import { ConfigStyleImport } from './styleImport';
+import { ConfigImageminPlugin } from './imagemin';
+import { ConfigVisualizerConfig } from './visualizer';
+
+export function createVitePlugins(env: ViteEnv, isBuild: boolean) {
+  const { VITE_USE_MOCK, VITE_USE_ERUDA } = env;
+
+  const vitePlugins: (PluginOption | PluginOption[])[] = [
+    // vue支持
+    vue(),
+    // JSX支持
+    vueJsx(),
+    // setup语法糖组件名支持
+    vueSetupExtend(),
+  ];
+
+  // 自动按需引入组件
+  vitePlugins.push(AutoRegistryComponents());
+
+  // 自动按需引入依赖
+  vitePlugins.push(AutoImportDeps());
+
+  // 自动生成路由
+  vitePlugins.push(ConfigPagesPlugin());
+
+  // 开启.gz压缩  rollup-plugin-gzip
+  vitePlugins.push(ConfigCompressPlugin());
+
+  // 监听配置文件改动重启
+  vitePlugins.push(ConfigRestartPlugin());
+
+  // 构建时显示进度条
+  vitePlugins.push(ConfigProgressPlugin());
+
+  //styleImport
+  vitePlugins.push(ConfigStyleImport());
+
+  // eruda
+  VITE_USE_ERUDA && vitePlugins.push(ConfigEruda());
+
+  // rollup-plugin-visualizer
+  vitePlugins.push(ConfigVisualizerConfig());
+
+  // vite-plugin-mock
+  VITE_USE_MOCK && vitePlugins.push(ConfigMockPlugin(isBuild));
+
+  // vite-plugin-svg-icons
+  vitePlugins.push(ConfigSvgIconsPlugin(isBuild));
+
+  if (isBuild) {
+    // vite-plugin-imagemin
+    vitePlugins.push(ConfigImageminPlugin());
+  }
+
+  return vitePlugins;
+}

+ 19 - 0
build/vite/plugins/mock.ts

@@ -0,0 +1,19 @@
+/**
+ * @name ConfigMockPlugin
+ * @description 引入mockjs,本地模拟接口
+ */
+
+import { viteMockServe } from 'vite-plugin-mock';
+export const ConfigMockPlugin = (isBuild: boolean) => {
+  return viteMockServe({
+    ignore: /^\_/,
+    mockPath: 'mock',
+    localEnabled: !isBuild,
+    prodEnabled: false, //实际开发请关闭,会影响打包体积
+    // https://github.com/anncwb/vite-plugin-mock/issues/9
+    injectCode: `
+       import { setupProdMockServer } from '../mock/_createProdMockServer';
+       setupProdMockServer();
+       `,
+  });
+};

+ 14 - 0
build/vite/plugins/pages.ts

@@ -0,0 +1,14 @@
+/**
+ * @name ConfigPagesPlugin
+ * @description 动态生成路由
+ */
+
+import Pages from 'vite-plugin-pages';
+export const ConfigPagesPlugin = () => {
+  return Pages({
+    pagesDir: [{ dir: 'src/pages', baseRoute: '' }],
+    extensions: ['vue', 'md'],
+    exclude: ['**/components/*.vue'],
+    nuxtStyle: true,
+  });
+};

+ 11 - 0
build/vite/plugins/progress.ts

@@ -0,0 +1,11 @@
+/**
+ * @name ConfigProgressPlugin
+ * @description 构建显示进度条
+ */
+
+import { Plugin } from 'vite';
+import progress from 'vite-plugin-progress';
+
+export const ConfigProgressPlugin = () => {
+  return progress() as Plugin;
+};

+ 12 - 0
build/vite/plugins/restart.ts

@@ -0,0 +1,12 @@
+/**
+ * @name ConfigRestartPlugin
+ * @description 监听配置文件修改自动重启Vite
+ */
+
+import ViteRestart from 'vite-plugin-restart';
+
+export const ConfigRestartPlugin = () => {
+  return ViteRestart({
+    restart: ['*.config.[jt]s', '**/config/*.[jt]s'],
+  });
+};

+ 22 - 0
build/vite/plugins/styleImport.ts

@@ -0,0 +1,22 @@
+/**
+ * @name ConfigRestartPlugin
+ * @description 按需引入样式文件
+ */
+
+import { createStyleImportPlugin, Lib, VantResolve } from 'vite-plugin-style-import';
+
+function NutuiResolve(): Lib {
+  return {
+    libraryName: '@nutui/nutui',
+    libraryNameChangeCase: 'pascalCase',
+    resolveStyle: (name) => {
+      return `@nutui/nutui/dist/packages/${name.toLowerCase()}/index.scss`;
+    },
+  };
+}
+
+export const ConfigStyleImport = () => {
+  return createStyleImportPlugin({
+    resolves: [NutuiResolve(), VantResolve()],
+  });
+};

+ 17 - 0
build/vite/plugins/svgIcons.ts

@@ -0,0 +1,17 @@
+/**
+ * @name SvgIconsPlugin
+ * @description 加载SVG文件,自动引入
+ */
+
+import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
+import path from 'path';
+
+export const ConfigSvgIconsPlugin = (isBuild: boolean) => {
+  return createSvgIconsPlugin({
+    // 指定需要缓存的图标文件夹
+    iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
+    // 指定symbolId格式
+    symbolId: 'icon-[dir]-[name]',
+    svgoOptions: isBuild,
+  });
+};

+ 19 - 0
build/vite/plugins/visualizer.ts

@@ -0,0 +1,19 @@
+/**
+ * @name ConfigVisualizerConfig
+ * @description 打包体积分析
+ */
+
+import visualizer from 'rollup-plugin-visualizer';
+import { IsReport } from '../../constant';
+
+export function ConfigVisualizerConfig() {
+  if (IsReport) {
+    return visualizer({
+      filename: './node_modules/.cache/visualizer/stats.html',
+      open: true,
+      gzipSize: true,
+      brotliSize: true,
+    });
+  }
+  return [];
+}

+ 20 - 0
build/vite/proxy.ts

@@ -0,0 +1,20 @@
+import { API_BASE_URL, API_TARGET_URL, MOCK_API_BASE_URL, MOCK_API_TARGET_URL } from '../constant';
+import { ProxyOptions } from 'vite';
+type ProxyTargetList = Record<string, ProxyOptions>;
+
+const init: ProxyTargetList = {
+  // test
+  [API_BASE_URL]: {
+    target: API_TARGET_URL,
+    changeOrigin: true,
+    rewrite: (path) => path.replace(new RegExp(`^${API_BASE_URL}`), ''),
+  },
+  // mock
+  [MOCK_API_BASE_URL]: {
+    target: MOCK_API_TARGET_URL,
+    changeOrigin: true,
+    rewrite: (path) => path.replace(new RegExp(`^${MOCK_API_BASE_URL}`), '/api'),
+  },
+};
+
+export default init;

+ 45 - 0
index.html

@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.ico" />
+    <meta
+      name="viewport"
+      content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0,viewport-fit=cover"
+    />
+    <meta name="format-detection" content="telephone=no, email=no, date=no, address=no" />
+    <title>四维时代售后</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+    <script>
+      window.onload = function () {
+        document.addEventListener('touchstart', function (event) {
+          if (event.touches.length > 1) {
+            event.preventDefault();
+          }
+        });
+
+        var lastTouchEnd = 0;
+
+        document.addEventListener(
+          'touchend',
+          function (event) {
+            var now = new Date().getTime();
+            if (now - lastTouchEnd <= 300) {
+              event.preventDefault();
+            }
+
+            lastTouchEnd = now;
+          },
+          false,
+        );
+
+        document.addEventListener('gesturestart', function (event) {
+          event.preventDefault();
+        });
+      };
+    </script>
+  </body>
+</html>

+ 22 - 0
mock/index.ts

@@ -0,0 +1,22 @@
+import { MockMethod, Recordable } from 'vite-plugin-mock';
+
+interface response {
+  body: Recordable;
+  query: Recordable;
+}
+
+export default [
+  {
+    url: '/api/login',
+    method: 'post',
+    response: ({ body, query }: response) => {
+      console.log('body>>>>>>>>', body);
+      console.log('query>>>>>>>>', query);
+      return {
+        code: 200,
+        message: 'ok',
+        data: { name: 'Evan', age: 26 },
+      };
+    },
+  },
+] as MockMethod[];

+ 47 - 0
nginx.conf

@@ -0,0 +1,47 @@
+#user  nobody;
+worker_processes  auto;
+events {
+    worker_connections  1024;
+}
+http {
+    include       mime.types;
+    default_type  application/octet-stream;
+    sendfile        on;
+    #tcp_nopush     on;
+    #keepalive_timeout  0;
+    keepalive_timeout  65;
+    gzip  on;
+
+    server {
+        listen       80;
+        server_name  localhost;
+
+        charset utf-8;
+
+        location / {
+            root  html;
+            try_files $uri $uri/ @router;
+            index  index.html index.htm;
+        }
+
+        location @router {
+            rewrite ^.*$ /index.html break;
+        }
+
+        location /backend/{
+            proxy_set_header Host $http_host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header REMOTE-HOST $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            client_max_body_size 20M;
+            proxy_send_timeout 500;
+            proxy_read_timeout 480;
+            proxy_pass http://localhost:8992;
+        }
+
+        error_page   500 502 503 504  /50x.html;
+        location = /50x.html {
+            root   html;
+        }
+    }
+}

+ 117 - 0
package.json

@@ -0,0 +1,117 @@
+{
+  "name": "vue-h5-template",
+  "version": "1.0.0",
+  "scripts": {
+    "dev": "vite",
+    "dev:test": "vite --mode test",
+    "dev:prod": "vite --mode production",
+    "build": "vue-tsc --noEmit && vite build",
+    "report": "cross-env REPORT=true npm run build",
+    "preview": "vite preview",
+    "lint:eslint": "eslint --cache --max-warnings 0  \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
+    "lint:prettier": "prettier --write  \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
+    "lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
+    "lint:lint-staged": "lint-staged",
+    "prepare": "husky install",
+    "deps": "yarn upgrade-interactive --latest",
+    "commit": "git add . && git-cz"
+  },
+  "dependencies": {
+    "@nutui/nutui": "^3.3.1",
+    "@varlet/ui": "^2.4.2",
+    "@vueuse/core": "9.8.2",
+    "@vueuse/integrations": "9.8.2",
+    "axios": "1.2.1",
+    "dayjs": "^1.11.7",
+    "mitt": "^3.0.0",
+    "pinia": "^2.0.28",
+    "pinia-plugin-persistedstate": "^3.0.1",
+    "universal-cookie": "^4.0.4",
+    "vant": "^4.0.3",
+    "vue": "^3.2.45",
+    "vue-i18n": "^9.2.2",
+    "vue-router": "^4.1.6"
+  },
+  "devDependencies": {
+    "@typescript-eslint/eslint-plugin": "^5.47.0",
+    "@typescript-eslint/parser": "^5.47.0",
+    "@vitejs/plugin-legacy": "^3.0.1",
+    "@vitejs/plugin-vue": "^4.0.0",
+    "@vitejs/plugin-vue-jsx": "^3.0.0",
+    "amfe-flexible": "^2.2.1",
+    "autoprefixer": "^10.4.13",
+    "cnjm-postcss-px-to-viewport": "^1.0.0",
+    "consola": "^2.15.3",
+    "cross-env": "^7.0.3",
+    "eruda": "^2.9.1",
+    "eslint": "^8.30.0",
+    "eslint-config-prettier": "^8.5.0",
+    "eslint-plugin-prettier": "^4.2.1",
+    "eslint-plugin-vue": "^9.8.0",
+    "git-cz": "^4.9.0",
+    "husky": "8.0.2",
+    "lint-staged": "13.1.0",
+    "mockjs": "^1.1.0",
+    "postcss": "^8.4.20",
+    "postcss-html": "1.5.0",
+    "postcss-less": "^6.0.0",
+    "prettier": "^2.8.1",
+    "rollup-plugin-visualizer": "^5.8.3",
+    "stylelint": "^14.16.0",
+    "stylelint-config-prettier": "^9.0.4",
+    "stylelint-config-recommended": "^9.0.0",
+    "stylelint-config-recommended-vue": "^1.4.0",
+    "stylelint-config-standard": "^29.0.0",
+    "stylelint-order": "^5.0.0",
+    "terser": "^5.16.1",
+    "typescript": "^4.9.4",
+    "unplugin-auto-import": "^0.12.1",
+    "unplugin-vue-components": "^0.22.12",
+    "vite": "^4.0.2",
+    "vite-plugin-compression": "^0.5.1",
+    "vite-plugin-eruda": "^1.0.1",
+    "vite-plugin-imagemin": "^0.6.1",
+    "vite-plugin-mock": "^2.9.6",
+    "vite-plugin-pages": "^0.28.0",
+    "vite-plugin-progress": "^0.0.6",
+    "vite-plugin-restart": "^0.3.0",
+    "vite-plugin-style-import": "^2.0.0",
+    "vite-plugin-svg-icons": "^2.0.1",
+    "vite-plugin-vue-setup-extend": "^0.4.0",
+    "vue-eslint-parser": "^9.1.0",
+    "vue-tsc": "^1.0.16"
+  },
+  "husky": {
+    "hooks": {
+      "pre-commit": "lint-staged"
+    }
+  },
+  "resolutions": {
+    "bin-wrapper": "npm:bin-wrapper-china",
+    "gifsicle": "5.2.0"
+  },
+  "lint-staged": {
+    "*.{js,jsx,ts,tsx}": [
+      "eslint --fix",
+      "prettier --write"
+    ],
+    "{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": [
+      "prettier --write--parser json"
+    ],
+    "package.json": [
+      "prettier --write"
+    ],
+    "*.vue": [
+      "eslint --fix",
+      "prettier --write",
+      "stylelint --fix"
+    ],
+    "*.{scss,less,styl,html}": [
+      "stylelint --fix",
+      "prettier --write"
+    ],
+    "*.md": [
+      "prettier --write"
+    ]
+  }
+}

File diff suppressed because it is too large
+ 8292 - 0
pnpm-lock.yaml


+ 30 - 0
postcss.config.js

@@ -0,0 +1,30 @@
+const path = require('path');
+
+const judgeComponent = (file) => {
+  const ignore = ['vant', '@nutui', '@varlet'];
+  return ignore.some((item) => path.join(file).includes(path.join('node_modules', item)));
+};
+
+module.exports = {
+  plugins: {
+    autoprefixer: { overrideBrowserslist: ['Android 4.1', 'iOS 7.1', 'Chrome > 31', 'ff > 31', 'ie >= 8'] },
+    'cnjm-postcss-px-to-viewport': {
+      unitToConvert: 'px', // 要转化的单位
+      viewportWidth: 375, // UI设计稿的宽度
+      unitPrecision: 6, // 转换后的精度,即小数点位数
+      propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
+      viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw
+      fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw
+      minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
+      mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
+      replace: true, // 是否转换后直接更换属性值
+      include: [],
+      exclude: [], // 设置忽略文件,用正则做目录名匹配
+      customFun: ({ file }) => {
+        // 这个自定义的方法是针对处理vant组件下的设计稿为375问题
+        const designWidth = judgeComponent(file) ? 375 : 375;
+        return designWidth;
+      },
+    },
+  },
+};

+ 10 - 0
prettier.config.js

@@ -0,0 +1,10 @@
+module.exports = {
+  printWidth: 140,
+  semi: true,
+  vueIndentScriptAndStyle: true,
+  singleQuote: true,
+  trailingComma: 'all',
+  proseWrap: 'never',
+  htmlWhitespaceSensitivity: 'strict',
+  endOfLine: 'auto',
+};

BIN
public/favicon.ico


+ 14 - 0
src/App.vue

@@ -0,0 +1,14 @@
+<template>
+  <router-view />
+</template>
+<script setup lang="ts"></script>
+
+<style>
+  #app {
+    font-family: PingFang SC-Regular, PingFang SC;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+    color: #2c3e50;
+    background-color: var(--color-bg-1);
+  }
+</style>

+ 12 - 0
src/api/index.ts

@@ -0,0 +1,12 @@
+import useAxiosApi from '/@/utils/useAxiosApi';
+
+/**
+ * 账号密码登录
+ * @returns UseAxiosReturn
+ */
+export function loginPassword() {
+  return useAxiosApi(`/api/login`, {
+    method: 'POST',
+    data: { name: '123' },
+  });
+}

+ 11 - 0
src/assets/app.css

@@ -0,0 +1,11 @@
+html,
+body,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p {
+  margin: 0;
+}

BIN
src/assets/button/en-us/confirm.png


BIN
src/assets/button/zh-cn/confirm.png


+ 536 - 0
src/assets/font/demo.css

@@ -0,0 +1,536 @@
+/* Logo 字体 */
+@font-face {
+  font-family: 'iconfont logo';
+  src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
+  src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
+    url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
+    url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
+    url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
+}
+
+.logo {
+  font-family: 'iconfont logo';
+  font-size: 160px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+/* tabs */
+.nav-tabs {
+  position: relative;
+}
+
+.nav-tabs .nav-more {
+  position: absolute;
+  right: 0;
+  bottom: 0;
+  height: 42px;
+  line-height: 42px;
+  color: #666;
+}
+
+#tabs {
+  border-bottom: 1px solid #eee;
+}
+
+#tabs li {
+  cursor: pointer;
+  width: 100px;
+  height: 40px;
+  line-height: 40px;
+  text-align: center;
+  font-size: 16px;
+  border-bottom: 2px solid transparent;
+  position: relative;
+  z-index: 1;
+  margin-bottom: -1px;
+  color: #666;
+}
+
+#tabs .active {
+  border-bottom-color: #f00;
+  color: #222;
+}
+
+.tab-container .content {
+  display: none;
+}
+
+/* 页面布局 */
+.main {
+  padding: 30px 100px;
+  width: 960px;
+  margin: 0 auto;
+}
+
+.main .logo {
+  color: #333;
+  text-align: left;
+  margin-bottom: 30px;
+  line-height: 1;
+  height: 110px;
+  margin-top: -50px;
+  overflow: hidden;
+  *zoom: 1;
+}
+
+.main .logo a {
+  font-size: 160px;
+  color: #333;
+}
+
+.helps {
+  margin-top: 40px;
+}
+
+.helps pre {
+  padding: 20px;
+  margin: 10px 0;
+  border: solid 1px #e7e1cd;
+  background-color: #fffdef;
+  overflow: auto;
+}
+
+.icon_lists {
+  width: 100% !important;
+  overflow: hidden;
+  *zoom: 1;
+}
+
+.icon_lists li {
+  width: 100px;
+  margin-bottom: 10px;
+  margin-right: 20px;
+  text-align: center;
+  list-style: none !important;
+  cursor: default;
+}
+
+.icon_lists li .code-name {
+  line-height: 1.2;
+}
+
+.icon_lists .icon {
+  display: block;
+  height: 100px;
+  line-height: 100px;
+  font-size: 42px;
+  margin: 10px auto;
+  color: #333;
+  -webkit-transition: font-size 0.25s linear, width 0.25s linear;
+  -moz-transition: font-size 0.25s linear, width 0.25s linear;
+  transition: font-size 0.25s linear, width 0.25s linear;
+}
+
+.icon_lists .icon:hover {
+  font-size: 100px;
+}
+
+.icon_lists .svg-icon {
+  /* 通过设置 font-size 来改变图标大小 */
+  width: 1em;
+  /* 图标和文字相邻时,垂直对齐 */
+  vertical-align: -0.15em;
+  /* 通过设置 color 来改变 SVG 的颜色/fill */
+  fill: currentColor;
+  /* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
+      normalize.css 中也包含这行 */
+  overflow: hidden;
+}
+
+.icon_lists li .name,
+.icon_lists li .code-name {
+  color: #666;
+}
+
+/* markdown 样式 */
+.markdown {
+  color: #666;
+  font-size: 14px;
+  line-height: 1.8;
+}
+
+.highlight {
+  line-height: 1.5;
+}
+
+.markdown img {
+  vertical-align: middle;
+  max-width: 100%;
+}
+
+.markdown h1 {
+  color: #404040;
+  font-weight: 500;
+  line-height: 40px;
+  margin-bottom: 24px;
+}
+
+.markdown h2,
+.markdown h3,
+.markdown h4,
+.markdown h5,
+.markdown h6 {
+  color: #404040;
+  margin: 1.6em 0 0.6em 0;
+  font-weight: 500;
+  clear: both;
+}
+
+.markdown h1 {
+  font-size: 28px;
+}
+
+.markdown h2 {
+  font-size: 22px;
+}
+
+.markdown h3 {
+  font-size: 16px;
+}
+
+.markdown h4 {
+  font-size: 14px;
+}
+
+.markdown h5 {
+  font-size: 12px;
+}
+
+.markdown h6 {
+  font-size: 12px;
+}
+
+.markdown hr {
+  height: 1px;
+  border: 0;
+  background: #e9e9e9;
+  margin: 16px 0;
+  clear: both;
+}
+
+.markdown p {
+  margin: 1em 0;
+}
+
+.markdown > p,
+.markdown > blockquote,
+.markdown > .highlight,
+.markdown > ol,
+.markdown > ul {
+  width: 80%;
+}
+
+.markdown ul > li {
+  list-style: circle;
+}
+
+.markdown > ul li,
+.markdown blockquote ul > li {
+  margin-left: 20px;
+  padding-left: 4px;
+}
+
+.markdown > ul li p,
+.markdown > ol li p {
+  margin: 0.6em 0;
+}
+
+.markdown ol > li {
+  list-style: decimal;
+}
+
+.markdown > ol li,
+.markdown blockquote ol > li {
+  margin-left: 20px;
+  padding-left: 4px;
+}
+
+.markdown code {
+  margin: 0 3px;
+  padding: 0 5px;
+  background: #eee;
+  border-radius: 3px;
+}
+
+.markdown strong,
+.markdown b {
+  font-weight: 600;
+}
+
+.markdown > table {
+  border-collapse: collapse;
+  border-spacing: 0px;
+  empty-cells: show;
+  border: 1px solid #e9e9e9;
+  width: 95%;
+  margin-bottom: 24px;
+}
+
+.markdown > table th {
+  white-space: nowrap;
+  color: #333;
+  font-weight: 600;
+}
+
+.markdown > table th,
+.markdown > table td {
+  border: 1px solid #e9e9e9;
+  padding: 8px 16px;
+  text-align: left;
+}
+
+.markdown > table th {
+  background: #f7f7f7;
+}
+
+.markdown blockquote {
+  font-size: 90%;
+  color: #999;
+  border-left: 4px solid #e9e9e9;
+  padding-left: 0.8em;
+  margin: 1em 0;
+}
+
+.markdown blockquote p {
+  margin: 0;
+}
+
+.markdown .anchor {
+  opacity: 0;
+  transition: opacity 0.3s ease;
+  margin-left: 8px;
+}
+
+.markdown .waiting {
+  color: #ccc;
+}
+
+.markdown h1:hover .anchor,
+.markdown h2:hover .anchor,
+.markdown h3:hover .anchor,
+.markdown h4:hover .anchor,
+.markdown h5:hover .anchor,
+.markdown h6:hover .anchor {
+  opacity: 1;
+  display: inline-block;
+}
+
+.markdown > br,
+.markdown > p > br {
+  clear: both;
+}
+
+.hljs {
+  display: block;
+  background: white;
+  padding: 0.5em;
+  color: #333333;
+  overflow-x: auto;
+}
+
+.hljs-comment,
+.hljs-meta {
+  color: #969896;
+}
+
+.hljs-string,
+.hljs-variable,
+.hljs-template-variable,
+.hljs-strong,
+.hljs-emphasis,
+.hljs-quote {
+  color: #df5000;
+}
+
+.hljs-keyword,
+.hljs-selector-tag,
+.hljs-type {
+  color: #a71d5d;
+}
+
+.hljs-literal,
+.hljs-symbol,
+.hljs-bullet,
+.hljs-attribute {
+  color: #0086b3;
+}
+
+.hljs-section,
+.hljs-name {
+  color: #63a35c;
+}
+
+.hljs-tag {
+  color: #333333;
+}
+
+.hljs-title,
+.hljs-attr,
+.hljs-selector-id,
+.hljs-selector-class,
+.hljs-selector-attr,
+.hljs-selector-pseudo {
+  color: #795da3;
+}
+
+.hljs-addition {
+  color: #55a532;
+  background-color: #eaffea;
+}
+
+.hljs-deletion {
+  color: #bd2c00;
+  background-color: #ffecec;
+}
+
+.hljs-link {
+  text-decoration: underline;
+}
+
+/* 代码高亮 */
+/* PrismJS 1.15.0
+https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
+/**
+ * prism.js default theme for JavaScript, CSS and HTML
+ * Based on dabblet (http://dabblet.com)
+ * @author Lea Verou
+ */
+code[class*='language-'],
+pre[class*='language-'] {
+  color: black;
+  background: none;
+  text-shadow: 0 1px white;
+  font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+  text-align: left;
+  white-space: pre;
+  word-spacing: normal;
+  word-break: normal;
+  word-wrap: normal;
+  line-height: 1.5;
+
+  -moz-tab-size: 4;
+  -o-tab-size: 4;
+  tab-size: 4;
+
+  -webkit-hyphens: none;
+  -moz-hyphens: none;
+  -ms-hyphens: none;
+  hyphens: none;
+}
+
+pre[class*='language-']::-moz-selection,
+pre[class*='language-'] ::-moz-selection,
+code[class*='language-']::-moz-selection,
+code[class*='language-'] ::-moz-selection {
+  text-shadow: none;
+  background: #b3d4fc;
+}
+
+pre[class*='language-']::selection,
+pre[class*='language-'] ::selection,
+code[class*='language-']::selection,
+code[class*='language-'] ::selection {
+  text-shadow: none;
+  background: #b3d4fc;
+}
+
+@media print {
+  code[class*='language-'],
+  pre[class*='language-'] {
+    text-shadow: none;
+  }
+}
+
+/* Code blocks */
+pre[class*='language-'] {
+  padding: 1em;
+  margin: 0.5em 0;
+  overflow: auto;
+}
+
+:not(pre) > code[class*='language-'],
+pre[class*='language-'] {
+  background: #f5f2f0;
+}
+
+/* Inline code */
+:not(pre) > code[class*='language-'] {
+  padding: 0.1em;
+  border-radius: 0.3em;
+  white-space: normal;
+}
+
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+  color: slategray;
+}
+
+.token.punctuation {
+  color: #999;
+}
+
+.namespace {
+  opacity: 0.7;
+}
+
+.token.property,
+.token.tag,
+.token.boolean,
+.token.number,
+.token.constant,
+.token.symbol,
+.token.deleted {
+  color: #905;
+}
+
+.token.selector,
+.token.attr-name,
+.token.string,
+.token.char,
+.token.builtin,
+.token.inserted {
+  color: #690;
+}
+
+.token.operator,
+.token.entity,
+.token.url,
+.language-css .token.string,
+.style .token.string {
+  color: #9a6e3a;
+  background: hsla(0, 0%, 100%, 0.5);
+}
+
+.token.atrule,
+.token.attr-value,
+.token.keyword {
+  color: #07a;
+}
+
+.token.function,
+.token.class-name {
+  color: #dd4a68;
+}
+
+.token.regex,
+.token.important,
+.token.variable {
+  color: #e90;
+}
+
+.token.important,
+.token.bold {
+  font-weight: bold;
+}
+
+.token.italic {
+  font-style: italic;
+}
+
+.token.entity {
+  cursor: help;
+}

+ 37 - 0
src/assets/font/iconfont.css

@@ -0,0 +1,37 @@
+@font-face {
+  font-family: 'iconfont'; /* Project id 3210904 */
+  src: url('iconfont.woff2?t=1646452970429') format('woff2'), url('iconfont.woff?t=1646452970429') format('woff'),
+    url('iconfont.ttf?t=1646452970429') format('truetype');
+}
+
+.iconfont {
+  font-family: 'iconfont' !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-custom-ok:before {
+  content: '\e631';
+}
+
+.icon-github-fill:before {
+  content: '\e885';
+}
+
+.icon-l-search:before {
+  content: '\e79e';
+}
+
+.icon-home:before {
+  content: '\e603';
+}
+
+.icon-member:before {
+  content: '\e602';
+}
+
+.icon-list:before {
+  content: '\e601';
+}

+ 51 - 0
src/assets/font/iconfont.json

@@ -0,0 +1,51 @@
+{
+  "id": "3210904",
+  "name": "fast-vue3",
+  "font_family": "iconfont",
+  "css_prefix_text": "icon-",
+  "description": "",
+  "glyphs": [
+    {
+      "icon_id": "517495",
+      "name": "ok",
+      "font_class": "custom-ok",
+      "unicode": "e631",
+      "unicode_decimal": 58929
+    },
+    {
+      "icon_id": "4937000",
+      "name": "github-fill",
+      "font_class": "github-fill",
+      "unicode": "e885",
+      "unicode_decimal": 59525
+    },
+    {
+      "icon_id": "12932129",
+      "name": "l-search",
+      "font_class": "l-search",
+      "unicode": "e79e",
+      "unicode_decimal": 59294
+    },
+    {
+      "icon_id": "109751",
+      "name": "home",
+      "font_class": "home",
+      "unicode": "e603",
+      "unicode_decimal": 58883
+    },
+    {
+      "icon_id": "663138",
+      "name": "member",
+      "font_class": "member",
+      "unicode": "e602",
+      "unicode_decimal": 58882
+    },
+    {
+      "icon_id": "21513638",
+      "name": "list",
+      "font_class": "list",
+      "unicode": "e601",
+      "unicode_decimal": 58881
+    }
+  ]
+}

BIN
src/assets/font/iconfont.ttf


BIN
src/assets/font/iconfont.woff


BIN
src/assets/font/iconfont.woff2


BIN
src/assets/image/case1.jpg


BIN
src/assets/image/tips/kj.png


BIN
src/assets/image/tips/kk.png


BIN
src/assets/image/tips/l-sh.png


BIN
src/assets/image/tips/ss.png


BIN
src/assets/image/tips/xf.png


BIN
src/assets/logo.png


+ 53 - 0
src/components/TitleBar/index.vue

@@ -0,0 +1,53 @@
+<template>
+  <div class="main-page">
+    <router-view v-slot="{ Component }">
+      <keep-alive>
+        <component :is="Component" />
+      </keep-alive>
+    </router-view>
+  </div>
+  <nut-tabbar unactive-color="#364636" active-color="#1989fa" @tab-switch="tabSwitch">
+    <nut-tabbar-item :tab-title="$t('tabbar.home')" font-class-name="iconfont" class-prefix="icon" icon="home" />
+    <nut-tabbar-item :tab-title="$t('tabbar.list')" font-class-name="iconfont" class-prefix="icon" icon="list" />
+    <nut-tabbar-item :tab-title="$t('tabbar.member')" font-class-name="iconfont" class-prefix="icon" icon="member" />
+  </nut-tabbar>
+</template>
+
+<script lang="ts" setup>
+  import { useRouter } from 'vue-router';
+
+  const router = useRouter();
+
+  const tabSwitch = (item, index) => {
+    console.log(item, index);
+    switch (index) {
+      case 0:
+        router.push('/home');
+        break;
+      case 1:
+        router.push('/list');
+        break;
+      case 2:
+        router.push('/member');
+        break;
+    }
+  };
+</script>
+
+<style scoped lang="scss">
+  .main-page {
+    height: calc(100vh - 50px);
+    overflow-y: scroll;
+    overflow-x: hidden;
+  }
+
+  .tabbar {
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    height: 50px;
+    border: none;
+    box-shadow: 0 0 20px -5px #9a9a9a;
+  }
+</style>

+ 27 - 0
src/i18n/index.ts

@@ -0,0 +1,27 @@
+import { createI18n } from 'vue-i18n';
+
+export function loadLang() {
+  const modules: Record<string, any> = import.meta.glob('./lang/*.ts', { eager: true });
+  const langs: Record<string, any> = {};
+
+  for (const path in modules) {
+    const name = path.replace(/(\.\/lang\/|\.ts)/g, '');
+    langs[name] = modules[path].lang;
+  }
+  return langs;
+}
+
+export const i18n = createI18n({
+  // globalInjection: true,
+  legacy: false,
+  locale: 'zh-cn',
+  fallbackLocale: 'zh-cn',
+  messages: loadLang(),
+});
+
+export function setLang(locale?: string) {
+  if (locale) {
+    localStorage.setItem('lang', locale);
+  }
+  i18n.global.locale.value = locale || localStorage.getItem('lang') || '';
+}

+ 20 - 0
src/i18n/lang/en-us.ts

@@ -0,0 +1,20 @@
+import { langType } from './lang-base';
+
+export const lang: langType = {
+  title: 'VUE H5 development template',
+  tabbar: {
+    home: 'Home',
+    list: 'List',
+    member: 'Member',
+    demo: 'demo',
+  },
+  language: {
+    en: 'English',
+    zh: 'Chinese',
+  },
+  introduction: 'A rapid development vue3 of mobile terminal template',
+  home: {
+    support: 'support',
+    cssMultiLanguage: 'CSS picture multi-language',
+  },
+};

+ 18 - 0
src/i18n/lang/lang-base.ts

@@ -0,0 +1,18 @@
+export type langType = {
+  title: string;
+  tabbar: {
+    home: string;
+    list: string;
+    member: string;
+    demo: string;
+  };
+  language: {
+    en: string;
+    zh: string;
+  };
+  introduction: string;
+  home: {
+    support: string;
+    cssMultiLanguage: string;
+  };
+};

+ 20 - 0
src/i18n/lang/zh-cn.ts

@@ -0,0 +1,20 @@
+import { langType } from './lang-base';
+
+export const lang: langType = {
+  title: 'VUE H5开发模板',
+  tabbar: {
+    home: '首页',
+    list: '列表',
+    member: '我的',
+    demo: '示例',
+  },
+  language: {
+    en: '英文',
+    zh: '中文',
+  },
+  introduction: '一个快速开发vue3的移动端模板',
+  home: {
+    support: '支持',
+    cssMultiLanguage: 'css图片多语言',
+  },
+};

+ 69 - 0
src/layout/basic/index.vue

@@ -0,0 +1,69 @@
+<template>
+  <!-- <nut-navbar :left-show="false" :title="$t($route.meta.title)" /> -->
+  <div class="main-page">
+    <!-- <keep-alive>
+      <router-view v-if="$route.meta.keepAlive" :key="$route.path" />
+    </keep-alive>
+    <router-view v-if="!$route.meta.keepAlive" :key="$route.path" /> -->
+    <RouterView v-slot="{ Component }" v-if="$route.meta.keepAlive">
+      <keep-alive>
+        <component :is="Component" :key="$route.path" />
+      </keep-alive>
+    </RouterView>
+    <RouterView v-if="!$route.meta.keepAlive" :key="$route.path" />
+  </div>
+  <!-- <nut-tabbar unactive-color="#364636" active-color="#1989fa" @tab-switch="tabSwitch" bottom v-model:visible="activeTab">
+    <nut-tabbar-item v-for="item in tabItem" :key="item.key" :tab-title="$t(`tabbar.${item.key}`)" :icon="item.icon" />
+  </nut-tabbar> -->
+</template>
+
+<script lang="ts" setup name="BasicLayoutPage">
+  import { useRouter } from 'vue-router';
+
+  // const tabItem = [
+  //   { key: 'home', icon: 'home' },
+  //   { key: 'list', icon: 'horizontal' },
+  //   { key: 'member', icon: 'my' },
+  //   { key: 'demo', icon: 'location' },
+  // ];
+
+  const router = useRouter();
+
+  const activeTab = ref(0);
+
+  onMounted(() => {
+    // activeTab.value = tabItem.findIndex((item) => item.key === router.currentRoute.value.path.replace('/', ''));
+  });
+
+  const tabSwitch = (_item, index) => {
+    switch (index) {
+      case 0:
+        router.push('/home');
+
+        break;
+      case 1:
+        router.push('/list');
+        break;
+      case 2:
+        router.push('/member');
+        break;
+      case 3:
+        router.push('/demo');
+    }
+    activeTab.value = index;
+  };
+</script>
+
+<style scoped lang="scss">
+  .nut-navbar {
+    margin-bottom: 0;
+  }
+
+  .main-page {
+    box-sizing: border-box;
+    // padding: 40px;
+    // height: calc(100vh - 200px);
+    overflow-y: scroll;
+    overflow-x: hidden;
+  }
+</style>

+ 30 - 0
src/main.ts

@@ -0,0 +1,30 @@
+import { createApp } from 'vue';
+import App from './App.vue';
+// import { nutUiComponents } from './plugins/nutUI';
+import { i18n } from '/@/i18n';
+import router from '/@/router';
+
+// ① 引入createPinia方法从pinia
+import { createPinia } from 'pinia'
+// ② 拿到pinia实例
+const pinia = createPinia()
+// 1 引入数据持久化插件
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
+// 2 pinia使用数据持久化插件
+pinia.use(piniaPluginPersistedstate)
+
+import './assets/font/iconfont.css';
+import './assets/app.css';
+
+const app = createApp(App);
+
+// 路由
+app.use(router);
+
+// 国际化
+app.use(i18n);
+
+// 状态管理
+app.use(pinia);
+
+app.mount('#app');

+ 71 - 0
src/plugins/nutUI.ts

@@ -0,0 +1,71 @@
+// nutui按需加载
+
+import {
+  Button,
+  Cell,
+  CellGroup,
+  Icon,
+  Input,
+  Tabbar,
+  TabbarItem,
+  Toast,
+  ShortPassword,
+  Price,
+  Layout,
+  Rate,
+  Popup,
+  Calendar,
+  Video,
+  NoticeBar,
+  NumberKeyboard,
+  CountDown,
+  Tag,
+  Badge,
+  SearchBar,
+  Avatar,
+  Menu,
+  MenuItem,
+  Popover,
+  Pagination,
+  Form,
+  FormItem,
+  Navbar,
+  Card,
+  Grid,
+  GridItem,
+} from '@nutui/nutui';
+
+export const nutUiComponents = [
+  Button,
+  Cell,
+  CellGroup,
+  Form,
+  FormItem,
+  Icon,
+  Input,
+  Tabbar,
+  TabbarItem,
+  Toast,
+  ShortPassword,
+  Price,
+  Layout,
+  Rate,
+  Popup,
+  Calendar,
+  Video,
+  NoticeBar,
+  NumberKeyboard,
+  CountDown,
+  Tag,
+  Badge,
+  SearchBar,
+  Avatar,
+  Menu,
+  MenuItem,
+  Popover,
+  Pagination,
+  Navbar,
+  Card,
+  Grid,
+  GridItem,
+];

+ 13 - 0
src/router/index.ts

@@ -0,0 +1,13 @@
+import { createRouter, createWebHistory, Router } from 'vue-router';
+import routes from './routes';
+
+const router: Router = createRouter({
+  history: createWebHistory('/'),
+  routes: routes,
+});
+
+router.beforeEach(async (_to, _from, next) => {
+  next();
+});
+
+export default router;

+ 99 - 0
src/router/routes.ts

@@ -0,0 +1,99 @@
+const routes = [
+  {
+    path: '/',
+    redirect: '/home',
+    component: () => import('/@/layout/basic/index.vue'),
+    children: [
+      {
+        path: 'home',
+        component: () => import('/@/views/home/index.vue'),
+        meta: {
+          title: 'tabbar.home',
+          keepAlive: true,
+        },
+      },
+      {
+        path: 'detail/:id',
+        component: () => import('/@/views/detail/index.vue'),
+        meta: {
+          title: ' .list',
+          keepAlive: true,
+        },
+      },
+      {
+        path: 'evaluate/:id',
+        component: () => import('/@/views/detail/evaluate.vue'),
+        meta: {
+          title: '评价',
+          keepAlive: true,
+        },
+      },
+      {
+        path: 'payment/:id',
+        component: () => import('/@/views/detail/payment.vue'),
+        meta: {
+          title: '支付',
+          keepAlive: true,
+        },
+      },
+      {
+        path: 'repair/:id',
+        component: () => import('/@/views/detail/repair.vue'),
+        meta: {
+          title: '维修',
+          keepAlive: true,
+        },
+      },
+      {
+        path: 'evaluate/:id',
+        component: () => import('/@/views/detail/evaluate.vue'),
+        meta: {
+          title: '评价',
+          keepAlive: true,
+        },
+      },
+      {
+        path: 'invoice/:id',
+        component: () => import('/@/views/detail/invoice.vue'),
+        meta: {
+          title: '申请开票',
+          keepAlive: true,
+        },
+      },{
+        path: 'list',
+        component: () => import('/@/views/list/index.vue'),
+        meta: {
+          title: 'tabbar.list',
+          keepAlive: true,
+        },
+      },
+      {
+        path: 'member',
+        component: () => import('/@/views/member/index.vue'),
+        meta: {
+          title: 'tabbar.member',
+          keepAlive: true,
+        },
+      },
+      {
+        path: 'demo',
+        component: () => import('/@/views/demo/index.vue'),
+        meta: {
+          title: 'tabbar.demo',
+          keepAlive: true,
+        },
+      },
+    ],
+  },
+  {
+    name: 'login',
+    path: '/login',
+    component: () => import('/@/views/login/index.vue'),
+    meta: {
+      title: '',
+      keepAlive: true,
+    },
+  },
+];
+
+export default routes;

+ 16 - 0
src/store/index.ts

@@ -0,0 +1,16 @@
+// import { createPinia } from 'pinia';
+// import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
+
+// const store = createPinia();
+// store.use(piniaPluginPersistedstate);
+
+// export default store;
+import { useUserStore } from "./modules/user";
+import { useHomeStore } from "./modules/home"
+ 
+export default function useStore() {
+  return {
+    user: useUserStore(),
+    home: useHomeStore(),
+  };
+}

+ 57 - 0
src/store/modules/home.ts

@@ -0,0 +1,57 @@
+// 每个状态管理文件都要引入此方法
+import { defineStore } from 'pinia'
+//引入接口
+// import { httpPost } from '../request/api'
+ 
+// 官方建议取名遵从 useXXXStore 形式
+// 'home' 为当前store的唯一标识 类似ID 
+// 取名建议与文件名称一致 便于记忆和管理
+// pinia舍弃了冗长的mutations属性 
+// 以下是pinia的一种写法 因与vuex相似 便于学习和记忆
+export const useHomeStore = defineStore('home',{
+    state:()=>{
+        return{
+            num:0,
+            token:''
+        }
+    },
+    //state也可写成这样
+    // state:()=>({
+    //     num:0
+    // }),
+    actions:{
+        changeNum(){
+        //这里可以使用this去拿到state里定义的变量 下面同理
+            this.num ++
+        },
+        changeToken(){
+        // ts 实在学的不咋地 这里就先any了
+        // httpPost().then((res:any)=>{
+        //     this.token = res.data.data.token
+        //     })
+        }
+    },
+    getters:{
+        // 这里取名不可与state里的变量一致 所以取名getNum
+        //简写
+        getNum:state=>state.num,
+        //全写
+        // getNum:(state)=>{
+        //     return state.num
+        // }
+        getToken:state=>state.token,
+    },
+    //数据持久化配置 这里是当前所有变量都持久化
+    // persist:true
+     
+    //按需配置数据持久化 这里指定变量num保持持久化
+    persist:{
+        //默认名称为当前store唯一标识 这里即home
+        key:'storeHomeNum',
+        //默认localStorage 本地储存 
+        //这里建议临时储存sessionStorage 也可写成window.sessionStorage
+        storage:sessionStorage,
+        //默认当前store里的所有变量都持久化
+        paths:['num']
+    }
+})

+ 45 - 0
src/store/modules/user.ts

@@ -0,0 +1,45 @@
+import { loginPassword } from '/@/api';
+import { useCookies } from '@vueuse/integrations/useCookies';
+import { defineStore } from 'pinia';
+
+const { VITE_TOKEN_KEY } = import.meta.env;
+const token = useCookies().get(VITE_TOKEN_KEY as string);
+
+interface StoreUser {
+  token: string;
+  info: Record<any, any>;
+}
+
+export const useUserStore = defineStore('user',{
+  id: 'user',
+  state: (): StoreUser => ({
+    token: token,
+    info: {
+      name:'test',
+    },
+  }),
+  getters: {
+    getUserInfo(): any {
+      return this.info || {};
+    },
+  },
+  actions: {
+    setInfo(info: any) {
+      this.info = info ? info : '';
+    },
+    login() {
+      return new Promise((resolve) => {
+        const { execute } = loginPassword();
+        execute().then((res) => {
+          this.setInfo(res);
+          resolve(res);
+        });
+      });
+    },
+  },
+  persist: {
+    key: 'token',
+    storage: localStorage,
+    paths: ['token'],
+  },
+});

+ 3 - 0
src/styles/index.scss

@@ -0,0 +1,3 @@
+.abc {
+  width: 10px;
+}

+ 23 - 0
src/styles/mixin.scss

@@ -0,0 +1,23 @@
+@mixin main-lang-bg($width, $height, $preUrl, $posUrl) {
+  width: $width;
+  height: $height;
+  background-repeat: no-repeat;
+  background-size: 100% 100%;
+  @include loop-lang-bg($preUrl, $posUrl);
+}
+// 背景图多语言
+@mixin loop-lang-bg($preUrl, $posUrl) {
+  $list: zh-cn, en-us;
+  @each $i in $list {
+    &.#{$i} {
+      background-image: url('#{$preUrl}/#{$i}/#{$posUrl}');
+    }
+  }
+}
+
+@mixin center {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}

+ 16 - 0
src/utils/index.ts

@@ -0,0 +1,16 @@
+export function typeCheck(param: any) {
+  return Object.prototype.toString.call(param);
+}
+
+/**
+ * 批量修改stage
+ */
+export function mutateState(state: Record<string, any>, payload: Record<string, any>) {
+  if (typeCheck(state) === '[object Object]' && typeCheck(payload) === '[object Object]') {
+    for (const key in payload) {
+      state[key] = payload[key];
+    }
+  } else {
+    console.error('expected plain Object');
+  }
+}

+ 72 - 0
src/utils/useAxiosApi.ts

@@ -0,0 +1,72 @@
+import { useAxios } from '@vueuse/integrations/useAxios';
+import axios, { AxiosRequestConfig } from 'axios';
+import { showToast } from 'vant/lib/toast';
+
+// create an axios instance
+const instance = axios.create({
+  withCredentials: false,
+  timeout: 5000,
+});
+
+// request interceptor
+instance.interceptors.request.use(
+  (config) => {
+    // do something before request is sent
+    // const token = store.state.user.token;
+
+    // if (token) {
+    //   // let each request carry token
+    //   config.headers = {
+    //     ...config.headers,
+    //     Authorization: `Bearer ${token}`
+    //   };
+    // }
+    return config;
+  },
+  (error) => {
+    // do something with request error
+    console.log(error); // for debug
+    return Promise.reject(error);
+  },
+);
+
+// response interceptor
+instance.interceptors.response.use(
+  /**
+   * If you want to get http information such as headers or status
+   * Please return  response => response
+   */
+
+  /**
+   * Determine the request status by custom code
+   * Here is just an example
+   * You can also judge the status by HTTP Status Code
+   */
+  (response) => {
+    const res = response.data;
+    // if the custom code is not 200, it is judged as an error.
+    if (res.code !== 200) {
+      showToast(res.msg);
+      // 412: Token expired;
+      if (res.code === 412) {
+        // store.dispatch('user/userLogout');
+      }
+      return Promise.reject(res.msg || 'Error');
+    } else {
+      return res;
+    }
+  },
+  (error) => {
+    console.log('err' + error);
+    showToast(error.message);
+    return Promise.reject(error.message);
+  },
+);
+
+/**
+ * reactive useFetchApi
+ */
+
+export default function useAxiosApi(url: string, config: AxiosRequestConfig) {
+  return useAxios(url, config, instance);
+}

+ 41 - 0
src/utils/useFetchApi.ts

@@ -0,0 +1,41 @@
+import { createFetch } from '@vueuse/core';
+import { showNotify } from 'vant';
+
+const useFetchApi = createFetch({
+  baseUrl: '',
+  options: {
+    async beforeFetch({ options }) {
+      const myToken = 'token';
+      options.headers = {
+        ...options.headers,
+        Authorization: `Bearer ${myToken}`,
+      };
+      return { options };
+    },
+    afterFetch(ctx) {
+      console.log(ctx);
+      const { data, response } = ctx;
+      if (response.status >= 200 && response.status < 300) {
+        try {
+          console.log(response);
+          const jsonObj = data;
+          if (jsonObj.code != 200) {
+            showNotify({ type: 'danger', message: jsonObj.message || 'Error' });
+          }
+
+          ctx.data = jsonObj.data;
+        } catch (error) {
+          console.error(error);
+          ctx.data = null;
+        }
+      } else {
+        showNotify({ type: 'danger', message: response.statusText || 'Error' });
+        ctx.data = null;
+      }
+
+      return ctx;
+    },
+  },
+});
+
+export default useFetchApi;

+ 63 - 0
src/views/demo/index.vue

@@ -0,0 +1,63 @@
+<template>
+  <section>
+    <span class="title">varlet</span>
+    <var-space :size="[10, 10]">
+      <var-button>demo</var-button><var-button type="primary">主要按钮</var-button>
+      <var-button type="info">信息按钮</var-button>
+      <var-button type="success">成功按钮</var-button>
+      <var-button type="warning">警告按钮</var-button>
+      <var-button type="danger">危险按钮</var-button>
+    </var-space>
+  </section>
+  <section>
+    <span class="title">vant</span>
+    <div class="demo">
+      <van-button type="primary">主要按钮</van-button>
+      <van-button type="success">成功按钮</van-button>
+      <van-button type="default">默认按钮</van-button>
+      <van-button type="warning">警告按钮</van-button>
+      <van-button type="danger">危险按钮</van-button>
+    </div>
+  </section>
+  <section>
+    <span class="title">nutUI</span>
+    <div class="demo">
+      <nut-button type="primary">主要按钮</nut-button>
+      <nut-button type="info">信息按钮</nut-button>
+      <nut-button type="default">默认按钮</nut-button>
+      <nut-button type="danger">危险按钮</nut-button>
+      <nut-button type="warning">警告按钮</nut-button>
+      <nut-button type="success">成功按钮</nut-button>
+    </div>
+  </section>
+  <section>
+    <div :class="['btn-add', i18n.global.locale]"></div>
+  </section>
+</template>
+
+<script setup name="DemoPage">
+  import { i18n } from '/@/i18n';
+  // import { useI18n } from 'vue-i18n';
+  // const { locale } = useI18n();
+</script>
+
+<style lang="scss" scoped>
+  @import '../../styles/mixin.scss';
+  section {
+    .title {
+      margin-bottom: 40px;
+      display: inline-block;
+    }
+    &:nth-child(2) {
+      .title {
+        margin-top: 20px;
+      }
+    }
+  }
+  .demo {
+    > :nth-child(n) {
+      margin-right: 20px;
+      margin-bottom: 20px;
+    }
+  }
+</style>

+ 56 - 0
src/views/detail/evaluate.vue

@@ -0,0 +1,56 @@
+// 评价页面
+<template>
+  <div class="evaluate">
+    <div class="evaluate_top">
+      <van-field
+        v-model="evaluate.message"
+        rows="3"
+        autosize
+        :border="false"
+        type="textarea"
+        maxlength="500"
+        placeholder="请留下您的宝贵评价,便于我们持续改进~"
+        show-word-limit
+      />
+      <div class="star">
+        <span class="text">本次服务</span>
+        <van-rate v-model="evaluate.value" :size="25" color="#ffd21e" void-icon="star" void-color="#eee" />
+      </div>
+    </div>
+    <div class="but">
+      <van-button type="primary" block>块级元素</van-button>
+    </div>
+  </div>
+</template>
+  
+  <script lang="ts" setup name="detailPage">
+import { reactive } from 'vue';
+const evaluate = ref({
+  message: '',
+  value: 2.5,
+});
+</script>
+ <style lang="scss" >
+.evaluate {
+  min-height: 100vh;
+  background-color: #f5f5f5;
+  .evaluate_top{
+    background-color: #fff;
+    .star{
+        line-height: 25px;
+        padding: 11px 16px;
+        border-top: 1px solid #E7E7E7;
+        .text{
+            margin-right: 15px;
+            font-family: PingFang SC-Regular, PingFang SC;
+            font-weight: 400;
+            color: #333333;
+        }
+    }
+  }
+  .but{
+    padding: 25px 15px;
+  }
+}
+</style>
+  

+ 339 - 0
src/views/detail/index.vue

@@ -0,0 +1,339 @@
+// 评价页面
+<template>
+  <div class="page">
+    <div class="stepList item_content" :key="stepShow">
+      <div class="stepList_title">维修单号 2022101200001</div>
+      <transition-group name="van-fade">
+      <!-- <div v-show="stepShow">Slide Right</div> -->
+      <div class="border stepItem" v-for="(item, index) in 3" :key="index+stepShow" v-show="stepShow?true: index<1">
+        <div class="step">
+            <div class="step_title">维修完毕</div>
+            <div>前台取回 / 快递寄回  SF151315352892</div>
+            <div>检测结果: 镜头失焦</div>
+            <div>所需备件: 镜头x2、电池x1</div>
+            <div>机器外观: 外壳有轻微划痕</div>
+            <div class="imgList">
+              <img src="../../assets/image/case1.jpg" v-for="imgItem in 6" :key="imgItem" alt="">
+            </div>
+            <div class="step_time">10-14 12:30</div>
+            <div class="doct" :class="index == 0?'oneSpot':'Spot'"></div>
+        </div>
+      </div>
+    </transition-group>
+      <van-divider>
+        <div @click="handleShow">
+          <van-icon :name="stepShow?'arrow-down':'arrow-up'" />
+          {{stepShow?"展开":"收起"}}
+        </div>
+      </van-divider>
+    </div>
+    <div class="page_top item_content">
+      <div class="title">客户信息</div>
+      <div class="content">
+        <div class="cost_list" style="border-bottom: none">
+          <div class="item">
+            <span>公司名称</span>
+            <span>某某某某公司</span>
+          </div>
+          <div class="item">
+            <span>联系人</span>
+            <span>老王</span>
+          </div>
+          <div class="item">
+            <span>联系电话</span>
+            <span>110</span>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="page_top item_content">
+      <div class="title">故障信息</div>
+      <div class="content">
+        <div class="text">四维看看 FJ6OA4W89H 镜头拆坏、底盖脱落、</div>
+        <div class="imgList">
+          <img src="../../assets/image/case1.jpg" v-for="imgItem in 6" :key="imgItem" alt="">
+        </div>
+      </div>
+    </div>
+    <div class="page_custinfo item_content">
+      <div class="title">费用明细</div>
+      <div class="content">
+        <div class="cost_list">
+          <div class="item" v-for="item in repair.costList" :key="item.name">
+            <span>{{ item.name }}</span>
+            <span>¥{{ item.value }} x{{ item.count }}</span>
+          </div>
+        </div>
+        <div class="cell" style="border: none">
+          <span>维修费用</span>
+          <span class="colortext">¥1000.00</span>
+        </div>
+      </div>
+    </div>
+    <div class="page_top item_content">
+      <div class="title">运输信息</div>
+      <div class="content">
+        <div class="cost_list">
+          <div class="item">
+            <span>送修方式</span>
+            <span>快递寄送</span>
+          </div>
+          <div class="item">
+            <span>快递单号</span>
+            <span>SF473827489237</span>
+          </div>
+          <div class="item">
+            <span>送修方式</span>
+            <span>快递寄送</span>
+          </div>
+          <div class="item">
+            <span>取回方式</span>
+            <span>快递寄送</span>
+          </div>
+          <div class="item">
+            <span>快递单号</span>
+            <span>SF1513153523892</span>
+          </div>
+          <div class="item">
+            <span>收件人</span>
+            <span>老王</span>
+          </div>
+          <div class="item">
+            <span>收件人电话</span>
+            <span>138 2222 5555</span>
+          </div>
+          <div class="item">
+            <span>收件地址</span>
+            <span>广东省广州市越秀区某大厦某某公司</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="page_top item_content">
+      <div class="title">评价信息</div>
+      <div class="content">
+        <div class="cost_list" style="border-bottom: none">
+          <div class="item">
+            <span>内容</span>
+            <span>整体态度和效率都不错</span>
+          </div>
+          <div class="item">
+            <span>评分</span>
+            <span>4.5分</span>
+          </div>
+        </div>
+      </div>
+    </div>
+    <!-- <div class="but">
+      <span class="tips">确认维修后,将直接开始维修。维修费用待维修完成后支付</span>
+      <van-button type="primary" color="#00B3EC" block>块级元素</van-button>
+      <van-button type="primary" color="#00B3EC" plain block>块级元素</van-button>
+    </div> -->
+  </div>
+</template>
+  
+  <script lang="ts" setup name="detailPage">
+import { reactive } from 'vue';
+const stepShow = ref(true)
+const repair = reactive({
+  info: [
+    {
+      name: '维修单号',
+      value: '2022101200001',
+    },
+    {
+      name: '设备信息',
+      value: '四维看看 FJ6OA4W89H',
+    },
+    {
+      name: '检测结果',
+      value: '3号镜头失焦',
+    },
+  ],
+  evaluate:{
+    message:'haisuanbucuo',
+    value:5,
+  },
+  costList: [
+    {
+      name: '镜头模组',
+      value: 700.0,
+      count: 1,
+    },
+    {
+      name: '镜头模组',
+      value: 700.0,
+      count: 1,
+    },
+    {
+      name: '镜头模组',
+      value: 700.0,
+      count: 1,
+    },
+  ],
+});
+const handleShow = () => {
+  stepShow.value = !stepShow.value
+}
+</script>
+ <style lang="scss" >
+.page {
+  min-height: 100vh;
+  background-color: #f5f5f5;
+  overflow: hidden;
+  .item_content {
+    background-color: #fff;
+    border-radius: 4px 4px 4px 4px;
+    margin: 12px;
+    .title {
+      font-size: 14px;
+      font-family: PingFang SC-Medium, PingFang SC;
+      font-weight: 500;
+      color: #000000;
+      padding: 15px 0;
+      margin:  0 15px;
+      border-bottom: 1px solid #e7e7e7;
+    }
+    .content{
+      padding: 24px 15px;
+    }
+    .item {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      line-height: 30px;
+      // margin-bottom: 8px;
+    }
+    
+    .imgList{
+      img{
+        height: 64px;
+        width: 64px;
+        padding: 0 8px 0 0;
+      }
+    }
+  }
+  .stepList{
+    padding: 24px 16px;
+    &_title{
+      font-size: 14px;
+      font-family: PingFang SC-Medium, PingFang SC;
+      font-weight: 500;
+      color: #000000;
+      padding-bottom: 15px;
+      margin-bottom: 24px;
+      border-bottom: 1px solid #F5F5F5;
+    }
+    .stepItem{
+      transition: all 3s 2s linear;
+    }
+    .step{
+      &:last-child{
+        // border: none;
+      }
+      padding: 0 15px;
+      border-left: 1px solid #F5F5F5;
+      &_title{
+        font-size: 14px;
+        line-height: 22px;
+      }
+      &_time{
+        font-size: 10px;
+        font-family: PingFang SC-Regular, PingFang SC;
+        font-weight: 400;
+        color: #999999;
+        line-height: 14px;
+        padding-bottom: 24px;
+      }
+    font-size: 12px;
+    font-family: PingFang SC-Regular, PingFang SC;
+    font-weight: 400;
+    color: #333333;
+    line-height: 30px;
+    position: relative;
+    .doct{
+      display: inline-block;
+      position: absolute;
+      left: 0;
+      top: 0;
+      width: 8px;
+      height: 8px;
+      border-radius: 50%;
+      transform: translat(-50%,-50%);
+    }
+    .oneSpot{
+      border: 4px solid #00B3EC;
+      left: -8px;
+      top: 4px;
+      background: #fff;
+    }
+    .Spot{
+      background: #CCCCCC;
+      left: -4px;
+      top: 8px;
+    }
+
+    }
+  }
+  .colortext {
+    color: #e34d59;
+  }
+  .cell {
+    border-top: 1px solid #e7e7e7;
+    &:first-child {
+      border-top: none;
+    }
+    // height: 48px;
+    margin-top: 12px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-size: 14px;
+    font-family: PingFang SC-Regular, PingFang SC;
+    font-weight: 400;
+    color: #333333;
+    line-height: 22px;
+  }
+  .page_custinfo {
+    .cost_list {
+      padding-bottom: 8px;
+      // margin: 0 15px;
+      // padding-top: 24px;
+      border: {
+        // top: 1px solid #e7e7e7;
+        bottom: 1px solid #e7e7e7;
+      }
+    }
+  }
+  .page_top {
+    .cost_list {
+      border: {
+        // bottom: 1px solid #e7e7e7;
+      }
+    }
+    .text{
+      line-height: 30px;
+    }
+  }
+  .repairInfo {
+    background-color: #fff;
+    margin-bottom: 12px;
+    border-radius: 4px 4px 4px 4px;
+  }
+  .but {
+    padding: 25px 15px;
+    .tips {
+      font-size: 12px;
+      font-family: PingFang SC-Regular, PingFang SC;
+      font-weight: 400;
+      color: #999999;
+      margin-bottom: 10px;
+    }
+    button {
+      margin-top: 16px;
+    }
+  }
+}
+</style>
+  

+ 136 - 0
src/views/detail/invoice.vue

@@ -0,0 +1,136 @@
+<template>
+  <div class="evaluate">
+    <div class="evaluate_top">
+      <div class="cell border">
+        <span>单位</span>
+        <span>四维时代</span>
+      </div>
+      <div class="cell">
+        <span>开票金额</span>
+        <span class="colortext">¥1000.00</span>
+      </div>
+    </div>
+    <div class="form invoiceForm">
+      <van-form @submit="onSubmit" style="margin-top: 10px; background: #fff">
+        <van-cell-group inset>
+          <van-field
+            input-align="right"
+            v-model="formData.nameL"
+            name="用户名"
+            label="公司名称"
+            placeholder="请填写公司名称"
+            :rules="[{ required: true, message: '请填写公司名称' }]"
+          />
+          <van-field name="radio" label="单选框" input-align="right">
+          <template #input>
+            <van-radio-group v-model="formData.checkeds" direction="horizontal">
+              <van-radio name="1">前台送修</van-radio>
+              <van-radio name="2">快递寄送</van-radio>
+            </van-radio-group>
+          </template>
+        </van-field>
+          <van-field
+            v-model="formData.nameL"
+            input-align="right"
+            name="发票抬头"
+            label="发票抬头"
+            placeholder="请填写发票抬头"
+            :rules="[{ required: true, message: '请填写发票抬头' }]"
+          />
+          <van-field
+            v-model="formData.nameL"
+            input-align="right"
+            name="税号"
+            label="税号"
+            placeholder="请填写税号"
+            :rules="[{ required: true, message: '请填写税号' }]"
+          />
+          <van-field
+            v-model="formData.nameL"
+            input-align="right"
+            name="开户银行"
+            label="开户银行"
+            placeholder="请填写开户银行"
+          />
+          <van-field
+            v-model="formData.nameL"
+            input-align="right"
+            name="银行账户"
+            label="银行账户"
+            placeholder="请填写银行账户"
+          />
+          <van-field
+            v-model="formData.nameL"
+            input-align="right"
+            name="企业地址"
+            label="企业地址"
+            placeholder="请填写企业地址"
+          />
+          <van-field
+            v-model="formData.nameL"
+            input-align="right"
+            name="企业电话"
+            label="企业电话"
+            placeholder="请填写企业电话"
+          />
+        </van-cell-group>
+      </van-form>
+      <div class="but">
+          <van-button type="primary" block native-type="submit">提交</van-button>
+        </div>
+    </div>
+  </div>
+</template>
+  
+  <script lang="ts" setup name="detailPage">
+import { reactive } from 'vue';
+const formData = reactive({
+  nameL: '',
+  lxr: '',
+  lxdh: '',
+  sncode: '',
+  gzms: '',
+  message: '',
+  file: [],
+  checked: '1',
+  checkeds: '1',
+});
+</script>
+ <style lang="scss" >
+ .van-cell-group{
+  margin: 0 !important;
+ }
+.evaluate {
+  min-height: 100vh;
+  background-color: #f5f5f5;
+  .invoiceForm{
+    .van-field__error-message{
+      text-align: right;
+    }
+  }
+  .evaluate_top {
+    background-color: #fff;
+    .border {
+      border-top: 1px solid #e7e7e7;
+    }
+    .colortext {
+      color: #e34d59;
+    }
+    .cell {
+      height: 48px;
+      padding: 0 15px;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      font-size: 14px;
+      font-family: PingFang SC-Regular, PingFang SC;
+      font-weight: 400;
+      color: #333333;
+    }
+  }
+  .but {
+    padding: 25px 15px;
+  }
+}
+</style>
+  

+ 55 - 0
src/views/detail/payment.vue

@@ -0,0 +1,55 @@
+<template>
+  <div class="evaluate">
+    <div class="evaluate_top">
+        <div class="cell">
+            <span>维修费用</span>
+            <span class="colortext">¥1000.00</span>
+        </div>
+        <div class="cell border">
+            <span>单位</span>
+            <span>四维时代</span>
+        </div>
+    </div>
+    <div class="but">
+      <van-button type="primary" block>块级元素</van-button>
+    </div>
+  </div>
+</template>
+  
+  <script lang="ts" setup name="detailPage">
+import { reactive } from 'vue';
+const evaluate = ref({
+  message: '',
+  value: 2.5,
+});
+</script>
+ <style lang="scss" >
+.evaluate {
+  min-height: 100vh;
+  background-color: #f5f5f5;
+  .evaluate_top{
+    background-color: #fff;
+    .border{
+        border-top: 1px solid #E7E7E7;
+    }
+    .colortext{
+        color: #E34D59;
+    }
+    .cell{
+        height: 48px;
+        padding: 0 15px;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        font-size: 14px;
+        font-family: PingFang SC-Regular, PingFang SC;
+        font-weight: 400;
+        color: #333333;
+    }
+  }
+  .but{
+    padding: 25px 15px;
+  }
+}
+</style>
+  

+ 128 - 0
src/views/detail/repair.vue

@@ -0,0 +1,128 @@
+// 评价页面
+<template>
+  <div class="page">
+    <div class="repairInfo">
+        <div class="cell border" v-for="item in repair.info" :key="item.name">
+            <span>{{item.name}}</span>
+            <span>{{item.value}}</span>
+        </div>
+    </div>
+    <div class="page_top">
+        <div class="title">费用明细</div>
+        <div class="cost_list">
+            <div class="item"  v-for="item in repair.costList" :key="item.name">
+                <span>{{item.name}}</span>
+                <span>¥{{ item.value }} x{{ item.count }}</span>
+            </div>
+        </div>
+        <div class="cell" style="border:none">
+            <span>维修费用</span>
+            <span class="colortext">¥1000.00</span>
+        </div>
+    </div>
+    <div class="but">
+      <span class="tips">确认维修后,将直接开始维修。维修费用待维修完成后支付</span>
+      <van-button type="primary" color="#00B3EC" block>块级元素</van-button>
+      <van-button type="primary" color="#00B3EC" plain  block>块级元素</van-button>
+    </div>
+  </div>
+</template>
+  
+  <script lang="ts" setup name="detailPage">
+import { reactive } from 'vue';
+const repair = reactive({
+    info:[{
+        name:'维修单号',
+        value:'2022101200001',
+    },{
+        name:'设备信息',
+        value:'四维看看 FJ6OA4W89H',
+    },{
+        name:'检测结果',
+        value:'3号镜头失焦',
+    }],
+    costList:[{
+        name:'镜头模组',
+        value:700.00,
+        count:1,
+    },{
+        name:'镜头模组',
+        value:700.00,
+        count:1,
+    },{
+        name:'镜头模组',
+        value:700.00,
+        count:1,
+    }]
+});
+</script>
+ <style lang="scss" >
+.page {
+  min-height: 100vh;
+  background-color: #f5f5f5;
+  
+    .title{
+        font-size: 14px;
+        font-family: PingFang SC-Medium, PingFang SC;
+        font-weight: 500;
+        color: #000000;
+        padding: 15px;
+    }
+    .colortext{
+        color: #E34D59;
+    }
+    .cell{
+        border-top: 1px solid #E7E7E7; 
+        &:first-child{
+            border-top: none; 
+        }
+        height: 48px;
+        padding: 0 15px;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        font-size: 14px;
+        font-family: PingFang SC-Regular, PingFang SC;
+        font-weight: 400;
+        color: #333333;
+    }
+  .page_top {
+    background-color: #fff;
+    border-radius: 4px 4px 4px 4px;
+    .cost_list{
+        margin: 0 15px;
+        padding-top: 24px;
+        border: {
+            top:1px solid #E7E7E7; 
+            bottom:1px solid #E7E7E7; 
+        }
+        .item{
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            margin-bottom: 8px;
+        }
+    }
+
+  }
+  .repairInfo{
+    background-color: #fff;
+    margin-bottom: 12px;
+    border-radius: 4px 4px 4px 4px;
+  }
+  .but{
+    padding: 25px 15px;
+    .tips{
+        font-size: 12px;
+        font-family: PingFang SC-Regular, PingFang SC;
+        font-weight: 400;
+        color: #999999;
+        margin-bottom: 10px;
+    }
+    button{
+        margin-top: 16px;
+    }
+  }
+}
+</style>
+  

+ 93 - 0
src/views/home/index.vue

@@ -0,0 +1,93 @@
+<template>
+  <div class="page">
+    <van-tabs v-model:active="active" @click-tab="onClickTab">
+      <van-tab title="设备报修">
+        <div></div>
+        <SubmitPage/>
+      </van-tab>
+      <van-tab title="报修记录">
+        <List/>
+      </van-tab>
+    </van-tabs>
+  </div>
+</template>
+
+<script lang="ts" setup name="HomePage">
+import { computed } from 'vue';
+import { useUserStore } from '/@/store/modules/user';
+import { setLang } from '/@/i18n';
+import store from '/@/store';
+import SubmitPage from './submit.vue'
+import List from './list.vue'
+import { useI18n } from 'vue-i18n';
+const { locale } = useI18n();
+const active = ref(0);
+const userStore = useUserStore();
+const getUserInfo = computed(() => {
+  const { name = '' } = userStore.getUserInfo || {};
+  return name;
+});
+const formData = reactive({
+  nameL: '',
+  lxr: '',
+  lxdh: '',
+  sncode: '',
+  gzms: '',
+  message: '',
+  file: [],
+  checked: '1',
+  checkeds: '1',
+});
+const onSubmit = () => {
+  console.log('onSubmit');
+};
+const onClickTab = (title) => {
+  console.log('title',title)
+  console.log('store',userStore,getUserInfo)
+}
+const changeLang = (type) => {
+  setLang(type);
+};
+</script>
+<style lang="scss" scoped>
+.page{
+  background-color: #F5F5F5;
+}
+.header {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 0 20px;
+  font-size: 40px;
+  img {
+    width: 90px;
+    height: 90px;
+  }
+}
+
+.intro-header {
+  margin-top: 20px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 16px;
+}
+
+.supportList {
+  margin: 0 16px;
+
+  .nut-cell-group__title {
+    margin-top: 30px;
+  }
+  .nut-icon {
+    color: green;
+  }
+}
+
+.btn-wrap {
+  margin: 20px;
+}
+.btn-confirm {
+  @include main-lang-bg(302px, 82px, '/@/assets/button', 'confirm.png');
+}
+</style>

+ 110 - 0
src/views/home/list.vue

@@ -0,0 +1,110 @@
+<template>
+  <div class="list">
+    <van-list style="overflow: hidden;" v-model:loading="loading" v-model:error="error" error-text="请求失败,点击重新加载" @load="onLoad">
+      <!-- <van-cell v-for="item in list" :key="item" :title="item" /> -->
+      
+    <div class="item" v-for="item in 5" :key="item" @click="goRoute(`detail/${item}`)">
+      <div class="item_top padddiv">
+        <div class="orderSn">工单号: <span>2022101100001</span></div>
+        <div class="state">待接单</div>
+      </div>
+      <div class="content padddiv">
+        <div class="camera">
+          <img src="../../assets/image/tips/kj.png" />
+          <span>{{ myData.name }}</span>
+        </div>
+        <div class="camera_text">镜头摔坏、底盖脱落、
+           其它待检
+        </div>
+      </div>
+      <div class="item_buttom padddiv">
+        <div class="time">报修日期 2022-10-11</div>
+        <div class="butList">
+          <!-- <van-button @click.stop="goRoute(`payment/${item}`)" hairline size="small" type="primary" >支付</van-button> -->
+          <van-button  @click.stop="goRoute(`evaluate/${item}`)" plain hairline  size="small" >评价</van-button>
+          <van-button @click.stop="goRoute(`invoice/${item}`)"  plain hairline  size="small" >申请开票</van-button>
+        </div>
+      </div>
+    </div>
+    </van-list>
+  </div>
+</template>
+<script lang="ts" setup name="HomeList">
+const router = useRouter();
+import sw from '/@/assets/image/tips/l-sh.png';
+import ss from '/@/assets/image/tips/ss.png';
+import kk from '/@/assets/image/tips/kk.png';
+import kj from '/@/assets/image/tips/kj.png';
+import { reactive } from 'vue';
+const myData = reactive({
+  name: 'text',
+  imgObj: {
+    1: sw,
+    2: kk,
+    3: kj,
+    4: ss,
+  },
+});
+const list = ref([]);
+const error = ref(false);
+const loading = ref(false);
+const onLoad = () => {
+  //   fetchSomeThing().catch(() => {
+  //     error.value = true;
+  //   });
+};
+// const onDetail = (item) => {
+//     console.log('onDetail',item)
+//     router.push(`detail/${item}`);
+// }
+const goRoute = (path) => {
+    if(!path) return
+    router.push(path);
+
+}
+</script>
+<style lang="scss" scoped>
+.list {
+    background-color: #F5F5F5;
+  .item {
+    margin: 10px 0;
+    background-color: #fff;
+    .padddiv{
+        padding: 0 16px;
+    }
+    .item_top {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 13px 16px;
+      .orderSn{
+        font-size: 14px;
+        font-family: PingFang SC-Regular, PingFang SC;
+        font-weight: 400;
+        color: rgba(102,102,102,0.6);
+      }
+    }
+    .content {
+      .camera {
+        margin: 12px 0;
+      }
+      .camera_text{
+        margin-bottom: 20px;
+      }
+    }
+    .item_buttom {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      height: 60px;
+
+      .butList {
+        .van-button {
+          min-width: 72px;
+          margin-left: 15px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 188 - 0
src/views/home/submit.vue

@@ -0,0 +1,188 @@
+<template>
+  <van-form @submit="onSubmit" style="margin-top:10px;background:#fff">
+    <van-cell-group inset>
+      <van-field
+        v-model="formData.nameL"
+        label-align="top"
+        name="用户名"
+        label="公司名称"
+        placeholder="请填写公司名称"
+        :rules="[{ required: true, message: '请填写公司名称' }]"
+      />
+      <van-field
+        v-model="formData.nameL"
+        label-align="top"
+        name="联系人"
+        label="联系人"
+        placeholder="请填写姓名"
+        :rules="[{ required: true, message: '请填写姓名' }]"
+      />
+      <van-field
+        v-model="formData.nameL"
+        label-align="top"
+        name="联系电话"
+        label="联系电话"
+        placeholder="请填写手机号"
+        :rules="[{ required: true, message: '请填写手机号' }]"
+      />
+      <van-field
+        v-model="formData.nameL"
+        label-align="top"
+        name="产品SN码"
+        label="产品SN码"
+        placeholder="请填写下划线后的数字母组合"
+        :rules="[{ required: true, message: '请填写密码' }]"
+      />
+      <van-field
+        v-model="formData.nameL"
+        label-align="top"
+        name="障描述"
+        label="障描述"
+        placeholder="请具体描述故障现象,相关操作等。"
+        :rules="[{ required: true, message: '请填写密码' }]"
+      />
+      <van-field name="uploader" label="文件上传" label-align="top">
+        <template #input>
+          <van-uploader v-model="formData.file" />
+        </template>
+      </van-field>
+      <van-field name="radio" label="单选框" label-align="top">
+        <template #input>
+          <van-radio-group v-model="formData.checked" direction="horizontal">
+            <van-radio name="1">前台送修</van-radio>
+            <van-radio name="2">快递寄送</van-radio>
+          </van-radio-group>
+        </template>
+      </van-field>
+
+      <van-field
+        v-model="formData.nameL"
+        label-align="top"
+        name="快递单号"
+        label="快递单号"
+        placeholder="请填写快递单号"
+        :rules="[{ required: true, message: '请填写快递单号' }]"
+      />
+
+      <van-field name="radio" label="单选框" label-align="top">
+        <template #input>
+          <van-radio-group v-model="formData.checkeds" direction="horizontal">
+            <van-radio name="1">前台送修</van-radio>
+            <van-radio name="2">快递寄送</van-radio>
+          </van-radio-group>
+        </template>
+      </van-field>
+
+      <van-field
+        v-model="formData.nameL"
+        label-align="top"
+        name="收件地址"
+        label="收件地址"
+        placeholder="请填写收件地址"
+        :rules="[{ required: true, message: '请填写收件地址' }]"
+      />
+      <van-field
+        v-model="formData.nameL"
+        label-align="top"
+        name="收件人"
+        label="收件人"
+        placeholder="请填写收件人"
+        :rules="[{ required: true, message: '请填写收件人' }]"
+      />
+      <van-field
+        v-model="formData.nameL"
+        label-align="top"
+        name="收件人电话"
+        label="收件人电话"
+        placeholder="请填写收件人电话"
+        :rules="[{ required: true, message: '请填写收件人电话' }]"
+      />
+
+      <van-field
+        v-model="formData.message"
+        label-align="top"
+        rows="2"
+        autosize
+        label="留言"
+        type="textarea"
+        maxlength="500"
+        placeholder="请输入留言"
+        show-word-limit
+      />
+    </van-cell-group>
+    <div style="margin: 16px">
+      <van-button round block type="primary" native-type="submit"> 提交 </van-button>
+    </div>
+  </van-form>
+</template>
+  
+<script lang="ts" setup name="HomeSubmit">
+import { computed } from 'vue';
+import { useUserStore } from '/@/store/modules/user';
+import { showToast } from 'vant';
+import { setLang } from '/@/i18n';
+import { useI18n } from 'vue-i18n';
+const { locale } = useI18n();
+const userStore = useUserStore();
+const getUserInfo = computed(() => {
+  const { name = '' } = userStore.getUserInfo || {};
+  return name;
+});
+const formData = reactive({
+  nameL: '',
+  lxr: '',
+  lxdh: '',
+  sncode: '',
+  gzms: '',
+  message: '',
+  file: [],
+  checked: '1',
+  checkeds: '1',
+});
+const onSubmit = () => {
+  console.log('onSubmit');
+};
+const changeLang = (type) => {
+  setLang(type);
+};
+</script>
+  <style lang="scss">
+.header {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 0 20px;
+  font-size: 40px;
+  img {
+    width: 90px;
+    height: 90px;
+  }
+}
+
+.intro-header {
+  margin-top: 20px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 16px;
+}
+
+.supportList {
+  margin: 0 16px;
+
+  .nut-cell-group__title {
+    margin-top: 30px;
+  }
+  .nut-icon {
+    color: green;
+  }
+}
+
+.btn-wrap {
+  margin: 20px;
+}
+.btn-confirm {
+  @include main-lang-bg(302px, 82px, '/@/assets/button', 'confirm.png');
+}
+</style>
+  

+ 17 - 0
src/views/list/index.vue

@@ -0,0 +1,17 @@
+<template>
+  <nut-card :img-url="state.imgUrl" :title="state.title" :price="state.price" :vip-price="state.vipPrice" :shop-name="state.shopName" />
+</template>
+
+<script lang="ts" setup name="ListPage">
+  import { reactive } from 'vue';
+
+  let state = reactive({
+    imgUrl: '//img10.360buyimg.com/n2/s240x240_jfs/t1/210890/22/4728/163829/6163a590Eb7c6f4b5/6390526d49791cb9.jpg!q70.jpg',
+    title: '活蟹】湖塘煙雨 阳澄湖大闸蟹公4.5两 母3.5两 4对8只 鲜活生鲜螃蟹现货水产礼盒海鲜水',
+    price: '388',
+    vipPrice: '378',
+    shopDesc: '自营',
+    delivery: '厂商配送',
+    shopName: '阳澄湖大闸蟹自营店>',
+  });
+</script>

+ 60 - 0
src/views/login/index.vue

@@ -0,0 +1,60 @@
+<template>
+  <div class="login">
+    <h2>登录</h2>
+    <nut-form ref="ruleForm" :model-value="formData">
+      <nut-form-item required prop="name" :rules="[{ required: true, message: '请输入用户名' }]">
+        <input v-model="formData.name" class="nut-input-text" placeholder="请输入用户名" type="text" />
+      </nut-form-item>
+      <nut-form-item required prop="pwd" :rules="[{ required: true, message: '请填写联系电话' }]">
+        <input v-model="formData.pwd" class="nut-input-text" placeholder="请输入密码" type="password" />
+      </nut-form-item>
+      <nut-button block type="info" @click="submit"> 登录 </nut-button>
+    </nut-form>
+  </div>
+</template>
+
+<script lang="ts" setup name="LoginPage">
+  import router from '/@/router';
+  import { reactive, ref } from 'vue';
+  import { useUserStore } from '/@/store/modules/user';
+
+  const userStore = useUserStore();
+  const formData = reactive({
+    name: '',
+    pwd: '',
+  });
+  const ruleForm = ref<any>(null);
+  const submit = () => {
+    ruleForm.value.validate().then(async ({ valid, errors }: any) => {
+      if (valid) {
+        const userInfo = await userStore.login();
+        console.log(userInfo);
+        if (userInfo) {
+          router.push({ path: '/home' });
+        }
+      } else {
+        console.log('error submit!!', errors);
+      }
+    });
+  };
+</script>
+
+<style scoped lang="scss">
+  .login {
+    padding: 20px;
+    h2 {
+      text-align: center;
+      letter-spacing: 10px;
+    }
+
+    .nut-form-item {
+      background: #f2f3f5;
+      border-radius: 20px;
+      margin-bottom: 20px;
+
+      input {
+        background: transparent;
+      }
+    }
+  }
+</style>

+ 60 - 0
src/views/member/index.vue

@@ -0,0 +1,60 @@
+<template>
+  <div class="avatar-wrap">
+    <nut-avatar
+      class="avatar"
+      size="large"
+      icon="https://img12.360buyimg.com/imagetools/jfs/t1/143702/31/16654/116794/5fc6f541Edebf8a57/4138097748889987.png"
+    />
+    <div class="member-detail">
+      <p class="nickname"> 昵称<nut-button shape="square" size="small" type="default" @click="goLogin"> 去登录 </nut-button> </p>
+      <p class="info"> 个人其他信息,后续补充.... </p>
+    </div>
+  </div>
+  <nut-grid>
+    <nut-grid-item icon="dongdong" text="文字" />
+    <nut-grid-item icon="dongdong" text="文字" />
+    <nut-grid-item icon="dongdong" text="文字" />
+    <nut-grid-item icon="dongdong" text="文字" />
+    <nut-grid-item icon="dongdong" text="文字" />
+    <nut-grid-item icon="dongdong" text="文字" />
+    <nut-grid-item icon="dongdong" text="文字" />
+    <nut-grid-item icon="dongdong" text="文字" />
+  </nut-grid>
+</template>
+
+<script lang="ts" setup name="MemberPage">
+  // import { useUserStore } from '@/store/modules/user';
+  import { useRouter } from 'vue-router';
+
+  const router = useRouter();
+  // const userStore = useUserStore();
+  // const getUserInfo = computed(() => {
+  //   const { name = '' } = userStore.getUserInfo || {};
+  //   return name;
+  // });
+  const goLogin = () => {
+    router.push('/login');
+  };
+</script>
+
+<style lang="scss">
+  .avatar-wrap {
+    display: flex;
+    margin: 0 10px 40px;
+    align-items: center;
+    .member-detail {
+      margin-left: 20px;
+      .nickname {
+        font-size: 16px;
+        font-weight: bold;
+        .nut-button {
+          margin-left: 10px;
+        }
+      }
+      .info {
+        margin-top: 10px;
+        font-size: 16px;
+      }
+    }
+  }
+</style>

+ 89 - 0
stylelint.config.js

@@ -0,0 +1,89 @@
+module.exports = {
+  root: true,
+  plugins: ['stylelint-order'],
+  extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
+  customSyntax: 'postcss-html',
+  rules: {
+    'function-no-unknown': null,
+    'selector-class-pattern': null,
+    'selector-pseudo-class-no-unknown': [
+      true,
+      {
+        ignorePseudoClasses: ['global'],
+      },
+    ],
+    'selector-pseudo-element-no-unknown': [
+      true,
+      {
+        ignorePseudoElements: ['v-deep'],
+      },
+    ],
+    'at-rule-no-unknown': [
+      true,
+      {
+        ignoreAtRules: ['tailwind', 'apply', 'variants', 'responsive', 'screen', 'function', 'if', 'each', 'include', 'mixin'],
+      },
+    ],
+    'no-empty-source': null,
+    'string-quotes': null,
+    'named-grid-areas-no-invalid': null,
+    'unicode-bom': 'never',
+    'no-descending-specificity': null,
+    'font-family-no-missing-generic-family-keyword': null,
+    'declaration-colon-space-after': 'always-single-line',
+    'declaration-colon-space-before': 'never',
+    // 'declaration-block-trailing-semicolon': 'always',
+    'rule-empty-line-before': [
+      'always',
+      {
+        ignore: ['after-comment', 'first-nested'],
+      },
+    ],
+    'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }],
+    'order/order': [
+      [
+        'dollar-variables',
+        'custom-properties',
+        'at-rules',
+        'declarations',
+        {
+          type: 'at-rule',
+          name: 'supports',
+        },
+        {
+          type: 'at-rule',
+          name: 'media',
+        },
+        'rules',
+      ],
+      { severity: 'warning' },
+    ],
+  },
+  ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts'],
+  overrides: [
+    {
+      files: ['*.vue', '**/*.vue', '*.html', '**/*.html'],
+      extends: ['stylelint-config-recommended'],
+      rules: {
+        'keyframes-name-pattern': null,
+        'selector-pseudo-class-no-unknown': [
+          true,
+          {
+            ignorePseudoClasses: ['deep', 'global'],
+          },
+        ],
+        'selector-pseudo-element-no-unknown': [
+          true,
+          {
+            ignorePseudoElements: ['v-deep', 'v-global', 'v-slotted'],
+          },
+        ],
+      },
+    },
+    {
+      files: ['*.less', '**/*.less'],
+      customSyntax: 'postcss-less',
+      extends: ['stylelint-config-standard', 'stylelint-config-recommended-vue'],
+    },
+  ],
+};

+ 44 - 0
tsconfig.json

@@ -0,0 +1,44 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "module": "esnext",
+    "moduleResolution": "node",
+    "strict": true,
+    "noLib": false,
+    "forceConsistentCasingInFileNames": true,
+    "allowSyntheticDefaultImports": true,
+    "strictFunctionTypes": false,
+    "jsx": "preserve",
+    "baseUrl": ".",
+    "allowJs": true,
+    "sourceMap": true,
+    "esModuleInterop": true,
+    "resolveJsonModule": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "experimentalDecorators": true,
+    "lib": ["dom", "esnext"],
+    "noImplicitAny": false,
+    "skipLibCheck": true,
+    "types": ["vite/client"],
+    "removeComments": true,
+    "paths": {
+      "/@/*": ["src/*"],
+      "/#/*": ["types/*"]
+    }
+  },
+  "include": [
+    "tests/**/*.ts",
+    "src/**/*.ts",
+    "src/**/*.d.ts",
+    "src/**/*.tsx",
+    "src/**/*.vue",
+    "types/**/*.d.ts",
+    "types/**/*.ts",
+    "config/**/*.ts",
+    "config/**/*.d.ts",
+    "mock/**/*.ts",
+    "vite.config.ts"
+  ],
+  "exclude": ["node_modules", "tests/server/**/*.ts", "dist", "**/*.js"]
+}

+ 73 - 0
types/auto-imports.d.ts

@@ -0,0 +1,73 @@
+// Generated by 'unplugin-auto-import'
+export {}
+declare global {
+  const EffectScope: typeof import('vue')['EffectScope']
+  const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
+  const computed: typeof import('vue')['computed']
+  const createApp: typeof import('vue')['createApp']
+  const createPinia: typeof import('pinia')['createPinia']
+  const customRef: typeof import('vue')['customRef']
+  const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
+  const defineComponent: typeof import('vue')['defineComponent']
+  const defineStore: typeof import('pinia')['defineStore']
+  const effectScope: typeof import('vue')['effectScope']
+  const getActivePinia: typeof import('pinia')['getActivePinia']
+  const getCurrentInstance: typeof import('vue')['getCurrentInstance']
+  const getCurrentScope: typeof import('vue')['getCurrentScope']
+  const h: typeof import('vue')['h']
+  const inject: typeof import('vue')['inject']
+  const isProxy: typeof import('vue')['isProxy']
+  const isReactive: typeof import('vue')['isReactive']
+  const isReadonly: typeof import('vue')['isReadonly']
+  const isRef: typeof import('vue')['isRef']
+  const mapActions: typeof import('pinia')['mapActions']
+  const mapGetters: typeof import('pinia')['mapGetters']
+  const mapState: typeof import('pinia')['mapState']
+  const mapStores: typeof import('pinia')['mapStores']
+  const mapWritableState: typeof import('pinia')['mapWritableState']
+  const markRaw: typeof import('vue')['markRaw']
+  const nextTick: typeof import('vue')['nextTick']
+  const onActivated: typeof import('vue')['onActivated']
+  const onBeforeMount: typeof import('vue')['onBeforeMount']
+  const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
+  const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
+  const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
+  const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
+  const onDeactivated: typeof import('vue')['onDeactivated']
+  const onErrorCaptured: typeof import('vue')['onErrorCaptured']
+  const onMounted: typeof import('vue')['onMounted']
+  const onRenderTracked: typeof import('vue')['onRenderTracked']
+  const onRenderTriggered: typeof import('vue')['onRenderTriggered']
+  const onScopeDispose: typeof import('vue')['onScopeDispose']
+  const onServerPrefetch: typeof import('vue')['onServerPrefetch']
+  const onUnmounted: typeof import('vue')['onUnmounted']
+  const onUpdated: typeof import('vue')['onUpdated']
+  const provide: typeof import('vue')['provide']
+  const reactive: typeof import('vue')['reactive']
+  const readonly: typeof import('vue')['readonly']
+  const ref: typeof import('vue')['ref']
+  const resolveComponent: typeof import('vue')['resolveComponent']
+  const resolveDirective: typeof import('vue')['resolveDirective']
+  const setActivePinia: typeof import('pinia')['setActivePinia']
+  const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
+  const shallowReactive: typeof import('vue')['shallowReactive']
+  const shallowReadonly: typeof import('vue')['shallowReadonly']
+  const shallowRef: typeof import('vue')['shallowRef']
+  const storeToRefs: typeof import('pinia')['storeToRefs']
+  const toRaw: typeof import('vue')['toRaw']
+  const toRef: typeof import('vue')['toRef']
+  const toRefs: typeof import('vue')['toRefs']
+  const triggerRef: typeof import('vue')['triggerRef']
+  const unref: typeof import('vue')['unref']
+  const useAttrs: typeof import('vue')['useAttrs']
+  const useCssModule: typeof import('vue')['useCssModule']
+  const useCssVars: typeof import('vue')['useCssVars']
+  const useLink: typeof import('vue-router')['useLink']
+  const useRoute: typeof import('vue-router')['useRoute']
+  const useRouter: typeof import('vue-router')['useRouter']
+  const useSlots: typeof import('vue')['useSlots']
+  const watch: typeof import('vue')['watch']
+  const watchEffect: typeof import('vue')['watchEffect']
+  const watchPostEffect: typeof import('vue')['watchPostEffect']
+  const watchSyncEffect: typeof import('vue')['watchSyncEffect']
+}

+ 53 - 0
types/axios.d.ts

@@ -0,0 +1,53 @@
+export type ErrorMessageMode = 'none' | 'modal' | 'message' | undefined;
+
+export interface RequestOptions {
+  // Splicing request parameters to url
+  joinParamsToUrl?: boolean;
+  // Format request parameter time
+  formatDate?: boolean;
+  // Whether to process the request result
+  isTransformResponse?: boolean;
+  // Whether to return native response headers
+  // For example: use this attribute when you need to get the response headers
+  isReturnNativeResponse?: boolean;
+  // Whether to join url
+  joinPrefix?: boolean;
+  // Interface address, use the default apiUrl if you leave it blank
+  apiUrl?: string;
+  // 请求拼接路径
+  urlPrefix?: string;
+  // Error message prompt type
+  errorMessageMode?: ErrorMessageMode;
+  // Whether to add a timestamp
+  joinTime?: boolean;
+  ignoreCancelToken?: boolean;
+  // Whether to send token in header
+  withToken?: boolean;
+  // 请求重试机制
+  retryRequest?: RetryRequest;
+}
+
+export interface RetryRequest {
+  isOpenRetry: boolean;
+  count: number;
+  waitTime: number;
+}
+export interface Result<T = any> {
+  code: number;
+  type: 'success' | 'error' | 'warning';
+  message: string;
+  result: T;
+}
+
+// multipart/form-data: upload file
+export interface UploadFileParams {
+  // Other parameters
+  data?: Recordable;
+  // File parameter interface field name
+  name?: string;
+  // file name
+  file: File | Blob;
+  // file name
+  filename?: string;
+  [key: string]: any;
+}

+ 30 - 0
types/components.d.ts

@@ -0,0 +1,30 @@
+// generated by unplugin-vue-components
+// We suggest you to commit this file into source control
+// Read more: https://github.com/vuejs/core/pull/3399
+import '@vue/runtime-core'
+
+export {}
+
+declare module '@vue/runtime-core' {
+  export interface GlobalComponents {
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
+    TitleBar: typeof import('./../src/components/TitleBar/index.vue')['default']
+    VanButton: typeof import('vant/es')['Button']
+    VanCell: typeof import('vant/es')['Cell']
+    VanCellGroup: typeof import('vant/es')['CellGroup']
+    VanDivider: typeof import('vant/es')['Divider']
+    VanField: typeof import('vant/es')['Field']
+    VanForm: typeof import('vant/es')['Form']
+    VanIcon: typeof import('vant/es')['Icon']
+    VanList: typeof import('vant/es')['List']
+    VanRadio: typeof import('vant/es')['Radio']
+    VanRadioGroup: typeof import('vant/es')['RadioGroup']
+    VanRate: typeof import('vant/es')['Rate']
+    VanTab: typeof import('vant/es')['Tab']
+    VanTabs: typeof import('vant/es')['Tabs']
+    VanUploader: typeof import('vant/es')['Uploader']
+    VarButton: typeof import('@varlet/ui')['_ButtonComponent']
+    VarSpace: typeof import('@varlet/ui')['_SpaceComponent']
+  }
+}

+ 162 - 0
types/config.d.ts

@@ -0,0 +1,162 @@
+import { MenuTypeEnum, MenuModeEnum, TriggerEnum, MixSidebarTriggerEnum } from '/@/enums/menuEnum';
+import {
+  ContentEnum,
+  PermissionModeEnum,
+  ThemeEnum,
+  RouterTransitionEnum,
+  SettingButtonPositionEnum,
+  SessionTimeoutProcessingEnum,
+} from '/@/enums/appEnum';
+
+import { CacheTypeEnum } from '/@/enums/cacheEnum';
+
+export type LocaleType = 'zh_CN' | 'en' | 'ru' | 'ja' | 'ko';
+
+export interface MenuSetting {
+  bgColor: string;
+  fixed: boolean;
+  collapsed: boolean;
+  siderHidden: boolean;
+  canDrag: boolean;
+  show: boolean;
+  hidden: boolean;
+  split: boolean;
+  menuWidth: number;
+  mode: MenuModeEnum;
+  type: MenuTypeEnum;
+  theme: ThemeEnum;
+  topMenuAlign: 'start' | 'center' | 'end';
+  trigger: TriggerEnum;
+  accordion: boolean;
+  closeMixSidebarOnChange: boolean;
+  collapsedShowTitle: boolean;
+  mixSideTrigger: MixSidebarTriggerEnum;
+  mixSideFixed: boolean;
+}
+
+export interface MultiTabsSetting {
+  cache: boolean;
+  show: boolean;
+  showQuick: boolean;
+  canDrag: boolean;
+  showRedo: boolean;
+  showFold: boolean;
+}
+
+export interface HeaderSetting {
+  bgColor: string;
+  fixed: boolean;
+  show: boolean;
+  theme: ThemeEnum;
+  // Turn on full screen
+  showFullScreen: boolean;
+  // Whether to show the lock screen
+  useLockPage: boolean;
+  // Show document button
+  showDoc: boolean;
+  // Show message center button
+  showNotice: boolean;
+  showSearch: boolean;
+}
+
+export interface LocaleSetting {
+  showPicker: boolean;
+  // Current language
+  locale: LocaleType;
+  // default language
+  fallback: LocaleType;
+  // available Locales
+  availableLocales: LocaleType[];
+}
+
+export interface TransitionSetting {
+  //  Whether to open the page switching animation
+  enable: boolean;
+  // Route basic switching animation
+  basicTransition: RouterTransitionEnum;
+  // Whether to open page switching loading
+  openPageLoading: boolean;
+  // Whether to open the top progress bar
+  openNProgress: boolean;
+}
+
+export interface ProjectConfig {
+  // Storage location of permission related information
+  permissionCacheType: CacheTypeEnum;
+  // Whether to show the configuration button
+  showSettingButton: boolean;
+  // Whether to show the theme switch button
+  showDarkModeToggle: boolean;
+  // Configure where the button is displayed
+  settingButtonPosition: SettingButtonPositionEnum;
+  // Permission mode
+  permissionMode: PermissionModeEnum;
+  // Session timeout processing
+  sessionTimeoutProcessing: SessionTimeoutProcessingEnum;
+  // Website gray mode, open for possible mourning dates
+  grayMode: boolean;
+  // Whether to turn on the color weak mode
+  colorWeak: boolean;
+  // Theme color
+  themeColor: string;
+
+  // The main interface is displayed in full screen, the menu is not displayed, and the top
+  fullContent: boolean;
+  // content width
+  contentMode: ContentEnum;
+  // Whether to display the logo
+  showLogo: boolean;
+  // Whether to show the global footer
+  showFooter: boolean;
+  // menuType: MenuTypeEnum;
+  headerSetting: HeaderSetting;
+  // menuSetting
+  menuSetting: MenuSetting;
+  // Multi-tab settings
+  multiTabsSetting: MultiTabsSetting;
+  // Animation configuration
+  transitionSetting: TransitionSetting;
+  // pageLayout whether to enable keep-alive
+  openKeepAlive: boolean;
+  // Lock screen time
+  lockTime: number;
+  // Show breadcrumbs
+  showBreadCrumb: boolean;
+  // Show breadcrumb icon
+  showBreadCrumbIcon: boolean;
+  // Use error-handler-plugin
+  useErrorHandle: boolean;
+  // Whether to open back to top
+  useOpenBackTop: boolean;
+  // Is it possible to embed iframe pages
+  canEmbedIFramePage: boolean;
+  // Whether to delete unclosed messages and notify when switching the interface
+  closeMessageOnSwitch: boolean;
+  // Whether to cancel the http request that has been sent but not responded when switching the interface.
+  removeAllHttpPending: boolean;
+}
+
+export interface GlobConfig {
+  // Site title
+  title: string;
+  // Service interface url
+  apiUrl: string;
+  // Upload url
+  uploadUrl?: string;
+  //  Service interface url prefix
+  urlPrefix?: string;
+  // Project abbreviation
+  shortName: string;
+}
+export interface GlobEnvConfig {
+  // Site title
+  VITE_GLOB_APP_TITLE: string;
+  // Service interface url
+  VITE_GLOB_API_URL: string;
+  // Service interface url prefix
+  VITE_GLOB_API_URL_PREFIX?: string;
+  // Project abbreviation
+  VITE_GLOB_APP_SHORT_NAME: string;
+  // Upload url
+  VITE_GLOB_UPLOAD_URL?: string;
+}

+ 76 - 0
types/global.d.ts

@@ -0,0 +1,76 @@
+import type { ComponentRenderProxy, VNode, VNodeChild, PropType as VuePropType } from 'vue';
+
+declare global {
+  const __APP_INFO__: {
+    pkg: {
+      name: string;
+      version: string;
+      dependencies: Recordable<string>;
+      devDependencies: Recordable<string>;
+    };
+    lastBuildTime: string;
+  };
+  // declare interface Window {
+  //   // Global vue app instance
+  //   __APP__: App<Element>;
+  // }
+
+  // vue
+  declare type PropType<T> = VuePropType<T>;
+  declare type VueNode = VNodeChild | JSX.Element;
+
+  export type Writable<T> = {
+    -readonly [P in keyof T]: T[P];
+  };
+
+  declare type Nullable<T> = T | null;
+  declare type NonNullable<T> = T extends null | undefined ? never : T;
+  declare type Recordable<T = any> = Record<string, T>;
+  declare type ReadonlyRecordable<T = any> = {
+    readonly [key: string]: T;
+  };
+  declare type Indexable<T = any> = {
+    [key: string]: T;
+  };
+  declare type DeepPartial<T> = {
+    [P in keyof T]?: DeepPartial<T[P]>;
+  };
+  declare type TimeoutHandle = ReturnType<typeof setTimeout>;
+  declare type IntervalHandle = ReturnType<typeof setInterval>;
+
+  declare interface ChangeEvent extends Event {
+    target: HTMLInputElement;
+  }
+
+  declare interface WheelEvent {
+    path?: EventTarget[];
+  }
+  interface ImportMetaEnv extends ViteEnv {
+    __: unknown;
+  }
+
+  declare interface ViteEnv {
+    VITE_USE_MOCK: Boolean;
+    VITE_USE_ERUDA: Boolean;
+  }
+
+  declare function parseInt(s: string | number, radix?: number): number;
+
+  declare function parseFloat(string: string | number): number;
+
+  namespace JSX {
+    // tslint:disable no-empty-interface
+    type Element = VNode;
+    // tslint:disable no-empty-interface
+    type ElementClass = ComponentRenderProxy;
+    interface ElementAttributesProperty {
+      $props: any;
+    }
+    interface IntrinsicElements {
+      [elem: string]: any;
+    }
+    interface IntrinsicAttributes {
+      [elem: string]: any;
+    }
+  }
+}

+ 27 - 0
types/index.d.ts

@@ -0,0 +1,27 @@
+declare interface Fn<T = any, R = T> {
+  (...arg: T[]): R;
+}
+
+declare interface PromiseFn<T = any, R = T> {
+  (...arg: T[]): Promise<R>;
+}
+
+declare type RefType<T> = T | null;
+
+declare type LabelValueOptions = {
+  label: string;
+  value: any;
+  [key: string]: string | number | boolean;
+}[];
+
+declare type EmitType = (event: string, ...args: any[]) => void;
+
+declare type TargetContext = '_self' | '_blank';
+
+declare interface ComponentElRef<T extends HTMLElement = HTMLDivElement> {
+  $el: T;
+}
+
+declare type ComponentRef<T extends HTMLElement = HTMLDivElement> = ComponentElRef<T> | null;
+
+declare type ElRef<T extends HTMLElement = HTMLDivElement> = Nullable<T>;

+ 16 - 0
types/module.d.ts

@@ -0,0 +1,16 @@
+declare module '*.vue' {
+  import { DefineComponent } from 'vue';
+  const Component: DefineComponent<{}, {}, any>;
+  export default Component;
+}
+
+declare module 'ant-design-vue/es/locale/*' {
+  import { Locale } from 'ant-design-vue/types/locale-provider';
+  const locale: Locale & ReadonlyRecordable;
+  export default locale as Locale & ReadonlyRecordable;
+}
+
+declare module 'virtual:*' {
+  const result: any;
+  export default result;
+}

+ 48 - 0
types/store.d.ts

@@ -0,0 +1,48 @@
+import { ErrorTypeEnum } from '/@/enums/exceptionEnum';
+import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum';
+import { RoleInfo } from '/@/api/sys/model/userModel';
+
+// Lock screen information
+export interface LockInfo {
+  // Password required
+  pwd?: string | undefined;
+  // Is it locked?
+  isLock?: boolean;
+}
+
+// Error-log information
+export interface ErrorLogInfo {
+  // Type of error
+  type: ErrorTypeEnum;
+  // Error file
+  file: string;
+  // Error name
+  name?: string;
+  // Error message
+  message: string;
+  // Error stack
+  stack?: string;
+  // Error detail
+  detail: string;
+  // Error url
+  url: string;
+  // Error time
+  time?: string;
+}
+
+export interface UserInfo {
+  userId: string | number;
+  username: string;
+  realName: string;
+  avatar: string;
+  desc?: string;
+  homePath?: string;
+  roles: RoleInfo[];
+}
+
+export interface BeforeMiniState {
+  menuCollapsed?: boolean;
+  menuSplit?: boolean;
+  menuMode?: MenuModeEnum;
+  menuType?: MenuTypeEnum;
+}

+ 5 - 0
types/utils.d.ts

@@ -0,0 +1,5 @@
+import type { ComputedRef, Ref } from 'vue';
+
+export type DynamicProps<T> = {
+  [P in keyof T]: Ref<T[P]> | T[P] | ComputedRef<T[P]>;
+};

+ 45 - 0
types/vue-router.d.ts

@@ -0,0 +1,45 @@
+export {};
+
+declare module 'vue-router' {
+  interface RouteMeta extends Record<string | number | symbol, unknown> {
+    orderNo?: number;
+    // title
+    title: string;
+    // dynamic router level.
+    dynamicLevel?: number;
+    // dynamic router real route path (For performance).
+    realPath?: string;
+    // Whether to ignore permissions
+    ignoreAuth?: boolean;
+    // role info
+    roles?: RoleEnum[];
+    // Whether not to cache
+    ignoreKeepAlive?: boolean;
+    // Is it fixed on tab
+    affix?: boolean;
+    // icon on tab
+    icon?: string;
+    frameSrc?: string;
+    // current page transition
+    transitionName?: string;
+    // Whether the route has been dynamically added
+    hideBreadcrumb?: boolean;
+    // Hide submenu
+    hideChildrenInMenu?: boolean;
+    // Carrying parameters
+    carryParam?: boolean;
+    // Used internally to mark single-level menus
+    single?: boolean;
+    // Currently active menu
+    currentActiveMenu?: string;
+    // Never show in tab
+    hideTab?: boolean;
+    // Never show in menu
+    hideMenu?: boolean;
+    isLink?: boolean;
+    // only build for Menu
+    ignoreRoute?: boolean;
+    // Hide path for children
+    hidePathForChildren?: boolean;
+  }
+}

+ 61 - 0
vite.config.ts

@@ -0,0 +1,61 @@
+import { createVitePlugins } from './build/vite/plugins';
+import { resolve } from 'path';
+import { ConfigEnv, loadEnv, UserConfig } from 'vite';
+import { wrapperEnv } from './build/utils';
+
+const pathResolve = (dir: string) => {
+  return resolve(process.cwd(), '.', dir);
+};
+
+// https://vitejs.dev/config/
+export default function ({ command, mode }: ConfigEnv): UserConfig {
+  const isProduction = command === 'build';
+  const root = process.cwd();
+  const env = loadEnv(mode, root);
+  const viteEnv = wrapperEnv(env);
+
+  return {
+    root,
+    resolve: {
+      alias: [
+        {
+          find: 'vue-i18n',
+          replacement: 'vue-i18n/dist/vue-i18n.cjs.js',
+        },
+        // /@/xxxx => src/xxxx
+        {
+          find: /\/@\//,
+          replacement: pathResolve('src') + '/',
+        },
+        // /#/xxxx => types/xxxx
+        {
+          find: /\/#\//,
+          replacement: pathResolve('types') + '/',
+        },
+      ],
+    },
+    server: {
+      host: true,
+      hmr: true,
+    },
+    plugins: createVitePlugins(viteEnv, isProduction),
+    build: {
+      minify: 'terser',
+      terserOptions: {
+        compress: {
+          //生产环境时移除console
+          drop_console: true,
+          drop_debugger: true,
+        },
+      },
+    },
+    css: {
+      preprocessorOptions: {
+        scss: {
+          // 配置 nutui 全局 scss 变量
+          additionalData: `@import "@nutui/nutui/dist/styles/variables.scss";@import '/@/styles/mixin.scss';`,
+        },
+      },
+    },
+  };
+}

+ 0 - 0
yarn.lock


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