相信我你也能从零搭建出一个CRM系统!

2,848 阅读2分钟

上一篇文章我们写了使用 webpack 从零开始搭建 vue 项目,这一篇内容在上一篇的基础上完成。 涉及内容和知识点如下: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

这个系列比较基础,但是做下来还是非常有收获,努力做到可复用的程度。哈哈,又进步了一点点!