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

refactor(challenge): 重设计家庭挑战主页 - 移除重复的快速创建区,模板移至弹窗内,简化布局

iwt 1 месяц назад
Родитель
Сommit
8d6ab7593a
3 измененных файлов с 474 добавлено и 417 удалено
  1. 471 414
      cfc-frontend/pages/health/challenge-manage.vue
  2. 1 1
      cfc-web/.last_build_commit
  3. 2 2
      cfc-web/package-lock.json

+ 471 - 414
cfc-frontend/pages/health/challenge-manage.vue

@@ -1,142 +1,178 @@
 <template>
-  <view class="challenge-manage-page">
+  <view class="page">
     <BaseLoading :loading="loading" text="加载中..." />
 
-    <view v-if="!loading" class="page-content">
-      <!-- 模板推荐区 -->
-      <view class="template-section" v-if="hasFamily">
-        <view class="section-title-row">
-          <text class="section-title">快速创建</text>
-          <text class="section-hint">选择模板,自定义参数</text>
+    <view v-if="!loading" class="content">
+      <!-- 首页挑战状态概览 -->
+      <view class="overview-card" v-if="activeChallenges.length > 0">
+        <view class="overview-header">
+          <text class="overview-title">🔥 进行中</text>
+          <text class="overview-count">{{ activeChallenges.length }} 个挑战</text>
         </view>
-        <scroll-view class="template-scroll" scroll-x>
-          <view class="template-scroll-inner">
-            <view class="template-card" v-for="tpl in templates" :key="tpl.type" @click="createFromTemplate(tpl)">
-              <view class="template-icon">{{ getTemplateEmoji(tpl.type) }}</view>
-              <text class="template-name">{{ tpl.title }}</text>
-            </view>
-            <view class="template-card template-card-custom" @click="createFromTemplate(null)">
-              <view class="template-icon">✏️</view>
-              <text class="template-name">自定义</text>
-            </view>
+        <view class="active-list">
+          <view class="active-item" v-for="ch in activeChallenges.slice(0, 2)" :key="ch.id" @click="viewChallenge(ch)">
+            <text class="active-item-title">{{ ch.title }}</text>
+            <text class="active-item-meta">{{ ch.targetValue }}/{{ ch.durationDays }}天</text>
           </view>
-        </scroll-view>
+        </view>
       </view>
-      <view v-else>
-        <FamilyEmptyState type="card" />
+
+      <!-- 操作栏 -->
+      <view class="action-bar">
+        <button class="btn-new" @click="openCreateModal">
+          <text class="btn-new-icon">+</text>
+          <text>新建挑战</text>
+        </button>
       </view>
 
-      <!-- 挑战列表 -->
-      <view class="list-section" v-if="hasFamily">
-        <view class="section-title-row">
+      <!-- 挑战分类列表 -->
+      <view class="section">
+        <view class="section-header">
           <text class="section-title">我的挑战</text>
-          <text class="list-add-btn" @click="openCreateModal(null)">+ 新建</text>
         </view>
 
         <!-- 进行中 -->
-        <view class="group" v-if="activeChallenges.length > 0">
-          <text class="group-label">进行中</text>
-          <view class="challenge-item" v-for="ch in activeChallenges" :key="ch.id" @click="viewChallenge(ch)">
-            <view class="challenge-item-header">
-              <text class="challenge-item-title">{{ ch.title }}</text>
-              <view class="challenge-item-actions">
-                <text class="action-btn action-edit" @click.stop="openEditModal(ch)">编辑</text>
-                <text class="action-btn action-delete" @click.stop="deleteChallenge(ch.id)">删除</text>
+        <view v-if="activeChallenges.length > 0" class="group">
+          <view class="group-label">进行中</view>
+          <view class="card-list">
+            <view class="card" v-for="ch in activeChallenges" :key="ch.id" @click="viewChallenge(ch)">
+              <view class="card-main">
+                <view class="card-header">
+                  <text class="card-title">{{ ch.title }}</text>
+                  <text class="card-status status-active">进行中</text>
+                </view>
+                <view class="card-meta">
+                  <text class="meta-item">{{ ch.targetMode === 'aggregate' ? '全家合计' : '每人目标' }}</text>
+                  <text class="meta-item">{{ ch.durationDays }}天</text>
+                  <text class="meta-item" v-if="ch.rewardPoints">+{{ ch.rewardPoints }}分</text>
+                </view>
+              </view>
+              <view class="card-actions">
+                <text class="action-btn edit" @click.stop="openEditModal(ch)">编辑</text>
+                <text class="action-btn del" @click.stop="deleteChallenge(ch.id)">删除</text>
               </view>
             </view>
-            <text class="challenge-item-desc" v-if="ch.description">{{ ch.description }}</text>
-            <view class="challenge-item-footer">
-              <text class="tag">{{ ch.targetMode === 'aggregate' ? '全家目标' : '每人目标' }}</text>
-              <text class="tag tag-days">{{ ch.durationDays }}天</text>
-              <text class="tag tag-points" v-if="ch.rewardPoints">🏆 +{{ ch.rewardPoints }}分</text>
-            </view>
-            <FamilyChallengeCard :challenge="ch" />
           </view>
         </view>
 
         <!-- 已完成 -->
