Просмотр исходного кода

feat(admin): 补全 Activities.vue 缺失字段 - 封面图上传/活动类型/地点/人数/价格/可见范围/签到积分

- 表单新增: coverImage(图片上传), activityType, location, maxParticipants, price(元转分), visibility, visibleScope, visibleTo, checkinPoints
- 表格新增列: 封面图/活动类型/地点/费用(元)/人数
- 复用 /api/admin/articles/upload/image 上传接口
- 价格前端用元展示,后端存分
- 后端 ActivityService 无需修改(已支持所有字段)
Sisyphus 4 месяцев назад
Родитель
Сommit
82e88dde0c
1 измененных файлов с 249 добавлено и 28 удалено
  1. 249 28
      cfc-web/src/views/admin/Activities.vue

+ 249 - 28
cfc-web/src/views/admin/Activities.vue

@@ -22,8 +22,26 @@
       </div>
       <el-table :data="activities" v-loading="loading" border stripe>
         <el-table-column prop="id" label="ID" width="80" />
-        <el-table-column prop="title" label="活动标题" min-width="160" />
-        <el-table-column prop="dimensionCode" label="维度" width="100" />
+        <el-table-column label="封面" width="80">
+          <template slot-scope="{ row }">
+            <img v-if="row.coverImage" :src="row.coverImage" style="width:40px;height:40px;object-fit:cover;border-radius:4px;" />
+            <span v-else style="color:#999">无</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="title" label="活动标题" min-width="160" show-overflow-tooltip />
+        <el-table-column prop="dimensionCode" label="维度" width="100">
+          <template slot-scope="{ row }">{{ dimensionLabel(row.dimensionCode) }}</template>
+        </el-table-column>
+        <el-table-column label="类型" width="90">
+          <template slot-scope="{ row }">{{ activityTypeLabel(row.activityType) }}</template>
+        </el-table-column>
+        <el-table-column prop="location" label="地点" width="120" show-overflow-tooltip />
+        <el-table-column label="费用(元)" width="90">
+          <template slot-scope="{ row }">{{ row.price ? (row.price / 100).toFixed(2) : '免费' }}</template>
+        </el-table-column>
+        <el-table-column label="人数" width="90">
+          <template slot-scope="{ row }">{{ row.currentParticipants || 0 }}/{{ row.maxParticipants || '不限' }}</template>
+        </el-table-column>
         <el-table-column label="状态" width="100">
           <template slot-scope="{ row }">
             <el-tag :type="statusType(row.status)" size="mini">
@@ -67,30 +85,117 @@
     <el-dialog
       :title="editId ? '编辑活动' : '创建活动'"
       :visible.sync="dialogVisible"
-      width="600px"
+      width="700px"
       @closed="resetForm"
     >
       <el-form label-width="120px">
         <el-form-item label="活动标题">
           <el-input v-model="form.title" placeholder="请输入活动标题" />
         </el-form-item>
-        <el-form-item label="活动描述">
-          <el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入活动描述" />
+
+        <el-form-item label="封面图片">
+          <el-upload
+            :action="uploadActionUrl"
+            :headers="uploadHeaders"
+            :on-success="handleUploadSuccess"
+            :on-error="handleUploadError"
+            :before-upload="beforeUpload"
+            :show-file-list="false"
+            accept="image/jpeg,image/png,image/gif,image/webp"
+          >
+            <img v-if="form.coverImage" :src="form.coverImage" class="cover-preview" />
+            <el-button v-else type="primary" plain>+ 上传封面</el-button>
+          </el-upload>
+          <el-button v-if="form.coverImage" size="mini" type="text" style="margin-left:10px;" @click="form.coverImage = ''">清除</el-button>
         </el-form-item>
-        <el-form-item label="维度">
-          <el-select v-model="form.dimensionCode" placeholder="请选择维度" style="width:100%">
-            <el-option label="身 (健康)" value="body" />
-            <el-option label="智 (智慧)" value="mind" />
-            <el-option label="行 (行动)" value="action" />
-            <el-option label="富 (财富)" value="wealth" />
-            <el-option label="心 (心灵)" value="heart" />
+
+        <el-form-item label="活动类型">
+          <el-select v-model="form.activityType" placeholder="请选择活动类型" style="width:100%">
+            <el-option label="线下活动" value="offline" />
+            <el-option label="线上活动" value="online" />
+            <el-option label="主题活动" value="campaign" />
           </el-select>
         </el-form-item>
