/** * 表格横向拖拽滚动 * 隐藏表格的横向滚动条,按住鼠标左键横向拖动即可查看后面的字段。 * 通过 document 事件委托全局生效,所有 el-table 自动支持,无需逐页绑定。 * * 实现要点(全面优化): * - 采用 Pointer Events + setPointerCapture:按下瞬间把指针捕获到滚动容器上, * 之后无论光标移动到固定列、表格下方还是窗口外,移动事件都不会中断,真正"跟手"。 * - requestAnimationFrame 合并每帧滚动,避免高频事件造成卡顿/闪烁。 * - 横向位移超过阈值即触发,允许轻微斜向/弧线;仅当纵向明显占优时才不触发, * 保留普通点击与纵向拖拽(如行排序)的区分。 * - 拖拽期间整页锁定光标与文本选中,光标保持默认箭头,不闪变为抓手/手型。 * - 拖拽结束(确实发生了滚动)后屏蔽本次 click,避免误触发行点击、复选框等。 */ const DRAG_THRESHOLD = 2 // 横向位移超过该像素即判定为拖动,越小越跟手 // 在这些元素上按下不触发拖拽,保证按钮、复选框等交互不受影响 const INTERACTIVE_SELECTOR = [ 'a', 'button', 'input', 'textarea', 'select', 'label', '.el-button', '.el-checkbox', '.el-radio', '.el-switch', '.el-select', '.el-link', '.el-dropdown', '.el-tooltip', '.el-color-picker', '[contenteditable="true"]' ].join(',') let wrapper = null let pointerId = null let startX = 0 let startY = 0 let startScrollLeft = 0 let dragging = false let rafId = null let pendingScrollLeft = 0 function isHorizontallyScrollable(el) { return el.scrollWidth > el.clientWidth + 1 } // 每帧只应用一次滚动位置,提升平滑度 function applyScroll() { if (!wrapper) return wrapper.scrollLeft = pendingScrollLeft rafId = null } function handlePointerDown(e) { if (e.button !== 0 && e.pointerType === 'mouse') return if (e.target.closest(INTERACTIVE_SELECTOR)) return // 固定列(如 fixed="right" 的操作列)渲染在 .el-table__fixed-body-wrapper 中,悬浮覆盖在 // 表体右侧,需一并识别;最终统一落到真正负责横向滚动的 .el-table__body-wrapper 上 const anyWrapper = e.target.closest('.el-table__body-wrapper, .el-table__fixed-body-wrapper') if (!anyWrapper) return const table = anyWrapper.closest('.el-table') const wrap = table && table.querySelector('.el-table__body-wrapper') if (!wrap || !isHorizontallyScrollable(wrap)) return wrapper = wrap pointerId = e.pointerId startX = e.clientX startY = e.clientY startScrollLeft = wrap.scrollLeft pendingScrollLeft = startScrollLeft dragging = false // 关键:捕获指针,之后所有 pointermove/pointerup 都持续派发到容器, // 即使光标移出表体/窗口也不中断 if (wrap.setPointerCapture && e.pointerId != null) { try { wrap.setPointerCapture(e.pointerId) } catch (err) { /* 忽略捕获失败 */ } } // 阻止默认行为,抑制原生图片/文本拖拽与选中 e.preventDefault() } function handlePointerMove(e) { if (!wrapper || e.pointerId !== pointerId) return e.preventDefault() const dx = e.clientX - startX const dy = e.clientY - startY if (!dragging) { // 仅当纵向明显占优(超过横向 3 倍且超过 12px)才判定为纵向操作、不触发横向拖动, // 其余情况(含轻微斜向/弧线)都正常触发,避免"拖不动" if (Math.abs(dx) <= DRAG_THRESHOLD || (Math.abs(dy) > Math.abs(dx) * 3 && Math.abs(dy) > 12)) return dragging = true wrapper.classList.add('is-dragging') // 整页锁定光标与文本选中,保证拖拽手感一致(光标不闪变、文字不误选) document.body.classList.add('el-table-dragging') } // 记录目标位置,交由 rAF 统一应用 pendingScrollLeft = startScrollLeft - (e.clientX - startX) if (rafId == null) rafId = requestAnimationFrame(applyScroll) } function endDrag(blockClick) { if (!wrapper) return if (rafId != null) { cancelAnimationFrame(rafId) rafId = null } wrapper.classList.remove('is-dragging') document.body.classList.remove('el-table-dragging') if (blockClick && dragging) { // 拖拽确实发生了滚动,屏蔽本次 click,避免误触发行点击、复选框等 const suppress = (ev) => { ev.preventDefault() ev.stopPropagation() document.removeEventListener('click', suppress, true) } document.addEventListener('click', suppress, true) } wrapper = null pointerId = null dragging = false } function handlePointerUp(e) { if (e.pointerId !== pointerId) return endDrag(true) } function handlePointerCancel() { endDrag(false) } const install = function() { if (install.installed) return install.installed = true document.addEventListener('pointerdown', handlePointerDown, true) document.addEventListener('pointermove', handlePointerMove, true) document.addEventListener('pointerup', handlePointerUp, true) document.addEventListener('pointercancel', handlePointerCancel, true) } export default install