diff --git a/code/fms/fms-vue/src/components/ui/input/input.vue b/code/fms/fms-vue/src/components/ui/input/input.vue
index efe4cc16..8853990a 100644
--- a/code/fms/fms-vue/src/components/ui/input/input.vue
+++ b/code/fms/fms-vue/src/components/ui/input/input.vue
@@ -34,6 +34,7 @@ const slots = useSlots()
/* ---------- 公共状态 ---------- */
const focused = ref(false)
const hovered = ref(false)
+const inputEl = ref(null)
/* ---------- 公共 computed ---------- */
const isTextarea = computed(() => props.type === 'textarea')
@@ -255,6 +256,13 @@ onMounted(() => {
nextTick(measureDefaultTextareaHeight)
})
+/* 程序化聚焦/选中文本(内联重命名等场景需要 focus + select) */
+defineExpose({
+ focus: () => inputEl.value?.focus(),
+ select: () => inputEl.value?.select(),
+ blur: () => inputEl.value?.blur(),
+})
+
watch(
() => [props.rows, isTextarea.value],
() => {
@@ -285,6 +293,7 @@ onBeforeUnmount(() => {
-
+
diff --git a/code/fms/fms-vue/src/views/system/module-management/components/structure-tree/ModuleStructureTree.vue b/code/fms/fms-vue/src/views/system/module-management/components/structure-tree/ModuleStructureTree.vue
index e9a6da51..15defeb4 100644
--- a/code/fms/fms-vue/src/views/system/module-management/components/structure-tree/ModuleStructureTree.vue
+++ b/code/fms/fms-vue/src/views/system/module-management/components/structure-tree/ModuleStructureTree.vue
@@ -14,7 +14,7 @@
* - 选中:受控 selectedKey,双向同步面板属性面板 / 预览高亮。
* - 键盘导航:roving tabindex(↑↓←→ / Enter / Space)。
*/
-import { computed, nextTick, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
+import { computed, nextTick, onBeforeUnmount, provide, ref, watch } from 'vue'
import {
Clipboard,
Copy,
@@ -25,6 +25,7 @@ import {
Trash2,
} from '@lucide/vue'
import draggable from 'vuedraggable'
+import { Dropdown } from '@/components/ui'
import StructureTreeNode from './StructureTreeNode.vue'
import { useGroupTree } from '../../composables/useGroupTree'
import { useFieldLookup } from '../../composables/useFieldLookup'
@@ -523,12 +524,17 @@ function resetDragVisual() {
removeDragBadge()
}
-/* ================= 右键菜单 ================= */
-const contextState = ref(null) // { items, x, y, node, key }
-const contextMenuEl = ref(null)
+/* ================= 右键菜单(ui Dropdown,trigger=contextmenu,鼠标位置锚定) ================= */
+// 右键目标节点:由 StructureTreeNode 在 contextmenu 冒泡前写入
+const contextNode = ref(null)
-function buildContextItems(node) {
- if (node.key === 'g:ungrouped') return []
+function setContextNode(node) {
+ contextNode.value = node
+}
+
+const contextItems = computed(() => {
+ const node = contextNode.value
+ if (!node || node.key === 'g:ungrouped') return []
if (node.kind === 'group') {
return [
{ key: 'add-sub', label: '新增子分组', icon: FolderPlus },
@@ -543,58 +549,14 @@ function buildContextItems(node) {
{ key: 'paste', label: '粘贴列属性', icon: Clipboard, disabled: !props.pasteEnabled },
{ key: 'delete', label: '删除列', icon: Trash2, danger: true },
]
-}
-
-function openContextMenu(node, event) {
- const items = buildContextItems(node)
- if (!items || !items.length) return
- contextState.value = { items, x: event.clientX, y: event.clientY, node, key: node.key }
- nextTick(() => {
- const el = contextMenuEl.value
- if (!el || !contextState.value) return
- const rect = el.getBoundingClientRect()
- const vw = window.innerWidth
- const vh = window.innerHeight
- let { x, y } = contextState.value
- if (x + rect.width > vw - 8) x = Math.max(8, x - rect.width)
- if (y + rect.height > vh - 8) y = Math.max(8, y - rect.height)
- contextState.value.x = x
- contextState.value.y = y
- })
-}
-
-function hideContextMenu() {
- contextState.value = null
-}
-
-function onMenuItemClick(item, event) {
- if (item.disabled) return
- const state = contextState.value
- emit('context-menu-click', {
- item,
- node: state?.node ?? null,
- key: state?.key ?? null,
- event,
- })
- hideContextMenu()
-}
-
-function onDocMouseDown(e) {
- if (!contextState.value) return
- const el = contextMenuEl.value
- if (el && el.contains(e.target)) return
- hideContextMenu()
-}
-function onDocKeydown(e) {
- if (e.key === 'Escape') hideContextMenu()
-}
-onMounted(() => {
- document.addEventListener('mousedown', onDocMouseDown, true)
- document.addEventListener('keydown', onDocKeydown, true)
})
+
+function onMenuItemClick({ item }) {
+ const node = contextNode.value
+ emit('context-menu-click', { item, node, key: node?.key ?? null })
+}
+
onBeforeUnmount(() => {
- document.removeEventListener('mousedown', onDocMouseDown, true)
- document.removeEventListener('keydown', onDocKeydown, true)
// 拖拽进行中卸载:清理鼠标/键盘监听与跟手卡片,避免泄漏
if (dragKey.value) cleanupDrag()
})
@@ -668,66 +630,44 @@ provide('moduleStructureTree', {
onNodeMove,
onNodeDelete,
onNodeAddSub,
- openContextMenu,
+ setContextNode,
commitRename,
cancelRename,
})
-
-
-
-
-
-
-
- 暂无分组
-
-
-
-
-
-
-
-
-
+
-
-
-
@@ -205,51 +210,47 @@ function onRenameCancel() {
-
-
-
+
-
-
+
-
-
+
-
+
@@ -438,22 +439,20 @@ function onRenameCancel() {
opacity: 0.85;
}
-.mst-node__act {
+/* 常驻操作按钮:20px 方形图标钮(覆盖 ui Button 默认尺寸) */
+.mst-node__act.button {
display: inline-flex;
align-items: center;
justify-content: center;
flex: none;
width: 20px;
height: 20px;
+ min-height: 0;
padding: 0;
- border: none;
border-radius: 4px;
background: transparent;
color: var(--fms-text-secondary);
- cursor: pointer;
- transition:
- background-color 0.15s ease,
- color 0.15s ease;
+ transform: none;
&:hover:not(:disabled) {
background: color-mix(in srgb, var(--fms-primary) 10%, transparent);
@@ -471,18 +470,15 @@ function onRenameCancel() {
}
}
-.mst-node__edit-input {
+/* 内联重命名输入框:压缩 ui Input 的默认高度/内边距以适配 28px 行 */
+.mst-node__edit-input.input-wrapper {
flex: 1 1 auto;
min-width: 80px;
height: 22px;
+ min-height: 0;
padding: 0 6px;
- border: 1px solid var(--fms-primary);
border-radius: 4px;
- background: var(--fms-card);
- color: var(--fms-text);
font-size: 12px;
- font-family: inherit;
- outline: none;
box-shadow: 0 0 0 2px color-mix(in srgb, var(--fms-primary) 12%, transparent);
}
@@ -493,25 +489,31 @@ function onRenameCancel() {
flex: none;
}
-.mst-node__edit-btn {
+/* 确认/取消:20px 方形图标钮(覆盖 ui Button 默认尺寸) */
+.mst-node__edit-btn.button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
+ min-height: 0;
padding: 0;
border: 1px solid var(--fms-border);
border-radius: 4px;
background: var(--fms-card);
color: var(--fms-text-secondary);
- cursor: pointer;
+ transition:
+ border-color 150ms ease,
+ color 150ms ease;
+ transform: none;
- &:hover {
+ &:hover:not(:disabled) {
border-color: var(--fms-primary);
color: var(--fms-primary);
+ background: var(--fms-card);
}
- &:last-child:hover {
+ &:last-child:hover:not(:disabled) {
border-color: #d95757;
color: #d95757;
}
diff --git a/code/fms/fms-vue/src/views/system/module-management/index.vue b/code/fms/fms-vue/src/views/system/module-management/index.vue
index ff289bb9..841250ab 100644
--- a/code/fms/fms-vue/src/views/system/module-management/index.vue
+++ b/code/fms/fms-vue/src/views/system/module-management/index.vue
@@ -371,9 +371,9 @@ loadModuleTree()