-        <el-form-item label="开始时间">
-          <el-date-picker v-model="form.startTime" type="datetime" placeholder="选择开始时间" style="width:100%" />
+
+        <el-form-item label="活动描述">
+          <el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入活动描述" />
+        </el-form-item>
+
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="维度">
+              <el-select v-model="form.dimensionCode" placeholder="请选择维度" style="width:100%">
+                <el-option label="身 (健康)" value="body" />
+                <el-option label="智 (智慧)" value="mind" />
+                <el-option label="行 (行动)" value="action" />
+                <el-option label="富 (财富)" value="wealth" />
+                <el-option label="心 (心灵)" value="heart" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="活动地点">
+              <el-input v-model="form.location" placeholder="请输入活动地点" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="最大人数">
+              <el-input-number v-model="form.maxParticipants" :min="0" :max="99999" placeholder="0表示不限" style="width:100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="价格(元)">
+              <el-input-number v-model="form.priceYuan" :min="0" :precision="2" :step="0.01" placeholder="0表示免费" style="width:100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="开始时间">
+              <el-date-picker v-model="form.startTime" type="datetime" placeholder="选择开始时间" style="width:100%" value-format="yyyy-MM-dd HH:mm:ss" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="结束时间">
+              <el-date-picker v-model="form.endTime" type="datetime" placeholder="选择结束时间" style="width:100%" value-format="yyyy-MM-dd HH:mm:ss" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-form-item label="可见范围">
+              <el-select v-model="form.visibility" placeholder="请选择可见范围" style="width:100%">
+                <el-option label="公开" value="public" />
+                <el-option label="受限" value="restricted" />
+                <el-option label="全体" value="all" />
+                <el-option label="会员" value="member" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="可见对象">
+              <el-select v-model="form.visibleScope" placeholder="请选择可见对象类型" style="width:100%">
+                <el-option label="不限" value="" />
+                <el-option label="家庭" value="family" />
+                <el-option label="角色" value="role" />
+                <el-option label="用户" value="user" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-form-item label="可见对象ID" v-if="form.visibleScope">
+          <el-input v-model="form.visibleTo" placeholder="多个用逗号分隔,如: family_1,family_2" />
         </el-form-item>
-        <el-form-item label="结束时间">
-          <el-date-picker v-model="form.endTime" type="datetime" placeholder="选择结束时间" style="width:100%" />
+
+        <el-form-item label="签到积分">
+          <el-input-number v-model="form.checkinPoints" :min="0" :max="9999" placeholder="0使用系统默认" style="width:200px" />
+          <span style="margin-left:8px;color:#999;font-size:12px;">0表示使用系统默认积分</span>
         </el-form-item>
       </el-form>
       <div slot="footer">
@@ -109,6 +214,18 @@ const STATUS_MAP = {
   published: { label: '已发布', type: 'success' },
   ended: { label: '已结束', type: 'danger' }
 }
+const ACTIVITY_TYPE_MAP = {
+  offline: '线下活动',
+  online: '线上活动',
+  campaign: '主题活动'
+}
+const DIMENSION_MAP = {
+  body: '身',
+  mind: '智',
+  action: '行',
+  wealth: '富',
+  heart: '心'
+}
 
 export default {
   name: 'Activities',
@@ -127,13 +244,32 @@ export default {
       },
       form: {
         title: '',
+        coverImage: '',
+        activityType: '',
         description: '',
         dimensionCode: '',
+        location: '',
+        maxParticipants: null,
+        priceYuan: null,
         startTime: null,
-        endTime: null
+        endTime: null,
+        visibility: 'public',
+        visibleScope: '',
+        visibleTo: '',
+        checkinPoints: 0
       }
     }
   },
+  computed: {
+    uploadActionUrl() {
+      const baseURL = process.env.VUE_APP_BASE_API || ''
+      return baseURL + '/api/admin/articles/upload/image'
+    },
+    uploadHeaders() {
+      const token = localStorage.getItem('token')
+      return token ? { Authorization: 'Bearer ' + token } : {}
+    }
+  },
   created() {
     this.loadActivities()
   },
