فهرست منبع

feat: enhance activity poster with template selection and smaller QR code

- Add template selector UI (gradient/minimal/card) to ContentSharePoster
- Add background color selector for gradient template
- Reduce QR code size from 150 to 100rpx for activities
- Auto-detect background color from activity dimension
- Pass template/bgColor as writable props via event updates
iwt 5 روز پیش
والد
کامیت
84d1c4c5a3
2فایلهای تغییر یافته به همراه213 افزوده شده و 22 حذف شده
  1. 193 21
      cfc-frontend/components/ContentSharePoster.vue
  2. 20 1
      cfc-frontend/pages/activity/activity-detail/activity-detail.vue

+ 193 - 21
cfc-frontend/components/ContentSharePoster.vue

@@ -2,6 +2,50 @@
   <view class="poster-overlay" v-if="show" @click.self="$emit('close')">
     <view class="poster-container">
       <canvas canvas-id="contentPosterCanvas" class="poster-canvas" id="contentPosterCanvas"></canvas>
+      <!-- 模板选择 -->
+      <view class="poster-tpl-row">
+        <view class="tpl-label">海报模板</view>
+        <view class="tpl-options">
+          <view
+            class="tpl-option"
+            :class="{ 'tpl-active': template === 'gradient' }"
+            @click="switchTemplate('gradient')"
+          >
+            <view class="tpl-dot tpl-dot-grad"></view>
+            <text class="tpl-text">渐变</text>
+          </view>
+          <view
+            class="tpl-option"
+            :class="{ 'tpl-active': template === 'minimal' }"
+            @click="switchTemplate('minimal')"
+          >
+            <view class="tpl-dot tpl-dot-dark"></view>
+            <text class="tpl-text">深色</text>
+          </view>
+          <view
+            class="tpl-option"
+            :class="{ 'tpl-active': template === 'card' }"
+            @click="switchTemplate('card')"
+          >
+            <view class="tpl-dot tpl-dot-light"></view>
+            <text class="tpl-text">卡片</text>
+          </view>
+        </view>
+      </view>
+      <!-- 背景颜色选择(渐变模板可用) -->
+      <view class="poster-bg-row" v-if="template === 'gradient'">
+        <view class="tpl-label">背景色</view>
+        <view class="bg-options">
+          <view
+            v-for="(c, i) in bgColors"
+            :key="i"
+            class="bg-option"
+            :class="{ 'bg-active': bgColor === c }"
+            :style="{ background: c }"
+            @click="switchBgColor(c)"
+          ></view>
+        </view>
+      </view>
       <view class="poster-actions">
         <button class="poster-btn save" @click="savePoster">保存到相册</button>
       </view>
@@ -23,6 +67,12 @@ export default {
     priceText: { type: String, default: '' },
     ctaText: { type: String, default: '长按识别二维码 · 查看详情' },
     inviteText: { type: String, default: '' },
+    // 海报模板: gradient(default) | minimal | card
+    template: { type: String, default: 'gradient' },
+    // 自定义背景色(覆盖模板默认背景)
+    bgColor: { type: String, default: '' },
+    // 二维码尺寸覆盖(默认150rpx,建议活动类可用 100 缩小)
+    qrSizeOverride: { type: Number, default: 150 },
     // 五维能量数据卡:{ label?: String, overall?: Number, dims: [{ key, label, score, color }] }
     // 传 null 时不画数据卡,按 coverImage -> brandFallback -> 占位图 顺序降级(原有行为不变)
     energyData: { type: Object, default: null },
@@ -38,6 +88,12 @@ export default {
       }
     }
   },
