feat: 任务和社区页面
This commit is contained in:
+76
-38
@@ -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
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user