-
- 主表表单
- 子表表格
-
+
+
+ {{ option.label }}
+
+
-
+
-
-
状态
-
-
- 必填
- emit('toggle', 'b_required')"
- />
-
-
- 只读
- emit('toggle', 'b_readonly')"
- />
-
-
- 禁用
- emit('toggle', 'b_disabled')"
- />
-
-
- 显示
- emit('toggle', 'b_visible')"
- />
-
+
+
+
+ {{ rowI18nKeyOf(selectedRow) || '无多语言键' }}
+
-
操作
+
状态
+
+
+
+
+
+
操作
-
+
启用
操作
+
+ 分组多语言
+
/**
* 子表多级表头预览表:band 行渲染、mock 数据行、预览拖拽落点指示。
- * 严格被动预览,唯一交互是列头右缘拖拽列宽与列头拖拽排序(均经 composable 处理)。
- * 列/分组结构数据由壳组件(TableEditPanel)经 props 传入,变更经 commitColDrag 提交。
+ * 交互:列头右缘拖拽列宽与列头拖拽排序(均经 composable 处理)。
+ * 列/分组结构数据由壳组件(TableEditPanel / ModuleListConfigPanel)经 props 传入,
+ * 变更经 commitColDrag 提交。
*/
import { computed, onBeforeUnmount } from 'vue'
import { useColumnResize } from './useColumnResize'
@@ -20,6 +21,8 @@ const props = defineProps({
/** 统一选中态:'g:' 分组带 | 'c:' 列 | null */
selectedNodeKey: { type: String, default: null },
loading: { type: Boolean, default: false },
+ /** 空态文案(列表配置等宿主无字段库,提示语不同) */
+ emptyText: { type: String, default: '暂无列:从左侧字段库点击添加字段' },
/** 取当前列宽(40–600 边界) */
widthOf: { type: Function, required: true },
fieldName: { type: Function, required: true },
@@ -241,7 +244,7 @@ function mockValue(col) {
>
未分组
-
暂无列:从左侧字段库点击添加字段
+
{{ emptyText }}
diff --git a/code/fms/fms-vue/src/views/system/module-management/composables/useBandLayout.js b/code/fms/fms-vue/src/views/system/module-management/composables/useBandLayout.js
new file mode 100644
index 00000000..748074fc
--- /dev/null
+++ b/code/fms/fms-vue/src/views/system/module-management/composables/useBandLayout.js
@@ -0,0 +1,91 @@
+/**
+ * 多级表头渲染推导(纯 computed,无状态):
+ * visualColumns(视觉列序:未分组列 + 分组树先序)/ bandRows(每层级一个表头行,
+ * 未分组列头 rowspan 贯穿)/ headerColumns(列头行)/ ungroupedInBand / ungroupedCount。
+ * 供 TableEditPanel(子表设计)与 ModuleListConfigPanel(列表配置)共用,
+ * 输入为 useGroupTree 的返回值(groupColumnsMap / bandMeta)。
+ */
+import { computed } from 'vue'
+
+/**
+ * @param {{ groupColumnsMap: import('vue').ComputedRef<{map: Map, ungrouped: Array}>,
+ * bandMeta: import('vue').ComputedRef<{roots: Array, maxLevel: number}> }} tree
+ */
+export function useBandLayout(tree) {
+ const { groupColumnsMap, bandMeta } = tree
+
+ // 视觉列序:未分组列在前,随后分组树先序(每分组:直属列 → 空叶子分组补 slot 占位)
+ const visualColumns = computed(() => {
+ const columns = groupColumnsMap.value
+ const list = []
+ for (const row of columns.ungrouped) {
+ list.push({ kind: 'col', key: `col-${row.b_field_id}`, row, gid: null })
+ }
+ const walk = (nodes) => {
+ for (const node of nodes) {
+ const own = columns.map.get(node.gid) || []
+ for (const row of own) {
+ list.push({ kind: 'col', key: `col-${row.b_field_id}`, row, gid: node.gid })
+ }
+ if (!own.length && !node.children.length) {
+ list.push({ kind: 'slot', key: `slot-${node.gid}`, gid: node.gid, group: node.group })
+ }
+ walk(node.children)
+ }
+ }
+ walk(bandMeta.value.roots)
+ return list
+ })
+
+ // 多级表头 band 行:每层一个 tr 的单元格序列;未分组列头在第 0 层,贯穿全部表头行高
+ const bandRows = computed(() => {
+ const { roots, maxLevel } = bandMeta.value
+ if (!roots.length) return []
+ const ungrouped = groupColumnsMap.value.ungrouped
+ const totalHeaderRows = maxLevel + 2
+ const byLevel = new Map()
+ const collect = (nodes) => {
+ for (const node of nodes) {
+ if (!byLevel.has(node.level)) byLevel.set(node.level, [])
+ byLevel.get(node.level).push(node)
+ collect(node.children)
+ }
+ }
+ collect(roots)
+ const rows = []
+ for (let level = 0; level <= maxLevel; level++) {
+ const nodes = byLevel.get(level) || []
+ const cells = []
+ if (level === 0 && ungrouped.length) {
+ for (const row of ungrouped) {
+ cells.push({ kind: 'ungrouped-col', row, span: 1, rowspan: totalHeaderRows })
+ }
+ }
+ cells.push(
+ ...nodes.map((node) => ({
+ kind: 'band',
+ gid: node.gid,
+ group: node.group,
+ level: node.level,
+ span: node.span,
+ rowspan: node.children.length ? 1 : maxLevel - node.level + 1,
+ })),
+ )
+ rows.push(cells)
+ }
+ return rows
+ })
+
+ const ungroupedCount = computed(() => groupColumnsMap.value.ungrouped.length)
+ const ungroupedInBand = computed(
+ () => bandRows.value.length > 0 && groupColumnsMap.value.ungrouped.length > 0,
+ )
+
+ // 列头行:未分组列头已渲染在 band 行时,这里只渲染分组内列
+ const headerColumns = computed(() => {
+ if (!ungroupedInBand.value) return visualColumns.value
+ return visualColumns.value.filter((c) => c.gid != null)
+ })
+
+ return { visualColumns, bandRows, headerColumns, ungroupedInBand, ungroupedCount }
+}
diff --git a/code/fms/fms-vue/src/views/system/module-management/composables/useModuleConfig.js b/code/fms/fms-vue/src/views/system/module-management/composables/useModuleConfig.js
index 92dcf2d3..b60cdaa1 100644
--- a/code/fms/fms-vue/src/views/system/module-management/composables/useModuleConfig.js
+++ b/code/fms/fms-vue/src/views/system/module-management/composables/useModuleConfig.js
@@ -2,8 +2,7 @@
* 草稿加载与脏检查:
* configLoading / originals / drafts / loadedModuleId / skipModuleLoad 约定、
* loadModuleConfig / hasModuleChanges / reloadModule / onModuleSelect /
- * watch(moduleSelectedId),以及草稿派生联动(onFieldsUpdate / applyDisabledFieldDefaults /
- * onAutoCodeUpdate / syncFields)。
+ * watch(moduleSelectedId),以及草稿派生联动(onFieldsUpdate / onAutoCodeUpdate / syncFields)。
*/
import { computed, watch } from 'vue'
import { Message } from '@/components/ui'
@@ -11,6 +10,7 @@ import { loadDataApi, describeApi } from '@/services/api'
import { cloneData } from '@/utils/dataChanges'
import { createTempId } from '@/utils/tempId'
import { buildModuleItemI18nKey } from '@/utils/i18n'
+import { listRowDefaults, queryRowDefaults } from '../fieldAreaDefaults'
export function useModuleConfig(deps) {
const {
@@ -76,6 +76,12 @@ export function useModuleConfig(deps) {
}
originals.value = original
drafts.value = cloneData(original)
+ // 列表配置自动包含全部启用字段:缺失的按控件类型生成默认列。
+ // 「删除列」语义为隐藏(b_visible=0)而非删行——行删了下次加载又会自动补回。
+ const listGenerated = buildMissingDefaultConfigs(original.module, original.fields)
+ if (listGenerated.listConfig.length) {
+ drafts.value.listConfig = [...(drafts.value.listConfig || []), ...listGenerated.listConfig]
+ }
loadedModuleId.value = id
activeSection.value = 'basic'
} catch (e) {
@@ -150,36 +156,13 @@ export function useModuleConfig(deps) {
}
/* ================= 草稿派生联动 ================= */
+ // 字段停用不做跨表联动:运行时按字段 b_canuse 过滤显示,各配置行原样保留,
+ // 重新启用字段即恢复原配置。
function onFieldsUpdate(newFields) {
if (!drafts.value) return
- applyDisabledFieldDefaults(drafts.value.fields, newFields)
drafts.value.fields = newFields
}
- function applyDisabledFieldDefaults(previousFields, nextFields) {
- const previousStates = new Map()
- for (const field of previousFields || []) {
- if (field.b_id != null) previousStates.set(String(field.b_id), Number(field.b_canuse) === 1)
- }
- const disabledIds = new Set()
- for (const field of nextFields || []) {
- if (field.b_id == null) continue
- if (previousStates.get(String(field.b_id)) && Number(field.b_canuse) !== 1) {
- disabledIds.add(String(field.b_id))
- }
- }
- if (!disabledIds.size) return
- for (const row of drafts.value.listConfig || []) {
- if (disabledIds.has(String(row.b_field_id))) row.b_visible = 0
- }
- for (const row of drafts.value.editConfig || []) {
- if (disabledIds.has(String(row.b_field_id))) row.b_visible = 0
- }
- for (const row of drafts.value.queryConfig || []) {
- if (disabledIds.has(String(row.b_field_id))) row.b_canuse = 0
- }
- }
-
function onAutoCodeUpdate(rows) {
if (!drafts.value) return
const previousRule = drafts.value.autoCode?.[0]
@@ -200,6 +183,130 @@ export function useModuleConfig(deps) {
}
/* ================= 模块:同步字段 ================= */
+ // 已配置过对应配置行的字段 id(列表/编辑/查询),用于判断新增字段是否缺默认配置
+ function configuredFieldIds(section) {
+ const ids = new Set()
+ for (const source of [originals.value, drafts.value]) {
+ for (const row of source?.[section] || []) {
+ if (row.b_field_id != null) ids.add(String(row.b_field_id))
+ }
+ }
+ return ids
+ }
+
+ // 为尚未配置的字段生成列表/查询默认行(参考旧系统 buildMissingDefaultConfigs)。
+ // 默认属性见 fieldAreaDefaults:默认不参与的字段(大文本等)同样生成行、开关置 0,
+ // 保留配置空间;表单设计(editConfig)不自动生成:字段由用户在表单设计器中逐个添加,
+ // 自动铺满会与设计器的画布/分组/栅格编辑冲突。
+ function buildMissingDefaultConfigs(module, fields) {
+ const moduleId = String(module.b_id)
+ const listFieldIds = configuredFieldIds('listConfig')
+ const queryFieldIds = configuredFieldIds('queryConfig')
+ const generated = { listConfig: [], queryConfig: [] }
+ let orderIndex = 0
+ for (const field of fields) {
+ if (Number(field.b_canuse) !== 1) continue
+ orderIndex++
+ const fieldId = String(field.b_id)
+ const order = orderIndex * 10
+ if (!listFieldIds.has(fieldId)) {
+ generated.listConfig.push({
+ b_id: createTempId(),
+ b_module_id: moduleId,
+ b_field_id: fieldId,
+ b_group_id: null,
+ b_i18n: field.b_i18n || null,
+ b_xh: order,
+ ...listRowDefaults(field),
+ })
+ }
+ if (!queryFieldIds.has(fieldId)) {
+ generated.queryConfig.push({
+ b_id: createTempId(),
+ b_module_id: moduleId,
+ b_field_module_id: moduleId,
+ b_field_id: fieldId,
+ b_relation_id: null,
+ b_default_value: null,
+ b_xh: order,
+ ...queryRowDefaults(field),
+ })
+ }
+ }
+ return generated
+ }
+
+ // 计算同步计划(纯计算,不改草稿):describe 结果与现有字段合并、
+ // 找出视图已不存在的字段、为新字段生成列表/编辑/查询默认配置。
+ function buildSyncPlan(module, columns) {
+ const viewColumnSet = new Set(columns.map((col) => String(col.name || '').toLowerCase()))
+ const existingMap = new Map()
+ for (const field of drafts.value.fields || []) {
+ if (field.b_field) existingMap.set(field.b_field.toLowerCase(), field)
+ }
+ const mergedFields = []
+ let addedCount = 0
+ for (const col of columns) {
+ const existing = existingMap.get(String(col.name).toLowerCase())
+ if (existing) {
+ mergedFields.push({ ...existing, b_canuse: 1 })
+ } else {
+ addedCount++
+ mergedFields.push({
+ b_id: createTempId(),
+ b_module_id: String(module.b_id),
+ b_field: col.name,
+ b_name: col.name,
+ b_i18n: null,
+ b_type: 'input',
+ b_dbtype: col.type || null,
+ b_length: col.size ?? null,
+ b_nullable: col.nullable ?? 1,
+ b_default_value: null,
+ b_canuse: 1,
+ })
+ }
+ }
+ // 视图中已不存在的字段:应用时移除并级联清理配置行与受管 i18n
+ const removedIds = new Set()
+ // 受管多语言的 _managedId 是 `field:${i18nKey}`(见 index.vue finalI18nItems),不是字段 b_id
+ const removedI18nIds = new Set()
+ for (const existing of drafts.value.fields || []) {
+ if (!viewColumnSet.has(String(existing.b_field).toLowerCase())) {
+ removedIds.add(String(existing.b_id))
+ const key = existing.b_i18n || buildModuleItemI18nKey('field', module.b_code, existing.b_field)
+ if (key) removedI18nIds.add(`field:${key}`)
+ }
+ }
+ for (const field of mergedFields) {
+ field.b_i18n = buildModuleItemI18nKey('field', module.b_code, field.b_field)
+ }
+ const generated = buildMissingDefaultConfigs(module, mergedFields)
+ const generatedCount = generated.listConfig.length + generated.queryConfig.length
+ return { mergedFields, removedIds, removedI18nIds, generated, addedCount, generatedCount }
+ }
+
+ function applySyncPlan(plan) {
+ if (plan.removedIds.size) {
+ for (const section of ['listConfig', 'editConfig', 'queryConfig']) {
+ drafts.value[section] = (drafts.value[section] || []).filter(
+ (row) => !plan.removedIds.has(String(row.b_field_id)),
+ )
+ }
+ i18nPanelRef.value?.removeByManagedIds?.(Array.from(plan.removedI18nIds))
+ }
+ for (const section of ['listConfig', 'queryConfig']) {
+ const rows = plan.generated[section]
+ if (!rows.length) continue
+ drafts.value[section] = [...(drafts.value[section] || []), ...rows]
+ }
+ drafts.value.fields = plan.mergedFields
+ const parts = [`已同步 ${plan.mergedFields.length} 个字段`]
+ if (plan.addedCount) parts.push(`新增 ${plan.addedCount} 个并补充 ${plan.generatedCount} 条默认配置`)
+ if (plan.removedIds.size) parts.push(`移除 ${plan.removedIds.size} 个及其关联配置`)
+ Message.success(parts.join(','))
+ }
+
async function syncFields() {
const module = drafts.value?.module
if (!module) return
@@ -208,54 +315,14 @@ export function useModuleConfig(deps) {
configSyncing.value = true
try {
const res = await describeApi(view)
- const columns = res.data || []
- const existingFields = drafts.value.fields || []
- const viewColumnSet = new Set(columns.map((col) => String(col.name || '').toLowerCase()))
- const existingMap = new Map()
- for (const field of existingFields) {
- if (field.b_field) existingMap.set(field.b_field.toLowerCase(), field)
+ const plan = buildSyncPlan(module, res.data || [])
+ if (!plan.addedCount && !plan.removedIds.size) {
+ Message.info('字段与视图结构一致,无需同步')
+ return
}
- const mergedFields = []
- for (const col of columns) {
- const existing = existingMap.get(String(col.name).toLowerCase())
- if (existing) {
- mergedFields.push({ ...existing, b_canuse: 1 })
- } else {
- mergedFields.push({
- b_id: createTempId(),
- b_module_id: String(module.b_id),
- b_field: col.name,
- b_name: col.name,
- b_i18n: null,
- b_type: 'input',
- b_dbtype: col.type || null,
- b_length: col.size ?? null,
- b_nullable: col.nullable ?? 1,
- b_default_value: null,
- b_canuse: 1,
- })
- }
- }
- // 视图中已不存在的字段:移除并清理关联配置行
- const removedIds = new Set()
- for (const existing of existingFields) {
- if (!viewColumnSet.has(String(existing.b_field).toLowerCase())) {
- removedIds.add(String(existing.b_id))
- }
- }
- if (removedIds.size) {
- for (const section of ['listConfig', 'editConfig', 'queryConfig']) {
- drafts.value[section] = (drafts.value[section] || []).filter(
- (row) => !removedIds.has(String(row.b_field_id)),
- )
- }
- i18nPanelRef.value?.removeByManagedIds?.(Array.from(removedIds).map((id) => `field:${id}`))
- }
- for (const field of mergedFields) {
- field.b_i18n = buildModuleItemI18nKey('field', module.b_code, field.b_field)
- }
- drafts.value.fields = mergedFields
- Message.success(`已同步 ${mergedFields.length} 个字段`)
+ // 同步只改草稿、不落库(保存前可整体撤销),直接执行;
+ // 差异明细由 applySyncPlan 的成功提示汇总
+ applySyncPlan(plan)
} catch (e) {
Message.error('同步字段失败:' + (e?.message || e))
} finally {
@@ -263,11 +330,52 @@ export function useModuleConfig(deps) {
}
}
+ /* ================= 欠配置统计与一键补充 ================= */
+ // 启用字段中尚未出现在指定配置(listConfig/queryConfig)的数量
+ function missingConfigCount(section) {
+ return computed(() => {
+ const fields = drafts.value?.fields || []
+ if (!fields.length) return 0
+ const configured = new Set()
+ for (const row of drafts.value?.[section] || []) {
+ if (row.b_field_id != null) configured.add(String(row.b_field_id))
+ }
+ let count = 0
+ for (const field of fields) {
+ if (Number(field.b_canuse) !== 1) continue
+ if (!configured.has(String(field.b_id))) count++
+ }
+ return count
+ })
+ }
+
+ // 为缺失的启用字段补充默认配置行(复用同步字段的默认行生成逻辑)
+ function fillMissingConfigs(sections) {
+ const module = drafts.value?.module
+ if (!module || busy.value) return
+ const generated = buildMissingDefaultConfigs(module, drafts.value.fields)
+ let total = 0
+ for (const section of sections) {
+ const rows = generated[section]
+ if (!rows?.length) continue
+ drafts.value[section] = [...(drafts.value[section] || []), ...rows]
+ total += rows.length
+ }
+ if (total) Message.success(`已补充 ${total} 条默认配置`)
+ else Message.info('没有需要补充的配置')
+ }
+
+ const missingListCount = missingConfigCount('listConfig')
+ const missingQueryCount = missingConfigCount('queryConfig')
+
return {
reloadModule,
onModuleSelect,
onFieldsUpdate,
onAutoCodeUpdate,
syncFields,
+ missingListCount,
+ missingQueryCount,
+ fillMissingConfigs,
}
}
\ No newline at end of file
diff --git a/code/fms/fms-vue/src/views/system/module-management/composables/useModuleSave.js b/code/fms/fms-vue/src/views/system/module-management/composables/useModuleSave.js
index c5af6d1c..d18bca93 100644
--- a/code/fms/fms-vue/src/views/system/module-management/composables/useModuleSave.js
+++ b/code/fms/fms-vue/src/views/system/module-management/composables/useModuleSave.js
@@ -146,6 +146,10 @@ export function useModuleSave({
}
await saveObjectApi(saveReqs)
+ // 保存过程原地改写了草稿行的 b_id / b_field_id(临时 id → 真实 id),数组引用未变,
+ // 各面板靠引用变化触发的 watch 不会重新同步——本地副本会残留临时 id,下次编辑回灌
+ // 就把字段 id 打回负数(引发重复插入与配置行悬空引用)。这里整体换引用强制各面板重新拉取。
+ drafts.value = cloneData(drafts.value)
originals.value = cloneData(drafts.value)
i18nPanel?.markSaved?.()
unsavedModuleIds.delete(moduleId)
diff --git a/code/fms/fms-vue/src/views/system/module-management/composables/useModuleTree.js b/code/fms/fms-vue/src/views/system/module-management/composables/useModuleTree.js
index 5f643af2..a9f771c1 100644
--- a/code/fms/fms-vue/src/views/system/module-management/composables/useModuleTree.js
+++ b/code/fms/fms-vue/src/views/system/module-management/composables/useModuleTree.js
@@ -291,28 +291,22 @@ export function useModuleTree(deps) {
onOk: async () => {
try {
const id2 = String(module.b_id)
- const reqs = []
- // 收集子表行
+ // 子表按 b_module_id 条件删(后端 deletes 行内所有列组成 AND 条件),
+ // 无需先查出各行 id;主表仍按主键删
const subTables = [
- ['s_field', 'b_module_id'],
- ['s_field_group', 'b_module_id'],
- ['s_field_view', 'b_module_id'],
- ['s_field_edit', 'b_module_id'],
- ['s_field_query', 'b_module_id'],
- ['s_autocode', 'b_module_id'],
+ 's_field',
+ 's_field_group',
+ 's_field_view',
+ 's_field_edit',
+ 's_field_query',
+ 's_autocode',
+ 's_power',
]
- for (const [table, field] of subTables) {
- const res = await loadDataApi(table, `${field} = ${id2}`)
- const rows = res.data || []
- if (rows.length) {
- reqs.push({ table, key_field: 'b_id', deletes: rows.map((r) => ({ b_id: r.b_id })) })
- }
- }
- const powerRes = await loadDataApi('s_power', `b_module_id = ${id2}`)
- const powers = powerRes.data || []
- if (powers.length) {
- reqs.push({ table: 's_power', key_field: 'b_id', deletes: powers.map((r) => ({ b_id: r.b_id })) })
- }
+ const reqs = subTables.map((table) => ({
+ table,
+ key_field: 'b_id',
+ deletes: [{ b_module_id: id2 }],
+ }))
// 多语言(s_i18n 无 b_module_id 字段,需按模块编码匹配 b_key 前缀)。
// 若不清理,删除后残留的孤儿键会被同编码新模块的多语言面板加载,
// 进而产生指向已不存在记录的 UPDATE,导致后端「修改记录数异常: 0」。
diff --git a/code/fms/fms-vue/src/views/system/module-management/composables/useStructureEditing.js b/code/fms/fms-vue/src/views/system/module-management/composables/useStructureEditing.js
new file mode 100644
index 00000000..6b870603
--- /dev/null
+++ b/code/fms/fms-vue/src/views/system/module-management/composables/useStructureEditing.js
@@ -0,0 +1,641 @@
+/**
+ * 结构编辑内核(分组树 + 行归属 + 平铺重编 + 选中态 + 列属性剪贴板)。
+ * 由 TableEditPanel(子表设计,s_field_edit 行)与 ModuleListConfigPanel(列表配置,
+ * s_field_view 行)共用:宿主只注入数据源(getter)、行工厂(差异字段)、
+ * 提交回调(转 emit)与重命名请求(转发结构树 startRename)。
+ *
+ * 语义约定:
+ * - 分组 b_xh 为「兄弟内编号」((i+1)*10),不同层可重复;
+ * - 行 b_xh 为「未分组在前 + 分组树先序」的全局平铺序,与运行时列序一致;
+ * - 删除分组:直接子分组提升为被删分组的父级,组内行归未分组;级联清理其它配置
+ * 分节的引用由 applyGroups(groups, removedGroupIds) 的宿主(index.vue)统一完成。
+ */
+import { computed, nextTick, ref, watch } from 'vue'
+import { Message } from '@/components/ui'
+import { createTempId } from '@/utils/tempId'
+import { useGroupTree, buildGroupTree } from './useGroupTree'
+import { applyClipboardProps, buildClipboardProps, renumberRows } from '../editPanelShared'
+
+const DEFAULT_CLIP_KEYS = [
+ 'b_required',
+ 'b_readonly',
+ 'b_disabled',
+ 'b_visible',
+ 'b_placeholder',
+ 'b_height',
+ 'b_width',
+]
+
+/**
+ * @param {object} opts
+ * @param {() => string} opts.moduleId
+ * @param {() => Array} opts.getGroups 宿主 props.groups(不可变基准)
+ * @param {() => Array} opts.getRows 宿主 props.editConfig / props.listConfig
+ * @param {() => boolean} opts.loading
+ * @param {(field: object, groupId: string|null) => object} opts.createRow 行工厂(差异字段)
+ * @param {(groups: Array, removedGroupIds?: string[]) => void} opts.applyGroups
+ * @param {(rows: Array) => void} opts.applyRows
+ * @param {(gid: string) => void} [opts.requestRename] 新建分组后请求结构树进入重命名
+ * @param {string[]} [opts.clipboardKeys] 列属性复制粘贴键集
+ * @param {(row: object) => string} [opts.fieldNameOf] 剪贴板条目命名(useFieldLookup 的 fieldName)
+ * @param {(fieldId: string) => object|null} [opts.getField] 字段库拖入的字段解析(fieldMap.get)
+ */
+export function useStructureEditing({
+ moduleId,
+ getGroups,
+ getRows,
+ loading,
+ createRow,
+ applyGroups,
+ applyRows,
+ requestRename = () => {},
+ clipboardKeys = DEFAULT_CLIP_KEYS,
+ fieldNameOf = (row) => String(row?.b_field_id || ''),
+ getField = null,
+}) {
+ /* ================= 本地草稿 ================= */
+ const localRows = ref([])
+ const localGroups = ref([])
+ const selectedNodeKey = ref(null)
+
+ const { groupColumnsMap, bandMeta, isDescendantGroup, groupIdOfRow } = useGroupTree(
+ localGroups,
+ localRows,
+ )
+
+ watch(
+ getGroups,
+ (val) => {
+ localGroups.value = val ? val.map((row) => ({ ...row })) : []
+ },
+ { immediate: true },
+ )
+
+ watch(
+ getRows,
+ (val) => {
+ if (val === localRows.value) return
+ localRows.value = val?.map((row) => ({ ...row })) || []
+ reorderSilently()
+ },
+ { immediate: true },
+ )
+
+ function findRowByFieldId(fid) {
+ return localRows.value.find((r) => String(r.b_field_id) === String(fid)) || null
+ }
+
+ /* ================= 结构规范化 ================= */
+ function reorderSilently() {
+ const columns = groupColumnsMap.value
+ const list = [...columns.ungrouped]
+ const walk = (nodes) => {
+ for (const node of nodes) {
+ for (const row of columns.map.get(node.gid) || []) list.push(row)
+ walk(node.children)
+ }
+ }
+ walk(bandMeta.value.roots)
+ if (list.length === localRows.value.length && list.every((r, i) => r === localRows.value[i])) {
+ return
+ }
+ localRows.value = list
+ }
+
+ function rebuildFlat(roots = bandMeta.value.roots) {
+ const columns = groupColumnsMap.value
+ const list = [...columns.ungrouped]
+ const walk = (nodes) => {
+ for (const node of nodes) {
+ for (const row of columns.map.get(node.gid) || []) list.push(row)
+ walk(node.children)
+ }
+ }
+ walk(roots)
+ localRows.value = list.map((row, i) => ({ ...row, b_xh: (i + 1) * 10 }))
+ applyRows(localRows.value)
+ }
+
+ // 由给定分组数组构建可用于重排的 band 根节点(gid + children),与 bandMeta 解耦
+ function toRoots(nodes) {
+ return nodes.map((node) => ({ gid: String(node.group.b_id), children: toRoots(node.children) }))
+ }
+ function bandRootsFromGroups(groups) {
+ return toRoots(buildGroupTree(groups))
+ }
+
+ // 分组 b_xh 按每层兄弟内编号(非全局序)
+ function renumberAllSiblings(groups) {
+ const roots = buildGroupTree(groups)
+ const result = []
+ const walk = (nodes) => {
+ nodes.forEach((node, i) => {
+ result.push({ ...node.group, b_xh: (i + 1) * 10 })
+ walk(node.children)
+ })
+ }
+ walk(roots)
+ return result
+ }
+ function emitGroupsRenumbered(nextGroups) {
+ applyGroups(renumberAllSiblings(nextGroups))
+ }
+
+ /* ================= 选择 ================= */
+ const selectedRow = computed(() => {
+ const key = selectedNodeKey.value
+ if (!key || !key.startsWith('c:')) return null
+ return findRowByFieldId(key.slice(2)) || null
+ })
+ const selectedBand = computed(() => {
+ const key = selectedNodeKey.value
+ if (!key || !key.startsWith('g:') || key === 'g:ungrouped') return null
+ return bandMeta.value.metaMap.get(key.slice(2)) || null
+ })
+ function selectRow(row) {
+ selectedNodeKey.value = row ? `c:${row.b_field_id}` : null
+ }
+ function selectBand(band) {
+ selectedNodeKey.value = band ? `g:${band.gid}` : null
+ }
+ function clearSelection() {
+ selectedNodeKey.value = null
+ }
+
+ /* ================= 分组带:增删改 ================= */
+ function nextGroupXh() {
+ return (getGroups() || []).reduce((m, g) => Math.max(m, Number(g.b_xh || 0)), 0) + 10
+ }
+
+ function addRootGroup() {
+ if (loading()) return
+ const newGroup = {
+ b_id: createTempId(),
+ b_module_id: moduleId(),
+ b_parent_id: null,
+ b_title: '',
+ b_xh: nextGroupXh(),
+ b_canuse: 1,
+ }
+ emitGroupsRenumbered([...(getGroups() || []), newGroup])
+ focusNewBandTitle(newGroup.b_id)
+ }
+
+ function addSubGroup(band) {
+ if (!band || loading()) return
+ const newGroup = {
+ b_id: createTempId(),
+ b_module_id: moduleId(),
+ b_parent_id: String(band.gid),
+ b_title: '',
+ b_xh: nextGroupXh(),
+ b_canuse: 1,
+ }
+ emitGroupsRenumbered([...(getGroups() || []), newGroup])
+ focusNewBandTitle(newGroup.b_id)
+ }
+
+ function focusNewBandTitle(gid) {
+ nextTick(() => {
+ if (!bandMeta.value.metaMap.has(String(gid))) return
+ selectedNodeKey.value = `g:${gid}`
+ requestRename(String(gid))
+ })
+ }
+
+ function deleteBand(band) {
+ if (!band || loading()) return
+ const gid = String(band.gid)
+ const full = bandMeta.value.metaMap.get(gid)
+ if (!full) return
+ const title = full.group.b_title || `分组-${gid}`
+ const affected = new Set()
+ const collect = (nodes) => {
+ for (const node of nodes) {
+ affected.add(String(node.gid))
+ collect(node.children)
+ }
+ }
+ collect([full])
+ const childGids = new Set(full.children.map((n) => String(n.gid)))
+ const parentId = full.group.b_parent_id == null ? null : String(full.group.b_parent_id)
+ let groups = (getGroups() || []).filter((g) => String(g.b_id) !== gid)
+ groups = groups.map((g) =>
+ childGids.has(String(g.b_id)) ? { ...g, b_parent_id: parentId } : g,
+ )
+ for (const row of localRows.value) {
+ if (groupIdOfRow(row) === gid) row.b_group_id = null
+ }
+ rebuildFlat(bandRootsFromGroups(groups))
+ // removedGroupIds 上抛:其它配置分节(列表/编辑/表单)的分组引用由宿主统一清理
+ applyGroups(groups, [gid])
+ if (selectedNodeKey.value === `g:${gid}`) selectedNodeKey.value = null
+ Message.success(
+ affected.size > 1 ? `已删除分组「${title}」,其下字段移至「未分组」` : `已删除分组「${title}」`,
+ )
+ }
+
+ function onRenameCommit({ gid, title }) {
+ const value = String(title ?? '').trim()
+ applyGroups(
+ (getGroups() || []).map((g) =>
+ String(g.b_id) === String(gid) ? { ...g, b_title: value } : g,
+ ),
+ )
+ }
+
+ function onBandTitleChange(value) {
+ const band = selectedBand.value
+ if (!band || loading()) return
+ const title = String(value ?? '').trim()
+ if (band.group.b_title === title) return
+ applyGroups(
+ (getGroups() || []).map((g) =>
+ String(g.b_id) === String(band.gid) ? { ...g, b_title: title } : g,
+ ),
+ )
+ }
+
+ function toggleBandEnable() {
+ const band = selectedBand.value
+ if (!band || loading()) return
+ applyGroups(
+ (getGroups() || []).map((g) =>
+ String(g.b_id) === String(band.gid)
+ ? { ...g, b_canuse: Number(g.b_canuse) === 1 ? 0 : 1 }
+ : g,
+ ),
+ )
+ }
+
+ /* ================= 列:增删改 ================= */
+ function bandOwnRangeEnd(gid, rows) {
+ let last = -1
+ rows.forEach((r, i) => {
+ if (groupIdOfRow(r) === gid) last = i
+ })
+ return last
+ }
+
+ function countColumnsBefore(gid, rows) {
+ const columns = groupColumnsMap.value
+ let count = 0
+ let found = false
+ const walk = (nodes) => {
+ if (found) return
+ for (const node of nodes) {
+ if (node.gid === gid) {
+ found = true
+ return
+ }
+ count += (columns.map.get(node.gid) || []).length
+ walk(node.children)
+ if (found) return
+ }
+ }
+ walk(bandMeta.value.roots)
+ return found ? count : rows.length
+ }
+
+ function appendIntoGroup(row, groupId, rows) {
+ if (groupId != null) {
+ const last = bandOwnRangeEnd(String(groupId), rows)
+ if (last >= 0) rows.splice(last + 1, 0, row)
+ else rows.splice(countColumnsBefore(String(groupId), rows), 0, row)
+ } else {
+ rows.push(row)
+ }
+ }
+
+ function insertField(field, { groupId = null, targetId = null, position = 'after' } = {}) {
+ if (!field || loading()) return null
+ const existing = localRows.value.find((r) => String(r.b_field_id) === String(field.b_id))
+ if (existing) return existing
+ const row = createRow(field, groupId)
+ if (targetId != null) {
+ const tIdx = localRows.value.findIndex(
+ (r) => String(r.b_field_id) === String(targetId),
+ )
+ if (tIdx >= 0) {
+ localRows.value.splice(tIdx + (position === 'after' ? 1 : 0), 0, row)
+ } else appendIntoGroup(row, groupId, localRows.value)
+ } else {
+ appendIntoGroup(row, groupId, localRows.value)
+ }
+ rebuildFlat()
+ return row
+ }
+
+ function removeRow(row) {
+ if (!row || loading()) return
+ localRows.value = renumberRows(
+ localRows.value.filter((item) => String(item.b_id) !== String(row.b_id)),
+ )
+ applyRows(localRows.value)
+ if (selectedNodeKey.value === `c:${row.b_field_id}`) selectedNodeKey.value = null
+ }
+
+ function removeSelected() {
+ const band = selectedBand.value
+ if (band) {
+ deleteBand(band)
+ return
+ }
+ removeRow(selectedRow.value)
+ }
+
+ function moveColumn(row, delta) {
+ const list = localRows.value
+ const gid = groupIdOfRow(row)
+ const own = []
+ for (let i = 0; i < list.length; i++) {
+ if (groupIdOfRow(list[i]) === gid) own.push(i)
+ }
+ const idx = own.findIndex((i) => String(list[i].b_id) === String(row.b_id))
+ const target = idx + (delta > 0 ? 1 : -1)
+ if (idx < 0 || target < 0 || target >= own.length) return
+ const a = own[idx]
+ const b = own[target]
+ const tmp = list[a]
+ list[a] = list[b]
+ list[b] = tmp
+ rebuildFlat()
+ selectedNodeKey.value = `c:${row.b_field_id}`
+ }
+
+ function moveBand(band, delta) {
+ const gid = String(band.gid)
+ const parentId = band.group.b_parent_id == null ? null : String(band.group.b_parent_id)
+ const parentNode = parentId != null ? bandMeta.value.metaMap.get(parentId) : null
+ const sibs = parentNode ? parentNode.children : bandMeta.value.roots
+ const idx = sibs.findIndex((n) => String(n.gid) === gid)
+ if (idx < 0) return
+ const target = idx + (delta > 0 ? 1 : -1)
+ if (target < 0 || target >= sibs.length) return
+ commitBandDrag(gid, {
+ kind: 'band-edge',
+ gid: sibs[target].gid,
+ pos: delta > 0 ? 'after' : 'before',
+ })
+ }
+
+ /* ================= 结构树节点常驻操作 ================= */
+ function onNodeMove({ key, direction }) {
+ if (loading() || !key || !direction) return
+ if (key.startsWith('c:')) {
+ const row = findRowByFieldId(key.slice(2))
+ if (row) moveColumn(row, direction)
+ } else if (key.startsWith('g:') && key !== 'g:ungrouped') {
+ const band = bandMeta.value.metaMap.get(key.slice(2))
+ if (band) moveBand(band, direction)
+ }
+ }
+ function onNodeDelete({ key }) {
+ if (loading() || !key) return
+ if (key.startsWith('c:')) {
+ const row = findRowByFieldId(key.slice(2))
+ if (row) removeRow(row)
+ } else if (key.startsWith('g:') && key !== 'g:ungrouped') {
+ const band = bandMeta.value.metaMap.get(key.slice(2))
+ if (band) deleteBand(band)
+ }
+ }
+ function onNodeAddSub({ key }) {
+ if (loading() || !key || !key.startsWith('g:') || key === 'g:ungrouped') return
+ const band = bandMeta.value.metaMap.get(key.slice(2))
+ if (band) addSubGroup(band)
+ }
+
+ /* ================= 字段定位/外部拖入(字段库) ================= */
+ function scrollToRowCell(fieldId) {
+ document
+ .querySelector(`.mm-tdesign__cell[data-field-id="${fieldId}"]`)
+ ?.scrollIntoView?.({ block: 'nearest', behavior: 'smooth' })
+ }
+
+ function selectField(field) {
+ if (!field || loading()) return
+ const existing = findRowByFieldId(String(field.b_id))
+ if (existing) {
+ selectRow(existing)
+ nextTick(() => scrollToRowCell(String(existing.b_field_id)))
+ return existing
+ }
+ const band = selectedBand.value
+ const gid = band ? String(band.gid) : null
+ const row = insertField(field, { groupId: gid })
+ if (!row) return null
+ selectRow(row)
+ nextTick(() => scrollToRowCell(String(row.b_field_id)))
+ return row
+ }
+
+ // 字段库外部拖入:dropKey 为 'g:ungrouped' / 'g:
' / 'c:'
+ function onFieldDrop({ fieldId, dropKey, dropPosition }) {
+ if (loading() || !fieldId || !getField) return
+ const field = getField(String(fieldId))
+ if (!field) return
+ const existing = findRowByFieldId(fieldId)
+ if (existing) {
+ if (dropKey === 'g:ungrouped') {
+ commitColDrag(fieldId, { kind: 'ungrouped' })
+ } else if (dropKey.startsWith('g:')) {
+ commitColDrag(fieldId, { kind: 'into-band', gid: dropKey.slice(2) })
+ } else {
+ const row = findRowByFieldId(dropKey.slice(2))
+ commitColDrag(fieldId, {
+ kind: 'col-edge',
+ gid: row ? groupIdOfRow(row) : null,
+ targetId: dropKey.slice(2),
+ pos: dropPosition === 'before' ? 'before' : 'after',
+ })
+ }
+ return
+ }
+ let groupId = null
+ let targetId = null
+ if (dropKey === 'g:ungrouped') {
+ groupId = null
+ } else if (dropKey.startsWith('g:')) {
+ groupId = dropKey.slice(2)
+ } else {
+ const row = findRowByFieldId(dropKey.slice(2))
+ groupId = row ? groupIdOfRow(row) : null
+ targetId = row ? dropKey.slice(2) : null
+ }
+ const created = insertField(field, {
+ groupId,
+ targetId,
+ position: dropPosition === 'before' ? 'before' : 'after',
+ })
+ if (!created) return
+ selectRow(created)
+ nextTick(() => scrollToRowCell(String(created.b_field_id)))
+ }
+
+ /* ================= 复制/粘贴(列属性) ================= */
+ const clipboardProps = ref(null)
+
+ function copyRowProps() {
+ const row = selectedRow.value
+ if (!row || loading()) return
+ clipboardProps.value = buildClipboardProps(row, fieldNameOf, clipboardKeys)
+ }
+
+ function pastePropsToSelected() {
+ const row = selectedRow.value
+ if (!row || loading() || !clipboardProps.value) return
+ applyClipboardProps(row, clipboardProps.value.props)
+ applyRows(localRows.value)
+ }
+
+ /* ================= 结构拖拽 commit ================= */
+ function commitColDrag(fieldId, line) {
+ const rows = localRows.value
+ const idx = rows.findIndex((r) => String(r.b_field_id) === String(fieldId))
+ if (idx < 0) return
+ const [moved] = rows.splice(idx, 1)
+ let insertAt = -1
+ if (line.kind === 'col-edge') {
+ const tIdx = rows.findIndex((r) => String(r.b_field_id) === String(line.targetId))
+ if (tIdx < 0) {
+ rows.splice(idx, 0, moved)
+ return
+ }
+ moved.b_group_id = line.gid == null ? null : String(line.gid)
+ insertAt = tIdx + (line.pos === 'after' ? 1 : 0)
+ } else if (line.kind === 'into-band') {
+ moved.b_group_id = String(line.gid)
+ const last = bandOwnRangeEnd(String(line.gid), rows)
+ insertAt = last >= 0 ? last + 1 : countColumnsBefore(String(line.gid), rows)
+ } else if (line.kind === 'col-group-edge') {
+ const target = bandMeta.value.metaMap.get(String(line.gid))
+ if (!target) {
+ rows.splice(idx, 0, moved)
+ return
+ }
+ moved.b_group_id = target.group.b_parent_id == null ? null : String(target.group.b_parent_id)
+ insertAt = countColumnsBefore(String(line.gid), rows)
+ if (line.pos === 'after') insertAt += target.span
+ } else if (line.kind === 'ungrouped') {
+ moved.b_group_id = null
+ insertAt = rows.length
+ } else {
+ rows.splice(idx, 0, moved)
+ return
+ }
+ if (insertAt < 0) insertAt = rows.length
+ rows.splice(insertAt, 0, moved)
+ rebuildFlat()
+ selectedNodeKey.value = `c:${moved.b_field_id}`
+ }
+
+ function commitBandDrag(sourceGid, line) {
+ const source = bandMeta.value.metaMap.get(String(sourceGid))
+ if (!source) return
+ let newParentId = null
+ let orderedSibGids = []
+ if (line.kind === 'band-nest') {
+ const target = bandMeta.value.metaMap.get(String(line.gid))
+ if (!target || isDescendantGroup(String(sourceGid), line.gid)) return
+ newParentId = String(target.gid)
+ orderedSibGids = [
+ ...target.children.filter((n) => n.gid !== sourceGid).map((n) => n.gid),
+ sourceGid,
+ ]
+ } else if (line.kind === 'band-edge') {
+ const target = bandMeta.value.metaMap.get(String(line.gid))
+ 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)
+ .filter((n) => n.gid !== sourceGid)
+ .map((n) => n.gid)
+ const tIdx = sibGids.indexOf(String(line.gid))
+ if (tIdx < 0) return
+ orderedSibGids = [...sibGids]
+ orderedSibGids.splice(line.pos === 'after' ? tIdx + 1 : tIdx, 0, sourceGid)
+ } else {
+ return
+ }
+ const sibSet = new Set(orderedSibGids.map(String))
+ const orderById = new Map(orderedSibGids.map((gid, i) => [String(gid), i]))
+ const groups = (getGroups() || []).map((g) => {
+ const c = { ...g }
+ const gid = String(g.b_id)
+ if (gid === String(sourceGid)) c.b_parent_id = newParentId
+ if (sibSet.has(gid)) c.b_xh = (orderById.get(gid) + 1) * 10
+ return c
+ })
+ applyGroups(groups)
+ // 乐观回写:不等宿主 props 往返,避免 bandMeta 在提交间隙用旧树
+ localGroups.value = groups.map((g) => ({ ...g }))
+ rebuildFlat(bandRootsFromGroups(groups))
+ selectedNodeKey.value = `g:${sourceGid}`
+ }
+
+ /* ================= 列宽(40–600 钳制) ================= */
+ function widthOf(row) {
+ const v = Number(row?.b_width)
+ return v >= 40 && v <= 600 ? v : 120
+ }
+
+ function onWidthCommit(value) {
+ const row = selectedRow.value
+ if (!row || loading()) return
+ const width = Math.min(600, Math.max(40, Number(value) || 120))
+ if (width === widthOf(row)) return
+ row.b_width = width
+ applyRows(localRows.value)
+ }
+
+ return {
+ // 状态
+ localRows,
+ localGroups,
+ selectedNodeKey,
+ // 选择
+ selectedRow,
+ selectedBand,
+ selectRow,
+ selectBand,
+ clearSelection,
+ // 树推导透传
+ groupColumnsMap,
+ bandMeta,
+ isDescendantGroup,
+ groupIdOfRow,
+ findRowByFieldId,
+ // 分组操作
+ addRootGroup,
+ addSubGroup,
+ deleteBand,
+ onRenameCommit,
+ onBandTitleChange,
+ toggleBandEnable,
+ commitBandDrag,
+ // 行操作
+ commitColDrag,
+ insertField,
+ removeRow,
+ removeSelected,
+ moveColumn,
+ moveBand,
+ // 树节点常驻操作
+ onNodeMove,
+ onNodeDelete,
+ onNodeAddSub,
+ // 字段定位
+ selectField,
+ scrollToRowCell,
+ // 字段库外部拖入
+ onFieldDrop,
+ // 剪贴板
+ clipboardProps,
+ copyRowProps,
+ pastePropsToSelected,
+ // 列宽
+ widthOf,
+ onWidthCommit,
+ }
+}
diff --git a/code/fms/fms-vue/src/views/system/module-management/editPanelShared.js b/code/fms/fms-vue/src/views/system/module-management/editPanelShared.js
index 3d11cb88..0e291a8d 100644
--- a/code/fms/fms-vue/src/views/system/module-management/editPanelShared.js
+++ b/code/fms/fms-vue/src/views/system/module-management/editPanelShared.js
@@ -1,6 +1,6 @@
/**
* 两个编辑面板共享的纯函数(无 Vue 依赖)。
- * 栅格重排、列属性复制/粘贴、可见性 Map、字段库搜索、编辑行构建等。
+ * 栅格重排、列属性复制/粘贴、字段库搜索、编辑行构建等。
*/
import { createTempId } from '@/utils/tempId'
@@ -9,17 +9,6 @@ export function renumberRows(rows) {
return rows.map((row, index) => ({ ...row, b_xh: (index + 1) * 10 }))
}
-/** 构建字段可见性 Map:b_field_id → b_visible(1/0) */
-export function buildVisibilityMap(rows) {
- const map = new Map()
- for (const row of rows) {
- if (row.b_field_id != null) {
- map.set(String(row.b_field_id), Number(row.b_visible) === 1 ? 1 : 0)
- }
- }
- return map
-}
-
/**
* 复制列属性:提取指定 keys 的标准化值,返回 { name, props }。
* 特殊键处理:b_placeholder → 字符串或 null;b_height/b_width → 数值或 null;
diff --git a/code/fms/fms-vue/src/views/system/module-management/fieldAreaDefaults.js b/code/fms/fms-vue/src/views/system/module-management/fieldAreaDefaults.js
new file mode 100644
index 00000000..44cbca1c
--- /dev/null
+++ b/code/fms/fms-vue/src/views/system/module-management/fieldAreaDefaults.js
@@ -0,0 +1,59 @@
+/**
+ * 字段区域默认策略(唯一真源):
+ * 「字段定义 → 列表列 / 查询条件默认配置行」的规则集中于此,供
+ * useModuleConfig.buildMissingDefaultConfigs(同步字段、补充缺失)与查询面板
+ * (选定字段后的组件/操作符重置)共用。
+ *
+ * 参与语义约定:配置行存在 = 已配置;是否参与运行时由行内开关表达
+ * (列表 b_visible、查询 b_canuse)。默认不参与的字段(大文本)同样生成行、
+ * 开关置 0——保留配置空间,勾选即恢复;删除行仅用于清掉不该存在的行(误加的查询条件)。
+ */
+
+/** 大文本字段:默认不进列表、默认不作为查询条件 */
+function isLargeText(field) {
+ return field.b_type === 'textarea' || field.b_dbtype === 'text'
+}
+
+/** 列表列默认宽度(按控件类型) */
+const LIST_WIDTH_DEFAULTS = {
+ input: 120,
+ textarea: 160,
+ number: 100,
+ money: 110,
+ date: 100,
+ datetime: 150,
+ checkbox: 60,
+ select: 110,
+}
+
+/** 字段 → 列表列默认属性(b_visible / b_width / b_sortable) */
+export function listRowDefaults(field) {
+ return {
+ b_visible: isLargeText(field) ? 0 : 1,
+ b_width: LIST_WIDTH_DEFAULTS[field.b_type] ?? 100,
+ b_sortable: 0,
+ }
+}
+
+/** 按控件类型给出查询组件/操作符默认值;未列出的(input/textarea)走通用 like */
+const QUERY_COMPONENT_DEFAULTS = {
+ number: { component: 'number', operator: 'eq' },
+ money: { component: 'number', operator: 'eq' },
+ date: { component: 'date', operator: 'ge' },
+ datetime: { component: 'datetime', operator: 'ge' },
+ select: { component: 'select', operator: 'eq' },
+ checkbox: { component: 'checkbox', operator: 'eq' },
+}
+
+/** 字段 → 查询条件默认属性(b_component / b_operator / b_canuse) */
+export function queryRowDefaults(field) {
+ const component = QUERY_COMPONENT_DEFAULTS[field.b_type] || {
+ component: 'input',
+ operator: 'like',
+ }
+ return {
+ b_component: component.component,
+ b_operator: component.operator,
+ b_canuse: isLargeText(field) ? 0 : 1,
+ }
+}
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 80b41ce6..55b49bd6 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
@@ -19,7 +19,6 @@ import {
List,
Pencil,
Hash,
- RefreshCw,
Shield,
Globe,
Info,
@@ -94,7 +93,6 @@ const isQueryModule = computed(() => moduleType.value === 'query')
/* ================= 编辑方式 / 导航 / i18n 展示(面板编排) ================= */
const editMode = computed(() => drafts.value?.module?.b_edit_mode || 'form')
-const editNavLabel = computed(() => (editMode.value === 'table' ? '表格设计' : '表单设计'))
const finalI18nItems = computed(() => {
const module = drafts.value?.module
@@ -117,6 +115,11 @@ const finalI18nItems = computed(() => {
const key = buildModuleItemI18nKey('power', module.b_code, power.b_code)
if (key) items.push({ key, managed: true, defaultValue: power.b_name || '', id: `power:${key}` })
}
+ // 分组标题多语言:b_i18n 为用户手填 key(空则运行时直接用标题原文,不纳入受管)
+ for (const group of drafts.value?.groups || []) {
+ const key = String(group.b_i18n || '').trim()
+ if (key) items.push({ key, managed: true, defaultValue: group.b_title || '', id: `group:${group.b_id}` })
+ }
return items
})
@@ -137,30 +140,18 @@ const configGroups = computed(() => {
items: [{ key: 'fields', label: '字段定义', icon: Table2 }],
})
- // 界面配置:表单设计(数据模块)→ 列表配置 → 查询配置
+ // 界面配置:表单设计(数据模块,含主表表单/子表表格两种编辑方式)→ 列表配置 → 查询配置
const uiItems = []
if (isDataModule.value) {
- const editLen = drafts.value?.editConfig?.length ?? 0
- uiItems.push({
- key: 'edit',
- label: editNavLabel.value,
- icon: Pencil,
- badge: editLen ? '已配置' : '',
- })
+ uiItems.push({ key: 'edit', label: '表单设计', icon: Pencil })
}
- const listLen = drafts.value?.listConfig?.length ?? 0
- uiItems.push({
- key: 'list',
- label: '列表配置',
- icon: List,
- badge: listLen ? '已配置' : '',
- })
- const queryLen = drafts.value?.queryConfig?.length ?? 0
+ // 列表配置行在加载时自动补全全部启用字段,无「缺失」概念,不带徽标
+ uiItems.push({ key: 'list', label: '列表配置', icon: List })
uiItems.push({
key: 'query',
label: '查询配置',
icon: Search,
- badge: queryLen ? '已配置' : '',
+ badge: missingQueryCount.value ? `缺 ${missingQueryCount.value}` : '',
})
groups.push({ key: 'display', label: '界面配置', items: uiItems })
@@ -195,6 +186,9 @@ const configNavItems = computed(() =>
configGroups.value.flatMap((group) => group.items.map((item) => ({ ...item }))),
)
+/* 编辑设计器:form/table 两面板 props/emits 契约一致,用动态组件消除模板重复 */
+const activeEditPanel = computed(() => (editMode.value === 'table' ? ModuleTableEditPanel : ModuleFormEditPanel))
+
/* ================= composables ================= */
const { saveModule } = useModuleSave({
selectedModule,
@@ -298,11 +292,12 @@ const {
})
const {
- reloadModule,
onModuleSelect,
onFieldsUpdate,
onAutoCodeUpdate,
syncFields,
+ missingQueryCount,
+ fillMissingConfigs,
} = useModuleConfig({
moduleSelectedId,
moduleTreeData,
@@ -337,6 +332,19 @@ function onModuleUpdate(section, newData) {
if (drafts.value) drafts.value[section] = newData
}
+/* 分组管理弹层提交:写回分组草稿,并清理被删分组在列表/编辑配置中的引用 */
+function onGroupsUpdate(groups, removedGroupIds) {
+ if (!drafts.value) return
+ drafts.value.groups = groups
+ const removed = new Set((removedGroupIds || []).map(String))
+ if (!removed.size) return
+ for (const section of ['listConfig', 'editConfig']) {
+ drafts.value[section] = (drafts.value[section] || []).map((row) =>
+ removed.has(String(row.b_group_id)) ? { ...row, b_group_id: null } : row,
+ )
+ }
+}
+
function onModuleI18nSaved() {
i18nPanelRef.value?.refreshKey?.()
}
@@ -408,6 +416,27 @@ loadModuleTree()
+
+
+
+ 保存
+
+
+ 同步字段
+
+
+
{{ item.label }}
{{ item.badge }}
-
-
-
@@ -456,9 +470,6 @@ loadModuleTree()
v-show="activeSection === 'fields'"
class="mm-config-pane"
>
-
- 同步字段
-
@@ -477,25 +487,23 @@ loadModuleTree()
>
onModuleUpdate('listConfig', v)"
- @sync-edit="(v) => onModuleUpdate('editConfig', v)"
- @sync-query="(v) => onModuleUpdate('queryConfig', v)"
+ @update-groups="onGroupsUpdate"
+ @i18n-saved="onModuleI18nSaved"
/>