当前位置: 首页 > news >正文

K-VXE-TABLE二次封装,含table‘自定义列功能

1.k-vxeTable表格二次封装(KVxeTable.vue文件)

<template><div class="table_k" :class="{ 'drag-no-select': didDrag }" :style="didDrag ? dragNoSelectStyle : null"><div style="flex: 1; overflow: hidden"><vxe-tableresizable:border="border"auto-resizesize="mini"show-overflowshow-header-overflowref="xTable2"row-id="id":data="dataSource":loading="loading":scroll-y="{ gt: 15, mode: 'wheel' }":checkbox-config="selectType == 'checkbox' ? { reserve: true, range: true, highlight: true, trigger: 'row' } : {}":radio-config="selectType == 'radio' ? { trigger: 'row' } : {}"@checkbox-change="onSelectChange"@checkbox-all="onSelectChange"@radio-change="radioChange"@cell-click="onCellClick"@cell-dblclick="onCellDblclick"@sort-change="onSortChange"@checkbox-range-end="onSelectChange":height="tableHeight":max-height="maxHeight":key="renderKey":row-style="isRowStyle ? rowStyle : {}":header-cell-style="headerCellStyle":cell-style="cellStyle":edit-config="editConfig"><vxe-table-column type="checkbox" v-if="selectType == 'checkbox'" width="40" fixed="left" align="center">{{null}}</vxe-table-column><vxe-table-column type="radio" v-if="selectType == 'radio'" width="40" fixed="left" align="center">{{null}}</vxe-table-column><vxe-table-columntype="seq"width="60"v-if="showSeq"title="序号"fixed="left"align="center"></vxe-table-column><!-- 固定列:紧跟在序号列之后 --><template v-for="(item, rowIndex) in fixedColumns"><vxe-table-columnv-if="!item.slotName && item.checked":key="'fixed-' + item.key":field="item.key":width="item.width":sortable="item.sortable"fixed="left":align="item.align"><template v-slot:header><a-popover:ref="`popover-fixed-${item.key}`"trigger="click"placement="bottom":overlayClassName="getPopoverClass(item.key)":getPopupContainer="getPopoverContainer"@visibleChange="onFixedPopoverVisibleChange(item.key, $event)"><template slot="content"><div class="copy-content"><a-buttontype="primary"size="small"@mousedown.stop.prevent="copyColumnData(item.key)"icon="copy">复制</a-button></div></template><span class="header-clickable" @click.stop="onHeaderClick(item.key, 'fixed')">{{ item.title }}</span></a-popover></template></vxe-table-column><slot v-else :name="item.slotName" :item="item" :rowIndex="rowIndex"></slot></template><!-- 普通列:不固定 --><template v-for="(item, rowIndex) in normalColumns"><vxe-table-columnv-if="!item.slotName && item.checked":key="'normal-' + item.key":field="item.key":width="item.width":sortable="item.sortable":align="item.align"><template v-slot:header><a-popover:ref="`popover-normal-${item.key}`"trigger="click"placement="bottom"overlayClassName="column-copy-popover":getPopupContainer="getPopoverContainer"@visibleChange="onNormalPopoverVisibleChange(item.key, $event)"><template slot="content"><div class="copy-content"><a-buttontype="primary"size="small"@mousedown.stop.prevent="copyColumnData(item.key)"icon="copy">复制</a-button></div></template><span class="header-clickable" @click.stop="onHeaderClick(item.key, 'normal')">{{ item.title }}</span></a-popover></template></vxe-table-column><slot v-else :name="item.slotName" :item="item" :rowIndex="rowIndex"></slot></template></vxe-table></div><div class="page_footer"><div class="page_footer_left"><slot name="pageFooterLeft"></slot></div><div class="page_footer_right"><a-button class="page_footer_right_zBtn" size="default" icon="setting" v-if="setCol" @click="setColumnsMethods">自定义列</a-button><vxe-pagerv-if="hasVxePage"border:loading="loading":current-page="tablePage.pageNo":page-size="tablePage.pageSize":total="tablePage.totalResult":layouts="layouts":page-sizes="pageSizes"@page-change="handlePageChange"></vxe-pager></div></div></div>
</template>
<script>
import { postAction } from '@/api/manage'
export default {props: {api: {type: Function,},columnsProps: {type: Array,default: () => [],},hasVxePage: {type: Boolean,default: true,},setCol: {type: Boolean,default: true,},showSeq: {type: Boolean,default: true,},overlayStyle: {type: Object,default: () => ({ width: '1000px' }),},selectType: {type: String,default: 'checkbox',},dataSourceProps: {type: Array,default: () => [],},tableHeight: {type: String,default: 'auto',},imm: {type: Boolean,default: false,},pageSizes: {type: Array,default: () => [20, 50, 100, 200, 500],},maxHeight: {type: Number,default: 0,},columnsMenu: {type: String,},isRowStyle: {type: Boolean,default: false,},// 是否启用区域选择功能(类似Excel的拖拽选择)enableAreaSelect: {type: Boolean,default: true,},getRowStyle: {type: Function,},border: {type: Boolean,default: true,},layouts: {type: Array,default: () => ['PrevPage', 'JumpNumber', 'NextPage', 'FullJump', 'Sizes', 'Total'],},tablePage: {type: Object,default: () => ({pageNo: 1,pageSize: 200,totalResult: 0,}),},editConfig: {type: Object,},},data() {return {renderKey: 1,dataSource: [],columns: [],loading: false,// tablePage: {//   pageNo: 1,//   pageSize: 20,//   totalResult: 0// },otherParams: {},selectRows: [],// 拖拽选择相关状态dragSelecting: false,startRowIndex: null,currentRowIndex: null,selectedRowIndexes: [], // 存储选中的行索引highlightedColumnKey: null, // 当前高亮显示的列keycolumnPopoverVisible: {}, // 各列复制弹层显隐状态didDrag: false, // 是否发生了拖拽(区分单击与拖动)dragNoSelectStyle: {userSelect: 'none',WebkitUserSelect: 'none',MozUserSelect: 'none',msUserSelect: 'none',},isMouseUpInit: false,}},computed: {// 固定列:需要固定在左侧的列fixedColumns() {const fixed = this.columns.filter((col) => col.fixed === 'left')return fixed},// 普通列:不固定的列normalColumns() {const normal = this.columns.filter((col) => col.fixed !== 'left')return normal},},created() {if (this.dataSourceProps.length > 0) {this.dataSource = this.dataSourceProps}},mounted() {// 初始化拖拽选择功能if (this.enableAreaSelect && !this.setCol) {this.initDragSelectEvents()}},beforeDestroy() {// 清理事件监听器this.removeDragSelectEvents()},watch: {dataSourceProps: {handler(val) {this.dataSource = val// 数据更新后重新初始化事件监听if (this.enableAreaSelect) {this.$nextTick(() => {this.initDragSelectEvents()})}},deep: true,},setCol: {handler(val) {const columns = JSON.parse(JSON.stringify(this.columnsProps)).map((item) => {return {...item,checked: item.hasOwnProperty('checked') ? item.checked : true,isEditWidth: item.hasOwnProperty('isEditWidth') ? item.isEditWidth : false,draggable: item.hasOwnProperty('draggable') ? item.draggable : true,fixed: item.hasOwnProperty('fixed') ? item.fixed : false,}})if (val) {this.getColumns(columns)} else {this.columns = this.normalizeColumns(columns)// 未走远端配置时,本地直接更新列也可能触发重渲染,需重新绑定拖拽事件if (this.enableAreaSelect) {this.$nextTick(() => {this.initDragSelectEvents()})}}},immediate: true,},// 新增:当列定义本身变更时(例如父组件动态传入),也需要重绑事件columnsProps: {handler() {if (this.enableAreaSelect) {this.$nextTick(() => {this.initDragSelectEvents()})}},deep: true,},// 新增:选择类型变化(checkbox/radio)会改变列结构,需要重绑事件selectType(val) {if (this.enableAreaSelect) {this.$nextTick(() => {this.initDragSelectEvents()})}},// 新增:显示序号列的开关变化也会影响列结构showSeq(val) {if (this.enableAreaSelect) {this.$nextTick(() => {this.initDragSelectEvents()})}},// 新增:开启拖拽选择后立即初始化;(关闭时如需更彻底可在此移除监听)enableAreaSelect(val) {if (val) {this.$nextTick(() => {this.initDragSelectEvents()})} else {// 当禁用区域选择时,移除所有相关事件监听器this.removeDragSelectEvents()}},},methods: {async loadData() {this.loading = trueconst { data, code, message, result } = await this.api({...this.tablePage,totalResult: undefined,...this.otherParams,})if (code == 200) {if (data) {this.dataSource = []if (data.list) {this.dataSource = data?.list || []this.tablePage.totalResult = Number(data.total)}if (data.pageResult) {this.dataSource = Array.isArray(data.pageResult.list) ? data.pageResult.list : []this.tablePage.totalResult = Number(data.pageResult.total)this.rawData = data}} else if (result.records) {this.dataSource = result?.records || []this.tablePage.totalResult = Number(result.total)} else {this.dataSource = result || []this.tablePage.totalResult = Number(result.total)}} else {this.$message.error(message)}this.loading = false// 数据加载后,表格DOM可能被重建,需重新绑定拖拽事件if (this.enableAreaSelect) {this.$nextTick(() => {this.initDragSelectEvents()})}},// 统一列配置:强制居中、强制可排序,保留 fixed 与 slotNamenormalizeColumns(list = []) {try {return (Array.isArray(list) ? list : []).map((col) => {const next = { ...col }next.align = 'center'next.sortable = truereturn next})} catch (e) {return list}},onSearch(obj = {}, type = 'search') {this.$refs.xTable2.scrollTo(0, 0)this.$refs.xTable2.clearCheckboxReserve()this.otherParams = objthis.selectRows = []this.tablePage.pageNo = type == 'search' ? this.tablePage.pageNo : 1this.loadData()},handlePageChange(val) {this.tablePage.pageNo = val.currentPagethis.tablePage.pageSize = val.pageSizethis.onSearch(this.otherParams)},onSelectChange({ records }) {this.selectRows = recordsthis.$emit('selectRowsMethods', records)},radioChange({ row }) {this.selectRows = [row]this.$emit('selectRowsMethods', [row])},clearSelectRows() {this.selectRows = []this.$emit('selectRowsMethods', [])},clearDataSource() {this.dataSource = []this.clearSelectRows()// 清空数据也可能导致DOM重绘,重绑拖拽事件if (this.enableAreaSelect) {this.$nextTick(() => {this.initDragSelectEvents()})}},setColumnsMethods() {this.$createPopup({ctor: () => import('./setColumnsDialog.vue'),props: {columnsList: this.columns,},on: {success: (col) => {this.saveColumns(col)},},})},// ========== VXE Table 3.6 兼容的拖拽多选实现 ==========// 单元格点击事件 - 用于测试onCellClick({ row, column, rowIndex, columnIndex, event }) {// 单元格点击处理逻辑},// 单元格双击:复制该单元格内容onCellDblclick({ row, column }) {const key = column.property || column.fieldif (!key) returnconst value = this.getValueByPath(row, key)const text = value !== null && value !== undefined ? String(value) : ''if (!text) {this.$message.warning('该单元格无内容可复制')return}// 使用现代或兼容方式复制if (navigator.clipboard && window.isSecureContext) {navigator.clipboard.writeText(text).then(() => this.$message.success('已复制内容到剪贴板')).catch(() => this.fallbackCopy(text))} else {this.fallbackCopy(text)}},fallbackCopy(text) {const textArea = document.createElement('textarea')textArea.value = texttextArea.style.position = 'fixed'textArea.style.left = '-999999px'textArea.style.top = '-999999px'document.body.appendChild(textArea)textArea.focus()textArea.select()try {document.execCommand('copy')this.$message.success('已复制内容到剪贴板')} catch (err) {this.$message.error('复制失败')}document.body.removeChild(textArea)},// 表头点击:打开弹层并高亮该列onHeaderClick(columnKey, type) {this.highlightedColumnKey = columnKey// 普通列行为:不受控,仅在显示后做一次去重this.$nextTick(() => setTimeout(() => this.ensureSinglePopover(columnKey), 50))},// 排序变化时,重新初始化拖拽选择并同步选中行onSortChange() {const table = this.$refs.xTable2// 记录当前已选中的记录(基于 id)let selected = []if (table) {selected = this.getSelectedRows() || []}const selectedIds = selected.map((r) => (r && r.id != null ? String(r.id) : null)).filter(Boolean)// 重置拖拽与索引状态,避免沿用排序前的索引this.dragSelecting = falsethis.didDrag = falsethis.startRowIndex = nullthis.currentRowIndex = nullthis.selectedRowIndexes = []// 重新初始化拖拽(DOM结构可能变化)并基于排序后顺序恢复选择if (this.enableAreaSelect) {this.$nextTick(() => {this.initDragSelectEvents()const processed = this.getProcessedData()// 基于 id 在排序后数据中重新定位索引if (selectedIds.length) {const reIndexes = []const reRows = []processed.forEach((row, idx) => {const idStr = row && row.id != null ? String(row.id) : nullif (idStr && selectedIds.includes(idStr)) {reIndexes.push(idx)reRows.push(row)}})this.selectedRowIndexes = reIndexes// 根据选择类型恢复 vxe 的选中状态if (table) {if (this.selectType === 'checkbox') {if (table.clearCheckboxRow) table.clearCheckboxRow()if (table.setCheckboxRow) {reRows.forEach((row) => table.setCheckboxRow(row, true))}} else if (this.selectType === 'radio' && reRows.length === 1) {if (table.setRadioRow) table.setRadioRow(reRows[0])}}}})}},// 弹层显示/隐藏:显示时保持高亮,隐藏时清除高亮onColumnPopoverVisibleChange(popoverKey, columnKey, visible) {// 仅处理固定列,普通列不做强控if (!String(popoverKey).startsWith('fixed-')) returnthis.$nextTick(() => {setTimeout(() => this.ensureSinglePopover(columnKey), 50)})if (visible) {this.highlightedColumnKey = columnKey} else if (this.highlightedColumnKey === columnKey) {this.highlightedColumnKey = null}},// 只保留当前列的第一个弹层显示ensureSinglePopover(columnKey) {try {const cls = this.getPopoverClass(columnKey)const nodes = Array.from(document.querySelectorAll(`.${cls}`))// 弹层容器为 ant-popover,需要定位到容器节点const popovers = nodes.map((node) => node.closest('.ant-popover')).filter(Boolean)// 清理历史内联 display 样式,避免“第一次隐藏后永久隐藏”的问题popovers.forEach((node) => node.style.removeProperty('display'))// 过滤掉处于 hidden/透明的动画中节点,仅保留当前真正可见的const visiblePopovers = popovers.filter((node) => {const cs = window.getComputedStyle(node)const isHiddenClass = node.classList.contains('ant-popover-hidden')return cs.visibility !== 'hidden' && !isHiddenClass})// 如果没有其它弹层打开,且当前这个保持打开状态,则无需处理if (visiblePopovers.length <= 1) return// 只保留第一个,其它设置为 display:nonevisiblePopovers.slice(1).forEach((node) => {node.style.display = 'none'})} catch (e) {}},// 查询当前列弹层是否显示isColumnPopoverVisible(columnKey) {return !!this.columnPopoverVisible[columnKey]},// 关闭列弹层并清理高亮closeColumnPopover(columnKey) {const popover = document.querySelectorAll('.vxe-table-column-copy-popover')if (popover.length > 1) {popover[0].style.display = 'none'}this.$set(this.columnPopoverVisible, columnKey, false)if (this.highlightedColumnKey === columnKey) {this.highlightedColumnKey = null}},// 头部单元格样式:保持原有样式并在选中列高亮headerCellStyle({ column }) {const key = column && (column.property || column.field)const style = { fontWeight: '400', color: 'rgb(56,58,64)' }if (this.highlightedColumnKey && key === this.highlightedColumnKey) {style.backgroundColor = '#fff7e6'}return style},// 内容单元格样式:选中列高亮cellStyle({ column }) {const key = column && (column.property || column.field)if (this.highlightedColumnKey && key === this.highlightedColumnKey) {return { backgroundColor: '#fff7e6' }}return {}},// 指定 Popover 挂载容器,避免固定列区域造成定位或遮挡问题getPopoverContainer(trigger) {return document.body},// 初始化DOM事件监听initDragSelectEvents() {// 确保全局鼠标抬起事件监听器被添加if (!this.isMouseUpInit) {this.initMouseUp()}this.$nextTick(() => {const table = this.$refs.xTable2if (!table || !table.$el) {return}// 主体与左右固定区域的 tbodyconst tbodys = []const mainBody = table.$el.querySelector('.vxe-table--body-wrapper .vxe-table--body tbody')if (mainBody) tbodys.push(mainBody)const leftBody = table.$el.querySelector('.vxe-table--fixed-left-wrapper .vxe-table--body tbody')if (leftBody) tbodys.push(leftBody)const rightBody = table.$el.querySelector('.vxe-table--fixed-right-wrapper .vxe-table--body tbody')if (rightBody) tbodys.push(rightBody)// 移除旧的事件监听器(避免重复添加)tbodys.forEach((body) => {body.removeEventListener('mousedown', this.onTableMouseDown)body.removeEventListener('mouseover', this.onTableMouseOver)body.removeEventListener('mousemove', this.onTableMouseMove)})// 添加事件监听器到每个区域tbodys.forEach((body) => {body.addEventListener('mousedown', this.onTableMouseDown)body.addEventListener('mouseover', this.onTableMouseOver)// 添加 mousemove 以兼容火狐浏览器body.addEventListener('mousemove', this.onTableMouseMove)})// 获取滚动容器,仅需监听主滚动容器const bodyWrapper = table.$el.querySelector('.vxe-table--body-wrapper')if (bodyWrapper) {bodyWrapper.removeEventListener('scroll', this.onTableScroll)bodyWrapper.addEventListener('scroll', this.onTableScroll)}})},// 表格鼠标按下事件onTableMouseDown(event) {if (!this.enableAreaSelect || this.selectType !== 'checkbox') returnconst cellElement = this.findCellElement(event.target)if (!cellElement) returnconst { rowIndex, columnIndex } = this.getCellIndexes(cellElement)if (rowIndex === -1 || columnIndex === -1) return// 检查是否是数据列(排除checkbox、序号列)if (!this.isDataColumn(columnIndex)) return// 阻止默认行为,防止火狐浏览器触发原生拖放行为event.preventDefault()this.dragSelecting = truethis.didDrag = falsethis.startRowIndex = rowIndexthis.currentRowIndex = rowIndexthis.selectedRowIndexes = [rowIndex]this.$emit('drag-select-start', { startRowIndex: this.startRowIndex })},// 表格鼠标悬停事件onTableMouseOver(event) {if (!this.enableAreaSelect || !this.dragSelecting) returnconst cellElement = this.findCellElement(event.target)if (!cellElement) returnconst { rowIndex, columnIndex } = this.getCellIndexes(cellElement)if (rowIndex === -1 || columnIndex === -1) return// 检查是否是数据列if (!this.isDataColumn(columnIndex)) returnif (rowIndex !== this.currentRowIndex) {this.didDrag = truethis.currentRowIndex = rowIndexthis.updateSelectedRows()// 拖拽过程中立即应用到视图this.selectCheckboxRows()}},// 表格鼠标移动事件(用于火狐浏览器兼容)onTableMouseMove(event) {// 复用 mouseover 的逻辑this.onTableMouseOver(event)},// 文档鼠标抬起事件onDocumentMouseUp() {if (this.dragSelecting) {this.endDragSelection()}},// 处理表格滚动事件onTableScroll(event) {// 在滚动时如果正在拖拽选择,需要更新选择状态if (this.dragSelecting) {// 拖拽过程中滚动也需要同步选中状态到视图this.selectCheckboxRows()}},// 查找单元格元素findCellElement(target) {let element = targetwhile (element && !element.classList.contains('vxe-body--column')) {element = element.parentElementif (element && element.tagName === 'TBODY') break}return element && element.classList.contains('vxe-body--column') ? element : null},// 获取单元格的行列索引(考虑虚拟滚动)getCellIndexes(cellElement) {const row = cellElement.parentElementif (!row) return { rowIndex: -1, columnIndex: -1 }// 获取实际的数据行索引(处理虚拟滚动)const actualRowIndex = this.getActualRowIndex(row)const columnIndex = Array.from(row.children).indexOf(cellElement)return { rowIndex: actualRowIndex, columnIndex }},// 获取处理后的(排序/筛选后)全量数据getProcessedData() {try {const table = this.$refs.xTable2if (!table) return this.dataSourceconst tableData = typeof table.getTableData === 'function' ? table.getTableData() : nullif (tableData) {if (Array.isArray(tableData.visibleData) && tableData.visibleData.length === this.dataSource.length) {return tableData.visibleData}if (Array.isArray(tableData.fullData)) return tableData.fullData}} catch (e) {return this.dataSource}},// 获取实际的可见数据索引(处理虚拟滚动&排序)getActualRowIndex(rowElement) {try {const table = this.$refs.xTable2if (!table) return -1// 获取当前DOM中的行索引const tbody = rowElement.parentElementconst domRowIndex = Array.from(tbody.children).indexOf(rowElement)// 计算可见数据索引:虚拟滚动时加上开始偏移let visibleIndex = domRowIndexif (table.scrollYLoad) {const scrollYStore = table.scrollYStore || {}const startIndex = scrollYStore.startIndex || 0visibleIndex = startIndex + domRowIndex}const processed = this.getProcessedData()if (visibleIndex >= 0 && visibleIndex < processed.length) {return visibleIndex}return -1} catch (error) {return -1}},// 通过DOM索引获取行数据getRowDataFromDOMIndex(domIndex) {try {const table = this.$refs.xTable2if (!table) return nullconst tableData = table.getTableData()const visibleData = tableData.visibleData || tableData.fullData || this.dataSourcereturn visibleData[domIndex] || null} catch (error) {return null}},// 检查是否是数据列isDataColumn(columnIndex) {let dataColumnStartIndex = 0if (this.selectType === 'checkbox' || this.selectType === 'radio') dataColumnStartIndex++if (this.showSeq) dataColumnStartIndex++return columnIndex >= dataColumnStartIndex},// 结束拖拽选择endDragSelection() {this.dragSelecting = false// 仅当发生拖拽时,才应用拖拽选择,避免覆盖默认点击选择交互if (this.didDrag) {this.selectCheckboxRows()}// 在选择同步到表格后,若有选中项则触发 selectRowsMethodsthis.$nextTick(() => {const records = this.getSelectedRows()if (records && records.length) {this.selectRows = recordsthis.$emit('selectRowsMethods', records)}// 触发选择完成事件this.$emit('drag-select-end', {selectedRowIndexes: this.selectedRowIndexes,rowCount: this.selectedRowIndexes.length,})})// 恢复文本选择this.didDrag = false},// 更新选中的行updateSelectedRows() {if (this.startRowIndex === null || this.currentRowIndex === null) returnconst minRow = Math.min(this.startRowIndex, this.currentRowIndex)const maxRow = Math.max(this.startRowIndex, this.currentRowIndex)this.selectedRowIndexes = []for (let rowIndex = minRow; rowIndex <= maxRow; rowIndex++) {this.selectedRowIndexes.push(rowIndex)}},// 选中checkbox行(基于排序后的可见数据进行索引映射)selectCheckboxRows() {if (this.selectType !== 'checkbox' || this.selectedRowIndexes.length === 0) {return}const processed = this.getProcessedData()const rowsToSelect = this.selectedRowIndexes.map((index) => processed[index]).filter((row) => row)this.$nextTick(() => {const table = this.$refs.xTable2if (table) {try {if (table.clearCheckboxRow) {table.clearCheckboxRow()}if (table.setCheckboxRow) {rowsToSelect.forEach((row) => {table.setCheckboxRow(row, true)})} else if (table.toggleCheckboxRow) {rowsToSelect.forEach((row) => {table.toggleCheckboxRow(row)})} else if (table.setAllCheckboxRow) {table.setAllCheckboxRow(false)rowsToSelect.forEach((row) => {if (table.setRowSelection) {table.setRowSelection(row, true)}})}} catch (error) {}}})},// 获取选中的行数据getSelectedRows() {const table = this.$refs.xTable2if (table && table.getCheckboxRecords) {return table.getCheckboxRecords()}return []},// 清除选择状态clearSelection() {this.selectedRowIndexes = []this.startRowIndex = nullthis.currentRowIndex = nullthis.dragSelecting = false// 清除checkbox选中状态const table = this.$refs.xTable2if (table && table.clearCheckboxRow) {table.clearCheckboxRow()}},//获取远程表头async getColumns(col, isRender = 0) {const strArr = this.$route.fullPath.split('/')let params = {menu: this.columnsMenu || strArr[strArr.length - 2],type: this.columnsMenu || strArr[strArr.length - 2],allColumnJson: JSON.stringify(col),}const res = await postAction('/sys/pColumnSetting/getColumn', params)if (res) {this.columns = this.normalizeColumns(res)if (isRender == 1) {this.renderKey++}} else {this.columns = this.normalizeColumns(col)}// 列配置更新后(可能触发重渲染),需要重新初始化拖拽选择事件if (this.enableAreaSelect) {this.$nextTick(() => {this.initDragSelectEvents()})}},//保存表头async saveColumns(columns) {const strArr = this.$route.fullPath.split('/')let params = {menu: this.columnsMenu || strArr[strArr.length - 2],type: this.columnsMenu || strArr[strArr.length - 2],content: JSON.stringify(columns),}const res = await postAction('/sys/pColumnSetting/setColumn', params)if (res.code == 200) {this.getColumns(columns, 1)} else {this.$message.error(res.message)}},//行内样式rowStyle({ row }) {if (this.isRowStyle) {return this?.getRowStyle(row)}return {}},// 复制列数据到剪贴板(按当前排序/筛选后的顺序)copyColumnData(columnKey) {const processed = this.getProcessedData()if (!processed || processed.length === 0) {this.$message.warning('暂无数据可复制')return}// 如果有勾选数据,则只复制勾选行;否则复制全部行let rowsToCopy = processedtry {const selected = this.getSelectedRows ? this.getSelectedRows() : []if (Array.isArray(selected) && selected.length > 0) {// 优先用 id 匹配以保持排序后的顺序const selectedIds = selected.map((r) => (r && r.id != null ? String(r.id) : null)).filter(Boolean)if (selectedIds.length > 0) {const idSet = new Set(selectedIds)rowsToCopy = processed.filter((r) => {const idStr = r && r.id != null ? String(r.id) : nullreturn idStr ? idSet.has(idStr) : false})} else {// 回退到引用匹配const selectedSet = new Set(selected)rowsToCopy = processed.filter((r) => selectedSet.has(r))}if (rowsToCopy.length === 0) {this.$message.warning('暂无选中数据可复制')return}}} catch (e) {}// 提取该列的所有数据(排序/筛选后顺序)const columnData = rowsToCopy.map((row) => {const value = this.getValueByPath(row, columnKey)return value !== null && value !== undefined ? String(value) : ''})const textToCopy = columnData.join('\n')if (navigator.clipboard && window.isSecureContext) {navigator.clipboard.writeText(textToCopy).then(() => {this.$message.success(`已复制 ${columnData.length} 条数据到剪贴板`)}).catch(() => {this.$message.error('复制失败')})} else {const textArea = document.createElement('textarea')textArea.value = textToCopytextArea.style.position = 'fixed'textArea.style.left = '-999999px'textArea.style.top = '-999999px'document.body.appendChild(textArea)textArea.focus()textArea.select()try {document.execCommand('copy')this.$message.success(`已复制 ${columnData.length} 条数据到剪贴板`)} catch (err) {this.$message.error('复制失败')}document.body.removeChild(textArea)}},// 强制关闭当前列相关的弹层,并清除高亮forceClosePopover(columnKey) {try {this.highlightedColumnKey = null// 固定列(带唯一类名)const fixedCls = this.getPopoverClass(columnKey)const fixedNodes = Array.from(document.querySelectorAll(`.${fixedCls}`))fixedNodes.map((node) => node.closest('.ant-popover')).filter(Boolean).forEach((node) => (node.style.display = 'none'))// 普通列(通用类名,通常只有一个可见)const normals = Array.from(document.querySelectorAll('.ant-popover.column-copy-popover'))normals.forEach((node) => (node.style.display = 'none'))} catch (e) {}},// 读取嵌套字段值(支持 a.b.c 形式)getValueByPath(obj, path) {if (!obj || !path) return undefinedif (obj.hasOwnProperty(path)) return obj[path]const segments = String(path).split('.')let cur = objfor (let i = 0; i < segments.length; i++) {if (cur == null) return undefinedcur = cur[segments[i]]}return cur},// 获取弹窗类名,用于控制单个弹窗的显示getPopoverClass(columnKey) {return `vxe-table-column-copy-popover-${columnKey}`},onFixedPopoverVisibleChange(columnKey, visible) {// 模拟普通列:不强控 visible,仅在显示时做一次去重if (visible) {this.highlightedColumnKey = columnKeythis.$nextTick(() => setTimeout(() => this.ensureSinglePopover(columnKey), 50))} else {if (this.highlightedColumnKey === columnKey) this.highlightedColumnKey = null}},onNormalPopoverVisibleChange(columnKey, visible) {if (!visible) {this.highlightedColumnKey = null}},// 移除所有拖拽选择相关的DOM事件监听器removeDragSelectEvents() {if (this.isMouseUpInit) {document.removeEventListener('mouseup', this.onDocumentMouseUp)this.isMouseUpInit = false}const table = this.$refs.xTable2if (table && table.$el) {// 主体与左右固定区域的 tbodyconst tbodys = []const mainBody = table.$el.querySelector('.vxe-table--body-wrapper .vxe-table--body tbody')if (mainBody) tbodys.push(mainBody)const leftBody = table.$el.querySelector('.vxe-table--fixed-left-wrapper .vxe-table--body tbody')if (leftBody) tbodys.push(leftBody)const rightBody = table.$el.querySelector('.vxe-table--fixed-right-wrapper .vxe-table--body tbody')if (rightBody) tbodys.push(rightBody)// 移除所有区域的事件监听器tbodys.forEach((body) => {body.removeEventListener('mousedown', this.onTableMouseDown)body.removeEventListener('mouseover', this.onTableMouseOver)body.removeEventListener('mousemove', this.onTableMouseMove)})// 移除滚动容器的事件监听器const bodyWrapper = table.$el.querySelector('.vxe-table--body-wrapper')if (bodyWrapper) {bodyWrapper.removeEventListener('scroll', this.onTableScroll)}}// 重置拖拽状态this.dragSelecting = falsethis.didDrag = falsethis.startRowIndex = nullthis.currentRowIndex = nullthis.selectedRowIndexes = []},initMouseUp() {document.addEventListener('mouseup', this.onDocumentMouseUp)this.isMouseUpInit = true},},
}
</script>
<style lang="less" scoped>
.table_k {height: 100%;overflow: hidden;display: flex;flex-direction: column;
}
/deep/ .vxe-table--header {height: 34px;
}
/deep/ .vxe-body--row {height: 34px;
}
/deep/ .vxe-input.size--mini {height: 28px !important;line-height: 28rpx !important;
}
.row {width: 100%;.col {width: 25%;display: inline-block;}
}
.page_footer {display: flex;align-items: center;justify-content: space-between;width: 100%;.page_footer_left {flex: 1;}.page_footer_right {display: flex;align-items: center;}.page_footer_right_zBtn {margin: 5px 0;}
}
/deep/ .vxe-pager.is--border .vxe-pager--num-btn.is--active {color: #ffa500;border-color: #ffa500;box-shadow: none;background: #fff;
}
/deep/ .vxe-pager.is--border .vxe-pager--num-btn:hover {color: #ffa500;border-color: #ffa500;box-shadow: none;
}/* 表头点击样式 */
.header-clickable {cursor: pointer;display: inline-block;width: 100%;transition: color 0.2s;
}.header-clickable:hover {color: #ffa500;
}/* 拖拽中禁用文本选择(通过 didDrag 控制) */
.table_k.drag-no-select {-webkit-user-select: none !important;-moz-user-select: none !important;-ms-user-select: none !important;user-select: none !important;
}
/* 增大纵向滚动条宽度,仅作用于表格主体滚动容器 */
/deep/ .vxe-table--body-wrapper {scrollbar-width: auto; /* Firefox */
}
/deep/ .vxe-table--body-wrapper::-webkit-scrollbar {width: 12px; /* Chrome/Safari */
}
/deep/ .vxe-table--body-wrapper::-webkit-scrollbar-thumb {background-color: rgba(0, 0, 0, 0.25);border-radius: 8px;
}
/deep/ .vxe-table--body-wrapper::-webkit-scrollbar-track {background-color: transparent;
}
</style>

2.table自定义列封装(setColumnsDialog.vue)

a-select-option<template><KModal:visible="visible"title="自定义列-勾选需要显示的列,拖动列名进行排序"@onConfirm="onConfirm":width="900":bodyStyle="{ padding: '24px' }"><!-- 搜索框 --><div class="search_container"><a-input v-model="searchText" placeholder="请输入列名进行搜索" allowClear style="width: 300px"> </a-input></div><div class="container_list" ref="columnsRef"><!-- 空状态展示 --><div v-if="searchText && filteredColumns.length === 0" class="empty_state"><div class="empty_icon"><a-icon type="search" style="font-size: 48px; color: #d9d9d9" /></div><div class="empty_text"><p>未找到匹配的列</p><p class="empty_tip">请尝试输入其他关键词</p></div></div><!-- 列表内容 --><template v-for="item in filteredColumns"><div class="each_item" :key="item.key" v-if="item.slotName != 'oprate'" :data-draggable="item.draggable"><div class="left"><a-checkbox v-model="item.checked"></a-checkbox></div><div class="right"><div class="r_l">{{ item.title }}<a-icon type="drag" /></div><div class="r_m"><span>固定</span><a-checkbox:checked="item.fixed === 'left'"@change="(e) => handleFixedChange(item, e.target.checked)"></a-checkbox></div><div class="r_r"><span>宽度</span><a-input-numberstyle="width: 80px !important; margin: 0 6px"size="small"v-model="item.width":min="50":max="10000000":disabled="item.isEditWidth"></a-input-number><span>PX</span></div></div></div></template></div></KModal>
</template><script>
import KModal from '@/components/kComponents/KBase/KModal'
import Sortable from 'sortablejs'
export default {components: { KModal },props: {columnsList: {type: Array,default: () => [],},},data() {return {visible: false,checked: '',value: '',columns: [],showColumns: [],searchText: '', // 搜索文本}},computed: {// 根据搜索文本过滤列(只搜索列名)filteredColumns() {if (!this.searchText || this.searchText.trim() === '') {return this.columns}const searchLower = this.searchText.toLowerCase()return this.columns.filter((column) => {return column.title && column.title.toLowerCase().includes(searchLower)})},},//打开弹窗就调用onCreateApi() {this.visible = truethis.searchText = '' // 重置搜索框// 确保每个列都有固定属性的默认值this.columns = JSON.parse(JSON.stringify(this.columnsList)).map((item) => {return {...item,fixed: item.hasOwnProperty('fixed') ? item.fixed : false,}})setTimeout(() => {new Sortable(this.$refs.columnsRef, {draggable: '.each_item',animation: 150,onEnd: (e) => {const { oldIndex, newIndex } = econst spliceColumns = this.columns.splice(oldIndex, 1)this.columns.splice(newIndex, 0, spliceColumns[0])},onMove: (evt) => {return evt.dragged.getAttribute('data-draggable')},})}, 10)},//关闭弹窗就调用unCreateApi() {this.onCancel()},methods: {handleFixedChange(item, checked) {// 根据复选框状态设置固定值item.fixed = checked ? 'left' : false},async onConfirm() {if (JSON.stringify(this.columnsList) == JSON.stringify(this.columns)) {this.onCancel()} else {this.$emit('success', this.columns)this.onCancel()}},onCancel() {this.visible = false},},
}
</script><style lang="less" scoped>
.search_container {margin-bottom: 16px;
}.container_list {max-height: 495px;overflow: auto;border: 1px solid #ddd;.each_item {display: flex;align-items: center;justify-content: space-between;border-bottom: 1px solid #ddd;&:hover {background: #e9f2fb;}&:last-child {border-bottom: none;}.left {width: 50px;display: flex;align-items: center;justify-content: center;border-right: 1px solid #ddd;padding: 4px 0;}.right {display: flex;align-items: center;flex: 1;padding: 4px 10px;.r_l {flex: 1;margin-right: 20px;}.r_m {display: flex;align-items: center;margin-right: 20px;span {margin-right: 8px;font-size: 12px;}}.r_r {display: flex;align-items: center;}}}
}.empty_state {display: flex;flex-direction: column;align-items: center;justify-content: center;padding: 60px 20px;text-align: center;.empty_icon {margin-bottom: 16px;}.empty_text {p {margin: 0;color: #666;font-size: 14px;&.empty_tip {margin-top: 8px;color: #999;font-size: 12px;}}}
}
</style>

3.文件

4.效果

http://www.dtcms.com/a/506925.html

相关文章:

  • 基于 GEE 开发的一种利用 OTSU 算法实现水体提取的便捷工具
  • Linux小课堂: 深入解析 top、htop、glances 及进程终止机制
  • 建设协会网站洛阳伟创科技
  • MongoDB 提供的 `GridFSTemplate` 操作 GridFS 大文件系统的常用查询方式
  • 2025年ASOC SCI2区TOP,基于模糊分组的多仓库多无人机电力杆巡检模因算法,深度解析+性能实测
  • 无人机地面站中不同的飞行模式具体含义释义(开源飞控常用的5种模式)
  • Inventor 转换为 3DXML 全流程技术指南:附迪威模型网在线方案
  • Maven POM 简介
  • pytorch踩坑记录
  • seo每天一贴博客南宁网站排名优化电话
  • 手机端网站开发书籍徐州vi设计公司
  • STM32F1和STM32F4在配置硬件SPI1时有什么不同?
  • 衣柜灯橱柜灯MCU方案开发
  • 数据访问对象模式(Data Access Object Pattern)
  • 滚动显示效果
  • Spring Cloud - Spring Cloud 微服务概述 (微服务的产生与特点、微服务的优缺点、微服务设计原则、微服务架构的核心组件)
  • YOLOv4:目标检测领域的 “速度与精度平衡大师”
  • agent设计模式:第二章节—路由
  • 玩转Docker | 使用Docker安装uptime-kuma监控工具
  • flutter开发小结
  • 【运维】鲲鹏麒麟V10 操作系统aarch64自制OpenSSH 9.8p1 rpm包 ssh漏洞修复
  • react学习(五) ---- hooks
  • 【C语言】程序的编译和链接(基础向)
  • 基于单片机的热量计测量系统设计
  • 显卡功能及原理介绍
  • 丽水网站建设明恩玉杰百度网址导航
  • 时序数据库选型指南:从大数据视角看IoTDB的核心优势
  • 免费域名网站的网站后台用什么做
  • HTML应用指南:利用GET请求获取全国沃尔沃门店位置信息
  • WPF/C#:使用Microsoft Agent Framework框架创建一个带有审批功能的终端Agent