# 极客园PC端

# 第一章:项目起步

# 01-项目介绍

目标:了解项目背景,了解项目功能。

项目背景:

  • 它对标 CSDN 博客园 等竞品,致力成为全球知名的IT技术交流平台,它包含 技术文章,问答内容,视频解答 的专业IT资讯平台,它提供原创,优质,完整内容的专业IT社区。它是 极客园 IT资讯社区。

项目功能:

  • 极客园-个人端PC 是PC桌面web个人自媒体管理端
  • 主要功能有:
    • 登录
    • 首页
    • 内容管理(筛选,删除)
    • 发布文章(包含编辑文章)

项目物料:http://geek.itheima.net

总结: 我们知道项目的大致功能即可。

# 02-使用技术

目标:了解会使用到的技术

开发依赖大致如下:

  • 基础环境:nodejss12+ vscode vuecli4.x
  • 配套工具:eslint babel less
  • 使用技术:vue2.6.12 element-ui vue-router

项目中的解决方案:

  • 使用vue-cli创建vue单页应用解决方案
  • 使用vue-router实现前端路由解决方案
  • 使用element-ui快速搭建PC界面解决方案

总结: 我们大概知道用了那些东西即可。

# 03-创建项目

目标:知道如何使用vue-cli创建项目

大致步骤:

  • 在某个目录打开命令行工具输入创建项目的命令
  • 安装项目需求选择具体的工具,然后等待创建吧
  • 最后进入创建好的项目,启动项目即可

具体如下:

  • 执行创建命令
vue create geek-client-pc
  • 选中自定义创建

1622440144872

  • 选择Vue版本,依赖Babel降级ES6语法,使用css预处理器,使用代码风格校验。

1625019450919

  • 选择vue2.0版本

1622440660281

  • 是否使用历史模式API,输入 n

1622440752792

  • 选择less这种css预处理器

1622440811661

  • 选择 通用语法风格配置

1622440877508

  • 语法风格校验的时机,保存代码校验,提交代码校验且自动修复。

1622441060425

  • 选择使用不同的配置文件对于所依赖工具

1622441142876

  • 是否记录此次操作记录,输入 n

1622441203780

  • 最后等待安装即可,安装完毕进入项目目录,执行 npm run serve 即可启动项目。

1625019627355

总结: 我们可以使用vuecli根据自己项目需求创建合适的项目。

# 04-调整目录

目标:根据项目功能调整下目录结构

大致步骤:

  • 配置文件解释说明
  • 调整src下目录结构

落地内容:

  • 根目录和配置文件。都是自动生成的,了解作用即可
├─node_modules
├─public
├─src
├─.browserslistrc     # 适配浏览器列表
├─.editorconfig       # 提供给编辑器的配置
├─.eslintrc.js        # eslint代码风格配置
├─.gitignore          # git忽略文件配置
├─.babel.config.js    # babelES降级配置
├─package-lock.json   # 包下载版本说明文件
├─package.json        # 项目包说明文件
├─README.md           # 说明MD文件
  • src 目录结构如下,仅供参考 (分模块的思维才重要)
├─assets           # 项目资源
│  ├─images          # 图片 
│  └─styles          # less代码
├─components       # 全局组件,通用组件
├─router           # 路由
├─utils            # 工具
└─views            # 路由组件(页面)
    ├─home            # 首页
    ├─login           # 登录
    ├─article         # 内容管理
    └─publish         # 发布文章

总结: 做好开发前的准备,调整下项目结构。

# 第二章:项目架构

# 01-使用element-ui

目标:引入element-ui组件库,能够在项目中使用它。

​ 在pc管理系统开发过程中,需要创建静态页面,如果自己去编写,成本比较高。使用第三方的UI库,来快速构建页面会是最好选择。我们使用的是基于vue最流行的UI组件库 element-ui 饿了么UI (opens new window),它提供了网页开发中常用的功能,而且基于vue封装成了组件,我们只需参考官方提供的使用文档来使用即可。

使用步骤:

  • 安装 npm i element-ui

  • 导入

    • 完整引入(建议学习阶段使用,简单粗暴)
    • 按需引入(建议开发阶段使用,优化体积)
    // 完整引入,main.js写入以下代码
    import ElementUI from 'element-ui'
    import 'element-ui/lib/theme-chalk/index.css'
    
    Vue.use(ElementUI)
    
  • 使用 App.vue

      <div id="app">
        <el-button type="success">成功按钮</el-button>
      </div>
    

小结一下:

  • 饿了么UI是什么?
    • 是一个基于vue的UI组件库
  • 如果使用饿了么UI?
    • 安装,完整导入,全局使用。

# 02-使用vue-router

目标:在项目中使用vue-router实现前端路由

使用步骤:

  1. 安装:npm i vue-router

  2. 引入:

    以前是在main.js完成的,但是路由相关代码会比较多,现在是在src/router/index.js 进行引入和配置。

    import VueRouter from 'vue-router'
    
  3. 注册:

    import Vue from 'vue'
    Vue.use(VueRouter)
    
  4. 初始化:

    const router = new VueRouter({
      routes: [] // 路由规则
    })
    
  5. 导出路由实例:

    export default router
    
  6. 导入路由实例:main.js

    // @是路径别名,代表 src
    import router from '@/router'
    
  7. 挂载:main.js

    new Vue({
      render: h => h(App),
    +  router
    }).$mount('#app')
    

总结:安装使用vue-router,提取路由模块。

# 03-约定路由

目的:约定好前端路由规则,什么路径渲染什么组件。

设计思路:

1625109377614

具体规则:

路由路径 路由对应组件(页面) 路由等级
/login 登录 一级路由
/ 首页 一级路由
├─ / 概览页面 二级路由
├─ /article 文章管理 二级路由
├─ /publish 发布文章 二级路由

总结: 登录---->首页,内容完整切换使用一级路由,概览,文章,发布,再Layout基础之上切换嵌套路由,二级路由切换。

# 第三章:登录模块

# 01-登录-路由与组件

目的:准备好登录组件,配置好路由。

组件:src/views/login.vue

<template>
  <div class="container">Login</div>
</template>
<script>
export default {
  name: 'Login'
}
</script>
<style scoped lang="less"></style>

路由:src/router/index.js

import Login from '@/views/login'
const router = new VueRouter({
  routes: [
+    { path: '/login', component: Login }
  ]
})

出口:src/App.vue

<template>
  <div id="app">
    <router-view></router-view>
  </div>
</template>

# 02-登录-基础布局

目的:完成登录页面基础布局

结构:src/views/login/index.vue

  <div class="container">
    <el-card>
      <img class="logo" src="../../assets/logo.png" alt="">
      <!-- 表单 -->
    </el-card>
  </div>

样式:

src/views/login/index.vue

.container {
  width: 100%;
  height: 100%;
  position: absolute;
  left: 0;
  top: 0;
  background: url(../../assets/login.png);
  .el-card {
    width: 440px;
    height: 380px;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%);
    box-shadow: 0 0 50px rgba(0,0,0,0.1);
    .logo {
      width: 200px;
      display: block;
      margin: 0 auto 20px;
    }
  }
}

App.vue

