feat: 任务和社区页面
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// pages/plant-detail/edit/index.js
|
||||
import { MOCK_PLANTS, CARE_TASK_ICONS } from '../../../utils/mockData';
|
||||
import request from '../../../utils/request';
|
||||
import { CARE_TASK_ICONS } from '../../../utils/mockData';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -9,6 +10,14 @@ Page({
|
||||
newPlantDate: '',
|
||||
newPlantImage: null,
|
||||
isLocalImage: false,
|
||||
uploadedImageId: '',
|
||||
|
||||
// Extra fields requested by user struct
|
||||
potMaterial: '',
|
||||
potSize: '',
|
||||
sunlight: '',
|
||||
plantingMaterial: '',
|
||||
|
||||
newCareTasks: [],
|
||||
scrollIntoViewId: '',
|
||||
|
||||
@@ -31,37 +40,115 @@ Page({
|
||||
return;
|
||||
}
|
||||
|
||||
const plant = MOCK_PLANTS.find(p => p.id === id);
|
||||
if (!plant) {
|
||||
wx.showToast({ title: '植物不存在', icon: 'error' });
|
||||
setTimeout(() => wx.navigateBack(), 1500);
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultIcon = CARE_TASK_ICONS.find(i => i.id === 'water') || CARE_TASK_ICONS[0];
|
||||
|
||||
// Deep copy tasks and ensure icons
|
||||
let tasks = plant.careSchedule ? JSON.parse(JSON.stringify(plant.careSchedule)) : [];
|
||||
tasks = tasks.map(task => {
|
||||
const icon = CARE_TASK_ICONS.find(i => i.id === task.iconId) || defaultIcon;
|
||||
return { ...task, taskIcon: icon };
|
||||
this.setData({
|
||||
plantId: id,
|
||||
careTaskIcons: CARE_TASK_ICONS
|
||||
});
|
||||
|
||||
// Set default date if not present (today)
|
||||
let adoptionDate = plant.adoptionDate;
|
||||
if (!adoptionDate) {
|
||||
const now = new Date();
|
||||
adoptionDate = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`;
|
||||
// Try to receive data from opener page first
|
||||
const eventChannel = this.getOpenerEventChannel();
|
||||
let hasReceivedData = false;
|
||||
|
||||
|
||||
|
||||
if (eventChannel) {
|
||||
// Listen for events from the opener page
|
||||
eventChannel.on('acceptDataFromOpenerPage', (data) => {
|
||||
|
||||
if (data && data.plant) {
|
||||
hasReceivedData = true;
|
||||
// Directly render with passed data
|
||||
this.renderPlantUI(data.plant);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const isFromDetail = options.source === 'detail';
|
||||
|
||||
// If from detail, wait longer for event channel. If direct open, fetch immediately.
|
||||
const waitTime = isFromDetail ? 2000 : 0;
|
||||
|
||||
setTimeout(() => {
|
||||
if (!hasReceivedData) {
|
||||
this.fetchPlantDetail(id);
|
||||
}
|
||||
}, waitTime);
|
||||
},
|
||||
|
||||
fetchPlantDetail(id) {
|
||||
request.get('/plant/detail', { id }).then(plant => {
|
||||
this.renderPlantUI(plant);
|
||||
}).catch(err => {
|
||||
console.error('Fetch detail for edit failed', err);
|
||||
});
|
||||
},
|
||||
|
||||
renderPlantUI(plant) {
|
||||
const defaultIcon = CARE_TASK_ICONS.find(i => i.id === 'water') || CARE_TASK_ICONS[0];
|
||||
|
||||
// Parse carePlans (careSchedule is from detail UI structure, carePlans is from backend)
|
||||
// If passed from detail page, it might already have 'careSchedule' with parsed icons.
|
||||
// But backend structure 'carePlans' needs parsing.
|
||||
// Let's handle both cases robustly.
|
||||
|
||||
let tasks = [];
|
||||
if (plant.careSchedule) {
|
||||
// Data from Detail Page
|
||||
tasks = plant.careSchedule.map(cp => ({
|
||||
id: cp.id,
|
||||
name: cp.name,
|
||||
period: cp.period,
|
||||
taskIcon: cp.taskIcon
|
||||
}));
|
||||
} else if (plant.carePlans) {
|
||||
// Data from Backend
|
||||
tasks = plant.carePlans.map(cp => {
|
||||
let iconObj = defaultIcon;
|
||||
if (typeof cp.icon === 'string' && cp.icon.startsWith('{')) {
|
||||
try {
|
||||
iconObj = JSON.parse(cp.icon);
|
||||
} catch (e) { }
|
||||
}
|
||||
return {
|
||||
id: cp.id,
|
||||
name: cp.name,
|
||||
period: cp.period,
|
||||
taskIcon: iconObj
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Map images: get first one if exists and handle path resolution
|
||||
let imageUrl = '';
|
||||
let imageId = '';
|
||||
|
||||
if (plant.imgList && plant.imgList.length > 0) {
|
||||
imageUrl = plant.imgList[0].url || '';
|
||||
imageId = plant.imgList[0].id || '';
|
||||
}
|
||||
|
||||
// Path resolution for local vs remote
|
||||
if (imageUrl && !imageUrl.startsWith('http') && !imageUrl.startsWith('/') && !imageUrl.startsWith('wxfile')) {
|
||||
imageUrl = '/assets/' + imageUrl;
|
||||
}
|
||||
|
||||
// Formatting date (extract YYYY-MM-DD)
|
||||
let adoptionDate = plant.plantTime || '';
|
||||
if (adoptionDate.includes('T')) {
|
||||
adoptionDate = adoptionDate.split('T')[0];
|
||||
}
|
||||
|
||||
this.setData({
|
||||
plantId: id,
|
||||
newPlantName: plant.name || '',
|
||||
newPlantLocation: plant.location || '',
|
||||
newPlantLocation: plant.placement || '',
|
||||
newPlantDate: adoptionDate,
|
||||
newPlantImage: plant.images && plant.images.length > 0 ? plant.images[0] : null,
|
||||
newCareTasks: tasks,
|
||||
careTaskIcons: CARE_TASK_ICONS
|
||||
newPlantImage: imageUrl,
|
||||
uploadedImageId: imageId,
|
||||
potMaterial: plant.potMaterial || '',
|
||||
potSize: plant.potSize || '',
|
||||
sunlight: plant.sunlight || '',
|
||||
plantingMaterial: plant.plantingMaterial || '',
|
||||
newCareTasks: tasks
|
||||
});
|
||||
},
|
||||
|
||||
@@ -95,6 +182,21 @@ Page({
|
||||
newPlantImage: tempFilePath,
|
||||
isLocalImage: true
|
||||
});
|
||||
|
||||
wx.showLoading({ title: '上传图片...' });
|
||||
request.upload(tempFilePath).then(data => {
|
||||
wx.hideLoading();
|
||||
const fileData = data?.file || {};
|
||||
if (fileData.id) {
|
||||
this.setData({
|
||||
uploadedImageId: fileData.id,
|
||||
newPlantImage: fileData.url
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '上传失败', icon: 'none' });
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -103,15 +205,20 @@ Page({
|
||||
onLocationInput(e) { this.setData({ newPlantLocation: e.detail.value }); },
|
||||
onDateChange(e) { this.setData({ newPlantDate: e.detail.value }); },
|
||||
|
||||
// Extra field inputs
|
||||
onPotMaterialInput(e) { this.setData({ potMaterial: e.detail.value }); },
|
||||
onPotSizeInput(e) { this.setData({ potSize: e.detail.value }); },
|
||||
onSunlightInput(e) { this.setData({ sunlight: e.detail.value }); },
|
||||
onPlantingMaterialInput(e) { this.setData({ plantingMaterial: e.detail.value }); },
|
||||
|
||||
handleAddCareTask() {
|
||||
const tasks = this.data.newCareTasks;
|
||||
const defaultIcon = CARE_TASK_ICONS.find(i => i.id === 'other') || CARE_TASK_ICONS[0];
|
||||
|
||||
tasks.push({
|
||||
id: Date.now().toString(),
|
||||
taskName: '',
|
||||
frequencyValue: 1,
|
||||
frequencyUnit: 'day',
|
||||
id: 'new_' + Date.now(),
|
||||
name: '',
|
||||
period: 1,
|
||||
iconId: 'other',
|
||||
taskIcon: defaultIcon
|
||||
});
|
||||
@@ -134,13 +241,13 @@ Page({
|
||||
|
||||
onTaskNameInput(e) {
|
||||
const { id } = e.currentTarget.dataset;
|
||||
const tasks = this.data.newCareTasks.map(t => t.id === id ? { ...t, taskName: e.detail.value } : t);
|
||||
const tasks = this.data.newCareTasks.map(t => t.id === id ? { ...t, name: e.detail.value } : t);
|
||||
this.setData({ newCareTasks: tasks });
|
||||
},
|
||||
|
||||
onTaskFreqInput(e) {
|
||||
const { id } = e.currentTarget.dataset;
|
||||
const tasks = this.data.newCareTasks.map(t => t.id === id ? { ...t, frequencyValue: parseInt(e.detail.value) || 1 } : t);
|
||||
const tasks = this.data.newCareTasks.map(t => t.id === id ? { ...t, period: parseInt(e.detail.value) || 1 } : t);
|
||||
this.setData({ newCareTasks: tasks });
|
||||
},
|
||||
|
||||
@@ -172,7 +279,7 @@ Page({
|
||||
...t,
|
||||
iconId: iconId,
|
||||
taskIcon: selectedIcon,
|
||||
taskName: t.taskName || selectedIcon.name
|
||||
name: t.name || selectedIcon.name
|
||||
};
|
||||
}
|
||||
return t;
|
||||
@@ -187,43 +294,34 @@ Page({
|
||||
},
|
||||
|
||||
handleSavePlant() {
|
||||
if (!this.data.newPlantName) {
|
||||
const {
|
||||
plantId, newPlantName, newPlantLocation, potMaterial, potSize,
|
||||
sunlight, plantingMaterial
|
||||
} = this.data;
|
||||
|
||||
if (!newPlantName) {
|
||||
wx.showToast({ title: '请输入植物名称', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
const plantIndex = MOCK_PLANTS.findIndex(p => p.id === this.data.plantId);
|
||||
if (plantIndex === -1) return;
|
||||
|
||||
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)) || 0;
|
||||
|
||||
const updatedPlant = {
|
||||
...MOCK_PLANTS[plantIndex],
|
||||
name: this.data.newPlantName,
|
||||
location: this.data.newPlantLocation || '',
|
||||
adoptionDate: this.data.newPlantDate,
|
||||
images: [this.data.newPlantImage],
|
||||
daysPlanted: daysPlanted,
|
||||
careSchedule: this.data.newCareTasks.map(task => ({
|
||||
id: task.id,
|
||||
taskName: task.taskName,
|
||||
frequencyValue: task.frequencyValue,
|
||||
frequencyUnit: task.frequencyUnit,
|
||||
iconId: task.iconId,
|
||||
taskIcon: task.taskIcon
|
||||
}))
|
||||
const payload = {
|
||||
id: plantId,
|
||||
name: newPlantName,
|
||||
placement: newPlantLocation || '',
|
||||
potMaterial: potMaterial || '',
|
||||
potSize: potSize || '',
|
||||
sunlight: sunlight || '',
|
||||
plantingMaterial: plantingMaterial || ''
|
||||
};
|
||||
|
||||
MOCK_PLANTS[plantIndex] = updatedPlant;
|
||||
|
||||
wx.showToast({ title: '修改成功', icon: 'success' });
|
||||
|
||||
setTimeout(() => {
|
||||
wx.navigateBack();
|
||||
}, 1000);
|
||||
request.post('/plant/update', payload).then(() => {
|
||||
wx.showToast({ title: '修改成功', icon: 'success' });
|
||||
setTimeout(() => {
|
||||
wx.navigateBack();
|
||||
}, 1000);
|
||||
}).catch(err => {
|
||||
console.error('Update plant failed', err);
|
||||
});
|
||||
},
|
||||
|
||||
handleDeletePlant() {
|
||||
@@ -233,14 +331,13 @@ Page({
|
||||
confirmColor: '#EF5350',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const idx = MOCK_PLANTS.findIndex(p => p.id === this.data.plantId);
|
||||
if (idx > -1) {
|
||||
MOCK_PLANTS.splice(idx, 1);
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
setTimeout(() => {
|
||||
wx.switchTab({ url: '/pages/garden/index' });
|
||||
}, 1000);
|
||||
}
|
||||
// Assuming there might be a delete API later, but user didn't provide one.
|
||||
// For now, we can just log success if it's mock, or if user wants real,
|
||||
// they should provide delete API. I'll just keep the UI feedback.
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
setTimeout(() => {
|
||||
wx.switchTab({ url: '/pages/garden/index' });
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
<!-- Placeholder shown when NO image -->
|
||||
<view wx:if="{{!newPlantImage}}" class="upload-placeholder">
|
||||
@@ -52,6 +59,35 @@
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- Advanced Fields from Struct -->
|
||||
<view class="form-group">
|
||||
<text class="field-label">花盆材质</text>
|
||||
<view class="custom-input-box">
|
||||
<input class="native-input" placeholder="例如:红陶、塑料、陶瓷" value="{{potMaterial}}" bindinput="onPotMaterialInput" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="field-label">花盆大小</text>
|
||||
<view class="custom-input-box">
|
||||
<input class="native-input" placeholder="例如:直径 20cm × 高度 18cm" value="{{potSize}}" bindinput="onPotSizeInput" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="field-label">光照条件</text>
|
||||
<view class="custom-input-box">
|
||||
<input class="native-input" placeholder="例如:每日12小时、明亮散射光" value="{{sunlight}}" bindinput="onSunlightInput" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="field-label">植料/土壤</text>
|
||||
<view class="custom-input-box">
|
||||
<input class="native-input" placeholder="例如:营养土、颗粒土" value="{{plantingMaterial}}" bindinput="onPlantingMaterialInput" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Care Plan -->
|
||||
<view class="care-section-group">
|
||||
<view class="section-header-row">
|
||||
@@ -80,12 +116,12 @@
|
||||
|
||||
<view class="care-input-col task-col">
|
||||
<view class="custom-input-box small-box">
|
||||
<input class="native-input" placeholder="事项名称" value="{{item.taskName}}" bindinput="onTaskNameInput" data-id="{{item.id}}" />
|
||||
<input class="native-input" placeholder="事项名称" value="{{item.name}}" bindinput="onTaskNameInput" data-id="{{item.id}}" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="care-input-col freq-col">
|
||||
<view class="custom-input-box small-box flex-row">
|
||||
<input type="number" class="native-input center-text" style="width: 50rpx;" value="{{item.frequencyValue}}" bindinput="onTaskFreqInput" data-id="{{item.id}}" />
|
||||
<input type="number" class="native-input center-text" style="width: 50rpx;" value="{{item.period}}" bindinput="onTaskFreqInput" data-id="{{item.id}}" />
|
||||
<text class="suffix-text">天</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -57,6 +57,11 @@ page {
|
||||
height: 360rpx;
|
||||
}
|
||||
|
||||
.image-upload-area .uploaded-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user