+  data() {
+    return {
+      // 可选的背景色(渐变模板下展示)
+      bgColors: ['#FF8C42', '#6366F1', '#F59E0B', '#10B981', '#FF6B9D', '#1E293B', '#FFFFFF']
+    }
+  },
   methods: {
     getColors() {
       var map = {
@@ -49,6 +105,35 @@ export default {
       }
       return map[this.dimensionCode] || { main: '#F97316', dark: '#EA580C', light: '#FFF7ED' }
     },
+    // 海报模板配色
+    getTemplateConfig() {
+      var colors = this.getColors()
+      var template = this.template || 'gradient'
+      var bgColor = this.bgColor || ''
+      // 梯度背景模板
+      if (template === 'gradient') {
+        return { template: 'gradient', bgColor: bgColor || colors.main, main: colors.main, dark: colors.dark, light: colors.light }
+      }
+      // 极简模板:深色背景+白色卡片
+      if (template === 'minimal') {
+        var dark = bgColor || '#1E293B'
+        return { template: 'minimal', bgColor: dark, main: '#F97316', dark: '#EF4444', light: '#F3F4F6' }
+      }
+      // 卡片模板:浅色背景+彩色顶栏
+      return { template: 'card', bgColor: bgColor || '#FFFFFF', main: colors.main, dark: colors.dark, light: colors.light }
+    },
+    // 获取当前模板的背景样式
+    getBgStyle() {
+      var cfg = this.getTemplateConfig()
+      if (cfg.template === 'gradient') {
+        var dark = cfg.dark
+        return 'linear-gradient(135deg, ' + cfg.main + ', ' + dark + ')'
+      }
+      if (cfg.template === 'minimal') {
+        return 'linear-gradient(135deg, ' + cfg.bgColor + ', ' + cfg.dark + ')'
+      }
+      return 'linear-gradient(135deg, ' + cfg.main + ', ' + (cfg.bgColor || '#FFFFFF') + ')'
+    },
     drawPoster() {
       var self = this
       uni.createSelectorQuery().in(this)
@@ -69,7 +154,7 @@ export default {
       var ctx = uni.createCanvasContext('contentPosterCanvas', this)
       var w = 500
       var h = 800
-      var colors = this.getColors()
+      var cfg = this.getTemplateConfig()
 
       // 画布 CSS 用 rpx,实际像素随设备变化(375px 屏约 310x496),而绘制按 500x800 设计坐标;
       // 不缩放坐标系时右侧/底部内容(含二维码 y≈552-702)会被裁剪出画布,导致海报显示不全。
@@ -77,20 +162,33 @@ export default {
         ctx.scale(scaleX, scaleY)
       }
 
-      var gradient = ctx.createLinearGradient(0, 0, w * 0.3, h)
-      gradient.addColorStop(0, colors.main)
-      gradient.addColorStop(0.55, colors.dark)
-      gradient.addColorStop(1, colors.dark)
-      ctx.setFillStyle(gradient)
-      ctx.fillRect(0, 0, w, h)
+      // 【模板1 · gradient】渐变背景
+      if (cfg.template === 'gradient') {
+        var gradient = ctx.createLinearGradient(0, 0, w * 0.3, h)
+        gradient.addColorStop(0, cfg.main)
+        gradient.addColorStop(0.55, cfg.dark)
+        gradient.addColorStop(1, cfg.dark)
+        ctx.setFillStyle(gradient)
+        ctx.fillRect(0, 0, w, h)
+      }
+      // 【模板2 · minimal】纯色深色背景
+      else if (cfg.template === 'minimal') {
+        ctx.setFillStyle(cfg.bgColor)
+        ctx.fillRect(0, 0, w, h)
+      }
+      // 【模板3 · card】白色背景
+      else {
+        ctx.setFillStyle('#FFFFFF')
+        ctx.fillRect(0, 0, w, h)
+      }
 
-      this._drawTopDecoration(ctx, w, colors)
+      this._drawTopDecoration(ctx, w, cfg)
 
       var cardX = 28
       var cardY = 112
       var cardW = 444
       var cardH = 600
-      this._drawContentCard(ctx, cardX, cardY, cardW, cardH, colors)
+      this._drawContentCard(ctx, cardX, cardY, cardW, cardH, cfg)
 
       var coverY = cardY + 20
       var coverW = cardW - 40
@@ -99,8 +197,8 @@ export default {
 
       if (this.energyData) {
         // 方案C:五维能量数据卡,canvas 直接绘制(文字锐利、数据真实、无网络开销)
-        this._drawEnergyCard(ctx, cardX + 20, coverY, coverW, coverH, colors)
-        this._drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, colors)
+        this._drawEnergyCard(ctx, cardX + 20, coverY, coverW, coverH, cfg)
+        this._drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, cfg)
       } else if (this.coverImage) {
         uni.downloadFile({
           url: this.coverImage,
@@ -112,29 +210,29 @@ export default {
                 success: function(info) {
                   var rect = self._fitCoverRect(info.width, info.height, cardX + 20, coverY, coverW, coverH)
                   ctx.drawImage(res.tempFilePath, rect.x, rect.y, rect.w, rect.h)
-                  self._drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, colors)
+                  self._drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, cfg)
                 },
                 fail: function() {
                   ctx.drawImage(res.tempFilePath, cardX + 20, coverY, coverW, coverH)
-                  self._drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, colors)
+                  self._drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, cfg)
                 }
               })
             } else {
               self._drawCoverPlaceholder(ctx, cardX + 20, coverY, coverW, coverH)
-              self._drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, colors)
+              self._drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, cfg)
             }
           },
           fail: function() {
             self._drawCoverPlaceholder(ctx, cardX + 20, coverY, coverW, coverH)
-            self._drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, colors)
+            self._drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, cfg)
           }
         })
       } else if (this.brandFallback) {
-        this._drawBrandCard(ctx, cardX + 20, coverY, coverW, coverH, colors)
-        this._drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, colors)
+        this._drawBrandCard(ctx, cardX + 20, coverY, coverW, coverH, cfg)
+        this._drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, cfg)
       } else {
         this._drawCoverPlaceholder(ctx, cardX + 20, coverY, coverW, coverH)
-        this._drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, colors)
+        this._drawContentAfterCover(ctx, w, h, cardX, cardW, coverY, coverH, cfg)
       }
     },
 
