chore: 重构前基线快照(工作区既有改动 + 重构计划文档)

This commit is contained in:
oneao committed 2026-08-28 14:14:03 +08:00
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,保证触发器与下拉宽度一致 */