Przeglądaj źródła

Merge remote-tracking branch 'origin/codex/page-management-config'

Fade 1 miesiąc temu
rodzic
commit
e779e38614

+ 28 - 15
pc-v3/src/mock/index.js

@@ -24,6 +24,7 @@ Mock.mock(/\/api\/login/, 'post', (options) => {
 const homeConfigData = {
   premier: {
     tabName: '卓越理财',
+    privilegeImageEnabled: false,
     buttons: { unlocked: '进入权益专区', locked: '待解锁' },
     header: {
       imageUrl: 'https://images.unsplash.com/photo-1518893494013-481c1d8ed3fd?w=800',
@@ -37,6 +38,7 @@ const homeConfigData = {
   },
   elite: {
     tabName: '卓越理财·尊尚',
+    privilegeImageEnabled: false,
     buttons: { unlocked: '进入权益专区', locked: '待解锁' },
     header: {
       imageUrl: 'https://images.unsplash.com/photo-1518893494013-481c1d8ed3fd?w=800',
@@ -50,6 +52,7 @@ const homeConfigData = {
   },
   global: {
     tabName: '环球私人银行',
+    privilegeImageEnabled: false,
     buttons: { unlocked: '进入权益专区', locked: '待解锁' },
     header: {
       imageUrl: 'https://images.unsplash.com/photo-1518893494013-481c1d8ed3fd?w=800',
@@ -83,29 +86,27 @@ const zoneConfigData = {
   identityModule: {
     advance: {
       bgImage: 'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?w=800',
-      identityText: '汇丰运筹理财',
-      showQualifyStatus: false,
       showAvgBalance: true,
-      avgBalanceRule: '月内日均余额 = 当月内每日账户余额之和 / 当月天数。'
+      avgBalanceName: '账户月内日均余额',
+      avgBalanceRule: '<p>月内日均余额 = 当月内每日账户余额之和 / 当月天数。</p>'
     },
     premier: {
       bgImage: 'https://images.unsplash.com/photo-1583847268964-b28dc8f51f92?w=800',
-      identityText: '汇丰卓越理财',
-      showQualifyStatus: true,
       showAvgBalance: true,
-      avgBalanceRule: '计算规则:截止到昨日的账户月内日均余额。'
+      avgBalanceName: '账户月内日均余额',
+      identityTipContent: '<p>卓越理财客户身份由客户 MSC Group 自动匹配展示。</p>',
+      avgBalanceRule: '<p>计算规则:截止到昨日的账户月内日均余额。</p>'
     },
     premierElite: {
       bgImage: 'https://images.unsplash.com/photo-1557682250-33bd709cbe85?w=800',
-      identityText: '汇丰卓越理财·尊尚',
-      showQualifyStatus: true,
       showAvgBalance: true,
-      avgBalanceRule: '计算规则:截止到昨日的账户月内日均余额。支持查看合格状态及复制编号。'
+      avgBalanceName: '账户月内日均余额',
+      identityTipContent: '<p>卓越理财·尊尚客户身份由客户 MSC Group 自动匹配展示。</p>',
+      avgBalanceRule: '<p>计算规则:截止到昨日的账户月内日均余额。</p>'
     },
     privateBank: {
       bgImage: 'https://images.unsplash.com/photo-1492684223066-81342ee5ff30?w=800',
-      identityText: '汇丰环球私人银行',
-      showPbLevel: true
+      identityTipContent: '<p>环球私人银行客户身份由客户 MSC Group 自动匹配展示。</p>'
     }
   },
   adCarousel: [{
@@ -145,12 +146,18 @@ Mock.mock(/\/api\/page-config\/zone/, 'put', (options) => {
 // =============================================
 // 页面管理 - 组装页
 // =============================================
-let assemblyIdCounter = 3000
+let assemblyIdCounter = 2
 
 const assemblyList = [
   {
-    id: 'A7538', name: '双十一迎新专属活动页', status: 'published',
+    id: '1', name: '双十一迎新专属活动页', status: 'published',
     createTime: '2025-10-01 10:00:00', updateTime: '2025-10-15 14:30:00',
+    shareConfig: {
+      enabled: true,
+      thumbnail: '',
+      title: '',
+      desc: ''
+    },
     blocks: [
       { 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次接送' }] } },
@@ -159,8 +166,14 @@ const assemblyList = [
     ]
   },
   {
-    id: 'A2091', name: '私行高净值沙龙引导页', status: 'draft',
+    id: '2', name: '私行高净值沙龙引导页', status: 'draft',
     createTime: '2025-11-20 09:15:00', updateTime: '2025-11-20 09:15:00',
+    shareConfig: {
+      enabled: true,
+      thumbnail: '',
+      title: '',
+      desc: ''
+    },
     blocks: [
       { type: 'image', id: 'b_init5', data: { imageUrl: 'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?w=800' } },
       { type: 'privilege', id: 'b_init6', data: { title: '尊享体验', items: [{ id: '1', title: '高尔夫权益', desc: 'VIP高尔夫俱乐部免会籍预约' }] } }
@@ -196,7 +209,7 @@ Mock.mock(/\/api\/assembly\/save/, 'post', (options) => {
       assemblyList[idx] = { ...assemblyList[idx], ...body, updateTime: now }
     }
   } else {
-    body.id = 'A' + (++assemblyIdCounter)
+    body.id = String(++assemblyIdCounter)
     body.createTime = now
     body.updateTime = now
     assemblyList.unshift(body)

+ 170 - 11
pc-v3/src/views/PageAssembly.vue

@@ -28,11 +28,10 @@
         </template>
 
         <el-table :data="tableData" stripe border style="width:100%">
-          <el-table-column label="操作" width="200" fixed="left">
+          <el-table-column label="操作" width="140" fixed="left">
             <template #default="scope">
               <el-button type="text" size="small" @click="handleEdit(scope.row)">编辑</el-button>
               <el-button type="text" size="small" @click="showLinkDialog(scope.row.id)">链接</el-button>
-              <el-button type="text" size="small" style="color:#F56C6C" @click="handleDelete(scope.row)">删除</el-button>
             </template>
           </el-table-column>
           <el-table-column prop="id" label="页面ID" width="120" align="center" />
@@ -86,6 +85,56 @@
         </div>
       </el-card>
 
+      <!-- 微信分享卡片配置 -->
+      <div class="block-card">
+        <div class="block-header">
+          <span><el-icon><ChatDotRound /></el-icon> 微信分享卡片配置</span>
+          <el-checkbox v-model="editData.shareConfig.enabled" @change="isModified = true" class="share-toggle-checkbox">自定义微信分享</el-checkbox>
+        </div>
+        <div class="block-body" v-if="editData.shareConfig.enabled">
+          <div class="share-card-body">
+            <div class="share-thumb-section">
+              <div class="share-field-label">分享缩略图 <span class="required-star">*</span></div>
+              <div class="share-thumb-upload" @click="triggerShareUpload">
+                <img v-if="editData.shareConfig.thumbnail" :src="editData.shareConfig.thumbnail" class="share-thumb-img" />
+                <div v-else class="upload-placeholder">
+                  <el-icon><Picture /></el-icon>
+                </div>
+                <div class="upload-overlay">
+                  <el-icon><Upload /></el-icon>
+                  <span>点击上传</span>
+                </div>
+              </div>
+              <div class="share-thumb-tip">建议 100x100</div>
+              <input ref="shareUpload" type="file" accept="image/*" style="display:none" @change="onShareImageUpload" />
+            </div>
+            <div class="share-fields">
+              <el-form label-position="top">
+                <el-form-item>
+                  <template #label>
+                    分享标题 <span class="required-star">*</span>
+                  </template>
+                  <el-input
+                    v-model="editData.shareConfig.title"
+                    placeholder="请输入分享标题"
+                    @input="isModified = true"
+                  />
+                </el-form-item>
+                <el-form-item label="分享描述">
+                  <el-input
+                    v-model="editData.shareConfig.desc"
+                    type="textarea"
+                    :rows="3"
+                    placeholder="请输入分享描述语"
+                    @input="isModified = true"
+                  />
+                </el-form-item>
+              </el-form>
+            </div>
+          </div>
+        </div>
+      </div>
+
       <!-- 模块列表 -->
       <div v-for="(block, bIdx) in editData.blocks" :key="block.id" class="block-card">
         <!-- 模块操作栏 -->
@@ -221,12 +270,19 @@
                   </el-form-item>
                   <!-- 弹窗配置 -->
                   <div v-if="btn.actionType === 'dialog' && btn.dialog" class="dialog-config">
-                    <div class="dialog-config-title"><el-icon><Document /></el-icon> 弹窗内容配置(包含取消、确认按钮)</div>
+                    <div class="dialog-config-title"><el-icon><Document /></el-icon> 弹窗内容配置</div>
                     <el-form-item label="弹窗标题">
                       <el-input v-model="btn.dialog.title" size="small" placeholder="弹窗标题" @input="isModified = true" />
                     </el-form-item>
                     <el-form-item label="弹窗描述">
-                      <el-input v-model="btn.dialog.desc" type="textarea" :rows="2" size="small" placeholder="弹窗正文提示..." @input="isModified = true" />
+                      <div class="dialog-desc-editor">
+                        <quill-editor
+                          v-model:content="btn.dialog.desc"
+                          content-type="html"
+                          :options="quillOptions"
+                          @change="isModified = true"
+                        />
+                      </div>
                     </el-form-item>
                     <el-row :gutter="16">
                       <el-col :span="12">
@@ -404,6 +460,15 @@ export default {
         }
       }
     },
+    createDefaultShareConfig(overrides = {}) {
+      return {
+        enabled: false,
+        thumbnail: '',
+        title: '',
+        desc: '',
+        ...overrides
+      }
+    },
     ensureReactiveField(target, key, value) {
       if (Object.prototype.hasOwnProperty.call(target, key)) {
         target[key] = value
@@ -429,6 +494,7 @@ export default {
       }
     },
     normalizeAssemblyData(data) {
+      data.shareConfig = this.createDefaultShareConfig(data.shareConfig || {})
       const blocks = data.blocks || []
       blocks.forEach(block => {
         if (block.type !== 'buttonGroup') return
@@ -441,12 +507,8 @@ export default {
     createEmptyAssembly() {
       return {
         id: '', name: '', status: 'draft',
-        blocks: [
-          { 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: [this.createDefaultButton({ text: '点击配置', style: 'primary' })] } }
-        ]
+        shareConfig: this.createDefaultShareConfig(),
+        blocks: []
       }
     },
     async fetchList() {
@@ -506,6 +568,16 @@ export default {
         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 = '未命名组装页'
       data.status = type === 'publish' ? 'published' : 'draft'
 
@@ -520,6 +592,22 @@ export default {
       if (this.quillReady) this.isModified = true
     },
 
+    triggerShareUpload() {
+      const ref = this.$refs.shareUpload
+      if (ref) ref.click()
+    },
+    onShareImageUpload(event) {
+      const file = event.target.files[0]
+      if (!file) return
+      const reader = new FileReader()
+      reader.onload = (e) => {
+        this.editData.shareConfig.thumbnail = e.target.result
+        this.isModified = true
+      }
+      reader.readAsDataURL(file)
+      event.target.value = ''
+    },
+
     // 模块操作
     addBlock(type) {
       const block = { type, id: genBlockId(), data: {} }
@@ -642,6 +730,76 @@ export default {
 .info-item { display: flex; align-items: center; gap: 8px; }
 .info-label { font-size: 13px; color: #909399; white-space: nowrap; }
 
+.required-star { color: #F56C6C; }
+.dialog-desc-editor {
+  width: 100%;
+  background: #fafafa;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  overflow: hidden;
+}
+.dialog-desc-editor .ql-container { height: 100px; font-size: 13px; }
+.dialog-desc-editor .ql-toolbar { background: #fff; border: none; border-bottom: 1px solid #ebeef5; }
+.share-toggle-checkbox {
+  position: absolute;
+  right: 0;
+  top: 0;
+  z-index: 10;
+  padding: 6px 8px;
+  background: #f5f7fa;
+  border-bottom: 1px solid #ebeef5;
+  border-left: 1px solid #ebeef5;
+  border-radius: 0 0 0 8px;
+  margin: 0;
+  color: #409EFF;
+  font-weight: normal;
+  font-size: 13px;
+}
+.share-card-body {
+  display: flex;
+  gap: 40px;
+  align-items: flex-start;
+  padding: 0 4px;
+}
+.share-thumb-section {
+  width: 108px;
+  flex-shrink: 0;
+}
+.share-field-label {
+  font-size: 13px;
+  color: #303133;
+  margin-bottom: 8px;
+}
+.share-thumb-upload {
+  width: 108px;
+  height: 108px;
+  background: #eef6fc;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  position: relative;
+  overflow: hidden;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.share-thumb-upload:hover .upload-overlay { display: flex; }
+.share-thumb-img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+.share-thumb-tip {
+  margin-top: 8px;
+  text-align: center;
+  font-size: 12px;
+  color: #909399;
+}
+.share-fields {
+  flex: 1;
+  min-width: 0;
+}
+
 .block-card {
   background: #fff; border-radius: 8px; border: 1px solid #ebeef5;
   margin-bottom: 16px; overflow: hidden; position: relative;
@@ -713,7 +871,7 @@ export default {
 
 .button-config-item {
   position: relative; border: 1px solid #ebeef5; border-radius: 6px;
-  padding: 24px 122px 20px 20px; margin-bottom: 12px; background: #fafafa;
+  padding: 24px 20px 20px; margin-bottom: 12px; background: #fafafa;
 }
 .btn-index {
   position: absolute; left: 0; top: 0;
@@ -729,6 +887,7 @@ export default {
   font-weight: 400;
 }
 .button-form .el-select { width: 100%; }
+.button-form > .el-row { padding-right: 102px; }
 .btn-config-actions {
   position: absolute; right: 20px; top: 32px;
   display: flex; gap: 6px;

+ 63 - 14
pc-v3/src/views/PageHomeConfig.vue

@@ -105,9 +105,14 @@
         <template #header>
           <div class="section-header">
             <el-icon><Grid /></el-icon> 权益列表配置
-            <el-button type="text" @click="addPrivilege" style="float:right">
-              <el-icon><CirclePlus /></el-icon>添加权益
-            </el-button>
+            <div class="header-right">
+              <el-checkbox v-model="currentTierData.privilegeImageEnabled" @change="onPrivilegeImageToggle">
+                展示权益图片
+              </el-checkbox>
+              <el-button type="text" @click="addPrivilege">
+                <el-icon><CirclePlus /></el-icon>添加权益
+              </el-button>
+            </div>
           </div>
         </template>
         <div v-if="currentTierData.privileges.length === 0" class="empty-tip">
@@ -135,15 +140,17 @@
             </el-button>
           </div>
           <div class="privilege-content">
-            <div class="privilege-image" @click="triggerUpload('privilege', index)">
-              <img v-if="item.imageUrl" :src="item.imageUrl" class="preview-img-sm" />
-              <div v-else class="upload-placeholder-sm">
-                <el-icon><Picture /></el-icon>
-                <span>未上传</span>
+            <div v-if="currentTierData.privilegeImageEnabled" class="privilege-image-wrap">
+              <div class="privilege-image" @click="triggerUpload('privilege', index)">
+                <img v-if="item.imageUrl" :src="item.imageUrl" class="preview-img-sm" />
+                <div v-else class="upload-placeholder-sm">
+                  <el-icon><Picture /></el-icon>
+                  <span>未上传</span>
+                </div>
+                <input :ref="'privilegeUpload' + index" type="file" accept="image/*" style="display:none" @change="onImageUpload($event, 'privilege', index)" />
               </div>
-              <input :ref="'privilegeUpload' + index" type="file" accept="image/*" style="display:none" @change="onImageUpload($event, 'privilege', index)" />
+              <div class="privilege-text-tip">必填,建议尺寸:<br/>100 * 100 px</div>
             </div>
-            <div class="privilege-text-tip">非必填<br/>无图展示圆点</div>
             <div class="privilege-fields">
               <el-form label-width="60px" inline>
                 <el-form-item label="标题">
@@ -248,9 +255,22 @@ export default {
   methods: {
     async fetchData() {
       const res = await request.get('/api/page-config/home')
-      this.configData = res.data.config
+      this.configData = this.normalizeHomeConfig(res.data.config || {})
       this.pageStatus = res.data.status
     },
+    normalizeHomeConfig(config) {
+      Object.keys(config).forEach((key) => {
+        const tier = config[key]
+        if (!tier) return
+        if (!Object.prototype.hasOwnProperty.call(tier, 'privilegeImageEnabled')) {
+          tier.privilegeImageEnabled = false
+        }
+        if (!Array.isArray(tier.privileges)) {
+          tier.privileges = []
+        }
+      })
+      return config
+    },
     async loadLinkOptions() {
       const { campaignOptions, channelOptions } = await fetchLinkOptions()
       this.campaignOptions = campaignOptions
@@ -299,12 +319,32 @@ export default {
       this.currentTierData.privileges.splice(to, 0, item)
       this.markModified()
     },
+    validatePrivilegeImages() {
+      const entries = Object.entries(this.configData || {})
+      for (const [key, tier] of entries) {
+        if (!tier || !tier.privilegeImageEnabled) continue
+        const missingIndex = (tier.privileges || []).findIndex(item => !item.imageUrl)
+        if (missingIndex >= 0) {
+          this.currentTier = key
+          ElMessage.error(`${tier.tabName || '当前客群'} 第 ${missingIndex + 1} 个权益未上传图片,请补充后再保存。`)
+          return false
+        }
+      }
+      return true
+    },
     async handleSave(type) {
+      if (!this.validatePrivilegeImages()) return
       const status = type === 'publish' ? 'published' : 'draft'
       await request.put('/api/page-config/home', { config: this.configData, status })
       this.pageStatus = status
       ElMessage.success(type === 'publish' ? '已发布权益介绍页配置!' : '已保存权益介绍页配置草稿!')
     },
+    onPrivilegeImageToggle(val) {
+      this.markModified()
+      if (!val) {
+        this.currentTierData.privileges.forEach(p => { p.imageUrl = '' })
+      }
+    },
     async showLinkDialog() {
       await this.loadLinkOptions()
       this.linkForm = { campaignId: '', channelId: '' }
@@ -337,8 +377,10 @@ export default {
 
 .config-sections { margin-top: 16px; }
 .section-card { margin-bottom: 16px; }
-.section-header { font-weight: 600; }
-.section-header i { margin-right: 6px; color: #909399; }
+.section-header { display: flex; align-items: center; gap: 12px; font-size: 15px; font-weight: 600; color: #303133; }
+.section-header i { color: #909399; font-size: 18px; }
+.header-right { margin-left: auto; display: flex; align-items: center; gap: 12px; }
+.header-right :deep(.el-checkbox) { font-weight: normal; }
 .header-config-form .el-form-item__label { white-space: nowrap; }
 
 .image-upload-area { display: flex; align-items: center; gap: 16px; }
@@ -384,6 +426,13 @@ export default {
   display: flex; gap: 6px;
 }
 .privilege-content { display: flex; align-items: flex-start; gap: 12px; margin-top: 8px; }
+.privilege-image-wrap {
+  width: 96px;
+  flex-shrink: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
 .privilege-image {
   width: 56px; height: 56px; flex-shrink: 0;
   background: #eef6fc; border: 1px solid #dcdfe6; border-radius: 4px;
@@ -396,7 +445,7 @@ export default {
   color: #c0c4cc; font-size: 10px;
 }
 .upload-placeholder-sm i { font-size: 18px; }
-.privilege-text-tip { font-size: 10px; color: #c0c4cc; text-align: center; line-height: 1.3; flex-shrink: 0; margin-top: 4px; }
+.privilege-text-tip { font-size: 10px; color: #909399; text-align: center; line-height: 1.35; margin-top: 6px; }
 .privilege-fields { flex: 1; }
 
 .empty-tip {

+ 113 - 49
pc-v3/src/views/PageZoneConfig.vue

@@ -28,7 +28,7 @@
         type="info"
         :closable="false"
         show-icon
-        title="前端将根据登录客户的实际客群 (MSC) 自动匹配展示以下对应的身份卡片内容。若关闭“显示账户月内日均余额”开关,则对应的计算规则弹窗配置项将自动隐藏,前端也不会展示。"
+        title="前端将根据登录客户的实际客户MSC Group自动匹配展示以下对应的身份卡片内容。"
         style="margin-bottom:16px"
       />
 
@@ -55,27 +55,20 @@
           </div>
         </div>
 
-        <!-- 身份标识文字 + 右侧开关 -->
-        <div class="identity-row identity-text-row">
-          <div class="identity-field">
-            <div class="identity-field-label">身份标识文字 <span class="muted">(显示于移动端卡片左上角)</span></div>
-            <el-input
-              v-model="configData.identityModule[m.key].identityText"
-              maxlength="20"
-              placeholder="请输入"
-              style="width:360px"
-              @input="markModified"
+        <!-- 等级说明 -->
+        <div v-if="hasGradeTipConfig(m.key)" class="identity-extra-block">
+          <div class="config-field">
+            <div class="config-field-label">等级说明</div>
+            <quill-editor
+              v-model:content="configData.identityModule[m.key].identityTipContent"
+              content-type="html"
+              :options="quillOptions"
+              @change="markModified"
             />
-          </div>
-          <div class="identity-toggles">
-            <template v-if="m.key !== 'privateBank'">
-              <el-checkbox v-model="configData.identityModule[m.key].showQualifyStatus" @change="markModified">显示合格状态 (及编号)</el-checkbox>
-              <el-checkbox v-model="configData.identityModule[m.key].showAvgBalance" @change="markModified">显示“账户月内日均余额”</el-checkbox>
-            </template>
-            <template v-else>
-              <el-checkbox v-model="configData.identityModule.privateBank.showPbLevel" @change="markModified">显示PB权益等级及编号 (支持复制)</el-checkbox>
-              <div class="pb-hint">(PB客群不显示月内日均余额)</div>
-            </template>
+            <div class="config-tip">
+              <el-icon><InfoFilled /></el-icon>
+              <span>移动端对应:用于配置【等级名称】旁边的“?”提示弹窗内容。</span>
+            </div>
           </div>
         </div>
 
@@ -84,17 +77,29 @@
           v-if="m.key !== 'privateBank' && configData.identityModule[m.key].showAvgBalance"
           class="rule-block"
         >
-          <div class="rule-label">
-            <el-icon><QuestionFilled /></el-icon>
-            日均余额计算规则说明 <span class="muted">(前端点击问号展示)</span>
+          <div class="config-field">
+            <div class="config-field-label">账户月内日均余额名称</div>
+            <el-input
+              v-model="configData.identityModule[m.key].avgBalanceName"
+              maxlength="20"
+              placeholder="请输入"
+              class="balance-name-input"
+              @input="markModified"
+            />
+          </div>
+          <div class="config-field">
+            <div class="config-field-label">余额说明</div>
+            <quill-editor
+              v-model:content="configData.identityModule[m.key].avgBalanceRule"
+              content-type="html"
+              :options="quillOptions"
+              @change="markModified"
+            />
+            <div class="config-tip">
+              <el-icon><InfoFilled /></el-icon>
+              <span>移动端对应:用于配置【账户月内日均余额】旁边的“?”提示弹窗内容。</span>
+            </div>
           </div>
-          <el-input
-            v-model="configData.identityModule[m.key].avgBalanceRule"
-            type="textarea"
-            :rows="3"
-            placeholder="请输入计算规则说明"
-            @input="markModified"
-          />
         </div>
       </div>
     </el-card>
@@ -329,7 +334,7 @@ export default {
         { key: 'advance', label: '运筹理财 (Advance)' },
         { key: 'premier', label: '卓越理财 (Premier)' },
         { key: 'premierElite', label: '卓越理财·尊尚 (Premier Elite)' },
-        { key: 'privateBank', label: '环球私人银行 (Private Bank)' }
+        { key: 'privateBank', label: '环球私人银行 (Global Private Bank)' }
       ],
       availableTags: [],
       pageStatus: 'draft',
@@ -340,7 +345,7 @@ export default {
       campaignOptions: [],
       channelOptions: [],
       quillOptions: {
-        placeholder: '请输入积分规则内容...',
+        placeholder: '请输入富文本内容...',
         modules: {
           toolbar: [
             ['bold', 'italic', 'underline'],
@@ -376,18 +381,46 @@ export default {
   },
   methods: {
     defaultIdentityModule() {
-      const base = { bgImage: '', identityText: '', showQualifyStatus: false, showAvgBalance: true, avgBalanceRule: '' }
+      const base = {
+        bgImage: '',
+        showAvgBalance: true,
+        avgBalanceName: '账户月内日均余额',
+        avgBalanceRule: '<p></p>'
+      }
+      const gradeTipBase = { identityTipContent: '<p>展示客户当前身份等级说明。</p>' }
       return {
         advance: { ...base },
-        premier: { ...base },
-        premierElite: { ...base },
-        privateBank: { bgImage: '', identityText: '', showPbLevel: true }
+        premier: { ...base, ...gradeTipBase },
+        premierElite: { ...base, ...gradeTipBase },
+        privateBank: {
+          bgImage: '',
+          identityTipContent: '<p>环球私人银行客户身份由客户 MSC Group 自动匹配展示。</p>'
+        }
       }
     },
+    hasGradeTipConfig(key) {
+      return ['premier', 'premierElite', 'privateBank'].includes(key)
+    },
+    normalizeIdentityModule(source = {}) {
+      const defaults = this.defaultIdentityModule()
+      Object.keys(defaults).forEach((key) => {
+        const current = source[key] || {}
+        source[key] = { ...defaults[key], ...current }
+        delete source[key].showQualifyStatus
+        delete source[key].showPbLevel
+        delete source[key].identityText
+        delete source[key].qualifyStatusText
+        if (source[key].avgBalanceRule && !String(source[key].avgBalanceRule).trim().startsWith('<')) {
+          source[key].avgBalanceRule = `<p>${source[key].avgBalanceRule}</p>`
+        }
+      })
+      return source
+    },
     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)
       this.configData = cfg
       this.availableTags = res.data.availableTags
       this.pageStatus = res.data.status
@@ -607,20 +640,51 @@ export default {
   display: flex; align-items: center; justify-content: center; flex-shrink: 0;
 }
 .bg-upload-box:hover .upload-overlay { display: flex; }
-.identity-text-row {
-  display: flex; align-items: flex-start; justify-content: space-between; gap: 24px;
-}
-.identity-field { flex: 1; }
-.identity-toggles {
-  display: flex; flex-direction: column; gap: 6px; padding-top: 22px;
+.identity-extra-block {
+  padding: 0;
+  margin: 8px 0 22px;
 }
-.pb-hint { font-size: 12px; color: #909399; margin-top: 4px; }
 .rule-block {
-  background: #f5f7fa; border: 1px solid #ebeef5; border-radius: 4px;
-  padding: 12px 14px;
+  padding: 22px 0 0;
+  border-top: 1px solid #ebeef5;
+}
+.config-field {
+  margin-bottom: 22px;
+}
+.config-field:last-child {
+  margin-bottom: 0;
+}
+.config-field-label {
+  font-size: 14px;
+  font-weight: 600;
+  color: #1f2d3d;
+  margin-bottom: 10px;
+  line-height: 20px;
+}
+.balance-name-input {
+  width: 420px;
+  max-width: 100%;
+}
+.config-tip {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  margin-top: 8px;
+  color: #606266;
+  font-size: 12px;
+  line-height: 18px;
+}
+.config-tip i {
+  color: #409EFF;
+  font-size: 14px;
+}
+:deep(.identity-card .ql-container.ql-snow) {
+  height: 120px;
+}
+:deep(.identity-card .ql-toolbar.ql-snow) {
+  border-radius: 4px 4px 0 0;
 }
-.rule-label {
-  font-size: 13px; color: #606266; margin-bottom: 8px;
+:deep(.identity-card .ql-container.ql-snow) {
+  border-radius: 0 0 4px 4px;
 }
-.rule-label i { color: #409EFF; margin-right: 4px; }
 </style>