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
+76 -38
View File
@@ -1,5 +1,6 @@
// pages/garden/add/index.js
import { MOCK_PLANTS, CARE_TASK_ICONS } from '../../../utils/mockData';
import request from '../../../utils/request';
Page({
data: {
@@ -11,6 +12,8 @@ Page({
newCareTasks: [],
scrollIntoViewId: '',
uploadedImageId: '', // Store the uploaded image ID
showActionSheet: false,
actionSheetItems: [
{ label: '拍摄', value: 'camera' },
@@ -73,13 +76,43 @@ Page({
camera: 'back',
success: (res) => {
const tempFilePath = res.tempFiles[0].tempFilePath;
// 1. Show temp image immediately for UX
this.setData({
newPlantImage: tempFilePath,
isLocalImage: true
});
// Show loading
wx.showLoading({ title: 'Uploading...' });
// Call upload API
request.upload(tempFilePath).then(data => {
wx.hideLoading();
// User provided response format: { data: { file: { url: ..., id: ... } } }
// request.js unwraps 'data', so 'data' here is { file: { ... } }
const fileData = data?.file || {};
const imageUrl = fileData.url;
const imageId = fileData.id;
if (imageUrl && imageId) {
this.setData({
newPlantImage: imageUrl,
uploadedImageId: imageId,
isLocalImage: true
});
wx.showToast({ title: 'Success', icon: 'success' });
} else {
wx.showToast({ title: 'No URL returned', icon: 'none' });
}
}).catch(err => {
wx.hideLoading();
wx.showToast({ title: 'Upload Failed', icon: 'none' });
});
},
fail: (err) => {
console.log('User cancelled', err);
// User cancelled or error
}
});
},
@@ -177,52 +210,57 @@ Page({
},
handleAddPlant() {
if (!this.data.newPlantName) {
const { newPlantName, newPlantLocation, newPlantDate, uploadedImageId, newCareTasks } = this.data;
// Basic Validation
if (!newPlantName) {
wx.showToast({ title: '请输入植物名称', icon: 'none' });
return;
}
if (!uploadedImageId) {
wx.showToast({ title: '请先上传图片', icon: 'none' });
return;
}
const newId = (MOCK_PLANTS.length + 1).toString();
const adoption = new Date(this.data.newPlantDate);
const today = new Date();
const diffTime = Math.abs(today.getTime() - adoption.getTime());
const daysPlanted = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
// Prepare care schedule with icon info
const careSchedule = this.data.newCareTasks.map(task => ({
id: task.id,
taskName: task.taskName,
frequencyValue: task.frequencyValue,
frequencyUnit: task.frequencyUnit,
iconId: task.iconId,
taskIcon: task.taskIcon
// Construct Care Plans
const carePlans = newCareTasks.map(task => ({
name: task.taskName || '未命名事项',
period: task.frequencyValue || 1,
icon: JSON.stringify(task.taskIcon || {}) // Serialize icon details
}));
const newPlant = {
id: newId,
name: this.data.newPlantName,
images: [this.data.newPlantImage || 'monstera_plant_1769757312755.png'],
daysPlanted: daysPlanted,
adoptionDate: this.data.newPlantDate,
location: this.data.newPlantLocation || '未分配位置',
scientificName: 'Unknown',
family: '未知科',
genus: '未知属',
description: '新添加的植物...',
difficulty: '⭐️',
toxicity: '未知',
flowerMsg: '充满希望',
careSchedule: careSchedule
// Construct Payload
const payload = {
name: newPlantName,
plantTime: newPlantDate,
placement: newPlantLocation || '',
ossIds: [uploadedImageId],
carePlans: carePlans,
// Default fields as not in UI yet
potMaterial: '',
potSize: '',
sunlight: '',
plantingMaterial: ''
};
// In a real app we would call an API or update global store
// For this mock, we append to the imported array (memory only)
MOCK_PLANTS.push(newPlant);
// Submit
wx.showLoading({ title: 'Creating...' });
request.post('/plant/add', payload).then(res => {
wx.hideLoading();
wx.showToast({ title: '添加成功', icon: 'success' });
wx.showToast({ title: '添加成功', icon: 'success' });
// Refresh previous page (e.g. garden list) if needed
// const pages = getCurrentPages();
// const prevPage = pages[pages.length - 2];
// if (prevPage && prevPage.onRefresh) prevPage.onRefresh();
setTimeout(() => {
wx.navigateBack();
}, 1000);
setTimeout(() => {
wx.navigateBack();
}, 1000);
}).catch(err => {
wx.hideLoading();
console.error('Add plant failed', err);
// Error handling is done inside request.post, but fallback here
});
}
})
+8 -1
View File
@@ -11,7 +11,14 @@
<!-- Upload Area -->
<view class="upload-section">
<view class="image-upload-area {{newPlantImage ? 'has-image' : ''}}" bindtap="showActionSheet">
<t-image wx:if="{{newPlantImage}}" src="{{tools.resolvePath(newPlantImage)}}" mode="aspectFill" width="100%" height="100%" />
<t-image
wx:if="{{newPlantImage}}"
src="{{newPlantImage}}"
mode="aspectFill"
width="100%"
height="100%"
t-class="uploaded-img"
/>
<view wx:else class="upload-placeholder">
<t-icon name="upload" size="64rpx" color="#999" />
<text>点击上传封面图</text>
+7
View File
@@ -62,6 +62,13 @@ scroll-view ::-webkit-scrollbar {
transition: all 0.2s;
}
.uploaded-img {
width: 100%;
height: 100%;
border-radius: 32rpx; /* Matches container */
object-fit: cover;
}
.image-upload-area:active {
border-color: #558B2F; /* var(--primary) */
background: #F1F8E9;
+67 -9
View File
@@ -1,16 +1,23 @@
// pages/garden/index.js
import { MOCK_PLANTS } from '../../utils/mockData';
import request from '../../utils/request';
Page({
data: {
plants: [],
dateString: '',
greeting: ''
greeting: '',
// Pagination
currentPage: 1,
pageSize: 6,
total: 0,
isLastPage: false,
isLoading: false
},
onLoad(options) {
this.initTime();
this.loadPlants();
this.loadPlants(true);
},
onShow() {
@@ -20,12 +27,62 @@ Page({
selected: 0
})
}
// Refresh list in case new plant was added
this.loadPlants();
// Refresh list on show to ensure data is up-to-date
// We use reset=true to reload from page 1
this.loadPlants(true);
},
loadPlants() {
this.setData({ plants: MOCK_PLANTS });
// Pull to refresh
onPullDownRefresh() {
this.loadPlants(true).then(() => {
wx.stopPullDownRefresh();
});
},
// Infinite scroll
onReachBottom() {
if (!this.data.isLastPage && !this.data.isLoading) {
this.loadPlants(false);
}
},
loadPlants(reset = false) {
if (this.data.isLoading) return Promise.resolve();
this.setData({ isLoading: true });
const page = reset ? 1 : this.data.currentPage + 1;
const payload = {
current: page,
pageSize: this.data.pageSize,
keyword: ''
};
return request.post('/plant/page', payload).then(res => {
const list = res.list || [];
const total = res.total || 0;
// Map backend data to UI structure: 1. Map imgList to images array with path resolution
const mappedList = list.map(item => {
let imageUrl = '';
if (item.imgList && item.imgList.length > 0) {
imageUrl = item.imgList[0].url;
}
return { ...item, images: [imageUrl] };
});
this.setData({
plants: reset ? mappedList : [...this.data.plants, ...mappedList],
currentPage: page,
total: total,
isLastPage: (reset ? mappedList.length : this.data.plants.length + mappedList.length) >= total,
isLoading: false
});
}).catch(err => {
console.error('Load plants failed', err);
this.setData({ isLoading: false });
wx.showToast({ title: '加载失败', icon: 'none' });
});
},
initTime() {
@@ -66,7 +123,8 @@ Page({
},
onScrollLower() {
console.log('Scroll to lower - loading more plants...');
// In a real app, this would trigger pagination
if (!this.data.isLastPage && !this.data.isLoading) {
this.loadPlants(false);
}
}
})
+41 -3
View File
@@ -13,16 +13,43 @@
<view class="banner-container">
<image src="https://images.unsplash.com/photo-1585320806297-9794b3e4eeae?w=800" class="garden-banner" mode="aspectFill" />
<view class="banner-overlay">
<text class="count-tag">共养护 {{plants.length}} 盆植物</text>
<text class="count-tag">共养护 {{total}} 盆植物</text>
</view>
</view>
<view class="garden-list-wrapper">
<scroll-view scroll-y class="garden-list-container" enhanced show-scrollbar="{{false}}" bindscrolltolower="onScrollLower">
<view wx:if="{{plants.length === 0}}" class="garden-empty-state">
<view class="empty-sky-scene">
<view class="sun-small"></view>
<view class="cloud-small"></view>
</view>
<view class="empty-center-icon">
<text class="emoji-sprout">🌱</text>
<view class="soil-base"></view>
</view>
<view class="empty-message">
<text class="msg-title">花园还在沉睡中...</text>
<text class="msg-sub">快种下第一颗种子,唤醒它吧 ✨</text>
</view>
<!-- Animated Guide Arrow -->
<view class="guide-arrow-box bounce-arrow">
<text class="arrow-text">点这里添加</text>
<t-icon name="arrow-right-down" size="48rpx" color="#558B2F" />
</view>
</view>
<scroll-view wx:else scroll-y class="garden-list-container" enhanced show-scrollbar="{{false}}" bindscrolltolower="onScrollLower">
<view class="plant-grid">
<view wx:for="{{plants}}" wx:key="id" class="plant-card" bindtap="navigateToDetail" data-id="{{item.id}}">
<view class="plant-image-container">
<t-image src="{{tools.resolvePath(item.images[0])}}" mode="aspectFill" width="100%" height="100%" />
<t-image
src="{{item.images[0]}}"
mode="aspectFill"
width="100%"
height="100%"
t-class="uploaded-img"
/>
<view class="days-badge">{{item.daysPlanted}}天</view>
</view>
<view class="plant-info">
@@ -33,6 +60,17 @@
</view>
</view>
</view>
<!-- Loading & No More Data Status -->
<view class="list-footer">
<t-loading wx:if="{{isLoading}}" theme="circular" size="40rpx" text="加载中..." />
<view wx:if="{{isLastPage && plants.length > 0}}" class="no-more-data">
<view class="divider-line"></view>
<text>没有更多了</text>
<view class="divider-line"></view>
</view>
</view>
<view style="height: 100rpx;"></view>
</scroll-view>
</view>
+155 -4
View File
@@ -41,8 +41,8 @@
}
.banner-container {
margin: 0 40rpx 48rpx;
height: 280rpx;
margin: 0 40rpx 24rpx;
height: 220rpx;
border-radius: 40rpx;
overflow: hidden;
position: relative;
@@ -124,10 +124,10 @@
overflow: hidden;
}
.plant-image-container t-image {
.plant-image-container .uploaded-img {
width: 100%;
height: 100%;
transition: transform 0.6s ease;
border-radius: 40rpx 40rpx 0 0;
}
.days-badge {
@@ -192,3 +192,154 @@
transform: scale(0.92);
box-shadow: 0 4rpx 16rpx rgba(85, 139, 47, 0.2);
}
/* List Footer */
.list-footer {
display: flex;
justify-content: center;
align-items: center;
padding: 32rpx 0;
}
.no-more-data {
display: flex;
align-items: center;
gap: 16rpx;
font-size: 24rpx;
color: #999;
}
.divider-line {
width: 60rpx;
height: 2rpx;
background: #E0E0E0;
}
/* Garden Empty State */
.garden-empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-bottom: 120rpx;
position: relative;
overflow: hidden;
}
.empty-sky-scene {
position: absolute;
top: 40rpx;
width: 100%;
height: 200rpx;
}
.sun-small {
position: absolute;
top: 20rpx;
right: 60rpx;
width: 80rpx;
height: 80rpx;
background: radial-gradient(circle, #FFF176 0%, #FDD835 100%);
border-radius: 50%;
box-shadow: 0 0 24rpx rgba(253, 216, 53, 0.4);
}
.cloud-small {
position: absolute;
top: 60rpx;
left: 40rpx;
width: 100rpx;
height: 36rpx;
background: #E1F5FE;
border-radius: 20rpx;
opacity: 0.7;
animation: cloudFloat 8s ease-in-out infinite alternate;
}
.empty-center-icon {
margin-bottom: 40rpx;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.emoji-sprout {
font-size: 120rpx;
line-height: 1;
position: relative;
z-index: 2;
animation: sproutSway 3s ease-in-out infinite;
transform-origin: bottom center;
}
.soil-base {
width: 100rpx;
height: 12rpx;
background: #EFEBE9;
border-radius: 50%;
margin-top: -10rpx;
z-index: 1;
}
.empty-message {
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
}
.msg-title {
font-size: 36rpx;
font-weight: 700;
color: #558B2F;
}
.msg-sub {
font-size: 28rpx;
color: #90A4AE;
font-weight: 500;
}
/* Guide Arrow Animation */
.guide-arrow-box {
position: fixed;
bottom: 170rpx;
right: 80rpx;
display: flex;
flex-direction: column;
align-items: center;
transform: rotate(-10deg);
z-index: 900;
pointer-events: none; /* Let touches pass through to underlying nav if needed */
}
.arrow-text {
font-size: 24rpx;
font-weight: 600;
color: #558B2F;
margin-bottom: -8rpx;
background: rgba(255, 255, 255, 0.9);
padding: 4rpx 12rpx;
border-radius: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
}
.bounce-arrow {
animation: bounceArrow 1.5s infinite;
}
@keyframes bounceArrow {
0%, 100% { transform: translateY(0) rotate(-15deg); }
50% { transform: translateY(10rpx) rotate(-15deg); }
}
@keyframes cloudFloat {
from { transform: translateX(0); }
to { transform: translateX(30rpx); }
}
@keyframes sproutSway {
0%, 100% { transform: rotate(-5deg); }
50% { transform: rotate(5deg); }
}