学习从零到一搭建后台管理系统(vue2+element-ui)的详细过程

702 阅读4分钟

项目的github地址: github.com/bailicangdu…

项目的在线网址:cangdu.org/manage/#/

1.项目初始化

vue-create直接创建一个vue2默认项目就行,然后刚开始要引入element,和less和less-loader,注意版本对应,还有vue-router。

2.搭建左侧菜单栏

image.png

用饿了么的navMenu组件搭建。

el-submenu就是下面有子选项的。

然后还有el-menu的default-active和router属性理解一下。

每个el-menu-item的index

用el-menu的background-color等属性实现侧边栏自定义。

ManageView这个页面要多加一个el-col里面包含router-view用来放/manage路由的子路由组件

<template>
  <div>
    <el-col :span="4" style="min-height: 100%; background-color: #324057">
      <el-menu
        :default-active="defaultActive"
        style="min-height: 100%"
        background-color="#545c64"
        text-color="#fff"
        active-text-color="#ffd04b"
        router
      >
        <!-- default-active	当前激活菜单的 index -->
        <!-- menu的router是否使用 vue-router 的模式,启用该模式会在激活导航时以 index 作为 path 进行路由跳转 -->
        <el-menu-item index="manage"
          ><i class="el-icon-menu"></i>首页</el-menu-item
        >
        <el-submenu index="2">
          <template slot="title"
            ><i class="el-icon-document"></i>数据管理</template
          >
          <el-menu-item index="userList">用户列表</el-menu-item>
          <el-menu-item index="shopList">商家列表</el-menu-item>
          <el-menu-item index="foodList">食品列表</el-menu-item>
          <el-menu-item index="orderList">订单列表</el-menu-item>
          <el-menu-item index="adminList">管理员列表</el-menu-item>
        </el-submenu>
        <el-submenu index="3">
          <template slot="title"><i class="el-icon-plus"></i>添加数据</template>
          <el-menu-item index="addShop">添加商铺</el-menu-item>
          <el-menu-item index="addGoods">添加商品</el-menu-item>
        </el-submenu>
        <el-submenu index="4">
          <template slot="title"><i class="el-icon-star-on"></i>图表</template>
          <el-menu-item index="visitor">用户分布</el-menu-item>
          <!-- <el-menu-item index="newMember">用户数据</el-menu-item> -->
        </el-submenu>
        <el-submenu index="5">
          <template slot="title"><i class="el-icon-edit"></i>编辑</template>
          <!-- <el-menu-item index="uploadImg">上传图片</el-menu-item> -->
          <el-menu-item index="vueEdit">文本编辑</el-menu-item>
        </el-submenu>
        <el-submenu index="6">
          <template slot="title"><i class="el-icon-setting"></i>设置</template>
          <el-menu-item index="adminSet">管理员设置</el-menu-item>
          <!-- <el-menu-item index="sendMessage">发送通知</el-menu-item> -->
        </el-submenu>
        <el-submenu index="7">
          <template slot="title"><i class="el-icon-warning"></i>说明</template>
          <el-menu-item index="explain">说明</el-menu-item>
        </el-submenu>
      </el-menu>
    </el-col>
     <el-col :span="20" style="height: 100%; overflow: auto">
      <keep-alive>
        <router-view></router-view>
      </keep-alive>
    </el-col>
  </div>
</template>

<script>
export default {
  name: "ManageView",
  data() {
    return {};
  },
  created() {
    console.log(this.$route.path);
  },
  computed: {
    defaultActive() {
      return this.$route.path.replace("/", "");
      // replace方法用于在字符串中用一些字符替换另一些字符
    },
  },
};
</script>
<style>
</style>

顶部headTop的开发

  1. 顶部左边的开发使用elm的Breadcrumb组件,separatord属性用来设置分隔符。
  2. 以页面说明页为例,给他配路由,并且meta写成['说明','说明'],然后el-breadcrumb循环这个meta。就可以达到如下效果。

