浏览代码

Merge branch 'master' of https://hub.banwotech.cn/fade/rongshu

Fade 1 月之前
父节点
当前提交
7c308d20cb

+ 15 - 13
pc/src/components/EquityLinkDialog.vue

@@ -3,11 +3,11 @@
     :title="title"
     :visible.sync="dialogVisible"
     width="680px"
-    @open="resetForm"
+    @open="handleOpen"
   >
     <el-form label-width="100px" inline>
       <el-form-item label="Campaign ID">
-        <el-select v-model="form.campaignId" clearable placeholder="请选择 (无)">
+        <el-select v-model="form.campaignId" clearable filterable placeholder="请选择 (无)" style="width:240px">
           <el-option
             v-for="opt in campaignOptions"
             :key="opt.value"
@@ -17,7 +17,7 @@
         </el-select>
       </el-form-item>
       <el-form-item label="Channel ID">
-        <el-select v-model="form.channelId" clearable placeholder="请选择 (无)">
+        <el-select v-model="form.channelId" clearable filterable placeholder="请选择 (无)" style="width:240px">
           <el-option
             v-for="opt in channelOptions"
             :key="opt.value"
@@ -30,12 +30,12 @@
     <div class="link-section">
       <div class="link-label"><span class="dot"></span> 权益链接</div>
       <div class="link-row">
-        <span class="link-type">H5 链接</span>
+        <span class="link-type">H5链接</span>
         <el-input :value="h5Url" readonly size="small" />
         <el-button size="small" type="primary" plain @click="copyLink(h5Url)">复制</el-button>
       </div>
       <div class="link-row">
-        <span class="link-type">小程序/APP</span>
+        <span class="link-type">小程序链接</span>
         <el-input :value="appUrl" readonly size="small" />
         <el-button size="small" type="primary" plain @click="copyLink(appUrl)">复制</el-button>
       </div>
@@ -44,6 +44,8 @@
 </template>
 
 <script>
