HM后台(五)
一,商品管理的商品列表页面搭建
1.1,商品列表组件路由配置
{ path: '/home', component: Home, // 重定向 redirect: '/welcome', children: [ { path: '/welcome', component: Welcome }, { path: '/users', component: User }, { path: '/rights', component: Rights }, { path: '/roles', component: Roles }, { path: '/categories', component: Cate }, { path: '/params', component: Params }, { path: '/goods', component: List }, ] }
1.2,页面渲染时,发送请求,获取商品列表数据
请求参数
请求参数定义
data() { return { // 查询参数对象 queryInfo: { query: '', pagenum: 1, pagesize: 10 }, // 商品列表 goodslist: [], // 总数据条数 total: 0 }
mounted() { this.getGoodsList() }, methods: { async getGoodsList() { const { data: res } = await this.$http.get('goods', { params: this.queryInfo }) if (res.meta.status !== 200) { return this.$message.error('获取商品列表失败!') } this.$message.success('获取商品列表成功!') console.log(res.data) this.goodslist = res.data.goods this.total = res.data.total } }
数据页面填充
<template> <div> <!-- 面包屑导航区域 --> <el-breadcrumb separator-class="el-icon-arrow-right"> <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item> <el-breadcrumb-item>商品管理</el-breadcrumb-item> <el-breadcrumb-item>商品列表</el-breadcrumb-item> </el-breadcrumb> <!-- 卡片视图区域 --> <el-card style="margin-top:20px"> <!-- 搜索框 --> <el-row :gutter="20"> <el-col :span="8"> <el-input placeholder="请输入内容" v-model="queryInfo.query" clearable > <el-button slot="append" icon="el-icon-search"></el-button> </el-input> </el-col> <el-col :span="4"> <el-button type="primary">添加商品</el-button> </el-col> </el-row> <!-- 商品表格 --> <el-table :data="goodslist" border stripe style="margin:20px 0"> <el-table-column type="index"></el-table-column> <el-table-column label="商品名称" prop="goods_name"></el-table-column> <el-table-column label="商品价格(元)" prop="goods_price" width="95px"></el-table-column> <el-table-column label="商品重量" prop="goods_weight" width="70px"></el-table-column> <el-table-column label="创建时间" prop="add_time" width="140px"></el-table-column> <el-table-column label="操作" width="130px"> <template slot-scope="{row, $index}"> <el-button type="primary" icon="el-icon-edit" size="mini"></el-button> <el-button type="danger" icon="el-icon-delete" size="mini" ></el-button> </template> </el-table-column> </el-table> </el-card> </div> </template>
1.3,此时我们看到创建时间是个时间戳,我们需要将他过滤成标准的日期时间,需要用到vue的filter过滤器
在main.js中全局注册过滤器,originVal接收传过来的时间戳参数(row.add_time),过滤器的名称dateFormat
// 时间过滤器 Vue.filter('dateFormat', function(originVal) { const dt = new Date(originVal) const y = dt.getFullYear() const m = dt.getMonth() + 1 const d = dt.getDate() const hh = dt.getHours() const mm = dt.getMinutes() const ss = dt.getSeconds() return `${y}-${m}-${d} ${hh}:${mm}:${ss}` })
然后在表格日期列,用插值法填充
<!-- 商品表格 --> <el-table :data="goodslist" border stripe style="margin:20px 0"> <el-table-column type="index"></el-table-column> <el-table-column label="商品名称" prop="goods_name"></el-table-column> <el-table-column label="商品价格(元)" prop="goods_price" width="95px"></el-table-column> <el-table-column label="商品重量" prop="goods_weight" width="70px"></el-table-column> <el-table-column label="创建时间" width="140px"> <template slot-scope="{row, $index}"> {{row.add_time | dateFormat}} </template> </el-table-column>
1.4,分页器页面搭建
<!-- 分页区域 --> <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="queryInfo.pagenum" :page-sizes="[5, 10, 15, 20]" :page-size="queryInfo.pagesize" layout="total, sizes, prev, pager, next, jumper" :total="total" background> </el-pagination>
handleSizeChange(newSize) { this.queryInfo.pagesize = newSize this.getGoodsList() }, handleCurrentChange(newPage) { this.queryInfo.pagenum = newPage this.getGoodsList() },
1.5,搜索框搜索关键字,重新发送请求,获取商品数据,点击清空按钮,重新发送请求,获取数据
<!-- 搜索框 --> <el-row :gutter="20"> <el-col :span="8"> <el-input placeholder="请输入内容" v-model="queryInfo.query" clearable @clear="getGoodsList" > <el-button slot="append" icon="el-icon-search" @click="getGoodsList"></el-button> </el-input> </el-col>
1.5,点击删除按钮,删除该商品
<el-table-column label="操作" width="130px"> <template slot-scope="{row, $index}"> <el-button type="primary" icon="el-icon-edit" size="mini"></el-button> <el-button type="danger" icon="el-icon-delete" size="mini" @click="removeById(row.goods_id)" ></el-button> </template> </el-table-column>
// 点击删除按钮 async removeById(id) { const confirmResult = await this.$confirm( '此操作将永久删除该商品, 是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' } ).catch(err => err) if (confirmResult !== 'confirm') { return this.$message.info('已经取消删除!') } const { data: res } = await this.$http.delete(`goods/${id}`) if (res.meta.status !== 200) { return this.$message.error('删除失败!') } this.$message.success('删除成功!') // 重新发送请求,获取新数据 this.getGoodsList() },
1.6.点击添加商品按钮,跳转到add组件,配置下路由对象
<!-- 搜索框 --> <el-row :gutter="20"> <el-col :span="8"> <el-input placeholder="请输入内容" v-model="queryInfo.query" clearable @clear="getGoodsList" > <el-button slot="append" icon="el-icon-search" @click="getGoodsList"></el-button> </el-input> </el-col> <el-col :span="4"> <el-button type="primary" @click="goAddpage">添加商品</el-button> </el-col> </el-row>
// 点击添加商品按钮,跳转到add组件 goAddpage() { this.$router.push('/goods/add') }
路由配置
{ path: '/home', component: Home, // 重定向 redirect: '/welcome', children: [ { path: '/welcome', component: Welcome }, { path: '/users', component: User }, { path: '/rights', component: Rights }, { path: '/roles', component: Roles }, { path: '/categories', component: Cate }, { path: '/params', component: Params }, { path: '/goods', component: List }, { path: '/goods/add', component: Add }, ] }
二,add添加商品组件页面搭建
active是nunber类型
<!-- 面包屑导航区域 --> <el-breadcrumb separator-class="el-icon-arrow-right"> <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item> <el-breadcrumb-item>商品管理</el-breadcrumb-item> <el-breadcrumb-item>添加商品</el-breadcrumb-item> </el-breadcrumb> <!-- 卡片视图 --> <el-card style="margin: 20px 0"> <el-alert title="添加商品信息" type="info" show-icon> </el-alert> <!-- 进度条 --> <el-steps :space="200" :active="0" finish-status="success" align-center style="margin: 15px 0"> <el-step title="基本信息"></el-step> <el-step title="商品参数"></el-step> <el-step title=" 商品属性"></el-step> <el-step title="商品图片"></el-step> <el-step title="商品内容"></el-step> <el-step title="完成"></el-step> </el-steps> </el-card>
2.1,左边选项卡页面
我们需要实现一个功能,数据联动效果,当点击左边选项卡第三项,顶部的进度条就在第三个,如何实现这个效果呢
el-tabs中的v-model记录的是el-tab-pane的name值(string),
而在el-teps中的active记录的是el-step的激活的值(number),0,1,2,3
此时只需要将v-modle中的值和active的值关联起来即可实现联动效果了
在data中动态定义一个属性
data() { return { activeIndex:'0' }
<!-- tabs栏区域 --> <el-tabs tab-position="left" v-model="activeIndex" style="height: 200px;"> <el-tab-pane label="基本信息" name="0">基本信息</el-tab-pane> <el-tab-pane label="商品参数" name="1">商品参数</el-tab-pane> <el-tab-pane label="商品属性" name="2">商品属性</el-tab-pane> <el-tab-pane label="商品图片" name="3">商品图片</el-tab-pane> <el-tab-pane label="商品内容" name="4">商品内容</el-tab-pane> </el-tabs>
我们然后在el-steps改造下,将字符串变成number类型
<!-- 进度条 --> <el-steps :space="200" :active="activeIndex - 0" finish-status="success" align-center style="margin: 15px 0"> <el-step title="基本信息"></el-step> <el-step title="商品参数"></el-step>
2.2,在左边的选项卡的基本信息项,是一个form验证表单,只能将el-form包裹着el-teps, 然后将el-tab-pane包裹着el-form-item
addForm的属性不能乱填,需要根据接口的参数来定
<!-- tabs栏区域 --> <el-form :model="addForm" :rules="addFormRules" ref="addFormRef" label-width="100px" label-position="top" > <el-tabs tab-position="left" v-model="activeIndex" > <el-tab-pane label="基本信息" name="0"> <el-form-item label="商品名称" prop="goods_name"> <el-input v-model="addForm.goods_name"></el-input> </el-form-item> <el-form-item label="商品价格" prop="goods_price"> <el-input v-model="addForm.goods_price" type="number"></el-input> </el-form-item> <el-form-item label="商品重量" prop="goods_weight"> <el-input v-model="addForm.goods_weight" type="number"></el-input> </el-form-item> <el-form-item label="商品数量" prop="goods_number"> <el-input v-model="addForm.goods_number" type="number"></el-input> </el-form-item> <el-form-item label="商品分类" prop="goods_cat"> </el-form-item> </el-tab-pane> <el-tab-pane label="商品参数" name="1">商品参数</el-tab-pane> <el-tab-pane label="商品属性" name="2">商品属性</el-tab-pane> <el-tab-pane label="商品图片" name="3">商品图片</el-tab-pane> <el-tab-pane label="商品内容" name="4">商品内容</el-tab-pane> </el-tabs> </el-form>
在data中定义属性
data() { return { activeIndex: '0', // 添加商品的表单数据对象 addForm: { goods_name: '', goods_price: 0, goods_weight: 0, goods_number: 0, }, addFormRules: { goods_name: [ { required: true, message: '请输入商品名称', trigger: 'blur' } ], goods_price: [ { required: true, message: '请输入商品价格', trigger: 'blur' } ], goods_weight: [ { required: true, message: '请输入商品重量', trigger: 'blur' } ], goods_number: [ { required: true, message: '请输入商品数量', trigger: 'blur' } ], goods_cat: [ { required: true, message: '请选择商品分类', trigger: 'blur' } ] }
2.3,在表单中的商品分类搭建级联选择器组
页面渲染时,发送请求,获取商品分类的数据
mounted() { this.getCateList() }, methods:{ // 获取所有商品分类数据 async getCateList() { const { data: res } = await this.$http.get('categories') if (res.meta.status !== 200) { return this.$message.error('获取商品分类数据失败!') } this.catelist = res.data // console.log(this.catelist) },
返回的数据
{ "data": [ { "cat_id": 1, "cat_name": "大家电", "cat_pid": 0, "cat_level": 0, "cat_deleted": false, "children": [ { "cat_id": 3, "cat_name": "电视", "cat_pid": 1, "cat_level": 1, "cat_deleted": false, "children": [ { "cat_id": 6, "cat_name": "曲面电视", "cat_pid": 3, "cat_level": 2, "cat_deleted": false }, { "cat_id": 7, "cat_name": "海信", "cat_pid": 3, "cat_level": 2, "cat_deleted": false } ] } ] } ], "meta": { "msg": "获取成功", "status": 200 } }
有了数据源,就搭建级联选择器组件了
v-model是选中的父级的id值,数组类型,获取的是cateList中的valueID值
<el-form-item label="商品分类" prop="goods_cat"> <el-cascader :options="catelist" :props="cateProps" v-model="addForm.goods_cat" @change="handleChange"> </el-cascader> </el-form-item> </el-tab-pane>
在data中定义
// 添加商品的表单数据对象 addForm: { goods_name: '', goods_price: 0, goods_weight: 0, goods_number: 0, // 商品所属的分类数组 goods_cat: [] }, // 商品分类列表 catelist: [], // cascader属性配置 cateProps: { label: 'cat_name', value: 'cat_id', children: 'children', expandTrigger:'hover', checkStrictly:true },
级联选择器只有三层,我们需要只选中第三级就可显示即可
// 级联选择器选中项变化,会触发这个函数 handleChange() { console.log(this.addForm.goods_cat) if (this.addForm.goods_cat.length !== 3) { this.addForm.goods_cat = [] } },
2.4,在基本信息tab中,只有商品分类项的三级分类选中了,才可以切换tab
el-tabs定义定义该属性,属性值时函数类型
<!-- tabs栏区域 --> <el-form :model="addForm" :rules="addFormRules" ref="addFormRef" label-width="100px" label-position="top" > <el-tabs tab-position="left" v-model="activeIndex" :before-leave="beforeTabLeave" >
// tabs切换触发 beforeTabLeave(activeName, oldActiveName) { // console.log('即将离开的标签页名字是:'+oldActiveName) // console.log('即将进入的标签页名字是:'+activeName) // return false if (oldActiveName === '0' && this.addForm.goods_cat.length !== '3') { this.$message.success('请选择商品分类') return false } }
2.5,在商品参数中,form中嵌套了checkbox多选框,当切换到商品参数tab,发送请求,获取动态参数数据
三级分类id计算出来,作为请求参数
computed: { cateId() { if (this.addForm.goods_cat.length === 3) { return this.addForm.goods_cat[2] } else { return null } } },
<el-tabs tab-position="left" v-model="activeIndex" :before-leave="beforeTabLeave" @tab-click="tabClicked" >
// tab被选中时触发 async tabClicked() { // console.log(this.activeIndex) // 发送请求,获取动态参数数据 const {data :res} = await this.$http.get(`categories/${this.cateId}/attributes`,{params:{sel:'many'}}) if(res.meta.status !==200){ this.$message.error('获取参数数据失败') } this.manyTableData = res.data console.log(res.data) }
返回的数据
{ "data": [ { "attr_id": 1, "attr_name": "cpu", "cat_id": 22, "attr_sel": "only", "attr_write": "manual", "attr_vals": "ffff" } ], "meta": { "msg": "获取成功", "status": 200 } }
用数据渲染商品参数的页面
此时返回的attr_vals参数值属性是字符串格式,我们需要把它改造成数组形式,用于checkbox的遍历,在返回数据需要整理下
// tab被选中时触发 async tabClicked() { // console.log(this.activeIndex) // 发送请求,获取动态参数数据 const { data: res } = await this.$http.get( `categories/${this.cateId}/attributes`, { params: { sel: 'many' } } ) if (res.meta.status !== 200) { this.$message.error('获取参数数据失败') } // 将attr_vals的字符串改造成数组 res.data.forEach(item => { item.attr_vals = item.attr_vals.length === 0 ? [] : item.attr_vals.split(',') }) this.manyTableData = res.data console.log(res.data) }
<el-tab-pane label="商品参数" name="1"> <el-form-item :label="item.attr_name" v-for="(item, index) in manyTableData" :key="item.attr_id" > <el-checkbox-group v-model="item.attr_vals"> <el-checkbox :label="i" v-for="(i, index1) in item.attr_vals" :key="index1" border></el-checkbox> </el-checkbox-group> </el-form-item> </el-tab-pane>
此时,复选框的间距不是一般齐的,我们需要优化下
.el-checkbox { margin: 0 10px 0 0 !important; }
2.6,商品属性的页面搭建
// tab被选中时触发 async tabClicked() { // console.log(this.activeIndex) // 发送请求,获取动态参数数据 if (this.activeIndex === '1') { const { data: res } = await this.$http.get( `categories/${this.cateId}/attributes`, { params: { sel: 'many' } } ) if (res.meta.status !== 200) { this.$message.error('获取参数数据失败') } // 将attr_vals的字符串改造成数组 res.data.forEach(item => { item.attr_vals = item.attr_vals.length === 0 ? [] : item.attr_vals.split(',') }) this.manyTableData = res.data // console.log(res.data) } else if (this.activeIndex === '2') { const { data: res } = await this.$http.get( `categories/${this.cateId}/attributes`, { params: { sel: 'only' } } ) if (res.meta.status !== 200) { return this.$message.error('获取静态属性失败!') } console.log(res.data) this.onlyTableData = res.data } }
<el-tab-pane label="商品属性" name="2"> <el-form-item :label="item.attr_name" v-for="item in onlyTableData" :key="item.attr_id"> <el-input v-model="item.attr_vals"></el-input> </el-form-item> </el-tab-pane>
2.7,商品图片页面搭建
在data中定义后台要上传图片的地址
// 上传图片的URL地址 uploadURL: 'http://127.0.0.1:8888/api/private/v1/upload',
<el-tab-pane label="商品图片" name="3"> <!-- action 表示图片要上传到的后台API地址 --> <el-upload :action="uploadURL" :on-preview="handlePreview" :on-remove="handleRemove" list-type="picture" > <el-button size="small" type="primary">点击上传</el-button> </el-upload> </el-tab-pane>
此时点击上传图片的按钮,虽然图片在页面上展示了,但是我们查看上传图片的请求,无效的token,后台上传图片的接口没有携带token
这个是我们发送平时请求需要添加的请求拦截器
// 配置请求拦截器,添加token axios.interceptors.request.use(config => { // console.log(config) config.headers.Authorization = window.sessionStorage.getItem('token') return config })
upload有个header属性,可以携带
<!-- action 表示图片要上传到的后台API地址 --> <el-upload :action="uploadURL" :on-preview="handlePreview" :on-remove="handleRemove" list-type="picture" :headers="headerObj"> <el-button size="small" type="primary">点击上传</el-button> </el-upload> </el-tab-pane>
在data中定义
// 图片上传组件的headers请求头对象 headerObj: { Authorization: window.sessionStorage.getItem('token') },
2.8, 上传图片成功时,我们添加商品请求参数有个pics需要收集(临时图片路劲),我们可以调用element的上传图片成功的函数去获取临时图片路劲
<!-- action 表示图片要上传到的后台API地址 --> <el-upload :action="uploadURL" :on-preview="handlePreview" :on-remove="handleRemove" list-type="picture" :headers="headerObj" :on-success="handleSuccess" > <el-button size="small" type="primary">点击上传</el-button> </el-upload>
// 图片上传成功触发 handleSuccess(response) { // console.log(response) // 需要把临时路径加载添加商品请求的参数中addForm的pics属性 const changePics = { pic: response.data.tmp_path } this.addForm.pics.push(changePics) console.log(this.addForm.pics) }
response的数据
在data中定义收集添加商品的请求参数
// 添加商品的表单数据对象 addForm: { goods_name: '', goods_price: 0, goods_weight: 0, goods_number: 0, // 商品所属的分类数组 goods_cat: [], // 图片临时路劲数组 pics: [] },
2.9,点击图片右上角叉号按钮,在this.addForm.pics移除图片的临时路劲
file返回的数据
// 删除图片的函数tmp_path handleRemove(file) { // console.log(file) //移除图片的临时地址 const filePath = file.response.data.tmp_path // 在addForm.pics中找到这个地址,然后清除掉 this.addForm.pics = this.addForm.pics.filter(item => { return item.pic !== filePath }) console.log(this.addForm.pics) },
url地址是服务器存储图片的地址,我们需要将他保存在data中,然后触发dialog弹框,里头包裹img即可
// 点击已上传的图片,预览 handlePreview(file) { console.log(file) this.previewPath = file.response.data.url this.previewVisible = true },
<!-- 图片预览 --> <el-dialog title="图片预览" :visible.sync="previewVisible" width="50%"> <img :src="previewPath" alt="" class="previewImg" /> </el-dialog>
data中定义
previewPath: '', previewVisible: false
.previewImg{ width:100% }
3.0,在商品内容中搭建富文本编辑器页面
安装第三方插件vue-quill-editor,npm install vue-quill-editor --save
在main.js全局注册
// 导入富文本编辑器 import VueQuillEditor from 'vue-quill-editor' // require styles 导入富文本编辑器对应的样式 import 'quill/dist/quill.core.css' import 'quill/dist/quill.snow.css' import 'quill/dist/quill.bubble.css' // 将富文本编辑器,注册为全局可用的组件 Vue.use(VueQuillEditor)
<el-tab-pane label="商品内容" name="4"> <!-- 富文本编辑器组件 --> <quill-editor v-model="addForm.goods_introduce" ></quill-editor> <!-- 添加商品的按钮 --> <el-button type="primary" style="margin-top:15px">添加商品</el-button> </el-tab-pane>
v-model绑定的数据在添加商品请求参数添加
// 添加商品的表单数据对象 addForm: { goods_name: '', goods_price: 0, goods_weight: 0, goods_number: 0, // 商品所属的分类数组 goods_cat: [], // 图片临时路劲数组 pics: [], // 商品的详情描述 goods_introduce: '', },
3.1,点击添加按钮,发送添加商品请求
注:此时addForm.goods_cat收集的是一个数组,而添加商品的请求参数是一个字符串,需要改造下
<el-tab-pane label="商品内容" name="4"> <!-- 富文本编辑器组件 --> <quill-editor v-model="addForm.goods_introduce"></quill-editor> <!-- 添加商品的按钮 --> <el-button type="primary" style="margin-top:15px" @click="add" >添加商品</el-button
此时会报错,因为级联选择器组件的v-modle必须是数组,把它改成字符串是报错的,我们需要深拷贝下,复制一份一样的数据,但他们不相关联
安装lodash, npm i --save lodash
引入,import _ from 'lodash'
此时添加商品的请求参数attrs是一个数组,我们也需要整理下
在data定义
// 添加商品的表单数据对象 addForm: { goods_name: '', goods_price: 0, goods_weight: 0, goods_number: 0, // 商品所属的分类数组 goods_cat: [], // 图片临时路劲数组 pics: [], // 商品的详情描述 goods_introduce: '', attrs: [] },
// 点击添加商品按钮 add() { this.$refs.addFormRef.validate(async valid => { if (!valid) { return this.$message.error('请填写必要的表单项!') } // 执行添加的业务逻辑 // lodash cloneDeep(obj) // 深拷贝 const form = _.cloneDeep(this.addForm) // 数组改造成字符串 form.goods_cat = form.goods_cat.join(',') // 处理动态参数 this.manyTableData.forEach(item => { const newInfo = { attr_id: item.attr_id, attr_value: item.attr_vals.join(' ') } this.addForm.attrs.push(newInfo) }) // 处理静态属性 this.onlyTableData.forEach(item => { const newInfo = { attr_id: item.attr_id, attr_value: item.attr_vals } this.addForm.attrs.push(newInfo) }) form.attrs = this.addForm.attrs console.log(form) // 发起请求添加商品 // 商品的名称,必须是唯一的 const { data: res } = await this.$http.post('goods', form) if (res.meta.status !== 201) { return this.$message.error('添加商品失败!') } this.$message.success('添加商品成功!') this.$router.push('/goods') }) }
注;动态属性的属性值attr_vals是一个数组,需要改造成字符串,而静态参数的attr_vals是一个本身就是字符串