环境准备

  1. node: v16.20.2
  2. npm: 8.19.4

依赖项

dependencies

依赖项版本描述
vue3.4.15vue框架
vue-router4.2.5页面路由管理
axios1.6.7http请求库
echarts5.5.0echarts图表
pinia2.1.7状态管理
element-plus2.6.0ui组件
chinese-lunar0.1.4阳历转农历
chinese-workday1.10.0根据日期获取节假日
@vueuse/core10.9.0工具类

devDependencies

依赖项版本描述
@element-plus/icons-vue2.3.1element-plus 内置图标 icons
@vitejs/plugin-vue5.0.3Vite 项目配置 Vue 相关的特殊行为,比如向 Vue 编译器传递相关选项,请查看
pinia-plugin-persistedstate3.2.1状态处理持久化插件
sass1.71.1scss样式与处理器
unplugin-auto-import0.17.5自动导入插件
unplugin-icons0.18.5自动导入-直接在模板中使用图标作为组件。
unplugin-vue-components0.26.0自动导入组件插件,
vite5.0.11vite构建工具
vite-plugin-svg-icons2.0.1一个svg图标的插件,无需每个svg都发起http请求, 组件可以改变color、size【仅支持单色】, 支持全部iconfont.cn上的svg
vite-plugin-vue-setup-extend0.4.0定义组件名称

初始化项目

1
npm create vue@latest

按照命令提示进行选择

1
2
3
4
5
6
7
8
9
10
11
12
✔ Project name: … <your-project-name>
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit testing? … No / Yes
✔ Add an End-to-End Testing Solution? … No / Cypress / Playwright
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formatting? … No / Yes

Scaffolding project in ./<your-project-name>...
Done.

进入项目

1
2
cd <your-project-name>
npm install or pnpm install

1
npm run dev

项目目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
── README.md
├── index.html
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
│   └── favicon.ico
├── src
│   ├── App.vue
│   ├── api
│   ├── assets
│   │   └── logo.svg
│   ├── axios
│   │   └── index.js
│   ├── components
│   │   ├── px-calander
│   │   │   └── index.vue
│   │   ├── px-card
│   │   │   └── index.vue
│   │   └── svg-icon
│   │   └── index.vue
│   ├── config
│   │   └── setting.js
│   ├── icons
│   │   ├── btns.svg
│   │   ├── card.svg
│   │   ├── compoents.svg
│   │   ├── dashbord.svg
│   │   └── login-bg.svg
│   ├── layout
│   │   ├── components
│   │   │   ├── AppHeader.vue
│   │   │   ├── AppMain.vue
│   │   │   ├── AppSidebar.vue
│   │   │   ├── Breadcrumb.vue
│   │   │   └── SubMenuItem.vue
│   │   └── index.vue
│   ├── main.js
│   ├── plugin
│   │   ├── icons.js
│   │   └── permission.js
│   ├── router
│   │   └── index.js
│   ├── store
│   │   ├── index.js
│   │   └── modules
│   │   ├── system.js
│   │   └── user.js
│   ├── styles
│   │   ├── app.scss
│   │   ├── element-variables.scss
│   │   ├── element.scss
│   │   ├── index.scss
│   │   ├── transition.scss
│   │   └── variables.scss
│   ├── utils
│   │   └── index.js
│   └── views
│   ├── components
│   │   ├── button
│   │   │   └── index.vue
│   │   └── card
│   │   └── index.vue
│   ├── dashboard
│   │   └── index.vue
│   ├── error-page
│   │   └── index.vue
│   └── login
│   └── index.vue
└── vite.config.js

这里按照以上工程目录来依次创建文件夹src/
apiassetsaxioscomponentsconfigiconslayoutpluginrouterstorestylesutilsviewshooks

目录名描述
apiapi接口
assets静态资源
axiosaxios封装
components组件
config配置
icons图标
layout布局
plugin插件
router路由
store状态管理
styles样式
utils工具
views页面
hookshooks

插件和入口文件配置

插件配置

这里在配置前,请确保package.jsondependenciesdevDependencies中已经安装了对应的依赖!

elementui plus

这里有全部导入 按需导入 手动导入三种方式,我这边用的按需导入配置!

按需导入

首先你需要安装unplugin-vue-componentsunplugin-auto-import这两款插件!

npm install -D unplugin-vue-components unplugin-auto-import

然后把下列代码插入到你的 Vite !

vite.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
// ...
plugins: [
// ...
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})

引入 elementui icons

1
npm install @element-plus/icons-vue -S

新建plugins/icons

icons
1
2
3
4
5
6
7
8
import * as ElementPlusIconsVue from "@element-plus/icons-vue";

export function setupElIcons( app ){
// 注册 element ui icon 组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
}

在 main.js 中引入即可!

1
2
3
4
import { setupElIcons } from '@/plugin/icons';
const app = createApp(App)
// 全局注册 element plus icons
setupElIcons(app);

pinia 状态管理

这是一个简单的配置!

main.js

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia)
app.mount('#app')

下面在项目中我门需要通过模块化配置!

新建store/index.js

