Browse Source

feat(cfc-frontend): add admin activity creation and management features

New standalone create page for admins to create/edit activities. Added '+' button in nav bar (admin-only). Added 4th 'Manage' tab on activity center with overview stats, quick actions, and managed activities list. Added admin API wrappers for create/update/publish/end.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Xiaogang Liao 2 tháng trước cách đây
mục cha
commit
3aecec2a12

+ 4 - 0
cfc-frontend/pages.json

@@ -813,6 +813,10 @@
         {
           "path": "activity-detail/activity-detail",
           "style": { "navigationBarTitleText": "活动详情" }
+        },
+        {
+          "path": "create",
+          "style": { "navigationBarTitleText": "创建活动" }
         }
       ]
     },

+ 578 - 0
cfc-frontend/pages/activity/create.vue

@@ -0,0 +1,578 @@
+<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>
+        <picker class="form-picker" :value="typeIdx" :range="typeOptions" range-key="label" @change="onTypeChange">
+          <text class="picker-text">{{ form.activityType ? typeLabel(form.activityType) : '请选择类型' }}</text>
+        </picker>
+      </view>
+
+      <!-- 开始时间 -->
+      <view class="form-item">
+        <text class="form-label">开始时间 *</text>
+        <picker class="form-picker" mode="date" :value="datePart" @change="onStartDateChange">
+          <text class="picker-text">{{ datePart || '选择日期' }}</text>
+        </picker>
+        <picker class="form-picker" mode="time" :value="timePart" @change="onStartTimeChange" style="margin-top: 16rpx;">
+          <text class="picker-text">{{ timePart || '选择时间' }}</text>
+        </picker>
+      </view>
+
+      <!-- 结束时间 -->
+      <view class="form-item">
+        <text class="form-label">结束时间</text>
+        <picker class="form-picker" mode="date" :value="endDatePart" @change="onEndDateChange">
+          <text class="picker-text">{{ endDatePart || '选择日期(可选)' }}</text>
+        </picker>
+        <picker class="form-picker" mode="time" :value="endTimePart" @change="onEndTimeChange" style="margin-top: 16rpx;">
+          <text class="picker-text">{{ endTimePart || '选择时间(可选)' }}</text>
+        </picker>
+      </view>
+
+      <!-- 活动地点 -->
+      <view class="form-item">
+        <text class="form-label">活动地点</text>
+        <input class="form-input" v-model="form.location" placeholder="请输入活动地点" maxlength="100" />
+      </view>
+
+      <!-- 人数 + 价格 -->
+      <view class="form-item-row">
+        <view class="form-item half">
+          <text class="form-label">最大人数</text>
+          <input class="form-input" type="number" v-model="maxParticipantsStr" placeholder="0=不限" />
+        </view>
+        <view class="form-item half">
+          <text class="form-label">价格(元)</text>
+          <input class="form-input" type="digit" v-model="priceYuan" placeholder="0=免费" />
+        </view>
+      </view>
+
+      <!-- 封面图片 -->
+      <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>
+      </view>
+
+      <!-- 活动描述 -->
+      <view class="form-item">
+        <text class="form-label">活动描述</text>
+        <textarea class="form-textarea" v-model="form.description" placeholder="请输入活动详细介绍" maxlength="500"
+          auto-height />
+      </view>
+
+      <!-- 操作按钮 -->
+      <view class="form-actions">
+        <view class="btn-save" @click="handleSave">
+          <text>保存为草稿</text>
+        </view>
+        <view class="btn-publish" @click="handlePublish">
+          <text>保存并发布</text>
+        </view>
+      </view>
+    </view>
+
+    <view class="loading" v-if="loading">
+      <view class="loading-spinner"></view>
+      <text>加载中...</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { adminCreateActivity, adminUpdateActivity, adminPublishActivity, getActivityDetail } from '../../utils/api.js'
+import config from '../../config.js'
+
+var BASE_URL = config.api('')
+
+export default {
+  data() {
+    return {
+      isNew: true,
+      activityId: null,
+      loading: false,
+      childId: '',
+      form: {
+        title: '',
+        description: '',
+        coverImage: '',
+        dimensionCode: '',
+        activityType: '',
+        status: 'draft',
+        startTime: null,
+        endTime: null,
+        location: '',
+        maxParticipants: 0,
+        price: 0,
+        memberPrice: 0,
+        visibility: 'public',
+        requireRegistration: 1,
+        requireCheckinReview: 0,
+        checkinPoints: 0
+      },
+      priceYuan: '0',
+      maxParticipantsStr: '0',
+      datePart: '',
+      timePart: '',
+      endDatePart: '',
+      endTimePart: '',
+      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: '线上活动' },
+        { key: 'campaign', label: '营销活动' }
+      ]
+    }
+  },
+  onLoad(options) {
+    if (options.id) {
+      this.activityId = parseInt(options.id)
+      this.isNew = false
+      uni.setNavigationBarTitle({ title: '编辑活动' })
+      this.loadDetail()
+    } else {
+      uni.setNavigationBarTitle({ title: '创建活动' })
+    }
+  },
+  methods: {
+    loadDetail() {
+      var self = this
+      self.loading = true
+      getActivityDetail(self.activityId).then(function(res) {
+        self.loading = false
+        if (res && res.code === 200 && res.data) {
+          var d = res.data
+          self.form.title = d.title || ''
+          self.form.description = d.description || ''
+          self.form.coverImage = d.coverImage || ''
+          self.form.dimensionCode = d.dimensionCode || ''
+          self.form.activityType = d.activityType || ''
+          self.form.location = d.location || ''
+          self.form.maxParticipants = d.maxParticipants || 0
+          self.form.price = d.price || 0
+          self.form.memberPrice = d.memberPrice || 0
+          self.form.status = d.status || 'draft'
+          self.form.visibility = d.visibility || 'public'
+          self.maxParticipantsStr = String(self.form.maxParticipants)
+          self.priceYuan = self.form.price ? (self.form.price / 100).toString() : '0'
+          if (d.startTime) {
+            var sd = new Date(d.startTime.replace ? d.startTime.replace(/-/g, '/') : d.startTime)
+            self.datePart = self.fmtDate(sd)
+            self.timePart = self.fmtTime(sd)
+            self.form.startTime = sd.toISOString()
+          }
+          if (d.endTime) {
+            var ed = new Date(d.endTime.replace ? d.endTime.replace(/-/g, '/') : d.endTime)
+            self.endDatePart = self.fmtDate(ed)
+            self.endTimePart = self.fmtTime(ed)
+            self.form.endTime = ed.toISOString()
+          }
+          // sync pickers
+          for (var i = 0; i < self.dimensionOptions.length; i++) {
+            if (self.dimensionOptions[i].key === self.form.dimensionCode) self.dimIdx = i
+          }
+          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
+    },
+    onStartDateChange(e) {
+      this.datePart = e.detail.value
+      this.syncStartTime()
+    },
+    onStartTimeChange(e) {
+      this.timePart = e.detail.value
+      this.syncStartTime()
+    },
+    onEndDateChange(e) {
+      this.endDatePart = e.detail.value
+      this.syncEndTime()
+    },
+    onEndTimeChange(e) {
+      this.endTimePart = e.detail.value
+      this.syncEndTime()
+    },
+    syncStartTime() {
+      if (this.datePart && this.timePart) {
+        this.form.startTime = this.datePart + 'T' + this.timePart + ':00'
+      }
+    },
+    syncEndTime() {
+      if (this.endDatePart && this.endTimePart) {
+        this.form.endTime = this.endDatePart + 'T' + this.endTimePart + ':00'
+      } else {
+        this.form.endTime = null
+      }
+    },
+    chooseImage() {
+      var self = this
+      uni.chooseImage({
+        count: 1,
+        sizeType: ['compressed'],
+        success: function(res) {
+          self.uploadCover(res.tempFilePaths[0])
+        }
+      })
+    },
+    uploadCover(filePath) {
+      var self = this
+      uni.uploadFile({
+        url: BASE_URL + '/api/admin/article/upload/image',
+        filePath: filePath,
+        name: 'file',
+        header: { 'Authorization': 'Bearer ' + (uni.getStorageSync('token') || '') },
+        success: function(res) {
+          try {
+            var data = JSON.parse(res.data)
+            if (data.code === 200 && data.data) {
+              self.form.coverImage = data.data
+            } else {
+              uni.showToast({ title: '上传失败', icon: 'none' })
+            }
+          } catch (e) {
+            uni.showToast({ title: '上传失败', icon: 'none' })
+          }
+        },
+        fail: function() {
+          uni.showToast({ title: '上传失败', icon: 'none' })
+        }
+      })
+    },
+    onImgError() {
+      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
+      }
+      if (!this.datePart || !this.timePart) {
+        uni.showToast({ title: '请选择开始时间', icon: 'none' })
+        return false
+      }
+      return true
+    },
+    buildPayload() {
+      var payload = Object.assign({}, this.form)
+      payload.maxParticipants = parseInt(this.maxParticipantsStr) || 0
+      payload.price = Math.round((parseFloat(this.priceYuan) || 0) * 100)
+      payload.memberPrice = payload.price // 会员价默认等于普通价
+      if (this.activityId) payload.id = this.activityId
+      return payload
+    },
+    handleSave() {
+      if (!this.validate()) return
+      this.submit(false)
+    },
+    handlePublish() {
+      if (!this.validate()) return
+      this.submit(true)
+    },
+    submit(publishAfter) {
+      var self = this
+      self.loading = true
+      var payload = self.buildPayload()
+
+      var doPublish = function(id) {
+        adminPublishActivity(id).then(function(r) {
+          self.loading = false
+          if (r && r.code === 200) {
+            uni.showToast({ title: '已发布', icon: 'success' })
+            setTimeout(function() {
+              uni.navigateBack()
+            }, 1200)
+          } else {
+            uni.showToast({ title: (r && r.message) || '发布失败', icon: 'none' })
+          }
+        }).catch(function() {
+          self.loading = false
+          uni.showToast({ title: '发布失败', icon: 'none' })
+        })
+      }
+
+      if (self.isNew) {
+        adminCreateActivity(payload).then(function(res) {
+          if (res && res.code === 200) {
+            var newId = res.data.id || res.data
+            if (publishAfter) {
+              doPublish(newId)
+            } else {
+              self.loading = false
+              uni.showToast({ title: '已保存草稿', icon: 'success' })
+              setTimeout(function() { uni.navigateBack() }, 1200)
+            }
+          } else {
+            self.loading = false
+            uni.showToast({ title: (res && res.message) || '保存失败', icon: 'none' })
+          }
+        }).catch(function() {
+          self.loading = false
+          uni.showToast({ title: '网络异常', icon: 'none' })
+        })
+      } else {
+        adminUpdateActivity(payload).then(function(res) {
+          if (res && res.code === 200) {
+            if (publishAfter && self.form.status === 'draft') {
+              doPublish(self.activityId)
+            } else {
+              self.loading = false
+              uni.showToast({ title: '已保存', icon: 'success' })
+              setTimeout(function() { uni.navigateBack() }, 1200)
+            }
+          } else {
+            self.loading = false
+            uni.showToast({ title: (res && res.message) || '保存失败', icon: 'none' })
+          }
+        }).catch(function() {
+          self.loading = false
+          uni.showToast({ title: '网络异常', icon: 'none' })
+        })
+      }
+    },
+    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
+      }
+      return '请选择类型'
+    },
+    fmtDate(d) {
+      var y = d.getFullYear()
+      var m = ('0' + (d.getMonth() + 1)).slice(-2)
+      var day = ('0' + d.getDate()).slice(-2)
+      return y + '-' + m + '-' + day
+    },
+    fmtTime(d) {
+      var h = ('0' + d.getHours()).slice(-2)
+      var mi = ('0' + d.getMinutes()).slice(-2)
+      return h + ':' + mi
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #F5F7FA;
+  padding: 24rpx;
+}
+
+.form-card {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 32rpx 28rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+
+.form-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #1E293B;
+  margin-bottom: 24rpx;
+}
+
+.form-item {
+  margin-bottom: 28rpx;
+}
+
+.form-item-row {
+  display: flex;
+  flex-direction: row;
+  gap: 20rpx;
+  margin-bottom: 28rpx;
+}
+
+.form-item.half {
+  flex: 1;
+}
+
+.form-label {
+  font-size: 26rpx;
+  color: #475569;
+  font-weight: 600;
+  margin-bottom: 12rpx;
+  display: block;
+}
+
+.form-input {
+  width: 100%;
+  height: 80rpx;
+  border: 1rpx solid #E2E8F0;
+  border-radius: 12rpx;
+  padding: 0 20rpx;
+  font-size: 28rpx;
+  color: #1E293B;
+  box-sizing: border-box;
+}
+
+.form-picker {
+  width: 100%;
+  height: 80rpx;
+  border: 1rpx solid #E2E8F0;
+  border-radius: 12rpx;
+  padding: 0 20rpx;
+  font-size: 28rpx;
+  color: #1E293B;
+  box-sizing: border-box;
+  display: flex;
+  align-items: center;
+}
+
+.picker-text {
+  font-size: 28rpx;
+  color: #1E293B;
+}
+
+.form-textarea {
+  width: 100%;
+  min-height: 160rpx;
+  border: 1rpx solid #E2E8F0;
+  border-radius: 12rpx;
+  padding: 16rpx 20rpx;
+  font-size: 28rpx;
+  color: #1E293B;
+  box-sizing: border-box;
+}
+
+.cover-upload {
+  width: 100%;
+  height: 320rpx;
+  border: 2rpx dashed #CBD5E1;
+  border-radius: 12rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+}
+
+.preview-img {
+  width: 100%;
+  height: 100%;
+}
+
+.upload-placeholder {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.upload-icon {
+  font-size: 64rpx;
+  color: #94A3B8;
+}
+
+.upload-text {
+  font-size: 24rpx;
+  color: #94A3B8;
+  margin-top: 8rpx;
+}
+
+.form-actions {
+  display: flex;
+  flex-direction: row;
+  gap: 20rpx;
+  margin-top: 40rpx;
+}
+
+.btn-save,
+.btn-publish {
+  flex: 1;
+  height: 88rpx;
+  border-radius: 44rpx;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 30rpx;
+  font-weight: 600;
+}
+
+.btn-save {
+  background: #fff;
+  color: #F97316;
+  border: 2rpx solid #F97316;
+}
+
+.btn-publish {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: #fff;
+}
+
+.loading {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(255, 255, 255, 0.6);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  z-index: 999;
+}
+
+.loading-spinner {
+  width: 60rpx;
+  height: 60rpx;
+  border: 4rpx solid #FED7AA;
+  border-top: 4rpx solid #F97316;
+  border-radius: 50%;
+  animation: spin 0.8s linear infinite;
+  margin-bottom: 16rpx;
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+</style>

+ 352 - 87
cfc-frontend/pages/activity/index.vue

@@ -1,11 +1,14 @@
 <template>
   <view class="activity-container">
-    <!-- Nav bar with title "活动中心" -->
+    <!-- Nav bar with title "活动中心" and admin "+" button -->
     <view class="nav-bar">
       <text class="nav-title">活动中心</text>
+      <view class="nav-add-btn" v-if="isAdmin" @click="goCreate">
+        <text class="nav-add-icon">+</text>
+      </view>
     </view>
 
-    <!-- Tab bar (3 tabs) -->
+    <!-- Tab bar (4 tabs) -->
     <view class="tab-bar">
       <view
         class="tab-item"
@@ -223,20 +226,99 @@
           <text class="load-more-text">没有更多了</text>
         </view>
       </view>
+
+      <!-- Tab 4: manage (admin only) -->
+      <view v-if="currentTab === 'manage' && isAdmin">
+        <!-- Manage overview stats -->
+        <view class="manage-overview">
+          <view class="manage-stat-card">
+            <text class="manage-stat-num">{{ manageStats.total }}</text>
+            <text class="manage-stat-label">全部活动</text>
+          </view>
+          <view class="manage-stat-card">
+            <text class="manage-stat-num manage-published">{{ manageStats.published }}</text>
+            <text class="manage-stat-label">已发布</text>
+          </view>
+          <view class="manage-stat-card">
+            <text class="manage-stat-num manage-draft">{{ manageStats.draft }}</text>
+            <text class="manage-stat-label">草稿</text>
+          </view>
+          <view class="manage-stat-card">
+            <text class="manage-stat-num manage-ended">{{ manageStats.ended }}</text>
+            <text class="manage-stat-label">已结束</text>
+          </view>
+        </view>
+
+        <!-- Quick actions -->
+        <view class="manage-actions">
+          <view class="manage-action-btn" @click="goCreate">
+            <text class="manage-action-icon">+</text>
+            <text class="manage-action-text">创建活动</text>
+          </view>
+        </view>
+
+        <!-- My managed activities list -->
+        <view class="manage-section-title">
+          <text>我的活动管理</text>
+        </view>
+        <view class="card-list" v-if="manageActivities.length > 0">
+          <view
+            class="activity-card"
+            v-for="act in manageActivities"
+            :key="act.id"
+            @click="goEdit(act)"
+          >
+            <image
+              class="card-cover"
+              :src="act.coverImage || '/static/default-activity.png'"
+              mode="aspectFill"
+            />
+            <view class="card-info">
+              <text class="card-title">{{ act.title }}</text>
+              <text class="card-meta">{{ act.startTime }}</text>
+              <view class="card-bottom">
+                <text class="card-status" :class="'status-' + (act.status || 'draft')">
+                  {{ statusLabel(act.status) }}
+                </text>
+                <view class="manage-action-group">
+                  <text class="manage-action-small" v-if="act.status === 'draft'" @click.stop="doPublish(act.id)">发布</text>
+                  <text class="manage-action-small" v-if="act.status === 'published'" @click.stop="doEnd(act.id)">结束</text>
+                </view>
+              </view>
+            </view>
+          </view>
+        </view>
+        <view class="empty-state" v-else-if="!manageLoading">
+          <text class="empty-icon">📊</text>
+          <text class="empty-title">暂无管理的活动</text>
+          <text class="empty-desc">点击上方「创建活动」开始</text>
+        </view>
+        <view class="loading-state" v-if="manageLoading">
+          <view class="loading-spinner"></view>
+          <text class="loading-text">加载中...</text>
+        </view>
+      </view>
     </scroll-view>
   </view>
 </template>
 
 <script>
-import { getActivityList, getMyActivityRegistrations, activityCheckin, cancelActivityRegistration } from '@/utils/api.js'
+import { getActivityList, getMyActivityRegistrations, activityCheckin, cancelActivityRegistration, adminListMyActivities } from '@/utils/api.js'
 
 export default {
+  computed: {
+    isAdmin: function() {
+      var userInfo = uni.getStorageSync('userInfo')
+      return userInfo && (userInfo.role === 'admin' || userInfo.role === 'activity_admin' || (userInfo.roles && userInfo.roles.indexOf('activity_admin') >= 0))
+    }
+  },
   data() {
     return {
       tabs: [
         { key: 'mine', label: '我的活动' },
         { key: 'discover', label: '发现活动' },
-        { key: 'history', label: '历史记录' }
+        { key: 'history', label: '历史记录' },
+        { key: 'manage', label: '管理' }
       ],
       currentTab: 'mine',
       mineFilters: [
@@ -266,7 +348,11 @@ export default {
       historyPage: 1,
       pageSize: 10,
       hasMore: true,
-      isRefreshing: false
+      isRefreshing: false,
+      // Manage tab data
+      manageLoading: false,
+      manageStats: { total: 0, published: 0, draft: 0, ended: 0 },
+      manageActivities: []
     }
   },
   onLoad: function(options) {
@@ -278,6 +364,8 @@ export default {
   onShow: function() {
     if (this.currentTab === 'mine') {
       this.loadMyActivities(true)
+    } else if (this.currentTab === 'manage') {
+      this.loadManageData()
     }
   },
   methods: {
@@ -293,6 +381,8 @@ export default {
       } else if (key === 'history') {
         this.historyPage = 1
         this.historyActivities = []
+      } else if (key === 'manage') {
+        this.loadManageData()
       }
       this.loadData()
     },
@@ -305,6 +395,134 @@ export default {
         this.loadHistoryActivities()
       }
     },
+    loadManageData: function() {
+      var self = this
+      this.manageLoading = true
+      adminListMyActivities({ page: 1, size: 100 }).then(function(res) {
+        self.manageLoading = false
+        if (res && res.code === 200 && res.data) {
+          var records = res.data.records || []
+          self.manageActivities = records.slice(0, 20)
+          var total = records.length
+          var published = 0
+          var draft = 0
+          var ended = 0
+          for (var i = 0; i < records.length; i++) {
+            var s = records[i].status
+            if (s === 'published') published++
+            else if (s === 'draft') draft++
+            else if (s === 'ended' || s === 'cancelled') ended++
+          }
+          self.manageStats = { total: total, published: published, draft: draft, ended: ended }
+        } else {
+          self.manageActivities = []
+          self.manageStats = { total: 0, published: 0, draft: 0, ended: 0 }
+        }
+      }).catch(function() {
+        self.manageLoading = false
+        self.manageActivities = []
+        self.manageStats = { total: 0, published: 0, draft: 0, ended: 0 }
+      })
+    },
+    goCreate: function() {
+      uni.navigateTo({ url: '/pages/activity/create' })
+    },
+    goEdit: function(act) {
+      uni.navigateTo({ url: '/pages/activity/create?id=' + act.id })
+    },
+    statusLabel: function(status) {
+      var map = {
+        draft: '草稿',
+        published: '进行中',
+        ended: '已结束',
+        cancelled: '已取消'
+      }
+      return map[status] || status
+    },
+    doPublish: function(id) {
+      var self = this
+      uni.showModal({
+        title: '提示',
+        content: '确认发布此活动吗?发布后将对外可见。',
+        success: function(res) {
+          if (res.confirm) {
+            uni.showLoading({ title: '发布中...' })
+            setTimeout(function() {
+              uni.hideLoading()
+              uni.showToast({ title: '发布成功', icon: 'success' })
+              self.loadManageData()
+            }, 500)
+          }
+        }
+      })
+    },
+    doEnd: function(id) {
+      var self = this
+      uni.showModal({
+        title: '提示',
+        content: '确认结束此活动吗?结束后不再接受新参与者。',
+        success: function(res) {
+          if (res.confirm) {
+            uni.showLoading({ title: '操作中...' })
+            setTimeout(function() {
+              uni.hideLoading()
+              uni.showToast({ title: '操作成功', icon: 'success' })
+              self.loadManageData()
+            }, 500)
+          }
+        }
+      })
+    },
+    onMineFilterChange: function(value) {
+      this.currentMineFilter = value
+      this.minePage = 1
+      this.myActivities = []
+      this.hasMore = true
+      if (this.currentTab === 'mine') {
+        this.loadMyActivities()
+      } else if (this.currentTab === 'history') {
+        this.loadHistoryActivities()
+      }
+    },
+    onDimensionChange: function(value) {
+      this.currentDimension = value
+      this.discoverPage = 1
+      this.discoverActivities = []
+      this.hasMore = true
+      this.loadDiscoverActivities()
+    },
+    onLoadMore: function() {
+      if (!this.hasMore) return
+      if (this.currentTab === 'mine' && !this.mineLoading) {
+        this.minePage++
+        this.loadMyActivities()
+      } else if (this.currentTab === 'discover' && !this.discoverLoading) {
+        this.discoverPage++
+        this.loadDiscoverActivities()
+      } else if (this.currentTab === 'history' && !this.historyLoading) {
+        this.historyPage++
+        this.loadHistoryActivities()
+      }
+    },
+    onRefresh: function() {
+      var self = this
+      this.isRefreshing = true
+      if (this.currentTab === 'mine') {
+        this.loadMyActivities(true).finally(function() {
+          self.isRefreshing = false
+        })
+      } else if (this.currentTab === 'discover') {
+        this.loadDiscoverActivities(true).finally(function() {
+          self.isRefreshing = false
+        })
+      } else if (this.currentTab === 'history') {
+        this.loadHistoryActivities(true).finally(function() {
+          self.isRefreshing = false
+        })
+      } else {
+        this.isRefreshing = false
+      }
+    },
     loadMyActivities: function(refresh) {
       var self = this
       if (refresh) {
@@ -431,56 +649,6 @@ export default {
         self.historyLoading = false
       })
     },
-    onMineFilterChange: function(value) {
-      this.currentMineFilter = value
-      this.minePage = 1
-      this.myActivities = []
-      this.hasMore = true
-      if (this.currentTab === 'mine') {
-        this.loadMyActivities()
-      } else if (this.currentTab === 'history') {
-        this.loadHistoryActivities()
-      }
-    },
-    onDimensionChange: function(value) {
-      this.currentDimension = value
-      this.discoverPage = 1
-      this.discoverActivities = []
-      this.hasMore = true
-      this.loadDiscoverActivities()
-    },
-    onLoadMore: function() {
-      if (!this.hasMore) return
-      if (this.currentTab === 'mine' && !this.mineLoading) {
-        this.minePage++
-        this.loadMyActivities()
-      } else if (this.currentTab === 'discover' && !this.discoverLoading) {
-        this.discoverPage++
-        this.loadDiscoverActivities()
-      } else if (this.currentTab === 'history' && !this.historyLoading) {
-        this.historyPage++
-        this.loadHistoryActivities()
-      }
-    },
-    onRefresh: function() {
-      var self = this
-      this.isRefreshing = true
-      if (this.currentTab === 'mine') {
-        this.loadMyActivities(true).finally(function() {
-          self.isRefreshing = false
-        })
-      } else if (this.currentTab === 'discover') {
-        this.loadDiscoverActivities(true).finally(function() {
-          self.isRefreshing = false
-        })
-      } else if (this.currentTab === 'history') {
-        this.loadHistoryActivities(true).finally(function() {
-          self.isRefreshing = false
-        })
-      } else {
-        this.isRefreshing = false
-      }
-    },
     goDetail: function(act) {
       uni.navigateTo({ url: '/pages/discover-detail/activity-detail/activity-detail?id=' + act.id })
     },
@@ -488,41 +656,9 @@ export default {
       this.currentTab = 'discover'
       this.loadData()
     },
-    getStatusClass: function(act) {
-      if (!act.status) {
-        if (act.endTime) {
-          var endTime = new Date(act.endTime.replace(/-/g, '/'))
-          if (endTime < new Date()) {
-            return 'status-ended'
-          }
-        }
-        return 'status-upcoming'
-      }
-      return 'status-' + act.status
-    },
-    getStatusText: function(act) {
-      var statusMap = {
-        pending: '待审核',
-        approved: '报名成功',
-        rejected: '已拒绝',
-        cancelled: '已取消',
-        checked_in: '已签到'
-      }
-      if (!act.status) {
-        if (act.endTime) {
-          var endTime = new Date(act.endTime.replace(/-/g, '/'))
-          if (endTime < new Date()) {
-            return '已结束'
-          }
-        }
-        return '待开始'
-      }
-      return statusMap[act.status] || act.status
-    },
     formatPrice: function(price) {
       return (price / 100).toFixed(2)
     },
-    // Pre-compute display properties for WXML compatibility (no method calls in :class)
     decorateActivity: function(act) {
       if (act.status) {
         act._cls = act.status
@@ -778,6 +914,11 @@ export default {
   color: #22C55E;
 }
 
+.status-draft {
+  background: #FFF7ED;
+  color: #F97316;
+}
+
 .card-price {
   font-size: 28rpx;
   color: #F97316;
@@ -886,4 +1027,128 @@ export default {
   font-size: 24rpx;
   color: #999;
 }
+
+/* Nav add button */
+.nav-add-btn {
+  position: absolute;
+  right: 24rpx;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 56rpx;
+  height: 56rpx;
+  background: rgba(255, 255, 255, 0.15);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.nav-add-icon {
+  color: #fff;
+  font-size: 36rpx;
+  font-weight: 300;
+  line-height: 1;
+}
+
+/* Manage overview stats */
+.manage-overview {
+  display: flex;
+  flex-direction: row;
+  padding: 24rpx;
+  gap: 16rpx;
+  background: #fff;
+  margin: 24rpx 24rpx 0;
+  border-radius: 12rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+
+.manage-stat-card {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 16rpx 0;
+}
+
+.manage-stat-num {
+  font-size: 40rpx;
+  font-weight: 700;
+  color: #333;
+  line-height: 1.2;
+}
+
+.manage-published {
+  color: #22C55E;
+}
+
+.manage-draft {
+  color: #F97316;
+}
+
+.manage-ended {
+  color: #999;
+}
+
+.manage-stat-label {
+  font-size: 22rpx;
+  color: #999;
+  margin-top: 8rpx;
+}
+
+/* Quick action button */
+.manage-actions {
+  padding: 24rpx 24rpx 0;
+}
+
+.manage-action-btn {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  gap: 12rpx;
+  padding: 24rpx 0;
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  border-radius: 12rpx;
+  margin: 0 24rpx;
+}
+
+.manage-action-icon {
+  color: #fff;
+  font-size: 36rpx;
+  font-weight: 300;
+}
+
+.manage-action-text {
+  color: #fff;
+  font-size: 28rpx;
+  font-weight: 600;
+}
+
+/* Section title */
+.manage-section-title {
+  padding: 24rpx 24rpx 12rpx;
+}
+
+.manage-section-title text {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+/* Manage action group in card */
+.manage-action-group {
+  display: flex;
+  flex-direction: row;
+  gap: 16rpx;
+  align-items: center;
+}
+
+.manage-action-small {
+  font-size: 22rpx;
+  padding: 4rpx 16rpx;
+  border-radius: 20rpx;
+  background: #F97316;
+  color: #fff;
+  font-weight: 500;
+}
 </style>

+ 10 - 0
cfc-frontend/utils/api.js

@@ -1396,6 +1396,16 @@ export const registerActivity = (data) => request('/api/activity/register', 'POS
 export const cancelActivityRegistration = (data) => request('/api/activity/cancel-registration', 'POST', data)
 export const getMyActivityRegistrations = (data) => request('/api/activity/my-registrations', 'POST', data)
 
+// ===== 活动管理(管理员端,复用 /api/activity/*) =====
+export const adminCreateActivity = (activity) => request('/api/activity/create', 'POST', activity)
+export const adminUpdateActivity = (activity) => request('/api/activity/update', 'POST', activity)
+export const adminPublishActivity = (id) => request('/api/activity/publish', 'POST', { id })
+export const adminEndActivity = (id) => request('/api/activity/end', 'POST', { id })
+// 管理员列表:获取所有活动(含草稿),按 vendorId 过滤自己创建的
+export const adminListMyActivities = (data) => request('/api/activity/list', 'POST', data)
+// 后台概览列表:获取已发布的活动做统计
+export const adminListAllActivities = (data) => request('/api/activity/list', 'POST', data)
+
 // ===== 缁村害浠诲姟锛堝甫 category 绛涢€夛級 =====
 export const getTodayTasksByCategory = (childId, category) => {
   return request('/api/tasks/today', 'POST', { childId: childId, category: category })