* {
  margin: 0;
  padding: 0;
}

总结:el-card是饿了么UI卡片组件,组件根元素有class 类 .el-card

# 03-登录-绘制表单

目的:使用el-form组件绘制登录表单

大致步骤:

  • 先去阅读 el-form 组件的使用文档
  • 了解表单元素组件类型
  • 绘制表单

落的内容:src/views/login.vue

  • el-form用法
1. el-form 是表单容器组件
2. el-form-item 是表单项组件,label可以设置表单元素文字说明,可以放置表单元素组件。
  • el的表单元素组件
el-input  el-checkbox el-select el-radio 等等
  • 绘制表单
      <el-form>
        <el-form-item>
          <el-input placeholder="请输入手机号"></el-input>
        </el-form-item>
        <el-form-item>
          <el-input placeholder="请输入手机号"></el-input>
        </el-form-item>
        <el-form-item>
          <el-checkbox :value="true">我已阅读并同意「用户协议」和「隐私条款」</el-checkbox>
        </el-form-item>
        <el-form-item>
          <el-button type="primary">登 录</el-button>
        </el-form-item>
      </el-form>
    .el-form {
      padding: 0 20px;
      .el-button {
        width: 100%;
      }
    }

总结: 知道绘制表单用的是el-form,el-form-item和其他的表单元素组件。

# 04-登录-基本的表单校验

目的:完成表单的基本校验

大致步骤:

  • 先阅读element-ui的表单组件文档,找到校验功能总结方法
  • 明确下校验需求
  • 实践下校验功能

落的内容:src/views/login.vue

  • 校验方法
1. el-form  需要加上一个属性  model  通过这个属性绑定表单数据对象
2. el-form  需要加上一个属性  rules  通过这个属性绑定校验规则对象(定义校验逻辑)
3. el-from-item 需要加上一个属性 prop 通过这个属性来指定当前表单项使用的校验规则
4. 校验规则的名称需要和绑定表单数据中的字段名一致
5. 具体的校验规则可参考官方示例
  • 校验需求
手机号:必填,1开头,3-9之间,9个数字
验证码:必填,6个字符
  • 实践代码
  data () {
    return {
      // 数据
      form: {
        mobile: '',
        code: ''
      },
      // 规则
      rules: {
        mobile: [
          { required: true, message: '请输入手机号', trigger: 'blur' }
        ],
        code: [
          { required: true, message: '请输入验证码', trigger: 'blur' },
          { len: 6, message: '验证码6个字符', trigger: 'blur' }
        ]
      }
    }
  }
+      <el-form :model="form" :rules="rules">
+        <el-form-item prop="mobile">
+          <el-input v-model="form.mobile" placeholder="请输入手机号"></el-input>
        </el-form-item>
+        <el-form-item prop="code">
+          <el-input v-model="form.code" placeholder="请输入验证码"></el-input>
        </el-form-item>
        <el-form-item>
          <el-checkbox :value="true">我已阅读并同意「用户协议」和「隐私条款」</el-checkbox>
        </el-form-item>
        <el-form-item>
          <el-button type="primary">登 录</el-button>
        </el-form-item>
      </el-form>

总结: el-form上绑定model指定表单数据对象,绑定rules指定表单校验对象,el-form-item的prop属性指定当前表单项使用校验规则中的 xxx 规则去校验表单对象中的 xxx 数据。

# 05-登录-自定义表单校验

目的:完成自己定义校验规则

大致步骤:

  • 先阅读element-ui的表单组件文档,找到自定校验规律
  • 实践自定义校验

落地内容:src/views/login.vue

  • 自定义校验规律
1. 在 校验规则 对象中,  validator 是用来指定自定义校验函数
2. 这个 自定义校验函数 必须先声明,在data的return之前声明即可
3. 自定义校验函数三个参数:rule  value  callback
3.1 rule  当前字段对应的校验对象  不会使用
3.2 value 当前字符段对应的值    我们需要校验这个值
3.3 callback 失败--->callback(new Error('校验失败的提示'))  成功---->callback()
  • 自定义校验实践
  data () {
+    const checkMobile = (rule, value, callback) => {
+      if (/^1[3-9]\d{9}$/.test(value)) {
+        callback()
+      } else {
+        callback(new Error('手机号格式不对'))
+      }
+    }
    return {
      // 数据
      form: {
        mobile: '',
        code: ''
      },
      // 规则
      rules: {
        mobile: [
+          { required: true, message: '请输入手机号', trigger: 'blur' },
          { validator: checkMobile, trigger: 'blur' }
        ],
        code: [
          { required: true, message: '请输入验证码', trigger: 'blur' },
          { len: 6, message: '验证码6个字符', trigger: 'blur' }
        ]
      }
    }
  }

总结: 使用validator属性可以指定一个校验函数,再校验函数中来校验value数据,调用callback代表校验完成,传入错误对象代表失败,不传代表成功。

# 06-登录-整体表单校验

目的:对整个表单进行校验

大致步骤:

  • 先阅读element-ui的表单组件文档,找到整体表单校验方式
  • 实践整体表单校验

落地内容:src/views/login.vue

  • 整体表单校验方式
