ソースを参照

feat: 活动管理功能完善——后端Service层+Web端CRUD+审核+小程序商品编辑

- 后端: 新增ActivityAdminService,AdminActivityController遵循Controller→Service→Mapper分层
- Web端: Activities.vue重写,使用api/activity.js封装,完整CRUD+分页+状态筛选
- Web端: ActivityReview.vue重写,实现真实审核通过/驳回(draft→publish/reject)
- Web端: 新增api/activity.js(6个API函数)
- 小程序: 新增vendor/product-edit商品编辑页(创建+编辑双模式)
- 修复: 移除Vue文件中直接axios调用和可选链?.
User 2 ヶ月 前
コミット
74009f3c05

+ 39 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminActivityController.java

@@ -0,0 +1,39 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.service.ActivityAdminService;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+/**
+ * 后台活动管理接口
+ * - 活动列表(包含草稿)
+ * - 审核活动(发布/删除)
+ */
+@RestController
+@RequestMapping("/api/admin/activity")
+public class AdminActivityController {
+
+    @Resource
+    private ActivityAdminService activityAdminService;
+
+    /**
+     * 分页查询活动列表(包含草稿)
+     * 支持按 status / dimensionCode 过滤
+     */
+    @PostMapping("/list")
+    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params) {
+        return activityAdminService.list(params);
+    }
+
+    /**
+     * 审核活动
+     * @param params action: "publish" | "reject", id: Long
+     */
+    @PostMapping("/review")
+    public Result<String> review(@RequestBody Map<String, Object> params) {
+        return activityAdminService.review(params);
+    }
+}

+ 81 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityAdminService.java

@@ -0,0 +1,81 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Activity;
+import com.etotem.cfc.mapper.ActivityMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 后台活动管理业务逻辑
+ * - 活动列表(含草稿,支持状态/维度过滤)
+ * - 审核活动(发布/驳回删除)
+ */
+@Service
+public class ActivityAdminService {
+
+    @Resource
+    private ActivityMapper activityMapper;
+
+    /**
+     * 分页查询活动列表(包含草稿)
+     * 支持按 status / dimensionCode 过滤
+     */
+    public Result<Map<String, Object>> list(Map<String, Object> params) {
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
+        String status = (String) params.get("status");
+        String dimensionCode = (String) params.get("dimensionCode");
+
+        LambdaQueryWrapper<Activity> wrapper = new LambdaQueryWrapper<>();
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(Activity::getStatus, status);
+        }
+        if (dimensionCode != null && !dimensionCode.isEmpty()) {
+            wrapper.eq(Activity::getDimensionCode, dimensionCode);
+        }
+        wrapper.orderByDesc(Activity::getUpdatedAt);
+
+        Page<Activity> pageResult = activityMapper.selectPage(new Page<>(page, size), wrapper);
+        Map<String, Object> data = new HashMap<>();
+        data.put("records", pageResult.getRecords());
+        data.put("total", pageResult.getTotal());
+        data.put("page", page);
+        data.put("size", size);
+        return Result.success(data);
+    }
+
+    /**
+     * 审核活动
+     * @param params action: "publish" | "reject", id: Long
+     */
+    public Result<String> review(Map<String, Object> params) {
+        String action = (String) params.get("action");
+        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        if (id == null) return Result.error("id不能为空");
+        if (action == null || action.isEmpty()) return Result.error("action不能为空");
+
+        Activity activity = activityMapper.selectById(id);
+        if (activity == null) return Result.error("活动不存在");
+
+        if ("publish".equals(action)) {
+            if (!"draft".equals(activity.getStatus())) {
+                return Result.error("仅草稿状态可发布");
+            }
+            activity.setStatus("published");
+            activityMapper.updateById(activity);
+            return Result.success("发布成功");
+        } else if ("reject".equals(action)) {
+            // 驳回:删除活动记录
+            activityMapper.deleteById(id);
+            return Result.success("已驳回并删除");
+        } else {
+            return Result.error("无效的action");
+        }
+    }
+}

