diff --git a/code/fms/fms-vue/src/views/system/module-management/components/ModuleFieldsPanel.vue b/code/fms/fms-vue/src/views/system/module-management/components/ModuleFieldsPanel.vue index 21ca04be..c8f4abab 100644 --- a/code/fms/fms-vue/src/views/system/module-management/components/ModuleFieldsPanel.vue +++ b/code/fms/fms-vue/src/views/system/module-management/components/ModuleFieldsPanel.vue @@ -2,16 +2,17 @@ /** * 字段定义面板(s_field):可编辑表格维护模块字段。 * i18n 键按 field.{moduleCode}.{fieldName} 自动同步。 - * 表单/列表/查询三列为「参与开关」:在一行内表达该字段参与了哪些界面, - * 开关即改即生效(生成/隐藏默认配置行);布局细节仍在对应设计器调整。 - * 语义见 fieldParticipation.js(沿用 fieldAreaDefaults 的行内开关约定)。 + * 表单/列表/查询三列为「参与勾选」:在一行内表达该字段参与了哪些界面, + * 勾选即改即生效(生成/隐藏默认配置行);布局细节仍在对应设计器调整。 + * 参与是派生态(配置行存在且行内开关为 1),物化为字段行的 __form/__list/__query + * 虚拟字段交给 FmsTable 内置 checkbox 渲染(与「启用」列同款),change 按列路由 + * 回 toggle 写配置草稿。语义见 fieldParticipation.js。 */ import { ref, shallowRef, watch } from 'vue' import FmsTable from '@/components/fms-table/FmsTable.vue' import { buildModuleItemI18nKey, syncModuleItemI18nKeys } from '@/utils/i18n' import I18nQuickSetModal from './I18nQuickSetModal.vue' import { createI18nKeyCell } from './I18nKeyCell' -import { createParticipationCell } from './ParticipationCell' import { isFormOn, isListOn, @@ -25,7 +26,7 @@ const props = defineProps({ moduleId: { type: String, required: true }, moduleCode: { type: String, default: '' }, fields: { type: Array, default: () => [] }, - /** 参与开关所需的三个配置草稿(由 index.vue 传入,切换后事件回写) */ + /** 参与勾选所需的三个配置草稿(由 index.vue 传入,切换后事件回写) */ editConfig: { type: Array, default: () => [] }, listConfig: { type: Array, default: () => [] }, queryConfig: { type: Array, default: () => [] }, @@ -81,11 +82,49 @@ watch( { immediate: true }, ) +// 参与状态物化到字段行上的 __ 虚拟字段(就地 patch,不替换数组引用,避免整表重建), +// 供 FmsTable 内置 checkbox 渲染;虚拟字段不进草稿/保存请求(emitUpdate 时剥离)。 +// 勾选变化经 onCellChange 路由到 toggle 写配置草稿,随后本 watch 重算虚拟字段 +const PARTICIPATION_KEYS = ['__form', '__list', '__query'] + +watch( + () => [localFields.value, props.editConfig, props.listConfig, props.queryConfig], + () => { + for (const row of localFields.value) { + row.__form = isFormOn(props.editConfig, row) ? 1 : 0 + row.__list = isListOn(props.listConfig, row) ? 1 : 0 + row.__query = isQueryOn(props.queryConfig, row) ? 1 : 0 + } + }, + { immediate: true }, +) + function emitUpdate() { - emit('update', localFields.value.map((row) => ({ ...row }))) + // 剥离参与虚拟字段:__ 前缀仅供表格渲染,不进字段草稿与保存请求 + emit( + 'update', + localFields.value.map((row) => { + const { __form, __list, __query, ...field } = row + return field + }), + ) } -function onCellChange() { +function onCellChange(row, col) { + // 参与勾选:内置 checkbox 已把值写在字段行虚拟字段上(展示用途), + // 真实状态经 toggle 写回对应配置草稿,不走字段 i18n 同步 + if (PARTICIPATION_KEYS.includes(col?.dataIndex)) { + if (!props.loading) { + if (col.dataIndex === '__form') { + emit('updateEditConfig', toggleForm(props.editConfig, row, props.moduleId)) + } else if (col.dataIndex === '__list') { + emit('updateListConfig', toggleList(props.listConfig, row, props.moduleId)) + } else { + emit('updateQueryConfig', toggleQuery(props.queryConfig, row, props.moduleId)) + } + } + return + } syncModuleItemI18nKeys(localFields.value, 'field', props.moduleCode, 'b_field') emitUpdate() } @@ -98,29 +137,6 @@ function openI18n(row) { const I18nKeyCell = createI18nKeyCell({ onOpen: openI18n }) -/* ---------- 参与开关(表单/列表/查询) ---------- */ -/* 字段行 → 配置行:字段定义行的 b_id 与配置行的 b_field_id 对应; - 字段本身未启用时开关禁用(停用字段运行时不显示,参与无意义) */ - -function onToggleForm(field) { - if (props.loading) return - emit('updateEditConfig', toggleForm(props.editConfig, field, props.moduleId)) -} - -function onToggleList(field) { - if (props.loading) return - emit('updateListConfig', toggleList(props.listConfig, field, props.moduleId)) -} - -function onToggleQuery(field) { - if (props.loading) return - emit('updateQueryConfig', toggleQuery(props.queryConfig, field, props.moduleId)) -} - -const FormCell = createParticipationCell({ isOn: (f) => isFormOn(props.editConfig, f), onToggle: onToggleForm, title: '参与表单' }) -const ListCell = createParticipationCell({ isOn: (f) => isListOn(props.listConfig, f), onToggle: onToggleList, title: '参与列表显示' }) -const QueryCell = createParticipationCell({ isOn: (f) => isQueryOn(props.queryConfig, f), onToggle: onToggleQuery, title: '作为查询条件' }) - const columns = shallowRef(buildColumns()) // 列宽拖拽结果随 v-model 写回 columns,仅在模块类型变化时重建 @@ -156,23 +172,14 @@ function buildColumns() { { dataIndex: 'b_default_value', title: '默认值', width: 120, editor: { type: 'input' } }, { dataIndex: 'b_i18n', title: '多语言键', width: 220, customCell: I18nKeyCell }, ] - // 表单参与列仅数据模块展示(查询模块无表单设计)。 - // align 管内容 td、headerAlign 管表头 th(stk-table 分开处理,见 FmsTable.getHeaderAlign) + // 参与勾选列:内置 checkbox editor(居中与交互隔离随「启用」列同款), + // 表单列仅数据模块展示(查询模块无表单设计) const participationColumns = [ ...(props.moduleType === 'data' - ? [ - { - dataIndex: '__form', - title: '表单', - width: 64, - align: 'center', - headerAlign: 'center', - customCell: FormCell, - }, - ] + ? [{ dataIndex: '__form', title: '表单', width: 70, align: 'center', editor: { type: 'checkbox' } }] : []), - { dataIndex: '__list', title: '列表', width: 64, align: 'center', headerAlign: 'center', customCell: ListCell }, - { dataIndex: '__query', title: '查询', width: 64, align: 'center', headerAlign: 'center', customCell: QueryCell }, + { dataIndex: '__list', title: '列表', width: 70, align: 'center', editor: { type: 'checkbox' } }, + { dataIndex: '__query', title: '查询', width: 70, align: 'center', editor: { type: 'checkbox' } }, ] const tailColumns = [ { diff --git a/code/fms/fms-vue/src/views/system/module-management/components/ParticipationCell.js b/code/fms/fms-vue/src/views/system/module-management/components/ParticipationCell.js deleted file mode 100644 index 1e16eb71..00000000 --- a/code/fms/fms-vue/src/views/system/module-management/components/ParticipationCell.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * 字段参与勾选单元格(h 渲染函数):ui Checkbox 居中渲染,勾选切换参与状态。 - * 供字段定义面板 FmsTable 的 表单/列表/查询 三列 customCell 使用。 - * 表格 cell 由 h() 动态渲染、不受 scoped 样式约束,样式放独立 scss 随本模块全局引入。 - */ -import { h } from 'vue' -import { Checkbox } from '@/components/ui' -import './participation-cell.scss' - -/** - * @param {object} opts - * @param {(row: object) => boolean} opts.isOn 该行当前是否参与 - * @param {(row: object) => void} opts.onToggle 勾选切换 - * @param {string} opts.title 提示文案(如「参与列表显示」) - */ -export function createParticipationCell({ isOn, onToggle, title }) { - return ({ row }) => - h('div', { class: 'mm-part-cell' }, [ - h(Checkbox, { - modelValue: isOn(row), - title, - 'aria-label': title, - onChange: () => onToggle(row), - }), - ]) -} diff --git a/code/fms/fms-vue/src/views/system/module-management/components/participation-cell.scss b/code/fms/fms-vue/src/views/system/module-management/components/participation-cell.scss deleted file mode 100644 index f342989a..00000000 --- a/code/fms/fms-vue/src/views/system/module-management/components/participation-cell.scss +++ /dev/null @@ -1,11 +0,0 @@ -/* 字段参与勾选单元格(ParticipationCell):随 JS 模块全局引入。 - * 与 FmsTable 内置勾选列(.fms-checkbox)同构:铺满单元格后 flex 居中, - * 垂直方向由 height:100% 与行高对齐。 */ -.mm-part-cell { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - line-height: 1; -} diff --git a/code/fms/fms-vue/tests/unit/module-fields-panel-participation.spec.js b/code/fms/fms-vue/tests/unit/module-fields-panel-participation.spec.js new file mode 100644 index 00000000..8ff6e356 --- /dev/null +++ b/code/fms/fms-vue/tests/unit/module-fields-panel-participation.spec.js @@ -0,0 +1,127 @@ +/** + * 字段定义面板参与勾选接线测试: + * - 虚拟字段 __form/__list/__query 由配置草稿物化(内置 checkbox 的勾选态) + * - change 事件按列路由:参与列 → updateXxxConfig(携带 toggle 后的配置),普通列 → update(字段) + * - update 事件剥离 __ 虚拟字段,不污染字段草稿 + */ +import { mount } from '@vue/test-utils' +import { beforeAll, describe, expect, it, vi } from 'vitest' +import { createPinia, setActivePinia } from 'pinia' +import { nextTick } from 'vue' +import { registerFeature, useAreaSelection } from 'stk-table-vue' +import ModuleFieldsPanel from '@/views/system/module-management/components/ModuleFieldsPanel.vue' + +// 与 main.js 一致:注册 stk-table 的按需 feature +registerFeature(useAreaSelection) + +// jsdom 无布局,容器 clientWidth 为 0 时 StkTable 走默认宽度并启用虚拟列裁剪, +// 只渲染首列——patch 成宽容器让全部列(含参与勾选列)真实渲染 +beforeAll(() => { + Object.defineProperty(HTMLElement.prototype, 'clientWidth', { + configurable: true, + get() { + return 5000 + }, + }) +}) + +vi.mock('@/services/api', () => ({ + loadDataApi: vi.fn(), + saveObjectApi: vi.fn(), + nextIdApi: vi.fn(), + describeApi: vi.fn(), +})) + +const fields = [ + { b_id: '10', b_field: 'amount', b_name: '金额', b_type: 'money', b_canuse: 1, b_i18n: null }, + { b_id: '11', b_field: 'remark', b_name: '备注', b_type: 'textarea', b_canuse: 1, b_i18n: null }, +] + +const listConfig = [ + { b_id: 'v1', b_field_id: '10', b_visible: 1 }, + { b_id: 'v2', b_field_id: '11', b_visible: 0 }, // 大文本默认隐藏 +] +const editConfig = [{ b_id: 'e1', b_field_id: '10', b_visible: 1 }] +const queryConfig = [] // 均未生成查询行 + +// 行内 checkbox 顺序 = 参与三列 + 启用列(普通编辑列不渲染 checkbox) +const FORM = 0 +const LIST = 1 +const QUERY = 2 + +function mountPanel(props = {}) { + return mount(ModuleFieldsPanel, { + props: { + moduleId: 'm1', + moduleCode: 'demo', + fields: fields.map((f) => ({ ...f })), + editConfig: editConfig.map((r) => ({ ...r })), + listConfig: listConfig.map((r) => ({ ...r })), + queryConfig: queryConfig.map((r) => ({ ...r })), + moduleType: 'data', + loading: false, + ...props, + }, + attachTo: document.body, + }) +} + +async function settle() { + await nextTick() + await new Promise((r) => setTimeout(r, 100)) +} + +function rowCheckboxes(wrapper, rowIndex) { + return wrapper.findAll('.stk-tbody-main tr')[rowIndex].findAll('input[type="checkbox"]') +} + +describe('ModuleFieldsPanel 参与勾选', () => { + it('虚拟字段按配置草稿物化:amount 表单/列表参与、查询未配置', async () => { + setActivePinia(createPinia()) + const wrapper = mountPanel() + await settle() + const boxes = rowCheckboxes(wrapper, 0) + expect(boxes.length).toBe(4) // __form / __list / __query / b_canuse + expect(boxes[FORM].element.checked).toBe(true) + expect(boxes[LIST].element.checked).toBe(true) + expect(boxes[QUERY].element.checked).toBe(false) + wrapper.unmount() + }) + + it('勾选查询列 → emit updateQueryConfig,生成默认查询行(b_canuse=1)', async () => { + setActivePinia(createPinia()) + const wrapper = mountPanel() + await settle() + await rowCheckboxes(wrapper, 0)[QUERY].setValue(true) + const events = wrapper.emitted('updateQueryConfig') + expect(events).toHaveLength(1) + const rows = events[0][0] + expect(rows).toHaveLength(1) + expect(rows[0].b_field_id).toBe('10') + expect(rows[0].b_canuse).toBe(1) + expect(rows[0].b_component).toBe('number') + wrapper.unmount() + }) + + it('普通列 change → emit update,且剥离 __ 虚拟字段', async () => { + setActivePinia(createPinia()) + const wrapper = mountPanel() + await settle() + const table = wrapper.findComponent({ name: 'FmsTable' }) + table.vm.$emit( + 'change', + { ...fields[0], __form: 1, __list: 1, __query: 0 }, + { dataIndex: 'b_name' }, + '新名称', + ) + await nextTick() + const events = wrapper.emitted('update') + expect(events).toBeTruthy() + const emitted = events.at(-1)[0] + expect(emitted[0].b_id).toBe('10') + expect('__form' in emitted[0]).toBe(false) + expect('__list' in emitted[0]).toBe(false) + expect('__query' in emitted[0]).toBe(false) + wrapper.unmount() + }) +})