1. 通过调组件实例的函数 validate 进行整体校验
2. validate(function(valid){ // valid 为 true 校验成功 })
3. 通过ref绑定el-form,然后this.$refs获取组件实例,再去调用validate函数
  • 实践整体表单校验,点击登录按钮校验
<el-form ref="form" :model="form" :rules="rules">
<el-button @click="login()" type="primary">登 录</el-button>
  methods: {
    login () {
      this.$refs.form.validate(valid => {
        console.log(valid)
      })
    }
  }

总结: 使用ref="xxx"绑定el-form组件,使用this.$refs.xxx.validate()校验整体表单。

# 07-登录-挂载axios

目的:创建一个axios实现挂载再Vue原型上,将来再任何vue组件下可通过this访问,方便开发。

大致步骤:

  • 安装axios
  • 创建axios
  • 挂载axios

落地代码:

  • 安装axios
npm i axios
  • 创建axios src/utils/http.js
import axios from 'axios'

const instance = axios.create({
  baseURL: 'http://geek.itheima.net/',
  timeout: 5000
})

export default instance

  • 挂载axios src/main.js
import http from '@/utils/http'

Vue.prototype.$http = http

总结: 创建一个axios实例通过prototype挂载到Vue上,所有vue实例可以访问。

# 08-登录-进行登录

目的:进行登录实现

大致步骤:

  • 发起登录请求
  • 成功:保存token在本地,跳转首页
  • 失败:提示错误消息

落地代码:

    login () {
      this.$refs.form.validate(async valid => {
        if (valid) {
          try {
           // 请求
            const res = await this.$http.post('/v1_0/authorizations', this.form)
            // 成功
            localStorage.setItem('geek-client-pc-store', res.data.data.token)
            this.$router.push('/')
          } catch (e) {
          // 失败
            this.$message.error(e.response.data.message || '登录失败')
          }
        }
      })
    }

try catch 的用法

try{
    // 写可能出现异常的代码片段
}catch(e) {
    // 如果出现异常执行catch函数
    // e 就是错误对象
}

总结: 发起登录请求,成功后存储token后跳转到首页。

# 09-登录-操作token工具

目标:封装一个操作本地token的工具模块auth.js

大致步骤:

  • 约定好存储数据的key定义成常量
  • 提供一个获取token函数
  • 提供一个设置token函数
  • 提供一个删除token函数

落地代码:

  • 定义工具 src/utils/auth.js
// 操作认证信息token的工具函数
const KEY = 'geek-client-pc-store'
export default {
  // 获取token
  getToken () {
    return localStorage.getItem(KEY)
  },
  // 设置token
  setToken (token) {
    localStorage.setItem(KEY, token)
  },
  // 删除token
  delToken () {
    localStorage.removeItem(KEY)
  }
}
  • 使用工具 src/views/login.vue
import auth from '@/utils/auth'
            // 请求
            const res = await this.$http.post('/v1_0/authorizations', this.form)
            // 成功
            // localStorage.setItem('geek-client-pc-store', res.data.data.token)
+            auth.setToken(res.data.data.token)
            this.$router.push('/')

# 10-登录-访问控制

目的:实现除去登录页面,其他页面访问都需要判断是否存储token,没有拦截到登录

大致步骤:

  • 弄清楚访问控制的逻辑
  • 使用路由导航守卫实现

落地代码:

  • 弄清楚访问控制的逻辑

1625128481411

  • 使用路由导航守卫实现 src/router/index.js
import auth from '@/utils/auth'
router.beforeEach((to, from, next) => {
  // 获取token
  const token = auth.getToken()
  // 不是访问登录,有没有token,跳转登录页面
  if (to.path !== '/login' && !token) return next('/login')
  // 其他情况放行
  next()
})

总结:使用router的breforeEach进行访问权限控制。

# 第四章:概览页面

# 00-代码片段

目的:使用vue快捷方式创建vue模版组件

步骤:

  • ctrl + shift + p 打开功能搜索框
  • 输入 代码片段 搜索功能 ,找到如下功能点击它

1626060260560

  • 点击新建代码片段

1626060308081

  • 将下面的代码复制到文件
{
	"vue-component": {
		"scope": "vue",
		"prefix": "vue",
		"body": [
			"<template>",
			"  <div class=\"container\"></div>",
			"</template>",
			"<script>",
			"export default {",
			"  name: ''",
			"}",
			"</script>",
			"<style scoped lang=\"less\"></style>",
			""
		],
		"description": "生成一个vue单文件组件"
	}
}

1626060373353

  • 最后再vue文件输入 vue 提示代码生成方式

# 01-概览-路由与布局组件

组件:src/views/Layout.vue

<template>
  <div class="container">Layout</div>
</template>
<script>
export default {
  name: 'Layout'
}
</script>
<style scoped lang="less"></style>

路由:src/router/index.js

import Layout from '@/views/Layout'
const router = new VueRouter({
  routes: [
    { path: '/login', component: Login },
+    {path: '/',component: Layout}
  ]
})

# 02-概览-布局组件架子

目标:使用element-ui布局容器准备Layout组件的页面架子

大致步骤:

  • 找到element-ui文档中的布局容器组件,找到对应的结构代码。
  • 在Layout.vue组件中使用代码,添加结构和样式

落地代码:src/views/Layout.vue

<template>
  <el-container class="container">
    <el-aside width="200px">
      <div class="logo"></div>
    </el-aside>
    <el-container>
      <el-header>
        <span style="margin-right:20px">用户名</span>
        <el-link icon="el-icon-unlock" :underline="false">退出</el-link>
      </el-header>
      <el-main>
        <router-view></router-view>
      </el-main>
    </el-container>
  </el-container>
</template>
<script>
export default {
  name: 'Layout'
}
</script>
<style scoped lang="less">
.container {
  width: 100%;
  height: 100%;
  position: absolute;
  left: 0;
  top: 0;
  .el-aside {
    background: #023;
    .logo {
      width: 200px;
      height: 60px;
      background:#023 url(../assets/logo.png) no-repeat center / 160px auto;
    }
  }
  .el-header {
    border-bottom: 1px solid #ddd;
    display: flex;
    justify-content: flex-end;
    align-items: center;
  }
}
</style>

# 03-概览-布局组件菜单

目的:在左侧栏加上el-menu组件搭建菜单

大致步骤:

  • 找到element-ui文档中的导航菜单组件,找到对应的结构代码。
  • 在Layout.vue组件中使用代码,改造成我们需要的菜单。

落的代码:

  • 分析示例代码
1. el-menu 是导航菜单容器
2. el-menu-item 是菜单项
3. 如果有子级菜单使用 el-submenu 包裹 el-menu-item 组件,如果有分组 el-menu-item 外包裹 el-menu-item-group 组件
  • 使用导航菜单
      <el-menu
        background-color="#023"
        style="border-right:none"
        text-color="#fff"
        default-active="/"
      >
        <el-menu-item index="/">
          <i class="el-icon-s-home"></i>
          <span slot="title">数据概览</span>
        </el-menu-item>
        <el-menu-item index="/article">
          <i class="el-icon-document"></i>
          <span slot="title">内容管理</span>
        </el-menu-item>
        <el-menu-item index="/publish">
          <i class="el-icon-s-promotion"></i>
          <span slot="title">发布文章</span>
        </el-menu-item>
      </el-menu>

属性:

  • el-menu ---> default-active 当前激活那个菜单项,值为el-menu-item的index的值。
  • el-menu-item ---> index 菜单项标识

总结: 使用element-ui组件,先分析,再使用。诀窍:试一试。

# 04-概览-路由与概览组件

目的:完成概览组件,完成二级路由配置,以及出口的配置。

组件:src/home/index.vue

<template>
  <div class="home"></div>
</template>
<script>
export default {
  name: 'Home'
}
</script>
<style scoped lang="less">
.home {
  width: 100%;
  height: 100%;
  background:#f5f5f5 url(../../assets/chart.png) no-repeat center / contain;
}
</style>

路由:src/router/index.js

import Home from '@/views/home'
const router = new VueRouter({
  routes: [
    { path: '/login', component: Login },
    {
      path: '/',
      component: Layout,
+      children: [
+        { path: '/', component: Home }
+      ]
    }
  ]
})

出口:src/views/Layout.vue

      <el-main>
+        <router-view></router-view>
      </el-main>

# 05-概览-获取个人信息

目的:Layout.vue组件中需要登录用户的名称,需要调用接口获取个人信息。

大致步骤:

  • data中声明 用户数据 对象
  • 组件初始化获取个人信息,设置用户信息
  • 模版中使用 name 没有就使用 mobile

落地代码:src/views/Layout.vue

  data () {
    return {
      user: {}
    }
  },
  async created () {
    const res = await this.$http.get('v1_0/user/profile')
    this.user = res.data.data
  },
      <el-header>
+        <span style="margin-right:20px">{{user.name||user.mobile}}</span>
        <el-link icon="el-icon-unlock" :underline="false">退出</el-link>
      </el-header>

1625198877293

由于我们请求的时候没有带上token请求后台,导致后台认为我们没有登录,响应401告诉你token失效或未传。

总结: 调用需要登录状态的后台接口是需要携带token的,下一节我们实现下携带token

# 06-概览-请求携带token

目的:在请求前获取本地存储的token设置在请求头

大致步骤:

  • 分析示例代码
  • 完成token携带

落地代码:src/utils/http.js

  • 分析-请求拦截器
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });
  • 使用
