From 361297040b0410252fb5637416e0fded991841fb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 15 Nov 2025 13:41:18 +0000 Subject: [PATCH] feat: Add autoTableWidth directive for adaptive column widths Co-authored-by: ming10655 --- .../order-system/src/directive/index.js | 2 + .../module/autoTableWidth.example.vue | 113 ++++++ .../src/directive/module/autoTableWidth.js | 337 ++++++++++++++++++ .../src/directive/module/autoTableWidth.md | 159 +++++++++ 4 files changed, 611 insertions(+) create mode 100644 order-UI/packages/order-system/src/directive/module/autoTableWidth.example.vue create mode 100644 order-UI/packages/order-system/src/directive/module/autoTableWidth.js create mode 100644 order-UI/packages/order-system/src/directive/module/autoTableWidth.md diff --git a/order-UI/packages/order-system/src/directive/index.js b/order-UI/packages/order-system/src/directive/index.js index 4e2ee5685..bc8503733 100644 --- a/order-UI/packages/order-system/src/directive/index.js +++ b/order-UI/packages/order-system/src/directive/index.js @@ -4,6 +4,7 @@ import dialogDrag from './dialog/drag'; import dialogDragWidth from './dialog/dragWidth'; import dialogDragHeight from './dialog/dragHeight'; import clipboard from './module/clipboard'; +import autoTableWidth from './module/autoTableWidth'; const install = function (Vue) { Vue.directive('hasRole', hasRole); @@ -12,6 +13,7 @@ const install = function (Vue) { Vue.directive('dialogDrag', dialogDrag); Vue.directive('dialogDragWidth', dialogDragWidth); Vue.directive('dialogDragHeight', dialogDragHeight); + Vue.directive('autoTableWidth', autoTableWidth); }; if (window.Vue) { diff --git a/order-UI/packages/order-system/src/directive/module/autoTableWidth.example.vue b/order-UI/packages/order-system/src/directive/module/autoTableWidth.example.vue new file mode 100644 index 000000000..110ea8830 --- /dev/null +++ b/order-UI/packages/order-system/src/directive/module/autoTableWidth.example.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/order-UI/packages/order-system/src/directive/module/autoTableWidth.js b/order-UI/packages/order-system/src/directive/module/autoTableWidth.js new file mode 100644 index 000000000..a29659c9b --- /dev/null +++ b/order-UI/packages/order-system/src/directive/module/autoTableWidth.js @@ -0,0 +1,337 @@ +/** + * v-auto-table-width 表格内容宽度自适应指令 + * 自动根据表格内容调整列宽,使表格列宽适应内容 + * + * 使用方法: + * + * + * + * + * 或者带参数: + * + * + * + */ + +export default { + inserted(el, binding) { + const options = binding.value || {}; + const { + minWidth = 80, // 最小列宽 + maxWidth = 500, // 最大列宽 + padding = 20, // 列宽额外padding + excludeColumns = [], // 排除的列索引数组,这些列不参与自适应 + delay = 100 // 延迟执行时间(毫秒) + } = options; + + // 防抖函数 + let adjustTimer = null; + const adjustWidth = () => { + if (adjustTimer) { + clearTimeout(adjustTimer); + } + adjustTimer = setTimeout(() => { + adjustTableWidth(el, { minWidth, maxWidth, padding, excludeColumns }); + }, delay); + }; + + // 等待DOM渲染完成 + adjustWidth(); + + // 监听数据变化,重新计算宽度 + const observer = new MutationObserver(() => { + adjustWidth(); + }); + + observer.observe(el, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['style', 'class'] + }); + + // 保存observer到元素上,以便在unbind时清理 + el._autoTableWidthObserver = observer; + el._autoTableWidthTimer = adjustTimer; + + // 监听窗口大小变化 + const resizeHandler = () => { + adjustWidth(); + }; + + window.addEventListener('resize', resizeHandler); + el._autoTableWidthResizeHandler = resizeHandler; + + // 监听Vue组件数据更新 + const vueInstance = el.__vue__; + if (vueInstance) { + const originalUpdate = vueInstance.$forceUpdate || vueInstance.$nextTick; + if (vueInstance.$forceUpdate) { + const forceUpdate = vueInstance.$forceUpdate.bind(vueInstance); + vueInstance.$forceUpdate = function() { + forceUpdate(); + adjustWidth(); + }; + } + } + }, + + update(el, binding) { + // 当绑定值更新时,重新计算 + const options = binding.value || {}; + const { + minWidth = 80, + maxWidth = 500, + padding = 20, + excludeColumns = [], + delay = 100 + } = options; + + if (el._autoTableWidthTimer) { + clearTimeout(el._autoTableWidthTimer); + } + + el._autoTableWidthTimer = setTimeout(() => { + adjustTableWidth(el, { minWidth, maxWidth, padding, excludeColumns }); + }, delay); + }, + + unbind(el) { + // 清理timer + if (el._autoTableWidthTimer) { + clearTimeout(el._autoTableWidthTimer); + delete el._autoTableWidthTimer; + } + + // 清理observer + if (el._autoTableWidthObserver) { + el._autoTableWidthObserver.disconnect(); + delete el._autoTableWidthObserver; + } + + // 清理resize监听 + if (el._autoTableWidthResizeHandler) { + window.removeEventListener('resize', el._autoTableWidthResizeHandler); + delete el._autoTableWidthResizeHandler; + } + } +}; + +/** + * 调整表格列宽 + * @param {HTMLElement} tableEl - 表格元素 + * @param {Object} options - 配置选项 + */ +function adjustTableWidth(tableEl, options) { + const { minWidth, maxWidth, padding, excludeColumns } = options; + + // 获取表格的Vue实例 + const table = tableEl.__vue__; + if (!table) { + return; + } + + // 获取表格的thead和tbody + const thead = tableEl.querySelector('.el-table__header-wrapper thead'); + const tbody = tableEl.querySelector('.el-table__body-wrapper tbody'); + + if (!thead) { + return; + } + + const headerCells = Array.from(thead.querySelectorAll('th')); + + // 如果没有数据行,只根据表头计算 + const bodyRows = tbody ? Array.from(tbody.querySelectorAll('tr')) : []; + + if (headerCells.length === 0) { + return; + } + + // 计算每一列的最大宽度 + const columnWidths = []; + + headerCells.forEach((headerCell, colIndex) => { + // 跳过排除的列 + if (excludeColumns.includes(colIndex)) { + return; + } + + // 检查列是否已经设置了固定宽度(在计算宽度时检查) + const column = getColumnByIndex(table, colIndex); + if (column) { + const columnProps = column.$options.propsData || {}; + if (columnProps.width) { + // 如果列已经设置了固定宽度,跳过自适应 + return; + } + } + + // 获取表头宽度 + let maxColWidth = getElementContentWidth(headerCell); + + // 遍历所有数据行,获取该列的最大宽度 + bodyRows.forEach(row => { + const cells = row.querySelectorAll('td'); + if (cells[colIndex]) { + const cellWidth = getElementContentWidth(cells[colIndex]); + maxColWidth = Math.max(maxColWidth, cellWidth); + } + }); + + // 应用最小和最大宽度限制,并添加padding + const finalWidth = Math.max(minWidth, Math.min(maxWidth, maxColWidth + padding)); + columnWidths[colIndex] = finalWidth; + }); + + // 应用宽度到列组件 + columnWidths.forEach((width, colIndex) => { + if (excludeColumns.includes(colIndex) || !width) { + return; + } + + const column = getColumnByIndex(table, colIndex); + if (column) { + // 检查列是否已经设置了固定宽度 + const columnProps = column.$options.propsData || {}; + if (columnProps.width) { + // 如果列已经设置了固定宽度,跳过自适应 + return; + } + + // 更新column组件的width属性 + if (column.$options.propsData) { + column.$options.propsData.width = width; + } + // 更新column实例的width属性 + if (column.width !== undefined) { + column.width = width; + } + // 触发列更新 + if (column.$forceUpdate) { + column.$forceUpdate(); + } + } + }); + + // 直接更新DOM元素的宽度 + headerCells.forEach((cell, index) => { + if (excludeColumns.includes(index)) { + return; + } + + const width = columnWidths[index]; + if (width) { + cell.style.width = width + 'px'; + cell.style.minWidth = width + 'px'; + } + }); + + if (tbody) { + bodyRows.forEach(row => { + const cells = row.querySelectorAll('td'); + cells.forEach((cell, index) => { + if (excludeColumns.includes(index)) { + return; + } + + const width = columnWidths[index]; + if (width) { + cell.style.width = width + 'px'; + cell.style.minWidth = width + 'px'; + } + }); + }); + } +} + +/** + * 根据索引获取列组件 + * @param {VueComponent} table - 表格Vue实例 + * @param {number} index - 列索引 + * @returns {VueComponent|null} 列组件实例 + */ +function getColumnByIndex(table, index) { + const columns = []; + findTableColumns(table, columns); + return columns[index] || null; +} + +/** + * 递归查找所有el-table-column组件 + * @param {VueComponent} component - Vue组件实例 + * @param {Array} columns - 存储列的数组 + */ +function findTableColumns(component, columns) { + if (component.$options.name === 'ElTableColumn') { + columns.push(component); + } + + if (component.$children && component.$children.length > 0) { + component.$children.forEach(child => { + findTableColumns(child, columns); + }); + } +} + +/** + * 获取元素的实际内容宽度 + * @param {HTMLElement} element - DOM元素 + * @returns {number} 元素内容宽度 + */ +function getElementContentWidth(element) { + if (!element) { + return 0; + } + + // 获取计算后的样式 + const computedStyle = window.getComputedStyle(element); + + // 创建一个临时元素来测量内容宽度 + const tempEl = document.createElement('div'); + tempEl.style.cssText = ` + position: absolute; + visibility: hidden; + white-space: nowrap; + top: -9999px; + left: -9999px; + font-size: ${computedStyle.fontSize}; + font-family: ${computedStyle.fontFamily}; + font-weight: ${computedStyle.fontWeight}; + letter-spacing: ${computedStyle.letterSpacing}; + text-transform: ${computedStyle.textTransform}; + box-sizing: ${computedStyle.boxSizing}; + `; + + // 获取元素的文本内容 + let textContent = ''; + + // 如果是表头或单元格,获取其文本内容 + if (element.textContent) { + textContent = element.textContent.trim(); + } else if (element.innerText) { + textContent = element.innerText.trim(); + } + + // 如果元素包含其他元素(如按钮、图标等),需要计算这些元素的宽度 + let additionalWidth = 0; + if (element.children.length > 0) { + Array.from(element.children).forEach(child => { + const childStyle = window.getComputedStyle(child); + const childWidth = child.offsetWidth || + (parseFloat(childStyle.width) || 0) + + (parseFloat(childStyle.marginLeft) || 0) + + (parseFloat(childStyle.marginRight) || 0); + additionalWidth += childWidth; + }); + } + + tempEl.textContent = textContent || ' '; + document.body.appendChild(tempEl); + + const textWidth = tempEl.offsetWidth; + document.body.removeChild(tempEl); + + // 返回文本宽度加上额外元素宽度 + return Math.max(textWidth, additionalWidth); +} diff --git a/order-UI/packages/order-system/src/directive/module/autoTableWidth.md b/order-UI/packages/order-system/src/directive/module/autoTableWidth.md new file mode 100644 index 000000000..79b7a61ef --- /dev/null +++ b/order-UI/packages/order-system/src/directive/module/autoTableWidth.md @@ -0,0 +1,159 @@ +# v-auto-table-width 表格内容宽度自适应指令 + +## 功能说明 + +`v-auto-table-width` 是一个 Vue 指令,用于自动根据表格内容调整列宽,使表格列宽适应内容,提升表格的可读性和美观度。 + +## 特性 + +- ✅ 自动计算每列内容的最大宽度 +- ✅ 支持最小宽度和最大宽度限制 +- ✅ 自动排除已设置固定宽度的列 +- ✅ 支持排除指定列索引 +- ✅ 响应数据变化自动重新计算 +- ✅ 响应窗口大小变化 +- ✅ 防抖处理,性能优化 + +## 使用方法 + +### 基础用法 + +在 `el-table` 组件上添加 `v-auto-table-width` 指令即可: + +```vue + +``` + +### 带参数用法 + +可以通过指令参数自定义配置: + +```vue + +``` + +### 配置参数说明 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| minWidth | Number | 80 | 列的最小宽度(像素) | +| maxWidth | Number | 500 | 列的最大宽度(像素) | +| padding | Number | 20 | 列宽的额外内边距(像素) | +| excludeColumns | Array | [] | 排除的列索引数组,这些列不参与自适应 | +| delay | Number | 100 | 延迟执行时间(毫秒),用于防抖 | + +### 注意事项 + +1. **固定宽度列**:如果列已经设置了 `width` 属性,该列将不会参与自适应计算 +2. **排除列**:可以通过 `excludeColumns` 参数排除不需要自适应的列(如操作列、选择列等) +3. **性能优化**:指令内部使用了防抖处理,避免频繁计算影响性能 +4. **数据更新**:当表格数据更新时,指令会自动重新计算列宽 + +## 使用示例 + +### 示例 1:基础表格 + +```vue + +``` + +### 示例 2:排除操作列 + +```vue + +``` + +### 示例 3:自定义宽度范围 + +```vue + +``` + +## 实现原理 + +1. **内容测量**:通过创建临时 DOM 元素测量每列内容的实际宽度 +2. **宽度计算**:比较表头和数据行中每列的最大宽度 +3. **宽度应用**:将计算出的宽度应用到列组件和 DOM 元素 +4. **响应更新**:监听 DOM 变化和窗口大小变化,自动重新计算 + +## 兼容性 + +- Vue 2.x +- Element UI 2.x +- 现代浏览器(Chrome、Firefox、Safari、Edge) + +## 注意事项 + +- 指令会在 DOM 插入后自动执行,无需手动调用 +- 如果表格数据是异步加载的,指令会在数据更新后自动重新计算 +- 建议在表格数据量较大时,适当调整 `delay` 参数以优化性能