image.png

  {
            path: '/manage',
            // component: manage,
            component:()=> import('@/views/ManageView.vue'),
            name: '',
            children:[{
                path:"/explain",
                component:()=> import('@/views/ExplainView.vue'),
                meta:['说明','说明']
            }]
 <el-breadcrumb separator="/">
        <!-- to	路由跳转对象,同 vue-router 的 to -->
      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
      <el-breadcrumb-item v-for="(item,index) in $route.meta" :key="index">{{item}}</el-breadcrumb-item>
    </el-breadcrumb>
  1. App.vue中不需要引入任何组件,只要加上router-view即可,用来放父路由/manage或者是/。(总结一下就是我们的没有子路由的路由要用router-view放的话,可以放在app.vue中,因为vue项目是单页面的,所有东西都在app.vue中。子路由组件要用router-view放的话,就是在父路由组件中加个router-view)
 routes: [
        {
            path: '/',
            component: () =>import('@/views/LoginView.vue')
        },
        {
            path: '/manage',
            // component: manage,
            component:()=> import('@/views/ManageView.vue'),
            name: '',
            children:[{
                path:"/explain",
                component:()=> import('@/views/ExplainView.vue'),
                meta:['说明','说明']
            }]
  1. HeadTop顶部栏组件右边的开发。用饿了么的Dropdown组件开发。暂时写成这样:
<template>
  <div class="header_container">
    <el-breadcrumb separator="/">
      <!-- to	路由跳转对象,同 vue-router 的 to -->
      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
      <el-breadcrumb-item v-for="(item, index) in $route.meta" :key="index">{{
        item
      }}</el-breadcrumb-item>
    </el-breadcrumb>
    <el-dropdown>
      <img src="../assets/images/user.png" class="avator" alt="" />
      <el-dropdown-menu slot="dropdown">
        <el-dropdown-item>首页</el-dropdown-item>
        <el-dropdown-item>退出</el-dropdown-item>
      </el-dropdown-menu>
    </el-dropdown>
  </div>
</template>

<script>
export default {
  name: "HeadTop",
};
</script>

<style lang="less" scoped>
.header_container {
  background-color: #eff2f7;
  height: 60px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding:0 20px;
  .avator {
    height: 36px;
    width: 36px;
    border-radius: 50%;
  }
}
</style>

HomeView组件的开发

image.png

  1. 在views文件夹下面建HomeView.vue组件,再去配路由
 {
            path: '/manage',
            // component: manage,
            component:()=> import('@/views/ManageView.vue'),
            name: '',
            children:[
                {
                    path:"",
                    component:()=>import('@/views/HomeView.vue'),
                    meta:[]
                },
  1. 上面那两行用到饿了么的分栏间隔,先这样,后面再调接口
<template>
  <div>
    <head-top></head-top>
    <section class="data_section">
      <header class="section_title">数据统计</header>
      <el-row :gutter="20">
        <el-col :span="4"
          ><div class="grid-content today_head">
            <span class="data_num head">当日数据:</span>
          </div></el-col
        >
        <el-col :span="4"><div class="grid-content">新增用户</div></el-col>
        <el-col :span="4"><div class="grid-content">新增订单</div></el-col>
        <el-col :span="4"><div class="grid-content">新增管理员</div></el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="4"
          ><div class="grid-content all_head">
            <span class="data_num head">总数据:</span>
          </div></el-col
        >
        <el-col :span="4"><div class="grid-content">注册用户</div></el-col>
        <el-col :span="4"><div class="grid-content">订单</div></el-col>
        <el-col :span="4"><div class="grid-content">管理员</div></el-col>
      </el-row>
    </section>
  </div>
</template>

<script>
import HeadTop from "@/components/HeadTop.vue";
export default {
  components: {
    HeadTop,
  },
};
</script>

<style lang="less" scoped>
.data_section {
  padding: 20px;
  margin-bottom: 40px;
  .section_title {
    text-align: center;
    font-size: 30px;
    margin-bottom: 10px;
  }
  .today_head {
    background: #ff9800;
  }
  .all_head {
    background: #20a0ff;
  }
  .data_num {
    color: #e5e9f2;
    font-size: 26px;
  }
}
.el-row {
  margin-bottom: 20px;
  &:last-child {
    margin-bottom: 0;
  }
}
.el-col {
  border-radius: 4px;
  text-align: center;
  line-height: 36px;
}
.bg-purple-dark {
  background: #99a9bf;
}
.bg-purple {
  background: #d3dce6;
}
.bg-purple-light {
  background: #e5e9f2;
}
.grid-content {
  border-radius: 4px;
  min-height: 36px;
  background-color: #e5e9f2;
}
.row-bg {
  padding: 10px 0;
  background-color: #f9fafc;
}
</style>
  1. 然后再做下面的折线图。

UserList用户列表的组件开发

用饿了么的table组件,上面还是那个HeadTop,然后Table,然后分页组件,假数据加基本组件的应用如图

image.png

<template>
  <div>
    <head-top></head-top>
    <div class="table_container">
      <el-table :data="tableData" highlight-current-row style="width: 100%">
        <el-table-column type="index" width="100"> </el-table-column>
        <el-table-column property="registe_time" label="注册日期" width="220">
        </el-table-column>
        <el-table-column property="username" label="用户姓名" width="220">
        </el-table-column>
        <el-table-column property="city" label="注册地址"> </el-table-column>
      </el-table>
      <div class="Pagination" style="text-align: left; margin-top: 10px">
        <el-pagination background small layout="prev, pager, next" :total="50">
        </el-pagination>
      </div>
    </div>
  </div>
</template>
<script>
import HeadTop from "@/components/HeadTop.vue";
export default {
  components: { HeadTop },
  data() {
    return {
      tableData: [
        {
          registe_time: "2016-05-02",
          username: "王小虎",
          city: "上海市普陀区金沙江路 1518 弄",
        },
        {
          registe_time: "2016-05-04",
          username: "王小虎",
          city: "上海市普陀区金沙江路 1517 弄",
        },
        {
          registe_time: "2016-05-01",
          username: "王小虎",
          city: "上海市普陀区金沙江路 1519 弄",
        },
        {
          registe_time: "2016-05-03",
          username: "王小虎",
          city: "上海市普陀区金沙江路 1516 弄",
        },
      ],
      currentRow: null,
      offset: 0,
      limit: 20,
      count: 0,
      currentPage: 1,
    };
  },
};
</script>
<style lang="less" scoped>
.table_container {
  padding: 20px;
}
</style>

然后就是要调接口展示数据了,纯展示。

在下步调完接口后,要对分页组件做下处理,控制用户列表页数据的展示。

<div class="Pagination" style="text-align: left; margin-top: 10px">
       <!-- @current-change是当前页改变的事件 -->
       <!-- total是整个分页的数据条数,共count条 -->
        <!-- current-page="currentPage"代表当前选中页 -->
        <el-pagination
          @current-change="handleCurrentChange"
          :current-page="currentPage"
          :page-size="20"
          layout="total, prev, pager, next"
          :total="count"
        >
        </el-pagination>

这个是控制分页组件当前页改变的回调

   // 当前页改变时的回调
    handleCurrentChange(val){
       //val是当前某页
      this.offset = (val - 1) * this.limit;
      console.log(this.offset); //这个offset没有接口文档也不懂啥意思,感觉像是补偿的意思。
      // 每次切换当前页发一次请求,返回数据条数还是限制为20
      this.getUserList();
    }

现在开始封装一下axios,源码用fetch做的,试试用axios写下。

src下面新建utils/request.js

import axios from 'axios'
import Vue from 'vue'
Vue.prototype.$http = axios

const request = axios.create({
    baseURL: "http://elm.cangdu.org",
    timeout: 5000
})


request.interceptors.request.use(function (config) {
    return config;
}, function (error) {
    return Promise.reject(error);
});

request.interceptors.response.use(function (response) {
    const res = response.data
    if (res.status !== 200) {
        // 请求失败
        return res
    }
    else {
        return res
    }
}, function (error) {
    return Promise.reject(error);
});

export default request

然后再src下新建api/user.js,放有关用户的调接口的方法。

import request from '@/utils/request'

export const getUserList = function(params={}){
    return request({
        method:'get',
        url:"/v1/users/list",
        params
    })
}

然后再以用户列表页为例,去调用接口获取数据,这里给接口传个params参数限制一下接收的列表的大小。

import { getUserList } from "@/api/user.js";
async getUserList() {
      try {
        this.tableData = await getUserList({ offset: this.offset, limit: this.limit });
        console.log(this.tableData);
      } catch (error) {
        console.log(error);
      }
    },

然后UserList页面完成,代码如下:

image.png

<template>
  <div>
    <head-top></head-top>
    <div class="table_container">
      <el-table :data="tableData" highlight-current-row style="width: 100%">
        <el-table-column type="index" width="100"> </el-table-column>
        <el-table-column property="registe_time" label="注册日期" width="220">
        </el-table-column>
        <el-table-column property="username" label="用户姓名" width="220">
        </el-table-column>
        <el-table-column property="city" label="注册地址"> </el-table-column>
      </el-table>
      <div class="Pagination" style="text-align: left; margin-top: 10px">
       <!-- @current-change是当前页改变的事件 -->
       <!-- total是整个分页的数据条数,共count条 -->
        <!-- current-page="currentPage"代表当前选中页 -->
        <el-pagination
          @current-change="handleCurrentChange"
          :current-page="currentPage"
          :page-size="20"
          layout="total, prev, pager, next"
          :total="count"
        >
        </el-pagination>
      </div>
    </div>
  </div>
</template>

<script>
import HeadTop from "@/components/HeadTop.vue";
import { getUserList } from "@/api/user.js";

export default {
  components: { HeadTop },
  data() {
    return {
      tableData: [],
      offset: 0,
      limit: 20,
      count: 100,
      currentPage: 1,
    };
  },
  created() {
    this.getUserList();
  },
  methods: {
    async getUserList() {
      try {
        this.tableData = await getUserList({
          offset: this.offset,
          limit: this.limit,
        });
        console.log(this.tableData);
      } catch (error) {
        console.log(error);
      }
    },
    // 当前页改变时的回调
    handleCurrentChange(val){
       //val是当前某页
      this.offset = (val - 1) * this.limit;
      console.log(this.offset); //这个offset没有接口文档也不懂啥意思,感觉像是补偿的意思。
      // 每次切换当前页发一次请求,返回数据条数还是限制为20
      this.getUserList();
    }
  },
};
</script>

<style lang="less" scoped>
.table_container {
  padding: 20px;
}
</style>

然后再做管理员列表组件,和这个用户列表组件非常相似

先去配路由

                {
                    path: '/adminList',
                    component: () => import('@/views/AdminList.vue'),
                    meta: ['数据管理', '管理员列表']
                }

再去api/新建admin.js封装有关管理员的请求方法。

import request from '@/utils/request'

export const getAdminList = function(params={}){
    return request({
        method:'get',
        url:"/admin/all",
        params
    })
}

再直接复制用户列表的代码再修改一下就完成了如下:

image.png

<template>
  <div>
    <head-top></head-top>
    <div class="table_container">
      <el-table :data="tableData" highlight-current-row style="width: 100%">
        <el-table-column width="100" property="user_name" label="姓名"> </el-table-column>
        <el-table-column property="create_time" label="注册日期" width="220">
        </el-table-column>
        <el-table-column property="city" label="地址" width="220">
        </el-table-column>
        <el-table-column property="admin" label="权限"> </el-table-column>
      </el-table>
      <div class="Pagination" style="text-align: left; margin-top: 10px">
       <!-- @current-change是当前页改变的事件 -->
       <!-- total是整个分页的数据条数,共count条 -->
        <!-- current-page="currentPage"代表当前选中页 -->
        <el-pagination
          @current-change="handleCurrentChange"
          :current-page="currentPage"
          :page-size="20"
          layout="total, prev, pager, next"
          :total="count"
        >
        </el-pagination>
      </div>
    </div>
  </div>
</template>

<script>
import HeadTop from "@/components/HeadTop.vue";
import { getAdminList } from "@/api/admin.js";

export default {
  components: { HeadTop },
  data() {
    return {
      tableData: [],
      offset: 0,
      limit: 20,
      count: 100,
      currentPage: 1,
    };
  },
  created() {
    this.getAdminList();
  },
  methods: {
    async getAdminList() {
      try {
        let result = await getAdminList({
          offset: this.offset,
          limit: this.limit,
        });
        this.tableData = result.data
        console.log(this.tableData);
      } catch (error) {
        console.log(error);
      }
    },
    // 当前页改变时的回调
    handleCurrentChange(val){
       //val是当前某页
      this.offset = (val - 1) * this.limit;
      //console.log(this.offset); //这个offset没有接口文档也不懂啥意思,感觉像是补偿的意思。
      // 每次切换当前页发一次请求,返回数据条数还是限制为20
      this.getAdminList();
    }
  },
};
</script>

<style lang="less" scoped>
.table_container {
  padding: 20px;
}
</style>

现在来做OrdList页面

image.png

这个页面的第一列展开可以通过设置 type="expand" 和 Scoped slot 可以开启展开行功能,可以看饿了么如下例子: image.png

el-table-column有个属性,type="expand" 设置了该列就变成了右箭头展开小按钮。

注意,这里的获取店铺详情的接口要传店铺id,它使用的是resulful接口风格,传参是如下这样的,

image.png

image.png

我们封装的接口应该是如下这样,路径+参数

export const getResturantDetail = function(restaurant_id=0,params={}){
    return request({
        method:'get',
        url:`/shopping/restaurant/`+ restaurant_id,
        params
    })
}

所以调用接口方法时候,就getResturantDetail(Resturant_id)就行了。

为了填补表单里面的数据,调用 table的expand-change的回调

 <el-table :data="tableData" @expand-change="change" style="width: 100%">

在这个回调里面调了获取餐馆详情,用户信息,地址信息的接口

 async change(row) {
      //  有某行的第一列展开时获取餐馆详情,
      let result = await getResturantDetail(row.restaurant_id);
      let userResult = await getUserInfo(row.user_id);
      let addressResult = await getAddressById(row.address_id)
      console.log(row);
      console.log(result);
      console.log(userResult);
      console.log(addressResult);
      console.log(this.tableData)
    },

image.png

现在要思考让他们更新到tableData里面,每行对应着tableData的每个对象。于是修改代码如下:

 async getOrderList() {
      // this.tableData = await getOrderList({
      //   offset: this.offset,
      //   limit: this.limit,
      // });
      // 这里不能像前面userListu组件那么简单了,否则prop那里的值不好弄
      let orders = await getOrderList({
        offset: this.offset,
        limit: this.limit,
      });
      let index = -1;
      orders.forEach((item) => {
        index++;
        const tableData = {};
        tableData.id = item.id;
        tableData.total_amount = item.total_amount;
        tableData.status = item.status_bar.title;
        tableData.restaurant_id = item.restaurant_id;
        tableData.user_id = item.user_id;
        tableData.address_id = item.address_id;
        tableData.index = index;
        this.tableData.push(tableData);
      });
    },

然后就可以点击展开行的时候,加上这么一段,修改对应tableData行的数据,让我们的表单可以显示

 this.tableData.splice(row.index, 1, {
        ...row,
        username: userResult.username, //用户名
        name: Fanresult.name, //店铺名字
        useraddress: addressResult.address, //收货地址
        address: Fanresult.address, //店铺地址
      });

我之前用的el-table的expan-change事件,el-table得加上row-key="id"属性,否则点开展开行,它是显示了然后立马又缩回去了,并且它这个事件是展开和缩回都执行一次,发了不必要的重复的请求。所以我们还得改造一下。

async change(row, status) {
      // 展开这个status是包含row的数组,缩回这个数组是空的
      if (!status.length) {
        // console.log("缩回");如果数组的长度为空,那就是缩回,下面的代码不用走了,避免重复发请求。
        return;
      }
      //  有某行的第一列展开时获取餐馆详情,
      let Fanresult = await getResturantDetail(row.restaurant_id);
      let userResult = await getUserInfo(row.user_id);
      let addressResult = await getAddressById(row.address_id);
      this.tableData.splice(row.index, 1, {
        ...row,
        username: userResult.username, //用户名
        name: Fanresult.name, //店铺名字
        useraddress: addressResult.address, //收货地址
        address: Fanresult.address, //店铺地址
      });
    },

还有一个问题,是每次展开行下面的数据总要白屏一秒才有。算了源码也没弄,懒得弄了。

再解决一个错误,点第一页,点第二页,再点第一页报错,说我的键重复了,顺便把那个index也改善了一下,就怎么点都不报错了。 image.png

 async getOrderList() {
      // this.tableData = await getOrderList({
      //   offset: this.offset,
      //   limit: this.limit,
      // });
      // 这里不能像前面userListu组件那么简单了,否则prop那里的值不好弄
      let orders = await getOrderList({
        offset: this.offset,
        limit: this.limit,
      });
      this.tableData = [] //加上这一句tabledata里面的数据就不会重复了,每次翻页里面就20条数据,就不会报重复性的错误了。
      orders.forEach((item,index) => {
        const tableData = {};
        tableData.id = item.id;
        tableData.total_amount = item.total_amount;
        tableData.status = item.status_bar.title;
        tableData.restaurant_id = item.restaurant_id;
        tableData.user_id = item.user_id;
        tableData.address_id = item.address_id;
        tableData.index = index;
        this.tableData.push(tableData);
      });
    },

以下是OrdList.vue的完整代码:

<template>
  <div>
    <head-top></head-top>
    <div class="table_container">
      <!-- row-key行数据的 Key,用来优化 Table 的渲染 -->
      <!-- expand-row-keys可以通过该属性设置 Table 目前的展开行,需要设置 row-key 属性才能使用,该属性为展开行的 keys 数组。 -->
      <el-table
        :data="tableData"
        row-key="id"
        @expand-change="change"
        style="width: 100%"
      >
        <!-- type对应列的类型。
      如果设置了 selection 则显示多选框;
      如果设置了 index 则显示该行的索引(从 1 开始计算);
      如果设置了 expand 则显示为一个可展开的按钮-->
        <el-table-column type="expand" width="48">
          <template slot-scope="props">
            <el-form  label-position="left" inline class="demo-table-expand">             
                <el-form-item label="用户名">
                  <!-- props.row里面是每行里所有单元格的内容,要取的时候就props.row.每列的prop -->
                  <span>{{ props.row.username }}</span>
                </el-form-item>
                <el-form-item label="店铺名称">
                  <!-- props.row里面是每行里所有单元格的内容,要取的时候就props.row.每列的prop -->
                  <span>{{ props.row.name }}</span>
                </el-form-item>
                <el-form-item label="收货地址">
                  <!-- props.row里面是每行里所有单元格的内容,要取的时候就props.row.每列的prop -->
                  <span>{{ props.row.useraddress }}</span>
                </el-form-item>
                <el-form-item label="店铺ID">
                  <!-- props.row里面是每行里所有单元格的内容,要取的时候就props.row.每列的prop -->
                  <span>{{ props.row.id }}</span>
                </el-form-item>
                <el-form-item label="店铺地址">
                  <!-- props.row里面是每行里所有单元格的内容,要取的时候就props.row.每列的prop -->
                  <span>{{ props.row.address }}</span>
                </el-form-item>        
            </el-form>
          </template>
        </el-table-column>
        <el-table-column prop="id" label="订单ID" width="180">
        </el-table-column>
        <el-table-column prop="total_amount" label="总价格" width="180">
        </el-table-column>
        <el-table-column prop="status" label="订单状态"> </el-table-column>
      </el-table>
    </div>
    <div class="Pagination" style="text-align: left; margin-top: 10px">
      <!-- @current-change是当前页改变的事件 -->
      <!-- total是整个分页的数据条数,共count条 -->
      <!-- current-page="currentPage"代表当前选中页 -->
      <el-pagination
        @current-change="handleCurrentChange"
        :current-page="currentPage"
        :page-size="20"
        layout="total, prev, pager, next"
        :total="count"
      >
      </el-pagination>
    </div>
  </div>
</template>

<script>
import HeadTop from "@/components/HeadTop.vue";
import {
  getOrderList,
  getOrderCount,
  getResturantDetail,
} from "@/api/order.js";
import { getUserInfo, getAddressById } from "@/api/user.js";
export default {
  created() {
    this.getInitData();
  },
  methods: {
    async getInitData() {
      let countData = await getOrderCount();
      this.count = countData.count;
      //这里获取的是订单列表总条数。
      this.getOrderList();
    },
    async getOrderList() {
      // this.tableData = await getOrderList({
      //   offset: this.offset,
      //   limit: this.limit,
      // });
      // 这里不能像前面userListu组件那么简单了,否则prop那里的值不好弄
      let orders = await getOrderList({
        offset: this.offset,
        limit: this.limit,
      });
      this.tableData = [] //加上这一句tabledata里面的数据就不会重复了,每次翻页就不会报错了。
      orders.forEach((item,index) => {
        const tableData = {};
        tableData.id = item.id;
        tableData.total_amount = item.total_amount;
        tableData.status = item.status_bar.title;
        tableData.restaurant_id = item.restaurant_id;
        tableData.user_id = item.user_id;
        tableData.address_id = item.address_id;
        tableData.index = index;
        this.tableData.push(tableData);
      });
    },
    // 当前页改变时的回调
    handleCurrentChange(val) {
      // val是当前某页
      this.offset = (val - 1) * this.limit;
      console.log(this.offset); //这个offset没有接口文档也不懂啥意思,感觉像是补偿的意思。
      // 每次切换当前页发一次请求,返回数据条数还是限制为20
      this.getOrderList();
    },
    async change(row, status) {
      // 展开这个status是包含row的数组,缩回这个数组是空的
      if (!status.length) {
        // console.log("缩回");如果数组的长度为空,那就是缩回,下面的代码不用走了,避免重复发请求。
        return;
      }
      //  有某行的第一列展开时获取餐馆详情,
      let Fanresult = await getResturantDetail(row.restaurant_id);
      let userResult = await getUserInfo(row.user_id);
      let addressResult = await getAddressById(row.address_id);
      this.tableData.splice(row.index, 1, {
        ...row,
        username: userResult.username, //用户名
        name: Fanresult.name, //店铺名字
        useraddress: addressResult.address, //收货地址
        address: Fanresult.address, //店铺地址
      });
    },
  },
  components: {
    HeadTop,
  },
  data() {
    return {
      tableData: [],
      currentPage: 1,
      count: 100,
      limit: 20,
      offset: 0,
      
    };
  },
};
</script>

<style lang="less" scoped>
.table_container {
  padding: 20px;
  .demo-table-expand {
    padding: 0px 15px;
    .el-form-item {
      margin-right: 0;
      margin-bottom: 0;
      width: 50%;
      // 这里咋都改不了form表单的label文字颜色
      label {
        .el-form-item__label {
          color: red;
        }
      }
    }
  }
}
</style>

接下来做ShopList商家列表页面

首先搭建基本的页面视图如下:把每个el-table-column的prop都配好,第一列是展开符,里面的表单也用template弄好,还有最后的也是用template,饿了么里面有例子。 image.png 代码如下:

<template>
  <div>
    <head-top></head-top>
    <div>
      <el-table :data="tableData" style="width: 100%" class="table_container">
        <el-table-column type="expand" width="40">
          <template slot-scope="props">
            <el-form label-position="left" inline class="demo-table-expand">
              <el-form-item label="店铺名称">
                <span>{{ props.row.name }}</span>
              </el-form-item>
              <el-form-item label="店铺地址">
                <span>{{ props.row.address }}</span>
              </el-form-item>
              <el-form-item label="店铺介绍">
                <span>{{ props.row.description }}</span>
              </el-form-item>
              <el-form-item label="店铺 ID">
                <span>{{ props.row.id }}</span>
              </el-form-item>
              <el-form-item label="联系电话">
                <span>{{ props.row.phone }}</span>
              </el-form-item>
              <el-form-item label="评分">
                <span>{{ props.row.rating }}</span>
              </el-form-item>
              <el-form-item label="销售量">
                <span>{{ props.row.recent_order_num }}</span>
              </el-form-item>
              <el-form-item label="分类">
                <span>{{ props.row.category }}</span>
              </el-form-item>
            </el-form>
          </template>
        </el-table-column>
        <el-table-column label="店铺名称" prop="name" width="437">
        </el-table-column>
        <el-table-column label="店铺地址" prop="address" width="437">
        </el-table-column>
        <el-table-column
          label="店铺介绍"
          prop="description"
          width="437"
        ></el-table-column>
        <el-table-column label="操作" width="200">
          <template slot-scope="scope">
            <div class="three_button">
              <el-button
                size="mini"
                @click="handleEdit(scope.$index, scope.row)"
                >编辑</el-button
              >
              <el-button
                size="mini"
                @click="handleEdit(scope.$index, scope.row)"
                >添加</el-button
              >
              <el-button
                size="mini"
                type="danger"
                @click="handleDelete(scope.$index, scope.row)"
                >删除</el-button
              >
            </div>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <div class="Pagination" style="text-align: left; margin-top: 10px">
      <!-- @current-change是当前页改变的事件 -->
      <!-- total是整个分页的数据条数,共count条 -->
      <!-- current-page="currentPage"代表当前选中页 -->
      <el-pagination
        @current-change="handleCurrentChange"
        :current-page="currentPage"
        :page-size="20"
        layout="total, prev, pager, next"
        :total="count"
      >
      </el-pagination>
    </div>
  </div>
</template>
<script>
import HeadTop from "@/components/HeadTop.vue";
export default {
  data() {
    return {
      currentPage: 1,
      count: 100,
      offset: 0,
      limit: 20,
      tableData: []
    };
  },
  components: {
    HeadTop,
  },
  methods: {
    // 当前页改变时的回调
    handleCurrentChange(val) {
      // val是当前某页
      this.offset = (val - 1) * this.limit;
      console.log(this.offset); //这个offset没有接口文档也不懂啥意思,感觉像是补偿的意思。
      // 每次切换当前页发一次请求,返回数据条数还是限制为20
    },
    handleEdit(index, row) {
      console.log(index, row);
    },
    handleDelete(index, row) {
      console.log(index, row);
    },
  },
};
</script>

要获取餐馆列表要根据cityGuess的参数来获取,首先要获取city定位,封装cityGuess接口调用方法,看线上项目,这个接口需要如下的参数,其实就是url查询参数,所以封装cityGuess方法时要有包含type:guess的params配置对象. image.png

export const cityGuess = function(params={type:"guess"}){
    return request({
        method:'get',
        url:`/v1/cities`,
        params
    })
}

获取餐馆列表接口方法也是要有params配置项,包含latitude, longitude, offset, limit这些属性,于是封装方法如下。

export const getResturantsList = function(params={}){
    return request({
        method:'get',
        url:`/shopping/restaurants`,
        params
    })
}

然后就可以调用接口获取商家列表数据了。可能因为我这个页面一创建就获取了很详细的商家列表数据,第一列展开行的name,address,dicription等属性在tabelData都有了,我连点击展开第一行的回调都不用写,数据就都过来了,此时页面代码如下: image.png

<template>
  <div>
    <head-top></head-top>
    <div>
      <el-table :data="tableData" style="width: 100%" class="table_container">
        <el-table-column type="expand" width="40">
          <template slot-scope="props">
            <el-form label-position="left" inline class="demo-table-expand">
              <el-form-item label="店铺名称">
                <span>{{ props.row.name }}</span>
              </el-form-item>
              <el-form-item label="店铺地址">
                <span>{{ props.row.address }}</span>
              </el-form-item>
              <el-form-item label="店铺介绍">
                <span>{{ props.row.description }}</span>
              </el-form-item>
              <el-form-item label="店铺 ID">
                <span>{{ props.row.id }}</span>
              </el-form-item>
              <el-form-item label="联系电话">
                <span>{{ props.row.phone }}</span>
              </el-form-item>
              <el-form-item label="评分">
                <span>{{ props.row.rating }}</span>
              </el-form-item>
              <el-form-item label="销售量">
                <span>{{ props.row.recent_order_num }}</span>
              </el-form-item>
              <el-form-item label="分类">
                <span>{{ props.row.category }}</span>
              </el-form-item>
            </el-form>
          </template>
        </el-table-column>
        <el-table-column label="店铺名称" prop="name" width="437">
        </el-table-column>
        <el-table-column label="店铺地址" prop="address" width="437">
        </el-table-column>
        <el-table-column
          label="店铺介绍"
          prop="description"
          width="437"
        ></el-table-column>
        <el-table-column label="操作" width="200">
          <template slot-scope="scope">
            <div class="three_button">
              <el-button
                size="mini"
                @click="handleEdit(scope.$index, scope.row)"
                >编辑</el-button
              >
              <el-button
                size="mini"
                @click="handleEdit(scope.$index, scope.row)"
                >添加</el-button
              >
              <el-button
                size="mini"
                type="danger"
                @click="handleDelete(scope.$index, scope.row)"
                >删除</el-button
              >
            </div>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <div class="Pagination" style="text-align: left; margin-top: 10px">
      <!-- @current-change是当前页改变的事件 -->
      <!-- total是整个分页的数据条数,共count条 -->
      <!-- current-page="currentPage"代表当前选中页 -->
      <el-pagination
        @current-change="handleCurrentChange"
        :current-page="currentPage"
        :page-size="20"
        layout="total, prev, pager, next"
        :total="count"
      >
      </el-pagination>
    </div>
  </div>
</template>

<script>
import HeadTop from "@/components/HeadTop.vue";
import { cityGuess, getResturantsList } from "@/api/resturant";
export default {
  data() {
    return {
      currentPage: 1,
      count: 100,
      offset: 0,
      limit: 20,
      tableData: [],
      latitude: 0,
      longitude: 0,
    };
  },
  components: {
    HeadTop,
  },
  created() {
    this.cityGuess();
  },
  methods: {
    async cityGuess() {
      let result = await cityGuess();
      const { latitude, longitude } = result;
      this.getResturantsList({
        latitude,
        longitude,
        limit: this.limit,
        offset: this.offset,
      });
    },
    async getResturantsList(data) {
      this.tableData = await getResturantsList(data);
      console.log(this.tableData);
    },
    // 当前页改变时的回调
    handleCurrentChange(val) {
      // val是当前某页
      this.offset = (val - 1) * this.limit;
      console.log(this.offset); //这个offset没有接口文档也不懂啥意思,感觉像是补偿的意思。
      // 每次切换当前页发一次请求,返回数据条数还是限制为20
    },
    handleEdit(index, row) {
      console.log(index, row);
    },
    handleDelete(index, row) {
      console.log(index, row);
    },
  },
};
</script>

<style lang="less" scoped>
.table_container {
  padding: 20px;
  .three_button {
    display: flex;
    button {
      padding: 5px 10px;
    }
  }
  .demo-table-expand {
    padding: 0px 15px;
    .el-form-item {
      margin-right: 0;
      margin-bottom: 0;
      width: 50%;
      // 这里咋都改不了form表单的label文字颜色
      label {
        .el-form-item__label {
          color: red;
        }
      }
    }
  }
}
</style>

然后开始处理右边的三个按钮,先弄最简单的,点击添加按钮进行路由跳转并传参,把店铺id传过去,到添加商品页。

 <el-table-column label="操作" width="200">
          <template slot-scope="scope">
            <div class="three_button">
              <el-button
                size="mini"
                @click="handleEdit(scope.$index, scope.row)"
                >编辑</el-button
              >
              <el-button
                size="mini"
                @click="addFood(scope.$index, scope.row)"
                >添加</el-button
              >
              <el-button
                size="mini"
                type="danger"
                @click="handleDelete(scope.$index, scope.row)"
                >删除</el-button
              >
            </div>
          </template>
        </el-table-column>

有点疑惑,slot-scope是什么意思

  addFood(index,row){
        console.log(index) //index就是这行索引
        console.log(row);  //row就是这行循环到的商家信息
    1        // 路由跳转并传参,把点到的这个店铺的id传过去
        this.$router.push({ path: 'addGoods', query: { restaurant_id: row.id }})
    },

效果如下: image.png

然后来做点击编辑按钮弹出的组件,直接放在shopList.vue里面,结构如下

image.png

image.png

    <!-- Form -->
    <!-- visible	是否显示 Dialog,支持 .sync 修饰符 -->
    <el-dialog title="收货地址" :visible.sync="dialogFormVisible">
      <h3 slot="title">修改店铺信息</h3>
      <el-form :model="form">
        <el-form-item label="店铺名称" label-width="100px">
          <el-input v-model="form.name" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="详细地址" label-width="100px">
          <el-input v-model="form.address" autocomplete="off"></el-input>
          <span>当前城市:{{ cityName }}</span>
        </el-form-item>
        <el-form-item label="店铺介绍" label-width="100px">
          <el-input v-model="form.description" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="联系电话" label-width="100px">
          <el-input v-model="form.phone" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="店铺分类" label-width="100px">
          <!-- value / v-model	选中项绑定值 -->
          <!-- options	可选项数据源,键名可通过 Props 属性配置 -->
          <!-- props	配置选项,具体看饿了官网 -->
          <!-- v-if="dialogFormVisible"其实本来不用加的,用来解决每次切换数据源时候报错的问题 -->
          <el-cascader v-model="selectFood" v-if="dialogFormVisible" :options="options" ></el-cascader>
        </el-form-item>
        <el-form-item label="店铺图片" label-width="100px">
          <!-- action	必选参数,上传的地址 -->
          <!-- on-success	文件上传成功时的钩子 -->
          <!-- show-file-list	是否显示已上传文件列表 -->
          <!-- before-upload	上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 reject,则停止上传。 -->
          <el-upload
            class="avatar-uploader"
            action="https://elm.cangdu.org/v1/addimg/shop"
            :show-file-list="false"
            :on-success="handleAvatarSuccess"
            :before-upload="beforeAvatarUpload"
          >
            <img v-if="selectTable.image_path" :src="showImg+selectTable.image_path" class="avatar" />
            <i v-else class="el-icon-plus avatar-uploader-icon"></i>
          </el-upload>
        </el-form-item>
      </el-form>
      <!-- slot: title	Dialog 标题区的内容 footer	Dialog 按钮操作区的内容 -->
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogFormVisible = false">取 消</el-button>
        <el-button type="primary" @click="updateShop">确 定</el-button>
      </div>
    </el-dialog>

方法和数据如下:

<script>
import HeadTop from "@/components/HeadTop.vue";
import {
  cityGuess,
  getResturantsList,
  foodCategory,
  getResturantsCount,
  updateResturant
} from "@/api/resturant";
export default {
  data() {
    return {
      currentPage: 1, // 当前页
      count: 0, //商家列表数据总条数
      offset: 0,
      limit: 20,
      tableData: [], //商家列表数据
      latitude: 0, //cityGuess获取的城市定位
      longitude: 0, //cityGuess获取的城市定位
      cityName: "", //cityGuess获取的城市名字
      dialogFormVisible: false, //点击编辑弹出的对话框显示与否
      form: {
        name: "",
        address: "",
        description: "",
        phone: "",
      }, //el-dialog里面的form的数据

      options: [], //el-cascader的数据源,里面应该是一个一个对象,对象的键大致分为value,label,children三种。
      selectFood:[], //食品分类菜单的选中项
      selectTable:{}, //后面图片上传成功后把图片路径保存进去,然后图片组件利用showImg加上上传成功后的图片路径,就能显示图片了.
      showImg:'https://elm.cangdu.org/img/' //图片显示的统一前缀
    };
  },
  components: {
    HeadTop,
  },
  created() {
    this.cityGuess();
    this.getResturantsCount();
  },
  methods: {
    // 获取城市定位的接口,要根据它返回的参数去得到商家列表
    async cityGuess() {
      let result = await cityGuess();
      const { latitude, longitude, name } = result;
      this.cityName = name;
      this.getResturantsList({
        latitude,
        longitude,
        limit: this.limit,
        offset: this.offset,
      });
    },
    // 获取商家列表的方法
    async getResturantsList(data) {
      this.tableData = await getResturantsList(data);
      console.log(this.tableData);
    },
    // 获取商家列表数量方法
    async getResturantsCount() {
      let { count } = await getResturantsCount();
      this.count = count;
    },
    //获取食品分类的方法
    async foodCategory() {
      let cateGrories = await foodCategory();
      cateGrories.forEach((item) => {
        let cateGoryItem = {};
        cateGoryItem.value = item.name;
        cateGoryItem.label = item.name;
        cateGoryItem.children = [];
        item.sub_categories.forEach((itemson) => {
          let cateGoryItemSon = {};
          cateGoryItemSon.value = itemson.name;
          cateGoryItemSon.label = itemson.name;
          cateGoryItem.children.push(cateGoryItemSon);
        });
        this.options.push(cateGoryItem);
      });
    },
    // 当前页改变时的回调
    handleCurrentChange(val) {
      // val是当前某页
      this.offset = (val - 1) * this.limit;
      console.log(this.offset); //这个offset没有接口文档也不懂啥意思,感觉像是补偿的意思。
      // 每次切换当前页发一次请求,返回数据条数还是限制为20
      this.cityGuess();
    },
    // 点击编辑按钮的回调
    handleEdit(index, row) {
      this.selectTable = row
      // index是当前点击列的下标,row是当前行的信息。
      this.dialogFormVisible = true;
      this.options = []; //每次点击编辑时把上一次push到options里面的数据清空
      this.form = row; //点击编辑按钮给el-dialog的里面的form表单的form赋值,让数据回显。
      // 调用获取食品分类的接口
      // row里面的category是食品分类菜单选中项的信息
      this.selectFood = row.category.split('/')
      if (!this.options.length) {
        // 看源码加了判断我就加了,没感觉有啥用
        this.foodCategory();
      }
    },
    addFood(index,row){
        console.log(index)
        console.log(row);
        // 路由跳转并传参,把点到的这个店铺的id传过去
        this.$router.push({ path: 'addGoods', query: { restaurant_id: row.id }})
    },
    handleDelete(index, row) {
      console.log(index, row);
    },
    // 点击对话框确定按钮的回调
     async updateShop(){
       this.dialogFormVisible = false; 
       let result = await updateResturant()
       console.log(result);
    },
    // 图片文件上传成功时的钩子
    handleAvatarSuccess(res, file) {
      if(res.status == 1){
        // 图片上传成功
          this.selectTable.image_path = res.image_path
      }else{
         this.$message.error('上传图片失败!');
      }
      this.imageUrl = URL.createObjectURL(file.raw);
    },
    // 图片文件上传前的钩子
    beforeAvatarUpload(file) {
      const isJPG = file.type === "image/jpeg";
      const isLt2M = file.size / 1024 / 1024 < 2;
      if (!isJPG) {
        this.$message.error("上传头像图片只能是 JPG 格式!");
      }
      if (!isLt2M) {
        this.$message.error("上传头像图片大小不能超过 2MB!");
      }
      return isJPG && isLt2M;
    },
  },
};
</script>

样式如下:

<style lang=less>
这个加在这里没用,加在common.css中有效
// .el-cascader-panel{
//   height: 100px;
// } 
.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>

上面把弹出框中el-form数据的回显,el-upload头像框的显示和上传,el-cascader食品分类框的数据处理,都完成了。

接下来还有未完成的部分就是弹出框下面点击确定按钮更新商家信息,这个需要用户登录才能用的功能,所以我们先去处理以下token.

做登录页

页面的结构和样式如下,

<template>
  <div class="login_page fillcontain">
    <!-- sections是h5中的新标签 -->
    <div class="login_container">
      <h3>elm后台管理系统</h3>
      <div class="form_container">
        <el-form>
          <el-form-item>
            <el-input placeholder="用户名"></el-input>
          </el-form-item>
          <el-form-item>
            <el-input type="password" placeholder="密码"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" style="width: 100%">登录</el-button>
          </el-form-item>
        </el-form>
         <div class="text">
              <p>温馨提示:</p>
              <p>未登录过的新用户,自动注册</p>
              <p>注册过的用户可凭账号密码登录</p>
            </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      show: true,
    };
  },
};
</script>

<style lang="less">
.login_page {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #324057;
  .login_container {
    h3 {
      color: #fff;
      font-size: 30px;
      font-weight: 500;
      height: 150px;
      line-height: 150px;
      text-align: center;
    }
    .form_container {
      border-radius: 5px;
      width: 380px;
      height: 260px;
      padding: 25px;
      background-color: #fff;
      .text {
        font-size: 12px;
        color: red;
        text-align: center;
        margin-top: -10px;
        p {
          height: 16px;
          line-height: 16px;
        }
      }
    }
  }
}
</style>

要使用this.$message,可以再main.js中注册:

import {Message} from 'element-ui'
// 全局注册message
Vue.prototype.$message = Message;

使用this.$message

this.$message({type:'success',message:'登录成功'})

登录页面完成代码如下:

<template>
  <div class="login_page fillcontain">
    <!-- sections是h5中的新标签 -->
    <div class="login_container">
      <h3>elm后台管理系统</h3>
      <div class="form_container">
        <!-- model	表单数据对象 -->
        <!-- rules	表单验证规则 -->
        <el-form ref="loginForm" :model="loginForm" :rules="rules">
          <!-- prop	表单域 model 字段,在使用 validate、resetFields 方法的情况下,该属性是必填的 -->
          <el-form-item prop="username">
            <el-input
              v-model="loginForm.username"
              placeholder="用户名"
            ></el-input>
          </el-form-item>
          <el-form-item prop="password">
            <el-input
              type="password"
              v-model="loginForm.password"
              placeholder="密码"
            ></el-input>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" style="width: 100%" @click="submitForm"
              >登录</el-button
            >
          </el-form-item>
        </el-form>
        <div class="text">
          <p>温馨提示:</p>
          <p>未登录过的新用户,自动注册</p>
          <p>注册过的用户可凭账号密码登录</p>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { login } from "@/api/user";

export default {
  data() {
    return {
      // 表单绑定的数据
      loginForm: {
        username: "",
        password: "",
      },
      // 表单校验规则
      rules: {
        username: [
          { required: true, message: "请输入用户名", trigger: "blur" },
        ],
        password: [{ required: true, message: "请输入密码", trigger: "blur" }],
      },
    };
  },
  methods: {
    submitForm() {
      // 点击登录按钮,先做表单验证
      this.$refs.loginForm.validate(async (isOk) => {
        if (isOk) {
          // 表单验证通过,则isOK为true,则走路由跳转和提示登录成功,并且发起login请求
          let result = await login({user_name: this.loginForm.username, password: this.loginForm.password});
          console.log(result);
          if (result.status === 1) {
            // 登录成功
            this.$router.push("/manage");
            this.$message({ type: "success", message: "登录成功" });
          } else {
            this.$message({
              type: "error",
              message: result.message,
            });
          }
        } else {
          // 表单验证不通过,提示请输入正确用户名和密码
          this.$message({
            type: "error",
            message: "请输入正确的用户名密码",
          });
          return false  //源码加的,不知道有什么用
        }
      });
    },
  },
};
</script>

<style lang="less">
.login_page {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #324057;
  .login_container {
    h3 {
      color: #fff;
      font-size: 30px;
      font-weight: 500;
      height: 150px;
      line-height: 150px;
      text-align: center;
    }
    .form_container {
      border-radius: 5px;
      width: 380px;
      height: 260px;
      padding: 25px;
      background-color: #fff;
      .text {
        font-size: 12px;
        color: red;
        text-align: center;
        margin-top: -10px;
        p {
          height: 16px;
          line-height: 16px;
        }
      }
    }
  }
}
</style>

image.png image.png image.png

这个项目有个不足之处是,我点浏览器的前进后退,可以随便切换到未登录页或者登录页。

还有一个功能没做,就是检测到您之前登录过,现在帮你自动登录,这个功能要结合vuex,将用户信息adminInfo存在vuex中,在登录页面创建好的时候,看下adminInfo是空的吗,是空的就调用,getAdminInfo的方法,然后在watch监听get过来的adminInfo的值。