import auth from '@/utils/auth'
instance.interceptors.request.use(config => {
  const token = auth.getToken()
  if (token) config.headers.Authorization = `Bearer ${token}`
  return config
}, err => Promise.reject(err))

总结: 使用axios请求拦截器可以在请求前给请求头带上token

# 07-概览-拦截token失效

目的:在token失效的时候拦截跳转到登录页面

大致步骤:

  • token在服务端是会去判断有效时间的,极客园的是2小时失效
  • 我们不能等两小时再来操作,可以在浏览器把token改成一个错误的,其实就是默认token失效。
  • 响应拦截器来根据401判断,分析示例代码
  • 完成token失效拦截

落地代码:src/utils/http.js

  • 分析-响应拦截器
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  });
  • 使用
import router from '@/router'
instance.interceptors.response.use(res => res, err => {
  // 失效token
  if (err.response && err.response.status === 401) {
    auth.delToken()
    router.push('/login')
  }
  return Promise.reject(err)
})

总结: 使用axios响应拦截器可以根据401响应状态码拦截token失效

# 08-概览-退出功能

目的:失效退出登录

大致步骤:

  • 本地删除token就是退出登录
  • 绑定按钮事件,事件中删除token跳转登录页面即可。

落地代码:src/views/Layout.vue

<el-link @click="logout()" icon="el-icon-unlock" :underline="false">退出</el-link>
import auth from '@/utils/auth.js'
  methods: {
    logout () {
      auth.delToken()
      this.$router.push('/login')
    }
  }

# 第五章:内容管理

# 01-内容管理-路由与组件

组件:src/views/article/index.vue

<template>
  <div class="article">Article</div>
</template>
<script>
export default {
  name: 'Article'
}
</script>
<style scoped lang="less"></style>

路由:src/router/index.js

import Article from '@/views/article'
      children: [
        { path: '/', component: Home },
+        { path: '/article', component: Article }
      ]

# 02-内容管理-开启菜单路由

目的:点击菜单进行路由跳转,刷新页面需要激活当前菜单。

大致步骤:

  • 开启导航菜单路由功能
  • 刷新页面后保存激活状态

落地代码:src/views/Layout.vue

  • 开启导航菜单路由功能
      <el-menu
        background-color="#023"
        style="border-right:none"
        text-color="#fff"
        default-active="/"
+        router
      >

加上router属性后,el-menu-item的index属性值就是跳转的路径

  • 刷新页面后保存激活状态
      <el-menu
        background-color="#023"
        style="border-right:none"
        text-color="#fff"
+        :default-active="$route.path"
        router
      >

$route.path 获取当前路由路径,设置给default-active属性,激活对应的el-menu-item菜单项。

# 03-内容管理-筛选区域

目的:完成筛选区域布局

大致步骤:

  • 准备基本结构
  • 添加-状态-单选框
  • 添加-频道-下拉框
  • 添加-时间-日期控件

落地代码:src/views/article/index.vue

1)基本结构

  <div class='article-page'>
    <!-- 筛选区域 -->
    <el-card>
      <div slot="header">
        <el-breadcrumb separator-class="el-icon-arrow-right">
          <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
          <el-breadcrumb-item>内容管理</el-breadcrumb-item>
        </el-breadcrumb>
      </div>
      <!-- 表单 -->
      <el-form label-width="80px">
        <el-form-item label="状态:"></el-form-item>
        <el-form-item label="频道:"></el-form-item>
        <el-form-item label="日期:"></el-form-item>
        <el-form-item label="">
          <el-button type="primary">筛选</el-button>
        </el-form-item>
      </el-form>
    </el-card>
    <!-- 结果区域 -->
  </div>

2)状态-单选框

        <el-form-item label="状态:">
          <el-radio-group v-model="reqParams.status">
            <el-radio :label="null">全部</el-radio>
            <el-radio :label="0">草稿</el-radio>
            <el-radio :label="1">待审核</el-radio>
            <el-radio :label="2">审核通过</el-radio>
            <el-radio :label="3">审核失败</el-radio>
            <el-radio :label="4">已删除</el-radio>
          </el-radio-group>
        </el-form-item>
  data () {
    return {
      // 提交给后台的参数
      reqParams: {
        // 默认值为null,使用axios提交的时候null值是不会传递的。
        status: null
      }
    }
  }

3)频道-下拉框

        <el-form-item label="频道:">
          <el-select v-model="reqParams.channel_id" placeholder="请选择">
            <el-option
              v-for="item in channelOptions"
              :key="item.value"
              :label="item.label"
              :value="item.value">
            </el-option>
          </el-select>
        </el-form-item>
  data () {
    return {
      // 提交给后台的参数
      reqParams: {
        // 默认值为null,使用axios提交的时候null值是不会传递的。
        status: null,
+        channel_id: null
      },
      // 频道选项
+      channelOptions: [{ value: 1, label: '前端' }, { value: 2, label: 'java' }]
    }
  }

4)时间-日期控件

        <el-form-item label="日期:">
          <el-date-picker
            v-model="dateArr"
            type="daterange"
            range-separator=""
            start-placeholder="开始日期"
            end-placeholder="结束日期">
          </el-date-picker>
        </el-form-item>
  data () {
    return {
      // 提交给后台的参数
      reqParams: {
        // 默认值为null,使用axios提交的时候null值是不会传递的。
        status: null,
        channel_id: null,
+        begin_pubdate: null,
+        end_pubdate: null
      },
      // 频道选项
      channelOptions: [{ value: 1, label: '前端' }, { value: 2, label: 'java' }],
      // 日期范围 [起始时间,结束时间]
+      dateArr: []
    }
  }

# 04-内容管理-结果区域

目的:完成结果区域布局

大致步骤:

  • 基本布局
  • 分析表格组件
  • 绘制表格组件
  • 使用分页组件

落地代码:src/views/article/index.vue

1)基本布局

    <!-- 结果区域 -->
    <el-card style="margin-top:20px">
      <div slot="header">根据筛选条件共查询到 46148 条结果:</div>
      <!-- 表格 -->
      <!-- 分页 -->
    </el-card>

2)分析表格组件

1625206748596

3)绘制表格组件

  • el-table 属性 data 接收表格数组数据,必填。
      <!-- 表格 -->
      <el-table :data="articles">
        <el-table-column label="封面"></el-table-column>
        <el-table-column label="标题"></el-table-column>
        <el-table-column label="状态"></el-table-column>
        <el-table-column label="发布时间"></el-table-column>
        <el-table-column label="阅读数"></el-table-column>
        <el-table-column label="评论数"></el-table-column>
        <el-table-column label="点赞数"></el-table-column> 
        <el-table-column label="操作"></el-table-column>
      </el-table>
      dateArr: [],
+      articles: []

4)使用分页组件

1625206824788

      <!-- 分页 -->
      <el-pagination
        style="margin-top:20px"
        background
        layout="prev, pager, next"
        :total="1000">
      </el-pagination>

