第十五章 前端轮播图管理

从本章节我们将着手开发lin-cms前端部分的内容,和后端部分内容的顺序一样,我们先从轮播图管理相关的页面开始入手,纵观整个专栏项目的前端部分内容,轮播图管理也是最复杂(相对来说)的一部分内容,因为里面涉及到很多前端页面的交互操作,这章节的内容啃下来后,后面的内容就基本没什么难度了,所以这一章节的内容将会很啰嗦,也会很基础,但不乏一些知识点,这和前面后端部分内容的特点也是很类似的,所以同样推荐您耐心阅读!事不宜迟,让我们开始写代码吧!

轮播图列表页面

这里我们第一个要实现的页面就是展示所有轮播图的列表页面,运行webStorm,通过IDE打开工程目录,按照lin-cms的开发规范,我们在根目录下的src\views下新增一个目录operation,接着在这个operation目录下面再新增一个目录banner,目录创建好之后右键这个banner目录——New——选择Vue Component(Vue 组件):

组件在Vue中是一个很重要的概念。一个由Vue构建而成项目,就是由一个根组件+无数个子组件组成的,而且子组件又可以嵌套子子组件,是树状结构的。我们平时看到的由Vue构建而成的web应用,在上面点击切换不同页面,其实就是在访问根组件,只不过是切换(路由)了里面不同的子组件显示不同的内容,所以我们也称使用Vue构建的web项目为单页面应用。我们在对Vue项目进行编译打包后,只会有一个index.html,这也是单页面应用的特征,这有别于以往我们编写的web前端项目,过去的项目目录下都会存在各种xxxx.html对应不同的页面

选择后会弹出一个对话框让你输入Vue Component的名字,这里我们起个名字叫List然后回车,IDE就会帮我们创建一个后缀名为.vue的文件,并自动填充一些模板代码:

一个组件由三部分组成,template(写html代码)、script(写js代码)、style(写CSS样式)。传统的web前端应用,这三种类型的内容我们是分开文件编写的,但是在Vue里面,都是写在同一个.vue文件的不同标签位置里面。这种开发模式让每一个组件可以保持其独立性,因为组件内写的内容只会对当前组件有效(样式除外,但一般我们会对组件的样式加上作用域的限制,让其样式是对当前组件生效),组件既可以直接作为一个页面来用,也可以作为其他页面的一部分。

vue文件创建好了,但这里我们先不着急写我们的页面,我们先在这个List.vue文件里写点测试代码,让它能够在lin-cms里显示出来:

<template>
    <div>
        我是轮播图列表
    </div>
</template>

<script>
export default {
  name: 'List',
}
</script>

<style scoped>

</style>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

注意:按照开发规范,src\views目录下的.vue文件都是作为页面组件(主要用于展示其他子组件),我们统一简称为页面。

页面文件定义好之后,我们要把这个页面添加到路由表里面,这样这个页面才能被访问到,lin-cms的路由配置文件都存放在项目根目录下的src\config\stage目录中,这里我们需要新增一个配置文件operation.js,并加入如下代码:

const operationRouter = {
  route: null,
  name: null,
  title: '运营管理',
  type: 'folder', // 类型: folder, tab, view
  icon: 'iconfont icon-tushuguanli', // 菜单图标
  filePath: 'views/operation/', // 文件路径
  order: 2,
  inNav: true,
  children: [
    {
      title: '轮播图管理',
      type: 'folder',
      route: '/operation/banner',
      filePath: 'views/operation/banner/',
      inNav: true,
      icon: 'iconfont icon-tushuguanli',
      children: [
        {
          title: '轮播图列表',
          type: 'view',
          route: '/operation/banner/list',
          filePath: 'views/operation/banner/List.vue',
          inNav: true,
          icon: 'iconfont icon-tushuguanli',
        },
      ],
    },
  ],
}

export default operationRouter
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

这里我们定义了个菜单运营管理,菜单下面包含一个子菜单轮播图管理,这个子菜单下面有一个叫轮播图列表的页面,从一些配置项可以看出,我们这个页面指向了我们刚刚创建的List.vue文件了,同时给这个页面定义了一个路由地址/operation/banner/list,当我们访问域名/operation/banner/list这个url的时候就会路由到这个.List.vue的页面内容。

是不是对这一坨配置项感觉有点蒙圈,不慌,虽然lin-cms路由配置的配置参数较多,但是官方开发文档有详细的解释,这里就不重复赘述了,读者可自行查阅官方的开发文档中关于路由配置部分的内容。

路由配置文件定义好之后,我们需要让lin-cms-vue加载这个路由,在配置文件同级目录下有一个index.js文件,这里就是负责加载所有路由配置文件的,双击打开这个文件,在文件顶部声明引入我们刚刚定义好的路由配置文件:

