添加表格数据
为按钮添加修改状态的点击事件:
<el-button
type="primary"
icon="el-icon-plus"
style="margin: 10px 0px"
@click="showDialog"
>添加</el-button
>
...
return {
//代表的分页器第几页
page: 1,
//当前页数展示数据条数
limit: 3,
//总共数据条数
total: 0,
//列表展示的数据
list: [],
//对话框显示与隐藏控制的属性
dialogFormVisible: false,
//收集品牌信息:对象身上的属性,不能瞎写,需要看文档的
tmForm: {
tmName: "",
logoUrl: "",
},
//表单验证规则
rules: {
//品牌名称的验证规则
//require:必须要校验字段(前面五角星有关系) message 提示信息 trigger:用户行为设置(事件的设置:blur、change)
tmName: [
{ required: true, message: "请输入品牌名称", trigger: "blur" },
//自定义校验规则
{ validator: validateTmName, trigger: "change" },
],
//品牌的logo验证规则
logoUrl: [{ required: true, message: "请选择品牌的图片" }],
},
};
...
//点击添加品牌的按钮,确保用户在添加新品牌时开始于一个空白的状态。
showDialog() {
//显示对话框
this.dialogFormVisible = true;
//清除数据
this.tmForm = { tmName: "", logoUrl: "" };
},
多级数据的添加
例如要添加三级分类的数据,按钮点击时除了三级分类id和等级外均为空数据:
//添加属性按钮的回调
addAttr() {
//切换table显示与隐藏
this.isShowTable = false;
//清除数据
//收集三级分类的id
this.attrInfo = {
attrName: "", //属性名
attrValueList: [
//属性值,因为属性值可以有多个因此用数组,每一个属性值都是一个对象需要attrId,valueName
],
categoryId: this.category3Id, //三级分类的id
categoryLevel: 3, //因为服务器也需要区分几级id
};
},
通过标识符展示添加数据的页面:
<!-- 添加属性|修改属性的结构 -->
<div v-show="!isShowTable">
<el-form :inline="true" ref="form" label-width="80px" :model="attrInfo">
<el-form-item label="属性名">
<el-input
placeholder="请输入属性名"
v-model="attrInfo.attrName"
></el-input>
</el-form-item>
</el-form>
<el-button
type="primary"
icon="el-icon-plus"
@click="addAttrValue"
:disabled="!attrInfo.attrName"
>添加属性值</el-button
>
<el-button @click="isShowTable = true">取消</el-button>
<el-table
style="width: 100%; margin: 20px 0px"
border
:data="attrInfo.attrValueList"
>
<el-table-column align="center" type="index" label="序号" width="80">
</el-table-column>
<el-table-column width="width" prop="prop" label="属性值名称">
<template slot-scope="{ row, $index }">
<!-- 这里结构需要用到span与input进行来回的切换 -->
<el-input
v-model="row.valueName"
placeholder="请输入属性值名称"
size="mini"
v-if="row.flag"
@blur="toLook(row)"
@keyup.native.enter="toLook(row)"
:ref="$index"
></el-input>
<span
v-else
@click="toEdit(row, $index)"
style="display: block"
>{{ row.valueName }}</span
>
</template>
</el-table-column>
<el-table-column width="width" prop="prop" label="操作">
<template slot-scope="{ row, $index }">
<!-- 气泡确认框 -->
<el-popconfirm :title="`确定删除${row.valueName}?`" @onConfirm="deleteAttrValue($index)">
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
slot="reference"
></el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-button type="primary" @click="addOrUpdateAttr" :disabled="attrInfo.attrValueList.length<1">保存</el-button>
<el-button @click="isShowTable = true">取消</el-button>
</div>
注意细节:
-
:disabled="!attrInfo.attrName"
只有属性名有值的时候,才可以继续添加属性值。 -
为达到
span
与input
进行来回的切换,使用row.flag属性进行控制。 -
@blur
和@keyup.native.enter
是事件监听器,分别在输入框失去焦点和按下回车键时触发toLook
方法,并将row
作为参数传递给toLook
方法。:ref
指令将当前输入框的索引值作为引用,可以在其他地方通过this.$refs[index]
来获取该输入框的实例。//添加属性值回调 addAttrValue() { //向属性值的数组里面添加元素 //attrId:是你相应的属性的id,目前而言我们是添加属性的操作,还没有相应的属性的id,目前而言带给服务器的id为undefined //valueName:相应的属性值的名称 this.attrInfo.attrValueList.push({ attrId: this.attrInfo.id, //对于修改某一个属性的时候,可以在已有的属性值基础之上新增新的属性值(新增属性值的时候,需要把已有的属性的id带上) valueName: "", flag: true, }); //flag属性:给每一个属性值添加一个标记flag,用户切换查看模式与编辑模式,好处,每一个属性值可以控制自己的模式切换 //当前flag属性,响应式数据(数据变化视图跟着变化)这段代码的作用是在数据变化后,将焦点聚焦在最后一个元素上。 this.$nextTick(() => { this.$refs[this.attrInfo.attrValueList.length - 1].focus(); }); }, //失却焦点的事件---切换为查看模式,展示span toLook(row) { // 如果属性值为空不能作为新的属性值,需要给用户提示,让他输入一个其他的属性值 if(row.valueName.trim()==''){ this.$message('请你输入一个正常的属性值'); return; } //新增的属性值不能与已有的属性值重复 let isRepat = this.attrInfo.attrValueList.some(item=>{ //需要将row从数组里面判断的时候去除 //row最新新增的属性值【数组的最后一项元素】 //判断的时候,需要把已有的数组当中新增的这个属性值去除 if(row!==item){ return row.valueName==item.valueName; } }); if(isRepat) return; // row:形参是当前用户添加的最新的属性值 // 当前编辑模式变为查看模式【让input消失,显示span】 row.flag = false; },
修改表格数据
<!--
表格组件
data:表格组件将来需要展示的数据------数组类型
border:是给表格添加边框
column属性
label:显示标题
width:对应列的宽度
align:标题的对齐方式
prop:对应列内容的字段名
注意1:elmentUI当中的table组件,展示的数据是以一列一列进行展示数据
-->
<el-table style="width: 100%" border :data="list">
<el-table-column type="index" label="序号" width="80px" align="center">
</el-table-column>
<el-table-column prop="tmName" label="品牌名称" width="width">
</el-table-column>
<el-table-column prop="logoUrl" label="品牌LOGO" width="width">
<template slot-scope="{ row, $index }">
<img :src="row.logoUrl" alt="" style="width: 100px; height: 100px" />
</template>
</el-table-column>
<el-table-column prop="prop" label="操作" width="width">
<template slot-scope="{ row, $index }">
<el-button
type="warning"
icon="el-icon-edit"
size="mini"
@click="updateTradeMark(row)"
>修改</el-button
>
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
@click="deleteTradeMark(row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
...
//修改某一个品牌
updateTradeMark(row) {
//row:当前用户选中这个品牌信息
//显示对话框
this.dialogFormVisible = true;
//将已有的品牌信息赋值给tmForm进行展示
//将服务器返回品牌的信息,直接赋值给了tmForm进行展示。
//也就是tmForm存储即为服务器返回品牌信息
this.tmForm = { ...row };
},
可以观察到,添加与修改使用了同一个弹出框,即实现这两个功能时都需要this.dialogFormVisible = true;
。
<!--
对话框
标题动态切换:因为修改操作将row赋值给了tmForm,即id值被赋值
:visible.sync:控制对话框显示与隐藏用的
Form 组件提供了表单验证的功能,只需要通过 rules 属性传入约定的验证规则,并将 Form-Item 的 prop 属性设置为需校验的字段名即可
-->
<el-dialog
:title="tmForm.id ? '修改品牌' : '添加品牌'"
:visible.sync="dialogFormVisible"
>
<!-- form表单 :model属性,这个属性的作用是,把表单的数据收集到那个对象的身上 ,将来表单验证,也需要这个属性-->
<el-form style="width: 80%" :model="tmForm" :rules="rules" ref="ruleForm">
<el-form-item label="品牌名称" label-width="100px" prop="tmName">
<el-input autocomplete="off" v-model="tmForm.tmName"></el-input>
</el-form-item>
<el-form-item label="品牌LOGO" label-width="100px" prop="logoUrl">
<!--这里收集数据:不能使用v-model,因为不是表单元素
action:设置图片上传的地址
:on-success:可以检测到图片上传成功,当图片上传成功,会执行一次
:before-upload:可以在上传图片之前,会执行一次
-->
<el-upload
class="avatar-uploader"
action="/dev-api/admin/product/fileUpload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img v-if="tmForm.logoUrl" :src="tmForm.logoUrl" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
<div slot="tip" class="el-upload__tip">
只能上传jpg/png文件,且不超过500kb
</div>
</el-upload>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="addOrUpdateTradeMark"
>确 定</el-button
>
</div>
</el-dialog>
...
//添加按钮(添加品牌|修改品牌)
addOrUpdateTradeMark() {
//当全部验证字段通过,再去书写业务逻辑
this.$refs.ruleForm.validate(async (success) => {
//如果全部字段符合条件
if (success) {
this.dialogFormVisible = false;
//发请求(添加品牌|修改品牌)
let result = await this.$API.trademark.reqAddOrUpdateTradeMark(
this.tmForm
);
if (result.code == 200) {
//弹出信息:添加品牌成功、修改品牌成功
this.$message({
type: "success",
message: this.tmForm.id ? "修改品牌成功" : "添加品牌成功",
});
//添加或者修改品牌成功以后,需要再次获取品牌列表进行展示
//如果添加品牌: 停留在第一页。如果修改品牌:修改品牌应该留在当前页面
this.getPageList(this.tmForm.id ? this.page : 1);
}
} else {
console.log("error submit!!");
return false;
}
});
},
图片上传
//图片上传之前 应用于:before-upload属性
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;
},
//图片上传成功,应用于on-success属性
handleAvatarSuccess(res, file) {
//res:上传成功之后返回前端数据
//file:上传成功之后服务器返回前端数据
//收集品牌图片数据,因为将来需要带给服务器,即添加|修改操作需要将表单数据作为参数
this.tmForm.logoUrl = res.data;
},
照片墙
<el-form-item label="SPU图片">
<!-- 上传图片:action图片上传的地址 list-type: 文件列表的类型
file-list:照片墙需要展示的数据【数组:数组里面的元素务必要有name、url属性】
on-preview:图片预览功能
on-remove:删除图片的时候会触发
-->
<el-upload
action="/dev-api/admin/product/fileUpload"
list-type="picture-card"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove"
:on-success="handlerSuccess"
:file-list="spuImageList"
>
<i class="el-icon-plus"></i>
</el-upload>
<el-dialog :visible.sync="dialogVisible">
<img width="100%" :src="dialogImageUrl" alt="" />
</el-dialog>
</el-form-item>
//收集SPU图片的信息
spuImageList: [
// {
// id: 0,
// imgName: "string",
// imgUrl: "string",
// spuId: 0,
// },
],
//照片墙图片预览的回调
handlePictureCardPreview(file) {
//将图片地址赋值给这个属性
this.dialogImageUrl = file.url;
//对话框显示
this.dialogVisible = true;
},
//照片墙删除某一个图片的时候会触发
handleRemove(file, fileList) {
//file参数:代表的是删除的那个张图片
//fileList:照片墙删除某一张图片以后,剩余的其他的图片
// console.log(file, fileList,22222);
//收集照片墙图片的数据
//对于已有的图片【照片墙中显示的图片:有name、url字段的】,因为照片墙显示数据务必要有这两个属性
//对于服务器而言,不需要name、url字段,将来对于有的图片的数据在提交给服务器的时候,需要处理的
this.spuImageList = fileList;
},
//照片墙图片上传成功的回调
handlerSuccess(response, file, fileList) {
//收集图片的信息
this.spuImageList = fileList;
},
多级数据的修改
<el-table-column prop="prop" label="操作" width="150">
<template slot-scope="{ row, $index }">
<el-button
type="warning"
icon="el-icon-edit"
size="mini"
@click="updateAttr(row)"
></el-button>
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
></el-button>
</template>
</el-table-column>
...
<span
v-else
@click="toEdit(row, $index)"
style="display: block"
>{{ row.valueName }}</span
>
//修改某一个属性
updateAttr(row) {
//isShowTable变为false
this.isShowTable = false;
//将选中的属性赋值给attrInfo
//由于数据结构当中存在对象里面套数组,数组里面有套对象,因此需要使用深拷贝解决这类问题
//深拷贝,浅拷贝在面试的时候出现频率很高,切记达到手写深拷贝与浅拷贝
this.attrInfo = cloneDeep(row);
//在修改某一个属性的时候,将相应的属性值元素添加上flag这个标记
this.attrInfo.attrValueList.forEach((item) => {
//这样书写也可以给属性值添加flag自动,但是会发现视图不会跟着变化(因为flag不是响应式数据)
//因为Vue无法探测普通的新增 property,这样书写的属性并非响应式属性(数据变化视图跟这边)
//第一个参数:对象 第二个参数:添加新的响应式属性 第三参数:新的属性的属性值
this.$set(item, "flag", false);
});
},
...
//点击span的回调,变为编辑模式
toEdit(row, index) {
row.flag = true;
//获取input节点,实现自动聚焦
//需要注意:点击span的时候,切换为input变为编辑模式,但是需要注意,对于浏览器而言,页面重绘与重拍耗时间的
//点击span的时候,重绘重排一个input它是需要耗费事件,因此我们不可能一点击span立马获取到input
//$nextTick,当节点渲染完毕了,会执行一次
this.$nextTick(() => {
//获取相应的input表单元素实现聚焦
this.$refs[index].focus();
});
},
//保存按钮:进行添加属性或者修改属性的操作
async addOrUpdateAttr(){
//整理参数:1,如果用户添加很多属性值,且属性值为空的不应该提交给服务器
//提交给服务器数据当中不应该出现flag字段
this.attrInfo.attrValueList = this.attrInfo.attrValueList.filter(item=>{
//过滤掉属性值不是空的
if(item.valueName!=''){
//删除掉flag属性
delete item.flag;
return true;
}
})
try {
//发请求
await this.$API.attr.reqAddOrUpdateAttr(this.attrInfo);
//展示平台属性的信号量进行切换
this.isShowTable = true;
//提示消失
this.$message({type:'success',message:'保存成功'});
//保存成功以后需要再次获取平台属性进行展示
this.getAttrList();
} catch (error) {
// this.$message('保存失败')
}
}
注意细节:
- 对象与数组嵌套时,考虑深拷贝。
- 添加响应式数据需要用到
this.$set()
- 元素切换时,重绘重排需要时间,所以需要考虑节点渲染完毕。
删除表格数据
//删除品牌的操作
deleteTradeMark(row) {
//弹框
this.$confirm(`你确定删除${row.tmName}?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
//当用户点击确定按钮的时候会出发
//向服务器发请求,此处没有使用vuex发起请求,注意是异步函数
let result = await this.$API.trademark.reqDeleteTradeMark(row.id);
//如果删除成功
if (result.code == 200) {
this.$message({
type: "success",
message: "删除成功!",
});
//再次获取品牌列表数据
this.getPageList(this.list.length>1?this.page:this.page-1);
}
})
.catch(() => {
//当用户点击取消按钮的时候会触发
this.$message({
type: "info",
message: "已取消删除",
});
});
},
注意细节:this.getPageList(this.list.length>1?this.page:this.page-1)
;这是因为getPageList
函数参数默认为page=1
,考虑以下情况:
- 删除了当前页面非最后一条数据,此时
page
为当前页面的page
,请求数据后仍然显示当前页面的所有数据。 - 删除了当前页面最后一条数据,此时
page
为当前页面的page-1
,请求数据后显示上一页的数据,即回到上一页。
分页器
<!--
分页器
当前第几页、数据总条数、每一页展示条数、连续页码数
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
current-page:代表的是当前第几页
total:代表分页器一共需要展示数据条数
page-size:代表的是每一页需要展示多少条数据
page-sizes:代表可以设置每一页展示多少条数据
layout:可以实现分页器布局设置分页组件的布局,包括上一页按钮、页码、下一页按钮、跳转输入框、每页显示数量选择框和总记录数的显示。
pager-count:按钮的数量 如果 9 连续页码是7
-->
<el-pagination
style="margin-top: 20px; text-align: center"
:current-page="page"
:total="total"
:page-size="limit"
:pager-count="7"
:page-sizes="[3, 5, 10]"
@current-change="getPageList"
@size-change="handleSizeChange"
layout="prev, pager, next, jumper,->,sizes,total"
>
</el-pagination>
...
//获取品牌列表的数据
async getPageList(pager = 1) {
//pager默认值为1,每次点击页码都会根据页码而变化
this.page = pager;
//解构出参数
const { page, limit } = this;
//获取品牌列表的接口
//当你向服务器发请求的时候,这个函数需要带参数,因此在data当中初始化两个字段,代表给服务器传递参数
let result = await this.$API.trademark.reqTradeMarkList(page, limit);
if (result.code == 200) {
//分别是展示数据总条数与列表展示的数据
this.total = result.data.total;
this.list = result.data.records;
}
},
//当分页器某一页需要展示数据条数发生变化的时候会触发
handleSizeChange(limit) {
//整理参数
this.limit = limit;
this.getPageList();
},
三级分类联动
注册全局组件:
src\components\CategorySelect\index.vue
<template>
<div>
<!-- inline:代表的是行内表单,代表一行可以放置多个表单元素 -->
<el-form :inline="true" class="demo-form-inline" :model="cForm">
<el-form-item label="一级分类">
<el-select
placeholder="请选择"
v-model="cForm.category1Id"
@change="handler1"
:disabled="show"
>
<el-option
:label="c1.name"
:value="c1.id"
v-for="(c1, index) in list1"
:key="c1.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="二级分类">
<el-select
placeholder="请选择"
v-model="cForm.category2Id"
@change="handler2"
:disabled="show"
>
<el-option
:label="c2.name"
:value="c2.id"
v-for="(c2, index) in list2"
:key="c2.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="三级分类">
<el-select
placeholder="请选择"
v-model="cForm.category3Id"
@change="handler3"
:disabled="show"
>
<el-option
:label="c3.name"
:value="c3.id"
v-for="(c3, index) in list3"
:key="c3.id"
></el-option>
</el-select>
</el-form-item>
</el-form>
</div>
</template>
-
获取一级分类数据:不需要携带参数
//获取一级分类数据的方法 async getCategory1List() { //获取一级分类的请求:不需要携带参数 let result = await this.$API.attr.reqCategory1List(); if (result.code == 200) { this.list1 = result.data; } },
-
获取一级分类数据后,再根据一级分类的id获取二级分类。
注意在发起请求前要清空数据,否则二级分类和一级分类的对应关系会发生混乱。
并触发一个自定义事件,传递参数为分类id以及分类的级别。同理三级分类的获取也是如此。
//一级分类的select事件回调(当一级分类的option发生变化的时候获取相应二级分类的数据) async handler1() { //清除数据 this.list2 = []; this.list3 = []; this.cForm.category2Id = ""; this.cForm.category3Id = ""; //解构出一级分类的id const { category1Id } = this.cForm; this.$emit("getCategoryId", { categoryId: category1Id, level: 1 }); //通过一级分类的id,获取二级分类的数据 let result = await this.$API.attr.reqCategory2List(category1Id); if (result.code == 200) { this.list2 = result.data; } }, //二级分类的select事件回调(当二级分类的option发生变化的时候获取相应三级分类的数据) async handler2() { //清除数据 this.list3 = []; this.cForm.category3Id = ""; //结构出数据 const { category2Id } = this.cForm; this.$emit("getCategoryId", { categoryId: category2Id, level: 2 }); let result = await this.$API.attr.reqCategory3List(category2Id); if (result.code == 200) { this.list3 = result.data; } }, //三级分类的事件回调 handler3() { //获取三级分类的id const { category3Id } = this.cForm; this.$emit("getCategoryId", { categoryId: category3Id, level: 3 }); },
src\views\product\Attr\index.vue
<el-card style="margin: 20px 0px">
<CategorySelect @getCategoryId="getCategoryId" :show="!isShowTable"></CategorySelect>
</el-card>
...
//自定义事件的回调
getCategoryId({ categoryId, level }) {
//区分三级分类相应的id,以及父组件进行存储
if (level == 1) {
this.category1Id = categoryId;
this.category2Id = "";
this.category3Id = "";
} else if (level == 2) {
this.category2Id = categoryId;
this.category3Id = "";
} else {
//代表三级分类的id有了
this.category3Id = categoryId;
//发请求获取平台的属性数据
this.getAttrList();
}
},
//获取平台属性的数据
//当用户确定三级分类的数据时候,可以向服务器发请求获取平台属性进行展示
async getAttrList() {
//获取分类的ID
const { category1Id, category2Id, category3Id } = this;
//获取属性列表的数据
let result = await this.$API.attr.reqAttrList(
category1Id,
category2Id,
category3Id
);
if (result.code == 200) {
this.attrList = result.data;
}
},
表格展示
<div v-show="isShowTable">
<el-button
type="primary"
icon="el-icon-plus"
:disabled="!category3Id"
@click="addAttr"
>添加属性</el-button
>
<!-- 表格:展示平台属性 -->
<el-table style="width: 100%" border :data="attrList">
<el-table-column type="index" label="序号" width="80" align="center">
</el-table-column>
<el-table-column prop="attrName" label="属性名称" width="150">
</el-table-column>
<el-table-column prop="prop" label="属性值列表" width="width">
<template slot-scope="{ row, $index }">
<el-tag
type="success"
v-for="(attrValue, index) in row.attrValueList"
:key="attrValue.id"
style="margin: 0px 20px"
>{{ attrValue.valueName }}</el-tag
>
</template>
</el-table-column>
<el-table-column prop="prop" label="操作" width="150">
<template slot-scope="{ row, $index }">
<el-button
type="warning"
icon="el-icon-edit"
size="mini"
@click="updateAttr(row)"
></el-button>
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
></el-button>
</template>
</el-table-column>
</el-table>
</div>
isShowTable
属性控制表格的显示与否,即在添加|修改属性时需要隐藏,此时三种级别的分类也进入不可修改的状态。- 每条属性可能有多条属性值,即在
el-tag
标签中再次循环。