项目大部分的页面布局都一样,考虑封装一个共用的布局组件,差异使用插槽的形式去做调整,使项目成员专注于当前页面的逻辑
有反对使用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) {
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) {
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才会生效
<page :page="pageOption">
</page>
pageOption:{
domOption: {
showSearch: true,
showFixedTopBtn: true,
showTable: true,
showOperateBtnContent: true,
showPagination: true,
图一中每个圈出的地方均可使用插槽渲染,最终表格的高度会被其他内容挤压,计算出剩余高度赋给表格,对应的插槽名称如下:
<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>
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: {
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的则进行插槽渲染
<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: {},
firstSearch
如果页面存在一些特殊逻辑,不需要组件自己第一次调用查询方法需设置为false,再通过ref去调用page组件的search方法来调用load方法进行表格数据获取
refs和插槽
在渲染完成后,会将每一个筛选项实例存储在refs中,以prop作为key值,以每一个搜索项的实例作为value
所以!调用searchOption.refs的时候必须确保组件已经渲染完成,建议放在mounted中去做处理
插槽名称默认以prop+Search的形式,加入存在自定义插槽名称则以自定义插槽名称作为插槽名称
<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有效
非插槽必填
类型值: input输入;select下拉单选多选;dateRange日期;year年; yearMonth年月;selectInput下拉加输入; selectGroup双下拉
将作为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值
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值
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) {
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">
:pageOption="pageOption"
@cell-click="cellClick"
@resetFn="resetFn"
@searchItemChange="searchItemChange"
@load="load"
<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>
<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: {
border: true,
searchOption: {
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,
hiddenItems: [
type: "input",
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">
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"
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>
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">
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%",
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;
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) {
this.tableSlotNames.push("tableEmpty");
return option;
props: {
pageOption: {
type: Object,
default: () => ({}),
methods: {
searchItemChange(event) {
this.$emit("searchItemChange", event);
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">
class="item-content"
v-for="item in searchOption.showItems"
:key="item.prop"
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">
class="item-content"
v-for="item in searchOption.hiddenItems"
:key="item.prop"
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>
slot="reference"
v-if="searchOption.hiddenItems.length"
:style="hiddenItemsHasValue ? { color: '#ff7800' } : {}"
class="cmIcon iconhight-filter"
</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);
element.type === "selectGroup" &&
element.multiple &&
!element.defaultValue
obj[element.prop].value = [];
return obj;
valueChange(item, str) {
if (str === "hidden") {
this.hiddenObj[item.prop] = item.value;
this.$emit("searchItemChange", item);
if (this.selfOption.immediate) {
this.searchFn();
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();
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">
<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"
<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>
<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>
<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>
<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>
<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>
<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: "",
placeholder: "",
clearable: true,
isSlot: false,
customerSlotName: "",
width: "",
immediate: false,
label: "",
multiple: false,
filterable: false,
typeDicData: [],
typeDicProps: {
label: "label",
value: "value",
dontDealTypeToKey: false,
dicData: [],
dicProps: {
label: "label",
value: "value",
prop: "",
defaultValue: "",
format: function (value, prop, searchValue, current) {
searchValue[prop] = value;
props: {
searchValue: {
type: Object,
itemOption: {
type: Object,
default: () => ({}),
defaultValue: {
type: Object | String | Array,
methods: {
resetFn() {
this.bindValue(this.defaultValue);
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;
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) {
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) {
if (!this.$parent.selfOption.immediate) {
this.$emit("searchFn");
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);
(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>
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"
<el-table-column
v-for="(column, index) in tableOption.columns"
v-bind="column"
:key="index"
<template slot-scope="scope">
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>
粉丝