465 lines
12 KiB
Vue
465 lines
12 KiB
Vue
|
|
<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>
|