|
|
@@ -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>
|