|
|
@@ -27,7 +27,7 @@
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
- <el-table :data="tableData" stripe border style="width:100%">
|
|
|
+ <el-table :data="pagedTableData" stripe border style="width:100%">
|
|
|
<el-table-column label="操作" width="120" fixed="left" align="center">
|
|
|
<template #default="scope">
|
|
|
<div class="list-actions">
|
|
|
@@ -40,14 +40,28 @@
|
|
|
<el-table-column prop="name" label="组装页名称" min-width="200" show-overflow-tooltip />
|
|
|
<el-table-column label="发布状态" width="100" align="center">
|
|
|
<template #default="scope">
|
|
|
- <el-tag :type="scope.row.status === 'published' ? 'success' : 'info'" size="small">
|
|
|
- {{ scope.row.status === 'published' ? '已上架' : '草稿' }}
|
|
|
- </el-tag>
|
|
|
+ <el-tooltip :content="getAssemblyStatusTag(scope.row.status).label" placement="top">
|
|
|
+ <el-tag :type="getAssemblyStatusTag(scope.row.status).type" size="small">
|
|
|
+ {{ getAssemblyListStatusTag(scope.row.status).label }}
|
|
|
+ </el-tag>
|
|
|
+ </el-tooltip>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="createTime" label="创建时间" width="180" align="center" />
|
|
|
<el-table-column prop="updateTime" label="更新时间" width="180" align="center" />
|
|
|
</el-table>
|
|
|
+ <div class="pagination-section">
|
|
|
+ <span class="total">Total {{ tableData.length }}</span>
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="pagination.currentPage"
|
|
|
+ v-model:page-size="pagination.pageSize"
|
|
|
+ layout="prev, pager, next, sizes, jumper"
|
|
|
+ :page-sizes="[10, 20, 50]"
|
|
|
+ :total="tableData.length"
|
|
|
+ @size-change="handlePageSizeChange"
|
|
|
+ @current-change="handleCurrentPageChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
</el-card>
|
|
|
</template>
|
|
|
|
|
|
@@ -60,9 +74,7 @@
|
|
|
<el-icon><ArrowLeft /></el-icon>返回
|
|
|
</el-button>
|
|
|
<h2 class="toolbar-title">{{ isNew ? '新建组装页' : '编辑组装页' }}</h2>
|
|
|
- <el-tag v-if="editData.status === 'published' && !isModified" type="success" size="small" effect="plain">正式线上环境</el-tag>
|
|
|
- <el-tag v-else-if="showModifiedStatus" type="warning" size="small" effect="plain">有未发布的修改</el-tag>
|
|
|
- <el-tag v-else size="info" effect="plain">草稿 (未发布)</el-tag>
|
|
|
+ <el-tag :type="pageStatusTag.type" size="small" effect="plain">{{ pageStatusTag.label }}</el-tag>
|
|
|
</div>
|
|
|
<div class="toolbar-right">
|
|
|
<el-button size="small" @click="showLinkDialog(editData.id)">
|
|
|
@@ -446,6 +458,10 @@ export default {
|
|
|
viewState: 'list',
|
|
|
tableData: [],
|
|
|
searchForm: { id: '', name: '' },
|
|
|
+ pagination: {
|
|
|
+ currentPage: 1,
|
|
|
+ pageSize: 10
|
|
|
+ },
|
|
|
editData: this.createEmptyAssembly(),
|
|
|
isNew: true,
|
|
|
isModified: false,
|
|
|
@@ -475,8 +491,14 @@ export default {
|
|
|
hasPrivilegeBlock() {
|
|
|
return this.editData.blocks.some(b => b.type === 'privilege')
|
|
|
},
|
|
|
- showModifiedStatus() {
|
|
|
- return this.editData.status === 'modified' || (this.editData.status === 'published' && this.isModified)
|
|
|
+ pagedTableData() {
|
|
|
+ const start = (this.pagination.currentPage - 1) * this.pagination.pageSize
|
|
|
+ return this.tableData.slice(start, start + this.pagination.pageSize)
|
|
|
+ },
|
|
|
+ pageStatusTag() {
|
|
|
+ if (this.isModified && !this.isNew) return { type: 'warning', label: '有未保存修改' }
|
|
|
+ if (this.editData.status === 'published') return { type: 'success', label: '正式线上环境' }
|
|
|
+ return { type: 'info', label: '草稿 (未发布)' }
|
|
|
},
|
|
|
canPublish() {
|
|
|
return !this.isModified && !!this.editData.id && ['draft', 'modified'].includes(this.editData.status)
|
|
|
@@ -500,6 +522,13 @@ export default {
|
|
|
created() {
|
|
|
this.fetchList()
|
|
|
},
|
|
|
+ beforeRouteLeave(to, from, next) {
|
|
|
+ if (!this.isModified) {
|
|
|
+ next()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.confirmLeaveUnsaved().then(() => next()).catch(() => next(false))
|
|
|
+ },
|
|
|
methods: {
|
|
|
createDefaultJumpConfig(overrides = {}) {
|
|
|
return {
|
|
|
@@ -534,7 +563,7 @@ export default {
|
|
|
},
|
|
|
createDefaultButton(overrides = {}) {
|
|
|
return {
|
|
|
- text: '新按钮',
|
|
|
+ text: '',
|
|
|
link: '',
|
|
|
style: 'secondary',
|
|
|
actionType: 'link',
|
|
|
@@ -556,6 +585,28 @@ export default {
|
|
|
delete config.desc
|
|
|
return config
|
|
|
},
|
|
|
+ isBlank(value) {
|
|
|
+ return !(value || '').trim()
|
|
|
+ },
|
|
|
+ showSaveError(message) {
|
|
|
+ ElMessage.error(`保存失败:${message}`)
|
|
|
+ return false
|
|
|
+ },
|
|
|
+ confirmLeaveUnsaved() {
|
|
|
+ return ElMessageBox.confirm('当前有未保存修改,离开后修改内容将丢失,确定离开吗?', '未保存修改', {
|
|
|
+ type: 'warning',
|
|
|
+ confirmButtonText: '确定离开',
|
|
|
+ cancelButtonText: '继续编辑'
|
|
|
+ })
|
|
|
+ },
|
|
|
+ getAssemblyStatusTag(status) {
|
|
|
+ if (status === 'published') return { type: 'success', label: '正式线上环境' }
|
|
|
+ return { type: 'info', label: '草稿 (未发布)' }
|
|
|
+ },
|
|
|
+ getAssemblyListStatusTag(status) {
|
|
|
+ if (status === 'published') return { type: 'success', label: '正式' }
|
|
|
+ return { type: 'info', label: '草稿' }
|
|
|
+ },
|
|
|
ensureReactiveField(target, key, value) {
|
|
|
if (Object.prototype.hasOwnProperty.call(target, key)) {
|
|
|
target[key] = value
|
|
|
@@ -693,6 +744,7 @@ export default {
|
|
|
async fetchList() {
|
|
|
const res = await request.get('/api/assembly/list', { params: this.searchForm })
|
|
|
this.tableData = res.data.sort((a, b) => b.id.localeCompare(a.id))
|
|
|
+ this.syncCurrentPage()
|
|
|
},
|
|
|
async loadLinkOptions() {
|
|
|
const { campaignOptions, channelOptions } = await fetchLinkOptions()
|
|
|
@@ -700,12 +752,24 @@ export default {
|
|
|
this.channelOptions = channelOptions
|
|
|
},
|
|
|
handleSearch() {
|
|
|
+ this.pagination.currentPage = 1
|
|
|
this.fetchList()
|
|
|
},
|
|
|
handleReset() {
|
|
|
this.searchForm = { id: '', name: '' }
|
|
|
+ this.pagination.currentPage = 1
|
|
|
this.fetchList()
|
|
|
},
|
|
|
+ handlePageSizeChange() {
|
|
|
+ this.pagination.currentPage = 1
|
|
|
+ },
|
|
|
+ handleCurrentPageChange(page) {
|
|
|
+ this.pagination.currentPage = page
|
|
|
+ },
|
|
|
+ syncCurrentPage() {
|
|
|
+ const maxPage = Math.max(1, Math.ceil(this.tableData.length / this.pagination.pageSize))
|
|
|
+ if (this.pagination.currentPage > maxPage) this.pagination.currentPage = maxPage
|
|
|
+ },
|
|
|
handleCreate() {
|
|
|
this.editData = this.createEmptyAssembly()
|
|
|
this.isNew = true
|
|
|
@@ -728,37 +792,21 @@ export default {
|
|
|
ElMessage.success('删除成功')
|
|
|
this.fetchList()
|
|
|
},
|
|
|
- backToList() {
|
|
|
+ async backToList() {
|
|
|
+ if (this.isModified) {
|
|
|
+ try {
|
|
|
+ await this.confirmLeaveUnsaved()
|
|
|
+ } catch (e) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
this.viewState = 'list'
|
|
|
this.fetchList()
|
|
|
},
|
|
|
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' && !this.validateButtonJumpConfig(btn)) hasError = true
|
|
|
- if (btn.actionType === 'dialog' && btn.dialog && !this.validateDialogConfig(btn.dialog)) hasError = true
|
|
|
- })
|
|
|
- }
|
|
|
- })
|
|
|
- if (hasError) {
|
|
|
- ElMessage.error('保存失败:按钮模块中存在未填写完整的弹窗或跳转配置!')
|
|
|
- return
|
|
|
- }
|
|
|
- if (data.shareConfig.enabled) {
|
|
|
- if (!data.shareConfig.thumbnail) {
|
|
|
- ElMessage.error('保存失败:分享缩略图为必填项!')
|
|
|
- return
|
|
|
- }
|
|
|
- if (!data.shareConfig.title.trim()) {
|
|
|
- ElMessage.error('保存失败:分享标题为必填项!')
|
|
|
- return
|
|
|
- }
|
|
|
- }
|
|
|
- if (!data.name.trim()) data.name = '未命名组装页'
|
|
|
+ if (!this.validateAssemblyConfig(data)) return
|
|
|
if (type === 'publish' && this.isModified) {
|
|
|
ElMessage.warning('请先保存草稿后再发布上线')
|
|
|
return
|
|
|
@@ -798,9 +846,9 @@ export default {
|
|
|
addBlock(type) {
|
|
|
const block = { type, id: genBlockId(), data: {} }
|
|
|
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: [this.createDefaultButton({ text: '点击跳转', style: 'primary' })] }
|
|
|
+ if (type === 'privilege') block.data = { title: '', items: [] }
|
|
|
+ if (type === 'richtext') block.data = { content: '<p></p>' }
|
|
|
+ if (type === 'buttonGroup') block.data = { buttons: [this.createDefaultButton({ style: 'primary' })] }
|
|
|
this.editData.blocks.push(block)
|
|
|
this.isModified = true
|
|
|
},
|
|
|
@@ -870,6 +918,18 @@ export default {
|
|
|
}
|
|
|
return false
|
|
|
},
|
|
|
+ getButtonJumpConfigError(btn) {
|
|
|
+ if (this.getPartnerFixedLink(btn.linkType)) return ''
|
|
|
+ if (btn.linkType === 'h5') {
|
|
|
+ return this.isBlank(btn.linkH5) ? '配置链接未填写' : ''
|
|
|
+ }
|
|
|
+ if (btn.linkType === 'miniProgram') {
|
|
|
+ if (this.isBlank(btn.miniAppId)) return 'AppID未填写'
|
|
|
+ if (this.isBlank(btn.miniAppPath)) return '页面路径未填写'
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+ return '跳转方式未选择'
|
|
|
+ },
|
|
|
isRichTextEmpty(value) {
|
|
|
const text = String(value || '')
|
|
|
.replace(/<[^>]*>/g, '')
|
|
|
@@ -877,6 +937,72 @@ export default {
|
|
|
.trim()
|
|
|
return !text
|
|
|
},
|
|
|
+ getDialogJumpConfigError(dialog) {
|
|
|
+ if (this.getPartnerFixedLink(dialog.confirmLinkType)) return ''
|
|
|
+ if (dialog.confirmLinkType === 'h5') {
|
|
|
+ return this.isBlank(dialog.confirmLinkH5) ? '配置链接未填写' : ''
|
|
|
+ }
|
|
|
+ if (dialog.confirmLinkType === 'miniProgram') {
|
|
|
+ if (this.isBlank(dialog.confirmMiniAppId)) return 'AppID未填写'
|
|
|
+ if (this.isBlank(dialog.confirmMiniAppPath)) return '页面路径未填写'
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+ return '跳转方式未选择'
|
|
|
+ },
|
|
|
+ getDialogConfigError(dialog) {
|
|
|
+ if (this.isBlank(dialog.title)) return '弹窗内容配置-弹窗标题未填写'
|
|
|
+ if (this.isRichTextEmpty(dialog.desc)) return '弹窗内容配置-弹窗描述未填写'
|
|
|
+ if (this.isBlank(dialog.cancelText)) return '弹窗内容配置-左侧取消按钮配置未填写'
|
|
|
+ if (dialog.showConfirm === false) return ''
|
|
|
+ if (this.isBlank(dialog.confirmText)) return '弹窗内容配置-右侧确认按钮配置未填写'
|
|
|
+ const jumpError = this.getDialogJumpConfigError(dialog)
|
|
|
+ return jumpError ? `确认按钮跳转链接配置-${jumpError}` : ''
|
|
|
+ },
|
|
|
+ validateAssemblyConfig(data) {
|
|
|
+ if (this.isBlank(data.name)) {
|
|
|
+ return this.showSaveError('基本信息-组装页名称未填写')
|
|
|
+ }
|
|
|
+ if (data.shareConfig.enabled) {
|
|
|
+ if (!data.shareConfig.thumbnail) {
|
|
|
+ return this.showSaveError('微信分享卡片配置-分享缩略图未上传')
|
|
|
+ }
|
|
|
+ if (this.isBlank(data.shareConfig.title)) {
|
|
|
+ return this.showSaveError('微信分享卡片配置-分享标题未填写')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ let imageIndex = 0
|
|
|
+ let buttonModuleIndex = 0
|
|
|
+ for (const block of (data.blocks || [])) {
|
|
|
+ if (block.type === 'image') {
|
|
|
+ imageIndex += 1
|
|
|
+ if (!block.data?.imageUrl) {
|
|
|
+ return this.showSaveError(`图片模块-第 ${imageIndex} 个-图片未上传`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (block.type === 'buttonGroup') {
|
|
|
+ buttonModuleIndex += 1
|
|
|
+ const buttons = block.data?.buttons || []
|
|
|
+ if (!buttons.length) {
|
|
|
+ return this.showSaveError(`按钮模块-第 ${buttonModuleIndex} 个模块-至少配置一个按钮`)
|
|
|
+ }
|
|
|
+ for (const [buttonIndex, btn] of buttons.entries()) {
|
|
|
+ const prefix = `按钮模块-第 ${buttonModuleIndex} 个模块-第 ${buttonIndex + 1} 个按钮`
|
|
|
+ if (this.isBlank(btn.text)) {
|
|
|
+ return this.showSaveError(`${prefix}-按钮文案未填写`)
|
|
|
+ }
|
|
|
+ if (btn.actionType === 'link') {
|
|
|
+ const jumpError = this.getButtonJumpConfigError(btn)
|
|
|
+ if (jumpError) return this.showSaveError(`${prefix}-跳转链接配置-${jumpError}`)
|
|
|
+ }
|
|
|
+ if (btn.actionType === 'dialog' && btn.dialog) {
|
|
|
+ const dialogError = this.getDialogConfigError(btn.dialog)
|
|
|
+ if (dialogError) return this.showSaveError(`${prefix}-${dialogError}`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true
|
|
|
+ },
|
|
|
validateDialogConfig(dialog) {
|
|
|
if (!(dialog.title || '').trim()) return false
|
|
|
if (this.isRichTextEmpty(dialog.desc)) return false
|
|
|
@@ -928,10 +1054,13 @@ export default {
|
|
|
ElMessage.warning('请先保存组装页再获取链接!')
|
|
|
return
|
|
|
}
|
|
|
+ if (this.viewState === 'edit' && this.isModified && id === this.editData.id) {
|
|
|
+ ElMessage.warning('当前链接基于上一次保存内容,最新修改需保存后生效')
|
|
|
+ }
|
|
|
await this.loadLinkOptions()
|
|
|
this.linkTargetId = id
|
|
|
const item = this.tableData.find(a => a.id === id) || this.editData
|
|
|
- this.linkTargetPublished = item && item.status === 'published'
|
|
|
+ this.linkTargetPublished = item && ['published', 'modified'].includes(item.status)
|
|
|
this.linkForm = { campaignId: '', channelId: '' }
|
|
|
this.linkDialogVisible = true
|
|
|
},
|
|
|
@@ -980,6 +1109,19 @@ export default {
|
|
|
line-height: 30px;
|
|
|
}
|
|
|
|
|
|
+.pagination-section {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ align-items: center;
|
|
|
+ gap: 20px;
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.pagination-section .total {
|
|
|
+ color: #606266;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
.required-star { color: #F56C6C; }
|
|
|
.dialog-desc-editor {
|
|
|
width: 100%;
|