feat: 任务和社区页面

This commit is contained in:
Blizzard
2026-02-06 17:27:35 +08:00
parent d42471e1d5
commit b800ea03b5
30 changed files with 1777 additions and 551 deletions
+158 -39
View File
@@ -1,5 +1,5 @@
// pages/tasks/index.js
import { MOCK_TASKS_DATA } from '../../utils/mockData';
import request from '../../utils/request';
Page({
data: {
@@ -11,8 +11,7 @@ Page({
},
onLoad() {
this.setData({ tasks: MOCK_TASKS_DATA });
this.processTasks();
this.fetchTodayTasks();
},
onShow() {
@@ -20,49 +19,136 @@ Page({
this.getTabBar()) {
this.getTabBar().setData({ selected: 1 });
}
// Refresh on show
this.fetchTodayTasks();
},
processTasks() {
const { tasks } = this.data;
// Calculate Progress (Simulated)
const completedCount = 3; // Mocked existing completed
const initialTotal = MOCK_TASKS_DATA.length + completedCount;
const currentCompleted = completedCount + (MOCK_TASKS_DATA.length - tasks.length);
const progress = Math.min(100, Math.round((currentCompleted / initialTotal) * 100));
// Grouping
const groups = {};
tasks.forEach(task => {
if (!groups[task.plantName]) {
groups[task.plantName] = {
plantName: task.plantName,
plantImage: task.plantImage,
tasks: [],
hasOverdue: false
};
}
groups[task.plantName].tasks.push(task);
if (task.isOverdue) groups[task.plantName].hasOverdue = true;
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();
});
},
// Sorting
const sortedGroups = Object.values(groups).sort((a, b) => {
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) {
let url = plant.imgList[0].url;
if (url && !url.startsWith('http') && !url.startsWith('/') && !url.startsWith('wxfile')) {
imageUrl = '/assets/' + url;
} else {
imageUrl = 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: sortedGroups,
progress
groupedTasks: groups,
progress,
tasks: groups
});
wx.stopPullDownRefresh();
},
handleTaskClick(e) {
// e.currentTarget.dataset.task might differ if TDesign changes event structure,
// but 'data-task' on t-button works similarly in Miniprogram.
const task = e.currentTarget.dataset.task;
if (task.isCompleted) return;
this.setData({
completingTask: task,
remark: ''
@@ -70,7 +156,6 @@ Page({
},
onPopupVisibleChange(e) {
// Handle both TDesign event and manual close tap
const visible = e.detail ? e.detail.visible : e.currentTarget.dataset.visible;
if (!visible) {
this.setData({ completingTask: null });
@@ -85,15 +170,49 @@ Page({
if (!this.data.completingTask) return;
const taskId = this.data.completingTask.id;
// Filter out the completed task
const newTasks = this.data.tasks.filter(t => t.id !== taskId);
const remark = this.data.remark || '';
this.setData({
tasks: newTasks,
completingTask: null
}, () => {
this.processTasks();
wx.showLoading({ title: '提交中...' });
request.post('/plant/completeTask', {
taskId: taskId,
remark: remark
}).then(() => {
wx.hideLoading();
wx.showToast({ title: '已完成', icon: 'success' });
// Optimistic UI Update
const groups = this.data.groupedTasks;
let updated = false;
for (let g of groups) {
const t = g.tasks.find(x => x.id === taskId);
if (t) {
t.isCompleted = true;
t.isOverdue = false;
// Do NOT sort. Just update status.
updated = true;
break;
}
}
if (updated) {
this.setData({ groupedTasks: groups, tasks: groups, completingTask: null, remark: '' });
} else {
this.setData({ completingTask: null, remark: '' });
}
// Sync with backend
this.fetchTodayTasks();
}).catch(err => {
wx.hideLoading();
console.error('Complete task failed', err);
wx.showToast({ title: '操作失败', icon: 'none' });
});
},
gotoGarden() {
wx.switchTab({
url: '/pages/garden/index'
});
}
})
+29 -5
View File
@@ -19,8 +19,29 @@
<view class="tasks-container">
<text class="section-title">今日待办</text>
<view wx:if="{{tasks.length === 0}}" class="empty-state">
<text>太棒了!所有任务都已完成 🎉</text>
<view wx:if="{{tasks.length === 0}}" class="empty-state-custom">
<view class="empty-scene">
<view class="sun-box">
<view class="sun-core"></view>
<view class="sun-rays"></view>
</view>
<view class="cloud cloud-1"></view>
<view class="cloud cloud-2"></view>
<!-- Replaced Image with Rich Icon Group -->
<view class="empty-icon-group plant-breathing">
<view class="icon-bg-circle"></view>
<text class="emoji-icon">🌿</text>
<view class="sparkle s1"></view>
<view class="sparkle s2"></view>
</view>
</view>
<view class="empty-text">
<text>今天很惬意</text>
<text class="empty-sub">植物们正在享受阳光 SPA ☀️</text>
</view>
<t-button theme="primary" size="large" shape="round" bind:tap="gotoGarden" class="empty-btn">去看看花园</t-button>
</view>
<scroll-view wx:else scroll-y class="task-list" enhanced show-scrollbar="{{false}}">
@@ -28,7 +49,7 @@
<view class="card-header-row">
<view class="plant-info-brief">
<view class="plant-thumb-small">
<image wx:if="{{item.plantImage}}" src="/assets/{{item.plantImage}}" mode="aspectFill"></image>
<image wx:if="{{item.plantImage}}" src="{{item.plantImage}}" mode="aspectFill"></image>
<view wx:else class="thumb-placeholder">{{item.plantName[0]}}</view>
</view>
<text class="plant-name-title">{{item.plantName}}</text>
@@ -50,8 +71,11 @@
<text wx:if="{{task.isOverdue}}" class="task-overdue-text">{{task.overdueDays}}天前</text>
</view>
</view>
<view class="mini-check-btn {{task.isOverdue ? 'btn-urgent' : ''}}" bindtap="handleTaskClick" data-task="{{task}}">
<t-icon name="check" size="32rpx" color="{{task.isOverdue ? '#EF5350' : '#E0E0E0'}}" />
<view class="mini-check-btn {{task.isOverdue ? 'btn-urgent' : ''}}"
style="{{task.isCompleted ? 'background-color: #4CAF50; border-color: #4CAF50;' : ''}}"
bindtap="handleTaskClick"
data-task="{{task}}">
<t-icon wx:if="{{task.isCompleted}}" name="check" size="28rpx" color="#FFF" />
</view>
</view>
</view>
+259 -38
View File
@@ -279,18 +279,32 @@
}
.plant-task-card {
background: white;
border-radius: 40rpx;
background: linear-gradient(160deg, #FFFFFF 0%, #F5F9F6 100%);
border-radius: 32rpx;
padding: 32rpx;
margin-bottom: 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.03);
border: 2rpx solid transparent;
box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.04);
border: 1rpx solid rgba(0,0,0,0.02);
transition: all 0.2s;
position: relative;
overflow: hidden;
}
/* Decorator for card */
.plant-task-card::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 120rpx;
height: 120rpx;
background: radial-gradient(circle at top right, #E8F5E9 0%, transparent 70%);
opacity: 0.6;
}
.plant-task-card.has-overdue {
border-color: rgba(239, 83, 80, 0.1);
background: #FFF8F8;
border: 2rpx solid rgba(239, 83, 80, 0.2);
background: linear-gradient(160deg, #FFEBEE 0%, #FFF 100%);
}
.card-header-row {
@@ -299,7 +313,7 @@
align-items: center;
margin-bottom: 32rpx;
padding-bottom: 24rpx;
border-bottom: 2rpx solid rgba(0, 0, 0, 0.03);
border-bottom: 2rpx dashed rgba(0, 0, 0, 0.05); /* Dashed line for ticket feel */
}
.plant-info-brief {
@@ -309,11 +323,13 @@
}
.plant-thumb-small {
width: 80rpx;
height: 80rpx;
width: 88rpx;
height: 88rpx;
border-radius: 24rpx;
overflow: hidden;
background: #f0f0f0;
box-shadow: 0 4rpx 10rpx rgba(0,0,0,0.05);
border: 2rpx solid #FFF;
}
.plant-thumb-small image {
@@ -324,40 +340,46 @@
.thumb-placeholder {
width: 100%;
height: 100%;
background: #E8F5E9;
color: #558B2F;
background: #C8E6C9;
color: #2E7D32;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 32rpx;
font-size: 36rpx;
}
.plant-name-title {
font-size: 30rpx;
font-weight: 700;
color: #263238;
font-size: 32rpx;
font-weight: 800;
color: #1B5E20; /* Dark Green theme */
letter-spacing: 1rpx;
}
.group-overdue-badge {
font-size: 20rpx;
color: #EF5350;
background: rgba(239, 83, 80, 0.1);
padding: 4rpx 16rpx;
color: #C62828;
background: #FFCDD2;
padding: 6rpx 18rpx;
border-radius: 20rpx;
font-weight: 600;
font-weight: 700;
}
.plant-tasks-list {
display: flex;
flex-direction: column;
gap: 24rpx;
gap: 28rpx;
}
.mini-task-row {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255,255,255,0.6);
padding: 16rpx;
border-radius: 20rpx;
width: 100%;
box-sizing: border-box; /* Important for padding */
}
.mini-task-left {
@@ -367,14 +389,15 @@
}
.task-type-icon-circle {
width: 64rpx;
height: 64rpx;
background: #F8F9FA;
border-radius: 50%;
width: 72rpx;
height: 72rpx;
background: #FFF;
border-radius: 24rpx; /* Softer square */
display: flex;
align-items: center;
justify-content: center;
color: #90A4AE;
box-shadow: 0 4rpx 8rpx rgba(0,0,0,0.03);
}
.task-icon {
@@ -385,45 +408,58 @@
.mini-task-text {
display: flex;
flex-direction: column;
gap: 4rpx;
gap: 6rpx;
}
.task-label {
font-size: 28rpx;
font-weight: 500;
font-size: 30rpx;
font-weight: 600;
color: #37474F;
}
.task-overdue-text {
font-size: 22rpx;
color: #EF5350;
font-weight: 600;
color: #D32F2F;
font-weight: 700;
}
/* Checkbox Styling - Square */
.mini-check-btn {
width: 64rpx;
height: 64rpx;
border-radius: 20rpx;
border: 3rpx solid #E0E0E0;
width: 56rpx;
height: 56rpx;
border-radius: 12rpx; /* Square with slight radius */
border: 4rpx solid #CFD8DC; /* Thick border */
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
box-sizing: border-box;
background: #FFF;
}
.mini-check-btn:active {
background: rgba(85, 139, 47, 0.05);
border-color: #558B2F;
background: #F5F5F5;
border-color: #90A4AE;
}
/* Checked State */
.mini-check-btn.btn-checked {
background: #4CAF50 !important;
border-color: #4CAF50 !important;
}
/* Urgent/Overdue State */
.mini-check-btn.btn-urgent {
border-color: rgba(239, 83, 80, 0.3);
border-color: #EF5350;
background: #FFEBEE;
}
.mini-check-btn.btn-urgent.btn-checked {
background: #EF5350; /* If overdue but checked (rare case?) */
}
.mini-check-btn.btn-urgent:active {
background: rgba(239, 83, 80, 0.05);
border-color: #EF5350;
background: #FFCDD2;
}
/* Modal Specifics */
@@ -547,3 +583,188 @@
.confirm-complete-btn:active {
transform: scale(0.98);
}
/* Empty State Custom */
.empty-state-custom {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 0;
gap: 40rpx;
position: relative;
overflow: hidden;
}
.empty-scene {
position: relative;
width: 100%;
height: 320rpx;
display: flex;
justify-content: center;
align-items: center;
}
/* Sun Animation */
.sun-box {
position: absolute;
top: 10rpx;
right: 120rpx;
width: 120rpx;
height: 120rpx;
animation: sunSpin 20s linear infinite;
z-index: 1;
}
.sun-core {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60rpx;
height: 60rpx;
background: radial-gradient(circle, #FFEB3B 0%, #FBC02D 100%);
border-radius: 50%;
box-shadow: 0 0 20rpx rgba(253, 216, 53, 0.6);
}
.sun-rays {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
border: 4rpx dashed #FDD835;
opacity: 0.6;
}
@keyframes sunSpin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Clouds */
.cloud {
position: absolute;
background: #E1F5FE;
border-radius: 20rpx;
opacity: 0.8;
z-index: 1;
}
.cloud-1 {
width: 100rpx;
height: 36rpx;
top: 40rpx;
left: 10%;
animation: cloudFloat 15s linear infinite alternate;
}
.cloud-2 {
width: 80rpx;
height: 28rpx;
top: 80rpx;
right: 15%;
animation: cloudFloat 12s linear infinite alternate-reverse;
}
@keyframes cloudFloat {
from { transform: translateX(-20rpx); }
to { transform: translateX(20rpx); }
}
/* Replaced Empty Img with Icon Group */
.empty-icon-group {
position: relative;
width: 200rpx;
height: 200rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.icon-bg-circle {
position: absolute;
width: 160rpx;
height: 160rpx;
background: white;
border-radius: 50%;
box-shadow: 0 8rpx 32rpx rgba(76, 175, 80, 0.15);
z-index: 1;
}
.emoji-icon {
font-size: 100rpx;
line-height: 1;
position: relative;
z-index: 2;
/* Slight shadow for depth */
text-shadow: 0 4rpx 8rpx rgba(0,0,0,0.1);
display: flex;
align-items: center;
justify-content: center;
}
.sparkle {
position: absolute;
background: #FFD54F;
clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
z-index: 3;
animation: sparkleTwinkle 2s ease-in-out infinite;
}
.s1 {
width: 24rpx;
height: 24rpx;
top: 10rpx;
right: 20rpx;
animation-delay: 0.2s;
}
.s2 {
width: 16rpx;
height: 16rpx;
bottom: 30rpx;
left: 20rpx;
animation-delay: 1.2s;
}
@keyframes sparkleTwinkle {
0%, 100% { transform: scale(0.8) rotate(0deg); opacity: 0.6; }
50% { transform: scale(1.2) rotate(180deg); opacity: 1; }
}
.plant-breathing {
animation: plantBreathe 4s ease-in-out infinite;
}
@keyframes plantBreathe {
0% { transform: scale(1) translateY(0); }
50% { transform: scale(1.05) translateY(-6rpx); }
100% { transform: scale(1) translateY(0); }
}
.empty-text {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
font-size: 34rpx;
font-weight: 700;
color: #2E7D32;
z-index: 2;
}
.empty-sub {
font-size: 26rpx;
color: #7CB342;
font-weight: 500;
}
.empty-btn {
margin-top: 20rpx;
box-shadow: 0 8rpx 16rpx rgba(85, 139, 47, 0.2);
}