// pages/tasks/index.js import request from '../../utils/request'; import { requestSubscription } from '../../utils/subscribe'; Page({ data: { tasks: [], groupedTasks: [], progress: 0, completingTask: null, remark: '', remarkPlaceholder: '', scrollTop: 0, // Reward Modals showLevelUpModal: false, levelUpData: null, showBadgeModal: false, badgeData: null }, onLoad() { this.fetchTodayTasks(); this._popupQueue = []; }, onShow() { if (typeof this.getTabBar === 'function' && this.getTabBar()) { this.getTabBar().setData({ selected: 1 }); } // Refresh on show this.fetchTodayTasks(); }, // ... (fetchTodayTasks, processTaskData Omitted) ... // Reward Queue Processing processPopupQueue() { if (this._popupQueue.length === 0) return; const next = this._popupQueue[0]; if (next.type === 'level') { this.setData({ levelUpData: next.data, showLevelUpModal: true }); } else if (next.type === 'badge') { this.setData({ badgeData: next.data, showBadgeModal: true }); } }, closeLevelUpModal() { this.setData({ showLevelUpModal: false }); this._popupQueue.shift(); setTimeout(() => { this.processPopupQueue(); }, 300); }, closeBadgeModal() { this.setData({ showBadgeModal: false }); this._popupQueue.shift(); setTimeout(() => { this.processPopupQueue(); }, 300); }, fetchTodayTasks() { request.get('/plant/todayTask').then(res => { // Check if res is array (list of PlantTaskVO) const list = Array.isArray(res) ? res : (res.list || []); this.processTaskData(list); }).catch(err => { console.error('Fetch tasks failed', err); wx.stopPullDownRefresh(); }); }, processTaskData(plantTaskVOList) { let totalPacketTasks = 0; let completedPacketTasks = 0; const groups = plantTaskVOList.map(vo => { const plant = vo.MyPlant || vo.myPlant; if (!plant) return null; // Parse Image let imageUrl = ''; if (plant.imgList && plant.imgList.length > 0) { imageUrl = plant.imgList[0].url || ''; } const plantGroup = { plantName: plant.name, plantImage: imageUrl, tasks: [], // Placeholder, will fill below hasOverdue: vo.hasExpired }; const rawTasks = vo.tasks || []; // 1. Update Global Counters rawTasks.forEach(t => { totalPacketTasks++; if (t.status == 2 || t.status == 3) { completedPacketTasks++; } }); // 2. Filter and Map Tasks for Display const displayTasks = rawTasks .filter(t => t.status == 1 || t.status == 2) .map(t => { // Status: 1 Pending, 2 Done const isCompleted = t.status == 2; // Parse Icon let taskIcon = null; if (t.icon && t.icon.startsWith('{')) { try { taskIcon = JSON.parse(t.icon); } catch (e) { } } // Check overdue (only for pending tasks) let isOverdue = false; let overdueDays = 0; if (!isCompleted && t.dueDate) { const due = new Date(t.dueDate); const today = new Date(); today.setHours(0, 0, 0, 0); if (due < today) { isOverdue = true; const diffTime = Math.abs(today - due); overdueDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } } return { id: t.id, taskType: t.name, taskIcon: taskIcon, isOverdue: isOverdue, overdueDays: overdueDays, plantName: plant.name, isCompleted: isCompleted, original: t }; }); // Sorting Removed: Tasks stay in original order // displayTasks.sort((a, b) => { // if (a.isCompleted === b.isCompleted) return 0; // return a.isCompleted ? 1 : -1; // }); plantGroup.tasks = displayTasks; if (plantGroup.tasks.length === 0) return null; return plantGroup; }).filter(g => g !== null); // Calculate Progress let progress = 0; if (totalPacketTasks > 0) { progress = Math.round((completedPacketTasks / totalPacketTasks) * 100); } // Sorting Groups: Overdue first groups.sort((a, b) => { if (a.hasOverdue && !b.hasOverdue) return -1; if (!a.hasOverdue && b.hasOverdue) return 1; return 0; }); this.setData({ groupedTasks: groups, progress, tasks: groups }); wx.stopPullDownRefresh(); }, handleTaskClick(e) { const task = e.currentTarget.dataset.task; if (task.isCompleted) return; // Compute placeholder based on task type or name let placeholder = '添加备注 (可选)...'; // Get name from taskIcon first, then name. // name from backend is usually the name (e.g. "浇水"). const typeRaw = (task.taskIcon ? task.taskIcon.name : task.name) || ''; const typeStr = typeRaw.toLowerCase(); if (typeStr.includes('浇水')) { placeholder = '例如:浇了 200ml 水...'; } else if (typeStr.includes('施肥')) { placeholder = '例如:施了通用液肥...'; } else if (typeStr.includes('修剪')) { placeholder = '例如:修剪了枯叶和黄叶...'; } else if (typeStr.includes('换盆')) { placeholder = '例如:更换了陶盆,加了底肥...'; } else if (typeStr.includes('换土')) { placeholder = '例如:更换了新配方土...'; } else if (typeStr.includes('喷雾')) { placeholder = '例如:给叶面喷了水...'; } else if (typeStr.includes('用药') || typeStr.includes('虫')) { placeholder = '例如:喷洒了杀虫剂...'; } else if (typeStr.includes('晒太阳') || typeStr.includes('日照')) { placeholder = '例如:晒了 2 小时太阳...'; } this.setData({ completingTask: task, remark: '', remarkPlaceholder: placeholder }); }, onPopupVisibleChange(e) { const visible = e.detail ? e.detail.visible : e.currentTarget.dataset.visible; if (!visible) { this.setData({ completingTask: null }); } }, onRemarkInput(e) { this.setData({ remark: e.detail.value }); }, handleConfirmComplete() { if (!this.data.completingTask) return; const taskId = this.data.completingTask.id; const remark = this.data.remark || ''; // Attempt to subscribe (silent mode avoids error popups if disabled) // This encourages "Always Allow" behavior for seamless experience requestSubscription(undefined, true).then((subResult) => { // Debug failure feedback if (!subResult.success) { if (subResult.isMainSwitchOff) { wx.showToast({ title: '提醒总开关已关闭', icon: 'none' }); } else { console.log('[Task] Subscription quota not increased:', subResult.res || subResult.error); } } wx.showLoading({ title: '提交中...', mask: true }); request.post('/plant/completeTask', { taskId: taskId, remark: remark }).then(res => { wx.hideLoading(); // Handle Rewards const queue = []; // Check if res has level up or badge data // Note: res is already data.data from request.js if (res && res.isLevelUp && res.currentLevel) { queue.push({ type: 'level', data: res.currentLevel }); } // Check for Badge using IsGetBadge flag (allowing for casing variance) if (res && (res.IsGetBadge === true || res.isGetBadge === true) && res.newBadge) { queue.push({ type: 'badge', data: res.newBadge }); } this._popupQueue = queue; // Optimistic UI Update Logic const groups = this.data.groupedTasks; let updated = false; // Need to deep clone possibly, but here we modify and set back for (let g of groups) { // g is an object. groupedTasks is array of objects. if (g.tasks) { const t = g.tasks.find(x => x && x.id === taskId); if (t) { t.isCompleted = true; t.isOverdue = false; updated = true; break; } } } // Trigger Animation and Close Modal this.setData({ completingTask: null, remark: '', groupedTasks: groups, // Update UI tasks: groups, showSunshine: true }); // Hide Animation after duration and Start showing modals setTimeout(() => { this.setData({ showSunshine: false }); // Show rewards after sunshine animation if (this._popupQueue.length > 0) { this.processPopupQueue(); } }, 1000); // 1.0s delay usually covers animation // Sync with backend silently this.fetchTodayTasks(); }).catch(err => { wx.hideLoading(); console.error('Complete task failed', err); wx.showToast({ title: '操作失败', icon: 'none' }); }); }); }, gotoGarden() { wx.switchTab({ url: '/pages/garden/index' }); }, onTabItemTap() { this.setData({ scrollTop: Math.random() * 0.01 }); } })