-        <view class="group" v-if="historyChallenges.length > 0">
-          <text class="group-label">已完成</text>
-          <view class="challenge-item" v-for="ch in historyChallenges" :key="ch.id" @click="viewChallenge(ch)">
-            <view class="challenge-item-header">
-              <text class="challenge-item-title">{{ ch.title }}</text>
-              <text class="status-badge status-completed">已完成</text>
+        <view v-if="historyChallenges.length > 0" class="group">
+          <view class="group-label">已完成</view>
+          <view class="card-list">
+            <view class="card" v-for="ch in historyChallenges" :key="ch.id" @click="viewChallenge(ch)">
+              <view class="card-main">
+                <view class="card-header">
+                  <text class="card-title">{{ ch.title }}</text>
+                  <text class="card-status status-done">已完成</text>
+                </view>
+                <view class="card-meta">
+                  <text class="meta-item">{{ ch.durationDays }}天</text>
+                </view>
+              </view>
             </view>
-            <text class="challenge-item-desc" v-if="ch.description">{{ ch.description }}</text>
           </view>
         </view>
 
         <!-- 空状态 -->
-        <view class="empty-state" v-if="activeChallenges.length === 0 && historyChallenges.length === 0">
+        <view v-if="activeChallenges.length === 0 && historyChallenges.length === 0" class="empty">
           <text class="empty-icon">🎯</text>
           <text class="empty-title">还没有挑战</text>
-          <text class="empty-desc">创建第一个家庭挑战,让全家一起成长!</text>
-          <view class="empty-btn" @click="openCreateModal(null)">立即创建</view>
+          <text class="empty-desc">创建挑战,让全家一起动起来!</text>
+          <button class="btn-empty" @click="openCreateModal">立即创建</button>
         </view>
       </view>
     </view>
 
     <!-- 创建/编辑弹窗 -->
-    <view class="modal-overlay" v-if="showModal" @click="closeModal">
-      <view class="modal-content" @click.stop>
+    <view class="overlay" v-if="showModal" @click="closeModal">
+      <view class="modal" @click.stop>
         <view class="modal-header">
-          <text class="modal-title">{{ editMode ? '编辑挑战' : '建挑战' }}</text>
+          <text class="modal-title">{{ editMode ? '编辑挑战' : '建挑战' }}</text>
           <text class="modal-close" @click="closeModal">×</text>
         </view>
-        <view class="modal-body">
-          <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>
-            <textarea class="form-textarea" v-model="form.description" placeholder="描述挑战内容" maxlength="200" />
-          </view>
-          <view class="form-item">
-            <text class="form-label">挑战类型</text>
-            <view class="type-picker">
-              <text class="type-item" :class="{ active: form.challengeType === t.type }"
-                    v-for="t in templates" :key="t.type"
-                    @click="form.challengeType = t.type">{{ t.title }}</text>
+
+        <!-- 模板选择(仅创建时) -->
+        <view v-if="!editMode" class="template-row">
+          <text class="tpl-label">快速模板</text>
+          <scroll-view scroll-x class="tpl-scroll">
+            <view class="tpl-list">
+              <view
+                class="tpl-item"
+                :class="{ active: form.challengeType === 'custom' }"
+                @click="selectTemplate(null)"
+              >
+                <text class="tpl-icon">✏️</text>
+                <text class="tpl-name">自定义</text>
+              </view>
+              <view
+                class="tpl-item"
+                v-for="tpl in templates"
+                :key="tpl.type"
+                :class="{ active: form.challengeType === tpl.type }"
+                @click="selectTemplate(tpl)"
+              >
+                <text class="tpl-icon">{{ tpl.emoji }}</text>
+                <text class="tpl-name">{{ tpl.title }}</text>
+              </view>
             </view>
+          </scroll-view>
+        </view>
+
+        <view class="form-body">
+          <view class="field">
+            <text class="field-label">挑战标题 <text class="required">*</text></text>
+            <input class="field-input" v-model="form.title" placeholder="给挑战起个名字" maxlength="50" />
+          </view>
+
+          <view class="field">
+            <text class="field-label">挑战描述</text>
+            <textarea class="field-textarea" v-model="form.description" placeholder="描述挑战内容(可选)" maxlength="200" />
           </view>
