## 提交规范

### 1. 提交消息格式

```bin
<Type>[optional Scope]: <Description>
[optional body]

[optional footer]
```

#### Type 提交的类型

> 表示提交的目的或影响的范围

- feat: 新功能（A new feature）
- fix: 修复 bug（A bug fix）
- docs: 文档变更（Documentation changes）
- style: 代码样式调整（Code style changes）
- refactor: 代码重构（Code refactoring）
- test: 测试相关的变更（Test-related changes）
- chore: 构建过程或工具变动（Build process or tooling changes）
- perf: 性能优化（Performance optimization）
- ci: CI/CD 相关的变动（Changes to the CI/CD configuration or scripts）
- revert: 撤销之前的提交（Revert a previous commit）

#### Scope 作用域

提交的作用域，表示本次提交影响的部分代码或模块，可以根据项目的需要选择性地添加。

#### Description 描述

简明扼要地描述本次提交的内容

#### body 正文

可选的详细描述，可以包含更多的信息和上下文

#### footer 脚注

可选的脚注，通常用于引用相关的问题编号或关闭问题。

## 示例提交消息：

```
feat(user): add login functionality

- Add login form and authentication logic
- Implement user authentication API endpoints

Closes #123
```

在这个示例中，提交类型为 feat（新功能），作用域为 user，描述了添加登录功能的内容。正文部分提供了更详细的说明，并引用了问题编号。

## 架构总览

```
├── babel.config.js						// babel配置，支持JSX
├── commitlint.config.js			// commitlint配置，校验commit信息
├── components.d.ts						// 组件注册TS声明
├── config										// 项目构建配置
│   ├── plugin								// 构建插件
│   ├── utils									// 构建工具方法
│   ├── vite.config.base.ts		// vite基础配置
│   ├── vite.config.dev.ts		// vite开发环境配置
│   └── vite.config.prod.ts		// vite 生产配置
├── index.html								// 单页面html模板
├── src
│   ├── App.vue								// 应用入口vue
│   ├── api										// 项目请求api封装
│   │   ├── http							// axios封装
│   │   ├── modules						// 各业务模块的请求方法
│   │   ├── requrls						// 按业务模块划分的接口地址
│   ├── assets								// 全局静态资源
│   │   ├── images
				- svg
│   │   ├── logo.svg
│   │   ├── style
│   ├── components						// 组件
│   ├── config								// 全局配置，常量类、JSON
│   ├── directive							// 自定义指令集
│   ├── enums									// 全局枚举定义
│   ├── hooks									// 全局hooks集
│   ├── layout								// 应用布局组件
│   ├── locale								// 国际化配置
│   ├── main.ts								// 项目主入口
│   ├── mock									// mock数据配置
│   ├── models								// 全局数据模型定义
│   ├── router								// 路由
│   ├── store									// pinia状态库
│   ├── types									// 全局TS声明
│   ├── utils									// 公共工具方法
│   └── views
│       ├── modules						// 页面模块
│       └── base							// 公共页面，403、404等
│   ├── env.d.ts							// 环境信息TS类型声明
└── .env.development					// 开发环境变量声明
└── .env.production						// 生产环境变量声明
└── .eslintrc.js							// eslint配置
└── .prettierrc.js						// prettier配置
└── tsconfig.json							// 全局TS配置
```

<a name="dd14f832"></a>

## -状态管理模块设计

Vue3 状态管理方案采用的是`pinia`，API 风格与`Redux`类的状态管理库类似，也是通过模块化的方式注册`模块store`，每个`store`中包含数据仓库`state`、数据包装过滤`getter`、同步/异步操作`action`。与`Vuex`相比，`pinia`提供了`compasition-API`、完整支持 TS 以及可扩展的`Plugin`功能。<br />整体模块划分为业务模块`modules/*`、注册入口`index`以及插件`plugins/*`。<br />首先，在`store/index.ts`中声明注册`pinia`并引入自定义的插件：

```typescript
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
import { debouncePlugin } from './plugins';
import useXXStore from './modules/xx';

const pinia = createPinia();
// 插件会在实例创建后应用，因此插件不能在pinia实例创建前使用
pinia.use(debouncePlugin).use(piniaPluginPersistedstate);
export { useXXStore }; // 导出模块store
export default pinia;
```