# 05-内容管理-渲染频道选项

目的:获取文章频道数据且进行渲染

大致步骤:

  • 在mehtods中定义一个获取选项的函数 ,获取数据赋值给data中的选项
  • 在created中调用改函数即可
  • 渲染模版

落地代码:src/views/article/index.vue

  • 获取选项函数
  methods: {
    async getChannelOptions () {
      const res = await this.$http.get('v1_0/channels')
      this.channelOptions = res.data.data.channels
    }
  }
  • 调用函数
  created () {
    this.getChannelOptions()
  },
  • 渲染模版
        <el-form-item label="频道:">
          <el-select v-model="reqParams.channel_id" placeholder="请选择">
            <el-option
              v-for="item in channelOptions"
+              :key="item.id"
+              :label="item.name"
+              :value="item.id">
            </el-option>
          </el-select>
        </el-form-item>

# 06-内容管理-渲染简单列

目的:获取文章列表数据,渲染 标题,发布时间,阅读数,评论数,点赞数 5列。

大致步骤:

  • 获取文章列表数据在组件初始化后
  • 渲染 标题,发布时间 两列,它们简单些

落地代码:src/views/article/index.vue

  • 获取数据
  data () {
    return {
      // 提交给后台的参数
      reqParams: {
        // 默认值为null,使用axios提交的时候null值是不会传递的。
        status: null,
        channel_id: null,
        begin_pubdate: null,
        end_pubdate: null,
+        page: 1,
+        per_page: 10
      },
methods: {
+    async getArticles () {
+      const res = await this.$http.get('v1_0/mp/articles', { params: this.reqParams })
+      this.articles = res.data.data.results
+    }
  created () {
    this.getChannelOptions()
+    this.getArticles()
  },
  • 渲染内容
      <!-- 表格 -->
      <el-table :data="articles">
        <el-table-column label="封面"></el-table-column>
+        <el-table-column label="标题" prop="title" width="400px"></el-table-column>
        <el-table-column label="状态"></el-table-column>
+        <el-table-column label="阅读数" width="120px" prop="read_count"></el-table-column>
+        <el-table-column label="评论数" width="120px" prop="comment_count"></el-table-column>
+        <el-table-column label="点赞数"  width="120px" prop="like_count"></el-table-column>
+        <el-table-column label="发布时间" prop="pubdate"></el-table-column>
        <el-table-column label="操作" width="120px"></el-table-column>
      </el-table>

总结: 使用el-table-column的prop属性指定当前列显示什么字段的数据

# 07-内容管理-渲染复杂列

目的:渲染 封面,状态,操作 三列。

大致步骤:

  • 参考element-ui文档,总结渲染自定义列套路
  • 渲染封面列
  • 渲染状态列
  • 渲染操作列

落地代码:src/views/article/index.vue

  • 参考 https://element.eleme.cn/#/zh-CN/component/table#zi-ding-yi-lie-mo-ban
1. 需要使用作用域插槽,暴露的数据 scope = { $index, row }
2. $index 是遍历数组(articles)的索引
3. row 是遍历数组(articles)的每一项 (行数据)
  • 渲染封面
        <el-table-column label="封面">
          <template slot-scope="scope">
            <el-image :src="scope.row.cover.images[0]" style="width:200px;height:150px">
              <div slot="error" class="image-slot">
                <img src="@/assets/error.png" alt="" width="200px" height="150px">
              </div>
            </el-image>
          </template>
        </el-table-column>
  • 渲染状态列
        <el-table-column label="状态">
          <template slot-scope="scope">
            <el-tag v-if="scope.row.status===0" type="info">草稿</el-tag>
            <el-tag v-if="scope.row.status===1">待审核</el-tag>
            <el-tag v-if="scope.row.status===2" type="success">审核通过</el-tag>
            <el-tag v-if="scope.row.status===3" type="warning">审核失败</el-tag>
            <el-tag v-if="scope.row.status===4" type="danger">已删除</el-tag>
          </template>
        </el-table-column>
  • 渲染操作列
        <el-table-column label="操作" width="120px">
          <template>
            <el-button type="primary" icon="el-icon-edit" circle plain></el-button>
            <el-button type="danger" icon="el-icon-delete" circle plain></el-button>
          </template>
        </el-table-column>

1625209403114

# 08-内容管理-实现分页功能

目标:完成分页渲染和切换功能

大致步骤:

  • 准备总条数数据total
  • 完成分页渲染,通过分页组件提供的属性
    • total属性,指定一共多少条
    • page-size属性,指定一页多少条
    • current-page属性,指定当前是第几页
  • 切换分页功能
    • current-change事件,触发事件得到当前改变的页码,通过页码去请求

落地代码:src/views/article/index.vue

1)total数据

      articles: [],
+      total: 0
    async getArticles () {
      const res = await this.$http.get('v1_0/mp/articles', { params: this.reqParams })
      this.articles = res.data.data.results
+      this.total = res.data.data.total_count
    },
<div slot="header">根据筛选条件共查询到 {{total}} 条结果:</div>

2)完成分页渲染

      <!-- 分页 -->
      <el-pagination
        style="margin-top:20px"
        background
        layout="prev, pager, next"
+        :current-page="reqParams.page"
+        :page-size="reqParams.per_page"
+        :total="total">
      </el-pagination>

3)切换分页

      <!-- 分页 -->
      <el-pagination
        style="margin-top:20px"
        background
        layout="prev, pager, next"
+        @current-change="togglePage"
        :current-page="reqParams.page"
        :page-size="reqParams.per_page"
        :total="total">
      </el-pagination>
    togglePage (changedPage) {
      this.reqParams.page = changedPage
      this.getArticles()
    }

# 09-内容管理-实现筛选功能

目标:实现文章筛选功能

大致步骤:src/views/article/index.vue

  • 绑定筛选按钮的点击事件,指定处理函数。
  • 需要带上筛选条件进行查询,而且页码需要变成第一页,发请求渲染列表即可。
    • 页码第一页,this.reqParams.page = 1 搞定
    • 双向绑定reqParams的条件不用管,只要改动了,reqParams中已经变化。
      • 状态 status
      • 频道 channel_id
    • 两个条件 begin_pubdate end_pubdate 需要在时间范围选择之后,给他们赋值。
  • 频道选项需要清空。

落地代码:

  • 筛选函数
<el-button type="primary" @click="filterArticles()">筛选</el-button>
    filterArticles () {
      this.reqParams.page = 1
      this.getArticles()
    },
  • 日期条件
          <el-date-picker
+           @change="changeDate"
            v-model="dateArr"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期">
          </el-date-picker>
    changeDate (dateArr) {
      // 日期访问改变后给reqParams中的日期赋值
      // dateArr 选择日期后 [start,end]
      // dateArr 用户做清空 null
      if (dateArr) {
        this.reqParams.begin_pubdate = dateArr[0]
        this.reqParams.end_pubdate = dateArr[1]
      } else {
        this.reqParams.begin_pubdate = null
        this.reqParams.end_pubdate = null
      }
    },
  • 频道清空
