瀏覽代碼

feat(activity): 创建时去掉维度、封面改上传;审核时必填维度

前端 create.vue:
- 删除「所属维度」picker(模板/data/methods/validate/buildPayload),维度不再出现在创建活动表单中
- buildPayload 中若 dimensionCode 为空则从 payload 删除,避免误清空已有维度
- 封面图片从输入 URL 改为点击上传 UI:复用已有 chooseImage/uploadCover,预览用 getCoverUrl 兼容相对路径,支持重新选择和移除

后端 AdminActivityController / ActivityAdminService:
- audit/review 两个审核接口均接收 dimensionCode 参数
- 审核通过(approved/publish)时必填维度,空则报错拒绝
- 审核通过时将 dimensionCode 写入 activity 实体

cfc-web 审核页:
- ActivityAudit.vue(审核中心):抽屉 body 加维度 el-select,approve 时校验必填并传入 dimensionCode
- ActivityReview.vue(旧入口):approve 改为  要求选择维度(下拉/输入校验),透传 dimensionCode
liaoxg 4 天之前
父節點
當前提交
eec3e4c48a

+ 4 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminActivityController.java

@@ -40,8 +40,8 @@ public class AdminActivityController {
     }
 
     /**
-     * 审核活动
-     * @param params action: "publish" | "reject", id: Long
+     * 审核活动(旧入口:ActivityReview.vue 使用)
+     * @param params action: "publish" | "reject", id: Long, dimensionCode: String(发布时必填)
      */
     @PostMapping("/review")
     public Result<String> review(@RequestBody Map<String, Object> params) {
@@ -74,8 +74,9 @@ public class AdminActivityController {
         Long id = ParamUtils.getLong(params.get("id"));
         String action = (String) params.get("action");
         String rejectReason = (String) params.get("rejectReason");
+        String dimensionCode = (String) params.get("dimensionCode");
         if (id == null || action == null) return Result.error("id和action不能为空");
-        activityAdminService.auditActivity(id, action, rejectReason, adminId);
+        activityAdminService.auditActivity(id, action, rejectReason, adminId, dimensionCode);
         return Result.success("已审核");
     }
 

+ 17 - 3
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityAdminService.java

@@ -83,8 +83,8 @@ public class ActivityAdminService {
     }
 
     /**
-     * 审核活动
-     * @param params action: "publish" | "reject", id: Long
+     * 审核活动(旧入口:ActivityReview.vue 使用)
+     * @param params action: "publish" | "reject", id: Long, dimensionCode: String(publish 时必填)
      */
     public Result<String> review(Map<String, Object> params) {
         String action = (String) params.get("action");
@@ -99,7 +99,13 @@ public class ActivityAdminService {
             if (!"draft".equals(activity.getStatus())) {
                 return Result.error("仅草稿状态可发布");
             }
+            // 审核通过时维度为必填
+            String dimensionCode = (String) params.get("dimensionCode");
+            if (dimensionCode == null || dimensionCode.trim().isEmpty()) {
+                return Result.error("审核通过时必须选择所属维度");
+            }
             activity.setStatus("published");
+            activity.setDimensionCode(dimensionCode.trim());
             activityMapper.updateById(activity);
             return Result.success("发布成功");
         } else if ("reject".equals(action)) {
@@ -125,8 +131,12 @@ public class ActivityAdminService {
         activityMapper.updateById(activity);
     }
 
+    /**
+     * 审核活动(审核中心:ActivityAudit.vue 使用)
+     * @param params action: "approved" | "rejected", id: Long, dimensionCode: String(approved 时必填), rejectReason: String
+     */
     @Transactional
-    public void auditActivity(Long id, String action, String auditReason, Long adminId) {
+    public void auditActivity(Long id, String action, String auditReason, Long adminId, String dimensionCode) {
         Activity activity = activityMapper.selectById(id);
         if (activity == null) {
             throw new IllegalStateException("活动不存在");
@@ -138,8 +148,12 @@ public class ActivityAdminService {
             throw new IllegalStateException("审核操作无效");
         }
         if ("approved".equals(action)) {
+            if (dimensionCode == null || dimensionCode.trim().isEmpty()) {
+                throw new IllegalStateException("审核通过时必须选择所属维度");
+            }
             activity.setStatus("published");
             activity.setAuditStatus("approved");
+            activity.setDimensionCode(dimensionCode.trim());
         } else {
             activity.setStatus("rejected");
             activity.setAuditStatus("rejected");

+ 42 - 39
cfc-frontend/pages/activity/create.vue

@@ -1,22 +1,12 @@
 <template>
   <view class="container">
     <view class="form-card">
-      <view class="form-title">{{ isNew ? '创建活动' : '编辑活动' }}</view>
-
       <!-- 活动标题 -->
       <view class="form-item">
         <text class="form-label">活动标题 *</text>
         <input class="form-input" v-model="form.title" placeholder="请输入活动标题" maxlength="50" />
       </view>
 
-      <!-- 活动维度 -->
-      <view class="form-item">
-        <text class="form-label">所属维度 *</text>
-        <picker class="form-picker" :value="dimIdx" :range="dimensionOptions" range-key="label" @change="onDimChange">
-          <text class="picker-text">{{ form.dimensionCode ? dimLabel(form.dimensionCode) : '请选择维度' }}</text>
-        </picker>
-      </view>
-
       <!-- 活动类型 -->
       <view class="form-item">
         <text class="form-label">活动类型 *</text>
@@ -95,9 +85,16 @@
       <!-- 封面图片 -->
       <view class="form-item">
         <text class="form-label">封面图片</text>
-        <input class="form-input" v-model="form.coverImage" placeholder="输入图片URL(可选)" />
-        <view class="cover-preview" v-if="form.coverImage">
-          <image class="preview-img" :src="form.coverImage" mode="aspectFill" @error="onImgError" />
+        <view class="cover-upload" @click="chooseImage">
+          <image v-if="form.coverImage" class="preview-img" :src="getCoverUrl(form.coverImage)" mode="aspectFill" @click.stop />
+          <view v-else class="upload-placeholder">
+            <text class="upload-icon">📷</text>
+            <text class="upload-text">点击上传封面图</text>
+          </view>
+        </view>
+        <view class="cover-actions" v-if="form.coverImage">
+          <text class="cover-action" @click="chooseImage">重新选择</text>
+          <text class="cover-action-danger" @click="removeCover">移除</text>
         </view>
       </view>
 
@@ -180,15 +177,7 @@ export default {
       endTimePart: '',
       regDatePart: '',
       regTimePart: '',
-      dimIdx: 0,
       typeIdx: 0,
-      dimensionOptions: [
-        { key: 'body', label: '身 · 主动健康' },
-        { key: 'mind', label: '心 · 情感丰盈' },
-        { key: 'wisdom', label: '智 · 赋能未来' },
-        { key: 'action', label: '行 · 知行合一' },
-        { key: 'wealth', label: '富 · 物质精神' }
-      ],
       typeOptions: [
         { key: 'offline', label: '线下活动' },
         { key: 'online', label: '线上活动' },
@@ -256,20 +245,13 @@ export default {
               self.form.registrationDeadline = rd.toISOString()
             }
           }
-          // sync pickers
-          for (var i = 0; i < self.dimensionOptions.length; i++) {
-            if (self.dimensionOptions[i].key === self.form.dimensionCode) self.dimIdx = i
-          }
+          // sync type picker
           for (var j = 0; j < self.typeOptions.length; j++) {
             if (self.typeOptions[j].key === self.form.activityType) self.typeIdx = j
           }
         }
       }).catch(function() { self.loading = false })
     },
-    onDimChange(e) {
-      this.dimIdx = e.detail.value
-      this.form.dimensionCode = this.dimensionOptions[this.dimIdx].key
-    },
     onTypeChange(e) {
       this.typeIdx = e.detail.value
       this.form.activityType = this.typeOptions[this.typeIdx].key
@@ -354,15 +336,21 @@ export default {
     onImgError() {
       this.form.coverImage = ''
     },
+    getCoverUrl(path) {
+      if (!path) return ''
+      var s = String(path).trim()
+      if (!s || s === 'null' || s === '/null') return ''
+      if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) return s
+      return BASE_URL + (s.charAt(0) === '/' ? s : '/' + s)
+    },
+    removeCover() {
+      this.form.coverImage = ''
+    },
     validate() {
       if (!this.form.title) {
         uni.showToast({ title: '请输入活动标题', icon: 'none' })
         return false
       }
-      if (!this.form.dimensionCode) {
-        uni.showToast({ title: '请选择所属维度', icon: 'none' })
-        return false
-      }
       if (!this.form.activityType) {
         uni.showToast({ title: '请选择活动类型', icon: 'none' })
         return false
@@ -381,6 +369,8 @@ export default {
       payload.parentFee = Math.round((parseFloat(this.parentFeeYuan) || 0) * 100)
       payload.parentMemberFee = Math.round((parseFloat(this.parentMemberFeeYuan) || 0) * 100)
       if (this.activityId) payload.id = this.activityId
+      // 维度由审核时设置,创建/编辑不提交空维度,避免误清空已有维度
+      if (!payload.dimensionCode) delete payload.dimensionCode
       return payload
     },
     handleSave() {
@@ -470,12 +460,6 @@ export default {
         })
       }
     },
-    dimLabel(key) {
-      for (var i = 0; i < this.dimensionOptions.length; i++) {
-        if (this.dimensionOptions[i].key === key) return this.dimensionOptions[i].label
-      }
-      return '请选择维度'
-    },
     typeLabel(key) {
       for (var i = 0; i < this.typeOptions.length; i++) {
         if (this.typeOptions[i].key === key) return this.typeOptions[i].label
@@ -614,6 +598,25 @@ export default {
   margin-top: 8rpx;
 }
 
+.cover-actions {
+  display: flex;
+  flex-direction: row;
+  gap: 24rpx;
+  margin-top: 12rpx;
+}
+
+.cover-action {
+  font-size: 24rpx;
+  color: #475569;
+  text-decoration: underline;
+}
+
+.cover-action-danger {
+  font-size: 24rpx;
+  color: #ef4444;
+  text-decoration: underline;
+}
+
 .form-actions {
   display: flex;
   flex-direction: row;

+ 15 - 2
cfc-web/src/views/admin/ActivityReview.vue

@@ -156,12 +156,25 @@ export default {
       this.loadPending()
     },
     async approve(row) {
+      if (!row) return
       try {
-        await reviewActivity({ id: row.id, action: 'publish' })
+        await this.$prompt('请选择所属维度(必填)', '审核通过', {
+          confirmButtonText: '通过',
+          cancelButtonText: '取消',
+          inputPlaceholder: '请输入维度编码(body/mind/wisdom/action/wealth)',
+          inputValidator: (val) => {
+            if (!val || !['body', 'mind', 'wisdom', 'action', 'wealth'].includes(val.trim().toLowerCase())) {
+              return '请从下拉选择或输入 body/mind/wisdom/action/wealth'
+            }
+            return true
+          }
+        }).then(({ value }) => {
+          return reviewActivity({ id: row.id, action: 'publish', dimensionCode: value.trim().toLowerCase() })
+        })
         this.$message.success('审核通过,活动已发布')
         this.loadPending()
       } catch (e) {
-        this.$message.error('操作失败')
+        if (e !== 'cancel') this.$message.error('操作失败')
       }
     },
     async reject(row) {

+ 22 - 4
cfc-web/src/views/admin/review/ActivityAudit.vue

@@ -60,6 +60,15 @@
         <el-form-item label="活动内容">
           <div style="white-space:pre-wrap;">{{ currentRow.description || currentRow.content || '-' }}</div>
         </el-form-item>
+        <el-form-item label="所属维度" v-if="currentRow && currentRow.auditStatus === 'pending'">
+          <el-select v-model="auditDimensionCode" placeholder="请选择维度" style="width:100%">
+            <el-option label="身 · 主动健康" value="body" />
+            <el-option label="心 · 情感丰盈" value="mind" />
+            <el-option label="智 · 赋能未来" value="wisdom" />
+            <el-option label="行 · 知行合一" value="action" />
+            <el-option label="富 · 物质精神" value="wealth" />
+          </el-select>
+        </el-form-item>
         <el-form-item label="驳回原因" v-if="currentRow.auditReason" style="color:#f56c6c;">
           {{ currentRow.auditReason }}
         </el-form-item>
@@ -86,7 +95,8 @@ export default {
     return {
       list: [], loading: false, keyword: '', statusFilter: 'pending',
       sortableColumns: { id: 'ID', title: '活动名称', auditReason: '原因' },
-      detailVisible: false, currentRow: null, auditLoading: false
+      detailVisible: false, currentRow: null, auditLoading: false,
+      auditDimensionCode: ''
     }
   },
   mounted() { this.loadData() },
@@ -105,14 +115,22 @@ export default {
       } catch (e) { this.$message.error('加载失败')
       } finally { this.loading = false }
     },
-    showDetail(row) { this.currentRow = row; this.detailVisible = true },
-    handleApprove() { this.handleReview('publish') },
+    showDetail(row) { this.currentRow = row; this.auditDimensionCode = ''; this.detailVisible = true },
+    handleApprove() {
+      if (!this.auditDimensionCode) {
+        this.$message.warning('审核通过时必须选择所属维度')
+        return
+      }
+      this.handleReview('publish')
+    },
     handleReject(reason) { this.handleReview('reject', reason) },
     async handleReview(action, reason) {
       if (!this.currentRow) return
       this.auditLoading = true
       try {
-        await auditActivity({ id: this.currentRow.id, action, rejectReason: action === 'reject' ? reason : '' })
+        const params = { id: this.currentRow.id, action, rejectReason: action === 'reject' ? reason : '' }
+        if (action === 'publish') params.dimensionCode = this.auditDimensionCode
+        await auditActivity(params)
         this.$message.success('审核完成')
         this.detailVisible = false
         this.loadData()