Fix page designer validation and generation
This commit is contained in:
2589
RuoYi-Vue/easycode-web/src/components/FrontendPageDesigner.vue
Normal file
2589
RuoYi-Vue/easycode-web/src/components/FrontendPageDesigner.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,267 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
|
||||
const currentDir = dirname(fileURLToPath(import.meta.url))
|
||||
const srcDir = resolve(currentDir, '..')
|
||||
|
||||
function readSource(relativePath) {
|
||||
return readFileSync(resolve(srcDir, relativePath), 'utf8')
|
||||
}
|
||||
|
||||
test('project api exposes frontend page design endpoints', () => {
|
||||
const source = readSource('api/project.js')
|
||||
|
||||
assert.equal(source.includes('export function listPageDesigns'), true)
|
||||
assert.equal(source.includes('export function initPageDesigns'), true)
|
||||
assert.equal(source.includes('export function createPageDesign'), true)
|
||||
assert.equal(source.includes('export function getPageDesign'), true)
|
||||
assert.equal(source.includes('export function savePageDesign'), true)
|
||||
assert.equal(source.includes('export function deletePageDesign'), true)
|
||||
assert.equal(source.includes('export function listBusinessBlocks'), true)
|
||||
assert.equal(source.includes('/page-designs/init'), true)
|
||||
assert.equal(source.includes('url: `/front/project/${projectId}/page-designs`'), true)
|
||||
assert.equal(source.includes('/business-blocks'), true)
|
||||
assert.equal(source.includes('scope = \'frontend\''), true)
|
||||
assert.equal(source.includes('params: { scope }'), true)
|
||||
})
|
||||
|
||||
test('frontend page designer edits layouts and emits saves', () => {
|
||||
assert.equal(existsSync(resolve(srcDir, 'components/FrontendPageDesigner.vue')), true)
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
|
||||
assert.equal(source.includes('defineEmits'), true)
|
||||
assert.equal(source.includes('update:designs'), true)
|
||||
assert.equal(source.includes('dirty-change'), true)
|
||||
assert.equal(source.includes('save-design'), true)
|
||||
assert.equal(source.includes('create-design'), true)
|
||||
assert.equal(source.includes('delete-design'), true)
|
||||
assert.equal(source.includes('init-designs'), true)
|
||||
assert.equal(source.includes('layoutJson'), true)
|
||||
assert.equal(source.includes('actionJson'), true)
|
||||
assert.equal(source.includes('draggable="true"'), true)
|
||||
assert.equal(source.includes('function commitCurrentDraft'), true)
|
||||
assert.equal(source.includes('defineExpose'), true)
|
||||
})
|
||||
|
||||
test('frontend page designer reports local draft edits as dirty before commit', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
|
||||
assert.equal(source.includes('function markDirty'), true)
|
||||
assert.equal(source.includes("emit('dirty-change', true)"), true)
|
||||
assert.equal(source.includes('const suppressDirty = ref(false)'), true)
|
||||
assert.equal(source.includes('syncJson({ silent: true })'), true)
|
||||
assert.equal(source.includes('v-model="draft.pageName" @input="markDirty"'), true)
|
||||
assert.equal(source.includes('v-model="draft.routePath" @input="markDirty"'), true)
|
||||
})
|
||||
|
||||
test('frontend page designer renders page navigation as a normal menu inside the canvas', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
|
||||
assert.equal(source.includes('<nav class="page-menu"'), false)
|
||||
assert.equal(source.includes('class="canvas-page-menu"'), true)
|
||||
assert.equal(source.includes('class="canvas-page-menu-bar"'), true)
|
||||
assert.equal(source.includes('v-for="group in pageMenuGroups"'), true)
|
||||
assert.equal(source.includes('@click.stop="selectMenuPage(child)"'), true)
|
||||
assert.equal(source.includes('@click.stop="selectMenuGroup(group)"'), true)
|
||||
assert.equal(source.includes('@click.stop="toggleMenuGroup(group.key)"'), true)
|
||||
assert.equal(source.includes('class="canvas-menu-config"'), true)
|
||||
assert.equal(source.includes('function selectMenuConfig'), true)
|
||||
assert.equal(source.includes('const pageMenuGroups = computed'), true)
|
||||
})
|
||||
|
||||
test('frontend page designer hides associated pages from the final menu while keeping them editable', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
|
||||
assert.equal(source.includes('const editableMenuPages = computed'), true)
|
||||
assert.equal(source.includes('const associatedPages = computed'), true)
|
||||
assert.equal(source.includes('const visibleAssociatedPages = computed'), true)
|
||||
assert.equal(source.includes('selectedMenuGroup.value?.children.length'), true)
|
||||
assert.equal(source.includes('shouldShowInMenu'), true)
|
||||
assert.equal(source.includes('isAssociatedPageType'), true)
|
||||
assert.equal(readSource('utils/pageDesignerMenu.js').includes('pageType === \'detail\''), true)
|
||||
assert.equal(source.includes('v-for="group in pageMenuGroups"'), true)
|
||||
assert.equal(source.includes('buildPageMenuGroups(editableMenuPages.value, { mode: activeMode() })'), true)
|
||||
assert.equal(source.includes('关联页面'), true)
|
||||
assert.equal(source.includes('v-for="design in visibleAssociatedPages"'), true)
|
||||
assert.equal(source.includes('v-model="navigationConfig.showInMenu"'), true)
|
||||
assert.equal(source.includes('显示在菜单'), true)
|
||||
})
|
||||
|
||||
test('frontend page designer selects primary page from menu label while arrow toggles children', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
const functionStart = source.indexOf('function selectMenuGroup(group)')
|
||||
const functionEnd = source.indexOf('function selectMenuPage', functionStart)
|
||||
const functionBody = source.slice(functionStart, functionEnd)
|
||||
|
||||
assert.equal(source.includes('@click.stop="selectMenuGroup(group)"'), true)
|
||||
assert.equal(source.includes('@click.stop="toggleMenuGroup(group.key)"'), true)
|
||||
assert.match(functionBody, /if \(group\?\.primary\)[\s\S]*?selectDesign\(group\.primary\)[\s\S]*?return/)
|
||||
assert.equal(functionBody.indexOf('if (group?.primary)') < functionBody.indexOf('if (group?.children?.length)'), true)
|
||||
})
|
||||
|
||||
test('frontend page designer keeps menu display name separate from page name', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
|
||||
assert.equal(source.includes('菜单显示名称'), true)
|
||||
assert.equal(source.includes('v-model="navigationConfig.menuName"'), true)
|
||||
assert.equal(source.includes("currentMenuLevel === 'primary'"), true)
|
||||
})
|
||||
|
||||
test('frontend page designer keeps left side focused on adding materials', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
|
||||
assert.equal(source.includes('class="add-content-materials"'), true)
|
||||
assert.equal(source.includes('@click="addPageDesign(\'primary\')"'), true)
|
||||
assert.equal(source.includes('@click="addPageDesign(\'secondary\')"'), true)
|
||||
assert.equal(source.includes('class="material-pane"'), true)
|
||||
assert.equal(source.includes('class="material-section field-materials"'), true)
|
||||
assert.equal(source.includes('class="material-section action-materials"'), true)
|
||||
assert.equal(source.includes('v-if="isBusinessBlockLayout" class="material-section business-materials"'), true)
|
||||
assert.equal(source.includes('<aside class="page-list">'), false)
|
||||
})
|
||||
|
||||
test('frontend page designer removes redundant structure and canvas action blocks', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
|
||||
assert.equal(source.includes('class="structure-materials"'), false)
|
||||
assert.equal(source.includes('当前页面结构'), false)
|
||||
assert.equal(source.includes('v-for="item in pageStructureItems"'), false)
|
||||
assert.equal(source.includes('class="canvas-menu-actions"'), false)
|
||||
assert.equal(source.includes('.canvas-menu-actions'), false)
|
||||
})
|
||||
|
||||
test('frontend page designer does not commit stale draft while reconciling deleted pages', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
|
||||
assert.equal(source.includes('function selectDesign(design, shouldCommit = true)'), true)
|
||||
assert.equal(source.includes('if (shouldCommit && draft.value && selectedKey.value && nextKey !== selectedKey.value)'), true)
|
||||
assert.equal(source.includes('selectDesign(selected, false)'), true)
|
||||
})
|
||||
|
||||
test('frontend page designer keeps right pane focused on selected configuration', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
const propertyPaneStart = source.indexOf('<aside class="property-pane">')
|
||||
const propertyPaneEnd = source.indexOf('</aside>', propertyPaneStart)
|
||||
const propertyPane = source.slice(propertyPaneStart, propertyPaneEnd)
|
||||
|
||||
assert.equal(propertyPane.includes('field-matrix'), false)
|
||||
assert.equal(propertyPane.includes('action-editor'), false)
|
||||
assert.equal(propertyPane.includes('字段池'), false)
|
||||
assert.equal(propertyPane.includes('按钮池'), false)
|
||||
assert.equal(source.includes('function addPageDesign'), true)
|
||||
assert.equal(source.includes('pendingSelectPageCode'), true)
|
||||
})
|
||||
|
||||
test('frontend page designer delegates business block pages without replacing normal layouts', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
|
||||
assert.equal(source.includes("import BusinessBlockDesigner from '@/components/business-blocks/BusinessBlockDesigner.vue'"), true)
|
||||
assert.equal(source.includes("import BlockConfigForm from '@/components/business-blocks/BlockConfigForm.vue'"), true)
|
||||
assert.equal(source.includes("blockDefinitions: { type: Array, default: () => [] }"), true)
|
||||
assert.equal(source.includes('business-blocks-v1'), true)
|
||||
assert.equal(source.includes('isBusinessBlockLayout'), true)
|
||||
assert.equal(source.includes('<BusinessBlockDesigner'), true)
|
||||
assert.equal(source.includes(':show-config="false"'), true)
|
||||
assert.equal(source.includes(':show-palette="false"'), true)
|
||||
assert.equal(source.includes('@select-block="handleBusinessBlockSelect"'), true)
|
||||
assert.equal(source.includes('addBusinessBlock'), true)
|
||||
assert.equal(source.includes('business-block-config-panel'), true)
|
||||
assert.equal(source.includes('updateSelectedBusinessBlockConfig'), true)
|
||||
assert.equal(source.includes('applyBusinessBlockLayoutJson'), true)
|
||||
})
|
||||
|
||||
test('frontend page designer supports embedded business blocks on normal pages', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
|
||||
assert.equal(source.includes('embedded-business-materials'), true)
|
||||
assert.equal(source.includes('embedded-block-band'), true)
|
||||
assert.equal(source.includes('embeddedBlocks'), true)
|
||||
assert.equal(source.includes('embeddedBlockDefinitions'), true)
|
||||
assert.equal(source.includes('function addEmbeddedBusinessBlock'), true)
|
||||
assert.equal(source.includes('function dropEmbeddedBusinessBlock'), true)
|
||||
assert.equal(source.includes('function removeEmbeddedBusinessBlock'), true)
|
||||
assert.equal(source.includes('selectedBusinessBlockZone'), true)
|
||||
assert.equal(source.includes("placement === 'embedded'"), true)
|
||||
assert.equal(source.includes('v-if="selectedBusinessBlock || isBusinessBlockLayout"'), true)
|
||||
})
|
||||
|
||||
test('frontend page designer can switch copy and defaults for admin mode', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
|
||||
assert.equal(source.includes('mode: { type: String, default: \'frontend\' }'), true)
|
||||
assert.equal(source.includes('后台页面设计'), true)
|
||||
assert.equal(source.includes('先从后台菜单和数据表初始化后台页面设计'), true)
|
||||
assert.equal(source.includes('admin-list-v1'), true)
|
||||
assert.equal(source.includes('admin-list-dense-v1'), true)
|
||||
assert.equal(source.includes('designerCopy'), true)
|
||||
})
|
||||
|
||||
test('frontend page designer exposes page-type scoped layout choices', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
|
||||
assert.equal(source.includes('布局类型'), true)
|
||||
assert.equal(source.includes('layoutOptionsByPageType'), true)
|
||||
assert.equal(source.includes('frontend-list-card-v1'), true)
|
||||
assert.equal(source.includes('frontend-current-user-card-v1'), true)
|
||||
assert.equal(source.includes('frontend-detail-media-v1'), true)
|
||||
assert.equal(source.includes('frontend-form-group-v1'), true)
|
||||
assert.equal(source.includes('@change="selectLayout"'), true)
|
||||
})
|
||||
|
||||
test('frontend page designer supports visual canvas regions and palette dragging', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
|
||||
assert.equal(source.includes('字段池'), true)
|
||||
assert.equal(source.includes('按钮池'), true)
|
||||
assert.equal(source.includes('删除区域'), true)
|
||||
assert.equal(source.includes('region-materials'), true)
|
||||
assert.equal(source.includes('图片字段'), true)
|
||||
assert.equal(source.includes('dropPaletteField'), true)
|
||||
assert.equal(source.includes('dropAction'), true)
|
||||
assert.equal(source.includes('cardRegion'), true)
|
||||
assert.equal(source.includes('setRegionVisible'), true)
|
||||
})
|
||||
|
||||
test('frontend page designer lets placed action buttons be removed', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
|
||||
assert.equal(source.includes('function removeAction'), true)
|
||||
assert.equal(source.includes("removeAction('toolbar', index)"), true)
|
||||
assert.equal(source.includes("removeAction('cardActions', actionIndex)"), true)
|
||||
assert.equal(source.includes("removeAction('table', actionIndex)"), true)
|
||||
assert.equal(source.includes('class="chip-remove"'), true)
|
||||
})
|
||||
|
||||
test('frontend page designer lets placed fields be removed from canvas regions', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
|
||||
assert.equal(source.includes('function removeRegionField'), true)
|
||||
assert.equal(source.includes("removeRegionField('query', index)"), true)
|
||||
assert.equal(source.includes("removeRegionField('table', index)"), true)
|
||||
assert.equal(source.includes("removeRegionField('card', fieldIndex)"), true)
|
||||
assert.equal(source.includes("removeRegionField('form', index)"), true)
|
||||
assert.equal(source.includes('title="删除字段"'), true)
|
||||
})
|
||||
|
||||
test('frontend page designer exposes row action drop zones in non-card layouts', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
|
||||
assert.equal(source.includes('class="table-actions drop-zone"'), true)
|
||||
assert.equal(source.includes("dropAction('table')"), true)
|
||||
assert.equal(source.includes("dropAction('table', actionIndex)"), true)
|
||||
assert.equal(source.includes("startRegionActionDrag('table', actionIndex)"), true)
|
||||
})
|
||||
|
||||
test('frontend page designer exposes business actions in the draggable action palette', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
|
||||
assert.equal(source.includes('businessActionPalette'), true)
|
||||
assert.equal(source.includes('sortBusinessActionPalette'), true)
|
||||
assert.equal(source.includes('businessActionCode(action)'), true)
|
||||
assert.equal(source.includes("`business:${action.code}`"), true)
|
||||
assert.equal(source.includes('props.database?.businessActions'), true)
|
||||
assert.equal(source.includes('action.ownerTable === draft.value?.tableName'), false)
|
||||
})
|
||||
Reference in New Issue
Block a user