|
@@ -1,163 +1,706 @@
|
|
|
-<template>
|
|
|
|
|
- <PlayfulCard elevation="1">
|
|
|
|
|
- <view class="container">
|
|
|
|
|
- <!-- 退出切换按钮 -->
|
|
|
|
|
- <view class="exit-switch" v-if="isSwitchedChild" @click="exitSwitch">
|
|
|
|
|
- <text>🔄 退出切换</text>
|
|
|
|
|
- </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' }">
|
|
|
|
|
- <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">
|
|
|
|
|
- <button
|
|
|
|
|
- v-if="task.status === 'pending'"
|
|
|
|
|
- class="btn-complete"
|
|
|
|
|
- @click="handleTaskClick(task)"
|
|
|
|
|
- >
|
|
|
|
|
- {{ task.category === '小游戏类' ? '开始游戏' : '完成' }}
|
|
|
|
|
- </button>
|
|
|
|
|
- <view v-else class="completed-badge">已完成</view>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <view class="empty" v-else>
|
|
|
|
|
- <text>暂无今日任务</text>
|
|
|
|
|
- </view>
|
|
|
|
|
- </view>
|
|
|
|
|
- </PlayfulCard>
|
|
|
|
|
-</template>
|
|
|
|
|
-
|
|
|
|
|
-<script>
|
|
|
|
|
-import PlayfulCard from '@/components/PlayfulCard.vue'
|
|
|
|
|
-import { getTodayTasks, completeTask as completeTaskApi, getChildren, switchBackToParent, verifyPassword } from '../../utils/api.js'
|
|
|
|
|
-
|
|
|
|
|
-export default {
|
|
|
|
|
- components: { PlayfulCard },
|
|
|
|
|
- data() {
|
|
|
|
|
- return {
|
|
|
|
|
- tasks: [],
|
|
|
|
|
- currentChildId: '',
|
|
|
|
|
- isSwitchedChild: false // 家长切换到孩子状态的标记
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- onShow() {
|
|
|
|
|
- this.isSwitchedChild = uni.getStorageSync('isSwitchedChild') || false
|
|
|
|
|
- this.loadData()
|
|
|
|
|
- },
|
|
|
|
|
- methods: {
|
|
|
|
|
- async loadData() {
|
|
|
|
|
- try {
|
|
|
|
|
- const childrenRes = await getChildren()
|
|
|
|
|
- const children = childrenRes.data || []
|
|
|
|
|
- const storedChildId = uni.getStorageSync('currentChildId')
|
|
|
|
|
- const currentChild = children.find(c => c.id == storedChildId) || children[0]
|
|
|
|
|
- if (!currentChild) {
|
|
|
|
|
- this.tasks = []
|
|
|
|
|
- this.currentChildId = ''
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- this.currentChildId = currentChild.id
|
|
|
|
|
- const res = await getTodayTasks(currentChild.id)
|
|
|
|
|
- this.tasks = res.data || []
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.error('加载孩子任务失败', e)
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- async completeChildTask(taskId) {
|
|
|
|
|
- try {
|
|
|
|
|
- const res = await completeTaskApi(taskId, this.currentChildId, '')
|
|
|
|
|
- uni.showToast({ title: `获得 ${res.data.pointsEarned} 积分`, icon: 'success' })
|
|
|
|
|
- this.loadData()
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.error('完成任务失败', e)
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- handleTaskClick(task) {
|
|
|
|
|
- // 如果是小游戏任务,跳转到对应的小游戏页面
|
|
|
|
|
- if (task.category === '小游戏类' && task.minigameCode) {
|
|
|
|
|
- const gamePages = {
|
|
|
|
|
- 'schulte': '/pages/games/schulte',
|
|
|
|
|
- '1a2b': '/pages/games/1a2b',
|
|
|
|
|
- 'sudoku': '/pages/games/sudoku'
|
|
|
|
|
- }
|
|
|
|
|
- const gamePage = gamePages[task.minigameCode]
|
|
|
|
|
- if (gamePage) {
|
|
|
|
|
- uni.navigateTo({
|
|
|
|
|
- url: `${gamePage}?taskId=${task.id}&childId=${this.currentChildId}`
|
|
|
|
|
- })
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- // 普通任务,直接完成
|
|
|
|
|
- this.completeChildTask(task.id)
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- formatTime(dateStr) {
|
|
|
|
|
- if (!dateStr) return ''
|
|
|
|
|
- const date = new Date(dateStr)
|
|
|
|
|
- return `${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`
|
|
|
|
|
- },
|
|
|
|
|
- exitSwitch() {
|
|
|
|
|
- // 只有家长切换到孩子状态时才显示退出切换功能
|
|
|
|
|
- if (!this.isSwitchedChild) return
|
|
|
|
|
-
|
|
|
|
|
- uni.showModal({
|
|
|
|
|
- title: '退出切换',
|
|
|
|
|
- content: '请输入家长密码以确认退出',
|
|
|
|
|
- editable: true,
|
|
|
|
|
- success: async (res) => {
|
|
|
|
|
- if (res.confirm && res.content) {
|
|
|
|
|
- try {
|
|
|
|
|
- const verifyResult = await verifyPassword(res.content)
|
|
|
|
|
- if (verifyResult.code === 200) {
|
|
|
|
|
- await this.switchBackToParentMode()
|
|
|
|
|
- } else {
|
|
|
|
|
- uni.showToast({ title: '密码错误', icon: 'none' })
|
|
|
|
|
- }
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- uni.showToast({ title: '密码验证失败', icon: 'none' })
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
- },
|
|
|
|
|
- async switchBackToParentMode() {
|
|
|
|
|
- try {
|
|
|
|
|
- const result = await switchBackToParent()
|
|
|
|
|
- if (result.data) {
|
|
|
|
|
- this.$store.commit('switchBackToParent')
|
|
|
|
|
- uni.showToast({ title: '已切换回家长模式', icon: 'success' })
|
|
|
|
|
- uni.reLaunch({ url: '/pages/index/parent-index' })
|
|
|
|
|
- }
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- uni.showToast({ title: '切换失败', icon: 'none' })
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-</script>
|
|
|
|
|
-
|
|
|
|
|
-<style scoped>
|
|
|
|
|
-.container { padding: 30rpx; }
|
|
|
|
|
-.exit-switch { background: #f5f5f5; padding: 20rpx; border-radius: 10rpx; margin-bottom: 20rpx; text-align: center; color: #666; font-size: 28rpx; }
|
|
|
|
|
-.task-item { 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: #FF6B6B; font-size: 24rpx; }
|
|
|
|
|
-.deadline { color: #999; font-size: 24rpx; }
|
|
|
|
|
-.btn-complete { background: #FF6B6B; 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; }
|
|
|
|
|
-.empty { text-align: center; color: #999; padding: 100rpx; }
|
|
|
|
|
-</style>
|
|
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="page-bg">
|
|
|
|
|
+ <!-- 退出切换(家长切换到孩子状态时显示) -->
|
|
|
|
|
+ <view class="exit-switch" v-if="isSwitchedChild" @click="exitSwitch">
|
|
|
|
|
+ <text class="exit-switch__icon">🔄</text>
|
|
|
|
|
+ <text class="exit-switch__text">退出切换</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 顶部标签栏 -->
|
|
|
|
|
+ <view class="tab-bar clay-card">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="tab in tabs"
|
|
|
|
|
+ :key="tab.key"
|
|
|
|
|
+ class="tab-item"
|
|
|
|
|
+ :class="{ 'tab-item--active': activeTab === tab.key }"
|
|
|
|
|
+ hover-class="tab-item--hover"
|
|
|
|
|
+ @click="switchTab(tab.key)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text class="tab-item__label">{{ tab.label }}</text>
|
|
|
|
|
+ <text class="tab-item__count" v-if="tab.count > 0">{{ tab.count }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 加载状态 -->
|
|
|
|
|
+ <BaseLoading :loading="loading" text="小助理正在准备任务..." />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 任务列表 -->
|
|
|
|
|
+ <view class="task-list" v-if="!loading && filteredTasks.length > 0">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="(task, index) in filteredTasks"
|
|
|
|
|
+ :key="task.id"
|
|
|
|
|
+ class="animate-slide-up"
|
|
|
|
|
+ :class="'animate-stagger-' + ((index % 20) + 1)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view
|
|
|
|
|
+ class="task-card clay-card"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'task-card--completed': task.status === 'completed',
|
|
|
|
|
+ 'task-card--overdue': getIsOverdue(task),
|
|
|
|
|
+ 'task-card--completing': completingTaskId === task.id
|
|
|
|
|
+ }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <!-- 完成时庆祝遮罩 -->
|
|
|
|
|
+ <view class="task-card__celebrate" v-if="completingTaskId === task.id">
|
|
|
|
|
+ <text class="task-card__celebrate-icon">🎉</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="task-card__inner">
|
|
|
|
|
+ <!-- 左侧图标 -->
|
|
|
|
|
+ <view class="task-icon">
|
|
|
|
|
+ <text class="task-icon__emoji">{{ getCategoryIcon(task.category) }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 中间内容 -->
|
|
|
|
|
+ <view class="task-content">
|
|
|
|
|
+ <view class="task-title">{{ task.title }}</view>
|
|
|
|
|
+ <view class="task-meta">
|
|
|
|
|
+ <BaseBadge variant="warning" size="xs">
|
|
|
|
|
+ <text class="points-text">+{{ task.points }} ⭐</text>
|
|
|
|
|
+ </BaseBadge>
|
|
|
|
|
+ <text
|
|
|
|
|
+ class="task-time"
|
|
|
|
|
+ v-if="task.status === 'pending' && !getIsOverdue(task)"
|
|
|
|
|
+ >
|
|
|
|
|
+ ⏱ {{ getTimeLeft(task) }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ <text
|
|
|
|
|
+ class="task-time task-time--overdue"
|
|
|
|
|
+ v-if="getIsOverdue(task)"
|
|
|
|
|
+ >
|
|
|
|
|
+ ⏰ 已超时{{ getOverdueDays(task) }}
|
|
|
|
|
+ </text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 右侧操作 -->
|
|
|
|
|
+ <view class="task-action">
|
|
|
|
|
+ <!-- 待完成任务 -->
|
|
|
|
|
+ <template v-if="task.status === 'pending' && !getIsOverdue(task)">
|
|
|
|
|
+ <PlayfulButton
|
|
|
|
|
+ variant="clay"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ :disabled="completingTaskId === task.id"
|
|
|
|
|
+ @click="handleTaskClick(task)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text v-if="task.category === '小游戏类'">🎮 开始游戏</text>
|
|
|
|
|
+ <text v-else>✅ 完成了!</text>
|
|
|
|
|
+ </PlayfulButton>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 已超时且未完成的任务,显示已超时状态 -->
|
|
|
|
|
+ <view
|
|
|
|
|
+ class="status-badge status-badge--overdue"
|
|
|
|
|
+ v-if="getIsOverdue(task) && task.status !== 'completed'"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text class="status-badge__text">⏰ 已超时</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 已完成任务 -->
|
|
|
|
|
+ <view
|
|
|
|
|
+ class="status-badge status-badge--completed"
|
|
|
|
|
+ v-if="task.status === 'completed'"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text class="status-badge__text">✅ 已完成</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 空状态 -->
|
|
|
|
|
+ <view class="empty-wrapper" v-if="!loading && filteredTasks.length === 0">
|
|
|
|
|
+ <BaseEmpty
|
|
|
|
|
+ :icon="emptyIcon"
|
|
|
|
|
+ :title="emptyTitle"
|
|
|
|
|
+ :description="emptyDescription"
|
|
|
|
|
+ >
|
|
|
|
|
+ </BaseEmpty>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import PlayfulButton from '@/components/PlayfulButton.vue'
|
|
|
|
|
+import BaseBadge from '@/components/BaseBadge.vue'
|
|
|
|
|
+import BaseEmpty from '@/components/BaseEmpty.vue'
|
|
|
|
|
+import BaseLoading from '@/components/BaseLoading.vue'
|
|
|
|
|
+import { getTodayTasks, completeTask as completeTaskApi, getChildren, switchBackToParent, verifyPassword } from '../../utils/api.js'
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ components: { PlayfulButton, BaseBadge, BaseEmpty, BaseLoading },
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ tasks: [],
|
|
|
|
|
+ currentChildId: '',
|
|
|
|
|
+ isSwitchedChild: false,
|
|
|
|
|
+ loading: false,
|
|
|
|
|
+ activeTab: 'pending',
|
|
|
|
|
+ completingTaskId: null
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ tabs() {
|
|
|
|
|
+ var pending = 0
|
|
|
|
|
+ var completed = 0
|
|
|
|
|
+ var overdue = 0
|
|
|
|
|
+ var task, i
|
|
|
|
|
+ for (i = 0; i < this.tasks.length; i++) {
|
|
|
|
|
+ task = this.tasks[i]
|
|
|
|
|
+ if (task.status === 'completed') {
|
|
|
|
|
+ completed++
|
|
|
|
|
+ } else if (this.checkIsOverdue(task)) {
|
|
|
|
|
+ overdue++
|
|
|
|
|
+ } else {
|
|
|
|
|
+ pending++
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return [
|
|
|
|
|
+ { key: 'pending', label: '📋 待完成', count: pending },
|
|
|
|
|
+ { key: 'completed', label: '✅ 已完成', count: completed },
|
|
|
|
|
+ { key: 'overdue', label: '⏰ 已超时', count: overdue }
|
|
|
|
|
+ ]
|
|
|
|
|
+ },
|
|
|
|
|
+ filteredTasks() {
|
|
|
|
|
+ var result = []
|
|
|
|
|
+ var task, i
|
|
|
|
|
+ for (i = 0; i < this.tasks.length; i++) {
|
|
|
|
|
+ task = this.tasks[i]
|
|
|
|
|
+ if (this.activeTab === 'pending') {
|
|
|
|
|
+ if (task.status === 'pending' && !this.checkIsOverdue(task)) {
|
|
|
|
|
+ result.push(task)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (this.activeTab === 'completed') {
|
|
|
|
|
+ if (task.status === 'completed') {
|
|
|
|
|
+ result.push(task)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (this.activeTab === 'overdue') {
|
|
|
|
|
+ if (this.checkIsOverdue(task)) {
|
|
|
|
|
+ result.push(task)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return result
|
|
|
|
|
+ },
|
|
|
|
|
+ emptyIcon() {
|
|
|
|
|
+ var icons = {
|
|
|
|
|
+ pending: '📝',
|
|
|
|
|
+ completed: '🎉',
|
|
|
|
|
+ overdue: '😴'
|
|
|
|
|
+ }
|
|
|
|
|
+ return icons[this.activeTab] || '📋'
|
|
|
|
|
+ },
|
|
|
|
|
+ emptyTitle() {
|
|
|
|
|
+ var titles = {
|
|
|
|
|
+ pending: '暂无待完成任务',
|
|
|
|
|
+ completed: '还没有已完成的任务',
|
|
|
|
|
+ overdue: '太棒了,没有超时任务!'
|
|
|
|
|
+ }
|
|
|
|
|
+ return titles[this.activeTab] || ''
|
|
|
|
|
+ },
|
|
|
|
|
+ emptyDescription() {
|
|
|
|
|
+ var descs = {
|
|
|
|
|
+ pending: '让家长布置一些任务吧~\n完成后可以获得积分奖励哦',
|
|
|
|
|
+ completed: '完成的任务都会出现在这里,\n继续保持好习惯!',
|
|
|
|
|
+ overdue: '继续保持良好的完成习惯,\n按时完成任务吧!'
|
|
|
|
|
+ }
|
|
|
|
|
+ return descs[this.activeTab] || ''
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ onShow() {
|
|
|
|
|
+ this.isSwitchedChild = uni.getStorageSync('isSwitchedChild') || false
|
|
|
|
|
+ this.loadData()
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ async loadData() {
|
|
|
|
|
+ var childrenRes, children, storedChildId, currentChild, i, res
|
|
|
|
|
+ this.loading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ childrenRes = await getChildren()
|
|
|
|
|
+ children = childrenRes.data || []
|
|
|
|
|
+ storedChildId = uni.getStorageSync('currentChildId')
|
|
|
|
|
+ currentChild = null
|
|
|
|
|
+ for (i = 0; i < children.length; i++) {
|
|
|
|
|
+ if (String(children[i].id) === String(storedChildId)) {
|
|
|
|
|
+ currentChild = children[i]
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!currentChild) {
|
|
|
|
|
+ currentChild = children[0]
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!currentChild) {
|
|
|
|
|
+ this.tasks = []
|
|
|
|
|
+ this.currentChildId = ''
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ this.currentChildId = currentChild.id
|
|
|
|
|
+ res = await getTodayTasks(currentChild.id)
|
|
|
|
|
+ this.tasks = res.data || []
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('加载孩子任务失败', e)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.loading = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ async completeChildTask(taskId) {
|
|
|
|
|
+ var res, points
|
|
|
|
|
+ this.completingTaskId = taskId
|
|
|
|
|
+ try {
|
|
|
|
|
+ res = await completeTaskApi(taskId, this.currentChildId, '')
|
|
|
|
|
+ points = res.data.pointsEarned
|
|
|
|
|
+ uni.showToast({ title: '获得 ' + points + ' 积分', icon: 'success' })
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.completingTaskId = null
|
|
|
|
|
+ this.loadData()
|
|
|
|
|
+ }, 600)
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ this.completingTaskId = null
|
|
|
|
|
+ console.error('完成任务失败', e)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ handleTaskClick(task) {
|
|
|
|
|
+ var gamePages, gamePage
|
|
|
|
|
+ if (this.completingTaskId === task.id) return
|
|
|
|
|
+ if (task.category === '小游戏类' && task.minigameCode) {
|
|
|
|
|
+ gamePages = {
|
|
|
|
|
+ 'schulte': '/pages/games/schulte',
|
|
|
|
|
+ '1a2b': '/pages/games/1a2b',
|
|
|
|
|
+ 'sudoku': '/pages/games/sudoku'
|
|
|
|
|
+ }
|
|
|
|
|
+ gamePage = gamePages[task.minigameCode]
|
|
|
|
|
+ if (gamePage) {
|
|
|
|
|
+ uni.navigateTo({
|
|
|
|
|
+ url: gamePage + '?taskId=' + task.id + '&childId=' + this.currentChildId
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.completeChildTask(task.id)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ getCategoryIcon(category) {
|
|
|
|
|
+ var icons = {
|
|
|
|
|
+ '学习类': '📚',
|
|
|
|
|
+ '生活类': '🏠',
|
|
|
|
|
+ '运动类': '⚽',
|
|
|
|
|
+ '小游戏类': '🎮',
|
|
|
|
|
+ '其他': '⭐'
|
|
|
|
|
+ }
|
|
|
|
|
+ return icons[category] || '📝'
|
|
|
|
|
+ },
|
|
|
|
|
+ getTimeLeft(task) {
|
|
|
|
|
+ var deadline, now, diff, hours, minutes, days
|
|
|
|
|
+ if (!task.deadline) return '无截止时间'
|
|
|
|
|
+ deadline = new Date(task.deadline)
|
|
|
|
|
+ now = new Date()
|
|
|
|
|
+ diff = deadline - now
|
|
|
|
|
+ if (diff < 0) return ''
|
|
|
|
|
+ hours = Math.floor(diff / (1000 * 60 * 60))
|
|
|
|
|
+ minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
|
|
|
|
|
+ if (hours > 24) {
|
|
|
|
|
+ days = Math.floor(hours / 24)
|
|
|
|
|
+ return '还剩 ' + days + ' 天'
|
|
|
|
|
+ }
|
|
|
|
|
+ if (hours > 0) return '还剩 ' + hours + ' 小时'
|
|
|
|
|
+ if (minutes > 0) return '还剩 ' + minutes + ' 分钟'
|
|
|
|
|
+ return '即将截止'
|
|
|
|
|
+ },
|
|
|
|
|
+ checkIsOverdue(task) {
|
|
|
|
|
+ var deadline, now
|
|
|
|
|
+ if (task.status === 'completed') return false
|
|
|
|
|
+ if (!task.deadline) return false
|
|
|
|
|
+ deadline = new Date(task.deadline)
|
|
|
|
|
+ now = new Date()
|
|
|
|
|
+ return deadline < now
|
|
|
|
|
+ },
|
|
|
|
|
+ getOverdueDays(task) {
|
|
|
|
|
+ var deadline, now, diff, days, hours
|
|
|
|
|
+ if (!task.deadline) return ''
|
|
|
|
|
+ deadline = new Date(task.deadline)
|
|
|
|
|
+ now = new Date()
|
|
|
|
|
+ diff = now - deadline
|
|
|
|
|
+ days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
|
|
|
|
+ if (days > 0) return ' ' + days + '天'
|
|
|
|
|
+ hours = Math.floor(diff / (1000 * 60 * 60))
|
|
|
|
|
+ if (hours > 0) return ' ' + hours + '小时'
|
|
|
|
|
+ return ''
|
|
|
|
|
+ },
|
|
|
|
|
+ switchTab(tabKey) {
|
|
|
|
|
+ this.activeTab = tabKey
|
|
|
|
|
+ },
|
|
|
|
|
+ exitSwitch() {
|
|
|
|
|
+ var self = this
|
|
|
|
|
+ if (!self.isSwitchedChild) return
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '退出切换',
|
|
|
|
|
+ content: '请输入家长密码以确认退出',
|
|
|
|
|
+ editable: true,
|
|
|
|
|
+ success: function(res) {
|
|
|
|
|
+ if (res.confirm && res.content) {
|
|
|
|
|
+ verifyPassword(res.content).then(function(verifyResult) {
|
|
|
|
|
+ if (verifyResult.code === 200) {
|
|
|
|
|
+ self.switchBackToParentMode()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ uni.showToast({ title: '密码错误', icon: 'none' })
|
|
|
|
|
+ }
|
|
|
|
|
+ }).catch(function() {
|
|
|
|
|
+ uni.showToast({ title: '密码验证失败', icon: 'none' })
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ async switchBackToParentMode() {
|
|
|
|
|
+ var result
|
|
|
|
|
+ try {
|
|
|
|
|
+ result = await switchBackToParent()
|
|
|
|
|
+ if (result.data) {
|
|
|
|
|
+ this.$store.commit('switchBackToParent')
|
|
|
|
|
+ uni.showToast({ title: '已切换回家长模式', icon: 'success' })
|
|
|
|
|
+ uni.reLaunch({ url: '/pages/index/parent-index' })
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (_) {
|
|
|
|
|
+ uni.showToast({ title: '切换失败', icon: 'none' })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+/* =============================================
|
|
|
|
|
+ 页面基础
|
|
|
|
|
+ ============================================= */
|
|
|
|
|
+.page-bg {
|
|
|
|
|
+ min-height: 100vh;
|
|
|
|
|
+ background: var(--bg, #FFF7ED);
|
|
|
|
|
+ padding-bottom: 40rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* =============================================
|
|
|
|
|
+ 退出切换横幅
|
|
|
|
|
+ ============================================= */
|
|
|
|
|
+.exit-switch {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ gap: 12rpx;
|
|
|
|
|
+ padding: 24rpx;
|
|
|
|
|
+ margin: 24rpx 32rpx 0;
|
|
|
|
|
+ background: var(--surface, #FFFFFF);
|
|
|
|
|
+ border: 2rpx solid var(--border, #FED7AA);
|
|
|
|
|
+ border-radius: var(--radius-lg, 32rpx);
|
|
|
|
|
+ box-shadow: var(--shadow-sm);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.exit-switch:active {
|
|
|
|
|
+ opacity: 0.7;
|
|
|
|
|
+ transform: scale(0.98);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.exit-switch__icon {
|
|
|
|
|
+ font-size: 32rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.exit-switch__text {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ color: var(--text-secondary, #6B5A4A);
|
|
|
|
|
+ font-weight: var(--font-weight-medium, 500);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* =============================================
|
|
|
|
|
+ 顶部标签栏
|
|
|
|
|
+ ============================================= */
|
|
|
|
|
+.tab-bar {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: row;
|
|
|
|
|
+ margin: 24rpx 32rpx 0;
|
|
|
|
|
+ padding: 8rpx;
|
|
|
|
|
+ border-radius: var(--radius-xl, 40rpx);
|
|
|
|
|
+ background: var(--surface, #FFFFFF);
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tab-item {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: row;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ gap: 8rpx;
|
|
|
|
|
+ padding: 20rpx 12rpx;
|
|
|
|
|
+ border-radius: var(--radius-lg, 32rpx);
|
|
|
|
|
+ transition: all var(--transition-base, 0.25s ease);
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tab-item--hover {
|
|
|
|
|
+ background: rgba(249, 115, 22, 0.06);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tab-item--active {
|
|
|
|
|
+ background: linear-gradient(145deg, var(--color-primary-light, #FB923C), var(--color-primary, #F97316));
|
|
|
|
|
+ box-shadow: inset -2rpx -2rpx 8rpx rgba(255, 255, 255, 0.3),
|
|
|
|
|
+ 4rpx 4rpx 12rpx rgba(249, 115, 22, 0.2);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tab-item__label {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ font-weight: var(--font-weight-medium, 500);
|
|
|
|
|
+ color: var(--text-secondary, #6B5A4A);
|
|
|
|
|
+ transition: color var(--transition-fast, 0.15s ease);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tab-item--active .tab-item__label {
|
|
|
|
|
+ color: var(--text-color-inverse, #FFFFFF);
|
|
|
|
|
+ font-weight: var(--font-weight-bold, 600);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tab-item__count {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ min-width: 36rpx;
|
|
|
|
|
+ height: 36rpx;
|
|
|
|
|
+ padding: 0 8rpx;
|
|
|
|
|
+ border-radius: 999rpx;
|
|
|
|
|
+ background: rgba(249, 115, 22, 0.12);
|
|
|
|
|
+ color: var(--color-primary, #F97316);
|
|
|
|
|
+ font-size: 20rpx;
|
|
|
|
|
+ font-weight: var(--font-weight-bold, 600);
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tab-item--active .tab-item__count {
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.25);
|
|
|
|
|
+ color: #FFFFFF;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* =============================================
|
|
|
|
|
+ 任务列表
|
|
|
|
|
+ ============================================= */
|
|
|
|
|
+.task-list {
|
|
|
|
|
+ padding: 24rpx 32rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* =============================================
|
|
|
|
|
+ 任务卡片(Claymorphism 风格)
|
|
|
|
|
+ ============================================= */
|
|
|
|
|
+.task-card {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ margin-bottom: 24rpx;
|
|
|
|
|
+ transition: transform var(--transition-fast, 0.15s ease),
|
|
|
|
|
+ opacity var(--transition-base, 0.25s ease);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-card:active {
|
|
|
|
|
+ transform: scale(0.98);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* --- 已完成状态 --- */
|
|
|
|
|
+.task-card--completed {
|
|
|
|
|
+ opacity: 0.7;
|
|
|
|
|
+ border-color: var(--color-success, #22C55E);
|
|
|
|
|
+ background: linear-gradient(145deg, #F0FDF4, #ECFDF5);
|
|
|
|
|
+ box-shadow: inset -2rpx -2rpx 6rpx rgba(34, 197, 94, 0.05),
|
|
|
|
|
+ 3rpx 3rpx 10rpx rgba(34, 197, 94, 0.08);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* --- 超时状态 --- */
|
|
|
|
|
+.task-card--overdue {
|
|
|
|
|
+ border-color: var(--color-error, #EF4444);
|
|
|
|
|
+ background: linear-gradient(145deg, #FEF2F2, #FEE2E2);
|
|
|
|
|
+ box-shadow: inset -2rpx -2rpx 6rpx rgba(239, 68, 68, 0.05),
|
|
|
|
|
+ 3rpx 3rpx 10rpx rgba(239, 68, 68, 0.08);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* --- 完成中动画 --- */
|
|
|
|
|
+.task-card--completing {
|
|
|
|
|
+ animation: cardComplete 0.6s ease forwards;
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-card__celebrate {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ inset: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.6);
|
|
|
|
|
+ border-radius: var(--radius-md, 20rpx);
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-card__celebrate-icon {
|
|
|
|
|
+ font-size: 80rpx;
|
|
|
|
|
+ animation: celebrateBounce 0.6s ease both;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* --- 内部布局 --- */
|
|
|
|
|
+.task-card__inner {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: row;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* =============================================
|
|
|
|
|
+ 任务图标
|
|
|
|
|
+ ============================================= */
|
|
|
|
|
+.task-icon {
|
|
|
|
|
+ width: 88rpx;
|
|
|
|
|
+ height: 88rpx;
|
|
|
|
|
+ border-radius: var(--radius-md, 20rpx);
|
|
|
|
|
+ background: linear-gradient(145deg, #FFF7ED, #FFEDD5);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ border: 2rpx solid var(--border-light, #FFEDD5);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-card--completed .task-icon {
|
|
|
|
|
+ background: linear-gradient(145deg, #F0FDF4, #DCFCE7);
|
|
|
|
|
+ border-color: #BBF7D0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-card--overdue .task-icon {
|
|
|
|
|
+ background: linear-gradient(145deg, #FEF2F2, #FECACA);
|
|
|
|
|
+ border-color: #FECACA;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-icon__emoji {
|
|
|
|
|
+ font-size: 40rpx;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* =============================================
|
|
|
|
|
+ 任务内容区
|
|
|
|
|
+ ============================================= */
|
|
|
|
|
+.task-content {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-title {
|
|
|
|
|
+ font-size: 32rpx;
|
|
|
|
|
+ font-weight: var(--font-weight-bold, 600);
|
|
|
|
|
+ color: var(--text, #3D2E1E);
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+ margin-bottom: 10rpx;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-card--completed .task-title {
|
|
|
|
|
+ text-decoration: line-through;
|
|
|
|
|
+ color: var(--text-secondary, #6B5A4A);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-meta {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: row;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 16rpx;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.points-text {
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-time {
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: var(--text-secondary, #6B5A4A);
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.task-time--overdue {
|
|
|
|
|
+ color: var(--color-error, #EF4444);
|
|
|
|
|
+ font-weight: var(--font-weight-medium, 500);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* =============================================
|
|
|
|
|
+ 右侧操作区
|
|
|
|
|
+ ============================================= */
|
|
|
|
|
+.task-action {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ margin-left: 8rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* =============================================
|
|
|
|
|
+ 状态徽章
|
|
|
|
|
+ ============================================= */
|
|
|
|
|
+.status-badge {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding: 16rpx 24rpx;
|
|
|
|
|
+ border-radius: 999rpx;
|
|
|
|
|
+ font-weight: var(--font-weight-bold, 600);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.status-badge--completed {
|
|
|
|
|
+ background: rgba(34, 197, 94, 0.12);
|
|
|
|
|
+ color: var(--color-success, #22C55E);
|
|
|
|
|
+ border: 2rpx solid rgba(34, 197, 94, 0.2);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.status-badge--overdue {
|
|
|
|
|
+ background: rgba(239, 68, 68, 0.12);
|
|
|
|
|
+ color: var(--color-error, #EF4444);
|
|
|
|
|
+ border: 2rpx solid rgba(239, 68, 68, 0.2);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.status-badge__text {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* =============================================
|
|
|
|
|
+ 空状态
|
|
|
|
|
+ ============================================= */
|
|
|
|
|
+.empty-wrapper {
|
|
|
|
|
+ padding-top: 60rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* =============================================
|
|
|
|
|
+ 关键帧动画
|
|
|
|
|
+ ============================================= */
|
|
|
|
|
+@keyframes cardComplete {
|
|
|
|
|
+ 0% {
|
|
|
|
|
+ transform: scale(1);
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ 30% {
|
|
|
|
|
+ transform: scale(1.05);
|
|
|
|
|
+ opacity: 0.9;
|
|
|
|
|
+ }
|
|
|
|
|
+ 100% {
|
|
|
|
|
+ transform: scale(0.95);
|
|
|
|
|
+ opacity: 0.6;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes celebrateBounce {
|
|
|
|
|
+ 0% {
|
|
|
|
|
+ transform: scale(0) rotate(-180deg);
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ 50% {
|
|
|
|
|
+ transform: scale(1.3) rotate(10deg);
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ 70% {
|
|
|
|
|
+ transform: scale(0.9) rotate(-5deg);
|
|
|
|
|
+ }
|
|
|
|
|
+ 100% {
|
|
|
|
|
+ transform: scale(1) rotate(0deg);
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|