项目用VUE3+vuex+axios+elementPlus完成了基础的单页面程序,暂时没有登录,权限等模块。
- npm 6.0以上
- node 20.0以上
- vue cli 3.0以上
先上干货git地址:
创建项目
(这个网上有很多例子,就不细说了)
项目目录结构
项目目录结构如下图所示(这个图是项目搭建完成后的图,项目刚创建完的话,有些文件是没有的(vue.config,api.lib,config这些文件刚创建的项目应该都没有)):
安装各个插件(如果在创建项目时选择了安装这些插件,直接看怎么配置即可)
安装elementPlus
npm install element-plus --save
全局引入elementPlus配置
在main.js中
import ElementPlus from 'element-plus';
import 'element-plus/lib/theme-chalk/index.css';
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
参考链接:elementPlus官网
安装axios和vue-axios
vue-axios是一个vue整合axios的工具
npm install --save axios vue-axios
//在main.js中
.use(VueAxios, axios)
参考链接:http://www.axios-js.com/zh-cn/docs/vue-axios.html
安装预编译css插件Less
npm install --save less less-loader
main.js如下
import { createApp } from 'vue';
import App from './App.vue';
import router from './router/index';
import store from './store';
import ElementPlus from 'element-plus'; //引入elementPLus
import 'element-plus/lib/theme-chalk/index.css'; //elementPlus的css
import axios from 'axios'; //axios请求插件
import VueAxios from 'vue-axios'; //可以让vue直接使用(use)axios的小插件
import config from '@/config'; //配置后端请求地址的文件
//创建app
const app = createApp(App);
app.config.globalProperties = config; //后端请求地址
app.use(store)
.use(router)
.use(ElementPlus)
.use(VueAxios, axios)
.mount('#app');
export default app
后端请求地址config.js
其实这个js有用的只有baseUrl
export default {
/**
* @description 配置显示在浏览器标签的title
*/
// title: '测试',
/**
* @description api请求基础路径
*/
baseUrl: {
dev: 'http://localhost:8080/', // 我的主机
pro: '/'//线上服务器地址
},
/**
* @description 默认打开的首页的路由name值,默认为home
*/
// homeName: 'home',
}
配置公共css、less的全局引用,配置@指向
配置后可以全局使用特定文件夹里面的css、less。
vue.config.js里面(这里是整个文件的js):
const path = require('path');
const resolve = dir => {
return path.join(__dirname, dir)
};
/**
*默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上,
*例如 https://www.my-app.com/。
*如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。
*如果你的应用被部署在 https://www.my-app.com/my-app/,
*则设置 publicPath 为 /my-app/。
*这个值也可以被设置为空字符串 ('') 或是相对路径 ('./'),这样所有的资源都会被链接为相对路径,
*这样打出来的包可以被部署在任意路径,也可以用在类似 Cordova hybrid 应用的文件系统中。
*/
const PUBLIC_PATH = process.env.NODE_ENV === 'production' ? '/' : '/';
/**
* 引入全局的less文件
* @param {*} rule
*/
const addStyleResource = (rule) => {
rule.use('style-resource')
.loader('style-resources-loader')
.options({
patterns: [
// 需要全局导入的less,这个地方一点要写./src才行,否则会报错...
path.resolve(__dirname, './src/assets/css/publicCss.less')
]
});
};
module.exports = {
// 选项...
publicPath:PUBLIC_PATH,
chainWebpack: config => {
const types = ['vue-modules', 'vue', 'normal-modules', 'normal'];
types.forEach(type => addStyleResource(config.module.rule('less').oneOf(type))); //轮询加载全局样式
config.resolve.alias
//配置@指向src
.set('@', resolve('src')) // key,value自行定义,比如.set('@@', resolve('src/components'))
//配置_c指向src
.set('_c', resolve('src/components'))
},
devServer: {
overlay: {
warnings: true,
errors: true
}
},
css: {
loaderOptions: {
less: {
javascriptEnabled: true
}
}
}
};
设置请求拦截和响应拦截
我的文件名是axios.js
import axios from "axios";
import app from '@/main';
// 退出登录
const logout = () => {
app.config.globalProperties.$router.push({name: 'login'});
clearAllSessionStorage()
};
// 清空sessionStorage
const clearAllSessionStorage = () => {
sessionStorage.removeItem('token')
}
// 响应拦截
const respFilter = res => {
// elementPlus的信息提示
let message = app.config.globalProperties.$message;
let msg = {"content": res.data.message, "duration": 5};
let code = res.data.code;
let method = res.config.method;
if (method) {
method = method.toLowerCase();
}
if ((method !== 'options') && code === 'timeout') {
logout()
} else if (code === 'Unauthorized') {
message.error(msg);
} else if (code === 'system_error') {
message.error(msg);
}
if (res.data.success === false) {
message.error(msg);
}
};
class HttpRequest {
constructor(baseUrl = baseURL) {
this.baseUrl = baseUrl;
this.queue = {}
}
getInsideConfig() {
return {
baseURL: this.baseUrl,
headers: {
token: localStorage.getItem('token') || '',
}
}
}
interceptors(instance) {
// 请求拦截
instance.interceptors.request.use(config => {
return config
}, error => {
return Promise.reject(error)
});
// 响应拦截
instance.interceptors.response.use(
res => {
respFilter(res);
const {data, status, headers} = res;
return {data, status, headers}
},
error => {
return Promise.reject(error)
})
}
request(options) {
const instance = axios.create();
options = Object.assign(this.getInsideConfig(), options);
this.interceptors(instance, options.url);
return instance(options)
}
}
export default HttpRequest
使用拦截器
文件名request.js
import HttpRequest from './axios';
import config from '@/config';
const baseUrl = process.env.NODE_ENV === 'development' ? config.baseUrl.dev : config.baseUrl.pro;
const axios = new HttpRequest(baseUrl);
export default axios
vuex的分模块使用
src/store/index.js
import { createStore } from 'vuex'
import user from './module/user'
export default createStore({
state: {
},
mutations: {
},
actions: {
},
modules: {
user,
}
})
src/store/module/user.js
import {login} from "@/api/user";
export default {
state:{
},
mutations:{
},
actions:{
userLogin({commit},data){
return new Promise((resolve, reject) => {
login(data).then(res => resolve(res)).catch(error => reject(error))
})
}
}
}
src/api/user.js
import axios from '@/lib/request'
export const login = (data) =>{
return axios.request({
url: 'login',
data: data,
method: 'post'
})
}
发起一次请求
以登陆为例
- 在vue组件中:
<template>
<el-button @click="login">login</el-button>
</template>
<script>
import {reactive} from 'vue'
import {useStore} from 'vuex';
export default {
name: 'Home',
setup() {
let store = useStore();
const data = reactive({
userName: '张三',
});
const login = () => {
console.log(store);
store.dispatch('userLogin', {name: data.userName, sex: '男'}).then(res => {
console.log(res);
})
};
return {
login,
}
}
}
</script>
- src/store/module/user.js代码在上面
- src/api/user.js代码在上面
利用vue-router 和elementPlus的Container、NavMenu完成单页面
src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Routes from '@/router/routers';
// 导入routers的路由
const routes = Routes;
// 创建初始化router
const router = createRouter({
/**
* hash模式:createWebHashHistory,
* history模式:createWebHistory
*/
history: createWebHistory(),
routes:routes,
});
//配置导航守卫
router.beforeEach((to,from,next) => {
//如果访问的是登录页,直接放行
if(to.path === '/login') return next();
// 获取token
// const userInfo = sessionStorage.getItem('token');
const userInfo = '565432';
// 判断token是否存在,如果不存在返回登录页,存在就允许跳转
if(!userInfo) return next('/login');
next()
});
export default router
src/router/routers/index.js
import main from '@/components/index.vue'
import view from '@/components/menus/menu.vue'
/**
*
* 路由配置规则:
* 一级菜单的component必须为 main||() => import('@/components/index.vue')
* {
* path:'',路径
* name:'',路由名称,生成menu时menu显示的name
* breadcrumb: boolean值,true是可以点击面包屑跳转到对应页面,false||不写||null||unfinde不跳转
* ---------- ps:一般父级菜单项都不跳转
* redirect:'', 路由转发,可以转发到另外一个页面。
* -------------例如:访问 / 会转发到home再转发到 /home/index,/home/index才是需要显示的首页
* isShow:'', 是否显示为菜单,为TRUE时为菜单,false不是菜单不会在导航栏显示
* icon:'',图标,显示在名称前面的图标
* children: [], 子路由,menu中的子menu 没有时可为空数组或者不填,
* ----------- 当一个菜单有三级或者以上的时候需要在他的父级使用:
* ----------- component:view||component: () => import('@/components/menus/menu.vue'),这个属性才能显示出多子级菜单
* }
*
*/
const Routes = [
{
path: '/',
redirect: '/home',
isShow: false
},
{
path: '/login',
name: '登录',
isShow: false,
component: () => import('@/components/login/login.vue'),
},
{
path: '/home',
name: '首页',
isShow: false,
component:main,
breadcrumb:true,
redirect:'/home/index',
children:[
{
path: '/home/index',
isShow: false,
icon: 'el-icon-s-home',
component: () => import('@/views/home.vue'),
},
]
},
{
path: '/system',
name: '系统管理',
isShow: true,
icon: 'el-icon-s-home',
breadcrumb:false,
component:main,
children:[
{
path: 'user',
name: '用户管理',
isShow: true,
icon: 'el-icon-user',
component:view,
children:[
{
path: '/system/user/home',
name: '添加用户',
isShow: true,
icon: 'el-icon-user',
breadcrumb:true,
component: () => import('@/views/system/Home.vue')
},
{
path: '/system/user/user',
name: '用户列表',
isShow: true,
icon: 'el-icon-user',
breadcrumb:true,
component: () => import('@/views/system/user.vue')
}
]
},
{
path: '/system/doc',
name: '文档',
isShow: true,
breadcrumb:true,
icon: 'el-icon-s-order',
component: () => import('@/views/system/About.vue'),
},
]
},
{
path: '/chart',
name: '图表',
isShow: true,
breadcrumb:true,
icon: 'el-icon-s-order',
component:main,
children:[
{
path: '/chart/echarts',
name: '柱状图',
isShow: true,
breadcrumb:true,
icon: 'el-icon-s-order',
component:() => import('@/views/system/echarts.vue')
}
]
},
];
export default Routes;
app.vue中添加
<router-view></router-view>
src/components/index.vue文件(页面布局文件)
<template>
<div>
<el-container>
<!-- 左侧导航栏-->
<el-aside width="200px">
<!-- 左侧导航栏组件-->
<Sidebar></Sidebar>
</el-aside>
<el-container>
<!-- 头部-->
<el-header style="background-color: blue">
<!-- 头部组件-->
<Header></Header>
</el-header>
<!-- 主体-->
<el-main>
<!-- 路由 占位符-->
<Menu></Menu>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
import Header from '@/components/Header/index.vue'
import Sidebar from '@/components/Sidebar/index.vue'
import Menu from '@/components/menus/menu.vue'
export default {
setup() {
},
components: {
Sidebar,
Header,
Menu
}
};
</script>
<style lang="less" scoped>
</style>
左侧导航采用递归展示菜单
src/components/Sidebar/index.vue
<template>
<!-- 导航栏的顶部-->
<el-affix class="el-affix-Cursor" :z-index="1200" style="" @click="gotoHome">
<div>
<span>vue3练习</span>
</div>
</el-affix>
<el-menu
:default-active="route.path"
class="el-menu-vertical-demo"
active-text-color="#ffd04b"
background-color="#545c64"
text-color="#fff"
:unique-opened="true"
router>
<!-- 导航组件-->
<Sidebar :routerList="routers"></Sidebar>
</el-menu>
</template>
<script>
import Sidebar from '@/components/Sidebar/sidebar.vue'
import {reactive, onMounted, toRefs} from 'vue'
import {useRouter,useRoute} from 'vue-router'
export default {
setup() {
const routers = useRouter();
const data = reactive({
routers:routers.options.routes,
route: useRoute(),
});
const gotoHome = () => {
routers.push({
path:'/home'
});
};
const handleOpen = (key, keyPath) => {
console.log(key, keyPath);
};
onMounted(() => {
},
);
return {
handleOpen,
gotoHome,
...toRefs(data)
};
},
components:{
Sidebar
}
};
</script>
<style lang="less" scoped>
.el-affix{
display: flex;
justify-content: center;
align-items: center;
height: 60px;
background-color: cadetblue;
cursor: pointer;
}
</style>
src/components/Sidebar/sidebar.vue
<template>
<!-- 递归显示导航菜单-->
<div v-for="(item) in routerArray" :key="item.path">
<!--只有一级菜单-->
<el-menu-item v-if="!item.children && item.isShow" :index="item.path">
<!--图标-->
<i :class="item.icon"></i>
<!--标题-->
<span>{{item.name}}</span>
</el-menu-item>
<!--有多级菜单-->
<el-submenu v-if="item.children && item.isShow" :index="item.path">
<template #title>
<i :class="item.icon"></i>
<span> {{item.name}}</span>
</template>
<!--递归组件,把遍历的值传回子组件,完成递归调用-->
<Sidebar :routerList="item.children"></Sidebar>
</el-submenu>
</div>
</template>
<script>
import {reactive, onBeforeMount, toRefs} from 'vue'
export default {
name: "sidebar",
props: {
routerList: {
type: Array,
},
},
setup(props) {
const data = reactive({
routerArray: props.routerList
});
onBeforeMount(() => {
});
return {
...toRefs(data)
}
}
}
</script>
<style lang="less" scoped>
</style>
到这项目基本就搭建完成了,缺少的主题内容和头部内容随便创建个组件就可以了(当然最主要的是它太长了QAQ),以后可能会拆分。