@@ -144,6 +280,37 @@ export default {
     statusLabel(status) {
       return (STATUS_MAP[status] && STATUS_MAP[status].label) || status
     },
+    activityTypeLabel(type) {
+      return ACTIVITY_TYPE_MAP[type] || type || '—'
+    },
+    dimensionLabel(code) {
+      return DIMENSION_MAP[code] || code || '—'
+    },
+    beforeUpload(file) {
+      const isImage = file.type === 'image/jpeg' || file.type === 'image/png' ||
+        file.type === 'image/gif' || file.type === 'image/webp'
+      if (!isImage) {
+        this.$message.error('只能上传 JPG/PNG/GIF/WebP 格式图片')
+        return false
+      }
+      const isLt5M = file.size / 1024 / 1024 < 5
+      if (!isLt5M) {
+        this.$message.error('图片大小不能超过 5MB')
+        return false
+      }
+      return true
+    },
+    handleUploadSuccess(resp) {
+      if (resp.code === 0 || resp.code === 200) {
+        this.form.coverImage = resp.data || resp
+        this.$message.success('封面上传成功')
+      } else {
+        this.$message.error((resp && resp.message) || '封面上传失败')
+      }
+    },
+    handleUploadError() {
+      this.$message.error('封面上传失败,请重试')
+    },
     async loadActivities() {
       this.loading = true
       try {
@@ -180,37 +347,80 @@ export default {
     resetForm() {
       this.form = {
         title: '',
+        coverImage: '',
+        activityType: '',
         description: '',
         dimensionCode: '',
+        location: '',
+        maxParticipants: null,
+        priceYuan: null,
         startTime: null,
-        endTime: null
+        endTime: null,
+        visibility: 'public',
+        visibleScope: '',
+        visibleTo: '',
+        checkinPoints: 0
       }
     },
     editActivity(row) {
       this.editId = row.id
       this.form = {
         title: row.title || '',
+        coverImage: row.coverImage || '',
+        activityType: row.activityType || '',
         description: row.description || '',
         dimensionCode: row.dimensionCode || '',
+        location: row.location || '',
+        maxParticipants: row.maxParticipants || null,
+        priceYuan: row.price ? row.price / 100 : null,
         startTime: row.startTime || null,
-        endTime: row.endTime || null
+        endTime: row.endTime || null,
+        visibility: row.visibility || 'public',
+        visibleScope: row.visibleScope || '',
+        visibleTo: row.visibleTo || '',
+        checkinPoints: row.checkinPoints || 0
       }
       this.dialogVisible = true
     },
     async saveActivity() {
+      if (!this.form.title) {
+        this.$message.warning('请填写活动标题')
+        return
+      }
+      if (!this.form.startTime || !this.form.endTime) {
+        this.$message.warning('请填写开始时间和结束时间')
+        return
+      }
       this.saving = true
       try {
+        const payload = {
+          title: this.form.title,
+          coverImage: this.form.coverImage,
+          activityType: this.form.activityType,
+          description: this.form.description,
+          dimensionCode: this.form.dimensionCode,
+          location: this.form.location,
+          maxParticipants: this.form.maxParticipants,
+          price: this.form.priceYuan != null ? Math.round(this.form.priceYuan * 100) : 0,
+          startTime: this.form.startTime,
+          endTime: this.form.endTime,
+          visibility: this.form.visibility,
+          visibleScope: this.form.visibleScope,
+          visibleTo: this.form.visibleTo,
+          checkinPoints: this.form.checkinPoints || 0
+        }
         if (this.editId) {
-          await updateActivity({ ...this.form, id: this.editId })
-          this.$message && this.$message.success && this.$message.success('更新成功')
+          payload.id = this.editId
+          await updateActivity(payload)
+          this.$message.success('更新成功')
         } else {
-          await createActivity(this.form)
-          this.$message && this.$message.success && this.$message.success('创建成功')
+          await createActivity(payload)
+          this.$message.success('创建成功')
         }
         this.dialogVisible = false
         this.loadActivities()
       } catch (e) {
-        this.$message && this.$message.error && this.$message.error('保存失败')
+        this.$message.error('保存失败')
       } finally {
         this.saving = false
       }
@@ -218,21 +428,32 @@ export default {
     async handlePublish(row) {
       try {
         await publishActivity(row.id)
-        this.$message && this.$message.success && this.$message.success('发布成功')
+        this.$message.success('发布成功')
         this.loadActivities()
       } catch (e) {
-        this.$message && this.$message.error && this.$message.error('发布失败')
+        this.$message.error('发布失败')
       }
     },
     async handleEnd(row) {
       try {
         await endActivity(row.id)
-        this.$message && this.$message.success && this.$message.success('活动已结束')
+        this.$message.success('活动已结束')
         this.loadActivities()
       } catch (e) {
-        this.$message && this.$message.error && this.$message.error('操作失败')
+        this.$message.error('操作失败')
       }
     }
   }
 }
 </script>
+
+<style scoped>
+.activities { padding: 20px; }
+.cover-preview {
+  width: 200px;
+  height: 120px;
+  object-fit: cover;
+  border-radius: 4px;
+  border: 1px solid #ddd;
+}
+</style>