Repository: 57code/vite2-in-action
Branch: master
Commit: 35f0ca9a0773
Files: 36
Total size: 47.7 KB
Directory structure:
gitextract_agjgct2v/
├── .gitignore
├── README.md
├── index.html
├── mock/
│ └── test.js
├── package.json
├── src/
│ ├── App.vue
│ ├── components/
│ │ ├── HelloWorld.vue
│ │ └── Pagination.vue
│ ├── layouts/
│ │ ├── components/
│ │ │ ├── AppMain.vue
│ │ │ ├── Breadcrumb.vue
│ │ │ ├── Navbar.vue
│ │ │ └── Sidebar/
│ │ │ ├── Item.vue
│ │ │ ├── Link.vue
│ │ │ ├── SidebarItem.vue
│ │ │ └── index.vue
│ │ └── index.vue
│ ├── locales/
│ │ ├── en.json
│ │ └── jp.json
│ ├── main.js
│ ├── plugins/
│ │ └── element3.js
│ ├── router/
│ │ └── index.js
│ ├── store/
│ │ └── index.js
│ ├── styles/
│ │ ├── index.scss
│ │ ├── mixin.scss
│ │ ├── sidebar.scss
│ │ └── variables.module.scss
│ ├── utils/
│ │ ├── request.js
│ │ └── validate.js
│ └── views/
│ ├── detail.vue
│ ├── home.vue
│ └── users/
│ ├── components/
│ │ └── detail.vue
│ ├── create.vue
│ ├── edit.vue
│ ├── list.vue
│ └── model/
│ └── userModel.js
└── vite.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
dist
dist-ssr
node_modules
*.local
.DS_Store
yarn.lock
package-lock.json
================================================
FILE: README.md
================================================
## Vite2项目最佳实践
### 配套视频演示
我专门录了一套视频演示本文所做的所有操作,喜欢看视频学习的小伙伴移步:
[「备战2021」Vite2 + Vue3项目最佳实践](https://www.bilibili.com/video/BV1vX4y1K7bQ)
制作不易,求`3连`,求`关注`
### vite2来了
`Vite1`还没用上,`Vite2`已经更新了,全新插件架构,丝滑的开发体验,和`Vue3`的完美结合。 2021年第一弹,村长打算以Vite2+Vue3为主题开启大家的前端学习之旅。
### 2021先学学vite准没错

### 本文目标
- `vite2`变化分析
- 项目中常见任务`vite2+vue3`实践
### 创建Vite2项目
闲言碎语不必说,下面我们表一表好汉`vite2`
使用npm:
```bash
$ npm init @vitejs/app
```
> 按提示指定项目名称和模板,或直接指定
>
> ```bash
> $ npm init @vitejs/app my-vue-app --template vue
> ```
### Vite2主要变化
对我们之前项目影响较大的我已经都标记出来了:
- 配置选项变化:`vue特有选项`、创建选项、css选项、jsx选项等
- `别名行为变化`:不再要求`/`开头或结尾
- `Vue支持`:通过 [@vitejs/plugin-vue](https://github.com/vitejs/vite/tree/main/packages/plugin-vue)插件支持
- React支持
- HMR API变化
- 清单格式变化
- `插件API重新设计`
#### Vue支持
Vue的整合也通过插件实现,和其他框架一视同仁:
<img src="https://gitee.com/57code/picgo/raw/master/image-20210114183159562.png" style="zoom:80%;" />
SFC定义默认使用`setup script`,语法比较激进,但更简洁,好评!
<img src="https://gitee.com/57code/picgo/raw/master/image-20210116192013356.png" style="zoom:40%;" />
#### 别名定义
不再需要像`vite1`一样在别名前后加上`/`,这和`webpack`项目配置可以保持一致便于移植,好评!
```js
import path from 'path'
export default {
alias: {
"@": path.resolve(__dirname, "src"),
"comps": path.resolve(__dirname, "src/components"),
},
}
```
`App.vue`里面用一下试试
```vue
<script setup>
import HelloWorld from 'comps/HelloWorld.vue'
</script>
```
#### 插件API重新设计
`Vite2`主要变化在插件体系,这样更标准化、易扩展。`Vite2`插件API扩展自`Rollup`插件体系,因此能兼容现存的`Rollup`插件,编写的Vite插件也可以同时运行于开发和创建,好评!
> 插件编写我会另开专题讨论,欢迎大家关注我。
##### Vue3 Jsx支持
`vue3`中`jsx`支持需要引入插件:`@vitejs/plugin-vue-jsx`
```bash
$ npm i @vitejs/plugin-vue-jsx -D
```
注册插件,`vite.config.js`
```js
import vueJsx from "@vitejs/plugin-vue-jsx";
export default {
plugins: [vue(), vueJsx()],
}
```
用法也有要求,改造一下`App.vue`
```vue
<!-- 1.标记为jsx -->
<script setup lang="jsx">
import { defineComponent } from "vue";
import HelloWorld from "comps/HelloWorld.vue";
import logo from "./assets/logo.png"
// 2.用defineComponent定义组件且要导出
export default defineComponent({
render: () => (
<>
<img alt="Vue logo" src={logo} />
<HelloWorld msg="Hello Vue 3 + Vite" />
</>
),
});
</script>
```
##### Mock插件应用
之前给大家介绍的[vite-plugin-mock](https://github.com/vbenjs/vite-plugin-mock)已经重构支持了Vite2。
安装插件
```bash
npm i mockjs -S
```
```bash
npm i vite-plugin-mock cross-env -D
```
配置,`vite.config.js`
```js
import { viteMockServe } from 'vite-plugin-mock'
export default {
plugins: [ viteMockServe({ supportTs: false }) ]
}
```
设置环境变量,`package.json`
```json
{
"scripts": {
"dev": "cross-env NODE_ENV=development vite",
"build": "vite build"
},
}
```
### 项目基础架构
#### 路由
安装`vue-router 4.x`
```js
npm i vue-router@next -S
```
<img src="https://gitee.com/57code/picgo/raw/master/image-20210118170758418.png" style="zoom:33%;" />
路由配置,`router/index.js`
```js
import { createRouter, createWebHashHistory } from 'vue-router';
const router = createRouter({
history: createWebHashHistory(),
routes: [
{ path: '/', component: () => import('views/home.vue') }
]
});
export default router
```
引入,`main.js`
```js
import router from "@/router";
createApp(App).use(router).mount("#app");
```
> 别忘了创建`home.vue`并修改`App.vue`
>
> 路由用法略有变化,[村长的视频教程](https://www.bilibili.com/video/BV1Wh411X7Xp?p=19)
#### 状态管理
安装`vuex 4.x`
```bash
npm i vuex@next -S
```
<img src="https://gitee.com/57code/picgo/raw/master/image-20210118181504903.png" alt="image" style="zoom:33%;" />
Store配置,`store/index.js`
```js
import {createStore} from 'vuex';
export default createStore({
state: {
couter: 0
}
});
```
引入,`main.js`
```js
import store from "@/store";
createApp(App).use(store).mount("#app");
```
> 用法和以前基本一样,[村长的视频教程](https://www.bilibili.com/video/BV1Wh411X7Xp?p=23)
#### 样式组织
安装sass
```bash
npm i sass -D
```
`styles`目录保存各种样式

`index.scss`作为出口组织这些样式,同时编写一些全局样式

最后在`main.js`导入
```js
import "styles/index.scss";
```
> 注意在`vite.config.js`添加`styles`别名
#### UI库
就用我们[花果山团队](https://www.yuque.com/hugsun)自家的[element3](https://github.com/hug-sun/element3)。
> [中文文档](https://element3-ui.com/)
安装
```bash
npm i element3 -S
```
完整引入,`main.js`
```js
import element3 from "element3";
import "element3/lib/theme-chalk/index.css";
createApp(App).use(element3)
```
按需引入,`main.js`
```js
import "element3/lib/theme-chalk/button.css";
import { ElButton } from "element3"
createApp(App).use(ElButton)
```
抽取成插件会更好,`plugins/element3.js`
```js
// 完整引入
import element3 from "element3";
import "element3/lib/theme-chalk/index.css";
// 按需引入
// import { ElButton } from "element3";
// import "element3/lib/theme-chalk/button.css";
export default function (app) {
// 完整引入
app.use(element3)
// 按需引入
// app.use(ElButton);
}
```
测试
```html
<el-button>my button</el-button>
```
#### 基础布局
我们应用需要一个基本布局页,类似下图,将来每个页面以布局页为父页面即可:

布局页面,`layout/index.vue`
```vue
<template>
<div class="app-wrapper">
<!-- 侧边栏 -->
<div class="sidebar-container"></div>
<!-- 内容容器 -->
<div class="main-container">
<!-- 顶部导航栏 -->
<navbar />
<!-- 内容区 -->
<app-main />
</div>
</div>
</template>
<script setup>
import AppMain from "./components/AppMain.vue";
import Navbar from "./components/Navbar.vue";
</script>
<style lang="scss" scoped>
@import "../styles/mixin.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
}
</style>
```
> 别忘了创建`AppMain.vue`和`Navbar.vue`
路由配置,`router/index.js`
```js
{
path: "/",
component: Layout,
children: [
{
path: "",
component: () => import('views/home.vue'),
name: "Home",
meta: { title: "首页", icon: "el-icon-s-home" },
},
],
},
```
#### 动态导航
##### 侧边导航
根据路由表动态生成侧边导航菜单。

首先创建侧边栏组件,递归输出`routes`中的配置为多级菜单,`layout/Sidebar/index.vue`
```vue
<template>
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
mode="vertical"
>
<sidebar-item
v-for="route in routes"
:key="route.path"
:item="route"
:base-path="route.path"
/>
</el-menu>
</el-scrollbar>
</template>
<script setup>
import SidebarItem from "./SidebarItem.vue";
import { computed } from "vue";
import { useRoute } from "vue-router";
import { routes } from "@/router";
import variables from "styles/variables.module.scss";
const activeMenu = computed(() => {
const route = useRoute();
const { meta, path } = route;
if (meta.activeMenu) {
return meta.activeMenu;
}
return path;
});
</script>
```
> 注意:`sass`文件导出变量解析需要用到`css module`,因此`variables`文件要加上`module`中缀。
添加相关样式:
- `styles/variables.module.scss`
- `styles/sidebar.scss`
- `styles/index.scss`中引入
创建`SidebarItem.vue`组件,解析当前路由是导航链接还是父菜单:

##### 面包屑
通过路由匹配数组可以动态生成面包屑。
面包屑组件,`layouts/components/Breadcrumb.vue`
```vue
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
<span
v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
class="no-redirect"
>{{ item.meta.title }}</span>
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup>
import { compile } from "path-to-regexp";
import { reactive, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
const levelList = ref(null);
const router = useRouter();
const route = useRoute();
const getBreadcrumb = () => {
let matched = route.matched.filter((item) => item.meta && item.meta.title);
const first = matched[0];
if (first.path !== "/") {
matched = [{ path: "/home", meta: { title: "首页" } }].concat(matched);
}
levelList.value = matched.filter(
(item) => item.meta && item.meta.title && item.meta.breadcrumb !== false
);
}
const pathCompile = (path) => {
var toPath = compile(path);
return toPath(route.params);
}
const handleLink = (item) => {
const { redirect, path } = item;
if (redirect) {
router.push(redirect);
return;
}
router.push(pathCompile(path));
}
getBreadcrumb();
watch(route, getBreadcrumb)
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>
```
> 别忘了添加依赖:`path-to-regexp`
>
> 注意:`vue-router4`已经不再使用`path-to-regexp`解析动态`path`,因此这里后续还需要改进。
#### 数据封装
统一封装数据请求服务,有利于解决一下问题:
- 统一配置请求
- 请求、响应统一处理
准备工作:
- 安装`axios`:
```bash
npm i axios -S
```
- 添加配置文件:`.env.development`
```
VITE_BASE_API=/api
```
请求封装,`utils/request.js`
```js
import axios from "axios";
import { Message, Msgbox } from "element3";
// 创建axios实例
const service = axios.create({
// 在请求地址前面加上baseURL
baseURL: import.meta.env.VITE_BASE_API,
// 当发送跨域请求时携带cookie
// withCredentials: true,
timeout: 5000,
});
// 请求拦截
service.interceptors.request.use(
(config) => {
// 模拟指定请求令牌
config.headers["X-Token"] = "my token";
return config;
},
(error) => {
// 请求错误的统一处理
console.log(error); // for debug
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
/**
* 通过判断状态码统一处理响应,根据情况修改
* 同时也可以通过HTTP状态码判断请求结果
*/
(response) => {
const res = response.data;
// 如果状态码不是20000则认为有错误
if (res.code !== 20000) {
Message.error({
message: res.message || "Error",
duration: 5 * 1000,
});
// 50008: 非法令牌; 50012: 其他客户端已登入; 50014: 令牌过期;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// 重新登录
Msgbox.confirm("您已登出, 请重新登录", "确认", {
confirmButtonText: "重新登录",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
store.dispatch("user/resetToken").then(() => {
location.reload();
});
});
}
return Promise.reject(new Error(res.message || "Error"));
} else {
return res;
}
},
(error) => {
console.log("err" + error); // for debug
Message({
message: error.message,
type: "error",
duration: 5 * 1000,
});
return Promise.reject(error);
}
);
export default service;
```
#### 业务处理
##### 结构化数据展示
使用`el-table`展示结构化数据,配合`el-pagination`做数据分页。

文件组织结构如下:`list.vue`展示列表,`edit.vue`和`create.vue`编辑或创建,内部复用`detail.vue`处理,`model`中负责数据业务处理。

`list.vue`中的数据展示
```vue
<el-table v-loading="loading" :data="list">
<el-table-column label="ID" prop="id"></el-table-column>
<el-table-column label="账户名" prop="name"></el-table-column>
<el-table-column label="年龄" prop="age"></el-table-column>
</el-table>
```
`list`和`loading`数据的获取逻辑,可以使用`compsition-api`提取到`userModel.js`
```js
export function useList() {
// 列表数据
const state = reactive({
loading: true, // 加载状态
list: [], // 列表数据
});
// 获取列表
function getList() {
state.loading = true;
return request({
url: "/getUsers",
method: "get",
}).then(({ data, total }) => {
// 设置列表数据
state.list = data;
}).finally(() => {
state.loading = false;
});
}
// 首次获取数据
getList();
return { state, getList };
}
```
`list.vue`中使用
```js
import { useList } from "./model/userModel";
```
```js
const { state, getList } = useList();
```
分页处理,`list.vue`
```html
<pagination
:total="total"
v-model:page="listQuery.page"
v-model:limit="listQuery.limit"
@pagination="getList"
></pagination>
```
数据也在`userModel`中处理
```js
const state = reactive({
total: 0, // 总条数
listQuery: {// 分页查询参数
page: 1, // 当前页码
limit: 5, // 每页条数
},
});
```
```js
request({
url: "/getUsers",
method: "get",
params: state.listQuery, // 在查询中加入分页参数
})
```
##### 表单处理
用户数据新增、编辑使用`el-form`处理
可用一个组件`detail.vue`来处理,区别仅在于初始化时是否获取信息回填到表单。
```html
<el-form ref="form" :model="model" :rules="rules">
<el-form-item prop="name" label="用户名">
<el-input v-model="model.name"></el-input>
</el-form-item>
<el-form-item prop="age" label="用户年龄">
<el-input v-model.number="model.age"></el-input>
</el-form-item>
<el-form-item>
<el-button @click="submitForm" type="primary">提交</el-button>
</el-form-item>
</el-form>
```
数据处理同样可以提取到`userModel`中处理。
```js
export function useItem(isEdit, id) {
const model = ref(Object.assign({}, defaultData));
// 初始化时,根据isEdit判定是否需要获取详情
onMounted(() => {
if (isEdit && id) {
// 获取详情
request({
url: "/getUser",
method: "get",
params: { id },
}).then(({ data }) => {
model.value = data;
});
}
});
return { model };
}
```
### 配套视频演示
我专门录了一套视频演示本文所做的所有操作,喜欢看视频学习的小伙伴移步:
[「备战2021」Vite2 + Vue3项目最佳实践](https://www.bilibili.com/video/BV1vX4y1K7bQ)
制作不易,求`3连`,求`关注`
### 关注村长
欢迎关注我的公众号「村长学前端」跟我一起学习最新前端知识。
================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
================================================
FILE: mock/test.js
================================================
const mockList = [
{ id: 1, name: "tom", age: 18 },
{ id: 2, name: "jerry", age: 18 },
{ id: 3, name: "mike", age: 18 },
{ id: 4, name: "jack", age: 18 },
{ id: 5, name: "larry", age: 18 },
{ id: 6, name: "white", age: 18 },
{ id: 7, name: "peter", age: 18 },
{ id: 8, name: "james", age: 18 },
];
module.exports = [
{
url: "/api/getUser",
type: "get",
response: () => {
return {
code: 20000,
data: { id: 1, name: "tom", age: 18 },
};
},
},
{
url: "/api/getUsers",
type: "get",
response: (config) => {
// 从查询参数中获取分页、过滤关键词等参数
const { page = 1, limit = 5 } = config.query;
// 分页
const data = mockList.filter(
(item, index) => index < limit * page && index >= limit * (page - 1)
);
return {
code: 20000,
data,
total: mockList.length,
};
},
},
{
url: "/api/addUser",
type: "post",
response: () => {
// 直接返回
return {
code: 20000,
};
},
},
{
url: "/api/updateUser",
type: "post",
response: () => {
return {
code: 20000,
};
},
},
{
url: "/api/deleteUser",
type: "get",
response: () => {
return {
code: 20000,
};
},
},
];
================================================
FILE: package.json
================================================
{
"name": "vite2-in-action",
"version": "0.0.0",
"license": "ISC",
"scripts": {
"dev": "cross-env NODE_ENV=development vite",
"build": "vite build"
},
"dependencies": {
"axios": "^0.21.1",
"element3": "0.0.39",
"js-yaml": "^4.0.0",
"mockjs": "^1.1.0",
"path-browserify": "^1.0.1",
"path-to-regexp": "^6.2.0",
"process": "^0.11.10",
"vue": "^3.0.5",
"vue-i18n": "^9.0.0-rc.7",
"vue-router": "^4.0.4",
"vuex": "^4.0.0"
},
"devDependencies": {
"@intlify/vite-plugin-vue-i18n": "^2.0.0-rc.2",
"@vitejs/plugin-vue": "^1.1.4",
"@vitejs/plugin-vue-jsx": "^1.1.0",
"@vue/compiler-sfc": "^3.0.5",
"cross-env": "^7.0.3",
"sass": "^1.32.8",
"vite": "^2.0.1",
"vite-plugin-mock": "^2.1.4"
}
}
================================================
FILE: src/App.vue
================================================
<template>
<router-view></router-view>
</template>
================================================
FILE: src/components/HelloWorld.vue
================================================
<template>
<h1>{{ msg }}</h1>
<p>{{ $store.state.counter }}</p>
<!-- 国际化 -->
<form>
<label>{{ t('language') }}</label>
<select v-model="locale">
<option value="en">en</option>
<option value="ja">ja</option>
</select>
</form>
<p>{{ t('hello') }}</p>
<el-button @click="state.count++">count is: {{ state.count }}</el-button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test hot module replacement.
</p>
</template>
<script setup>
import { defineProps, reactive } from "vue";
import { useI18n } from "vue-i18n";
defineProps({
msg: String,
});
const state = reactive({ count: 0 });
const { locale, t } = useI18n({
inheritLocale: true,
});
</script>
<i18n>
{
"en": {
"language": "Language",
"hello": "hello, world!"
},
"ja": {
"language": "言語",
"hello": "こんにちは、世界!"
}
}
</i18n>
<style scoped>
a {
color: #42b983;
}
</style>
================================================
FILE: src/components/Pagination.vue
================================================
<template>
<div :class="{ hidden: hidden }" class="pagination-container">
<el-pagination
:background="background"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:layout="layout"
:page-sizes="pageSizes"
:total="total"
v-bind="$attrs"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script>
export default {
name: "Pagination",
props: {
total: {
required: true,
type: Number,
},
page: {
type: Number,
default: 1,
},
limit: {
type: Number,
default: 20,
},
pageSizes: {
type: Array,
default() {
return [10, 20, 30, 50];
},
},
layout: {
type: String,
default: "total, sizes, prev, pager, next, jumper",
},
background: {
type: Boolean,
default: true,
},
hidden: {
type: Boolean,
default: false,
},
},
emits: ["update:page", "update:limit", "pagination"],
computed: {
currentPage: {
get() {
return this.page;
},
set(val) {
this.$emit("update:page", val);
},
},
pageSize: {
get() {
return this.limit;
},
set(val) {
this.$emit("update:limit", val);
},
},
},
methods: {
handleSizeChange(val) {
this.$emit("pagination", { page: this.currentPage, limit: val });
},
handleCurrentChange(val) {
this.$emit("pagination", { page: val, limit: this.pageSize });
},
},
};
</script>
<style scoped>
.pagination-container {
background: #fff;
padding: 32px 16px;
}
.pagination-container.hidden {
display: none;
}
</style>
================================================
FILE: src/layouts/components/AppMain.vue
================================================
<template>
<section class="app-main">
<!-- 内部应该显示子路由页面信息 -->
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
</section>
</template>
<script>
export default {
name: "AppMain",
};
</script>
<style scoped>
.app-main {
/*50 = navbar */
min-height: calc(100vh - 50px);
width: 100%;
position: relative;
overflow: hidden;
}
</style>
================================================
FILE: src/layouts/components/Breadcrumb.vue
================================================
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
<span
v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
class="no-redirect"
>{{ item.meta.title }}</span
>
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script setup>
import { compile } from "path-to-regexp";
import { reactive, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
const levelList = ref(null);
const router = useRouter();
const route = useRoute();
const getBreadcrumb = () => {
let matched = route.matched.filter((item) => item.meta && item.meta.title);
const first = matched[0];
if (first.path !== "/") {
matched = [{ path: "/home", meta: { title: "首页" } }].concat(matched);
}
levelList.value = matched.filter(
(item) => item.meta && item.meta.title && item.meta.breadcrumb !== false
);
}
const pathCompile = (path) => {
var toPath = compile(path);
return toPath(route.params);
}
const handleLink = (item) => {
const { redirect, path } = item;
if (redirect) {
router.push(redirect);
return;
}
router.push(pathCompile(path));
}
getBreadcrumb();
watch(route, getBreadcrumb)
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>
================================================
FILE: src/layouts/components/Navbar.vue
================================================
<template>
<div class="navbar">
<!-- 面包屑 -->
<breadcrumb class="breadcrumb-container"></breadcrumb>
<!-- 右侧下拉菜单 -->
<div class="right-menu">
<el-dropdown class="avatar-container" trigger="click">
<div class="avatar-wrapper">
<img src="/src/assets/logo.png" class="user-avatar" />
<i class="el-icon-caret-bottom" />
</div>
<el-dropdown-menu class="user-dropdown">
<router-link to="/">
<el-dropdown-item> 首页 </el-dropdown-item>
</router-link>
<a target="_blank" href="https://github.com/57code/vite2-in-action/">
<el-dropdown-item>我的Github</el-dropdown-item>
</a>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
<script setup>
import Breadcrumb from "./Breadcrumb.vue";
</script>
<style lang="scss" scoped>
.navbar {
height: 50px;
overflow: hidden;
position: relative;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.breadcrumb-container {
float: left;
}
.right-menu {
float: right;
height: 100%;
line-height: 50px;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background 0.3s;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
}
.avatar-container {
margin-right: 30px;
.avatar-wrapper {
margin-top: 5px;
position: relative;
.user-avatar {
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 10px;
}
.el-icon-caret-bottom {
cursor: pointer;
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
}
}
}
}
</style>
================================================
FILE: src/layouts/components/Sidebar/Item.vue
================================================
<template>
<i v-if="icon" class="sub-el-icon" :class="icon"></i>
<span v-if="title">{{ title }}</span>
</template>
<script setup>
import { defineProps } from "vue";
defineProps({
icon: {
type: String,
default: "",
},
title: {
type: String,
default: "",
},
});
</script>
<style scoped>
.sub-el-icon {
color: currentColor;
width: 1em;
height: 1em;
}
</style>
================================================
FILE: src/layouts/components/Sidebar/Link.vue
================================================
<template>
<component :is="type" v-bind="linkProps(to)">
<slot />
</component>
</template>
<script setup>
import { isExternal as isExt } from "utils/validate";
import { computed, defineProps } from "vue";
const props = defineProps({
to: {
type: String,
required: true,
},
});
const isExternal = computed(() => isExt(props.to));
// type是一个计算属性
const type = computed(() => {
if (isExternal.value) {
return "a";
}
return "router-link";
});
const linkProps = (to) => {
if (isExternal.value) {
return {
href: to,
target: "_blank",
rel: "noopener",
};
}
return { to };
};
</script>
================================================
FILE: src/layouts/components/Sidebar/SidebarItem.vue
================================================
<template>
<div v-if="!item.hidden">
<template
v-if="
hasOneShowingChild(item.children, item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren) &&
!item.alwaysShow
"
>
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item :index="resolvePath(onlyOneChild.path)">
<item
:icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"
:title="onlyOneChild.meta.title"
/>
</el-menu-item>
</app-link>
</template>
<el-submenu
v-else
ref="subMenu"
:index="resolvePath(item.path)"
popper-append-to-body
>
<template #title>
<item
v-if="item.meta"
:icon="item.meta && item.meta.icon"
:title="item.meta.title"
/>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-submenu>
</div>
</template>
<script setup>
import path from "path-browserify";
import Item from "./Item.vue";
import AppLink from "./Link.vue";
import { isExternal } from "utils/validate";
import { defineProps, ref } from "vue";
const props = defineProps({
// route object
item: {
type: Object,
required: true,
},
isNest: {
type: Boolean,
default: false,
},
basePath: {
type: String,
default: "",
},
});
const onlyOneChild = ref(null);
const hasOneShowingChild = (children = [], parent) => {
const showingChildren = children.filter((item) => {
if (item.hidden) {
return false;
} else {
// Temp set(will be used if only has one showing child)
onlyOneChild.value = item;
return true;
}
});
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true;
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: "", noShowingChildren: true };
return true;
}
return false;
};
const resolvePath = (routePath) => {
if (isExternal(routePath)) {
return routePath;
}
if (isExternal(props.basePath)) {
return props.basePath;
}
return path.resolve(props.basePath, routePath);
};
</script>
================================================
FILE: src/layouts/components/Sidebar/index.vue
================================================
<template>
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
mode="vertical"
>
<sidebar-item
v-for="route in routes"
:key="route.path"
:item="route"
:base-path="route.path"
/>
</el-menu>
</el-scrollbar>
</template>
<script setup>
import SidebarItem from "./SidebarItem.vue";
import { computed } from "vue";
import { useRoute } from "vue-router";
import { routes } from "/@/router";
import variables from "styles/variables.module.scss";
const activeMenu = computed(() => {
const route = useRoute();
const { meta, path } = route;
if (meta.activeMenu) {
return meta.activeMenu;
}
return path;
});
</script>
================================================
FILE: src/layouts/index.vue
================================================
<template>
<div class="app-wrapper">
<!-- 侧边栏 -->
<sidebar class="sidebar-container"></sidebar>
<!-- 内容容器 -->
<div class="main-container">
<!-- 顶部导航栏 -->
<navbar />
<!-- 内容区 -->
<app-main />
</div>
</div>
</template>
<script setup>
import AppMain from "./components/AppMain.vue";
import Navbar from "./components/Navbar.vue";
import Sidebar from "./components/Sidebar/index.vue";
</script>
<style lang="scss" scoped>
@import "../styles/mixin.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
}
</style>
================================================
FILE: src/locales/en.json
================================================
{
"language": "Language",
"hello": "hello, world!"
}
================================================
FILE: src/locales/jp.json
================================================
{
"language": "言語",
"hello": "こんにちは、世界!"
}
================================================
FILE: src/main.js
================================================
import { createApp } from "vue";
import App from "./App.vue";
// 全局样式
import "styles/index.scss";
// element3
import element3 from "plugins/element3";
// router
import router from "/@/router";
// store
import store from "/@/store";
// i18n
import { createI18n } from "vue-i18n";
import messages from "@intlify/vite-plugin-vue-i18n/messages";
const i18n = createI18n({
legacy: false,
locale: "en",
messages,
});
createApp(App).use(element3).use(router).use(store).use(i18n).mount("#app");
================================================
FILE: src/plugins/element3.js
================================================
// 完整引入
import element3 from "element3";
import "element3/lib/theme-chalk/index.css";
// 按需引入
// import "element3/lib/theme-chalk/button.css";
// import {
// ElRow,
// ElCol,
// ElContainer,
// ElHeader,
// ElFooter,
// ElAside,
// ElMain,
// ElIcon,
// ElButton,
// ElLink,
// ElRadio,
// ElRadioButton,
// ElRadioGroup,
// ElCheckbox,
// ElCheckboxButton,
// ElCheckboxGroup,
// ElInput,
// ElInputNumber,
// ElSelect,
// ElOption,
// ElOptionGroup,
// ElCascader,
// ElCascaderPanel,
// ElSwitch,
// ElSlider,
// ElTimePicker,
// ElTimeSelect,
// ElDatePicker,
// ElUpload,
// ElRate,
// ElColorPicker,
// ElTransfer,
// ElForm,
// ElFormItem,
// ElTag,
// ElProgress,
// ElTree,
// ElPagination,
// ElBadge,
// ElAvatar,
// ElAlert,
// ElLoading,
// ElMenu,
// ElMenuItem,
// ElSubmenu,
// ElMenuItemGroup,
// ElTabs,
// ElTabPane,
// ElBreadcrumb,
// ElBreadcrumbItem,
// ElPageHeader,
// ElDropdown,
// ElDropdownItem,
// ElDropdownMenu,
// ElSteps,
// ElStep,
// ElDialog,
// ElTooltip,
// ElPopover,
// ElPopconfirm,
// ElCard,
// ElCarousel,
// ElCarouselItem,
// ElCollapse,
// ElCollapseItem,
// ElTimeline,
// ElTimelineItem,
// ElDivider,
// ElCalendar,
// ElImage,
// ElBacktop,
// ElInfiniteScroll,
// ElDrawer,
// ElScrollbar,
// } from "element3";
export default function (app) {
// 完整引入
app.use(element3)
// 按需引入
// app.use(ElButton);
}
================================================
FILE: src/router/index.js
================================================
import { createRouter, createWebHashHistory } from "vue-router";
import Layout from "layouts/index.vue";
/**
* Note: 子菜单仅当路由的children.length >= 1时才出现
*
* hidden: true 设置为true时路由将显示在sidebar中(默认false)
* alwaysShow: true 如果设置为true则总是显示在菜单根目录
* 如果不设置alwaysShow, 当路由有超过一个子路由时,
* 将会变为嵌套模式, 否则不会显示根菜单
* redirect: noRedirect 如果设置noRedirect时,breadcrumb中点击将不会跳转
* name:'router-name' name用于<keep-alive> (必须设置!!!)
* meta : {
roles: ['admin','editor'] 页面可访问角色设置
title: 'title' sidebar和breadcrumb显示的标题
icon: 'svg-name'/'el-icon-x' sidebar中显示的图标
breadcrumb: false 设置为false,将不会出现在面包屑中
activeMenu: '/example/list' 如果设置一个path, sidebar将会在高亮匹配项
}
*/
export const routes = [
{
path: "/",
redirect: "/home",
component: Layout,
meta: { title: "导航", icon: "el-icon-s-home" },
children: [
{
path: "home",
component: () => import("views/home.vue"),
name: "Home",
meta: { title: "首页", icon: "el-icon-s-home" },
children: [
{
path: ":id",
component: () => import("views/detail.vue"),
name: "Detail",
hidden: true,
meta: {
title: "详情",
icon: "el-icon-s-home",
activeMenu: "/home",
},
},
],
},
],
},
{
path: "/users",
component: Layout,
meta: {
title: "用户管理",
icon: "el-icon-user-solid",
},
redirect: '/users/list',
children: [
{
path: "list",
component: () => import("views/users/list.vue"),
meta: {
title: "用户列表",
icon: "el-icon-document",
},
},
{
path: "create",
component: () => import("views/users/create.vue"),
hidden: true,
meta: {
title: "创建新用户",
activeMenu: "/users/list",
},
},
{
path: "edit/:id(\\d+)",
name: "userEdit",
component: () => import("views/users/edit.vue"),
hidden: true,
meta: {
title: "编辑用户信息",
activeMenu: "/users/list",
},
},
],
},
];
const router = createRouter({
history: createWebHashHistory(),
routes,
});
export default router;
================================================
FILE: src/store/index.js
================================================
import { createStore } from "vuex";
const store = createStore({
state: {
counter: 0,
},
});
export default store;
================================================
FILE: src/styles/index.scss
================================================
@import "./mixin.scss";
@import "./variables.module.scss";
@import "./sidebar.scss";
// 编写全局样式
body {
height: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB,
Microsoft YaHei, Arial, sans-serif;
margin: 0;
}
label {
font-weight: 700;
}
html {
height: 100%;
box-sizing: border-box;
}
#app {
height: 100%;
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
a:focus,
a:active {
outline: none;
}
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}
div:focus {
outline: none;
}
.clearfix {
&:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
}
// main-container global css
.app-container {
padding: 20px;
}
================================================
FILE: src/styles/mixin.scss
================================================
@mixin clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}
@mixin scrollBar {
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
@mixin relative {
position: relative;
width: 100%;
height: 100%;
}
================================================
FILE: src/styles/sidebar.scss
================================================
#app {
.main-container {
min-height: 100%;
transition: margin-left .28s;
margin-left: $sideBarWidth;
position: relative;
}
.sidebar-container {
transition: width 0.28s;
width: $sideBarWidth !important;
background-color: $menuBg;
height: 100%;
position: fixed;
font-size: 0px;
top: 0;
bottom: 0;
left: 0;
z-index: 1001;
overflow: hidden;
// reset element-ui css
.horizontal-collapse-transition {
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
}
.scrollbar-wrapper {
overflow-x: hidden !important;
}
.el-scrollbar__bar.is-vertical {
right: 0px;
}
.el-scrollbar {
height: 100%;
}
&.has-logo {
.el-scrollbar {
height: calc(100% - 50px);
}
}
.is-horizontal {
display: none;
}
a {
display: inline-block;
width: 100%;
overflow: hidden;
}
.svg-icon {
margin-right: 16px;
}
.sub-el-icon {
margin-right: 12px;
margin-left: -2px;
}
.el-menu {
border: none;
height: 100%;
width: 100% !important;
}
// menu hover
.submenu-title-noDropdown,
.el-submenu__title {
&:hover {
background-color: $menuHover !important;
}
}
.is-active>.el-submenu__title {
color: $subMenuActiveText !important;
}
& .nest-menu .el-submenu>.el-submenu__title,
& .el-submenu .el-menu-item {
min-width: $sideBarWidth !important;
background-color: $subMenuBg !important;
&:hover {
background-color: $subMenuHover !important;
}
}
}
.hideSidebar {
.sidebar-container {
width: 50px !important;
}
.main-container {
margin-left: 54px;
}
.submenu-title-noDropdown {
padding: 0 !important;
position: relative;
.el-tooltip {
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
.sub-el-icon {
margin-left: 19px;
}
}
}
.el-submenu {
overflow: hidden;
&>.el-submenu__title {
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
.sub-el-icon {
margin-left: 19px;
}
.el-submenu__icon-arrow {
display: none;
}
}
}
.el-menu--collapse {
.el-submenu {
&>.el-submenu__title {
&>span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
}
}
}
}
.el-menu--collapse .el-menu .el-submenu {
min-width: $sideBarWidth !important;
}
// mobile responsive
.mobile {
.main-container {
margin-left: 0px;
}
.sidebar-container {
transition: transform .28s;
width: $sideBarWidth !important;
}
&.hideSidebar {
.sidebar-container {
pointer-events: none;
transition-duration: 0.3s;
transform: translate3d(-$sideBarWidth, 0, 0);
}
}
}
.withoutAnimation {
.main-container,
.sidebar-container {
transition: none;
}
}
}
// when menu collapsed
.el-menu--vertical {
&>.el-menu {
.svg-icon {
margin-right: 16px;
}
.sub-el-icon {
margin-right: 12px;
margin-left: -2px;
}
}
.nest-menu .el-submenu>.el-submenu__title,
.el-menu-item {
&:hover {
// you can use $subMenuHover
background-color: $menuHover !important;
}
}
// the scroll bar appears when the subMenu is too long
>.el-menu--popup {
max-height: 100vh;
overflow-y: auto;
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
}
================================================
FILE: src/styles/variables.module.scss
================================================
// sidebar
$menuText:#bfcbd9;
$menuActiveText:#409EFF;
$subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951
$menuBg:#304156;
$menuHover:#263445;
$subMenuBg:#1f2d3d;
$subMenuHover:#001528;
$sideBarWidth: 210px;
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
menuText: $menuText;
menuActiveText: $menuActiveText;
subMenuActiveText: $subMenuActiveText;
menuBg: $menuBg;
menuHover: $menuHover;
subMenuBg: $subMenuBg;
subMenuHover: $subMenuHover;
sideBarWidth: $sideBarWidth;
}
================================================
FILE: src/utils/request.js
================================================
import axios from "axios";
import { Message, Msgbox } from "element3";
import store from "/@/store";
// 创建axios实例
const service = axios.create({
// 在请求地址前面加上baseURL
baseURL: import.meta.env.VITE_BASE_API,
// 当发送跨域请求时携带cookie
// withCredentials: true,
timeout: 5000,
});
// 请求拦截
service.interceptors.request.use(
(config) => {
// 指定请求令牌
// if (store.getters.token) {
// // 自定义令牌的字段名为X-Token,根据咱们后台再做修改
// config.headers["X-Token"] = store.getters.token;
// }
config.headers["X-Token"] = "my token";
return config;
},
(error) => {
// 请求错误的统一处理
console.log(error); // for debug
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* 通过判断状态码统一处理响应,根据情况修改
* 同时也可以通过HTTP状态码判断请求结果
*/
(response) => {
const res = response.data;
// 如果状态码不是20000则认为有错误
if (res.code !== 20000) {
Message.error({
message: res.message || "Error",
duration: 5 * 1000,
});
// 50008: 非法令牌; 50012: 其他客户端已登入; 50014: 令牌过期;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// 重新登录
Msgbox.confirm("您已登出, 请重新登录", "确认", {
confirmButtonText: "重新登录",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
store.dispatch("user/resetToken").then(() => {
location.reload();
});
});
}
return Promise.reject(new Error(res.message || "Error"));
} else {
return res;
}
},
(error) => {
console.log("err" + error); // for debug
Message({
message: error.message,
type: "error",
duration: 5 * 1000,
});
return Promise.reject(error);
}
);
export default service;
================================================
FILE: src/utils/validate.js
================================================
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path);
}
================================================
FILE: src/views/detail.vue
================================================
<template>
<div>
detail <span>{{$route.params.id}}</span>
</div>
</template>
<script setup>
</script>
<style scoped>
</style>
================================================
FILE: src/views/home.vue
================================================
<template>
<div>
<HelloWorld msg="hello vue3 + vite"></HelloWorld>
<router-link to="/home/1">detail1</router-link>
<router-link to="/home/2">detail2</router-link>
<router-view></router-view>
</div>
</template>
<script setup>
import HelloWorld from '/@/components/HelloWorld.vue';
</script>
<style scoped>
</style>
================================================
FILE: src/views/users/components/detail.vue
================================================
<template>
<div class="container">
<el-form ref="form" :model="model" :rules="rules">
<el-form-item prop="name" label="用户名">
<el-input v-model="model.name"></el-input>
</el-form-item>
<el-form-item prop="age" label="用户年龄">
<el-input v-model.number="model.age"></el-input>
</el-form-item>
<el-form-item>
<el-button @click="submitForm" type="primary">提交</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { Message } from "element3";
import { reactive, ref } from "vue";
import { useRoute } from "vue-router";
import { useItem } from "../model/userModel";
export default {
props: {
isEdit: {
type: Boolean,
default: false,
},
},
setup(props) {
// 路由
const route = useRoute();
const { model, addUser, updateUser } = useItem(props.isEdit, route.params.id);
const rules = reactive({
// 校验规则
name: [{ required: true, message: "用户名为必填项" }],
});
// 表单实例
const form = ref(null);
// 提交表单
function submitForm() {
// 校验
form.value.validate((valid) => {
if (valid) {
// 提交
if (props.isEdit) {
updateUser().then(() => {
// 操作成功提示信息
Message.success({
title: "操作成功",
message: "更新用户数据成功",
duration: 2000,
});
});
} else {
addUser().then(() => {
// 操作成功提示信息
Message.success({
title: "操作成功",
message: "新增玩家数据成功",
duration: 2000,
});
});
}
}
});
}
return {
model,
rules,
form,
submitForm,
};
},
};
</script>
<style scoped>
.container {
padding: 10px;
}
</style>
<style>
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
================================================
FILE: src/views/users/create.vue
================================================
<template>
<detail :is-edit="false"></detail>
</template>
<script>
import Detail from "./components/detail.vue";
export default {
components: {
Detail,
},
};
</script>
================================================
FILE: src/views/users/edit.vue
================================================
<template>
<detail :is-edit="true"></detail>
</template>
<script>
import Detail from "./components/detail.vue";
export default {
components: {
Detail,
},
};
</script>
================================================
FILE: src/views/users/list.vue
================================================
<template>
<div class="app-container">
<div class="btn-container">
<!-- 新增按钮 -->
<router-link to="/users/create">
<el-button type="success" icon="el-icon-edit">创建用户</el-button>
</router-link>
</div>
<el-table
v-loading="loading"
:data="list"
border
fit
highlight-current-row
style="width: 100%"
>
<el-table-column align="center" label="ID" prop="id"></el-table-column>
<el-table-column align="center" label="账户名" prop="name">
</el-table-column>
<el-table-column align="center" label="年龄" prop="age">
</el-table-column>
<!-- 操作列 -->
<el-table-column label="操作" align="center">
<template v-slot="scope">
<el-button
type="primary"
icon="el-icon-edit"
@click="handleEdit(scope)"
>更新</el-button
>
<el-button
type="danger"
icon="el-icon-remove"
@click="handleDelete(scope)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination
v-show="total > 0"
:total="total"
v-model:page="listQuery.page"
v-model:limit="listQuery.limit"
@pagination="getList"
></pagination>
</div>
</template>
<script>
import { toRefs } from "vue";
import { useRouter } from "vue-router";
import { Message } from "element3";
import Pagination from "comps/Pagination.vue";
import { useList } from "./model/userModel";
export default {
name: "UserList",
components: {
Pagination,
},
setup() {
// 玩家列表数据
const router = useRouter();
const { state, getList, delItem } = useList();
// 用户更新
function handleEdit({ row }) {
router.push({
name: "userEdit",
params: { id: row.id },
});
}
// 删除玩家
function handleDelete({ row }) {
delItem(row.id).then(() => {
// todo:删除这一行,或者重新获取数据
// 通知用户
Message.success("删除成功!");
});
}
return {
...toRefs(state),
getList,
handleEdit,
handleDelete,
};
},
};
</script>
<style scoped>
.btn-container {
text-align: left;
padding: 0px 10px 20px 0px;
}
</style>
================================================
FILE: src/views/users/model/userModel.js
================================================
import { reactive, onMounted, ref } from "vue";
import request from "utils/request";
export function useList() {
// 列表数据
const state = reactive({
loading: true, // 加载状态
list: [], // 列表数据
total: 0,
listQuery: {
page: 1,
limit: 5,
},
});
// 获取列表
function getList() {
state.loading = true;
return request({
url: "/getUsers",
method: "get",
params: state.listQuery,
})
.then(({ data, total }) => {
// 设置列表数据
state.list = data;
state.total = total;
})
.finally(() => {
state.loading = false;
});
}
// 删除项
function delItem(id) {
state.loading = true;
return request({
url: "/deleteUser",
method: "get",
params: { id },
}).finally(() => {
state.loading = false;
});
}
// 首次获取数据
getList();
return { state, getList, delItem };
}
const defaultData = {
name: "",
age: undefined,
};
export function useItem(isEdit, id) {
const model = ref(Object.assign({}, defaultData));
// 初始化时,根据isEdit判定是否需要获取玩家详情
onMounted(() => {
if (isEdit && id) {
// 获取玩家详情
request({
url: "/getUser",
method: "get",
params: { id },
}).then(({ data }) => {
model.value = data;
});
}
});
const updateUser = () => {
return request({
url: "/updateUser",
method: "post",
data: model.value,
});
};
const addUser = () => {
return request({
url: "/addUser",
method: "post",
data: model.value,
});
};
return { model, updateUser, addUser };
}
================================================
FILE: vite.config.js
================================================
import path from "path";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import { viteMockServe } from "vite-plugin-mock";
// 导入插件
import vueI18n from '@intlify/vite-plugin-vue-i18n'
export default {
resolve: {
alias: {
"/@": path.resolve(__dirname, "src"),
comps: path.resolve(__dirname, "src/components"),
styles: path.resolve(__dirname, "src/styles"),
plugins: path.resolve(__dirname, "src/plugins"),
views: path.resolve(__dirname, "src/views"),
layouts: path.resolve(__dirname, "src/layouts"),
utils: path.resolve(__dirname, "src/utils"),
apis: path.resolve(__dirname, "src/apis"),
dirs: path.resolve(__dirname, "src/directives"),
},
},
plugins: [vue(), vueJsx(), viteMockServe({ supportTs: false }), vueI18n({
// if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
// compositionOnly: false,
// you need to set i18n resource including paths !
include: path.resolve(__dirname, './src/locales/**')
})],
};
gitextract_agjgct2v/ ├── .gitignore ├── README.md ├── index.html ├── mock/ │ └── test.js ├── package.json ├── src/ │ ├── App.vue │ ├── components/ │ │ ├── HelloWorld.vue │ │ └── Pagination.vue │ ├── layouts/ │ │ ├── components/ │ │ │ ├── AppMain.vue │ │ │ ├── Breadcrumb.vue │ │ │ ├── Navbar.vue │ │ │ └── Sidebar/ │ │ │ ├── Item.vue │ │ │ ├── Link.vue │ │ │ ├── SidebarItem.vue │ │ │ └── index.vue │ │ └── index.vue │ ├── locales/ │ │ ├── en.json │ │ └── jp.json │ ├── main.js │ ├── plugins/ │ │ └── element3.js │ ├── router/ │ │ └── index.js │ ├── store/ │ │ └── index.js │ ├── styles/ │ │ ├── index.scss │ │ ├── mixin.scss │ │ ├── sidebar.scss │ │ └── variables.module.scss │ ├── utils/ │ │ ├── request.js │ │ └── validate.js │ └── views/ │ ├── detail.vue │ ├── home.vue │ └── users/ │ ├── components/ │ │ └── detail.vue │ ├── create.vue │ ├── edit.vue │ ├── list.vue │ └── model/ │ └── userModel.js └── vite.config.js
SYMBOL INDEX (3 symbols across 2 files)
FILE: src/utils/validate.js
function isExternal (line 1) | function isExternal(path) {
FILE: src/views/users/model/userModel.js
function useList (line 4) | function useList() {
function useItem (line 59) | function useItem(isEdit, id) {
Condensed preview — 36 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (59K chars).
[
{
"path": ".gitignore",
"chars": 73,
"preview": "dist\ndist-ssr\nnode_modules\n\n*.local\n.DS_Store\nyarn.lock\npackage-lock.json"
},
{
"path": "README.md",
"chars": 13956,
"preview": "## Vite2项目最佳实践\n\n### 配套视频演示\n\n我专门录了一套视频演示本文所做的所有操作,喜欢看视频学习的小伙伴移步:\n[「备战2021」Vite2 + Vue3项目最佳实践](https://www.bilibili.com/vi"
},
{
"path": "index.html",
"chars": 313,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <link rel=\"icon\" href=\"/favicon.ico\" />\n <meta name="
},
{
"path": "mock/test.js",
"chars": 1301,
"preview": "const mockList = [\n { id: 1, name: \"tom\", age: 18 },\n { id: 2, name: \"jerry\", age: 18 },\n { id: 3, name: \"mike\", age:"
},
{
"path": "package.json",
"chars": 783,
"preview": "{\n \"name\": \"vite2-in-action\",\n \"version\": \"0.0.0\",\n \"license\": \"ISC\",\n \"scripts\": {\n \"dev\": \"cross-env NODE_ENV=d"
},
{
"path": "src/App.vue",
"chars": 53,
"preview": "<template>\n <router-view></router-view>\n</template>\n"
},
{
"path": "src/components/HelloWorld.vue",
"chars": 913,
"preview": "<template>\n <h1>{{ msg }}</h1>\n <p>{{ $store.state.counter }}</p>\n\n <!-- 国际化 -->\n <form>\n <label>{{ t('language')"
},
{
"path": "src/components/Pagination.vue",
"chars": 1728,
"preview": "<template>\n <div :class=\"{ hidden: hidden }\" class=\"pagination-container\">\n <el-pagination\n :background=\"backgr"
},
{
"path": "src/layouts/components/AppMain.vue",
"chars": 397,
"preview": "<template>\n <section class=\"app-main\">\n <!-- 内部应该显示子路由页面信息 -->\n <router-view v-slot=\"{ Component }\">\n <compo"
},
{
"path": "src/layouts/components/Breadcrumb.vue",
"chars": 1657,
"preview": "<template>\n <el-breadcrumb class=\"app-breadcrumb\" separator=\"/\">\n <transition-group name=\"breadcrumb\">\n <el-bre"
},
{
"path": "src/layouts/components/Navbar.vue",
"chars": 1989,
"preview": "<template>\n <div class=\"navbar\">\n <!-- 面包屑 -->\n <breadcrumb class=\"breadcrumb-container\"></breadcrumb>\n\n <!-- "
},
{
"path": "src/layouts/components/Sidebar/Item.vue",
"chars": 393,
"preview": "<template>\n <i v-if=\"icon\" class=\"sub-el-icon\" :class=\"icon\"></i>\n <span v-if=\"title\">{{ title }}</span>\n</template>\n<"
},
{
"path": "src/layouts/components/Sidebar/Link.vue",
"chars": 643,
"preview": "<template>\n <component :is=\"type\" v-bind=\"linkProps(to)\">\n <slot />\n </component>\n</template>\n\n<script setup>\nimpor"
},
{
"path": "src/layouts/components/Sidebar/SidebarItem.vue",
"chars": 2439,
"preview": "<template>\n <div v-if=\"!item.hidden\">\n <template\n v-if=\"\n hasOneShowingChild(item.children, item) &&\n "
},
{
"path": "src/layouts/components/Sidebar/index.vue",
"chars": 890,
"preview": "<template>\n <el-scrollbar wrap-class=\"scrollbar-wrapper\">\n <el-menu\n :default-active=\"activeMenu\"\n :backgr"
},
{
"path": "src/layouts/index.vue",
"chars": 600,
"preview": "<template>\n <div class=\"app-wrapper\">\n <!-- 侧边栏 -->\n <sidebar class=\"sidebar-container\"></sidebar>\n <!-- 内容容器 "
},
{
"path": "src/locales/en.json",
"chars": 56,
"preview": "{\n \"language\": \"Language\",\n \"hello\": \"hello, world!\"\n}"
},
{
"path": "src/locales/jp.json",
"chars": 46,
"preview": "{\n \"language\": \"言語\",\n \"hello\": \"こんにちは、世界!\"\n}"
},
{
"path": "src/main.js",
"chars": 500,
"preview": "import { createApp } from \"vue\";\nimport App from \"./App.vue\";\n\n// 全局样式\nimport \"styles/index.scss\";\n\n// element3\nimport e"
},
{
"path": "src/plugins/element3.js",
"chars": 1514,
"preview": "// 完整引入\nimport element3 from \"element3\";\nimport \"element3/lib/theme-chalk/index.css\";\n\n// 按需引入\n// import \"element3/lib/t"
},
{
"path": "src/router/index.js",
"chars": 2399,
"preview": "import { createRouter, createWebHashHistory } from \"vue-router\";\nimport Layout from \"layouts/index.vue\";\n\n/**\n * Note: 子"
},
{
"path": "src/store/index.js",
"chars": 124,
"preview": "import { createStore } from \"vuex\";\n\nconst store = createStore({\n state: {\n counter: 0,\n },\n});\n\nexport default sto"
},
{
"path": "src/styles/index.scss",
"chars": 1058,
"preview": "@import \"./mixin.scss\";\n@import \"./variables.module.scss\";\n@import \"./sidebar.scss\";\n\n// 编写全局样式\nbody {\n height: 100%;\n "
},
{
"path": "src/styles/mixin.scss",
"chars": 384,
"preview": "@mixin clearfix {\n &:after {\n content: \"\";\n display: table;\n clear: both;\n }\n}\n\n@mixin scrollBar {\n &::-webk"
},
{
"path": "src/styles/sidebar.scss",
"chars": 3980,
"preview": "#app {\n\n .main-container {\n min-height: 100%;\n transition: margin-left .28s;\n margin-left: $sideBarWidth;\n "
},
{
"path": "src/styles/variables.module.scss",
"chars": 616,
"preview": "// sidebar\n$menuText:#bfcbd9;\n$menuActiveText:#409EFF;\n$subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/"
},
{
"path": "src/utils/request.js",
"chars": 1873,
"preview": "import axios from \"axios\";\nimport { Message, Msgbox } from \"element3\";\nimport store from \"/@/store\";\n\n// 创建axios实例\nconst"
},
{
"path": "src/utils/validate.js",
"chars": 83,
"preview": "export function isExternal(path) {\n return /^(https?:|mailto:|tel:)/.test(path);\n}"
},
{
"path": "src/views/detail.vue",
"chars": 139,
"preview": "<template>\n <div>\n detail <span>{{$route.params.id}}</span>\n </div>\n</template>\n\n<script setup>\n \n</script>\n\n<styl"
},
{
"path": "src/views/home.vue",
"chars": 338,
"preview": "<template>\n <div>\n <HelloWorld msg=\"hello vue3 + vite\"></HelloWorld>\n <router-link to=\"/home/1\">detail1</router-l"
},
{
"path": "src/views/users/components/detail.vue",
"chars": 2286,
"preview": "<template>\n <div class=\"container\">\n <el-form ref=\"form\" :model=\"model\" :rules=\"rules\">\n <el-form-item prop=\"na"
},
{
"path": "src/views/users/create.vue",
"chars": 180,
"preview": "<template>\n <detail :is-edit=\"false\"></detail>\n</template>\n\n<script>\nimport Detail from \"./components/detail.vue\";\n\nexp"
},
{
"path": "src/views/users/edit.vue",
"chars": 179,
"preview": "<template>\n <detail :is-edit=\"true\"></detail>\n</template>\n\n<script>\nimport Detail from \"./components/detail.vue\";\n\nexpo"
},
{
"path": "src/views/users/list.vue",
"chars": 2260,
"preview": "<template>\n <div class=\"app-container\">\n <div class=\"btn-container\">\n <!-- 新增按钮 -->\n <router-link to=\"/use"
},
{
"path": "src/views/users/model/userModel.js",
"chars": 1629,
"preview": "import { reactive, onMounted, ref } from \"vue\";\nimport request from \"utils/request\";\n\nexport function useList() {\n // 列"
},
{
"path": "vite.config.js",
"chars": 1061,
"preview": "import path from \"path\";\nimport vue from \"@vitejs/plugin-vue\";\nimport vueJsx from \"@vitejs/plugin-vue-jsx\";\nimport { vit"
}
]
About this extraction
This page contains the full source code of the 57code/vite2-in-action GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 36 files (47.7 KB), approximately 15.6k tokens, and a symbol index with 3 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.