import adminConfig from './admin'
import bookConfig from './book' // 引入图书管理路由文件
import operationConfig from './operation' // 引入运营管理路由文件
import pluginsConfig from './plugins'
import Utils from '@/lin/utils/util'


let homeRouter = [
...................................
...................................
...................................

1
2
3
4
5
6
7
8
9
10
11
12

引入了之后,我们就需要把这个配置文件加入到下面homeRouter的定义中去:

import adminConfig from './admin'
import bookConfig from './book' // 引入图书管理路由文件
import operationConfig from './operation' // 引入运营管理路由文件
import pluginsConfig from './plugins'
import Utils from '@/lin/utils/util'

let homeRouter = [
  {
    title: '林间有风',
    type: 'view',
    name: Symbol('about'),
    route: '/about',
    filePath: 'views/about/About.vue',
    inNav: true,
    icon: 'iconfont icon-iconset0103',
    order: 0,
  },
  {
    title: '日志管理',
    type: 'view',
    name: Symbol('log'),
    route: '/log',
    filePath: 'views/log/Log.vue',
    inNav: true,
    icon: 'iconfont icon-rizhiguanli',
    order: 1,
    right: ['查询所有日志'],
  },
  {
    title: '404',
    type: 'view',
    name: Symbol('404'),
    route: '/404',
    filePath: 'views/error-page/404.vue',
    inNav: false,
    icon: 'iconfont icon-rizhiguanli',
  },
  bookConfig,
  adminConfig,
  // 运营管理的路由配置文件
  operationConfig,
]

...................................
...................................
...................................

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

从上面的代码可以看出,其实我们直接把operationConfig.js里的内容直接写在这个homeRouter数组里面也是可以的,但是随着你页面越来越多,这里的路由配置信息就会很长,后面维护和管理都会是灾难,所以这里我们选择为每个页面模块建立一个配置文件,然后在index.js这里导入即可。

这个和Vue 的组件化开发思想是一致的。把原本一个很复杂的页面组件内容拆分成由多个组件组成,方便维护和扩展、复用。

如果一切顺利,这时候如果我们运行lin-cms-vue,就可以在左边菜单看到一个新的菜单项运营管理了,在IDE中召唤出命令行工具,输入npm run serve,在开发环境启动完成之后,访问lin-cms-vue的地址并登陆:

开发过程中请保持lin-cms-tp5和lin-cms-vue内置web服务器的开启和MySQL数据库能正常访问,后面章节的内容不会再重复提醒。

这里可以看到左边的菜单多出来了一个菜单项,同时这个菜单项的目录结构也与我们在operation.js中定义的内容是一样的,接着我们点击一下这个轮播图列表的菜单项,点击之后可以看到当前页面的url发生了变化:

http://localhost:8080/#/operation/banner/list

同时lin-cms打开了一个新的标签页,标签页中显示了我们在List.vue文件中写的测试内容。

这里说明我们的页面已经是被正确加载并显示了,搞清楚怎么让一个新建的页面跑起来这个过程很重要,特别是第一次接触一个陌生的框架的时候,后面的章节内容中我们就不会再重复这里的过程了。

这里读者可能会问,为什么作者知道要这么写,答案就是看lin-cms-vue本身的示例代码,复制粘贴加阅读文档,当然需要再加一点点点vue使用经验。

测试页面跑起来之后,我们就要来正儿八经的写我们的页面了,首先我们要先想想这页面要写成什么样子,一般页面都会有专门的设计师或者UI给你画好设计图,不过这里我们自然是没有的,我们后续的所有页面就都参考线上demo的样式来编写,当然如果你有其他的想法也可以随意编写。

从线上demo的轮播图列表页面可以观察到,我们这个展示轮播图列表的页面主要分成三个区域,标题、按钮区、表单区域,确定好这一点就后,我们先把这个骨架搭建一下,回到我们banner\List.vue文件中,添加以下内容:

<template>
    <div>
        <div>轮播图列表</div>
        <div>
            <el-button>新增</el-button>
        </div>
        <div>
            <el-table></el-table>
        </div>
    </div>
</template>

<script>
</script>

<style scoped>
</style>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

保存后,会看到切换窗口到浏览器页面,会发现页面刷新了,然后会看到我们刚刚已经编写好的元素:

由于还没有编写样式,所以比较丑,接下来我们处理下样式问题:

<template>
    <div class="lin-container">
        <div class="lin-title">轮播图列表</div>
        <div class="button-container">
            <!-- 指定button类型 -->
            <el-button type="primary">新增</el-button>
        </div>
        <div class="table-container">
            <el-table></el-table>
        </div>
    </div>
</template>

<script>
export default {
  name: 'List',
}
</script>

<style lang="scss" scoped>
    .button-container{
        margin-top: 30px;
        padding-left: 30px;
    }

    .table-container{
        margin-top: 30px;
        padding-left: 30px;
        padding-right: 30px;
    }
</style>

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

这里我们给几个div标签加了class属性并指定了样式名,其中lin-containerlin-title这两个样式是属于lin-cms-vue内置的公共样式,从样式类名可以看出就是用来给页面容器和标题添加样式的。

这里为什么我知道有这两个样式呢?答案就是通过查看lin-cms-vue的示例页面,觉得这个样式好看,然后去看对应页面的源码。

button-containertable-container是我们自定义的样式类名,主要就是调整一些边距和内边距,保存一下,回到浏览器中,刷新一下页面:

lin-cms-vue提供了很多示例代码和页面,位于左侧菜单的自定义组件UI这两个菜单下,推荐读者了解一下。一方面我们可以知道lin-cms-vue封装了什么好用的组件,另一方面可以参考一些布局样式。

现在看起来就舒服多了,但是我们可以发现下面这个表单里面并没有数据,接下来我们就要来实现从前端发起HTTP请求从后端接口获取数据,根据返回的数据来让这个表单显示数据。在src\models目录下,我们新增一个banner.js文件:

models,很熟悉的文件夹名,是的,和之前我们在开发后端部分的内容一样,我们也会给前端的工程做软件架构分层,目的也是一样的,就是为了实现解耦。我们可以选择直接在List.vue里发起一个HTTP请求,但是那样代码就太臃肿了,特别是当你页面需要发起很多个不同的请求的时候。我们划分出一个models层,这个层里面的js文件就对应不同功能模块的模型类,每个模型类负责处理具体业务逻辑。List.vue是一个页面文件,它只需要从模型层里拿数据即可,至于数据怎么来了它不关心。这么做有利于模型类的复用(当其他页面也需要请求同一个接口),同时能保持展示页面代码的简洁(比如现在我们在编写的List.vue),对后期维护或者扩展也很有帮助。

在模型层下的banner.js中,我们定义了一个模型类Banner,我们在模型类中新增一个方法getBanners():

import {
  get,
} from '@/lin/plugins/axios'

class Banner {
  async getBanners() {
    const res = await get('v1/banner')
    return res
  }
}

export default new Banner()

1
2
3
4
5
6
7
8
9
10
11
12
13

这里我们首先从@/lin/plugins/axios引入了一个get()函数,这个文件位于项目根目录下的src\lin\plugins\axios.js(这里的@等同于到src这一层级的路径地址),axios.js里的内容其实就是对著名HTTP请求库axios的封装,通过对其封装实现了全局统一的异常处理和附加请求参数等机制,以往这些内容都需要我们自己来封装,但使用lin-cms-vue的话不需要,已经封装好了。同时为了简化调用代码,提供了诸如get()post()put()_delete()函数对应不同的HTTP请求类型。这里我们利用了get()函数来实现发起一个GET请求,该函数接收一个参数,参数值就是接口地址,前面我们在第三章《LinCMS全家桶》章节的时候已经配置过了根目录下的.env.development文件:

ENV = 'development'

VUE_APP_BASE_URL = 'http://localhost:8000/'
1
2
3

我们在利用lin-cms-vue封装好的请求类库里的请求函数时,只需要传递接口地址中除去域名部分以外的内容,axios.js的内部实现会在发起请求时根据这个VUE_APP_BASE_URL环境变量里的值去拼接完整路径,这么做的好处很简单,如果有一天我们改变了接口地址,只需要修改这里即可。

.env.development是针对开发环境的配置文件,即运行npm run serve后启动了内置web服务器的情况下才会应用配置。目录下还有一个.env.production,从文件名可以看出,这个是针对生成环境的,即最终我们开发完毕了准备部署了,运行npm run build后才会应用的配置。

这里我们还使用了ES6标准提供的新特性async/await。在函数名前面加上async表示声明这个函数里面有异步操作,在函数内的某一行代码语句前加上await表示要等待这条语句执行完才继续执行后面的代码。

async/await的作用就是使得异步操作变得更加方便,它是为了解决异步编程开发体验问题的解决方案,让你像写同步代码一样写异步代码。具体的差异读者可自行百度关于JavaScript异步编程的资料,看看使用旧语法的情况下写异步代码时多么麻烦和臃肿就明白了。

模型方法定义好之后,让我们回到页面中调用一下,打开List.vue,添加以下内容:

<!-- src/views/operation/banner/List.vue -->
<template>
    ..................
    ..................
    ..................
</template>

<script>
// 可以简写成 
//import banner from '@/models/banner'
import banner from '../../../models/banner' 

export default {
  name: 'List',
  // 组件的数据对象
  data() {
    return {
      // 定义一个数据对象
      bannerList: [],
    }
  },
  // 生命周期钩子,在该组件被创建后触发,执行该函数内的逻辑
  created() {
    // 当组件被创建时,调用组件内的getBanners()方法
    this.getBanners()
  },
  // 组件的选项之一,用于定义该组件所拥有的方法
  methods: {
    // 获取所有轮播图数据
    async getBanners() {
      // 调用banner模型类下的方法,并将结果赋值给组件的数据对象实现数据绑定并响应渲染到页面上
      this.bannerList = await banner.getBanners()
    },
  },
}
</script>

<!-- lang用于声明使用什么预编译技术来书写css,这里指定为scss,具体好处读者自行百度 -->
<!-- 写上scoped属性,代表这个style标签里的内容只对当前组件生效 -->
<style lang="scss" scoped>
   ..................
   ..................
   ..................
</style>

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

在script标签内部,我们在data(){}中申明一个组件数据对象bannerList,默认值是一个空数组,只有在这个data()里声明的数据对象,才可以动态绑定到template标签内的页面元素里。接着我们在Vue组件的生命周期钩子created()里调用一个我们定义的组件方法getBanners(),组件方法内部的实现就是调用我们的banner模型类里的方法获取轮播图数据,然后赋值给我们的bannerList数据对象。

每一个Vue组件,从创建到销毁,会经历多个不同的阶段,这叫组件的生命周期,就像人一样,从出生,幼年,青年,中年,老年,死亡一样,这是人的生命周期;Vue框架给组件生命周期里的这几个阶段定义了不同的“钩子函数”,组件到了什么阶段,就触发对应的钩子函数,方便开发者在不同阶段实现一些特别的行为,比如这里,我们在组件被创建的阶段(从生命周期上来说,这个组件还没被渲染到页面中)就调用一个方法发起请求并把结果绑定到组件的数据对象中,这样给用户使用的感觉就是一打开页面数据就加载好了。

获取数据的逻辑我们编写好了,接下来就是要把这个bannerList的内容绑定到页面上去然后渲染出来,前面我们已经把el-table标签放置在了页面中,但是我们并没有给这个组件传递数据,所以目前我们的页面虽然显示了一个表格,但是没有数据,接下来我们就要让这个标签能够接收并渲染bannerList里的内容:

<!-- src/views/operation/banner/List.vue -->
<template>
    <div class="lin-container">
        <div class="lin-title">轮播图列表</div>
        <div class="button-container">
            <el-button type="primary">新增</el-button>
        </div>
        <div class="table-container">
            <!-- <el-table v-bind:data="bannerList"> -->
            <!-- v-bind:data可以简写成: -->
            <el-table :data="bannerList">
                <!-- label定义列头显示的文本,prop定义要渲染data数组中元素的哪个字段,width定义列宽 -->
                <el-table-column label="id" prop="id" width="120"></el-table-column>
                <el-table-column label="轮播图名称" prop="name"></el-table-column>
                <el-table-column label="轮播图简介" prop="description"></el-table-column>
            </el-table>
        </div>
    </div>    
</template>

....................................
....................................

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

通过给el-table标签的data属性传递我们的数据对象bannerList,传递的方法就是通过数据绑定语法v-bind:属性名=值,告诉这个标签里面要渲染什么数据,接着在el-table标签内部嵌套一个el-table-column标签定义表格每一列要渲染的内容。

lin-cms-vue集成了知名的ui库element ui,是的,就是那个日夜陪伴你的饿了么APP团队开源的ui库。el-table也是其中一个组件,所以这里如果你要问我为什么知道这个组件是这么用的,答案就是看文档,请读者自行阅读和收藏Element UI 文档,这对于你看懂本专栏的示例代码很重要,因为每个组件都有很多属性和用法,作者无法事无巨细的逐一解释。另外养成自己阅读文档的习惯也有利于你后面独立开发,因为你时不时就会需要去文档里看看有啥组件能满足你的需求。

定义完之后,让我们回到浏览器中,刷新一下页面:

看到没有,看到没有,我们的轮播图数据都展示在了页面上了!!不过这里还有些需要完善的地方,一般在列表页面,我需要给表格中的每一行提供一些操作按钮,比如说编辑、删除,要实现按钮的方式也很简单,就是给表格加一列:

<!-- src/views/operation/banner/List.vue -->
<template>
      ...............................
      ...............................
<!-- <el-table v-bind:data="bannerList"> -->
<!-- v-bind:data可以简写成: -->
     <el-table :data="bannerList">
                <!-- label定义列头显示的文本,prop定义要渲染data数组中元素的哪个字段,width定义列宽 -->
                <el-table-column label="id" prop="id" width="120"></el-table-column>
                <el-table-column label="轮播图名称" prop="name"></el-table-column>
                <el-table-column label="轮播图简介" prop="description"></el-table-column>
                <el-table-column label="操作" fixed="right" width="170">
                    <!-- <el-table-column>标签支持在标签内嵌套一个<template>标签实现复杂的页面元素 -->
                    <template slot-scope="scope">
                        <el-button plain size="mini" type="primary" @click="handleEdit(scope.row)">编辑</el-button>
                        <el-button plain size="mini" type="danger" @click="handleDel(scope.row.id)" v-auth="'删除轮播图'">删除</el-button>
                    </template>
                </el-table-column>
            </el-table>
      ...............................
      ...............................        
</template>

....................................
....................................

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

<template>标签的使用就是Vue里关于插槽的内容,插槽是什么读者可以通过文档轻松理解,但是这里使用的是插槽的另一种高级用法——作用域插槽(slot-scope="scope")。要理解什么作用域插槽之前,我们先明确一个概念。首先,我们的List.vue本身也是属于一个组件,我们使用的el-table、el-button标签其实也是一个组件,el-table、el-button标签是引入了element ui库之后提供给我们使用的。概念知道了,接下来看看我们之前都做了什么,我们把List.vue中定义的数据对象bannerList传递给了el-table实现表格渲染,数据是传进去了,表格也出来了,但是问题也来了,我们使用表格一般都会伴随着对某一行或者列的操作,这些操作其实都是在操作el-table组件。前面我们说了,组件的特性就是独立性,本身内部的数据是互相隔离的,那么这里我们放置的el-button组件需要监听点击事情来确定操作了哪一行,这个答案只有el-table知道,但是因为组件内部数据是隔离的,el-button是不能直接访问el-table里的数据的,而解决办法就是作用域插槽。作用域插槽在这里的作用就是把当前行的数据暴露出来给外部,就是<template slot-scope="scope">"这一段,通过声明暴露一个scope属性,后面el-button就可以用scope.row来访问到当前行的数据,也就是bannerList数组里的某个元素。关于作用域插槽的解释,官方文档也是解释得比较简陋,这里权当补充,如果读者还是无法理解,不要紧,用多几次,你就明白了,我也是这么过来的。

这里我们新增了一个el-table-column标签,就是增加一列,然后在标签里面嵌套了一个template标签,标签里面又放置了两个el-button标签,就是按钮,这里我们给这两个按钮添加了点击事件的监听,利用vue提供的@click=回调方法这种语法。当点击按钮的时候就会触发相应的回调方法,回调方法里面实现具体处理逻辑。方法需要我们在script标签里的methods选项中定义,定义和具体的实现我们先不着急写,这里先介绍一下删除按钮上面的v-auth,这个是lin-cms-vue封装的自定义指令,通过这个指令,我们可以实现具体到页面中某个元素的权限控制,还记得之前我们在编写后端删除轮播图接口时,我们给接口增加了权限控制:

这里我们给删除轮播图这个接口定义的权限名叫删除轮播图,我们把这个权限名写到v-auth指令中,这样如果当前登录cms的账户没有拥有一个名叫“删除轮播图”的权限,那么这个按钮就会被隐藏。

lin-cms-vue提供两种方式的权限控制,一种是基于路由配置,后面我们使用到的时候再介绍;另一种就是具体到某个页面元素,在元素标签上使用v-auth指令。后面我们将根据我们之前在编写后端接口权限时的定义,在路由配置中或者页面元素中配置相关的权限控制参数来达到隐藏菜单和按钮。

添加完按钮后,让我们再一次回到浏览器中,刷新一下:

可以看到这里多出了一列,同时每一行都有两个按钮,整个页面现在有模有样了,但是还少了点东西,之前我们在编写查询所有轮播图接口的时候特别让这个接口同时也返回每个轮播图下所展示的轮播图元素,那么具体到页面展示上要怎么体现呢?答案是通过el-table组件提供的展开行特性,要开启这个特性同样很简单,同样是el-table标签内增加一段代码:

<!-- src/views/operation/banner/List.vue -->
<template>
      ...............................
      ...............................
<!-- <el-table v-bind:data="bannerList"> -->
<!-- v-bind:data可以简写成: -->
     <el-table :data="bannerList">
          <el-table-column type="expand">
              <template slot-scope="scope">
                  <div class="expand-container">
                      <div v-for="(img,index) in scope.row.items" :key="index">
                          <img class="img" :src="img.img.url">
                      </div>
                  </div>
              </template>
          </el-table-column>
          <!-- label定义列头显示的文本,prop定义要渲染data数组中元素的哪个字段,width定义列宽 -->
          <el-table-column label="id" prop="id" width="120"></el-table-column>
          .........................................................
          .........................................................
    </el-table>
      ...............................
      ...............................        
</template>

....................................
....................................

<style lang="scss" scoped>
    .button-container{
        margin-top: 30px;
        padding-left: 30px;
    }

    .table-container{
        margin-top: 30px;
        padding-left: 30px;
        padding-right: 30px;

        .expand-container {
            display: flex;
            justify-content: flex-start;
            align-items: center;

            .img {
                margin: 10px;
                width: 200px;
            }
        }
    }
</style>

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

这里我们在原来id列增加了新的一个列,不同的是在el-table-column标签中声明了一个type="expand",增了这个属性声明之后,那么在表格的每行的最前面就会多出一行可以展开收缩的按钮,在这个el-table-column里面我们同样嵌套了一个template,标签的内容就是使用v-for循环渲染(一个轮播图下面可能会1个或者多个轮播图元素)当前行的轮播图下所拥有的轮播图元素。定义完之后,让我们回到浏览器中,刷新一下:

这里可以看到在id列前面多了个蓝色的小箭头,点击后当前行下面会展开出一个新的区域,里面显示的就是当前行的轮播图所拥有的轮播图元素,通过这个小功能可以实现轮播图元素的快速预览。

你可能会发现展开后图片都是显示不出来的,提示图片地址404,这是因为前面我们在测试新增轮播图元素的时候插入的是测试数据不一定是真实存在的。这里可以暂时不必理会,后面我们实现了新增、编辑等功能后就可以修正这些测试数据。

到这里,我们的轮播图列表页面就算是完成得差不多了,当然本章节我们只是完成了数据展示的内容,页面上的新增编辑删除按钮目前我们点击是没有什么反应的,但不要紧,这些内容都将在后续的章节中逐一实现,本小节只是一个热身,实现也很简单,但是后面就会涉及到一些组件间的路由呀,数据传递的知识,那些才会相对复杂些。做好准备!接下来就让我们愉快的进入下一章的学习吧!

删除轮播图

本小节我们来实现一下轮播图列表页面中删除表格中的某一条轮播图记录的功能,在前面小节中,我们给删除按钮绑定了一个点击事件的回调方法:

<!-- src/views/operation/banner/List.vue -->
<template>
  <div class="lin-container">
    ...............................
    ...............................
    <div class="table-container">
      <el-table :data="bannerList">
            .........................................................
            .........................................................
            <el-table-column label="操作" fixed="right" width="170">
                <template slot-scope="scope">
                  <el-button plain size="mini" type="primary" @click="handleEdit(scope.row)">编辑</el-button>
                  <el-button plain size="mini" type="danger" @click="handleDel(scope.row.id)" v-auth="'删除轮播图'">删除</el-button>
                </template>
            </el-table-column>
      </el-table>
    </div>
  </div>    
</template>
....................................
....................................

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

当点击删除按钮的时候,就会触发handleDel()方法,同时会给这个方法传递当前行数据中的id字段的值(scope.row.id),目前我们还没有定义这个方法,接下来就需要来定义一下这个方法:

<!-- src/views/operation/banner/List.vue -->
<template>...</template>

<script>
// 可以简写成 
//import banner from '@/models/banner'
import banner from '../../../models/banner' 

export default {
  name: 'List',
  // 组件的数据对象
  data() {...},
  // 生命周期钩子,在该组件被创建后触发,执行该函数内的逻辑
  created() {...},
  // 组件的选项之一,用于定义该组件所拥有的方法
  methods: {
    // 获取所有轮播图数据
    async getBanners() {
      // 调用banner模型类下的方法,并将结果赋值给组件的数据对象实现数据绑定并响应渲染到页面上
      this.bannerList = await banner.getBanners()
    },
    // 删除按钮的点击事件
    async handleDel(id) {
      console.log(`点击了删除id为${id}的轮播图`)
    },
  },
}
</script>
............................................
............................................

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

我们在methods选项中新增了一个handleDel(id)方法,为了验证一下这个点击事件真的能进到这个方法并获取到轮播图id,我们在方法体里面打印一点调试信息,然后回到浏览器中,刷新一下页面,按下F12打开控制台,点击一下某条轮播图记录的删除按钮:

这里可以看到随着我们的不停的点击删除按钮,控制台也相应打印出了一些信息,说明我们的按钮点击事件回调的方法是可以正常运行的。

这是一个很常用而且有效的调试手段,对于排查各种“为什么点击了没反应”问题很有帮助。

回调方法没问题,那么就要开始正儿八经写代码了,在这个回调方法中,我们同样需要去调用我们前面编写好的后端接口实现删除这个轮播图记录。打开我们的banner模型,新增一个delBannerByIds()方法:

// src/models/banner.js

import {
  get,
  _delete, // 引入封装好的delete方法,保留字冲突,所以前面加了个_
} from '@/lin/plugins/axios'

class Banner {

  // 是否自行处理接口异常
  handleError = true


  async getBanners() {
    const res = await get('v1/banner')
    return res
  }

  async delBannerByIds(ids) {
    // { ids } 等价于 { ids : ids },对象的key和value命名相同时的一种简写
    const res = await _delete('v1/banner', { ids }, { handleError: this.handleError })
    return res
  }

}

export default new Banner()

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

_delete方法的第三个参数接受一个params对象,当传入handleError配置项,且值为true时,如果这个请求后端发生了异常,lin-cms-vue会将这个异常信息抛出,由用户自行try/catch来捕获并处理异常

这里我们给Banner模型新增了一个模型方法delBannerByIds(),方法接收一个ids参数,是一个数组,包含了待删除的id,接着调用lin-cms封装好的_delete()方法实现发起一个DELETE类型的HTTP请求。模型方法定义好之后,我们还需要对这个_delete()方法的源码做一些小改动,打开src\lin\plugins\axios.js文件,在文件的最下方,是_delete()方法的源码,按如下内容修改:

// src\lin\plugins\axios.js

// 源码
// export function _delete(url, params = {}) {
//   return _axios({
//     method: 'delete',
//     url,
//     params,
//   })
// }

// 增加一个data参数
export function _delete(url, data = {}, params = {}) {
  return _axios({
    method: 'delete',
    url,
    params,
    data,
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

修改完毕之后,让我们回归正题,到页面中来调用一下刚刚定义的模型方法:

<!-- src/views/operation/banner/List.vue -->
<template>...</template>

<script>
// 可以简写成 
//import banner from '@/models/banner'
import banner from '../../../models/banner' 

export default {
  name: 'List',
  // 组件的数据对象
  data() {...},
  // 生命周期钩子,在该组件被创建后触发,执行该函数内的逻辑
  created() {...},
  // 组件的选项之一,用于定义该组件所拥有的方法
  methods: {
    // 获取所有轮播图数据
    async getBanners() {
      // 调用banner模型类下的方法,并将结果赋值给组件的数据对象实现数据绑定并响应渲染到页面上
      this.bannerList = await banner.getBanners()
    },
    // 删除按钮的点击事件
    async handleDel(id) {
      // delBannerByIds接收一个数组参数,这里需要用[]来包裹一下
      await banner.delBannerByIds([id])
    },
  },
}
</script>
............................................
............................................

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

然后我们到浏览器中来测试一下,刷新一下页面,然后点击删除原来表格中id为10的轮播图:

点击删除按钮之后,刷新一下页面:

这里我们发现表格中id为10的轮播图记录已经消失了,这说明我们已经删除成功了,但是这里整个交互体验并不友好,第一是点击删除后没有一个提示,如果我们只是不小心点击到了删除按钮,那么就没救了;第二,我们点击删除之后还需要手动刷新页面才能知道操作后的结果,这简直弱爆了。当然这里两个问题也很好解决,首先是第一个问题的解决方案,我们在每次点击删除按钮之后,给一个提示框,让用户再次确认,根据操作结果来决定是否发起一个删除轮播图的请求,这里我们需要引入element ui 的Dialog组件,同时调整handleDel()的实现逻辑:

<!-- src/views/operation/banner/List.vue -->
<template>
  <div class="lin-container">
    ...............................
    ...............................
    <div class="table-container">
      <el-table :data="bannerList">
            .........................................................
            .........................................................
            <el-table-column label="操作" fixed="right" width="170">
                <template slot-scope="scope">
                  <el-button plain size="mini" type="primary" @click="handleEdit(scope.row)">编辑</el-button>
                  <el-button plain size="mini" type="danger" @click="handleDel(scope.row.id)" v-auth="'删除轮播图'">删除</el-button>
                </template>
            </el-table-column>
      </el-table>
    </div>
    <el-dialog
        title="提示"
        :visible.sync="showDialog"
        width="30%"
        center>
        <span>确定删除id为{{id}}的轮播图?</span>
        <span slot="footer" class="dialog-footer">
          <el-button @click="showDialog = false">取 消</el-button>
          <el-button type="primary" @click="deleteBanner">确 定</el-button>
        </span>
     </el-dialog>
  </div>        
</template>

<script>
// 可以简写成 
//import banner from '@/models/banner'
import banner from '../../../models/banner' 

export default {
  name: 'List',
  // 组件的数据对象
  data() {
    return {
      bannerList: [],
      // 控制对话框显示/隐藏,默认不显示
      showDialog: false,
      // 轮播图id
      id: null,
    }
  },
  // 生命周期钩子,在该组件被创建后触发,执行该函数内的逻辑
  created() {...},
  // 组件的选项之一,用于定义该组件所拥有的方法
  methods: {
    // 获取所有轮播图数据
    async getBanners() {
      // 调用banner模型类下的方法,并将结果赋值给组件的数据对象实现数据绑定并响应渲染到页面上
      this.bannerList = await banner.getBanners()
    },

    // 删除按钮的点击事件
    handleDel(id) {      
      // 数据绑定,用于显示对话框内容
      this.id = id
      // 数据绑定,显示对话框
      this.showDialog = true
    },

    // 执行删除轮播图请求
    async deleteBanner() {
      // 关闭对话框
      this.showDialog = false
      await banner.delBannerByIds([this.id])
    },
  },
}
</script>

....................................
....................................

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

这里我们在页面中引入了element ui 的Dialog组件,原来我们是在删除按钮的点击事件中调用Banner模型的方法实现删除轮播图,现在不是了,我们把真正模型方法的调用,延迟到了对话框中的“确定”按钮的点击事件回调方法deleteBanner()中,原来的删除按钮点击事件只负责做数据绑定工作,控制对话框组件的打开和内容渲染。调整完毕之后,让我们到浏览器中刷新一下页面看看效果:

可以看到当我们点击删除按钮的时候,会弹出一个对话框提示我们是否要删除,点击取消的话,对话框消失,刷新一下页面,表格的数据也不会有变化,点击确认之后,会实现和之前一样的效果,表格的某一条记录会被删除,这样我们的第一个问题就算是解决了。而第二个问题的解决方法,同样是简单的添加一些代码即可实现:

<!-- src/views/operation/banner/List.vue -->
<template>
  <div class="lin-container">
    ...............................
    ...............................
    <div class="table-container">
      <el-table v-loading="loading" :data="bannerList">
            .........................................................
            .........................................................
      </el-table>
    </div>
    <!-- 对话框 -->
    <el-dialog>...</el-dialog>
  </div>        
</template>

<script>
// 可以简写成 
//import banner from '@/models/banner'
import banner from '../../../models/banner' 

export default {
  name: 'List',
  // 组件的数据对象
  data() {
    return {
      bannerList: [],
      // 控制对话框显示/隐藏,默认不显示
      showDialog: false,
      // 轮播图id
      id: null,
      // 显示加载状态
      loading: false,
    }
  },
  // 生命周期钩子,在该组件被创建后触发,执行该函数内的逻辑
  created() {...},
  // 组件的选项之一,用于定义该组件所拥有的方法
  methods: {
    // 获取所有轮播图数据
    async getBanners() {
      // 调用banner模型类下的方法,并将结果赋值给组件的数据对象实现数据绑定并响应渲染到页面上
      this.bannerList = await banner.getBanners()
    },

    // 删除按钮的点击事件
    handleDel(id) {      
      // 数据绑定,用于显示对话框内容
      this.id = id
      // 数据绑定,显示对话框
      this.showDialog = true
    },

    // 执行删除轮播图请求
    async deleteBanner() {
      // 关闭对话框
      this.showDialog = false
      // 显示加载状态
      this.loading = true
      // 调用模型方法删除轮播图
      await banner.delBannerByIds([this.id])
      // 再次调用获取所有轮播图的方法
      this.getBanners()
      // 关闭加载状态
      this.loading = false
    },
  },
}
</script>

....................................
....................................

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

这里我们给el-table标签添加了一个element ui封装的自定义指令v-loading,当这个自定义指令的值为true的时候,那么el-table标签的覆盖范围就会呈现一个加载中的状态,这里我们定义了一个loading对象来动态控制加载状态的显示和隐藏,默认是不显示,控制的地方就是在deleteBanner()方法中,每当确认删除轮播图的时候,对话框关闭之后,表格开始显示加载状态,接着调用模型方法,然后再次调用我们前一节编写的获取所有轮播图的组件方法,这一步就是实现了重新渲染表格数据,最后关闭加载状态。通过这个改动,我们从点击了“删除”按钮开始,整个过程就显得有反馈和人性化,是不是感觉流畅和舒服多了?

记得七月老师曾经说过,前端项目有时候就是在写交互体验。个人感觉也是如此,如果一个前端项目不考虑太多交互体验的问题,那确实没什么难度,当真要充分考虑用户体验的时候,第三方的组件库可以有效提高我们的开发效率和降低开发难度。当然,个别追求极致的可能还需要对组件库进行二次开发。

最后,我们还需要对一些极端情况做处理。当我们确定删除一个轮播图的时候,表格会进入加载状态,但如果banner.delBannerByIds([this.id])这个模型方法执行发生了异常(可能是模型方法内部问题或者后端接口问题),那么表格的加载状态就不会取消,因为这时候loading的值还是true,你就不得不强制刷新浏览器页面来消除这个加载状态,这同样是一个糟糕的体验。所以这里我们需要用一个try/catch包裹一下捕获这些异常:

// 执行删除轮播图请求
    async deleteBanner() {
      // 关闭对话框
      this.showDialog = false
      // 显示加载状态
      this.loading = true
      try {
        // 调用模型方法删除轮播图
        const res = await banner.delBannerByIds([this.id])
        // 再次调用获取所有轮播图的方法
        this.getBanners()
        // 关闭加载状态
        this.loading = false
        // 消息提示
        this.$message({
          message: res.msg,
          type: 'success',
        })
      }catch(e) {
        this.loading = false
        this.$message({
          message: e.data.msg,
          type: 'error',
        })
      }
    },
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

这里我们在删除成功后利用element ui的消息提示给用户一个删除成功的提示,进一步提升用户体验。而当发生程序异常时,我们同样执行关闭加载状态的处理,同时给出一个错误类型的消息提示,提示内容是我们前面在实现后端接口的时候定义的异常格式中的msg字段内容。