然后，在项目入口`mian.ts`中使用：

```typescript
import { createApp } from 'vue';
import store from '@/store';
import app from '@/App.vue';

createApp(app).use(store).mount('#app');
```

<a name="piniaPlugins.ts"></a>

### plugins

在此文件内编写`pinia`插件，并在注册的时候引入使用即可：

```typescript
import { debounce, isObject } from 'lodash-es';
// 首先得声明插件使用到的额外属性，因为pinia的TS类型声明中，每个store只有三个原生属性state、getters、actions，若没有使用到额外属性则无需声明
declare module 'pinia' {
  export interface DefineStoreOptionsBase<S, Store> {
    cache?: Partial<CacheType<S, Store>>; // 缓存配置
    debounce?: Partial<Record<keyof StoreActions<Store>, number>>; // 节流配置
  }
}
// 基于lodash的防抖函数封装，读取store中cache配置的属性，针对已配置的属性更改操作进行防抖处理
export const debouncePlugin = ({ options, store }: PiniaPluginContext) => {
  if (options.debounce) {
    return Object.keys(options.debounce).reduce((debounceActions: debounceAction, action) => {
      debounceActions[action] = debounce(store[action], options.debounce![action]);
      return debounceActions;
    }, {});
  }
};
```

上述自定义插件，在 store 中如下配置：

```typescript
export const useStore = defineStore('demo', {
  state: () => ({
    testDebounce: '',
  }),
  getters: {},
  actions: {},
  debounce: {
    // 防抖设置
    testDebounce: 500, // 值为防抖缓冲时间
  },
});
```

<a name="520bb47a"></a>

## -网络模块设计

网络模块包含：请求 url 封装`api/requrls/*`、请求方法封装`api/modules/*`、请求工具封装`api/http/*`。
<a name="slaQE"></a>

### requrls

将项目接口地址收敛至此文件夹下管理，避免出现一个项目多个重复接口 url、方便接口地址复用且方便统一处理

```typescript
export const LoginUrl = '/api/user/login';
export const LogoutUrl = '/api/user/logout';
export const GetUserInfoUrl = '/api/user/info';
export const GetMenuListUrl = '/api/user/menu';
```

<a name="y561q"></a>

### modules

将项目实际请求方法按业务模块划分，统一管理实际业务请求，将请求方法与接口地址解耦

```typescript
import axios from 'axios';
import { LoginUrl, LogoutUrl, GetUserInfoUrl, GetMenuListUrl } from '@/api/requrls/user';
import type { RouteRecordNormalized } from 'vue-router';
import type { UserState } from '@/store/modules/user/types';
import type { LoginData, LoginRes } from '@/models/user';

export function login(data: LoginData) {
  return axios.post<LoginRes>(LoginUrl, data);
}

export function logout() {
  return axios.post<LoginRes>(LogoutUrl);
}

export function getUserInfo() {
  return axios.post<UserState>(GetUserInfoUrl);
}

export function getMenuList() {
  return axios.post<RouteRecordNormalized[]>(GetMenuListUrl);
}
```

最终通过`index.ts`将请求方法暴露出去

```typescript
export * from './modules/user';
export * from './modules/dashboard';
export * from './modules/message';
```

<a name="0f0a9627"></a>

#### 请求工具封装

基于`axios`封装请求方法，提供`form-data/json/urlencoded`格式的数据处理、自定义头部处理、响应拦截错误处理、分级提示（modal、message、none）、get 请求防缓存。

```typescript
// 分级错误信息提示，none为静默模式即不提示、modal为对话框提示、message为tips消息提示
// ErrorMessageMode默认为message模式，通过传入请求方法的options.errorMessageMode入参判断
export type ErrorMessageMode = 'none' | 'modal' | 'message' | undefined;

// 三种入参数据格式
export enum ContentTypeEnum {
  // json
  JSON = 'application/json;charset=UTF-8',
  // form-data qs序列化处理
  FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
  // form-data  upload文件流处理
  FORM_DATA = 'multipart/form-data;charset=UTF-8',
}

// 对上传文件请求入参格式额外定义
export interface UploadFileParams {
  // 除文件流外的参数
  data?: Recordable;
  // 文件流字段名
  name?: string;
  // 文件内容
  file: File | Blob;
  // 文件名
  filename?: string;
  // 其他parmas
  [key: string]: any;
}

// 基准返回数据格式
export interface Result<T = any> {
  code: number;
  type: 'success' | 'error' | 'warning';
  message: string;
  result: T;
}
```

