[
  {
    "path": ".gitignore",
    "content": "dist\ndist-ssr\nnode_modules\n\n*.local\n.DS_Store\nyarn.lock\npackage-lock.json"
  },
  {
    "path": "README.md",
    "content": "## Vite2项目最佳实践\n\n### 配套视频演示\n\n我专门录了一套视频演示本文所做的所有操作，喜欢看视频学习的小伙伴移步：\n[「备战2021」Vite2 + Vue3项目最佳实践](https://www.bilibili.com/video/BV1vX4y1K7bQ)\n\n制作不易，求`3连`，求`关注`\n\n### vite2来了\n\n`Vite1`还没用上，`Vite2`已经更新了，全新插件架构，丝滑的开发体验，和`Vue3`的完美结合。 2021年第一弹，村长打算以Vite2+Vue3为主题开启大家的前端学习之旅。\n\n### 2021先学学vite准没错\n\n![img](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a26ab28cab8d45a981986b581ae71d04~tplv-k3u1fbpfcp-zoom-1.image)\n\n### 本文目标\n\n- `vite2`变化分析\n- 项目中常见任务`vite2+vue3`实践\n\n\n### 创建Vite2项目\n\n闲言碎语不必说，下面我们表一表好汉`vite2`\n\n使用npm:\n\n```bash\n$ npm init @vitejs/app\n```\n\n> 按提示指定项目名称和模板，或直接指定\n>\n> ```bash\n> $ npm init @vitejs/app my-vue-app --template vue\n> ```\n\n\n\n### Vite2主要变化\n\n对我们之前项目影响较大的我已经都标记出来了：\n\n- 配置选项变化：`vue特有选项`、创建选项、css选项、jsx选项等\n- `别名行为变化`：不再要求`/`开头或结尾\n- `Vue支持`：通过 [@vitejs/plugin-vue](https://github.com/vitejs/vite/tree/main/packages/plugin-vue)插件支持\n- React支持\n- HMR API变化\n- 清单格式变化\n- `插件API重新设计`\n\n\n\n#### Vue支持\n\nVue的整合也通过插件实现，和其他框架一视同仁：\n\n<img src=\"https://gitee.com/57code/picgo/raw/master/image-20210114183159562.png\" style=\"zoom:80%;\" />\n\n\n\nSFC定义默认使用`setup script`，语法比较激进，但更简洁，好评！\n\n<img src=\"https://gitee.com/57code/picgo/raw/master/image-20210116192013356.png\" style=\"zoom:40%;\" />\n\n#### 别名定义\n\n不再需要像`vite1`一样在别名前后加上`/`，这和`webpack`项目配置可以保持一致便于移植，好评！\n\n```js\nimport path from 'path'\n\nexport default {\n  alias: {\n    \"@\": path.resolve(__dirname, \"src\"),\n    \"comps\": path.resolve(__dirname, \"src/components\"),\n  },\n}\n```\n\n`App.vue`里面用一下试试\n\n```vue\n<script setup>\nimport HelloWorld from 'comps/HelloWorld.vue'\n</script>\n```\n\n\n\n#### 插件API重新设计\n\n`Vite2`主要变化在插件体系，这样更标准化、易扩展。`Vite2`插件API扩展自`Rollup`插件体系，因此能兼容现存的`Rollup`插件，编写的Vite插件也可以同时运行于开发和创建，好评！\n\n> 插件编写我会另开专题讨论，欢迎大家关注我。\n\n\n\n##### Vue3 Jsx支持\n\n`vue3`中`jsx`支持需要引入插件：`@vitejs/plugin-vue-jsx`\n\n```bash\n$ npm i @vitejs/plugin-vue-jsx -D\n```\n\n注册插件，`vite.config.js`\n\n```js\nimport vueJsx from \"@vitejs/plugin-vue-jsx\";\n\nexport default {\n  plugins: [vue(), vueJsx()],\n}\n```\n\n用法也有要求，改造一下`App.vue`\n\n```vue\n<!-- 1.标记为jsx -->\n<script setup lang=\"jsx\">\nimport { defineComponent } from \"vue\";\nimport HelloWorld from \"comps/HelloWorld.vue\";\nimport logo from \"./assets/logo.png\"\n\n// 2.用defineComponent定义组件且要导出\nexport default defineComponent({\n  render: () => (\n    <>\n      <img alt=\"Vue logo\" src={logo} />\n      <HelloWorld msg=\"Hello Vue 3 + Vite\" />\n    </>\n  ),\n});\n</script>\n```\n\n\n\n##### Mock插件应用\n\n之前给大家介绍的[vite-plugin-mock](https://github.com/vbenjs/vite-plugin-mock)已经重构支持了Vite2。\n\n\n\n安装插件\n\n```bash\nnpm i mockjs -S\n```\n\n```bash\nnpm i vite-plugin-mock cross-env -D\n```\n\n\n\n配置，`vite.config.js`\n\n```js\nimport { viteMockServe } from 'vite-plugin-mock'\n\nexport default {\n  plugins: [ viteMockServe({ supportTs: false }) ]\n}\n```\n\n\n\n设置环境变量，`package.json`\n\n```json\n{\n  \"scripts\": {\n    \"dev\": \"cross-env NODE_ENV=development vite\",\n    \"build\": \"vite build\"\n  },\n} \n```\n\n\n\n\n\n### 项目基础架构\n\n#### 路由\n\n安装`vue-router 4.x`\n\n```js\nnpm i vue-router@next -S\n```\n\n<img src=\"https://gitee.com/57code/picgo/raw/master/image-20210118170758418.png\" style=\"zoom:33%;\" />\n\n\n\n路由配置，`router/index.js`\n\n```js\nimport { createRouter, createWebHashHistory } from 'vue-router';\n\nconst router = createRouter({\n  history: createWebHashHistory(),\n  routes: [\n    { path: '/', component: () => import('views/home.vue') }\n  ]\n});\n\nexport default router\n```\n\n\n\n引入，`main.js`\n\n```js\nimport router from \"@/router\";\ncreateApp(App).use(router).mount(\"#app\");\n```\n\n> 别忘了创建`home.vue`并修改`App.vue`\n>\n> 路由用法略有变化，[村长的视频教程](https://www.bilibili.com/video/BV1Wh411X7Xp?p=19)\n\n\n\n#### 状态管理\n\n安装`vuex 4.x`\n\n```bash\nnpm i vuex@next -S\n```\n\n<img src=\"https://gitee.com/57code/picgo/raw/master/image-20210118181504903.png\" alt=\"image\" style=\"zoom:33%;\" />\n\n\n\nStore配置，`store/index.js`\n\n```js\nimport {createStore} from 'vuex';\n\nexport default createStore({\n  state: {\n    couter: 0\n  }\n});\n```\n\n\n\n引入，`main.js`\n\n```js\nimport store from \"@/store\";\ncreateApp(App).use(store).mount(\"#app\");\n```\n\n> 用法和以前基本一样，[村长的视频教程](https://www.bilibili.com/video/BV1Wh411X7Xp?p=23)\n\n\n\n\n\n#### 样式组织\n\n 安装sass\n\n```bash\nnpm i sass -D\n```\n\n\n\n`styles`目录保存各种样式\n\n![截屏2020-12-24 上午11.51.30](https://gitee.com/57code/picgo/raw/master/%E6%88%AA%E5%B1%8F2020-12-24%20%E4%B8%8A%E5%8D%8811.51.30.png)\n\n`index.scss`作为出口组织这些样式，同时编写一些全局样式\n\n![image-20201224115414266](https://gitee.com/57code/picgo/raw/master/image-20201224115414266.png)\n\n最后在`main.js`导入\n\n```js\nimport \"styles/index.scss\";\n```\n\n> 注意在`vite.config.js`添加`styles`别名\n\n\n\n#### UI库\n\n就用我们[花果山团队](https://www.yuque.com/hugsun)自家的[element3](https://github.com/hug-sun/element3)。\n\n> [中文文档](https://element3-ui.com/)\n\n\n\n安装\n\n```bash\nnpm i element3 -S\n```\n\n\n\n完整引入，`main.js`\n\n```js\nimport element3 from \"element3\";\nimport \"element3/lib/theme-chalk/index.css\";\n\ncreateApp(App).use(element3)\n```\n\n\n\n按需引入，`main.js`\n\n```js\nimport \"element3/lib/theme-chalk/button.css\";\nimport { ElButton } from \"element3\"\ncreateApp(App).use(ElButton)\n```\n\n\n\n抽取成插件会更好，`plugins/element3.js`\n\n```js\n// 完整引入\nimport element3 from \"element3\";\nimport \"element3/lib/theme-chalk/index.css\";\n\n// 按需引入\n// import { ElButton } from \"element3\";\n// import \"element3/lib/theme-chalk/button.css\";\n\nexport default function (app) {\n  // 完整引入\n  app.use(element3)\n\n  // 按需引入\n  // app.use(ElButton);\n}\n```\n\n\n\n测试\n\n```html\n<el-button>my button</el-button>\n```\n\n\n\n#### 基础布局\n\n我们应用需要一个基本布局页，类似下图，将来每个页面以布局页为父页面即可：\n\n![image-20201223143247535](https://gitee.com/57code/picgo/raw/master/image-20201223143247535.png)\n\n\n\n布局页面，`layout/index.vue`\n\n```vue\n<template>\n  <div class=\"app-wrapper\">\n    <!-- 侧边栏 -->\n    <div class=\"sidebar-container\"></div>\n    <!-- 内容容器 -->\n    <div class=\"main-container\">\n      <!-- 顶部导航栏 -->\n      <navbar />\n      <!-- 内容区 -->\n      <app-main />\n    </div>\n  </div>\n</template>\n\n<script setup>\nimport AppMain from \"./components/AppMain.vue\";\nimport Navbar from \"./components/Navbar.vue\";\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"../styles/mixin.scss\";\n\n.app-wrapper {\n  @include clearfix;\n  position: relative;\n  height: 100%;\n  width: 100%;\n}\n</style>\n```\n\n> 别忘了创建`AppMain.vue`和`Navbar.vue`\n\n\n\n路由配置，`router/index.js`\n\n```js\n{\n  path: \"/\",\n\tcomponent: Layout,\n  children: [\n    {\n      path: \"\",\n      component: () => import('views/home.vue'),\n      name: \"Home\",\n      meta: { title: \"首页\", icon: \"el-icon-s-home\" },\n    },\n  ],\n},\n```\n\n\n\n#### 动态导航\n\n##### 侧边导航\n\n根据路由表动态生成侧边导航菜单。\n\n![image-20201225180300250](https://gitee.com/57code/picgo/raw/master/image-20201225180300250.png)\n\n\n\n首先创建侧边栏组件，递归输出`routes`中的配置为多级菜单，`layout/Sidebar/index.vue`\n\n```vue\n<template>\n  <el-scrollbar wrap-class=\"scrollbar-wrapper\">\n    <el-menu\n      :default-active=\"activeMenu\"\n      :background-color=\"variables.menuBg\"\n      :text-color=\"variables.menuText\"\n      :unique-opened=\"false\"\n      :active-text-color=\"variables.menuActiveText\"\n      mode=\"vertical\"\n    >\n      <sidebar-item\n        v-for=\"route in routes\"\n        :key=\"route.path\"\n        :item=\"route\"\n        :base-path=\"route.path\"\n      />\n    </el-menu>\n  </el-scrollbar>\n</template>\n\n<script setup>\nimport SidebarItem from \"./SidebarItem.vue\";\nimport { computed } from \"vue\";\nimport { useRoute } from \"vue-router\";\nimport { routes } from \"@/router\";\nimport variables from \"styles/variables.module.scss\";\n\nconst activeMenu = computed(() => {\n  const route = useRoute();\n  const { meta, path } = route;\n  if (meta.activeMenu) {\n    return meta.activeMenu;\n  }\n  return path;\n});\n</script>\n\n```\n\n> 注意：`sass`文件导出变量解析需要用到`css module`，因此`variables`文件要加上`module`中缀。\n\n\n\n添加相关样式：\n\n- `styles/variables.module.scss`\n- `styles/sidebar.scss`\n- `styles/index.scss`中引入\n\n\n\n创建`SidebarItem.vue`组件，解析当前路由是导航链接还是父菜单：\n\n![image-20201229123955087](https://gitee.com/57code/picgo/raw/master/image-20201229123955087.png)\n\n\n\n##### 面包屑\n\n通过路由匹配数组可以动态生成面包屑。\n\n\n\n面包屑组件，`layouts/components/Breadcrumb.vue`\n\n```vue\n<template>\n  <el-breadcrumb class=\"app-breadcrumb\" separator=\"/\">\n      <el-breadcrumb-item v-for=\"(item, index) in levelList\" :key=\"item.path\">\n        <span\n          v-if=\"item.redirect === 'noRedirect' || index == levelList.length - 1\"\n          class=\"no-redirect\"\n          >{{ item.meta.title }}</span>\n        <a v-else @click.prevent=\"handleLink(item)\">{{ item.meta.title }}</a>\n      </el-breadcrumb-item>\n  </el-breadcrumb>\n</template>\n\n<script setup>\nimport { compile } from \"path-to-regexp\";\nimport { reactive, ref, watch } from \"vue\";\nimport { useRoute, useRouter } from \"vue-router\";\n\nconst levelList = ref(null);\nconst router = useRouter();\nconst route = useRoute();\n\nconst getBreadcrumb = () => {\n  let matched = route.matched.filter((item) => item.meta && item.meta.title);\n\n  const first = matched[0];\n  if (first.path !== \"/\") {\n    matched = [{ path: \"/home\", meta: { title: \"首页\" } }].concat(matched);\n  }\n\n  levelList.value = matched.filter(\n    (item) => item.meta && item.meta.title && item.meta.breadcrumb !== false\n  );\n}\n\nconst pathCompile = (path) => {  \n  var toPath = compile(path);\n  return toPath(route.params);\n}\n\nconst handleLink = (item) => {\n  const { redirect, path } = item;\n  if (redirect) {\n    router.push(redirect);\n    return;\n  }\n  router.push(pathCompile(path));\n}\n\ngetBreadcrumb();\nwatch(route, getBreadcrumb)\n\n</script>\n\n<style lang=\"scss\" scoped>\n.app-breadcrumb.el-breadcrumb {\n  display: inline-block;\n  font-size: 14px;\n  line-height: 50px;\n  margin-left: 8px;\n\n  .no-redirect {\n    color: #97a8be;\n    cursor: text;\n  }\n}\n</style>\n```\n\n> 别忘了添加依赖：`path-to-regexp`\n>\n> 注意：`vue-router4`已经不再使用`path-to-regexp`解析动态`path`，因此这里后续还需要改进。\n\n\n\n#### 数据封装\n\n统一封装数据请求服务，有利于解决一下问题：\n\n- 统一配置请求\n- 请求、响应统一处理\n\n\n\n准备工作：\n\n- 安装`axios`: \n\n  ```bash\n  npm i axios -S\n  ```\n\n- 添加配置文件：`.env.development`\n\n  ```\n  VITE_BASE_API=/api\n  ```\n\n\n\n请求封装，`utils/request.js`\n\n```js\nimport axios from \"axios\";\nimport { Message, Msgbox } from \"element3\";\n\n// 创建axios实例\nconst service = axios.create({\n  // 在请求地址前面加上baseURL\n  baseURL: import.meta.env.VITE_BASE_API,\n  // 当发送跨域请求时携带cookie\n  // withCredentials: true,\n  timeout: 5000,\n});\n\n// 请求拦截\nservice.interceptors.request.use(\n  (config) => {\n    // 模拟指定请求令牌\n    config.headers[\"X-Token\"] = \"my token\";\n    return config;\n  },\n  (error) => {\n    // 请求错误的统一处理\n    console.log(error); // for debug\n    return Promise.reject(error);\n  }\n);\n\n// 响应拦截器\nservice.interceptors.response.use(\n  /**\n   * 通过判断状态码统一处理响应，根据情况修改\n   * 同时也可以通过HTTP状态码判断请求结果\n   */\n  (response) => {\n    const res = response.data;\n\n    // 如果状态码不是20000则认为有错误\n    if (res.code !== 20000) {\n      Message.error({\n        message: res.message || \"Error\",\n        duration: 5 * 1000,\n      });\n\n      // 50008: 非法令牌; 50012: 其他客户端已登入; 50014: 令牌过期;\n      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {\n        // 重新登录\n        Msgbox.confirm(\"您已登出, 请重新登录\", \"确认\", {\n          confirmButtonText: \"重新登录\",\n          cancelButtonText: \"取消\",\n          type: \"warning\",\n        }).then(() => {\n          store.dispatch(\"user/resetToken\").then(() => {\n            location.reload();\n          });\n        });\n      }\n      return Promise.reject(new Error(res.message || \"Error\"));\n    } else {\n      return res;\n    }\n  },\n  (error) => {\n    console.log(\"err\" + error); // for debug\n    Message({\n      message: error.message,\n      type: \"error\",\n      duration: 5 * 1000,\n    });\n    return Promise.reject(error);\n  }\n);\n\nexport default service;\n\n```\n\n\n\n#### 业务处理\n\n##### 结构化数据展示\n\n使用`el-table`展示结构化数据，配合`el-pagination`做数据分页。\n\n![image-20210201110626262](https://gitee.com/57code/picgo/raw/master/image-20210201110626262.png)\n\n文件组织结构如下：`list.vue`展示列表，`edit.vue`和`create.vue`编辑或创建，内部复用`detail.vue`处理，`model`中负责数据业务处理。\n\n![image-20210201110542893](https://gitee.com/57code/picgo/raw/master/image-20210201110542893.png)\n\n`list.vue`中的数据展示\n\n```vue\n<el-table v-loading=\"loading\" :data=\"list\">\n  <el-table-column label=\"ID\" prop=\"id\"></el-table-column>\n  <el-table-column label=\"账户名\" prop=\"name\"></el-table-column>\n  <el-table-column label=\"年龄\" prop=\"age\"></el-table-column>\n</el-table>\n```\n\n\n\n`list`和`loading`数据的获取逻辑，可以使用`compsition-api`提取到`userModel.js`\n\n```js\nexport function useList() {\n  // 列表数据\n  const state = reactive({\n    loading: true, // 加载状态\n    list: [], // 列表数据\n  });\n\n  // 获取列表\n  function getList() {\n    state.loading = true;\n    return request({\n      url: \"/getUsers\",\n      method: \"get\",\n    }).then(({ data, total }) => {\n      // 设置列表数据\n      state.list = data;\n    }).finally(() => {\n      state.loading = false;\n    });\n  }\n  \n  // 首次获取数据\n  getList();\n\n  return { state, getList };\n}\n```\n\n\n\n`list.vue`中使用\n\n```js\nimport { useList } from \"./model/userModel\";\n```\n\n```js\nconst { state, getList } = useList();\n```\n\n\n\n分页处理，`list.vue`\n\n```html\n<pagination\n      :total=\"total\"\n      v-model:page=\"listQuery.page\"\n      v-model:limit=\"listQuery.limit\"\n      @pagination=\"getList\"\n    ></pagination>\n```\n\n数据也在`userModel`中处理\n\n```js\nconst state = reactive({\n  total: 0,   // 总条数\n  listQuery: {// 分页查询参数\n    page: 1,  // 当前页码\n    limit: 5, // 每页条数\n  },\n});\n```\n\n```js\nrequest({\n  url: \"/getUsers\",\n  method: \"get\",\n  params: state.listQuery, // 在查询中加入分页参数\n})\n```\n\n\n\n##### 表单处理\n\n用户数据新增、编辑使用`el-form`处理\n\n\n\n可用一个组件`detail.vue`来处理，区别仅在于初始化时是否获取信息回填到表单。\n\n```html\n<el-form ref=\"form\" :model=\"model\" :rules=\"rules\">\n  <el-form-item prop=\"name\" label=\"用户名\">\n    <el-input v-model=\"model.name\"></el-input>\n  </el-form-item>\n  <el-form-item prop=\"age\" label=\"用户年龄\">\n    <el-input v-model.number=\"model.age\"></el-input>\n  </el-form-item>\n  <el-form-item>\n    <el-button @click=\"submitForm\" type=\"primary\">提交</el-button>\n  </el-form-item>\n</el-form>\n```\n\n\n\n数据处理同样可以提取到`userModel`中处理。\n\n```js\nexport function useItem(isEdit, id) {\n  const model = ref(Object.assign({}, defaultData));\n\n  // 初始化时，根据isEdit判定是否需要获取详情\n  onMounted(() => {\n    if (isEdit && id) {\n      // 获取详情\n      request({\n        url: \"/getUser\",\n        method: \"get\",\n        params: { id },\n      }).then(({ data }) => {\n        model.value = data;\n      });\n    }\n  });\n  return { model };\n}\n```\n\n### 配套视频演示\n\n我专门录了一套视频演示本文所做的所有操作，喜欢看视频学习的小伙伴移步：\n[「备战2021」Vite2 + Vue3项目最佳实践](https://www.bilibili.com/video/BV1vX4y1K7bQ)\n\n制作不易，求`3连`，求`关注`\n\n\n### 关注村长\n\n欢迎关注我的公众号「村长学前端」跟我一起学习最新前端知识。\n\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <link rel=\"icon\" href=\"/favicon.ico\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Vite App</title>\n</head>\n<body>\n  <div id=\"app\"></div>\n  <script type=\"module\" src=\"/src/main.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "mock/test.js",
    "content": "const mockList = [\n  { id: 1, name: \"tom\", age: 18 },\n  { id: 2, name: \"jerry\", age: 18 },\n  { id: 3, name: \"mike\", age: 18 },\n  { id: 4, name: \"jack\", age: 18 },\n  { id: 5, name: \"larry\", age: 18 },\n  { id: 6, name: \"white\", age: 18 },\n  { id: 7, name: \"peter\", age: 18 },\n  { id: 8, name: \"james\", age: 18 },\n];\n\nmodule.exports = [\n  {\n    url: \"/api/getUser\",\n    type: \"get\",\n    response: () => {\n      return {\n        code: 20000,\n        data: { id: 1, name: \"tom\", age: 18 },\n      };\n    },\n  },\n  {\n    url: \"/api/getUsers\",\n    type: \"get\",\n    response: (config) => {\n      // 从查询参数中获取分页、过滤关键词等参数\n      const { page = 1, limit = 5 } = config.query;\n\n      // 分页\n      const data = mockList.filter(\n        (item, index) => index < limit * page && index >= limit * (page - 1)\n      );\n\n      return {\n        code: 20000,\n        data,\n        total: mockList.length,\n      };\n    },\n  },\n  {\n    url: \"/api/addUser\",\n    type: \"post\",\n    response: () => {\n      // 直接返回\n      return {\n        code: 20000,\n      };\n    },\n  },\n  {\n    url: \"/api/updateUser\",\n    type: \"post\",\n    response: () => {\n      return {\n        code: 20000,\n      };\n    },\n  },\n  {\n    url: \"/api/deleteUser\",\n    type: \"get\",\n    response: () => {\n      return {\n        code: 20000,\n      };\n    },\n  },\n];\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"vite2-in-action\",\n  \"version\": \"0.0.0\",\n  \"license\": \"ISC\",\n  \"scripts\": {\n    \"dev\": \"cross-env NODE_ENV=development vite\",\n    \"build\": \"vite build\"\n  },\n  \"dependencies\": {\n    \"axios\": \"^0.21.1\",\n    \"element3\": \"0.0.39\",\n    \"js-yaml\": \"^4.0.0\",\n    \"mockjs\": \"^1.1.0\",\n    \"path-browserify\": \"^1.0.1\",\n    \"path-to-regexp\": \"^6.2.0\",\n    \"process\": \"^0.11.10\",\n    \"vue\": \"^3.0.5\",\n    \"vue-i18n\": \"^9.0.0-rc.7\",\n    \"vue-router\": \"^4.0.4\",\n    \"vuex\": \"^4.0.0\"\n  },\n  \"devDependencies\": {\n    \"@intlify/vite-plugin-vue-i18n\": \"^2.0.0-rc.2\",\n    \"@vitejs/plugin-vue\": \"^1.1.4\",\n    \"@vitejs/plugin-vue-jsx\": \"^1.1.0\",\n    \"@vue/compiler-sfc\": \"^3.0.5\",\n    \"cross-env\": \"^7.0.3\",\n    \"sass\": \"^1.32.8\",\n    \"vite\": \"^2.0.1\",\n    \"vite-plugin-mock\": \"^2.1.4\"\n  }\n}\n"
  },
  {
    "path": "src/App.vue",
    "content": "<template>\n  <router-view></router-view>\n</template>\n"
  },
  {
    "path": "src/components/HelloWorld.vue",
    "content": "<template>\n  <h1>{{ msg }}</h1>\n  <p>{{ $store.state.counter }}</p>\n\n  <!-- 国际化 -->\n  <form>\n    <label>{{ t('language') }}</label>\n    <select v-model=\"locale\">\n      <option value=\"en\">en</option>\n      <option value=\"ja\">ja</option>\n    </select>\n  </form>\n  <p>{{ t('hello') }}</p>\n\n  <el-button @click=\"state.count++\">count is: {{ state.count }}</el-button>\n  <p>\n    Edit\n    <code>components/HelloWorld.vue</code> to test hot module replacement.\n  </p>\n</template>\n\n<script setup>\nimport { defineProps, reactive } from \"vue\";\nimport { useI18n } from \"vue-i18n\";\n\ndefineProps({\n  msg: String,\n});\n\nconst state = reactive({ count: 0 });\nconst { locale, t } = useI18n({\n  inheritLocale: true,\n});\n</script>\n\n<i18n>\n{\n  \"en\": {\n    \"language\": \"Language\",\n    \"hello\": \"hello, world!\"\n  },\n  \"ja\": {\n    \"language\": \"言語\",\n    \"hello\": \"こんにちは、世界！\"\n  }\n}\n</i18n>\n\n<style scoped>\na {\n  color: #42b983;\n}\n</style>\n"
  },
  {
    "path": "src/components/Pagination.vue",
    "content": "<template>\n  <div :class=\"{ hidden: hidden }\" class=\"pagination-container\">\n    <el-pagination\n      :background=\"background\"\n      v-model:current-page=\"currentPage\"\n      v-model:page-size=\"pageSize\"\n      :layout=\"layout\"\n      :page-sizes=\"pageSizes\"\n      :total=\"total\"\n      v-bind=\"$attrs\"\n      @size-change=\"handleSizeChange\"\n      @current-change=\"handleCurrentChange\"\n    />\n  </div>\n</template>\n\n<script>\nexport default {\n  name: \"Pagination\",\n  props: {\n    total: {\n      required: true,\n      type: Number,\n    },\n    page: {\n      type: Number,\n      default: 1,\n    },\n    limit: {\n      type: Number,\n      default: 20,\n    },\n    pageSizes: {\n      type: Array,\n      default() {\n        return [10, 20, 30, 50];\n      },\n    },\n    layout: {\n      type: String,\n      default: \"total, sizes, prev, pager, next, jumper\",\n    },\n    background: {\n      type: Boolean,\n      default: true,\n    },\n    hidden: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  emits: [\"update:page\", \"update:limit\", \"pagination\"],\n  computed: {\n    currentPage: {\n      get() {\n        return this.page;\n      },\n      set(val) {\n        this.$emit(\"update:page\", val);\n      },\n    },\n    pageSize: {\n      get() {\n        return this.limit;\n      },\n      set(val) {\n        this.$emit(\"update:limit\", val);\n      },\n    },\n  },\n  methods: {\n    handleSizeChange(val) {\n      this.$emit(\"pagination\", { page: this.currentPage, limit: val });\n    },\n    handleCurrentChange(val) {\n      this.$emit(\"pagination\", { page: val, limit: this.pageSize });\n    },\n  },\n};\n</script>\n\n<style scoped>\n.pagination-container {\n  background: #fff;\n  padding: 32px 16px;\n}\n.pagination-container.hidden {\n  display: none;\n}\n</style>\n"
  },
  {
    "path": "src/layouts/components/AppMain.vue",
    "content": "<template>\n  <section class=\"app-main\">\n    <!-- 内部应该显示子路由页面信息 -->\n    <router-view v-slot=\"{ Component }\">\n      <component :is=\"Component\" />\n    </router-view>\n  </section>\n</template>\n\n<script>\nexport default {\n  name: \"AppMain\",\n};\n</script>\n\n<style scoped>\n.app-main {\n  /*50 = navbar  */\n  min-height: calc(100vh - 50px);\n  width: 100%;\n  position: relative;\n  overflow: hidden;\n}\n</style>\n"
  },
  {
    "path": "src/layouts/components/Breadcrumb.vue",
    "content": "<template>\n  <el-breadcrumb class=\"app-breadcrumb\" separator=\"/\">\n    <transition-group name=\"breadcrumb\">\n      <el-breadcrumb-item v-for=\"(item, index) in levelList\" :key=\"item.path\">\n        <span\n          v-if=\"item.redirect === 'noRedirect' || index == levelList.length - 1\"\n          class=\"no-redirect\"\n          >{{ item.meta.title }}</span\n        >\n        <a v-else @click.prevent=\"handleLink(item)\">{{ item.meta.title }}</a>\n      </el-breadcrumb-item>\n    </transition-group>\n  </el-breadcrumb>\n</template>\n\n<script setup>\nimport { compile } from \"path-to-regexp\";\nimport { reactive, ref, watch } from \"vue\";\nimport { useRoute, useRouter } from \"vue-router\";\n\nconst levelList = ref(null);\nconst router = useRouter();\nconst route = useRoute();\n\nconst getBreadcrumb = () => {\n  let matched = route.matched.filter((item) => item.meta && item.meta.title);\n\n  const first = matched[0];\n  if (first.path !== \"/\") {\n    matched = [{ path: \"/home\", meta: { title: \"首页\" } }].concat(matched);\n  }\n\n  levelList.value = matched.filter(\n    (item) => item.meta && item.meta.title && item.meta.breadcrumb !== false\n  );\n}\n\nconst pathCompile = (path) => {  \n  var toPath = compile(path);\n  return toPath(route.params);\n}\n\nconst handleLink = (item) => {\n  const { redirect, path } = item;\n  if (redirect) {\n    router.push(redirect);\n    return;\n  }\n  router.push(pathCompile(path));\n}\n\ngetBreadcrumb();\nwatch(route, getBreadcrumb)\n\n</script>\n\n<style lang=\"scss\" scoped>\n.app-breadcrumb.el-breadcrumb {\n  display: inline-block;\n  font-size: 14px;\n  line-height: 50px;\n  margin-left: 8px;\n\n  .no-redirect {\n    color: #97a8be;\n    cursor: text;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/layouts/components/Navbar.vue",
    "content": "<template>\n  <div class=\"navbar\">\n    <!-- 面包屑 -->\n    <breadcrumb class=\"breadcrumb-container\"></breadcrumb>\n\n    <!-- 右侧下拉菜单 -->\n    <div class=\"right-menu\">\n      <el-dropdown class=\"avatar-container\" trigger=\"click\">\n        <div class=\"avatar-wrapper\">\n          <img src=\"/src/assets/logo.png\" class=\"user-avatar\" />\n          <i class=\"el-icon-caret-bottom\" />\n        </div>\n        <el-dropdown-menu class=\"user-dropdown\">\n          <router-link to=\"/\">\n            <el-dropdown-item> 首页 </el-dropdown-item>\n          </router-link>\n          <a target=\"_blank\" href=\"https://github.com/57code/vite2-in-action/\">\n            <el-dropdown-item>我的Github</el-dropdown-item>\n          </a>\n        </el-dropdown-menu>\n      </el-dropdown>\n    </div>\n  </div>\n</template>\n<script setup>\nimport Breadcrumb from \"./Breadcrumb.vue\";\n</script>\n<style lang=\"scss\" scoped>\n.navbar {\n  height: 50px;\n  overflow: hidden;\n  position: relative;\n  background: #fff;\n  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);\n\n  .breadcrumb-container {\n    float: left;\n  }\n\n  .right-menu {\n    float: right;\n    height: 100%;\n    line-height: 50px;\n\n    &:focus {\n      outline: none;\n    }\n\n    .right-menu-item {\n      display: inline-block;\n      padding: 0 8px;\n      height: 100%;\n      font-size: 18px;\n      color: #5a5e66;\n      vertical-align: text-bottom;\n\n      &.hover-effect {\n        cursor: pointer;\n        transition: background 0.3s;\n\n        &:hover {\n          background: rgba(0, 0, 0, 0.025);\n        }\n      }\n    }\n\n    .avatar-container {\n      margin-right: 30px;\n\n      .avatar-wrapper {\n        margin-top: 5px;\n        position: relative;\n\n        .user-avatar {\n          cursor: pointer;\n          width: 40px;\n          height: 40px;\n          border-radius: 10px;\n        }\n\n        .el-icon-caret-bottom {\n          cursor: pointer;\n          position: absolute;\n          right: -20px;\n          top: 25px;\n          font-size: 12px;\n        }\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/layouts/components/Sidebar/Item.vue",
    "content": "<template>\n  <i v-if=\"icon\" class=\"sub-el-icon\" :class=\"icon\"></i>\n  <span v-if=\"title\">{{ title }}</span>\n</template>\n<script setup>\nimport { defineProps } from \"vue\";\n\ndefineProps({\n  icon: {\n    type: String,\n    default: \"\",\n  },\n  title: {\n    type: String,\n    default: \"\",\n  },\n});\n</script>\n\n<style scoped>\n.sub-el-icon {\n  color: currentColor;\n  width: 1em;\n  height: 1em;\n}\n</style>\n"
  },
  {
    "path": "src/layouts/components/Sidebar/Link.vue",
    "content": "<template>\n  <component :is=\"type\" v-bind=\"linkProps(to)\">\n    <slot />\n  </component>\n</template>\n\n<script setup>\nimport { isExternal as isExt } from \"utils/validate\";\nimport { computed, defineProps } from \"vue\";\n\nconst props = defineProps({\n  to: {\n    type: String,\n    required: true,\n  },\n});\n\nconst isExternal = computed(() => isExt(props.to));\n\n// type是一个计算属性\nconst type = computed(() => {\n  if (isExternal.value) {\n    return \"a\";\n  }\n  return \"router-link\";\n});\n\nconst linkProps = (to) => {\n  if (isExternal.value) {\n    return {\n      href: to,\n      target: \"_blank\",\n      rel: \"noopener\",\n    };\n  }\n  return { to };\n};\n</script>\n"
  },
  {
    "path": "src/layouts/components/Sidebar/SidebarItem.vue",
    "content": "<template>\n  <div v-if=\"!item.hidden\">\n    <template\n      v-if=\"\n        hasOneShowingChild(item.children, item) &&\n        (!onlyOneChild.children || onlyOneChild.noShowingChildren) &&\n        !item.alwaysShow\n      \"\n    >\n      <app-link v-if=\"onlyOneChild.meta\" :to=\"resolvePath(onlyOneChild.path)\">\n        <el-menu-item :index=\"resolvePath(onlyOneChild.path)\">\n          <item\n            :icon=\"onlyOneChild.meta.icon || (item.meta && item.meta.icon)\"\n            :title=\"onlyOneChild.meta.title\"\n          />\n        </el-menu-item>\n      </app-link>\n    </template>\n\n    <el-submenu\n      v-else\n      ref=\"subMenu\"\n      :index=\"resolvePath(item.path)\"\n      popper-append-to-body\n    >\n      <template #title>\n        <item\n          v-if=\"item.meta\"\n          :icon=\"item.meta && item.meta.icon\"\n          :title=\"item.meta.title\"\n        />\n      </template>\n      <sidebar-item\n        v-for=\"child in item.children\"\n        :key=\"child.path\"\n        :is-nest=\"true\"\n        :item=\"child\"\n        :base-path=\"resolvePath(child.path)\"\n        class=\"nest-menu\"\n      />\n    </el-submenu>\n  </div>\n</template>\n\n<script setup>\nimport path from \"path-browserify\";\nimport Item from \"./Item.vue\";\nimport AppLink from \"./Link.vue\";\nimport { isExternal } from \"utils/validate\";\nimport { defineProps, ref } from \"vue\";\n\nconst props = defineProps({\n  // route object\n  item: {\n    type: Object,\n    required: true,\n  },\n  isNest: {\n    type: Boolean,\n    default: false,\n  },\n  basePath: {\n    type: String,\n    default: \"\",\n  },\n});\nconst onlyOneChild = ref(null);\nconst hasOneShowingChild = (children = [], parent) => {\n  const showingChildren = children.filter((item) => {\n    if (item.hidden) {\n      return false;\n    } else {\n      // Temp set(will be used if only has one showing child)\n      onlyOneChild.value = item;\n      return true;\n    }\n  });\n  // When there is only one child router, the child router is displayed by default\n  if (showingChildren.length === 1) {\n    return true;\n  }\n  // Show parent if there are no child router to display\n  if (showingChildren.length === 0) {\n    onlyOneChild.value = { ...parent, path: \"\", noShowingChildren: true };\n    return true;\n  }\n  return false;\n};\nconst resolvePath = (routePath) => {\n  if (isExternal(routePath)) {\n    return routePath;\n  }\n  if (isExternal(props.basePath)) {\n    return props.basePath;\n  }\n  return path.resolve(props.basePath, routePath);\n};\n</script>\n"
  },
  {
    "path": "src/layouts/components/Sidebar/index.vue",
    "content": "<template>\n  <el-scrollbar wrap-class=\"scrollbar-wrapper\">\n    <el-menu\n      :default-active=\"activeMenu\"\n      :background-color=\"variables.menuBg\"\n      :text-color=\"variables.menuText\"\n      :unique-opened=\"false\"\n      :active-text-color=\"variables.menuActiveText\"\n      mode=\"vertical\"\n    >\n      <sidebar-item\n        v-for=\"route in routes\"\n        :key=\"route.path\"\n        :item=\"route\"\n        :base-path=\"route.path\"\n      />\n    </el-menu>\n  </el-scrollbar>\n</template>\n\n<script setup>\nimport SidebarItem from \"./SidebarItem.vue\";\nimport { computed } from \"vue\";\nimport { useRoute } from \"vue-router\";\nimport { routes } from \"/@/router\";\nimport variables from \"styles/variables.module.scss\";\n\nconst activeMenu = computed(() => {\n  const route = useRoute();\n  const { meta, path } = route;\n  if (meta.activeMenu) {\n    return meta.activeMenu;\n  }\n  return path;\n});\n</script>\n\n"
  },
  {
    "path": "src/layouts/index.vue",
    "content": "<template>\n  <div class=\"app-wrapper\">\n    <!-- 侧边栏 -->\n    <sidebar class=\"sidebar-container\"></sidebar>\n    <!-- 内容容器 -->\n    <div class=\"main-container\">\n      <!-- 顶部导航栏 -->\n      <navbar />\n      <!-- 内容区 -->\n      <app-main />\n    </div>\n  </div>\n</template>\n\n<script setup>\nimport AppMain from \"./components/AppMain.vue\";\nimport Navbar from \"./components/Navbar.vue\";\nimport Sidebar from \"./components/Sidebar/index.vue\";\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"../styles/mixin.scss\";\n\n.app-wrapper {\n  @include clearfix;\n  position: relative;\n  height: 100%;\n  width: 100%;\n}\n</style>\n"
  },
  {
    "path": "src/locales/en.json",
    "content": "{\n  \"language\": \"Language\",\n  \"hello\": \"hello, world!\"\n}"
  },
  {
    "path": "src/locales/jp.json",
    "content": "{\n  \"language\": \"言語\",\n  \"hello\": \"こんにちは、世界！\"\n}"
  },
  {
    "path": "src/main.js",
    "content": "import { createApp } from \"vue\";\nimport App from \"./App.vue\";\n\n// 全局样式\nimport \"styles/index.scss\";\n\n// element3\nimport element3 from \"plugins/element3\";\n\n// router\nimport router from \"/@/router\";\n\n// store\nimport store from \"/@/store\";\n\n// i18n\nimport { createI18n } from \"vue-i18n\";\nimport messages from \"@intlify/vite-plugin-vue-i18n/messages\";\nconst i18n = createI18n({\n  legacy: false,\n  locale: \"en\",\n  messages,\n});\n\ncreateApp(App).use(element3).use(router).use(store).use(i18n).mount(\"#app\");\n"
  },
  {
    "path": "src/plugins/element3.js",
    "content": "// 完整引入\nimport element3 from \"element3\";\nimport \"element3/lib/theme-chalk/index.css\";\n\n// 按需引入\n// import \"element3/lib/theme-chalk/button.css\";\n// import {\n  // ElRow,\n  // ElCol,\n  // ElContainer,\n  // ElHeader,\n  // ElFooter,\n  // ElAside,\n  // ElMain,\n  // ElIcon,\n  // ElButton,\n  // ElLink,\n  // ElRadio,\n  // ElRadioButton,\n  // ElRadioGroup,\n  // ElCheckbox,\n  // ElCheckboxButton,\n  // ElCheckboxGroup,\n  // ElInput,\n  // ElInputNumber,\n  // ElSelect,\n  // ElOption,\n  // ElOptionGroup,\n  // ElCascader,\n  // ElCascaderPanel,\n  // ElSwitch,\n  // ElSlider,\n  // ElTimePicker,\n  // ElTimeSelect,\n  // ElDatePicker,\n  // ElUpload,\n  // ElRate,\n  // ElColorPicker,\n  // ElTransfer,\n  // ElForm,\n  // ElFormItem,\n  // ElTag,\n  // ElProgress,\n  // ElTree,\n  // ElPagination,\n  // ElBadge,\n  // ElAvatar,\n  // ElAlert,\n  // ElLoading,\n  // ElMenu,\n  // ElMenuItem,\n  // ElSubmenu,\n  // ElMenuItemGroup,\n  // ElTabs,\n  // ElTabPane,\n  // ElBreadcrumb,\n  // ElBreadcrumbItem,\n  // ElPageHeader,\n  // ElDropdown,\n  // ElDropdownItem,\n  // ElDropdownMenu,\n  // ElSteps,\n  // ElStep,\n  // ElDialog,\n  // ElTooltip,\n  // ElPopover,\n  // ElPopconfirm,\n  // ElCard,\n  // ElCarousel,\n  // ElCarouselItem,\n  // ElCollapse,\n  // ElCollapseItem,\n  // ElTimeline,\n  // ElTimelineItem,\n  // ElDivider,\n  // ElCalendar,\n  // ElImage,\n  // ElBacktop,\n  // ElInfiniteScroll,\n  // ElDrawer,\n  // ElScrollbar,\n// } from \"element3\";\n\nexport default function (app) {\n  // 完整引入\n  app.use(element3)\n\n  // 按需引入\n  // app.use(ElButton);\n}\n"
  },
  {
    "path": "src/router/index.js",
    "content": "import { createRouter, createWebHashHistory } from \"vue-router\";\nimport Layout from \"layouts/index.vue\";\n\n/**\n * Note: 子菜单仅当路由的children.length >= 1时才出现\n *\n * hidden: true                   设置为true时路由将显示在sidebar中(默认false)\n * alwaysShow: true               如果设置为true则总是显示在菜单根目录\n *                                如果不设置alwaysShow, 当路由有超过一个子路由时,\n *                                将会变为嵌套模式, 否则不会显示根菜单\n * redirect: noRedirect           如果设置noRedirect时，breadcrumb中点击将不会跳转\n * name:'router-name'             name用于<keep-alive> (必须设置!!!)\n * meta : {\n    roles: ['admin','editor']    页面可访问角色设置 \n    title: 'title'               sidebar和breadcrumb显示的标题 \n    icon: 'svg-name'/'el-icon-x' sidebar中显示的图标\n    breadcrumb: false            设置为false，将不会出现在面包屑中\n    activeMenu: '/example/list'  如果设置一个path, sidebar将会在高亮匹配项\n  }\n */\nexport const routes = [\n  {\n    path: \"/\",\n    redirect: \"/home\",\n    component: Layout,\n    meta: { title: \"导航\", icon: \"el-icon-s-home\" },\n    children: [\n      {\n        path: \"home\",\n        component: () => import(\"views/home.vue\"),\n        name: \"Home\",\n        meta: { title: \"首页\", icon: \"el-icon-s-home\" },\n        children: [\n          {\n            path: \":id\",\n            component: () => import(\"views/detail.vue\"),\n            name: \"Detail\",\n            hidden: true,\n            meta: {\n              title: \"详情\",\n              icon: \"el-icon-s-home\",\n              activeMenu: \"/home\",\n            },\n          },\n        ],\n      },\n    ],\n  },\n\n  {\n    path: \"/users\",\n    component: Layout,\n    meta: {\n      title: \"用户管理\",\n      icon: \"el-icon-user-solid\",\n    },\n    redirect: '/users/list',\n    children: [\n      {\n        path: \"list\",\n        component: () => import(\"views/users/list.vue\"),\n        meta: {\n          title: \"用户列表\",\n          icon: \"el-icon-document\",\n        },\n      },\n      {\n        path: \"create\",\n        component: () => import(\"views/users/create.vue\"),\n        hidden: true,\n        meta: {\n          title: \"创建新用户\",\n          activeMenu: \"/users/list\",\n        },\n      },\n      {\n        path: \"edit/:id(\\\\d+)\",\n        name: \"userEdit\",\n        component: () => import(\"views/users/edit.vue\"),\n        hidden: true,\n        meta: {\n          title: \"编辑用户信息\",\n          activeMenu: \"/users/list\",\n        },\n      },\n    ],\n  },\n];\n\nconst router = createRouter({\n  history: createWebHashHistory(),\n  routes,\n});\n\nexport default router;\n"
  },
  {
    "path": "src/store/index.js",
    "content": "import { createStore } from \"vuex\";\n\nconst store = createStore({\n  state: {\n    counter: 0,\n  },\n});\n\nexport default store;\n"
  },
  {
    "path": "src/styles/index.scss",
    "content": "@import \"./mixin.scss\";\n@import \"./variables.module.scss\";\n@import \"./sidebar.scss\";\n\n// 编写全局样式\nbody {\n  height: 100%;\n  -moz-osx-font-smoothing: grayscale;\n  -webkit-font-smoothing: antialiased;\n  text-rendering: optimizeLegibility;\n  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB,\n    Microsoft YaHei, Arial, sans-serif;\n  margin: 0;\n}\n\nlabel {\n  font-weight: 700;\n}\n\nhtml {\n  height: 100%;\n  box-sizing: border-box;\n}\n\n#app {\n  height: 100%;\n  font-family: Avenir, Helvetica, Arial, sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  text-align: center;\n  color: #2c3e50;\n}\n\n*,\n*:before,\n*:after {\n  box-sizing: inherit;\n}\n\na:focus,\na:active {\n  outline: none;\n}\n\na,\na:focus,\na:hover {\n  cursor: pointer;\n  color: inherit;\n  text-decoration: none;\n}\n\ndiv:focus {\n  outline: none;\n}\n\n.clearfix {\n  &:after {\n    visibility: hidden;\n    display: block;\n    font-size: 0;\n    content: \" \";\n    clear: both;\n    height: 0;\n  }\n}\n\n// main-container global css\n.app-container {\n  padding: 20px;\n}\n"
  },
  {
    "path": "src/styles/mixin.scss",
    "content": "@mixin clearfix {\n  &:after {\n    content: \"\";\n    display: table;\n    clear: both;\n  }\n}\n\n@mixin scrollBar {\n  &::-webkit-scrollbar-track-piece {\n    background: #d3dce6;\n  }\n\n  &::-webkit-scrollbar {\n    width: 6px;\n  }\n\n  &::-webkit-scrollbar-thumb {\n    background: #99a9bf;\n    border-radius: 20px;\n  }\n}\n\n@mixin relative {\n  position: relative;\n  width: 100%;\n  height: 100%;\n}\n"
  },
  {
    "path": "src/styles/sidebar.scss",
    "content": "#app {\n\n  .main-container {\n    min-height: 100%;\n    transition: margin-left .28s;\n    margin-left: $sideBarWidth;\n    position: relative;\n  }\n\n  .sidebar-container {\n    transition: width 0.28s;\n    width: $sideBarWidth !important;\n    background-color: $menuBg;\n    height: 100%;\n    position: fixed;\n    font-size: 0px;\n    top: 0;\n    bottom: 0;\n    left: 0;\n    z-index: 1001;\n    overflow: hidden;\n\n    // reset element-ui css\n    .horizontal-collapse-transition {\n      transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;\n    }\n\n    .scrollbar-wrapper {\n      overflow-x: hidden !important;\n    }\n\n    .el-scrollbar__bar.is-vertical {\n      right: 0px;\n    }\n\n    .el-scrollbar {\n      height: 100%;\n    }\n\n    &.has-logo {\n      .el-scrollbar {\n        height: calc(100% - 50px);\n      }\n    }\n\n    .is-horizontal {\n      display: none;\n    }\n\n    a {\n      display: inline-block;\n      width: 100%;\n      overflow: hidden;\n    }\n\n    .svg-icon {\n      margin-right: 16px;\n    }\n\n    .sub-el-icon {\n      margin-right: 12px;\n      margin-left: -2px;\n    }\n\n    .el-menu {\n      border: none;\n      height: 100%;\n      width: 100% !important;\n    }\n\n    // menu hover\n    .submenu-title-noDropdown,\n    .el-submenu__title {\n      &:hover {\n        background-color: $menuHover !important;\n      }\n    }\n\n    .is-active>.el-submenu__title {\n      color: $subMenuActiveText !important;\n    }\n\n    & .nest-menu .el-submenu>.el-submenu__title,\n    & .el-submenu .el-menu-item {\n      min-width: $sideBarWidth !important;\n      background-color: $subMenuBg !important;\n\n      &:hover {\n        background-color: $subMenuHover !important;\n      }\n    }\n  }\n\n  .hideSidebar {\n    .sidebar-container {\n      width: 50px !important;\n    }\n\n    .main-container {\n      margin-left: 54px;\n    }\n\n    .submenu-title-noDropdown {\n      padding: 0 !important;\n      position: relative;\n\n      .el-tooltip {\n        padding: 0 !important;\n\n        .svg-icon {\n          margin-left: 20px;\n        }\n\n        .sub-el-icon {\n          margin-left: 19px;\n        }\n      }\n    }\n\n    .el-submenu {\n      overflow: hidden;\n\n      &>.el-submenu__title {\n        padding: 0 !important;\n\n        .svg-icon {\n          margin-left: 20px;\n        }\n\n        .sub-el-icon {\n          margin-left: 19px;\n        }\n\n        .el-submenu__icon-arrow {\n          display: none;\n        }\n      }\n    }\n\n    .el-menu--collapse {\n      .el-submenu {\n        &>.el-submenu__title {\n          &>span {\n            height: 0;\n            width: 0;\n            overflow: hidden;\n            visibility: hidden;\n            display: inline-block;\n          }\n        }\n      }\n    }\n  }\n\n  .el-menu--collapse .el-menu .el-submenu {\n    min-width: $sideBarWidth !important;\n  }\n\n  // mobile responsive\n  .mobile {\n    .main-container {\n      margin-left: 0px;\n    }\n\n    .sidebar-container {\n      transition: transform .28s;\n      width: $sideBarWidth !important;\n    }\n\n    &.hideSidebar {\n      .sidebar-container {\n        pointer-events: none;\n        transition-duration: 0.3s;\n        transform: translate3d(-$sideBarWidth, 0, 0);\n      }\n    }\n  }\n\n  .withoutAnimation {\n\n    .main-container,\n    .sidebar-container {\n      transition: none;\n    }\n  }\n}\n\n// when menu collapsed\n.el-menu--vertical {\n  &>.el-menu {\n    .svg-icon {\n      margin-right: 16px;\n    }\n    .sub-el-icon {\n      margin-right: 12px;\n      margin-left: -2px;\n    }\n  }\n\n  .nest-menu .el-submenu>.el-submenu__title,\n  .el-menu-item {\n    &:hover {\n      // you can use $subMenuHover\n      background-color: $menuHover !important;\n    }\n  }\n\n  // the scroll bar appears when the subMenu is too long\n  >.el-menu--popup {\n    max-height: 100vh;\n    overflow-y: auto;\n\n    &::-webkit-scrollbar-track-piece {\n      background: #d3dce6;\n    }\n\n    &::-webkit-scrollbar {\n      width: 6px;\n    }\n\n    &::-webkit-scrollbar-thumb {\n      background: #99a9bf;\n      border-radius: 20px;\n    }\n  }\n}\n"
  },
  {
    "path": "src/styles/variables.module.scss",
    "content": "// sidebar\n$menuText:#bfcbd9;\n$menuActiveText:#409EFF;\n$subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951\n\n$menuBg:#304156;\n$menuHover:#263445;\n\n$subMenuBg:#1f2d3d;\n$subMenuHover:#001528;\n\n$sideBarWidth: 210px;\n\n// the :export directive is the magic sauce for webpack\n// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass\n:export {\n  menuText: $menuText;\n  menuActiveText: $menuActiveText;\n  subMenuActiveText: $subMenuActiveText;\n  menuBg: $menuBg;\n  menuHover: $menuHover;\n  subMenuBg: $subMenuBg;\n  subMenuHover: $subMenuHover;\n  sideBarWidth: $sideBarWidth;\n}\n"
  },
  {
    "path": "src/utils/request.js",
    "content": "import axios from \"axios\";\nimport { Message, Msgbox } from \"element3\";\nimport store from \"/@/store\";\n\n// 创建axios实例\nconst service = axios.create({\n  // 在请求地址前面加上baseURL\n  baseURL: import.meta.env.VITE_BASE_API,\n  // 当发送跨域请求时携带cookie\n  // withCredentials: true,\n  timeout: 5000,\n});\n\n// 请求拦截\nservice.interceptors.request.use(\n  (config) => {\n    // 指定请求令牌\n    // if (store.getters.token) {\n    // // 自定义令牌的字段名为X-Token，根据咱们后台再做修改\n    // config.headers[\"X-Token\"] = store.getters.token;\n    // }\n    config.headers[\"X-Token\"] = \"my token\";\n    return config;\n  },\n  (error) => {\n    // 请求错误的统一处理\n    console.log(error); // for debug\n    return Promise.reject(error);\n  }\n);\n\n// 响应拦截器\nservice.interceptors.response.use(\n  /**\n   * If you want to get http information such as headers or status\n   * Please return  response => response\n   */\n\n  /**\n   * 通过判断状态码统一处理响应，根据情况修改\n   * 同时也可以通过HTTP状态码判断请求结果\n   */\n  (response) => {\n    const res = response.data;\n\n    // 如果状态码不是20000则认为有错误\n    if (res.code !== 20000) {\n      Message.error({\n        message: res.message || \"Error\",\n        duration: 5 * 1000,\n      });\n\n      // 50008: 非法令牌; 50012: 其他客户端已登入; 50014: 令牌过期;\n      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {\n        // 重新登录\n        Msgbox.confirm(\"您已登出, 请重新登录\", \"确认\", {\n          confirmButtonText: \"重新登录\",\n          cancelButtonText: \"取消\",\n          type: \"warning\",\n        }).then(() => {\n          store.dispatch(\"user/resetToken\").then(() => {\n            location.reload();\n          });\n        });\n      }\n      return Promise.reject(new Error(res.message || \"Error\"));\n    } else {\n      return res;\n    }\n  },\n  (error) => {\n    console.log(\"err\" + error); // for debug\n    Message({\n      message: error.message,\n      type: \"error\",\n      duration: 5 * 1000,\n    });\n    return Promise.reject(error);\n  }\n);\n\nexport default service;\n"
  },
  {
    "path": "src/utils/validate.js",
    "content": "export function isExternal(path) {\n  return /^(https?:|mailto:|tel:)/.test(path);\n}"
  },
  {
    "path": "src/views/detail.vue",
    "content": "<template>\n  <div>\n    detail <span>{{$route.params.id}}</span>\n  </div>\n</template>\n\n<script setup>\n  \n</script>\n\n<style scoped>\n\n</style>"
  },
  {
    "path": "src/views/home.vue",
    "content": "<template>\n  <div>\n    <HelloWorld msg=\"hello vue3 + vite\"></HelloWorld>\n    <router-link to=\"/home/1\">detail1</router-link>\n    <router-link to=\"/home/2\">detail2</router-link>\n    <router-view></router-view>\n  </div>\n</template>\n\n<script setup>\n  import HelloWorld from '/@/components/HelloWorld.vue';\n</script>\n\n<style scoped>\n\n</style>"
  },
  {
    "path": "src/views/users/components/detail.vue",
    "content": "<template>\n  <div class=\"container\">\n    <el-form ref=\"form\" :model=\"model\" :rules=\"rules\">\n      <el-form-item prop=\"name\" label=\"用户名\">\n        <el-input v-model=\"model.name\"></el-input>\n      </el-form-item>\n      <el-form-item prop=\"age\" label=\"用户年龄\">\n        <el-input v-model.number=\"model.age\"></el-input>\n      </el-form-item>\n      <el-form-item>\n        <el-button @click=\"submitForm\" type=\"primary\">提交</el-button>\n      </el-form-item>\n    </el-form>\n  </div>\n</template>\n\n<script>\nimport { Message } from \"element3\";\nimport { reactive, ref } from \"vue\";\nimport { useRoute } from \"vue-router\";\nimport { useItem } from \"../model/userModel\";\n\nexport default {\n  props: {\n    isEdit: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  setup(props) {\n    // 路由\n    const route = useRoute();\n    const { model, addUser, updateUser } = useItem(props.isEdit, route.params.id);\n    const rules = reactive({\n      // 校验规则\n      name: [{ required: true, message: \"用户名为必填项\" }],\n    });\n\n    // 表单实例\n    const form = ref(null);\n    // 提交表单\n    function submitForm() {\n      // 校验\n      form.value.validate((valid) => {\n        if (valid) {\n          // 提交\n          if (props.isEdit) {\n            updateUser().then(() => {\n              // 操作成功提示信息\n              Message.success({\n                title: \"操作成功\",\n                message: \"更新用户数据成功\",\n                duration: 2000,\n              });\n            });\n          } else {\n            addUser().then(() => {\n              // 操作成功提示信息\n              Message.success({\n                title: \"操作成功\",\n                message: \"新增玩家数据成功\",\n                duration: 2000,\n              });\n            });\n          }\n        }\n      });\n    }\n\n    return {\n      model,\n      rules,\n      form,\n      submitForm,\n    };\n  },\n};\n</script>\n\n<style scoped>\n.container {\n  padding: 10px;\n}\n</style>\n<style>\n.avatar-uploader .el-upload {\n  border: 1px dashed #d9d9d9;\n  border-radius: 6px;\n  cursor: pointer;\n  position: relative;\n  overflow: hidden;\n}\n.avatar-uploader .el-upload:hover {\n  border-color: #409eff;\n}\n.avatar-uploader-icon {\n  font-size: 28px;\n  color: #8c939d;\n  width: 178px;\n  height: 178px;\n  line-height: 178px;\n  text-align: center;\n}\n.avatar {\n  width: 178px;\n  height: 178px;\n  display: block;\n}\n</style>\n"
  },
  {
    "path": "src/views/users/create.vue",
    "content": "<template>\n  <detail :is-edit=\"false\"></detail>\n</template>\n\n<script>\nimport Detail from \"./components/detail.vue\";\n\nexport default {\n  components: {\n    Detail,\n  },\n};\n</script>\n"
  },
  {
    "path": "src/views/users/edit.vue",
    "content": "<template>\n  <detail :is-edit=\"true\"></detail>\n</template>\n\n<script>\nimport Detail from \"./components/detail.vue\";\n\nexport default {\n  components: {\n    Detail,\n  },\n};\n</script>\n"
  },
  {
    "path": "src/views/users/list.vue",
    "content": "<template>\n  <div class=\"app-container\">\n    <div class=\"btn-container\">\n      <!-- 新增按钮 -->\n      <router-link to=\"/users/create\">\n        <el-button type=\"success\" icon=\"el-icon-edit\">创建用户</el-button>\n      </router-link>\n    </div>\n\n    <el-table\n      v-loading=\"loading\"\n      :data=\"list\"\n      border\n      fit\n      highlight-current-row\n      style=\"width: 100%\"\n    >\n      <el-table-column align=\"center\" label=\"ID\" prop=\"id\"></el-table-column>\n      <el-table-column align=\"center\" label=\"账户名\" prop=\"name\">\n      </el-table-column>\n      <el-table-column align=\"center\" label=\"年龄\" prop=\"age\">\n      </el-table-column>\n      <!-- 操作列 -->\n      <el-table-column label=\"操作\" align=\"center\">\n        <template v-slot=\"scope\">\n          <el-button\n            type=\"primary\"\n            icon=\"el-icon-edit\"\n            @click=\"handleEdit(scope)\"\n            >更新</el-button\n          >\n          <el-button\n            type=\"danger\"\n            icon=\"el-icon-remove\"\n            @click=\"handleDelete(scope)\"\n            >删除</el-button\n          >\n        </template>\n      </el-table-column>\n    </el-table>\n\n    <!-- 分页 -->\n    <pagination\n      v-show=\"total > 0\"\n      :total=\"total\"\n      v-model:page=\"listQuery.page\"\n      v-model:limit=\"listQuery.limit\"\n      @pagination=\"getList\"\n    ></pagination>\n  </div>\n</template>\n\n<script>\nimport { toRefs } from \"vue\";\nimport { useRouter } from \"vue-router\";\nimport { Message } from \"element3\";\nimport Pagination from \"comps/Pagination.vue\";\nimport { useList } from \"./model/userModel\";\n\nexport default {\n  name: \"UserList\",\n  components: {\n    Pagination,\n  },\n  setup() {\n    // 玩家列表数据\n    const router = useRouter();\n    const { state, getList, delItem } = useList();\n\n    // 用户更新\n    function handleEdit({ row }) {\n      router.push({\n        name: \"userEdit\",\n        params: { id: row.id },\n      });\n    }\n\n    // 删除玩家\n    function handleDelete({ row }) {\n      delItem(row.id).then(() => {\n        // todo:删除这一行，或者重新获取数据\n        // 通知用户\n        Message.success(\"删除成功！\");\n      });\n    }\n\n    return {\n      ...toRefs(state),\n      getList,\n      handleEdit,\n      handleDelete,\n    };\n  },\n};\n</script>\n\n<style scoped>\n.btn-container {\n  text-align: left;\n  padding: 0px 10px 20px 0px;\n}\n</style>\n"
  },
  {
    "path": "src/views/users/model/userModel.js",
    "content": "import { reactive, onMounted, ref } from \"vue\";\nimport request from \"utils/request\";\n\nexport function useList() {\n  // 列表数据\n  const state = reactive({\n    loading: true, // 加载状态\n    list: [], // 列表数据\n    total: 0,\n    listQuery: {\n      page: 1,\n      limit: 5,\n    },\n  });\n\n  // 获取列表\n  function getList() {\n    state.loading = true;\n\n    return request({\n      url: \"/getUsers\",\n      method: \"get\",\n      params: state.listQuery,\n    })\n      .then(({ data, total }) => {\n        // 设置列表数据\n        state.list = data;\n        state.total = total;\n      })\n      .finally(() => {\n        state.loading = false;\n      });\n  }\n\n  // 删除项\n  function delItem(id) {\n    state.loading = true;\n\n    return request({\n      url: \"/deleteUser\",\n      method: \"get\",\n      params: { id },\n    }).finally(() => {\n      state.loading = false;\n    });\n  }\n\n  // 首次获取数据\n  getList();\n\n  return { state, getList, delItem };\n}\n\nconst defaultData = {\n  name: \"\",\n  age: undefined,\n};\n\nexport function useItem(isEdit, id) {\n  const model = ref(Object.assign({}, defaultData));\n\n  // 初始化时，根据isEdit判定是否需要获取玩家详情\n  onMounted(() => {\n    if (isEdit && id) {\n      // 获取玩家详情\n      request({\n        url: \"/getUser\",\n        method: \"get\",\n        params: { id },\n      }).then(({ data }) => {\n        model.value = data;\n      });\n    }\n  });\n\n  const updateUser = () => {\n    return request({\n      url: \"/updateUser\",\n      method: \"post\",\n      data: model.value,\n    });\n  };\n\n  const addUser = () => {\n    return request({\n      url: \"/addUser\",\n      method: \"post\",\n      data: model.value,\n    });\n  };\n\n  return { model, updateUser, addUser };\n}\n"
  },
  {
    "path": "vite.config.js",
    "content": "import path from \"path\";\nimport vue from \"@vitejs/plugin-vue\";\nimport vueJsx from \"@vitejs/plugin-vue-jsx\";\nimport { viteMockServe } from \"vite-plugin-mock\";\n// 导入插件\nimport vueI18n from '@intlify/vite-plugin-vue-i18n'\n\nexport default {\n  resolve: {\n    alias: {\n      \"/@\": path.resolve(__dirname, \"src\"),\n      comps: path.resolve(__dirname, \"src/components\"),\n      styles: path.resolve(__dirname, \"src/styles\"),\n      plugins: path.resolve(__dirname, \"src/plugins\"),\n      views: path.resolve(__dirname, \"src/views\"),\n      layouts: path.resolve(__dirname, \"src/layouts\"),\n      utils: path.resolve(__dirname, \"src/utils\"),\n      apis: path.resolve(__dirname, \"src/apis\"),\n      dirs: path.resolve(__dirname, \"src/directives\"),\n    },\n  },\n  plugins: [vue(), vueJsx(), viteMockServe({ supportTs: false }), vueI18n({\n    // if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`\n    // compositionOnly: false,\n\n    // you need to set i18n resource including paths !\n    include: path.resolve(__dirname, './src/locales/**')\n  })],\n};\n"
  }
]