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)
|
||||
})
|
||||
464
RuoYi-Vue/easycode-web/src/views/PageDesignerView.vue
Normal file
464
RuoYi-Vue/easycode-web/src/views/PageDesignerView.vue
Normal file
@@ -0,0 +1,464 @@
|
||||
<template>
|
||||
<section class="designer-shell-page" v-loading="loading">
|
||||
<header class="designer-topbar">
|
||||
<div class="designer-title">
|
||||
<span class="breadcrumb">{{ projectName || '项目' }} / 页面设计器</span>
|
||||
<h1>{{ scopeTitle }}</h1>
|
||||
<p>{{ scopeDescription }}</p>
|
||||
</div>
|
||||
<div class="designer-actions">
|
||||
<span v-if="hasUnsavedChanges" class="dirty-badge">有未保存修改</span>
|
||||
<el-button :disabled="!projectId" :loading="previewing" @click="handlePreview">预览页面</el-button>
|
||||
<el-button type="primary" :disabled="!projectId" :loading="saving" @click="saveAllDesigns">保存设计</el-button>
|
||||
<el-button @click="handleBack">返回工作台</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="designer-toolbar">
|
||||
<el-segmented v-model="scopeProxy" :options="scopeOptions" />
|
||||
<div class="toolbar-meta">
|
||||
<span>{{ pageDesigns.length }} 个页面</span>
|
||||
<span>{{ database.tables.length }} 张数据表</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-alert
|
||||
v-if="errorMessage"
|
||||
class="designer-alert"
|
||||
type="warning"
|
||||
:title="errorMessage"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<div class="designer-card">
|
||||
<FrontendPageDesigner
|
||||
ref="designerRef"
|
||||
:designs="pageDesigns"
|
||||
:database="database"
|
||||
:block-definitions="scope === 'frontend' ? businessBlockDefinitions : []"
|
||||
:mode="scope"
|
||||
:show-header="false"
|
||||
:saving="saving"
|
||||
:initializing="initializing"
|
||||
@update:designs="handleDesignsUpdate"
|
||||
@dirty-change="handleDirtyChange"
|
||||
@init-designs="handleInitPageDesigns"
|
||||
@create-design="handleCreatePageDesign"
|
||||
@delete-design="handleDeletePageDesign"
|
||||
@save-design="handleSavePageDesign"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { onBeforeRouteLeave, onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import FrontendPageDesigner from '@/components/FrontendPageDesigner.vue'
|
||||
import {
|
||||
createPageDesign,
|
||||
deletePageDesign,
|
||||
generateProject,
|
||||
getDatabase,
|
||||
getProject,
|
||||
initPageDesigns,
|
||||
listBusinessBlocks,
|
||||
listPageDesigns,
|
||||
savePageDesign
|
||||
} from '@/api/project'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const designerRef = ref(null)
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const initializing = ref(false)
|
||||
const previewing = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const projectName = ref('')
|
||||
const database = ref({ tables: [], sql: '', erDiagram: emptyErDiagram(), businessActions: [] })
|
||||
const pageDesigns = ref([])
|
||||
const businessBlockDefinitions = ref([])
|
||||
const hasUnsavedChanges = ref(false)
|
||||
|
||||
const projectId = computed(() => normalizeProjectId(route.params.projectId || route.query.projectId))
|
||||
const scope = computed(() => route.query.scope === 'admin' ? 'admin' : 'frontend')
|
||||
const scopeProxy = computed({
|
||||
get: () => scope.value,
|
||||
set: (value) => {
|
||||
router.replace({
|
||||
path: `/project/${projectId.value}/page-designer`,
|
||||
query: { ...route.query, scope: value }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const scopeOptions = [
|
||||
{ label: '前台', value: 'frontend' },
|
||||
{ label: '后台', value: 'admin' }
|
||||
]
|
||||
|
||||
const scopeTitle = computed(() => scope.value === 'admin' ? '后台页面设计' : '前台页面设计')
|
||||
const scopeDescription = computed(() => scope.value === 'admin'
|
||||
? '配置后台管理页的查询区、工具栏、表格列、表单字段、批量操作和行按钮。'
|
||||
: '配置前台页面的字段、查询区、卡片列表、按钮和公告、轮播等业务块。')
|
||||
|
||||
function emptyErDiagram() {
|
||||
return {
|
||||
positions: {},
|
||||
relations: [],
|
||||
deletedEdgeIds: []
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeProjectId(value) {
|
||||
const id = Number(value)
|
||||
return Number.isFinite(id) && id > 0 ? id : null
|
||||
}
|
||||
|
||||
function normalizeErDiagram(value) {
|
||||
const source = value && typeof value === 'object' ? value : {}
|
||||
return {
|
||||
positions: source.positions && typeof source.positions === 'object' ? source.positions : {},
|
||||
relations: Array.isArray(source.relations) ? source.relations : [],
|
||||
deletedEdgeIds: Array.isArray(source.deletedEdgeIds) ? source.deletedEdgeIds : []
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDatabase(payload) {
|
||||
const data = payload?.data || payload || {}
|
||||
return {
|
||||
tables: Array.isArray(data.tables) ? data.tables : [],
|
||||
sql: data.sql || '',
|
||||
erDiagram: normalizeErDiagram(data.erDiagram),
|
||||
businessActions: Array.isArray(data.businessActions) ? data.businessActions : []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDesignerData() {
|
||||
if (!projectId.value) {
|
||||
errorMessage.value = '缺少项目 ID,请从工作台重新进入页面设计器'
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const [projectResult, databaseResult, designResult, blockResult] = await Promise.all([
|
||||
getProject(projectId.value),
|
||||
getDatabase(projectId.value),
|
||||
listPageDesigns(projectId.value, scope.value),
|
||||
listBusinessBlocks(projectId.value)
|
||||
])
|
||||
projectName.value = projectResult?.projectName || ''
|
||||
database.value = normalizeDatabase(databaseResult)
|
||||
pageDesigns.value = Array.isArray(designResult) ? designResult : []
|
||||
businessBlockDefinitions.value = Array.isArray(blockResult) ? blockResult : []
|
||||
hasUnsavedChanges.value = false
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message || '加载页面设计器失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDesignsUpdate(designs) {
|
||||
pageDesigns.value = Array.isArray(designs) ? designs : []
|
||||
hasUnsavedChanges.value = true
|
||||
}
|
||||
|
||||
function handleDirtyChange(isDirty) {
|
||||
if (isDirty) {
|
||||
hasUnsavedChanges.value = true
|
||||
}
|
||||
}
|
||||
|
||||
async function handleInitPageDesigns() {
|
||||
if (!projectId.value) return
|
||||
initializing.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const result = await initPageDesigns(projectId.value, scope.value)
|
||||
pageDesigns.value = Array.isArray(result?.designs) ? result.designs : []
|
||||
if (!pageDesigns.value.length) {
|
||||
const refreshed = await listPageDesigns(projectId.value, scope.value)
|
||||
pageDesigns.value = Array.isArray(refreshed) ? refreshed : []
|
||||
}
|
||||
hasUnsavedChanges.value = false
|
||||
ElMessage.success(`${scopeTitle.value}已初始化`)
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message || `初始化${scopeTitle.value}失败`
|
||||
} finally {
|
||||
initializing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreatePageDesign(design) {
|
||||
if (!projectId.value) return
|
||||
const wasDirty = hasUnsavedChanges.value
|
||||
saving.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const saved = await createPageDesign(projectId.value, design, scope.value)
|
||||
pageDesigns.value = pageDesigns.value.concat(saved)
|
||||
hasUnsavedChanges.value = wasDirty
|
||||
ElMessage.success('页面已新增')
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message || '新增页面失败'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSavePageDesign(design) {
|
||||
if (!projectId.value || !design?.designId) return
|
||||
saving.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const saved = await savePageDesign(projectId.value, design.designId, design, scope.value)
|
||||
replacePageDesign(saved)
|
||||
hasUnsavedChanges.value = false
|
||||
ElMessage.success('页面设计已保存')
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message || '保存页面设计失败'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeletePageDesign(design) {
|
||||
if (!projectId.value || !design?.designId) return
|
||||
const confirmed = await confirmDeletePage(design)
|
||||
if (!confirmed) return
|
||||
const wasDirty = hasUnsavedChanges.value
|
||||
saving.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
await deletePageDesign(projectId.value, design.designId, scope.value)
|
||||
removePageDesign(design)
|
||||
hasUnsavedChanges.value = wasDirty
|
||||
ElMessage.success('页面已删除')
|
||||
} catch (error) {
|
||||
if (shouldRemoveMissingPage(error)) {
|
||||
removePageDesign(design)
|
||||
ElMessage.warning('页面已不存在,已从列表移除')
|
||||
return
|
||||
}
|
||||
errorMessage.value = error.message || '删除页面失败'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function replacePageDesign(saved) {
|
||||
pageDesigns.value = pageDesigns.value.map((item) => {
|
||||
const sameDesign = String(item.designId || item.pageCode) === String(saved.designId || saved.pageCode)
|
||||
return sameDesign ? saved : item
|
||||
})
|
||||
}
|
||||
|
||||
function removePageDesign(deleted) {
|
||||
pageDesigns.value = pageDesigns.value.filter((item) => {
|
||||
return String(item.designId || item.pageCode) !== String(deleted.designId || deleted.pageCode)
|
||||
})
|
||||
}
|
||||
|
||||
function shouldRemoveMissingPage(error) {
|
||||
const message = String(error?.message || '')
|
||||
return message.includes('不存在') || message.includes('无权限') || message.includes('404')
|
||||
}
|
||||
|
||||
async function confirmDeletePage(design) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除页面“${design.pageName || design.pageCode}”?`, '删除页面', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAllDesigns() {
|
||||
if (!projectId.value) return
|
||||
designerRef.value?.commitCurrentDraft?.()
|
||||
const pendingDesigns = pageDesigns.value.filter((design) => design?.designId)
|
||||
if (!pendingDesigns.length) {
|
||||
hasUnsavedChanges.value = false
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
for (const design of pendingDesigns) {
|
||||
const saved = await savePageDesign(projectId.value, design.designId, design, scope.value)
|
||||
replacePageDesign(saved)
|
||||
}
|
||||
hasUnsavedChanges.value = false
|
||||
ElMessage.success('页面设计已保存')
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message || '保存页面设计失败'
|
||||
throw error
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePreview() {
|
||||
if (!projectId.value) return
|
||||
previewing.value = true
|
||||
try {
|
||||
await saveAllDesigns()
|
||||
await generateProject(projectId.value)
|
||||
router.push(`/project/${projectId.value}/preview`)
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message || '生成预览失败'
|
||||
} finally {
|
||||
previewing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmLeaveDirty() {
|
||||
if (!hasUnsavedChanges.value) return true
|
||||
try {
|
||||
await ElMessageBox.confirm('当前页面设计有未保存修改,确认离开吗?', '未保存修改', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '离开',
|
||||
cancelButtonText: '继续编辑'
|
||||
})
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBack() {
|
||||
router.push({ path: '/generate', query: { projectId: projectId.value } })
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [projectId.value, scope.value],
|
||||
() => {
|
||||
loadDesignerData()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(loadDesignerData)
|
||||
|
||||
onBeforeRouteLeave(async () => {
|
||||
return confirmLeaveDirty()
|
||||
})
|
||||
|
||||
onBeforeRouteUpdate(async () => {
|
||||
return confirmLeaveDirty()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.designer-shell-page {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
min-height: calc(100vh - 76px);
|
||||
padding: 20px;
|
||||
background: #f4f7fb;
|
||||
}
|
||||
|
||||
.designer-topbar,
|
||||
.designer-toolbar,
|
||||
.designer-card {
|
||||
border: 1px solid #dfe6f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.designer-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.designer-title {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
color: #667085;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.designer-title h1 {
|
||||
margin: 0;
|
||||
color: #172033;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.designer-title p {
|
||||
margin: 0;
|
||||
color: #667085;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.designer-actions,
|
||||
.designer-toolbar,
|
||||
.toolbar-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.designer-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.designer-toolbar {
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.toolbar-meta {
|
||||
color: #667085;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dirty-badge {
|
||||
padding: 5px 9px;
|
||||
border-radius: 999px;
|
||||
background: #fff8e6;
|
||||
color: #b7791f;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.designer-alert {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.designer-card {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.designer-card :deep(.page-designer-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.designer-topbar,
|
||||
.designer-toolbar {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.designer-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
64
RuoYi-Vue/easycode-web/src/views/pageDesignerView.test.mjs
Normal file
64
RuoYi-Vue/easycode-web/src/views/pageDesignerView.test.mjs
Normal file
@@ -0,0 +1,64 @@
|
||||
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('router exposes standalone page designer route', () => {
|
||||
const routerSource = readSource('router/index.js')
|
||||
|
||||
assert.equal(routerSource.includes("import PageDesignerView from '@/views/PageDesignerView.vue'"), true)
|
||||
assert.equal(routerSource.includes("path: '/project/:projectId/page-designer'"), true)
|
||||
assert.equal(routerSource.includes("name: 'page-designer'"), true)
|
||||
assert.equal(routerSource.includes('component: PageDesignerView'), true)
|
||||
})
|
||||
|
||||
test('standalone page designer wraps the focused designer component', () => {
|
||||
assert.equal(existsSync(resolve(srcDir, 'views/PageDesignerView.vue')), true)
|
||||
const source = readSource('views/PageDesignerView.vue')
|
||||
|
||||
assert.equal(source.includes("import FrontendPageDesigner from '@/components/FrontendPageDesigner.vue'"), true)
|
||||
assert.equal(source.includes('<FrontendPageDesigner'), true)
|
||||
assert.equal(source.includes(':show-header="false"'), true)
|
||||
assert.equal(source.includes('designer-shell-page'), true)
|
||||
assert.equal(source.includes('designer-topbar'), true)
|
||||
assert.equal(source.includes('保存设计'), true)
|
||||
assert.equal(source.includes('返回工作台'), true)
|
||||
assert.equal(source.includes('预览页面'), true)
|
||||
})
|
||||
|
||||
test('standalone page designer loads scope-specific designs and business blocks', () => {
|
||||
const source = readSource('views/PageDesignerView.vue')
|
||||
|
||||
assert.equal(source.includes('const scope = computed'), true)
|
||||
assert.equal(source.includes("route.query.scope === 'admin'"), true)
|
||||
assert.equal(source.includes('listPageDesigns(projectId.value, scope.value)'), true)
|
||||
assert.equal(source.includes('listBusinessBlocks(projectId.value)'), true)
|
||||
assert.equal(source.includes('initPageDesigns(projectId.value, scope.value)'), true)
|
||||
assert.equal(source.includes('createPageDesign(projectId.value, design, scope.value)'), true)
|
||||
assert.equal(source.includes('savePageDesign(projectId.value, design.designId, design, scope.value)'), true)
|
||||
assert.equal(source.includes('deletePageDesign(projectId.value, design.designId, scope.value)'), true)
|
||||
})
|
||||
|
||||
test('standalone page designer guards dirty local edits across scope route changes', () => {
|
||||
const source = readSource('views/PageDesignerView.vue')
|
||||
|
||||
assert.equal(source.includes('@dirty-change="handleDirtyChange"'), true)
|
||||
assert.equal(source.includes('function handleDirtyChange'), true)
|
||||
assert.equal(source.includes('onBeforeRouteUpdate'), true)
|
||||
assert.equal(source.includes('return confirmLeaveDirty()'), true)
|
||||
})
|
||||
|
||||
test('frontend page designer can hide its internal header when hosted by standalone page', () => {
|
||||
const source = readSource('components/FrontendPageDesigner.vue')
|
||||
|
||||
assert.equal(source.includes('showHeader: { type: Boolean, default: true }'), true)
|
||||
assert.equal(source.includes('v-if="showHeader"'), true)
|
||||
})
|
||||
@@ -0,0 +1,759 @@
|
||||
package com.ruoyi.generator.service;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.generator.domain.GenProject;
|
||||
import com.ruoyi.generator.domain.GenTable;
|
||||
import com.ruoyi.generator.domain.GenTableColumn;
|
||||
import com.ruoyi.generator.domain.GeneratedBusinessBlockFile;
|
||||
import com.ruoyi.generator.domain.front.FrontProjectPageDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.block.BusinessBlockConfigField;
|
||||
import com.ruoyi.generator.domain.front.dto.block.BusinessBlockDefinition;
|
||||
import com.ruoyi.generator.domain.front.dto.block.BusinessBlockInstance;
|
||||
import com.ruoyi.generator.domain.front.dto.block.BusinessBlockTemplateFile;
|
||||
import com.ruoyi.generator.domain.front.dto.block.PageBusinessBlockLayout;
|
||||
import com.ruoyi.generator.service.front.BusinessBlockRegistryService;
|
||||
import com.ruoyi.generator.util.VelocityInitializer;
|
||||
import com.ruoyi.generator.util.VelocityUtils;
|
||||
import org.apache.velocity.Template;
|
||||
import org.apache.velocity.VelocityContext;
|
||||
import org.apache.velocity.app.Velocity;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class BusinessBlockGenerationService
|
||||
{
|
||||
private static final String PAGE_TEMPLATE_RESOURCE = "business-blocks/page/business-block-page.vue.vm";
|
||||
private static final String FRONTEND_PAGE_TEMPLATE_RESOURCE = "qing/index.vue.vm";
|
||||
private static final String BUSINESS_BLOCK_CATEGORY_PREFIX = "business-block:";
|
||||
private static final String PAGE_DESIGN_CATEGORY_PREFIX = "page-design:";
|
||||
|
||||
@Autowired
|
||||
private BusinessBlockRegistryService businessBlockRegistryService;
|
||||
|
||||
public List<GeneratedBusinessBlockFile> listFiles(GenProject project, String templateType)
|
||||
{
|
||||
if (project == null || StringUtils.isEmpty(project.getPageDesigns()))
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<GeneratedBusinessBlockFile> files = new ArrayList<GeneratedBusinessBlockFile>();
|
||||
for (FrontProjectPageDesign pageDesign : project.getPageDesigns())
|
||||
{
|
||||
if (pageDesign == null || "admin".equals(pageDesign.getPageScope()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int index = 1;
|
||||
if (isFrontendBusinessBlockPage(pageDesign))
|
||||
{
|
||||
PageBusinessBlockLayout layout = registry().parseLayout(pageDesign.getLayoutJson());
|
||||
if (BusinessBlockRegistryService.BUSINESS_BLOCK_CANVAS.equals(layout.getCanvas()))
|
||||
{
|
||||
if ("frontend".equals(templateType))
|
||||
{
|
||||
files.add(pageFile(pageDesign));
|
||||
}
|
||||
index = appendBlockFiles(files, project, pageDesign, safeBlocks(layout), templateType, index);
|
||||
}
|
||||
}
|
||||
else if ("frontend".equals(templateType) && isFrontendDesignedPage(pageDesign))
|
||||
{
|
||||
files.add(frontendPageFile(pageDesign));
|
||||
}
|
||||
appendBlockFiles(files, project, pageDesign, embeddedBlocks(pageDesign), templateType, index);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
private int appendBlockFiles(List<GeneratedBusinessBlockFile> files, GenProject project,
|
||||
FrontProjectPageDesign pageDesign, List<BusinessBlockInstance> instances, String templateType, int startIndex)
|
||||
{
|
||||
int index = startIndex;
|
||||
for (BusinessBlockInstance instance : instances)
|
||||
{
|
||||
if (instance == null || StringUtils.isEmpty(instance.getBlockCode()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
BusinessBlockDefinition definition = registry().findDefinition(instance.getBlockCode());
|
||||
if (StringUtils.isEmpty(definition.getTemplates()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (BusinessBlockTemplateFile templateFile : definition.getTemplates())
|
||||
{
|
||||
String fileTemplateType = templateType(templateFile);
|
||||
if (!StringUtils.equals(fileTemplateType, templateType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
files.add(blockFile(project, pageDesign, definition, instance, templateFile, index));
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
public GeneratedBusinessBlockFile findFile(GenProject project, String templateType, String keyword)
|
||||
{
|
||||
if (StringUtils.isEmpty(keyword))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String normalizedKeyword = normalizePath(keyword);
|
||||
for (GeneratedBusinessBlockFile file : listFiles(project, templateType))
|
||||
{
|
||||
if (StringUtils.equals(keyword, file.getCategory())
|
||||
|| StringUtils.equals(normalizedKeyword, normalizePath(file.getOutputPath()))
|
||||
|| StringUtils.equals(normalizedKeyword, fileName(file.getOutputPath())))
|
||||
{
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String renderFile(GenProject project, GeneratedBusinessBlockFile file)
|
||||
{
|
||||
if (file == null)
|
||||
{
|
||||
throw new ServiceException("涓氬姟鍧楁枃浠朵笉瀛樺湪");
|
||||
}
|
||||
if ("frontendPage".equals(file.getTemplateKey()))
|
||||
{
|
||||
return renderFrontendPageDesignFile(project, file);
|
||||
}
|
||||
try
|
||||
{
|
||||
VelocityInitializer.initVelocity();
|
||||
VelocityContext context = VelocityUtils.prepareContextProject(project);
|
||||
putFileContext(context, project, file);
|
||||
Template template = Velocity.getTemplate(file.getTemplateResource(), "UTF-8");
|
||||
StringWriter writer = new StringWriter();
|
||||
template.merge(context, writer);
|
||||
return writer.toString();
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
throw new ServiceException("渲染业务块模板失败: " + file.getCategory() + " - "
|
||||
+ e.getClass().getSimpleName() + ": " + StringUtils.defaultString(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isBusinessBlockCategory(String category)
|
||||
{
|
||||
return StringUtils.isNotEmpty(category) && category.startsWith(BUSINESS_BLOCK_CATEGORY_PREFIX);
|
||||
}
|
||||
|
||||
private String renderFrontendPageDesignFile(GenProject project, GeneratedBusinessBlockFile file)
|
||||
{
|
||||
FrontProjectPageDesign pageDesign = file.getPageDesign();
|
||||
GenTable table = findTable(project, pageDesign == null ? null : pageDesign.getTableName());
|
||||
if (table == null)
|
||||
{
|
||||
throw new ServiceException("页面设计绑定的数据表不存在: " + file.getPageCode());
|
||||
}
|
||||
if (project != null)
|
||||
{
|
||||
table.setPackageName(project.getPackageName());
|
||||
}
|
||||
if (StringUtils.isEmpty(table.getModuleName()))
|
||||
{
|
||||
table.setModuleName("app");
|
||||
}
|
||||
if (StringUtils.isEmpty(table.getBusinessName()))
|
||||
{
|
||||
table.setBusinessName(StringUtils.defaultIfEmpty(StringUtils.uncapitalize(table.getClassName()),
|
||||
StringUtils.defaultString(table.getTableName())));
|
||||
}
|
||||
if (StringUtils.isEmpty(table.getFunctionName()))
|
||||
{
|
||||
table.setFunctionName(StringUtils.defaultIfEmpty(table.getTableComment(), table.getClassName()));
|
||||
}
|
||||
VelocityInitializer.initVelocity();
|
||||
VelocityContext context = VelocityUtils.prepareContext(table);
|
||||
VelocityUtils.putPageDesignVelocityContext(context, project, table, "frontend", file.getPageCode());
|
||||
putEmbeddedBlockVelocityContext(context, project, pageDesign);
|
||||
putPageDesignBusinessActions(context, project);
|
||||
Template template = Velocity.getTemplate(file.getTemplateResource(), "UTF-8");
|
||||
StringWriter writer = new StringWriter();
|
||||
template.merge(context, writer);
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
private void putPageDesignBusinessActions(VelocityContext context, GenProject project)
|
||||
{
|
||||
List<BusinessActionDesign> candidates = project == null || StringUtils.isEmpty(project.getBusinessActions())
|
||||
? Collections.<BusinessActionDesign>emptyList() : project.getBusinessActions();
|
||||
List<BusinessActionDesign> toolbarBusinessActions = filterPageDesignBusinessActions(
|
||||
candidates, context.get("pageDesignToolbarActionCodes"));
|
||||
List<BusinessActionDesign> rowBusinessActions = filterPageDesignBusinessActions(
|
||||
candidates, context.get("pageDesignRowActionCodes"));
|
||||
List<BusinessActionDesign> allActions = mergeBusinessActions(toolbarBusinessActions, rowBusinessActions);
|
||||
context.put("pageDesignToolbarBusinessActions", toolbarBusinessActions);
|
||||
context.put("hasPageDesignToolbarBusinessActions", StringUtils.isNotEmpty(toolbarBusinessActions));
|
||||
context.put("pageDesignRowBusinessActions", rowBusinessActions);
|
||||
context.put("hasPageDesignRowBusinessActions", StringUtils.isNotEmpty(rowBusinessActions));
|
||||
context.put("pageDesignBusinessActions", allActions);
|
||||
context.put("hasPageDesignBusinessActions", StringUtils.isNotEmpty(allActions));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<BusinessActionDesign> filterPageDesignBusinessActions(List<BusinessActionDesign> candidates,
|
||||
Object actionCodesObject)
|
||||
{
|
||||
if (StringUtils.isEmpty(candidates) || !(actionCodesObject instanceof List))
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> requestedCodes = new ArrayList<String>();
|
||||
for (Object actionCode : (List<Object>) actionCodesObject)
|
||||
{
|
||||
String code = StringUtils.defaultString(String.valueOf(actionCode));
|
||||
if (code.startsWith("business:"))
|
||||
{
|
||||
requestedCodes.add(code.substring("business:".length()));
|
||||
}
|
||||
}
|
||||
if (requestedCodes.isEmpty())
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<BusinessActionDesign> result = new ArrayList<BusinessActionDesign>();
|
||||
for (String requestedCode : requestedCodes)
|
||||
{
|
||||
for (BusinessActionDesign candidate : candidates)
|
||||
{
|
||||
if (candidate != null && StringUtils.equals(requestedCode, candidate.getCode())
|
||||
&& !result.contains(candidate))
|
||||
{
|
||||
result.add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<BusinessActionDesign> mergeBusinessActions(List<BusinessActionDesign> first,
|
||||
List<BusinessActionDesign> second)
|
||||
{
|
||||
List<BusinessActionDesign> result = new ArrayList<BusinessActionDesign>();
|
||||
if (first != null)
|
||||
{
|
||||
result.addAll(first);
|
||||
}
|
||||
if (second != null)
|
||||
{
|
||||
for (BusinessActionDesign action : second)
|
||||
{
|
||||
if (action != null && !result.contains(action))
|
||||
{
|
||||
result.add(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private GeneratedBusinessBlockFile pageFile(FrontProjectPageDesign pageDesign)
|
||||
{
|
||||
GeneratedBusinessBlockFile file = new GeneratedBusinessBlockFile();
|
||||
String pageCode = safePageCode(pageDesign);
|
||||
file.setCategory(BUSINESS_BLOCK_CATEGORY_PREFIX + "frontend:" + pageCode + ":page");
|
||||
file.setTemplateType("frontend");
|
||||
file.setOutputPath("src/views/pages/" + pageCode + "/index.vue");
|
||||
file.setTemplateResource(PAGE_TEMPLATE_RESOURCE);
|
||||
file.setPageCode(pageCode);
|
||||
file.setTemplateKey("page");
|
||||
file.setPageDesign(pageDesign);
|
||||
return file;
|
||||
}
|
||||
|
||||
private GeneratedBusinessBlockFile frontendPageFile(FrontProjectPageDesign pageDesign)
|
||||
{
|
||||
GeneratedBusinessBlockFile file = new GeneratedBusinessBlockFile();
|
||||
String pageCode = safePageCode(pageDesign);
|
||||
file.setCategory(PAGE_DESIGN_CATEGORY_PREFIX + "frontend:" + pageCode + ":page");
|
||||
file.setTemplateType("frontend");
|
||||
file.setOutputPath("src/views/pages/" + pageCode + "/index.vue");
|
||||
file.setTemplateResource(FRONTEND_PAGE_TEMPLATE_RESOURCE);
|
||||
file.setPageCode(pageCode);
|
||||
file.setTemplateKey("frontendPage");
|
||||
file.setPageDesign(pageDesign);
|
||||
return file;
|
||||
}
|
||||
|
||||
private GeneratedBusinessBlockFile blockFile(GenProject project, FrontProjectPageDesign pageDesign,
|
||||
BusinessBlockDefinition definition, BusinessBlockInstance instance, BusinessBlockTemplateFile templateFile,
|
||||
int index)
|
||||
{
|
||||
String pageCode = safePageCode(pageDesign);
|
||||
String blockCode = StringUtils.defaultString(instance.getBlockCode());
|
||||
String instanceCode = instanceCode(instance, blockCode, index);
|
||||
String instanceClassName = toPascalCase(instanceCode) + "Block";
|
||||
String fileTemplateType = templateType(templateFile);
|
||||
GeneratedBusinessBlockFile file = new GeneratedBusinessBlockFile();
|
||||
file.setCategory(BUSINESS_BLOCK_CATEGORY_PREFIX + fileTemplateType + ":" + pageCode + ":" + instanceCode
|
||||
+ ":" + StringUtils.defaultString(templateFile.getKey()));
|
||||
file.setTemplateType(fileTemplateType);
|
||||
file.setOutputPath(resolveOutputPath(project, pageCode, blockCode, instanceCode, instanceClassName,
|
||||
templateFile.getOutputPath()));
|
||||
file.setTemplateResource("business-blocks/" + blockCode + "/" + normalizePath(templateFile.getTemplate()));
|
||||
file.setPageCode(pageCode);
|
||||
file.setBlockCode(blockCode);
|
||||
file.setInstanceCode(instanceCode);
|
||||
file.setInstanceClassName(instanceClassName);
|
||||
file.setTemplateKey(templateFile.getKey());
|
||||
file.setPageDesign(pageDesign);
|
||||
file.setDefinition(definition);
|
||||
file.setInstance(instance);
|
||||
file.setTemplateFile(templateFile);
|
||||
return file;
|
||||
}
|
||||
|
||||
private void putFileContext(VelocityContext context, GenProject project, GeneratedBusinessBlockFile file)
|
||||
{
|
||||
FrontProjectPageDesign pageDesign = file.getPageDesign();
|
||||
context.put("pageDesign", pageDesign);
|
||||
context.put("pageCode", file.getPageCode());
|
||||
context.put("pageName", pageDesign == null ? "" : StringUtils.defaultString(pageDesign.getPageName()));
|
||||
context.put("routePath", pageDesign == null ? "" : StringUtils.defaultString(pageDesign.getRoutePath()));
|
||||
context.put("pageComponentName", toPascalCase(file.getPageCode()));
|
||||
context.put("businessBlocks", pageBlocks(project, pageDesign));
|
||||
|
||||
BusinessBlockInstance instance = file.getInstance();
|
||||
if (instance == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Map<String, String> config = instance.getConfig() == null
|
||||
? Collections.<String, String>emptyMap() : instance.getConfig();
|
||||
Map<String, String> renderConfig = renderConfig(project, file.getDefinition(), config);
|
||||
context.put("definition", file.getDefinition());
|
||||
context.put("instance", instance);
|
||||
context.put("config", renderConfig);
|
||||
context.put("rawConfig", config);
|
||||
context.put("blockCode", file.getBlockCode());
|
||||
context.put("instanceCode", file.getInstanceCode());
|
||||
context.put("InstanceClassName", file.getInstanceClassName());
|
||||
context.put("instanceClassName", StringUtils.uncapitalize(file.getInstanceClassName()));
|
||||
context.put("apiBasePath", "/business-blocks/" + file.getInstanceCode());
|
||||
context.put("apiModulePath", "@/api/business-blocks/" + file.getInstanceCode());
|
||||
context.put("cartPkColumn", pkColumnName(project, renderConfig.get("cartTable")));
|
||||
context.put("noticePkColumn", pkColumnName(project, renderConfig.get("noticeTable")));
|
||||
context.put("carouselPkColumn", pkColumnName(project, renderConfig.get("carouselTable")));
|
||||
context.put("orderPkColumn", pkColumnName(project, renderConfig.get("orderTable")));
|
||||
context.put("orderItemPkColumn", pkColumnName(project, renderConfig.get("orderItemTable")));
|
||||
context.put("productPkColumn", pkColumnName(project, renderConfig.get("productTable")));
|
||||
context.put("masterPkColumn", pkColumnName(project, renderConfig.get("masterTable")));
|
||||
context.put("detailPkColumn", pkColumnName(project, renderConfig.get("detailTable")));
|
||||
context.put("hasProductImage", StringUtils.isNotEmpty(renderConfig.get("productImage")));
|
||||
context.put("hasStock", StringUtils.isNotEmpty(renderConfig.get("stock")));
|
||||
context.put("hasNoticeStatus", StringUtils.isNotEmpty(renderConfig.get("status")));
|
||||
context.put("hasNoticePublishTime", StringUtils.isNotEmpty(renderConfig.get("publishTime")));
|
||||
context.put("hasCarouselLink", StringUtils.isNotEmpty(renderConfig.get("link")));
|
||||
context.put("hasCarouselSort", StringUtils.isNotEmpty(renderConfig.get("sort")));
|
||||
context.put("hasCarouselStatus", StringUtils.isNotEmpty(renderConfig.get("status")));
|
||||
context.put("hasOrderCreateTime", StringUtils.isNotEmpty(renderConfig.get("orderCreateTime")));
|
||||
context.put("hasOrderProductImage", StringUtils.isNotEmpty(renderConfig.get("productImage")));
|
||||
context.put("hasMasterDescription", StringUtils.isNotEmpty(renderConfig.get("masterDescription")));
|
||||
context.put("hasDetailValue", StringUtils.isNotEmpty(renderConfig.get("detailValue")));
|
||||
context.put("hasDetailTime", StringUtils.isNotEmpty(renderConfig.get("detailTime")));
|
||||
for (Map.Entry<String, String> entry : renderConfig.entrySet())
|
||||
{
|
||||
context.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> renderConfig(GenProject project, BusinessBlockDefinition definition,
|
||||
Map<String, String> config)
|
||||
{
|
||||
Map<String, String> renderConfig = new LinkedHashMap<String, String>();
|
||||
if (config != null)
|
||||
{
|
||||
renderConfig.putAll(config);
|
||||
}
|
||||
List<BusinessBlockConfigField> fields = definition == null || definition.getConfigs() == null
|
||||
? Collections.<BusinessBlockConfigField>emptyList() : definition.getConfigs();
|
||||
for (BusinessBlockConfigField field : fields)
|
||||
{
|
||||
if (field == null || !"field".equals(field.getType()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String value = config == null ? null : config.get(field.getKey());
|
||||
if (StringUtils.isEmpty(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String tableName = config == null ? null : config.get(field.getTable());
|
||||
renderConfig.put(field.getKey(), columnName(project, tableName, value));
|
||||
}
|
||||
return renderConfig;
|
||||
}
|
||||
|
||||
public void putEmbeddedBlockVelocityContext(VelocityContext context, GenProject project,
|
||||
FrontProjectPageDesign pageDesign)
|
||||
{
|
||||
List<Map<String, Object>> topBlocks = embeddedPageBlocks(project, pageDesign, "top");
|
||||
List<Map<String, Object>> bottomBlocks = embeddedPageBlocks(project, pageDesign, "bottom");
|
||||
List<Map<String, Object>> allBlocks = new ArrayList<Map<String, Object>>();
|
||||
allBlocks.addAll(topBlocks);
|
||||
allBlocks.addAll(bottomBlocks);
|
||||
context.put("pageDesignTopBusinessBlocks", topBlocks);
|
||||
context.put("pageDesignBottomBusinessBlocks", bottomBlocks);
|
||||
context.put("pageDesignEmbeddedBusinessBlocks", allBlocks);
|
||||
context.put("hasPageDesignTopBusinessBlocks", StringUtils.isNotEmpty(topBlocks));
|
||||
context.put("hasPageDesignBottomBusinessBlocks", StringUtils.isNotEmpty(bottomBlocks));
|
||||
context.put("hasPageDesignEmbeddedBusinessBlocks", StringUtils.isNotEmpty(allBlocks));
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> pageBlocks(GenProject project, FrontProjectPageDesign pageDesign)
|
||||
{
|
||||
if (pageDesign == null)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
PageBusinessBlockLayout layout = registry().parseLayout(pageDesign.getLayoutJson());
|
||||
GeneratedBusinessBlockFile pageFile = pageFile(pageDesign);
|
||||
return blockViewModels(project, pageDesign, safeBlocks(layout), parentPath(pageFile.getOutputPath()), false);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> embeddedPageBlocks(GenProject project, FrontProjectPageDesign pageDesign,
|
||||
String zone)
|
||||
{
|
||||
Map<String, List<BusinessBlockInstance>> zones = registry().parseEmbeddedBlockZones(
|
||||
pageDesign == null ? null : pageDesign.getLayoutJson());
|
||||
List<BusinessBlockInstance> instances = zones.get(zone);
|
||||
return blockViewModels(project, pageDesign, instances, null, true);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> blockViewModels(GenProject project, FrontProjectPageDesign pageDesign,
|
||||
List<BusinessBlockInstance> instances, String pageDir, boolean useVueAlias)
|
||||
{
|
||||
List<Map<String, Object>> blocks = new ArrayList<Map<String, Object>>();
|
||||
int index = 1;
|
||||
for (BusinessBlockInstance instance : instances == null
|
||||
? Collections.<BusinessBlockInstance>emptyList() : instances)
|
||||
{
|
||||
if (instance == null || StringUtils.isEmpty(instance.getBlockCode()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
BusinessBlockDefinition definition = registry().findDefinition(instance.getBlockCode());
|
||||
BusinessBlockTemplateFile templateFile = frontendComponentTemplate(definition);
|
||||
if (templateFile == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
GeneratedBusinessBlockFile blockFile = blockFile(project, pageDesign, definition, instance, templateFile, index);
|
||||
Map<String, Object> block = new LinkedHashMap<String, Object>();
|
||||
block.put("componentName", blockFile.getInstanceClassName());
|
||||
block.put("relativeImportPath", useVueAlias
|
||||
? vueAliasImportPath(blockFile.getOutputPath())
|
||||
: relativeImportPath(pageDir, blockFile.getOutputPath()));
|
||||
block.put("instanceCode", blockFile.getInstanceCode());
|
||||
block.put("blockCode", blockFile.getBlockCode());
|
||||
block.put("name", StringUtils.defaultIfEmpty(instance.getName(), definition.getName()));
|
||||
blocks.add(block);
|
||||
index++;
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private BusinessBlockTemplateFile frontendComponentTemplate(BusinessBlockDefinition definition)
|
||||
{
|
||||
if (definition == null || StringUtils.isEmpty(definition.getTemplates()))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
for (BusinessBlockTemplateFile templateFile : definition.getTemplates())
|
||||
{
|
||||
if ("frontendComponent".equals(templateFile.getKey()))
|
||||
{
|
||||
return templateFile;
|
||||
}
|
||||
}
|
||||
for (BusinessBlockTemplateFile templateFile : definition.getTemplates())
|
||||
{
|
||||
if ("frontend".equals(templateType(templateFile)) && normalizePath(templateFile.getOutputPath()).endsWith(".vue"))
|
||||
{
|
||||
return templateFile;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveOutputPath(GenProject project, String pageCode, String blockCode, String instanceCode,
|
||||
String instanceClassName, String outputPath)
|
||||
{
|
||||
String path = normalizePath(outputPath);
|
||||
String packageName = project == null ? "" : StringUtils.defaultString(project.getPackageName());
|
||||
path = path.replace("${pageCode}", pageCode);
|
||||
path = path.replace("${blockCode}", blockCode);
|
||||
path = path.replace("${instanceCode}", instanceCode);
|
||||
path = path.replace("${InstanceClassName}", instanceClassName);
|
||||
path = path.replace("${instanceClassName}", StringUtils.uncapitalize(instanceClassName));
|
||||
path = path.replace("${packageName}", packageName);
|
||||
path = path.replace("${packagePath}", packageName.replace('.', '/'));
|
||||
while (path.startsWith("/"))
|
||||
{
|
||||
path = path.substring(1);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private String templateType(BusinessBlockTemplateFile templateFile)
|
||||
{
|
||||
String template = normalizePath(templateFile == null ? null : templateFile.getTemplate());
|
||||
String outputPath = normalizePath(templateFile == null ? null : templateFile.getOutputPath());
|
||||
if (template.contains("/frontend/") || outputPath.startsWith("src/views/") || outputPath.startsWith("src/api/"))
|
||||
{
|
||||
return "frontend";
|
||||
}
|
||||
return "backend";
|
||||
}
|
||||
|
||||
private boolean isFrontendBusinessBlockPage(FrontProjectPageDesign pageDesign)
|
||||
{
|
||||
if (pageDesign == null || "admin".equals(pageDesign.getPageScope()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if ("business_block".equals(pageDesign.getPageType()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return StringUtils.isNotEmpty(pageDesign.getLayoutJson())
|
||||
&& pageDesign.getLayoutJson().contains(BusinessBlockRegistryService.BUSINESS_BLOCK_CANVAS);
|
||||
}
|
||||
|
||||
private boolean isFrontendDesignedPage(FrontProjectPageDesign pageDesign)
|
||||
{
|
||||
return pageDesign != null && !"admin".equals(pageDesign.getPageScope())
|
||||
&& StringUtils.isNotEmpty(pageDesign.getPageCode())
|
||||
&& StringUtils.isNotEmpty(pageDesign.getTableName());
|
||||
}
|
||||
|
||||
private List<BusinessBlockInstance> safeBlocks(PageBusinessBlockLayout layout)
|
||||
{
|
||||
return layout == null || layout.getBlocks() == null
|
||||
? Collections.<BusinessBlockInstance>emptyList() : layout.getBlocks();
|
||||
}
|
||||
|
||||
private List<BusinessBlockInstance> embeddedBlocks(FrontProjectPageDesign pageDesign)
|
||||
{
|
||||
Map<String, List<BusinessBlockInstance>> zones = registry().parseEmbeddedBlockZones(
|
||||
pageDesign == null ? null : pageDesign.getLayoutJson());
|
||||
List<BusinessBlockInstance> blocks = new ArrayList<BusinessBlockInstance>();
|
||||
for (List<BusinessBlockInstance> zoneBlocks : zones.values())
|
||||
{
|
||||
if (zoneBlocks != null)
|
||||
{
|
||||
blocks.addAll(zoneBlocks);
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private String safePageCode(FrontProjectPageDesign pageDesign)
|
||||
{
|
||||
return sanitizeIdentifier(pageDesign == null ? "business_page" : pageDesign.getPageCode(), "business_page");
|
||||
}
|
||||
|
||||
private String instanceCode(BusinessBlockInstance instance, String blockCode, int index)
|
||||
{
|
||||
String raw = instance == null ? null : instance.getId();
|
||||
if (StringUtils.isEmpty(raw))
|
||||
{
|
||||
raw = StringUtils.defaultIfEmpty(blockCode, "block") + "_" + index;
|
||||
}
|
||||
return sanitizeIdentifier(raw, "block_" + index);
|
||||
}
|
||||
|
||||
private String sanitizeIdentifier(String value, String fallback)
|
||||
{
|
||||
String normalized = StringUtils.defaultString(value).replaceAll("[^A-Za-z0-9_-]", "_");
|
||||
return StringUtils.isEmpty(normalized) ? fallback : normalized;
|
||||
}
|
||||
|
||||
private String toPascalCase(String value)
|
||||
{
|
||||
String source = StringUtils.defaultString(value);
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean upperNext = true;
|
||||
for (int i = 0; i < source.length(); i++)
|
||||
{
|
||||
char ch = source.charAt(i);
|
||||
if (!Character.isLetterOrDigit(ch))
|
||||
{
|
||||
upperNext = true;
|
||||
continue;
|
||||
}
|
||||
if (result.length() == 0 && !Character.isJavaIdentifierStart(ch))
|
||||
{
|
||||
result.append("Block");
|
||||
}
|
||||
result.append(upperNext ? Character.toUpperCase(ch) : ch);
|
||||
upperNext = false;
|
||||
}
|
||||
return result.length() == 0 ? "BusinessBlock" : result.toString();
|
||||
}
|
||||
|
||||
private String pkColumnName(GenProject project, String tableName)
|
||||
{
|
||||
GenTable table = findTable(project, tableName);
|
||||
if (table == null)
|
||||
{
|
||||
return "id";
|
||||
}
|
||||
if (table.getPkColumn() != null && StringUtils.isNotEmpty(table.getPkColumn().getColumnName()))
|
||||
{
|
||||
return table.getPkColumn().getColumnName();
|
||||
}
|
||||
if (StringUtils.isNotEmpty(table.getColumns()))
|
||||
{
|
||||
for (GenTableColumn column : table.getColumns())
|
||||
{
|
||||
if (column != null && "1".equals(column.getIsPk()) && StringUtils.isNotEmpty(column.getColumnName()))
|
||||
{
|
||||
return column.getColumnName();
|
||||
}
|
||||
}
|
||||
for (GenTableColumn column : table.getColumns())
|
||||
{
|
||||
if (column != null && "id".equals(column.getColumnName()))
|
||||
{
|
||||
return "id";
|
||||
}
|
||||
}
|
||||
}
|
||||
return "id";
|
||||
}
|
||||
|
||||
private String columnName(GenProject project, String tableName, String fieldName)
|
||||
{
|
||||
GenTable table = findTable(project, tableName);
|
||||
if (table == null || StringUtils.isEmpty(table.getColumns()) || StringUtils.isEmpty(fieldName))
|
||||
{
|
||||
return fieldName;
|
||||
}
|
||||
for (GenTableColumn column : table.getColumns())
|
||||
{
|
||||
if (column == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (StringUtils.equals(fieldName, column.getColumnName())
|
||||
|| StringUtils.equals(fieldName, column.getJavaField()))
|
||||
{
|
||||
return StringUtils.defaultIfEmpty(column.getColumnName(), fieldName);
|
||||
}
|
||||
}
|
||||
return fieldName;
|
||||
}
|
||||
|
||||
private GenTable findTable(GenProject project, String tableName)
|
||||
{
|
||||
if (project == null || StringUtils.isEmpty(project.getTables()) || StringUtils.isEmpty(tableName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
for (GenTable table : project.getTables())
|
||||
{
|
||||
if (table != null && StringUtils.equals(tableName, table.getTableName()))
|
||||
{
|
||||
return table;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String relativeImportPath(String fromDir, String targetPath)
|
||||
{
|
||||
String[] from = normalizePath(fromDir).split("/");
|
||||
String[] target = normalizePath(targetPath).split("/");
|
||||
int common = 0;
|
||||
while (common < from.length && common < target.length && from[common].equals(target[common]))
|
||||
{
|
||||
common++;
|
||||
}
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (int i = common; i < from.length; i++)
|
||||
{
|
||||
if (from[i].length() > 0)
|
||||
{
|
||||
result.append("../");
|
||||
}
|
||||
}
|
||||
for (int i = common; i < target.length; i++)
|
||||
{
|
||||
if (target[i].length() == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (result.length() > 0 && !result.toString().endsWith("/"))
|
||||
{
|
||||
result.append("/");
|
||||
}
|
||||
result.append(target[i]);
|
||||
}
|
||||
if (result.length() == 0)
|
||||
{
|
||||
return "./";
|
||||
}
|
||||
if (!result.toString().startsWith("."))
|
||||
{
|
||||
result.insert(0, "./");
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private String vueAliasImportPath(String targetPath)
|
||||
{
|
||||
String normalized = normalizePath(targetPath);
|
||||
int index = normalized.indexOf("src/");
|
||||
if (index >= 0)
|
||||
{
|
||||
normalized = normalized.substring(index + 4);
|
||||
}
|
||||
return normalized.startsWith("@/") ? normalized : "@/" + normalized;
|
||||
}
|
||||
|
||||
private String parentPath(String path)
|
||||
{
|
||||
String normalized = normalizePath(path);
|
||||
int index = normalized.lastIndexOf('/');
|
||||
return index < 0 ? "" : normalized.substring(0, index);
|
||||
}
|
||||
|
||||
private String fileName(String path)
|
||||
{
|
||||
String normalized = normalizePath(path);
|
||||
int index = normalized.lastIndexOf('/');
|
||||
return index < 0 ? normalized : normalized.substring(index + 1);
|
||||
}
|
||||
|
||||
private String normalizePath(String path)
|
||||
{
|
||||
return StringUtils.defaultString(path).replace('\\', '/').replaceAll("/{2,}", "/");
|
||||
}
|
||||
|
||||
private BusinessBlockRegistryService registry()
|
||||
{
|
||||
if (businessBlockRegistryService == null)
|
||||
{
|
||||
businessBlockRegistryService = new BusinessBlockRegistryService();
|
||||
}
|
||||
return businessBlockRegistryService;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,11 @@ import com.ruoyi.generator.domain.SysProjectStructure;
|
||||
import com.ruoyi.generator.domain.SysProjectTemplate;
|
||||
import com.ruoyi.generator.domain.Template;
|
||||
import com.ruoyi.generator.domain.TemplateFile;
|
||||
import com.ruoyi.generator.domain.GeneratedBusinessBlockFile;
|
||||
import com.ruoyi.generator.domain.front.FrontProjectPageDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.AppPageDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionUiBindingDesign;
|
||||
import com.ruoyi.generator.mapper.GenProjectMapper;
|
||||
import com.ruoyi.generator.mapper.GenTableMapper;
|
||||
import com.ruoyi.generator.util.VelocityInitializer;
|
||||
@@ -23,6 +27,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
@@ -36,6 +41,16 @@ import java.util.zip.ZipOutputStream;
|
||||
|
||||
@Service
|
||||
public class GenProjectServiceImpl implements IGenProjectService {
|
||||
private static final String SQL_TEMPLATE_TYPE = "sql";
|
||||
private static final String SQL_CATEGORY = "sql";
|
||||
private static final Long RUNNABLE_BACKEND_TEMPLATE_ID = 9201L;
|
||||
private static final Long RUNNABLE_ADMIN_FRONTEND_TEMPLATE_ID = 9202L;
|
||||
private static final Long RUNNABLE_FRONTEND_TEMPLATE_ID = 9203L;
|
||||
private static final String BACKEND_CONTROLLER_TEMPLATE_FILE = "controller.java.vm";
|
||||
private static final String BUNDLED_BACKEND_CONTROLLER_TEMPLATE = "qing/controller.java.vm";
|
||||
private static final String FRONTEND_INDEX_TEMPLATE_FILE = "frontend-index.vue.vm";
|
||||
private static final String BUNDLED_FRONTEND_INDEX_TEMPLATE = "qing/index.vue.vm";
|
||||
|
||||
@Autowired
|
||||
private GenProjectMapper genProjectMapper;
|
||||
@Autowired
|
||||
@@ -52,6 +67,8 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
private ISysProjectTemplateService sysProjectTemplateService;
|
||||
@Autowired
|
||||
private ISysProjectStructureService sysProjectStructureService;
|
||||
@Autowired
|
||||
private BusinessBlockGenerationService businessBlockGenerationService;
|
||||
|
||||
@Override
|
||||
public GenProject selectGenProjectById(Long projectId) {
|
||||
@@ -100,6 +117,9 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
projectTables = new ArrayList<GenTable>();
|
||||
project.setTables(projectTables);
|
||||
}
|
||||
if (isSqlType(type)) {
|
||||
return getSqlProjectStructure(project);
|
||||
}
|
||||
|
||||
List<Map<String, Object>> structure = new ArrayList<Map<String, Object>>();
|
||||
SysProjectTemplate projectTemplate = resolveProjectTemplate(project, type);
|
||||
@@ -136,6 +156,7 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
addNodeToParent(structure, nodeData, node, nodeMap, sourceNodeMap, null);
|
||||
}
|
||||
}
|
||||
appendBusinessBlockFiles(structure, project, type);
|
||||
return structure;
|
||||
}
|
||||
|
||||
@@ -207,7 +228,15 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
if (project == null) {
|
||||
throw new ServiceException("项目信息不能为空");
|
||||
}
|
||||
if (isSqlType(type)) {
|
||||
return previewSqlCode(project, tableId, keyword);
|
||||
}
|
||||
Map<String, String> dataMap = new LinkedHashMap<String, String>();
|
||||
GeneratedBusinessBlockFile businessBlockFile = findBusinessBlockFile(project, type, keyword);
|
||||
if (businessBlockFile != null) {
|
||||
dataMap.put(businessBlockFile.getOutputPath(), businessBlockService().renderFile(project, businessBlockFile));
|
||||
return dataMap;
|
||||
}
|
||||
List<SysProjectModule> modules = getProjectModules(project);
|
||||
|
||||
VelocityInitializer.initVelocity();
|
||||
@@ -225,6 +254,14 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
if (project == null) {
|
||||
throw new ServiceException("项目信息不能为空");
|
||||
}
|
||||
if (isSqlType(type) || SQL_CATEGORY.equals(category)) {
|
||||
GenTable table = getRequiredTable(project, tableId);
|
||||
return getSqlFileContent(table);
|
||||
}
|
||||
GeneratedBusinessBlockFile businessBlockFile = findBusinessBlockFile(project, type, category);
|
||||
if (businessBlockFile != null) {
|
||||
return businessBlockService().renderFile(project, businessBlockFile);
|
||||
}
|
||||
List<SysProjectModule> modules = getProjectModules(project);
|
||||
|
||||
VelocityInitializer.initVelocity();
|
||||
@@ -242,6 +279,93 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
throw new ServiceException("未找到模板文件:" + category);
|
||||
}
|
||||
|
||||
private boolean isSqlType(String type) {
|
||||
return SQL_TEMPLATE_TYPE.equals(type);
|
||||
}
|
||||
|
||||
private GeneratedBusinessBlockFile findBusinessBlockFile(GenProject project, String type, String keyword) {
|
||||
return businessBlockService().findFile(project, type, keyword);
|
||||
}
|
||||
|
||||
private BusinessBlockGenerationService businessBlockService() {
|
||||
if (businessBlockGenerationService == null) {
|
||||
businessBlockGenerationService = new BusinessBlockGenerationService();
|
||||
}
|
||||
return businessBlockGenerationService;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> getSqlProjectStructure(GenProject project) {
|
||||
Map<String, Object> sqlFolder = new LinkedHashMap<String, Object>();
|
||||
sqlFolder.put("id", SQL_TEMPLATE_TYPE);
|
||||
sqlFolder.put("name", "sql");
|
||||
sqlFolder.put("type", "folder");
|
||||
sqlFolder.put("category", SQL_CATEGORY);
|
||||
List<Map<String, Object>> children = new ArrayList<Map<String, Object>>();
|
||||
sqlFolder.put("children", children);
|
||||
|
||||
if (StringUtils.isNotEmpty(project.getTables())) {
|
||||
for (GenTable table : project.getTables()) {
|
||||
Map<String, Object> fileNode = new LinkedHashMap<String, Object>();
|
||||
String fileName = getSqlFileName(table);
|
||||
fileNode.put("id", SQL_TEMPLATE_TYPE + "-" + table.getTableId());
|
||||
fileNode.put("name", fileName);
|
||||
fileNode.put("type", "file");
|
||||
fileNode.put("category", SQL_CATEGORY);
|
||||
fileNode.put("tableId", table.getTableId());
|
||||
children.add(fileNode);
|
||||
}
|
||||
}
|
||||
return Collections.singletonList(sqlFolder);
|
||||
}
|
||||
|
||||
private Map<String, String> previewSqlCode(GenProject project, Long tableId, String keyword) {
|
||||
Map<String, String> dataMap = new LinkedHashMap<String, String>();
|
||||
if (StringUtils.isEmpty(project.getTables())) {
|
||||
return dataMap;
|
||||
}
|
||||
for (GenTable table : project.getTables()) {
|
||||
String fileName = getSqlFileName(table);
|
||||
if (matchesSqlFile(table, tableId, keyword, fileName)) {
|
||||
dataMap.put(fileName, getSqlFileContent(table));
|
||||
}
|
||||
}
|
||||
return dataMap;
|
||||
}
|
||||
|
||||
private boolean matchesSqlFile(GenTable table, Long tableId, String keyword, String fileName) {
|
||||
if (tableId != null && tableId != -1) {
|
||||
return tableId.equals(table.getTableId());
|
||||
}
|
||||
if (StringUtils.isEmpty(keyword) || SQL_CATEGORY.equals(keyword)) {
|
||||
return true;
|
||||
}
|
||||
String normalizedKeyword = keyword.replace('\\', '/');
|
||||
return fileName.equals(normalizedKeyword) || normalizedKeyword.endsWith("/" + fileName);
|
||||
}
|
||||
|
||||
private String getSqlFileName(GenTable table) {
|
||||
String name = table == null ? "" : StringUtils.defaultString(table.getBusinessName());
|
||||
if (StringUtils.isEmpty(name) && table != null) {
|
||||
name = StringUtils.defaultString(table.getTableName());
|
||||
}
|
||||
if (StringUtils.isEmpty(name)) {
|
||||
name = "schema";
|
||||
}
|
||||
name = name.trim().replaceAll("[\\\\/:*?\"<>|]", "_").replaceAll("\\s+", "_");
|
||||
if (StringUtils.isEmpty(name)) {
|
||||
name = "schema";
|
||||
}
|
||||
return name.endsWith(".sql") ? name : name + ".sql";
|
||||
}
|
||||
|
||||
private String getSqlFileContent(GenTable table) {
|
||||
String sql = table == null ? "" : StringUtils.defaultString(table.getCreateTableSql());
|
||||
if (StringUtils.isEmpty(sql)) {
|
||||
return "-- 未生成建表 SQL";
|
||||
}
|
||||
return sql.endsWith("\n") ? sql : sql + "\n";
|
||||
}
|
||||
|
||||
private List<SysProjectModule> getProjectModules(GenProject project) {
|
||||
if (project.isFrontDraft()) {
|
||||
return project.getModules() == null ? new ArrayList<SysProjectModule>() : project.getModules();
|
||||
@@ -275,18 +399,45 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
templateFilter.setTemplateType(type);
|
||||
List<Template> templates = templateService.selectTemplateList(templateFilter);
|
||||
if (StringUtils.isNotEmpty(templates)) {
|
||||
Template firstActiveTemplate = null;
|
||||
Long preferredTemplateId = preferredRunnableTemplateId(type);
|
||||
for (Template template : templates) {
|
||||
if (template != null && template.getTemplateId() != null && Long.valueOf(0L).equals(template.getTemplateStatus())) {
|
||||
if (preferredTemplateId != null && preferredTemplateId.equals(template.getTemplateId())) {
|
||||
return toProjectTemplate(projectId, type, template);
|
||||
}
|
||||
if (firstActiveTemplate == null) {
|
||||
firstActiveTemplate = template;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (firstActiveTemplate != null) {
|
||||
return toProjectTemplate(projectId, type, firstActiveTemplate);
|
||||
}
|
||||
}
|
||||
throw new ServiceException("未配置可用模板:" + type);
|
||||
}
|
||||
|
||||
private Long preferredRunnableTemplateId(String type) {
|
||||
if ("backend".equals(type)) {
|
||||
return RUNNABLE_BACKEND_TEMPLATE_ID;
|
||||
}
|
||||
if ("admin_frontend".equals(type)) {
|
||||
return RUNNABLE_ADMIN_FRONTEND_TEMPLATE_ID;
|
||||
}
|
||||
if ("frontend".equals(type)) {
|
||||
return RUNNABLE_FRONTEND_TEMPLATE_ID;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private SysProjectTemplate toProjectTemplate(Long projectId, String type, Template template) {
|
||||
SysProjectTemplate fallbackTemplate = new SysProjectTemplate();
|
||||
fallbackTemplate.setProjectId(projectId);
|
||||
fallbackTemplate.setTemplateId(template.getTemplateId());
|
||||
fallbackTemplate.setTemplateType(type);
|
||||
return fallbackTemplate;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new ServiceException("未配置可用模板:" + type);
|
||||
}
|
||||
|
||||
private List<SysProjectStructure> selectStructureNodes(GenProject project, SysProjectTemplate projectTemplate) {
|
||||
if (project.isFrontDraft()) {
|
||||
@@ -341,7 +492,10 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
}
|
||||
String result = nodeName;
|
||||
result = result.replace("{projectName}", StringUtils.defaultString(project.getProjectFileName()));
|
||||
result = result.replace("{packageName}", StringUtils.defaultString(project.getPackageName()));
|
||||
result = result.replace("{projectClassName}", toProjectClassName(project.getProjectFileName()));
|
||||
String packageName = StringUtils.defaultString(project.getPackageName());
|
||||
result = result.replace("{packageName}", packageName);
|
||||
result = result.replace("{packagePath}", packageName.replace('.', '/'));
|
||||
if (table != null) {
|
||||
result = result.replace("{moduleName}", StringUtils.defaultString(table.getModuleName()));
|
||||
result = result.replace("{ClassName}", StringUtils.defaultString(table.getClassName()));
|
||||
@@ -352,6 +506,28 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
return result;
|
||||
}
|
||||
|
||||
private String toProjectClassName(String projectFileName) {
|
||||
String source = StringUtils.defaultString(projectFileName);
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean uppercaseNext = true;
|
||||
for (int i = 0; i < source.length(); i++) {
|
||||
char ch = source.charAt(i);
|
||||
if (!Character.isLetterOrDigit(ch)) {
|
||||
uppercaseNext = true;
|
||||
continue;
|
||||
}
|
||||
if (result.length() == 0 && !Character.isJavaIdentifierStart(ch)) {
|
||||
result.append("App");
|
||||
}
|
||||
result.append(uppercaseNext ? Character.toUpperCase(ch) : ch);
|
||||
uppercaseNext = false;
|
||||
}
|
||||
if (result.length() == 0) {
|
||||
return "GeneratedProject";
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private Map<String, Object> mergeTableScopedFolderNode(Map<String, Map<String, Object>> mergedFolderMap,
|
||||
SysProjectStructure node, GenProject project, GenTable table,
|
||||
Map<Long, SysProjectStructure> sourceNodeMap,
|
||||
@@ -425,6 +601,81 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
nodes.add(nodeData);
|
||||
}
|
||||
|
||||
private void appendBusinessBlockFiles(List<Map<String, Object>> structure, GenProject project, String type) {
|
||||
List<GeneratedBusinessBlockFile> files = businessBlockService().listFiles(project, type);
|
||||
if (StringUtils.isEmpty(files)) {
|
||||
return;
|
||||
}
|
||||
for (GeneratedBusinessBlockFile file : files) {
|
||||
addGeneratedFileNode(structure, file);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void addGeneratedFileNode(List<Map<String, Object>> structure, GeneratedBusinessBlockFile file) {
|
||||
if (file == null || StringUtils.isEmpty(file.getOutputPath())) {
|
||||
return;
|
||||
}
|
||||
List<Map<String, Object>> current = generatedFileRoot(structure);
|
||||
String[] parts = file.getOutputPath().replace('\\', '/').split("/");
|
||||
StringBuilder path = new StringBuilder();
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
String part = parts[i];
|
||||
if (StringUtils.isEmpty(part)) {
|
||||
continue;
|
||||
}
|
||||
if (path.length() > 0) {
|
||||
path.append("/");
|
||||
}
|
||||
path.append(part);
|
||||
boolean last = i == parts.length - 1;
|
||||
if (last) {
|
||||
if (findChildNode(current, part, "file") == null) {
|
||||
Map<String, Object> fileNode = new LinkedHashMap<String, Object>();
|
||||
fileNode.put("id", file.getCategory());
|
||||
fileNode.put("name", part);
|
||||
fileNode.put("type", "file");
|
||||
fileNode.put("category", file.getCategory());
|
||||
fileNode.put("tableId", null);
|
||||
current.add(fileNode);
|
||||
}
|
||||
} else {
|
||||
Map<String, Object> folderNode = findChildNode(current, part, "folder");
|
||||
if (folderNode == null) {
|
||||
folderNode = new LinkedHashMap<String, Object>();
|
||||
folderNode.put("id", "business-block-folder:" + path.toString());
|
||||
folderNode.put("name", part);
|
||||
folderNode.put("type", "folder");
|
||||
folderNode.put("category", null);
|
||||
folderNode.put("tableId", null);
|
||||
folderNode.put("children", new ArrayList<Map<String, Object>>());
|
||||
current.add(folderNode);
|
||||
}
|
||||
current = (List<Map<String, Object>>) folderNode.get("children");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Map<String, Object>> generatedFileRoot(List<Map<String, Object>> structure) {
|
||||
if (structure.size() == 1 && "folder".equals(structure.get(0).get("type"))) {
|
||||
Object children = structure.get(0).get("children");
|
||||
if (children instanceof List) {
|
||||
return (List<Map<String, Object>>) children;
|
||||
}
|
||||
}
|
||||
return structure;
|
||||
}
|
||||
|
||||
private Map<String, Object> findChildNode(List<Map<String, Object>> nodes, String name, String type) {
|
||||
for (Map<String, Object> node : nodes) {
|
||||
if (StringUtils.equals(name, (String) node.get("name")) && StringUtils.equals(type, (String) node.get("type"))) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean matchesTemplateFile(TemplateFile templateFile, String templateFileName) {
|
||||
if (templateFile == null || StringUtils.isEmpty(templateFileName)) {
|
||||
return false;
|
||||
@@ -436,10 +687,74 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
private String renderTemplate(GenProject project, Long tableId, List<SysProjectModule> modules, TemplateFile templateFile) {
|
||||
VelocityContext context = prepareVelocityContext(project, tableId, modules, templateFile);
|
||||
StringWriter sw = new StringWriter();
|
||||
Velocity.evaluate(context, sw, "", StringUtils.defaultString(templateFile.getFileContent()));
|
||||
Velocity.evaluate(context, sw, "", resolveTemplateContent(templateFile));
|
||||
return sw.toString();
|
||||
}
|
||||
|
||||
private String resolveTemplateContent(TemplateFile templateFile) {
|
||||
String content = StringUtils.defaultString(templateFile.getFileContent());
|
||||
if (shouldUseBundledBackendControllerTemplate(templateFile, content)) {
|
||||
String bundledContent = readClasspathTemplate(BUNDLED_BACKEND_CONTROLLER_TEMPLATE);
|
||||
if (StringUtils.isNotEmpty(bundledContent)) {
|
||||
return bundledContent;
|
||||
}
|
||||
}
|
||||
if (shouldUseBundledFrontendIndexTemplate(templateFile, content)) {
|
||||
String bundledContent = readClasspathTemplate(BUNDLED_FRONTEND_INDEX_TEMPLATE);
|
||||
if (StringUtils.isNotEmpty(bundledContent)) {
|
||||
return bundledContent;
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
private boolean shouldUseBundledBackendControllerTemplate(TemplateFile templateFile, String content) {
|
||||
if (templateFile == null || !RUNNABLE_BACKEND_TEMPLATE_ID.equals(templateFile.getTemplateId())) {
|
||||
return false;
|
||||
}
|
||||
boolean isControllerTemplate = StringUtils.equals(BACKEND_CONTROLLER_TEMPLATE_FILE, templateFile.getFileName())
|
||||
|| StringUtils.equals(BACKEND_CONTROLLER_TEMPLATE_FILE, templateFile.getFilePath());
|
||||
String templateContent = StringUtils.defaultString(content);
|
||||
return isControllerTemplate && !templateContent.contains("__currentUserOnly");
|
||||
}
|
||||
|
||||
private boolean shouldUseBundledFrontendIndexTemplate(TemplateFile templateFile, String content) {
|
||||
if (templateFile == null || !RUNNABLE_FRONTEND_TEMPLATE_ID.equals(templateFile.getTemplateId())) {
|
||||
return false;
|
||||
}
|
||||
boolean isFrontendIndexTemplate = StringUtils.equals(FRONTEND_INDEX_TEMPLATE_FILE, templateFile.getFileName())
|
||||
|| StringUtils.equals(FRONTEND_INDEX_TEMPLATE_FILE, templateFile.getFilePath());
|
||||
String templateContent = StringUtils.defaultString(content);
|
||||
return isFrontendIndexTemplate && (!templateContent.contains("$hasCurrentFrontendPageDesign")
|
||||
|| !templateContent.contains("businessActionMap")
|
||||
|| !templateContent.contains("$renderForm")
|
||||
|| !templateContent.contains("handleView(row)")
|
||||
|| !templateContent.contains("__currentUserOnly"));
|
||||
}
|
||||
|
||||
private String readClasspathTemplate(String resourceName) {
|
||||
InputStream inputStream = GenProjectServiceImpl.class.getClassLoader().getResourceAsStream(resourceName);
|
||||
if (inputStream == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[4096];
|
||||
int length;
|
||||
while ((length = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, length);
|
||||
}
|
||||
return new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
} finally {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private VelocityContext prepareVelocityContext(GenProject project, Long tableId, List<SysProjectModule> modules, TemplateFile templateFile) {
|
||||
if (tableId != null && tableId != -1) {
|
||||
GenTable table = getRequiredTable(project, tableId);
|
||||
@@ -448,6 +763,13 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
setSubTable(table);
|
||||
setPkColumn(table);
|
||||
VelocityContext context = VelocityUtils.prepareContext(table);
|
||||
String pageDesignScope = RUNNABLE_ADMIN_FRONTEND_TEMPLATE_ID.equals(templateFile.getTemplateId())
|
||||
? "admin" : "frontend";
|
||||
VelocityUtils.putPageDesignVelocityContext(context, project, table, pageDesignScope);
|
||||
if ("frontend".equals(pageDesignScope)) {
|
||||
businessBlockService().putEmbeddedBlockVelocityContext(context, project,
|
||||
(FrontProjectPageDesign) context.get("currentPageDesign"));
|
||||
}
|
||||
putTableBusinessActions(context, project, table);
|
||||
putSnippetVariables(context, table, modules, templateFile);
|
||||
return context;
|
||||
@@ -459,6 +781,29 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
List<BusinessActionDesign> tableBusinessActions = filterTableBusinessActions(project, table);
|
||||
context.put("tableBusinessActions", tableBusinessActions);
|
||||
context.put("hasTableBusinessActions", StringUtils.isNotEmpty(tableBusinessActions));
|
||||
Map<String, List<BusinessActionDesign>> slotBusinessActions = buildSlotBusinessActions(tableBusinessActions);
|
||||
context.put("slotBusinessActions", slotBusinessActions);
|
||||
context.put("hasSlotBusinessActions", !slotBusinessActions.isEmpty());
|
||||
List<BusinessActionDesign> listRowBusinessActions = filterTableSlotActions(project, table, tableBusinessActions, "list.rowActions");
|
||||
List<BusinessActionDesign> detailPrimaryBusinessActions = filterTableSlotActions(project, table, tableBusinessActions, "detail.primaryActions");
|
||||
context.put("listRowBusinessActions", listRowBusinessActions);
|
||||
context.put("hasListRowBusinessActions", StringUtils.isNotEmpty(listRowBusinessActions));
|
||||
context.put("detailPrimaryBusinessActions", detailPrimaryBusinessActions);
|
||||
context.put("hasDetailPrimaryBusinessActions", StringUtils.isNotEmpty(detailPrimaryBusinessActions));
|
||||
List<BusinessActionDesign> pageDesignCandidateActions = project == null || StringUtils.isEmpty(project.getBusinessActions())
|
||||
? Collections.<BusinessActionDesign>emptyList()
|
||||
: project.getBusinessActions();
|
||||
List<BusinessActionDesign> toolbarBusinessActions = filterPageDesignBusinessActions(
|
||||
pageDesignCandidateActions, context.get("pageDesignToolbarActionCodes"));
|
||||
List<BusinessActionDesign> rowBusinessActions = filterPageDesignBusinessActions(
|
||||
pageDesignCandidateActions, context.get("pageDesignRowActionCodes"));
|
||||
List<BusinessActionDesign> pageDesignBusinessActions = mergeBusinessActions(toolbarBusinessActions, rowBusinessActions);
|
||||
context.put("pageDesignToolbarBusinessActions", toolbarBusinessActions);
|
||||
context.put("hasPageDesignToolbarBusinessActions", StringUtils.isNotEmpty(toolbarBusinessActions));
|
||||
context.put("pageDesignRowBusinessActions", rowBusinessActions);
|
||||
context.put("hasPageDesignRowBusinessActions", StringUtils.isNotEmpty(rowBusinessActions));
|
||||
context.put("pageDesignBusinessActions", pageDesignBusinessActions);
|
||||
context.put("hasPageDesignBusinessActions", StringUtils.isNotEmpty(pageDesignBusinessActions));
|
||||
}
|
||||
|
||||
private List<BusinessActionDesign> filterTableBusinessActions(GenProject project, GenTable table) {
|
||||
@@ -474,6 +819,134 @@ public class GenProjectServiceImpl implements IGenProjectService {
|
||||
return tableBusinessActions;
|
||||
}
|
||||
|
||||
private Map<String, List<BusinessActionDesign>> buildSlotBusinessActions(List<BusinessActionDesign> actions) {
|
||||
Map<String, List<BusinessActionDesign>> slotActions = new LinkedHashMap<String, List<BusinessActionDesign>>();
|
||||
if (StringUtils.isEmpty(actions)) {
|
||||
return slotActions;
|
||||
}
|
||||
for (BusinessActionDesign action : actions) {
|
||||
if (action == null || StringUtils.isEmpty(action.getUiBindings())) {
|
||||
continue;
|
||||
}
|
||||
for (BusinessActionUiBindingDesign binding : action.getUiBindings()) {
|
||||
if (binding == null || StringUtils.isEmpty(binding.getPageCode()) || StringUtils.isEmpty(binding.getSlot())) {
|
||||
continue;
|
||||
}
|
||||
String key = binding.getPageCode() + ":" + binding.getSlot();
|
||||
List<BusinessActionDesign> grouped = slotActions.get(key);
|
||||
if (grouped == null) {
|
||||
grouped = new ArrayList<BusinessActionDesign>();
|
||||
slotActions.put(key, grouped);
|
||||
}
|
||||
grouped.add(action);
|
||||
}
|
||||
}
|
||||
return slotActions;
|
||||
}
|
||||
|
||||
private List<BusinessActionDesign> filterPageDesignBusinessActions(List<BusinessActionDesign> tableBusinessActions,
|
||||
Object actionCodesObject) {
|
||||
if (StringUtils.isEmpty(tableBusinessActions) || !(actionCodesObject instanceof List)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Map<String, BusinessActionDesign> actionsByCode = new LinkedHashMap<String, BusinessActionDesign>();
|
||||
for (BusinessActionDesign action : tableBusinessActions) {
|
||||
if (action != null && StringUtils.isNotEmpty(action.getCode())) {
|
||||
actionsByCode.put(action.getCode(), action);
|
||||
}
|
||||
}
|
||||
List<BusinessActionDesign> result = new ArrayList<BusinessActionDesign>();
|
||||
for (Object codeObject : (List<?>) actionCodesObject) {
|
||||
String rawCode = StringUtils.defaultString(String.valueOf(codeObject));
|
||||
if (StringUtils.isEmpty(rawCode)) {
|
||||
continue;
|
||||
}
|
||||
boolean explicitBusinessAction = rawCode.startsWith("business:");
|
||||
String actionCode = explicitBusinessAction ? rawCode.substring("business:".length()) : rawCode;
|
||||
if (!explicitBusinessAction && isBuiltinPageAction(actionCode)) {
|
||||
continue;
|
||||
}
|
||||
BusinessActionDesign action = actionsByCode.get(actionCode);
|
||||
if (action != null && !result.contains(action)) {
|
||||
result.add(action);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<BusinessActionDesign> mergeBusinessActions(List<BusinessActionDesign> first, List<BusinessActionDesign> second) {
|
||||
Map<String, BusinessActionDesign> merged = new LinkedHashMap<String, BusinessActionDesign>();
|
||||
for (BusinessActionDesign action : first) {
|
||||
if (action != null && StringUtils.isNotEmpty(action.getCode())) {
|
||||
merged.put(action.getCode(), action);
|
||||
}
|
||||
}
|
||||
for (BusinessActionDesign action : second) {
|
||||
if (action != null && StringUtils.isNotEmpty(action.getCode())) {
|
||||
merged.put(action.getCode(), action);
|
||||
}
|
||||
}
|
||||
return new ArrayList<BusinessActionDesign>(merged.values());
|
||||
}
|
||||
|
||||
private boolean isBuiltinPageAction(String actionCode) {
|
||||
String normalized = StringUtils.defaultString(actionCode).toLowerCase();
|
||||
return "create".equals(normalized) || "add".equals(normalized)
|
||||
|| "batchdelete".equals(normalized) || "batch_delete".equals(normalized)
|
||||
|| "view".equals(normalized) || "detail".equals(normalized)
|
||||
|| "edit".equals(normalized) || "update".equals(normalized)
|
||||
|| "delete".equals(normalized) || "remove".equals(normalized);
|
||||
}
|
||||
|
||||
private List<BusinessActionDesign> filterTableSlotActions(GenProject project, GenTable table,
|
||||
List<BusinessActionDesign> tableBusinessActions, String slot) {
|
||||
if (StringUtils.isEmpty(tableBusinessActions)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<BusinessActionDesign> slotActions = new ArrayList<BusinessActionDesign>();
|
||||
for (BusinessActionDesign action : tableBusinessActions) {
|
||||
if (action == null || StringUtils.isEmpty(action.getUiBindings())) {
|
||||
continue;
|
||||
}
|
||||
for (BusinessActionUiBindingDesign binding : action.getUiBindings()) {
|
||||
if (binding != null && StringUtils.equals(slot, binding.getSlot())
|
||||
&& bindingMatchesTablePage(project, table, binding, slot)) {
|
||||
slotActions.add(action);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotEmpty(slotActions)) {
|
||||
return slotActions;
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private boolean bindingMatchesTablePage(GenProject project, GenTable table, BusinessActionUiBindingDesign binding, String slot) {
|
||||
if (project == null || table == null || binding == null) {
|
||||
return false;
|
||||
}
|
||||
if (StringUtils.isEmpty(project.getFrontendPages())) {
|
||||
return true;
|
||||
}
|
||||
for (AppPageDesign page : project.getFrontendPages()) {
|
||||
if (page == null || !StringUtils.equals(page.getCode(), binding.getPageCode())) {
|
||||
continue;
|
||||
}
|
||||
if (!StringUtils.equals(page.getTableName(), table.getTableName())) {
|
||||
return false;
|
||||
}
|
||||
if (slot != null && slot.startsWith("detail.")) {
|
||||
return "detail".equals(page.getPageType());
|
||||
}
|
||||
if (slot != null && slot.startsWith("list.")) {
|
||||
return "list".equals(page.getPageType()) || "current_user_list".equals(page.getPageType());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private GenTable getRequiredTable(GenProject project, Long tableId) {
|
||||
if (project != null && StringUtils.isNotEmpty(project.getTables())) {
|
||||
for (GenTable projectTable : project.getTables()) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,11 +2,17 @@ package com.ruoyi.generator.util;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import com.ruoyi.generator.domain.*;
|
||||
import com.ruoyi.generator.domain.front.FrontProjectPageDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.AppMenuDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.AppPageDesign;
|
||||
import org.apache.velocity.VelocityContext;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
@@ -41,6 +47,7 @@ public class VelocityUtils {
|
||||
* @return 模板列表
|
||||
*/
|
||||
public static VelocityContext prepareContext(GenTable genTable) {
|
||||
TableOperationResolver.applyDefaults(genTable);
|
||||
String moduleName = genTable.getModuleName();
|
||||
String businessName = genTable.getBusinessName();
|
||||
String packageName = genTable.getPackageName();
|
||||
@@ -65,7 +72,20 @@ public class VelocityUtils {
|
||||
velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName));
|
||||
velocityContext.put("columns", genTable.getColumns());
|
||||
velocityContext.put("table", genTable);
|
||||
velocityContext.put("adminMenuName", genTable.getAdminMenuName());
|
||||
velocityContext.put("allowAdd", genTable.getAllowAdd());
|
||||
velocityContext.put("allowEdit", genTable.getAllowEdit());
|
||||
velocityContext.put("allowRemove", genTable.getAllowRemove());
|
||||
velocityContext.put("removeType", genTable.getRemoveType());
|
||||
velocityContext.put("operationNote", genTable.getOperationNote());
|
||||
velocityContext.put("dicts", getDicts(genTable));
|
||||
List<Map<String, String>> qingRemoteSelects = getQingRemoteSelects(genTable);
|
||||
velocityContext.put("qingRemoteSelects", qingRemoteSelects);
|
||||
velocityContext.put("qingRemoteSelectFields", getQingRemoteSelectFields(qingRemoteSelects));
|
||||
GenTableColumn qingCurrentUserColumn = getQingCurrentUserColumn(genTable);
|
||||
velocityContext.put("qingCurrentUserColumn", qingCurrentUserColumn);
|
||||
velocityContext.put("hasQingCurrentUserColumn", qingCurrentUserColumn != null);
|
||||
putPageDesignDefaults(velocityContext);
|
||||
setMenuVelocityContext(velocityContext, genTable);
|
||||
if (GenConstants.TPL_TREE.equals(tplCategory)) {
|
||||
setTreeVelocityContext(velocityContext, genTable);
|
||||
@@ -124,9 +144,20 @@ public class VelocityUtils {
|
||||
*/
|
||||
public static VelocityContext prepareContextProject(GenProject genProject) {
|
||||
VelocityContext velocityContext = new VelocityContext();
|
||||
velocityContext.put("packageName", genProject.getPackageName());
|
||||
velocityContext.put("projectName", genProject.getProjectName());
|
||||
velocityContext.put("projectFileName", genProject.getProjectFileName());
|
||||
String packageName = StringUtils.defaultString(genProject.getPackageName());
|
||||
String projectName = StringUtils.defaultString(genProject.getProjectName());
|
||||
String projectFileName = StringUtils.defaultString(genProject.getProjectFileName());
|
||||
String projectClassName = toProjectClassName(projectFileName);
|
||||
|
||||
velocityContext.put("packageName", packageName);
|
||||
velocityContext.put("projectName", projectName);
|
||||
velocityContext.put("projectFileName", projectFileName);
|
||||
velocityContext.put("projectClassName", projectClassName);
|
||||
velocityContext.put("functionName", StringUtils.isNotEmpty(projectName) ? projectName : projectFileName);
|
||||
velocityContext.put("businessName", StringUtils.isNotEmpty(projectFileName) ? projectFileName : projectName);
|
||||
velocityContext.put("BusinessName", projectClassName);
|
||||
velocityContext.put("ClassName", projectClassName);
|
||||
velocityContext.put("className", StringUtils.uncapitalize(projectClassName));
|
||||
List<GenTable> tables = genProject.getTables() == null
|
||||
? Collections.<GenTable>emptyList() : genProject.getTables();
|
||||
velocityContext.put("tables", tables);
|
||||
@@ -138,17 +169,65 @@ public class VelocityUtils {
|
||||
? Collections.emptyList() : genProject.getFrontendMenus();
|
||||
List<?> adminMenus = genProject.getAdminMenus() == null
|
||||
? Collections.emptyList() : genProject.getAdminMenus();
|
||||
List<FrontProjectPageDesign> pageDesigns = genProject.getPageDesigns() == null
|
||||
? Collections.<FrontProjectPageDesign>emptyList() : genProject.getPageDesigns();
|
||||
List<FrontProjectPageDesign> frontendPageDesigns = new ArrayList<FrontProjectPageDesign>();
|
||||
List<FrontProjectPageDesign> adminPageDesigns = new ArrayList<FrontProjectPageDesign>();
|
||||
Map<String, FrontProjectPageDesign> pageDesignsByCode = new LinkedHashMap<String, FrontProjectPageDesign>();
|
||||
Map<String, FrontProjectPageDesign> pageDesignsByTable = new LinkedHashMap<String, FrontProjectPageDesign>();
|
||||
Map<String, FrontProjectPageDesign> adminPageDesignsByCode = new LinkedHashMap<String, FrontProjectPageDesign>();
|
||||
Map<String, FrontProjectPageDesign> adminPageDesignsByTable = new LinkedHashMap<String, FrontProjectPageDesign>();
|
||||
for (FrontProjectPageDesign pageDesign : pageDesigns) {
|
||||
if (pageDesign == null) {
|
||||
continue;
|
||||
}
|
||||
enrichPageDesign(pageDesign);
|
||||
boolean adminScope = "admin".equals(pageDesign.getPageScope());
|
||||
List<FrontProjectPageDesign> scopedDesigns = adminScope ? adminPageDesigns : frontendPageDesigns;
|
||||
Map<String, FrontProjectPageDesign> scopedByCode = adminScope ? adminPageDesignsByCode : pageDesignsByCode;
|
||||
Map<String, FrontProjectPageDesign> scopedByTable = adminScope ? adminPageDesignsByTable : pageDesignsByTable;
|
||||
scopedDesigns.add(pageDesign);
|
||||
if (StringUtils.isNotEmpty(pageDesign.getPageCode())) {
|
||||
scopedByCode.put(pageDesign.getPageCode(), pageDesign);
|
||||
}
|
||||
if (StringUtils.isNotEmpty(pageDesign.getTableName()) && !scopedByTable.containsKey(pageDesign.getTableName())) {
|
||||
scopedByTable.put(pageDesign.getTableName(), pageDesign);
|
||||
}
|
||||
}
|
||||
List<AppPageDesign> blueprintFrontendPages = genProject.getFrontendPages() == null
|
||||
? Collections.<AppPageDesign>emptyList() : genProject.getFrontendPages();
|
||||
List<AppPageDesign> frontendPages = mergeFrontendPageDesigns(blueprintFrontendPages, frontendPageDesigns);
|
||||
List<Map<String, Object>> frontendNavItems = buildFrontendNavItems(
|
||||
genProject.getFrontendMenus(), frontendPages, tables, pageDesignsByCode);
|
||||
List<Map<String, Object>> frontendRouteItems = buildFrontendRouteItems(
|
||||
frontendNavItems, frontendPages, tables, pageDesignsByCode);
|
||||
List<Map<String, Object>> frontendRouteComponents = buildFrontendRouteComponents(frontendRouteItems);
|
||||
velocityContext.put("appRoles", appRoles);
|
||||
velocityContext.put("frontendMenus", frontendMenus);
|
||||
velocityContext.put("adminMenus", adminMenus);
|
||||
velocityContext.put("frontendPages", frontendPages);
|
||||
velocityContext.put("frontendNavItems", frontendNavItems);
|
||||
velocityContext.put("frontendRouteItems", frontendRouteItems);
|
||||
velocityContext.put("frontendRouteComponents", frontendRouteComponents);
|
||||
velocityContext.put("defaultFrontendPath", resolveDefaultFrontendPath(frontendNavItems, frontendRouteItems));
|
||||
velocityContext.put("hasAppBlueprint", genProject.getAppBlueprint() != null);
|
||||
velocityContext.put("hasAppRoles", StringUtils.isNotEmpty(appRoles));
|
||||
velocityContext.put("hasFrontendMenus", StringUtils.isNotEmpty(frontendMenus));
|
||||
velocityContext.put("hasFrontendNavItems", StringUtils.isNotEmpty(frontendNavItems));
|
||||
velocityContext.put("hasFrontendRouteItems", StringUtils.isNotEmpty(frontendRouteItems));
|
||||
velocityContext.put("hasAdminMenus", StringUtils.isNotEmpty(adminMenus));
|
||||
List<?> businessActions = genProject.getBusinessActions() == null
|
||||
? Collections.emptyList() : genProject.getBusinessActions();
|
||||
velocityContext.put("businessActions", businessActions);
|
||||
velocityContext.put("hasBusinessActions", StringUtils.isNotEmpty(businessActions));
|
||||
velocityContext.put("frontendPageDesigns", frontendPageDesigns);
|
||||
velocityContext.put("frontendPageDesignsByCode", pageDesignsByCode);
|
||||
velocityContext.put("frontendPageDesignsByTable", pageDesignsByTable);
|
||||
velocityContext.put("hasFrontendPageDesigns", StringUtils.isNotEmpty(frontendPageDesigns));
|
||||
velocityContext.put("adminPageDesigns", adminPageDesigns);
|
||||
velocityContext.put("adminPageDesignsByCode", adminPageDesignsByCode);
|
||||
velocityContext.put("adminPageDesignsByTable", adminPageDesignsByTable);
|
||||
velocityContext.put("hasAdminPageDesigns", StringUtils.isNotEmpty(adminPageDesigns));
|
||||
StringBuffer createTableSql = new StringBuffer();
|
||||
for (GenTable table : tables) {
|
||||
createTableSql.append("\n");
|
||||
@@ -159,6 +238,779 @@ public class VelocityUtils {
|
||||
return velocityContext;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void enrichPageDesign(FrontProjectPageDesign pageDesign) {
|
||||
if (pageDesign.getLayout() == null && StringUtils.isNotEmpty(pageDesign.getLayoutJson())) {
|
||||
try {
|
||||
pageDesign.setLayout(JSON.parseObject(pageDesign.getLayoutJson(), Map.class));
|
||||
} catch (RuntimeException e) {
|
||||
pageDesign.setLayout(Collections.<String, Object>emptyMap());
|
||||
}
|
||||
}
|
||||
if (pageDesign.getAction() == null && StringUtils.isNotEmpty(pageDesign.getActionJson())) {
|
||||
try {
|
||||
pageDesign.setAction(JSON.parseObject(pageDesign.getActionJson(), Map.class));
|
||||
} catch (RuntimeException e) {
|
||||
pageDesign.setAction(Collections.<String, Object>emptyMap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<AppPageDesign> mergeFrontendPageDesigns(List<AppPageDesign> pages,
|
||||
List<FrontProjectPageDesign> pageDesigns) {
|
||||
Map<String, AppPageDesign> pagesByCode = new LinkedHashMap<String, AppPageDesign>();
|
||||
if (StringUtils.isNotEmpty(pages)) {
|
||||
for (AppPageDesign page : pages) {
|
||||
if (page == null || StringUtils.isEmpty(page.getCode())) {
|
||||
continue;
|
||||
}
|
||||
AppPageDesign copy = copyPageDesign(page);
|
||||
pagesByCode.put(copy.getCode(), copy);
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotEmpty(pageDesigns)) {
|
||||
for (FrontProjectPageDesign pageDesign : pageDesigns) {
|
||||
if (pageDesign == null || StringUtils.isEmpty(pageDesign.getPageCode())
|
||||
|| "admin".equals(pageDesign.getPageScope())) {
|
||||
continue;
|
||||
}
|
||||
AppPageDesign page = pagesByCode.get(pageDesign.getPageCode());
|
||||
if (page == null) {
|
||||
page = new AppPageDesign();
|
||||
page.setCode(pageDesign.getPageCode());
|
||||
pagesByCode.put(page.getCode(), page);
|
||||
}
|
||||
page.setMenuCode(StringUtils.defaultIfEmpty(pageDesign.getMenuCode(), page.getMenuCode()));
|
||||
page.setName(StringUtils.defaultIfEmpty(pageDesign.getPageName(), page.getName()));
|
||||
page.setPath(StringUtils.defaultIfEmpty(pageDesign.getRoutePath(), page.getPath()));
|
||||
page.setPageType(isFrontendBusinessBlockPageDesign(pageDesign)
|
||||
? "business_block" : StringUtils.defaultIfEmpty(pageDesign.getPageType(), page.getPageType()));
|
||||
page.setTableName(StringUtils.defaultIfEmpty(pageDesign.getTableName(), page.getTableName()));
|
||||
}
|
||||
}
|
||||
return new ArrayList<AppPageDesign>(pagesByCode.values());
|
||||
}
|
||||
|
||||
private static AppPageDesign copyPageDesign(AppPageDesign source) {
|
||||
AppPageDesign copy = new AppPageDesign();
|
||||
copy.setCode(source.getCode());
|
||||
copy.setMenuCode(source.getMenuCode());
|
||||
copy.setName(source.getName());
|
||||
copy.setPath(source.getPath());
|
||||
copy.setPageType(source.getPageType());
|
||||
copy.setTableName(source.getTableName());
|
||||
copy.setParentPageCode(source.getParentPageCode());
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static boolean isFrontendBusinessBlockPageDesign(FrontProjectPageDesign pageDesign) {
|
||||
if (pageDesign == null || StringUtils.isEmpty(pageDesign.getPageCode()) || "admin".equals(pageDesign.getPageScope())) {
|
||||
return false;
|
||||
}
|
||||
if ("business_block".equals(pageDesign.getPageType())) {
|
||||
return true;
|
||||
}
|
||||
return StringUtils.isNotEmpty(pageDesign.getLayoutJson()) && pageDesign.getLayoutJson().contains("business-blocks-v1");
|
||||
}
|
||||
|
||||
public static void putPageDesignVelocityContext(VelocityContext context, GenProject project, GenTable table) {
|
||||
putPageDesignVelocityContext(context, project, table, "frontend");
|
||||
}
|
||||
|
||||
public static void putPageDesignVelocityContext(VelocityContext context, GenProject project, GenTable table, String scope) {
|
||||
putPageDesignVelocityContext(context, project, table, scope, null);
|
||||
}
|
||||
|
||||
public static void putPageDesignVelocityContext(VelocityContext context, GenProject project, GenTable table,
|
||||
String scope, String pageCode) {
|
||||
FrontProjectPageDesign pageDesign = findPageDesign(project, table, scope, pageCode);
|
||||
putPageDesignDefaults(context);
|
||||
context.put("currentFrontendPageDesign", pageDesign);
|
||||
context.put("currentPageDesign", pageDesign);
|
||||
context.put("hasCurrentFrontendPageDesign", pageDesign != null);
|
||||
context.put("hasCurrentPageDesign", pageDesign != null);
|
||||
if (pageDesign == null) {
|
||||
return;
|
||||
}
|
||||
enrichPageDesign(pageDesign);
|
||||
String canvas = resolvePageDesignCanvas(pageDesign);
|
||||
context.put("pageDesignCanvas", canvas);
|
||||
context.put("pageDesignIsCardList", canvas.contains("card"));
|
||||
context.put("pageDesignIsSplitList", canvas.contains("split"));
|
||||
context.put("pageDesignIsCurrentUserList", "current_user_list".equals(pageDesign.getPageType()));
|
||||
Map<String, GenTableColumn> columnsByField = buildColumnsByField(table);
|
||||
Map<String, Object> queryRegion = findLayoutRegion(pageDesign, "query");
|
||||
Map<String, Object> toolbarRegion = findLayoutRegion(pageDesign, "toolbar");
|
||||
Map<String, Object> tableRegion = findLayoutRegion(pageDesign, "table");
|
||||
Map<String, Object> cardRegion = findLayoutRegion(pageDesign, "cardList");
|
||||
if (cardRegion == null) {
|
||||
cardRegion = findLayoutRegion(pageDesign, "card");
|
||||
}
|
||||
Map<String, Object> cardActionsRegion = findLayoutRegion(pageDesign, "cardActions");
|
||||
Map<String, Object> formRegion = findLayoutRegion(pageDesign, "dialogForm");
|
||||
if (formRegion == null) {
|
||||
formRegion = findLayoutRegion(pageDesign, "form");
|
||||
}
|
||||
List<String> toolbarActionCodes = stringList(toolbarRegion == null ? null : toolbarRegion.get("actions"));
|
||||
List<String> rowActionCodes = stringList(tableRegion == null ? null : tableRegion.get("rowActions"));
|
||||
List<String> cardActionCodes = stringList(cardActionsRegion == null ? null : cardActionsRegion.get("actions"));
|
||||
if (cardActionCodes.isEmpty()) {
|
||||
cardActionCodes = rowActionCodes;
|
||||
}
|
||||
|
||||
context.put("pageDesignQueryColumns", columnsFromRegion(queryRegion, columnsByField));
|
||||
context.put("pageDesignTableColumns", columnsFromRegion(tableRegion, columnsByField));
|
||||
context.put("pageDesignFormColumns", columnsFromRegion(formRegion, columnsByField));
|
||||
context.put("pageDesignCardColumns", cardColumnsFromRegion(cardRegion, columnsByField));
|
||||
context.put("pageDesignCardTitleColumn", columnFromRegionField(cardRegion, "titleField", columnsByField));
|
||||
context.put("pageDesignCardImageColumn", columnFromRegionField(cardRegion, "imageField", columnsByField));
|
||||
context.put("pageDesignToolbarActionCodes", toolbarActionCodes);
|
||||
context.put("pageDesignRowActionCodes", pageDesign.getLayout() != null && canvas.contains("card") ? cardActionCodes : rowActionCodes);
|
||||
context.put("pageDesignShowQuery", regionVisible(queryRegion));
|
||||
context.put("pageDesignShowToolbar", regionVisible(toolbarRegion));
|
||||
context.put("pageDesignShowForm", regionVisible(formRegion));
|
||||
context.put("pageDesignAllowAdd", containsAny(toolbarActionCodes, "create", "add"));
|
||||
context.put("pageDesignAllowBatchDelete", containsAny(toolbarActionCodes, "batchDelete", "batch_delete"));
|
||||
context.put("pageDesignAllowView", containsAny(canvas.contains("card") ? cardActionCodes : rowActionCodes, "view", "detail"));
|
||||
context.put("pageDesignAllowEdit", containsAny(canvas.contains("card") ? cardActionCodes : rowActionCodes, "edit", "update"));
|
||||
context.put("pageDesignAllowDelete", containsAny(canvas.contains("card") ? cardActionCodes : rowActionCodes, "delete", "remove"));
|
||||
}
|
||||
|
||||
private static void putPageDesignDefaults(VelocityContext context) {
|
||||
context.put("currentFrontendPageDesign", null);
|
||||
context.put("currentPageDesign", null);
|
||||
context.put("hasCurrentFrontendPageDesign", false);
|
||||
context.put("hasCurrentPageDesign", false);
|
||||
context.put("pageDesignCanvas", "frontend-list-v1");
|
||||
context.put("pageDesignIsCardList", false);
|
||||
context.put("pageDesignIsSplitList", false);
|
||||
context.put("pageDesignIsCurrentUserList", false);
|
||||
context.put("pageDesignShowQuery", true);
|
||||
context.put("pageDesignShowToolbar", true);
|
||||
context.put("pageDesignShowForm", true);
|
||||
context.put("pageDesignCardColumns", Collections.<GenTableColumn>emptyList());
|
||||
context.put("pageDesignCardTitleColumn", null);
|
||||
context.put("pageDesignCardImageColumn", null);
|
||||
context.put("pageDesignToolbarActionCodes", Collections.<String>emptyList());
|
||||
context.put("pageDesignRowActionCodes", Collections.<String>emptyList());
|
||||
context.put("pageDesignAllowView", false);
|
||||
context.put("pageDesignAllowAdd", false);
|
||||
context.put("pageDesignAllowBatchDelete", false);
|
||||
context.put("pageDesignAllowEdit", false);
|
||||
context.put("pageDesignAllowDelete", false);
|
||||
context.put("pageDesignToolbarBusinessActions", Collections.emptyList());
|
||||
context.put("hasPageDesignToolbarBusinessActions", false);
|
||||
context.put("pageDesignRowBusinessActions", Collections.emptyList());
|
||||
context.put("hasPageDesignRowBusinessActions", false);
|
||||
context.put("pageDesignBusinessActions", Collections.emptyList());
|
||||
context.put("hasPageDesignBusinessActions", false);
|
||||
context.put("pageDesignTopBusinessBlocks", Collections.emptyList());
|
||||
context.put("pageDesignBottomBusinessBlocks", Collections.emptyList());
|
||||
context.put("pageDesignEmbeddedBusinessBlocks", Collections.emptyList());
|
||||
context.put("hasPageDesignTopBusinessBlocks", false);
|
||||
context.put("hasPageDesignBottomBusinessBlocks", false);
|
||||
context.put("hasPageDesignEmbeddedBusinessBlocks", false);
|
||||
}
|
||||
|
||||
private static String resolvePageDesignCanvas(FrontProjectPageDesign pageDesign) {
|
||||
if (pageDesign == null || pageDesign.getLayout() == null) {
|
||||
return "frontend-list-v1";
|
||||
}
|
||||
Object canvas = pageDesign.getLayout().get("canvas");
|
||||
return canvas == null || StringUtils.isEmpty(String.valueOf(canvas))
|
||||
? "frontend-list-v1" : String.valueOf(canvas);
|
||||
}
|
||||
|
||||
private static FrontProjectPageDesign findPageDesign(GenProject project, GenTable table, String scope) {
|
||||
return findPageDesign(project, table, scope, null);
|
||||
}
|
||||
|
||||
private static FrontProjectPageDesign findPageDesign(GenProject project, GenTable table, String scope, String pageCode) {
|
||||
if (project == null || table == null || StringUtils.isEmpty(project.getPageDesigns())) {
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isNotEmpty(pageCode)) {
|
||||
for (FrontProjectPageDesign pageDesign : project.getPageDesigns()) {
|
||||
if (pageDesign != null && pageDesignScopeMatches(pageDesign, scope)
|
||||
&& StringUtils.equals(pageCode, pageDesign.getPageCode())
|
||||
&& StringUtils.equals(pageDesign.getTableName(), table.getTableName())) {
|
||||
return pageDesign;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (FrontProjectPageDesign pageDesign : project.getPageDesigns()) {
|
||||
if (pageDesign == null) {
|
||||
continue;
|
||||
}
|
||||
if (pageDesignScopeMatches(pageDesign, scope) && StringUtils.equals(pageDesign.getTableName(), table.getTableName())) {
|
||||
return pageDesign;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean pageDesignScopeMatches(FrontProjectPageDesign pageDesign, String scope) {
|
||||
String expectedScope = "admin".equals(scope) ? "admin" : "frontend";
|
||||
String actualScope = StringUtils.isEmpty(pageDesign.getPageScope()) ? "frontend" : pageDesign.getPageScope();
|
||||
return expectedScope.equals(actualScope);
|
||||
}
|
||||
|
||||
private static Map<String, GenTableColumn> buildColumnsByField(GenTable table) {
|
||||
Map<String, GenTableColumn> result = new LinkedHashMap<String, GenTableColumn>();
|
||||
if (table == null || StringUtils.isEmpty(table.getColumns())) {
|
||||
return result;
|
||||
}
|
||||
for (GenTableColumn column : table.getColumns()) {
|
||||
if (column == null) {
|
||||
continue;
|
||||
}
|
||||
if (StringUtils.isNotEmpty(column.getJavaField())) {
|
||||
result.put(column.getJavaField(), column);
|
||||
}
|
||||
if (StringUtils.isNotEmpty(column.getColumnName())) {
|
||||
result.put(column.getColumnName(), column);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, Object> findLayoutRegion(FrontProjectPageDesign pageDesign, String type) {
|
||||
if (pageDesign == null || pageDesign.getLayout() == null) {
|
||||
return null;
|
||||
}
|
||||
Object regionsObject = pageDesign.getLayout().get("regions");
|
||||
if (!(regionsObject instanceof List)) {
|
||||
return null;
|
||||
}
|
||||
for (Object regionObject : (List<Object>) regionsObject) {
|
||||
if (!(regionObject instanceof Map)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> region = (Map<String, Object>) regionObject;
|
||||
Object regionType = region.get("type");
|
||||
Object regionId = region.get("id");
|
||||
if (type.equals(regionType) || type.equals(regionId)) {
|
||||
return region;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<GenTableColumn> columnsFromRegion(Map<String, Object> region, Map<String, GenTableColumn> columnsByField) {
|
||||
List<GenTableColumn> columns = new ArrayList<GenTableColumn>();
|
||||
if (region == null || columnsByField == null) {
|
||||
return columns;
|
||||
}
|
||||
for (String field : stringList(region.get("fields"))) {
|
||||
GenTableColumn column = columnsByField.get(field);
|
||||
if (column != null && !columns.contains(column)) {
|
||||
columns.add(column);
|
||||
}
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
private static List<GenTableColumn> cardColumnsFromRegion(Map<String, Object> region, Map<String, GenTableColumn> columnsByField) {
|
||||
List<GenTableColumn> columns = columnsFromRegion(region, columnsByField);
|
||||
GenTableColumn imageColumn = columnFromRegionField(region, "imageField", columnsByField);
|
||||
if (imageColumn != null) {
|
||||
columns.remove(imageColumn);
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
private static GenTableColumn columnFromRegionField(Map<String, Object> region, String key, Map<String, GenTableColumn> columnsByField) {
|
||||
if (region == null || columnsByField == null) {
|
||||
return null;
|
||||
}
|
||||
Object field = region.get(key);
|
||||
if (field == null) {
|
||||
return null;
|
||||
}
|
||||
return columnsByField.get(String.valueOf(field));
|
||||
}
|
||||
|
||||
private static boolean regionVisible(Map<String, Object> region) {
|
||||
if (region == null || !region.containsKey("visible")) {
|
||||
return true;
|
||||
}
|
||||
Object visible = region.get("visible");
|
||||
if (visible instanceof Boolean) {
|
||||
return ((Boolean) visible).booleanValue();
|
||||
}
|
||||
return !"false".equalsIgnoreCase(String.valueOf(visible));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<String> stringList(Object value) {
|
||||
List<String> result = new ArrayList<String>();
|
||||
if (!(value instanceof List)) {
|
||||
return result;
|
||||
}
|
||||
for (Object item : (List<Object>) value) {
|
||||
if (item != null) {
|
||||
result.add(String.valueOf(item));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean containsAny(List<String> values, String... expectedValues) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
Set<String> normalized = new HashSet<String>();
|
||||
for (String value : values) {
|
||||
normalized.add(StringUtils.defaultString(value).toLowerCase());
|
||||
}
|
||||
for (String expected : expectedValues) {
|
||||
if (normalized.contains(StringUtils.defaultString(expected).toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<Map<String, Object>> buildFrontendNavItems(List<AppMenuDesign> menus,
|
||||
List<AppPageDesign> pages,
|
||||
List<GenTable> tables,
|
||||
Map<String, FrontProjectPageDesign> pageDesignsByCode) {
|
||||
List<Map<String, Object>> items = new ArrayList<Map<String, Object>>();
|
||||
Set<String> paths = new HashSet<String>();
|
||||
if (StringUtils.isNotEmpty(menus)) {
|
||||
int index = 1;
|
||||
for (AppMenuDesign menu : menus) {
|
||||
if (menu == null) {
|
||||
continue;
|
||||
}
|
||||
AppPageDesign page = findMenuPage(menu, pages);
|
||||
if (page != null && !shouldShowPageInMenu(page, pageDesignsByCode)) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> item = frontendRouteItemForMenu(menu, page, pages, tables,
|
||||
pageDesignsByCode, index);
|
||||
if (item == null) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
addRouteItemIfAbsent(items, paths, item);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotEmpty(pages)) {
|
||||
int index = 1;
|
||||
for (AppPageDesign page : pages) {
|
||||
if (!shouldShowPageInMenu(page, pageDesignsByCode)) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> item = frontendRouteItemForPage(page, tables, true, pageDesignsByCode, index);
|
||||
addRouteItemIfAbsent(items, paths, item);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
if (items.isEmpty() && StringUtils.isEmpty(menus) && StringUtils.isEmpty(pages)) {
|
||||
items.addAll(buildTableRouteItems(tables));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private static List<Map<String, Object>> buildFrontendRouteItems(List<Map<String, Object>> navItems,
|
||||
List<AppPageDesign> pages,
|
||||
List<GenTable> tables,
|
||||
Map<String, FrontProjectPageDesign> pageDesignsByCode) {
|
||||
List<Map<String, Object>> items = new ArrayList<Map<String, Object>>();
|
||||
Set<String> paths = new HashSet<String>();
|
||||
for (Map<String, Object> navItem : navItems) {
|
||||
addRouteItemIfAbsent(items, paths, navItem);
|
||||
}
|
||||
if (StringUtils.isNotEmpty(pages)) {
|
||||
int index = 1;
|
||||
for (AppPageDesign page : pages) {
|
||||
Map<String, Object> pageItem = frontendRouteItemForPage(page, tables, true, pageDesignsByCode, index);
|
||||
addRouteItemIfAbsent(items, paths, pageItem);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
for (Map<String, Object> tableItem : buildTableRouteItems(tables)) {
|
||||
addRouteItemIfAbsent(items, paths, tableItem);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private static Map<String, Object> frontendRouteItemForMenu(AppMenuDesign menu, AppPageDesign page,
|
||||
List<AppPageDesign> pages, List<GenTable> tables,
|
||||
Map<String, FrontProjectPageDesign> pageDesignsByCode,
|
||||
int index) {
|
||||
if (page != null) {
|
||||
Map<String, Object> item = frontendRouteItemForPage(page, tables,
|
||||
Boolean.TRUE.equals(menu.getRequiresLogin()), pageDesignsByCode, index);
|
||||
if (item != null) {
|
||||
item.put("code", sanitizeRouteName(StringUtils.defaultIfEmpty(menu.getCode(), page.getCode()), null));
|
||||
item.put("name", StringUtils.defaultIfEmpty(menu.getName(), page.getName()));
|
||||
item.put("path", normalizeFrontendPath(StringUtils.defaultIfEmpty(menu.getPath(), page.getPath()),
|
||||
defaultPagePath(page)));
|
||||
}
|
||||
return item;
|
||||
}
|
||||
GenTable table = resolveMenuTable(menu, pages, tables);
|
||||
if (table == null) {
|
||||
return null;
|
||||
}
|
||||
return frontendRouteItem(
|
||||
StringUtils.defaultIfEmpty(menu.getCode(), "frontend_menu_" + index),
|
||||
StringUtils.defaultIfEmpty(menu.getName(), table.getFunctionName()),
|
||||
normalizeFrontendPath(menu.getPath(), defaultTablePath(table)),
|
||||
Boolean.TRUE.equals(menu.getRequiresLogin()),
|
||||
table);
|
||||
}
|
||||
|
||||
private static Map<String, Object> frontendRouteItemForPage(AppPageDesign page, List<GenTable> tables,
|
||||
boolean requiresLogin,
|
||||
Map<String, FrontProjectPageDesign> pageDesignsByCode,
|
||||
int index) {
|
||||
if (page == null) {
|
||||
return null;
|
||||
}
|
||||
if (isBusinessBlockPage(page)) {
|
||||
return frontendDesignedPageRouteItem(
|
||||
StringUtils.defaultIfEmpty(page.getCode(), "business_block_" + index),
|
||||
StringUtils.defaultIfEmpty(page.getName(), page.getCode()),
|
||||
normalizeFrontendPath(page.getPath(), defaultBusinessBlockPagePath(page)),
|
||||
requiresLogin,
|
||||
page);
|
||||
}
|
||||
GenTable table = findTableByName(tables, page.getTableName());
|
||||
if (hasPageDesign(page, pageDesignsByCode)) {
|
||||
return frontendDesignedPageRouteItem(
|
||||
StringUtils.defaultIfEmpty(page.getCode(), "frontend_page_" + index),
|
||||
StringUtils.defaultIfEmpty(page.getName(), table == null ? page.getCode() : table.getFunctionName()),
|
||||
normalizeFrontendPath(page.getPath(), table == null ? defaultPagePath(page) : defaultTablePath(table)),
|
||||
requiresLogin,
|
||||
page);
|
||||
}
|
||||
if (table == null) {
|
||||
return null;
|
||||
}
|
||||
return frontendRouteItem(
|
||||
StringUtils.defaultIfEmpty(page.getCode(), "frontend_page_" + index),
|
||||
StringUtils.defaultIfEmpty(page.getName(), table.getFunctionName()),
|
||||
normalizeFrontendPath(page.getPath(), defaultTablePath(table)),
|
||||
requiresLogin,
|
||||
table);
|
||||
}
|
||||
|
||||
private static boolean addRouteItemIfAbsent(List<Map<String, Object>> items, Set<String> paths,
|
||||
Map<String, Object> item) {
|
||||
if (item == null) {
|
||||
return false;
|
||||
}
|
||||
String path = StringUtils.defaultString((String) item.get("path"));
|
||||
if (paths.add(path)) {
|
||||
items.add(item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<Map<String, Object>> buildTableRouteItems(List<GenTable> tables) {
|
||||
List<Map<String, Object>> items = new ArrayList<Map<String, Object>>();
|
||||
if (StringUtils.isEmpty(tables)) {
|
||||
return items;
|
||||
}
|
||||
int index = 1;
|
||||
for (GenTable table : tables) {
|
||||
if (table == null) {
|
||||
continue;
|
||||
}
|
||||
items.add(frontendRouteItem(
|
||||
StringUtils.defaultIfEmpty(table.getBusinessName(), "table_" + index),
|
||||
StringUtils.defaultIfEmpty(table.getFunctionName(), table.getBusinessName()),
|
||||
defaultTablePath(table),
|
||||
true,
|
||||
table));
|
||||
index++;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private static List<Map<String, Object>> buildFrontendRouteComponents(List<Map<String, Object>> routeItems) {
|
||||
List<Map<String, Object>> components = new ArrayList<Map<String, Object>>();
|
||||
Set<String> componentNames = new HashSet<String>();
|
||||
for (Map<String, Object> routeItem : routeItems) {
|
||||
String componentName = (String) routeItem.get("componentName");
|
||||
if (StringUtils.isNotEmpty(componentName) && componentNames.add(componentName)) {
|
||||
Map<String, Object> component = new LinkedHashMap<String, Object>();
|
||||
component.put("componentName", componentName);
|
||||
component.put("componentPath", routeItem.get("componentPath"));
|
||||
components.add(component);
|
||||
}
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
private static Map<String, Object> frontendRouteItem(String code, String name, String path,
|
||||
boolean requiresLogin, GenTable table) {
|
||||
Map<String, Object> item = new LinkedHashMap<String, Object>();
|
||||
item.put("code", sanitizeRouteName(code, table));
|
||||
item.put("name", StringUtils.defaultIfEmpty(name, StringUtils.defaultString(table.getFunctionName())));
|
||||
item.put("path", normalizeFrontendPath(path, defaultTablePath(table)));
|
||||
item.put("requiresLogin", requiresLogin);
|
||||
item.put("componentName", table.getClassName());
|
||||
item.put("componentPath", "@/views/" + table.getModuleName() + "/" + table.getBusinessName() + "/index.vue");
|
||||
return item;
|
||||
}
|
||||
|
||||
private static Map<String, Object> frontendDesignedPageRouteItem(String code, String name, String path,
|
||||
boolean requiresLogin, AppPageDesign page) {
|
||||
Map<String, Object> item = new LinkedHashMap<String, Object>();
|
||||
String pageCode = StringUtils.defaultIfEmpty(page == null ? null : page.getCode(), "frontend_page");
|
||||
item.put("code", sanitizeRouteName(code, null));
|
||||
item.put("name", StringUtils.defaultIfEmpty(name, pageCode));
|
||||
item.put("path", normalizeFrontendPath(path, defaultPagePath(page)));
|
||||
item.put("requiresLogin", requiresLogin);
|
||||
item.put("componentName", toProjectClassName(pageCode));
|
||||
item.put("componentPath", "@/views/pages/" + pageCode + "/index.vue");
|
||||
return item;
|
||||
}
|
||||
|
||||
private static boolean hasPageDesign(AppPageDesign page, Map<String, FrontProjectPageDesign> pageDesignsByCode) {
|
||||
return page != null && pageDesignsByCode != null && pageDesignsByCode.containsKey(page.getCode());
|
||||
}
|
||||
|
||||
private static boolean shouldShowPageInMenu(AppPageDesign page,
|
||||
Map<String, FrontProjectPageDesign> pageDesignsByCode) {
|
||||
if (page == null) {
|
||||
return false;
|
||||
}
|
||||
FrontProjectPageDesign pageDesign = pageDesignsByCode == null ? null : pageDesignsByCode.get(page.getCode());
|
||||
Object explicit = navigationValue(pageDesign, "showInMenu");
|
||||
if (explicit instanceof Boolean) {
|
||||
return ((Boolean) explicit).booleanValue();
|
||||
}
|
||||
if (explicit != null) {
|
||||
return !"false".equalsIgnoreCase(String.valueOf(explicit));
|
||||
}
|
||||
return !isAssociatedPageType(page.getPageType());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Object navigationValue(FrontProjectPageDesign pageDesign, String key) {
|
||||
if (pageDesign == null) {
|
||||
return null;
|
||||
}
|
||||
enrichPageDesign(pageDesign);
|
||||
if (pageDesign.getLayout() == null) {
|
||||
return null;
|
||||
}
|
||||
Object navigation = pageDesign.getLayout().get("navigation");
|
||||
if (!(navigation instanceof Map)) {
|
||||
return null;
|
||||
}
|
||||
return ((Map<String, Object>) navigation).get(key);
|
||||
}
|
||||
|
||||
private static AppPageDesign findMenuPage(AppMenuDesign menu, List<AppPageDesign> pages) {
|
||||
if (menu == null || StringUtils.isEmpty(pages)) {
|
||||
return null;
|
||||
}
|
||||
for (AppPageDesign page : pages) {
|
||||
if (page != null && matchesMenuPage(menu, page)) {
|
||||
return page;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isBusinessBlockPage(AppPageDesign page) {
|
||||
return page != null && "business_block".equals(page.getPageType());
|
||||
}
|
||||
|
||||
private static boolean isAssociatedPageType(String pageType) {
|
||||
return "detail".equals(pageType) || "form".equals(pageType);
|
||||
}
|
||||
|
||||
private static GenTable resolveMenuTable(AppMenuDesign menu, List<AppPageDesign> pages, List<GenTable> tables) {
|
||||
if (StringUtils.isEmpty(tables)) {
|
||||
return null;
|
||||
}
|
||||
GenTable table = resolveMenuTableByPage(menu, pages, tables);
|
||||
if (table != null) {
|
||||
return table;
|
||||
}
|
||||
String menuKey = normalizeIdentifier(StringUtils.defaultString(menu.getCode()) + " "
|
||||
+ StringUtils.defaultString(menu.getPath()) + " " + StringUtils.defaultString(menu.getName()));
|
||||
for (GenTable candidate : tables) {
|
||||
if (matchesMenuTableKey(menuKey, candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return tables.get(0);
|
||||
}
|
||||
|
||||
private static GenTable resolveMenuTableByPage(AppMenuDesign menu, List<AppPageDesign> pages, List<GenTable> tables) {
|
||||
if (menu == null || StringUtils.isEmpty(pages)) {
|
||||
return null;
|
||||
}
|
||||
for (AppPageDesign page : pages) {
|
||||
if (page == null || !matchesMenuPage(menu, page)) {
|
||||
continue;
|
||||
}
|
||||
GenTable table = findTableByName(tables, page.getTableName());
|
||||
if (table != null) {
|
||||
return table;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean matchesMenuPage(AppMenuDesign menu, AppPageDesign page) {
|
||||
return StringUtils.isNotEmpty(menu.getPageCode()) && menu.getPageCode().equals(page.getCode())
|
||||
|| StringUtils.isNotEmpty(menu.getCode()) && menu.getCode().equals(page.getMenuCode());
|
||||
}
|
||||
|
||||
private static GenTable findTableByName(List<GenTable> tables, String tableName) {
|
||||
String expected = normalizeIdentifier(tableName);
|
||||
if (StringUtils.isEmpty(expected)) {
|
||||
return null;
|
||||
}
|
||||
for (GenTable table : tables) {
|
||||
if (table == null) {
|
||||
continue;
|
||||
}
|
||||
if (expected.equals(normalizeIdentifier(table.getTableName()))
|
||||
|| expected.equals(normalizeIdentifier(table.getBusinessName()))
|
||||
|| expected.equals(normalizeIdentifier(table.getClassName()))) {
|
||||
return table;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean matchesMenuTableKey(String menuKey, GenTable table) {
|
||||
if (StringUtils.isEmpty(menuKey) || table == null) {
|
||||
return false;
|
||||
}
|
||||
List<String> tableKeys = new ArrayList<String>();
|
||||
tableKeys.add(table.getBusinessName());
|
||||
tableKeys.add(table.getTableName());
|
||||
tableKeys.add(table.getClassName());
|
||||
tableKeys.add(table.getFunctionName());
|
||||
for (String tableKey : tableKeys) {
|
||||
String normalized = normalizeIdentifier(tableKey);
|
||||
if (StringUtils.isEmpty(normalized)) {
|
||||
continue;
|
||||
}
|
||||
if (menuKey.contains(normalized) || normalized.contains(menuKey)) {
|
||||
return true;
|
||||
}
|
||||
for (String token : splitIdentifierTokens(tableKey)) {
|
||||
if (token.length() >= 4 && menuKey.contains(token)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<String> splitIdentifierTokens(String value) {
|
||||
String source = StringUtils.defaultString(value).replaceAll("([a-z])([A-Z])", "$1 $2");
|
||||
String[] parts = source.split("[^A-Za-z0-9]+");
|
||||
List<String> tokens = new ArrayList<String>();
|
||||
for (String part : parts) {
|
||||
String token = normalizeIdentifier(part);
|
||||
if (StringUtils.isNotEmpty(token)) {
|
||||
tokens.add(token);
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private static String resolveDefaultFrontendPath(List<Map<String, Object>> navItems,
|
||||
List<Map<String, Object>> routeItems) {
|
||||
if (StringUtils.isNotEmpty(navItems)) {
|
||||
return (String) navItems.get(0).get("path");
|
||||
}
|
||||
if (StringUtils.isNotEmpty(routeItems)) {
|
||||
return (String) routeItems.get(0).get("path");
|
||||
}
|
||||
return "/empty";
|
||||
}
|
||||
|
||||
private static String defaultTablePath(GenTable table) {
|
||||
if (table == null) {
|
||||
return "/empty";
|
||||
}
|
||||
return "/" + StringUtils.defaultString(table.getModuleName()) + "/" + StringUtils.defaultString(table.getBusinessName());
|
||||
}
|
||||
|
||||
private static String defaultBusinessBlockPagePath(AppPageDesign page) {
|
||||
if (page == null || StringUtils.isEmpty(page.getCode())) {
|
||||
return "/business-block";
|
||||
}
|
||||
return "/" + page.getCode().replace("_", "-");
|
||||
}
|
||||
|
||||
private static String defaultPagePath(AppPageDesign page) {
|
||||
if (isBusinessBlockPage(page)) {
|
||||
return defaultBusinessBlockPagePath(page);
|
||||
}
|
||||
if (page == null || StringUtils.isEmpty(page.getCode())) {
|
||||
return "/page";
|
||||
}
|
||||
return "/" + page.getCode().replace("_", "-");
|
||||
}
|
||||
|
||||
private static String normalizeFrontendPath(String path, String fallback) {
|
||||
String value = StringUtils.defaultString(path).trim();
|
||||
if (StringUtils.isEmpty(value)) {
|
||||
value = fallback;
|
||||
}
|
||||
if (!value.startsWith("/")) {
|
||||
value = "/" + value;
|
||||
}
|
||||
return value.replaceAll("/{2,}", "/");
|
||||
}
|
||||
|
||||
private static String sanitizeRouteName(String code, GenTable table) {
|
||||
String value = StringUtils.defaultString(code).replaceAll("[^A-Za-z0-9_]", "_");
|
||||
if (StringUtils.isEmpty(value)) {
|
||||
value = table == null ? "GeneratedRoute" : StringUtils.defaultString(table.getClassName());
|
||||
}
|
||||
if (!Character.isLetter(value.charAt(0)) && value.charAt(0) != '_') {
|
||||
value = "_" + value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String normalizeIdentifier(String value) {
|
||||
return StringUtils.defaultString(value).toLowerCase().replaceAll("[^a-z0-9]", "");
|
||||
}
|
||||
|
||||
private static String toProjectClassName(String projectFileName) {
|
||||
String source = projectFileName == null ? "" : projectFileName;
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean uppercaseNext = true;
|
||||
for (int i = 0; i < source.length(); i++) {
|
||||
char ch = source.charAt(i);
|
||||
if (!Character.isLetterOrDigit(ch)) {
|
||||
uppercaseNext = true;
|
||||
continue;
|
||||
}
|
||||
if (result.length() == 0 && !Character.isJavaIdentifierStart(ch)) {
|
||||
result.append("App");
|
||||
}
|
||||
result.append(uppercaseNext ? Character.toUpperCase(ch) : ch);
|
||||
uppercaseNext = false;
|
||||
}
|
||||
if (result.length() == 0) {
|
||||
return "GeneratedProject";
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public static void setMenuVelocityContext(VelocityContext context, GenTable genTable) {
|
||||
String options = genTable.getOptions();
|
||||
JSONObject paramsObj = JSON.parseObject(options);
|
||||
@@ -279,7 +1131,8 @@ public class VelocityUtils {
|
||||
*/
|
||||
public static void addDicts(Set<String> dicts, List<GenTableColumn> columns) {
|
||||
for (GenTableColumn column : columns) {
|
||||
if (!column.isSuperColumn() && StringUtils.isNotEmpty(column.getDictType()) && StringUtils.equalsAny(
|
||||
if (!column.isSuperColumn() && StringUtils.isNotEmpty(column.getDictType())
|
||||
&& !isQingRemoteDictType(column.getDictType()) && StringUtils.equalsAny(
|
||||
column.getHtmlType(),
|
||||
new String[]{GenConstants.HTML_SELECT, GenConstants.HTML_RADIO, GenConstants.HTML_CHECKBOX})) {
|
||||
dicts.add("'" + column.getDictType() + "'");
|
||||
@@ -287,6 +1140,70 @@ public class VelocityUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Map<String, String>> getQingRemoteSelects(GenTable genTable) {
|
||||
List<Map<String, String>> remoteSelects = new ArrayList<Map<String, String>>();
|
||||
List<GenTableColumn> columns = genTable.getColumns();
|
||||
if (columns == null) {
|
||||
return remoteSelects;
|
||||
}
|
||||
for (GenTableColumn column : columns) {
|
||||
if (column == null || !isQingRemoteDictType(column.getDictType())) {
|
||||
continue;
|
||||
}
|
||||
Map<String, String> remoteSelect = parseQingRemoteSelect(genTable, column);
|
||||
if (!remoteSelect.isEmpty()) {
|
||||
remoteSelects.add(remoteSelect);
|
||||
}
|
||||
}
|
||||
return remoteSelects;
|
||||
}
|
||||
|
||||
private static Set<String> getQingRemoteSelectFields(List<Map<String, String>> remoteSelects) {
|
||||
Set<String> fields = new HashSet<String>();
|
||||
for (Map<String, String> remoteSelect : remoteSelects) {
|
||||
fields.add(remoteSelect.get("field"));
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
private static GenTableColumn getQingCurrentUserColumn(GenTable genTable) {
|
||||
if (genTable == null || genTable.getColumns() == null) {
|
||||
return null;
|
||||
}
|
||||
for (GenTableColumn column : genTable.getColumns()) {
|
||||
if (column == null) {
|
||||
continue;
|
||||
}
|
||||
if ("userId".equals(column.getJavaField()) || "user_id".equals(column.getColumnName())) {
|
||||
return column;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Map<String, String> parseQingRemoteSelect(GenTable genTable, GenTableColumn column) {
|
||||
Map<String, String> remoteSelect = new LinkedHashMap<String, String>();
|
||||
String[] parts = StringUtils.split(column.getDictType(), ":");
|
||||
if (parts == null || parts.length < 4) {
|
||||
return remoteSelect;
|
||||
}
|
||||
String remoteTable = parts[1];
|
||||
String valueField = StringUtils.toCamelCase(parts[2]);
|
||||
String labelField = StringUtils.toCamelCase(parts[3]);
|
||||
if (StringUtils.isAnyEmpty(remoteTable, valueField, labelField)) {
|
||||
return remoteSelect;
|
||||
}
|
||||
remoteSelect.put("field", column.getJavaField());
|
||||
remoteSelect.put("path", "/" + genTable.getModuleName() + "/" + StringUtils.toCamelCase(remoteTable) + "/page");
|
||||
remoteSelect.put("valueField", valueField);
|
||||
remoteSelect.put("labelField", labelField);
|
||||
return remoteSelect;
|
||||
}
|
||||
|
||||
private static boolean isQingRemoteDictType(String dictType) {
|
||||
return StringUtils.defaultString(dictType).startsWith("remote:");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限前缀
|
||||
*
|
||||
|
||||
@@ -1,28 +1,37 @@
|
||||
package ${packageName}.controller;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.poi.excel.ExcelUtil;
|
||||
import cn.hutool.poi.excel.ExcelReader;
|
||||
import cn.hutool.poi.excel.ExcelUtil;
|
||||
import cn.hutool.poi.excel.ExcelWriter;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Map;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import javax.annotation.Resource;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.example.springboot.common.Result;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.example.springboot.entity.User;
|
||||
import com.example.springboot.utils.TokenUtils;
|
||||
import com.example.springboot.service.IUserService;
|
||||
|
||||
import ${packageName}.service.I${ClassName}Service;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import ${packageName}.common.Result;
|
||||
import ${packageName}.entity.${ClassName};
|
||||
#if($hasQingCurrentUserColumn)
|
||||
import ${packageName}.entity.PortalUser;
|
||||
import ${packageName}.security.PortalAuthTokenStore;
|
||||
#end
|
||||
import ${packageName}.service.I${ClassName}Service;
|
||||
import java.io.InputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
#if($hasQingCurrentUserColumn)
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
#end
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* ${functionName}Controller
|
||||
@@ -37,93 +46,108 @@ public class ${ClassName}Controller
|
||||
@Resource
|
||||
private I${ClassName}Service ${className}Service;
|
||||
|
||||
private final String now = DateUtil.now();
|
||||
#if($hasQingCurrentUserColumn)
|
||||
@Resource
|
||||
private PortalAuthTokenStore tokenStore;
|
||||
|
||||
// 新增或者更新
|
||||
#end
|
||||
|
||||
#if($allowAdd == '1' || $allowEdit == '1')
|
||||
@PostMapping
|
||||
public Result save(@RequestBody ${ClassName} ${className}) {
|
||||
if (${className}.getId() == null) {
|
||||
//${className}.setTime(DateUtil.now());
|
||||
//${className}.setUser(TokenUtils.getCurrentUser().getNickname());
|
||||
//${className}.setUserid(TokenUtils.getCurrentUser().getId());
|
||||
}
|
||||
public Result<Void> save(@RequestBody ${ClassName} ${className})
|
||||
{
|
||||
#if($allowAdd == '1' && $allowEdit == '1')
|
||||
${className}Service.saveOrUpdate(${className});
|
||||
#elseif($allowAdd == '1')
|
||||
${className}Service.save(${className});
|
||||
#else
|
||||
${className}Service.updateById(${className});
|
||||
#end
|
||||
return Result.success();
|
||||
}
|
||||
#end
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public Result delete(@PathVariable Integer id) {
|
||||
${className}Service.removeById(id);
|
||||
#if($allowRemove == '1')
|
||||
@DeleteMapping("/{${pkColumn.javaField}}")
|
||||
public Result<Void> delete(@PathVariable ${pkColumn.javaType} ${pkColumn.javaField})
|
||||
{
|
||||
${className}Service.removeById(${pkColumn.javaField});
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@PostMapping("/del/batch")
|
||||
public Result deleteBatch(@RequestBody List<Integer> ids) {
|
||||
${className}Service.removeByIds(ids);
|
||||
public Result<Void> deleteBatch(@RequestBody List<${pkColumn.javaType}> ${pkColumn.javaField}s)
|
||||
{
|
||||
${className}Service.removeByIds(${pkColumn.javaField}s);
|
||||
return Result.success();
|
||||
}
|
||||
#end
|
||||
|
||||
@GetMapping
|
||||
public Result findAll() {
|
||||
public Result<List<${ClassName}>> findAll()
|
||||
{
|
||||
return Result.success(${className}Service.list());
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Result findOne(@PathVariable Integer id) {
|
||||
return Result.success(${className}Service.getById(id));
|
||||
@GetMapping("/{${pkColumn.javaField}}")
|
||||
public Result<${ClassName}> findOne(@PathVariable ${pkColumn.javaType} ${pkColumn.javaField})
|
||||
{
|
||||
return Result.success(${className}Service.getById(${pkColumn.javaField}));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
public Result findPage(@RequestParam(required = false) Integer pageNum,
|
||||
@RequestParam(required = false) Integer pageSize,
|
||||
@RequestParam(required = false) Map<String, Object> params) {
|
||||
if (pageNum == null) {
|
||||
pageNum = 1;
|
||||
public Result<Page<${ClassName}>> findPage(@RequestParam(required = false, defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(required = false, defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) Map<String, Object> params#if($hasQingCurrentUserColumn),
|
||||
HttpServletRequest request#end)
|
||||
{
|
||||
QueryWrapper<${ClassName}> queryWrapper = new QueryWrapper<${ClassName}>();
|
||||
#if($hasQingCurrentUserColumn)
|
||||
if (isCurrentUserOnly(params))
|
||||
{
|
||||
PortalUser currentUser = tokenStore.getPortalUser(resolveToken(request));
|
||||
if (currentUser == null)
|
||||
{
|
||||
return Result.success(new Page<${ClassName}>(pageNum, pageSize));
|
||||
}
|
||||
if (pageSize == null) {
|
||||
pageSize = 10;
|
||||
queryWrapper.eq("${qingCurrentUserColumn.columnName}", currentUser.getUserId());
|
||||
}
|
||||
QueryWrapper<${ClassName}> queryWrapper = new QueryWrapper<>();
|
||||
#end
|
||||
#foreach ($column in $columns)
|
||||
#if($column.isQuery == "1")
|
||||
#set($queryType=$column.queryType)
|
||||
#set($javaField=$column.javaField)
|
||||
#set($columnName=$column.columnName)
|
||||
#if($column.queryType == "EQ")
|
||||
queryWrapper.eq(StringUtils.isNotEmpty((String)params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
|
||||
#if($queryType == "EQ")
|
||||
queryWrapper.eq(hasValue(params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
|
||||
#elseif($queryType == "NE")
|
||||
queryWrapper.ne(StringUtils.isNotEmpty((String)params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
|
||||
queryWrapper.ne(hasValue(params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
|
||||
#elseif($queryType == "GT")
|
||||
queryWrapper.gt(StringUtils.isNotEmpty((String)params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
|
||||
queryWrapper.gt(hasValue(params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
|
||||
#elseif($queryType == "GTE")
|
||||
queryWrapper.ge(StringUtils.isNotEmpty((String)params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
|
||||
queryWrapper.ge(hasValue(params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
|
||||
#elseif($queryType == "LT")
|
||||
queryWrapper.lt(StringUtils.isNotEmpty((String)params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
|
||||
queryWrapper.lt(hasValue(params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
|
||||
#elseif($queryType == "LTE")
|
||||
queryWrapper.le(StringUtils.isNotEmpty((String)params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
|
||||
queryWrapper.le(hasValue(params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
|
||||
#elseif($queryType == "LIKE")
|
||||
queryWrapper.like(StringUtils.isNotEmpty((String)params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
|
||||
queryWrapper.like(hasValue(params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
|
||||
#else
|
||||
queryWrapper.eq(hasValue(params.get("${javaField}")), "${columnName}", params.get("${javaField}"));
|
||||
#end
|
||||
#end
|
||||
#end
|
||||
queryWrapper.orderByDesc("${pkColumn.columnName}");
|
||||
return Result.success(${className}Service.page(new Page<>(pageNum, pageSize), queryWrapper));
|
||||
return Result.success(${className}Service.page(new Page<${ClassName}>(pageNum, pageSize), queryWrapper));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出接口
|
||||
*/
|
||||
@GetMapping("/export")
|
||||
public void export(HttpServletResponse response) throws Exception {
|
||||
// 从数据库查询出所有的数据
|
||||
public void export(HttpServletResponse response) throws Exception
|
||||
{
|
||||
List<${ClassName}> list = ${className}Service.list();
|
||||
// 在内存操作,写出到浏览器
|
||||
ExcelWriter writer = ExcelUtil.getWriter(true);
|
||||
|
||||
// 一次性写出list内的对象到excel,使用默认样式,强制输出标题
|
||||
writer.write(list, true);
|
||||
|
||||
// 设置浏览器响应的格式
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
|
||||
String fileName = URLEncoder.encode("${functionName}信息表", "UTF-8");
|
||||
response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
|
||||
@@ -134,19 +158,37 @@ public class ${ClassName}Controller
|
||||
writer.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* excel 导入
|
||||
* @param file
|
||||
* @throws Exception
|
||||
*/
|
||||
#if($allowAdd == '1')
|
||||
@PostMapping("/import")
|
||||
public Result imp(MultipartFile file) throws Exception {
|
||||
public Result<Void> imp(MultipartFile file) throws Exception
|
||||
{
|
||||
InputStream inputStream = file.getInputStream();
|
||||
ExcelReader reader = ExcelUtil.getReader(inputStream);
|
||||
// 通过 javabean的方式读取Excel内的对象,但是要求表头必须是英文,跟javabean的属性要对应起来
|
||||
List<${ClassName}> list = reader.readAll(${ClassName}.class);
|
||||
|
||||
${className}Service.saveBatch(list);
|
||||
return Result.success();
|
||||
}
|
||||
#end
|
||||
|
||||
private boolean hasValue(Object value)
|
||||
{
|
||||
return value != null && StringUtils.isNotBlank(String.valueOf(value));
|
||||
}
|
||||
|
||||
#if($hasQingCurrentUserColumn)
|
||||
private boolean isCurrentUserOnly(Map<String, Object> params)
|
||||
{
|
||||
return params != null && "true".equalsIgnoreCase(String.valueOf(params.get("__currentUserOnly")));
|
||||
}
|
||||
|
||||
private String resolveToken(HttpServletRequest request)
|
||||
{
|
||||
String authorization = request.getHeader("Authorization");
|
||||
if (authorization != null && authorization.startsWith("Bearer "))
|
||||
{
|
||||
return authorization.substring("Bearer ".length());
|
||||
}
|
||||
return request.getHeader("X-Portal-Token");
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,463 @@
|
||||
package com.ruoyi.generator.service;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.velocity.VelocityContext;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import com.ruoyi.generator.domain.GenProject;
|
||||
import com.ruoyi.generator.domain.GenTable;
|
||||
import com.ruoyi.generator.domain.GenTableColumn;
|
||||
import com.ruoyi.generator.domain.GeneratedBusinessBlockFile;
|
||||
import com.ruoyi.generator.domain.front.FrontProjectPageDesign;
|
||||
import com.ruoyi.generator.service.front.BusinessBlockRegistryService;
|
||||
|
||||
public class BusinessBlockGenerationServiceTest
|
||||
{
|
||||
private BusinessBlockGenerationService service;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception
|
||||
{
|
||||
service = new BusinessBlockGenerationService();
|
||||
setField("businessBlockRegistryService", new BusinessBlockRegistryService());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listFilesCreatesFrontendPageAndBlockFiles()
|
||||
{
|
||||
List<GeneratedBusinessBlockFile> files = service.listFiles(project(), "frontend");
|
||||
|
||||
assertTrue(containsPath(files, "src/views/pages/cart_page/index.vue"));
|
||||
assertTrue(containsPath(files, "src/views/pages/cart_page/blocks/cart_001/CartBlock.vue"));
|
||||
assertTrue(containsPath(files, "src/api/business-blocks/cart_001.js"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listFilesCreatesBackendFilesWithPackagePath()
|
||||
{
|
||||
List<GeneratedBusinessBlockFile> files = service.listFiles(project(), "backend");
|
||||
|
||||
assertTrue(containsPath(files, "src/main/java/com/example/demo/controller/block/Cart001BlockController.java"));
|
||||
assertTrue(containsPath(files, "src/main/java/com/example/demo/service/block/ICart001BlockService.java"));
|
||||
assertTrue(containsPath(files, "src/main/java/com/example/demo/service/block/impl/Cart001BlockServiceImpl.java"));
|
||||
assertTrue(containsPath(files, "src/main/java/com/example/demo/mapper/block/Cart001BlockMapper.java"));
|
||||
assertTrue(containsPath(files, "src/main/resources/mapper/block/Cart001BlockMapper.xml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderFileUsesBusinessBlockContext()
|
||||
{
|
||||
GenProject project = project();
|
||||
GeneratedBusinessBlockFile pageFile = findPath(service.listFiles(project, "frontend"),
|
||||
"src/views/pages/cart_page/index.vue");
|
||||
GeneratedBusinessBlockFile blockFile = findPath(service.listFiles(project, "frontend"),
|
||||
"src/views/pages/cart_page/blocks/cart_001/CartBlock.vue");
|
||||
GeneratedBusinessBlockFile apiFile = findPath(service.listFiles(project, "frontend"),
|
||||
"src/api/business-blocks/cart_001.js");
|
||||
GeneratedBusinessBlockFile controllerFile = findPath(service.listFiles(project, "backend"),
|
||||
"src/main/java/com/example/demo/controller/block/Cart001BlockController.java");
|
||||
|
||||
String pageContent = service.renderFile(project, pageFile);
|
||||
String blockContent = service.renderFile(project, blockFile);
|
||||
String apiContent = service.renderFile(project, apiFile);
|
||||
String controllerContent = service.renderFile(project, controllerFile);
|
||||
|
||||
assertTrue(pageContent.contains("import Cart001Block from \"./blocks/cart_001/CartBlock.vue\""));
|
||||
assertTrue(pageContent.contains("<Cart001Block />"));
|
||||
assertTrue(blockContent.contains("listCart001BlockItems"));
|
||||
assertTrue(blockContent.contains("@/api/business-blocks/cart_001"));
|
||||
assertTrue(apiContent.contains("url: \"/business-blocks/cart_001/\" + id + \"/quantity\""));
|
||||
assertTrue(apiContent.contains("url: \"/business-blocks/cart_001/\" + id"));
|
||||
assertTrue(controllerContent.contains("class Cart001BlockController"));
|
||||
assertTrue(controllerContent.contains("@RequestMapping(\"/business-blocks/cart_001\")"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderFileConvertsConfiguredJavaFieldsToSqlColumns()
|
||||
{
|
||||
GenProject project = project();
|
||||
GeneratedBusinessBlockFile mapperFile = findPath(service.listFiles(project, "backend"),
|
||||
"src/main/resources/mapper/block/Cart001BlockMapper.xml");
|
||||
|
||||
String mapperContent = service.renderFile(project, mapperFile);
|
||||
|
||||
assertTrue(mapperContent.contains("left join shop_product p on c.product_id = p.id"));
|
||||
assertTrue(mapperContent.contains("where c.user_id = #{userId}"));
|
||||
assertTrue(mapperContent.contains("set quantity = #{quantity}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listFilesCreatesBundledBusinessBlockFiles()
|
||||
{
|
||||
List<GeneratedBusinessBlockFile> files = service.listFiles(businessBlocksProject(), "frontend");
|
||||
|
||||
assertTrue(containsPath(files, "src/views/pages/business_page/blocks/notice_001/NoticeBlock.vue"));
|
||||
assertTrue(containsPath(files, "src/views/pages/business_page/blocks/carousel_001/CarouselBlock.vue"));
|
||||
assertTrue(containsPath(files, "src/views/pages/business_page/blocks/order_001/OrderBlock.vue"));
|
||||
assertTrue(containsPath(files, "src/views/pages/business_page/blocks/master_detail_001/MasterDetailBlock.vue"));
|
||||
assertTrue(containsPath(files, "src/api/business-blocks/notice_001.js"));
|
||||
assertTrue(containsPath(files, "src/api/business-blocks/carousel_001.js"));
|
||||
assertTrue(containsPath(files, "src/api/business-blocks/order_001.js"));
|
||||
assertTrue(containsPath(files, "src/api/business-blocks/master_detail_001.js"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderFileSupportsBundledBusinessBlocks()
|
||||
{
|
||||
GenProject project = businessBlocksProject();
|
||||
String noticeMapper = service.renderFile(project, findPath(service.listFiles(project, "backend"),
|
||||
"src/main/resources/mapper/block/Notice001BlockMapper.xml"));
|
||||
String carouselComponent = service.renderFile(project, findPath(service.listFiles(project, "frontend"),
|
||||
"src/views/pages/business_page/blocks/carousel_001/CarouselBlock.vue"));
|
||||
String orderMapper = service.renderFile(project, findPath(service.listFiles(project, "backend"),
|
||||
"src/main/resources/mapper/block/Order001BlockMapper.xml"));
|
||||
String orderComponent = service.renderFile(project, findPath(service.listFiles(project, "frontend"),
|
||||
"src/views/pages/business_page/blocks/order_001/OrderBlock.vue"));
|
||||
String masterDetailMapper = service.renderFile(project, findPath(service.listFiles(project, "backend"),
|
||||
"src/main/resources/mapper/block/MasterDetail001BlockMapper.xml"));
|
||||
|
||||
assertTrue(noticeMapper.contains("from sys_notice n"));
|
||||
assertTrue(noticeMapper.contains("n.notice_title as title"));
|
||||
assertTrue(carouselComponent.contains("listCarousel001BlockCarouselItems"));
|
||||
assertTrue(orderMapper.contains("insert into shop_order"));
|
||||
assertTrue(orderMapper.contains("left join shop_product p on i.product_id = p.id"));
|
||||
assertTrue(orderComponent.contains("this.$message.success(\"订单已提交\")"));
|
||||
assertTrue(masterDetailMapper.contains("from library_book m"));
|
||||
assertTrue(masterDetailMapper.contains("where d.book_id = #{masterId}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderFileRendersEveryBundledBusinessBlockFile()
|
||||
{
|
||||
GenProject project = businessBlocksProject();
|
||||
assertRenderedFiles(project, service.listFiles(project, "frontend"));
|
||||
assertRenderedFiles(project, service.listFiles(project, "backend"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listFilesCreatesEmbeddedBlockFilesWithNormalPage()
|
||||
{
|
||||
List<GeneratedBusinessBlockFile> files = service.listFiles(embeddedBlocksProject(), "frontend");
|
||||
|
||||
assertTrue(containsPath(files, "src/views/pages/book_catalog/index.vue"));
|
||||
assertTrue(containsPath(files, "src/views/pages/book_catalog/blocks/notice_001/NoticeBlock.vue"));
|
||||
assertTrue(containsPath(files, "src/views/pages/book_catalog/blocks/carousel_001/CarouselBlock.vue"));
|
||||
assertTrue(containsPath(files, "src/api/business-blocks/notice_001.js"));
|
||||
assertTrue(containsPath(files, "src/api/business-blocks/carousel_001.js"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderFileUsesExactNormalPageDesignContext()
|
||||
{
|
||||
GenProject project = embeddedBlocksProject();
|
||||
GeneratedBusinessBlockFile pageFile = findPath(service.listFiles(project, "frontend"),
|
||||
"src/views/pages/book_catalog/index.vue");
|
||||
|
||||
String pageContent = service.renderFile(project, pageFile);
|
||||
|
||||
assertTrue(pageContent.contains("portal-page"));
|
||||
assertTrue(pageContent.contains("import Notice001Block from \"@/views/pages/book_catalog/blocks/notice_001/NoticeBlock.vue\""));
|
||||
assertTrue(pageContent.contains("<Notice001Block />"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void putEmbeddedBlockVelocityContextExposesTopAndBottomBlockImports()
|
||||
{
|
||||
GenProject project = embeddedBlocksProject();
|
||||
VelocityContext context = new VelocityContext();
|
||||
|
||||
service.putEmbeddedBlockVelocityContext(context, project, project.getPageDesigns().get(0));
|
||||
|
||||
List<Map<String, Object>> topBlocks = (List<Map<String, Object>>) context.get("pageDesignTopBusinessBlocks");
|
||||
List<Map<String, Object>> bottomBlocks = (List<Map<String, Object>>) context.get("pageDesignBottomBusinessBlocks");
|
||||
|
||||
assertEquals(1, topBlocks.size());
|
||||
assertEquals(1, bottomBlocks.size());
|
||||
assertEquals("Notice001Block", topBlocks.get(0).get("componentName"));
|
||||
assertEquals("@/views/pages/book_catalog/blocks/notice_001/NoticeBlock.vue",
|
||||
topBlocks.get(0).get("relativeImportPath"));
|
||||
assertEquals("Carousel001Block", bottomBlocks.get(0).get("componentName"));
|
||||
assertEquals(Boolean.TRUE, context.get("hasPageDesignEmbeddedBusinessBlocks"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findFileMatchesCategoryOrPath()
|
||||
{
|
||||
GenProject project = project();
|
||||
GeneratedBusinessBlockFile file = findPath(service.listFiles(project, "frontend"),
|
||||
"src/api/business-blocks/cart_001.js");
|
||||
|
||||
assertEquals(file.getOutputPath(), service.findFile(project, "frontend", file.getCategory()).getOutputPath());
|
||||
assertEquals(file.getCategory(), service.findFile(project, "frontend", file.getOutputPath()).getCategory());
|
||||
}
|
||||
|
||||
private void setField(String name, Object value) throws Exception
|
||||
{
|
||||
Field field = BusinessBlockGenerationService.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(service, value);
|
||||
}
|
||||
|
||||
private boolean containsPath(List<GeneratedBusinessBlockFile> files, String outputPath)
|
||||
{
|
||||
return findPath(files, outputPath) != null;
|
||||
}
|
||||
|
||||
private GeneratedBusinessBlockFile findPath(List<GeneratedBusinessBlockFile> files, String outputPath)
|
||||
{
|
||||
for (GeneratedBusinessBlockFile file : files)
|
||||
{
|
||||
if (outputPath.equals(file.getOutputPath()))
|
||||
{
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void assertRenderedFiles(GenProject project, List<GeneratedBusinessBlockFile> files)
|
||||
{
|
||||
for (GeneratedBusinessBlockFile file : files)
|
||||
{
|
||||
String content = service.renderFile(project, file);
|
||||
assertTrue(file.getOutputPath(), content.trim().length() > 0);
|
||||
}
|
||||
}
|
||||
|
||||
private GenProject project()
|
||||
{
|
||||
GenProject project = new GenProject();
|
||||
project.setProjectId(10L);
|
||||
project.setProjectName("Demo");
|
||||
project.setProjectFileName("demo");
|
||||
project.setPackageName("com.example.demo");
|
||||
project.setTables(Arrays.asList(cartTable(), productTable()));
|
||||
project.setPageDesigns(Arrays.asList(cartPageDesign()));
|
||||
return project;
|
||||
}
|
||||
|
||||
private GenProject businessBlocksProject()
|
||||
{
|
||||
GenProject project = new GenProject();
|
||||
project.setProjectId(11L);
|
||||
project.setProjectName("BusinessBlocks");
|
||||
project.setProjectFileName("business-blocks");
|
||||
project.setPackageName("com.example.demo");
|
||||
project.setTables(Arrays.asList(
|
||||
noticeTable(),
|
||||
carouselTable(),
|
||||
orderTable(),
|
||||
orderItemTable(),
|
||||
productTable(),
|
||||
masterTable(),
|
||||
detailTable()));
|
||||
project.setPageDesigns(Arrays.asList(businessBlocksPageDesign()));
|
||||
return project;
|
||||
}
|
||||
|
||||
private GenProject embeddedBlocksProject()
|
||||
{
|
||||
GenProject project = new GenProject();
|
||||
project.setProjectId(12L);
|
||||
project.setProjectName("EmbeddedBlocks");
|
||||
project.setProjectFileName("embedded-blocks");
|
||||
project.setPackageName("com.example.demo");
|
||||
project.setTables(Arrays.asList(noticeTable(), carouselTable(), masterTable()));
|
||||
project.setPageDesigns(Arrays.asList(embeddedBlocksPageDesign()));
|
||||
return project;
|
||||
}
|
||||
|
||||
private FrontProjectPageDesign cartPageDesign()
|
||||
{
|
||||
FrontProjectPageDesign design = new FrontProjectPageDesign();
|
||||
design.setPageCode("cart_page");
|
||||
design.setPageName("Cart");
|
||||
design.setPageScope("frontend");
|
||||
design.setPageType("business_block");
|
||||
design.setRoutePath("/cart");
|
||||
design.setTableName("shop_cart");
|
||||
design.setLayoutJson("{\"canvas\":\"business-blocks-v1\",\"blocks\":[{\"id\":\"cart_001\",\"blockCode\":\"cart\",\"config\":{"
|
||||
+ "\"cartTable\":\"shop_cart\","
|
||||
+ "\"productTable\":\"shop_product\","
|
||||
+ "\"cartProductId\":\"productId\","
|
||||
+ "\"productId\":\"id\","
|
||||
+ "\"productName\":\"name\","
|
||||
+ "\"price\":\"price\","
|
||||
+ "\"quantity\":\"quantity\","
|
||||
+ "\"userId\":\"userId\","
|
||||
+ "\"stock\":\"stock\""
|
||||
+ "}}]}");
|
||||
return design;
|
||||
}
|
||||
|
||||
private FrontProjectPageDesign businessBlocksPageDesign()
|
||||
{
|
||||
FrontProjectPageDesign design = new FrontProjectPageDesign();
|
||||
design.setPageCode("business_page");
|
||||
design.setPageName("Business Blocks");
|
||||
design.setPageScope("frontend");
|
||||
design.setPageType("business_block");
|
||||
design.setRoutePath("/business");
|
||||
design.setLayoutJson("{\"canvas\":\"business-blocks-v1\",\"blocks\":["
|
||||
+ "{\"id\":\"notice_001\",\"blockCode\":\"notice\",\"config\":{"
|
||||
+ "\"noticeTable\":\"sys_notice\",\"title\":\"noticeTitle\",\"content\":\"noticeContent\",\"status\":\"status\",\"publishTime\":\"createTime\"}},"
|
||||
+ "{\"id\":\"carousel_001\",\"blockCode\":\"carousel\",\"config\":{"
|
||||
+ "\"carouselTable\":\"home_banner\",\"image\":\"imageUrl\",\"title\":\"title\",\"link\":\"linkUrl\",\"sort\":\"sortNo\",\"status\":\"status\"}},"
|
||||
+ "{\"id\":\"order_001\",\"blockCode\":\"order\",\"config\":{"
|
||||
+ "\"orderTable\":\"shop_order\",\"orderItemTable\":\"shop_order_item\",\"productTable\":\"shop_product\","
|
||||
+ "\"orderUserId\":\"userId\",\"orderNo\":\"orderNo\",\"orderAmount\":\"totalAmount\",\"orderStatus\":\"status\",\"orderCreateTime\":\"createTime\","
|
||||
+ "\"itemOrderId\":\"orderId\",\"itemProductId\":\"productId\",\"itemQuantity\":\"quantity\",\"itemPrice\":\"price\","
|
||||
+ "\"productId\":\"id\",\"productName\":\"name\",\"productImage\":\"imageUrl\",\"productPrice\":\"price\"}},"
|
||||
+ "{\"id\":\"master_detail_001\",\"blockCode\":\"master_detail\",\"config\":{"
|
||||
+ "\"masterTable\":\"library_book\",\"detailTable\":\"library_borrowing\","
|
||||
+ "\"masterId\":\"id\",\"detailMasterId\":\"bookId\",\"masterTitle\":\"bookName\",\"masterDescription\":\"description\","
|
||||
+ "\"detailTitle\":\"borrowerName\",\"detailValue\":\"status\",\"detailTime\":\"createTime\"}}"
|
||||
+ "]}");
|
||||
return design;
|
||||
}
|
||||
|
||||
private FrontProjectPageDesign embeddedBlocksPageDesign()
|
||||
{
|
||||
FrontProjectPageDesign design = new FrontProjectPageDesign();
|
||||
design.setPageCode("book_catalog");
|
||||
design.setPageName("Book Catalog");
|
||||
design.setPageScope("frontend");
|
||||
design.setPageType("list");
|
||||
design.setRoutePath("/books");
|
||||
design.setTableName("library_book");
|
||||
design.setLayoutJson("{\"canvas\":\"frontend-list-v1\",\"regions\":[{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"bookName\"]}],"
|
||||
+ "\"embeddedBlocks\":{\"top\":[{\"id\":\"notice_001\",\"blockCode\":\"notice\",\"config\":{"
|
||||
+ "\"noticeTable\":\"sys_notice\",\"title\":\"noticeTitle\",\"content\":\"noticeContent\",\"publishTime\":\"createTime\"}}],"
|
||||
+ "\"bottom\":[{\"id\":\"carousel_001\",\"blockCode\":\"carousel\",\"config\":{"
|
||||
+ "\"carouselTable\":\"home_banner\",\"image\":\"imageUrl\",\"title\":\"title\",\"link\":\"linkUrl\"}}]}}");
|
||||
return design;
|
||||
}
|
||||
|
||||
private GenTable cartTable()
|
||||
{
|
||||
GenTable table = table(201L, "shop_cart", "ShopCart");
|
||||
table.setColumns(Arrays.asList(
|
||||
column("id", "id", "1"),
|
||||
column("product_id", "productId", "0"),
|
||||
column("quantity", "quantity", "0"),
|
||||
column("user_id", "userId", "0")));
|
||||
table.setPkColumn(table.getColumns().get(0));
|
||||
return table;
|
||||
}
|
||||
|
||||
private GenTable productTable()
|
||||
{
|
||||
GenTable table = table(202L, "shop_product", "ShopProduct");
|
||||
table.setColumns(Arrays.asList(
|
||||
column("id", "id", "1"),
|
||||
column("name", "name", "0"),
|
||||
column("image_url", "imageUrl", "0"),
|
||||
column("price", "price", "0"),
|
||||
column("stock", "stock", "0")));
|
||||
table.setPkColumn(table.getColumns().get(0));
|
||||
return table;
|
||||
}
|
||||
|
||||
private GenTable noticeTable()
|
||||
{
|
||||
GenTable table = table(203L, "sys_notice", "SysNotice");
|
||||
table.setColumns(Arrays.asList(
|
||||
column("id", "id", "1"),
|
||||
column("notice_title", "noticeTitle", "0"),
|
||||
column("notice_content", "noticeContent", "0"),
|
||||
column("status", "status", "0"),
|
||||
column("create_time", "createTime", "0")));
|
||||
table.setPkColumn(table.getColumns().get(0));
|
||||
return table;
|
||||
}
|
||||
|
||||
private GenTable carouselTable()
|
||||
{
|
||||
GenTable table = table(204L, "home_banner", "HomeBanner");
|
||||
table.setColumns(Arrays.asList(
|
||||
column("id", "id", "1"),
|
||||
column("image_url", "imageUrl", "0"),
|
||||
column("title", "title", "0"),
|
||||
column("link_url", "linkUrl", "0"),
|
||||
column("sort_no", "sortNo", "0"),
|
||||
column("status", "status", "0")));
|
||||
table.setPkColumn(table.getColumns().get(0));
|
||||
return table;
|
||||
}
|
||||
|
||||
private GenTable orderTable()
|
||||
{
|
||||
GenTable table = table(205L, "shop_order", "ShopOrder");
|
||||
table.setColumns(Arrays.asList(
|
||||
column("id", "id", "1"),
|
||||
column("user_id", "userId", "0"),
|
||||
column("order_no", "orderNo", "0"),
|
||||
column("total_amount", "totalAmount", "0"),
|
||||
column("status", "status", "0"),
|
||||
column("create_time", "createTime", "0")));
|
||||
table.setPkColumn(table.getColumns().get(0));
|
||||
return table;
|
||||
}
|
||||
|
||||
private GenTable orderItemTable()
|
||||
{
|
||||
GenTable table = table(206L, "shop_order_item", "ShopOrderItem");
|
||||
table.setColumns(Arrays.asList(
|
||||
column("id", "id", "1"),
|
||||
column("order_id", "orderId", "0"),
|
||||
column("product_id", "productId", "0"),
|
||||
column("quantity", "quantity", "0"),
|
||||
column("price", "price", "0")));
|
||||
table.setPkColumn(table.getColumns().get(0));
|
||||
return table;
|
||||
}
|
||||
|
||||
private GenTable masterTable()
|
||||
{
|
||||
GenTable table = table(207L, "library_book", "LibraryBook");
|
||||
table.setColumns(Arrays.asList(
|
||||
column("id", "id", "1"),
|
||||
column("book_name", "bookName", "0"),
|
||||
column("description", "description", "0")));
|
||||
table.setPkColumn(table.getColumns().get(0));
|
||||
return table;
|
||||
}
|
||||
|
||||
private GenTable detailTable()
|
||||
{
|
||||
GenTable table = table(208L, "library_borrowing", "LibraryBorrowing");
|
||||
table.setColumns(Arrays.asList(
|
||||
column("id", "id", "1"),
|
||||
column("book_id", "bookId", "0"),
|
||||
column("borrower_name", "borrowerName", "0"),
|
||||
column("status", "status", "0"),
|
||||
column("create_time", "createTime", "0")));
|
||||
table.setPkColumn(table.getColumns().get(0));
|
||||
return table;
|
||||
}
|
||||
|
||||
private GenTable table(Long tableId, String tableName, String className)
|
||||
{
|
||||
GenTable table = new GenTable();
|
||||
table.setTableId(tableId);
|
||||
table.setTableName(tableName);
|
||||
table.setClassName(className);
|
||||
return table;
|
||||
}
|
||||
|
||||
private GenTableColumn column(String columnName, String javaField, String isPk)
|
||||
{
|
||||
GenTableColumn column = new GenTableColumn();
|
||||
column.setColumnName(columnName);
|
||||
column.setJavaField(javaField);
|
||||
column.setIsPk(isPk);
|
||||
return column;
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,13 @@ import com.ruoyi.generator.domain.SysProjectStructure;
|
||||
import com.ruoyi.generator.domain.SysProjectTemplate;
|
||||
import com.ruoyi.generator.domain.Template;
|
||||
import com.ruoyi.generator.domain.TemplateFile;
|
||||
import com.ruoyi.generator.domain.GeneratedBusinessBlockFile;
|
||||
import com.ruoyi.generator.domain.front.FrontProjectPageDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
|
||||
import com.ruoyi.generator.domain.front.dto.BusinessActionUiBindingDesign;
|
||||
import com.ruoyi.generator.mapper.GenProjectMapper;
|
||||
import com.ruoyi.generator.mapper.GenTableMapper;
|
||||
import com.ruoyi.generator.service.front.BusinessBlockRegistryService;
|
||||
import com.ruoyi.generator.util.VelocityInitializer;
|
||||
import com.ruoyi.generator.util.VelocityUtils;
|
||||
import org.apache.velocity.VelocityContext;
|
||||
@@ -24,14 +28,20 @@ import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -73,6 +83,10 @@ public class GenProjectServiceImplTest {
|
||||
setField("sysProjectModuleService", sysProjectModuleService);
|
||||
setField("sysProjectTemplateService", sysProjectTemplateService);
|
||||
setField("sysProjectStructureService", sysProjectStructureService);
|
||||
BusinessBlockGenerationService businessBlockGenerationService = new BusinessBlockGenerationService();
|
||||
setObjectField(businessBlockGenerationService, BusinessBlockGenerationService.class,
|
||||
"businessBlockRegistryService", new BusinessBlockRegistryService());
|
||||
trySetField("businessBlockGenerationService", businessBlockGenerationService);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -178,6 +192,158 @@ public class GenProjectServiceImplTest {
|
||||
assertFalse(content.contains("return_book"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void previewCodeGroupsBusinessActionsByFrontendPageSlot() {
|
||||
GenProject project = project();
|
||||
project.setBusinessActions(Arrays.asList(
|
||||
businessActionWithBinding("borrow_book", "sys_user", "book_detail_page", "detail.primaryActions"),
|
||||
businessActionWithBinding("return_book", "sys_user", "my_borrows_page", "list.rowActions")
|
||||
));
|
||||
GenTable table = table(11L, "sys_user", "User");
|
||||
|
||||
when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
|
||||
when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
|
||||
when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
|
||||
when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList(projectModule(1L)));
|
||||
when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
|
||||
when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "frontend")).thenReturn(projectTemplate(100L));
|
||||
when(templateFileService.selectTemplateFilesByTemplateId(100L)).thenReturn(Arrays.asList(
|
||||
templateFile("detail.vue.vm", "#set($actions = $slotBusinessActions.get(\"book_detail_page:detail.primaryActions\"))#foreach($action in $actions)${action.code} #end")
|
||||
));
|
||||
|
||||
Map<String, String> preview = service.previewCode(1L, 11L, "detail.vue.vm", "frontend");
|
||||
|
||||
String content = preview.get("detail.vue.vm");
|
||||
assertTrue(content.contains("borrow_book"));
|
||||
assertFalse(content.contains("return_book"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void previewCodeUsesPageDesignActionPositionsForBusinessActionButtons() {
|
||||
GenProject project = project();
|
||||
project.setBusinessActions(Arrays.asList(
|
||||
businessAction("approve_book", "sys_user"),
|
||||
businessAction("return_book", "sys_user"),
|
||||
businessAction("sync_role", "sys_role")
|
||||
));
|
||||
project.setPageDesigns(Arrays.asList(pageDesign("user_page", "sys_user",
|
||||
"{\"canvas\":\"frontend-list-v1\",\"regions\":["
|
||||
+ "{\"id\":\"toolbar\",\"type\":\"toolbar\",\"actions\":[\"create\",\"business:approve_book\"]},"
|
||||
+ "{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"id\"],\"rowActions\":[\"edit\",\"business:return_book\"]},"
|
||||
+ "{\"id\":\"form\",\"type\":\"dialogForm\",\"fields\":[\"id\"]}"
|
||||
+ "]}")));
|
||||
GenTable table = table(11L, "sys_user", "User");
|
||||
|
||||
when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
|
||||
when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
|
||||
when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
|
||||
when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList(projectModule(1L)));
|
||||
when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
|
||||
when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "frontend")).thenReturn(projectTemplate(100L));
|
||||
when(templateFileService.selectTemplateFilesByTemplateId(100L)).thenReturn(Arrays.asList(
|
||||
templateFile("list.vue.vm",
|
||||
"#foreach($action in $pageDesignToolbarBusinessActions)toolbar:${action.code}:${action.path};#end"
|
||||
+ "#foreach($action in $pageDesignRowBusinessActions)row:${action.code}:${action.path};#end"
|
||||
+ "#foreach($action in $pageDesignBusinessActions)all:${action.code};#end")
|
||||
));
|
||||
|
||||
Map<String, String> preview = service.previewCode(1L, 11L, "list.vue.vm", "frontend");
|
||||
|
||||
String content = preview.get("list.vue.vm");
|
||||
assertTrue(content.contains("toolbar:approve_book:/approve_book;"));
|
||||
assertTrue(content.contains("row:return_book:/return_book;"));
|
||||
assertTrue(content.contains("all:approve_book;"));
|
||||
assertTrue(content.contains("all:return_book;"));
|
||||
assertFalse(content.contains("sync_role"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void previewCodeUsesPageDesignedBusinessActionsFromOtherOwnerTables() {
|
||||
GenProject project = project();
|
||||
project.setBusinessActions(Arrays.asList(
|
||||
businessAction("borrow_book", "borrow_record"),
|
||||
businessAction("reserve_book", "reservation")
|
||||
));
|
||||
project.setPageDesigns(Arrays.asList(pageDesign("book_catalog_page", "book",
|
||||
"{\"canvas\":\"frontend-list-card-v1\",\"regions\":["
|
||||
+ "{\"id\":\"card\",\"type\":\"cardList\",\"fields\":[\"id\"]},"
|
||||
+ "{\"id\":\"cardActions\",\"type\":\"actions\",\"actions\":[\"business:borrow_book\",\"delete\"]}"
|
||||
+ "]}")));
|
||||
GenTable table = table(11L, "book", "Book");
|
||||
|
||||
when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
|
||||
when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
|
||||
when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
|
||||
when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList(projectModule(1L)));
|
||||
when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
|
||||
when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "frontend")).thenReturn(projectTemplate(100L));
|
||||
when(templateFileService.selectTemplateFilesByTemplateId(100L)).thenReturn(Arrays.asList(
|
||||
templateFile("list.vue.vm",
|
||||
"#foreach($action in $pageDesignRowBusinessActions)row:${action.code}:${action.ownerTable};#end"
|
||||
+ "#foreach($action in $pageDesignBusinessActions)all:${action.code};#end")
|
||||
));
|
||||
|
||||
Map<String, String> preview = service.previewCode(1L, 11L, "list.vue.vm", "frontend");
|
||||
|
||||
String content = preview.get("list.vue.vm");
|
||||
assertTrue(content.contains("row:borrow_book:borrow_record;"));
|
||||
assertTrue(content.contains("all:borrow_book;"));
|
||||
assertFalse(content.contains("reserve_book"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void previewCodeUsesBundledFrontendIndexWhenDefaultRunnableTemplateIsStale() {
|
||||
GenProject project = project();
|
||||
GenTable table = table(11L, "sys_user", "User");
|
||||
TemplateFile staleTemplate = templateFile("frontend-index.vue.vm",
|
||||
"#set($designed = $hasCurrentFrontendPageDesign)\n"
|
||||
+ "<template><div>old page design template</div></template>\n"
|
||||
+ "<script>export default { data() { return { businessActionMap: {} } } }</script>");
|
||||
staleTemplate.setTemplateId(9203L);
|
||||
|
||||
when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
|
||||
when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
|
||||
when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
|
||||
when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList());
|
||||
when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
|
||||
when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "frontend")).thenReturn(projectTemplate(9203L));
|
||||
when(templateFileService.selectTemplateFilesByTemplateId(9203L)).thenReturn(Arrays.asList(staleTemplate));
|
||||
|
||||
Map<String, String> preview = service.previewCode(1L, 11L, "frontend-index.vue.vm", "frontend");
|
||||
|
||||
String content = preview.get("frontend-index.vue.vm");
|
||||
assertFalse(content.contains("old page design template"));
|
||||
assertTrue(content.contains("portal-page"));
|
||||
assertTrue(content.contains("content-board"));
|
||||
assertTrue(content.contains("businessActionMap"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void previewCodeUsesBundledControllerWhenDefaultRunnableTemplateIsStale() {
|
||||
GenProject project = project();
|
||||
GenTable table = table(11L, "sys_user", "User");
|
||||
table.setColumns(Arrays.asList(pkColumn(11L), column(11L, "user_id", "userId")));
|
||||
table.setPkColumn(table.getColumns().get(0));
|
||||
TemplateFile staleTemplate = templateFile("controller.java.vm",
|
||||
"package ${packageName}.controller; public class ${ClassName}Controller {}");
|
||||
staleTemplate.setTemplateId(9201L);
|
||||
|
||||
when(genProjectMapper.selectGenProjectById(1L)).thenReturn(project);
|
||||
when(genTableMapper.selectGenTablesByProjectId(1L)).thenReturn(Arrays.asList(table));
|
||||
when(genTableMapper.selectGenTableById(11L)).thenReturn(table);
|
||||
when(sysProjectModuleService.selectSysProjectModuleList(any(SysProjectModule.class))).thenReturn(Arrays.asList());
|
||||
when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList());
|
||||
when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "backend")).thenReturn(projectTemplate(9201L));
|
||||
when(templateFileService.selectTemplateFilesByTemplateId(9201L)).thenReturn(Arrays.asList(staleTemplate));
|
||||
|
||||
Map<String, String> preview = service.previewCode(1L, 11L, "controller.java.vm", "backend");
|
||||
|
||||
String content = preview.get("controller.java.vm");
|
||||
assertFalse(content.contains("public class UserController {}"));
|
||||
assertTrue(content.contains("@GetMapping(\"/page\")"));
|
||||
assertTrue(content.contains("__currentUserOnly"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void businessBlueprintSqlTemplateGeneratesExecutableActionEffects() throws Exception {
|
||||
Path sqlPath = Paths.get("sql/business_blueprint_templates.sql");
|
||||
@@ -217,6 +383,66 @@ public class GenProjectServiceImplTest {
|
||||
assertFalse(sql.contains("insert into generated_menu"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void businessBlueprintSqlVueTemplatesRenderFrontendActionSlots() throws Exception {
|
||||
String sql = readBusinessBlueprintSql();
|
||||
|
||||
assertTrue(sql.contains("@click.stop=\"openDetail(row)\""));
|
||||
assertTrue(sql.contains("\u67e5\u770b\u8be6\u60c5"));
|
||||
assertTrue(sql.contains("$hasListRowBusinessActions"));
|
||||
assertTrue(sql.contains("$listRowBusinessActions"));
|
||||
assertTrue(sql.contains("$hasDetailPrimaryBusinessActions"));
|
||||
assertTrue(sql.contains("$detailPrimaryBusinessActions"));
|
||||
assertTrue(sql.contains("resolveSlotActions"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void businessBlueprintBackendTemplateGeneratesRunnableSpringBootProject() throws Exception {
|
||||
String sql = readBusinessBlueprintSql();
|
||||
|
||||
assertTrue(sql.contains("<artifactId>spring-boot-starter-parent</artifactId>"));
|
||||
assertTrue(sql.contains("<ruoyi.version>3.8.8</ruoyi.version>"));
|
||||
assertTrue(sql.contains("<version>${ruoyi.version}</version>"));
|
||||
assertTrue(sql.contains("<artifactId>mysql-connector-java</artifactId>"));
|
||||
assertTrue(sql.contains("<artifactId>spring-boot-maven-plugin</artifactId>"));
|
||||
assertTrue(sql.contains("Application.java.vm"));
|
||||
assertTrue(sql.contains("@MapperScan(\"${packageName}.mapper\")"));
|
||||
assertTrue(sql.contains("SecurityAutoConfiguration.class"));
|
||||
assertTrue(sql.contains("SpringApplication.run(${projectClassName}Application.class, args);"));
|
||||
assertTrue(sql.contains("port: ${d}{SERVER_PORT:8080}"));
|
||||
assertTrue(sql.contains("url: ${d}{DB_URL:jdbc:mysql://localhost:3306/vip?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8}"));
|
||||
assertTrue(sql.contains("mapperLocations: classpath:mapper/**/*.xml"));
|
||||
assertFalse(sql.contains("mapperLocations: classpath*:mapper/**/*.xml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void businessBlueprintAdminTemplateGeneratesRunnableVueCliProject() throws Exception {
|
||||
String sql = readBusinessBlueprintSql();
|
||||
String packageJson = extractTemplateSqlContent(sql, "910201");
|
||||
|
||||
assertTrue(packageJson.contains("\"dev\": \"vue-cli-service serve --host 0.0.0.0\""));
|
||||
assertTrue(packageJson.contains("\"@vue/cli-service\":"));
|
||||
assertTrue(packageJson.contains("\"@vue/cli-plugin-babel\":"));
|
||||
assertTrue(packageJson.contains("\"vue-template-compiler\":"));
|
||||
assertTrue(packageJson.contains("\"axios\":"));
|
||||
assertTrue(packageJson.contains("\"core-js\":"));
|
||||
|
||||
for (String templateFileId : Arrays.asList("910206", "910207", "910208", "910209", "910210", "910211", "910212", "910213")) {
|
||||
assertFalse(extractTemplateSqlContent(sql, templateFileId).trim().isEmpty());
|
||||
}
|
||||
|
||||
assertTrue(sql.contains("'public', 'folder', NULL, 9102"));
|
||||
assertTrue(sql.contains("'index.html', 'file', NULL, 9102, -1, 'index.html.vm'"));
|
||||
assertTrue(sql.contains("'main.js', 'file', NULL, 9102, -1, 'main.js.vm'"));
|
||||
assertTrue(sql.contains("'App.vue', 'file', NULL, 9102, -1, 'App.vue.vm'"));
|
||||
assertTrue(sql.contains("'router', 'folder', NULL, 9102"));
|
||||
assertTrue(sql.contains("'index.js', 'file', NULL, 9102, -1, 'router.js.vm'"));
|
||||
assertTrue(sql.contains("'utils', 'folder', NULL, 9102"));
|
||||
assertTrue(sql.contains("'request.js', 'file', NULL, 9102, -1, 'request.js.vm'"));
|
||||
assertTrue(sql.contains("'components', 'folder', NULL, 9102"));
|
||||
assertTrue(sql.contains("'Pagination.vue', 'file', NULL, 9102, -1, 'Pagination.vue.vm'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void businessBlueprintSqlVueTemplatesAreVelocityParseable() throws Exception {
|
||||
String sql = readBusinessBlueprintSql();
|
||||
@@ -235,6 +461,41 @@ public class GenProjectServiceImplTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void businessBlueprintAdminShellTemplatesAreVelocityParseable() throws Exception {
|
||||
String sql = readBusinessBlueprintSql();
|
||||
GenProject project = project();
|
||||
VelocityContext context = VelocityUtils.prepareContextProject(project);
|
||||
VelocityInitializer.initVelocity();
|
||||
|
||||
for (String templateFileId : Arrays.asList("910206", "910207", "910208", "910209", "910210", "910211", "910212", "910213")) {
|
||||
StringWriter output = new StringWriter();
|
||||
Velocity.evaluate(context, output, "", extractTemplateSqlContent(sql, templateFileId));
|
||||
assertFalse(output.toString().trim().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void qingControllerUsesAddOnlySaveWhenEditDisabled() throws Exception {
|
||||
GenTable table = table(11L, "order_info", "Order");
|
||||
table.setPackageName("com.example.demo");
|
||||
table.setPkColumn(table.getColumns().get(0));
|
||||
table.setAllowAdd("1");
|
||||
table.setAllowEdit("0");
|
||||
table.setAllowRemove("0");
|
||||
table.setRemoveType("none");
|
||||
VelocityInitializer.initVelocity();
|
||||
|
||||
StringWriter output = new StringWriter();
|
||||
Velocity.evaluate(VelocityUtils.prepareContext(table), output, "", readQingControllerTemplate());
|
||||
|
||||
String content = output.toString();
|
||||
assertTrue(content.contains("orderService.save(order);"));
|
||||
assertFalse(content.contains("saveOrUpdate"));
|
||||
assertFalse(content.contains("updateById"));
|
||||
assertFalse(content.contains("@DeleteMapping"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getProjectStructureForDraftProjectUsesFirstActiveTemplateWithoutLoadingProject() {
|
||||
GenProject project = project();
|
||||
@@ -297,6 +558,31 @@ public class GenProjectServiceImplTest {
|
||||
assertEquals("0", structureCaptor.getValue().getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void frontDraftProjectStructurePrefersRunnableFrontendTemplateWhenAvailable() {
|
||||
GenProject project = project();
|
||||
project.setFrontDraft(true);
|
||||
project.setTables(Arrays.asList(table(11L, "sys_user", "User")));
|
||||
|
||||
when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(
|
||||
template(9103L, 0L),
|
||||
template(9203L, 0L)
|
||||
));
|
||||
when(sysProjectStructureService.selectSysProjectStructureList(any(SysProjectStructure.class))).thenReturn(Arrays.asList(
|
||||
structure(1L, 0L, "{projectName}", "folder", null, null),
|
||||
structure(2L, 1L, "index.vue", "file", null, "frontend-index.vue.vm")
|
||||
));
|
||||
|
||||
List<Map<String, Object>> structure = service.getProjectStructure(project, "frontend");
|
||||
|
||||
assertEquals("demo-app", structure.get(0).get("name"));
|
||||
|
||||
ArgumentCaptor<SysProjectStructure> structureCaptor = ArgumentCaptor.forClass(SysProjectStructure.class);
|
||||
verify(sysProjectStructureService).selectSysProjectStructureList(structureCaptor.capture());
|
||||
assertEquals(Long.valueOf(9203L), structureCaptor.getValue().getTemplateId());
|
||||
assertEquals("0", structureCaptor.getValue().getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getProjectStructureReplacesAndGroupsTableScopedModuleFolders() {
|
||||
GenProject project = project();
|
||||
@@ -380,6 +666,135 @@ public class GenProjectServiceImplTest {
|
||||
verify(sysProjectModuleService, never()).selectSysProjectModuleList(any(SysProjectModule.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sqlProjectStructureShowsSqlFilesUnderSqlFolder() {
|
||||
GenProject project = project();
|
||||
project.setFrontDraft(true);
|
||||
GenTable table = table(11L, "sys_user", "User");
|
||||
table.setBusinessName("user");
|
||||
table.setCreateTableSql("create table sys_user (id bigint);");
|
||||
project.setTables(Arrays.asList(table));
|
||||
|
||||
List<Map<String, Object>> structure = service.getProjectStructure(project, "sql");
|
||||
|
||||
Map<String, Object> sqlFolder = structure.get(0);
|
||||
assertEquals("sql", sqlFolder.get("name"));
|
||||
assertEquals("folder", sqlFolder.get("type"));
|
||||
Map<String, Object> sqlFile = child(sqlFolder, 0);
|
||||
assertEquals("user.sql", sqlFile.get("name"));
|
||||
assertEquals("file", sqlFile.get("type"));
|
||||
assertEquals("sql", sqlFile.get("category"));
|
||||
assertEquals(Long.valueOf(11L), sqlFile.get("tableId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sqlPreviewReturnsCreateTableSqlForSelectedTable() {
|
||||
GenProject project = project();
|
||||
project.setFrontDraft(true);
|
||||
GenTable table = table(11L, "sys_user", "User");
|
||||
table.setBusinessName("user");
|
||||
table.setCreateTableSql("create table sys_user (id bigint);");
|
||||
project.setTables(Arrays.asList(table));
|
||||
|
||||
Map<String, String> preview = service.previewCode(project, 11L, "user.sql", "sql");
|
||||
|
||||
assertEquals(1, preview.size());
|
||||
assertTrue(preview.get("user.sql").contains("create table sys_user"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sqlDownloadPackagesFilesUnderSqlFolder() throws Exception {
|
||||
GenProject project = project();
|
||||
project.setFrontDraft(true);
|
||||
GenTable table = table(11L, "sys_user", "User");
|
||||
table.setBusinessName("user");
|
||||
table.setCreateTableSql("create table sys_user (id bigint);");
|
||||
project.setTables(Arrays.asList(table));
|
||||
|
||||
byte[] data = service.downloadStructure(project, "sql");
|
||||
|
||||
Set<String> entries = zipEntries(data);
|
||||
assertTrue(entries.contains("sql/user.sql"));
|
||||
assertFalse(entries.contains("user.sql"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getProjectStructureAddsBusinessBlockFrontendFiles() {
|
||||
GenProject project = businessBlockProject();
|
||||
project.setFrontDraft(true);
|
||||
|
||||
when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(template(9203L, 0L)));
|
||||
when(sysProjectStructureService.selectSysProjectStructureList(any(SysProjectStructure.class))).thenReturn(Arrays.asList(
|
||||
structure(1L, 0L, "{projectName}", "folder", null, null),
|
||||
structure(2L, 1L, "src", "folder", null, null)
|
||||
));
|
||||
|
||||
List<Map<String, Object>> structure = service.getProjectStructure(project, "frontend");
|
||||
|
||||
Set<String> paths = structurePaths(structure);
|
||||
assertTrue(paths.contains("demo-app/src/views/pages/cart_page/index.vue"));
|
||||
assertTrue(paths.contains("demo-app/src/views/pages/cart_page/blocks/cart_001/CartBlock.vue"));
|
||||
assertTrue(paths.contains("demo-app/src/api/business-blocks/cart_001.js"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getProjectStructureAddsNormalFrontendPageDesignFiles() {
|
||||
GenProject project = project();
|
||||
project.setFrontDraft(true);
|
||||
FrontProjectPageDesign design = pageDesign("book_catalog_page", "sys_user",
|
||||
"{\"navigation\":{\"showInMenu\":true},\"regions\":[{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"id\"],\"rowActions\":[\"view\"]}]}");
|
||||
design.setPageName("Books");
|
||||
design.setRoutePath("/books");
|
||||
project.setPageDesigns(Arrays.asList(design));
|
||||
|
||||
when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(template(9203L, 0L)));
|
||||
when(sysProjectStructureService.selectSysProjectStructureList(any(SysProjectStructure.class))).thenReturn(Arrays.asList(
|
||||
structure(1L, 0L, "{projectName}", "folder", null, null),
|
||||
structure(2L, 1L, "src", "folder", null, null)
|
||||
));
|
||||
|
||||
List<Map<String, Object>> structure = service.getProjectStructure(project, "frontend");
|
||||
|
||||
Set<String> paths = structurePaths(structure);
|
||||
assertTrue(paths.contains("demo-app/src/views/pages/book_catalog_page/index.vue"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void previewCodeReturnsBusinessBlockFileByCategory() {
|
||||
GenProject project = businessBlockProject();
|
||||
project.setFrontDraft(true);
|
||||
BusinessBlockGenerationService generationService = new BusinessBlockGenerationService();
|
||||
setObjectFieldUnchecked(generationService, BusinessBlockGenerationService.class,
|
||||
"businessBlockRegistryService", new BusinessBlockRegistryService());
|
||||
GeneratedBusinessBlockFile file = findBusinessBlockPath(generationService.listFiles(project, "frontend"),
|
||||
"src/views/pages/cart_page/blocks/cart_001/CartBlock.vue");
|
||||
|
||||
Map<String, String> preview = service.previewCode(project, null, file.getCategory(), "frontend");
|
||||
|
||||
assertEquals(1, preview.size());
|
||||
assertTrue(preview.containsKey(file.getOutputPath()));
|
||||
assertTrue(preview.get(file.getOutputPath()).contains("listCart001BlockItems"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void downloadStructurePackagesBusinessBlockFiles() throws Exception {
|
||||
GenProject project = businessBlockProject();
|
||||
project.setFrontDraft(true);
|
||||
|
||||
when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(template(9203L, 0L)));
|
||||
when(sysProjectStructureService.selectSysProjectStructureList(any(SysProjectStructure.class))).thenReturn(Arrays.asList(
|
||||
structure(1L, 0L, "{projectName}", "folder", null, null),
|
||||
structure(2L, 1L, "src", "folder", null, null)
|
||||
));
|
||||
|
||||
byte[] data = service.downloadStructure(project, "frontend");
|
||||
|
||||
Set<String> entries = zipEntries(data);
|
||||
assertTrue(entries.contains("demo-app/src/views/pages/cart_page/index.vue"));
|
||||
assertTrue(entries.contains("demo-app/src/views/pages/cart_page/blocks/cart_001/CartBlock.vue"));
|
||||
assertTrue(entries.contains("demo-app/src/api/business-blocks/cart_001.js"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> child(Map<String, Object> node, int index) {
|
||||
return children(node).get(index);
|
||||
@@ -396,6 +811,27 @@ public class GenProjectServiceImplTest {
|
||||
field.set(service, value);
|
||||
}
|
||||
|
||||
private void trySetField(String name, Object value) throws Exception {
|
||||
try {
|
||||
setField(name, value);
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void setObjectField(Object target, Class<?> targetClass, String name, Object value) throws Exception {
|
||||
Field field = targetClass.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
|
||||
private void setObjectFieldUnchecked(Object target, Class<?> targetClass, String name, Object value) {
|
||||
try {
|
||||
setObjectField(target, targetClass, name, value);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private GenProject project() {
|
||||
GenProject project = new GenProject();
|
||||
project.setProjectId(1L);
|
||||
@@ -500,6 +936,109 @@ public class GenProjectServiceImplTest {
|
||||
return action;
|
||||
}
|
||||
|
||||
private BusinessActionDesign businessActionWithBinding(String code, String ownerTable, String pageCode, String slot) {
|
||||
BusinessActionDesign action = businessAction(code, ownerTable);
|
||||
BusinessActionUiBindingDesign binding = new BusinessActionUiBindingDesign();
|
||||
binding.setTarget("frontend");
|
||||
binding.setPageCode(pageCode);
|
||||
binding.setSlot(slot);
|
||||
binding.setComponent("button");
|
||||
action.setUiBindings(Arrays.asList(binding));
|
||||
return action;
|
||||
}
|
||||
|
||||
private FrontProjectPageDesign pageDesign(String pageCode, String tableName, String layoutJson) {
|
||||
FrontProjectPageDesign design = new FrontProjectPageDesign();
|
||||
design.setProjectId(1L);
|
||||
design.setPageCode(pageCode);
|
||||
design.setPageName(pageCode);
|
||||
design.setPageType("list");
|
||||
design.setTableName(tableName);
|
||||
design.setLayoutJson(layoutJson);
|
||||
design.setActionJson("{\"toolbarActions\":[],\"rowActions\":[]}");
|
||||
return design;
|
||||
}
|
||||
|
||||
private GenProject businessBlockProject() {
|
||||
GenProject project = project();
|
||||
GenTable cart = table(201L, "shop_cart", "ShopCart");
|
||||
cart.setColumns(Arrays.asList(
|
||||
pkColumn(201L),
|
||||
column(201L, "product_id", "productId"),
|
||||
column(201L, "quantity", "quantity"),
|
||||
column(201L, "user_id", "userId")));
|
||||
cart.setPkColumn(cart.getColumns().get(0));
|
||||
GenTable product = table(202L, "shop_product", "ShopProduct");
|
||||
product.setColumns(Arrays.asList(
|
||||
pkColumn(202L),
|
||||
column(202L, "name", "name"),
|
||||
column(202L, "price", "price"),
|
||||
column(202L, "stock", "stock")));
|
||||
product.setPkColumn(product.getColumns().get(0));
|
||||
project.setTables(Arrays.asList(cart, product));
|
||||
project.setPageDesigns(Arrays.asList(businessBlockPageDesign()));
|
||||
return project;
|
||||
}
|
||||
|
||||
private FrontProjectPageDesign businessBlockPageDesign() {
|
||||
FrontProjectPageDesign design = pageDesign("cart_page", "shop_cart",
|
||||
"{\"canvas\":\"business-blocks-v1\",\"blocks\":[{\"id\":\"cart_001\",\"blockCode\":\"cart\",\"config\":{"
|
||||
+ "\"cartTable\":\"shop_cart\","
|
||||
+ "\"productTable\":\"shop_product\","
|
||||
+ "\"cartProductId\":\"product_id\","
|
||||
+ "\"productId\":\"id\","
|
||||
+ "\"productName\":\"name\","
|
||||
+ "\"price\":\"price\","
|
||||
+ "\"quantity\":\"quantity\","
|
||||
+ "\"userId\":\"user_id\","
|
||||
+ "\"stock\":\"stock\""
|
||||
+ "}}]}");
|
||||
design.setPageName("Cart");
|
||||
design.setPageType("business_block");
|
||||
design.setRoutePath("/cart");
|
||||
return design;
|
||||
}
|
||||
|
||||
private GenTableColumn column(Long tableId, String columnName, String javaField) {
|
||||
GenTableColumn column = new GenTableColumn();
|
||||
column.setTableId(tableId);
|
||||
column.setColumnName(columnName);
|
||||
column.setColumnType("varchar");
|
||||
column.setJavaType("String");
|
||||
column.setJavaField(javaField);
|
||||
column.setIsPk("0");
|
||||
return column;
|
||||
}
|
||||
|
||||
private GeneratedBusinessBlockFile findBusinessBlockPath(List<GeneratedBusinessBlockFile> files, String outputPath) {
|
||||
for (GeneratedBusinessBlockFile file : files) {
|
||||
if (outputPath.equals(file.getOutputPath())) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Missing business block file: " + outputPath);
|
||||
}
|
||||
|
||||
private Set<String> structurePaths(List<Map<String, Object>> structure) {
|
||||
Set<String> paths = new HashSet<String>();
|
||||
collectStructurePaths(paths, "", structure);
|
||||
return paths;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void collectStructurePaths(Set<String> paths, String prefix, List<Map<String, Object>> nodes) {
|
||||
if (nodes == null) {
|
||||
return;
|
||||
}
|
||||
for (Map<String, Object> node : nodes) {
|
||||
String path = prefix.length() == 0 ? (String) node.get("name") : prefix + "/" + node.get("name");
|
||||
if ("file".equals(node.get("type"))) {
|
||||
paths.add(path);
|
||||
}
|
||||
collectStructurePaths(paths, path, (List<Map<String, Object>>) node.get("children"));
|
||||
}
|
||||
}
|
||||
|
||||
private String readBusinessBlueprintSql() throws Exception {
|
||||
Path sqlPath = Paths.get("sql/business_blueprint_templates.sql");
|
||||
if (!Files.exists(sqlPath)) {
|
||||
@@ -508,6 +1047,14 @@ public class GenProjectServiceImplTest {
|
||||
return new String(Files.readAllBytes(sqlPath), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private String readQingControllerTemplate() throws Exception {
|
||||
Path templatePath = Paths.get("ruoyi-generator/src/main/resources/qing/controller.java.vm");
|
||||
if (!Files.exists(templatePath)) {
|
||||
templatePath = Paths.get("src/main/resources/qing/controller.java.vm");
|
||||
}
|
||||
return new String(Files.readAllBytes(templatePath), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private String extractTemplateSqlContent(String sql, String templateFileId) {
|
||||
String marker = "(" + templateFileId + ",";
|
||||
int rowStart = sql.indexOf(marker);
|
||||
@@ -519,4 +1066,16 @@ public class GenProjectServiceImplTest {
|
||||
assertTrue("Template content end not found: " + templateFileId, contentEnd >= 0);
|
||||
return sql.substring(contentStart, contentEnd);
|
||||
}
|
||||
|
||||
private Set<String> zipEntries(byte[] data) throws IOException {
|
||||
Set<String> entries = new HashSet<String>();
|
||||
ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(data));
|
||||
ZipEntry entry;
|
||||
while ((entry = zip.getNextEntry()) != null) {
|
||||
entries.add(entry.getName());
|
||||
zip.closeEntry();
|
||||
}
|
||||
zip.close();
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,564 @@
|
||||
package com.ruoyi.generator.service.front;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.generator.domain.front.FrontProject;
|
||||
import com.ruoyi.generator.domain.front.FrontProjectColumn;
|
||||
import com.ruoyi.generator.domain.front.FrontProjectPageDesign;
|
||||
import com.ruoyi.generator.domain.front.FrontProjectTable;
|
||||
import com.ruoyi.generator.domain.front.dto.FrontendPageDesignInitResponse;
|
||||
import com.ruoyi.generator.domain.front.dto.FrontendPageDesignRequest;
|
||||
import com.ruoyi.generator.domain.front.dto.FrontendPageDesignResponse;
|
||||
import com.ruoyi.generator.mapper.front.FrontProjectColumnMapper;
|
||||
import com.ruoyi.generator.mapper.front.FrontProjectPageDesignMapper;
|
||||
import com.ruoyi.generator.mapper.front.FrontProjectTableMapper;
|
||||
|
||||
public class FrontendPageDesignServiceTest
|
||||
{
|
||||
private FrontendPageDesignService service;
|
||||
|
||||
@Mock
|
||||
private IFrontProjectService frontProjectService;
|
||||
@Mock
|
||||
private FrontProjectPageDesignMapper pageDesignMapper;
|
||||
@Mock
|
||||
private FrontProjectTableMapper frontProjectTableMapper;
|
||||
@Mock
|
||||
private FrontProjectColumnMapper frontProjectColumnMapper;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception
|
||||
{
|
||||
MockitoAnnotations.initMocks(this);
|
||||
service = new FrontendPageDesignService();
|
||||
setField("frontProjectService", frontProjectService);
|
||||
setField("pageDesignMapper", pageDesignMapper);
|
||||
setField("frontProjectTableMapper", frontProjectTableMapper);
|
||||
setField("frontProjectColumnMapper", frontProjectColumnMapper);
|
||||
setField("businessBlockRegistryService", new BusinessBlockRegistryService());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initializeCreatesMissingDesignFromFrontendPages()
|
||||
{
|
||||
when(frontProjectService.getProject(7L, 10L)).thenReturn(project());
|
||||
when(pageDesignMapper.selectDesignsByProjectId(10L)).thenReturn(Collections.<FrontProjectPageDesign>emptyList());
|
||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.singletonList(bookTable()));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(99L)).thenReturn(Arrays.asList(column("title", "title", "1", "1", "1"),
|
||||
column("status", "status", "1", "1", "1")));
|
||||
|
||||
FrontendPageDesignInitResponse response = service.initializePageDesigns(7L, 10L);
|
||||
|
||||
List<FrontendPageDesignResponse> designs = response.getDesigns();
|
||||
assertEquals(1, designs.size());
|
||||
assertEquals("book_catalog_page", designs.get(0).getPageCode());
|
||||
assertTrue(designs.get(0).getLayoutJson().contains("\"type\":\"query\""));
|
||||
assertTrue(designs.get(0).getLayoutJson().contains("\"title\""));
|
||||
verify(pageDesignMapper).insertFrontProjectPageDesign(any(FrontProjectPageDesign.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initializeAdminDesignsMergesAdminMenusAndTables()
|
||||
{
|
||||
when(frontProjectService.getProject(7L, 10L)).thenReturn(projectWithAdminMenu());
|
||||
when(pageDesignMapper.selectDesignsByProjectId(10L)).thenReturn(Collections.<FrontProjectPageDesign>emptyList());
|
||||
FrontProjectTable bookTable = bookTable();
|
||||
bookTable.setAdminMenuName("Book Admin");
|
||||
FrontProjectTable customerTable = customerTable();
|
||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Arrays.asList(bookTable, customerTable));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(99L)).thenReturn(Arrays.asList(column("title", "title", "1", "1", "1")));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(100L)).thenReturn(Arrays.asList(column("customer_name", "customerName", "1", "1", "1")));
|
||||
|
||||
FrontendPageDesignInitResponse response = service.initializePageDesigns(7L, 10L, "admin");
|
||||
|
||||
List<FrontendPageDesignResponse> designs = response.getDesigns();
|
||||
assertEquals(2, designs.size());
|
||||
assertEquals("admin", designs.get(0).getPageScope());
|
||||
assertEquals("book_admin", designs.get(0).getPageCode());
|
||||
assertEquals("book_info", designs.get(0).getTableName());
|
||||
assertEquals("admin_customer_info", designs.get(1).getPageCode());
|
||||
assertEquals("customer_info", designs.get(1).getTableName());
|
||||
assertTrue(designs.get(1).getLayoutJson().contains("\"canvas\":\"admin-list-v1\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createPageDesignPersistsNewFrontendPage()
|
||||
{
|
||||
when(frontProjectService.getProject(7L, 10L)).thenReturn(project());
|
||||
when(pageDesignMapper.selectDesignsByProjectIdAndScope(10L, "frontend"))
|
||||
.thenReturn(Collections.<FrontProjectPageDesign>emptyList());
|
||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.singletonList(bookTable()));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(99L)).thenReturn(Arrays.asList(
|
||||
column("book_title", "bookTitle", "1", "1", "1"),
|
||||
column("author", "author", "0", "1", "1")));
|
||||
FrontendPageDesignRequest request = new FrontendPageDesignRequest();
|
||||
request.setPageCode("custom_books");
|
||||
request.setPageName("Custom Books");
|
||||
request.setRoutePath("/custom-books");
|
||||
request.setPageType("list");
|
||||
request.setTableName("book_info");
|
||||
|
||||
service.createPageDesign(7L, 10L, request, "frontend");
|
||||
|
||||
ArgumentCaptor<FrontProjectPageDesign> captor = ArgumentCaptor.forClass(FrontProjectPageDesign.class);
|
||||
verify(pageDesignMapper).insertFrontProjectPageDesign(captor.capture());
|
||||
assertEquals("frontend", captor.getValue().getPageScope());
|
||||
assertEquals("custom_books", captor.getValue().getPageCode());
|
||||
assertEquals("custom_books", captor.getValue().getMenuCode());
|
||||
assertEquals(Integer.valueOf(1), captor.getValue().getVersion());
|
||||
assertTrue(captor.getValue().getLayoutJson().contains("\"canvas\":\"frontend-list-v1\""));
|
||||
assertTrue(captor.getValue().getLayoutJson().contains("bookTitle"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deletePageDesignRemovesOwnedDesign()
|
||||
{
|
||||
FrontProjectPageDesign current = currentDesign();
|
||||
when(frontProjectService.getProject(7L, 10L)).thenReturn(project());
|
||||
when(pageDesignMapper.selectFrontProjectPageDesignById(88L)).thenReturn(current);
|
||||
|
||||
service.deletePageDesign(7L, 10L, 88L, "frontend");
|
||||
|
||||
verify(pageDesignMapper).deleteFrontProjectPageDesignById(88L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateRejectsUnknownLayoutField()
|
||||
{
|
||||
FrontProjectPageDesign current = currentDesign();
|
||||
when(frontProjectService.getProject(7L, 10L)).thenReturn(project());
|
||||
when(pageDesignMapper.selectFrontProjectPageDesignById(88L)).thenReturn(current);
|
||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.singletonList(bookTable()));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(99L)).thenReturn(Collections.singletonList(column("title", "title", "1", "1", "1")));
|
||||
FrontendPageDesignRequest request = new FrontendPageDesignRequest();
|
||||
request.setVersion(1);
|
||||
request.setLayoutJson("{\"regions\":[{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"missingField\"]}]}");
|
||||
request.setActionJson("{\"toolbarActions\":[],\"rowActions\":[]}");
|
||||
|
||||
ServiceException exception = expectServiceException(new ThrowingRunnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
service.updatePageDesign(7L, 10L, 88L, request);
|
||||
}
|
||||
});
|
||||
|
||||
assertTrue(exception.getMessage().contains("missingField"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updatePersistsValidLayoutAndIncrementsVersion()
|
||||
{
|
||||
FrontProjectPageDesign current = currentDesign();
|
||||
when(frontProjectService.getProject(7L, 10L)).thenReturn(project());
|
||||
when(pageDesignMapper.selectFrontProjectPageDesignById(88L)).thenReturn(current);
|
||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.singletonList(bookTable()));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(99L)).thenReturn(Arrays.asList(column("book_title", "bookTitle", "1", "1", "1")));
|
||||
FrontendPageDesignRequest request = new FrontendPageDesignRequest();
|
||||
request.setVersion(1);
|
||||
request.setPageName("Books");
|
||||
request.setRoutePath("/books");
|
||||
request.setPageType("list");
|
||||
request.setTableName("book_info");
|
||||
request.setLayoutJson("{\"regions\":[{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"book_title\"]}]}");
|
||||
request.setActionJson("{\"toolbarActions\":[{\"code\":\"create\"}],\"rowActions\":[{\"code\":\"view\"}]}");
|
||||
|
||||
service.updatePageDesign(7L, 10L, 88L, request);
|
||||
|
||||
ArgumentCaptor<FrontProjectPageDesign> captor = ArgumentCaptor.forClass(FrontProjectPageDesign.class);
|
||||
verify(pageDesignMapper).updateFrontProjectPageDesign(captor.capture());
|
||||
assertEquals(Long.valueOf(88L), captor.getValue().getDesignId());
|
||||
assertEquals(Integer.valueOf(2), captor.getValue().getVersion());
|
||||
assertTrue(captor.getValue().getLayoutJson().contains("bookTitle"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateRejectsDisabledBuiltinLayoutAction()
|
||||
{
|
||||
FrontProjectPageDesign current = currentDesign();
|
||||
FrontProjectTable table = bookTable();
|
||||
table.setAllowRemove("0");
|
||||
when(frontProjectService.getProject(7L, 10L)).thenReturn(project());
|
||||
when(pageDesignMapper.selectFrontProjectPageDesignById(88L)).thenReturn(current);
|
||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.singletonList(table));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(99L)).thenReturn(Collections.singletonList(column("book_title", "bookTitle", "1", "1", "1")));
|
||||
FrontendPageDesignRequest request = new FrontendPageDesignRequest();
|
||||
request.setVersion(1);
|
||||
request.setPageType("list");
|
||||
request.setTableName("book_info");
|
||||
request.setLayoutJson("{\"regions\":[{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"book_title\"],\"rowActions\":[\"delete\"]}]}");
|
||||
request.setActionJson("{\"toolbarActions\":[],\"rowActions\":[]}");
|
||||
|
||||
ServiceException exception = expectServiceException(new ThrowingRunnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
service.updatePageDesign(7L, 10L, 88L, request);
|
||||
}
|
||||
});
|
||||
|
||||
assertTrue(exception.getMessage().contains("delete"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateRejectsDisabledBuiltinActionJson()
|
||||
{
|
||||
FrontProjectPageDesign current = currentDesign();
|
||||
FrontProjectTable table = bookTable();
|
||||
table.setAllowAdd("0");
|
||||
when(frontProjectService.getProject(7L, 10L)).thenReturn(project());
|
||||
when(pageDesignMapper.selectFrontProjectPageDesignById(88L)).thenReturn(current);
|
||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.singletonList(table));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(99L)).thenReturn(Collections.singletonList(column("book_title", "bookTitle", "1", "1", "1")));
|
||||
FrontendPageDesignRequest request = new FrontendPageDesignRequest();
|
||||
request.setVersion(1);
|
||||
request.setPageType("list");
|
||||
request.setTableName("book_info");
|
||||
request.setLayoutJson("{\"regions\":[{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"book_title\"],\"rowActions\":[]}]}");
|
||||
request.setActionJson("{\"toolbarActions\":[{\"code\":\"create\"}],\"rowActions\":[]}");
|
||||
|
||||
ServiceException exception = expectServiceException(new ThrowingRunnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
service.updatePageDesign(7L, 10L, 88L, request);
|
||||
}
|
||||
});
|
||||
|
||||
assertTrue(exception.getMessage().contains("create"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateRejectsLayoutCanvasOutsidePageType()
|
||||
{
|
||||
FrontProjectPageDesign current = currentDesign();
|
||||
when(frontProjectService.getProject(7L, 10L)).thenReturn(project());
|
||||
when(pageDesignMapper.selectFrontProjectPageDesignById(88L)).thenReturn(current);
|
||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.singletonList(bookTable()));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(99L)).thenReturn(Collections.singletonList(column("book_title", "bookTitle", "1", "1", "1")));
|
||||
FrontendPageDesignRequest request = new FrontendPageDesignRequest();
|
||||
request.setVersion(1);
|
||||
request.setPageType("list");
|
||||
request.setLayoutJson("{\"canvas\":\"frontend-form-group-v1\",\"regions\":[{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"bookTitle\"]}]}");
|
||||
request.setActionJson("{\"toolbarActions\":[],\"rowActions\":[]}");
|
||||
|
||||
ServiceException exception = expectServiceException(new ThrowingRunnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
service.updatePageDesign(7L, 10L, 88L, request);
|
||||
}
|
||||
});
|
||||
|
||||
assertTrue(exception.getMessage().contains("布局"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updatePersistsLayoutCanvasForMatchingPageType()
|
||||
{
|
||||
FrontProjectPageDesign current = currentDesign();
|
||||
when(frontProjectService.getProject(7L, 10L)).thenReturn(project());
|
||||
when(pageDesignMapper.selectFrontProjectPageDesignById(88L)).thenReturn(current);
|
||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.singletonList(bookTable()));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(99L)).thenReturn(Collections.singletonList(column("book_title", "bookTitle", "1", "1", "1")));
|
||||
FrontendPageDesignRequest request = new FrontendPageDesignRequest();
|
||||
request.setVersion(1);
|
||||
request.setPageType("list");
|
||||
request.setLayoutJson("{\"canvas\":\"frontend-list-card-v1\",\"regions\":[{\"id\":\"table\",\"type\":\"table\",\"fields\":[\"book_title\"]}]}");
|
||||
request.setActionJson("{\"toolbarActions\":[],\"rowActions\":[]}");
|
||||
|
||||
service.updatePageDesign(7L, 10L, 88L, request);
|
||||
|
||||
ArgumentCaptor<FrontProjectPageDesign> captor = ArgumentCaptor.forClass(FrontProjectPageDesign.class);
|
||||
verify(pageDesignMapper).updateFrontProjectPageDesign(captor.capture());
|
||||
assertTrue(captor.getValue().getLayoutJson().contains("\"canvas\":\"frontend-list-card-v1\""));
|
||||
assertTrue(captor.getValue().getLayoutJson().contains("bookTitle"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateNormalizesCardRegionFieldsAndImageField()
|
||||
{
|
||||
FrontProjectPageDesign current = currentDesign();
|
||||
when(frontProjectService.getProject(7L, 10L)).thenReturn(project());
|
||||
when(pageDesignMapper.selectFrontProjectPageDesignById(88L)).thenReturn(current);
|
||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.singletonList(bookTable()));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(99L)).thenReturn(Arrays.asList(
|
||||
column("book_title", "bookTitle", "1", "1", "1"),
|
||||
column("cover_url", "coverUrl", "0", "1", "1")));
|
||||
FrontendPageDesignRequest request = new FrontendPageDesignRequest();
|
||||
request.setVersion(1);
|
||||
request.setPageType("list");
|
||||
request.setLayoutJson("{\"canvas\":\"frontend-list-card-v1\",\"regions\":["
|
||||
+ "{\"id\":\"query\",\"type\":\"query\",\"visible\":false,\"fields\":[\"book_title\"]},"
|
||||
+ "{\"id\":\"card\",\"type\":\"cardList\",\"titleField\":\"book_title\",\"imageField\":\"cover_url\",\"fields\":[\"book_title\"]},"
|
||||
+ "{\"id\":\"cardActions\",\"type\":\"actions\",\"actions\":[\"edit\"]}"
|
||||
+ "]}");
|
||||
request.setActionJson("{\"toolbarActions\":[],\"rowActions\":[{\"code\":\"edit\"}]}");
|
||||
|
||||
service.updatePageDesign(7L, 10L, 88L, request);
|
||||
|
||||
ArgumentCaptor<FrontProjectPageDesign> captor = ArgumentCaptor.forClass(FrontProjectPageDesign.class);
|
||||
verify(pageDesignMapper).updateFrontProjectPageDesign(captor.capture());
|
||||
assertTrue(captor.getValue().getLayoutJson().contains("\"titleField\":\"bookTitle\""));
|
||||
assertTrue(captor.getValue().getLayoutJson().contains("\"imageField\":\"coverUrl\""));
|
||||
assertTrue(captor.getValue().getLayoutJson().contains("\"visible\":false"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updatePersistsValidBusinessBlockLayout()
|
||||
{
|
||||
FrontProjectPageDesign current = currentBusinessBlockDesign();
|
||||
when(frontProjectService.getProject(7L, 10L)).thenReturn(project());
|
||||
when(pageDesignMapper.selectFrontProjectPageDesignById(88L)).thenReturn(current);
|
||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Arrays.asList(cartTable(), productTable()));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(201L)).thenReturn(Arrays.asList(
|
||||
column("id", "id", "0", "1", "1"),
|
||||
column("product_id", "productId", "0", "1", "1"),
|
||||
column("quantity", "quantity", "0", "1", "1"),
|
||||
column("user_id", "userId", "0", "1", "1")));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(202L)).thenReturn(Arrays.asList(
|
||||
column("id", "id", "0", "1", "1"),
|
||||
column("name", "name", "0", "1", "1"),
|
||||
column("price", "price", "0", "1", "1"),
|
||||
column("stock", "stock", "0", "1", "1")));
|
||||
FrontendPageDesignRequest request = businessBlockRequest(validBusinessBlockLayout());
|
||||
|
||||
service.updatePageDesign(7L, 10L, 88L, request);
|
||||
|
||||
ArgumentCaptor<FrontProjectPageDesign> captor = ArgumentCaptor.forClass(FrontProjectPageDesign.class);
|
||||
verify(pageDesignMapper).updateFrontProjectPageDesign(captor.capture());
|
||||
assertTrue(captor.getValue().getLayoutJson().contains("\"canvas\":\"business-blocks-v1\""));
|
||||
assertTrue(captor.getValue().getLayoutJson().contains("\"blockCode\":\"cart\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateRejectsBusinessBlockMissingRequiredConfig()
|
||||
{
|
||||
ServiceException exception = expectBusinessBlockUpdateFailure("{\"canvas\":\"business-blocks-v1\",\"blocks\":[{\"id\":\"cart_001\",\"blockCode\":\"cart\",\"config\":{\"productTable\":\"shop_product\"}}]}");
|
||||
|
||||
assertTrue(exception.getMessage().contains("cartTable"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateRejectsBusinessBlockUnknownTable()
|
||||
{
|
||||
String layout = validBusinessBlockLayout().replace("\"shop_cart\"", "\"missing_cart\"");
|
||||
|
||||
ServiceException exception = expectBusinessBlockUpdateFailure(layout);
|
||||
|
||||
assertTrue(exception.getMessage().contains("missing_cart"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateRejectsBusinessBlockUnknownField()
|
||||
{
|
||||
String layout = validBusinessBlockLayout().replace("\"name\"", "\"missing_name\"");
|
||||
|
||||
ServiceException exception = expectBusinessBlockUpdateFailure(layout);
|
||||
|
||||
assertTrue(exception.getMessage().contains("missing_name"));
|
||||
}
|
||||
|
||||
private void setField(String name, Object value) throws Exception
|
||||
{
|
||||
Field field = FrontendPageDesignService.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(service, value);
|
||||
}
|
||||
|
||||
private FrontProject project()
|
||||
{
|
||||
FrontProject project = new FrontProject();
|
||||
project.setProjectId(10L);
|
||||
project.setUserId(7L);
|
||||
project.setAppBlueprint("{\"frontendPages\":[{\"code\":\"book_catalog_page\",\"name\":\"Book Catalog\",\"menuCode\":\"book_catalog\",\"path\":\"/books\",\"pageType\":\"list\",\"tableName\":\"book_info\"}]}");
|
||||
return project;
|
||||
}
|
||||
|
||||
private FrontProject projectWithAdminMenu()
|
||||
{
|
||||
FrontProject project = new FrontProject();
|
||||
project.setProjectId(10L);
|
||||
project.setUserId(7L);
|
||||
project.setAppBlueprint("{\"adminMenus\":[{\"code\":\"book_admin\",\"name\":\"Book Admin\",\"path\":\"/admin/books\",\"requiresLogin\":true,\"visibleRoles\":[\"admin\"],\"dataScope\":\"ROLE\"}]}");
|
||||
return project;
|
||||
}
|
||||
|
||||
private FrontProjectTable bookTable()
|
||||
{
|
||||
FrontProjectTable table = new FrontProjectTable();
|
||||
table.setTableId(99L);
|
||||
table.setProjectId(10L);
|
||||
table.setTableName("book_info");
|
||||
table.setAllowAdd("1");
|
||||
table.setAllowEdit("1");
|
||||
table.setAllowRemove("1");
|
||||
return table;
|
||||
}
|
||||
|
||||
private FrontProjectTable customerTable()
|
||||
{
|
||||
FrontProjectTable table = new FrontProjectTable();
|
||||
table.setTableId(100L);
|
||||
table.setProjectId(10L);
|
||||
table.setTableName("customer_info");
|
||||
table.setTableComment("Customer info");
|
||||
table.setAdminMenuName("Customer Management");
|
||||
table.setAllowAdd("1");
|
||||
table.setAllowEdit("1");
|
||||
table.setAllowRemove("1");
|
||||
return table;
|
||||
}
|
||||
|
||||
private FrontProjectTable cartTable()
|
||||
{
|
||||
FrontProjectTable table = new FrontProjectTable();
|
||||
table.setTableId(201L);
|
||||
table.setProjectId(10L);
|
||||
table.setTableName("shop_cart");
|
||||
table.setAllowAdd("1");
|
||||
table.setAllowEdit("1");
|
||||
table.setAllowRemove("1");
|
||||
return table;
|
||||
}
|
||||
|
||||
private FrontProjectTable productTable()
|
||||
{
|
||||
FrontProjectTable table = new FrontProjectTable();
|
||||
table.setTableId(202L);
|
||||
table.setProjectId(10L);
|
||||
table.setTableName("shop_product");
|
||||
table.setAllowAdd("1");
|
||||
table.setAllowEdit("1");
|
||||
table.setAllowRemove("1");
|
||||
return table;
|
||||
}
|
||||
|
||||
private FrontProjectColumn column(String columnName, String javaField, String isQuery, String isList, String isEdit)
|
||||
{
|
||||
FrontProjectColumn column = new FrontProjectColumn();
|
||||
column.setColumnName(columnName);
|
||||
column.setJavaField(javaField);
|
||||
column.setIsQuery(isQuery);
|
||||
column.setIsList(isList);
|
||||
column.setIsEdit(isEdit);
|
||||
column.setIsInsert(isEdit);
|
||||
return column;
|
||||
}
|
||||
|
||||
private FrontProjectPageDesign currentDesign()
|
||||
{
|
||||
FrontProjectPageDesign design = new FrontProjectPageDesign();
|
||||
design.setDesignId(88L);
|
||||
design.setProjectId(10L);
|
||||
design.setUserId(7L);
|
||||
design.setPageCode("book_catalog_page");
|
||||
design.setPageScope("frontend");
|
||||
design.setPageName("Book Catalog");
|
||||
design.setRoutePath("/books");
|
||||
design.setPageType("list");
|
||||
design.setTableName("book_info");
|
||||
design.setVersion(1);
|
||||
return design;
|
||||
}
|
||||
|
||||
private FrontProjectPageDesign currentBusinessBlockDesign()
|
||||
{
|
||||
FrontProjectPageDesign design = currentDesign();
|
||||
design.setPageType("business_block");
|
||||
design.setTableName("shop_cart");
|
||||
design.setLayoutJson("{\"canvas\":\"business-blocks-v1\",\"blocks\":[]}");
|
||||
return design;
|
||||
}
|
||||
|
||||
private FrontendPageDesignRequest businessBlockRequest(String layoutJson)
|
||||
{
|
||||
FrontendPageDesignRequest request = new FrontendPageDesignRequest();
|
||||
request.setVersion(1);
|
||||
request.setPageName("Cart");
|
||||
request.setRoutePath("/cart");
|
||||
request.setPageType("business_block");
|
||||
request.setTableName("shop_cart");
|
||||
request.setLayoutJson(layoutJson);
|
||||
request.setActionJson("{\"toolbarActions\":[],\"rowActions\":[]}");
|
||||
return request;
|
||||
}
|
||||
|
||||
private String validBusinessBlockLayout()
|
||||
{
|
||||
return "{\"canvas\":\"business-blocks-v1\",\"blocks\":[{\"id\":\"cart_001\",\"blockCode\":\"cart\",\"config\":{"
|
||||
+ "\"cartTable\":\"shop_cart\","
|
||||
+ "\"productTable\":\"shop_product\","
|
||||
+ "\"cartProductId\":\"product_id\","
|
||||
+ "\"productId\":\"id\","
|
||||
+ "\"productName\":\"name\","
|
||||
+ "\"price\":\"price\","
|
||||
+ "\"quantity\":\"quantity\","
|
||||
+ "\"userId\":\"user_id\","
|
||||
+ "\"stock\":\"stock\""
|
||||
+ "}}]}";
|
||||
}
|
||||
|
||||
private ServiceException expectBusinessBlockUpdateFailure(String layoutJson)
|
||||
{
|
||||
FrontProjectPageDesign current = currentBusinessBlockDesign();
|
||||
when(frontProjectService.getProject(7L, 10L)).thenReturn(project());
|
||||
when(pageDesignMapper.selectFrontProjectPageDesignById(88L)).thenReturn(current);
|
||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Arrays.asList(cartTable(), productTable()));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(201L)).thenReturn(Arrays.asList(
|
||||
column("id", "id", "0", "1", "1"),
|
||||
column("product_id", "productId", "0", "1", "1"),
|
||||
column("quantity", "quantity", "0", "1", "1"),
|
||||
column("user_id", "userId", "0", "1", "1")));
|
||||
when(frontProjectColumnMapper.selectColumnsByTableId(202L)).thenReturn(Arrays.asList(
|
||||
column("id", "id", "0", "1", "1"),
|
||||
column("name", "name", "0", "1", "1"),
|
||||
column("price", "price", "0", "1", "1"),
|
||||
column("stock", "stock", "0", "1", "1")));
|
||||
final FrontendPageDesignRequest request = businessBlockRequest(layoutJson);
|
||||
return expectServiceException(new ThrowingRunnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
service.updatePageDesign(7L, 10L, 88L, request);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ServiceException expectServiceException(ThrowingRunnable runnable)
|
||||
{
|
||||
try
|
||||
{
|
||||
runnable.run();
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
return e;
|
||||
}
|
||||
throw new AssertionError("Expected ServiceException");
|
||||
}
|
||||
|
||||
private interface ThrowingRunnable
|
||||
{
|
||||
void run();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user