store/index
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { createPinia } from 'pinia'
// 引入持久化插件
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";

const pinia = createPinia();

export function setupPinia( app ){
app.use(pinia);
}

// 持久化
pinia.use(piniaPluginPersistedstate);

// 用户
export * from "./modules/user";
// 系统
export * from "./modules/system";

export { pinia };

新建store/modules/user.jsstore/modules/system.js

store/modules/user.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import defaultSetting from "@/config/setting";
import { defineStore } from "pinia";

export const useUserStore = defineStore("user", {
state: () => ({
username: "",
token: ""
}),
persist: {
storage: sessionStorage,
},
actions: {
// 登录
async login( params ){
return new Promise((resolve, reject) =>{
console.log("useUserStore login params", params);
let { defaultUserName, defaultPassword } = defaultSetting;
let { username, password } = params;
let result = {
success: true,
data: { username: "admin", token: Date.now() },
};

if (username !== defaultUserName ) {
return resolve({
success: false,
message: "用户名填写不正确,请检查与'defaultUserName'配置是否匹配一致!",
});
}
if (password !== defaultPassword ) {
return resolve({
success: false,
message: "密码填写不正确,请检查与'defaultPassword'配置是否匹配一致!",
});
}
this.token = result.data.token;
this.username = username;
return resolve(result);
})
},
// 退出登录
async logout() {
return new Promise((resolve, reject) => {
this.token = "";
this.username = "";
return resolve({success: true, message: "注销成功!"});
});
},
}
});
store/modules/system.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { defineStore } from "pinia";

export const useSystemStore = defineStore("system", {
state: () => ({
isCollapse: false,
theme: "light",
size: "default",
locale: "zh-cn",
}),
persist: {
storage: sessionStorage,
},
actions: {
// 设置 sidebar collapse status
setCollapse(isCollapse) {
this.isCollapse = isCollapse;
},
// 设置 主题
setTheme(theme) {
this.theme = theme;
},
// 设置 组件大小
setSize(size) {
this.size = size;
},
// 设置 本地语言
setLocale(locale) {
this.locale = locale;
},
},
});

为了防止刷新数据丢失情况!
persist: sessionStorage 为持久化插件配置,可选择 localStoragesessionStorage方式进行存储!
npm install pinia-plugin-persistedstate -S

然后在 index.js 中引入模块即可!

main.js中引用store/index.js

1
2
3
4
5
6
7
import { createApp } from 'vue'
import App from "./App.vue";
import { setupPinia } from "@/store/index.js";
const app = createApp(App)
// 全局注册 store
setupPinia(app);
app.mount('#app')

vue-router

新建src/router/index.js

router/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import { createRouter, createWebHistory } from 'vue-router'
import Layout from "@/layout/index.vue";
import Login from '@/views/login/index.vue'

const routes = [
{
path: "/login",
name: "Login",
component: Login,
meta: { title: "登录", hidden: true },
},
{
path: "/",
name: "dashboard",
component: Layout,
redirect: "/dashboard",
meta: { title: "首页", icon: "el-icon-dashbord" },
children: [
{
path: "dashboard",
name: "Dashboard",
component: () =>
import(
/* webpackChunkName: "dashboard" */ "@/views/dashboard/index.vue"
),
meta: { title: "首页", icon: "el-icon-dashbord" },
},
],
},
{
path: "/components",
name: "组件",
component: Layout,
redirect: "/components/button",
meta: {
title: "组件",
icon: "el-icon-compoents",
},
children: [
{
path: "button",
name: "按钮",
component: () =>
import(
/* webpackChunkName: "button" */ "@/views/components/button/index.vue"
),
meta: {
title: "按钮",
icon: "el-icon-btns",
transition: "slide-left",
},
},
{
path: "card",
name: "卡片",
component: () =>
import(
/* webpackChunkName: "card" */ "@/views/components/card/index.vue"
),
meta: {
title: "卡片",
icon: "el-icon-card",
transition: "slide-left",
},
},
],
},
// 将匹配所有内容并将其放在 `$route.params.pathMatch` 下
{
path: "/:pathMatch(.*)*",
name: "NotFound",
component: Layout,
meta: { hidden: true },
children: [
{
path: "/:pathMatch(.*)*",
name: "NotFound",
component: () =>
import(/* webpackChunkName: "404" */ "@/views/error-page/index.vue"),
},
],
},
];

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: routes,
// 刷新时,滚动条位置还原
scrollBehavior: () => ({ left: 0, top: 0 }),
});

export default router

配置路由权限
新建plugins/permission.js

permission
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import router from "@/router"
import { useUserStore } from "@/store";
import { setPageTitle } from "@/utils";

export function setupPermissions(){
let whites = ['Login']
const userStore = useUserStore();
router.beforeEach((to, from) => {
to.meta?.title && setPageTitle(to.meta.title);
const tokenIsNull = !!userStore.token;
// 不是登录页 且 token 为空 跳转到登录页
if (whites.indexOf(to.name) === -1 && !tokenIsNull) {
return "/login"
}
});
}

main.js中引用router/index.js

