init: initial commit

This commit is contained in:
Blizzard
2026-02-04 14:02:31 +08:00
commit 6ceda92e9d
2234 changed files with 38231 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
// pages/tasks/index.js
import { MOCK_TASKS_DATA } from '../../utils/mockData';
Page({
data: {
tasks: [],
groupedTasks: [],
progress: 0,
completingTask: null,
remark: ''
},
onLoad() {
this.setData({ tasks: MOCK_TASKS_DATA });
this.processTasks();
},
onShow() {
if (typeof this.getTabBar === 'function' &&
this.getTabBar()) {
this.getTabBar().setData({ selected: 1 });
}
},
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;
});
// Sorting
const sortedGroups = Object.values(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
});
},
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;
this.setData({
completingTask: task,
remark: ''
});
},
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 });
}
},
onRemarkInput(e) {
this.setData({ remark: e.detail.value });
},
handleConfirmComplete() {
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);
this.setData({
tasks: newTasks,
completingTask: null
}, () => {
this.processTasks();
wx.showToast({ title: '已完成', icon: 'success' });
});
}
})
+13
View File
@@ -0,0 +1,13 @@
{
"navigationBarTitleText": "我的任务",
"usingComponents": {
"t-fab": "tdesign-miniprogram/fab/fab",
"t-popup": "tdesign-miniprogram/popup/popup",
"t-progress": "tdesign-miniprogram/progress/progress",
"t-button": "tdesign-miniprogram/button/button",
"t-textarea": "tdesign-miniprogram/textarea/textarea",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-tag": "tdesign-miniprogram/tag/tag"
}
}
+105
View File
@@ -0,0 +1,105 @@
<!--pages/tasks/index.wxml-->
<view class="tasks-page">
<!-- Progress Card -->
<view class="progress-section">
<view class="progress-card">
<view class="progress-info">
<text class="progress-card-title">今日完成进度</text>
<text class="progress-card-desc">{{progress}}% - 继续保持!🌿</text>
</view>
<view class="progress-circle">
<view class="progress-ring" style="--progress-deg: calc({{progress}} * 3.6deg);"></view>
<text class="percentage">{{progress}}%</text>
</view>
</view>
</view>
<!-- Tasks Container -->
<view class="tasks-container">
<text class="section-title">今日待办</text>
<view wx:if="{{tasks.length === 0}}" class="empty-state">
<text>太棒了!所有任务都已完成 🎉</text>
</view>
<scroll-view wx:else scroll-y class="task-list" enhanced show-scrollbar="{{false}}">
<view wx:for="{{groupedTasks}}" wx:key="plantName" class="plant-task-card {{item.hasOverdue ? 'has-overdue' : ''}}">
<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>
<view wx:else class="thumb-placeholder">{{item.plantName[0]}}</view>
</view>
<text class="plant-name-title">{{item.plantName}}</text>
</view>
<text wx:if="{{item.hasOverdue}}" class="group-overdue-badge">有任务逾期</text>
</view>
<view class="plant-tasks-list">
<view wx:for="{{item.tasks}}" wx:key="id" wx:for-item="task" class="mini-task-row">
<view class="mini-task-left">
<!-- Task Icon with dynamic colors from backend -->
<view class="task-type-icon-circle" style="background: {{task.taskIcon ? task.taskIcon.bgColor : '#f5f5f5'}};">
<t-icon wx:if="{{task.taskIcon}}" name="{{task.taskIcon.icon}}" size="36rpx" color="{{task.taskIcon.color}}" />
<t-icon wx:else name="calendar-1" size="36rpx" color="#999" />
</view>
<view class="mini-task-text">
<!-- Use taskType name from taskIcon or fallback to computed name -->
<text class="task-label">{{task.taskIcon ? task.taskIcon.name : (task.taskType === 'water'?'浇水':(task.taskType==='fertilize'?'施肥':(task.taskType==='prune'?'修剪':(task.taskType==='repot'?'换盆':'任务'))))}}</text>
<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>
</view>
</view>
</view>
<view style="height: 120rpx;"></view>
</scroll-view>
</view>
<!-- Complete Task Popup -->
<t-popup visible="{{completingTask}}" bind:visible-change="onPopupVisibleChange" placement="center">
<view class="modal-card">
<view class="modal-header">
<text class="modal-title">确认完成任务</text>
<view class="close-btn" bindtap="onPopupVisibleChange" data-visible="{{false}}">
<t-icon name="close" size="40rpx" color="#90A4AE" />
</view>
</view>
<view class="task-confirm-content" wx:if="{{completingTask}}">
<view class="confirm-plant-info">
<view class="confirm-icon" style="background: {{completingTask.taskIcon ? completingTask.taskIcon.bgColor : '#f5f5f5'}};">
<t-icon wx:if="{{completingTask.taskIcon}}" name="{{completingTask.taskIcon.icon}}" size="48rpx" color="{{completingTask.taskIcon.color}}" />
<t-icon wx:else name="calendar-1" size="48rpx" color="#999" />
</view>
<view class="confirm-text">
<text class="cp-name">{{completingTask.plantName}}</text>
<text class="cp-task">{{completingTask.taskIcon ? completingTask.taskIcon.name : (completingTask.taskType === 'water'?'浇水':(completingTask.taskType==='fertilize'?'施肥':(completingTask.taskType==='prune'?'修剪':(completingTask.taskType==='repot'?'换盆':'日常任务'))))}}</text>
</view>
</view>
<view class="remark-section">
<text class="remark-label">添加记录备注 (可选)</text>
<textarea
class="remark-input"
placeholder="例如:浇了500ml水..."
value="{{remark}}"
bindinput="onRemarkInput"
fixed="{{true}}"
/>
</view>
<view class="confirm-btn-wrap">
<view class="confirm-complete-btn" bindtap="handleConfirmComplete">
<t-icon name="check" size="36rpx" color="#FFF" style="margin-right: 12rpx;" />
<text>确认完成并记录</text>
</view>
</view>
</view>
</view>
</t-popup>
</view>
+549
View File
@@ -0,0 +1,549 @@
/** pages/tasks/index.wxss **/
.tasks-page {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #F8F9FA;
position: relative;
overflow: hidden;
}
/* Progress Section */
.progress-section {
padding: 0;
margin: 40rpx 48rpx 40rpx;
}
.progress-card {
background: linear-gradient(135deg, #E8F5E9 0%, #C8E6C9 100%);
border-radius: 40rpx;
padding: 32rpx 40rpx;
display: flex;
align-items: center;
justify-content: space-between;
color: #2E7D32;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.02);
position: relative;
overflow: hidden;
min-height: 170rpx;
box-sizing: border-box;
}
.progress-card::after {
content: '';
position: absolute;
right: -50rpx;
top: -50rpx;
width: 200rpx;
height: 200rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
pointer-events: none;
}
.progress-info {
display: flex;
flex-direction: column;
gap: 8rpx;
position: relative;
z-index: 1;
flex: 1;
}
.progress-card-title {
font-size: 30rpx;
font-weight: 700;
}
.progress-card-desc {
font-size: 22rpx;
opacity: 0.85;
font-weight: 500;
}
/* Custom CSS Progress Ring */
.progress-ring-container {
width: 104rpx;
height: 104rpx;
position: relative;
margin-left: 20rpx;
z-index: 2;
border-radius: 50%;
background: rgba(255,255,255,0.3); /* Track color */
display: flex;
align-items: center;
justify-content: center;
}
.progress-ring-inner {
width: 100%;
height: 100%;
border-radius: 50%;
background: conic-gradient(#2E7D32 var(--progress), transparent 0deg);
position: absolute;
top: 0;
left: 0;
transform: rotate(-90deg); /* Start from top */
z-index: 1;
}
/* Mask circle to create ring effect */
.progress-ring-mask {
width: 88rpx; /* 104 - (8*2) = 88 */
height: 88rpx;
background: #E8F5E9; /* Match card gradient start roughly or transparent? */
/* Actually background needs to match parent. Since parent is gradient, solid color mask won't match perfectly.
BETTER approach: Use border-radius border trick.
*/
border-radius: 50%;
position: absolute;
z-index: 2;
/* This method has a flaw with gradient backgrounds.
Alternative: pure border with clip-path?
Or simpler:
.ring {
width: 104rpx; height: 104rpx;
border-radius: 50%;
padding: 8rpx;
background: conic-gradient(currentColor var(--val), transparent 0) content-box;
-webkit-mask: ...
}
*/
}
/* Updated Approach: CSS Border Ring using masks */
.css-progress-ring {
width: 104rpx;
height: 104rpx;
border-radius: 50%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: conic-gradient(#FFFFFF var(--progress), rgba(255,255,255,0.3) 0deg);
/* The ring color is White vs Track transparent/white-alpha */
}
/* The inner hole */
.css-progress-ring::before {
content: "";
position: absolute;
inset: 10rpx; /* stroke width ~ 10rpx/2 ?? No. 104 wide. inset 8rpx -> 88 inner. stroke 8. */
border-radius: 50%;
background-color: transparent; /* We need to show the card gradient behind... difficult with this technique unless we use mask-image */
}
/* Let's try standard mask composite approach or just SVG image which is easiest.
But user wants style resolved. SVG support in WXML view is limited to background-image.
I will use the `t-progress` but FIX the overflow by making sure container is large enough and REMOVE OVERFLOW HIDDEN from the card if necessary, OR fix the container size.
Actually, the user said "Banner is too wide".
I will stick to `t-progress` but I will Wrap it in a `view` that has definitive size and `overflow: visible`.
However, user said "overflows banner".
Let's use a SIMPLIFIED CSS Ring that doesn't rely on transparency tricks that fail on gradients:
Use 4 borders rotated? No.
Let's go with the SVG Image solution in WXML. It is supported via <image> with svg data uri, or just careful CSS.
Wait, the user's "style not resolved" likely implies `t-progress` is just broken essentially.
I will implement a pure WXML/WXSS ring using `view` borders (two halves). This allows transparency center.
*/
.ring-container {
width: 104rpx;
height: 104rpx;
position: relative;
margin-left: 20rpx;
z-index: 5;
}
.ring-text {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
font-weight: 800;
color: #1B5E20;
z-index: 10;
}
/* Pure CSS Progress Circle (Left/Right spin method) */
.circle-wrap {
width: 104rpx;
height: 104rpx;
background: rgba(255,255,255,0.3); /* Track */
border-radius: 50%;
position: relative;
overflow: hidden; /* This clips the half-circles but center is filled? No. */
/* This method clears center by adding a smaller circle on top */
}
.circle-wrap .mask,
.circle-wrap .fill {
width: 104rpx;
height: 104rpx;
position: absolute;
border-radius: 50%;
}
.circle-wrap .mask {
clip: rect(0px, 104rpx, 104rpx, 52rpx);
}
.circle-wrap .fill {
clip: rect(0px, 52rpx, 104rpx, 0px);
background-color: #FFFFFF; /* Progress Color */
}
.circle-wrap .mask.full,
.circle-wrap .fill {
animation: fill ease-in-out 3s;
transform: rotate(126deg); /* Dynamic via style */
}
/* Inner hole */
.circle-inside {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
background: #D2E8D5; /* Approximated gradient mid-point or use transparent if specific layout allows */
/* Since gradient is complex, solid color looks bad.
The "Progress Circle" style issue is likely the 'cut off' or solid center blocking gradient.
BEST SOLUTION: Canvas 2D. But cumbersome.
Let's try t-progress one last time with correct constraints? No, user is annoyed.
Let's use a transparent PNG for the track and `conic-gradient` for the fill with a specific mix-blend-mode? No support.
Let's use `conic-gradient` with `mask-image`. Most modern webviews support it.
*/
}
/* Progress Circle - Matching Prototype */
.progress-circle {
position: relative;
width: 120rpx;
height: 120rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 20rpx;
z-index: 1;
}
.progress-ring {
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
background: conic-gradient(#2E7D32 var(--progress-deg), rgba(255, 255, 255, 0.3) 0deg);
-webkit-mask: radial-gradient(transparent 60%, black 61%);
mask: radial-gradient(transparent 60%, black 61%);
}
.percentage {
position: relative;
z-index: 2;
font-size: 28rpx;
font-weight: 700;
color: #1B5E20;
}
.tasks-container {
flex: 1;
background: white;
border-top-left-radius: 60rpx;
border-top-right-radius: 60rpx;
padding: 48rpx 40rpx 0;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 -8rpx 32rpx rgba(0,0,0,0.03);
min-height: 0; /* Critical for flex scrolling */
}
.section-title {
font-size: 36rpx;
font-weight: 800;
color: #263238;
margin-bottom: 32rpx;
padding-left: 8rpx;
flex-shrink: 0;
}
.task-list {
flex: 1;
height: 0; /* Force flex container to define height */
}
.plant-task-card {
background: white;
border-radius: 40rpx;
padding: 32rpx;
margin-bottom: 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.03);
border: 2rpx solid transparent;
transition: all 0.2s;
}
.plant-task-card.has-overdue {
border-color: rgba(239, 83, 80, 0.1);
background: #FFF8F8;
}
.card-header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
padding-bottom: 24rpx;
border-bottom: 2rpx solid rgba(0, 0, 0, 0.03);
}
.plant-info-brief {
display: flex;
align-items: center;
gap: 20rpx;
}
.plant-thumb-small {
width: 80rpx;
height: 80rpx;
border-radius: 24rpx;
overflow: hidden;
background: #f0f0f0;
}
.plant-thumb-small image {
width: 100%;
height: 100%;
}
.thumb-placeholder {
width: 100%;
height: 100%;
background: #E8F5E9;
color: #558B2F;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 32rpx;
}
.plant-name-title {
font-size: 30rpx;
font-weight: 700;
color: #263238;
}
.group-overdue-badge {
font-size: 20rpx;
color: #EF5350;
background: rgba(239, 83, 80, 0.1);
padding: 4rpx 16rpx;
border-radius: 20rpx;
font-weight: 600;
}
.plant-tasks-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.mini-task-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.mini-task-left {
display: flex;
align-items: center;
gap: 24rpx;
}
.task-type-icon-circle {
width: 64rpx;
height: 64rpx;
background: #F8F9FA;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #90A4AE;
}
.task-icon {
width: 32rpx;
height: 32rpx;
}
.mini-task-text {
display: flex;
flex-direction: column;
gap: 4rpx;
}
.task-label {
font-size: 28rpx;
font-weight: 500;
color: #37474F;
}
.task-overdue-text {
font-size: 22rpx;
color: #EF5350;
font-weight: 600;
}
.mini-check-btn {
width: 64rpx;
height: 64rpx;
border-radius: 20rpx;
border: 3rpx solid #E0E0E0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
box-sizing: border-box;
}
.mini-check-btn:active {
background: rgba(85, 139, 47, 0.05);
border-color: #558B2F;
}
.mini-check-btn.btn-urgent {
border-color: rgba(239, 83, 80, 0.3);
}
.mini-check-btn.btn-urgent:active {
background: rgba(239, 83, 80, 0.05);
border-color: #EF5350;
}
/* Modal Specifics */
.modal-card {
background: white;
width: 640rpx;
border-radius: 64rpx;
padding: 48rpx;
box-sizing: border-box;
position: relative;
overflow: hidden;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 48rpx;
}
.modal-title {
font-size: 36rpx;
font-weight: 800;
color: #263238;
}
.close-btn {
padding: 8rpx;
}
.task-confirm-content {
display: flex;
flex-direction: column;
gap: 40rpx;
}
.confirm-plant-info {
display: flex;
align-items: center;
gap: 24rpx;
background: #F4F6F0;
padding: 32rpx;
border-radius: 32rpx;
}
.confirm-icon {
width: 96rpx;
height: 96rpx;
background: white;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
}
.icon-32 {
width: 48rpx;
height: 48rpx;
}
.confirm-text {
display: flex;
flex-direction: column;
gap: 8rpx;
flex: 1;
}
.cp-name {
font-size: 32rpx;
font-weight: 700;
color: #263238;
}
.cp-task {
font-size: 26rpx;
color: #90A4AE;
}
.remark-section {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.remark-label {
font-size: 26rpx;
font-weight: 600;
color: #263238;
}
.remark-input {
width: 100%;
height: 160rpx;
background: #F8F9FA;
border: 2rpx solid #ECEFF1;
border-radius: 24rpx;
padding: 24rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.confirm-btn-wrap {
margin-top: 16rpx;
}
.confirm-complete-btn {
width: 100%;
background: #558B2F;
color: white;
height: 100rpx;
border-radius: 50rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 600;
box-shadow: 0 8rpx 24rpx rgba(85, 139, 47, 0.4);
}
.confirm-complete-btn:active {
transform: scale(0.98);
}