+          <el-select  clearable v-model="reqParams.channel_id" placeholder="请选择" >
            <el-option
              v-for="item in channelOptions"
              :key="item.id"
              :label="item.name"
              :value="item.id">
            </el-option>
          </el-select>

# 10-内容管理-实现删除功能

目的:实现文章删除功能

大致步骤:

  • 给删除按钮绑定点击事件,指定处理函数,把文章ID传递给函数。
  • 弹出一个确认框,标题:温馨提示,内容:此操作将永久删除该文章, 是否继续?
  • 点击确认的时候,发送删除请求,删除成功之后,更新当前列表。

落地代码:src/views/article/index.vue

+          <template slot-scope="scope">
            <el-button type="primary" icon="el-icon-edit" circle plain></el-button>
+           <el-button @click="deleteArticle(scope.row.id)"  type="danger" icon="el-icon-delete" circle plain></el-button>
          </template>
    deleteArticle (id) {
      this.$confirm('此操作将永久删除该文章, 是否继续?', '温馨提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(async () => {
        // 发送删除请求
        await this.$http.delete(`v1_0/mp/articles/${id}`)
        // 删除成功
        this.$message.success('删除文章成功')
        this.getArticles()
      }).catch(() => {
        // 取消不做任何事情
      })
    }

# 第六章:发布文章

# 01-发布文章-路由与组件

组件:src/views/publish/index.vue

<template>
  <div class="publish">Publish</div>
</template>
<script>
export default {
  name: 'Publish'
}
</script>
<style scoped lang="less"></style>

路由:src/router/index.vue

import Publish from '@/views/publish'
      children: [
        { path: '/', component: Home },
        { path: '/article', component: Article },
+        { path: '/publish', component: Publish }
      ]

# 02-发布文章-基础布局

目的:完成发布文章基础布局

大致步骤:

  • 卡片容器 el-card
    • 头部,面板屑
    • 内容,表单组件 el-form
      • 标题 el-input
      • 频道 封装频道组件(空)
      • 封面 el-radio 单选框 + 上传组件(空)
      • 内容 富文本组件(空)
      • 按钮

落地代码:

<template>
  <div class='publish-page'>
    <el-card>
      <div slot="header">
        <el-breadcrumb separator-class="el-icon-arrow-right">
          <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
          <el-breadcrumb-item>发布文章</el-breadcrumb-item>
        </el-breadcrumb>
      </div>
      <!-- 表单 -->
      <el-form label-width="120px">
        <el-form-item label="标题:">
          <el-input v-model="articleForm.title" placeholder="请输入文章标题" style="width:400px"></el-input>
        </el-form-item>
        <el-form-item label="频道:">频道组件</el-form-item>
        <el-form-item label="封面:">
          <el-radio-group v-model="articleForm.cover.type">
            <el-radio :label="1">单图</el-radio>
            <el-radio :label="3">三图</el-radio>
            <el-radio :label="0">无图</el-radio>
            <el-radio :label="-1">自动</el-radio>
          </el-radio-group>
          <div>上传组件</div>
        </el-form-item>
        <el-form-item label="内容:">富文本</el-form-item>
        <el-form-item label="">
          <el-button type="primary">发布文章</el-button>
          <el-button>存入草稿</el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>

<script>
export default {
  name: 'PublishPage',
  data () {
    return {
      // 表单数据
      articleForm: {
        title: null,
        content: null,
        cover: {
          type: 1,
          images: []
        },
        channel_id: null
      }
    }
  }
}
</script>

<style scoped lang='less'></style>

# 03-发布文章-频道组件-功能实现

目的:实现频道组件默认功能,能选择即可。

大致步骤:

  • 在components中新建一个组件,实现频道下拉选择功能。
  • 在main.js中注册为一个全局组件。
  • 在内容管理,发布文章,使用这个组件。

落地代码:

  • 定义组件 src/components/my-channel.vue
<template>
  <el-select
    v-model="channelId"
    placeholder="请选择"
    clearable
  >
    <el-option
      v-for="item in channelOptions"
      :key="item.id"
      :label="item.name"
      :value="item.id"
    >
    </el-option>
  </el-select>
</template>

<script>
export default {
  name: 'MyChannel',
  data () {
    return {
      channelId: null,
      channelOptions: []
    }
  },
  created () {
    this.getChannelOptions()
  },
  methods: {
    async getChannelOptions () {
      const res = await this.$http.get('/v1_0/channels')
      this.channelOptions = res.data.data.channels
    }
  }
}
</script>

<style scoped lang='less'></style>
  • 注册组件 src/main.js
// 注册全局组件
import MyChannel from '@/components/my-channel'
Vue.component(MyChannel.name, MyChannel)
  • 使用组件

src/views/article.vue

        <el-form-item label="频道:">
+          <my-channel></my-channel>
        </el-form-item>

总结: 这样只是封装了功能,数据还需要实现双向数据绑定。

# 04-发布文章-频道组件-双向绑定

目的:实现频道组件双向数据绑定功能,支持v-model指令。

大致步骤:

  • 回顾父子传值,以及v-model语法糖

    • 父组件传递子组件:父 :value="count" ----> 子 props:['value']
    • 子组件传递父组件:子 $emit('input', 100) ----> 父 @input="count=$event"
    • 然而 <my-com :value="count" @input="count=$event" ></my-com> 是可以简写成
      • <my-com v-model="count"></my-com> 基于v-model语法糖原理
  • 频道ID父传子

  • 频道ID子传父

  • 使用v-model绑定频道ID

落地代码:

  1. 父传子
  • 父组件
<my-channel :value="reqParams.channel_id"></my-channel>
  • 子组件
  // props接收的数据,特点:只读
+  props: ['value'],
  <el-select
    @change="changeChannel"
-    v-model="channeId"
+    :value="value"
  1. 子传父
  • 父组件
<my-channel :value="reqParams.channel_id" @input="reqParams.channel_id=$event"></my-channel>
  • 子组件
  data () {
    return {
+      // channelId: null,
      channelOptions: []
    }
  },
  created () {
    this.getChannelOptions()
  },
  methods: {
    changeChannel (changedChannelId) {
      // 通知父组件,频道ID已变化,你也需要改变
+      this.$emit('input', changedChannelId)
    },
  1. v-model的使用
        <el-form-item label="频道:">
          <!-- 使用my-channel.vue组件 -->
          <!-- <my-channel :value="reqParams.channel_id" @input="reqParams.channel_id=$event"></my-channel> -->
          <my-channel v-model="reqParams.channel_id"></my-channel>
        </el-form-item>

4)发布文章中使用组件

        <el-form-item label="频道:">
          <my-channel v-model="articleForm.channel_id"></my-channel>
        </el-form-item>
.el-select {
  width: 400px;
}

axios传参:后端提供接口

  • 地址?后键值对 query 对应axios axios({params:参数对象}) axios.get(url,{params:参数对象})
  • 请求体传参 body 对应axios axios({data:参数对象}) axios.post(url, 参数对象) put patch
  • 地址/后参数 路径参数 对应axios axios({url:'进行拼接'})
  • 请求头参数 headers 对应axios axios({headers:参数对象})

# 05-发布文章-封面-上传组件

目的:使用上传组件完成上传图片位置布局和交互

