diff --git a/code/fms/fms-vue/src/views/system/module-management/components/ModuleTableEditPanel.vue b/code/fms/fms-vue/src/views/system/module-management/components/ModuleTableEditPanel.vue index a01ead5d..bece4200 100644 --- a/code/fms/fms-vue/src/views/system/module-management/components/ModuleTableEditPanel.vue +++ b/code/fms/fms-vue/src/views/system/module-management/components/ModuleTableEditPanel.vue @@ -26,6 +26,7 @@ import { Message, Button, Select } from '@/components/ui' import ModuleStructureTree from './structure-tree/ModuleStructureTree.vue' import FmsTable from '@/components/fms-table/FmsTable.vue' import { createTempId } from '@/utils/tempId' +import { useGroupTree, buildGroupTree } from '../composables/useGroupTree' const props = defineProps({ moduleId: { type: String, required: true }, @@ -91,10 +92,6 @@ function renumberRows(rows) { return rows.map((row, index) => ({ ...row, b_xh: (index + 1) * 10 })) } -function groupIdOfRow(row) { - return row?.b_group_id == null ? null : String(row.b_group_id) -} - /* ================= 公共查询表 ================= */ const fieldNameMap = computed(() => { const map = new Map() @@ -337,94 +334,14 @@ function onFieldDragEnd() { draggingFieldId.value = null } -/* ================= 分组树(多级表头) ================= */ -// 分组是模块级树(s_field_group.b_parent_id)。多级表头 = 分组树: -// 每个分组节点渲染一个表头分组带(band),列挂在分组下,band 的 colspan/rowspan 由树推导。 -function sortGroupNodesByXh(nodes) { - // toSorted 返回新数组,必须回写原数组,否则 buildGroupTree 的排序不生效 - const sorted = nodes.toSorted((a, b) => (a.group.b_xh || 0) - (b.group.b_xh || 0)) - nodes.splice(0, nodes.length, ...sorted) - return nodes -} - -function buildGroupTree(groups) { - const map = new Map() - const roots = [] - for (const g of groups || []) { - map.set(String(g.b_id), { group: g, children: [] }) - } - for (const g of groups || []) { - const node = map.get(String(g.b_id)) - const parentId = g.b_parent_id == null ? null : String(g.b_parent_id) - if (parentId != null && map.has(parentId)) map.get(parentId).children.push(node) - else roots.push(node) - } - sortGroupNodesByXh(roots) - const walk = (nodes) => { - for (const node of nodes) { - sortGroupNodesByXh(node.children) - walk(node.children) - } - } - walk(roots) - return roots -} - -const groupTree = computed(() => buildGroupTree(localGroups.value)) - -const groupNodeMap = computed(() => { - const map = new Map() - const walk = (nodes) => { - for (const node of nodes) { - map.set(String(node.group.b_id), node) - walk(node.children) - } - } - walk(groupTree.value) - return map -}) - -// groupId → 直属列(不递归收集);未分组列单独收集 -const groupColumnsMap = computed(() => { - const map = new Map() - const ungrouped = [] - for (const row of localEdit.value) { - const gid = groupIdOfRow(row) - if (gid != null && groupNodeMap.value.has(gid)) { - if (!map.has(gid)) map.set(gid, []) - map.get(gid).push(row) - } else { - ungrouped.push(row) - } - } - return { map, ungrouped } -}) - -// 分组带元信息:层级(根=0)、直属列数、子树列数(span,空分组至少 1) -const bandMeta = computed(() => { - const columns = groupColumnsMap.value - const metaMap = new Map() - let maxLevel = 0 - const build = (nodes, level) => - nodes.map((node) => { - const gid = String(node.group.b_id) - const meta = { - gid, - group: node.group, - level, - ownCount: (columns.map.get(gid) || []).length, - children: [], - span: 0, - } - metaMap.set(gid, meta) - meta.children = build(node.children, level + 1) - meta.span = Math.max(1, meta.ownCount + meta.children.reduce((s, c) => s + c.span, 0)) - if (level > maxLevel) maxLevel = level - return meta - }) - const roots = build(groupTree.value, 0) - return { roots, metaMap, maxLevel } -}) +/* ================= 分组树(多级表头) ================= + * 分组是模块级树(s_field_group.b_parent_id)。多级表头 = 分组树: + * 每个分组节点渲染一个表头分组带(band),列挂在分组下,band 的 colspan/rowspan 由树推导。 + * 树构建与元信息推导见 composables/useGroupTree(面板与结构树共用)。 */ +const { groupNodeMap, groupColumnsMap, bandMeta, isDescendantGroup, groupIdOfRow } = useGroupTree( + localGroups, + localEdit, +) // 视觉列序 = 未分组列在前 + 分组树前序(分组直属列 → 子分组列);空分组给占位列;分组默认在最后面 const visualColumns = computed(() => { @@ -1050,19 +967,6 @@ function pastePropsToSelected() { 画布已降级为纯预览,列/分组的排序、嵌套、跨组移动全部在结构树中完成; 组件按领域语义直接给出落点,本面板 commitColDrag / commitBandDrag 复用。 */ -function isDescendantOf(ancestorGid, nodeGid) { - const meta = bandMeta.value.metaMap.get(String(nodeGid)) - if (!meta) return false - let cur = meta.group.b_parent_id == null ? null : String(meta.group.b_parent_id) - while (cur != null) { - if (cur === String(ancestorGid)) return true - const m = bandMeta.value.metaMap.get(cur) - if (!m) break - cur = m.group.b_parent_id == null ? null : String(m.group.b_parent_id) - } - return false -} - // 某分组直属列在数组中的最后索引(不含则不归属) function bandOwnRangeEnd(gid, rows) { let last = -1 @@ -1240,13 +1144,13 @@ function commitBandDrag(sourceGid, line) { let orderedSibGids = [] // 新的兄弟顺序(含 source) if (line.kind === 'band-nest') { const target = bandMeta.value.metaMap.get(String(line.gid)) - if (!target || isDescendantOf(String(sourceGid), line.gid)) return + if (!target || isDescendantGroup(String(sourceGid), line.gid)) return newParentId = String(target.gid) const children = target.children.filter((n) => n.gid !== sourceGid) orderedSibGids = [...children.map((n) => n.gid), sourceGid] } else if (line.kind === 'band-edge') { const target = bandMeta.value.metaMap.get(String(line.gid)) - if (!target || isDescendantOf(String(sourceGid), line.gid)) return + if (!target || isDescendantGroup(String(sourceGid), line.gid)) return newParentId = target.group.b_parent_id == null ? null : String(target.group.b_parent_id) const parentNode = newParentId != null ? bandMeta.value.metaMap.get(newParentId) : null const sibGids = (parentNode ? parentNode.children : bandMeta.value.roots) 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 01f95624..4e1684d9 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 @@ -26,6 +26,7 @@ import { } from '@lucide/vue' import draggable from 'vuedraggable' import StructureTreeNode from './StructureTreeNode.vue' +import { useGroupTree } from '../../composables/useGroupTree' const props = defineProps({ groups: { type: Array, default: () => [] }, @@ -82,90 +83,13 @@ function controlTypeLabel(row) { const field = row?.b_field_id != null ? fieldMap.value.get(String(row.b_field_id)) : null return FIELD_TYPE_LABELS[field?.b_type] || field?.b_type || '未知' } -function groupIdOfRow(row) { - return row?.b_group_id == null ? null : String(row.b_group_id) -} -/* ================= 分组树构建(与面板 bandMeta 同源) ================= */ -function sortGroupNodesByXh(nodes) { - // toSorted 返回新数组,必须回写原数组,否则 buildGroupTree 的排序不生效 - const sorted = nodes.toSorted((a, b) => (a.group.b_xh || 0) - (b.group.b_xh || 0)) - nodes.splice(0, nodes.length, ...sorted) - return nodes -} -function buildGroupTree(groups) { - const map = new Map() - const roots = [] - for (const g of groups || []) map.set(String(g.b_id), { group: g, children: [] }) - for (const g of groups || []) { - const node = map.get(String(g.b_id)) - const parentId = g.b_parent_id == null ? null : String(g.b_parent_id) - if (parentId != null && map.has(parentId)) map.get(parentId).children.push(node) - else roots.push(node) - } - sortGroupNodesByXh(roots) - const walk = (nodes) => { - for (const node of nodes) { - sortGroupNodesByXh(node.children) - walk(node.children) - } - } - walk(roots) - return roots -} -const groupTree = computed(() => buildGroupTree(props.groups)) - -const groupNodeMap = computed(() => { - const map = new Map() - const walk = (nodes) => { - for (const node of nodes) { - map.set(String(node.group.b_id), node) - walk(node.children) - } - } - walk(groupTree.value) - return map -}) - -// groupId → 直属列(不递归收集);未分组列单独收集 -const groupColumnsMap = computed(() => { - const map = new Map() - const ungrouped = [] - for (const row of props.rows || []) { - const gid = groupIdOfRow(row) - if (gid != null && groupNodeMap.value.has(gid)) { - if (!map.has(gid)) map.set(gid, []) - map.get(gid).push(row) - } else { - ungrouped.push(row) - } - } - return { map, ungrouped } -}) - -// 分组带元信息:层级 + 子树列数(span,空分组至少 1) -const bandMeta = computed(() => { - const columns = groupColumnsMap.value - const metaMap = new Map() - const build = (nodes, level) => - nodes.map((node) => { - const gid = String(node.group.b_id) - const meta = { - gid, - group: node.group, - level, - ownCount: (columns.map.get(gid) || []).length, - children: [], - span: 0, - } - metaMap.set(gid, meta) - meta.children = build(node.children, level + 1) - meta.span = Math.max(1, meta.ownCount + meta.children.reduce((s, c) => s + c.span, 0)) - return meta - }) - const roots = build(groupTree.value, 0) - return { roots, metaMap } -}) +/* ================= 分组树构建 ================= + * 树构建与元信息推导见 composables/useGroupTree(面板与结构树共用)。 */ +const { groupNodeMap, groupColumnsMap, bandMeta, isDescendantGroup, groupIdOfRow } = useGroupTree( + computed(() => props.groups), + computed(() => props.rows), +) /* ================= 树数据 ================= */ function makeColNode(row) { @@ -347,19 +271,6 @@ const dropPosition = ref(null) // 'before' | 'inside' | 'after' // 布局冻结(onSortMove 返回 false):拖拽中不重排 DOM,悬停仅亮落点指示, // 松手(onSortEnd)才提交一次 —— 避免「拖过即换」的重排反馈循环(抖动/冲突)。 -function isDescendantGroup(ancestorGid, gid) { - const meta = bandMeta.value.metaMap.get(String(gid)) - if (!meta) return false - let cur = meta.group.b_parent_id == null ? null : String(meta.group.b_parent_id) - while (cur != null) { - if (cur === String(ancestorGid)) return true - const m = bandMeta.value.metaMap.get(cur) - if (!m) break - cur = m.group.b_parent_id == null ? null : String(m.group.b_parent_id) - } - return false -} - /* ========== 内部拖拽:vuedraggable(SortableJS)========== * 布局冻结 + 自绘全不透明标题卡片跟随(同主表设计器); * 自动滚动由 SortableJS 内置提供,无需手写。 */ diff --git a/code/fms/fms-vue/src/views/system/module-management/composables/useGroupTree.js b/code/fms/fms-vue/src/views/system/module-management/composables/useGroupTree.js new file mode 100644 index 00000000..31821fc9 --- /dev/null +++ b/code/fms/fms-vue/src/views/system/module-management/composables/useGroupTree.js @@ -0,0 +1,118 @@ +/** + * 分组树推导(s_field_group 分组扁平数组 → 树 + 多级表头 band 元信息)。 + * 供 ModuleTableEditPanel(多级表头预览)与 structure-tree/ModuleStructureTree + * (分组结构树)共用:树构建、gid→节点/直属列映射、层级/span 元信息、祖先判断。 + */ +import { computed } from 'vue' + +// toSorted 返回新数组,必须回写原数组,否则 buildGroupTree 的排序不生效 +export function sortGroupNodesByXh(nodes) { + const sorted = nodes.toSorted((a, b) => (a.group.b_xh || 0) - (b.group.b_xh || 0)) + nodes.splice(0, nodes.length, ...sorted) + return nodes +} + +export function buildGroupTree(groups) { + const map = new Map() + const roots = [] + for (const g of groups || []) { + map.set(String(g.b_id), { group: g, children: [] }) + } + for (const g of groups || []) { + const node = map.get(String(g.b_id)) + const parentId = g.b_parent_id == null ? null : String(g.b_parent_id) + if (parentId != null && map.has(parentId)) map.get(parentId).children.push(node) + else roots.push(node) + } + sortGroupNodesByXh(roots) + const walk = (nodes) => { + for (const node of nodes) { + sortGroupNodesByXh(node.children) + walk(node.children) + } + } + walk(roots) + return roots +} + +export function groupIdOfRow(row) { + return row?.b_group_id == null ? null : String(row.b_group_id) +} + +/** + * @param {import('vue').Ref|import('vue').ComputedRef} groupsRef 分组扁平数组 + * @param {import('vue').Ref|import('vue').ComputedRef} rowsRef 编辑行数组 + */ +export function useGroupTree(groupsRef, rowsRef) { + const groupTree = computed(() => buildGroupTree(groupsRef.value)) + + const groupNodeMap = computed(() => { + const map = new Map() + const walk = (nodes) => { + for (const node of nodes) { + map.set(String(node.group.b_id), node) + walk(node.children) + } + } + walk(groupTree.value) + return map + }) + + // groupId → 直属列(不递归收集);未分组列单独收集 + const groupColumnsMap = computed(() => { + const map = new Map() + const ungrouped = [] + for (const row of rowsRef.value || []) { + const gid = groupIdOfRow(row) + if (gid != null && groupNodeMap.value.has(gid)) { + if (!map.has(gid)) map.set(gid, []) + map.get(gid).push(row) + } else { + ungrouped.push(row) + } + } + return { map, ungrouped } + }) + + // 分组带元信息:层级(根=0)、直属列数、子树列数(span,空分组至少 1)、最大层级 + const bandMeta = computed(() => { + const columns = groupColumnsMap.value + const metaMap = new Map() + let maxLevel = 0 + const build = (nodes, level) => + nodes.map((node) => { + const gid = String(node.group.b_id) + const meta = { + gid, + group: node.group, + level, + ownCount: (columns.map.get(gid) || []).length, + children: [], + span: 0, + } + metaMap.set(gid, meta) + meta.children = build(node.children, level + 1) + meta.span = Math.max(1, meta.ownCount + meta.children.reduce((s, c) => s + c.span, 0)) + if (level > maxLevel) maxLevel = level + return meta + }) + const roots = build(groupTree.value, 0) + return { roots, metaMap, maxLevel } + }) + + // gid 是否位于 ancestorGid 的子树内(防拖入自身子树成环) + function isDescendantGroup(ancestorGid, gid) { + const meta = bandMeta.value.metaMap.get(String(gid)) + if (!meta) return false + let cur = meta.group.b_parent_id == null ? null : String(meta.group.b_parent_id) + while (cur != null) { + if (cur === String(ancestorGid)) return true + const m = bandMeta.value.metaMap.get(cur) + if (!m) break + cur = m.group.b_parent_id == null ? null : String(m.group.b_parent_id) + } + return false + } + + return { groupTree, groupNodeMap, groupColumnsMap, bandMeta, isDescendantGroup, groupIdOfRow } +}