背景
项目大部分的页面布局都一样,考虑封装一个共用的布局组件,差异使用插槽的形式去做调整,使项目成员专注于当前页面的逻辑
有反对使用json去封装的文章,可参考对比下juejin.cn/post/704357…
解决的问题:
- 提高开发效率,减少html的编写
- 公共逻辑抽离
- 交互及样式的统一 优势:
- 原本比较多的html模板更改为json的形式去做渲染
- 页面的样式能够做到统一
- 基本继承了elementUI的属性,可直接参考elementUi的表格Api去处理 劣势:
- 需要写大量的json,但是基本做了默认配置,只需要针对自己的要求做一些小改动或增加属性
- 由于高度封装,修改问题的话可能会比较难定位
- 代码维护不易,成员需要看这个Api去开发,增肌初次接触成员的学习成本
实现思路
- 关于配置属性
- 在组件内部定义一套全的属性,再由调用组件传需要修改的属性值,减少使用者json的配置量
- 使用$attr和v-bind,将属性带给组件,在表格上加v-bind表格的attr对象,那原先我们需要给el-table传的属性就可以直接写在attr对象中,使表格组件的属性得到继承
- 关于事件
- 利用$listener去劫持传给page的事件,再由el-table去承接,这样你在el-table上的事件就能直接被调用组件接受,但是别和自定义的事件重名
- 关于插槽
- 插槽的作用更重要的是组件的灵活性,对于本组件不符合预期的,可以通过插槽的形式去自定义
- 插槽的传递:在page定义了很多的插槽,主要是通过插槽的名称去收集对应的插槽要传递给谁,在page的逻辑里面通过$slot拿到全量的插槽,通过插槽的名称针对table和search的插槽进行了区分,再讲对应的插槽数组传递给表格组件和search组件
- 插槽作用域,怎么将插槽作用域一级一级传递出来,使用的是v-bind,实际我们知道我们给插槽赋值的属性,最终都会以属性的方式传出来,对于中间组件,将插槽作用域scope的所有属性绑定在插槽上即可
- 关于灵活性
- 我将搜索组件的每一个项的实例,最终赋值给了传进去的这个searchOption的refs对象,这样调用者能通过prop属性,去获取到对应的渲染组件,也能直接取调用里面的方法,但是调用链略长,本来想放在pageOption上的,但是考虑了单独引用搜索组件的情况,还是放在了searchOption上
- 利用对象的传递性,将对象作为参数传出,在由调用者自定义的方法去操作对象
format: function (value, prop, searchValue, current) {
// 默认是将change的value赋值给submitObj对应的key值,可自行修改赋值逻辑,顺便把当前的组件this也传进去,有其他操作自己玩
searchValue[prop] = value;
}
...
valueChange() {
// 调用赋值方法,可自定义逻辑
this.option.format(this.value, this.option.prop, this.searchValue, this);
this.$emit("valueChange", { prop: this.option.prop, value: this.value });
if (this.option.immediate) {
// 如果单项是立即查询的,并且整个搜索组件不是立即查询的,name调用父级的搜索
if (!this.$parent.selfOption.immediate) {
this.$emit("searchFn");
}
}
},
每次赋值实际都是将绑定的值,传给绑定的searchValue对象,所以定义了formate属性,每次值发生变化的时候都会触发,这样可以结合searchOption的refs对象去进行搜索项之间的联动
- 利用回调函数,做一些赋值,这里存在了双下拉的组件,第一个下拉的值发生变化后会触发typeValueChange方法,需要在配置项定义getDicDataByType方法去根据第一个下拉获取第二个下拉的项,创建了一个done方法作为回调函数,获取到第二个下拉项后进行数据处理再通过done方法将值传给option的dicData。其实原本这里我是想通过url传接口地址的形式去获取数据的,后面考虑到后台接口数据的格式不稳定性,还是决定这个获取数据的逻辑还是交由调用者自己去实现
整体页面布局
布局及插槽使用
图一
整个页面由搜索,右上角全局按钮,操作栏,表格,分页构成,各个模块由pageOption中的domOption各个属性控制,不传domOption则默认全部展示,需要隐藏最上面一栏需要设置showSearch和showFixedTopBtn为false才会生效
代码块1
<page :page="pageOption">
</page>
pageOption:{
domOption: {
showSearch: true,// 是否展示搜索组件 默认为true
showFixedTopBtn: true, // 是否展示右上角的操作按钮 导出设置什么的 默认为true
showTable: true, // 是否展示表格 默认为true
showOperateBtnContent: true, // 是否展示表格上方操作栏 默认为true
showPagination: true, // 是否展示分页
}
}
图一中每个圈出的地方均可使用插槽渲染,最终表格的高度会被其他内容挤压,计算出剩余高度赋给表格,对应的插槽名称如下:
<page>
<div slot="searchContent">这是搜索插槽</div>
<div slot="fixedTopBtnContent">这是右上方按钮插槽</div>
<div slot="operateBtnContent">表格上方整个操作栏插槽</div>
<div slot="btnContentLeft">表格上方操作栏左边内容</div>
<div slot="btnContentRight">表格上方操作栏右边内容</div>
<div slot="tableContent">表格内容插槽</div>
</page>
page组件方法
resetFn()
当筛选项存在插槽时,内置的重置方法无法修改插槽内容,必需绑定resetFn方法修改插槽内容
searchValue:绑定的筛选值对象
done:回调方法,当你处理完自己的插槽内容时,调用done方法触发组件内部的查询方法
resetFn({ searchValue, done }) {
...
done();
},
load()
加载表格数据,参数:{currentPage,pageSize, searchValue}
currentPage:当前页;pageSize:分页大小;searchValue筛选对象
load({ currentPage, pageSize, searchValue }) {
console.log("@@@@", searchValue);
console.log(currentPage, pageSize);
this.getTableData();
},
searchItemChange()
每一个筛选项值发生变化时触发
searchItemChange(item) {
console.log(item);
},
pageOption属性值:
domOption:
布局组件显示的dom元素,具体属性控制如代码块1所示
paginationOption:
分页控制器配置,需要配一个total属性用于记录列表总条数
...
load(){
...
this.pageOption.paginationOption.total = 100;
}
其他属性不传则按默认配置渲染,默认配置如下
paginationOption: {
background: true,
layout: "total, sizes, prev, pager, next, jumper",
pageSizes: [10, 20, 30, 50],
attrs: {
autoScroll: true,
hidden: false,
},
},
分页方法调用
每次触发currentPage变化 或者 pageSize变化时,会调用页面的load方法,带有参数为{currentPage,pageSize,submitObj}
<page @load="load">
</page>
...
// currentPage:当前页,pageSize每页大小,searchValue搜索组件传出来的对象
load({currentPage,pageSize,searchValue}){
...
}
tableOption:
tableOption: {
columns: [
{
prop: "name",
label: "姓名",
width:'80px'
},
{
prop: "status",
label: "状态",
isSlot: true,
},
{
prop: "old",
label: "年龄",
},
{
label: "操作",
isSlot: true,
customerSlotName: "operateCloum",
},
],
dataList: [],
attrs: {
// 覆盖element-table的属性
border: true,
},
},
列表数据
在load方法中调用获取表格数据,赋值给tableOption的dataList数组
列表属性
attrs: {
height: "100%"
},
列表的属性继承了<el-table>的属性,默认只赋值高度100%,其他属性如border等需自己设置
其中特殊处理的只有width和height两个属性,其作用在表格的容器上,实际表格的宽高仍是100%
对于行合并以及列合并等方法绑定依旧写在attrs的span-method属性上,绑定一个方法进行操作
列表方法
原先写在<el-table>上的方法,现在放在<page>组件上,以列表cell-click为例
<page @cell-click="cellClick">
</page>
...
cellClick(row, column, cell, event) {
console.log("!!!", row);
},
columns 列数据
特别注意
这里的列插槽没有做dom递归处理,所以存在无法渲染多级表头的情况,暂时先使用tableContent插槽自己写一个表格
属性
继承了element的列属性,如果需要可以直接在columns数组子项中加入对应的属性,例如给姓名这一列加上列宽,width:"80px"
{
label: "操作", // 列名
prop:"operate", // 对应表格的数据
isSlot: true, // 当前列是否插槽渲染
customerSlotName: "operateCloum", // 是否自定义插槽名称
},
基础的属性包括了:label列的名称;prop该列对应的表格行数据的key值
插槽
设置了isSlot为true的则进行插槽渲染
<page>
<div slot="status" slot-scope="scope">
{{ statusList[scope.row.status] }}
</div>
<div slot="operateCloum" slot-scope="scope">
<el-button v-if="scope.row.status !== 0" @click="deleteFn(scope)"
>删除</el-button>
</div>
</page>
上面定义了两个列插槽,第一个是状态,第二个是操作,由于状态在columns中未定义自定义插槽名称,所以使用prop作为插槽名称,而操作列有定义自定义插槽名称则使用自定义插槽名称作为插槽名称
插槽作用域
与<el-table-cloumn>的插槽作用域一致
searchOption:
基础属性
默认值,需要修改时请对其进行覆盖
firstSearch: true, // 在渲染完成默认值赋值后 调用查询方法
immediate: false, // 是否每一筛选项发生变化时立即查询
submitText: '查询', // 查询按钮文本
resetTetx: '重置', // 重置按钮文本
showItems: [], // 展示的筛选项
hiddenItems: [], // 隐藏的筛选项
refs: {}, // 不需要设置,默认会在hfSearch组件渲染完成之后,以prop作为key值,以每一个搜索项的实例作为value赋值
firstSearch
如果页面存在一些特殊逻辑,不需要组件自己第一次调用查询方法需设置为false,再通过ref去调用page组件的search方法来调用load方法进行表格数据获取
refs和插槽
在渲染完成后,会将每一个筛选项实例存储在refs中,以prop作为key值,以每一个搜索项的实例作为value
所以!调用searchOption.refs的时候必须确保组件已经渲染完成,建议放在mounted中去做处理
插槽名称默认以prop+Search的形式,加入存在自定义插槽名称则以自定义插槽名称作为插槽名称
<page>
<el-input
slot-scope="scope"
slot="yearSearch"
placeholder="这是插槽,绑定year"
v-model="scope.searchValue.year"
/>
</page>
筛选项实例方法
setValue()
更改筛选项绑定值,会触发值变化方法
setOption()
更改配置项,常用于下拉项赋值,下拉组合型首下拉项赋值
mounted() {
this.pageOption.searchOption.refs.old.setOption("dicData", [
{
label: "19岁",
value: 19,
},
{
label: "20岁",
value: 20,
},
{
label: "21岁",
value: 21,
},
{
label: "22岁",
value: 22,
},
]);
setTimeout(() => {
this.pageOption.searchOption.refs.dataTypeSelect.setOption(
"typeDicData",
[
{
label: "SKU1",
value: "sku1",
},
{
label: "品名2",
value: "pinming2",
},
]
);
}, 500);
}
筛选项属性
showItems: [
{
type: "input",
placeholder: "", // 提示
label: "名字", // 搜索项名称
prop: "name",
defaultValue: "江穆",
},
{
type: "select",
placeholder: "请选择年龄",
prop: "old",
label: "年龄",
multiple: true,
defaultValue: [19, 20],
dicData: [],
},
{
type: "year",
prop: "creatTime",
placeholder: "请选择预算年份",
label: "预算年份",
format: this.dataTimeformat,
},
{
type: "selectInput",
prop: "dataType",
typeDicData: [
{
label: "SKU",
value: "sku",
},
{
label: "品名",
value: "pinming",
},
],
},
{
type: "selectGroup",
prop: "dataTypeSelect",
multiple: true,
getDicDataByType: this.getDicDataByType,
},
]
isSlot
是否插槽渲染 默认值false
customerSlotName
自定义插槽名称 isSlot为true有效
type
非插槽必填
类型值: input输入;select下拉单选多选;dateRange日期;year年; yearMonth年月;selectInput下拉加输入; selectGroup双下拉
prop
必填
将作为searchValue的key值,搜索项的值作为value
以及作为refs对应实例的key值
label
搜索项名称
defaultValue
默认值,重置时会恢复成默认值 对应的默认值类型为
input: "",
select: "", // 多选时为数组
dateRange: [],
month: "",
year: "",
yearMonth: "",
radio: "",
selectGroup: {
typeValue:'',
value:''// 多选时为数组
},
selectInput: {
typeValue:'',
value:''
}
placeholder
默认会根据label结合type去生成默认的提示语,可覆盖
clearable
是否可清除,默认值true
width
自定义宽度
immediate
该项是否值发生变化立即触发查询,默认为false 慎改
特别注意,如果该项带有默认值,第一次赋值时会触发查询 联动时 如果B的修改会触发A的值变化,那么当A的immediate设置为true时,也会触发查询
dicData
type为select 或者为selectGroup时,该属性有效
下拉项的数据
dicProps
type为select 或者为selectGroup时,该属性有效
下拉项数据渲染格式,默认下拉项以label和value进行展示和值绑定,可以更改对应的key值 默认值
{
// 下拉渲染的key value值
label: "label",
value: "value"
}
multiple
多选
type为select 或者为selectGroup时,该属性有效,默认值为false
filterable
是否支持选项过滤 默认为 type为select 或者为selectGroup时,该属性有效,默认值为false
typeDicData
type为selectInput或者selectGroup有效,指下拉组合的首下拉项数据,默认会以第一项的value值进行绑定
typeDicProps
type为selectInput或者selectGroup有效,指下拉组合的首下拉项数据渲染格式,默认下拉项以label和value进行展示和值绑定,可以更改对应的key值 默认值
{
// 组合类型的,下拉渲染的key value值
label: "label",
value: "value"
}
dontDealTypeToKey
type为selectInput或者selectGroup有效,默认值为false 默认将组合类型的第一个下拉value值作为searchValue的key值,第二项的值作为value传出去,设置为true时,当前项将会以prop作为key值,value值为
prop:{
typeValue:'xxx',
value:xxx
}
format
值发生变化时触发的方法,默认时以当前的value值赋值给searchValue对应的key为prop的值
format: function (value, prop, searchValue, current) {
// 默认是将change的value赋值给submitObj对应的key值,可自行修改赋值逻辑,顺便把当前的组件this也传进去,有其他操作自己玩
searchValue[prop] = value;
}
当传一个方法进来时,该方法会接受四个参数,
value当前组件的value值,value类型参考defaultValue示例
prop当前筛选项的prop 用于操作searchValue用
searchValue 查询时传递出来的对象
current 当前组件的实例
!注意
当利用format进行筛选项联动时,例如A的值发生变化会触发B的值改变时,不要searchValue[B]=xxx,不会触发组件的更新的,应该利用searchOption.refs.B.setValue(xxx)去触发B筛选项的值更新
getDicDataByType
类型为selectGroup有效且必填,该属性是一个方法,用于首下拉值改变时联动获取第二个下拉框的选项数组,没有默认值
该方法会传三个参数
type:第一个下拉选择的值
done:回调方法,接收一个数组,会将接收到的数组赋值给dicData属性
typeObj:第一个下拉选中值对应的对象,用于一些特殊场景的处理,可不接收处理
...
{
type: "selectGroup",
prop: "dataTypeSelect",
multiple: true,
typeDicData:[
{
label: "SKU1",
value: "sku1",
},
{
label: "品名2",
value: "pinming2",
}
],
getDicDataByType: this.getDicDataByType,
},
...
getDicDataByType(type, done, typeObj) {
let obj = {
sku1: [
{
label: "sku1",
value: 1,
},
{
label: "sku2",
value: 2,
},
],
pinming2: [
{
label: "品名1",
value: 11,
},
],
};
setTimeout(() => {
done(obj[type]);
}, 2000);
},
代码
示例代码
<template>
<div class="test-components">
<page
:pageOption="pageOption"
@cell-click="cellClick"
@resetFn="resetFn"
@searchItemChange="searchItemChange"
@load="load"
>
<!-- <div slot="operateBtnContent">
<el-button @click="getTableData">获取表格数据</el-button>
</div> -->
<div slot="btnContentLeft">
<el-button @click="getTableData">新增</el-button>
</div>
<div slot="btnContentRight">
<el-button>删除</el-button>
</div>
<div slot="fixedTopBtnContent">
<el-button>导出</el-button>
</div>
<div slot="status" slot-scope="scope">
{{ statusList[scope.row.status] }}
</div>
<!-- <div style="background: red;width:100%;height:100%" slot="tableContent"></div> -->
<!-- 搜索插槽 -->
<el-input
slot-scope="scope"
slot="yearSearch"
placeholder="这是插槽,绑定year"
v-model="scope.searchValue.year"
/>
<div slot="operateCloum" slot-scope="scope">
<el-button v-if="scope.row.status !== 0" @click="deleteFn(scope)"
>删除</el-button
>
</div>
</page>
</div>
</template>
<script>
import page from "@/components/pageInit/index.vue";
export default {
components: { page },
data() {
return {
statusList: ["实习", "试用", "正式"],
pageOption: {
tableOption: {
columns: [
{
prop: "name",
label: "姓名",
},
{
prop: "status",
label: "状态",
isSlot: true,
},
{
prop: "old",
label: "年龄",
},
{
label: "操作",
isSlot: true,
customerSlotName: "operateCloum",
},
],
dataList: [],
attrs: {
// 覆盖element-table的属性
border: true,
},
},
searchOption: {
// firstSearch: false,
// immediate: true,
// submitText: "查询",
showItems: [
{
type: "input", // 类型 input输入 select下拉单选多选 dateRange日期 month月份 year年 yearMonth年月 radio平铺选项单选 selectInput下拉加输入 selectGroup双下拉
placeholder: "", // 提示
label: "名字", // 搜索项名称
prop: "name",
defaultValue: "江穆",
},
{
type: "select",
placeholder: "请选择年龄",
prop: "old",
label: "年龄",
multiple: true,
// filterable: true,
defaultValue: [19, 20],
dicData: [],
},
{
type: "year",
prop: "creatTime",
placeholder: "请选择预算年份",
label: "预算年份",
format: this.dataTimeformat,
},
{
type: "selectInput",
prop: "dataType",
typeDicData: [
{
label: "SKU",
value: "sku",
},
{
label: "品名",
value: "pinming",
},
],
},
{
type: "selectGroup",
prop: "dataTypeSelect",
multiple: true,
getDicDataByType: this.getDicDataByType,
},
],
hiddenItems: [
{
type: "input", // 类型 input输入 select下拉单选多选 dateRange日期 month月份 year年 yearMonth年月 radio平铺选项单选 selectInput下拉加输入 selectGroup双下拉
placeholder: "", // 提示
label: "名字", // 搜索项名称
prop: "name2",
defaultValue: "江穆",
immediate: true,
},
{
type: "select",
placeholder: "请选择年龄",
prop: "old2",
label: "年龄",
dicData: [],
},
{
type: "year",
prop: "creatTime2",
placeholder: "请选择预算年份",
label: "预算年份",
format: this.dataTimeformat,
},
{
type: "selectInput",
prop: "dataType2",
typeDicData: [
{
label: "SKU",
value: "sku3",
},
{
label: "品名",
value: "pinming3",
},
],
defaultValue: {
typeValue: "pinming3",
value: "sss",
},
},
],
},
paginationOption: {
total: 0,
},
},
};
},
methods: {
resetFn({ searchValue, done }) {
done();
},
cellClick(row, column, cell, event) {
console.log("!!!", row);
},
searchItemChange(item) {
console.log(item);
},
load({ currentPage, pageSize, searchValue }) {
console.log("@@@@", searchValue);
console.log(currentPage, pageSize);
this.getTableData();
},
dataTimeformat(value, prop, searchValue, vm) {
searchValue[prop] = value;
this.pageOption.searchOption.refs.name.setValue("1");
},
getTableData() {
this.pageOption.paginationOption.total = 100;
this.pageOption.tableOption.dataList = [
{
name: "weili",
status: 0,
old: 18,
},
{
name: "jiangmu",
status: 1,
old: 28,
},
{
name: "yuncheng",
status: 2,
old: 25,
},
];
},
pageChange({ currentPage, pageSize }) {
console.log(currentPage, pageSize);
},
deleteFn(params) {},
// 根据类型获取下拉数据
getDicDataByType(type, done, typeObj) {
let obj = {
sku1: [
{
label: "sku1",
value: 1,
},
{
label: "sku2",
value: 2,
},
],
pinming2: [
{
label: "品名1",
value: 11,
},
],
};
setTimeout(() => {
done(obj[type]);
}, 2000);
},
},
mounted() {
this.pageOption.searchOption.refs.old.setOption("dicData", [
{
label: "19岁",
value: 19,
},
{
label: "20岁",
value: 20,
},
{
label: "21岁",
value: 21,
},
{
label: "22岁",
value: 22,
},
]);
setTimeout(() => {
this.pageOption.searchOption.refs.dataTypeSelect.setOption(
"typeDicData",
[
{
label: "SKU1",
value: "sku1",
},
{
label: "品名2",
value: "pinming2",
},
]
);
}, 500);
},
};
</script>
<style lang="scss">
.test-components {
width: 100%;
height: 100%;
}
</style>
page组件
<template>
<div class="customer-page">
<div
class="page-top-content"
:style="
!option.domOption.showSearch && option.domOption.showFixedTopBtn
? { 'justify-content': 'right' }
: {}
"
v-if="option.domOption.showSearch || option.domOption.showFixedTopBtn"
>
<div class="page-search-content" v-if="option.domOption.showSearch">
<!-- 插槽自定义全部搜索内容 -->
<slot name="searchContent">
<!-- 默认渲染搜索组件 -->
<hf-search
@search="search"
@resetFn="resetFn"
@searchItemChange="searchItemChange"
:searchOption="option.searchOption"
ref="search"
>
<slot
v-for="slotName in searchSlotNames"
:name="slotName"
:slot="slotName"
slot-scope="scope"
v-bind="scope"
>
</slot>
</hf-search>
</slot>
</div>
<div class="fixed-top-btn" v-if="option.domOption.showFixedTopBtn">
<slot name="fixedTopBtnContent"></slot>
</div>
</div>
<div
class="page-operate-btn-content"
v-if="option.domOption.showOperateBtnContent"
>
<!-- 插槽自定义表格上方操作栏 -->
<slot name="operateBtnContent">
<!-- 默认左右都有操作按钮,如果单纯想左或者想右,请在插入具名插槽 -->
<div class="operate-btn-content">
<!-- 流式左右布局 -->
<slot name="btnContentLeft">
<div></div>
</slot>
<slot name="btnContentRight">
<div></div>
</slot>
</div>
</slot>
</div>
<div class="page-customer-table-content" v-if="option.domOption.showTable">
<slot name="tableContent">
<hf-table :tableOption="option.tableOption" v-on="$listeners">
<slot
v-for="slotName in tableSlotNames"
:name="slotName"
:slot="slotName"
slot-scope="scope"
v-bind="scope"
>
</slot>
</hf-table>
</slot>
</div>
<div class="page-pagination-content" v-if="option.domOption.showPagination">
<el-pagination
:background="option.paginationOption.background"
:current-page.sync="pagination.currentPage"
:page-size.sync="pagination.pageSize"
:layout="option.paginationOption.layout"
:page-sizes="option.paginationOption.pageSizes"
:total="option.paginationOption.total"
v-bind="option.paginationOption.attrs"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script>
import hfSearch from "./components/hfSearch.vue";
import hfTable from "./components/hfTable.vue";
export default {
components: { hfTable, hfSearch },
data() {
return {
searchValue: {},
pagination: {
currentPage: 1,
pageSize: 20,
},
tableSlotNames: [],
searchSlotNames: [],
};
},
computed: {
option() {
let option = {
// 默认展示全部模块
domOption: {
showSearch: true,
showTable: true,
showOperateBtnContent: true,
showPagination: true,
showFixedTopBtn: true, // 是否展示右上角的操作按钮 导出设置什么的
},
// 搜索默认配置
searchOption: {
showItems: [],
hiddenItems: [],
},
// 分页默认配置
paginationOption: {
background: true,
layout: "total, sizes, prev, pager, next, jumper",
pageSizes: [10, 20, 30, 50],
attrs: {
autoScroll: true,
hidden: false,
},
},
// 表格配置
tableOption: {
columns: [],
dataList: [],
attrs: {
height: "100%",
},
},
};
// 合并展示dom配置
option.domOption = {
...option.domOption,
...this.pageOption.domOption,
};
// 合并搜索配置
option.searchOption = this.pageOption.searchOption;
// 获取搜索插槽名称
this.searchSlotNames = [];
option.searchOption.showItems.forEach((ele) => {
if (ele.isSlot) {
this.searchSlotNames.push(
ele.customerSlotName || ele.prop + "Search"
);
}
});
option.searchOption.hiddenItems.forEach((ele) => {
if (ele.isSlot) {
this.searchSlotNames.push(
ele.customerSlotName || ele.prop + "Search"
);
}
});
// 合并分页配置项
option.paginationOption = {
...option.paginationOption,
...this.pageOption.paginationOption,
};
// 合并表格配置项
let attrs = {
...option.tableOption.attrs,
...this.pageOption.tableOption.attrs,
};
option.tableOption = {
...option.tableOption,
...this.pageOption.tableOption,
};
option.tableOption.attrs = attrs;
// 处理插槽,传递到hftable中 多级表头的要递归处理插槽
this.tableSlotNames = [];
let setSlotName = (arr) => {
arr.forEach((element) => {
if (element.isSlot) {
this.tableSlotNames.push(element.customerSlotName || element.prop);
}
if (element.children && element.children.length) {
setSlotName(element.children);
}
});
};
setSlotName(option.tableOption.columns);
if (this.$slots.tableEmpty) {
// 如果存在表格为空时的插槽时,也把表格空插槽传给hftable
this.tableSlotNames.push("tableEmpty");
}
// 合并搜索配置项
return option;
},
},
props: {
pageOption: {
type: Object,
default: () => ({}),
},
},
methods: {
searchItemChange(event) {
this.$emit("searchItemChange", event);
},
// 重置,用于页面插槽重置 event:{searchValue,done}
resetFn(event) {
this.$emit("resetFn", event);
},
// 更新搜索的下拉项 不传值时全部更新,传字符串更新单个,传数组更新部分
updateSearchSelectOption(itemProps) {
this.$refs.search.updateSearchSelectOption(itemProps);
},
handleSizeChange(val) {
this.pagination.currentPage = 1;
this.pagination.pageSize = val;
this.$emit("load", { ...this.pagination, searchValue: this.searchValue });
},
handleCurrentChange(val) {
this.pagination.currentPage = val;
this.$emit("load", { ...this.pagination, searchValue: this.searchValue });
},
search(searchValue) {
this.searchValue = searchValue;
this.pagination.currentPage = 1;
this.$emit("load", { ...this.pagination, searchValue: this.searchValue });
},
},
mounted() {},
};
</script>
<style lang="scss" scoped>
.customer-page {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
> div {
width: 100%;
background-color: #fff;
padding: 0 20px;
box-sizing: border-box;
}
.page-top-content {
margin-bottom: 20px;
display: flex;
justify-content: space-between;
.page-search-content {
}
.fixed-top-btn {
display: flex;
align-items: center;
}
}
.page-customer-table-content {
flex: 1;
}
.page-pagination-content {
height: 60px;
display: flex;
justify-content: right;
align-items: center;
align-content: center;
}
.page-operate-btn-content {
.operate-btn-content {
display: flex;
justify-content: space-between;
padding: 12px 0px;
}
}
}
</style>
hfSearch
<template>
<div class="hf-search">
<div
class="item-content"
v-for="item in searchOption.showItems"
:key="item.prop"
>
<slot
v-if="item.isSlot"
:ref="item.prop"
:searchValue="searchValue"
:name="item.customerSlotName || item.prop + 'Search'"
>
</slot>
<hf-search-item
v-else
:ref="item.prop"
@searchFn="searchFn"
@valueChange="valueChange"
:itemOption="item"
:searchValue="searchValue"
:defaultValue="showObj[item.prop]"
></hf-search-item>
</div>
<el-button type="primary" style="margin-left: 8px" @click="searchFn">{{
submitText
}}</el-button>
<el-button type="primary" @click="resetFn">{{ resetTetx }}</el-button>
<el-popover placement="bottom" width="360" v-model="visible">
<div class="hidden-search-content">
<div
class="item-content"
v-for="item in searchOption.hiddenItems"
:key="item.prop"
>
<slot
v-if="item.isSlot"
:searchValue="searchValue"
:ref="item.prop"
:name="item.customerSlotName || item.prop + 'Search'"
>
</slot>
<hf-search-item
v-else
:ref="item.prop"
@searchFn="searchFn"
@valueChange="valueChange($event, 'hidden')"
:itemOption="item"
:searchValue="searchValue"
:defaultValue="hiddenObjDefaultValue[item.prop]"
></hf-search-item>
</div>
</div>
<div style="text-align: right; margin: 10px 0">
<el-button size="mini" @click="visible = false">取消</el-button>
<el-button
type="primary"
size="mini"
@click="
searchFn();
visible = false;
"
>{{ submitText }}</el-button
>
</div>
<i
slot="reference"
v-if="searchOption.hiddenItems.length"
:style="hiddenItemsHasValue ? { color: '#ff7800' } : {}"
class="cmIcon iconhight-filter"
></i>
</el-popover>
</div>
</template>
<script>
import { isObjectValueEqual } from "@/utils/index.js";
import hfSearchItem from "./hfSearchItem.vue";
export default {
components: { hfSearchItem },
props: {
searchOption: {
type: Object,
default: () => ({
showItems: [],
hiddenItems: [],
}),
},
},
data() {
return {
submitText: "查询",
resetTetx: "重置",
visible: false,
dealValuePropList: [],
searchValue: {},
showObj: {},
hiddenObj: {},
hiddenObjDefaultValue: {},
};
},
computed: {
// 是否有插槽子项
hasSlot() {
let flag =
this.searchOption.showItems.filter((i) => i.isSlot).length ||
this.searchOption.hiddenItems.filter((i) => i.isSlot).length;
return flag;
},
// 隐藏项是否有绑定值
hiddenItemsHasValue() {
return !isObjectValueEqual(this.hiddenObj, this.hiddenObjDefaultValue, [
"__ob__",
"typeValue",
]);
},
selfOption() {
let option = {
immediate: false, // 值发生变化立即触发查询
firstSearch: true, // 首次渲染完默认值立即查询
};
option = { ...option, ...this.searchOption };
return option;
},
},
methods: {
// 构造绑定值
dataInit(arr) {
let obj = {};
let defaultValueObj = {
input: "",
select: "",
dateRange: [],
month: "",
year: "",
yearMonth: "",
radio: "",
};
arr.forEach((element) => {
obj[element.prop] =
element.defaultValue || defaultValueObj[element.type];
if (element.type === "select" && element.multiple) {
// 下拉多选
obj[element.prop] = element.defaultValue || [];
}
if (element.type === "selectGroup" || element.type === "selectInput") {
obj[element.prop] = element.defaultValue || {
typeValue: "",
value: "",
};
if (!element.dontDealTypeToKey) {
this.dealValuePropList.push(element.prop);
}
}
if (
element.type === "selectGroup" &&
element.multiple &&
!element.defaultValue
) {
// 组合下拉多选
obj[element.prop].value = [];
}
});
return obj;
},
valueChange(item, str) {
// 如果是隐藏项修改的话,赋值给hiddenObj,用于计算隐藏项是否有选值
if (str === "hidden") {
this.hiddenObj[item.prop] = item.value;
}
this.$emit("searchItemChange", item);
if (this.selfOption.immediate) {
this.searchFn();
}
},
// 抛出submitObj
searchFn() {
console.log(this.dealValuePropList);
let searchValue = JSON.parse(JSON.stringify(this.searchValue));
this.dealValuePropList.forEach((prop) => {
if (searchValue[prop].typeValue && searchValue[prop].value) {
if (Array.isArray(searchValue[prop].value)) {
if (searchValue[prop].value.length) {
searchValue[searchValue[prop].typeValue] =
searchValue[prop].value;
}
} else {
searchValue[searchValue[prop].typeValue] = searchValue[prop].value;
}
}
delete searchValue[prop];
});
this.$emit("search", searchValue);
},
// 重置
resetFn() {
this.dealValuePropList = [];
// 初始化默认值
this.showObj = this.dataInit(this.searchOption.showItems);
// 用于对比隐藏项的值是否有修改
this.hiddenObjDefaultValue = this.dataInit(this.searchOption.hiddenItems);
this.hiddenObj = JSON.parse(JSON.stringify(this.hiddenObjDefaultValue));
this.searchValue = { ...this.showObj, ...this.hiddenObj };
// 调用子组件的重置
Object.keys(this.searchOption.refs).forEach((item) => {
this.searchOption.refs[item].resetFn &&
this.searchOption.refs[item].resetFn();
});
// 判断是否有插槽 emit出去提供插槽内容的重置,并提供done方法在修改完成后进行搜索
let _this = this;
if (this.hasSlot) {
function done() {
_this.searchFn();
}
this.$emit("resetFn", { searchValue: this.searchValue, done });
} else {
this.searchFn();
}
},
},
mounted() {
this.searchOption.refs = {};
Object.keys(this.$refs).forEach((key) => {
this.searchOption.refs[key] = this.$refs[key][0] || this.$refs[key];
});
},
watch: {
searchOption: {
deep: true,
immediate: true,
handler() {
// 按钮文字
this.submitText = this.searchOption.submitText || "查询";
this.resetTetx = this.searchOption.resetTetx || "重置";
// 构造绑定值
this.dealValuePropList = [];
this.showObj = this.dataInit(this.searchOption.showItems);
this.hiddenObjDefaultValue = this.dataInit(
this.searchOption.hiddenItems
);
this.hiddenObj = JSON.parse(JSON.stringify(this.hiddenObjDefaultValue));
this.searchValue = { ...this.showObj, ...this.hiddenObj };
if (this.selfOption.firstSearch) {
this.$nextTick(() => {
this.searchFn();
});
}
},
},
},
};
</script>
<style lang="scss" scoped>
.iconhight-filter {
// color: #ff7800;
color: #e6e6e6;
font-size: 16px;
margin-left: 8px;
position: relative;
bottom: 5px;
cursor: pointer;
}
.hidden-search-content {
width: 100%;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
align-items: center;
align-content: flex-start;
}
.hf-search {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
padding-bottom: 16px;
align-items: end;
}
</style>
hfSearchItem
<template>
<div class="search-items">
<!-- input输入 -->
<el-input
class="hf-input"
:style="option.width ? { width: option.width } : {}"
v-if="option.type === 'input'"
:clearable="option.clearable"
v-model="value"
@keyup.enter="valueChange"
@change="valueChange"
:placeholder="option.placeholder || '请输入' + option.label"
/>
<!-- select下拉单选多选 -->
<el-select
v-if="option.type === 'select'"
:clearable="option.clearable"
class="hf-input"
:style="option.width ? { width: option.width } : {}"
v-model="value"
collapse-tags
:multiple="option.multiple"
:filterable="option.filterable"
@change="valueChange"
:placeholder="option.placeholder || '请选择' + option.label"
>
<el-option
v-for="item in option.dicData"
:label="item[option.dicProps.label]"
:value="item[option.dicProps.value]"
:key="item[option.dicProps.value]"
>
</el-option>
</el-select>
<!-- dateRange日期 -->
<div v-if="option.type === 'dateRange'" class="item-label-input">
<span class="label el-form-item__label">
{{ option.label }}
</span>
<el-date-picker
@change="valueChange"
v-model="value"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
/>
</div>
<!-- month月份 -->
<!-- year年 -->
<div v-if="option.type === 'year'" class="item-label-input">
<span class="label el-form-item__label">
{{ option.label }}
</span>
<el-button size="small" @click="yearChange('pre')">上一年</el-button>
<el-date-picker
v-model="value"
type="year"
placeholder="选择年"
@change="valueChange"
size="small"
style="width: 110px"
:clearable="false"
value-format="yyyy"
format="yyyy"
/>
<el-button size="small" @click="yearChange('next')">下一年</el-button>
</div>
<!-- yearMonth年月 -->
<div v-if="option.type === 'yearMonth'" class="item-label-input">
<span class="label el-form-item__label">
{{ option.label }}
</span>
<el-button size="small" @click="monthChange('pre')">上一月</el-button>
<el-date-picker
v-model="value"
type="month"
@change="valueChange"
:placeholder="option.placeholder || '选择月'"
size="small"
style="width: 110px"
:clearable="false"
value-format="yyyy-MM"
format="yyyy-MM"
/>
<el-button size="small" @click="monthChange('next')">下一月</el-button>
</div>
<!-- selectInput下拉加输入 -->
<div v-if="option.type === 'selectInput'">
<el-select
style="width: 80px"
v-model="value.typeValue"
@change="typeValueChange"
>
<el-option
v-for="item in option.typeDicData"
:label="item[option.typeDicProps.label]"
:value="item[option.typeDicProps.value]"
:key="item[option.typeDicProps.value]"
>
</el-option>
</el-select>
<el-input
class="hf-input"
:style="option.width ? { width: option.width } : {}"
:clearable="option.clearable"
v-model="value.value"
@keyup.enter="valueChange"
@change="valueChange"
placeholder="请输入"
/>
</div>
<!-- selectGroup双下拉 -->
<div v-if="option.type === 'selectGroup'">
<el-select
style="width: 80px"
v-model="value.typeValue"
@change="typeValueChange"
>
<el-option
v-for="item in option.typeDicData"
:label="item[option.typeDicProps.label]"
:value="item[option.typeDicProps.value]"
:key="item[option.typeDicProps.value]"
>
</el-option>
</el-select>
<el-select
:clearable="option.clearable"
class="hf-input"
:style="option.width ? { width: option.width } : {}"
v-model="value.value"
collapse-tags
:multiple="option.multiple"
:filterable="option.filterable"
@change="valueChange"
placeholder="请选择"
>
<el-option
v-for="item in option.dicData"
:label="item[option.dicProps.label]"
:value="item[option.dicProps.value]"
:key="item[option.dicProps.value]"
>
</el-option>
</el-select>
</div>
</div>
</template>
<script>
import dayjs from "dayjs";
export default {
data() {
return {
value: "",
option: {
type: "", // 类型 input输入 select下拉单选多选 dateRange日期 month月份 year年 yearMonth年月 radio平铺选项单选 selectInput下拉加输入 selectGroup双下拉
placeholder: "", // 提示
clearable: true, // 是否可清除
isSlot: false, // 是否插槽
customerSlotName: "", // 自定义插槽名称
width: "", // 自定义宽度
immediate: false, // 是否立即触发查询
label: "", // 搜索项名称
multiple: false, // 是否多选
filterable: false, // 是否可以搜索
typeDicData: [], // 组合类型的,类型下拉数据
typeDicProps: {
// 组合类型的,下拉渲染的key value值
label: "label",
value: "value",
},
dontDealTypeToKey: false, // 不要 (将组合类型的type作为submitObj的key 子项的值作为value传出去,不影响里面 只在最外层做操作)
dicData: [], // 下拉数组
dicProps: {
// 下拉渲染的key value
label: "label",
value: "value",
},
prop: "", // submitObj对应的key值
defaultValue: "", // 初始值,重置搜索项的赋值
format: function (value, prop, searchValue, current) {
// 默认是将change的value赋值给submitObj对应的key值,可自行修改赋值逻辑,顺便把当前的组件this也传进去,有其他操作自己玩
searchValue[prop] = value;
},
},
};
},
props: {
// searchValue 用于赋值
searchValue: {
type: Object,
},
// 配置项
itemOption: {
type: Object,
default: () => ({}),
},
// 默认值
defaultValue: {
type: Object | String | Array,
},
},
methods: {
// 重置
resetFn() {
this.bindValue(this.defaultValue);
if (
this.option.type === "selectInput" ||
this.option.type === "selectGroup"
) {
this.option.dicData = [];
if (this.option.typeDicData[0]) {
this.value.typeValue =
this.option.typeDicData[0][this.option.typeDicProps.value];
this.typeValueChange();
}
}
},
// 组合类型发生变化
typeValueChange() {
// 置空值
if (this.option.multiple) {
this.value.value = [];
} else {
this.value.value = "";
}
this.option.dicData = [];
if (this.option.getDicDataByType) {
let _this = this;
function done(list = []) {
_this.option.dicData = list;
}
// 找到整个类型对象抛出去,避免有其他骚操作
let typeObj = this.option.typeDicData.find(
(i) => i[this.option.typeDicProps.value] === this.value.typeValue
);
this.option.getDicDataByType(this.value.typeValue, done, typeObj);
}
},
// 上一年 下一年
yearChange(e) {
if (!this.value) {
// 当前没有年不允许上下年
return;
}
this.value =
e === "pre"
? dayjs(this.value).subtract(1, "year").format("YYYY")
: (this.value = dayjs(this.value).add(1, "year").format("YYYY"));
this.valueChange();
},
// 上个月 下个月
monthChange(type) {
if (!this.value) {
// 当前没有月份不允许上下月
return;
}
const lastDate = new Date(
dayjs(new Date(this.value)).subtract(1, "month")
);
const nextDate = new Date(dayjs(new Date(this.value)).add(1, "month"));
if (type === "pre") {
this.value = this.formatToYearMonth(lastDate);
} else {
this.value = this.formatToYearMonth(nextDate);
}
this.valueChange();
},
// 转换年月
formatToYearMonth(date) {
const Year = date.getFullYear();
const month =
date.getMonth() + 1 < 10
? `0${date.getMonth() + 1}`
: date.getMonth() + 1;
return Year + "-" + month;
},
// 年月格式化
// 更改配置项 默认还原value为默认值,赋值的请重新将值传进来
setOption(key, value, thisValue = this.defaultValue) {
this.option[key] = value;
this.bindValue(thisValue);
if (key === "typeDicData") {
// 组合类型的,修改了类型的下拉项,要清空子下拉的下拉项和值
this.option.dicData = [];
// 默认类型取第一项的值
if (value[0]) {
this.value.typeValue = value[0][this.option.typeDicProps.value];
this.typeValueChange();
}
}
this.valueChange();
},
bindValue(value) {
if (
this.option.type === "selectInput" ||
this.option.type === "selectGroup"
) {
this.$set(this, "value", {
typeValue: "",
value: this.option.multiple ? [] : "",
});
this.value.typeValue = value.typeValue;
this.value.value = value.value;
} else {
this.$set(this, "value", value);
}
},
setValue(value) {
this.bindValue(value);
this.valueChange();
},
valueChange() {
// 调用赋值方法,可自定义逻辑
this.option.format(this.value, this.option.prop, this.searchValue, this);
this.$emit("valueChange", { prop: this.option.prop, value: this.value });
if (this.option.immediate) {
// 如果单项是立即查询的,并且整个搜索组件不是立即查询的,name调用父级的搜索
if (!this.$parent.selfOption.immediate) {
this.$emit("searchFn");
}
}
},
// 组件初始化,不在create里调用,在watch里调用
init() {
// 做一下数据隔离,这样不会更改原先的配置
let option = JSON.parse(JSON.stringify(this.itemOption));
this.itemOption.vm = this;
if (this.itemOption.format) {
option.format = this.itemOption.format;
}
if (this.itemOption.getDicDataByType) {
option.getDicDataByType = this.itemOption.getDicDataByType;
}
// 初始配置覆盖
this.option = { ...this.option, ...option };
// 赋值成默认值
this.bindValue(this.defaultValue);
// 组合类型,默认取类型的第一个项
if (
(this.option.type === "selectInput" ||
this.option.type === "selectGroup") &&
this.option.typeDicData[0] &&
!this.value.value
) {
this.value.typeValue =
this.option.typeDicData[0][this.option.typeDicProps.value];
this.typeValueChange();
}
},
},
watch: {
itemOption: {
immediate: true,
deep: true,
handler() {
// 配置发生变化时 重新渲染当前组件
this.init();
},
},
},
};
</script>
<style lang="scss" scoped>
.search-items {
margin: 16px 8px 0 0;
.hf-input {
width: 140px;
}
.item-label-input {
.label {
line-height: 32px;
text-align: right;
vertical-align: middle;
float: left;
font-size: 14px;
font-weight: 700;
color: #606266;
padding: 0 12px 0 0;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
}
}
</style>
hfTable
<template>
<div
class="hf-table"
:style="{
width: tableOption.attrs.width || '100%',
height: tableOption.attrs.height || '100%',
}"
>
<el-table
ref="table"
height="100%"
width="100%"
v-bind="tableOption.attrs"
v-on="$listeners"
:data="tableOption.dataList"
>
<!-- <hf-table-column
v-for="column in tableOption.columns"
:columnOption="column"
:key="column.label"
>
<slot
v-for="slotName in tableSlotNames"
:name="slotName"
:slot="slotName"
slot-scope="scope"
v-bind="scope"
/>
</hf-table-column> -->
<el-table-column
v-for="(column, index) in tableOption.columns"
v-bind="column"
:key="index"
>
<template slot-scope="scope">
<slot
v-if="column.isSlot"
v-bind="scope"
:name="column.customerSlotName || column.prop"
></slot>
<span v-else>{{ scope.row[column.prop] }}</span>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import hfTableColumn from "./hfTableColumn.vue";
export default {
components: { hfTableColumn },
data() {
return {
tableSlotNames: [],
};
},
props: {
tableOption: {
type: Object,
default: () => ({
columns: [],
dataList: [],
attrs: {},
}),
},
},
updated() {
this.$refs.table.doLayout();
},
created() {
this.tableSlotNames = [];
let setSlotName = (arr) => {
arr.forEach((element) => {
if (element.isSlot) {
this.tableSlotNames.push(element.customerSlotName || element.prop);
}
if (element.children && element.children.length) {
setSlotName(element.children);
}
});
};
setSlotName(this.tableOption.columns);
},
mounted() {},
};
</script>
<style lang="scss" scoped>
</style>