refactor(module-management): 阶段一——抽取分组树推导 composable useGroupTree

- 新建 composables/useGroupTree.js:buildGroupTree/sortGroupNodesByXh/groupIdOfRow
  纯函数导出 + useGroupTree(groupsRef, rowsRef) 返回 groupTree/groupNodeMap/
  groupColumnsMap/bandMeta(统一含 maxLevel)/isDescendantGroup/groupIdOfRow
- ModuleTableEditPanel.vue:删除本地 7 份同体实现,改用 composable;
  isDescendantOf 重命名为 isDescendantGroup(两处函数体逐字相同)
- structure-tree/ModuleStructureTree.vue:同样替换;删除「与面板 bandMeta 同源」注释
  (复制关系已不成立);bandMeta 多出的 maxLevel 字段对其消费处无害
- ModuleTableEditPanel 中 watch(props.editConfig, …, { immediate: true }) 注册位置
  保持不变,仍位于 bandMeta 等 computed 之后(避免 immediate 回调 TDZ)

行为不变;测试结果与基线一致(184 通过,2 个既有失败与本重构无关)
This commit is contained in:
oneao committed 2026-08-28 14:20:58 +08:00
1 parent 61ac01075b
commit 158eb3c029
3 files changed
+136 -203

No files matched your search

@@ -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)
@@ -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 内置提供,无需手写。 */
@@ -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<Array>|import('vue').ComputedRef<Array>} groupsRef 分组扁平数组
* @param {import('vue').Ref<Array>|import('vue').ComputedRef<Array>} 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 }
}