feat: 前台404页与通配路由; 积木拖拽排序(vuedraggable); nginx SPA说明
Made-with: Cursor
This commit is contained in:
@@ -1,14 +1,28 @@
|
||||
<template>
|
||||
<div class="page-builder-blocks">
|
||||
<div v-for="(block, idx) in modelBlocks" :key="block.id || idx" class="block-card">
|
||||
<div class="block-head">
|
||||
<span class="block-type">{{ typeLabel(block.type) }}</span>
|
||||
<div class="block-actions">
|
||||
<el-button link type="primary" :disabled="idx === 0" @click="move(idx, -1)">上移</el-button>
|
||||
<el-button link type="primary" :disabled="idx >= modelBlocks.length - 1" @click="move(idx, 1)">下移</el-button>
|
||||
<el-button link type="danger" @click="remove(idx)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<Draggable
|
||||
v-model="blocksWritable"
|
||||
item-key="id"
|
||||
handle=".drag-handle"
|
||||
:animation="220"
|
||||
ghost-class="block-ghost"
|
||||
chosen-class="block-chosen"
|
||||
drag-class="block-dragging"
|
||||
class="blocks-drag-list"
|
||||
>
|
||||
<template #item="{ element: block, index: idx }">
|
||||
<div class="block-card">
|
||||
<div class="block-head">
|
||||
<span class="block-head-left">
|
||||
<span class="drag-handle" title="按住拖拽排序">
|
||||
<el-icon><Rank /></el-icon>
|
||||
</span>
|
||||
<span class="block-type">{{ typeLabel(block.type) }}</span>
|
||||
</span>
|
||||
<div class="block-actions">
|
||||
<el-button link type="danger" @click="remove(idx)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="block.type === 'heading'">
|
||||
<el-form label-width="88px" size="small">
|
||||
@@ -118,7 +132,9 @@
|
||||
<template v-else>
|
||||
<span class="muted">未知类型 {{ block.type }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
|
||||
<div class="add-bar">
|
||||
<el-dropdown trigger="click" @command="addBlock">
|
||||
@@ -146,7 +162,8 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
import { ArrowDown, Rank } from '@element-plus/icons-vue'
|
||||
import Draggable from 'vuedraggable'
|
||||
import LinkPickerDialog from './LinkPickerDialog.vue'
|
||||
import PageBuilderAnimFields from './PageBuilderAnimFields.vue'
|
||||
|
||||
@@ -158,7 +175,14 @@ const props = defineProps({
|
||||
})
|
||||
const emit = defineEmits(['update:blocks'])
|
||||
|
||||
const modelBlocks = computed(() => props.blocks)
|
||||
const blocksWritable = computed({
|
||||
get() {
|
||||
return props.blocks
|
||||
},
|
||||
set(val) {
|
||||
emit('update:blocks', val)
|
||||
}
|
||||
})
|
||||
|
||||
function typeLabel(t) {
|
||||
const m = {
|
||||
@@ -225,16 +249,6 @@ function remove(idx) {
|
||||
emit('update:blocks', list)
|
||||
}
|
||||
|
||||
function move(idx, delta) {
|
||||
const list = [...props.blocks]
|
||||
const j = idx + delta
|
||||
if (j < 0 || j >= list.length) return
|
||||
const t = list[idx]
|
||||
list[idx] = list[j]
|
||||
list[j] = t
|
||||
emit('update:blocks', list)
|
||||
}
|
||||
|
||||
const pickerVisible = ref(false)
|
||||
const pickTarget = ref(null)
|
||||
|
||||
@@ -317,4 +331,41 @@ function patchSectionChildren(idx, children) {
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
.blocks-drag-list {
|
||||
min-height: 4px;
|
||||
}
|
||||
.block-head-left {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 6px;
|
||||
margin-right: 4px;
|
||||
color: #909399;
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.drag-handle:hover {
|
||||
color: #409eff;
|
||||
background: #ecf5ff;
|
||||
}
|
||||
.drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
:deep(.block-ghost) {
|
||||
opacity: 0.55;
|
||||
background: #ecf5ff !important;
|
||||
border: 1px dashed #409eff !important;
|
||||
}
|
||||
:deep(.block-chosen) {
|
||||
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.25);
|
||||
}
|
||||
:deep(.block-dragging) {
|
||||
opacity: 0.95;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -30,7 +30,12 @@ const blocks = ref([])
|
||||
const jsonDraft = ref('')
|
||||
let syncingFromParent = false
|
||||
|
||||
function newBlockId() {
|
||||
return 'b-' + Date.now() + '-' + Math.random().toString(36).slice(2, 10)
|
||||
}
|
||||
|
||||
function normalizeBlock(b) {
|
||||
if (!b.id) b.id = newBlockId()
|
||||
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 = []
|
||||
@@ -86,6 +91,7 @@ function applyJsonDraft() {
|
||||
}
|
||||
syncingFromParent = true
|
||||
blocks.value = JSON.parse(JSON.stringify(j.blocks))
|
||||
blocks.value.forEach(normalizeBlock)
|
||||
syncingFromParent = false
|
||||
emit('update:modelValue', stringify())
|
||||
ElMessage.success('已应用')
|
||||
|
||||
Reference in New Issue
Block a user