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:
1 parent
61ac01075b
commit
158eb3c029
3 files changed
+136
-203
No files matched your search
+11
-107
@@ -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)
|
||||
|
||||
+7
-96
@@ -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 }
|
||||
}
|
||||
Reference in new issue
Block a user