上一篇文章我们写了(使用 webpack 从零开始搭建 vue 项目)[juejin.cn/post/703209… 涉及内容和知识点如下:flex 布局、vue-router、vuex、vue 组件间传参、watch、computed、webpack配置路径别名、axios 封装 这里只讲大概每个步骤的核心代码或知识点,具体代码移步github,每一个步骤按 commit 区分了。 非常适合没有自己动手搭建过的同学们,最后我们将完成一个 crm 的系统框架。
安装 less-loader
在布局之前,我们先让项目支持 less
npm i less less-loader -D
复制代码
在 webpack.config.js 中新增 rules
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader'
]
},
复制代码
flex 实现页面布局
使用 flex 实现左右、上下布局
- 实现一列固定,一列自适应的两列布局
<div class="right">
<div class="top"></div>
<div class="bottom"></div>
</div>
复制代码
.right {
flex: 1;
display: flex;
flex-direction: column;
.top {
height: 48px;
}
.bottom {
flex: 1;
}
}
复制代码
添加左侧菜单
我们项目建立在 element-ui 的基础上,现在直接使用 el-menu 来实现:
<el-menu
default-active="2"
class="el-menu-vertical-demo"
:router="true"
@open="handleOpen"
@close="handleClose"
@select="handleSelect"
>
<el-menu-item index="/home">
<i class="el-icon-menu"></i>
<span slot="title">首页</span>
</el-menu-item>
<el-menu-item index="/report">
<i class="el-icon-s-data"></i>
<span slot="title">报表</span>
</el-menu-item>
</el-menu>
复制代码
设置 router 为 true 时,启用 vue-router 模式,会在激活导航时以 index 作为 path 进行路由跳转。 安装 vue-router:
npm i vue-router
复制代码
引入 vue-router
import VueRouter from "vue-router";
let routes = [
{
path: "/home",
name: "",
},
];
let Routes = new VueRouter({
mode: "history",
routes: routes,
});
Vue.use(Routes); // use方法的作用全局注入插件
new Vue({
router: routes,
});
复制代码
router-link 的作用:
<router-link to="/home">首页</router-link>
复制代码
点击 link 跳转到指定 path。 router-view 的作用:
<router-view></router-view>
复制代码
在该位置渲染 path 对应的组件内容。
实现菜单展开收起(父子组件传参)
借助父子组件之间传参,实现菜单折叠: 父组件:
<!-- 父组件 -->
<template>
<Header :collapse="collapse" @toggaleCollapsed="changeCollapsed"></Header>
</template>
<script>
import Header from './header.vue';
export default {
components: {
Header,
}
}
</script>
复制代码
子组件:
<!-- 子组件 -->
<template>
<div>
<div class="btnBox" style="margin-right: 10px">
<el-button type="primary" size="mini" @click="toggleCollapsed">
<el-icon :class="collapse ? 'el-icon-s-unfold' : 'el-icon-s-fold'" />
</el-button>
</div>
</div>
</template>
<script>
export default {
props: ["collapse"],
methods: {
toggleCollapsed() {
this.collapse = !this.collapse;
this.$emit("toggaleCollapsed", this.collapse);
},
},
};
</script>
复制代码
添加菜单选项卡(watch)
常规 CRM 系统都会包含菜单选项卡,是为了切换方便,查看界面更直观。 菜单选项卡的交互逻辑是:
-
点击左侧菜单,增加对应选项卡,并激活
-
点击选项卡, 打开对应左侧菜单 借助父子组件传参数实现菜单选项卡:
在菜单组件中: activeMenu 使用父组件传递的 menu。
<template>
<el-menu :default-active="defaultActive"></el-menu>
</template>
<script>
export default {
props: ['defaultActive'],
}
</script>
复制代码
在 app 组件中: 负责管理 activeMenu
<template>
<Menu style="flex:1" :isCollapse="collapse" :defaultActive="defaultActive"></Menu>
<menu-tabs @changeActiveMenu="changeActiveMenu"></menu-tabs>
</template>
<script>
import MenuTabs from "./menuTabs.vue"
export default {
props: ['defaultActive'],
components: {
MenuTabs,
},
data(){
return {
defaultActive: "/home",
}
},
methods: {
changeActiveMenu(path){
this.defaultActive = path;
}
}
}
</script>
复制代码
在菜单选项卡组件中: 使用 watch 方法监听 path 变化,当变化时修改 tabList 并激活对应选项卡。点击 tab 时,通知 app 组件修改 activeMenu。
<template>
<div>
<el-tabs v-model="activeName" type="card" @tab-click="handleClick">
<el-tab-pane
v-for="tab in tabs"
:key="tab.name"
:label="tab.label"
:name="tab.name"
>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
export default {
data() {
return {
activeName: "/home",
tabs: [
{
label: "首页",
name: "/home",
path: "/home",
},
],
};
},
watch: {
$route(to, from) {
this.handleTabList(to);
},
},
methods: {
handleClick(tab, event) {
console.log(tab, event);
this.$router.push({ path: tab.name });
this.$emit("changeActiveMenu", tab.name);
},
handleTabList(to) {
let flag = this.tabs.find((item) => item.path === to.path);
if (!flag) {
this.tabs.push({
path: to.path,
label: to.name,
name: to.path,
});
}
this.activeName = to.path;
},
},
};
</script>
复制代码
这个功能看起来复杂,实际拆分以后很容易就实现了。
添加path别名
为了在import的时候更方便的找到相对路径,我们来给路径添加别名,修改webpack.config.js:
export default {
resolve: {
alias: {
"@": path.resolve("src"),
}
}
}
复制代码
现在我们使用 src 目录时就可以直接用 @ 代替了。
axios封装
import axios from 'axios';
import { Message } from 'element-ui';
// 设置baseURL
axios.defaults.baseURL = '/';
// 超时设置
axios.defaults.timeout = 30000;
// post请求的默认请求头
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
// 请求拦截器
axios.interceptors.request.use(
config => {
return config;
},
error => {
Message.error({
message: error.message || '请求出错',
duration: 1000,
});
return Promise.error(error);
})
// 响应拦截器
axios.interceptors.response.use(
response => {
// 如果返回的状态码为200,说明接口请求成功
// 否则的话抛出错误
if (response && response.status === 200) {
return Promise.resolve(response);
} else {
return Promise.reject(response);
}
},
error => {
Message({
type: 'error',
message: error || '响应出错',
duration: 5000,
});
if (error.response.status) {
switch (error.response.status) {
case 404:
Message.error({
message: '请求不存在',
duration: 1500,
});
break;
default:
Message.error({
message: error.response.data.message,
duration: 1500,
});
}
return Promise.reject(error.response);
}
}
);
// get 方法
export function get(url, params) {
return new Promise((resolve, reject) => {
axios.get(url, {
params: params
}).then(res => {
resolve(res.data);
}).catch(err => {
reject(err.data)
})
});
}
// post方法
export function post(url, params) {
return new Promise((resolve, reject) => {
axios.post(url, JSON.stringify(params), { headers: { 'Content-Type': 'application/json;charset=UTF-8' } })
.then(res => {
resolve(res.data);
})
.catch(err => {
reject(err.data)
})
});
}
// put方法
export function put(url, params) {
return new Promise((resolve, reject) => {
axios.put(url, JSON.stringify(params), { headers: { 'Content-Type': 'application/json;charset=UTF-8' } })
.then(res => {
resolve(res.data);
})
.catch(err => {
reject(err.data)
})
});
}
// delete方法
export function del(url, params) {
return new Promise((resolve, reject) => {
axios.delete(`${url}?id=${params.id}`,)
.then(res => {
resolve(res.data);
})
.catch(err => {
reject(err.data)
})
});
}
复制代码
以上封装了http请求的增删改查四种方法,现在将api请求都放到一个文件里:
import { get, post, put, del } from './axios';
// 添加成员
export const addMember = p => post('/api/add', p);
// 删除成员
export const deleteMember = p => del('/api/delete', p);
// 修改成员
export const updateMemberInfo = p => put('/api/edit', p);
// 查询成员列表
export const getMemberList = p => get('/mock/api/query', p);
复制代码
使用mockjs模拟接口请求数据
由于现在我们没有后端配合,想要获取数据只能自己mock了。这里选用mockjs来实现:
- 安装mockjs
npm i mockjs -D
复制代码
- mock api
// 引入mockjs
const Mock = require('mockjs')
// 获取 mock.Random 对象
const Random = Mock.Random
// mock数据,包括标题title、内容content、创建时间createdTime
const getData = function () {
let newsList = []
for (let i = 0; i < 10; i++) {
let newObject = {
title: Random.ctitle(), // Random.ctitle( min, max ) 随机产生一个中文标题,长度默认在3-7之间
content: Random.cparagraph(), // Random.cparagraph(min, max) 随机生成一个中文段落,段落里的句子个数默认3-7个
createdTime: Random.date() // Random.date()指示生成的日期字符串的格式,默认为yyyy-MM-dd;
}
newsList.push(newObject)
}
return newsList
}
// 请求该url,就可以返回newsList
Mock.mock('/mock/api/query', getData) // 后面讲这个api的使用细节
复制代码
修改report页面,使用table展示测试一下:
<template>
<div>
<el-table
border
:data="tableData"
:header-cell-style="{
background: '#f2f2f2 !important',
fontSize: '12px',
}"
>
<el-table-column label="标题" prop="title"></el-table-column>
<el-table-column
label="内容"
prop="content"
show-overflow-tooltip
></el-table-column>
<el-table-column label="创建时间" prop="createdTime"></el-table-column>
</el-table>
</div>
</template>
<script>
import { getMemberList } from "@/request/api.js";
export default {
data() {
return {
tableData: [],
};
},
created() {
getMemberList()
.then((res) => {
console.log(res);
this.tableData = res;
})
.catch((e) => {});
},
};
</script>
复制代码
这里只是展示查询mock,除此之外增加和删除也可以使用mock实现。
总结
到这里我们就搭建出了一个开箱即用的vue2开发环境了,完整代码已经同步到github,感兴趣的同学可以clone练习,下一篇我们来看看组件封装。
tips
这个系列比较基础,但是做下来还是非常有收获,努力做到可复用的程度。哈哈,又进步了一点点!