chore(fms-vue): WIP 基线快照——收编工作区未提交的前端改动
不含 fms-api Java 侧改动与后续导航状态化阶段提交。主要内容: - ui 组件库:date/dropdown/form/input/popover/select/tooltip 调整, 新增 Segmented 组件;fms-table 可编辑单元格渲染器与 FileCard 更新 - 模块管理:列表配置加载自动补全、查询缺配徽标与一键补充、 结构编辑内核 useStructureEditing/useBandLayout、子表表格编辑方式、 分组标题多语言、I18nKeyCell 等(含配套 spec) - 布局/主题/仪表盘/演示页小幅更新
This commit is contained in:
1 parent
a1079a7150
commit
05d0c7f304
58 files changed
+3698
-1343
No files matched your search
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* useModuleSave 保存编排测试:守护跨表保存的关键不变量。
|
||||
* 这些顺序约束此前只存在于注释中,被改动时不会有任何报警:
|
||||
* 1. 校验失败时不产生任何保存请求,且定位到对应分节;
|
||||
* 2. s_module 有修改走 updates、新建走 inserts;
|
||||
* 3. 临时 id 先分配真实主键,再跨表重映射 b_field_id(req 行与草稿行同引用);
|
||||
* 4. s_autocode 的 updates 剥离服务端维护的 b_currentperiod / b_currentvalue;
|
||||
* 5. i18n inserts 分配真实主键并回写面板,避免二次保存重复插入。
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
import { useModuleSave } from '@/views/system/module-management/composables/useModuleSave'
|
||||
import { saveObjectApi, nextIdApi } from '@/services/api'
|
||||
import { Message } from '@/components/ui'
|
||||
|
||||
vi.mock('@/services/api', () => ({
|
||||
saveObjectApi: vi.fn(),
|
||||
nextIdApi: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/components/ui', () => ({
|
||||
Message: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() },
|
||||
}))
|
||||
|
||||
let idSeq
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
idSeq = 9000
|
||||
nextIdApi.mockImplementation(async (count) => {
|
||||
const ids = []
|
||||
for (let i = 0; i < count; i++) ids.push(String(++idSeq))
|
||||
return { data: ids }
|
||||
})
|
||||
})
|
||||
|
||||
function baseDrafts(over = {}) {
|
||||
const drafts = {
|
||||
// b_i18n 与 buildModuleI18nKey('data', 'demo') 派生值一致:saveModule 每次保存都会物化该键
|
||||
module: { b_id: '1', b_code: 'demo', b_name: '演示模块', b_module_type: 'data', b_i18n: 'module.demo' },
|
||||
fields: [],
|
||||
groups: [],
|
||||
listConfig: [],
|
||||
editConfig: [],
|
||||
queryConfig: [],
|
||||
autoCode: [],
|
||||
powers: [],
|
||||
}
|
||||
return Object.assign(drafts, over)
|
||||
}
|
||||
|
||||
function setup({ drafts, originals, basicPanel = null, autoCodePanel = null, i18nPanel = null }) {
|
||||
const unsavedModuleIds = new Set()
|
||||
const deps = {
|
||||
selectedModule: ref({ b_id: '1', b_code: 'demo', b_name: '演示模块' }),
|
||||
drafts: ref(drafts),
|
||||
originals: ref(originals),
|
||||
loadedModuleId: ref('1'),
|
||||
moduleSelectedId: ref('1'),
|
||||
unsavedModuleIds,
|
||||
configSaving: ref(false),
|
||||
busy: ref(false),
|
||||
activeSection: ref('basic'),
|
||||
moduleTreeData: ref([]),
|
||||
findModuleNode: () => null,
|
||||
basicPanelRef: ref(basicPanel),
|
||||
autoCodePanelRef: ref(autoCodePanel),
|
||||
i18nPanelRef: ref(i18nPanel),
|
||||
getFinalI18nItems: () => [{ key: 'module.demo', managed: true, id: 'module:demo' }],
|
||||
}
|
||||
return { saveModule: useModuleSave(deps).saveModule, unsavedModuleIds, activeSection: deps.activeSection }
|
||||
}
|
||||
|
||||
function reqOf(table) {
|
||||
return saveObjectApi.mock.calls.at(-1)[0].find((req) => req.table === table)
|
||||
}
|
||||
|
||||
describe('useModuleSave 保存编排', () => {
|
||||
it('无变更时不发请求并提示', async () => {
|
||||
const drafts = baseDrafts()
|
||||
const { saveModule } = setup({ drafts, originals: JSON.parse(JSON.stringify(drafts)) })
|
||||
|
||||
const ok = await saveModule()
|
||||
|
||||
expect(ok).toBe(true)
|
||||
expect(saveObjectApi).not.toHaveBeenCalled()
|
||||
expect(Message.info).toHaveBeenCalledWith('没有需要保存的变更')
|
||||
})
|
||||
|
||||
it('基础面板校验失败:不发请求,定位到 basic 分节', async () => {
|
||||
const drafts = baseDrafts()
|
||||
const { saveModule, activeSection } = setup({
|
||||
drafts,
|
||||
originals: JSON.parse(JSON.stringify(drafts)),
|
||||
basicPanel: { validate: async () => ['编码不能为空'] },
|
||||
})
|
||||
|
||||
const ok = await saveModule()
|
||||
|
||||
expect(ok).toBe(false)
|
||||
expect(saveObjectApi).not.toHaveBeenCalled()
|
||||
expect(activeSection.value).toBe('basic')
|
||||
expect(Message.warning).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('s_module 修改走 updates,新建走 inserts', async () => {
|
||||
const drafts = baseDrafts({ module: { b_id: '1', b_code: 'demo', b_name: '改名后' } })
|
||||
const originals = baseDrafts()
|
||||
const { saveModule } = setup({ drafts, originals })
|
||||
await saveModule()
|
||||
expect(reqOf('s_module').updates[0].b_name).toBe('改名后')
|
||||
|
||||
const { saveModule: saveNew, unsavedModuleIds } = setup({
|
||||
drafts: baseDrafts(),
|
||||
originals: baseDrafts(),
|
||||
})
|
||||
unsavedModuleIds.add('1')
|
||||
await saveNew()
|
||||
expect(reqOf('s_module').inserts[0].b_code).toBe('demo')
|
||||
})
|
||||
|
||||
it('临时 id 分配真实主键并跨表重映射 b_field_id', async () => {
|
||||
// 新字段(临时 id -1)+ 引用它的列表列:编辑行同引用,重映射必须同时落到请求与草稿
|
||||
const listRow = { b_id: '100', b_module_id: '1', b_field_id: '-1', b_visible: 1, b_xh: 10 }
|
||||
const drafts = baseDrafts({
|
||||
fields: [{ b_id: '-1', b_module_id: '1', b_field: 'name', b_name: '姓名' }],
|
||||
listConfig: [listRow],
|
||||
})
|
||||
const originals = baseDrafts({
|
||||
listConfig: [{ b_id: '100', b_module_id: '1', b_field_id: '9999', b_visible: 1, b_xh: 10 }],
|
||||
})
|
||||
const { saveModule } = setup({ drafts, originals })
|
||||
|
||||
const ok = await saveModule()
|
||||
|
||||
expect(ok).toBe(true)
|
||||
expect(nextIdApi).toHaveBeenCalled()
|
||||
const fieldReq = reqOf('s_field')
|
||||
expect(fieldReq.inserts[0].b_id).toBe('9001')
|
||||
// 列表列由旧字段改挂新字段:update 行内的临时外键必须已换成真实 id
|
||||
const viewReq = reqOf('s_field_view')
|
||||
expect(viewReq.updates[0].b_field_id).toBe('9001')
|
||||
// 草稿行与请求行同引用:重映射后草稿也持有真实 id
|
||||
expect(drafts.listConfig[0].b_field_id).toBe('9001')
|
||||
})
|
||||
|
||||
it('s_autocode updates 剥离服务端维护的期间/当前值', async () => {
|
||||
const rule = {
|
||||
b_id: '200',
|
||||
b_module_id: '1',
|
||||
b_field_id: '100',
|
||||
b_canuse: 1,
|
||||
b_currentperiod: 2026,
|
||||
b_currentvalue: 5,
|
||||
}
|
||||
const drafts = baseDrafts({
|
||||
autoCode: [rule],
|
||||
module: { b_id: '1', b_code: 'demo', b_name: '演示模块', b_module_type: 'data', b_i18n: 'module.demo', b_savetable: 't_demo' },
|
||||
})
|
||||
const { saveModule } = setup({
|
||||
drafts,
|
||||
// 原件行与草稿行存在差异(当前值不同)才会产生 update,从而走到剥离逻辑
|
||||
originals: baseDrafts({ autoCode: [{ ...rule, b_currentvalue: 4 }] }),
|
||||
})
|
||||
|
||||
await saveModule()
|
||||
|
||||
const update = reqOf('s_autocode').updates[0]
|
||||
expect(update).not.toHaveProperty('b_currentperiod')
|
||||
expect(update).not.toHaveProperty('b_currentvalue')
|
||||
expect(update.b_field_id).toBe('100')
|
||||
})
|
||||
|
||||
it('i18n inserts 分配真实主键并回写面板,事务内提交', async () => {
|
||||
const drafts = baseDrafts({ module: { b_id: '1', b_code: 'demo', b_name: '新名称' } })
|
||||
const originals = baseDrafts()
|
||||
const i18nPanel = {
|
||||
reconcileManagedRows: vi.fn(),
|
||||
buildSaveRequest: vi.fn(() => ({
|
||||
table: 's_i18n',
|
||||
key_field: 'b_id',
|
||||
inserts: [{ b_id: '-9', b_key: 'module.demo', b_locale: 'zh-CN', b_value: '新名称' }],
|
||||
})),
|
||||
applyInsertedIds: vi.fn(),
|
||||
markSaved: vi.fn(),
|
||||
}
|
||||
const { saveModule } = setup({ drafts, originals, i18nPanel })
|
||||
|
||||
const ok = await saveModule()
|
||||
|
||||
expect(ok).toBe(true)
|
||||
// 受管键 reconcile 在构建请求前执行
|
||||
expect(i18nPanel.reconcileManagedRows).toHaveBeenCalledWith([
|
||||
{ key: 'module.demo', managed: true, id: 'module:demo' },
|
||||
])
|
||||
// 临时主键分配后回写面板,避免二次保存重复插入
|
||||
expect(i18nPanel.applyInsertedIds).toHaveBeenCalledWith([
|
||||
expect.objectContaining({ b_id: '9001', b_key: 'module.demo' }),
|
||||
])
|
||||
expect(reqOf('s_i18n').inserts[0].b_id).toBe('9001')
|
||||
expect(i18nPanel.markSaved).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('跨面板校验前置:启用自动编码但未配置保存表则拦截', async () => {
|
||||
const rule = { b_id: '200', b_module_id: '1', b_field_id: '100', b_canuse: 1 }
|
||||
const drafts = baseDrafts({ autoCode: [rule] })
|
||||
const { saveModule } = setup({ drafts, originals: baseDrafts() })
|
||||
|
||||
const ok = await saveModule()
|
||||
|
||||
expect(ok).toBe(false)
|
||||
expect(saveObjectApi).not.toHaveBeenCalled()
|
||||
expect(Message.warning).toHaveBeenCalledWith('启用自动编码前请先配置保存表')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user