課程管理(一)

該功能涉及到的因素更多,分為兩部分記錄過程
功能分析:

  • 展示上架狀態(tài)
  • 上下架操作
  • 添加課程
  • 編輯課程
  • 課程內(nèi)容管理(第二部分再做講解)
    基礎(chǔ)布局等不再多做贅述

course/index.vue 課程組件

<template>
  <div class="course">
    <course-list></course-list>
  </div>
</template>

<script>
import CourseList from './components/list.vue'

export default {
  name: 'CourseIndex',
  components: {
    CourseList
  }
}
</script>

<style lang="scss" scoped></style>

course/components/list.vue(新建)

<template>
  <div class="course-list">
    <el-card>
      <div slot="header">
        <span>數(shù)據(jù)篩選</span>
      </div>
      <el-form
        :inline="true"
        ref="form"
        label-position="left"
        :model="filterParams"
      >
        <el-form-item label="課程名稱:" prop="courseName">
          <el-input v-model="filterParams.courseName"></el-input>
        </el-form-item>
        <el-form-item label="狀態(tài):" prop="status">
          <el-select v-model="filterParams.status">
            <el-option label="全部" value=""></el-option>
            <el-option label="上架" value="1"></el-option>
            <el-option label="下架" value="0"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button
            :disabled="isLoading"
            @click="handleReset"
          >重置</el-button>
          <el-button
            type="primary"
            :disabled="isLoading"
            @click="handleFilter"
          >查詢</el-button>
        </el-form-item>
      </el-form>
    </el-card>

    <el-card>
      <div slot="header">
        <span>查詢結(jié)果:</span>
        <el-button
          style="float: right; margin-top: -10px"
          type="primary"
        >添加課程</el-button>
      </div>
      <el-table
        :data="courses"
        v-loading="isLoading"
        style="width: 100%; margin-bottom: 20px"
      >
        <el-table-column
          prop="id"
          label="ID"
          width="100">
        </el-table-column>
        <el-table-column
          prop="courseName"
          label="課程名稱"
          width="230">
        </el-table-column>
        <el-table-column
          prop="price"
          label="價(jià)格">
        </el-table-column>
        <el-table-column
          prop="sortNum"
          label="排序">
        </el-table-column>
        <el-table-column
          prop="status"
          label="上架狀態(tài)">
        待處理
        </el-table-column>
        <el-table-column
          prop="price"
          label="操作"
          width="200"
          align="center"
        >
          <template>
            <el-button>編輯</el-button>
            <el-button>內(nèi)容管理</el-button>
          </template>
        </el-table-column>
      </el-table>
      <el-pagination
        background
        layout="prev, pager, next"
        :total="totalCount"
        :disabled="isLoading"
        :current-page="filterParams.currentPage"
        @current-change="handleCurrentChange"
      />
    </el-card>
  </div>
</template>

<script>
import { getQueryCourses } from '@/services/course'

export default {
  name: 'CourseList',
  data () {
    return {
      // 篩選功能參數(shù)(表單數(shù)據(jù))
      filterParams: {
        currentPage: 1,
        pageSize: 10,
        courseName: '',
        status: ''
      },
      // 課程信息
      courses: [],
      // 數(shù)據(jù)總條數(shù)
      totalCount: 0,
      // 加載狀態(tài)
      isLoading: true
    }
  },

  created () {
    // 加載課程
    this.loadCourses()
  },

  methods: {
    // 加載課程
    async loadCourses () {
      this.isLoading = true
      const { data } = await getQueryCourses(this.filterParams)
      if (data.code === '000000') {
        // 保存課程信息
        this.courses = data.data.records
        this.totalCount = data.data.total
        this.isLoading = false
      }
    },
    // 分頁頁碼點(diǎn)擊操作
    handleCurrentChange (page) {
      this.filterParams.currentPage = page
      this.loadCourses()
    },
    // 篩選操作
    handleFilter () {
      this.filterParams.currentPage = 1
      this.loadCourses()
    },
    // 重置操作
    handleReset () {
      this.$refs.form.resetFields()
      this.filterParams.currentPage = 1
      this.loadCourses()
    }
  }
}
</script>

<style lang="scss" scoped>
.el-card {
  margin-bottom: 20px;
}
</style>

services/course.js 課程接口模塊(新建)

// 分頁查詢課程信息
export const getQueryCourses = data => {
  return request({
    method: 'POST',
    url: '/boss/course/getQueryCourses',
    data
  })
}

至此,準(zhǔn)備工作已完成

上下架功能

上架狀態(tài)展示

