From 05d0c7f30450e4abcc39268157c16f46e7694504 Mon Sep 17 00:00:00 2001 From: oneao Date: Sun, 30 Aug 2026 20:06:19 +0800 Subject: [PATCH] =?UTF-8?q?chore(fms-vue):=20WIP=20=E5=9F=BA=E7=BA=BF?= =?UTF-8?q?=E5=BF=AB=E7=85=A7=E2=80=94=E2=80=94=E6=94=B6=E7=BC=96=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8C=BA=E6=9C=AA=E6=8F=90=E4=BA=A4=E7=9A=84=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E6=94=B9=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 不含 fms-api Java 侧改动与后续导航状态化阶段提交。主要内容: - ui 组件库:date/dropdown/form/input/popover/select/tooltip 调整, 新增 Segmented 组件;fms-table 可编辑单元格渲染器与 FileCard 更新 - 模块管理:列表配置加载自动补全、查询缺配徽标与一键补充、 结构编辑内核 useStructureEditing/useBandLayout、子表表格编辑方式、 分组标题多语言、I18nKeyCell 等(含配套 spec) - 布局/主题/仪表盘/演示页小幅更新 --- .../src/components/fms-file/FileCard.vue | 22 +- .../src/components/fms-table/FmsTable.vue | 109 +-- .../utils/createEditableCellRenderer.js | 10 +- .../src/components/ui/button/index.scss | 9 +- .../src/components/ui/date/date-picker.vue | 1 + .../fms-vue/src/components/ui/date/index.scss | 10 +- .../src/components/ui/date/range-picker.vue | 1 + .../src/components/ui/demo/popover.vue | 53 +- .../src/components/ui/demo/segmented.vue | 80 ++ .../src/components/ui/dropdown/dropdown.vue | 7 +- .../src/components/ui/dropdown/index.scss | 7 - .../src/components/ui/dropdown/menu.vue | 1 + .../src/components/ui/form/form-item.vue | 15 +- .../fms-vue/src/components/ui/form/form.vue | 7 +- code/fms/fms-vue/src/components/ui/index.js | 2 + .../fms-vue/src/components/ui/input/input.vue | 9 +- .../src/components/ui/popover/index.scss | 38 +- .../src/components/ui/popover/popover.vue | 90 ++- .../src/components/ui/segmented/index.scss | 160 ++++ .../src/components/ui/segmented/segmented.vue | 179 +++++ .../src/components/ui/select/index.scss | 14 +- .../src/components/ui/select/select.vue | 25 +- .../src/components/ui/tooltip/tooltip.vue | 1 + .../src/layouts/components/AppSidebar.vue | 9 - .../src/layouts/components/AppTabs.vue | 6 - code/fms/fms-vue/src/theme/tokens.css | 14 + .../fms/fms-vue/src/views/dashboard/index.vue | 12 +- code/fms/fms-vue/src/views/demo/index.vue | 23 + .../components/I18nKeyCell.js | 40 + .../components/ModuleBasicPanel.vue | 32 +- .../components/ModuleFieldsPanel.vue | 57 +- .../components/ModuleI18nPanel.vue | 90 ++- .../components/ModuleListConfigPanel.vue | 607 ++++++++++++--- .../components/ModulePowersPanel.vue | 3 +- .../components/ModuleQueryConfigPanel.vue | 116 ++- .../components/edit-panels/EditDataView.vue | 26 +- .../components/form-edit/FormEditPanel.vue | 111 +-- .../components/form-edit/FormInspector.vue | 1 - .../components/i18n-key-cell.scss | 35 + .../structure-tree/ModuleStructureTree.vue | 40 +- .../structure-tree/StructureTreeNode.vue | 21 +- .../components/table-edit/TableEditPanel.vue | 702 +++++------------- .../components/table-edit/TableInspector.vue | 234 +++--- .../components/table-edit/TablePreview.vue | 9 +- .../composables/useBandLayout.js | 91 +++ .../composables/useModuleConfig.js | 256 +++++-- .../composables/useModuleSave.js | 4 + .../composables/useModuleTree.js | 34 +- .../composables/useStructureEditing.js | 641 ++++++++++++++++ .../module-management/editPanelShared.js | 13 +- .../module-management/fieldAreaDefaults.js | 59 ++ .../views/system/module-management/index.vue | 140 ++-- code/fms/fms-vue/src/views/tmp-dnd-panel.vue | 2 +- .../fms-vue/tests/components/popover.spec.js | 160 ++++ .../tests/unit/form-edit-panel-mount.spec.js | 96 +++ .../unit/module-list-config-panel-dnd.spec.js | 291 ++++++++ .../unit/module-save-orchestration.spec.js | 213 ++++++ code/fms/fms-vue/vite.config.js | 3 +- 58 files changed, 3698 insertions(+), 1343 deletions(-) create mode 100644 code/fms/fms-vue/src/components/ui/demo/segmented.vue create mode 100644 code/fms/fms-vue/src/components/ui/segmented/index.scss create mode 100644 code/fms/fms-vue/src/components/ui/segmented/segmented.vue create mode 100644 code/fms/fms-vue/src/views/system/module-management/components/I18nKeyCell.js create mode 100644 code/fms/fms-vue/src/views/system/module-management/components/i18n-key-cell.scss create mode 100644 code/fms/fms-vue/src/views/system/module-management/composables/useBandLayout.js create mode 100644 code/fms/fms-vue/src/views/system/module-management/composables/useStructureEditing.js create mode 100644 code/fms/fms-vue/src/views/system/module-management/fieldAreaDefaults.js create mode 100644 code/fms/fms-vue/tests/components/popover.spec.js create mode 100644 code/fms/fms-vue/tests/unit/form-edit-panel-mount.spec.js create mode 100644 code/fms/fms-vue/tests/unit/module-list-config-panel-dnd.spec.js create mode 100644 code/fms/fms-vue/tests/unit/module-save-orchestration.spec.js 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 >