封装的一些常用方法,如请求列表,render函数,复选框操作,合并行(合并列后续再添加),自适应高度;
列表请求支持两种方式:
1、子组件内请求(对于无需修改源数据的场景)
2、父组件请求,再把数据传到子组件(对于需要大量修改源数据的场景)
子组件MyTable/index.vue
<template>
<div
ref="tableBox"
class="MyTable"
>
<!-- v-bind="$props": 可以将父组件的所有props下发给它的子组件,子组件需要在其props:{} 中定义要接受的props -->
<!-- v-bind="$attrs": 将调用组件时的组件标签上绑定的非props的特性(class和style除外)向下传递。
在子组件中应当添加inheritAttrs: false(避免父作用域的不被认作props的特性绑定应用在子组件的根元素上) -->
<el-table
ref="MyTable"
v-loading="tableLoading"
v-bind="$attrs"
:data="dataSource"
border
fit
highlight-current-row
:height="relHeight"
element-loading-text="数据加载中"
element-loading-spinner="el-icon-loading"
:row-class-name="tableRowClassName"
:span-method="spanMethod"
:row-key="getRowKey"
@row-contextmenu="rowMenu"
@selection-change="handleSelectionChange"
>
<!-- 复选框-->
<el-table-column
v-if="showSelection"
type="selection"
align="center"
width="55"
fixed
:reserve-selection="!!reserveSelection"
/>
<el-table-column
v-if="showIndex"
label="序号"
type="index"
width="55"
min-width="50"
max-width="60"
align="center"
/>
<template v-for="(col, key) in columns || []">
<!-- 操作列/自定义列 -->
<slot v-if="col.slot" :name="col.prop || col.slot" />
<el-table-column
v-else
:key="key"
:prop="col.prop"
:label="col.label"
:fixed="col.fixed"
:width="col.width"
:min-width="col.minWidth"
:formatter="col.formatter"
:align="col.align || 'left'"
>
<template slot-scope="scope">
<!-- 正常渲染 -->
<template v-if="!col.render">
<template v-if="col.formatter">
<span v-html="col.formatter(scope.row, col)" />
</template>
<template v-else>
<span>{{ scope.row[col.prop] }}</span>
</template>
</template>
<!-- render函数 -->
<template v-else>
{{ renderToHtml(col, scope.row) }}
<slot :name="col.prop" />
</template>
</template>
</el-table-column>
</template>
<!-- <template slot="empty">
</template> -->
</el-table>
<!-- 是否调用分页 -->
<el-pagination
v-if="showPage"
class="pagination-box"
background
:page-size="page.pageSize"
:page-sizes="page.pageSizes"
:current-page="page.current"
layout="total,sizes, prev, pager, next,jumper"
:total="page.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script>
import { equalsArr, unique, isObject } from '@/utils'
import request from '@/utils/request'
export default {
name: 'MyTable',
props: {
// 是否显示分页
showPage: {
type: Boolean,
default: () => { return false }
},
// 请求的url
url: {
type: String,
default: () => { return '' }
},
// 请求参数
params: {
type: Object,
default: () => { return {} }
},
// 是否需要复选框
showSelection: {
type: Boolean,
default: () => { return false }
},
// 请求类型
requestType: {
type: String,
default: () => { return 'get' }
},
// 表格数据,为兼容在父组件请求能使用子组件的页码,所以传进来的数据要求是一个对象
tableData: {
type: Object,
default: () => { return { } }
},
// 表格列
columns: {
type: Array,
default: () => { return [] }
},
// 表格高度
height: {
type: Number,
default: () => { return null }
},
// 是否显示序号
showIndex: {
type: Boolean,
default: () => { return true }
},
// 需要合并行的数组,传列的字段即可,如['name','title']
spanProps: {
type: Array,
default: () => { return [] }
},
loading: {
type: Boolean,
default: () => { return false }
},
// 复选框选中的行
selectRows: {
type: Array,
default: () => { return [] }
},
// 每行的唯一字段
rowKey: {
type: [Number, String],
default: () => { return null }
},
// 数据更新之后保留之前选中的数据(需指定 row-key)
reserveSelection: {
type: Boolean,
default: () => { return false }
},
// 数据结构支持传自定义的
resConfig: {
type: String,
default: () => { return null }
},
// 数据结构字段支持传自定义的
resName: {
type: Object,
default: () => {
// return {
// data: 'data',
// total: 'total',
// pages: 'pages'
// }
return {}
}
}
},
data() {
return {
dataSource: this.tableData.data || [],
tableLoading: this.loading,
relHeight: this.height,
multipleSelection: [],
searchParams: {},
resizeFn: null,
page: {
pageSize: 10,
current: 1,
pageSizes: [10, 20, 50, 100],
total: 0
}
}
},
watch: {
loading(val) {
if (!this.url) {
this.tableLoading = val
}
},
tableData(val) {
if (!this.url && isObject(val)) { // 父组件请求并且传过来的是一个对象
this.dataSource = val.data
if (val.total) this.$set(this.page, 'total', val.total) // 强制更新total
this.setHeight()
}
},
// 监听父组件传入的选中数据
// 需要默认选中某些复选框时父组件传selectRows数组即可
selectRows(val) {
try {
// 父组件选中的数组与子组件选中的数组比较,避免重复触发
if (!equalsArr(val, this.multipleSelection)) {
const oldSelectData = [...this.multipleSelection]
this.$nextTick(async() => {
await this.toggleSelection(val, oldSelectData)
})
}
} catch (error) {
console.log(' ~ file: index.vue ~ line 177 ~ selectRows ~ error', error)
}
}
},
mounted() {
this.resizeFn = this.resize()
this.$nextTick(() => {
if (!this.height) {
// 监听窗口变化
window.addEventListener('resize', this.resizeFn)
}
})
},
// 组件卸载
destroyed() {
// 移除窗口监听
window.removeEventListener('resize', this.resizeFn)
this.resizeFn = null
},
methods: {
// 监听浏览器高度变化
resize() {
let timer = null
return () => {
if (timer) {
window.clearTimeout(timer)
}
timer = window.setTimeout(() => {
this.setHeight()
}, 100)
}
},
// 给表格行赋值唯一标识
getRowKey(row) {
return row[this.rowKey] || row.id || row.rowIndex || row.index
},
// 给数据增加index,为 Table 中的某一行添加 class
tableRowClassName({ row, rowIndex }) {
row.rowIndex = rowIndex
if (row.rowClass) {
return row.rowClass
}
},
rowMenu(row, column, event) {
this.$emit('handRowMenu', {
row: row,
column: column,
event: event
})
},
// 利用slot实现render函数jsx语法渲染
renderToHtml(col, row) {
if (typeof col.render === 'function') {
this.$slots[col.prop] = [col.render(row)]
}
},
// 复选框选择
handleSelectionChange(val) {
this.multipleSelection = val
this.$emit('getSelectRows', val || [])
},
// 表格自适应高度
setHeight() {
this.relHeight = null
this.$nextTick(() => {
// this.relHeight = document.documentElement.clientHeight - 60 - 54;
const MyTableRef = this.$refs.MyTable
// 先清空上一次表格的高度再重新赋值,避免每次拿到的都是上一次设定的高度
MyTableRef.$el.style.height = 'auto'
if (this.height) {
this.relHeight = this.height
} else { // 父组件不传高度时表格高度自适应
const tableBox = this.$refs.tableBox
const alHeight = tableBox.clientHeight
const tableBoxTop = tableBox.offsetTop
const bodyHeight = document.body.clientHeight
const tableBoxBottom = bodyHeight - tableBoxTop
const isOverHeight = !!((alHeight || 0) - (tableBoxBottom || 0) > 0)
if (isOverHeight) { // 表格高度是否超出浏览器高度
this.relHeight = this.showPage ? tableBoxBottom - 42 - 120 : tableBoxBottom - 120
} else {
this.relHeight = this.showPage ? alHeight - 42 : alHeight
}
}
})
},
// 如果是在父组件请求又需要分页,则在父组件调用这个方法
setPage(params = {}) {
if (!isObject(params)) return // 判断是否是对象
const paramsNew = {
...this.searchParams,
page: this.page.current,
size: this.page.pageSize,
...params
}
this.searchParams = { ...paramsNew }
return paramsNew
},
// 切换每页展示条数
handleSizeChange(val) {
this.$set(this.page, 'current', 1)
this.$set(this.page, 'pageSize', val)
if (!this.url) { // 如果是在父组件请求
const params = {
...this.searchParams,
page: this.page.current,
size: this.page.pageSize
}
// 如果是在父组件请求,抛出这个切换方法给父组件
this.$emit('handlePageChange', params)
} else {
this.getList()
}
},
// 切换页码
handleCurrentChange(val) {
this.$set(this.page, 'current', val)
if (!this.url) { // 如果是在父组件请求
const params = {
...this.searchParams,
page: this.page.current,
size: this.page.pageSize
}
// 如果是在父组件请求,抛出这个切换方法给父组件
this.$emit('handlePageChange', params)
} else {
this.getList().then(res => {
this.setHeight()
})
}
},
// 搜索
search(param = {}) {
this.$set(this.page, 'current', 1)
this.searchParams = { ...this.searchParams, ...param }
this.getList(param)
},
// 重置
handleReset() {
return new Promise((resolve, reject) => {
this.$set(this.page, 'current', 1)
this.$set(this.page, 'pageSize', 10)
this.searchParams = {}
this.getList().then(res => {
resolve(res)
})
})
},
// 请求列表
getList(param = {}) {
return new Promise((resolve, reject) => {
// 父组件可以传params,如果不传,可在调用getList方法时以参数形式传
// param调用方法时传的参数
// this.params通过调用组件传的参数
// this.searchParams子组件用来缓存所有传进来的参数,方便切换页码时用到
this.tableLoading = true
this.dataSource = []
this.searchParams = { ...this.searchParams, ...param }
let paramsNew = Object.assign({}, this.params)
paramsNew = { ...paramsNew, ...this.searchParams }
// 是否需要分页
if (this.showPage) {
paramsNew.page = this.page.current
paramsNew.size = this.page.pageSize
}
const paramsKey = { params: paramsNew }
if (!['get', 'GET'].includes(this.requestType)) {
// 非get请求时的处理
paramsKey.data = paramsNew
delete paramsKey.params
}
request({
url: this.url,
method: this.requestType || 'GET',
...paramsKey
})
.then((res) => {
this.tableLoading = false
// 数据结构及字段支持传自定义的
// :resConfig="res.data.data" 数据结构
// :resName="{data:'data',total:'total',pages:'pages'}" 字段名称
const option = this.resConfig && typeof this.resConfig === 'string'
let newRes = res
if (option) {
const level = this.resConfig.split('.')
if (level.length > 1) {
level.map((item, index) => {
if (item !== 'res') {
newRes = newRes [item]
}
})
}
}
// ?.是可选链符的写法,其实props已经定义了this.resName的默认值为{},可以不用可选链符判断,直接写成this.resName.data也可
const records = res[this?.resName?.data] || res.records || []
const total = res[this?.resName?.total] || res.total || 0
const pages = res[this?.resName?.pages] || res.pages || 0
if (pages) {
this.$set(this.page, 'total', total) // 强制更新界面
this.dataSource = option ? newRes || [] : records
// this.setHeight()
resolve(records)
}
})
.catch((err) => {
this.tableLoading = false
reject()
})
})
},
// 复选框默认选中处理
async toggleSelection(val, oldSelectData) {
// 提取rowKey数组
const selectKeys = (val || []).map(item => (item[this.rowKey] || item.id))
// 解决操作删除标签时,上一页的数据无法被删除问题
const deleteData = (oldSelectData || []).filter(item => !selectKeys.includes(item[this.rowKey]));
(deleteData || []).map((item) => {
this.$refs.MyTable.toggleRowSelection(item, false)
})
// 调用toggleRowSelection方法会触发复选框change事件,选中的值会被改变,所以要合并数组
let newDataSource = [...this.dataSource, ...val]
newDataSource = unique(newDataSource, this.rowKey); // 去重
(newDataSource || []).map(item => {
const rowKey = item[this.rowKey] || item.id
if (selectKeys.includes(rowKey)) {
// 设置选中
this.$refs.MyTable.toggleRowSelection(item, true)
} else {
// 取消选中
this.$refs.MyTable.toggleRowSelection(item, false)
}
})
return Promise.resolve()
},
// 合并行
// 参数:dataSource-表格数据,spanProps-需要进行合并计算的字段列表,rowIndex-当前行数,columnIndex-当前列数,column-当前列
spanMethodFunc(dataSource = [], spanProps = [], rowIndex, columnIndex, column) {
// if (columnIndex >= spanProps.length || !spanProps[columnIndex]) {
if (!spanProps.includes(column.property)) {
// 根据传入的字段列表,判断不需要合并的列
return [1, 1]
} else {
// 使用try-catch,如果方法错误返回错误信息
try {
// let _spanProps = spanProps.slice(columnIndex, columnIndex + 1); //截取需要用到判断的字段名
// 过滤出需要合并的当前列字段
const _spanProps = spanProps.filter(item => item == column.property)
// 判断是否从本行开始合并
const merge = _spanProps.some((item) => {
// 如果当前行所需要判断合并的字段中有一个跟前一条数据不一样,本条数据即为合并的起点,第一条数据直接为合并起点
return rowIndex == 0 || (item && dataSource[rowIndex][item] != dataSource[rowIndex - 1][item])
})
// 如果本条数据是合并起点,获取需要合并的数据条数
if (merge) {
const _list = dataSource.slice(rowIndex) // 截取从本条数据开始的列表
// 获取合并行数
const _lineNum = _list.findIndex((item, index) => {
// 同merge判断,找到合并的终点
return (
index &&
_spanProps.some((item1) => {
return item1 && item[item1] != _list[0][item1]
})
)
})
// 合并行数为-1时,输出_list的长度,否则输出_lineNum
return [_lineNum === -1 ? _list.length : _lineNum, 1]
} else {
// 否则,返回[0,0],即本条数据被合并
return [0, 0]
}
} catch (err) {
console.error('spanMethodFunc error:', err)
}
}
},
// 合并行
spanMethod({ row, column, rowIndex, columnIndex }) {
return !this.spanProps || this.spanProps.length === 0 ? false : this.spanMethodFunc(this.dataSource, this.spanProps, rowIndex, columnIndex, column)
}
}
}
</script>
<style scoped lang="scss">
.pagination-box {
margin-top: 10px;
display: flex;
justify-content: flex-end;
align-items: center;
text-align: right;
}
</style>
以上就是封装的组件了,下面开始使用
父组件调用(方式一:直接调用子组件内的请求方法,一定要传url)
<template>
<div>
<standar-table
ref="elTableList"
:url="url.getWKEventListlUrl"
:isShowPage="true"
:showSelection="true"
:columns="columns"
@getSelectRows="getSelectRows"
>
<el-table-column
slot="oprate"
:key="oprate"
label="是否启用"
align="center"
>
<template slot-scope="scope">
<el-switch
class="mySwitch"
v-model="scope.row.status"
active-text="启用"
inactive-text="禁用"
:active-value="1"
:inactive-value="0"
@change="handleStatusChange($event,scope.row)"
>
</el-switch>
</template>
</el-table-column>
</standar-table>
</div>
</template>
// 存放接口文件:point.js
// export const getWKEventListlUrl = '/api/aaaaa/bbbbb';
<script>
import standarTable from '@/components/MyTable/index'
import {getWKEventListlUrl,eventExport} from "@/api/point";
export default {
components:{standarTable},
data() {
return {
searchform: {},
url:{getWKEventListlUrl},
columns: [
{
label: "埋点事件",
prop: "eventName",
align:'center',
// render:(row) => {
// retrun row.eventName
// }
},
{
label: "类型",
prop: "typeName",
align:'center',
},
{
label: "是否启用",
slot: "oprate",
prop: "oprate",
align:'center',
},
],
selectRows:[],
selectRowsData:[],
};
},
methods: {
// 公共参数
getParams(){
const params = {
...this.searchform,
type:this.searchform.type.join(',')
};
return params;
},
// 请求列表
getListData() {
this.$refs.elTableList.search(this.getParams())
},
// 表格复选框选中
getSelectRows(data){
this.selectRows = data || [];
},
// 状态change
handleStatusChange(v,{eventCode = '',status = 0}){
const messageText = {
0:'禁用成功',
1:'启用成功'
}
const params = {eventCode,status};
setEventStatus(params).then(res => {
if(res.data.code === 200){
this.$message.success(messageText[v]);
this.getListData();
}
}).catch(err => {
console.log(' ~ file: wkEvent.vue ~ line 154 ~ setEventStatus ~ err', err);
})
},
// 清空选中的数据
clearData(){
this.selectRowsData = [];
}
},
mounted() {
// 通过ref调用子组件里面的列表请求方法
this.$refs.elTableList.getList(this.getParams());
}
}
</script>
效果图:
父组件调用(方式二:在父组件请求,不需要页码,需要合并行,传数据到子组件,不必传url了,tableData必须是一个对象)
<template>
<div>
<standar-table
ref="elTableList"
:columns="columns"
:showIndex="false"
:tableData="tableData"
:loading="tableLoading"
:spanProps="spanProps"
:height="'90vh'"
>
<el-table-column
slot="oprate"
key="oprate"
label="门槛条件"
align="center"
>
<template slot-scope="scope">
<span class="mgr10">经验值达到</span>
<inputNumber
:value="scope.row.experienctThreshole"
v-model="scope.row.experienctThreshole"
:controls="false"
:min="0"
@change="inputNumberChange($event,scope.row,'experienctThreshole')"
/>
</template>
</el-table-column>
<el-table-column
slot="rankStartThreshole"
key="rankStartThreshole"
label="排名要求"
align="center"
>
<template slot-scope="scope">
<div v-if="sameType1.includes(scope.row.titleName)">--</div>
<div v-else>
<span class="mgr10">排名</span>
<inputNumber
:value="scope.row.rankStartThreshole"
v-model="scope.row.rankStartThreshole"
:controls="false"
:min="0"
style="width:30%"
@change="inputNumberChange($event,scope.row,'rankStartThreshole')"
/> -
<inputNumber
:value="scope.row.rankEndThreshole"
v-model="scope.row.rankEndThreshole"
:controls="false"
:min="0"
style="width:30%"
@change="inputNumberChange($event,scope.row,'rankEndThreshole')"
/>
<span class="mgl10">以内</span>
</div>
</template>
</el-table-column>
</standar-table>
</div>
</template>
// 存放接口文件:point.js
// export const getStatusLevelList= '/api/aaaaa/bbbbb';
<script>
import standarTable from '@/components/MyTable/index'
import {getStatusLevelList} from "@/api/point";
export default {
components:{standarTable},
data() {
return {
columns: [
{
label: "学籍",
prop: "titleName",
align:'center',
},
{
label: "段位分类",
prop: "topRank",
align:'center',
render:(row) => {
const sameType1 = ['童生','秀才'];
const sameType2 = ['举人','进士'];
const sameType3 = ['探花','榜眼','状元'];
let topText = '低级';
let bottomText = `比例后60%(${row.lowRank}为预设总数)`;
const type2 = sameType2.includes(row.titleName);
const type3 = sameType3.includes(row.titleName);
if(type2) topText = '中级';
if(type3) topText = '高级';
return <div class="textLeft">{`${topText}段位排名 ${row.topRank} - ${row.lowRank + bottomText}`}</div>
}
},
{
label: "等级分类",
prop: "titleCount",
align:'center',
},
{
label: "门槛条件",
slot: "oprate",
prop: "oprate",
align:'center',
},
{
label: "排名要求",
prop: "rankStartThreshole",
slot: "rankStartThreshole",
align:'center',
},
{
label: "排名称号",
prop: "experienceRank",
align:'center',
},
],
tableData:{},
tableLoading:false,
spanProps:['titleName','topRank','titleCount'], // 需要合并行的字段
};
},
methods: {
// 公共参数
getParams(){
const params = {
...this.searchform,
};
return params;
},
// 请求列表
getListData() {
this.tableLoading = true;
try {
getStatusLevelList(this.getParams()).then(res => {
if(res.data.code === 200){
this.tableLoading = false;
const data = res.data.data || [];
const newData = this.filterData(data);
this.tableData = {data:newData};
}
}).catch(err => {
this.tableLoading = false;
})
} catch (error) {
this.tableLoading = false;
}
},
// 列表数据处理
filterData(data = []){
let tableData = [];
(data || []).map(item => {
const pItem = Object.assign({},item);
const detailListNew = (item.detailList || []).map(cItem => {
return {
...pItem,
...cItem,
}
});
tableData = [...tableData,...detailListNew]
});
return tableData;
},
// inputNumber change
inputNumberChange(v,row,type){
},
},
mounted() {
this.getListData();
}
</script>
效果图:
父组件调用(方式三:在父组件请求,并且需要页码,不传url,tableData必须是一个对象)
<template>
<div class="containerMargin">
<el-form
ref="searchForm"
:model="searchForm"
label-position="right"
>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item>
<el-input
v-model="searchForm.tagName"
placeholder="客户标签"
clearable
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<standarTable
ref="myTable"
:columns="columns"
:show-page="true"
:show-index="false"
request-type="post"
:table-data="tableData"
@handlePageChange="handlePageChange"
/>
</div>
</template>
<script>
// 存放接口文件:point.js
import {page} from "@/api/point";
import standarTable from '@/components/MyTable/index'
export default {
name: 'MassMediaDeliveryManagement',
components: { standarTable },
data() {
return {
searchForm: {}, // 分页参数
tableData: {},
columns: [
{
label: 'ID',
prop: 'id',
align: 'center',
width: 200
},
{
label: '标签类型',
prop: 'deleteTag',
align: 'center'
},
{
label: '客户标签',
prop: 'tagName',
align: 'center'
},
{
label: '创建人',
prop: 'createUser',
align: 'center'
},
{
label: '创建时间',
prop: 'createTime',
align: 'center',
render: (row) => {
// return formatDate(row.createTime, true)
}
}
]
}
},
mounted() {
this.getListData()
},
methods: {
// 公共参数
getParams() {
const params = {
...this.searchForm
}
return params
},
// 请求列表
getListData() {
this.$nextTick(() => {
const params = this.$refs.myTable.setPage(this.getParams())
page(params).then(res => {
this.tableData = { data: res.records || [], total: res.total }
})
})
},
// 切换页码
handlePageChange(params) {
this.getListData()
}
}
}
</script>
公共方法: utils/index.js
/** 判断是否是数组
* @param {Object} value
* 返回true或false
*/
export function isArrayFn(value) {
if (typeof Array.isArray === 'function') {
return Array.isArray(value)
}
return Object.prototype.toString.call(value) === '[object Array]'
}
/**
* 比较两个数组是否相等
*@param{Arry}arr1 需要比较的数组1
*@param{Arry}arr2 需要比较的数组2
*/
export function equalsArr(arr1 = [], arr2 = []) {
if (!isArrayFn(arr1) || !isArrayFn(arr2)) return
if (JSON.stringify(arr1) == JSON.stringify(arr2)) return true
return false
}
// 数组去重
export function unique(data = [], key) {
if (!data) return
const res = new Map()
return data.filter(a => !res.has(a[key]) && res.set(a[key], 1))
}
/** 判断是否是对象
* @param {Object} value
* 返回true或false
*/
export function isObject(value) {
if (Object.prototype.toString.call(value) === '[object Object]') {
return true
}
return false
}