使用Element的Switch開關(guān)組件進(jìn)行設(shè)置,這樣可以將狀態(tài)展示和上下架操作結(jié)合為一個(gè)組件,操作更加直觀了
添加到上架狀態(tài)對(duì)應(yīng)的位置(上述代碼中待處理標(biāo)記)

  • 每條數(shù)據(jù)中的status代表上架狀態(tài),上架為1,下架為0
  • 通過v-model結(jié)合作用域插槽獲取數(shù)據(jù)進(jìn)行綁定
    • 由于組件默認(rèn)通過布爾值判斷,需要通過組件拓展的value類型進(jìn)行設(shè)置


上下架操作處理

通過課程上下架接口操作:地址
這里需要注意一點(diǎn):為什么我這里不寫data了?因?yàn)檫@塊是GET請(qǐng)求方式,需要設(shè)置為params屬性,這是axios內(nèi)部的特點(diǎn)

// services/course.js
...

// 課程上下架
export const changeState = params => {
  return request({
    method: 'GET',
    url: '/boss/course/changeState',
    params
  })
}

引入之后,切換開關(guān)時(shí)發(fā)送請(qǐng)求,通過文檔得知,Switch組件具有change事件,進(jìn)行設(shè)置。

  • 默認(rèn)參數(shù)為切換后新的狀態(tài)值,這里我們需要的是要切換的課程信息用于請(qǐng)求操作
<el-switch
    ...
  @change="onStateChange(scope.row)">
</el-switch>
... 
<script>
  ...
    // 上下架按鈕操作
    async onStateChange (course) {
      // 接收操作的課程對(duì)象,并發(fā)送請(qǐng)求更改上下架狀態(tài)
      const { data } = await changeState({
        courseId: course.id,
        status: course.status
      })
      if (data.code === '000000') {
        this.$message.success(`${course.status === 0 ? '下架' : '上架'}成功`)
      }
    }
  }
}
</script>

設(shè)置完畢之后,為了避免用戶在一次上下架未完成時(shí)頻繁點(diǎn)擊,可以進(jìn)行觸發(fā)限制

  • 在請(qǐng)求課程信息后,在每條課程信息對(duì)象添加siStatusLoading屬性
// list.vue
...
// 加載課程(準(zhǔn)備工作中設(shè)置)
async loadCourses () {
  this.isLoading = true
  const { data } = await getQueryCourses(this.filterParams)
  if (data.code === '000000') {
    // 給媒體數(shù)據(jù)設(shè)置屬性,標(biāo)識(shí)狀態(tài)是否處于切換中,默認(rèn) false(本小節(jié)添加的功能)
    data.data.records.forEach(item => {
      item.isStatusLoading = false
    })
    // 保存課程信息
    this.courses = data.data.records
    this.totalCount = data.data.total
    this.isLoading = false
  }
},
...

將屬性綁定Switch組件的disabled屬性,當(dāng)狀態(tài)更改過程中,組件自動(dòng)禁用

// list.vue 
...
<el-switch
  :disabled="scope.row.isStatusLoading"
  ...>
</el-switch>
...

最后呢,在請(qǐng)求操作過程中設(shè)置isStatusLoading屬性值就可以了

// list.vue
...
// 上下架按鈕操作
async onStateChange (course) {
  // 請(qǐng)求發(fā)送前,更改課程操作狀態(tài)
  course.isStatusLoading = true
  ...
  if (data.code === '000000') {
    ...
    // 請(qǐng)求完畢,更改課程操作狀態(tài)
    course.isStatusLoading = false
  }
}
...

添加課程

準(zhǔn)備工作一如既往,course/中創(chuàng)建create.vue組件,并設(shè)置路由與list.vue點(diǎn)擊的跳轉(zhuǎn)操作

// course/create.vue
<template>
  <div class="course-create">
    <el-card>添加課程</el-card>
  </div>
</template>

<script>
export default {
  name: 'CourseCreate'
}
</script>

<style lang="scss" scoped>
</style>
// router/index.js
...
  {
    path: '/course/create',
    name: 'course-create',
    component: () => import(/* webpackChunkName: 'course-create' */ '@/views/course/create.vue')
  }
]
// course/list.vue
...
<el-button
  style="float: right; margin-top: -10px"
  type="primary"
  @click="$router.push({ name: 'course-create' })"
>添加課程</el-button>
...

步驟條設(shè)置

對(duì)于功能比較多的操作,可以通過步驟條的方式引導(dǎo)用戶操作,增強(qiáng)體驗(yàn)。
使用的是Element的Steps步驟條組件進(jìn)行處理,同時(shí)將create.vue的頭部區(qū)域內(nèi)寫入該組件,將active動(dòng)態(tài)綁定,以后在操作中可以更改步驟條的進(jìn)度

