|
|
@@ -158,16 +158,16 @@
|
|
|
|
|
|
<!-- 按钮模块 -->
|
|
|
<template v-else-if="block.type === 'buttonGroup'">
|
|
|
- <div class="block-header">
|
|
|
- <span><i class="el-icon-thumb"></i> 按钮模块</span>
|
|
|
- <el-button type="text" icon="el-icon-circle-plus-outline" size="small" @click="addButton(bIdx)" style="float:right">添加按钮</el-button>
|
|
|
+ <div class="block-header button-module-header">
|
|
|
+ <span class="button-module-title"><i class="el-icon-thumb"></i> 按钮模块</span>
|
|
|
+ <el-button class="add-button-btn" type="primary" plain icon="el-icon-circle-plus-outline" size="small" @click="addButton(bIdx)">添加按钮</el-button>
|
|
|
</div>
|
|
|
<div class="block-body">
|
|
|
- <div v-if="block.data.buttons.length === 0" class="empty-tip">暂无按钮</div>
|
|
|
- <div v-for="(btn, btnIdx) in block.data.buttons" :key="btnIdx" class="button-config-item">
|
|
|
+ <div v-if="!block.data.buttons || block.data.buttons.length === 0" class="empty-tip">暂无按钮</div>
|
|
|
+ <div v-for="(btn, btnIdx) in (block.data.buttons || [])" :key="btnIdx" class="button-config-item">
|
|
|
<div class="btn-index">BTN {{ btnIdx + 1 }}</div>
|
|
|
<div class="btn-config-body">
|
|
|
- <el-form label-width="80px">
|
|
|
+ <el-form class="button-form" label-position="top">
|
|
|
<el-row :gutter="16">
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="按钮文案">
|
|
|
@@ -176,7 +176,7 @@
|
|
|
</el-col>
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="点击动作">
|
|
|
- <el-select v-model="btn.actionType" size="small" @change="isModified = true">
|
|
|
+ <el-select v-model="btn.actionType" size="small" @change="handleButtonActionChange(btn)">
|
|
|
<el-option label="跳转链接" value="link" />
|
|
|
<el-option label="弹窗提示" value="dialog" />
|
|
|
</el-select>
|
|
|
@@ -185,21 +185,20 @@
|
|
|
<el-col :span="8">
|
|
|
<el-form-item label="按钮样式">
|
|
|
<el-select v-model="btn.style" size="small" @change="isModified = true">
|
|
|
- <el-option label="突出显示 (红色)" value="primary" />
|
|
|
- <el-option label="次要操作 (白色)" value="secondary" />
|
|
|
+ <el-option label="突出显示(红色)" value="primary" />
|
|
|
+ <el-option label="次要操作(白色)" value="secondary" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
<!-- 跳转链接 -->
|
|
|
- <el-form-item v-if="btn.actionType === 'link'" label="跳转链接">
|
|
|
+ <el-form-item v-if="btn.actionType === 'link'" label="跳转链接 *">
|
|
|
<el-input v-model="btn.link" size="small" placeholder="必填,输入跳转URL" @input="isModified = true">
|
|
|
- <template slot="append"><span style="color:#F56C6C">*</span></template>
|
|
|
</el-input>
|
|
|
</el-form-item>
|
|
|
<!-- 弹窗配置 -->
|
|
|
- <div v-if="btn.actionType === 'dialog'" class="dialog-config">
|
|
|
- <div class="dialog-config-title"><i class="el-icon-monitor"></i> 弹窗内容配置 (包含取消、确认按钮)</div>
|
|
|
+ <div v-if="btn.actionType === 'dialog' && btn.dialog" class="dialog-config">
|
|
|
+ <div class="dialog-config-title"><i class="el-icon-document"></i> 弹窗内容配置(包含取消、确认按钮)</div>
|
|
|
<el-form-item label="弹窗标题">
|
|
|
<el-input v-model="btn.dialog.title" size="small" placeholder="弹窗标题" @input="isModified = true" />
|
|
|
</el-form-item>
|
|
|
@@ -208,12 +207,12 @@
|
|
|
</el-form-item>
|
|
|
<el-row :gutter="16">
|
|
|
<el-col :span="12">
|
|
|
- <el-form-item label="确认文案">
|
|
|
- <el-input v-model="btn.dialog.confirmText" size="small" placeholder="例如:继续前往" @input="isModified = true" />
|
|
|
+ <el-form-item label="右侧确认按钮文案">
|
|
|
+ <el-input v-model="btn.dialog.confirmText" size="small" placeholder="确认" @input="isModified = true" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
- <el-form-item label="确认链接">
|
|
|
+ <el-form-item label="确认按钮跳转链接">
|
|
|
<el-input v-model="btn.dialog.confirmLink" size="small" placeholder="选填,无链接则仅关闭弹窗" @input="isModified = true" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
@@ -252,27 +251,25 @@
|
|
|
<el-dialog title="获取页面链接" :visible.sync="linkDialogVisible" width="680px">
|
|
|
<el-form label-width="100px" inline>
|
|
|
<el-form-item label="Campaign ID">
|
|
|
- <el-select v-model="linkForm.campaignId" clearable placeholder="请选择 (无)">
|
|
|
- <el-option label="CAMP202511" value="CAMP202511" />
|
|
|
- <el-option label="CAMP_WINTER" value="CAMP_WINTER" />
|
|
|
+ <el-select v-model="linkForm.campaignId" clearable filterable placeholder="请选择 (无)" style="width:240px">
|
|
|
+ <el-option v-for="opt in campaignOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
<el-form-item label="Channel ID">
|
|
|
- <el-select v-model="linkForm.channelId" clearable placeholder="请选择 (无)">
|
|
|
- <el-option label="APP推送" value="CH_APP" />
|
|
|
- <el-option label="微信公众号" value="CH_WECHAT" />
|
|
|
+ <el-select v-model="linkForm.channelId" clearable filterable placeholder="请选择 (无)" style="width:240px">
|
|
|
+ <el-option v-for="opt in channelOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
<div v-if="linkTargetPublished" class="link-section">
|
|
|
<div class="link-label"><span class="dot dot-green"></span> 正式环境链接</div>
|
|
|
<div class="link-row">
|
|
|
- <span class="link-type">H5 链接</span>
|
|
|
+ <span class="link-type">H5链接</span>
|
|
|
<el-input :value="assemblyProdH5" readonly size="small" />
|
|
|
<el-button size="small" type="primary" plain @click="copyLink(assemblyProdH5)">复制</el-button>
|
|
|
</div>
|
|
|
<div class="link-row">
|
|
|
- <span class="link-type">小程序/APP</span>
|
|
|
+ <span class="link-type">小程序链接</span>
|
|
|
<el-input :value="assemblyProdApp" readonly size="small" />
|
|
|
<el-button size="small" type="primary" plain @click="copyLink(assemblyProdApp)">复制</el-button>
|
|
|
</div>
|
|
|
@@ -290,6 +287,7 @@
|
|
|
|
|
|
<script>
|
|
|
import request from '@/utils/request'
|
|
|
+import { fetchLinkOptions } from '@/utils/linkOptions'
|
|
|
|
|
|
let blockIdCounter = 0
|
|
|
function genBlockId() {
|
|
|
@@ -310,6 +308,8 @@ export default {
|
|
|
sortTarget: 1,
|
|
|
linkDialogVisible: false,
|
|
|
linkForm: { campaignId: '', channelId: '' },
|
|
|
+ campaignOptions: [],
|
|
|
+ channelOptions: [],
|
|
|
linkTargetId: '',
|
|
|
linkTargetPublished: false,
|
|
|
quillOptions: {
|
|
|
@@ -350,6 +350,56 @@ export default {
|
|
|
this.fetchList()
|
|
|
},
|
|
|
methods: {
|
|
|
+ createDefaultButtonDialog() {
|
|
|
+ return { title: '', desc: '', confirmText: '确认', confirmLink: '' }
|
|
|
+ },
|
|
|
+ createDefaultButton(overrides = {}) {
|
|
|
+ return {
|
|
|
+ text: '新按钮',
|
|
|
+ link: '',
|
|
|
+ style: 'secondary',
|
|
|
+ actionType: 'link',
|
|
|
+ ...overrides,
|
|
|
+ dialog: {
|
|
|
+ ...this.createDefaultButtonDialog(),
|
|
|
+ ...(overrides.dialog || {})
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ ensureReactiveField(target, key, value) {
|
|
|
+ if (Object.prototype.hasOwnProperty.call(target, key)) {
|
|
|
+ target[key] = value
|
|
|
+ } else {
|
|
|
+ this.$set(target, key, value)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ normalizeButton(btn) {
|
|
|
+ if (!btn) return
|
|
|
+ if (!btn.text) this.ensureReactiveField(btn, 'text', '')
|
|
|
+ if (!btn.style) this.ensureReactiveField(btn, 'style', 'secondary')
|
|
|
+ if (!btn.actionType) this.ensureReactiveField(btn, 'actionType', 'link')
|
|
|
+ if (!Object.prototype.hasOwnProperty.call(btn, 'link')) this.ensureReactiveField(btn, 'link', '')
|
|
|
+ if (!btn.dialog) {
|
|
|
+ this.ensureReactiveField(btn, 'dialog', this.createDefaultButtonDialog())
|
|
|
+ } else {
|
|
|
+ const defaults = this.createDefaultButtonDialog()
|
|
|
+ Object.keys(defaults).forEach(key => {
|
|
|
+ if (!Object.prototype.hasOwnProperty.call(btn.dialog, key)) {
|
|
|
+ this.$set(btn.dialog, key, defaults[key])
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ normalizeAssemblyData(data) {
|
|
|
+ const blocks = data.blocks || []
|
|
|
+ blocks.forEach(block => {
|
|
|
+ if (block.type !== 'buttonGroup') return
|
|
|
+ if (!block.data) this.$set(block, 'data', { buttons: [] })
|
|
|
+ if (!block.data.buttons) this.$set(block.data, 'buttons', [])
|
|
|
+ block.data.buttons.forEach(btn => this.normalizeButton(btn))
|
|
|
+ })
|
|
|
+ return data
|
|
|
+ },
|
|
|
createEmptyAssembly() {
|
|
|
return {
|
|
|
id: '', name: '', status: 'draft',
|
|
|
@@ -357,7 +407,7 @@ export default {
|
|
|
{ type: 'image', id: genBlockId(), data: { imageUrl: '' } },
|
|
|
{ type: 'privilege', id: genBlockId(), data: { title: '权益标题', items: [{ id: '1', title: '权益名称', desc: '权益简介' }] } },
|
|
|
{ type: 'richtext', id: genBlockId(), data: { content: '<p>组装页富文本区块内容...</p>' } },
|
|
|
- { type: 'buttonGroup', id: genBlockId(), data: { buttons: [{ text: '点击配置', link: '', style: 'primary', actionType: 'link', dialog: { title: '', desc: '', confirmText: '确认', confirmLink: '' } }] } }
|
|
|
+ { type: 'buttonGroup', id: genBlockId(), data: { buttons: [this.createDefaultButton({ text: '点击配置', style: 'primary' })] } }
|
|
|
]
|
|
|
}
|
|
|
},
|
|
|
@@ -365,6 +415,11 @@ export default {
|
|
|
const res = await request.get('/api/assembly/list', { params: this.searchForm })
|
|
|
this.tableData = res.data
|
|
|
},
|
|
|
+ async loadLinkOptions() {
|
|
|
+ const { campaignOptions, channelOptions } = await fetchLinkOptions()
|
|
|
+ this.campaignOptions = campaignOptions
|
|
|
+ this.channelOptions = channelOptions
|
|
|
+ },
|
|
|
handleSearch() {
|
|
|
this.fetchList()
|
|
|
},
|
|
|
@@ -381,7 +436,7 @@ export default {
|
|
|
this.$nextTick(() => { this.quillReady = true })
|
|
|
},
|
|
|
handleEdit(row) {
|
|
|
- this.editData = JSON.parse(JSON.stringify(row))
|
|
|
+ this.editData = this.normalizeAssemblyData(JSON.parse(JSON.stringify(row)))
|
|
|
this.isNew = false
|
|
|
this.isModified = false
|
|
|
this.quillReady = false
|
|
|
@@ -400,11 +455,12 @@ export default {
|
|
|
},
|
|
|
async handleSaveAssembly(type) {
|
|
|
const data = this.editData
|
|
|
+ this.normalizeAssemblyData(data)
|
|
|
let hasError = false
|
|
|
data.blocks.forEach(block => {
|
|
|
if (block.type === 'buttonGroup') {
|
|
|
block.data.buttons.forEach(btn => {
|
|
|
- if (btn.actionType === 'link' && !btn.link.trim()) hasError = true
|
|
|
+ if (btn.actionType === 'link' && !(btn.link || '').trim()) hasError = true
|
|
|
})
|
|
|
}
|
|
|
})
|
|
|
@@ -432,7 +488,7 @@ export default {
|
|
|
if (type === 'image') block.data = { imageUrl: '' }
|
|
|
if (type === 'privilege') block.data = { title: '新权益标题', items: [{ id: '1', title: '模拟权益', desc: '描述' }] }
|
|
|
if (type === 'richtext') block.data = { content: '<p>输入富文本内容...</p>' }
|
|
|
- if (type === 'buttonGroup') block.data = { buttons: [{ text: '点击跳转', link: '', style: 'primary', actionType: 'link', dialog: { title: '', desc: '', confirmText: '确认', confirmLink: '' } }] }
|
|
|
+ if (type === 'buttonGroup') block.data = { buttons: [this.createDefaultButton({ text: '点击跳转', style: 'primary' })] }
|
|
|
this.editData.blocks.push(block)
|
|
|
this.isModified = true
|
|
|
},
|
|
|
@@ -469,10 +525,13 @@ export default {
|
|
|
|
|
|
// 按钮操作
|
|
|
addButton(bIdx) {
|
|
|
- this.editData.blocks[bIdx].data.buttons.push({
|
|
|
- text: '新按钮', link: '', style: 'secondary', actionType: 'link',
|
|
|
- dialog: { title: '', desc: '', confirmText: '确认', confirmLink: '' }
|
|
|
- })
|
|
|
+ const block = this.editData.blocks[bIdx]
|
|
|
+ if (!block.data.buttons) this.$set(block.data, 'buttons', [])
|
|
|
+ block.data.buttons.push(this.createDefaultButton())
|
|
|
+ this.isModified = true
|
|
|
+ },
|
|
|
+ handleButtonActionChange(btn) {
|
|
|
+ this.normalizeButton(btn)
|
|
|
this.isModified = true
|
|
|
},
|
|
|
removeButton(bIdx, btnIdx) {
|
|
|
@@ -505,11 +564,12 @@ export default {
|
|
|
},
|
|
|
|
|
|
// 链接弹窗
|
|
|
- showLinkDialog(id) {
|
|
|
+ async showLinkDialog(id) {
|
|
|
if (!id) {
|
|
|
this.$message.warning('请先保存组装页再获取链接!')
|
|
|
return
|
|
|
}
|
|
|
+ await this.loadLinkOptions()
|
|
|
this.linkTargetId = id
|
|
|
const item = this.tableData.find(a => a.id === id) || this.editData
|
|
|
this.linkTargetPublished = item && item.status === 'published'
|
|
|
@@ -550,10 +610,18 @@ export default {
|
|
|
box-shadow: 0 1px 4px rgba(0,0,0,.04);
|
|
|
}
|
|
|
.block-header {
|
|
|
- background: #f5f7fa; padding: 10px 16px;
|
|
|
+ background: #f5f7fa; padding: 10px 160px 10px 16px; min-height: 44px;
|
|
|
border-bottom: 1px solid #ebeef5; font-weight: 600; font-size: 14px;
|
|
|
+ display: flex; align-items: center;
|
|
|
}
|
|
|
.block-header i { margin-right: 6px; color: #909399; }
|
|
|
+.button-module-header { gap: 14px; }
|
|
|
+.button-module-title { display: inline-flex; align-items: center; }
|
|
|
+.add-button-btn {
|
|
|
+ padding: 7px 10px;
|
|
|
+ background: #ecf5ff;
|
|
|
+ border-color: #d9ecff;
|
|
|
+}
|
|
|
.block-hint { font-size: 12px; color: #c0c4cc; font-weight: normal; margin-left: 8px; }
|
|
|
.block-body { padding: 16px; }
|
|
|
.block-actions {
|
|
|
@@ -607,7 +675,7 @@ export default {
|
|
|
|
|
|
.button-config-item {
|
|
|
position: relative; border: 1px solid #ebeef5; border-radius: 6px;
|
|
|
- padding: 20px 12px 12px; margin-bottom: 12px; background: #fafafa;
|
|
|
+ padding: 24px 122px 20px 20px; margin-bottom: 12px; background: #fafafa;
|
|
|
}
|
|
|
.btn-index {
|
|
|
position: absolute; left: 0; top: 0;
|
|
|
@@ -615,17 +683,25 @@ export default {
|
|
|
padding: 2px 8px; border-radius: 6px 0 6px 0;
|
|
|
}
|
|
|
.btn-config-body { flex: 1; }
|
|
|
+.button-form .el-form-item { margin-bottom: 16px; }
|
|
|
+.button-form .el-form-item__label {
|
|
|
+ line-height: 20px;
|
|
|
+ padding: 0 0 6px;
|
|
|
+ color: #303133;
|
|
|
+ font-weight: 400;
|
|
|
+}
|
|
|
+.button-form .el-select { width: 100%; }
|
|
|
.btn-config-actions {
|
|
|
- position: absolute; right: 12px; top: 12px;
|
|
|
+ position: absolute; right: 20px; top: 32px;
|
|
|
display: flex; gap: 6px;
|
|
|
}
|
|
|
.dialog-config {
|
|
|
- background: #fff; padding: 12px; border: 1px solid #ebeef5; border-radius: 6px;
|
|
|
+ background: #fff; padding: 18px 20px 4px; border: 1px solid #ebeef5; border-radius: 4px;
|
|
|
margin-top: 4px;
|
|
|
}
|
|
|
.dialog-config-title {
|
|
|
- font-size: 13px; font-weight: 600; color: #606266;
|
|
|
- border-bottom: 1px solid #ebeef5; padding-bottom: 8px; margin-bottom: 12px;
|
|
|
+ font-size: 14px; font-weight: 500; color: #303133;
|
|
|
+ border-bottom: 1px solid #ebeef5; padding-bottom: 12px; margin-bottom: 14px;
|
|
|
}
|
|
|
|
|
|
.add-block-bar {
|