大致步骤:

  • 查看element-ui文档找到el-upload组件照片墙示例,分析代码
  • 使用el-upload组件
  • 完成根据封面图类型切换组件形态

落地代码:

  • 一张图,三张图才使用组件,上传图片的限制和type的值一致。
          <div v-if="articleForm.cover.type===1||articleForm.cover.type===3">
            <el-upload
              ref="upload"
              action="https://jsonplaceholder.typicode.com/posts/"
              list-type="picture-card"
              :limit="articleForm.cover.type"
            >
              <i class="el-icon-plus"></i>
            </el-upload>
          </div>
  • 当类型修改的时候,需要重置选择的图片
+          <el-radio-group @change="changeCoverType()" v-model="articleForm.cover.type">
            <el-radio :label="1">单图</el-radio>
            <el-radio :label="3">三图</el-radio>
            <el-radio :label="0">无图</el-radio>
            <el-radio :label="-1">自动</el-radio>
          </el-radio-group>
    changeCoverType () {
      this.$refs.upload && this.$refs.upload.clearFiles()
    },
  • 修改样式,上传组件动画
::v-deep .el-upload-list__item {
  transition: none;
}

总结: 能够基本使用el-upload组件完成上传图片的布局和交互

# 06-发布文章-封面-完成上传

目的:调用后台接口完成上传和数据保存

大致步骤:

  • 阅读接口文档得到:地址,参数,返回数据
  • 找到el-upload组件设置 地址,参数,请求头,的属性并且设置
  • 找到el-upload组件设置 上传成功函数,处理成功后的逻辑

落地代码:

  • 配置请求地址,参数,请求头
            <el-upload
              ref="upload"
+              :action="`${$http.defaults.baseURL}v1_0/upload`"
+              name="image"
+              :headers="{Authorization: `Bearer ${token}`}"
              list-type="picture-card"
              :limit="articleForm.cover.type"
            >
              <i class="el-icon-plus"></i>
            </el-upload>
// data 中数据
+      token: auth.getToken()
import auth from '@/utils/auth'
  • 配置上传成功函数,处理数据
            <el-upload
              ref="upload"
              :action="`${$http.defaults.baseURL}v1_0/upload`"
              name="image"
              :headers="{Authorization: `Bearer ${token}`}"
              list-type="picture-card"
              :limit="articleForm.cover.type"
+              :on-success="uploadSuccess"
            >
              <i class="el-icon-plus"></i>
            </el-upload>
    uploadSuccess (res) {
      this.articleForm.cover.images.push(res.data.url)
    }

切换类型清理数据

    changeCoverType () {
+      this.articleForm.cover.images = []
      this.$refs.upload && this.$refs.upload.clearFiles()
    },

# 07-发布文章-富文本使用

目的:掌握在vue项目中使用富文本。

大致步骤:

  • 找到我们需要的富文本组件
  • 安装
  • 注册
  • 使用

落地代码:

  • vue项目中使用富文本:

    • https://github.com/vuejs/awesome-vue 找vue相关的技术
    • https://github.com/surmon-china/vue-quill-editor 富文本编辑器
  • 安装

npm install vue-quill-editor
  • 局部注册
+import 'quill/dist/quill.core.css'
+import 'quill/dist/quill.snow.css'
+import 'quill/dist/quill.bubble.css'

