diff --git a/code/fms/fms-vue/src/views/system/module-management/components/ModuleFormEditPanel.vue b/code/fms/fms-vue/src/views/system/module-management/components/ModuleFormEditPanel.vue index 289e7b2f..0c10cc45 100644 --- a/code/fms/fms-vue/src/views/system/module-management/components/ModuleFormEditPanel.vue +++ b/code/fms/fms-vue/src/views/system/module-management/components/ModuleFormEditPanel.vue @@ -36,6 +36,17 @@ import { } from '@/components/ui' import FmsTable from '@/components/fms-table/FmsTable.vue' import { createTempId } from '@/utils/tempId' +import { groupIdOfRow as groupIdOf } from '../composables/useGroupTree' +import { useFieldLookup } from '../composables/useFieldLookup' +import { + applyClipboardProps, + buildClipboardProps, + buildVisibilityMap, + createEditRowFactory, + filterLibraryFields, + renumberRows, + toggleFlag, +} from '../editPanelShared' const props = defineProps({ moduleId: { type: String, required: true }, @@ -186,14 +197,6 @@ function emitRows(rows) { emitUpdate() } -function renumberRows(rows) { - return rows.map((row, index) => ({ ...row, b_xh: (index + 1) * 10 })) -} - -function groupIdOf(row) { - return row?.b_group_id == null ? null : String(row.b_group_id) -} - /* 逻辑行推导:与 CSS grid 自动换行一致——从分组段首部顺序向前打包, 累计宽度超过 24 即换行。返回目标字段所在视觉行的索引,及行内其它字段宽度和 */ function collectLineIndexes(rows, insertedIndex) { @@ -245,21 +248,9 @@ function rebalanceInsert(rows, insertedIndex, { fill = true } = {}) { } /* ================= 公共查询表 ================= */ -const fieldNameMap = computed(() => { - const map = new Map() - for (const field of props.fields || []) { - map.set(String(field.b_id), field.b_name || field.b_field) - } - return map -}) - -const fieldMap = computed(() => { - const map = new Map() - for (const field of props.fields || []) { - map.set(String(field.b_id), field) - } - return map -}) +const { fieldNameMap, fieldMap, fieldOf, fieldName, controlTypeLabel } = useFieldLookup( + () => props.fields, +) const groupMap = computed(() => { const map = new Map() @@ -281,12 +272,7 @@ const groupOptions = computed(() => { /* ================= 数据视图:同步到列表 / 查询 ================= */ function syncToList() { if (props.loading) return - const visibleMap = new Map() - for (const row of localEdit.value) { - if (row.b_field_id != null) { - visibleMap.set(String(row.b_field_id), Number(row.b_visible) === 1 ? 1 : 0) - } - } + const visibleMap = buildVisibilityMap(localEdit.value) if (visibleMap.size === 0) { Message.info('编辑配置中没有字段') return @@ -306,12 +292,7 @@ function syncToList() { function syncToQuery() { if (props.loading) return - const visibleMap = new Map() - for (const row of localEdit.value) { - if (row.b_field_id != null) { - visibleMap.set(String(row.b_field_id), Number(row.b_visible) === 1 ? 1 : 0) - } - } + const visibleMap = buildVisibilityMap(localEdit.value) if (visibleMap.size === 0) { Message.info('编辑配置中没有字段') return @@ -389,17 +370,6 @@ function formDataColumns() { const dataColumns = computed(() => formDataColumns()) /* ================= 设计视图:字段/分组推导 ================= */ -const FIELD_TYPE_LABELS = { - input: '输入框', - textarea: '文本域', - number: '数字', - money: '金额', - date: '日期', - datetime: '日期时间', - checkbox: '复选框', - select: '下拉选择', -} - // 24 栅格制下的常用宽度选项(栅格宽度 b_colspan) const COLSPAN_OPTIONS = [ { label: '整行 24/24', value: '24' }, @@ -421,22 +391,6 @@ function spanOf(row) { return v >= 1 && v <= 24 ? v : 24 } -function fieldOf(row) { - if (row?.b_field_id == null) return null - return fieldMap.value.get(String(row.b_field_id)) || null -} - -function fieldName(row) { - const fid = row?.b_field_id - if (fid == null) return '' - return fieldNameMap.value.get(String(fid)) || String(fid) -} - -function controlTypeLabel(row) { - const field = fieldOf(row) - return FIELD_TYPE_LABELS[field?.b_type] || field?.b_type || '未知' -} - function isTextarea(row) { return fieldOf(row)?.b_type === 'textarea' } @@ -470,19 +424,7 @@ function heightOf(row) { const libSearch = ref('') // 字段库:展示模块的全部字段(含未加入表单的字段),按名称 a-z 排序(无类型分组小标题); // 搜索时按名称/编码过滤,命中才显示 -const libraryFields = computed(() => { - const kw = libSearch.value.trim().toLowerCase() - return [...(props.fields || [])] - .filter( - (f) => - !kw || - String(f.b_name || '').toLowerCase().includes(kw) || - String(f.b_field || '').toLowerCase().includes(kw), - ) - .sort((a, b) => - String(a.b_name || a.b_field || '').localeCompare(String(b.b_name || b.b_field || ''), 'zh'), - ) -}) +const libraryFields = computed(() => filterLibraryFields(props.fields, libSearch.value)) function isFieldAdded(field) { return localEdit.value.some((row) => String(row.b_field_id) === String(field.b_id)) @@ -732,24 +674,14 @@ function selectRow(row) { } // 字段库 → 画布:已加入则定位选中,未加入则按目标位置插入。 -function createEditRow(field, groupId) { - const sectionKey = groupId == null ? 'ungrouped' : `g-${String(groupId)}` - return { - b_id: createTempId(), - b_module_id: props.moduleId, - b_field_id: String(field.b_id), - b_group_id: groupId || null, - b_visible: 1, - b_required: 0, - b_readonly: 0, - b_disabled: 0, - // 新字段宽度取该分组设置的默认宽度(默认 24 独占一行) - b_colspan: fieldColsOf(sectionKey), - b_height: field.b_type === 'textarea' ? 120 : null, - b_placeholder: null, - b_xh: 0, - } -} +// 表单行默认:b_colspan 取该分组设置的默认宽度(默认 24 独占一行),无 b_width +const createEditRow = createEditRowFactory({ + moduleId: props.moduleId, + getExtra: (field, groupId) => { + const sectionKey = groupId == null ? 'ungrouped' : `g-${String(groupId)}` + return { b_colspan: fieldColsOf(sectionKey) } + }, +}) function insertField( field, @@ -1219,7 +1151,7 @@ function moveSelectedRow(delta) { function toggleRowFlag(row, key) { if (!row || props.loading) return - row[key] = Number(row[key]) === 1 ? 0 : 1 + toggleFlag(row, key) emitUpdate() } @@ -1230,28 +1162,21 @@ function onLabelChip(row, key) { /* 复制/粘贴:仅复制布局与状态属性(不含标识、排序号、分组),用于把某字段的配置套用到其它字段 */ const clipboardProps = ref(null) +// 表单复制属性键:布局与状态属性(栅格宽度 b_colspan 需按 spanOf 归一) +const CLIP_KEYS = ['b_colspan', 'b_required', 'b_readonly', 'b_disabled', 'b_visible', 'b_placeholder', 'b_height'] function copyRowProps() { const row = selectedRow.value if (!row || props.loading) return - clipboardProps.value = { - name: fieldName(row), - props: { - b_colspan: spanOf(row), - b_required: Number(row.b_required) === 1 ? 1 : 0, - b_readonly: Number(row.b_readonly) === 1 ? 1 : 0, - b_disabled: Number(row.b_disabled) === 1 ? 1 : 0, - b_visible: Number(row.b_visible) === 1 ? 1 : 0, - b_placeholder: row.b_placeholder || null, - b_height: row.b_height == null ? null : Number(row.b_height) || null, - }, - } + clipboardProps.value = buildClipboardProps(row, fieldName, CLIP_KEYS, { + b_colspan: (r) => spanOf(r), + }) } function pastePropsToSelected() { const row = selectedRow.value if (!row || props.loading || !clipboardProps.value) return - Object.assign(row, clipboardProps.value.props) + applyClipboardProps(row, clipboardProps.value.props) emitUpdate() } @@ -1354,7 +1279,7 @@ function libClone(field) { function toggleInspector(key) { const row = selectedRow.value if (!row || props.loading) return - row[key] = Number(row[key]) === 1 ? 0 : 1 + toggleFlag(row, key) emitUpdate() } 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 bece4200..07ea400b 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 @@ -27,6 +27,16 @@ 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' +import { useFieldLookup } from '../composables/useFieldLookup' +import { + applyClipboardProps, + buildClipboardProps, + buildVisibilityMap, + createEditRowFactory, + filterLibraryFields, + renumberRows, + toggleFlag, +} from '../editPanelShared' const props = defineProps({ moduleId: { type: String, required: true }, @@ -83,41 +93,19 @@ function emitUpdate() { /* ================= 数据视图:行排序 ================= */ function onRowOrderChange(rows) { - const sorted = rows.map((row, index) => ({ ...row, b_xh: (index + 1) * 10 })) - localEdit.value = sorted + localEdit.value = renumberRows(rows) emitUpdate() } -function renumberRows(rows) { - return rows.map((row, index) => ({ ...row, b_xh: (index + 1) * 10 })) -} - /* ================= 公共查询表 ================= */ -const fieldNameMap = computed(() => { - const map = new Map() - for (const field of props.fields || []) { - map.set(String(field.b_id), field.b_name || field.b_field) - } - return map -}) - -const fieldMap = computed(() => { - const map = new Map() - for (const field of props.fields || []) { - map.set(String(field.b_id), field) - } - return map -}) +const { fieldNameMap, fieldMap, fieldOf, fieldName, controlTypeLabel } = useFieldLookup( + () => props.fields, +) /* ================= 数据视图:同步到列表 / 查询 ================= */ function syncToList() { if (props.loading) return - const visibleMap = new Map() - for (const row of localEdit.value) { - if (row.b_field_id != null) { - visibleMap.set(String(row.b_field_id), Number(row.b_visible) === 1 ? 1 : 0) - } - } + const visibleMap = buildVisibilityMap(localEdit.value) if (visibleMap.size === 0) { Message.info('编辑配置中没有字段') return @@ -137,12 +125,7 @@ function syncToList() { function syncToQuery() { if (props.loading) return - const visibleMap = new Map() - for (const row of localEdit.value) { - if (row.b_field_id != null) { - visibleMap.set(String(row.b_field_id), Number(row.b_visible) === 1 ? 1 : 0) - } - } + const visibleMap = buildVisibilityMap(localEdit.value) if (visibleMap.size === 0) { Message.info('编辑配置中没有字段') return @@ -205,16 +188,6 @@ function tableDataColumns() { const dataColumns = computed(() => tableDataColumns()) /* ================= 设计视图:字段/分组推导 ================= */ -const FIELD_TYPE_LABELS = { - input: '输入框', - textarea: '文本域', - number: '数字', - money: '金额', - date: '日期', - datetime: '日期时间', - checkbox: '复选框', - select: '下拉选择', -} /* 子表常用列宽(px) */ const COLUMN_WIDTH_OPTIONS = [ @@ -225,22 +198,6 @@ const COLUMN_WIDTH_OPTIONS = [ { label: '大 240', value: '240' }, ] -function fieldOf(row) { - if (row?.b_field_id == null) return null - return fieldMap.value.get(String(row.b_field_id)) || null -} - -function fieldName(row) { - const fid = row?.b_field_id - if (fid == null) return '' - return fieldNameMap.value.get(String(fid)) || String(fid) -} - -function controlTypeLabel(row) { - const field = fieldOf(row) - return FIELD_TYPE_LABELS[field?.b_type] || field?.b_type || '未知' -} - /* ---- 表格(子表)模式:列宽 ---- */ function widthOf(row) { const v = Number(row?.b_width) @@ -298,19 +255,7 @@ const libSearch = ref('') const draggingFieldId = ref(null) // 字段库:展示模块的全部字段(含未加入表格的字段),按名称 a-z 排序; // 搜索时按名称/编码过滤,命中才显示 -const libraryFields = computed(() => { - const kw = libSearch.value.trim().toLowerCase() - return [...(props.fields || [])] - .filter( - (f) => - !kw || - String(f.b_name || '').toLowerCase().includes(kw) || - String(f.b_field || '').toLowerCase().includes(kw), - ) - .toSorted((a, b) => - String(a.b_name || a.b_field || '').localeCompare(String(b.b_name || b.b_field || ''), 'zh'), - ) -}) +const libraryFields = computed(() => filterLibraryFields(props.fields, libSearch.value)) function isFieldAdded(field) { return localEdit.value.some((row) => String(row.b_field_id) === String(field.b_id)) @@ -781,23 +726,11 @@ function toggleBandEnable() { } /* ================= 列:增删改 ================= */ -function createEditRow(field, groupId) { - return { - b_id: createTempId(), - b_module_id: props.moduleId, - b_field_id: String(field.b_id), - b_group_id: groupId || null, - b_visible: 1, - b_required: 0, - b_readonly: 0, - b_disabled: 0, - b_colspan: 24, - b_height: field.b_type === 'textarea' ? 120 : null, - b_placeholder: null, - b_width: defaultWidthOf(field), - b_xh: 0, - } -} +// 子表行默认:b_colspan=24(表格不使用),额外补 b_width(按字段类型取默认列宽) +const createEditRow = createEditRowFactory({ + moduleId: props.moduleId, + getExtra: (field) => ({ b_width: defaultWidthOf(field) }), +}) // 插入到某分组带的直属列末尾(空分组按树前序位置) function appendIntoGroup(row, groupId, rows) { @@ -938,28 +871,19 @@ function scrollToRowCell(fieldId) { /* ================= 复制/粘贴(列属性) ================= */ const clipboardProps = ref(null) +// 子表复制属性键:布局与状态属性(不含标识、排序号、分组) +const CLIP_KEYS = ['b_required', 'b_readonly', 'b_disabled', 'b_visible', 'b_placeholder', 'b_height', 'b_width'] function copyRowProps() { const row = selectedRow.value if (!row || props.loading) return - clipboardProps.value = { - name: fieldName(row), - props: { - b_required: Number(row.b_required) === 1 ? 1 : 0, - b_readonly: Number(row.b_readonly) === 1 ? 1 : 0, - b_disabled: Number(row.b_disabled) === 1 ? 1 : 0, - b_visible: Number(row.b_visible) === 1 ? 1 : 0, - b_placeholder: row.b_placeholder || null, - b_height: row.b_height == null ? null : Number(row.b_height) || null, - b_width: row.b_width == null ? null : Number(row.b_width) || null, - }, - } + clipboardProps.value = buildClipboardProps(row, fieldName, CLIP_KEYS) } function pastePropsToSelected() { const row = selectedRow.value if (!row || props.loading || !clipboardProps.value) return - Object.assign(row, clipboardProps.value.props) + applyClipboardProps(row, clipboardProps.value.props) emitUpdate() } @@ -1241,7 +1165,7 @@ function onColResizePointerEnd() { function toggleInspector(key) { const row = selectedRow.value if (!row || props.loading) return - row[key] = Number(row[key]) === 1 ? 0 : 1 + toggleFlag(row, key) emitUpdate() } 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 4e1684d9..e9a6da51 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 @@ -27,6 +27,7 @@ import { import draggable from 'vuedraggable' import StructureTreeNode from './StructureTreeNode.vue' import { useGroupTree } from '../../composables/useGroupTree' +import { useFieldLookup } from '../../composables/useFieldLookup' const props = defineProps({ groups: { type: Array, default: () => [] }, @@ -53,36 +54,7 @@ const emit = defineEmits([ ]) /* ================= 字段/名称推导 ================= */ -const FIELD_TYPE_LABELS = { - input: '输入框', - textarea: '文本域', - number: '数字', - money: '金额', - date: '日期', - datetime: '日期时间', - checkbox: '复选框', - select: '下拉选择', -} - -const fieldNameMap = computed(() => { - const map = new Map() - for (const field of props.fields || []) map.set(String(field.b_id), field.b_name || field.b_field) - return map -}) -const fieldMap = computed(() => { - const map = new Map() - for (const field of props.fields || []) map.set(String(field.b_id), field) - return map -}) - -function fieldName(row) { - if (row?.b_field_id == null) return '' - return fieldNameMap.value.get(String(row.b_field_id)) || String(row.b_field_id) -} -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 || '未知' -} +const { fieldName, controlTypeLabel } = useFieldLookup(() => props.fields) /* ================= 分组树构建 ================= * 树构建与元信息推导见 composables/useGroupTree(面板与结构树共用)。 */ diff --git a/code/fms/fms-vue/src/views/system/module-management/composables/useFieldLookup.js b/code/fms/fms-vue/src/views/system/module-management/composables/useFieldLookup.js new file mode 100644 index 00000000..3abd6ec1 --- /dev/null +++ b/code/fms/fms-vue/src/views/system/module-management/composables/useFieldLookup.js @@ -0,0 +1,59 @@ +/** + * 字段查询与标签共享逻辑。 + * 供 ModuleTableEditPanel / ModuleFormEditPanel / ModuleStructureTree 共用: + * 字段名 Map、字段对象 Map、字段查询函数、控件类型标签。 + */ +import { computed } from 'vue' + +export const FIELD_TYPE_LABELS = { + input: '输入框', + textarea: '文本域', + number: '数字', + money: '金额', + date: '日期', + datetime: '日期时间', + checkbox: '复选框', + select: '下拉选择', +} + +/** + * @param {import('vue').Ref|import('vue').ComputedRef|(() => Array)} fieldsRef + * 字段数组 ref/computed,或返回字段数组的 getter 函数 + */ +export function useFieldLookup(fieldsRef) { + const fields = typeof fieldsRef === 'function' ? computed(fieldsRef) : fieldsRef + + const fieldNameMap = computed(() => { + const map = new Map() + for (const field of fields.value || []) { + map.set(String(field.b_id), field.b_name || field.b_field) + } + return map + }) + + const fieldMap = computed(() => { + const map = new Map() + for (const field of fields.value || []) { + map.set(String(field.b_id), field) + } + return map + }) + + function fieldOf(row) { + if (row?.b_field_id == null) return null + return fieldMap.value.get(String(row.b_field_id)) || null + } + + function fieldName(row) { + const fid = row?.b_field_id + if (fid == null) return '' + return fieldNameMap.value.get(String(fid)) || String(fid) + } + + function controlTypeLabel(row) { + const field = fieldOf(row) + return FIELD_TYPE_LABELS[field?.b_type] || field?.b_type || '未知' + } + + return { fieldNameMap, fieldMap, fieldOf, fieldName, controlTypeLabel } +} \ No newline at end of file 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 new file mode 100644 index 00000000..3d11cb88 --- /dev/null +++ b/code/fms/fms-vue/src/views/system/module-management/editPanelShared.js @@ -0,0 +1,88 @@ +/** + * 两个编辑面板共享的纯函数(无 Vue 依赖)。 + * 栅格重排、列属性复制/粘贴、可见性 Map、字段库搜索、编辑行构建等。 + */ +import { createTempId } from '@/utils/tempId' + +/** 按索引重排 b_xh:从 10 开始,步长 10 */ +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; + * 其余按键的 0/1 标志归一。normalize 可覆盖个别键的自定义取值(如 b_colspan 需 spanOf)。 + */ +export function buildClipboardProps(row, fieldNameOf, keys, normalize = {}) { + const props = {} + for (const k of keys) { + if (normalize[k]) props[k] = normalize[k](row) + else if (k === 'b_placeholder') props[k] = row.b_placeholder || null + else if (k === 'b_height' || k === 'b_width') props[k] = row[k] == null ? null : Number(row[k]) || null + else props[k] = Number(row[k]) === 1 ? 1 : 0 + } + return { name: fieldNameOf(row), props } +} + +/** 将剪切板属性对象应用到行的目标属性 */ +export function applyClipboardProps(row, props) { + Object.assign(row, props) +} + +/** 切换字段布尔标志(纯 mutation) */ +export function toggleFlag(row, key) { + row[key] = Number(row[key]) === 1 ? 0 : 1 +} + +/** 按关键字过滤字段(名称/编码),按名称排序 */ +export function filterLibraryFields(fields, keyword) { + const kw = String(keyword || '').trim().toLowerCase() + return [...(fields || [])] + .filter( + (f) => + !kw || + String(f.b_name || '').toLowerCase().includes(kw) || + String(f.b_field || '').toLowerCase().includes(kw), + ) + .toSorted((a, b) => + String(a.b_name || a.b_field || '').localeCompare(String(b.b_name || b.b_field || ''), 'zh'), + ) +} + +/** + * 创建编辑行工厂函数。 + * 共享:b_id / b_module_id / b_field_id / b_group_id / b_visible / b_required / b_readonly / b_disabled + * / b_colspan(默认24) / b_height(textarea 120) / b_placeholder / b_xh + * 差异:由 getExtra(field, groupId) 补充(如 b_width / b_colspan 覆盖)。 + * @param {{ moduleId: string, getExtra?: (field: object, groupId: string|null) => object }} opts + * @returns {(field: object, groupId: string|null) => object} + */ +export function createEditRowFactory({ moduleId, getExtra }) { + return (field, groupId) => ({ + b_id: createTempId(), + b_module_id: moduleId, + b_field_id: String(field.b_id), + b_group_id: groupId || null, + b_visible: 1, + b_required: 0, + b_readonly: 0, + b_disabled: 0, + b_colspan: 24, + b_height: field.b_type === 'textarea' ? 120 : null, + b_placeholder: null, + b_xh: 0, + ...(getExtra ? getExtra(field, groupId) : {}), + }) +} \ No newline at end of file