// create.vue
<template>
  <div class="course-create">
    <el-card>
      <!-- 設(shè)置 slot 后 Element 會(huì)自動(dòng)設(shè)置為上下兩部分的布局樣式(具有分割線) -->
      <div slot="header">
        <el-steps :active="activeStep" simple>
          <el-step title="基本信息" icon="el-icon-edit"></el-step>
          <el-step title="課程封面" icon="el-icon-upload"></el-step>
          <el-step title="銷售信息" icon="el-icon-picture"></el-step>
          <el-step title="秒殺信息" icon="el-icon-picture"></el-step>
          <el-step title="課程詳情" icon="el-icon-picture"></el-step>
        </el-steps>
      </div>
    </el-card>
  </div>
</template>
...
<script>
...
  data () {
    return {
      // 步驟條進(jìn)度
      activeStep: 0
    }
  }
}
</script>

由于步驟條的每一部分都是非常類似的結(jié)構(gòu),所以我們建議將數(shù)據(jù)保存到data中,結(jié)構(gòu)更改為遍歷創(chuàng)建的方式(這里由于沒有進(jìn)行詳細(xì)的樣式設(shè)計(jì)所以后期需要自行修改)

// create.vue
...
<el-steps :active="activeStep" simple>
  <el-step
    v-for="(item, i) in steps"
    :key="item.id"
    :title="item.title"
    :icon="item.icon"
  ></el-step>
