diff --git a/code/fms/fms-vue/src/components/fms-file/FileCard.vue b/code/fms/fms-vue/src/components/fms-file/FileCard.vue index d844b745..492b7713 100644 --- a/code/fms/fms-vue/src/components/fms-file/FileCard.vue +++ b/code/fms/fms-vue/src/components/fms-file/FileCard.vue @@ -182,9 +182,9 @@ function onMoreClick(info) { height: 32px; margin: 0; padding: 0; - /* 底部三个操作按钮必须是等宽 flex 子项:在 flex-basis: 0% 下浏览器按 - 内容盒均分剩余空间,任何一方额外的 padding / border 都会让其 border-box - 更宽。故这里与 .popover-trigger 统一 padding:0 + border:none, + /* 底部三个操作按钮(含「更多」Dropdown 零包裹后的 Button)必须是等宽 flex 子项: + 在 flex-basis: 0% 下浏览器按内容盒均分剩余空间,任何一方额外的 padding / border + 都会让其 border-box 更宽。故统一 padding:0 + border:none, 让三个按钮 border-box 完全相等 */ border: none; border-radius: 0; @@ -219,22 +219,6 @@ function onMoreClick(info) { } } - /* 「更多」按钮被 Dropdown 包裹:Popover 的 .popover-trigger 才是 flex 子项, - 必须让它与两个直接按钮一样 flex:1 且 padding/border 保持一致, - 三个操作按钮宽度才会一致 */ - :deep(.popover-trigger) { - flex: 1; - min-width: 0; - display: flex; - justify-self: auto; - box-sizing: border-box; - padding: 0; - border: none; - - .button { - width: 100%; - } - } } /* 操作栏按钮间的竖向分隔线:独立元素,不依赖按钮类名 / 禁用态, diff --git a/code/fms/fms-vue/src/components/fms-table/FmsTable.vue b/code/fms/fms-vue/src/components/fms-table/FmsTable.vue index 8500cb07..6264eb36 100644 --- a/code/fms/fms-vue/src/components/fms-table/FmsTable.vue +++ b/code/fms/fms-vue/src/components/fms-table/FmsTable.vue @@ -11,6 +11,8 @@ const props = defineProps({ virtual: { type: Boolean, default: true }, virtualX: { type: Boolean, default: true }, colResizable: { type: Boolean, default: true }, + // 边框自绘:true=外框圆角描边+行分隔线(默认);false=无外框仅行分隔线(嵌入卡片场景)。 + // stk-table 库内边框恒为 false,不用其渐变边框。 bordered: { type: [Boolean, String], default: true }, noDataFull: { type: Boolean, default: true }, showOverflow: { type: Boolean, default: true }, @@ -37,6 +39,9 @@ const props = defineProps({ default: undefined, validator: (value) => value === 'light' || value === 'dark', }, + // 行高:唯一真源,同时驱动虚拟滚动计算与库注入的 --row-height 等 CSS 变量,外部勿用 CSS 覆盖 + rowHeight: { type: Number, default: 34 }, + headerRowHeight: { type: Number, default: 40 }, }) const emit = defineEmits(['update:columns', 'change', 'row-order-change']) @@ -185,6 +190,7 @@ function rebindActiveEditingRow(rows) { state.value = { editing: true, focusCol: previousState.focusCol, + focused: previousState.focused, initialValues: captureEditableValues(replacement), } } @@ -203,12 +209,15 @@ function activateEditing(row, focusCol) { focusCol, initialValues: captureEditableValues(row), } + pendingFocusKey = String(getRowKey(row)) return } const current = state.value if (current.editing && current.focusCol === focusCol) return state.value = { ...current, editing: true, focusCol } + // 同行切换列:编辑器会因 focusCol 变化重挂载,标记待聚焦以恢复焦点+全选 + pendingFocusKey = String(getRowKey(row)) } function editorValuesEqual(col, previous, current) { @@ -218,11 +227,29 @@ function editorValuesEqual(col, previous, current) { return String(previous ?? '') === String(current ?? '') } -const selectOnFocus = ref(true) +// 最近激活、等待编辑器挂载后聚焦的行 key。用于区分「用户激活单元格(点击/键盘/程序化)」 +// 与「数据变更引发的重挂载」:前者需要自动聚焦+全选,后者不应聚焦——否则失焦引发的 +// 重渲染会把焦点抢回来(表现为离开单元格时高亮一闪而过)。 +let pendingFocusKey = null const editingCtx = { isEditing: (row) => props.editable && getRowEditingState(row)?.value.editing, shouldFocus: (row, col) => getRowEditingState(row)?.value.focusCol === col.dataIndex, - shouldSelectOnFocus: selectOnFocus, + // 焦点恢复判定依据:重渲染前该行编辑器是否持有焦点(blur 时已置 false) + wasFocused: (row) => Boolean(getRowEditingState(row)?.value.focused), + consumePendingFocus: (row) => { + if (pendingFocusKey == null) return false + if (pendingFocusKey !== String(getRowKey(row))) return false + pendingFocusKey = null + return true + }, + onEditorFocus: (row) => { + const state = getRowEditingState(row) + if (state?.value) state.value = { ...state.value, focused: true } + }, + onEditorBlur: (row) => { + const state = getRowEditingState(row) + if (state?.value) state.value = { ...state.value, focused: false } + }, shouldNotify: (row, col, newValue) => { const values = getRowEditingState(row)?.value.initialValues if (!values?.has(col.dataIndex)) return true @@ -429,14 +456,11 @@ function onWholeRowDragEnd() { wholeRowDragSource.value = -1 } -// 行高同时作为 CSS 变量与 StkTable 的 row-height / header-row-height -const rowHeight = 34 -const headerRowHeight = 40 +// 行高/表头行高通过 prop 传给 StkTable:库内部会将其注入为滚动容器上的 +// --row-height / --header-row-height / --footer-row-height,并参与虚拟滚动计算, +// 此处只补充库不管理的表头/表尾背景色。 const customVars = { - '--header-row-height': `${headerRowHeight}px`, - '--row-height': `${rowHeight}px`, - '--footer-row-height': '36px', - '--th-bgc': 'var(--fms-card)', + '--th-bgc': 'var(--fms-table-header-bg)', '--tf-bgc': 'var(--fms-background)', } @@ -456,7 +480,7 @@ async function scrollToRow(rowKey) { if (!table) return const idx = props.dataSource.findIndex((row) => String(getRowKey(row)) === String(rowKey)) if (idx < 0) return - table.scrollTo(idx * rowHeight, null) + table.scrollTo(idx * props.rowHeight, null) } // 新增行等程序化场景使用:滚动到目标行,并让该行立即进入编辑状态。 @@ -603,20 +627,11 @@ function handleAreaSelectionChange(ranges) { return } - // 非键盘导航(鼠标点击等):立即激活。 - // 同一行已编辑时仍更新 focusCol(保证焦点切换到新列), - // 但暂时禁用 select 全选(避免鼠标点击触发全选文本)。 + // 非键盘导航(鼠标点击等):立即激活。同行切换列时编辑器重挂载并恢复焦点+全选 + // (直接输入即替换整格内容);点击已聚焦编辑器内部则由原生行为定位光标。 pendingEditingTarget = null clearEditActivateTimer() - if (activeEditingRow === row) { - selectOnFocus.value = false - activateEditing(row, focusCol) - nextTick(() => { - selectOnFocus.value = true - }) - } else { - activateEditing(row, focusCol) - } + activateEditing(row, focusCol) lastEditActivateAt = performance.now() } @@ -675,7 +690,10 @@ defineExpose({
.fms-cell-editor-root) { + height: var(--row-height); +} + .fms-table__inner td:has(> .fms-cell-editor-root) { - padding-inline: 3px; + padding: 3px; } // actions 列按钮容器 diff --git a/code/fms/fms-vue/src/components/fms-table/utils/createEditableCellRenderer.js b/code/fms/fms-vue/src/components/fms-table/utils/createEditableCellRenderer.js index 558cfc9b..aeae57c6 100644 --- a/code/fms/fms-vue/src/components/fms-table/utils/createEditableCellRenderer.js +++ b/code/fms/fms-vue/src/components/fms-table/utils/createEditableCellRenderer.js @@ -78,11 +78,17 @@ function isEditorDisabled(editor, row) { function createFocusHandler(ctx, row, col, selectText = false) { return ({ component, el }) => { if (!ctx.shouldFocus(row, col)) return + // 仅两类场景聚焦:用户刚激活该单元格(consumePendingFocus 一次性), + // 或重渲染前本编辑器就持有焦点(wasFocused,数据回灌重挂载后恢复焦点)。 + // 其余重挂载不聚焦,避免失焦引发的更新把焦点抢回来。 + const pending = ctx.consumePendingFocus?.(row) + const wasFocused = Boolean(ctx.wasFocused?.(row)) + if (!pending && !wasFocused) return const input = el?.matches?.('input') ? el : el?.querySelector?.('input') if (document.activeElement === input) return const instance = component?.exposed || component?.proxy - const shouldSelect = selectText && (ctx.shouldSelectOnFocus?.value ?? true) + const shouldSelect = selectText && (pending || wasFocused) const opts = shouldSelect ? { cursor: 'all', preventScroll: true } : { preventScroll: true } if (instance?.focus) { instance.focus(opts) @@ -104,7 +110,9 @@ function createInput(row, col, editor, ctx) { 'onUpdate:modelValue': (value) => { setInputDraft(row, col.dataIndex, value) }, + onFocus: () => ctx.onEditorFocus?.(row), onBlur: (event) => { + ctx.onEditorBlur?.(row) const value = event.target.value commitInputDraft(row, col.dataIndex, value) notifyChange(ctx, editor, row, col, value) diff --git a/code/fms/fms-vue/src/components/ui/button/index.scss b/code/fms/fms-vue/src/components/ui/button/index.scss index 11e0a568..6d99bb9a 100644 --- a/code/fms/fms-vue/src/components/ui/button/index.scss +++ b/code/fms/fms-vue/src/components/ui/button/index.scss @@ -9,6 +9,8 @@ :deep(svg) { width: 1em; height: 1em; + /* 补偿光学视觉差:图标十字视觉重心偏高,微调下移与中文字形对齐 */ + margin-top: 1px; } padding: 0 var(--fms-control-padding-x); font-size: var(--fms-font-size); @@ -21,12 +23,7 @@ transition: background-color 150ms ease, border-color 150ms ease, - color 150ms ease, - transform 150ms ease; -} - -.button:active { - transform: scale(0.97); + color 150ms ease; } .button:focus-visible { diff --git a/code/fms/fms-vue/src/components/ui/date/date-picker.vue b/code/fms/fms-vue/src/components/ui/date/date-picker.vue index 01d9722a..c4811aa5 100644 --- a/code/fms/fms-vue/src/components/ui/date/date-picker.vue +++ b/code/fms/fms-vue/src/components/ui/date/date-picker.vue @@ -139,6 +139,7 @@ watch(pickerConfig, () => { :offset="8" :disabled="disabled" :follow-trigger-width="true" + unwrapped >