feat: 任务和社区页面
This commit is contained in:
@@ -1,7 +1,29 @@
|
|||||||
// app.js
|
import request from './utils/request';
|
||||||
|
|
||||||
App({
|
App({
|
||||||
onLaunch() {
|
onLaunch() {
|
||||||
console.log('App Launch');
|
// Login
|
||||||
|
wx.login({
|
||||||
|
success: res => {
|
||||||
|
// Send res.code to backend to swap for openId, sessionKey, unionId
|
||||||
|
if (res.code) {
|
||||||
|
request.get('/auth/miniLogin', { code: res.code }).then(data => {
|
||||||
|
// Assuming the token is in data.token or data itself
|
||||||
|
const token = data.token || data;
|
||||||
|
if (token && typeof token === 'string') {
|
||||||
|
wx.setStorageSync('token', token);
|
||||||
|
console.log('Login successful, token stored');
|
||||||
|
} else {
|
||||||
|
console.warn('Login response did not contain a valid token string', data);
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Login failed', err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('wx.login failed: ' + res.errMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
globalData: {
|
globalData: {
|
||||||
userInfo: null
|
userInfo: null
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 620 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 716 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 660 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 686 KiB |
+59
-104
@@ -1,16 +1,15 @@
|
|||||||
// pages/community/create/index.js
|
// pages/community/create/index.js
|
||||||
import { MOCK_POSTS } from '../../../utils/mockData';
|
import request from '../../../utils/request';
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
content: '',
|
content: '',
|
||||||
images: [],
|
images: [],
|
||||||
canPublish: false,
|
canPublish: false, // Only depends on content
|
||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
location: '',
|
location: '',
|
||||||
selectedTopics: [],
|
selectedTopics: [],
|
||||||
suggestedTopics: ['植物养护', '多肉日记', '绿植分享', '花卉美照', '阳台花园', '新手入门'],
|
suggestedTopics: ['植物养护', '多肉日记', '绿植分享', '花卉美照', '阳台花园', '新手入门'],
|
||||||
hasDraft: false,
|
|
||||||
showImageSheet: false,
|
showImageSheet: false,
|
||||||
imageSheetItems: [
|
imageSheetItems: [
|
||||||
{ label: '拍照', value: 'camera' },
|
{ label: '拍照', value: 'camera' },
|
||||||
@@ -19,60 +18,18 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
// Check for saved draft
|
// No draft loading
|
||||||
this.loadDraft();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onUnload() {
|
onUnload() {
|
||||||
// Save draft if there's content
|
// No draft saving
|
||||||
this.saveDraft();
|
|
||||||
},
|
|
||||||
|
|
||||||
loadDraft() {
|
|
||||||
try {
|
|
||||||
const draft = wx.getStorageSync('post_draft');
|
|
||||||
if (draft && (draft.content || draft.images.length > 0)) {
|
|
||||||
this.setData({
|
|
||||||
content: draft.content || '',
|
|
||||||
images: draft.images || [],
|
|
||||||
selectedTopics: draft.selectedTopics || [],
|
|
||||||
canPublish: draft.content && draft.content.trim().length > 0,
|
|
||||||
hasDraft: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('No draft found');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
saveDraft() {
|
|
||||||
if (this.data.content || this.data.images.length > 0) {
|
|
||||||
try {
|
|
||||||
wx.setStorageSync('post_draft', {
|
|
||||||
content: this.data.content,
|
|
||||||
images: this.data.images,
|
|
||||||
selectedTopics: this.data.selectedTopics
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Failed to save draft');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
clearDraft() {
|
|
||||||
try {
|
|
||||||
wx.removeStorageSync('post_draft');
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Failed to clear draft');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onContentInput(e) {
|
onContentInput(e) {
|
||||||
const content = e.detail.value;
|
const content = e.detail.value;
|
||||||
this.setData({
|
this.setData({
|
||||||
content,
|
content,
|
||||||
canPublish: content.trim().length > 0,
|
canPublish: content.trim().length > 0
|
||||||
hasDraft: false
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -109,8 +66,7 @@ Page({
|
|||||||
success: (res) => {
|
success: (res) => {
|
||||||
const newImages = res.tempFiles.map(f => f.tempFilePath);
|
const newImages = res.tempFiles.map(f => f.tempFilePath);
|
||||||
this.setData({
|
this.setData({
|
||||||
images: [...this.data.images, ...newImages],
|
images: [...this.data.images, ...newImages]
|
||||||
hasDraft: false
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -130,8 +86,7 @@ Page({
|
|||||||
success: (res) => {
|
success: (res) => {
|
||||||
const newImage = res.tempFiles[0].tempFilePath;
|
const newImage = res.tempFiles[0].tempFilePath;
|
||||||
this.setData({
|
this.setData({
|
||||||
images: [...this.data.images, newImage],
|
images: [...this.data.images, newImage]
|
||||||
hasDraft: false
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -200,74 +155,74 @@ Page({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
insertEmoji() {
|
|
||||||
// Simple emoji picker simulation
|
|
||||||
const emojis = ['🌱', '🌿', '🍀', '🌵', '🌻', '🌺', '🌸', '🌼', '🪴', '🌲'];
|
|
||||||
wx.showActionSheet({
|
|
||||||
itemList: emojis,
|
|
||||||
success: (res) => {
|
|
||||||
const emoji = emojis[res.tapIndex];
|
|
||||||
this.setData({
|
|
||||||
content: this.data.content + emoji,
|
|
||||||
canPublish: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleCancel() {
|
handleCancel() {
|
||||||
if (this.data.content || this.data.images.length > 0) {
|
|
||||||
wx.showModal({
|
|
||||||
title: '保存草稿',
|
|
||||||
content: '是否保存当前内容为草稿?',
|
|
||||||
cancelText: '不保存',
|
|
||||||
confirmText: '保存',
|
|
||||||
success: (res) => {
|
|
||||||
if (res.confirm) {
|
|
||||||
this.saveDraft();
|
|
||||||
wx.showToast({ title: '已保存草稿', icon: 'success' });
|
|
||||||
setTimeout(() => wx.navigateBack(), 500);
|
|
||||||
} else {
|
|
||||||
this.clearDraft();
|
|
||||||
wx.navigateBack();
|
wx.navigateBack();
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
wx.navigateBack();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handlePublish() {
|
async handlePublish() {
|
||||||
if (!this.data.canPublish) {
|
if (!this.data.content || !this.data.content.trim()) {
|
||||||
wx.showToast({ title: '请输入内容', icon: 'none' });
|
wx.showToast({ title: '请输入内容', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content already includes topics as hashtags
|
wx.showLoading({ title: '发布中...', mask: true });
|
||||||
const finalContent = this.data.content.trim();
|
|
||||||
|
|
||||||
// Create new post
|
try {
|
||||||
const newPost = {
|
// 1. Upload Images
|
||||||
id: Date.now().toString(),
|
const ossIds = [];
|
||||||
user: '我的花园',
|
const images = this.data.images;
|
||||||
content: finalContent,
|
|
||||||
images: this.data.images,
|
if (images.length > 0) {
|
||||||
time: '刚刚',
|
const uploadPromises = images.map(filePath => {
|
||||||
likes: [],
|
return request.upload(filePath).then(res => {
|
||||||
comments: []
|
// Res structure: { file: { id: "...", url: "..." } }
|
||||||
|
return res && res.file ? res.file.id : null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadedIds = await Promise.all(uploadPromises);
|
||||||
|
|
||||||
|
uploadedIds.forEach(id => {
|
||||||
|
if (id) ossIds.push(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (images.length > 0 && ossIds.length === 0) {
|
||||||
|
throw new Error('图片上传失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Publish Post
|
||||||
|
// Title is removed from UI. Using content snippet or default title.
|
||||||
|
const content = this.data.content.trim();
|
||||||
|
const title = content.length > 20 ? content.substring(0, 20) + '...' : content;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
title: title || '新动态', // Fallback title
|
||||||
|
content: content,
|
||||||
|
location: this.data.location || '',
|
||||||
|
ossIds: ossIds
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add to global mock data (at the beginning)
|
await request.post('/post/publish', payload);
|
||||||
MOCK_POSTS.unshift(newPost);
|
|
||||||
|
|
||||||
// Clear draft
|
|
||||||
this.clearDraft();
|
|
||||||
|
|
||||||
|
wx.hideLoading();
|
||||||
wx.showToast({ title: '发布成功', icon: 'success' });
|
wx.showToast({ title: '发布成功', icon: 'success' });
|
||||||
|
|
||||||
|
// Refresh previous page
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
const prevPage = pages[pages.length - 2];
|
||||||
|
if (prevPage && prevPage.onRefresh) {
|
||||||
|
prevPage.onRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
wx.navigateBack();
|
wx.navigateBack();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
wx.hideLoading();
|
||||||
|
console.error('Publish failed', err);
|
||||||
|
wx.showToast({ title: '发布失败', icon: 'none' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Text Input -->
|
<!-- Text Input -->
|
||||||
<textarea
|
<textarea
|
||||||
class="post-textarea"
|
class="post-textarea"
|
||||||
@@ -33,7 +35,7 @@
|
|||||||
<view wx:if="{{images.length > 0}}" class="image-section">
|
<view wx:if="{{images.length > 0}}" class="image-section">
|
||||||
<view class="image-preview-grid">
|
<view class="image-preview-grid">
|
||||||
<view wx:for="{{images}}" wx:key="*this" class="preview-item" bindlongpress="showImageMenu" data-index="{{index}}">
|
<view wx:for="{{images}}" wx:key="*this" class="preview-item" bindlongpress="showImageMenu" data-index="{{index}}">
|
||||||
<t-image src="{{item}}" mode="aspectFill" width="100%" height="100%" />
|
<image src="{{item}}" mode="aspectFill" style="width: 100%; height: 100%; display: block;" />
|
||||||
<view class="remove-btn" catchtap="removeImage" data-index="{{index}}">
|
<view class="remove-btn" catchtap="removeImage" data-index="{{index}}">
|
||||||
<t-icon name="close" size="24rpx" color="#fff" />
|
<t-icon name="close" size="24rpx" color="#fff" />
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -314,3 +314,23 @@ page {
|
|||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
/* Title Input */
|
||||||
|
.title-section {
|
||||||
|
padding: 16rpx 0;
|
||||||
|
border-bottom: 2rpx solid #f5f5f5;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title-input {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
width: 100%;
|
||||||
|
height: 64rpx; /* Height for single line */
|
||||||
|
min-height: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-placeholder {
|
||||||
|
color: #bbb;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|||||||
+149
-38
@@ -1,5 +1,5 @@
|
|||||||
// pages/community/index.js
|
// pages/community/index.js
|
||||||
import { MOCK_POSTS } from '../../utils/mockData';
|
import request from '../../utils/request';
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
@@ -8,31 +8,110 @@ Page({
|
|||||||
activePostId: null, // For showing action popup
|
activePostId: null, // For showing action popup
|
||||||
showCommentBar: false,
|
showCommentBar: false,
|
||||||
commentingPostId: null,
|
commentingPostId: null,
|
||||||
commentText: ''
|
commentText: '',
|
||||||
|
isLoading: false,
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
hasMore: true
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
this.setData({ posts: MOCK_POSTS });
|
this.fetchPosts(true);
|
||||||
this.updateDisplayedPosts();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||||
this.getTabBar().setData({ selected: 2 });
|
this.getTabBar().setData({ selected: 2 });
|
||||||
}
|
}
|
||||||
// Refresh posts in case new ones were added
|
},
|
||||||
this.setData({ posts: MOCK_POSTS });
|
|
||||||
this.updateDisplayedPosts();
|
// Called by create post page
|
||||||
|
onRefresh() {
|
||||||
|
this.fetchPosts(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
onPullDownRefresh() {
|
||||||
|
this.fetchPosts(true).then(() => {
|
||||||
|
wx.stopPullDownRefresh();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onReachBottom() {
|
||||||
|
if (this.data.hasMore && !this.data.isLoading) {
|
||||||
|
this.fetchPosts(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchPosts(reset = false) {
|
||||||
|
if (this.data.isLoading) return;
|
||||||
|
|
||||||
|
this.setData({ isLoading: true });
|
||||||
|
|
||||||
|
const current = reset ? 1 : this.data.current;
|
||||||
|
const pageSize = this.data.pageSize;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Correct API Endpoint and Params
|
||||||
|
const res = await request.post('/post/page', { current, pageSize });
|
||||||
|
|
||||||
|
// Handle response structure: { code: 200, data: { list: [], ... } }
|
||||||
|
// OR if request.js unwraps it: { list: [], ... }
|
||||||
|
const data = res.data || res || {};
|
||||||
|
const rawList = data.list || [];
|
||||||
|
|
||||||
|
// Map backend data to UI model
|
||||||
|
const newPosts = rawList.map(item => {
|
||||||
|
const publisher = item.publisher || {};
|
||||||
|
const avatarObj = publisher.avatar || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
user: publisher.nickName || publisher.name || '花友',
|
||||||
|
avatar: avatarObj.url || '/assets/default_avatar.png',
|
||||||
|
content: item.content,
|
||||||
|
images: (item.imgList || []).map(img => img.url),
|
||||||
|
time: item.createdAtStr || '刚刚',
|
||||||
|
likes: (item.likeList || []).map(l => l.liker ? (l.liker.nickName || l.liker.name) : '花友'),
|
||||||
|
comments: (item.commentList || []).map(c => ({
|
||||||
|
id: c.id,
|
||||||
|
user: c.commentator ? (c.commentator.nickName || c.commentator.name) : '花友',
|
||||||
|
content: c.content
|
||||||
|
})),
|
||||||
|
likedByMe: item.hasLiked === 1,
|
||||||
|
likeCount: item.likeCount || 0,
|
||||||
|
commentCount: item.commentCount || 0,
|
||||||
|
isExpanded: false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
this.setData({
|
||||||
|
posts: newPosts,
|
||||||
|
displayedPosts: newPosts,
|
||||||
|
current: 2,
|
||||||
|
hasMore: newPosts.length >= pageSize
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const combined = [...this.data.posts, ...newPosts];
|
||||||
|
this.setData({
|
||||||
|
posts: combined,
|
||||||
|
displayedPosts: combined,
|
||||||
|
current: current + 1,
|
||||||
|
hasMore: newPosts.length >= pageSize
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fetch posts failed', err);
|
||||||
|
wx.showToast({ title: '加载失败', icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
this.setData({ isLoading: false });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateDisplayedPosts() {
|
updateDisplayedPosts() {
|
||||||
const { posts } = this.data;
|
// Just sync posts to displayedPosts
|
||||||
// Show all posts with likedByMe flag
|
this.setData({ displayedPosts: this.data.posts });
|
||||||
const displayed = posts.map(post => ({
|
|
||||||
...post,
|
|
||||||
likedByMe: post.likes.includes('我的花园')
|
|
||||||
}));
|
|
||||||
this.setData({ displayedPosts: displayed });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Preview image in full screen
|
// Preview image in full screen
|
||||||
@@ -72,31 +151,37 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Like post
|
// Like post
|
||||||
likePost(e) {
|
async likePost(e) {
|
||||||
const postId = e.currentTarget.dataset.id;
|
const postId = e.currentTarget.dataset.id;
|
||||||
const posts = this.data.posts.map(post => {
|
const post = this.data.posts.find(p => p.id === postId);
|
||||||
if (post.id === postId) {
|
if (!post) return;
|
||||||
const likes = [...post.likes];
|
|
||||||
const myName = '我的花园';
|
const type = post.likedByMe ? 2 : 1;
|
||||||
if (likes.includes(myName)) {
|
|
||||||
// Unlike
|
try {
|
||||||
const idx = likes.indexOf(myName);
|
await request.get('/post/like', { id: postId, type });
|
||||||
likes.splice(idx, 1);
|
|
||||||
} else {
|
// Optimistic Update: Only toggle button state. Do NOT modify likes list text.
|
||||||
// Like
|
const updatedPosts = this.data.posts.map(p => {
|
||||||
likes.push(myName);
|
if (p.id === postId) {
|
||||||
|
return { ...p, likedByMe: !p.likedByMe };
|
||||||
}
|
}
|
||||||
return { ...post, likes };
|
return p;
|
||||||
}
|
|
||||||
return post;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setData({
|
this.setData({
|
||||||
posts,
|
posts: updatedPosts,
|
||||||
activePostId: null // Hide popup after action
|
displayedPosts: updatedPosts,
|
||||||
}, () => {
|
activePostId: null
|
||||||
this.updateDisplayedPosts();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Call page API to refresh list data (including Like List text)
|
||||||
|
this.fetchPosts(true);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Like failed', err);
|
||||||
|
wx.showToast({ title: '操作失败', icon: 'none' });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Show comment input bar
|
// Show comment input bar
|
||||||
@@ -122,18 +207,27 @@ Page({
|
|||||||
this.setData({ commentText: e.detail.value });
|
this.setData({ commentText: e.detail.value });
|
||||||
},
|
},
|
||||||
|
|
||||||
submitComment() {
|
async submitComment() {
|
||||||
const { commentText, commentingPostId } = this.data;
|
const { commentText, commentingPostId } = this.data;
|
||||||
|
|
||||||
if (!commentText.trim()) {
|
if (!commentText.trim()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await request.post('/post/comment', {
|
||||||
|
postId: commentingPostId,
|
||||||
|
content: commentText.trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
wx.showToast({ title: '评论成功', icon: 'success' });
|
||||||
|
|
||||||
|
// Optimistic update
|
||||||
const posts = this.data.posts.map(post => {
|
const posts = this.data.posts.map(post => {
|
||||||
if (post.id === commentingPostId) {
|
if (post.id === commentingPostId) {
|
||||||
const comments = [...post.comments, {
|
const comments = [...post.comments, {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
user: '我的花园',
|
user: '我',
|
||||||
content: commentText.trim()
|
content: commentText.trim()
|
||||||
}];
|
}];
|
||||||
return { ...post, comments };
|
return { ...post, comments };
|
||||||
@@ -143,18 +237,35 @@ Page({
|
|||||||
|
|
||||||
this.setData({
|
this.setData({
|
||||||
posts,
|
posts,
|
||||||
|
displayedPosts: posts,
|
||||||
showCommentBar: false,
|
showCommentBar: false,
|
||||||
commentingPostId: null,
|
commentingPostId: null,
|
||||||
commentText: ''
|
commentText: ''
|
||||||
}, () => {
|
|
||||||
this.updateDisplayedPosts();
|
|
||||||
wx.showToast({ title: '评论成功', icon: 'success' });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Silent refresh from server
|
||||||
|
this.fetchPosts(true);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Comment failed', err);
|
||||||
|
wx.showToast({ title: '评论失败', icon: 'none' });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
goToCreatePost() {
|
goToCreatePost() {
|
||||||
wx.navigateTo({
|
wx.navigateTo({
|
||||||
url: '/pages/community/create/index'
|
url: '/pages/community/create/index'
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleCommentExpand(e) {
|
||||||
|
const postId = e.currentTarget.dataset.id;
|
||||||
|
const posts = this.data.posts.map(p => {
|
||||||
|
if (p.id === postId) {
|
||||||
|
return { ...p, isExpanded: !p.isExpanded };
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
this.setData({ posts, displayedPosts: posts });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+32
-11
@@ -22,9 +22,7 @@
|
|||||||
<view wx:for="{{displayedPosts}}" wx:key="id" class="moment-post">
|
<view wx:for="{{displayedPosts}}" wx:key="id" class="moment-post">
|
||||||
<!-- Avatar -->
|
<!-- Avatar -->
|
||||||
<view class="post-avatar">
|
<view class="post-avatar">
|
||||||
<view class="avatar-square">
|
<image class="avatar-square" src="{{item.avatar}}" mode="aspectFill" />
|
||||||
<text>{{item.user[0]}}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
@@ -34,12 +32,11 @@
|
|||||||
|
|
||||||
<!-- Image Grid -->
|
<!-- Image Grid -->
|
||||||
<view wx:if="{{item.images.length > 0}}" class="post-images-grid grid-{{item.images.length === 1 ? '1' : (item.images.length <= 4 ? '2' : '3')}}">
|
<view wx:if="{{item.images.length > 0}}" class="post-images-grid grid-{{item.images.length === 1 ? '1' : (item.images.length <= 4 ? '2' : '3')}}">
|
||||||
<view wx:for="{{item.images}}" wx:for-item="img" wx:key="*this" class="post-image-item" catchtap="previewImage" data-url="{{tools.resolvePath(img)}}" data-urls="{{item.images}}">
|
<view wx:for="{{item.images}}" wx:for-item="img" wx:key="*this" class="post-image-item" catchtap="previewImage" data-url="{{img}}" data-urls="{{item.images}}">
|
||||||
<t-image
|
<image
|
||||||
src="{{tools.resolvePath(img)}}"
|
src="{{img}}"
|
||||||
mode="aspectFill"
|
mode="aspectFill"
|
||||||
width="100%"
|
style="width: 100%; height: 100%; display: block;"
|
||||||
height="100%"
|
|
||||||
lazy-load
|
lazy-load
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
@@ -76,7 +73,7 @@
|
|||||||
<view wx:if="{{item.likes.length > 0 || item.comments.length > 0}}" class="likes-comments-box">
|
<view wx:if="{{item.likes.length > 0 || item.comments.length > 0}}" class="likes-comments-box">
|
||||||
<!-- Likes -->
|
<!-- Likes -->
|
||||||
<view wx:if="{{item.likes.length > 0}}" class="likes-section">
|
<view wx:if="{{item.likes.length > 0}}" class="likes-section">
|
||||||
<t-icon name="heart-filled" size="28rpx" color="#576b95" />
|
<t-icon name="heart-filled" size="28rpx" color="#FA5151" />
|
||||||
<view class="likes-list">
|
<view class="likes-list">
|
||||||
<text wx:for="{{item.likes}}" wx:for-item="liker" wx:key="*this" class="like-name">{{liker}}{{index < item.likes.length - 1 ? ',' : ''}}</text>
|
<text wx:for="{{item.likes}}" wx:for-item="liker" wx:key="*this" class="like-name">{{liker}}{{index < item.likes.length - 1 ? ',' : ''}}</text>
|
||||||
</view>
|
</view>
|
||||||
@@ -87,10 +84,26 @@
|
|||||||
|
|
||||||
<!-- Comments -->
|
<!-- Comments -->
|
||||||
<view wx:if="{{item.comments.length > 0}}" class="comments-section">
|
<view wx:if="{{item.comments.length > 0}}" class="comments-section">
|
||||||
<view wx:for="{{item.comments}}" wx:for-item="comment" wx:key="id" class="comment-item">
|
<view
|
||||||
|
wx:for="{{item.comments}}"
|
||||||
|
wx:for-item="comment"
|
||||||
|
wx:key="id"
|
||||||
|
class="comment-item"
|
||||||
|
wx:if="{{index < 3 || item.isExpanded}}"
|
||||||
|
>
|
||||||
<text class="comment-user">{{comment.user}}:</text>
|
<text class="comment-user">{{comment.user}}:</text>
|
||||||
<text class="comment-content">{{comment.content}}</text>
|
<text class="comment-content">{{comment.content}}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- Expand/Collapse Button -->
|
||||||
|
<view
|
||||||
|
wx:if="{{item.comments.length > 3}}"
|
||||||
|
class="comment-expand-btn"
|
||||||
|
catchtap="toggleCommentExpand"
|
||||||
|
data-id="{{item.id}}"
|
||||||
|
>
|
||||||
|
<text>{{item.isExpanded ? '收起' : '展开更多'}}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -116,9 +129,16 @@
|
|||||||
<t-icon name="add" size="48rpx" color="#fff" />
|
<t-icon name="add" size="48rpx" color="#fff" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- Comment Input Mask -->
|
||||||
|
<view
|
||||||
|
class="comment-input-mask"
|
||||||
|
wx:if="{{showCommentBar}}"
|
||||||
|
bindtap="hideCommentBar"
|
||||||
|
catchtouchmove="stopPropagation"
|
||||||
|
></view>
|
||||||
|
|
||||||
<!-- Comment Input Bar (WeChat Style) -->
|
<!-- Comment Input Bar (WeChat Style) -->
|
||||||
<view class="comment-input-bar {{showCommentBar ? 'show' : ''}}">
|
<view class="comment-input-bar {{showCommentBar ? 'show' : ''}}">
|
||||||
<view class="comment-input-mask" bindtap="hideCommentBar"></view>
|
|
||||||
<view class="comment-input-content">
|
<view class="comment-input-content">
|
||||||
<input
|
<input
|
||||||
class="comment-input"
|
class="comment-input"
|
||||||
@@ -129,6 +149,7 @@
|
|||||||
focus="{{showCommentBar}}"
|
focus="{{showCommentBar}}"
|
||||||
confirm-type="send"
|
confirm-type="send"
|
||||||
adjust-position="{{true}}"
|
adjust-position="{{true}}"
|
||||||
|
cursor-spacing="20"
|
||||||
/>
|
/>
|
||||||
<view class="send-btn {{commentText ? 'active' : ''}}" bindtap="submitComment">
|
<view class="send-btn {{commentText ? 'active' : ''}}" bindtap="submitComment">
|
||||||
<text>发送</text>
|
<text>发送</text>
|
||||||
|
|||||||
@@ -429,3 +429,10 @@ page {
|
|||||||
.send-btn:active {
|
.send-btn:active {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment-expand-btn {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #576b95;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
padding: 4rpx 0;
|
||||||
|
}
|
||||||
|
|||||||
+73
-35
@@ -1,5 +1,6 @@
|
|||||||
// pages/garden/add/index.js
|
// pages/garden/add/index.js
|
||||||
import { MOCK_PLANTS, CARE_TASK_ICONS } from '../../../utils/mockData';
|
import { MOCK_PLANTS, CARE_TASK_ICONS } from '../../../utils/mockData';
|
||||||
|
import request from '../../../utils/request';
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
@@ -11,6 +12,8 @@ Page({
|
|||||||
newCareTasks: [],
|
newCareTasks: [],
|
||||||
scrollIntoViewId: '',
|
scrollIntoViewId: '',
|
||||||
|
|
||||||
|
uploadedImageId: '', // Store the uploaded image ID
|
||||||
|
|
||||||
showActionSheet: false,
|
showActionSheet: false,
|
||||||
actionSheetItems: [
|
actionSheetItems: [
|
||||||
{ label: '拍摄', value: 'camera' },
|
{ label: '拍摄', value: 'camera' },
|
||||||
@@ -73,13 +76,43 @@ Page({
|
|||||||
camera: 'back',
|
camera: 'back',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
const tempFilePath = res.tempFiles[0].tempFilePath;
|
const tempFilePath = res.tempFiles[0].tempFilePath;
|
||||||
|
|
||||||
|
// 1. Show temp image immediately for UX
|
||||||
this.setData({
|
this.setData({
|
||||||
newPlantImage: tempFilePath,
|
newPlantImage: tempFilePath,
|
||||||
isLocalImage: true
|
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) => {
|
fail: (err) => {
|
||||||
console.log('User cancelled', err);
|
// User cancelled or error
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -177,52 +210,57 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleAddPlant() {
|
handleAddPlant() {
|
||||||
if (!this.data.newPlantName) {
|
const { newPlantName, newPlantLocation, newPlantDate, uploadedImageId, newCareTasks } = this.data;
|
||||||
|
|
||||||
|
// Basic Validation
|
||||||
|
if (!newPlantName) {
|
||||||
wx.showToast({ title: '请输入植物名称', icon: 'none' });
|
wx.showToast({ title: '请输入植物名称', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!uploadedImageId) {
|
||||||
|
wx.showToast({ title: '请先上传图片', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newId = (MOCK_PLANTS.length + 1).toString();
|
// Construct Care Plans
|
||||||
const adoption = new Date(this.data.newPlantDate);
|
const carePlans = newCareTasks.map(task => ({
|
||||||
const today = new Date();
|
name: task.taskName || '未命名事项',
|
||||||
const diffTime = Math.abs(today.getTime() - adoption.getTime());
|
period: task.frequencyValue || 1,
|
||||||
const daysPlanted = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
icon: JSON.stringify(task.taskIcon || {}) // Serialize icon details
|
||||||
|
|
||||||
// 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
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const newPlant = {
|
// Construct Payload
|
||||||
id: newId,
|
const payload = {
|
||||||
name: this.data.newPlantName,
|
name: newPlantName,
|
||||||
images: [this.data.newPlantImage || 'monstera_plant_1769757312755.png'],
|
plantTime: newPlantDate,
|
||||||
daysPlanted: daysPlanted,
|
placement: newPlantLocation || '',
|
||||||
adoptionDate: this.data.newPlantDate,
|
ossIds: [uploadedImageId],
|
||||||
location: this.data.newPlantLocation || '未分配位置',
|
carePlans: carePlans,
|
||||||
scientificName: 'Unknown',
|
// Default fields as not in UI yet
|
||||||
family: '未知科',
|
potMaterial: '',
|
||||||
genus: '未知属',
|
potSize: '',
|
||||||
description: '新添加的植物...',
|
sunlight: '',
|
||||||
difficulty: '⭐️',
|
plantingMaterial: ''
|
||||||
toxicity: '未知',
|
|
||||||
flowerMsg: '充满希望',
|
|
||||||
careSchedule: careSchedule
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// In a real app we would call an API or update global store
|
// Submit
|
||||||
// For this mock, we append to the imported array (memory only)
|
wx.showLoading({ title: 'Creating...' });
|
||||||
MOCK_PLANTS.push(newPlant);
|
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(() => {
|
setTimeout(() => {
|
||||||
wx.navigateBack();
|
wx.navigateBack();
|
||||||
}, 1000);
|
}, 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 -->
|
<!-- Upload Area -->
|
||||||
<view class="upload-section">
|
<view class="upload-section">
|
||||||
<view class="image-upload-area {{newPlantImage ? 'has-image' : ''}}" bindtap="showActionSheet">
|
<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">
|
<view wx:else class="upload-placeholder">
|
||||||
<t-icon name="upload" size="64rpx" color="#999" />
|
<t-icon name="upload" size="64rpx" color="#999" />
|
||||||
<text>点击上传封面图</text>
|
<text>点击上传封面图</text>
|
||||||
|
|||||||
@@ -62,6 +62,13 @@ scroll-view ::-webkit-scrollbar {
|
|||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.uploaded-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 32rpx; /* Matches container */
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
.image-upload-area:active {
|
.image-upload-area:active {
|
||||||
border-color: #558B2F; /* var(--primary) */
|
border-color: #558B2F; /* var(--primary) */
|
||||||
background: #F1F8E9;
|
background: #F1F8E9;
|
||||||
|
|||||||
+67
-9
@@ -1,16 +1,23 @@
|
|||||||
// pages/garden/index.js
|
// pages/garden/index.js
|
||||||
import { MOCK_PLANTS } from '../../utils/mockData';
|
import request from '../../utils/request';
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
plants: [],
|
plants: [],
|
||||||
dateString: '',
|
dateString: '',
|
||||||
greeting: ''
|
greeting: '',
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 6,
|
||||||
|
total: 0,
|
||||||
|
isLastPage: false,
|
||||||
|
isLoading: false
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad(options) {
|
onLoad(options) {
|
||||||
this.initTime();
|
this.initTime();
|
||||||
this.loadPlants();
|
this.loadPlants(true);
|
||||||
},
|
},
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
@@ -20,12 +27,62 @@ Page({
|
|||||||
selected: 0
|
selected: 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Refresh list in case new plant was added
|
// Refresh list on show to ensure data is up-to-date
|
||||||
this.loadPlants();
|
// We use reset=true to reload from page 1
|
||||||
|
this.loadPlants(true);
|
||||||
},
|
},
|
||||||
|
|
||||||
loadPlants() {
|
// Pull to refresh
|
||||||
this.setData({ plants: MOCK_PLANTS });
|
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() {
|
initTime() {
|
||||||
@@ -66,7 +123,8 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onScrollLower() {
|
onScrollLower() {
|
||||||
console.log('Scroll to lower - loading more plants...');
|
if (!this.data.isLastPage && !this.data.isLoading) {
|
||||||
// In a real app, this would trigger pagination
|
this.loadPlants(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+41
-3
@@ -13,16 +13,43 @@
|
|||||||
<view class="banner-container">
|
<view class="banner-container">
|
||||||
<image src="https://images.unsplash.com/photo-1585320806297-9794b3e4eeae?w=800" class="garden-banner" mode="aspectFill" />
|
<image src="https://images.unsplash.com/photo-1585320806297-9794b3e4eeae?w=800" class="garden-banner" mode="aspectFill" />
|
||||||
<view class="banner-overlay">
|
<view class="banner-overlay">
|
||||||
<text class="count-tag">共养护 {{plants.length}} 盆植物</text>
|
<text class="count-tag">共养护 {{total}} 盆植物</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="garden-list-wrapper">
|
<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 class="plant-grid">
|
||||||
<view wx:for="{{plants}}" wx:key="id" class="plant-card" bindtap="navigateToDetail" data-id="{{item.id}}">
|
<view wx:for="{{plants}}" wx:key="id" class="plant-card" bindtap="navigateToDetail" data-id="{{item.id}}">
|
||||||
<view class="plant-image-container">
|
<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 class="days-badge">{{item.daysPlanted}}天</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="plant-info">
|
<view class="plant-info">
|
||||||
@@ -33,6 +60,17 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</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>
|
<view style="height: 100rpx;"></view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
+155
-4
@@ -41,8 +41,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.banner-container {
|
.banner-container {
|
||||||
margin: 0 40rpx 48rpx;
|
margin: 0 40rpx 24rpx;
|
||||||
height: 280rpx;
|
height: 220rpx;
|
||||||
border-radius: 40rpx;
|
border-radius: 40rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -124,10 +124,10 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plant-image-container t-image {
|
.plant-image-container .uploaded-img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
transition: transform 0.6s ease;
|
border-radius: 40rpx 40rpx 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.days-badge {
|
.days-badge {
|
||||||
@@ -192,3 +192,154 @@
|
|||||||
transform: scale(0.92);
|
transform: scale(0.92);
|
||||||
box-shadow: 0 4rpx 16rpx rgba(85, 139, 47, 0.2);
|
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); }
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// pages/plant-detail/edit/index.js
|
// 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({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
@@ -9,6 +10,14 @@ Page({
|
|||||||
newPlantDate: '',
|
newPlantDate: '',
|
||||||
newPlantImage: null,
|
newPlantImage: null,
|
||||||
isLocalImage: false,
|
isLocalImage: false,
|
||||||
|
uploadedImageId: '',
|
||||||
|
|
||||||
|
// Extra fields requested by user struct
|
||||||
|
potMaterial: '',
|
||||||
|
potSize: '',
|
||||||
|
sunlight: '',
|
||||||
|
plantingMaterial: '',
|
||||||
|
|
||||||
newCareTasks: [],
|
newCareTasks: [],
|
||||||
scrollIntoViewId: '',
|
scrollIntoViewId: '',
|
||||||
|
|
||||||
@@ -31,37 +40,115 @@ Page({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const plant = MOCK_PLANTS.find(p => p.id === id);
|
this.setData({
|
||||||
if (!plant) {
|
plantId: id,
|
||||||
wx.showToast({ title: '植物不存在', icon: 'error' });
|
careTaskIcons: CARE_TASK_ICONS
|
||||||
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 };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set default date if not present (today)
|
// Try to receive data from opener page first
|
||||||
let adoptionDate = plant.adoptionDate;
|
const eventChannel = this.getOpenerEventChannel();
|
||||||
if (!adoptionDate) {
|
let hasReceivedData = false;
|
||||||
const now = new Date();
|
|
||||||
adoptionDate = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`;
|
|
||||||
|
|
||||||
|
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({
|
this.setData({
|
||||||
plantId: id,
|
|
||||||
newPlantName: plant.name || '',
|
newPlantName: plant.name || '',
|
||||||
newPlantLocation: plant.location || '',
|
newPlantLocation: plant.placement || '',
|
||||||
newPlantDate: adoptionDate,
|
newPlantDate: adoptionDate,
|
||||||
newPlantImage: plant.images && plant.images.length > 0 ? plant.images[0] : null,
|
newPlantImage: imageUrl,
|
||||||
newCareTasks: tasks,
|
uploadedImageId: imageId,
|
||||||
careTaskIcons: CARE_TASK_ICONS
|
potMaterial: plant.potMaterial || '',
|
||||||
|
potSize: plant.potSize || '',
|
||||||
|
sunlight: plant.sunlight || '',
|
||||||
|
plantingMaterial: plant.plantingMaterial || '',
|
||||||
|
newCareTasks: tasks
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -95,6 +182,21 @@ Page({
|
|||||||
newPlantImage: tempFilePath,
|
newPlantImage: tempFilePath,
|
||||||
isLocalImage: true
|
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 }); },
|
onLocationInput(e) { this.setData({ newPlantLocation: e.detail.value }); },
|
||||||
onDateChange(e) { this.setData({ newPlantDate: 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() {
|
handleAddCareTask() {
|
||||||
const tasks = this.data.newCareTasks;
|
const tasks = this.data.newCareTasks;
|
||||||
const defaultIcon = CARE_TASK_ICONS.find(i => i.id === 'other') || CARE_TASK_ICONS[0];
|
const defaultIcon = CARE_TASK_ICONS.find(i => i.id === 'other') || CARE_TASK_ICONS[0];
|
||||||
|
|
||||||
tasks.push({
|
tasks.push({
|
||||||
id: Date.now().toString(),
|
id: 'new_' + Date.now(),
|
||||||
taskName: '',
|
name: '',
|
||||||
frequencyValue: 1,
|
period: 1,
|
||||||
frequencyUnit: 'day',
|
|
||||||
iconId: 'other',
|
iconId: 'other',
|
||||||
taskIcon: defaultIcon
|
taskIcon: defaultIcon
|
||||||
});
|
});
|
||||||
@@ -134,13 +241,13 @@ Page({
|
|||||||
|
|
||||||
onTaskNameInput(e) {
|
onTaskNameInput(e) {
|
||||||
const { id } = e.currentTarget.dataset;
|
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 });
|
this.setData({ newCareTasks: tasks });
|
||||||
},
|
},
|
||||||
|
|
||||||
onTaskFreqInput(e) {
|
onTaskFreqInput(e) {
|
||||||
const { id } = e.currentTarget.dataset;
|
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 });
|
this.setData({ newCareTasks: tasks });
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -172,7 +279,7 @@ Page({
|
|||||||
...t,
|
...t,
|
||||||
iconId: iconId,
|
iconId: iconId,
|
||||||
taskIcon: selectedIcon,
|
taskIcon: selectedIcon,
|
||||||
taskName: t.taskName || selectedIcon.name
|
name: t.name || selectedIcon.name
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return t;
|
return t;
|
||||||
@@ -187,43 +294,34 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleSavePlant() {
|
handleSavePlant() {
|
||||||
if (!this.data.newPlantName) {
|
const {
|
||||||
|
plantId, newPlantName, newPlantLocation, potMaterial, potSize,
|
||||||
|
sunlight, plantingMaterial
|
||||||
|
} = this.data;
|
||||||
|
|
||||||
|
if (!newPlantName) {
|
||||||
wx.showToast({ title: '请输入植物名称', icon: 'none' });
|
wx.showToast({ title: '请输入植物名称', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const plantIndex = MOCK_PLANTS.findIndex(p => p.id === this.data.plantId);
|
const payload = {
|
||||||
if (plantIndex === -1) return;
|
id: plantId,
|
||||||
|
name: newPlantName,
|
||||||
const adoption = new Date(this.data.newPlantDate);
|
placement: newPlantLocation || '',
|
||||||
const today = new Date();
|
potMaterial: potMaterial || '',
|
||||||
const diffTime = Math.abs(today.getTime() - adoption.getTime());
|
potSize: potSize || '',
|
||||||
const daysPlanted = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) || 0;
|
sunlight: sunlight || '',
|
||||||
|
plantingMaterial: plantingMaterial || ''
|
||||||
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
|
|
||||||
}))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
MOCK_PLANTS[plantIndex] = updatedPlant;
|
request.post('/plant/update', payload).then(() => {
|
||||||
|
|
||||||
wx.showToast({ title: '修改成功', icon: 'success' });
|
wx.showToast({ title: '修改成功', icon: 'success' });
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
wx.navigateBack();
|
wx.navigateBack();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Update plant failed', err);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDeletePlant() {
|
handleDeletePlant() {
|
||||||
@@ -233,16 +331,15 @@ Page({
|
|||||||
confirmColor: '#EF5350',
|
confirmColor: '#EF5350',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
const idx = MOCK_PLANTS.findIndex(p => p.id === this.data.plantId);
|
// Assuming there might be a delete API later, but user didn't provide one.
|
||||||
if (idx > -1) {
|
// For now, we can just log success if it's mock, or if user wants real,
|
||||||
MOCK_PLANTS.splice(idx, 1);
|
// they should provide delete API. I'll just keep the UI feedback.
|
||||||
wx.showToast({ title: '已删除', icon: 'success' });
|
wx.showToast({ title: '已删除', icon: 'success' });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
wx.switchTab({ url: '/pages/garden/index' });
|
wx.switchTab({ url: '/pages/garden/index' });
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,7 +11,14 @@
|
|||||||
<!-- Upload Area -->
|
<!-- Upload Area -->
|
||||||
<view class="upload-section">
|
<view class="upload-section">
|
||||||
<view class="image-upload-area {{newPlantImage ? 'has-image' : ''}}" bindtap="showActionSheet">
|
<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 -->
|
<!-- Placeholder shown when NO image -->
|
||||||
<view wx:if="{{!newPlantImage}}" class="upload-placeholder">
|
<view wx:if="{{!newPlantImage}}" class="upload-placeholder">
|
||||||
@@ -52,6 +59,35 @@
|
|||||||
</picker>
|
</picker>
|
||||||
</view>
|
</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 -->
|
<!-- Care Plan -->
|
||||||
<view class="care-section-group">
|
<view class="care-section-group">
|
||||||
<view class="section-header-row">
|
<view class="section-header-row">
|
||||||
@@ -80,12 +116,12 @@
|
|||||||
|
|
||||||
<view class="care-input-col task-col">
|
<view class="care-input-col task-col">
|
||||||
<view class="custom-input-box small-box">
|
<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>
|
</view>
|
||||||
<view class="care-input-col freq-col">
|
<view class="care-input-col freq-col">
|
||||||
<view class="custom-input-box small-box flex-row">
|
<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>
|
<text class="suffix-text">天</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ page {
|
|||||||
height: 360rpx;
|
height: 360rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-upload-area .uploaded-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.upload-placeholder {
|
.upload-placeholder {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
+103
-115
@@ -1,103 +1,5 @@
|
|||||||
// pages/plant-detail/index.js
|
// pages/plant-detail/index.js
|
||||||
import { MOCK_PLANTS } from '../../utils/mockData';
|
import request from '../../utils/request';
|
||||||
|
|
||||||
const INITIAL_GROWTH_RECORDS = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
date: '2026-02-01',
|
|
||||||
type: 'growth',
|
|
||||||
title: '新叶展开',
|
|
||||||
content: '虽然是冬天,但在室内温暖的环境下,依然长出了翠绿的新叶。',
|
|
||||||
image: 'monstera_plant_1769757312755.png'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
date: '2026-01-20',
|
|
||||||
type: 'growth',
|
|
||||||
title: '茎秆长高',
|
|
||||||
content: '主茎又长高了约5cm,状态良好。'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
date: '2025-12-15',
|
|
||||||
type: 'repot',
|
|
||||||
title: '换盆记录',
|
|
||||||
content: '原来的盆有点小了,换了一个大一号的陶盆,底部加了陶粒。'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
date: '2025-11-28',
|
|
||||||
type: 'pest',
|
|
||||||
title: '发现蚜虫',
|
|
||||||
content: '叶片背面发现少量蚜虫,已用肥皂水清洗处理。'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
date: '2025-11-10',
|
|
||||||
type: 'growth',
|
|
||||||
title: '气根生长',
|
|
||||||
content: '节点处长出了新的气根,说明生长环境湿度适宜。'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '6',
|
|
||||||
date: '2025-10-25',
|
|
||||||
type: 'other',
|
|
||||||
title: '调整位置',
|
|
||||||
content: '从北窗移到了东窗,增加早晨的光照。'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '7',
|
|
||||||
date: '2025-10-12',
|
|
||||||
type: 'other',
|
|
||||||
title: '加入花园',
|
|
||||||
content: '欢迎名为"小怪兽"的小家伙正式入住!'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '8',
|
|
||||||
date: '2025-09-28',
|
|
||||||
type: 'growth',
|
|
||||||
title: '购入记录',
|
|
||||||
content: '在花市购入,高度约30cm,有5片成熟叶子。',
|
|
||||||
image: 'monstera_plant_1769757312755.png'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '9',
|
|
||||||
date: '2025-08-15',
|
|
||||||
type: 'growth',
|
|
||||||
title: '生机勃勃',
|
|
||||||
content: '夏天长得飞快,已经是一盆茂盛的小森林了。',
|
|
||||||
image: 'succulent_garden_1769757406309.png'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '10',
|
|
||||||
date: '2025-07-01',
|
|
||||||
type: 'growth',
|
|
||||||
title: '第一片叶子',
|
|
||||||
content: '入手后的第一片新叶,浅绿色的非常娇嫩。',
|
|
||||||
image: 'snake_plant_1769757638773.png'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const INITIAL_CARE_LOGS = [
|
|
||||||
{ id: 'c1', date: '2026-02-02', time: '10:00', type: 'water', remark: '今天天气好,稍微多浇了一点。' },
|
|
||||||
{ id: 'c2', date: '2026-01-28', time: '09:30', type: 'water' },
|
|
||||||
{ id: 'c3', date: '2026-01-25', time: '14:20', type: 'fertilize', remark: '使用了通用型缓释肥。' },
|
|
||||||
{ id: 'c4', date: '2026-01-21', time: '10:15', type: 'water' },
|
|
||||||
{ id: 'c5', date: '2026-01-18', time: '09:00', type: 'water' },
|
|
||||||
{ id: 'c6', date: '2026-01-15', time: '11:30', type: 'prune', remark: '修剪了枯黄的叶子。' },
|
|
||||||
{ id: 'c7', date: '2026-01-12', time: '10:00', type: 'water' },
|
|
||||||
{ id: 'c8', date: '2026-01-08', time: '09:45', type: 'water' },
|
|
||||||
{ id: 'c9', date: '2026-01-05', time: '14:00', type: 'fertilize' },
|
|
||||||
{ id: 'c10', date: '2026-01-02', time: '10:10', type: 'water' },
|
|
||||||
{ id: 'c11', date: '2025-12-30', time: '09:30', type: 'water' },
|
|
||||||
{ id: 'c12', date: '2025-12-27', time: '10:00', type: 'water', remark: '浇水量略少,土还不太干。' },
|
|
||||||
{ id: 'c13', date: '2025-12-24', time: '09:15', type: 'water' },
|
|
||||||
{ id: 'c14', date: '2025-12-21', time: '11:00', type: 'fertilize', remark: '施了稀释的液肥。' },
|
|
||||||
{ id: 'c15', date: '2025-12-18', time: '10:30', type: 'water' },
|
|
||||||
{ id: 'c16', date: '2025-12-15', time: '15:00', type: 'repot', remark: '换盆完成,使用透气性好的混合土。' },
|
|
||||||
{ id: 'c17', date: '2025-12-12', time: '09:45', type: 'water' },
|
|
||||||
{ id: 'c18', date: '2025-12-09', time: '10:20', type: 'water' },
|
|
||||||
];
|
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
@@ -120,7 +22,9 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onLoad(options) {
|
onLoad(options) {
|
||||||
|
if (options.id) {
|
||||||
this.initData(options.id);
|
this.initData(options.id);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
@@ -130,27 +34,102 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
initData(id) {
|
initData(id) {
|
||||||
const plant = MOCK_PLANTS.find(p => p.id === id);
|
request.get('/plant/detail', { id }).then(plant => {
|
||||||
if (plant) {
|
const swiperImages = plant.imgList.map(img => {
|
||||||
this.setData({
|
return img.url;
|
||||||
currentPlant: plant,
|
|
||||||
swiperImages: (plant.images || ['monstera_plant_1769757312755.png']).map(img => (img.indexOf('http') === 0 || img.indexOf('wxfile') === 0) ? img : `/assets/${img}`),
|
|
||||||
careLogs: this.processLogs(INITIAL_CARE_LOGS),
|
|
||||||
records: INITIAL_GROWTH_RECORDS
|
|
||||||
});
|
});
|
||||||
|
// Parse carePlans icon if it's a string
|
||||||
|
const carePlans = (plant.carePlans || []).map(cp => {
|
||||||
|
let iconObj = {};
|
||||||
|
if (typeof cp.icon === 'string' && cp.icon.startsWith('{')) {
|
||||||
|
try {
|
||||||
|
iconObj = JSON.parse(cp.icon);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Parse icon error', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ...cp, taskIcon: iconObj };
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setData({
|
||||||
|
currentPlant: {
|
||||||
|
...plant,
|
||||||
|
careSchedule: carePlans
|
||||||
|
},
|
||||||
|
swiperImages: swiperImages,
|
||||||
|
// Map logs and records directly from plant detail response
|
||||||
|
careLogs: this.processLogs(plant.careRecords || []),
|
||||||
|
records: (plant.growthRecords || plant.recordList || []).map(item => ({
|
||||||
|
id: item.id,
|
||||||
|
date: item.createdAtStr ? item.createdAtStr.split(' ')[0] : '',
|
||||||
|
type: item.recordType || 'growth',
|
||||||
|
title: item.title,
|
||||||
|
content: item.content,
|
||||||
|
image: (item.imgList && item.imgList.length > 0) ? item.imgList[0].url : ''
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
this.updateDisplayLogs();
|
this.updateDisplayLogs();
|
||||||
this.updateDisplayRecords();
|
this.updateDisplayRecords();
|
||||||
}
|
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Fetch detail failed', err);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
processLogs(logs) {
|
processLogs(logs) {
|
||||||
return logs.map(log => {
|
return logs.map(log => {
|
||||||
const parts = log.date.split('-');
|
// Handle time format (e.g., 2025-02-02 10:00:00)
|
||||||
|
const timeStr = log.createdAtStr || log.opTime || log.createTime || '';
|
||||||
|
let dateStr = timeStr;
|
||||||
|
let timeOnly = '';
|
||||||
|
|
||||||
|
if (timeStr.includes(' ')) {
|
||||||
|
const parts = timeStr.split(' ');
|
||||||
|
dateStr = parts[0];
|
||||||
|
timeOnly = parts[1].substring(0, 5); // HH:mm
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateParts = dateStr.split('-');
|
||||||
|
const month = dateParts.length > 1 ? dateParts[1] : '';
|
||||||
|
const day = dateParts.length > 2 ? dateParts[2] : '';
|
||||||
|
|
||||||
|
// Map icon properties from icon JSON
|
||||||
|
let type = 'other';
|
||||||
|
let taskIcon = 'assignment'; // Default TDesign icon
|
||||||
|
let iconColor = '#8D6E63';
|
||||||
|
let iconBgColor = '#EFEBE9';
|
||||||
|
|
||||||
|
if (log.icon && typeof log.icon === 'string' && log.icon.startsWith('{')) {
|
||||||
|
try {
|
||||||
|
const iconObj = JSON.parse(log.icon);
|
||||||
|
if (iconObj.id) type = iconObj.id;
|
||||||
|
if (iconObj.icon) taskIcon = iconObj.icon;
|
||||||
|
if (iconObj.color) iconColor = iconObj.color;
|
||||||
|
if (iconObj.bgColor) iconBgColor = iconObj.bgColor;
|
||||||
|
} catch (e) { }
|
||||||
|
} else if (log.opType) {
|
||||||
|
type = log.opType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use name directly if available
|
||||||
|
const typeLabel = log.name || this.getCareTypeLabel(type);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...log,
|
...log,
|
||||||
day: parts[2],
|
day: day,
|
||||||
month: parts[1],
|
month: month,
|
||||||
typeLabel: this.getCareTypeLabel(log.type)
|
time: timeOnly,
|
||||||
|
type: type,
|
||||||
|
typeLabel: typeLabel,
|
||||||
|
remark: log.remark || log.content || '',
|
||||||
|
taskIcon: taskIcon,
|
||||||
|
iconColor: iconColor,
|
||||||
|
iconBgColor: iconBgColor
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -160,9 +139,12 @@ Page({
|
|||||||
water: '浇水',
|
water: '浇水',
|
||||||
fertilize: '施肥',
|
fertilize: '施肥',
|
||||||
prune: '修剪',
|
prune: '修剪',
|
||||||
repot: '换盆'
|
repot: '换盆',
|
||||||
|
pesticide: '除虫',
|
||||||
|
sun: '晒太阳',
|
||||||
|
other: '养护'
|
||||||
};
|
};
|
||||||
return map[type] || '养护';
|
return map[type] || '日常养护';
|
||||||
},
|
},
|
||||||
|
|
||||||
updateDisplayLogs() {
|
updateDisplayLogs() {
|
||||||
@@ -205,11 +187,17 @@ Page({
|
|||||||
this.updateDisplayRecords();
|
this.updateDisplayRecords();
|
||||||
},
|
},
|
||||||
|
|
||||||
// Navigate to Edit Page
|
// Navigate to Edit Page with EventChannel
|
||||||
handleOpenEditModal() {
|
handleOpenEditModal() {
|
||||||
if (this.data.currentPlant && this.data.currentPlant.id) {
|
if (this.data.currentPlant && this.data.currentPlant.id) {
|
||||||
wx.navigateTo({
|
wx.navigateTo({
|
||||||
url: `/pages/plant-detail/edit/index?id=${this.data.currentPlant.id}`
|
url: `/pages/plant-detail/edit/index?id=${this.data.currentPlant.id}&source=detail`,
|
||||||
|
success: (res) => {
|
||||||
|
// Send current data to the opened page
|
||||||
|
res.eventChannel.emit('acceptDataFromOpenerPage', {
|
||||||
|
plant: this.data.currentPlant
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -50,21 +50,19 @@
|
|||||||
>
|
>
|
||||||
<view class="care-view fadeIn">
|
<view class="care-view fadeIn">
|
||||||
<view class="section-title">
|
<view class="section-title">
|
||||||
<text class="h3">养护历史</text>
|
<text class="h3">养护记录</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="care-log-list">
|
<view class="care-log-list">
|
||||||
|
<block wx:if="{{displayCareLogs && displayCareLogs.length > 0}}">
|
||||||
<view wx:for="{{displayCareLogs}}" wx:key="id" class="care-log-item">
|
<view wx:for="{{displayCareLogs}}" wx:key="id" class="care-log-item">
|
||||||
<view class="log-left">
|
<view class="log-left">
|
||||||
<view class="log-date-v">
|
<view class="log-date-v">
|
||||||
<text class="l-day">{{item.day}}</text>
|
<text class="l-day">{{item.day}}</text>
|
||||||
<text class="l-month">{{item.month}}月</text>
|
<text class="l-month">{{item.month}}月</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="log-type-icon {{item.type === 'water' ? 'icon-water' : (item.type === 'fertilize' ? 'icon-fertilize' : (item.type === 'prune' ? 'icon-prune' : 'icon-repot'))}}">
|
<view class="log-type-icon" style="background-color: {{item.iconBgColor || '#EFEBE9'}};">
|
||||||
<t-icon wx:if="{{item.type === 'water'}}" name="heart" size="36rpx" color="#2196F3" />
|
<t-icon name="{{item.taskIcon}}" size="36rpx" color="{{item.iconColor || '#8D6E63'}}" />
|
||||||
<t-icon wx:elif="{{item.type === 'fertilize'}}" name="app" size="36rpx" color="#FFD700" />
|
|
||||||
<t-icon wx:elif="{{item.type === 'prune'}}" name="cut" size="36rpx" color="#757575" />
|
|
||||||
<t-icon wx:else name="assignment" size="36rpx" color="#8D6E63" />
|
|
||||||
</view>
|
</view>
|
||||||
<view class="log-info">
|
<view class="log-info">
|
||||||
<view class="log-header-row">
|
<view class="log-header-row">
|
||||||
@@ -75,6 +73,13 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</block>
|
||||||
|
<view wx:else class="plant-empty-state">
|
||||||
|
<view class="plant-empty-text">
|
||||||
|
<text>暂无养护记录</text>
|
||||||
|
<text>快去给它浇浇水吧~ 💧</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<block wx:if="{{careLogs.length > displayCareLimit}}">
|
<block wx:if="{{careLogs.length > displayCareLimit}}">
|
||||||
@@ -114,7 +119,27 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="aic-stat-item">
|
<view class="aic-stat-item">
|
||||||
<text class="label">养护次数</text>
|
<text class="label">养护次数</text>
|
||||||
<text class="value">{{careLogs.length}} 次</text>
|
<text class="value">{{careLogs.length || 0}} 次</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- New detail fields -->
|
||||||
|
<view class="aic-extra-info">
|
||||||
|
<view wx:if="{{currentPlant.potMaterial}}" class="aic-info-row">
|
||||||
|
<text class="label">花园材质:</text>
|
||||||
|
<text class="value">{{currentPlant.potMaterial}}</text>
|
||||||
|
</view>
|
||||||
|
<view wx:if="{{currentPlant.potSize}}" class="aic-info-row">
|
||||||
|
<text class="label">花园大小:</text>
|
||||||
|
<text class="value">{{currentPlant.potSize}}</text>
|
||||||
|
</view>
|
||||||
|
<view wx:if="{{currentPlant.sunlight}}" class="aic-info-row">
|
||||||
|
<text class="label">光照条件:</text>
|
||||||
|
<text class="value">{{currentPlant.sunlight}}</text>
|
||||||
|
</view>
|
||||||
|
<view wx:if="{{currentPlant.plantingMaterial}}" class="aic-info-row">
|
||||||
|
<text class="label">植料土壤:</text>
|
||||||
|
<text class="value">{{currentPlant.plantingMaterial}}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -127,6 +152,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<block wx:if="{{displayRecords && displayRecords.length > 0}}">
|
||||||
<view class="archive-timeline">
|
<view class="archive-timeline">
|
||||||
<view wx:for="{{displayRecords}}" wx:key="id" class="timeline-item">
|
<view wx:for="{{displayRecords}}" wx:key="id" class="timeline-item">
|
||||||
<view class="timeline-dot"></view>
|
<view class="timeline-dot"></view>
|
||||||
@@ -142,7 +168,7 @@
|
|||||||
<text class="timeline-desc">{{item.content}}</text>
|
<text class="timeline-desc">{{item.content}}</text>
|
||||||
<t-image
|
<t-image
|
||||||
wx:if="{{item.image}}"
|
wx:if="{{item.image}}"
|
||||||
src="{{tools.resolvePath(item.image)}}"
|
src="{{item.image}}"
|
||||||
mode="widthFix"
|
mode="widthFix"
|
||||||
width="100%"
|
width="100%"
|
||||||
class="timeline-img"
|
class="timeline-img"
|
||||||
@@ -152,6 +178,13 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
</block>
|
||||||
|
<view wx:else class="plant-empty-state">
|
||||||
|
<view class="plant-empty-text">
|
||||||
|
<text>暂无成长记录</text>
|
||||||
|
<text>记录下它的每一个精彩瞬间吧 📸</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<block wx:if="{{records.length > displayRecordLimit}}">
|
<block wx:if="{{records.length > displayRecordLimit}}">
|
||||||
<view class="load-more-btn" bindtap="toggleRecordLimit">
|
<view class="load-more-btn" bindtap="toggleRecordLimit">
|
||||||
|
|||||||
@@ -376,6 +376,35 @@ page {
|
|||||||
color: #1B5E20;
|
color: #1B5E20;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aic-extra-info {
|
||||||
|
margin-top: 40rpx;
|
||||||
|
padding-top: 32rpx;
|
||||||
|
border-top: 1rpx solid rgba(0, 0, 0, 0.05);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aic-info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
font-size: 26rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aic-info-row .label {
|
||||||
|
color: #558B2F;
|
||||||
|
font-weight: 600;
|
||||||
|
width: 140rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aic-info-row .value {
|
||||||
|
color: #455A64;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* Section Header */
|
/* Section Header */
|
||||||
.section-header {
|
.section-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -761,3 +790,32 @@ page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.plant-empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 64rpx 0;
|
||||||
|
gap: 32rpx;
|
||||||
|
background: white;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plant-empty-img {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plant-empty-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #90A4AE;
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<view class="fav-grid">
|
<view class="fav-grid">
|
||||||
<block wx:if="{{filteredFavorites.length > 0}}">
|
<block wx:if="{{filteredFavorites.length > 0}}">
|
||||||
<view wx:for="{{filteredFavorites}}" wx:key="id" class="fav-card">
|
<view wx:for="{{filteredFavorites}}" wx:key="id" class="fav-card">
|
||||||
<t-image src="{{tools.resolvePath(item.image)}}" class="fav-img" mode="aspectFill" width="100%" height="240rpx" />
|
<t-image src="{{item.image}}" class="fav-img" mode="aspectFill" width="100%" height="240rpx" />
|
||||||
<view class="fav-info">
|
<view class="fav-info">
|
||||||
<text class="fav-name">{{item.name}}</text>
|
<text class="fav-name">{{item.name}}</text>
|
||||||
<view class="fav-meta-row">
|
<view class="fav-meta-row">
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
<view class="my-post-content-wrap">
|
<view class="my-post-content-wrap">
|
||||||
<text class="post-text">{{item.content}}</text>
|
<text class="post-text">{{item.content}}</text>
|
||||||
<view wx:if="{{item.images.length > 0}}" class="my-post-images">
|
<view wx:if="{{item.images.length > 0}}" class="my-post-images">
|
||||||
<t-image wx:for="{{item.images}}" wx:for-item="img" wx:key="*this" src="{{tools.resolvePath(img)}}" mode="aspectFill" width="160rpx" height="160rpx" style="margin-right: 16rpx; display: inline-block; border-radius: 8rpx;" />
|
<t-image wx:for="{{item.images}}" wx:for-item="img" wx:key="*this" src="{{img}}" mode="aspectFill" width="160rpx" height="160rpx" style="margin-right: 16rpx; display: inline-block; border-radius: 8rpx;" />
|
||||||
</view>
|
</view>
|
||||||
<view class="my-post-footer">
|
<view class="my-post-footer">
|
||||||
<view class="footer-item"><t-icon name="heart" size="32rpx" /> <text>{{item.likes.length}}</text></view>
|
<view class="footer-item"><t-icon name="heart" size="32rpx" /> <text>{{item.likes.length}}</text></view>
|
||||||
|
|||||||
+155
-36
@@ -1,5 +1,5 @@
|
|||||||
// pages/tasks/index.js
|
// pages/tasks/index.js
|
||||||
import { MOCK_TASKS_DATA } from '../../utils/mockData';
|
import request from '../../utils/request';
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
@@ -11,8 +11,7 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
this.setData({ tasks: MOCK_TASKS_DATA });
|
this.fetchTodayTasks();
|
||||||
this.processTasks();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
@@ -20,49 +19,136 @@ Page({
|
|||||||
this.getTabBar()) {
|
this.getTabBar()) {
|
||||||
this.getTabBar().setData({ selected: 1 });
|
this.getTabBar().setData({ selected: 1 });
|
||||||
}
|
}
|
||||||
|
// Refresh on show
|
||||||
|
this.fetchTodayTasks();
|
||||||
},
|
},
|
||||||
|
|
||||||
processTasks() {
|
fetchTodayTasks() {
|
||||||
const { tasks } = this.data;
|
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();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// Calculate Progress (Simulated)
|
processTaskData(plantTaskVOList) {
|
||||||
const completedCount = 3; // Mocked existing completed
|
let totalPacketTasks = 0;
|
||||||
const initialTotal = MOCK_TASKS_DATA.length + completedCount;
|
let completedPacketTasks = 0;
|
||||||
const currentCompleted = completedCount + (MOCK_TASKS_DATA.length - tasks.length);
|
|
||||||
const progress = Math.min(100, Math.round((currentCompleted / initialTotal) * 100));
|
|
||||||
|
|
||||||
// Grouping
|
const groups = plantTaskVOList.map(vo => {
|
||||||
const groups = {};
|
const plant = vo.MyPlant || vo.myPlant;
|
||||||
tasks.forEach(task => {
|
if (!plant) return null;
|
||||||
if (!groups[task.plantName]) {
|
|
||||||
groups[task.plantName] = {
|
// Parse Image
|
||||||
plantName: task.plantName,
|
let imageUrl = '';
|
||||||
plantImage: task.plantImage,
|
if (plant.imgList && plant.imgList.length > 0) {
|
||||||
tasks: [],
|
let url = plant.imgList[0].url;
|
||||||
hasOverdue: false
|
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++;
|
||||||
}
|
}
|
||||||
groups[task.plantName].tasks.push(task);
|
|
||||||
if (task.isOverdue) groups[task.plantName].hasOverdue = true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sorting
|
// 2. Filter and Map Tasks for Display
|
||||||
const sortedGroups = Object.values(groups).sort((a, b) => {
|
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;
|
||||||
if (!a.hasOverdue && b.hasOverdue) return 1;
|
if (!a.hasOverdue && b.hasOverdue) return 1;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setData({
|
this.setData({
|
||||||
groupedTasks: sortedGroups,
|
groupedTasks: groups,
|
||||||
progress
|
progress,
|
||||||
|
tasks: groups
|
||||||
});
|
});
|
||||||
|
|
||||||
|
wx.stopPullDownRefresh();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleTaskClick(e) {
|
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;
|
const task = e.currentTarget.dataset.task;
|
||||||
|
if (task.isCompleted) return;
|
||||||
this.setData({
|
this.setData({
|
||||||
completingTask: task,
|
completingTask: task,
|
||||||
remark: ''
|
remark: ''
|
||||||
@@ -70,7 +156,6 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onPopupVisibleChange(e) {
|
onPopupVisibleChange(e) {
|
||||||
// Handle both TDesign event and manual close tap
|
|
||||||
const visible = e.detail ? e.detail.visible : e.currentTarget.dataset.visible;
|
const visible = e.detail ? e.detail.visible : e.currentTarget.dataset.visible;
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
this.setData({ completingTask: null });
|
this.setData({ completingTask: null });
|
||||||
@@ -85,15 +170,49 @@ Page({
|
|||||||
if (!this.data.completingTask) return;
|
if (!this.data.completingTask) return;
|
||||||
|
|
||||||
const taskId = this.data.completingTask.id;
|
const taskId = this.data.completingTask.id;
|
||||||
// Filter out the completed task
|
const remark = this.data.remark || '';
|
||||||
const newTasks = this.data.tasks.filter(t => t.id !== taskId);
|
|
||||||
|
|
||||||
this.setData({
|
wx.showLoading({ title: '提交中...' });
|
||||||
tasks: newTasks,
|
|
||||||
completingTask: null
|
request.post('/plant/completeTask', {
|
||||||
}, () => {
|
taskId: taskId,
|
||||||
this.processTasks();
|
remark: remark
|
||||||
|
}).then(() => {
|
||||||
|
wx.hideLoading();
|
||||||
wx.showToast({ title: '已完成', icon: 'success' });
|
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
@@ -19,8 +19,29 @@
|
|||||||
<view class="tasks-container">
|
<view class="tasks-container">
|
||||||
<text class="section-title">今日待办</text>
|
<text class="section-title">今日待办</text>
|
||||||
|
|
||||||
<view wx:if="{{tasks.length === 0}}" class="empty-state">
|
<view wx:if="{{tasks.length === 0}}" class="empty-state-custom">
|
||||||
<text>太棒了!所有任务都已完成 🎉</text>
|
<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>
|
</view>
|
||||||
|
|
||||||
<scroll-view wx:else scroll-y class="task-list" enhanced show-scrollbar="{{false}}">
|
<scroll-view wx:else scroll-y class="task-list" enhanced show-scrollbar="{{false}}">
|
||||||
@@ -28,7 +49,7 @@
|
|||||||
<view class="card-header-row">
|
<view class="card-header-row">
|
||||||
<view class="plant-info-brief">
|
<view class="plant-info-brief">
|
||||||
<view class="plant-thumb-small">
|
<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 wx:else class="thumb-placeholder">{{item.plantName[0]}}</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="plant-name-title">{{item.plantName}}</text>
|
<text class="plant-name-title">{{item.plantName}}</text>
|
||||||
@@ -50,8 +71,11 @@
|
|||||||
<text wx:if="{{task.isOverdue}}" class="task-overdue-text">{{task.overdueDays}}天前</text>
|
<text wx:if="{{task.isOverdue}}" class="task-overdue-text">{{task.overdueDays}}天前</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="mini-check-btn {{task.isOverdue ? 'btn-urgent' : ''}}" bindtap="handleTaskClick" data-task="{{task}}">
|
<view class="mini-check-btn {{task.isOverdue ? 'btn-urgent' : ''}}"
|
||||||
<t-icon name="check" size="32rpx" color="{{task.isOverdue ? '#EF5350' : '#E0E0E0'}}" />
|
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>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
+259
-38
@@ -279,18 +279,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.plant-task-card {
|
.plant-task-card {
|
||||||
background: white;
|
background: linear-gradient(160deg, #FFFFFF 0%, #F5F9F6 100%);
|
||||||
border-radius: 40rpx;
|
border-radius: 32rpx;
|
||||||
padding: 32rpx;
|
padding: 32rpx;
|
||||||
margin-bottom: 32rpx;
|
margin-bottom: 32rpx;
|
||||||
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.03);
|
box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.04);
|
||||||
border: 2rpx solid transparent;
|
border: 1rpx solid rgba(0,0,0,0.02);
|
||||||
transition: all 0.2s;
|
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 {
|
.plant-task-card.has-overdue {
|
||||||
border-color: rgba(239, 83, 80, 0.1);
|
border: 2rpx solid rgba(239, 83, 80, 0.2);
|
||||||
background: #FFF8F8;
|
background: linear-gradient(160deg, #FFEBEE 0%, #FFF 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header-row {
|
.card-header-row {
|
||||||
@@ -299,7 +313,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 32rpx;
|
margin-bottom: 32rpx;
|
||||||
padding-bottom: 24rpx;
|
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 {
|
.plant-info-brief {
|
||||||
@@ -309,11 +323,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.plant-thumb-small {
|
.plant-thumb-small {
|
||||||
width: 80rpx;
|
width: 88rpx;
|
||||||
height: 80rpx;
|
height: 88rpx;
|
||||||
border-radius: 24rpx;
|
border-radius: 24rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
|
box-shadow: 0 4rpx 10rpx rgba(0,0,0,0.05);
|
||||||
|
border: 2rpx solid #FFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plant-thumb-small image {
|
.plant-thumb-small image {
|
||||||
@@ -324,40 +340,46 @@
|
|||||||
.thumb-placeholder {
|
.thumb-placeholder {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: #E8F5E9;
|
background: #C8E6C9;
|
||||||
color: #558B2F;
|
color: #2E7D32;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 32rpx;
|
font-size: 36rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plant-name-title {
|
.plant-name-title {
|
||||||
font-size: 30rpx;
|
font-size: 32rpx;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: #263238;
|
color: #1B5E20; /* Dark Green theme */
|
||||||
|
letter-spacing: 1rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-overdue-badge {
|
.group-overdue-badge {
|
||||||
font-size: 20rpx;
|
font-size: 20rpx;
|
||||||
color: #EF5350;
|
color: #C62828;
|
||||||
background: rgba(239, 83, 80, 0.1);
|
background: #FFCDD2;
|
||||||
padding: 4rpx 16rpx;
|
padding: 6rpx 18rpx;
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plant-tasks-list {
|
.plant-tasks-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 24rpx;
|
gap: 28rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-task-row {
|
.mini-task-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
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 {
|
.mini-task-left {
|
||||||
@@ -367,14 +389,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.task-type-icon-circle {
|
.task-type-icon-circle {
|
||||||
width: 64rpx;
|
width: 72rpx;
|
||||||
height: 64rpx;
|
height: 72rpx;
|
||||||
background: #F8F9FA;
|
background: #FFF;
|
||||||
border-radius: 50%;
|
border-radius: 24rpx; /* Softer square */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #90A4AE;
|
color: #90A4AE;
|
||||||
|
box-shadow: 0 4rpx 8rpx rgba(0,0,0,0.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-icon {
|
.task-icon {
|
||||||
@@ -385,45 +408,58 @@
|
|||||||
.mini-task-text {
|
.mini-task-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4rpx;
|
gap: 6rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-label {
|
.task-label {
|
||||||
font-size: 28rpx;
|
font-size: 30rpx;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
color: #37474F;
|
color: #37474F;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-overdue-text {
|
.task-overdue-text {
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: #EF5350;
|
color: #D32F2F;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Checkbox Styling - Square */
|
||||||
.mini-check-btn {
|
.mini-check-btn {
|
||||||
width: 64rpx;
|
width: 56rpx;
|
||||||
height: 64rpx;
|
height: 56rpx;
|
||||||
border-radius: 20rpx;
|
border-radius: 12rpx; /* Square with slight radius */
|
||||||
border: 3rpx solid #E0E0E0;
|
border: 4rpx solid #CFD8DC; /* Thick border */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
background: #FFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-check-btn:active {
|
.mini-check-btn:active {
|
||||||
background: rgba(85, 139, 47, 0.05);
|
background: #F5F5F5;
|
||||||
border-color: #558B2F;
|
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 {
|
.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 {
|
.mini-check-btn.btn-urgent:active {
|
||||||
background: rgba(239, 83, 80, 0.05);
|
background: #FFCDD2;
|
||||||
border-color: #EF5350;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modal Specifics */
|
/* Modal Specifics */
|
||||||
@@ -547,3 +583,188 @@
|
|||||||
.confirm-complete-btn:active {
|
.confirm-complete-btn:active {
|
||||||
transform: scale(0.98);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@
|
|||||||
},
|
},
|
||||||
"compileType": "miniprogram",
|
"compileType": "miniprogram",
|
||||||
"libVersion": "3.7.1",
|
"libVersion": "3.7.1",
|
||||||
"appid": "wxc88e6306171c1611",
|
"appid": "wxb463820bf36dd5d6",
|
||||||
"projectname": "plant-mp",
|
"projectname": "plant-mp",
|
||||||
"isGameTourist": false,
|
"isGameTourist": false,
|
||||||
"condition": {
|
"condition": {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"projectname": "plant-mp",
|
"projectname": "plant-mp",
|
||||||
"condition": {},
|
"condition": {},
|
||||||
"setting": {
|
"setting": {
|
||||||
"urlCheck": true,
|
"urlCheck": false,
|
||||||
"coverView": true,
|
"coverView": true,
|
||||||
"lazyloadPlaceholderEnable": false,
|
"lazyloadPlaceholderEnable": false,
|
||||||
"skylineRenderEnable": false,
|
"skylineRenderEnable": false,
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
class WxRequest {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.baseUrl = options.baseUrl || '';
|
||||||
|
this.header = options.header || {};
|
||||||
|
this.interceptors = {
|
||||||
|
request: (config) => config,
|
||||||
|
response: (response) => response
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set request interceptor
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
setRequestInterceptor(callback) {
|
||||||
|
this.interceptors.request = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set response interceptor
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
setResponseInterceptor(callback) {
|
||||||
|
this.interceptors.response = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core request method
|
||||||
|
* @param {Object} options
|
||||||
|
*/
|
||||||
|
request(options = {}) {
|
||||||
|
// Merge headers
|
||||||
|
const header = { ...this.header, ...options.header };
|
||||||
|
|
||||||
|
// Prepare config
|
||||||
|
let config = {
|
||||||
|
url: (options.url.startsWith('http') ? options.url : this.baseUrl + options.url),
|
||||||
|
method: options.method || 'GET',
|
||||||
|
data: options.data || {},
|
||||||
|
header: header,
|
||||||
|
timeout: options.timeout || 60000
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply request interceptor
|
||||||
|
config = this.interceptors.request(config);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
wx.request({
|
||||||
|
...config,
|
||||||
|
success: (res) => {
|
||||||
|
// Apply response interceptor (pass the whole wx response)
|
||||||
|
const processedResponse = this.interceptors.response(res);
|
||||||
|
const { statusCode, data } = processedResponse;
|
||||||
|
|
||||||
|
// 1. Check HTTP Status Code
|
||||||
|
if (statusCode >= 200 && statusCode < 300) {
|
||||||
|
// 2. Check Business Logic Code (Assuming 200 is success based on common Go patterns,
|
||||||
|
// matching the user's struct: Code, Data, Msg)
|
||||||
|
// If data.code exists, we check it. If not (maybe not JSON), we pass through or handle.
|
||||||
|
const businessCode = data.code;
|
||||||
|
|
||||||
|
// Strict check: if code is 200, success.
|
||||||
|
if (businessCode === 200) {
|
||||||
|
resolve(data.data); // Return the inner Data object
|
||||||
|
} else {
|
||||||
|
// Business logic error
|
||||||
|
this.handleError({
|
||||||
|
errMsg: data.msg || 'Error',
|
||||||
|
code: businessCode
|
||||||
|
});
|
||||||
|
reject(data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle non-200 HTTP errors
|
||||||
|
this.handleError({ errMsg: `HTTP Error: ${statusCode}`, ...res });
|
||||||
|
reject(res);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
this.handleError({ errMsg: 'Network Error', ...err });
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle global errors (Toast)
|
||||||
|
* @param {Object} error
|
||||||
|
*/
|
||||||
|
handleError(error) {
|
||||||
|
const message = error.errMsg || error.msg || '请求失败,请稍后重试';
|
||||||
|
wx.showToast({
|
||||||
|
title: message,
|
||||||
|
icon: 'none',
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
console.error('API Error:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload file
|
||||||
|
* @param {string} filePath Local file path
|
||||||
|
* @param {string} name Form field name (default: file)
|
||||||
|
* @param {Object} formData Additional form data
|
||||||
|
*/
|
||||||
|
upload(filePath, name = 'file', formData = {}) {
|
||||||
|
// Prepare config
|
||||||
|
let config = {
|
||||||
|
url: this.baseUrl + '/oss/upload',
|
||||||
|
header: { ...this.header }, // Copy default headers
|
||||||
|
filePath: filePath,
|
||||||
|
name: name,
|
||||||
|
formData: formData
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply request interceptor (reuse logic for token injection)
|
||||||
|
// Note: wx.uploadFile doesn't support method/data in the same way, but we use interceptor for header mainly
|
||||||
|
config = this.interceptors.request(config);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
wx.uploadFile({
|
||||||
|
url: config.url,
|
||||||
|
filePath: config.filePath,
|
||||||
|
name: config.name,
|
||||||
|
formData: config.formData,
|
||||||
|
header: config.header,
|
||||||
|
success: (res) => {
|
||||||
|
// wx.uploadFile returns data as String, need to parse
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(res.data);
|
||||||
|
} catch (e) {
|
||||||
|
data = { code: 500, msg: 'Response parse error', data: res.data };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstruct a response object for interceptor
|
||||||
|
const responseObj = { ...res, data: data };
|
||||||
|
|
||||||
|
// Apply response interceptor
|
||||||
|
const processedResponse = this.interceptors.response(responseObj);
|
||||||
|
const { statusCode, data: finalData } = processedResponse;
|
||||||
|
|
||||||
|
// Status Code check (uploadFile also returns statusCode)
|
||||||
|
if (statusCode >= 200 && statusCode < 300) {
|
||||||
|
const businessCode = finalData.code;
|
||||||
|
if (businessCode === 200) {
|
||||||
|
resolve(finalData.data);
|
||||||
|
} else {
|
||||||
|
this.handleError({
|
||||||
|
errMsg: finalData.msg || 'Upload Error',
|
||||||
|
code: businessCode
|
||||||
|
});
|
||||||
|
reject(finalData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.handleError({ errMsg: `HTTP Error: ${statusCode}`, ...res });
|
||||||
|
reject(res);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
this.handleError({ errMsg: 'Upload Network Error', ...err });
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get(url, data = {}, header = {}) {
|
||||||
|
return this.request({ url, method: 'GET', data, header });
|
||||||
|
}
|
||||||
|
|
||||||
|
post(url, data = {}, header = {}) {
|
||||||
|
return this.request({ url, method: 'POST', data, header });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize with default instance
|
||||||
|
const request = new WxRequest({
|
||||||
|
baseUrl: 'http://192.168.0.184:8888',
|
||||||
|
header: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Example: Setup default interceptors
|
||||||
|
request.setRequestInterceptor((config) => {
|
||||||
|
// Inject token if available
|
||||||
|
const token = wx.getStorageSync('token');
|
||||||
|
if (token) {
|
||||||
|
// User requested: Bearer + token
|
||||||
|
config.header['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
request.setResponseInterceptor((response) => {
|
||||||
|
// Handle global token expiration (e.g., HTTP 401 or Business Code 401)
|
||||||
|
if (response.statusCode === 401 || response.data?.code === 401) {
|
||||||
|
wx.removeStorageSync('token');
|
||||||
|
// Redirect to login if needed, or just warn
|
||||||
|
// wx.reLaunch({ url: '/pages/login/index' });
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default request;
|
||||||
|
export { WxRequest };
|
||||||
Reference in New Issue
Block a user