-          <view class="form-row">
-            <view class="form-item" style="flex:1">
-              <text class="form-label">目标模式 *</text>
-              <view class="mode-picker">
-                <text class="mode-item" :class="{ active: form.targetMode === 'aggregate' }"
-                      @click="form.targetMode = 'aggregate'">全家合计</text>
-                <text class="mode-item" :class="{ active: form.targetMode === 'all_members' }"
-                      @click="form.targetMode = 'all_members'">每人目标</text>
+
+          <view class="field-row">
+            <view class="field half">
+              <text class="field-label">目标模式</text>
+              <view class="radio-group">
+                <view class="radio-btn" :class="{ active: form.targetMode === 'aggregate' }" @click="form.targetMode = 'aggregate'">全家合计</view>
+                <view class="radio-btn" :class="{ active: form.targetMode === 'all_members' }" @click="form.targetMode = 'all_members'">每人目标</view>
               </view>
             </view>
-            <view class="form-item" style="flex:1">
-              <text class="form-label">目标值 *</text>
-              <input class="form-input" type="digit" v-model="form.targetValue" placeholder="如: 300" />
+            <view class="field half">
+              <text class="field-label">目标值 <text class="required">*</text></text>
+              <input class="field-input" type="digit" v-model="form.targetValue" placeholder="如: 300" />
             </view>
           </view>
-          <view class="form-row">
-            <view class="form-item" style="flex:1">
-              <text class="form-label">持续天数</text>
-              <input class="form-input" type="number" v-model="form.durationDays" placeholder="7" />
+
+          <view class="field-row">
+            <view class="field half">
+              <text class="field-label">持续天数</text>
+              <input class="field-input" type="number" v-model="form.durationDays" placeholder="7" />
             </view>
-            <view class="form-item" style="flex:1">
-              <text class="form-label">奖励积分</text>
-              <input class="form-input" type="number" v-model="form.rewardPoints" placeholder="50" />
-        </view>
-      </view>
-          <view class="form-item" v-if="!editMode">
-            <text class="form-label">参与成员</text>
-            <view class="member-picker">
-              <text class="member-pick-item" :class="{ active: isMemberSelected(m.id) }"
-                    v-for="m in familyMembers" :key="m.id"
-                    @click="toggleMember(m.id)">{{ m.nickname }}</text>
+            <view class="field half">
+              <text class="field-label">奖励积分</text>
+              <input class="field-input" type="number" v-model="form.rewardPoints" placeholder="50" />
             </view>
           </view>
-    </view>
+
+          <view class="field" v-if="!editMode">
+            <text class="field-label">参与成员</text>
+            <view class="member-grid">
+              <view
+                class="member-item"
+                :class="{ active: isMemberSelected(m.id) }"
+                v-for="m in familyMembers"
+                :key="m.id"
+                @click="toggleMember(m.id)"
+              >
+                <text>{{ m.nickname || m.phone || '成员' }}</text>
+              </view>
+            </view>
+          </view>
+        </view>
+
         <view class="modal-footer">
-          <view class="modal-btn modal-btn-cancel" @click="closeModal">取消</view>
-          <view class="modal-btn modal-btn-submit" @click="submitForm">{{ editMode ? '保存' : '创建' }}</view>
+          <button class="btn-cancel" @click="closeModal">取消</button>
+          <button class="btn-submit" @click="submitForm">{{ editMode ? '保存' : '创建' }}</button>
         </view>
       </view>
     </view>
@@ -144,15 +180,12 @@
 </template>
 
 <script>