+import { fetchLinkOptions } from '@/utils/linkOptions'
+
 export default {
   name: 'EquityLinkDialog',
   props: {
@@ -55,14 +57,8 @@ export default {
   data() {
     return {
       form: { campaignId: '', channelId: '' },
-      campaignOptions: [
-        { label: 'CAMP202511', value: 'CAMP202511' },
-        { label: 'CAMP_WINTER', value: 'CAMP_WINTER' }
-      ],
-      channelOptions: [
-        { label: 'APP推送', value: 'CH_APP' },
-        { label: '微信公众号', value: 'CH_WECHAT' }
-      ]
+      campaignOptions: [],
+      channelOptions: []
     }
   },
   computed: {
@@ -84,6 +80,12 @@ export default {
     }
   },
   methods: {
+    async handleOpen() {
+      this.resetForm()
+      const { campaignOptions, channelOptions } = await fetchLinkOptions()
+      this.campaignOptions = campaignOptions
+      this.channelOptions = channelOptions
+    },
     resetForm() {
       this.form = { campaignId: '', channelId: '' }
     },

+ 15 - 1
pc/src/components/Layout.vue

@@ -180,6 +180,7 @@ export default {
   display: flex;
   flex-direction: column;
   flex-shrink: 0;
+  overflow: hidden;
 }
 .sidebar.collapsed { width: 64px; }
 
@@ -198,9 +199,22 @@ export default {
 }
 .logo i { font-size: 22px; flex-shrink: 0; }
 
-.el-menu { border: none; flex: 1; overflow: hidden; }
+.el-menu {
+  border: none;
+  flex: 1;
+  min-height: 0;
+  overflow-x: hidden;
+  overflow-y: auto;
+}
 .el-menu--collapse { width: 64px; }
 
+.el-menu::-webkit-scrollbar { width: 6px; }
+.el-menu::-webkit-scrollbar-thumb {
+  background: rgba(191, 203, 217, .28);
+  border-radius: 6px;
+}
+.el-menu::-webkit-scrollbar-track { background: transparent; }
+
 .main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
 
 .header {

+ 7 - 1
pc/src/mock/index.js

@@ -155,7 +155,7 @@ const assemblyList = [
       { type: 'image', id: 'b_init1', data: { imageUrl: 'https://images.unsplash.com/photo-1557683316-973673baf926?w=800' } },
       { type: 'privilege', id: 'b_init2', data: { title: '专属权益', items: [{ id: '1', title: '机场贵宾厅服务', desc: '全年免费使用6次指定机场贵宾厅' }, { id: '2', title: '境内礼宾车', desc: '免费2次接送' }] } },
       { type: 'richtext', id: 'b_init3', data: { content: '<p>欢迎参与双十一专属福利活动...</p>' } },
-      { type: 'buttonGroup', id: 'b_init4', data: { buttons: [{ text: '立即参与', link: '/join', style: 'primary', actionType: 'link' }] } }
+      { type: 'buttonGroup', id: 'b_init4', data: { buttons: [{ text: '立即参与', link: '/join', style: 'primary', actionType: 'link', dialog: { title: '', desc: '', confirmText: '确认', confirmLink: '' } }] } }
     ]
   },
   {
@@ -257,13 +257,19 @@ Mock.mock(/\/api\/channel\/dict\/save/, 'post', (options) => {
   const body = JSON.parse(options.body)
   const { target, type, category, oldValue, newValue, mode } = body
   if (target === 'type') {
+    if (mode === 'add' && channelDict[newValue]) return { code: 400, data: null, msg: 'Channel 类型已存在,不能重复新增' }
+    if (mode === 'edit' && oldValue !== newValue && channelDict[newValue]) return { code: 400, data: null, msg: 'Channel 类型已存在,不能修改为重复值' }
     if (mode === 'add') { channelDict[newValue] = {} }
     else { channelDict[newValue] = channelDict[oldValue]; delete channelDict[oldValue]; channelRows.forEach(r => { if (r.type === oldValue) r.type = newValue }) }
   } else if (target === 'category') {
+    if (mode === 'add' && channelDict[type][newValue]) return { code: 400, data: null, msg: 'Channel 类别已存在,不能重复新增' }
+    if (mode === 'edit' && oldValue !== newValue && channelDict[type][newValue]) return { code: 400, data: null, msg: 'Channel 类别已存在,不能修改为重复值' }
     if (mode === 'add') { channelDict[type][newValue] = [] }
     else { channelDict[type][newValue] = channelDict[type][oldValue]; delete channelDict[type][oldValue]; channelRows.forEach(r => { if (r.type === type && r.category === oldValue) r.category = newValue }) }
   } else if (target === 'name') {
     const list = channelDict[type][category]
+    if (mode === 'add' && list.includes(newValue)) return { code: 400, data: null, msg: 'Channel 名称已存在,不能重复新增' }
+    if (mode === 'edit' && oldValue !== newValue && list.includes(newValue)) return { code: 400, data: null, msg: 'Channel 名称已存在,不能修改为重复值' }
     if (mode === 'add') { list.push(newValue) }
     else { const idx = list.indexOf(oldValue); if (idx >= 0) list[idx] = newValue; channelRows.forEach(r => { if (r.type === type && r.category === category && r.name === oldValue) r.name = newValue }) }
   }

+ 19 - 0
pc/src/utils/linkOptions.js

@@ -0,0 +1,19 @@
+import request from '@/utils/request'
+
+export async function fetchLinkOptions() {
+  const [campaignRes, channelRes] = await Promise.all([
+    request.get('/api/campaign/list'),
+    request.get('/api/channel/list')
+  ])
+
+  return {
+    campaignOptions: (campaignRes.data || []).map(item => ({
+      label: `${item.id} - ${item.name}`,
+      value: item.id
+    })),
+    channelOptions: (channelRes.data || []).map(item => ({
+      label: `${item.id} - ${item.subName || item.name || '-'}`,
+      value: item.id
+    }))
+  }
+}

+ 20 - 0
pc/src/views/ChannelManagement.vue

@@ -243,6 +243,16 @@ export default {
       const value = this.panelInput.trim()
       if (!value) { this.$message.warning('请输入内容'); return }
       const oldValue = target === 'type' ? this.form.type : target === 'category' ? this.form.category : this.form.name
+      const existingValues = this.getPanelExistingValues(target)
+      const exists = existingValues.some(item => item === value)
+      if (this.panelMode === 'add' && exists) {
+        this.$message.warning(`${this.getPanelTargetName(target)}已存在,不能重复新增`)
+        return
+      }
+      if (this.panelMode === 'edit' && value !== oldValue && exists) {
+        this.$message.warning(`${this.getPanelTargetName(target)}已存在,不能修改为重复值`)
+        return
+      }
       const res = await request.post('/api/channel/dict/save', {
         target,
         type: this.form.type,
@@ -265,6 +275,16 @@ export default {
       this.$message.success(this.panelMode === 'add' ? '新增成功' : '编辑成功')
       this.fetchList()
     },
+    getPanelExistingValues(target) {
+      if (target === 'type') return Object.keys(this.channelDict)
+      if (target === 'category') return Object.keys(this.channelDict[this.form.type] || {})
+      return ((this.channelDict[this.form.type] || {})[this.form.category] || [])
+    },
+    getPanelTargetName(target) {
+      if (target === 'type') return 'Channel 类型'
+      if (target === 'category') return 'Channel 类别'
+      return 'Channel 名称'
+    },
     async submitModal() {
       if (!this.form.type || !this.form.category || !this.form.name) {
         this.$message.warning('请完整选择类型、类别和名称')

+ 115 - 39
pc/src/views/PageAssembly.vue

@@ -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 {

+ 19 - 11
pc/src/views/PageHomeConfig.vue

@@ -44,7 +44,7 @@
         <div slot="header" class="section-header">
           <i class="el-icon-picture-outline"></i> 头部配置
         </div>
-        <el-form label-width="100px">
+        <el-form class="header-config-form" label-width="130px">
           <el-form-item label="头图配图">
             <div class="image-upload-area">
               <div class="upload-box" @click="triggerUpload('header')">
@@ -145,27 +145,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="请选择 (无)" @change="updateLinks">
-            <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" @change="updateLinks">
+            <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="请选择 (无)" @change="updateLinks">
-            <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" @change="updateLinks">
+            <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="pageStatus === 'published'" 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="prodH5Url" readonly size="small" />
           <el-button size="small" type="primary" plain @click="copyLink(prodH5Url)">复制</el-button>
         </div>
         <div class="link-row">
-          <span class="link-type">小程序/APP</span>
+          <span class="link-type">小程序链接</span>
           <el-input :value="prodAppUrl" readonly size="small" />
           <el-button size="small" type="primary" plain @click="copyLink(prodAppUrl)">复制</el-button>
         </div>
@@ -183,6 +181,7 @@
 
 <script>
 import request from '@/utils/request'
+import { fetchLinkOptions } from '@/utils/linkOptions'
 
 export default {
   name: 'PageHomeConfig',
@@ -198,7 +197,9 @@ export default {
       pageStatus: 'draft',
       sortTarget: 1,
       linkDialogVisible: false,
-      linkForm: { campaignId: '', channelId: '' }
+      linkForm: { campaignId: '', channelId: '' },
+      campaignOptions: [],
+      channelOptions: []
     }
   },
   computed: {
@@ -231,6 +232,11 @@ export default {
       this.configData = res.data.config
       this.pageStatus = res.data.status
     },
+    async loadLinkOptions() {
+      const { campaignOptions, channelOptions } = await fetchLinkOptions()
+      this.campaignOptions = campaignOptions
+      this.channelOptions = channelOptions
+    },
     markModified() {
       if (this.pageStatus === 'published') {
         this.pageStatus = 'modified'
@@ -280,7 +286,8 @@ export default {
       this.pageStatus = status
       this.$message.success(type === 'publish' ? '已发布权益介绍页配置!' : '已保存权益介绍页配置草稿!')
     },
-    showLinkDialog() {
+    async showLinkDialog() {
+      await this.loadLinkOptions()
       this.linkForm = { campaignId: '', channelId: '' }
       this.linkDialogVisible = true
     },
@@ -313,6 +320,7 @@ export default {
 .section-card { margin-bottom: 16px; }
 .section-header { font-weight: 600; }
 .section-header i { margin-right: 6px; color: #909399; }
+.header-config-form .el-form-item__label { white-space: nowrap; }
 
 .image-upload-area { display: flex; align-items: center; gap: 16px; }
 .upload-box {

+ 16 - 9
pc/src/views/PageZoneConfig.vue

@@ -249,27 +249,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="pageStatus === 'published'" 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="prodH5Url" readonly size="small" />
           <el-button size="small" type="primary" plain @click="copyLink(prodH5Url)">复制</el-button>
         </div>
         <div class="link-row">
-          <span class="link-type">小程序/APP</span>
+          <span class="link-type">小程序链接</span>
           <el-input :value="prodAppUrl" readonly size="small" />
           <el-button size="small" type="primary" plain @click="copyLink(prodAppUrl)">复制</el-button>
         </div>
@@ -287,6 +285,7 @@
 
 <script>
 import request from '@/utils/request'
+import { fetchLinkOptions } from '@/utils/linkOptions'
 
 export default {
   name: 'PageZoneConfig',
@@ -310,6 +309,8 @@ export default {
       sortTarget: 1,
       linkDialogVisible: false,
       linkForm: { campaignId: '', channelId: '' },
+      campaignOptions: [],
+      channelOptions: [],
       quillOptions: {
         placeholder: '请输入积分规则内容...',
         modules: {
@@ -379,6 +380,11 @@ export default {
       reader.readAsDataURL(file)
       event.target.value = ''
     },
+    async loadLinkOptions() {
+      const { campaignOptions, channelOptions } = await fetchLinkOptions()
+      this.campaignOptions = campaignOptions
+      this.channelOptions = channelOptions
+    },
     markModified() {
       if (!this.dataLoaded) return
       if (this.pageStatus === 'published') {
@@ -436,7 +442,8 @@ export default {
       this.pageStatus = status
       this.$message.success(type === 'publish' ? '已发布权益首页配置!' : '已保存权益首页配置草稿!')
     },
-    showLinkDialog() {
+    async showLinkDialog() {
+      await this.loadLinkOptions()
       this.linkForm = { campaignId: '', channelId: '' }
       this.linkDialogVisible = true
     },