@@ -370,8 +468,8 @@ export default {
       ctx.setTextBaseline('top')
       ctx.fillText(this.inviteText || '长按识别二维码 · 查看详情', w / 2, inviteY)
 
-      // 二维码
-      var qrSize = 150
+      // 二维码 - 可配置尺寸(默认150,可通过 qrSizeOverride 调整)
+      var qrSize = this.qrSizeOverride || 150
       var qrX = (w - qrSize) / 2
       var qrY = inviteY + 24
 
@@ -526,6 +624,21 @@ export default {
       ctx.closePath()
     },
 
+    switchTemplate(tpl) {
+      this.$emit('update:template', tpl)
+      var self = this
+      this.$nextTick(function() {
+        setTimeout(self.drawPoster.bind(self), 100)
+      })
+    },
+    switchBgColor(color) {
+      this.$emit('update:bgColor', color)
+      var self = this
+      this.$nextTick(function() {
+        setTimeout(self.drawPoster.bind(self), 100)
+      })
+    },
+
     savePoster() {
       var self = this
       uni.canvasToTempFilePath({
@@ -610,4 +723,63 @@ export default {
   font-weight: 600;
 }
 .poster-btn.save:active { opacity: 0.85; }
-</style>
+.poster-tpl-row {
+  display: flex;
+  align-items: center;
+  margin: 12rpx 0;
+  font-size: 24rpx;
+}
+.tpl-label {
+  margin-right: 20rpx;
+  color: #666;
+}
+.tpl-options {
+  display: flex;
+}
+.tpl-option {
+  flex: 1;
+  margin: 0 4rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  cursor: pointer;
+}
+.tpl-dot {
+  width: 24rpx;
+  height: 24rpx;
+  border-radius: 999rpx;
+  margin-bottom: 4rpx;
+}
+.tpl-dot-grad { background: linear-gradient(135deg, #FF8C42, #EA580C); }
+.tpl-dot-dark { background: #1E293B; }
+.tpl-dot-light { background: #FFFFFF; }
+.tpl-text {
+  font-size: 14rpx;
+  text-align: center;
+  color: #666;
+}
+.tpl-active .tpl-text { color: #F97316; }
+
+.poster-bg-row {
+  display: flex;
+  align-items: center;
+  margin: 12rpx 0;
+  font-size: 24rpx;
+}
+.bg-label {
+  margin-right: 20rpx;
+  color: #666;
+}
+.bg-options {
+  display: flex;
+}
+.bg-option {
+  width: 32rpx;
+  height: 32rpx;
+  border-radius: 999rpx;
+  margin: 0 4rpx;
+  cursor: pointer;
+}
+.bg-active {
+  border: 2rpx solid #F97316;
+}</style>

+ 20 - 1
cfc-frontend/pages/activity/activity-detail/activity-detail.vue

@@ -261,7 +261,12 @@
       :summary="posterSummary || ''"
       :priceText="posterPriceText || ''"
       :inviteText="posterInviteText || ''"
+      :template="posterTemplate"
+      :bgColor="posterBgColor"
+      :qrSizeOverride="100"
       @close="closePoster"
+      @update:template="posterTemplate = $event"
+      @update:bgColor="posterBgColor = $event"
     />
   </view>
 </template>
@@ -310,7 +315,9 @@ export default {
       posterQrCode: '',
       posterSummary: '',
       posterPriceText: '',
-      posterInviteText: ''
+      posterInviteText: '',
+      posterTemplate: 'gradient',
+      posterBgColor: ''   // 封面背景色
     }
   },
   onLoad: function(options) {
@@ -688,6 +695,7 @@ export default {
           this.posterSummary = this.buildPosterSummary()
           this.posterPriceText = this.activity.price > 0 ? '¥' + this.activity.price : '免费'
           this.posterInviteText = '「' + (uni.getStorageSync('nickname') || '好友') + '」邀请你一起参加这个活动'
+          this.posterBgColor = this.buildPosterBgColor()
           this.showPoster = true
         }
       } catch (e) {
@@ -701,6 +709,17 @@ export default {
       desc = desc.replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/\s+/g, ' ').trim()
       return desc.length > 60 ? desc.substring(0, 60) + '...' : desc
     },
+    buildPosterBgColor: function() {
+      var dim = this.activity && this.activity.dimensionCode
+      var map = {
+        body: '#FF8C42',
+        mind: '#FF6B9D',
+        wisdom: '#6366F1',
+        action: '#10B981',
+        wealth: '#F59E0B'
+      }
+      return map[dim] || '#F97316'
+    },
     closePoster: function() {
       this.showPoster = false
     },