-import { getChallengeList, getChallengeHistory, createChallenge, updateChallenge, deleteChallenge, getChallengeTemplates, getParentDashboard, listFamilyMembers } from '../../utils/api.js'
-import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
+import { getChallengeList, getChallengeHistory, createChallenge, updateChallenge, deleteChallenge, getChallengeTemplates, listFamilyMembers } from '../../utils/api.js'
 
 export default {
-  components: { FamilyChallengeCard },
   data() {
     return {
       loading: true,
-      familyId: null,
       templates: [],
       activeChallenges: [],
       historyChallenges: [],
@@ -172,415 +205,405 @@ export default {
       }
     }
   },
-  computed: {
-    hasFamily: function() {
-      if (this.loading) return true
-      if (this.familyId) return true
-      return this.$store.getters.hasFamily
-    }
-  },
   onLoad() {
-    this.loadFamilyId()
-    this.loadData()
     this.loadTemplates()
     this.loadMembers()
+    this.loadData()
   },
   onShow() {
-    if (this.familyId) {
-      this.loadData()
-    }
+    this.loadData()
   },
   methods: {
-    getUserRole: function() {
-      // store state 顶层有 role 字段(见 store/index.js:15)
-      return this.$store && this.$store.state ? this.$store.state.role : ''
-    },
-    loadFamilyId: function() {
-      var self = this
+    async loadData() {
+      this.loading = true
       try {
-        var dashPromise = getParentDashboard()
-        dashPromise.then(function(res) {
-          var data = res.data || {}
-          if (data.familyId) {
-            self.familyId = data.familyId
-          }
-        }).catch(function() {})
+        const [res1, res2] = await Promise.all([
+          getChallengeList({}),
+          getChallengeHistory({})
+        ])
+        this.activeChallenges = Array.isArray(res1.data) ? res1.data : []
+        this.historyChallenges = Array.isArray(res2.data) ? res2.data : []
       } catch (e) {
-        console.log('获取familyId失败', e)
+        this.activeChallenges = []
+        this.historyChallenges = []
+      } finally {
+        this.loading = false
       }
     },
-    loadData: function() {
-      var self = this
-      if (!self.familyId) return
-      self.loading = true
-      Promise.all([
-        getChallengeList({}),
-        getChallengeHistory({})
-      ]).then(function(results) {
-        self.activeChallenges = Array.isArray(results[0].data) ? results[0].data : []
-        self.historyChallenges = Array.isArray(results[1].data) ? results[1].data : []
-      }).catch(function() {
-        self.activeChallenges = []
-        self.historyChallenges = []
-      }).finally(function() {
-        self.loading = false
-      })
-    },
-    loadTemplates: function() {
-      var self = this
-      getChallengeTemplates().then(function(res) {
+    async loadTemplates() {
+      try {
+        const res = await getChallengeTemplates()
         if (res.code === 200) {
-          self.templates = res.data || []
+          this.templates = (res.data || []).map(t => ({
+            ...t,
+            emoji: this.getEmoji(t.type)
+          }))
         }
-      }).catch(function() {
-        self.templates = [
-          { type: 'full_checkin', title: '全员打卡', targetMode: 'all_members', targetValue: '3', durationDays: '3', rewardPoints: '50', description: '全家连续打卡' },
-          { type: 'sports', title: '运动PK', targetMode: 'aggregate', targetValue: '300', durationDays: '7', rewardPoints: '100', description: '全家运动时长' },
-          { type: 'reading', title: '阅读挑战', targetMode: 'aggregate', targetValue: '200', durationDays: '7', rewardPoints: '80', description: '全家阅读时长' },
-          { type: 'gratitude', title: '感恩日记', targetMode: 'all_members', targetValue: '7', durationDays: '7', rewardPoints: '40', description: '每天感恩记录' }
+      } catch (e) {
+        this.templates = [
+          { type: 'full_checkin', title: '全员打卡', emoji: '✅', targetMode: 'all_members', targetValue: 3, durationDays: 3, rewardPoints: 50 },
+          { type: 'sports', title: '运动PK', emoji: '🏃', targetMode: 'aggregate', targetValue: 300, durationDays: 7, rewardPoints: 100 },
+          { type: 'reading', title: '阅读挑战', emoji: '📚', targetMode: 'aggregate', targetValue: 200, durationDays: 7, rewardPoints: 80 },
+          { type: 'gratitude', title: '感恩日记', emoji: '🙏', targetMode: 'all_members', targetValue: 7, durationDays: 7, rewardPoints: 40 }
         ]
-      })
+      }
     },
-    getTemplateEmoji: function(type) {
-      var map = {
-        full_checkin: '✅',
-        sports: '🏃',
-        health_week: '💚',
-        reading: '📚',
-        no_screen: '📵',
-        gratitude: '🙏',
-        custom: '✏️'
+    getEmoji(type) {
+      const map = {
+        full_checkin: '✅', sports: '🏃', reading: '📚',
+        gratitude: '🙏', no_screen: '📵', custom: '✏️'
       }
       return map[type] || '🎯'
     },
-    loadMembers: function() {
-      var self = this
-      listFamilyMembers().then(function(res) {
-        var list = (res && res.data) || []
-        self.familyMembers = Array.isArray(list) ? list : []
-        // 默认全选
-        self.selectedMemberIds = []
-        for (var i = 0; i < self.familyMembers.length; i++) {
-          self.selectedMemberIds.push(self.familyMembers[i].id)
-        }
-      }).catch(function() {
-        self.familyMembers = []
-        self.selectedMemberIds = []
-      })
+    async loadMembers() {
+      try {
+        const res = await listFamilyMembers()
+        this.familyMembers = Array.isArray(res.data) ? res.data : []
+        this.selectedMemberIds = (this.familyMembers || []).map(m => m.id)
+      } catch (e) {
+        this.familyMembers = []
+        this.selectedMemberIds = []
+      }
     },
-    isMemberSelected: function(memberId) {
-      return this.selectedMemberIds.indexOf(memberId) >= 0
+    isMemberSelected(id) {
+      return this.selectedMemberIds.indexOf(id) >= 0
     },
-    toggleMember: function(memberId) {
-      var idx = this.selectedMemberIds.indexOf(memberId)
-      if (idx >= 0) {
-        this.selectedMemberIds.splice(idx, 1)
+    toggleMember(id) {
+      var idx = this.selectedMemberIds.indexOf(id)
+      if (idx >= 0) this.selectedMemberIds.splice(idx, 1)
+      else this.selectedMemberIds.push(id)
+    },
+    selectTemplate(tpl) {
+      this.form.challengeType = tpl ? tpl.type : 'custom'
+      if (tpl) {
+        this.form.targetMode = tpl.targetMode || 'all_members'
+        this.form.targetValue = String(tpl.targetValue || 7)
+        this.form.durationDays = String(tpl.durationDays || 7)
+        this.form.rewardPoints = String(tpl.rewardPoints || 50)
+        this.form.title = tpl.title || ''
+        this.form.description = tpl.description || ''
       } else {
-        this.selectedMemberIds.push(memberId)
+        this.form.targetMode = 'all_members'
+        this.form.targetValue = '7'
+        this.form.durationDays = '7'
+        this.form.rewardPoints = '50'
+        this.form.title = ''
+        this.form.description = ''
       }
     },
-    createFromTemplate: function(template) {
+    openCreateModal() {
       this.editMode = false
       this.editingId = null
       this.form = {
-        title: template ? template.title : '',
-        description: template ? template.description : '',
-        challengeType: template ? template.type : 'custom',
-        targetMode: template ? template.targetMode : 'all_members',
-        targetValue: template ? String(template.targetValue) : '7',
-        durationDays: template ? String(template.durationDays) : '7',
-        rewardPoints: template ? String(template.rewardPoints) : '50'
+        title: '', description: '', challengeType: 'custom',
+        targetMode: 'all_members', targetValue: '7', durationDays: '7', rewardPoints: '50'
       }
       this.showModal = true
     },
-    openCreateModal: function(template) {
-      this.createFromTemplate(template)
-    },
-    openEditModal: function(challenge) {
+    openEditModal(ch) {
       this.editMode = true
-      this.editingId = challenge.id
+      this.editingId = ch.id
       this.form = {
-        title: challenge.title || '',
-        description: challenge.description || '',
-        challengeType: challenge.challengeType || 'custom',
-        targetMode: challenge.targetMode || 'all_members',
-        targetValue: String(challenge.targetValue || '7'),
-        durationDays: String(challenge.durationDays || '7'),
-        rewardPoints: String(challenge.rewardPoints || '50')
+        title: ch.title || '',
+        description: ch.description || '',
+        challengeType: ch.challengeType || 'custom',
+        targetMode: ch.targetMode || 'all_members',
+        targetValue: String(ch.targetValue || 7),
+        durationDays: String(ch.durationDays || 7),
+        rewardPoints: String(ch.rewardPoints || 50)
       }
       this.showModal = true
     },
-    closeModal: function() {
+    closeModal() {
       this.showModal = false
     },
-    submitForm: function() {
-      var self = this
-      var title = self.form.title && self.form.title.trim()
-      if (!title) {
+    async submitForm() {
+      if (!this.form.title.trim()) {
         uni.showToast({ title: '请填写挑战标题', icon: 'none' })
         return
       }
-      var targetValue = parseInt(self.form.targetValue || '0', 10)
+      var targetValue = parseInt(this.form.targetValue || 0, 10)
       if (targetValue <= 0) {
         uni.showToast({ title: '目标值必须大于0', icon: 'none' })
         return
       }
       var data = {
-        title: title,
-        description: self.form.description && self.form.description.trim(),
-        challengeType: self.form.challengeType,
-        targetMode: self.form.targetMode,
+        title: this.form.title.trim(),
+        description: this.form.description.trim(),
+        challengeType: this.form.challengeType,
+        targetMode: this.form.targetMode,
         targetValue: targetValue,
-        durationDays: parseInt(self.form.durationDays || '7', 10),
-        rewardPoints: parseInt(self.form.rewardPoints || '0', 10)
+        durationDays: parseInt(this.form.durationDays || 7, 10),
+        rewardPoints: parseInt(this.form.rewardPoints || 0, 10)
       }
-      if (!self.editMode) {
-        data.participantMemberIds = self.selectedMemberIds
-      }
-      self.loading = true
-      var promise
-      if (self.editMode && self.editingId) {
-        promise = updateChallenge(self.editingId, data)
-      } else {
-        promise = createChallenge(data)
-      }
-      promise.then(function(res) {
+      if (!this.editMode) data.participantMemberIds = this.selectedMemberIds
+
+      this.loading = true
+      try {
+        var promise
+        if (this.editMode) {
+          promise = updateChallenge(this.editingId, data)
+        } else {
+          promise = createChallenge(data)
+        }
+        var res = await promise
         if (res.code === 200) {
-          var msg = self.editMode ? '更新成功' : '创建成功'
-          uni.showToast({ title: msg, icon: 'success' })
-          self.closeModal()
-          self.loadData()
+          uni.showToast({ title: this.editMode ? '保存成功' : '创建成功', icon: 'success' })
+          this.closeModal()
+          this.loadData()
         } else {
           uni.showToast({ title: res.message || '操作失败', icon: 'none' })
         }
-      }).catch(function(e) {
+      } catch (e) {
         uni.showToast({ title: '操作失败', icon: 'none' })
-        console.log('挑战操作失败', e)
-      }).finally(function() {
-        self.loading = false
-      })
+      } finally {
+        this.loading = false
+      }
     },
-    deleteChallenge: function(id) {
-      var self = this
+    deleteChallenge(id) {
       uni.showModal({
         title: '确认删除',
         content: '确定要取消这个挑战吗?',
-        success: function(r) {
+        success: (r) => {
           if (r.confirm) {
-            deleteChallenge(id).then(function(res) {
+            deleteChallenge(id).then((res) => {
               if (res.code === 200) {
                 uni.showToast({ title: '已删除', icon: 'success' })
-                self.loadData()
+                this.loadData()
               } else {
                 uni.showToast({ title: res.message || '删除失败', icon: 'none' })
               }
-            }).catch(function() {
-              uni.showToast({ title: '删除失败', icon: 'none' })
-            })
+            }).catch(() => uni.showToast({ title: '删除失败', icon: 'none' }))
           }
         }
       })
     },
-    viewChallenge: function(challenge) {
-      uni.showToast({ title: challenge.title, icon: 'none' })
+    viewChallenge(ch) {
+      // 跳转到挑战详情
+      uni.navigateTo({ url: '/pages/activity/index' })
     }
   }
 }
 </script>
 
 <style scoped>
-.challenge-manage-page {
+.page {
   min-height: 100vh;
-  background: #F5F9FC;
-  padding: 20rpx 24rpx 40rpx;
+  background: #F5F7FA;
+  padding: 24rpx;
+  padding-bottom: 60rpx;
 }
-.page-content {
+.content {
   display: flex;
   flex-direction: column;
-  gap: 28rpx;
+  gap: 24rpx;
 }
 
-/* 模板推荐区 */
-.template-section {
-  margin-bottom: 8rpx;
-}
-.section-title-row {
+/* 概览卡片 */
+.overview-card {
+  background: linear-gradient(135deg, #FFF7ED, #FFEDD5);
+  border-radius: 20rpx;
+  padding: 28rpx;
+  border: 2rpx solid #FED7AA;
+}
+.overview-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  margin-bottom: 12rpx;
+  margin-bottom: 20rpx;
 }
-.section-title {
+.overview-title {
   font-size: 30rpx;
   font-weight: 700;
-  color: #1E293B;
+  color: #92400E;
 }
-.section-hint {
-  font-size: 22rpx;
-  color: #94A3B8;
-}
-.template-scroll {
-  width: 100%;
+.overview-count {
+  font-size: 24rpx;
+  color: #B45309;
 }
-.template-scroll-inner {
+.active-list {
   display: flex;
-  gap: 12rpx;
+  flex-direction: column;
+  gap: 16rpx;
 }
-.template-card {
-  flex-shrink: 0;
-  width: 130rpx;
-  padding: 16rpx 12rpx;
-  background: #fff;
-  border-radius: 16rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+.active-item {
   display: flex;
-  flex-direction: column;
+  justify-content: space-between;
   align-items: center;
-  gap: 6rpx;
+  background: rgba(255,255,255,0.7);
+  border-radius: 12rpx;
+  padding: 16rpx 20rpx;
 }
-.template-card:active { opacity: 0.8; }
-.template-card-custom {
-  background: linear-gradient(135deg, #10B981, #34D399);
+.active-item-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #7C2D12;
 }
-.template-icon {
-  font-size: 40rpx;
-  line-height: 1;
+.active-item-meta {
+  font-size: 24rpx;
+  color: #B45309;
 }
-.template-name {
-  font-size: 22rpx;
-  color: #475569;
-  font-weight: 600;
+
+/* 操作栏 */
+.action-bar {
+  display: flex;
+  justify-content: flex-end;
 }
-.template-card-custom .template-name {
+.btn-new {
+  display: flex;
+  align-items: center;
+  gap: 8rpx;
+  background: linear-gradient(135deg, #10B981, #34D399);
   color: #fff;
+  font-size: 26rpx;
+  font-weight: 600;
+  padding: 16rpx 32rpx;
+  border-radius: 40rpx;
+  border: none;
+}
+.btn-new-icon {
+  font-size: 32rpx;
+  line-height: 1;
 }
 
 /* 列表区 */
-.list-section {
+.section {
   display: flex;
   flex-direction: column;
+  gap: 20rpx;
 }
-.list-add-btn {
-  font-size: 24rpx;
-  color: #10B981;
-  font-weight: 600;
+.section-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
 }
-.list-add-btn:active { opacity: 0.7; }
-
-.group {
-  margin-bottom: 20rpx;
+.section-title {
+  font-size: 30rpx;
+  font-weight: 700;
+  color: #1E293B;
 }
 .group-label {
   font-size: 26rpx;
   font-weight: 600;
   color: #64748B;
-  margin-bottom: 10rpx;
+  margin-bottom: 12rpx;
 }
-.challenge-item {
+.card-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+.card {
   background: #fff;
   border-radius: 16rpx;
-  padding: 16rpx;
-  margin-bottom: 12rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+  padding: 24rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.card-main {
+  flex: 1;
 }
-.challenge-item-header {
+.card-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  margin-bottom: 4rpx;
+  margin-bottom: 10rpx;
 }
-.challenge-item-title {
-  font-size: 26rpx;
+.card-title {
+  font-size: 28rpx;
   font-weight: 700;
   color: #1E293B;
 }
-.challenge-item-actions {
-  display: flex;
-  gap: 12rpx;
-}
-.action-btn {
+.card-status {
   font-size: 22rpx;
   padding: 4rpx 12rpx;
-  border-radius: 8rpx;
+  border-radius: 20rpx;
 }
-.action-edit {
-  color: #5B9BD5;
-  background: rgba(91,155,213,0.1);
+.status-active {
+  background: #DCFCE7;
+  color: #16A34A;
 }
-.action-delete {
-  color: #EF4444;
-  background: rgba(239,68,68,0.1);
+.status-done {
+  background: #F1F5F9;
+  color: #64748B;
+}
+.card-meta {
+  display: flex;
+  gap: 16rpx;
+  flex-wrap: wrap;
 }
-.challenge-item-desc {
+.meta-item {
   font-size: 22rpx;
   color: #64748B;
-  margin-bottom: 6rpx;
+  background: #F8FAFC;
+  padding: 4rpx 12rpx;
+  border-radius: 8rpx;
 }
-.challenge-item-footer {
+.card-actions {
   display: flex;
-  gap: 8rpx;
-  margin-bottom: 4rpx;
+  gap: 12rpx;
+  margin-left: 16rpx;
 }
-.tag {
-  font-size: 20rpx;
-  padding: 2rpx 10rpx;
-  border-radius: 999rpx;
-  background: #E2E8F0;
-  color: #475569;
+.action-btn {
+  font-size: 24rpx;
+  padding: 8rpx 16rpx;
+  border-radius: 8rpx;
 }
-.tag-days { background: #DBEAFE; color: #2563EB; }
-.tag-points { background: #FEF3C7; color: #D97706; }
-.status-badge {
-  font-size: 20rpx;
-  padding: 2rpx 10rpx;
-  border-radius: 999rpx;
+.action-btn.edit {
+  color: #5B9BD5;
+  background: rgba(91,155,213,0.1);
 }
-.status-completed {
-  background: #F1F5F9;
-  color: #64748B;
+.action-btn.del {
+  color: #EF4444;
+  background: rgba(239,68,68,0.1);
 }
 
 /* 空状态 */
-.empty-state {
+.empty {
   display: flex;
   flex-direction: column;
   align-items: center;
-  padding: 60rpx 0 20rpx;
+  padding: 80rpx 0;
 }
 .empty-icon { font-size: 80rpx; margin-bottom: 16rpx; }
 .empty-title { font-size: 32rpx; font-weight: 700; color: #1E293B; }
-.empty-desc { font-size: 24rpx; color: #94A3B8; margin: 8rpx 0 24rpx; }
-.empty-btn {
+.empty-desc { font-size: 26rpx; color: #94A3B8; margin: 8rpx 0 32rpx; }
+.btn-empty {
   background: #10B981;
   color: #fff;
-  font-size: 26rpx;
+  font-size: 28rpx;
   font-weight: 600;
-  padding: 16rpx 40rpx;
-  border-radius: 44rpx;
+  padding: 16rpx 48rpx;
+  border-radius: 40rpx;
+  border: none;
 }
 
 /* 弹窗 */
-.modal-overlay {
+.overlay {
   position: fixed;
   top: 0; left: 0; right: 0; bottom: 0;
   background: rgba(0,0,0,0.5);
   display: flex;
   align-items: flex-end;
-  justify-content: center;
   z-index: 999;
 }
-.modal-content {
+.modal {
   width: 100%;
   background: #fff;
   border-radius: 32rpx 32rpx 0 0;
-  padding: 24rpx;
+  padding: 32rpx 28rpx 40rpx;
   box-sizing: border-box;
+  max-height: 85vh;
+  display: flex;
+  flex-direction: column;
 }
 .modal-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  margin-bottom: 20rpx;
+  margin-bottom: 24rpx;
 }
 .modal-title {
   font-size: 32rpx;
@@ -588,122 +611,156 @@ export default {
   color: #1E293B;
 }
 .modal-close {
-  font-size: 40rpx;
+  font-size: 44rpx;
   color: #94A3B8;
   line-height: 1;
 }
-.modal-body {
+.form-body {
   display: flex;
   flex-direction: column;
-  gap: 16rpx;
-  margin-bottom: 20rpx;
-  max-height: 70vh;
+  gap: 20rpx;
+  flex: 1;
   overflow-y: auto;
+  margin-bottom: 24rpx;
 }
-.form-item {
+
+/* 模板行 */
+.template-row {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  margin-bottom: 4rpx;
+}
+.tpl-label {
+  font-size: 26rpx;
+  color: #475569;
+  font-weight: 600;
+  white-space: nowrap;
+}
+.tpl-scroll {
+  flex: 1;
+}
+.tpl-list {
+  display: flex;
+  gap: 12rpx;
+}
+.tpl-item {
+  flex-shrink: 0;
   display: flex;
   flex-direction: column;
+  align-items: center;
   gap: 6rpx;
+  padding: 14rpx 18rpx;
+  background: #F8FAFC;
+  border-radius: 14rpx;
+  border: 2rpx solid transparent;
 }
-.form-label {
-  font-size: 24rpx;
+.tpl-item.active {
+  border-color: #10B981;
+  background: #DCFCE7;
+}
+.tpl-icon { font-size: 32rpx; }
+.tpl-name { font-size: 22rpx; color: #475569; font-weight: 500; }
+.tpl-item.active .tpl-name { color: #16A34A; }
+
+/* 表单字段 */
+.field {
+  display: flex;
+  flex-direction: column;
+  gap: 8rpx;
+}
+.field-label {
+  font-size: 26rpx;
   color: #475569;
   font-weight: 600;
 }
-.form-input {
+.required { color: #EF4444; }
+.field-input {
   background: #F8FAFC;
   border: 1rpx solid #E2E8F0;
   border-radius: 12rpx;
-  padding: 14rpx 16rpx;
-  font-size: 26rpx;
+  padding: 16rpx 18rpx;
+  font-size: 28rpx;
   color: #1E293B;
 }
-.form-textarea {
+.field-textarea {
   background: #F8FAFC;
   border: 1rpx solid #E2E8F0;
   border-radius: 12rpx;
-  padding: 14rpx 16rpx;
-  font-size: 26rpx;
+  padding: 16rpx 18rpx;
+  font-size: 28rpx;
   color: #1E293B;
-  height: 80rpx;
+  height: 100rpx;
   width: 100%;
   box-sizing: border-box;
 }
-.form-row {
+.field-row {
   display: flex;
-  gap: 12rpx;
+  gap: 16rpx;
 }
-.type-picker {
+.field.half { flex: 1; }
+
+/* 单选组 */
+.radio-group {
   display: flex;
-  flex-wrap: wrap;
-  gap: 8rpx;
+  gap: 12rpx;
 }
-.type-item {
-  font-size: 22rpx;
-  padding: 6rpx 14rpx;
-  border-radius: 999rpx;
-  background: #F1F5F9;
+.radio-btn {
+  flex: 1;
+  text-align: center;
+  padding: 12rpx 0;
+  border-radius: 12rpx;
+  font-size: 26rpx;
   color: #475569;
+  background: #F1F5F9;
 }
-.type-item.active {
+.radio-btn.active {
   background: #10B981;
   color: #fff;
   font-weight: 600;
 }
-.mode-picker {
+
+/* 成员选择 */
+.member-grid {
   display: flex;
-  gap: 8rpx;
+  flex-wrap: wrap;
+  gap: 12rpx;
 }
-.mode-item {
-  flex: 1;
+.member-item {
+  padding: 10rpx 22rpx;
+  border-radius: 24rpx;
   font-size: 24rpx;
-  padding: 10rpx 0;
-  border-radius: 12rpx;
-  background: #F1F5F9;
   color: #475569;
-  text-align: center;
+  background: #F1F5F9;
+  border: 2rpx solid transparent;
 }
-.mode-item.active {
-  background: #10B981;
-  color: #fff;
-  font-weight: 600;
+.member-item.active {
+  background: #DCFCE7;
+  color: #16A34A;
+  border-color: #10B981;
 }
+
+/* 底部按钮 */
 .modal-footer {
   display: flex;
-  gap: 12rpx;
+  gap: 16rpx;
+  padding-top: 8rpx;
 }
-.modal-btn {
+.btn-cancel, .btn-submit {
   flex: 1;
-  text-align: center;
-  padding: 16rpx 0;
-  border-radius: 44rpx;
-  font-size: 28rpx;
+  height: 84rpx;
+  line-height: 84rpx;
+  border-radius: 42rpx;
+  font-size: 30rpx;
   font-weight: 600;
+  border: none;
 }
-.modal-btn-cancel {
+.btn-cancel {
   background: #F1F5F9;
   color: #64748B;
 }
-.modal-btn-submit {
-  background: #10B981;
+.btn-submit {
+  background: linear-gradient(135deg, #10B981, #34D399);
   color: #fff;
 }
-.member-picker {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 12rpx;
-}
-.member-pick-item {
-  padding: 8rpx 20rpx;
-  border-radius: 999rpx;
-  background: #F1F5F9;
-  color: #475569;
-  font-size: 22rpx;
-  border: 2rpx solid transparent;
-}
-.member-pick-item.active {
-  background: #DCFCE7;
-  color: #16A34A;
-  border-color: #10B981;
-}
 </style>

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-115f80296c1237af90d4c4b23502ca21dbddfc68
+2f268fead06b05438f01fb0c47c18b220e007c33

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.886",
+  "version": "1.0.887",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.886",
+      "version": "1.0.887",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",