+import { quillEditor } from 'vue-quill-editor'
export default {
  name: 'PostPage',
+  components: { quillEditor },
  • 使用
        <el-form-item label="内容:">
          <quill-editor v-model="articleForm.content" :options="editorOption"/>
        </el-form-item>
  data () {
    return {
      // 表单数据
      articleForm: {
        title: null,
        content: null,
        cover: {
          type: 1,
          images: []
        },
        channel_id: null
      },
      // 富文本配置
+      editorOption: {}
    }
  }
::v-deep .ql-editor {
  min-height: 300px;
}

# 08-发布文章-基本校验

目的:完成发布文章表单的基本校验功能

大致步骤:

  • 绑定表单数据对象和校验规则对象
  • 准备校验规则对象中具体的规则,
  • 通过prop给表单项添加校验

落地代码:

  • 校验规则对象
      // 校验规则
      articleRules: {
        title: [
          { required: true, message: '请输入文章标题', trigger: 'blur' },
          { min: 4, max: 50, message: '文章标题4-50字符', trigger: 'blur' }
        ],
        cover: [
          // 需要自定义
        ],
        channel_id: [
          { required: true, message: '请选择文章频道', trigger: 'change' }
        ],
        content: [
          { required: true, message: '请输入文章内容', trigger: 'blur' }
        ]
      },
  • 使用校验规则
      <!-- 表单 -->
+      <el-form label-width="120px" :model="articleForm" :rules="articleRules">
+        <el-form-item label="标题:" prop="title">
          <el-input v-model="articleForm.title" placeholder="请输入文章标题" style="width:400px"></el-input>
        </el-form-item>
+        <el-form-item label="频道:" prop="channel_id">
          <my-channel v-model="articleForm.channel_id"></my-channel>
        </el-form-item>
+        <el-form-item label="封面:" prop="cover">
          <el-radio-group @change="changeCoverType()" v-model="articleForm.cover.type">
            <el-radio :label="1">单图</el-radio>
            <el-radio :label="3">三图</el-radio>
            <el-radio :label="0">无图</el-radio>
            <el-radio :label="-1">自动</el-radio>
          </el-radio-group>
          <div v-if="articleForm.cover.type===1||articleForm.cover.type===3">
            <el-upload
              ref="upload"
              action="https://jsonplaceholder.typicode.com/posts/"
              name="image"
              :headers="{Authorization: `Bearer ${token}`}"
              list-type="picture-card"
              :limit="articleForm.cover.type"
              :on-success="uploadSuccess"
            >
              <i class="el-icon-plus"></i>
            </el-upload>
          </div>
        </el-form-item>
+        <el-form-item label="内容:" prop="content">
          <quill-editor v-model="articleForm.content" :options="editorOption"/>
        </el-form-item>
        <el-form-item label="">
          <el-button type="primary">发布文章</el-button>
          <el-button>存入草稿</el-button>
        </el-form-item>
      </el-form>

总结: 还是按照表单校验的套路完成基本的校验功能。

# 09-发布文章-特殊校验

目的:完成特殊表单项的校验,封面图,富文本。

大致步骤:

  • 完成 文章内容 校验
  • 完成 封面图 校验

落地代码:

  • 完成 文章内容 校验
  methods: {
    checkContent () {
      // 校验内容表单项
      this.$refs.articleForm.validateField('content')
    },
        <el-form-item label="内容:" prop="content">
          <quill-editor @blur="checkContent()" v-model="articleForm.content" :options="editorOption"/>
        </el-form-item>
+<el-form ref="articleForm" :model="articleForm" :rules="articleRules" label-width="120px">
  • 完成 封面图 校验
    // 自定义校验规则
    const checkCoverFn = (rule, value, cb) => {
      // 自己对value进行校验
      if (value.type === 1) {
        if (!value.images[0]) {
          return cb(new Error('请选择一张封面图'))
        }
      }
      if (value.type === 3) {
        if (!(value.images[0] && value.images[1] && value.images[2])) {
          return cb(new Error('请选择三张封面图'))
        }
      }
      // 代码走到这,校验成功
      cb()
    }
        cover: [
          { validator: checkCoverFn, trigger: 'change' }
        ]
    // 上传图片成功
    uploadSuccess (res) {
      // res就是响应数据
      this.articleForm.cover.images.push(res.data.url)
+      // 触发一次校验
+      this.$refs.articleForm.validateField('cover')
    }
  • 边界情况:删除的时候需要删除数据还需要进行校验,三张图上传成功切换一张图(清空了图片数据)加一次校验。
            <el-upload
              ref="upload"
              :action="$http.defaults.baseURL+'v1_0/upload'"
              name="image"
              :headers="{Authorization: `Bearer ${token}`}"
              :limit="articleForm.cover.type"
              :on-success="uploadSuccess"
+              :on-remove="removeFile"
              list-type="picture-card">
              <i class="el-icon-plus"></i>
            </el-upload>
    // 删除文件
    removeFile (file) {
      // 主动:删除images数组中对应的图片
      // file 中保存了之前上传图片响应的信息 response.data.url 图片地址
      // 根据这个图片地址找到images对应的索引,通过splice(索引,1) 删除图片
      const index = this.articleForm.cover.images.findIndex(item => item === file.response.data.url)
      this.articleForm.cover.images.splice(index, 1)
      // 自己再次校验
      this.$refs.articleForm.validateField('cover')
    }
    // 切换封面类型
    changeCoverType () {
      // 重置数据
      this.articleForm.cover.images = []
      // 重置组件
      this.$refs.upload && this.$refs.upload.clearFiles()
+      // 自己再次校验
+      this.$refs.articleForm.validateField('cover')
    },

总结: 遇到校验规则需要自定义使用 validator 配置,遇到非element-ui表单组件自己触发校验方法。

# 10-发布文章-进行提交

目的:能够完成发布文章功能

大致步骤:

  • 绑定 发布文章 和 存入草稿 按钮点击事件,传入不同标识区别操作
  • 再函数中先校验整体表单,通过后发送请求
  • 成功:提示,跳转列表
  • 失败:提示

落地代码:

  • 绑定事件
        <el-form-item label="">
          <el-button @click="submit(false)" type="primary">发布文章</el-button>
          <el-button @click="submit(true)">存入草稿</el-button>
        </el-form-item>
  • 提交函数
    submit (draft) {
      this.$refs.articleForm.validate(valid => {
        if (valid) {
          this.$http.post('/v1_0/mp/articles?draft=' + draft, this.articleForm).then(res => {
            this.$message.success('发布成功')
            this.$router.push('/article')
          }).catch(() => {
            this.$message.error('发布失败')
          })
        }
      })
    }

# 11-修改文章-填充数据

目的:当你是进入编辑文件页面,获取文章详情,填充表单,修改标题和按钮。

大致步骤:

  • 文章管理页面添加跳转逻辑代码,带上文章ID跳转到文章编辑
  • 从文章管理进入文章编辑,组件初始化的时候,判断是否有ID,获取文章详情数据,填充表单
  • 切换标题,和操作按钮

落地代码:

src/views/article/index.vue

<el-button @click="$router.push('/publish?id='+scope.row.id)" type="primary" icon="el-icon-edit" circle plain></el-button>

src/views/publish/index.vue

  created () {
    if (this.$route.query.id) {
      this.getArticle(this.$route.query.id)
    }
  },
  methods: {
    getArticle (id) {
      this.$http.get('/v1_0/mp/articles/' + id).then(res => {
        this.articleForm = res.data.data
      })
    },
<el-breadcrumb-item>{{$route.query.id?'修改':'发布'}}文章</el-breadcrumb-item>
        <el-form-item label="" v-if="$route.query.id">
          <el-button type="primary">修改文章</el-button>
          <el-button>存入草稿</el-button>
        </el-form-item>
        <el-form-item label="" v-else>
          <el-button @click="submit(false)" type="primary">发布文章</el-button>
          <el-button @click="submit(true)">存入草稿</el-button>
        </el-form-item>

填充图片

:file-list="fileList"
computed: {
   fileList () {
       return this.articleForm.cover.images.map(item=>({name:item,url:item}))
   } 
}

# 12-修改文章-路由组件钩子

目的:在由编辑切换发布组件不会初始化,可以使用路由组件钩子

大致步骤:

  • 地址栏参数变化,路径不变化,对应的组件不会重新初始化。
  • 我们使用 beforeRouteUpdate 来监听,编辑和发布 之间的切换。
  • 在钩子函数中根据将要切换的地址,ID就填充表单,没有就重置表单。

落地代码:

  beforeRouteUpdate (to, from, next) {
    if (to.query.id) {
      this.getArticle(to.query.id)
    } else {
      this.$refs.articleForm.resetFields()
    }
    next()
  },

总结:

  • 何时地址变化不会初始化组件?参数变化,路径不变
  • 如何监听路由参数的变化?beforeRouteUpdate 或者 watch也行

# 13-修改文章-进行修改

目的:完成文章的修改

  • 大致步骤:
    • 绑定 发布文章 和 存入草稿 按钮点击事件,传入不同标识区别操作
    • 再函数中先校验整体表单,通过后发送请求
    • 成功:提示,跳转列表
    • 失败:提示

落地代码:

  • 绑定事件
        <el-form-item label="" v-if="$route.query.id">
          <el-button @click="update(false)" type="primary">修改文章</el-button>
          <el-button @click="update(true)">存入草稿</el-button>
        </el-form-item>
  • 进行修改
    update (draft) {
      this.$refs.articleForm.validate(valid => {
        if (valid) {
          this.$http.put('/v1_0/mp/articles/' + this.$route.query.id + '?draft=' + draft, this.articleForm).then(res => {
            this.$message.success('修改成功')
            this.$router.push('/article')
          }).catch(() => {
            this.$message.error('修改失败')
          })
        }
      })
    }

# 第七章:项目收尾

# 01-项目打包

目的:能够打包项目启动托管服务,通过路由懒加载优化首屏加载速度。

大致步骤:

  • 先做一次简单的打包
  • 然后做项目文件托管
  • 最后优化下首屏加载

落地代码:

  • 先做一次简单的打包
npm run build
  • 然后做项目文件托管 npm i serve -g

安装 serve 工具,在 dist 目录执行 serve 通过 http:localhost:5000 访问

  • 最后优化下首屏加载 src/router/index.js
const Login = () => import('@/views/login')
const Layout = () => import('@/views/Layout')
const Home = () => import('@/views/home')
const Article = () => import('@/views/article')
const Publish = () => import('@/views/publish')

路由懒加载:访问 /login 路由,只去加载登录对应的资源

# 02-项目总结

总结: 项目中重点内容。

  • 使用vue-cli创建项目
  • element-ui需要会用
  • vue-router需要会用
  • axios需要会用,配置,拦截器
  • 登录模块,校验需要使用
  • 首页模块,导航菜单
  • 内容管理,筛选,列表,一套实现
  • 发布文章,频道组件封装,合并修改