Files
yidaima/RuoYi-Vue/easycode-web/src/views/PageDesignerView.vue
2026-05-29 11:17:39 +08:00

465 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>