Bladeren bron

feat(activity): 创建活动页新增主办方和海报模板选择功能

前端create.vue:
- 新增主办方输入框(vendorName)
- 新增海报模板选择区域(调用getPosterTemplates接口)
- 集成ContentSharePoster预览组件,支持点击预览海报效果
- loadDetail时还原vendorName和posterTemplateId
- buildPayload中处理vendorName和posterTemplateId(非空才提交)

后端:
- Activity实体新增posterTemplateId字段
- ActivityDTO同步新增posterTemplateId及from映射
- schema.sql新增poster_template_id列定义
- DatabaseInitializer迁移388: activities表添加poster_template_id列
liaoxg 4 dagen geleden
bovenliggende
commit
8f93672779

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -3115,6 +3115,10 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         //      平台赠送券(JOIN/POPULATION/MANUAL)无对应兑换记录,退款时不退 CF。
         ensureColumn("family_platform_exchange_record", "family_coupon_id",
                 "BIGINT DEFAULT NULL COMMENT '兑换发放的优惠券实例ID(family_coupon.id), 退款时据此退还CF值'");
+
+        // 迁移388: activities 添加 poster_template_id 列(小程序端创建活动时选择海报模板并保存,详情页生成海报时默认使用)
+        ensureColumn("activities", "poster_template_id",
+                "BIGINT NULL COMMENT '创建时选择的海报模板ID(activity_poster_templates.id, 小程序端海报模板选择)'");
     }
 
     private void migrateHealthDimensionsTable() {

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/ActivityDTO.java

@@ -64,6 +64,9 @@ public class ActivityDTO {
     /** 评价维度列表JSON(如 ["组织形式","会场气氛","主持人","专家"]) */
     private String feedbackDimensions;
 
+    /** 创建时选择的海报模板ID(小程序端海报模板选择) */
+    private Long posterTemplateId;
+
     public static ActivityDTO from(Activity activity) {
         return from(activity, false, null);
     }
@@ -98,6 +101,7 @@ public class ActivityDTO {
         dto.setParentMemberFee(activity.getParentMemberFee());
         dto.setCreatorId(activity.getCreatorId());
         dto.setFeedbackDimensions(activity.getFeedbackDimensions());
+        dto.setPosterTemplateId(activity.getPosterTemplateId());
         // Member-aware pricing: resolve final display price based on membership level
         // Guests (no memberLevel) see base price; members see memberPrice if applicable
         Integer resolvedPrice = resolveMemberPrice(activity.getPrice(), activity.getMemberPrice(), memberLevel);

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Activity.java

@@ -109,6 +109,9 @@ public class Activity implements Serializable {
     /** 评价维度列表JSON(如 ["组织形式","会场气氛","主持人","专家"]) */
     private String feedbackDimensions;
 
+    /** 创建时选择的海报模板ID(小程序端海报模板选择,迁移388) */
+    private Long posterTemplateId;
+
     private Date createdAt;
 
     private Date updatedAt;

+ 1 - 0
cfc-backend/src/main/resources/schema.sql

@@ -2127,6 +2127,7 @@ CREATE TABLE IF NOT EXISTS activities (
     platform_points INT DEFAULT 0 COMMENT '活动奖励CF值(0=不发)',
     visibility VARCHAR(20) DEFAULT 'public' COMMENT '浏览权限:public/login/private',
     visible_to TEXT COMMENT '私密指定人群JSON',
+    poster_template_id BIGINT NULL COMMENT '创建时选择的海报模板ID(小程序端海报模板选择,迁移388)',
     created_at       DATETIME DEFAULT CURRENT_TIMESTAMP,
     updated_at       DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     INDEX idx_dimension (dimension_code),

+ 151 - 2
cfc-frontend/pages/activity/create.vue

@@ -82,6 +82,12 @@
         <input class="form-input" type="digit" v-model="parentMemberFeeYuan" placeholder="0=免费" />
       </view>
 
+      <!-- 主办方 -->
+      <view class="form-item">
+        <text class="form-label">主办方</text>
+        <input class="form-input" v-model="form.vendorName" placeholder="请输入主办方名称(可选)" maxlength="50" />
+      </view>
+
       <!-- 封面图片 -->
       <view class="form-item">
         <text class="form-label">封面图片</text>
@@ -116,6 +122,46 @@
       </view>
     </view>
 
+    <!-- 海报模板选择 + 预览 -->
+    <view class="form-card" v-if="posterTemplates.length > 0">
+      <view class="form-title">活动海报模板(可选)</view>
+      <view class="poster-tpl-list">
+        <view
+          v-for="tpl in posterTemplates"
+          :key="tpl.id"
+          class="poster-tpl-card"
+          :class="{ 'poster-tpl-active': form.posterTemplateId === tpl.id }"
+          @click="selectPosterTemplate(tpl)"
+        >
+          <view class="tpl-colors">
+            <view class="tpl-dot tpl-main" :style="{ background: tpl.mainColor || '#F97316' }"></view>
+            <view class="tpl-dot tpl-dark" :style="{ background: tpl.darkColor || '#EA580C' }"></view>
+            <view class="tpl-dot tpl-light" :style="{ background: tpl.lightColor || '#FFF7ED' }"></view>
+          </view>
+          <text class="tpl-name">{{ tpl.name }}</text>
+        </view>
+      </view>
+      <view class="poster-preview-row" v-if="form.posterTemplateId">
+        <text class="poster-preview-hint">已选择海报模板,点击「预览」查看效果</text>
+        <view class="btn-preview" @click="openPosterPreview">预览</view>
+      </view>
+    </view>
+
+    <!-- 海报预览弹层 -->
+    <content-share-poster
+      v-if="showPosterPreview"
+      :show="showPosterPreview"
+      :title="form.title || '活动海报'"
+      :cover-image="form.coverImage"
+      :dimension-code="form.dimensionCode"
+      :template="previewTemplateKey"
+      :templates="posterTemplates"
+      :poster-qr-code="''"
+      @close="showPosterPreview = false"
+      @update:template="previewTemplateKey = $event"
+      @update:bgColor="previewBgColor = $event"
+    ></content-share-poster>
+
     <view class="loading" v-if="loading">
       <view class="loading-spinner"></view>
       <text>加载中...</text>
@@ -124,15 +170,17 @@
 </template>
 
 <script>
-import { adminCreateActivity, adminUpdateActivity, adminPublishActivity, submitActivityForReview, getActivityDetail } from '../../utils/api.js'
+import { adminCreateActivity, adminUpdateActivity, adminPublishActivity, submitActivityForReview, getActivityDetail, getPosterTemplates } from '../../utils/api.js'
 import config from '../../config.js'
 import { parseDate } from '../../utils/format.js'
 import { hasActivityManageRole } from '../../utils/activity-role.js'
 import { pickImage } from '../../utils/image-picker.js'
+import ContentSharePoster from '../../components/ContentSharePoster.vue'
 
 var BASE_URL = config.api('')
 
 export default {
+  components: { ContentSharePoster },
   computed: {
     // 后台活动管理员可直接发布;服务商账号只能保存后提交审核
     isAdmin: function() {
@@ -182,7 +230,13 @@ export default {
         { key: 'offline', label: '线下活动' },
         { key: 'online', label: '线上活动' },
         { key: 'campaign', label: '营销活动' }
-      ]
+      ],
+      // 海报模板
+      posterTemplates: [],
+      selectedPosterTemplate: null,
+      showPosterPreview: false,
+      previewTemplateKey: 'gradient',
+      previewBgColor: ''
     }
   },
   onLoad(options) {
@@ -194,6 +248,8 @@ export default {
     } else {
       uni.setNavigationBarTitle({ title: '创建活动' })
     }
+    // 加载海报模板列表(未登录也会返回空列表,不阻塞创建)
+    this.loadPosterTemplates()
   },
   methods: {
     loadDetail() {
@@ -206,6 +262,8 @@ export default {
           self.form.title = d.title || ''
           self.form.description = d.description || ''
           self.form.coverImage = d.coverImage || ''
+          self.form.vendorName = d.vendorName || ''
+          self.form.posterTemplateId = d.posterTemplateId || null
           self.form.dimensionCode = d.dimensionCode || ''
           self.form.activityType = d.activityType || ''
           self.form.location = d.location || ''
@@ -346,6 +404,29 @@ export default {
     removeCover() {
       this.form.coverImage = ''
     },
+    async loadPosterTemplates() {
+      try {
+        var res = await getPosterTemplates()
+        if (res && res.code === 200) {
+          this.posterTemplates = res.data || []
+        }
+      } catch (e) { /* 模板拉取失败不影响创建 */ }
+    },
+    selectPosterTemplate(tpl) {
+      this.form.posterTemplateId = tpl.id
+      this.selectedPosterTemplate = tpl
+      this.previewTemplateKey = tpl.templateKey || 'gradient'
+      // 默认背景色取模板主色
+      this.previewBgColor = tpl.mainColor || ''
+      uni.showToast({ title: '已选择:' + tpl.name, icon: 'none' })
+    },
+    openPosterPreview() {
+      if (!this.form.posterTemplateId) {
+        uni.showToast({ title: '请先选择海报模板', icon: 'none' })
+        return
+      }
+      this.showPosterPreview = true
+    },
     validate() {
       if (!this.form.title) {
         uni.showToast({ title: '请输入活动标题', icon: 'none' })
@@ -371,6 +452,9 @@ export default {
       if (this.activityId) payload.id = this.activityId
       // 维度由审核时设置,创建/编辑不提交空维度,避免误清空已有维度
       if (!payload.dimensionCode) delete payload.dimensionCode
+      // 主办方和海报模板仅非空时提交
+      if (!payload.vendorName) delete payload.vendorName
+      if (!payload.posterTemplateId) delete payload.posterTemplateId
       return payload
     },
     handleSave() {
@@ -617,6 +701,71 @@ export default {
   text-decoration: underline;
 }
 
+/* 海报模板选择 */
+.poster-tpl-list {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  gap: 16rpx;
+  margin-top: 20rpx;
+}
+
+.poster-tpl-card {
+  width: 200rpx;
+  border: 2rpx solid #E2E8F0;
+  border-radius: 12rpx;
+  padding: 20rpx 16rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  background: #fff;
+}
+
+.poster-tpl-active {
+  border-color: #F97316;
+  background: #FFF7ED;
+}
+
+.tpl-colors {
+  display: flex;
+  flex-direction: row;
+  gap: 8rpx;
+  margin-bottom: 12rpx;
+}
+
+.tpl-dot {
+  width: 32rpx;
+  height: 32rpx;
+  border-radius: 50%;
+}
+
+.tpl-name {
+  font-size: 24rpx;
+  color: #1E293B;
+  text-align: center;
+}
+
+.poster-preview-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: 20rpx;
+}
+
+.poster-preview-hint {
+  font-size: 24rpx;
+  color: #64748B;
+}
+
+.btn-preview {
+  padding: 12rpx 32rpx;
+  border-radius: 32rpx;
+  background: #F97316;
+  color: #fff;
+  font-size: 26rpx;
+}
+
 .form-actions {
   display: flex;
   flex-direction: row;