Explorar el Código

完善页面跳转配置

csk hace 1 mes
padre
commit
55497f6749

+ 2 - 2
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/mock/index.js

@@ -121,8 +121,8 @@ const zoneConfigData = {
   }],
   exchangeConfig: {
     items: [
-      { imageUrl: 'https://images.unsplash.com/photo-1436491865332-7a61a109cc05?w=200', title: '航空里程', link: '/flight' },
-      { imageUrl: 'https://images.unsplash.com/photo-1518893494013-481c1d8ed3fd?w=200', title: '贵宾厅', link: '/lounge' }
+      { imageUrl: 'https://images.unsplash.com/photo-1436491865332-7a61a109cc05?w=200', title: '航空里程', link: 'https://test.hsbc.com' },
+      { imageUrl: 'https://images.unsplash.com/photo-1518893494013-481c1d8ed3fd?w=200', title: '贵宾厅', link: 'https://test.hsbc.com' }
     ]
   },
   ruleModal: {

+ 5 - 0
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/utils/linkOptions.js

@@ -1,5 +1,10 @@
 import request from '@/utils/request'
 
+export const FIXED_JUMP_OPTIONS = [
+  { label: '太古地产', value: '太古地产' },
+  { label: '龙腾', value: '龙腾' }
+]
+
 export async function fetchLinkOptions() {
   const [campaignRes, channelRes] = await Promise.all([
     request.get('/api/campaign/list'),

+ 280 - 20
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/views/PageAssembly.vue

@@ -266,10 +266,36 @@
                     </el-col>
                   </el-row>
                   <!-- 跳转链接 -->
-                  <el-form-item v-if="btn.actionType === 'link'" label="跳转链接 *">
-                    <el-input v-model="btn.link" size="small" placeholder="必填,输入跳转URL" @input="isModified = true">
-                    </el-input>
-                  </el-form-item>
+                  <div v-if="btn.actionType === 'link'" class="jump-config-panel">
+                    <div class="jump-config-title"><el-icon><Link /></el-icon> 跳转链接配置</div>
+                    <el-form-item class="jump-type-item" label-width="0">
+                      <el-radio-group v-model="btn.linkType" @change="handleButtonJumpTypeChange(btn)">
+                        <el-radio label="h5">H5链接跳转</el-radio>
+                        <el-radio label="fixed">固定链接跳转</el-radio>
+                        <el-radio label="miniProgram">小程序跳转</el-radio>
+                      </el-radio-group>
+                    </el-form-item>
+                    <el-form-item v-if="btn.linkType === 'fixed'" label="目标链接">
+                      <el-select v-model="btn.fixedLink" size="small" placeholder="请选择目标链接" style="width:100%" @change="handleButtonJumpFieldChange(btn)">
+                        <el-option v-for="opt in fixedJumpOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
+                      </el-select>
+                    </el-form-item>
+                    <el-form-item v-if="btn.linkType === 'h5'" label="配置链接">
+                      <el-input v-model="btn.linkH5" size="small" placeholder="例如:https://hsbc.com" @input="handleButtonJumpFieldChange(btn)" />
+                    </el-form-item>
+                    <el-row v-if="btn.linkType === 'miniProgram'" :gutter="16">
+                      <el-col :span="12">
+                        <el-form-item label="AppID">
+                          <el-input v-model="btn.miniAppId" size="small" placeholder="例如:wx1234567890abcdef" @input="handleButtonJumpFieldChange(btn)" />
+                        </el-form-item>
+                      </el-col>
+                      <el-col :span="12">
+                        <el-form-item label="页面路径">
+                          <el-input v-model="btn.miniAppPath" size="small" placeholder="例如:pages/index/index" @input="handleButtonJumpFieldChange(btn)" />
+                        </el-form-item>
+                      </el-col>
+                    </el-row>
+                  </div>
                   <!-- 弹窗配置 -->
                   <div v-if="btn.actionType === 'dialog' && btn.dialog" class="dialog-config">
                     <div class="dialog-config-title"><el-icon><Document /></el-icon> 弹窗内容配置</div>
@@ -286,18 +312,49 @@
                         />
                       </div>
                     </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>
-                      </el-col>
-                      <el-col :span="12">
-                        <el-form-item label="确认按钮跳转链接">
-                          <el-input v-model="btn.dialog.confirmLink" size="small" placeholder="选填,无链接则仅关闭弹窗" @input="isModified = true" />
-                        </el-form-item>
-                      </el-col>
-                    </el-row>
+                    <div class="dialog-button-config">
+                      <div class="confirm-config-header">
+                        <span>左侧取消按钮配置</span>
+                      </div>
+                      <el-input v-model="btn.dialog.cancelText" size="small" placeholder="取消" @input="isModified = true" />
+                    </div>
+                    <div class="dialog-button-config confirm-button-config">
+                      <div class="confirm-config-header">
+                        <span>右侧确认按钮配置</span>
+                        <el-checkbox v-model="btn.dialog.showConfirm" @change="isModified = true">展示</el-checkbox>
+                      </div>
+                      <el-input v-if="btn.dialog.showConfirm" v-model="btn.dialog.confirmText" size="small" placeholder="确认" @input="isModified = true" />
+                    </div>
+                    <div v-if="btn.dialog.showConfirm" class="jump-config-panel dialog-jump-config">
+                      <div class="jump-config-title"><el-icon><Link /></el-icon> 确认按钮跳转链接配置</div>
+                      <el-form-item class="jump-type-item" label-width="0">
+                        <el-radio-group v-model="btn.dialog.confirmLinkType" @change="handleDialogJumpTypeChange(btn.dialog)">
+                          <el-radio label="h5">H5链接跳转</el-radio>
+                          <el-radio label="fixed">固定链接跳转</el-radio>
+                          <el-radio label="miniProgram">小程序跳转</el-radio>
+                        </el-radio-group>
+                      </el-form-item>
+                      <el-form-item v-if="btn.dialog.confirmLinkType === 'fixed'" label="目标链接">
+                        <el-select v-model="btn.dialog.confirmFixedLink" size="small" placeholder="请选择目标链接" style="width:100%" @change="handleDialogJumpFieldChange(btn.dialog)">
+                          <el-option v-for="opt in fixedJumpOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
+                        </el-select>
+                      </el-form-item>
+                      <el-form-item v-if="btn.dialog.confirmLinkType === 'h5'" label="配置链接">
+                        <el-input v-model="btn.dialog.confirmLinkH5" size="small" placeholder="例如:https://hsbc.com" @input="handleDialogJumpFieldChange(btn.dialog)" />
+                      </el-form-item>
+                      <el-row v-if="btn.dialog.confirmLinkType === 'miniProgram'" :gutter="16">
+                        <el-col :span="12">
+                          <el-form-item label="AppID">
+                            <el-input v-model="btn.dialog.confirmMiniAppId" size="small" placeholder="例如:wx1234567890abcdef" @input="handleDialogJumpFieldChange(btn.dialog)" />
+                          </el-form-item>
+                        </el-col>
+                        <el-col :span="12">
+                          <el-form-item label="页面路径">
+                            <el-input v-model="btn.dialog.confirmMiniAppPath" size="small" placeholder="例如:pages/index/index" @input="handleDialogJumpFieldChange(btn.dialog)" />
+                          </el-form-item>
+                        </el-col>
+                      </el-row>
+                    </div>
                   </div>
                 </el-form>
               </div>
@@ -383,7 +440,7 @@
 <script>
 import { ElMessage, ElMessageBox } from 'element-plus'
 import request from '@/utils/request'
-import { fetchLinkOptions } from '@/utils/linkOptions'
+import { FIXED_JUMP_OPTIONS, fetchLinkOptions } from '@/utils/linkOptions'
 
 let blockIdCounter = 0
 function genBlockId() {
@@ -406,6 +463,7 @@ export default {
       linkForm: { campaignId: '', channelId: '' },
       campaignOptions: [],
       channelOptions: [],
+      fixedJumpOptions: FIXED_JUMP_OPTIONS,
       linkTargetId: '',
       linkTargetPublished: false,
       quillOptions: {
@@ -446,8 +504,36 @@ export default {
     this.fetchList()
   },
   methods: {
+    createDefaultJumpConfig(overrides = {}) {
+      return {
+        linkType: 'h5',
+        fixedLink: '',
+        linkH5: '',
+        miniAppId: '',
+        miniAppPath: '',
+        ...overrides
+      }
+    },
+    createDefaultDialogJumpConfig(overrides = {}) {
+      return {
+        confirmLinkType: 'h5',
+        confirmFixedLink: '',
+        confirmLinkH5: '',
+        confirmMiniAppId: '',
+        confirmMiniAppPath: '',
+        ...overrides
+      }
+    },
     createDefaultButtonDialog() {
-      return { title: '', desc: '', confirmText: '确认', confirmLink: '' }
+      return {
+        title: '',
+        desc: '',
+        cancelText: '取消',
+        showConfirm: true,
+        confirmText: '确认',
+        confirmLink: '',
+        ...this.createDefaultDialogJumpConfig()
+      }
     },
     createDefaultButton(overrides = {}) {
       return {
@@ -455,6 +541,7 @@ export default {
         link: '',
         style: 'secondary',
         actionType: 'link',
+        ...this.createDefaultJumpConfig(),
         ...overrides,
         dialog: {
           ...this.createDefaultButtonDialog(),
@@ -478,12 +565,89 @@ export default {
         target[key] = value
       }
     },
+    isFixedJumpValue(value) {
+      return FIXED_JUMP_OPTIONS.some(opt => opt.value === value)
+    },
+    inferButtonJumpType(btn = {}) {
+      if (btn.linkType) return btn.linkType
+      if (btn.miniAppId || btn.miniAppPath) return 'miniProgram'
+      if (this.isFixedJumpValue(btn.link)) return 'fixed'
+      return 'h5'
+    },
+    inferDialogJumpType(dialog = {}) {
+      if (dialog.confirmLinkType) return dialog.confirmLinkType
+      if (dialog.confirmMiniAppId || dialog.confirmMiniAppPath) return 'miniProgram'
+      if (this.isFixedJumpValue(dialog.confirmLink)) return 'fixed'
+      if (dialog.confirmLinkH5 || dialog.confirmLink) return 'h5'
+      return 'h5'
+    },
+    syncButtonLegacyLink(btn) {
+      if (btn.linkType === 'fixed') btn.link = btn.fixedLink || ''
+      if (btn.linkType === 'h5') btn.link = btn.linkH5 || ''
+      if (btn.linkType === 'miniProgram') btn.link = ''
+    },
+    syncDialogLegacyLink(dialog) {
+      if (dialog.confirmLinkType === 'fixed') dialog.confirmLink = dialog.confirmFixedLink || ''
+      if (dialog.confirmLinkType === 'h5') dialog.confirmLink = dialog.confirmLinkH5 || ''
+      if (dialog.confirmLinkType === 'miniProgram') dialog.confirmLink = ''
+    },
+    clearInactiveButtonJumpFields(btn) {
+      if (btn.linkType !== 'fixed') btn.fixedLink = ''
+      if (btn.linkType !== 'h5') btn.linkH5 = ''
+      if (btn.linkType !== 'miniProgram') {
+        btn.miniAppId = ''
+        btn.miniAppPath = ''
+      }
+      this.syncButtonLegacyLink(btn)
+    },
+    clearInactiveDialogJumpFields(dialog) {
+      if (dialog.confirmLinkType !== 'fixed') dialog.confirmFixedLink = ''
+      if (dialog.confirmLinkType !== 'h5') dialog.confirmLinkH5 = ''
+      if (dialog.confirmLinkType !== 'miniProgram') {
+        dialog.confirmMiniAppId = ''
+        dialog.confirmMiniAppPath = ''
+      }
+      this.syncDialogLegacyLink(dialog)
+    },
+    normalizeButtonJumpConfig(btn) {
+      const linkType = this.inferButtonJumpType(btn)
+      const legacyLink = btn.link || ''
+      const normalized = this.createDefaultJumpConfig({
+        ...btn,
+        linkType,
+        fixedLink: btn.fixedLink || (this.isFixedJumpValue(legacyLink) ? legacyLink : '太古地产'),
+        linkH5: btn.linkH5 || (!this.isFixedJumpValue(legacyLink) ? legacyLink : ''),
+        miniAppId: btn.miniAppId || '',
+        miniAppPath: btn.miniAppPath || ''
+      })
+      Object.keys(normalized).forEach(key => {
+        btn[key] = normalized[key]
+      })
+      this.clearInactiveButtonJumpFields(btn)
+    },
+    normalizeDialogJumpConfig(dialog) {
+      const confirmLinkType = this.inferDialogJumpType(dialog)
+      const legacyLink = dialog.confirmLink || ''
+      const normalized = this.createDefaultDialogJumpConfig({
+        ...dialog,
+        confirmLinkType,
+        confirmFixedLink: dialog.confirmFixedLink || (this.isFixedJumpValue(legacyLink) ? legacyLink : '太古地产'),
+        confirmLinkH5: dialog.confirmLinkH5 || (!this.isFixedJumpValue(legacyLink) ? legacyLink : ''),
+        confirmMiniAppId: dialog.confirmMiniAppId || '',
+        confirmMiniAppPath: dialog.confirmMiniAppPath || ''
+      })
+      Object.keys(normalized).forEach(key => {
+        dialog[key] = normalized[key]
+      })
+      this.clearInactiveDialogJumpFields(dialog)
+    },
     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', '')
+      this.normalizeButtonJumpConfig(btn)
       if (!btn.dialog) {
         this.ensureReactiveField(btn, 'dialog', this.createDefaultButtonDialog())
       } else {
@@ -494,6 +658,7 @@ export default {
           }
         })
       }
+      this.normalizeDialogJumpConfig(btn.dialog)
     },
     normalizeAssemblyData(data) {
       data.shareConfig = this.createDefaultShareConfig(data.shareConfig || {})
@@ -562,12 +727,13 @@ export default {
       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' && !this.validateButtonJumpConfig(btn)) hasError = true
+            if (btn.actionType === 'dialog' && btn.dialog && btn.dialog.showConfirm !== false && !this.validateDialogJumpConfig(btn.dialog)) hasError = true
           })
         }
       })
       if (hasError) {
-        ElMessage.error('保存失败:按钮模块中存在未填写的跳转链接!')
+        ElMessage.error('保存失败:按钮模块中存在未填写完整的跳转链接配置!')
         return
       }
       if (data.shareConfig.enabled) {
@@ -662,6 +828,40 @@ export default {
       this.normalizeButton(btn)
       this.isModified = true
     },
+    handleButtonJumpTypeChange(btn) {
+      if (btn.linkType === 'fixed' && !btn.fixedLink) btn.fixedLink = '太古地产'
+      this.clearInactiveButtonJumpFields(btn)
+      this.isModified = true
+    },
+    handleButtonJumpFieldChange(btn) {
+      this.syncButtonLegacyLink(btn)
+      this.isModified = true
+    },
+    handleDialogJumpTypeChange(dialog) {
+      if (dialog.confirmLinkType === 'fixed' && !dialog.confirmFixedLink) dialog.confirmFixedLink = '太古地产'
+      this.clearInactiveDialogJumpFields(dialog)
+      this.isModified = true
+    },
+    handleDialogJumpFieldChange(dialog) {
+      this.syncDialogLegacyLink(dialog)
+      this.isModified = true
+    },
+    validateButtonJumpConfig(btn) {
+      if (btn.linkType === 'fixed') return !!btn.fixedLink
+      if (btn.linkType === 'h5') return !!(btn.linkH5 || '').trim()
+      if (btn.linkType === 'miniProgram') {
+        return !!(btn.miniAppId || '').trim() && !!(btn.miniAppPath || '').trim()
+      }
+      return false
+    },
+    validateDialogJumpConfig(dialog) {
+      if (dialog.confirmLinkType === 'fixed') return !!dialog.confirmFixedLink
+      if (dialog.confirmLinkType === 'h5') return !!(dialog.confirmLinkH5 || '').trim()
+      if (dialog.confirmLinkType === 'miniProgram') {
+        return !!(dialog.confirmMiniAppId || '').trim() && !!(dialog.confirmMiniAppPath || '').trim()
+      }
+      return false
+    },
     removeButton(bIdx, btnIdx) {
       this.editData.blocks[bIdx].data.buttons.splice(btnIdx, 1)
       this.isModified = true
@@ -732,6 +932,23 @@ export default {
 .info-item { display: flex; align-items: center; gap: 8px; }
 .info-label { font-size: 13px; color: #909399; white-space: nowrap; }
 
+.page-assembly :deep(.el-input:not(.el-textarea)) {
+  --el-input-height: 32px;
+}
+.page-assembly :deep(.el-input:not(.el-textarea) .el-input__wrapper),
+.page-assembly :deep(.el-select__wrapper) {
+  height: 32px;
+  min-height: 32px;
+}
+.page-assembly :deep(.el-input:not(.el-textarea) .el-input__inner) {
+  height: 30px;
+  line-height: 30px;
+}
+.page-assembly :deep(.el-select .el-select__placeholder),
+.page-assembly :deep(.el-select .el-select__selected-item) {
+  line-height: 30px;
+}
+
 .required-star { color: #F56C6C; }
 .dialog-desc-editor {
   width: 100%;
@@ -902,6 +1119,49 @@ export default {
   font-size: 14px; font-weight: 500; color: #303133;
   border-bottom: 1px solid #ebeef5; padding-bottom: 12px; margin-bottom: 14px;
 }
+.dialog-button-config {
+  width: 33.333%;
+  min-width: 280px;
+  margin-bottom: 16px;
+}
+.confirm-config-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 22px;
+  margin-bottom: 6px;
+  color: #303133;
+  font-size: 14px;
+  line-height: 20px;
+}
+.confirm-config-header :deep(.el-checkbox) {
+  height: 20px;
+  margin-right: 0;
+}
+.jump-config-panel {
+  background: #fff;
+  padding: 16px 18px 4px;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  margin-top: 4px;
+}
+.dialog-jump-config {
+  margin-top: 2px;
+}
+.jump-config-title {
+  font-size: 13px;
+  font-weight: 600;
+  color: #303133;
+  border-bottom: 1px solid #ebeef5;
+  padding-bottom: 10px;
+  margin-bottom: 12px;
+}
+.jump-type-item {
+  margin-bottom: 18px;
+}
+.jump-config-panel :deep(.el-radio) {
+  margin-right: 22px;
+}
 
 .add-block-bar {
   display: flex; gap: 16px; justify-content: center;

+ 104 - 14
工作空间/产研/权益/汇丰全行/产品原型/pc-demo/src/views/PageZoneConfig.vue

@@ -177,21 +177,30 @@
           <!-- 跳转链接配置 -->
           <div class="link-config-block">
             <div class="link-config-title"><el-icon><Link /></el-icon> 跳转链接配置</div>
-            <el-form label-width="140px">
-              <el-form-item label="H5 跳转链接">
-                <el-input v-model="item.linkH5" @input="markModified" />
+            <el-form class="jump-config-form" label-position="top">
+              <el-form-item class="jump-type-item" label-width="0">
+                <el-radio-group v-model="item.jumpType" @change="handleZoneJumpTypeChange(item)">
+                  <el-radio label="h5">H5链接跳转</el-radio>
+                  <el-radio label="fixed">固定链接跳转</el-radio>
+                  <el-radio label="miniProgram">小程序跳转</el-radio>
+                </el-radio-group>
               </el-form-item>
-              <el-form-item label="APP 跳转链接">
-                <el-input v-model="item.linkApp" @input="markModified" />
+              <el-form-item v-if="item.jumpType === 'fixed'" label="目标链接">
+                <el-select v-model="item.fixedLink" placeholder="请选择目标链接" style="width:100%" @change="markModified">
+                  <el-option v-for="opt in fixedJumpOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
+                </el-select>
               </el-form-item>
-              <el-row :gutter="20">
+              <el-form-item v-if="item.jumpType === 'h5'" label="配置链接">
+                <el-input v-model="item.linkH5" placeholder="例如:https://hsbc.com" @input="markModified" />
+              </el-form-item>
+              <el-row v-if="item.jumpType === 'miniProgram'" :gutter="20">
                 <el-col :span="12">
-                  <el-form-item label="微信小程序 AppID">
+                  <el-form-item label="AppID">
                     <el-input v-model="item.wechatAppId" placeholder="例如:wx1234567890abcdef" @input="markModified" />
                   </el-form-item>
                 </el-col>
                 <el-col :span="12">
-                  <el-form-item label="微信小程序页面路径">
+                  <el-form-item label="页面路径">
                     <el-input v-model="item.wechatPath" placeholder="例如:pages/index/index" @input="markModified" />
                   </el-form-item>
                 </el-col>
@@ -248,7 +257,7 @@
               <el-input v-model="item.title" style="width:200px" @input="markModified" />
             </el-form-item>
             <el-form-item label="跳转链接">
-              <el-input v-model="item.link" style="width:280px" @input="markModified" />
+              <el-input v-model="item.link" style="width:280px" placeholder="例如:https://test.hsbc.com" @input="markModified" />
             </el-form-item>
           </el-form>
         </div>
@@ -318,7 +327,7 @@
 <script>
 import { ElMessage } from 'element-plus'
 import request from '@/utils/request'
-import { fetchLinkOptions } from '@/utils/linkOptions'
+import { FIXED_JUMP_OPTIONS, fetchLinkOptions } from '@/utils/linkOptions'
 
 export default {
   name: 'PageZoneConfig',
@@ -344,6 +353,7 @@ export default {
       linkForm: { campaignId: '', channelId: '' },
       campaignOptions: [],
       channelOptions: [],
+      fixedJumpOptions: FIXED_JUMP_OPTIONS,
       quillOptions: {
         placeholder: '请输入富文本内容...',
         modules: {
@@ -416,11 +426,65 @@ export default {
       })
       return source
     },
+    createDefaultJumpConfig(overrides = {}) {
+      return {
+        jumpType: 'h5',
+        fixedLink: '',
+        linkH5: '',
+        linkApp: '',
+        wechatAppId: '',
+        wechatPath: '',
+        ...overrides
+      }
+    },
+    inferJumpType(item = {}) {
+      if (item.jumpType) return item.jumpType
+      if (item.wechatAppId || item.wechatPath) return 'miniProgram'
+      if (item.linkH5) return 'h5'
+      return 'h5'
+    },
+    normalizeJumpConfig(item) {
+      const normalized = this.createDefaultJumpConfig({
+        ...item,
+        jumpType: this.inferJumpType(item)
+      })
+      Object.keys(normalized).forEach(key => {
+        item[key] = normalized[key]
+      })
+      this.clearInactiveJumpFields(item)
+    },
+    normalizeCarouselItems(items = []) {
+      items.forEach(item => this.normalizeJumpConfig(item))
+      return items
+    },
+    clearInactiveJumpFields(item) {
+      if (item.jumpType !== 'fixed') item.fixedLink = ''
+      if (item.jumpType !== 'h5') item.linkH5 = ''
+      if (item.jumpType !== 'miniProgram') {
+        item.wechatAppId = ''
+        item.wechatPath = ''
+      }
+      item.linkApp = ''
+    },
+    handleZoneJumpTypeChange(item) {
+      if (item.jumpType === 'fixed' && !item.fixedLink) item.fixedLink = '太古地产'
+      this.clearInactiveJumpFields(item)
+      this.markModified()
+    },
+    validateJumpConfig(item) {
+      if (item.jumpType === 'fixed') return !!item.fixedLink
+      if (item.jumpType === 'h5') return !!(item.linkH5 || '').trim()
+      if (item.jumpType === 'miniProgram') {
+        return !!(item.wechatAppId || '').trim() && !!(item.wechatPath || '').trim()
+      }
+      return false
+    },
     async fetchData() {
       const res = await request.get('/api/page-config/zone')
       const cfg = res.data.config || {}
       if (!cfg.identityModule) cfg.identityModule = this.defaultIdentityModule()
       cfg.identityModule = this.normalizeIdentityModule(cfg.identityModule)
+      cfg.adCarousel = this.normalizeCarouselItems(cfg.adCarousel || [])
       this.configData = cfg
       this.availableTags = res.data.availableTags
       this.pageStatus = res.data.status
@@ -473,7 +537,7 @@ export default {
     },
     addCarousel() {
       this.configData.adCarousel.push({
-        imageUrl: '', linkH5: '', linkApp: '', wechatAppId: '', wechatPath: '',
+        imageUrl: '', ...this.createDefaultJumpConfig(),
         title: '新广告', subtitle: '', tags: []
       })
       this.markModified()
@@ -498,6 +562,12 @@ export default {
       this.markModified()
     },
     async handleSave(type) {
+      this.normalizeCarouselItems(this.configData.adCarousel || [])
+      const invalidCarouselIndex = (this.configData.adCarousel || []).findIndex(item => !this.validateJumpConfig(item))
+      if (invalidCarouselIndex > -1) {
+        ElMessage.error(`保存失败:第 ${invalidCarouselIndex + 1} 个活动广告轮播图跳转链接配置未填写完整!`)
+        return
+      }
       const status = type === 'publish' ? 'published' : 'draft'
       await request.put('/api/page-config/zone', { config: this.configData, status })
       this.pageStatus = status
@@ -571,12 +641,32 @@ export default {
 }
 
 .link-config-block {
-  background: #f5f7fa; padding: 16px; border-radius: 6px;
+  background: #f5f7fa; padding: 18px 20px 20px; border-radius: 6px;
   border: 1px solid #ebeef5; margin-top: 8px;
 }
 .link-config-title {
-  font-size: 13px; font-weight: 600; color: #606266;
-  border-bottom: 1px solid #ebeef5; padding-bottom: 8px; margin-bottom: 12px;
+  font-size: 13px; font-weight: 600; color: #303133;
+  border-bottom: 1px solid #dcdfe6; padding-bottom: 12px; margin-bottom: 14px;
+}
+.jump-config-form :deep(.el-form-item) {
+  margin-bottom: 14px;
+}
+.jump-config-form :deep(.el-form-item:last-child) {
+  margin-bottom: 0;
+}
+.jump-config-form :deep(.el-form-item__label) {
+  line-height: 20px;
+  padding: 0 0 6px;
+  color: #303133;
+}
+.jump-config-form :deep(.el-form-item__content) {
+  width: 100%;
+}
+.jump-type-item {
+  margin-bottom: 18px;
+}
+.link-config-block :deep(.el-radio) {
+  margin-right: 22px;
 }
 
 .exchange-body { display: flex; align-items: flex-start; gap: 16px; margin-top: 8px; }