feat(admin): 积木页面可视化编辑与链接选择器(站内页/可下载文件)

Made-with: Cursor
This commit is contained in:
whm
2026-03-19 16:33:54 +08:00
parent ea163dbf8e
commit 6df5cf029d
6 changed files with 641 additions and 5 deletions

View File

@@ -0,0 +1,105 @@
<template>
<div class="page-builder-editor">
<PageBuilderBlocks v-model:blocks="blocks" :site-id="siteId" />
<el-collapse class="adv-collapse" accordion>
<el-collapse-item title="高级:直接编辑 JSON慎用" name="json">
<el-input
v-model="jsonDraft"
type="textarea"
:rows="10"
placeholder="修改后会覆盖上方可视化内容"
/>
<el-button type="warning" style="margin-top: 8px" @click="applyJsonDraft">应用 JSON</el-button>
</el-collapse-item>
</el-collapse>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import PageBuilderBlocks from './PageBuilderBlocks.vue'
const props = defineProps({
modelValue: { type: String, default: '' },
siteId: { type: String, default: '' }
})
const emit = defineEmits(['update:modelValue'])
const blocks = ref([])
const jsonDraft = ref('')
let syncingFromParent = false
function normalizeBlock(b) {
if (!b.props) b.props = {}
if (!b.animation) b.animation = { enter: 'fadeIn', delay_ms: 0, duration_ms: 600 }
if (b.type === 'link_list' && !Array.isArray(b.props.items)) b.props.items = []
if (b.type === 'section' && !Array.isArray(b.children)) b.children = []
if (Array.isArray(b.children)) b.children.forEach(normalizeBlock)
}
function parseFromString(s) {
syncingFromParent = true
try {
const j = JSON.parse(s || '{}')
const raw = Array.isArray(j.blocks) ? JSON.parse(JSON.stringify(j.blocks)) : []
raw.forEach(normalizeBlock)
blocks.value = raw
jsonDraft.value = JSON.stringify({ version: j.version || 1, blocks: blocks.value }, null, 2)
} catch {
blocks.value = []
jsonDraft.value = '{"version":1,"blocks":[]}'
}
syncingFromParent = false
}
function stringify() {
return JSON.stringify({ version: 1, blocks: blocks.value }, null, 2)
}
watch(
() => props.modelValue,
(v) => {
if (syncingFromParent) return
parseFromString(v)
},
{ immediate: true }
)
watch(
blocks,
() => {
if (syncingFromParent) return
const s = stringify()
jsonDraft.value = s
emit('update:modelValue', s)
},
{ deep: true }
)
function applyJsonDraft() {
try {
const j = JSON.parse(jsonDraft.value || '{}')
if (!Array.isArray(j.blocks)) {
ElMessage.error('JSON 须包含 blocks 数组')
return
}
syncingFromParent = true
blocks.value = JSON.parse(JSON.stringify(j.blocks))
syncingFromParent = false
emit('update:modelValue', stringify())
ElMessage.success('已应用')
} catch (e) {
ElMessage.error('JSON 格式错误')
}
}
</script>
<style scoped>
.page-builder-editor {
width: 100%;
}
.adv-collapse {
margin-top: 12px;
}
</style>