feat(admin): 积木页面可视化编辑与链接选择器(站内页/可下载文件)
Made-with: Cursor
This commit is contained in:
105
admin/src/components/PageBuilderEditor.vue
Normal file
105
admin/src/components/PageBuilderEditor.vue
Normal 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>
|
||||
Reference in New Issue
Block a user