<a name="c53518d5"></a>

## -directive 指令集

指令入口`index.ts`导入并注册定义的全部指令

```typescript
import { App } from 'vue';
import permission from './permission';

export default {
  install(Vue: App) {
    Vue.directive('permission', permission);
  },
};
```

权限指令（或其他自定义指令）在同级目录下创建，以指令名称命名文件夹，下面是示例：

```typescript
import { DirectiveBinding } from 'vue';
import { useUserStore } from '@/store';

function checkPermission(el: HTMLElement, binding: DirectiveBinding) {
  const { value } = binding;
  const userStore = useUserStore();
  const { role } = userStore;

  if (Array.isArray(value)) {
    if (value.length > 0) {
      const permissionValues = value;
      const hasPermission = permissionValues.includes(role);
      if (!hasPermission && el.parentNode) {
        el.parentNode.removeChild(el);
      }
    }
  } else {
    throw new Error(`need roles! Like v-permission="['admin','user']"`);
  }
}

export default {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    checkPermission(el, binding);
  },
  updated(el: HTMLElement, binding: DirectiveBinding) {
    checkPermission(el, binding);
  },
};
```

<a name="Kw0kR"></a>

## -hooks

全局抽象钩子集（与 Vue2 的 mixins 类似），这里只写业务逻辑的钩子！！！通过`@vueuse/core`我们已经可以得到非常多实用的钩子函数，没必要重复造轮子，在编写钩子功能前先去[Function List](https://vueuse.org/functions.html)查看是否已经有相同功能的钩子了，没有再自己写。导出钩子`export default function useXxx`，以权限钩子示例：

```typescript
import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router';
import { useUserStore } from '@/store';

export default function usePermission() {
  const userStore = useUserStore();
  return {
    accessRouter(route: RouteLocationNormalized | RouteRecordRaw) {
      // do something
    },
    findFirstPermissionRoute(_routers: any, role = 'admin') {
      // do something
    },
    // You can add any rules you want
  };
}
```

<a name="In07K"></a>

## -layout

项目布局设计模块，将最上层布局组件放置此文件夹内，统一管理项目各类上层布局（若存在，例如左侧菜单-右侧内容布局、顶层菜单-中间内容-底部页脚布局等等），示例：

```vue
<template>
  <!-- router-view内为实际路由切换变化的内容，视为路由页面容器 -->
  <router-view v-slot="{ Component, route }">
    <!-- transition为页面录音切换时提供进出动画，平滑过渡页面渲染 -->
    <transition name="fade" mode="out-in" appear>
      <component :is="Component" v-if="route.meta.ignoreCache" :key="route.fullPath" />
      <!-- keep-alive提供组件状态缓存，以便快速渲染组件内容 -->
      <keep-alive v-else :include="cacheList">
        <component :is="Component" :key="route.fullPath" />
      </keep-alive>
    </transition>
  </router-view>
</template>

<script lang="ts" setup>
  import { computed } from 'vue';
  import { useTabBarStore } from '@/store';

  const tabBarStore = useTabBarStore();
  const cacheList = computed(() => tabBarStore.getCacheList);
</script>
```

<a name="KZkaZ"></a>

## -models

全局数据模型，将涉及请求、组件 props、状态库的公共属性抽象为数据模型，在此文件夹内声明，保证全局数据的类型统一、方便维护。下面是请求模型示例：

```typescript
export interface HttpResponse<T = unknown> {
  status: number;
  msg: string;
  code: number;
  data: T;
}
```

也可以是业务相关模型：

```typescript
export interface LoginData {
  username: string;
  password: string;
}

export interface LoginRes {
  token: string;
}
```

<a name="d3Gdu"></a>

## -mock

提供全局接口数据 mock 功能，避免因为调试而修改请求代码导致出现问题，按功能模块划分文件，通过`index.ts`暴露，示例如下：

```typescript
import Mock from 'mockjs';
import './user';
import '@/views/dashboard/workplace/mock';

Mock.setup({
  timeout: '600-1000',
});
```

```typescript
import Mock from 'mockjs';
import setupMock, { successResponseWrap, failResponseWrap } from '@/utils/setup-mock';
import { isLogin } from '@/utils/auth';

import { MockParams } from '@/types/mock';

setupMock({
  setup() {
    // 用户信息
    Mock.mock(new RegExp('/api/user/info'), () => {
      if (isLogin()) {
        const role = window.localStorage.getItem('userRole') || 'admin';
        return successResponseWrap({
          name: '王立群',
          avatar: '//lf1-xgcdn-tos.pstatp.com/obj/vcloud/vadmin/start.8e0e4855ee346a46ccff8ff3e24db27b.png',
          email: 'wangliqun@email.com',
          job: 'frontend',
          jobName: '前端艺术家',
          organization: 'Frontend',
          organizationName: '前端',
          location: 'beijing',
          locationName: '北京',
          introduction: '人潇洒，性温存',
          personalWebsite: 'https://www.arco.design',
          phone: '150****0000',
          registrationDate: '2013-05-10 12:10:00',
          accountId: '15012312300',
          certification: 1,
          role,
        });
      }
      return failResponseWrap(null, '未登录', 50008);
    });
    // 登出
    Mock.mock(new RegExp('/api/user/logout'), () => {
      return successResponseWrap(null);
    });
  },
});
```

<a name="QgxDQ"></a>

## -router

项目路由管理模块，入口文件`index.ts`注册并暴露全部路由，以模块命名文件夹划分模块路由放置在`routes/*`下；`guard/*`下放置路由导航控制，包含权限、登录重定向等；`app-menus/index.ts`为菜单相关的路由信息；`constants.ts`定义路由常量，包括路由白名单、重定向路由名、默认主页路由信息等
<a name="YZWcg"></a>

## -enums

全局的枚举类型声明模块，将可枚举的类型统一管理，以`类型+Enum`命名，方便维护。示例如下：

```typescript
/**
 * @description: Request result set
 */
export enum ResultEnum {
  SUCCESS = 0,
  ERROR = 1,
  TIMEOUT = 401,
  TYPE = 'success',
}

/**
 * @description: request method
 */
export enum RequestEnum {
  GET = 'GET',
  POST = 'POST',
  PUT = 'PUT',
  DELETE = 'DELETE',
}

/**
 * @description:  contentTyp
 */
export enum ContentTypeEnum {
  // json
  JSON = 'application/json;charset=UTF-8',
  // form-data qs
  FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
  // form-data  upload
  FORM_DATA = 'multipart/form-data;charset=UTF-8',
}
```

<a name="I0KEf"></a>

## -locale

国际化模块，存放项目声明的国际化配置，按语种划分模块。模块入口文件为`index.ts`，负责定义菜单、导航栏等公共的国际化配置，其他的按系统功能声明并导入`index.ts`，还有项目内页面组件的国际化配置也需要导入至`index.ts`，页面组件的国际化配置在页面的目录下声明，如：`views/dashboard/workplace/locale`。

```typescript
import { createI18n } from 'vue-i18n';
import en from './en-US';
import cn from './zh-CN';

export const LOCALE_OPTIONS = [
  { label: '中文', value: 'zh-CN' },
  { label: 'English', value: 'en-US' },
];
const defaultLocale = localStorage.getItem('MS-locale') || 'zh-CN';
const i18n = createI18n({
  locale: defaultLocale,
  fallbackLocale: 'en-US',
  legacy: false,
  allowComposition: true,
  messages: {
    'en-US': en,
    'zh-CN': cn,
  },
});

export default i18n;
```

<a name="fZfbw"></a>

## -types

项目级别的类型声明，与业务无关的类型声明，与`models`、`enums`不同的是，这里声明的是项目模块级别的类型，或者工具模块的类型声明，例如：`axios`的配置声明、第三方插件不提供 TS 支持但需要我们自定义的声明等
<a name="hvu5O"></a>

## -utils

公共方法、工具，按功能类型命名文件，单个文件内工具方法的功能要对应命名
<a name="FFTqz"></a>

## -views

页面模块，按功能模块划分，公共模块有`login`、`base`，其中`base`模块内包含 404、403 页面等
<a name="RwVcu"></a>

## -theme 主题配置
1. 去Desing Lab 创建主题 https://arco.design/themes/home
2. 以`ms-theme-` 命名为开头
3. 点击页面的配置主题
**“CSS 变量” + “Tailwind 配置变量” + “基于 css 变量自行计算混合色覆盖 arco-theme 变量”**

## -.env.\*环境变量配置

在`vite`中内置了环境变量配置功能，只需要在项目根目录下创建以`.env.*`开头的文件，在内部写入配置的变量即可，默认`.env`为生产环境、`.env.development`为开发环境、`.env.XXX`为自定义`XXX`环境（注意：自定义环境需要在`package.json`的项目运行命令后加入`--mode XXX`），各类环境变量配置示例如下（ ⚠️ 注意！！环境变量文件内使用注释要用`#`，不能使用`//`）:

```bash
# .env.production 为生产环境配置
NODE_ENV=production # 代码中通过import.meta.env.NODE_ENV访问
VITE_STG=1 # 自定义变量必须用VITE_开头，代码中通过import.meta.env.VITE_XXX访问

# .env.development 为开发环境配置
NODE_ENV=development
VITE_APP_ENV = dev
VITE_APP_TITLE = 我是标题

# .env.XXX 为自定义环境配置
VITE_MYENV = 1 # 代码中通过import.meta.env.VITE_MYENV访问
```

⚠️ 上述环境变量在代码中正常都可以用`import.meta.env.VITE_XXX`访问，但是在`vite.config.ts`配置文件中无法使用此方法访问，原因是此访问链是 vite 在初始化后通过读取本地`.env.XXX`文件并注入到`import.meta`中的，但是在`vite.config.ts`中 vite 此时还未初始化，所以无法通过上述方法访问，应通过下面方法访问：

```typescript
import { defineConfig, loadEnv } from 'vite'; // 导入loadEnv方法
loadEnv(mode, process.cwd()).VITE_XXX; // 在需要访问env里变量的地方使用此方法访问即可
```

<a name="c61428f2"></a>

## -TailwindCSS 配置

```javascript
module.exports = {
  content: ['./index.html', './src/**/*.{html,js,vue}', './src/*.{html,js,vue}'], // 需要解析的文件路径
  theme: {
    // 自定义主题配置
    backgroundColor: {
      // 自定义背景色
      menuHover: '#272D39',
      headerBg: '#191E29',
    },
    textColor: (theme) => ({
      // 自定义字体颜色
      ...theme('colors'), // 这里必须解构原本有的颜色，不然会导致在页面style中使用@apply应用内部字体颜色类的时候报错找不到内部字体颜色类
      '40Gray': 'rgba(255,255,255,0.40)',
      '65Gray': 'rgba(255,255,255,0.65)',
    }),
    extend: {},
  },
  plugins: [],
};
```

<a name="c7baab5c"></a>

## -TS 配置-tsconfig.json

```json
{
  "include": [
    "src/**/*.ts",
    "src/**/*.d.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "src/components.d.ts",
    "auto-imports.d.ts"
  ], // TS解析路径配置
  "exclude": ["node_modules"],
  "compilerOptions": {
    "allowJs": true, // 允许编译器编译JS，JSX文件
    "noEmit": true,
    "target": "esnext", // 使用es最新语法
    "useDefineForClassFields": true,
    "allowSyntheticDefaultImports": true, // 允许异步导入模块，配合自动导入插件使用
    "module": "esnext", // 使用ES模块语法
    "moduleResolution": "node",
    "strict": true, // 严格模式
    "jsx": "preserve",
    "sourceMap": true, // 代码映射
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "lib": ["esnext", "dom"],
    "skipLibCheck": true, // 跳过node依赖包语法检查
    "types": [
      // "vitest/globals",
      // "vite-plugin-svg-icons/client"
    ], // 手动导入TS类型声明文件
    "baseUrl": ".",
    "paths": {
      // 路径映射
      "@/*": ["./src/*"],
      "#/*": ["types/*"]
    }
  }
}
```

<a name="50a6c7f6"></a>

## -vite 配置-插件详解

`vite`构建生产资源使用`rollup`打包，所以可以在`vite`中使用`rollup`支持的所有插件。

- 组件自动导入，使用`unplugin-auto-import/vite`插件实现自动导入组件、`unplugin-vue-components/vite`插件按需导入自定义组件，具体用法如下：

```typescript
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ArcoResolver } from 'unplugin-vue-components/resolvers'
import { vitePluginForArco } from '@arco-plugins/vite-vue'

export default () => defineConfig({
  plugins: [
    // 自定义组件自动引入
    AutoImport({å
      dts: 'src/auto-import.d.ts', // 输出声明文件地址(在使用typescript时必须配置，不然会导致页面使用未导入的组件时报错)
      resolvers: [ElementPlusResolver()], // ElementPlus自动导入
    }),
    // 自定义组件按需引入
    Components({
      dts: 'src/components.d.ts', // 输出声明文件地址(在使用typescript时必须配置，不然会导致页面使用未导入的组件时报错)
      dirs: ['src/components'], // 按需加载的文件夹
      resolvers: [
        ArcoResolver(), // ArcoDesignVue按需加载
      ],
    }),
    // 样式自动导入
    vitePluginForArco({})
  ]
})
```

- 使用`vite-plugin-svg-icons`插件实现自动加载 svg 图片，并通过封装 svg 组件的方式，一行代码使用 svg，配置如下：

```typescript
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';

export default () =>
  defineConfig({
    plugins: [
      createSvgIconsPlugin({
        // 指定svg读取的文件夹
        iconDirs: [resolve(process.cwd(), 'src/assets/icons/svg')],
        // 指定icon的读取名字，使用svg文件名为icon名字
        symbolId: 'icon-[dir]-[name]',
      }),
    ],
  });
```

- 使用`vite`自带插件`loadEnv`读取环境信息，可作环境判断，使用`rollup-plugin-visualizer`插件可分析打包后的文件体积，配置如下：

```typescript
import { defineConfig, loadEnv } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';

// 这里的mode参数为package.json文件配置的环境参数，使用`--mode XXX`，如"report: rimraf dist && vite build --mode analyze"
export default ({ mode }) =>
  defineConfig({
    plugins: [
      // 这里通过--mode analyze配置为分析模式，使用visualizer插件分析方法，输出report.html分析报告
      loadEnv(mode, process.cwd()).VITE_ANALYZE === 'Y'
        ? visualizer({ open: true, brotliSize: true, filename: 'report.html' })
        : null,
    ],
  });
```

<a name="0c0978d0"></a>

## -vite 配置-build 详解

上面讲述了插件应用，下面讲解除了插件外，在`vite`中的 build 构建生产资源时，`rollupOptions`的配置：

- `output`为输出产物配置，我们可以通过`manualChunks`去配置分包策略（与`webpack`分包机制类似），配置如下：

```typescript
import { mergeConfig } from 'vite';
import baseConfig from './vite.config.base';
import configCompressPlugin from './plugin/compress';
import configVisualizerPlugin from './plugin/visualizer';
import configArcoResolverPlugin from './plugin/arcoResolver';
import configImageminPlugin from './plugin/imagemin';

export default mergeConfig(
  {
    mode: 'production',
    plugins: [
      configCompressPlugin('gzip'),
      configVisualizerPlugin(),
      configArcoResolverPlugin(),
      configImageminPlugin(),
    ],
    build: {
      rollupOptions: {
        output: {
          manualChunks: {
            arco: ['@arco-design/web-vue'],
            chart: ['echarts', 'vue-echarts'],
            vue: ['vue', 'vue-router', 'pinia', '@vueuse/core', 'vue-i18n'],
          },
        },
      },
      chunkSizeWarningLimit: 2000,
    },
  },
  baseConfig
);
```

## 单元测试

测试框架：vitest

单元测试实用工具库： @vue/test-utils

测试环境: js-dom

报告: @vitest/coverage-c8

```bash
pnpm add -D vitest @vue/test-utils js-dom @vitest/coverage-c8
```

配置

根目录新建 vitest.config.ts

```typescript
import { mergeConfig } from 'vite';
import { defineConfig } from 'vitest/config';
import viteConfig from './config/vite.config.dev';

export default mergeConfig(
  viteConfig,
  defineConfig({
    test: {
      environment: 'jsdom',
    },
  })
);
```
