Files
laws-sinotruk-client/src/components/jero/JTable.vue
T
2023-10-31 18:47:12 +08:00

670 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<a-table
class="j-table"
ref="table"
:bordered="bordered"
:columns="resultColumns"
:components="components"
:scroll="scroll || selfScroll"
v-bind="$attrs"
v-on="$listeners">
<!-- 如果插槽是作用域插槽将作用域插槽的props包装成对象传递给slot -->
<template v-for="(_, slotName) in $scopedSlots" :slot="slotName" slot-scope="text, record, index">
<slot :name="slotName" v-bind="{text, record, index}" />
</template>
<!-- 如果是普通插槽直接使用$slots渲染 -->
<slot v-for="(_, slotName) in $slots" :name="slotName" :slot="slotName" />
<template slot="text" slot-scope="text">
<a-tooltip overlay-class-name="tooltip-style">
<template slot="title">{{ text || text === 0 ? text : global.emptyLine }}</template>
<div class="table-text">{{ text || text === 0 ? text : global.emptyLine }}</div>
</a-tooltip>
</template>
<!-- 操作栏 -->
<template slot="action" slot-scope="text, record, index">
<slot name="action" v-bind="{text, record, index}">
<div class="action-span-cell">
<!-- 传了operSpecialJudgeField需要特殊判断操作列是否显示更多 -->
<template v-if="operSpecialJudgeField">
<template v-if="operSpecialJudge(record).length > operateMaxNum + 1">
<!-- 需要更多按钮 -->
<a @click="operationClick(operSpecialJudge(record)[0],record)">{{ operSpecialJudge(record)[0].text }}</a>
<template v-if="operSpecialJudge(record).length > operateMaxNum">
<a-divider type="vertical" />
<a-dropdown placement="bottomCenter">
<a-icon type="more" class="operate-icon-btn operate-icon-more" />
<a-menu slot="overlay">
<a-menu-item v-for="(operation, index) in operSpecialJudge(record).slice(operateMaxNum)" :key="index">
<a @click="operationClick(operation,record)">
<div class="operate-btn">
{{ operation.text }}
</div>
</a>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
</template>
<template v-else>
<!-- 不需要更多按钮 -->
<template v-for="(operation, index) in operSpecialJudge(record)">
<a :key="index" @click="operationClick(operation,record)">{{ operation.text }}</a>
<a-divider type="vertical" v-if="index !== operSpecialJudge(record).length - 1" />
</template>
</template>
</template>
<template v-else>
<!-- 没有特殊判断直接渲染operationList -->
<!-- 需要更多按钮的情况 -->
<template v-if="getLineOperationList(record).length > operateMaxNum + 1">
<a @click="operationClick(getLineOperationList(record)[0],record)">{{ getLineOperationList(record)[0].text }}</a>
<template v-if="getLineOperationList(record).length > operateMaxNum">
<a-divider type="vertical" />
<a-dropdown placement="bottomCenter">
<a-icon type="more" class="operate-icon-btn operate-icon-more" />
<a-menu slot="overlay">
<a-menu-item v-for="(operation, index) in getLineOperationList(record).slice(operateMaxNum)" :key="index">
<a @click="operationClick(operation,record)">
<div class="operate-btn">
{{ operation.text }}
</div>
</a>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
</template>
<template v-else-if="getLineOperationList(record).length > 0">
<template v-for="(operation, index) in getLineOperationList(record)">
<a :key="index" @click="operationClick(operation,record)">{{ operation.text }}</a>
<a-divider type="vertical" v-if="index !== getLineOperationList(record).length - 1" />
</template>
</template>
</template>
</div>
</slot>
</template>
<div slot="settingDropdown">
<a-card v-if="needSettingColumnHideOrFreeze">
<a-table
rowKey="key"
:style="settingStyle"
tableLayout="fixed"
size="small"
bordered
:pagination="false"
:scroll="settingScroll"
:data-source="settingDataSource"
:columns="settingColumns">
<a-checkbox slot="hide" slot-scope="text, record"
:checked="text"
:disabled="record.disabledHide"
@change="changeSetting(text, 'hide', record)"></a-checkbox>
<a-checkbox slot="freeze" slot-scope="text, record"
:checked="text"
:disabled="record.disabledFreeze"
@change="changeSetting(text, 'freeze', record)"></a-checkbox>
</a-table>
</a-card>
</div>
<a-icon ref="settingBtn" slot="settingIcon" type="setting" :style="{ fontSize:'16px', color: '#108ee9' }"
v-if="needSettingColumnHideOrFreeze" />
</a-table>
</template>
<script>
import '@assets/less/common.less'
import Vue from 'vue'
import { cloneDeep } from 'lodash'
import VueDraggableResizable from 'vue-draggable-resizable'
import { isHasPermission } from '@/utils/hasPermission'
Vue.component('vue-draggable-resizable', VueDraggableResizable)
const actionKey = 'action'
// 保存localStorage中的后缀
const saveSuffix = ':JTable'
// 拿到column的key
const getKey = col => col.key || col.dataIndex
// 所有JTable的缓存key
const J_TABLE_KEYS = 'J_TABLE_KEYS'
export default {
name: 'JTable',
props: {
// 用于持久化存储列配置,保证唯一
tableKey: {
type: String,
required: false,
default: 'j-table'
},
columns: {
type: Array,
required: true
},
// 配置表的样式
settingStyle: {
type: Object,
required: false,
default: () => ({ width: '300px' })
},
// 配置表的scroll
settingScroll: {
type: Object,
required: false,
default: () => ({ y: 300 })
},
// 如果需要拖拽后增加总列宽而不是占用其他列宽度:需要父组件 :scroll.sync="scroll"
scroll: {
type: Object,
required: false
},
// 列最小宽度
columnMinWidth: {
type: Number,
required: false,
default: 100
},
bordered: {
type: Boolean,
required: false
},
// 是否需要设置冻结列和显隐列
needSettingColumnHideOrFreeze: {
type: Boolean,
required: false,
default: false
},
// 是否需要将冻结和显隐的设置存储在localStorage中
needSaveSettingInLocal: {
type: Boolean,
required: false,
default: false
},
// 是否可拖拽
canDrag: {
type: Boolean,
required: false,
default: false
},
// 操作列数据
operationList: {
type: Array,
required: false,
default: () => {
return []
}
},
// 操作列显示更多前的最大个数
operateMaxNum: {
type: Number,
required: false,
default: 1
},
// 操作列对于哪个字段有特殊判断
operSpecialJudgeField: {
type: String,
required: false,
default: ''
},
// 操作列需要特殊判断的字段是哪些值时,过滤不符合条件按钮
operSpecialJudgeValue: {
type: [Array, String, Boolean],
required: false,
default: true
},
// 操作列特殊判断时需要删掉的按钮
operSpecialJudgeDelBtn: {
type: [String, Array],
required: false,
default: ''
}
},
data () {
return {
// settingVisible: false,
// 设置表格
settingColumns: [
{ title: '列名', dataIndex: 'title', align: 'center', ellipsis: true, width: 150 },
{
title: '隐藏',
dataIndex: 'hide',
align: 'center',
ellipsis: true,
width: 60,
scopedSlots: { customRender: 'hide' }
},
{
title: '冻结',
dataIndex: 'freeze',
align: 'center',
ellipsis: true,
width: 60,
scopedSlots: { customRender: 'freeze' }
}
],
settingDataSource: [],
settingColumnsObj: { hide: [], freeze: [] }, // 本地存储的配置对象
resultColumns: [],
components: {
header: {
cell: this.initDrag(this.columns)
}
},
selfScroll: {}
}
},
watch: {
// 配置列改变,将配置转换成配置表格数据
settingColumnsObj: {
deep: true, // 深度监听
immediate: true, // 挂载先执行一次
handler () {
const dataSource = []
this.columns.forEach(col => {
const tempKey = getKey(col)
// 操作栏不可配置
if (tempKey === actionKey) {
return
}
// hideSettingColumn: true 可以让不显示字段控制
if (col.hideSettingColumn) {
return
}
dataSource.push({
title: col.title || col.columnTitle, // 用于显示的字段
key: tempKey, // 用于存储的字段
hide: this.settingColumnsObj.hide.includes(tempKey), // 是否隐藏
freeze: this.settingColumnsObj.freeze.includes(tempKey), // 是否冻结
disabledHide: col.disabledHide, // 禁用隐藏
disabledFreeze: col.disabledFreeze // 禁用冻结
})
})
this.settingDataSource = dataSource
// 自定义列配置修改后,更新表格字段
this.setResultColumns(this.columns)
}
},
columns: {
handler () {
this.setResultColumns(this.columns)
},
deep: true
}
},
methods: {
isHasPermission,
// 操作列特殊判断,返回经过特殊判断后的操作列数据
operSpecialJudge (record) {
let operationList = []
if (typeof this.operSpecialJudgeValue === 'boolean' || typeof this.operSpecialJudgeValue === 'string') {
// 只需要判断某个字段是否符合某一个值
if (record[this.operSpecialJudgeField] === this.operSpecialJudgeValue) {
// 符合特殊条件,需要过滤操作按钮
if (this.operSpecialJudgeDelBtn && typeof this.operSpecialJudgeDelBtn === 'string') {
// 是字符串类型,只需要过滤掉一个操作按钮
operationList = this.operationList.filter(item => item.text !== this.operSpecialJudgeDelBtn)
} else if (this.operSpecialJudgeDelBtn && typeof this.operSpecialJudgeDelBtn === 'object') {
// 是数组类型需要过滤掉多个操作按钮
operationList = this.operationList.filter(item => !this.operSpecialJudgeDelBtn.includes(item.text))
} else {
operationList = this.operationList
}
} else {
// 不符合条件,不需要过滤
operationList = this.operationList
}
} else if (typeof this.operSpecialJudgeValue === 'object') {
// 需要判断某个字段是否符合某些值
if (this.operSpecialJudgeValue.includes(record[this.operSpecialJudgeField])) {
// 符合特殊条件,需要过滤操作按钮
if (this.operSpecialJudgeDelBtn && typeof this.operSpecialJudgeDelBtn === 'string') {
// 是字符串类型,只需要过滤掉一个操作按钮
operationList = this.operationList.filter(item => item.text !== this.operSpecialJudgeDelBtn)
} else if (this.operSpecialJudgeDelBtn && typeof this.operSpecialJudgeDelBtn === 'object') {
// 师叔祖类型需要过滤掉多个操作按钮
operationList = this.operationList.filter(item => !this.operSpecialJudgeDelBtn.includes(item.text))
} else {
operationList = this.operationList
}
} else {
// 不符合条件,不需要过滤
operationList = this.operationList
}
}
return operationList
},
// 操作栏按钮点击
operationClick (operation, record) {
this.$emit('operationClick', operation, record)
},
// 将父组件的columns进行处理:增加设置列按钮
setResultColumns (columns) {
// 深度克隆,防止直接修改父组件数据
columns = cloneDeep(columns)
// 有设置功能再进行这个插槽的配置
if (this.needSettingColumnHideOrFreeze) {
// 设置按钮插槽配置
const settingOption = {
filterDropdown: 'settingDropdown',
filterIcon: 'settingIcon'
}
let findActionIndex = columns.findIndex(col => col && (getKey(col) === actionKey))
// 将设置按钮放到表格右上角,如果有操作栏就固定到右侧(操作栏固定右侧,没有的话就最后一列)
if (findActionIndex === -1) {
findActionIndex = columns.length - 1
}
if (columns[findActionIndex].scopedSlots instanceof Object) {
Object.assign(columns[findActionIndex].scopedSlots, settingOption)
} else {
columns[findActionIndex].scopedSlots = settingOption
}
// 自定义设置弹框(尝试解决表格刷新丢失弹框的问题)此方法会出现两个弹框
// columns[findActionIndex].filterDropdownVisible = this.settingVisible
// columns[findActionIndex].onFilterDropdownVisibleChange = (visible) => {
// this.settingVisible = visible
// }
}
// 最后过滤掉需要隐藏的字段,按定位进行排序
const startArr = [] // 冻结左侧的列
const midArr = [] // 没有冻结的列
const endArr = [] // 冻结右侧的列(只能是action,暂时不能自定义固定右侧)
columns.forEach(col => {
const key = getKey(col)
// 如果不需要配置冻结列,就按表头本身传的参数决定固定列
const fixed = this.needSettingColumnHideOrFreeze ? this.settingColumnsObj.freeze.includes(key) : col.fixed
col.hide = this.settingColumnsObj.hide.includes(key)
col.fixed = fixed
// 如果是字符串就转换成数字(因为实现拖拽,单位只能是px)
col.width = col.width && parseInt(col.width + '')
// 如果没有最小宽度就获取统一设置的最小宽度
col.minWidth = (col.minWidth && parseInt(col.minWidth + '')) || this.columnMinWidth
// 如果没有配置align,就默认居中
col.align = col.align || 'center'
if (!col.scopedSlots) {
col.scopedSlots = {
customRender: 'text'
}
}
if (!col.hide) {
// 如果没有宽度就取minWidth(不添加原数据的width属性,这里只对需要固定的minWidth生效)
const width = col.width || col.minWidth
switch (col.fixed) {
case true:
case 'left':
startArr.push({ ...col, width })
break
case 'right':
endArr.push({ ...col, width })
break
default:
midArr.push(col)
}
}
})
// 更新父组件的数据,将排序后的进行渲染
this.$emit('update:columns', columns)
// 按冻结顺序排序
this.resultColumns = [
...startArr.sort((a, b) => {
const freezeArr = this.settingColumnsObj.freeze
return freezeArr.indexOf(getKey(a)) - freezeArr.indexOf(getKey(b))
}),
...midArr,
...endArr
]
return this.resultColumns
},
// 初始化表格拖拽
initDrag (columns) {
// 根据配置项决定是否可以拖拽,本身是根据是否有边框配置的,但是有边框且有固定操作列的情况下,拖动会有一个空白列
if (!this.canDrag) {
return
}
// 如果没有任何列开启拖拽就不需要拖拽
// if (!columns.some(col => col.resizable)) {
// return
// }
// 第一步:列宽映射
const draggingMap = {}
columns.forEach((col) => {
draggingMap[getKey(col)] = col.width
})
const draggingState = Vue.observable(draggingMap)
// 第二步:表头渲染
return (h, props, children) => {
// 表头DOM
let thDom = null
// 获取列的key值和特性
const { key, ...restProps } = props
// 获取最新列配置
const columns = cloneDeep(this.columns)
let col
if (key === 'selection-column') {
col = {}
} else {
col = columns.find(col => getKey(col) === key) || {}
}
// 没有开启拖拽 或 没有宽度 或 有定位,都不能拖拽(防止布局异常,至少有一列不设width并且不能有fixed)
if (!col.width || !!col.fixed) {
return <th {...restProps}>{children}</th>
}
// 开始拖拽监听
const onDragging = (x) => {
const beforeWidth = col.width
const scroll = cloneDeep(this.scroll) || {}
draggingState[key] = 0
col.width = Math.max(x, col.minWidth || this.columnMinWidth)
// 如果有scroll.x就计算差值,否则获取所有列的和
if (scroll.x) {
scroll.x = scroll.x + (col.width - beforeWidth)
} else {
scroll.x = 0
this.columns.forEach(col => (scroll.x += (col.width || col.minWidth || this.columnMinWidth)))
}
// 将计算好的宽度更新
if (this.scroll) {
this.$emit('update:scroll', scroll)
} else {
this.selfScroll = scroll
}
// 解决拖拽重置其他单元格宽度的问题
const colIndex = this.resultColumns.findIndex(tt => getKey(tt) === key)
const nowResultColumns = cloneDeep(this.resultColumns)
nowResultColumns.splice(colIndex, 1, col)
// 修改列宽后,更新表格字段
this.setResultColumns(nowResultColumns)
}
// 停止拖拽监听
const onDragstop = () => {
draggingState[key] = thDom.getBoundingClientRect().width
}
// 控制最小拖拽宽度
const onDrag = (x) => {
return x >= (col.minWidth || this.columnMinWidth)
}
// 解决都有dataIndex时会有横向滚动条的问题,让最后一列的拖拽容器宽度为0
return (
<th
{...restProps}
v-ant-ref={(r) => (thDom = r)}
width={col.width}
class="resize-table-th"
>
{children}
<vue-draggable-resizable
key={getKey(col)}
class="table-draggable-handle"
minw={10}
w={10}
x={col.width || draggingState[key]}
z={1}
axis="x"
draggable={true}
resizable={false}
props={{ onDrag }}
onDragging={onDragging}
onDragstop={onDragstop}
></vue-draggable-resizable>
</th>
)
}
},
/**
* 配置表中复选框改变状态的事件
* @param text 修改前的状态
* @param type 隐藏还是冻结
* @param record 当前行数据
*/
changeSetting (text, type, record) {
const checked = !text // text是点击前的状态,取反就是要修改的状态
// console.log('修改了配置', checked, type, record)
// 修改当前行配置
record[type] = !checked
// 勾选就添加,取消就删除
if (checked) {
this.settingColumnsObj[type].push(record.key)
} else {
this.settingColumnsObj[type].splice(this.settingColumnsObj[type].findIndex(item => item === record.key), 1)
}
this.saveSetting()
/**
* 弹框消失问题 冻结列出现和消失会导致dom结构改变而从更新结构丢失弹框
* 解决方案,刷新前获取dom,刷新后再次获取,如果获取不一致就触发点击事件点开弹框
*/
const beforeIsFixed = !!this.$refs.table.$el.querySelector('.ant-table-fixed-left')
this.$nextTick(() => {
const afterIsFixed = !!this.$refs.table.$el.querySelector('.ant-table-fixed-left')
// console.log(beforeIsFixed !== afterIsFixed)
if (beforeIsFixed !== afterIsFixed) {
this.$refs.settingBtn.$el.click()
}
})
},
// 初始化配置,从columns里获取
initSetting (columns) {
const hide = []
const freeze = []
columns.forEach(col => {
const key = getKey(col)
// hide属性控制隐藏
if (col.hide) {
hide.push(key)
}
// fixed冻结到左侧
if (col.fixed === true || col.fixed === 'left') {
freeze.push(key)
}
})
this.settingColumnsObj = { hide, freeze }
this.saveSetting()
},
// 配置保存到本地
saveSetting () {
if (!this.needSaveSettingInLocal) {
return
}
// 获取所有的jtable缓存的key数组(用于清除缓存,没有就初始化数组)
const jTableKeys = Vue.ls.get(J_TABLE_KEYS) || []
const saveKey = this.tableKey + saveSuffix
// 如果不在数组里面就追加进去
if (!jTableKeys.includes(saveKey)) {
jTableKeys.push(saveKey)
}
Vue.ls.set(J_TABLE_KEYS, jTableKeys, 7 * 24 * 60 * 60 * 10)
Vue.ls.set(saveKey, this.settingColumnsObj, 7 * 24 * 60 * 60 * 10)
},
// 还原默认配置
resteColumns () {
this.initSetting(this.columnsBak)
},
// 清除本地的配置(当前的JTable)
clearSetting () {
this.settingColumnsObj = { hide: [], freeze: [] }
Vue.ls.remove(this.tableKey + saveSuffix)
this.resteColumns()
this.$message.success('成功清除当前JTable的缓存!')
},
// 清除所有缓存
clearAllCacheSetting () {
// 获取到之后遍历删除,最后把keys数组删除
const jTableKeys = Vue.ls.get(J_TABLE_KEYS)
jTableKeys && jTableKeys.forEach(key => {
Vue.ls.remove(key)
})
Vue.ls.remove(J_TABLE_KEYS)
this.resteColumns() // 这里只能刷新当前的表
this.$message.success('成功清除全局JTable的缓存!')
},
getLineOperationList (record) {
const hasPermissionList = this.operationList.filter(tt => !tt.has || isHasPermission(tt.has))
const lineOperateList = []
hasPermissionList.forEach(item => {
if (!item.show) {
lineOperateList.push(item)
} else if (typeof item.show === 'function') {
if (item.show(record)) {
lineOperateList.push(item)
}
}
})
return lineOperateList
}
},
mounted () {
// 备份默认配置
this.columnsBak = cloneDeep(this.columns)
let settingColumnsObj
if (this.needSaveSettingInLocal) {
settingColumnsObj = Vue.ls.get(this.tableKey + saveSuffix)
}
// 第一次进页面或清空缓存进行初始化
if (settingColumnsObj) {
this.settingColumnsObj = settingColumnsObj
} else {
this.initSetting(this.columns)
}
},
created () {
}
}
</script>
<style lang="less">
.j-table {
.resize-table-th {
position: relative;
// 让拖动的地方有个线
border-right: 1px #e8e8e8 solid;
.table-draggable-handle {
transform: none !important;
position: absolute !important;
height: 100% !important;
bottom: 0;
left: auto !important;
right: -5px;
//width: 10px !important;
cursor: col-resize;
touch-action: none;
}
}
// 解决每行都有dataIndex时会有横向滚动条的问题
.resize-table-th:last-child {
border-right: none;
.table-draggable-handle {
width: 0px !important;
min-width: 0px !important;
}
}
}
</style>