chore: 重构前基线快照(工作区既有改动 + 重构计划文档)
This commit is contained in:
1 parent
35ca496b5f
commit
61ac01075b
9 files changed
+1474
-238
No files matched your search
@@ -26,6 +26,9 @@ const props = defineProps({
|
||||
activeRowKey: { type: [String, Number], default: null },
|
||||
// 行拖拽排序:开启后显示拖拽手柄,并通过 row-order-change 回传排序后的完整数据。
|
||||
rowDraggable: { type: Boolean, default: false },
|
||||
// 行拖拽触发方式:true=仅拖拽手柄列(默认,显示手柄图标);
|
||||
// false=整行可拖拽(不显示手柄列,拖拽任意单元格即可排序;输入/复选框控件除外,避免破坏编辑)。
|
||||
rowDragHandle: { type: Boolean, default: true },
|
||||
// 存在 editor 配置的列默认可编辑;设为 false 可整体禁用编辑。
|
||||
editable: { type: Boolean, default: true },
|
||||
// 默认跟随应用深浅色模式,也允许单个表格显式覆盖。
|
||||
@@ -80,21 +83,34 @@ let lastSize = null
|
||||
|
||||
let resizeObserver = null
|
||||
onMounted(() => {
|
||||
if (typeof ResizeObserver === 'undefined' || !rootRef.value) return
|
||||
resizeObserver = new ResizeObserver(([entry]) => {
|
||||
const { width, height } = entry.contentRect
|
||||
if (width <= 0 || height <= 0) return
|
||||
clearTimeout(resizeDebounceTimer)
|
||||
resizeDebounceTimer = setTimeout(() => {
|
||||
resizeDebounceTimer = 0
|
||||
const xOnly = lastSize != null && lastSize.height === height && lastSize.width !== width
|
||||
lastSize = { width, height }
|
||||
refreshLayout(xOnly)
|
||||
}, RESIZE_DEBOUNCE_MS)
|
||||
})
|
||||
resizeObserver.observe(rootRef.value)
|
||||
if (typeof ResizeObserver !== 'undefined' && rootRef.value) {
|
||||
resizeObserver = new ResizeObserver(([entry]) => {
|
||||
const { width, height } = entry.contentRect
|
||||
if (width <= 0 || height <= 0) return
|
||||
clearTimeout(resizeDebounceTimer)
|
||||
resizeDebounceTimer = setTimeout(() => {
|
||||
resizeDebounceTimer = 0
|
||||
const xOnly = lastSize != null && lastSize.height === height && lastSize.width !== width
|
||||
lastSize = { width, height }
|
||||
refreshLayout(xOnly)
|
||||
}, RESIZE_DEBOUNCE_MS)
|
||||
})
|
||||
resizeObserver.observe(rootRef.value)
|
||||
}
|
||||
// 整行拖拽:挂载后等表格渲染完再给数据行加 draggable
|
||||
setupWholeRowDrag()
|
||||
})
|
||||
|
||||
// 数据/开关变化后重跑整行拖拽初始化(行增删、虚拟滚动重建时兜底)
|
||||
watch(
|
||||
() => [props.rowDraggable, props.rowDragHandle, props.dataSource?.length],
|
||||
async () => {
|
||||
await nextTick()
|
||||
setupWholeRowDrag()
|
||||
},
|
||||
{ flush: 'post' },
|
||||
)
|
||||
|
||||
const NAVIGATION_KEYS = new Set(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab'])
|
||||
const rowEditingStates = new WeakMap()
|
||||
let activeEditingRow = null
|
||||
@@ -282,7 +298,7 @@ const displayColumns = computed(() => {
|
||||
resizable: false,
|
||||
})
|
||||
}
|
||||
if (props.rowDraggable) cols.unshift(dragRowColumn)
|
||||
if (props.rowDraggable && props.rowDragHandle) cols.unshift(dragRowColumn)
|
||||
if (hasRowSelection.value) cols.unshift(selectionColumn)
|
||||
return cols.map((column) => ({
|
||||
...column,
|
||||
@@ -313,6 +329,106 @@ function handleRowOrderChange(sourceIndex, targetIndex) {
|
||||
emit('row-order-change', [...rows], sourceIndex, targetIndex)
|
||||
}
|
||||
|
||||
/* ================= 整行拖拽(无需手柄列) =================
|
||||
* stk-table 原生行拖拽必须通过 dragRow 手柄列触发;当 rowDraggable && !rowDragHandle
|
||||
* 时改用「整行可拖拽」:MutationObserver 给 tbody 数据行加 draggable 属性(兜底虚拟
|
||||
* 滚动新建的行),根节点上 dragstart/dragover/drop 事件委托落点与手柄模式一致。 */
|
||||
const wholeRowDragSource = ref(-1)
|
||||
let wholeRowDragTr = null
|
||||
let wholeRowOverTr = null
|
||||
let rowDragObserver = null
|
||||
|
||||
function applyDraggableToRows() {
|
||||
const root = rootRef.value
|
||||
if (!root) return
|
||||
root.querySelectorAll('tbody tr[data-row-i]').forEach((tr) => {
|
||||
tr.setAttribute('draggable', 'true')
|
||||
})
|
||||
}
|
||||
|
||||
function setupWholeRowDrag() {
|
||||
teardownWholeRowDrag()
|
||||
if (!props.rowDraggable || props.rowDragHandle || !rootRef.value) return
|
||||
applyDraggableToRows()
|
||||
const tbody = rootRef.value.querySelector('.stk-tbody-main')
|
||||
if (tbody) {
|
||||
rowDragObserver = new MutationObserver(applyDraggableToRows)
|
||||
rowDragObserver.observe(tbody, { childList: true, subtree: true })
|
||||
}
|
||||
}
|
||||
|
||||
function teardownWholeRowDrag() {
|
||||
rowDragObserver?.disconnect()
|
||||
rowDragObserver = null
|
||||
clearWholeRowDragVisuals()
|
||||
wholeRowDragSource.value = -1
|
||||
}
|
||||
|
||||
function clearWholeRowDragVisuals() {
|
||||
wholeRowDragTr?.classList.remove('tr-dragging')
|
||||
wholeRowDragTr = null
|
||||
wholeRowOverTr?.classList.remove('tr-dragging-over')
|
||||
wholeRowOverTr = null
|
||||
}
|
||||
|
||||
function isInteractiveDragTarget(target) {
|
||||
return !!target?.closest?.(
|
||||
'input, textarea, select, button, a, [contenteditable], .fms-checkbox, .fms-cell-editor-root',
|
||||
)
|
||||
}
|
||||
|
||||
function onWholeRowDragStart(e) {
|
||||
if (!props.rowDraggable || props.rowDragHandle) return
|
||||
const tr = e.target?.closest?.('tr[data-row-i]')
|
||||
if (!tr) return
|
||||
if (isInteractiveDragTarget(e.target)) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
wholeRowDragSource.value = Number(tr.dataset.rowI)
|
||||
wholeRowDragTr = tr
|
||||
tr.classList.add('tr-dragging')
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.effectAllowed = 'move'
|
||||
e.dataTransfer.setData('text/plain', String(wholeRowDragSource.value))
|
||||
e.dataTransfer.setDragImage?.(tr, 0, tr.offsetHeight / 2)
|
||||
}
|
||||
}
|
||||
|
||||
function onWholeRowDragOver(e) {
|
||||
if (wholeRowDragSource.value < 0) return
|
||||
e.preventDefault()
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'
|
||||
const tr = e.target?.closest?.('tr[data-row-i]')
|
||||
if (tr && tr !== wholeRowOverTr) {
|
||||
wholeRowOverTr?.classList.remove('tr-dragging-over')
|
||||
wholeRowOverTr = tr
|
||||
tr.classList.add('tr-dragging-over')
|
||||
}
|
||||
}
|
||||
|
||||
function onWholeRowDrop(e) {
|
||||
const source = wholeRowDragSource.value
|
||||
if (source < 0) return
|
||||
e.preventDefault()
|
||||
const tr = e.target?.closest?.('tr[data-row-i]')
|
||||
clearWholeRowDragVisuals()
|
||||
wholeRowDragSource.value = -1
|
||||
if (!tr) return
|
||||
const target = Number(tr.dataset.rowI)
|
||||
if (Number.isNaN(target) || target === source) return
|
||||
const rows = [...props.dataSource]
|
||||
const [moved] = rows.splice(source, 1)
|
||||
rows.splice(target, 0, moved)
|
||||
tableRef.value?.clearSelectedArea?.()
|
||||
emit('row-order-change', rows, source, target)
|
||||
}
|
||||
|
||||
function onWholeRowDragEnd() {
|
||||
clearWholeRowDragVisuals()
|
||||
wholeRowDragSource.value = -1
|
||||
}
|
||||
|
||||
// 行高同时作为 CSS 变量与 StkTable 的 row-height / header-row-height
|
||||
const rowHeight = 34
|
||||
const headerRowHeight = 40
|
||||
@@ -538,6 +654,7 @@ onBeforeUnmount(() => {
|
||||
clearTimeout(resizeDebounceTimer)
|
||||
resizeObserver?.disconnect()
|
||||
resizeObserver = null
|
||||
teardownWholeRowDrag()
|
||||
removeNavigationListeners()
|
||||
clearEditActivateTimer()
|
||||
pressedNavigationKeys.clear()
|
||||
@@ -555,7 +672,16 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="rootRef" class="fms-table" @keydown.capture="handleKeydownCapture">
|
||||
<div
|
||||
ref="rootRef"
|
||||
class="fms-table"
|
||||
:class="{ 'is-whole-row-drag': rowDraggable && !rowDragHandle }"
|
||||
@keydown.capture="handleKeydownCapture"
|
||||
@dragstart="onWholeRowDragStart"
|
||||
@dragover="onWholeRowDragOver"
|
||||
@drop="onWholeRowDrop"
|
||||
@dragend="onWholeRowDragEnd"
|
||||
>
|
||||
<StkTable
|
||||
ref="tableRef"
|
||||
class="fms-table__inner"
|
||||
@@ -566,13 +692,14 @@ defineExpose({
|
||||
:header-row-height="headerRowHeight"
|
||||
:virtual="effectiveVirtual"
|
||||
:virtual-x="virtualX"
|
||||
:smooth-scroll="true"
|
||||
:auto-resize="false"
|
||||
:col-resizable="colResizable"
|
||||
:header-drag="columnDragConfig"
|
||||
:columns="displayColumns"
|
||||
@update:columns="onColumnsUpdate"
|
||||
:data-source="dataSource"
|
||||
:drag-row-config="{ mode: rowDraggable ? 'insert' : 'none' }"
|
||||
:drag-row-config="{ mode: rowDraggable && rowDragHandle ? 'insert' : 'none' }"
|
||||
:row-key="rowKey"
|
||||
:no-data-full="noDataFull"
|
||||
:show-overflow="showOverflow"
|
||||
@@ -606,6 +733,20 @@ defineExpose({
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 整行拖拽:数据行鼠标提示可拖拽;拖拽中的行半透明、落点高亮 */
|
||||
.fms-table.is-whole-row-drag .stk-tbody-main tr[data-row-i] {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.fms-table.is-whole-row-drag .stk-tbody-main tr[data-row-i].tr-dragging {
|
||||
cursor: grabbing;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.fms-table.is-whole-row-drag .stk-tbody-main tr[data-row-i].tr-dragging-over {
|
||||
box-shadow: inset 0 2px 0 0 var(--fms-primary);
|
||||
}
|
||||
|
||||
.fms-table__inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
.select {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
min-width: 200px;
|
||||
width: 100%;
|
||||
|
||||
/* Popover 触发器默认 inline-flex 会收缩到内容宽度,
|
||||
改为块级填满 .select,保证触发器与下拉宽度一致 */
|
||||
|
||||
Reference in new issue
Block a user