+ 248 - 0
cfc-frontend/pages/vendor/product-edit/product-edit.vue

@@ -0,0 +1,248 @@
+<template>
+  <view class="container">
+    <view class="form-section">
+      <view class="form-item">
+        <text class="form-label">商品名称 *</text>
+        <input class="form-input" v-model="form.name" placeholder="请输入商品名称" />
+      </view>
+
+      <view class="form-item">
+        <text class="form-label">商品类型 *</text>
+        <picker class="form-picker" :value="typeIndex" :range="typeOptions" range-key="label" @change="onTypeChange">
+          <view class="picker-value">{{ typeOptions[typeIndex].label }}</view>
+        </picker>
+      </view>
+
+      <view class="form-item">
+        <text class="form-label">价格(分)*</text>
+        <input class="form-input" type="number" v-model="form.price" placeholder="价格,单位:分(0=免费)" />
+      </view>
+
+      <view class="form-item">
+        <text class="form-label">库存 *</text>
+        <input class="form-input" type="number" v-model="form.stock" placeholder="库存数量" />
+      </view>
+
+      <view class="form-item">
+        <text class="form-label">封面图URL</text>
+        <input class="form-input" v-model="form.coverImage" placeholder="图片地址,留空使用默认图" />
+        <view v-if="form.coverImage" class="cover-preview">
+          <image :src="form.coverImage" mode="aspectFill" class="cover-thumb" />
+        </view>
+      </view>
+
+      <view class="form-item">
+        <text class="form-label">商品描述</text>
+        <textarea class="form-textarea" v-model="form.description" placeholder="商品描述(可选)" />
+      </view>
+    </view>
+
+    <view class="form-actions">
+      <button class="btn-submit" :disabled="submitting" @click="onSubmit">
+        {{ submitting ? '提交中...' : (isEdit ? '保存修改' : '立即发布') }}
+      </button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { productCreate, productUpdate, productDetail } from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      isEdit: false,
+      productId: null,
+      submitting: false,
+      typeIndex: 0,
+      typeOptions: [
+        { label: '实物商品', value: 'physical' },
+        { label: '数字商品', value: 'digital' },
+        { label: '服务', value: 'service' },
+        { label: '课程', value: 'course' },
+        { label: '活动', value: 'activity' }
+      ],
+      form: {
+        name: '',
+        productType: 'physical',
+        price: 0,
+        stock: 0,
+        coverImage: '',
+        description: ''
+      }
+    }
+  },
+  onLoad(options) {
+    if (options.id) {
+      this.isEdit = true
+      this.productId = options.id
+      uni.setNavigationBarTitle({ title: '编辑商品' })
+      this.loadProduct(options.id)
+    } else {
+      uni.setNavigationBarTitle({ title: '发布商品' })
+    }
+  },
+  methods: {
+    onTypeChange(e) {
+      this.typeIndex = e.detail.value
+      this.form.productType = this.typeOptions[e.detail.value].value
+    },
+    loadProduct(id) {
+      uni.showLoading({ title: '加载中...' })
+      productDetail({ id: Number(id) }).then(res => {
+        uni.hideLoading()
+        if (res.code === 200 && res.data) {
+          const p = res.data
+          this.form.name = p.name || ''
+          this.form.description = p.description || ''
+          this.form.coverImage = p.coverImage || ''
+          this.form.price = p.price || 0
+          this.form.stock = p.stock || 0
+          this.form.productType = p.productType || 'physical'
+          // sync picker index
+          var idx = this.typeOptions.findIndex(function(t) { return t.value === this.form.productType }, this)
+          if (idx >= 0) this.typeIndex = idx
+        } else {
+          uni.showToast({ title: res.message || '加载失败', icon: 'none' })
+        }
+      }).catch(function() {
+        uni.hideLoading()
+        uni.showToast({ title: '加载失败', icon: 'none' })
+      })
+    },
+    onSubmit() {
+      if (!this.form.name || !this.form.name.trim()) {
+        uni.showToast({ title: '请输入商品名称', icon: 'none' })
+        return
+      }
+      if (this.form.price === '' || this.form.price === null) {
+        uni.showToast({ title: '请输入价格', icon: 'none' })
+        return
+      }
+      if (this.form.stock === '' || this.form.stock === null) {
+        uni.showToast({ title: '请输入库存', icon: 'none' })
+        return
+      }
+      this.submitting = true
+      var data = {
+        name: this.form.name.trim(),
+        productType: this.form.productType,
+        price: Number(this.form.price),
+        stock: Number(this.form.stock),
+        coverImage: this.form.coverImage || '',
+        description: this.form.description || ''
+      }
+      var apiCall = this.isEdit
+        ? productUpdate(Object.assign({ id: this.productId }, data))
+        : productCreate(data)
+      apiCall.then(function(res) {
+        this.submitting = false
+        if (res.code === 200) {
+          uni.showToast({ title: this.isEdit ? '修改成功' : '发布成功', icon: 'success' })
+          var _self = this
+          setTimeout(function() {
+            uni.navigateBack()
+          }, 1500)
+        } else {
+          uni.showToast({ title: res.message || '操作失败', icon: 'none' })
+        }
+      }.bind(this)).catch(function() {
+        this.submitting = false
+        uni.showToast({ title: '网络错误', icon: 'none' })
+      }.bind(this))
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f5f5;
+  padding: 24rpx;
+}
+.form-section {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  margin-bottom: 24rpx;
+}
+.form-item {
+  margin-bottom: 32rpx;
+}
+.form-item:last-child {
+  margin-bottom: 0;
+}
+.form-label {
+  display: block;
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 500;
+  margin-bottom: 12rpx;
+}
+.form-input {
+  width: 100%;
+  box-sizing: border-box;
+  height: 80rpx;
+  padding: 0 24rpx;
+  border: 2rpx solid #e5e5e5;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+  background: #fafafa;
+}
+.form-textarea {
+  width: 100%;
+  box-sizing: border-box;
+  height: 160rpx;
+  padding: 16rpx 24rpx;
+  border: 2rpx solid #e5e5e5;
+  border-radius: 12rpx;
+  font-size: 28rpx;
+  background: #fafafa;
+  resize: none;
+}
+.form-picker {
+  height: 80rpx;
+  padding: 0 24rpx;
+  border: 2rpx solid #e5e5e5;
+  border-radius: 12rpx;
+  background: #fafafa;
+  display: flex;
+  align-items: center;
+}
+.picker-value {
+  font-size: 28rpx;
+  color: #333;
+}
+.cover-preview {
+  margin-top: 12rpx;
+}
+.cover-thumb {
+  width: 200rpx;
+  height: 160rpx;
+  border-radius: 8rpx;
+  background: #f0f0f0;
+}
+.form-actions {
+  padding: 0 24rpx;
+}
+.btn-submit {
+  width: 100%;
+  height: 88rpx;
+  background: #F97316;
+  color: #fff;
+  font-size: 32rpx;
+  font-weight: 700;
+  border-radius: 44rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: none;
+}
+.btn-submit:active {
+  opacity: 0.85;
+}
+.btn-submit[disabled] {
+  background: #ccc;
+}
+</style>

+ 55 - 0
cfc-web/src/api/activity.js

@@ -0,0 +1,55 @@
+import request from '@/utils/request'
+
+// 后台活动列表(分页)
+export function getActivityList(params) {
+  return request({
+    url: '/api/admin/activity/list',
+    method: 'post',
+    data: params
+  })
+}
+
+// 后台审核活动(发布/驳回)
+export function reviewActivity(data) {
+  return request({
+    url: '/api/admin/activity/review',
+    method: 'post',
+    data: data
+  })
+}
+
+// 创建活动(透传后端已有接口)
+export function createActivity(data) {
+  return request({
+    url: '/api/activity/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新活动(透传后端已有接口)
+export function updateActivity(data) {
+  return request({
+    url: '/api/activity/update',
+    method: 'post',
+    data: data
+  })
+}
+
+// 发布活动(透传后端已有接口)
+export function publishActivity(id) {
+  return request({
+    url: '/api/activity/publish',
+    method: 'post',
+    data: { id }
+  })
+}
+
+// 结束活动(透传后端已有接口)
+export function endActivity(id) {
+  return request({
+    url: '/api/activity/end',
+    method: 'post',
+    data: { id }
+  })
+}

+ 205 - 18
cfc-web/src/views/admin/Activities.vue

@@ -3,48 +3,235 @@
     <el-card>
       <div slot="header">
         <span>活动管理</span>
-        <el-button type="primary" size="mini" style="float:right" @click="showCreate = true">创建活动</el-button>
+        <div style="float:right">
+          <el-select
+            v-model="filter.status"
+            placeholder="状态筛选"
+            size="mini"
+            style="margin-right:12px;width:130px"
+            clearable
+            @change="loadActivities"
+          >
+            <el-option label="全部" value="" />
+            <el-option label="草稿" value="draft" />
+            <el-option label="已发布" value="published" />
+            <el-option label="已结束" value="ended" />
+          </el-select>
+          <el-button type="primary" size="mini" @click="openCreate">创建活动</el-button>
+        </div>
       </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="活动标题" />
-        <el-table-column prop="status" label="状态" width="100" />
-        <el-table-column prop="startTime" label="开始时间" width="180" />
-        <el-table-column prop="endTime" label="结束时间" width="180" />
-        <el-table-column label="操作" width="150">
+        <el-table-column prop="title" label="活动标题" min-width="160" />
+        <el-table-column prop="dimensionCode" label="维度" width="100" />
+        <el-table-column label="状态" width="100">
           <template slot-scope="{ row }">
-            <el-button size="mini" @click="editActivity(row)">编辑</el-button>
+            <el-tag :type="statusType(row.status)" size="mini">
+              {{ statusLabel(row.status) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="startTime" label="开始时间" width="170" />
+        <el-table-column prop="endTime" label="结束时间" width="170" />
+        <el-table-column label="操作" width="240">
+          <template slot-scope="{ row }">
+            <el-button size="mini" @click="editActivity(row)" :disabled="row.status === 'ended'">编辑</el-button>
+            <el-button
+              size="mini"
+              type="success"
+              @click="handlePublish(row)"
+              v-if="row.status === 'draft'"
+            >发布</el-button>
+            <el-button
+              size="mini"
+              type="warning"
+              @click="handleEnd(row)"
+              v-if="row.status === 'published'"
+            >结束</el-button>
           </template>
         </el-table-column>
       </el-table>
+      <el-pagination
+        v-if="total > 0"
+        @size-change="handleSizeChange"
+        @current-change="handlePageChange"
+        :current-page="filter.page"
+        :page-sizes="[10, 20, 50]"
+        :page-size="filter.size"
+        layout="total, sizes, prev, pager, next, jumper"
+        :total="total"
+        style="margin-top:16px;text-align:right"
+      />
     </el-card>
+
+    <el-dialog
+      :title="editId ? '编辑活动' : '创建活动'"
+      :visible.sync="dialogVisible"
+      width="600px"
+      @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>
+        <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-form-item label="开始时间">
+          <el-date-picker v-model="form.startTime" type="datetime" placeholder="选择开始时间" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="结束时间">
+          <el-date-picker v-model="form.endTime" type="datetime" placeholder="选择结束时间" style="width:100%" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="saveActivity" :loading="saving">保存</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script>
+import { getActivityList, createActivity, updateActivity, publishActivity, endActivity } from '@/api/activity'
+
+const STATUS_MAP = {
+  draft: { label: '草稿', type: 'info' },
+  published: { label: '已发布', type: 'success' },
+  ended: { label: '已结束', type: 'danger' }
+}
+
 export default {
   name: 'Activities',
-  data() { return { loading: false, activities: [], showCreate: false } },
-  created() { this.loadActivities() },
+  data() {
+    return {
+      loading: false,
+      saving: false,
+      activities: [],
+      total: 0,
+      editId: null,
+      dialogVisible: false,
+      filter: {
+        status: '',
+        page: 1,
+        size: 10
+      },
+      form: {
+        title: '',
+        description: '',
+        dimensionCode: '',
+        startTime: null,
+        endTime: null
+      }
+    }
+  },
+  created() {
+    this.loadActivities()
+  },
   methods: {
+    statusType(status) {
+      return (STATUS_MAP[status] && STATUS_MAP[status].type) || 'info'
+    },
+    statusLabel(status) {
+      return (STATUS_MAP[status] && STATUS_MAP[status].label) || status
+    },
     async loadActivities() {
       this.loading = true
       try {
-        const baseURL = process.env.VUE_APP_BASE_API || ''
-        const token = localStorage.getItem('token')
-        const axios = this.$axios || (await import('axios')).default
-        const res = await axios.post(baseURL + '/api/activity/list', {}, {
-          headers: { Authorization: 'Bearer ' + token }
-        })
-        this.activities = res.data?.data || []
+        const params = {
+          page: this.filter.page,
+          size: this.filter.size
+        }
+        if (this.filter.status) {
+          params.status = this.filter.status
+        }
+        const res = await getActivityList(params)
+        this.activities = (res.data && res.data.records) || []
+        this.total = (res.data && res.data.total) || 0
       } catch (e) {
-        this.$message?.error?.('加载活动列表失败')
+        this.$message && this.$message.error && this.$message.error('加载活动列表失败')
       } finally {
         this.loading = false
       }
     },
+    handleSizeChange(size) {
+      this.filter.size = size
+      this.filter.page = 1
+      this.loadActivities()
+    },
+    handlePageChange(page) {
+      this.filter.page = page
+      this.loadActivities()
+    },
+    openCreate() {
+      this.editId = null
+      this.resetForm()
+      this.dialogVisible = true
+    },
+    resetForm() {
+      this.form = {
+        title: '',
+        description: '',
+        dimensionCode: '',
+        startTime: null,
+        endTime: null
+      }
+    },
     editActivity(row) {
-      this.$message.info('活动编辑功能待实现')
+      this.editId = row.id
+      this.form = {
+        title: row.title || '',
+        description: row.description || '',
+        dimensionCode: row.dimensionCode || '',
+        startTime: row.startTime || null,
+        endTime: row.endTime || null
+      }
+      this.dialogVisible = true
+    },
+    async saveActivity() {
+      this.saving = true
+      try {
+        if (this.editId) {
+          await updateActivity({ ...this.form, id: this.editId })
+          this.$message && this.$message.success && this.$message.success('更新成功')
+        } else {
+          await createActivity(this.form)
+          this.$message && this.$message.success && this.$message.success('创建成功')
+        }
+        this.dialogVisible = false
+        this.loadActivities()
+      } catch (e) {
+        this.$message && this.$message.error && this.$message.error('保存失败')
+      } finally {
+        this.saving = false
+      }
+    },
+    async handlePublish(row) {
+      try {
+        await publishActivity(row.id)
+        this.$message && this.$message.success && this.$message.success('发布成功')
+        this.loadActivities()
+      } catch (e) {
+        this.$message && this.$message.error && this.$message.error('发布失败')
+      }
+    },
+    async handleEnd(row) {
+      try {
+        await endActivity(row.id)
+        this.$message && this.$message.success && this.$message.success('活动已结束')
+        this.loadActivities()
+      } catch (e) {
+        this.$message && this.$message.error && this.$message.error('操作失败')
+      }
     }
   }
 }

+ 87 - 14
cfc-web/src/views/admin/ActivityReview.vue

@@ -1,47 +1,120 @@
 <template>
   <div class="activity-review">
     <el-card>
-      <div slot="header"><span>活动审核</span></div>
+      <div slot="header">
+        <span>活动审核</span>
+      </div>
       <el-table :data="pendingActivities" v-loading="loading" border stripe>
         <el-table-column prop="id" label="ID" width="80" />
         <el-table-column prop="title" label="活动标题" />
         <el-table-column prop="applicant" label="申请人" width="120" />
         <el-table-column prop="createdAt" label="申请时间" width="180" />
+        <el-table-column label="状态" width="100">
+          <template slot-scope="{ row }">
+            <el-tag v-if="row.status === 'draft'" type="warning">待审核</el-tag>
+            <el-tag v-else-if="row.status === 'published'" type="success">已发布</el-tag>
+            <el-tag v-else-if="row.status === 'rejected'" type="danger">已驳回</el-tag>
+            <el-tag v-else type="info">{{ row.status }}</el-tag>
+          </template>
+        </el-table-column>
         <el-table-column label="操作" width="200">
           <template slot-scope="{ row }">
-            <el-button size="mini" type="success" @click="approve(row)">通过</el-button>
-            <el-button size="mini" type="danger" @click="reject(row)">驳回</el-button>
+            <el-button
+              v-if="row.status === 'draft'"
+              size="mini"
+              type="success"
+              @click="approve(row)"
+            >通过</el-button>
+            <el-button
+              v-if="row.status === 'draft'"
+              size="mini"
+              type="danger"
+              @click="reject(row)"
+            >驳回</el-button>
+            <span v-else class="no-op">—</span>
           </template>
         </el-table-column>
       </el-table>
+      <el-pagination
+        v-if="total > 0"
+        @current-change="handlePageChange"
+        :current-page="page"
+        :page-size="size"
+        :total="total"
+        layout="total, prev, pager, next"
+        style="margin-top: 20px; text-align: right;"
+      />
     </el-card>
   </div>
 </template>
 
 <script>
+import { getActivityList, reviewActivity } from '@/api/activity'
+
 export default {
   name: 'ActivityReview',
-  data() { return { loading: false, pendingActivities: [] } },
-  created() { this.loadPending() },
+  data() {
+    return {
+      loading: false,
+      pendingActivities: [],
+      page: 1,
+      size: 20,
+      total: 0
+    }
+  },
+  created() {
+    this.loadPending()
+  },
   methods: {
     async loadPending() {
       this.loading = true
       try {
-        const baseURL = process.env.VUE_APP_BASE_API || ''
-        const token = localStorage.getItem('token')
-        const axios = this.$axios || (await import('axios')).default
-        const res = await axios.post(baseURL + '/api/activity/list', { status: 'pending' }, {
-          headers: { Authorization: 'Bearer ' + token }
+        const res = await getActivityList({
+          status: 'draft',
+          page: this.page,
+          size: this.size
         })
-        this.pendingActivities = res.data?.data || []
+        this.pendingActivities = (res.data && res.data.records) || []
+        this.total = (res.data && res.data.total) || 0
       } catch (e) {
-        this.$message?.error?.('加载待审核活动失败')
+        this.$message.error('加载待审核活动失败')
       } finally {
         this.loading = false
       }
     },
-    approve(row) { this.$message.info('审核通过功能待实现') },
-    reject(row) { this.$message.info('驳回功能待实现') }
+    handlePageChange(val) {
+      this.page = val
+      this.loadPending()
+    },
+    async approve(row) {
+      try {
+        await reviewActivity({ id: row.id, action: 'publish' })
+        this.$message.success('审核通过,活动已发布')
+        this.loadPending()
+      } catch (e) {
+        this.$message.error('操作失败')
+      }
+    },
+    async reject(row) {
+      try {
+        await this.$confirm('确认驳回该活动?驳回后活动将被删除', '提示', {
+          confirmButtonText: '确认',
+          cancelButtonText: '取消',
+          type: 'warning'
+        })
+        await reviewActivity({ id: row.id, action: 'reject' })
+        this.$message.success('已驳回并删除')
+        this.loadPending()
+      } catch (e) {
+        // 取消或操作失败均不做处理
+      }
+    }
   }
 }
 </script>
+
+<style scoped>
+.no-op {
+  color: #ccc;
+}
+</style>