Эх сурвалжийг харах

feat(tasks): 任务首页重设计 - 三Tab布局 + 家庭成员任务分配 + 接受/拒绝 + 关系分数调整

iwt 1 сар өмнө
parent
commit
483420984d

+ 721 - 772
cfc-frontend/pages/tasks/tasks.vue

@@ -1,772 +1,721 @@
-<template>
-  <PlayfulCard elevation="1">
-  <view class="container">
-    <!-- 批量操作栏 -->
-    <view class="batch-bar" v-if="batchMode">
-      <checkbox :checked="isAllSelected" @click="toggleSelectAll" />
-      <text class="batch-label">全选</text>
-      <view class="batch-actions">
-        <button class="batch-btn complete" @click="batchComplete" :disabled="selectedTasks.length === 0">批量完成</button>
-        <button class="batch-btn delete" @click="batchDelete" :disabled="selectedTasks.length === 0">批量删除</button>
-      </view>
-      <button class="batch-exit" @click="exitBatchMode">退出</button>
-    </view>
-
-    <!-- 正常模式顶部栏 -->
-    <view class="header-bar" v-else>
-      <text class="header-title">今日任务</text>
-      <button class="btn-batch" @click="enterBatchMode" v-if="tasks.length > 0">批量操作</button>
-    </view>
-
-    <!-- 任务列表 -->
-    <view class="task-list" v-if="tasks.length > 0">
-      <view 
-        v-for="task in tasks" 
-        :key="task.id" 
-        class="task-item"
-        :class="{ completed: task.status === 'completed', selected: selectedTasks.includes(task.id) }"
-      >
-        <view class="task-checkbox" v-if="batchMode" @click="toggleTaskSelect(task.id)">
-          <checkbox :checked="selectedTasks.includes(task.id)" />
-        </view>
-        <view class="task-left">
-          <view class="task-title">
-            {{ task.title }}
-            <text v-if="task.category === '小游戏类'" class="minigame-badge">🎮</text>
-          </view>
-          <view class="task-meta">
-            <text class="points">+{{ task.points }}分</text>
-            <text class="deadline">截止 {{ formatTime(task.deadline) }}</text>
-          </view>
-        </view>
-        <view class="task-right" v-if="!batchMode">
-          <button 
-            v-if="task.status === 'pending'" 
-            class="btn-complete" 
-            @click="isParent ? completeParentTask(task.id) : completeTask(task.id)"
-          >
-            完成
-          </button>
-          <view v-else class="completed-badge">已完成</view>
-        </view>
-      </view>
-    </view>
-
-    <view class="empty" v-else>
-      <text>暂无今日任务</text>
-    </view>
-
-    <!-- 家长模式:添加任务按钮 -->
-    <button 
-      v-if="isParent && !batchMode" 
-      class="btn-primary add-btn" 
-      @click="showAddModal = true"
-    >
-      + 添加任务
-    </button>
-
-    <!-- 添加任务弹窗 -->
-    <view class="modal-mask" v-if="showAddModal" @click="showAddModal = false">
-      <view class="modal modal-large" @click.stop>
-        <view class="modal-title">创建任务</view>
-        
-        <!-- 任务模板选择 -->
-        <view class="form-item">
-          <view class="form-label">从模板选择(可选)</view>
-          <picker mode="selector" :range="taskTemplates" range-key="title" @change="onTemplateChange">
-            <view class="picker">{{ selectedTemplate ? selectedTemplate.title : '选择任务模板' }}</view>
-          </picker>
-        </view>
-        
-        <!-- 必填:标题 -->
-        <view class="form-item">
-          <view class="form-label">任务标题 <text class="required">*</text></view>
-          <input v-model="newTask.title" placeholder="请输入任务标题" />
-        </view>
-        
-        <!-- 内容 -->
-        <view class="form-item">
-          <view class="form-label">任务内容</view>
-          <textarea v-model="newTask.description" placeholder="请输入任务描述" class="textarea" />
-        </view>
-        
-        <!-- 类型 -->
-        <view class="form-item">
-          <view class="form-label">类型 <text class="required">*</text></view>
-          <picker mode="selector" :range="categories" @change="onCategoryChange">
-            <view class="picker">{{ newTask.category || '请选择类型' }}</view>
-          </picker>
-        </view>
-        
-        <!-- 完成方式 -->
-        <view class="form-item">
-          <view class="form-label">完成方式 <text class="required">*</text></view>
-          <checkbox-group @change="onCompleteTypeChange">
-            <label v-for="type in completeTypes" :key="type.value">
-              <checkbox :value="type.value" :checked="newTask.completeTypes.includes(type.value)" />
-              <text>{{ type.label }}</text>
-            </label>
-          </checkbox-group>
-        </view>
-        
-        <!-- 积分和时长 -->
-        <view class="form-row">
-          <view class="form-item half">
-            <view class="form-label">积分 <text class="required">*</text></view>
-            <input v-model="newTask.points" type="number" placeholder="1-10" />
-          </view>
-          <view class="form-item half">
-            <view class="form-label">时长(分钟)</view>
-            <input v-model="newTask.duration" type="number" placeholder="时长" />
-          </view>
-        </view>
-        
-        <!-- 完成孩子(多选) -->
-        <view class="form-item">
-          <view class="form-label">完成孩子 <text class="required">*</text></view>
-          <checkbox-group @change="onChildSelectChange">
-            <label v-for="child in children" :key="child.id">
-              <checkbox :value="child.id" :checked="newTask.memberIds.includes(child.id)" />
-              <text>{{ child.nickname }}</text>
-            </label>
-          </checkbox-group>
-        </view>
-        
-        <!-- 任务性质 -->
-        <view class="form-item">
-          <view class="form-label">任务性质 <text class="required">*</text></view>
-          <radio-group @change="onTaskTypeChange">
-            <label><radio value="onetime" :checked="newTask.taskType === 'onetime'" />一次性</label>
-            <label><radio value="recurring" :checked="newTask.taskType === 'recurring'" />循环任务</label>
-          </radio-group>
-        </view>
-        
-        <!-- 循环任务:频率和次数 -->
-        <view class="form-item" v-if="newTask.taskType === 'recurring'">
-          <view class="form-label">频率</view>
-          <picker mode="selector" :range="frequencies" @change="onFrequencyChange">
-            <view class="picker">{{ newTask.frequency || '请选择频率' }}</view>
-          </picker>
-        </view>
-        <view class="form-item" v-if="newTask.taskType === 'recurring'">
-          <view class="form-label">周期内最多完成次数</view>
-          <input v-model="newTask.maxFrequency" type="number" placeholder="超过次数不记积分" />
-        </view>
-        
-        <!-- 审核设置 -->
-        <view class="form-item">
-          <view class="form-label">需要审核</view>
-          <switch :checked="newTask.needReview === 1" @change="onNeedReviewChange" />
-        </view>
-        <view class="form-item" v-if="newTask.needReview === 1">
-          <view class="form-label">审核人</view>
-          <picker mode="selector" :range="reviewTypes" range-key="label" @change="onReviewTypeChange">
-            <view class="picker">{{ getReviewTypeLabel(newTask.reviewType) }}</view>
-          </picker>
-        </view>
-        
-        <!-- 非必填:时间 -->
-        <view class="form-item">
-          <view class="form-label">最早开始时间</view>
-          <picker mode="datetime" :value="newTask.earliestStart" @change="onEarliestStartChange">
-            <view class="picker">{{ newTask.earliestStart || '选择时间(不填表示不限时)' }}</view>
-          </picker>
-        </view>
-        <view class="form-item">
-          <view class="form-label">最晚结束时间</view>
-          <picker mode="datetime" :value="newTask.latestEnd" @change="onLatestEndChange">
-            <view class="picker">{{ newTask.latestEnd || '选择时间(不填表示不限时)' }}</view>
-          </picker>
-        </view>
-        
-        <view class="modal-btns">
-          <button @click="showAddModal = false">取消</button>
-          <button class="btn-primary" @click="createTask">创建任务</button>
-        </view>
-      </view>
-    </view>
-  </view>
-  </PlayfulCard>
-</template>
-
-<script>
-import { getTodayTasks, completeTask as completeTaskApi, createTask as createTaskApi, getChildren, getTodayParentTasks, completeParentTask as completeParentTaskApi, batchCompleteTasks, batchDeleteTasks } from '../../utils/api.js'
-import { parseDate } from '../../utils/format.js'
-import PlayfulCard from '@/components/PlayfulCard.vue'
-
-export default {
-  components: { PlayfulCard },
-  data() {
-    return {
-      tasks: [],
-      isParent: false,
-      currentRole: 'parent',  // 当前角色
-      currentChildId: '',
-      children: [],  // 孩子列表
-      taskTemplates: [],  // 任务模板
-      selectedTemplate: null,
-      showAddModal: false,
-      batchMode: false,
-      selectedTasks: [],
-      newTask: {
-        title: '',
-        description: '',
-        category: '',
-        points: 2,
-        duration: '',
-        memberIds: [],
-        taskType: 'onetime',
-        frequency: '',
-        maxFrequency: '',
-        completeTypes: ['checkin'],
-        needReview: 0,
-        reviewType: 'parent',
-        earliestStart: '',
-        latestEnd: ''
-      },
-      categories: ['学习类', '生活类', '运动类', '小游戏类', '其他'],
-      completeTypes: [
-        { value: 'checkin', label: '打卡' },
-        { value: 'text', label: '文本' },
-        { value: 'audio', label: '语音' },
-        { value: 'image', label: '图片' },
-        { value: 'video', label: '视频' }
-      ],
-      frequencies: ['每天', '每周', '每月'],
-      reviewTypes: [
-        { value: 'creator', label: '创建人' },
-        { value: 'parent', label: '家长' },
-        { value: 'teacher', label: '成长规划师' },
-        { value: 'ai', label: 'AI自动' }
-      ]
-    }
-  },
-  onShow() {
-    // 规划师角色跳转到家庭管理页
-    const role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
-    if (role === 'teacher') {
-      uni.redirectTo({ url: '/pages/teacher/families' })
-      return
-    }
-    // 加载任务数据
-    this.loadParentTasks()
-  },
-  methods: {
-    async loadParentTasks() {
-      try {
-        const currentUserId = uni.getStorageSync('userId')
-        const res = await getTodayParentTasks()
-        this.tasks = (res.data || []).slice(0, 5)
-      } catch (e) {
-        console.error('加载任务失败', e)
-      }
-    },
-    async loadChildren() {
-      try {
-        const res = await getChildren()
-        this.children = res.data || []
-        // 默认选中第一个孩子
-        if (this.children.length > 0) {
-          this.newTask.memberIds = [this.children[0].id]
-        }
-      } catch (e) {
-        console.error('加载孩子失败', e)
-      }
-    },
-    // 加载家长任务(分配给家长的任务)
-    async loadParentTasks() {
-      try {
-        const res = await getTodayParentTasks()
-        this.tasks = res.data || []
-      } catch (e) {
-        console.error('加载家长任务失败', e)
-      }
-    },
-    async loadTasks() {
-      try {
-        const childrenRes = await getChildren()
-        const children = childrenRes.data
-        if (children && children.length > 0) {
-          this.currentChildId = children[0].id
-          const res = await getTodayTasks(this.currentChildId)
-          this.tasks = res.data || []
-        }
-      } catch (e) {
-        console.error('加载任务失败', e)
-      }
-    },
-    // 完成任务(孩子模式)
-    async completeTask(taskId) {
-      try {
-        const res = await completeTaskApi(taskId, this.currentChildId, '')
-        uni.showToast({
-          title: `获得 ${res.data.pointsEarned} 积分`,
-          icon: 'success'
-        })
-        this.loadTasks()
-      } catch (e) {
-        console.error('完成任务失败', e)
-      }
-    },
-    // 完成任务(家长模式)
-    async completeParentTask(taskId) {
-      try {
-        const res = await completeParentTaskApi(taskId)
-        uni.showToast({
-          title: `获得 ${res.data.pointsEarned} 积分`,
-          icon: 'success'
-        })
-        this.loadParentTasks()
-      } catch (e) {
-        console.error('完成任务失败', e)
-      }
-    },
-    // 模板选择
-    onTemplateChange(e) {
-      if (e.detail.value >= 0) {
-        this.selectedTemplate = this.taskTemplates[e.detail.value]
-        this.applyTemplate(this.selectedTemplate)
-      }
-    },
-    applyTemplate(template) {
-      this.newTask.title = template.title || ''
-      this.newTask.description = template.description || ''
-      this.newTask.category = template.category || ''
-      this.newTask.points = template.points || 2
-      this.newTask.duration = template.duration || ''
-      this.newTask.completeTypes = template.completeTypes ? template.completeTypes.split(',') : ['checkin']
-      this.newTask.needReview = template.needReview || 0
-      this.newTask.reviewType = template.reviewType || 'parent'
-    },
-    // 类型选择
-    onCategoryChange(e) {
-      this.newTask.category = this.categories[e.detail.value]
-    },
-    // 完成方式选择
-    onCompleteTypeChange(e) {
-      this.newTask.completeTypes = e.detail.value
-    },
-    // 孩子多选
-    onChildSelectChange(e) {
-      this.newTask.memberIds = e.detail.value.map(id => parseInt(id))
-    },
-    // 任务性质
-    onTaskTypeChange(e) {
-      this.newTask.taskType = e.detail.value
-    },
-    // 频率选择
-    onFrequencyChange(e) {
-      const freq = this.frequencies[e.detail.value]
-      this.newTask.frequency = freq === '每天' ? 'daily' : (freq === '每周' ? 'weekly' : 'monthly')
-    },
-    // 是否需要审核
-    onNeedReviewChange(e) {
-      this.newTask.needReview = e.detail.value ? 1 : 0
-    },
-    // 审核人选择
-    onReviewTypeChange(e) {
-      this.newTask.reviewType = this.reviewTypes[e.detail.value].value
-    },
-    getReviewTypeLabel(value) {
-      const found = this.reviewTypes.find(r => r.value === value)
-      return found ? found.label : '请选择审核人'
-    },
-    // 时间选择
-    onEarliestStartChange(e) {
-      this.newTask.earliestStart = e.detail.value
-    },
-    onLatestEndChange(e) {
-      this.newTask.latestEnd = e.detail.value
-    },
-    async createTask() {
-      // 验证必填
-      if (!this.newTask.title) {
-        uni.showToast({ title: '请输入任务标题', icon: 'none' })
-        return
-      }
-      if (!this.newTask.category) {
-        uni.showToast({ title: '请选择类型', icon: 'none' })
-        return
-      }
-      if (!this.newTask.completeTypes || this.newTask.completeTypes.length === 0) {
-        uni.showToast({ title: '请选择完成方式', icon: 'none' })
-        return
-      }
-      if (!this.newTask.memberIds || this.newTask.memberIds.length === 0) {
-        uni.showToast({ title: '请选择完成孩子', icon: 'none' })
-        return
-      }
-
-      try {
-        await createTaskApi({
-          title: this.newTask.title,
-          description: this.newTask.description,
-          category: this.newTask.category,
-          points: parseInt(this.newTask.points) || 2,
-          duration: parseInt(this.newTask.duration) || null,
-          memberIds: this.newTask.memberIds,
-          taskType: this.newTask.taskType,
-          frequency: this.newTask.frequency,
-          maxFrequency: parseInt(this.newTask.maxFrequency) || null,
-          completeTypes: this.newTask.completeTypes,
-          needReview: this.newTask.needReview,
-          reviewType: this.newTask.reviewType,
-          earliestStart: this.newTask.earliestStart || null,
-          latestEnd: this.newTask.latestEnd || null
-        })
-        uni.showToast({ title: '创建成功', icon: 'success' })
-        this.showAddModal = false
-        this.resetNewTask()
-        this.loadTasks()
-      } catch (e) {
-        console.error('创建任务失败', e)
-        uni.showToast({ title: '创建失败', icon: 'none' })
-      }
-    },
-    resetNewTask() {
-      this.newTask = {
-        title: '',
-        description: '',
-        category: '',
-        points: 2,
-        duration: '',
-        memberIds: this.children.length > 0 ? [this.children[0].id] : [],
-        taskType: 'onetime',
-        frequency: '',
-        maxFrequency: '',
-        completeTypes: ['checkin'],
-        needReview: 0,
-        reviewType: 'parent',
-        earliestStart: '',
-        latestEnd: ''
-      }
-      this.selectedTemplate = null
-    },
-    formatTime(dateStr) {
-      if (!dateStr) return ''
-      const date = parseDate(dateStr)
-      if (!date) return ''
-      return `${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`
-    },
-    enterBatchMode() {
-      this.batchMode = true
-      this.selectedTasks = []
-    },
-    exitBatchMode() {
-      this.batchMode = false
-      this.selectedTasks = []
-    },
-    toggleTaskSelect(taskId) {
-      const index = this.selectedTasks.indexOf(taskId)
-      if (index > -1) {
-        this.selectedTasks.splice(index, 1)
-      } else {
-        this.selectedTasks.push(taskId)
-      }
-    },
-    toggleSelectAll() {
-      if (this.isAllSelected) {
-        this.selectedTasks = []
-      } else {
-        this.selectedTasks = this.tasks.filter(t => t.status === 'pending').map(t => t.id)
-      }
-    },
-    async batchComplete() {
-      if (this.selectedTasks.length === 0) {
-        uni.showToast({ title: '请选择任务', icon: 'none' })
-        return
-      }
-      try {
-        await batchCompleteTasks(this.selectedTasks)
-        uni.showToast({ title: '批量完成成功', icon: 'success' })
-        this.exitBatchMode()
-        this.loadParentTasks()
-      } catch (e) {
-        console.error('批量完成失败', e)
-        uni.showToast({ title: '批量完成失败', icon: 'none' })
-      }
-    },
-    async batchDelete() {
-      if (this.selectedTasks.length === 0) {
-        uni.showToast({ title: '请选择任务', icon: 'none' })
-        return
-      }
-      uni.showModal({
-        title: '确认删除',
-        content: `确定要删除选中的 ${this.selectedTasks.length} 个任务吗?`,
-        success: async (res) => {
-          if (res.confirm) {
-            try {
-              await batchDeleteTasks(this.selectedTasks)
-              uni.showToast({ title: '批量删除成功', icon: 'success' })
-              this.exitBatchMode()
-              this.loadParentTasks()
-            } catch (e) {
-              console.error('批量删除失败', e)
-              uni.showToast({ title: '批量删除失败', icon: 'none' })
-            }
-          }
-        }
-      })
-    }
-  },
-  computed: {
-    isAllSelected() {
-      if (this.selectedTasks.length === 0) return false
-      const pendingTasks = this.tasks.filter(t => t.status === 'pending')
-      return pendingTasks.length > 0 && pendingTasks.every(t => this.selectedTasks.includes(t.id))
-    }
-  }
-}
-</script>
-
-<style scoped>
-.container {
-  padding: 30rpx;
-  padding-bottom: 120rpx;
-}
-
-.header-bar {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 30rpx;
-}
-
-.header-title {
-  font-size: 32rpx;
-  font-weight: bold;
-  color: #333;
-}
-
-.btn-batch {
-  background: #F97316;
-  color: #fff;
-  font-size: 24rpx;
-  padding: 10rpx 30rpx;
-  border-radius: 30rpx;
-}
-
-.batch-bar {
-  display: flex;
-  align-items: center;
-  background: #f5f5f5;
-  padding: 20rpx 30rpx;
-  border-radius: 16rpx;
-  margin-bottom: 30rpx;
-}
-
-.batch-label {
-  font-size: 28rpx;
-  color: #333;
-  margin-left: 10rpx;
-  margin-right: auto;
-}
-
-.batch-actions {
-  display: flex;
-  gap: 20rpx;
-}
-
-.batch-btn {
-  font-size: 24rpx;
-  padding: 10rpx 30rpx;
-  border-radius: 30rpx;
-  background: #F97316;
-  color: #fff;
-}
-
-.batch-btn.complete {
-  background: #52C41A;
-}
-
-.batch-btn.delete {
-  background: #FF4D4F;
-}
-
-.batch-btn[disabled] {
-  background: #ccc;
-}
-
-.batch-exit {
-  font-size: 24rpx;
-  color: #999;
-  margin-left: 20rpx;
-}
-
-.task-item {
-  background: #fff;
-  border-radius: 16rpx;
-  padding: 30rpx;
-  margin-bottom: 20rpx;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-
-.task-item.selected {
-  background: #FFF1F0;
-  border: 2rpx solid #F97316;
-}
-
-.task-checkbox {
-  margin-right: 20rpx;
-}
-
-.task-title {
-  background: #fff;
-  border-radius: 16rpx;
-  padding: 30rpx;
-  margin-bottom: 20rpx;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-
-.task-item.completed {
-  opacity: 0.6;
-}
-
-.task-title {
-  font-size: 30rpx;
-  color: #333;
-  margin-bottom: 10rpx;
-}
-
-.task-meta {
-  display: flex;
-  gap: 20rpx;
-}
-
-.points {
-  color: #F97316;
-  font-size: 24rpx;
-}
-
-.deadline {
-  color: #999;
-  font-size: 24rpx;
-}
-
-.btn-complete {
-  background: #F97316;
-  color: #fff;
-  font-size: 26rpx;
-  padding: 10rpx 30rpx;
-  border-radius: 30rpx;
-}
-
-  .completed-badge {
-    color: #52C41A;
-    font-size: 26rpx;
-  }
-
-  .minigame-badge {
-    font-size: 28rpx;
-    margin-left: 10rpx;
-  }
-
-  .add-btn {
-  position: fixed;
-  bottom: 30rpx;
-  left: 30rpx;
-  right: 30rpx;
-}
-
-.empty {
-  text-align: center;
-  color: #999;
-  padding: 100rpx;
-}
-
-.modal-mask {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: rgba(0, 0, 0, 0.5);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.modal {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 40rpx;
-  width: 80%;
-  max-height: 80vh;
-  overflow-y: auto;
-}
-
-.modal-large {
-  width: 90%;
-}
-
-.modal-title {
-  font-size: 32rpx;
-  font-weight: bold;
-  margin-bottom: 30rpx;
-  text-align: center;
-}
-
-.form-item {
-  margin-bottom: 20rpx;
-}
-
-.form-label {
-  font-size: 28rpx;
-  color: #333;
-  margin-bottom: 10rpx;
-}
-
-.form-label .required {
-  color: #F97316;
-}
-
-.form-row {
-  display: flex;
-  gap: 20rpx;
-}
-
-.form-item.half {
-  flex: 1;
-}
-
-.form-item input, .picker, .textarea {
-  border: 1rpx solid #ddd;
-  border-radius: 10rpx;
-  padding: 20rpx;
-  font-size: 28rpx;
-  width: 100%;
-  box-sizing: border-box;
-}
-
-.textarea {
-  min-height: 100rpx;
-}
-
-.form-item checkbox-group {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 20rpx;
-}
-
-.form-item label {
-  display: flex;
-  align-items: center;
-  gap: 10rpx;
-  font-size: 28rpx;
-}
-
-.modal-btns {
-  display: flex;
-  gap: 20rpx;
-  margin-top: 30rpx;
-}
-
-.modal-btns button {
-  flex: 1;
-}
-</style>
+<template>
+  <view class="container">
+    <!-- 顶部 Tab 切换 -->
+    <view class="tab-bar">
+      <view class="tab-item" :class="{ active: activeTab === 'pending' }" @click="switchTab('pending')">
+        <text class="tab-text">待接受</text>
+        <text class="tab-badge" v-if="pendingTasks.length > 0">{{ pendingTasks.length }}</text>
+      </view>
+      <view class="tab-item" :class="{ active: activeTab === 'today' }" @click="switchTab('today')">
+        <text class="tab-text">今日任务</text>
+      </view>
+      <view class="tab-item" :class="{ active: activeTab === 'created' }" @click="switchTab('created')">
+        <text class="tab-text">我创建的</text>
+      </view>
+    </view>
+
+    <!-- 待接受任务 -->
+    <view class="task-list" v-if="activeTab === 'pending'">
+      <view class="task-item assigned" v-for="task in pendingTasks" :key="task.id">
+        <view class="task-left">
+          <view class="task-title">{{ task.title }}</view>
+          <view class="task-meta">
+            <text class="points">+{{ task.points }}分</text>
+            <text class="creator">创建人:{{ task.creatorName || '家人' }}</text>
+            <text class="deadline">截止 {{ formatTime(task.deadline) }}</text>
+          </view>
+        </view>
+        <view class="task-actions">
+          <button class="btn-accept" @click="acceptTask(task.id)">接受</button>
+          <button class="btn-reject" @click="showRejectModal(task)">拒绝</button>
+        </view>
+      </view>
+      <view class="empty" v-if="!loading && pendingTasks.length === 0">
+        <text>暂无待接受任务</text>
+      </view>
+    </view>
+
+    <!-- 今日任务 -->
+    <view class="task-list" v-if="activeTab === 'today'">
+      <view class="task-item" v-for="task in todayTasks" :key="task.id" :class="{ completed: task.status === 'completed' }">
+        <view class="task-left">
+          <view class="task-title">
+            {{ task.title }}
+            <text v-if="task.category === '小游戏类'" class="minigame-badge">🎮</text>
+          </view>
+          <view class="task-meta">
+            <text class="points">+{{ task.points }}分</text>
+            <text class="deadline">截止 {{ formatTime(task.deadline) }}</text>
+          </view>
+        </view>
+        <view class="task-right" v-if="task.status === 'pending'">
+          <button class="btn-complete" @click="completeTask(task.id)">完成</button>
+        </view>
+        <view class="completed-badge" v-else>已完成</view>
+      </view>
+      <view class="empty" v-if="!loading && todayTasks.length === 0">
+        <text>暂无今日任务</text>
+      </view>
+    </view>
+
+    <!-- 我创建的任务 -->
+    <view class="task-list" v-if="activeTab === 'created'">
+      <view class="task-item" v-for="task in createdTasks" :key="task.id">
+        <view class="task-left">
+          <view class="task-title">{{ task.title }}</view>
+          <view class="task-meta">
+            <text class="points">+{{ task.points }}分</text>
+            <text class="assignee">{{ task.executorName || '未分配' }}</text>
+            <text class="status-tag" :class="'status-' + task.status">{{ statusLabel(task.status) }}</text>
+          </view>
+        </view>
+      </view>
+      <view class="empty" v-if="!loading && createdTasks.length === 0">
+        <text>暂无创建的任务</text>
+      </view>
+    </view>
+
+    <!-- 添加任务按钮 -->
+    <button v-if="activeTab === 'today' && isParent" class="add-btn" @click="showAddModal = true">
+      + 添加任务
+    </button>
+
+    <!-- 拒绝确认弹窗 -->
+    <view class="modal-mask" v-if="showRejectModalFlag" @click="showRejectModalFlag = false">
+      <view class="modal" @click.stop>
+        <view class="modal-title">确认拒绝</view>
+        <view class="modal-content">
+          <text>拒绝后,创建人对你的信任度和亲密度将各减1分,沟通质量各加1分。</text>
+        </view>
+        <view class="modal-btns">
+          <button @click="showRejectModalFlag = false">取消</button>
+          <button class="btn-primary" @click="confirmReject">确认拒绝</button>
+        </view>
+      </view>
+    </view>
+
+    <!-- 创建任务弹窗 -->
+    <view class="modal-mask" v-if="showAddModal" @click="showAddModal = false">
+      <view class="modal modal-large" @click.stop>
+        <view class="modal-title">创建任务</view>
+
+        <view class="form-item">
+          <view class="form-label">任务标题 <text class="required">*</text></view>
+          <input v-model="newTask.title" placeholder="请输入任务标题" />
+        </view>
+
+        <view class="form-item">
+          <view class="form-label">任务内容</view>
+          <textarea v-model="newTask.description" placeholder="请输入任务描述" class="textarea" />
+        </view>
+
+        <view class="form-item">
+          <view class="form-label">类型 <text class="required">*</text></view>
+          <picker mode="selector" :range="categories" @change="onCategoryChange">
+            <view class="picker">{{ newTask.category || '请选择类型' }}</view>
+          </picker>
+        </view>
+
+        <view class="form-item">
+          <view class="form-label">积分 <text class="required">*</text></view>
+          <input v-model="newTask.points" type="number" placeholder="1-10" />
+        </view>
+
+        <view class="form-item">
+          <view class="form-label">截止时间 <text class="required">*</text></view>
+          <picker mode="datetime" :value="newTask.deadline" @change="onDeadlineChange">
+            <view class="picker">{{ newTask.deadline || '选择截止时间' }}</view>
+          </picker>
+        </view>
+
+        <view class="form-item">
+          <view class="form-label">分配给 <text class="required">*</text></view>
+          <view class="assignee-tabs">
+            <view class="assignee-tab" :class="{ active: newTask.executorType === 'child' }" @click="newTask.executorType = 'child'">
+              <text>孩子</text>
+            </view>
+            <view class="assignee-tab" :class="{ active: newTask.executorType === 'member' }" @click="newTask.executorType = 'member'">
+              <text>家庭成员</text>
+            </view>
+          </view>
+        </view>
+
+        <view class="form-item" v-if="newTask.executorType === 'child'">
+          <view class="form-label">选择孩子 <text class="required">*</text></view>
+          <checkbox-group @change="onChildSelectChange">
+            <label v-for="child in children" :key="child.id">
+              <checkbox :value="String(child.id)" :checked="newTask.memberIds.includes(String(child.id))" />
+              <text>{{ child.nickname }}</text>
+            </label>
+          </checkbox-group>
+        </view>
+
+        <view class="form-item" v-if="newTask.executorType === 'member'">
+          <view class="form-label">选择家庭成员 <text class="required">*</text></view>
+          <checkbox-group @change="onMemberSelectChange">
+            <label v-for="member in familyMembers" :key="member.id">
+              <checkbox :value="String(member.id)" :checked="newTask.memberIds.includes(String(member.id))" />
+              <text>{{ member.nickname }}</text>
+            </label>
+          </checkbox-group>
+        </view>
+
+        <view class="modal-btns">
+          <button @click="showAddModal = false">取消</button>
+          <button class="btn-primary" @click="createTask">创建</button>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getMyPendingTasks, acceptTask as acceptTaskApi, rejectTask as rejectTaskApi, getTodayTasks, completeTask as completeTaskApi, createTask as createTaskApi, getChildren, getVisibleFamilyMembers } from '../../utils/api.js'
+import { parseDate } from '../../utils/format.js'
+
+export default {
+  data() {
+    return {
+      activeTab: 'today',
+      loading: false,
+      pendingTasks: [],
+      todayTasks: [],
+      createdTasks: [],
+      isParent: false,
+      currentMemberId: null,
+      currentUserId: null,
+      children: [],
+      familyMembers: [],
+      showAddModal: false,
+      showRejectModalFlag: false,
+      rejectTaskId: null,
+      newTask: {
+        title: '',
+        description: '',
+        category: '',
+        points: 2,
+        deadline: '',
+        executorType: 'child',
+        memberIds: []
+      },
+      categories: ['学习类', '生活类', '运动类', '其他']
+    }
+  },
+  onShow() {
+    const role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
+    this.isParent = role === 'parent'
+    this.currentUserId = uni.getStorageSync('userId')
+    this.currentMemberId = uni.getStorageSync('currentChildId') || this.currentUserId
+    this.loadInitialData()
+  },
+  methods: {
+    switchTab(tab) {
+      this.activeTab = tab
+      if (tab === 'pending') this.loadPendingTasks()
+      else if (tab === 'today') this.loadTodayTasks()
+      else if (tab === 'created') this.loadCreatedTasks()
+    },
+    async loadInitialData() {
+      this.loading = true
+      try {
+        const [childrenRes, membersRes] = await Promise.all([
+          getChildren(),
+          getVisibleFamilyMembers()
+        ])
+        this.children = childrenRes.data || []
+        this.familyMembers = membersRes.data || []
+        this.loadTodayTasks()
+        this.loadPendingTasks()
+        this.loadCreatedTasks()
+      } catch (e) {
+        console.error('加载数据失败', e)
+      } finally {
+        this.loading = false
+      }
+    },
+    async loadPendingTasks() {
+      try {
+        const res = await getMyPendingTasks(this.currentMemberId)
+        this.pendingTasks = res.data || []
+      } catch (e) {
+        console.error('加载待接受任务失败', e)
+      }
+    },
+    async loadTodayTasks() {
+      try {
+        const res = await getTodayTasks(this.currentMemberId)
+        this.todayTasks = res.data || []
+      } catch (e) {
+        console.error('加载今日任务失败', e)
+      }
+    },
+    async loadCreatedTasks() {
+      try {
+        const res = await getTodayTasks(this.currentUserId)
+        this.createdTasks = res.data || []
+      } catch (e) {
+        console.error('加载创建的任务失败', e)
+      }
+    },
+    async completeTask(taskId) {
+      try {
+        const res = await completeTaskApi(taskId, this.currentMemberId, '')
+        uni.showToast({ title: `获得 ${res.data.pointsEarned} 积分`, icon: 'success' })
+        this.loadTodayTasks()
+      } catch (e) {
+        console.error('完成任务失败', e)
+      }
+    },
+    acceptTask(taskId) {
+      uni.showLoading({ title: '接受中...' })
+      acceptTaskApi(taskId, this.currentMemberId).then(res => {
+        uni.hideLoading()
+        if (res.data) {
+          uni.showToast({ title: '已接受任务', icon: 'success' })
+          this.loadPendingTasks()
+          this.loadTodayTasks()
+        } else {
+          uni.showToast({ title: '接受失败', icon: 'none' })
+        }
+      }).catch(e => {
+        uni.hideLoading()
+        uni.showToast({ title: '接受失败', icon: 'none' })
+      })
+    },
+    showRejectModal(task) {
+      this.rejectTaskId = task.id
+      this.showRejectModalFlag = true
+    },
+    confirmReject() {
+      uni.showLoading({ title: '处理中...' })
+      rejectTaskApi(this.rejectTaskId, this.currentMemberId).then(res => {
+        uni.hideLoading()
+        if (res.data) {
+          uni.showToast({ title: '已拒绝', icon: 'success' })
+          this.loadPendingTasks()
+        } else {
+          uni.showToast({ title: '操作失败', icon: 'none' })
+        }
+      }).catch(e => {
+        uni.hideLoading()
+        uni.showToast({ title: '操作失败', icon: 'none' })
+      })
+      this.showRejectModalFlag = false
+    },
+    async createTask() {
+      if (!this.newTask.title) {
+        uni.showToast({ title: '请输入任务标题', icon: 'none' })
+        return
+      }
+      if (!this.newTask.category) {
+        uni.showToast({ title: '请选择类型', icon: 'none' })
+        return
+      }
+      if (this.newTask.memberIds.length === 0) {
+        uni.showToast({ title: '请选择分配对象', icon: 'none' })
+        return
+      }
+      try {
+        await createTaskApi({
+          title: this.newTask.title,
+          description: this.newTask.description,
+          category: this.newTask.category,
+          points: parseInt(this.newTask.points) || 2,
+          deadline: this.newTask.deadline ? new Date(this.newTask.deadline).getTime() : null,
+          executorType: this.newTask.executorType,
+          executorId: this.newTask.executorType === 'member' ? Number(this.newTask.memberIds[0]) : null,
+          memberId: this.newTask.executorType === 'child' ? Number(this.newTask.memberIds[0]) : null,
+          memberIds: this.newTask.executorType === 'child' ? this.newTask.memberIds.map(id => Number(id)) : []
+        })
+        uni.showToast({ title: '创建成功', icon: 'success' })
+        this.showAddModal = false
+        this.resetNewTask()
+        this.loadCreatedTasks()
+        this.loadPendingTasks()
+      } catch (e) {
+        console.error('创建任务失败', e)
+        uni.showToast({ title: '创建失败', icon: 'none' })
+      }
+    },
+    resetNewTask() {
+      this.newTask = {
+        title: '',
+        description: '',
+        category: '',
+        points: 2,
+        deadline: '',
+        executorType: 'child',
+        memberIds: []
+      }
+    },
+    onCategoryChange(e) {
+      this.newTask.category = this.categories[e.detail.value]
+    },
+    onDeadlineChange(e) {
+      this.newTask.deadline = e.detail.value
+    },
+    onChildSelectChange(e) {
+      this.newTask.memberIds = e.detail.value
+    },
+    onMemberSelectChange(e) {
+      this.newTask.memberIds = e.detail.value
+    },
+    formatTime(dateStr) {
+      if (!dateStr) return ''
+      const date = parseDate(dateStr)
+      if (!date) return ''
+      return `${date.getMonth()+1}月${date.getDate()}日 ${String(date.getHours()).padStart(2,'0')}:${String(date.getMinutes()).padStart(2,'0')}`
+    },
+    statusLabel(status) {
+      const map = {
+        'pending': '待审核',
+        'assigned': '待接受',
+        'in_progress': '进行中',
+        'completed': '已完成',
+        'rejected': '已拒绝',
+        'overdue': '已过期',
+        'cancelled': '已取消'
+      }
+      return map[status] || status
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  padding: 30rpx;
+  padding-bottom: 120rpx;
+  background: #f5f7fa;
+  min-height: 100vh;
+}
+
+.tab-bar {
+  display: flex;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 10rpx;
+  margin-bottom: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+}
+
+.tab-item {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 20rpx 0;
+  border-radius: 12rpx;
+  position: relative;
+}
+
+.tab-item.active {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+}
+
+.tab-text {
+  font-size: 28rpx;
+  color: #666;
+  font-weight: 500;
+}
+
+.tab-item.active .tab-text {
+  color: #fff;
+}
+
+.tab-badge {
+  position: absolute;
+  top: 8rpx;
+  right: 30rpx;
+  background: #FF4D4F;
+  color: #fff;
+  font-size: 20rpx;
+  padding: 2rpx 10rpx;
+  border-radius: 20rpx;
+  min-width: 32rpx;
+  text-align: center;
+}
+
+.task-list {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+
+.task-item {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 30rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.task-item.assigned {
+  border: 2rpx solid #F97316;
+}
+
+.task-item.completed {
+  opacity: 0.6;
+}
+
+.task-left {
+  flex: 1;
+}
+
+.task-title {
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 10rpx;
+}
+
+.task-meta {
+  display: flex;
+  gap: 20rpx;
+  flex-wrap: wrap;
+}
+
+.points {
+  color: #F97316;
+  font-size: 24rpx;
+  font-weight: 500;
+}
+
+.creator, .assignee {
+  color: #999;
+  font-size: 24rpx;
+}
+
+.deadline {
+  color: #999;
+  font-size: 24rpx;
+}
+
+.status-tag {
+  font-size: 22rpx;
+  padding: 4rpx 12rpx;
+  border-radius: 12rpx;
+  background: #f0f0f0;
+  color: #666;
+}
+
+.status-assigned {
+  background: #FFF7ED;
+  color: #F97316;
+}
+
+.status-in_progress {
+  background: #ECFDF5;
+  color: #10B981;
+}
+
+.status-rejected {
+  background: #FEF2F2;
+  color: #EF4444;
+}
+
+.task-right {
+  margin-left: 20rpx;
+}
+
+.btn-complete, .btn-accept {
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: #fff;
+  font-size: 26rpx;
+  padding: 12rpx 30rpx;
+  border-radius: 30rpx;
+  border: none;
+}
+
+.btn-reject {
+  background: #fff;
+  color: #EF4444;
+  font-size: 26rpx;
+  padding: 12rpx 30rpx;
+  border-radius: 30rpx;
+  border: 2rpx solid #EF4444;
+  margin-left: 16rpx;
+}
+
+.task-actions {
+  display: flex;
+  align-items: center;
+}
+
+.completed-badge {
+  color: #52C41A;
+  font-size: 26rpx;
+}
+
+.minigame-badge {
+  font-size: 28rpx;
+  margin-left: 10rpx;
+}
+
+.empty {
+  text-align: center;
+  color: #999;
+  padding: 100rpx 0;
+  font-size: 28rpx;
+}
+
+.add-btn {
+  position: fixed;
+  bottom: 40rpx;
+  left: 30rpx;
+  right: 30rpx;
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: #fff;
+  font-size: 30rpx;
+  font-weight: bold;
+  padding: 24rpx 0;
+  border-radius: 40rpx;
+  border: none;
+  box-shadow: 0 8rpx 24rpx rgba(249,115,22,0.3);
+}
+
+.modal-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.modal {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 40rpx;
+  width: 85%;
+  max-height: 80vh;
+  overflow-y: auto;
+}
+
+.modal-large {
+  width: 90%;
+}
+
+.modal-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  margin-bottom: 30rpx;
+  text-align: center;
+  color: #333;
+}
+
+.modal-content {
+  font-size: 28rpx;
+  color: #666;
+  line-height: 1.6;
+  margin-bottom: 30rpx;
+  text-align: center;
+}
+
+.modal-btns {
+  display: flex;
+  gap: 20rpx;
+  margin-top: 30rpx;
+}
+
+.modal-btns button {
+  flex: 1;
+  height: 80rpx;
+  line-height: 80rpx;
+  border-radius: 40rpx;
+  font-size: 28rpx;
+  border: none;
+}
+
+.modal-btns button:first-child {
+  background: #f5f5f5;
+  color: #666;
+}
+
+.form-item {
+  margin-bottom: 24rpx;
+}
+
+.form-label {
+  font-size: 28rpx;
+  color: #333;
+  margin-bottom: 12rpx;
+  font-weight: 500;
+}
+
+.form-label .required {
+  color: #F97316;
+}
+
+.form-item input, .form-item .picker {
+  border: 1rpx solid #ddd;
+  border-radius: 10rpx;
+  padding: 20rpx;
+  font-size: 28rpx;
+  width: 100%;
+  box-sizing: border-box;
+  background: #fafafa;
+}
+
+.form-item .picker {
+  color: #333;
+}
+
+.textarea {
+  min-height: 120rpx;
+  width: 100%;
+  box-sizing: border-box;
+  font-size: 28rpx;
+  padding: 20rpx;
+  border: 1rpx solid #ddd;
+  border-radius: 10rpx;
+  background: #fafafa;
+}
+
+.assignee-tabs {
+  display: flex;
+  gap: 20rpx;
+}
+
+.assignee-tab {
+  flex: 1;
+  padding: 20rpx;
+  text-align: center;
+  border-radius: 10rpx;
+  border: 2rpx solid #ddd;
+  font-size: 28rpx;
+  color: #666;
+  background: #fafafa;
+}
+
+.assignee-tab.active {
+  border-color: #F97316;
+  background: #FFF7ED;
+  color: #F97316;
+  font-weight: bold;
+}
+
+.form-item checkbox-group {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20rpx;
+}
+
+.form-item label {
+  display: flex;
+  align-items: center;
+  gap: 10rpx;
+  font-size: 28rpx;
+  color: #333;
+  padding: 16rpx 24rpx;
+  border: 1rpx solid #ddd;
+  border-radius: 10rpx;
+  background: #fafafa;
+}
+</style>

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-1120fbee57fac7d9c3e6ab0aabf1605fc594bb36
+ff6590645a6e1e45fda17890b014dff36a00c732

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

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