</el-steps>
...
<script>
export default {
  ...
      steps: [
        { id: 1, title: '基本信息', icon: 'el-icon-edit' },
        { id: 2, title: '課程封面', icon: 'el-icon-upload' },
        { id: 3, title: '銷售信息', icon: 'el-icon-picture' },
        { id: 4, title: '秒殺信息', icon: 'el-icon-picture' },
        { id: 5, title: '課程詳情', icon: 'el-icon-picture' }
      ]
...

給不同步驟設(shè)置對(duì)應(yīng)的布局容器

  • 根據(jù)activeStep設(shè)置對(duì)應(yīng)容器的顯示和隱藏
  • 設(shè)置下一步按鈕,點(diǎn)擊后切換功能模塊
  • 操作到最后一步,隱藏下一步按鈕,并且設(shè)置提交按鈕
// create.vue
...
<el-card>
  ...
  <!-- 步驟容器 -->
  <el-form>
    <div v-show="activeStep === 0">
      基本信息
    </div>
    <div v-show="activeStep === 1">
      課程封面
    </div>
    <div v-show="activeStep === 2">
      銷售信息
    </div>
    <div v-show="activeStep === 3">
      秒殺活動(dòng)
    </div>
    <div v-show="activeStep === 4">
      課程詳情
      <!-- 最后步驟中設(shè)置保存按鈕 -->
      <el-form-item>
        <el-button type="primary">保存</el-button>
      </el-form-item>
    </div>
    <!-- 下一步 -->
    <el-form-item v-if="activeStep !== steps.length - 1">
      <el-button @click="activeStep++">下一步</el-button>
    </el-form-item>
  </el-form>
</el-card>

點(diǎn)擊步驟標(biāo)題按鈕,跳轉(zhuǎn)到對(duì)應(yīng)的步驟,并修改鼠標(biāo)樣式

  • 由于組件沒有click事件應(yīng)添加.native設(shè)置原生事件
  • 設(shè)置樣式,修改鼠標(biāo)樣式
// create.vue
...
<el-steps :active="activeStep" simple>
  <el-step 
        ...
    @click.native="activeStep = i"
    ></el-step>
</el-steps>
...

<style lang="scss" scoped>
.el-step {
  cursor: pointer
}
</style>

表單結(jié)構(gòu)搭建

基本信息

完善表單結(jié)構(gòu)(封面是在第二步驟)

// create.vue
...
<div v-show="activeStep === 0">
  <el-form-item label="課程名稱">
    <el-input></el-input>
  </el-form-item>
  <el-form-item label="課程簡介">
    <el-input></el-input>
  </el-form-item>
  <el-form-item label="課程概述">
    <el-input></el-input>
  </el-form-item>
  <el-form-item label="講師姓名">
    <el-input></el-input>
  </el-form-item>
  <el-form-item label="講師簡介">
    <el-input></el-input>
  </el-form-item>
  <el-form-item label="課程排序">
    <!-- 計(jì)數(shù)器組件 -->
    <el-input-number
      label="描述文字"
    ></el-input-number>
  </el-form-item>
</div>
...

課程封面
使用Element的Upload上傳組件完成
根據(jù)文檔所述,我們需要在頁面中設(shè)置:

  • action:提交地址
  • show-file-list:展示文件列表
  • on-success:成功處理函數(shù)
  • before-upload:上傳前的處理函數(shù)
// create.vue
...
<!-- 課程封面 -->
<div v-show="activeStep === 1">
  <el-form-item label="課程封面">
    <el-upload
      class="avatar-uploader"
      action="https://jsonplaceholder.typicode.com/posts/"
      :show-file-list="false"
      :on-success="handleAvatarSuccess"
      :before-upload="beforeAvatarUpload">
      <img v-if="imageUrl" :src="imageUrl" class="avatar">
      <i v-else class="el-icon-plus avatar-uploader-icon"></i>
    </el-upload>
  </el-form-item>
  <!-- 解鎖封面 -->
  <el-form-item label="解鎖封面">
    <el-upload
      class="avatar-uploader"
      action="https://jsonplaceholder.typicode.com/posts/"
      :show-file-list="false"
      :on-success="handleAvatarSuccess"
      :before-upload="beforeAvatarUpload">
      <!-- 顯示預(yù)覽圖片的元素 -->
      <img v-if="imageUrl" :src="imageUrl" class="avatar">
      <i v-else class="el-icon-plus avatar-uploader-icon"></i>
    </el-upload>
  </el-form-item>
</div>
...
<script>
...
data () {
  return {
    ...
    // 本地預(yù)覽圖片地址
    imageUrl: ''
  }
},
methods: {
  // 文件上傳成功時(shí)的鉤子
  handleAvatarSuccess (res, file) {
    // 保存預(yù)覽圖片地址
    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
  }
}
...
<style lang="scss" scoped>
...
.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>

演示效果很不錯(cuò),但是邊框的樣式并未生效
原因在于:

  • 樣式選擇器為.avatar-uopload .el-upload,說明選擇器選取的元素以及不存在與create.vue這個(gè)組件中,而是出于create.vue的子組件<el-upload>
  • 同時(shí),由于當(dāng)前組件設(shè)置了scoped,使得樣式只能作用在當(dāng)前組件中的元素,讓選擇器無法生效
    如果組件沒有設(shè)置scoped的話,就不存在這種問題,但是如果兩種需求都需要的話,可以使用一種叫做深度作用選擇器的東西:

深度選擇器

這個(gè)內(nèi)容可以參考Vue Loader文檔中,深度作用選擇器相關(guān)欄目
如果希望scoped中的某個(gè)選擇器能夠作用得更深,比如影響子組件樣式,就需要使用>>>操作符

  • 這個(gè)寫法不是CSS語法或者預(yù)處理器語法,而是Vue單文件組件中提供的一種語法
  • >>>/deep/::v-deep功能相同,我們推薦使用::deep
  • 官方稱之為深度作用選擇器,也稱之為樣式穿透
// create.vue
<style lang="scss" scoped>
.el-step {
  cursor: pointer
}
// 只有作用于非子組件根元素的選擇器才需要設(shè)置 ::v-deep
::v-deep .avatar-uploader .el-upload {
  border: 1px dashed #d9d9d9;
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
}
::v-deep .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>

銷售信息

使用Element的Input輸入框組件的復(fù)合型輸入框進(jìn)行單位設(shè)置

  • 設(shè)置方式通過組件插槽來設(shè)置前置或者后置的內(nèi)容
    添加完細(xì)節(jié)之后的部分代碼為
// create.vue
...
<!-- 銷售信息 -->
<div v-show="activeStep === 2">
  <el-form-item label="售賣價(jià)格">
    <el-input>
      <template slot="append">元</template>
    </el-input>
  </el-form-item>
  <el-form-item label="商品原價(jià)">
    <el-input>
      <template slot="append">元</template>
    </el-input>
  </el-form-item>
  <el-form-item label="銷量">
    <el-input>
      <template slot="append">單</template>
    </el-input>
  </el-form-item>
  <el-form-item label="活動(dòng)標(biāo)簽">
    <el-input></el-input>
  </el-form-item>
</div>
...

秒殺活動(dòng)

通過開關(guān)控制底部結(jié)構(gòu)展示與否

  • 開關(guān)就通過我們已經(jīng)講解過的Switch組件來設(shè)置
// create.vue
...
<!-- 秒殺活動(dòng) -->
<div v-show="activeStep === 3">
  <!-- 設(shè)置秒殺狀態(tài)開關(guān) -->
  <el-form-item label="限時(shí)秒殺開關(guān)" label-width="120px">
    <el-switch
      v-model="isSeckill"
      active-color="#13ce66"
      inactive-color="#ff4949">
    </el-switch>
  </el-form-item>
...
data () {
  return {
    ...
    // 秒殺狀態(tài)
    isSeckill: false
  }
},

而秒殺底部的內(nèi)容部分通過v-if判斷來實(shí)現(xiàn)

// create.vue
...
<div v-show="activeStep === 3">
  <!-- 設(shè)置秒殺狀態(tài)開關(guān) -->
  <el-form-item label="限時(shí)秒殺開關(guān)" label-width="120px">
    ...
  </el-form-item>
  <template v-if="isSeckill">
        <!-- 其他部分的基礎(chǔ)結(jié)構(gòu) -->
  </template>
</div>
...

細(xì)節(jié)部分不做贅述,都是重復(fù)工作
有一點(diǎn)要提到的是,秒殺的開始和結(jié)束時(shí)間應(yīng)該使用Element組件中的DateTimePicker日期時(shí)間選擇器組件設(shè)置

// create.vue
...
<el-form-item label="開始時(shí)間">
  <!-- <el-input></el-input> -->
  <el-date-picker
    type="datetime"
    placeholder="選擇開始時(shí)間">
  </el-date-picker>
</el-form-item>
<el-form-item label="結(jié)束時(shí)間">
  <!-- <el-input></el-input> -->
  <el-date-picker
    type="datetime"
    placeholder="選擇結(jié)束時(shí)間">
  </el-date-picker>
</el-form-item>
...

課程詳情

課程詳情部分先試用一個(gè)文本域代替一下富文本,最后再進(jìn)行富文本插入的辦法講解

基本數(shù)據(jù)綁定

老規(guī)矩,接口操作

// services/course.js
...
// 保存或者更改課程信息
export const saveOrUpdateCourse = data => {
  return request({
    method: 'POST',
    url: '/boss/course/saveOrUpdateCourse',
    data
  })
}

引入,并且提交時(shí)要提交所有的保存了的數(shù)據(jù)信息,屬性很多,要注意區(qū)分
接口文檔詳細(xì)信息自行參考:接口
其接口的數(shù)據(jù)要添加到data中,無用數(shù)據(jù)可以自行刪除
都是重復(fù)性的工作,不再多做贅述

上傳課程封面

觀察文檔接口,接口中需要的兩個(gè)屬性,courseListImg,courseImgUrl類型均為String,代表的是一個(gè)服務(wù)器的圖片地址,所以說,在選取圖片之后要先上傳到服務(wù)器獲取線上地址,在提交時(shí)將這個(gè)線上地址發(fā)送給接口

// services/course.js
...
// 上傳圖片
export const uploadCourseImage = (data, onUploadProgress) => {
  // 接口要求的請(qǐng)求數(shù)據(jù)類型為:multipart/form-data
  // 所以需要提交 FormData 數(shù)據(jù)對(duì)象
  return request({
    method: 'POST',
    url: '/boss/course/upload',
    data
  })
}

引入到頁面中
要進(jìn)行圖片上傳,有兩種方式:

  • Element的Upload組件支持自動(dòng)上傳,根據(jù)文檔中的Attribute進(jìn)行對(duì)應(yīng)的屬性配置就可以了
    • 通過屬性方式設(shè)置。屬性很多,配置比較繁瑣
    • 由于Element內(nèi)部不是通過Axios發(fā)送請(qǐng)求,所以Token信息還需要單獨(dú)設(shè)置
  • 自定義上傳(推薦)
    • Upload組件提供了http-request屬性用于覆蓋默認(rèn)的上傳行為,用于實(shí)現(xiàn)自定義上傳
      • 設(shè)置處理函數(shù),組件取消自動(dòng)上傳了,同時(shí)將上傳文件的信息通過參數(shù)Option傳入
        • options.file為選擇的文件信息,通過Formdata發(fā)送
// 自定義文件上傳操作
async handleUpload (options) {
  // 創(chuàng)建 FormData 對(duì)象保存數(shù)據(jù)
  const fd = new FormData()
  // 添加數(shù)據(jù)的鍵要根據(jù)接口文檔設(shè)置
  fd.append('file', options.file)
  // 發(fā)送請(qǐng)求
  const { data } = await uploadCourseImage(fd)
  if (data.code === '000000') {
    // 圖片預(yù)覽為組件在 on-success 時(shí)設(shè)置的本地預(yù)覽功能
    // 默認(rèn)檢測 imgUrl, 這里更換為 course中對(duì)應(yīng)地址即可
    // before-upload 用于在上傳文件前進(jìn)行規(guī)則校驗(yàn)(例如文件格式與大小,可自行調(diào)整)
    // data.data.name 為服務(wù)器提供的地址
    this.course.courseListImg = data.data.name
    // 提示
    this.$message.success('上傳成功')
  }
}
<!-- 自定義上傳 -->
<el-upload ... >
  <!-- 圖片預(yù)覽修改為當(dāng)前Upload對(duì)應(yīng)數(shù)據(jù) -->
  <img v-if="course.courseListImg" :src="course.courseListImg" class="avatar">
  <i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>

封裝組件,不止一個(gè)位置需要上傳圖片的功能,所以我們封裝為組件便于使用
引入

// create.vue
...
// 引入圖片上傳組件
import CourseImage from './components/course-image'

export default {
  name: 'CourseCreate',
  components: {
    CourseImage
  },
...

喜聞樂見的子傳父父傳子的操作,所以我們無需再多做贅述



封裝這個(gè)組建之前,可以通過傳值設(shè)置必選數(shù)據(jù)之外,還可以通過傳參增強(qiáng)組件的使用靈活性,這里演示通過傳參定制上傳文件的的大小

// course-image.vue
...
props: {    
    ...
  // 限制上傳大小
  limit: {
    type: Number,
    default: 2
  }
},
...
// 上傳文件之前的鉤子
beforeAvatarUpload (file) {
  ...
  const isLt2M = file.size / 1024 / 1024 < this.limit
  if (!isJPG) {
    this.$message.error('上傳頭像圖片只能是 JPG 格式!')
  }
  if (!isLt2M) {
    this.$message.error(`上傳頭像圖片大小不能超過 ${this.limit}MB!`)
  }
  return isJPG && isLt2M
},
...

傳參時(shí)進(jìn)行不同參數(shù)的定制就可以了

// create.vue
...
<!-- 課程封面圖上傳 -->
<course-image v-model="course.courseListImg" :limit="2"></course-image>
<!-- 解鎖封面圖上傳 -->
<course-image v-model="course.courseImgUrl" :limit="5"></course-image>
...

上傳進(jìn)度

upload組件自帶上傳進(jìn)度功能,Progress進(jìn)度條
將Progress組件設(shè)置到Upload同級(jí),并且調(diào)整尺寸

// course-image.vue
...
<!-- 進(jìn)度條組件 -->
<el-progress 
  type="circle" 
  :percentage="0"
  :width="178"
></el-progress>
<!-- 上傳組件 -->
<el-upload ... >
...

根據(jù)上傳的情況,應(yīng)該顯示兩個(gè)組件之一,通過v-if v-else控制兩個(gè)組件的顯示情況

// course-image.vue
...
<script>
...
data () {
  return {
    ...
    // 保存下載狀態(tài)
    isUploading: false
  }
},
...
async handleUpload (options) {
  // 設(shè)置進(jìn)度信息展示
  this.isUploading = true
  ...
  if (data.code === '000000') {
    ...
    // 關(guān)閉進(jìn)度信息展示
    this.isUploading = false
  }
}
..
</script>
...
<!-- 進(jìn)度條組件 -->
<el-progress
  v-if="isUploading"
  ...
></el-progress>
<!-- 上傳組件 -->
<el-upload
  v-else
  ...
>
...

進(jìn)度條百分比顯示

Upload本身就具有上傳進(jìn)度處理的on-progress屬性,設(shè)置http-request屬性進(jìn)行自定義上傳之后這個(gè)屬性就會(huì)無效化
這個(gè)時(shí)候我們可以通過Axios的請(qǐng)求配置項(xiàng)onUploadProgress進(jìn)行進(jìn)度檢測
onUploadProgress本子就是對(duì)H5的xhr.upload.onprogress的封裝

// services/course.js
...
// 上傳圖片(添加配置項(xiàng)與參數(shù))
export const uploadCourseImage = (data, onUploadProgress) => {
  return request({
    method: 'POST',
    url: '/boss/course/upload',
    data,
    // Axios 將 HTML5 新增的上傳進(jìn)度事件:progress
    onUploadProgress (event) {
        console.log(event.loaded, event.total)
    }
  })
}

將onUploadProgress設(shè)置為參數(shù)

// services/course.js
...
// 上傳圖片(添加配置項(xiàng)與參數(shù))
export const uploadCourseImage = (data, onUploadProgress) => {
  return request({
    method: 'POST',
    url: '/boss/course/upload',
    data,
    // Axios 將 HTML5 新增的上傳進(jìn)度事件:progress
    onUploadProgress
  })
}

請(qǐng)求時(shí)設(shè)置一個(gè)回調(diào)函數(shù),計(jì)算百分比存儲(chǔ)在data中

// course-image.vue
...
data () {
  return {
    ...
    // 保存上傳進(jìn)度百分比
    precentage: 0
  }
},
...
async handleUpload (options) {
  ...
  // 設(shè)置進(jìn)度回調(diào),進(jìn)行百分比計(jì)算
  const { data } = await uploadCourseImage(fd, (event) => {
    this.precentage = Math.floor(event.loaded / event.total * 100)
  })
  ...
}

最后綁定給el-progress組件就好了

// course-image.vue
...
<el-progress
  ...
  :percentage="precentage"
></el-progress>
...

重復(fù)進(jìn)行上傳時(shí)可能會(huì)出現(xiàn)回退現(xiàn)象,我們只需要在完成上傳后清空數(shù)據(jù)就好

// course-image.vue
...
async handleUpload (options) {
  ...
  if (data.code === '000000') {
    ...
    // 上傳成功后,設(shè)置進(jìn)度信息歸零,避免下次上傳出現(xiàn)回退效果
    this.precentage = 0
  }
}
...

給進(jìn)度條設(shè)置status區(qū)分上傳的不同狀態(tài)

// course-image.vue
...
<el-progress
  ...
  :status="precentage === 100 ? 'success' : undefined"
></el-progress>
...

銷售和秒殺 都是簡單的綁定數(shù)據(jù)輸入框傳遞,除了要注意一下秒殺需要一個(gè)開關(guān)來設(shè)置視圖顯示與否,所以不再多做贅述,另外,我們已經(jīng)提到膩的內(nèi)容就是修改需要id,添加不需要id,通過接口傳送數(shù)據(jù)這種事情我們已經(jīng)是熟練的老手了(不)
只需要注意一點(diǎn):
后端接口如果不支持秒殺時(shí)間中的時(shí)分秒,測試的時(shí)候只需要日期就行了,或者設(shè)置type=date改成DatePicker日期選擇器(但是實(shí)際上的項(xiàng)目是都可以選擇的)

富文本編輯器

普通的textarea沒有格式,需要輸入大段文本內(nèi)容時(shí)就非常的不友好,這個(gè)時(shí)候可以通過富文本編輯器來輸入有格式的文本內(nèi)容

  • 使用起來接近日常使用的文檔形式,類似于md,word
  • 本質(zhì)上是插件將輸入內(nèi)容自動(dòng)通過不同標(biāo)簽組織起來,最終生成帶有標(biāo)簽的文本
    常見的有:
  • CKeditor
  • quill
  • wangEditor
  • ueditor
  • tinymce
    我們以wangEditor為例

安裝

npm i wangeditor -S

如若安裝有問題,可以通過npm audit -fix修復(fù),沒出現(xiàn)問題就忽略

使用

根據(jù)wangEditor的文檔操作就行了

import E from "wangeditor";
const editor = new E("#div1");
editor.create();

封裝一下富文本編輯器,作為公共組件,以便復(fù)用

  • 如果要將富文本編輯器換為其他的,可以在組件里直接動(dòng)手,方便直接
// src/components/TextEditor/index.vue --- 公共組件目錄
<template>
  <div ref="editor" class="text-editor"></div>
</template>

<script>
// 引入富文本編輯器
import E from 'wangeditor'
export default {
  name: 'TextEditor',
  // 由于需要進(jìn)行 DOM 操作,使用 mounted 鉤子
  mounted () {
    // 初始化富文本編輯器
    this.initEditor()
  },
  methods: {
    initEditor () {
      // 創(chuàng)建富文本編輯器實(shí)例
      const editor = new E(this.$refs.editor)
      // 初始化富文本編輯器
      editor.create()
    }
  }
}
</script>

<style lang="scss" scoped></style>

引入,綁定數(shù)據(jù)。父組件使用v-model,公共組件接收,經(jīng)典時(shí)尚重復(fù)操作,不再贅述
如果父組件使用時(shí)希望給編輯器設(shè)置初始值,通過方法設(shè)置

  • 測試時(shí),修改父組件的course.courseDescriptionMarkDown 的初始值
// TextEditor/index.vue
...
// 由于需要進(jìn)行 DOM 操作,使用 mounted 鉤子
mounted () {
  // 初始化富文本編輯器
  this.initEditor()
},
methods: {
  initEditor () {
    ...
    // 初始化后設(shè)置內(nèi)容
    editor.txt.html(this.value)
  }
}
...

當(dāng)富文本編輯器輸入完畢之后需要提交,需要將內(nèi)容傳出給父組件,這個(gè)時(shí)候使用編輯器提供的方法操作

  • onChange回調(diào)用于在內(nèi)容改變時(shí)觸發(fā)
  • 回調(diào)必須設(shè)置在editor.create()前,否則編輯器就已經(jīng)創(chuàng)建完畢,設(shè)置無效
  • 通過組件自定義事件傳出給父組件的v-model綁定
// TextEditor/index.vue
...
methods: {
  initEditor () {
    const editor = new E(this.$refs.editor)
    // 設(shè)置回調(diào)
    editor.config.onchange = function (value) {
      // value 為輸入的內(nèi)容,通過自定義事件傳出即可 (注意 this 指向,建議使用箭頭函數(shù))
      this.$emit('input', value)
    }
    editor.create()
    editor.txt.html(this.value)
  }
}

富文本編輯器圖片上傳處理

wangEditor默認(rèn)支持圖片上傳,可以通過“網(wǎng)絡(luò)圖片”選項(xiàng)的輸入線上圖片地址處理
鑒于服務(wù)器響應(yīng)格式有需求,我們自定義上傳
設(shè)置到頁面中觀察,選擇文件后觸發(fā)customUploadImg回調(diào)

  • 參數(shù)1 resultFiles為文件信息所在的數(shù)組,上傳時(shí)取出數(shù)據(jù)發(fā)送就可以了
  • 參數(shù)2 insertImgFn為上傳完畢接收到地址后,根據(jù)圖片地址生成img標(biāo)簽并插入到富文本編輯器時(shí)使用
    引入之前封裝的圖片上傳函數(shù),進(jìn)行處理
// TextEditor/index.vue
...
// 引入文件上傳接口
import { uploadCourseImage } from '@/services/course'
...
initEditor () {
  ...
  // 配置 自定義上傳圖片 功能
  editor.config.customUploadImg = async function (resultFiles, insertImgFn) {
    // 發(fā)送請(qǐng)求(參數(shù)需要 FormData 類型)
    const fd = new FormData()
    fd.append('file', resultFiles[0])
    const { data } = await uploadCourseImage(fd)
    if (data.code === '000000') {
      // 根據(jù)地址創(chuàng)建 img 并插入到富文本編輯器
      insertImgFn(data.data.name)
    }
  }
  ...
}
...

一套測試完成,無BUG

抽離組件

編輯和新增是類似的,可以封裝到create-or-edit.vue組件中
引入組件的時(shí)候,其他地方的組件目錄等級(jí)要記得修改
經(jīng)典時(shí)尚編輯或修改,不再贅述

圖片上傳組件改進(jìn)(如果不需要設(shè)置本地預(yù)覽的話就無需這個(gè)操作)

測試之后發(fā)現(xiàn),課程封面圖無法顯示,需要在course-image中判斷是否傳入了圖片

  • 新增:value為空,imageUrl為空,選擇后imageUrl為預(yù)覽地址
  • 編輯:value為地址,imageUrl為空,選擇后均有值,但是應(yīng)該顯示imageUrl。上述代碼比較復(fù)雜,應(yīng)該使用計(jì)算屬性設(shè)置
// course/components/course-images.vue
...
computed: {
  previewUrl () {
    // 有 imageUrl 優(yōu)先使用,沒有時(shí)使用 value,都沒有返回 undefined
    return this.imageUrl || this.value
  }
},
...
<!-- 替換原來的 imageUrl 即可 -->
<img v-if="previewUrl" :src="previewUrl" class="avatar">

秒殺細(xì)節(jié)改進(jìn)

如果編輯的課程沒有處于秒殺狀態(tài),就響應(yīng)數(shù)據(jù)的activityCourseDTO為null,這個(gè)時(shí)候操作秒殺按鈕就會(huì)報(bào)錯(cuò),要在這里添加檢測,如果不是秒殺狀態(tài),那么就將這個(gè)對(duì)象屬性初始化就可以了

// create-or-edit.vue
...
async loadCourse () {
  const { data } = await getCourseById(this.courseId)
  if (data.code === '000000') {
    // 為非秒殺課程初始化屬性
    if (!data.data.activityCourse) {
      data.data.activityCourseDTO = {
        beginTime: '',
        endTime: '',
        amount: 0,
        stock: 0
      }
    }
    this.course = data.data
  }
},

富文本編輯器組件改進(jìn)

由于編輯請(qǐng)求為異步操作,而富文本編輯器中的DOM功能為同步,所以編輯時(shí)會(huì)出現(xiàn)富文本編輯器顯示默認(rèn)文本的情況,這個(gè)時(shí)候通過watch來偵聽value變化,并進(jìn)行初始化內(nèi)容更新(新增功能不存在這個(gè)問題)

// src/components/TextEditor.vue
...
data () {
  return {
    editor: null,
    // 要編輯的數(shù)據(jù)是否加載完畢
    isLoaded: false
  }
},
watch: {
  value () {
    // 編輯數(shù)據(jù)加載成功后,為富文本編輯器更新初始內(nèi)容即可
    if (!this.isLoaded) {
      this.editor.txt.html(this.value)
      this.isLoaded = true
    }
  }
},
...
initEditor () {
  ...
  // 將富文本編輯器實(shí)例保存給 this 以便在 watch 中操作
  this.editor = editor
}

大功告成!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容