1
2
3
4
5
6
7
8
import { createApp } from 'vue'
import App from "./App.vue";
import router from './router'
const app = createApp(App)
app.use(router)
// 路由权限
setupPermissions();
app.mount('#app')

axios

入口文件main.js

main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import './styles/index.scss'
// elementui 暗黑主题
import "element-plus/theme-chalk/dark/css-vars.css";
import { createApp } from 'vue'
// import { createPinia } from 'pinia'
import { setupPinia } from "@/store/index.js";
import { setupElIcons } from '@/plugin/icons';
import { setupPermissions } from "@/plugin/permission";
// import ElementPlus from "element-plus";
// import "element-plus/dist/index.css";
// import * as ElementPlusIconsVue from "@element-plus/icons-vue";
import App from "./App.vue";
import router from './router'

// 本地SVG图标
import "virtual:svg-icons-register";

const app = createApp(App)
// app.use(createPinia())
app.use(router)
// app.use(ElementPlus)
// 全局注册 element plus icons
setupElIcons(app);
// 全局注册 store
setupPinia(app);
// 路由权限
setupPermissions();
app.mount('#app')

vite.config.js

vite.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import Icons from "unplugin-icons/vite";
import { createSvgIconsPlugin } from "vite-plugin-svg-icons";

import { resolve } from "path";
const pathSrc = resolve(__dirname, "src");

// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
server: {
/*
@default: 5173
指定开发服务器端口。注意:如果端口已经被使用,Vite 会自动尝试下一个可用的端口,所以这可能不是开发服务器最终监听的实际端口。
*/
port: 8089,
/*
@default: localhost
指定服务器应该监听哪个 IP 地址。 如果将此设置为 0.0.0.0 或者 true 将监听所有地址,包括局域网和公网地址。
*/
host: "0.0.0.0",
// 设为 true 时若端口已被占用则会直接退出,而不是尝试下一个可用端口。
strictPort: true,
},
proxy: {
// 使用 proxy 实例
"/api": {
target: "http://jsonplaceholder.typicode.com",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
css: {
// CSS 预处理器
preprocessorOptions: {
// 定义全局 SCSS 变量
scss: {
javascriptEnabled: true,
additionalData: `
@use "@/styles/element-variables.scss" as *;
@use "@/styles/variables.scss" as *;
`,
},
},
},
plugins: [
vue(),
// 自动导入
AutoImport({
// 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
imports: [
"vue",
"@vueuse/core",
"pinia",
"vue-router" /* , "vue-i18n" */,
],
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
// 指定自定义组件位置(默认:src/components) "src/**/components"
// 指定后缀为 vue
extensions: ["vue"],
dirs: ["src/components/"],
}),
// 配置icons
Icons({
autoInstall: true,
}),
createSvgIconsPlugin({
// 指定需要缓存的图标文件夹
iconDirs: [resolve(pathSrc, "icons")],
// 指定symbolId格式
symbolId: "icon-[dir]-[name]",
}),
],
});

公共组件

svg-icons

该组件方便我们在项目中使用svg图标!

新建svg-icons组件

components/svg-icons/index.vue

svg-icons
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<template>
<svg
aria-hidden="true"
class="svg-icon"
:style="'width:' + size + ';height:' + size"
>
<use :xlink:href="symbolId" :fill="color" />
</svg>
</template>

<script setup lang="ts">
import { computed } from "vue";

const props = defineProps({
prefix: {
type: String,
default: "icon",
},
iconClass: {
type: String,
required: false,
default: "",
},
color: {
type: String,
default: "",
},
size: {
type: String,
default: "1em",
},
});

const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`);
</script>

<style scoped>
.svg-icon {
display: inline-block;
width: 1em;
height: 1em;
overflow: hidden;
vertical-align: -0.15em; /* 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 */
outline: none;
fill: currentColor; /* 定义元素的颜色,currentColor是一个变量,这个变量的值就表示当前元素的color值,如果当前元素未设置color值,则从父元素继承 */
}
</style>

入口文件中引用

1
2
// 本地SVG图标
import "virtual:svg-icons-register";

配置vite.config.js

1
2
npm install unplugin-icons -D # 配置自动导入
npm install vite-plugin-svg-icons -D # 配置本地SVG图标
vite.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import Icons from "unplugin-icons/vite";
import { createSvgIconsPlugin } from "vite-plugin-svg-icons";

export default defineConfig({
plugins: [
vue(),
// 自动导入
AutoImport({
// 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
imports: [
"vue",
"@vueuse/core",
"pinia",
"vue-router" /* , "vue-i18n" */,
],
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
// 指定自定义组件位置(默认:src/components) "src/**/components"
// 指定后缀为 vue
extensions: ["vue"],
dirs: ["src/components/"],
}),
// 配置icons
Icons({
autoInstall: true,
}),
createSvgIconsPlugin({
// 指定需要缓存的图标文件夹
iconDirs: [resolve(pathSrc, "icons")],
// 指定symbolId格式
symbolId: "icon-[dir]-[name]",
}),
]
})

页面布局

页面构建

登录页

首页