feat: 整体页面优化,删除无用svg
@@ -15,7 +15,9 @@
|
||||
"pages/profile/identify-history/index",
|
||||
"pages/profile/badges/index",
|
||||
"pages/profile/badges/level-detail/index",
|
||||
"pages/profile/badges/badge-wall/index"
|
||||
"pages/profile/badges/badge-wall/index",
|
||||
"pages/profile/favorites/index",
|
||||
"pages/profile/posts/index"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
|
||||
|
Before Width: | Height: | Size: 374 B |
|
Before Width: | Height: | Size: 478 B |
|
Before Width: | Height: | Size: 437 B |
|
Before Width: | Height: | Size: 674 B |
|
Before Width: | Height: | Size: 377 B |
|
Before Width: | Height: | Size: 355 B |
|
Before Width: | Height: | Size: 687 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 494 B |
|
Before Width: | Height: | Size: 733 B |
|
Before Width: | Height: | Size: 857 B |
|
Before Width: | Height: | Size: 608 B |
|
Before Width: | Height: | Size: 548 B |
|
Before Width: | Height: | Size: 857 B |
|
Before Width: | Height: | Size: 547 B |
|
Before Width: | Height: | Size: 749 B |
|
Before Width: | Height: | Size: 686 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 751 B |
|
Before Width: | Height: | Size: 615 B |
|
Before Width: | Height: | Size: 863 B |
|
Before Width: | Height: | Size: 675 B |
|
Before Width: | Height: | Size: 447 B |
|
Before Width: | Height: | Size: 689 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -13,7 +13,9 @@ Page({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
hasMore: true,
|
||||
userInfo: null
|
||||
userInfo: null,
|
||||
scrollTop: 0,
|
||||
isRefreshing: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -33,9 +35,16 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// Called by create post page
|
||||
// Called by create post page or Pull Down
|
||||
onRefresh() {
|
||||
this.fetchPosts(true);
|
||||
this.setData({ isRefreshing: true });
|
||||
this.fetchPosts(true).then(() => {
|
||||
this.setData({ isRefreshing: false });
|
||||
});
|
||||
},
|
||||
|
||||
onTabItemTap() {
|
||||
this.setData({ scrollTop: Math.random() * 0.01 });
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
@@ -60,7 +69,7 @@ Page({
|
||||
|
||||
try {
|
||||
// Correct API Endpoint and Params
|
||||
const res = await request.post('/post/page', { current, pageSize });
|
||||
const res = await request.post('/post/page', { current, pageSize, hasReviewed: 1 });
|
||||
|
||||
// Handle response structure: { code: 200, data: { list: [], ... } }
|
||||
// OR if request.js unwraps it: { list: [], ... }
|
||||
@@ -79,14 +88,16 @@ Page({
|
||||
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) : '花友'),
|
||||
likes: item.hasLiked === 1 ? ['我'] : [],
|
||||
comments: (item.commentList || []).map(c => ({
|
||||
id: c.id,
|
||||
user: c.commentator ? (c.commentator.nickName || c.commentator.name) : '花友',
|
||||
content: c.content
|
||||
})),
|
||||
likedByMe: item.hasLiked === 1,
|
||||
isFavorited: item.hasStar === 1,
|
||||
likeCount: item.likeCount || 0,
|
||||
stars: (item.starList || []).map(s => s.starer ? (s.starer.nickName || s.starer.name) : '花友'),
|
||||
commentCount: item.commentCount || 0,
|
||||
isExpanded: false
|
||||
};
|
||||
@@ -125,12 +136,7 @@ Page({
|
||||
// Preview image in full screen
|
||||
previewImage(e) {
|
||||
const { url, urls } = e.currentTarget.dataset;
|
||||
const resolvedUrls = urls.map(img => {
|
||||
if (img.indexOf('http') === 0 || img.indexOf('wxfile') === 0) {
|
||||
return img;
|
||||
}
|
||||
return `/assets/${img}`;
|
||||
});
|
||||
const resolvedUrls = urls.map(img => img);
|
||||
|
||||
wx.previewImage({
|
||||
current: url,
|
||||
@@ -169,10 +175,15 @@ Page({
|
||||
try {
|
||||
await request.get('/post/like', { id: postId, type });
|
||||
|
||||
// Optimistic Update: Only toggle button state. Do NOT modify likes list text.
|
||||
// Optimistic Update
|
||||
const updatedPosts = this.data.posts.map(p => {
|
||||
if (p.id === postId) {
|
||||
return { ...p, likedByMe: !p.likedByMe };
|
||||
const liked = !p.likedByMe;
|
||||
return {
|
||||
...p,
|
||||
likedByMe: liked,
|
||||
likes: liked ? ['我'] : []
|
||||
};
|
||||
}
|
||||
return p;
|
||||
});
|
||||
@@ -192,6 +203,42 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// Collect post
|
||||
async collectPost(e) {
|
||||
const postId = e.currentTarget.dataset.id;
|
||||
const post = this.data.posts.find(p => p.id === postId);
|
||||
if (!post) return;
|
||||
|
||||
const type = post.isFavorited ? 2 : 1; // 1: Collect, 2: Cancel
|
||||
|
||||
try {
|
||||
await request.get('/post/star', { id: postId, type });
|
||||
|
||||
// Optimistic Update
|
||||
const updatedPosts = this.data.posts.map(p => {
|
||||
if (p.id === postId) {
|
||||
return { ...p, isFavorited: !p.isFavorited };
|
||||
}
|
||||
return p;
|
||||
});
|
||||
|
||||
this.setData({
|
||||
posts: updatedPosts,
|
||||
displayedPosts: updatedPosts,
|
||||
activePostId: null
|
||||
});
|
||||
|
||||
// Refresh list to sync detailed data (like IDs if needed)
|
||||
this.fetchPosts(true);
|
||||
|
||||
wx.showToast({ title: type === 1 ? '已收藏' : '已取消', icon: 'success' });
|
||||
|
||||
} catch (err) {
|
||||
console.error('Collect failed', err);
|
||||
wx.showToast({ title: '操作失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
// Show comment input bar
|
||||
showCommentInput(e) {
|
||||
const postId = e.currentTarget.dataset.id;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"navigationBarTitleText": "植友动态",
|
||||
"navigationBarTitleText": "花友动态",
|
||||
"usingComponents": {
|
||||
"t-avatar": "tdesign-miniprogram/avatar/avatar",
|
||||
"t-image": "tdesign-miniprogram/image/image",
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
enhanced="{{true}}"
|
||||
show-scrollbar="{{false}}"
|
||||
bindtap="hideActionPopup"
|
||||
scroll-top="{{scrollTop}}"
|
||||
bindscrolltolower="onReachBottom"
|
||||
refresher-enabled="{{true}}"
|
||||
bindrefresherrefresh="onRefresh"
|
||||
refresher-triggered="{{isRefreshing}}"
|
||||
>
|
||||
<block wx:if="{{displayedPosts.length > 0}}">
|
||||
<view wx:for="{{displayedPosts}}" wx:key="id" class="moment-post">
|
||||
@@ -49,10 +54,15 @@
|
||||
<!-- WeChat Style Popup -->
|
||||
<view class="action-popup {{activePostId === item.id ? 'show' : ''}}" catchtap="stopPropagation">
|
||||
<view class="popup-btn like-btn" catchtap="likePost" data-id="{{item.id}}">
|
||||
<t-icon name="heart" size="32rpx" color="#fff" />
|
||||
<t-icon name="{{item.likedByMe ? 'heart-filled' : 'heart'}}" size="32rpx" color="{{item.likedByMe ? '#FA5151' : '#fff'}}" />
|
||||
<text>{{item.likedByMe ? '取消' : '赞'}}</text>
|
||||
</view>
|
||||
<view class="popup-divider"></view>
|
||||
<view class="popup-btn fav-btn" catchtap="collectPost" data-id="{{item.id}}">
|
||||
<t-icon name="{{item.isFavorited ? 'star-filled' : 'star'}}" size="32rpx" color="{{item.isFavorited ? '#FFC107' : '#fff'}}" />
|
||||
<text>{{item.isFavorited ? '取消' : '收藏'}}</text>
|
||||
</view>
|
||||
<view class="popup-divider"></view>
|
||||
<view class="popup-btn comment-btn" catchtap="showCommentInput" data-id="{{item.id}}">
|
||||
<t-icon name="chat" size="32rpx" color="#fff" />
|
||||
<text>评论</text>
|
||||
@@ -68,17 +78,26 @@
|
||||
</view>
|
||||
|
||||
<!-- Likes & Comments Box -->
|
||||
<view wx:if="{{item.likes.length > 0 || item.comments.length > 0}}" class="likes-comments-box">
|
||||
<!-- Likes -->
|
||||
<view wx:if="{{item.likes.length > 0}}" class="likes-section">
|
||||
<t-icon name="heart-filled" size="28rpx" color="#FA5151" />
|
||||
<view class="likes-list">
|
||||
<view wx:if="{{item.likes.length > 0 || item.isFavorited || item.comments.length > 0}}" class="likes-comments-box">
|
||||
<!-- Likes & Stars Combined -->
|
||||
<view wx:if="{{item.likes.length > 0 || item.isFavorited}}" class="likes-section" style="flex-wrap: wrap;">
|
||||
<block wx:if="{{item.likes.length > 0}}">
|
||||
<t-icon name="{{item.likedByMe ? 'heart-filled' : 'heart'}}" size="28rpx" color="{{item.likedByMe ? '#FA5151' : '#576b95'}}" />
|
||||
<view class="likes-list" style="flex: unset; margin-right: 24rpx;">
|
||||
<text wx:for="{{item.likes}}" wx:for-item="liker" wx:key="*this" class="like-name">{{liker}}{{index < item.likes.length - 1 ? ',' : ''}}</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block wx:if="{{item.isFavorited}}">
|
||||
<t-icon name="star-filled" size="28rpx" color="#FFC107" />
|
||||
<view class="likes-list" style="flex: unset;">
|
||||
<text class="like-name">我</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- Divider -->
|
||||
<view wx:if="{{item.likes.length > 0 && item.comments.length > 0}}" class="divider"></view>
|
||||
<view wx:if="{{(item.likes.length > 0 || item.isFavorited) && item.comments.length > 0}}" class="divider"></view>
|
||||
|
||||
<!-- Comments -->
|
||||
<view wx:if="{{item.comments.length > 0}}" class="comments-section">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// pages/garden/add/index.js
|
||||
import { CARE_TASK_ICONS } from '../../../utils/mockData';
|
||||
import { CARE_TASK_ICONS } from '../../../utils/constant';
|
||||
import request from '../../../utils/request';
|
||||
import { requestSubscription, checkSubscriptionSettings } from '../../../utils/subscribe';
|
||||
|
||||
@@ -253,7 +253,8 @@ Page({
|
||||
const carePlans = newCareTasks.map(task => ({
|
||||
name: task.taskName || '未命名事项',
|
||||
period: parseInt(task.frequencyValue) || 1,
|
||||
icon: JSON.stringify(task.taskIcon || {}) // Serialize icon details
|
||||
icon: JSON.stringify(task.taskIcon || {}), // Serialize icon details
|
||||
targetAction: (task.taskIcon && task.taskIcon.targetAction) ? task.taskIcon.targetAction : ''
|
||||
}));
|
||||
|
||||
// Construct Payload
|
||||
|
||||
@@ -12,7 +12,8 @@ Page({
|
||||
pageSize: 6,
|
||||
total: 0,
|
||||
isLastPage: false,
|
||||
isLoading: false
|
||||
isLoading: false,
|
||||
scrollTop: 0
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
@@ -66,9 +67,23 @@ Page({
|
||||
const mappedList = list.map(item => {
|
||||
let imageUrl = '';
|
||||
if (item.imgList && item.imgList.length > 0) {
|
||||
imageUrl = item.imgList[0].url;
|
||||
imageUrl = item.imgList[0].url || '';
|
||||
}
|
||||
return { ...item, images: [imageUrl] };
|
||||
|
||||
// Calculate days
|
||||
let days = 1;
|
||||
if (item.plantTime) {
|
||||
try {
|
||||
const start = new Date(item.plantTime).getTime();
|
||||
const now = Date.now();
|
||||
const diff = now - start;
|
||||
if (diff > 0) {
|
||||
days = Math.ceil(diff / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
return { ...item, images: [imageUrl], daysPlanted: days };
|
||||
});
|
||||
|
||||
this.setData({
|
||||
@@ -139,5 +154,11 @@ Page({
|
||||
return {
|
||||
title: '我的植物花园 - Sundynix Plant'
|
||||
};
|
||||
},
|
||||
|
||||
onTabItemTap() {
|
||||
// Reset scroll when switching to this tab
|
||||
// Use random small value to force data change detection
|
||||
this.setData({ scrollTop: Math.random() * 0.01 });
|
||||
}
|
||||
})
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view wx:else scroll-y class="garden-list-container" enhanced show-scrollbar="{{false}}" bindscrolltolower="onScrollLower">
|
||||
<scroll-view wx:else scroll-y class="garden-list-container" enhanced show-scrollbar="{{false}}" bindscrolltolower="onScrollLower" scroll-top="{{scrollTop}}">
|
||||
<view class="plant-grid">
|
||||
<view wx:for="{{plants}}" wx:key="id" class="plant-card" bindtap="navigateToDetail" data-id="{{item.id}}">
|
||||
<view class="plant-image-container">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// pages/plant-detail/edit/index.js
|
||||
import request from '../../../utils/request';
|
||||
import { CARE_TASK_ICONS } from '../../../utils/mockData';
|
||||
import { CARE_TASK_ICONS } from '../../../utils/constant';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -296,7 +296,8 @@ Page({
|
||||
id: String(task.id),
|
||||
name: task.name,
|
||||
period: parseInt(task.period) || 1,
|
||||
icon: JSON.stringify(task.taskIcon || {})
|
||||
icon: JSON.stringify(task.taskIcon || {}),
|
||||
targetAction: (task.taskIcon && task.taskIcon.targetAction) ? task.taskIcon.targetAction : ''
|
||||
}));
|
||||
|
||||
// Build payload for /plant/update (UpdateMyPlant struct)
|
||||
@@ -317,7 +318,8 @@ Page({
|
||||
plantId: plantId,
|
||||
name: task.name,
|
||||
period: parseInt(task.period) || 1,
|
||||
icon: JSON.stringify(task.taskIcon || {})
|
||||
icon: JSON.stringify(task.taskIcon || {}),
|
||||
targetAction: (task.taskIcon && task.taskIcon.targetAction) ? task.taskIcon.targetAction : ''
|
||||
}))
|
||||
} : null;
|
||||
|
||||
@@ -358,17 +360,5 @@ Page({
|
||||
});
|
||||
},
|
||||
|
||||
handleDeletePlant() {
|
||||
wx.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个植物吗?删除后无法恢复。',
|
||||
confirmColor: '#EF5350',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
setTimeout(() => { wx.switchTab({ url: '/pages/garden/index' }); }, 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
@@ -157,12 +157,7 @@
|
||||
</view>
|
||||
|
||||
<!-- Delete Button for Edit Page -->
|
||||
<view class="delete-section" style="margin-top: 16rpx; padding-bottom: 40rpx;">
|
||||
<view class="delete-page-btn" bindtap="handleDeletePlant">
|
||||
<t-icon name="delete" size="32rpx" />
|
||||
<text>删除植物档案</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<view style="height: 180rpx;"></view>
|
||||
</scroll-view>
|
||||
|
||||
@@ -244,4 +244,31 @@ Page({
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleDeletePlant() {
|
||||
if (!this.data.currentPlant || !this.data.currentPlant.id) return;
|
||||
|
||||
wx.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这个植物吗?删除后无法恢复。',
|
||||
confirmColor: '#EF5350',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.showLoading({ title: '删除中...' });
|
||||
// Attempting to use consistent API pattern: POST /plant/deletePlant with ids array
|
||||
request.post('/plant/deletePlant', { ids: [this.data.currentPlant.id] }).then(() => {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
setTimeout(() => {
|
||||
wx.switchTab({ url: '/pages/garden/index' });
|
||||
}, 1000);
|
||||
}).catch(err => {
|
||||
wx.hideLoading();
|
||||
console.error('Delete plant failed', err);
|
||||
wx.showToast({ title: '删除失败', icon: 'none' });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
|
||||
@@ -92,6 +92,11 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="delete-plant-box" bindtap="handleDeletePlant">
|
||||
<t-icon name="delete" size="32rpx" color="#EF5350" />
|
||||
<text class="delete-text">删除植物</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
|
||||
@@ -811,3 +811,21 @@ page {
|
||||
font-weight: 500;
|
||||
color: #90A4AE;
|
||||
}
|
||||
|
||||
.delete-plant-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
padding: 24rpx;
|
||||
margin-top: 40rpx;
|
||||
background: #FFF;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(239, 83, 80, 0.1);
|
||||
}
|
||||
|
||||
.delete-text {
|
||||
font-size: 28rpx;
|
||||
color: #EF5350;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import request from '../../../utils/request';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
favTab: 'all',
|
||||
favorites: [],
|
||||
filteredFavorites: []
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadFavorites();
|
||||
},
|
||||
|
||||
loadFavorites() {
|
||||
// TODO: Call API
|
||||
this.filterFavorites();
|
||||
},
|
||||
|
||||
onFavTabChange(e) {
|
||||
const val = e.currentTarget.dataset.value;
|
||||
this.setData({ favTab: val }, () => {
|
||||
this.filterFavorites();
|
||||
});
|
||||
},
|
||||
|
||||
filterFavorites() {
|
||||
const { favorites, favTab } = this.data;
|
||||
const filtered = favorites.filter(item => {
|
||||
if (favTab === 'all') return true;
|
||||
return item.type === favTab;
|
||||
});
|
||||
this.setData({ filteredFavorites: filtered });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationBarTitleText": "我的收藏",
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-image": "tdesign-miniprogram/image/image"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<view class="favorites-page">
|
||||
<view class="category-filter">
|
||||
<view class="filter-chip {{favTab === 'all' ? 'active' : ''}}" bindtap="onFavTabChange" data-value="all">全部</view>
|
||||
<view class="filter-chip {{favTab === 'plant' ? 'active' : ''}}" bindtap="onFavTabChange" data-value="plant">植物</view>
|
||||
<view class="filter-chip {{favTab === 'article' ? 'active' : ''}}" bindtap="onFavTabChange" data-value="article">文章</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="fav-scroll" enhanced show-scrollbar="{{false}}">
|
||||
<view wx:if="{{filteredFavorites.length > 0}}" class="fav-grid">
|
||||
<view wx:for="{{filteredFavorites}}" wx:key="id" class="fav-card">
|
||||
<t-image src="{{item.image}}" mode="aspectFill" width="100%" height="240rpx" class="fav-img" />
|
||||
<view class="fav-info">
|
||||
<text class="fav-name">{{item.name}}</text>
|
||||
<view class="fav-meta-row">
|
||||
<t-icon name="{{item.type === 'plant' ? 'heart' : 'book'}}" size="28rpx" color="#90A4AE" />
|
||||
<text class="fav-type">{{item.meta}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:else class="empty-state">
|
||||
<t-icon name="star" size="80rpx" color="#E0E0E0" style="margin-bottom: 24rpx;" />
|
||||
<text class="empty-text">暂无收藏内容</text>
|
||||
</view>
|
||||
<view style="height: 60rpx;"></view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@@ -0,0 +1,98 @@
|
||||
.favorites-page {
|
||||
background: #F4F6F0;
|
||||
height: 100vh;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.category-filter {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
padding: 12rpx 32rpx;
|
||||
background: #fff;
|
||||
border: 2rpx solid transparent;
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
color: #6B7280;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-chip.active {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.fav-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fav-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.fav-card {
|
||||
background: white;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fav-img {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.fav-info {
|
||||
padding: 16rpx 20rpx;
|
||||
}
|
||||
|
||||
.fav-name {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1F2937;
|
||||
margin-bottom: 8rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fav-meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.fav-type {
|
||||
font-size: 22rpx;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #9CA3AF;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
@@ -29,7 +29,9 @@ Page({
|
||||
myDrafts: [],
|
||||
|
||||
// App version
|
||||
appVersion: '1.0.0'
|
||||
// App version
|
||||
appVersion: '1.0.0',
|
||||
scrollTop: 0
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -44,6 +46,13 @@ Page({
|
||||
this.loadUserInfo();
|
||||
},
|
||||
|
||||
onTabItemTap() {
|
||||
this.setData({
|
||||
view: 'profile',
|
||||
scrollTop: Math.random() * 0.01
|
||||
});
|
||||
},
|
||||
|
||||
// ======== User Info ========
|
||||
loadUserInfo() {
|
||||
request.get('/profile/detail').then(res => {
|
||||
@@ -57,6 +66,7 @@ Page({
|
||||
this.setData({
|
||||
userName: res.nickname || '植物爱好者',
|
||||
userAvatar: avatarUrl,
|
||||
currentAvatarId: res.avatarId || (res.avatar ? res.avatar.id : ''),
|
||||
|
||||
// Stats
|
||||
plantCount: res.plantCount || 0,
|
||||
@@ -88,127 +98,16 @@ Page({
|
||||
|
||||
|
||||
// ======== Navigation ========
|
||||
setView(e) {
|
||||
const view = e.currentTarget.dataset.view;
|
||||
this.setData({ view });
|
||||
|
||||
if (view === 'favorites') {
|
||||
this.loadFavorites();
|
||||
} else if (view === 'posts') {
|
||||
this.loadMyPosts();
|
||||
this.loadDrafts();
|
||||
}
|
||||
},
|
||||
|
||||
goBack() {
|
||||
this.setData({ view: 'profile' });
|
||||
},
|
||||
|
||||
// ======== Favorites ========
|
||||
loadFavorites() {
|
||||
// TODO: Call favorites API when available
|
||||
// request.get('/user/favorites').then(...)
|
||||
this.filterFavorites();
|
||||
goToFavorites() {
|
||||
wx.navigateTo({ url: '/pages/profile/favorites/index' });
|
||||
},
|
||||
|
||||
onFavTabChange(e) {
|
||||
this.setData({ favTab: e.detail.value }, () => {
|
||||
this.filterFavorites();
|
||||
});
|
||||
},
|
||||
|
||||
filterFavorites() {
|
||||
const { favorites, favTab } = this.data;
|
||||
const filtered = favorites.filter(item => {
|
||||
if (favTab === 'all') return true;
|
||||
return item.type === favTab;
|
||||
});
|
||||
this.setData({ filteredFavorites: filtered });
|
||||
},
|
||||
|
||||
// ======== Posts ========
|
||||
loadMyPosts() {
|
||||
request.post('/post/page', { current: 1, pageSize: 50, onlyMine: true }).then(res => {
|
||||
const records = res.records || res.list || [];
|
||||
const posts = records.map(item => {
|
||||
const publisher = item.publisher || {};
|
||||
const imgList = item.imgList || [];
|
||||
return {
|
||||
id: item.id,
|
||||
content: item.content || '',
|
||||
time: this._formatTime(item.createdAt || item.createTime),
|
||||
images: imgList.map(img => img.url),
|
||||
likes: item.likeList || [],
|
||||
comments: item.commentList || []
|
||||
};
|
||||
});
|
||||
this.setData({ myPublishedPosts: posts });
|
||||
}).catch(() => {
|
||||
this.setData({ myPublishedPosts: [] });
|
||||
});
|
||||
},
|
||||
|
||||
loadDrafts() {
|
||||
try {
|
||||
const draft = wx.getStorageSync('post_draft');
|
||||
if (draft && (draft.content || (draft.images && draft.images.length > 0))) {
|
||||
this.setData({
|
||||
myDrafts: [{
|
||||
id: 'draft_1',
|
||||
content: draft.content || '',
|
||||
images: draft.images || [],
|
||||
selectedTopics: draft.selectedTopics || []
|
||||
}]
|
||||
});
|
||||
} else {
|
||||
this.setData({ myDrafts: [] });
|
||||
}
|
||||
} catch (e) {
|
||||
this.setData({ myDrafts: [] });
|
||||
}
|
||||
},
|
||||
|
||||
onPostsTabChange(e) {
|
||||
this.setData({ postsTab: e.detail.value });
|
||||
},
|
||||
|
||||
deletePost(e) {
|
||||
const postId = e.currentTarget.dataset.id;
|
||||
wx.showModal({
|
||||
title: '删除动态',
|
||||
content: '确定要删除这条动态吗?',
|
||||
confirmColor: '#EF5350',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return;
|
||||
wx.showLoading({ title: '删除中...' });
|
||||
request.get('/post/delete', { id: postId }).then(() => {
|
||||
wx.hideLoading();
|
||||
this.loadMyPosts();
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
}).catch(() => {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '删除失败', icon: 'none' });
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
editDraft() {
|
||||
wx.navigateTo({ url: '/pages/community/create/index' });
|
||||
},
|
||||
|
||||
deleteDraft() {
|
||||
wx.showModal({
|
||||
title: '删除草稿',
|
||||
content: '确定要删除这份草稿吗?',
|
||||
confirmColor: '#EF5350',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return;
|
||||
try { wx.removeStorageSync('post_draft'); } catch (e) { }
|
||||
this.setData({ myDrafts: [] });
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
}
|
||||
});
|
||||
goToPosts() {
|
||||
wx.navigateTo({ url: '/pages/profile/posts/index' });
|
||||
},
|
||||
|
||||
// ======== Menu Actions ========
|
||||
@@ -284,10 +183,12 @@ Page({
|
||||
},
|
||||
|
||||
async saveProfile() {
|
||||
const { tempAvatar, tempNickname, userName, userAvatar } = this.data;
|
||||
const { tempAvatar, tempNickname, userName, userAvatar, currentAvatarId } = this.data;
|
||||
|
||||
// Check if anything changed
|
||||
const isNameChanged = tempNickname && tempNickname !== userName;
|
||||
// Determine if there are changes
|
||||
// tempNickname might be undefined if user didn't edit nickname input
|
||||
const finalNickname = tempNickname !== undefined ? tempNickname : userName;
|
||||
const isNameChanged = finalNickname !== userName;
|
||||
const isAvatarChanged = tempAvatar && tempAvatar !== userAvatar;
|
||||
|
||||
if (!isNameChanged && !isAvatarChanged) {
|
||||
@@ -295,61 +196,65 @@ Page({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!finalNickname.trim()) {
|
||||
wx.showToast({ title: '昵称不能为空', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
wx.showLoading({ title: '保存中...', mask: true });
|
||||
|
||||
try {
|
||||
const updatePayload = {};
|
||||
let finalAvatarId = currentAvatarId;
|
||||
|
||||
// 1. Upload avatar if changed
|
||||
// Upload new avatar if changed
|
||||
if (isAvatarChanged) {
|
||||
const uploadRes = await request.upload(tempAvatar);
|
||||
// Correctly extract ID from file object based on known API response
|
||||
let fileId = '';
|
||||
if (uploadRes && uploadRes.file && uploadRes.file.id) {
|
||||
fileId = uploadRes.file.id;
|
||||
finalAvatarId = uploadRes.file.id;
|
||||
} else if (uploadRes && uploadRes.id) {
|
||||
fileId = uploadRes.id;
|
||||
}
|
||||
|
||||
if (fileId) {
|
||||
updatePayload.avatarId = fileId;
|
||||
finalAvatarId = uploadRes.id;
|
||||
} else {
|
||||
throw new Error('Avatar upload failed, no ID returned');
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Set nickname if changed
|
||||
if (isNameChanged) {
|
||||
updatePayload.nickname = tempNickname;
|
||||
}
|
||||
// Construct Full Payload
|
||||
const updatePayload = {
|
||||
nickname: finalNickname,
|
||||
avatarId: finalAvatarId
|
||||
};
|
||||
|
||||
// 3. Call update API
|
||||
if (Object.keys(updatePayload).length > 0) {
|
||||
// Call API
|
||||
await request.post('/profile/update', updatePayload);
|
||||
}
|
||||
|
||||
wx.hideLoading();
|
||||
|
||||
// 4. Update local state
|
||||
// Update UI State
|
||||
this.setData({
|
||||
userName: tempNickname || userName,
|
||||
userAvatar: tempAvatar || userAvatar, // Use tempAvatar for immediate display
|
||||
userName: finalNickname,
|
||||
userAvatar: tempAvatar || userAvatar,
|
||||
currentAvatarId: finalAvatarId,
|
||||
showProfileEditor: false
|
||||
});
|
||||
|
||||
wx.showToast({ title: '资料已更新', icon: 'success' });
|
||||
|
||||
// 5. Update globalData
|
||||
// Update Global Data
|
||||
const userInfo = app.globalData.userInfo || {};
|
||||
if (updatePayload.nickname) userInfo.name = updatePayload.nickname;
|
||||
if (updatePayload.avatarId) userInfo.avatarId = updatePayload.avatarId;
|
||||
// Also update URL if we have it locally?
|
||||
// Better to re-fetch profile to get canonical URL, but optimistic update is fine.
|
||||
userInfo.avatar = tempAvatar;
|
||||
userInfo.nickname = finalNickname;
|
||||
userInfo.name = finalNickname;
|
||||
// Update avatar structure in global store too
|
||||
if (isAvatarChanged) {
|
||||
userInfo.avatar = { ...(userInfo.avatar || {}), url: tempAvatar, id: finalAvatarId };
|
||||
}
|
||||
userInfo.avatarId = finalAvatarId;
|
||||
|
||||
app.globalData.userInfo = userInfo;
|
||||
wx.setStorageSync('userInfo', userInfo);
|
||||
|
||||
} catch (err) {
|
||||
wx.hideLoading();
|
||||
console.error('Save profile failed', err);
|
||||
wx.showToast({ title: '保存失败', icon: 'none' });
|
||||
} finally {
|
||||
wx.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,70 +1,7 @@
|
||||
<view class="profile-page">
|
||||
|
||||
<!-- ======== FAVORITES VIEW ======== -->
|
||||
<view wx:if="{{view === 'favorites'}}" class="sub-view info-view-anim">
|
||||
<view class="sub-nav" bindtap="goBack">
|
||||
<t-icon name="chevron-left" size="40rpx" />
|
||||
<text class="sub-nav-title">我的收藏</text>
|
||||
</view>
|
||||
|
||||
<view class="category-filter">
|
||||
<view class="filter-chip {{favTab === 'all' ? 'active' : ''}}" bindtap="onFavTabChange" data-value="all">全部</view>
|
||||
<view class="filter-chip {{favTab === 'plant' ? 'active' : ''}}" bindtap="onFavTabChange" data-value="plant">植物</view>
|
||||
<view class="filter-chip {{favTab === 'article' ? 'active' : ''}}" bindtap="onFavTabChange" data-value="article">文章</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="sub-scroll">
|
||||
<view wx:if="{{filteredFavorites.length > 0}}" class="fav-grid">
|
||||
<view wx:for="{{filteredFavorites}}" wx:key="id" class="fav-card">
|
||||
<t-image src="{{item.image}}" mode="aspectFill" width="100%" height="240rpx" class="fav-img" />
|
||||
<view class="fav-info">
|
||||
<text class="fav-name">{{item.name}}</text>
|
||||
<view class="fav-meta-row">
|
||||
<t-icon name="{{item.type === 'plant' ? 'heart' : 'book'}}" size="28rpx" color="#90A4AE" />
|
||||
<text class="fav-type">{{item.meta}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:else class="empty-state">
|
||||
<text class="empty-text">暂无收藏内容</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- ======== POSTS VIEW ======== -->
|
||||
<view wx:elif="{{view === 'posts'}}" class="sub-view info-view-anim">
|
||||
<view class="sub-nav" bindtap="goBack">
|
||||
<t-icon name="chevron-left" size="40rpx" />
|
||||
<text class="sub-nav-title">我的发布</text>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="sub-scroll">
|
||||
<view wx:for="{{myPublishedPosts}}" wx:key="id" class="my-post-card">
|
||||
<view class="my-post-time">{{item.time}}</view>
|
||||
<view class="my-post-content-wrap">
|
||||
<text class="post-text">{{item.content}}</text>
|
||||
<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="{{img}}" mode="aspectFill" width="140rpx" height="140rpx"
|
||||
shape="round" />
|
||||
</view>
|
||||
<view class="my-post-footer">
|
||||
<view class="footer-item"><t-icon name="heart" size="28rpx" /> <text>{{item.likes.length}}</text></view>
|
||||
<view class="footer-item"><t-icon name="chat" size="28rpx" /> <text>{{item.comments.length}}</text></view>
|
||||
<view class="footer-item" catchtap="deletePost" data-id="{{item.id}}" style="margin-left:auto; color: #EF5350;">
|
||||
<t-icon name="delete" size="28rpx" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<!-- ======== ABOUT VIEW ======== -->
|
||||
<view wx:elif="{{view === 'about'}}" class="sub-view info-view-anim">
|
||||
<view wx:if="{{view === 'about'}}" class="sub-view info-view-anim">
|
||||
<view class="sub-nav" bindtap="goBack">
|
||||
<t-icon name="chevron-left" size="40rpx" />
|
||||
<text class="sub-nav-title">关于我们</text>
|
||||
@@ -134,12 +71,12 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="profile-content" enhanced show-scrollbar="{{false}}">
|
||||
<scroll-view scroll-y class="profile-content" enhanced show-scrollbar="{{false}}" scroll-top="{{scrollTop}}">
|
||||
<!-- Menu -->
|
||||
<view class="profile-menu">
|
||||
<view class="menu-group-title">常用功能</view>
|
||||
|
||||
<view class="menu-item" bindtap="setView" data-view="favorites">
|
||||
<view class="menu-item" bindtap="goToFavorites">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-bg" style="background: #FFF3E0">
|
||||
<t-icon name="star" size="36rpx" color="#FF9800" />
|
||||
@@ -149,7 +86,7 @@
|
||||
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
|
||||
</view>
|
||||
|
||||
<view class="menu-item" bindtap="setView" data-view="posts">
|
||||
<view class="menu-item" bindtap="goToPosts">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-bg" style="background: #E3F2FD">
|
||||
<t-icon name="file-copy" size="36rpx" color="#2196F3" />
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import request from '../../../utils/request';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
myPublishedPosts: [],
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
hasMore: true,
|
||||
isLoading: false,
|
||||
isRefreshing: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadMyPosts(true);
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
if (this.data.isLoading) return;
|
||||
this.setData({ isRefreshing: true });
|
||||
this.loadMyPosts(true).then(() => {
|
||||
this.setData({ isRefreshing: false });
|
||||
});
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
if (this.data.hasMore && !this.data.isLoading) {
|
||||
this.loadMyPosts(false);
|
||||
}
|
||||
},
|
||||
|
||||
async loadMyPosts(reset = false) {
|
||||
if (this.data.isLoading) return;
|
||||
this.setData({ isLoading: true });
|
||||
|
||||
const current = reset ? 1 : this.data.current;
|
||||
const { pageSize } = this.data;
|
||||
|
||||
try {
|
||||
const res = await request.post('/post/myPost', {
|
||||
current,
|
||||
pageSize
|
||||
});
|
||||
|
||||
const data = res.data || res || {};
|
||||
const records = data.records || data.list || [];
|
||||
|
||||
const posts = records.map(item => {
|
||||
const imgList = item.imgList || [];
|
||||
return {
|
||||
id: item.id,
|
||||
content: item.content || '',
|
||||
time: this._formatTime(item.createdAt || item.createTime),
|
||||
images: imgList.map(img => img.url),
|
||||
likes: item.likeList || [],
|
||||
comments: item.commentList || [],
|
||||
hasReviewed: item.hasReviewed
|
||||
};
|
||||
});
|
||||
|
||||
if (reset) {
|
||||
this.setData({
|
||||
myPublishedPosts: posts,
|
||||
current: current + 1,
|
||||
hasMore: posts.length >= pageSize,
|
||||
isLoading: false
|
||||
});
|
||||
} else {
|
||||
this.setData({
|
||||
myPublishedPosts: [...this.data.myPublishedPosts, ...posts],
|
||||
current: current + 1,
|
||||
hasMore: posts.length >= pageSize,
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Load my posts failed', err);
|
||||
this.setData({ isLoading: false });
|
||||
wx.showToast({ title: '加载失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
_formatTime(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const d = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diffMs = now - d;
|
||||
const diffMin = Math.floor(diffMs / 60000);
|
||||
if (diffMin < 1) return '刚刚';
|
||||
if (diffMin < 60) return diffMin + '分钟前';
|
||||
const diffHour = Math.floor(diffMin / 60);
|
||||
if (diffHour < 24) return diffHour + '小时前';
|
||||
const diffDay = Math.floor(diffHour / 24);
|
||||
if (diffDay < 7) return diffDay + '天前';
|
||||
const month = (d.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = d.getDate().toString().padStart(2, '0');
|
||||
return `${month}-${day}`;
|
||||
},
|
||||
|
||||
deletePost(e) {
|
||||
const postId = e.currentTarget.dataset.id;
|
||||
wx.showModal({
|
||||
title: '删除动态',
|
||||
content: '确定要删除这条动态吗?',
|
||||
confirmColor: '#EF5350',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return;
|
||||
wx.showLoading({ title: '删除中...' });
|
||||
|
||||
// Use new API: POST /post/delete with ids array
|
||||
request.post('/post/delete', { ids: [postId] }).then(() => {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '已删除', icon: 'success' });
|
||||
// Remove from list locally
|
||||
const newList = this.data.myPublishedPosts.filter(p => p.id !== postId);
|
||||
this.setData({ myPublishedPosts: newList });
|
||||
}).catch(() => {
|
||||
wx.hideLoading();
|
||||
wx.showToast({ title: '删除失败', icon: 'none' });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationBarTitleText": "我的发布",
|
||||
"disableScroll": true,
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-image": "tdesign-miniprogram/image/image"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<view class="posts-page">
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="sub-scroll"
|
||||
enhanced
|
||||
show-scrollbar="{{false}}"
|
||||
bindscrolltolower="onReachBottom"
|
||||
refresher-enabled="{{true}}"
|
||||
bindrefresherrefresh="onPullDownRefresh"
|
||||
refresher-triggered="{{isRefreshing}}"
|
||||
>
|
||||
<view wx:if="{{myPublishedPosts.length > 0}}" class="posts-list">
|
||||
<view wx:for="{{myPublishedPosts}}" wx:key="id" class="my-post-card">
|
||||
<view class="my-post-time">{{item.time}}</view>
|
||||
<view class="my-post-content-wrap">
|
||||
<view class="post-header" wx:if="{{item.hasReviewed !== undefined}}">
|
||||
<view class="status-tag pending" wx:if="{{item.hasReviewed === 0}}">待审核</view>
|
||||
<view class="status-tag success" wx:if="{{item.hasReviewed === 1}}">已发布</view>
|
||||
</view>
|
||||
<text class="post-text">{{item.content}}</text>
|
||||
<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="{{img}}" mode="aspectFill" width="140rpx" height="140rpx"
|
||||
shape="round" />
|
||||
</view>
|
||||
<view class="my-post-footer">
|
||||
<view class="footer-item"><t-icon name="heart" size="28rpx" /> <text>{{item.likes.length}}</text></view>
|
||||
<view class="footer-item"><t-icon name="chat" size="28rpx" /> <text>{{item.comments.length}}</text></view>
|
||||
<view class="footer-item" catchtap="deletePost" data-id="{{item.id}}" style="margin-left:auto; color: #EF5350;">
|
||||
<t-icon name="delete" size="28rpx" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:else class="empty-state">
|
||||
<t-icon name="file-copy" size="80rpx" color="#E0E0E0" style="margin-bottom: 24rpx;" />
|
||||
<text class="empty-text">暂无发布内容</text>
|
||||
</view>
|
||||
<view style="height: 60rpx;"></view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@@ -0,0 +1,170 @@
|
||||
.posts-page {
|
||||
background: #F4F6F0;
|
||||
height: 100vh;
|
||||
padding: 32rpx 32rpx 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sub-scroll {
|
||||
flex: 1;
|
||||
height: 0; /* Crucial for scrolling in flex layout */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.posts-list {
|
||||
position: relative;
|
||||
padding-bottom: 60rpx;
|
||||
}
|
||||
|
||||
/* Timeline Line */
|
||||
.posts-list::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 40rpx;
|
||||
bottom: 40rpx;
|
||||
left: 112rpx; /* Adjust based on time width */
|
||||
width: 2rpx;
|
||||
background: #E0E0E0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.my-post-card {
|
||||
display: flex;
|
||||
gap: 32rpx;
|
||||
margin-bottom: 48rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Time Column */
|
||||
.my-post-time {
|
||||
width: 100rpx;
|
||||
flex-shrink: 0;
|
||||
font-size: 26rpx;
|
||||
color: #90A4AE;
|
||||
font-weight: 700;
|
||||
text-align: right;
|
||||
padding-top: 28rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Timeline Dot */
|
||||
.my-post-time::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 40rpx;
|
||||
right: -24rpx; /* Center on line */
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
background: #fff;
|
||||
border: 4rpx solid #CFD8DC;
|
||||
border-radius: 50%;
|
||||
z-index: 2;
|
||||
box-shadow: 0 0 0 4rpx #F4F6F0; /* Outline mask */
|
||||
}
|
||||
|
||||
/* Highlight dot for today/recent? Optional */
|
||||
|
||||
/* Content Card */
|
||||
.my-post-content-wrap {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 28rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.03);
|
||||
padding: 32rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.my-post-content-wrap:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Post Text */
|
||||
.post-text {
|
||||
font-size: 30rpx;
|
||||
color: #374151;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24rpx;
|
||||
display: block;
|
||||
min-height: 48rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
/* Status Tags - Corner Style */
|
||||
.status-tag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 8rpx 20rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
border-bottom-left-radius: 24rpx;
|
||||
z-index: 10;
|
||||
box-shadow: -4rpx 4rpx 12rpx rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.status-tag.pending {
|
||||
background: #FFF8E1;
|
||||
color: #F57C00;
|
||||
}
|
||||
|
||||
.status-tag.success {
|
||||
background: #E8F5E9;
|
||||
color: #388E3C;
|
||||
}
|
||||
|
||||
/* Images */
|
||||
.my-post-images {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.my-post-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-top: 2rpx solid #F9FAFB;
|
||||
padding-top: 24rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.footer-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #9CA3AF;
|
||||
margin-right: 32rpx;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 160rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #B0BEC5;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Hide Scrollbar Globally */
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
color: transparent;
|
||||
display: none;
|
||||
}
|
||||
@@ -7,11 +7,20 @@ Page({
|
||||
groupedTasks: [],
|
||||
progress: 0,
|
||||
completingTask: null,
|
||||
remark: ''
|
||||
remark: '',
|
||||
remarkPlaceholder: '',
|
||||
scrollTop: 0,
|
||||
|
||||
// Reward Modals
|
||||
showLevelUpModal: false,
|
||||
levelUpData: null,
|
||||
showBadgeModal: false,
|
||||
badgeData: null
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.fetchTodayTasks();
|
||||
this._popupQueue = [];
|
||||
},
|
||||
|
||||
onShow() {
|
||||
@@ -23,6 +32,42 @@ Page({
|
||||
this.fetchTodayTasks();
|
||||
},
|
||||
|
||||
// ... (fetchTodayTasks, processTaskData Omitted) ...
|
||||
|
||||
// Reward Queue Processing
|
||||
processPopupQueue() {
|
||||
if (this._popupQueue.length === 0) return;
|
||||
|
||||
const next = this._popupQueue[0];
|
||||
if (next.type === 'level') {
|
||||
this.setData({
|
||||
levelUpData: next.data,
|
||||
showLevelUpModal: true
|
||||
});
|
||||
} else if (next.type === 'badge') {
|
||||
this.setData({
|
||||
badgeData: next.data,
|
||||
showBadgeModal: true
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
closeLevelUpModal() {
|
||||
this.setData({ showLevelUpModal: false });
|
||||
this._popupQueue.shift();
|
||||
setTimeout(() => {
|
||||
this.processPopupQueue();
|
||||
}, 300);
|
||||
},
|
||||
|
||||
closeBadgeModal() {
|
||||
this.setData({ showBadgeModal: false });
|
||||
this._popupQueue.shift();
|
||||
setTimeout(() => {
|
||||
this.processPopupQueue();
|
||||
}, 300);
|
||||
},
|
||||
|
||||
fetchTodayTasks() {
|
||||
request.get('/plant/todayTask').then(res => {
|
||||
// Check if res is array (list of PlantTaskVO)
|
||||
@@ -45,12 +90,7 @@ Page({
|
||||
// Parse Image
|
||||
let imageUrl = '';
|
||||
if (plant.imgList && plant.imgList.length > 0) {
|
||||
let url = plant.imgList[0].url;
|
||||
if (url && !url.startsWith('http') && !url.startsWith('/') && !url.startsWith('wxfile')) {
|
||||
imageUrl = '/assets/' + url;
|
||||
} else {
|
||||
imageUrl = url;
|
||||
}
|
||||
imageUrl = plant.imgList[0].url || '';
|
||||
}
|
||||
|
||||
const plantGroup = {
|
||||
@@ -149,9 +189,36 @@ Page({
|
||||
handleTaskClick(e) {
|
||||
const task = e.currentTarget.dataset.task;
|
||||
if (task.isCompleted) return;
|
||||
|
||||
// Compute placeholder based on task type or name
|
||||
let placeholder = '添加备注 (可选)...';
|
||||
// Get name from taskIcon first, then name.
|
||||
// name from backend is usually the name (e.g. "浇水").
|
||||
const typeRaw = (task.taskIcon ? task.taskIcon.name : task.name) || '';
|
||||
const typeStr = typeRaw.toLowerCase();
|
||||
|
||||
if (typeStr.includes('浇水')) {
|
||||
placeholder = '例如:浇了 200ml 水...';
|
||||
} else if (typeStr.includes('施肥')) {
|
||||
placeholder = '例如:施了通用液肥...';
|
||||
} else if (typeStr.includes('修剪')) {
|
||||
placeholder = '例如:修剪了枯叶和黄叶...';
|
||||
} else if (typeStr.includes('换盆')) {
|
||||
placeholder = '例如:更换了陶盆,加了底肥...';
|
||||
} else if (typeStr.includes('换土')) {
|
||||
placeholder = '例如:更换了新配方土...';
|
||||
} else if (typeStr.includes('喷雾')) {
|
||||
placeholder = '例如:给叶面喷了水...';
|
||||
} else if (typeStr.includes('用药') || typeStr.includes('虫')) {
|
||||
placeholder = '例如:喷洒了杀虫剂...';
|
||||
} else if (typeStr.includes('晒太阳') || typeStr.includes('日照')) {
|
||||
placeholder = '例如:晒了 2 小时太阳...';
|
||||
}
|
||||
|
||||
this.setData({
|
||||
completingTask: task,
|
||||
remark: ''
|
||||
remark: '',
|
||||
remarkPlaceholder: placeholder
|
||||
});
|
||||
},
|
||||
|
||||
@@ -172,16 +239,29 @@ Page({
|
||||
const taskId = this.data.completingTask.id;
|
||||
const remark = this.data.remark || '';
|
||||
|
||||
// Optimistic Update immediately for better feel?
|
||||
// Or wait for server? Wait is safer.
|
||||
wx.showLoading({ title: '提交中...', mask: true });
|
||||
|
||||
request.post('/plant/completeTask', {
|
||||
taskId: taskId,
|
||||
remark: remark
|
||||
}).then(() => {
|
||||
}).then(res => {
|
||||
wx.hideLoading();
|
||||
|
||||
// Handle Rewards
|
||||
const queue = [];
|
||||
// Check if res has level up or badge data
|
||||
// Note: res is already data.data from request.js
|
||||
if (res && res.isLevelUp && res.currentLevel) {
|
||||
queue.push({ type: 'level', data: res.currentLevel });
|
||||
}
|
||||
|
||||
// Check for Badge using IsGetBadge flag (allowing for casing variance)
|
||||
if (res && (res.IsGetBadge === true || res.isGetBadge === true) && res.newBadge) {
|
||||
queue.push({ type: 'badge', data: res.newBadge });
|
||||
}
|
||||
|
||||
this._popupQueue = queue;
|
||||
|
||||
// Optimistic UI Update Logic
|
||||
const groups = this.data.groupedTasks;
|
||||
let updated = false;
|
||||
@@ -208,10 +288,14 @@ Page({
|
||||
showSunshine: true
|
||||
});
|
||||
|
||||
// Hide Animation after duration
|
||||
// Hide Animation after duration and Start showing modals
|
||||
setTimeout(() => {
|
||||
this.setData({ showSunshine: false });
|
||||
}, 3000);
|
||||
// Show rewards after sunshine animation
|
||||
if (this._popupQueue.length > 0) {
|
||||
this.processPopupQueue();
|
||||
}
|
||||
}, 1000); // 1.0s delay usually covers animation
|
||||
|
||||
// Sync with backend silently
|
||||
this.fetchTodayTasks();
|
||||
@@ -227,5 +311,9 @@ Page({
|
||||
wx.switchTab({
|
||||
url: '/pages/garden/index'
|
||||
});
|
||||
},
|
||||
|
||||
onTabItemTap() {
|
||||
this.setData({ scrollTop: Math.random() * 0.01 });
|
||||
}
|
||||
})
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<t-button theme="primary" size="large" shape="round" bind:tap="gotoGarden" class="empty-btn">去看看花园</t-button>
|
||||
</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}}" scroll-top="{{scrollTop}}">
|
||||
<view wx:for="{{groupedTasks}}" wx:key="plantName" class="plant-task-card {{item.hasOverdue ? 'has-overdue' : ''}}">
|
||||
<view class="card-header-row">
|
||||
<view class="plant-info-brief">
|
||||
@@ -85,7 +85,7 @@
|
||||
</view>
|
||||
|
||||
<!-- Complete Task Popup -->
|
||||
<t-popup visible="{{completingTask}}" bind:visible-change="onPopupVisibleChange" placement="center">
|
||||
<t-popup visible="{{completingTask}}" bind:visible-change="onPopupVisibleChange" placement="center" close-on-overlay-click="{{false}}">
|
||||
<view class="modal-card">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">确认完成任务</text>
|
||||
@@ -110,7 +110,7 @@
|
||||
<text class="remark-label">添加记录备注 (可选)</text>
|
||||
<textarea
|
||||
class="remark-input"
|
||||
placeholder="例如:浇了500ml水..."
|
||||
placeholder="{{remarkPlaceholder}}"
|
||||
value="{{remark}}"
|
||||
bindinput="onRemarkInput"
|
||||
fixed="{{true}}"
|
||||
@@ -131,6 +131,48 @@
|
||||
</view>
|
||||
</t-popup>
|
||||
|
||||
<!-- Level Up Modal -->
|
||||
<t-popup visible="{{showLevelUpModal}}" bind:visible-change="closeLevelUpModal" placement="center" close-on-overlay-click="{{false}}">
|
||||
<view class="reward-modal">
|
||||
<view class="reward-floating-icon level-icon-bg">
|
||||
<t-icon name="chart-bar" size="80rpx" color="#fff" />
|
||||
</view>
|
||||
<view class="reward-content">
|
||||
<text class="reward-title">恭喜升级!</text>
|
||||
<view class="reward-level-tag">
|
||||
<text>Lv.{{levelUpData.level}} {{levelUpData.title}}</text>
|
||||
</view>
|
||||
<view class="reward-desc-box" wx:if="{{levelUpData.perks}}">
|
||||
<text style="font-weight: bold; display: block; margin-bottom: 8rpx; color: #EF6C00;">解锁特权</text>
|
||||
<text>{{levelUpData.perks}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="reward-btn-container">
|
||||
<t-button t-class="reward-btn btn-level" size="large" block bindtap="closeLevelUpModal">太棒了</t-button>
|
||||
</view>
|
||||
</view>
|
||||
</t-popup>
|
||||
|
||||
<!-- New Badge Modal -->
|
||||
<t-popup visible="{{showBadgeModal}}" bind:visible-change="closeBadgeModal" placement="center" close-on-overlay-click="{{false}}">
|
||||
<view class="reward-modal">
|
||||
<view class="reward-floating-icon badge-icon-bg">
|
||||
<image class="reward-badge-img-large" src="{{badgeData.icon.url}}" mode="aspectFit" wx:if="{{badgeData.icon && badgeData.icon.url}}" />
|
||||
<t-icon name="medal" size="80rpx" color="#fff" wx:else />
|
||||
</view>
|
||||
<view class="reward-content">
|
||||
<text class="reward-title">解锁新徽章!</text>
|
||||
<text class="reward-level-tag" style="background: #E3F2FD; color: #1565C0; border-color: #BBDEFB;">{{badgeData.name}}</text>
|
||||
<view class="reward-desc-box">
|
||||
<text>{{badgeData.description}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="reward-btn-container">
|
||||
<t-button t-class="reward-btn btn-badge" size="large" block bindtap="closeBadgeModal">收入囊中</t-button>
|
||||
</view>
|
||||
</view>
|
||||
</t-popup>
|
||||
|
||||
<!-- Sunshine Animation Layer -->
|
||||
<view class="sunshine-layer" wx:if="{{showSunshine}}">
|
||||
<view class="sunshine-pkg">
|
||||
|
||||
@@ -864,3 +864,142 @@
|
||||
0% { transform: translateY(20rpx); opacity: 0; }
|
||||
100% { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Reward Modals - Enhanced */
|
||||
.reward-modal {
|
||||
background: #fff;
|
||||
width: 580rpx;
|
||||
border-radius: 40rpx;
|
||||
padding-top: 100rpx; /* Space for floating icon */
|
||||
padding-bottom: 40rpx;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
overflow: visible; /* Allow floating elements */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 24rpx 64rpx rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Background decorator inside modal (top curve) */
|
||||
.reward-modal::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
background: linear-gradient(180deg, #F5F5F5 0%, #FFFFFF 100%);
|
||||
border-radius: 40rpx 40rpx 0 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Floating Icon Wrapper */
|
||||
.reward-floating-icon {
|
||||
position: absolute;
|
||||
top: -80rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 16rpx 32rpx rgba(0,0,0,0.15);
|
||||
z-index: 2;
|
||||
background: white; /* Fallback */
|
||||
}
|
||||
|
||||
.level-icon-bg {
|
||||
background: linear-gradient(135deg, #FFB74D, #EF6C00);
|
||||
border: 8rpx solid #fff;
|
||||
}
|
||||
|
||||
.badge-icon-bg {
|
||||
background: linear-gradient(135deg, #64B5F6, #1565C0);
|
||||
border: 8rpx solid #fff;
|
||||
}
|
||||
|
||||
/* Actual Image/Icon */
|
||||
.reward-badge-img-large {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
filter: drop-shadow(0 4rpx 8rpx rgba(0,0,0,0.2));
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.reward-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
padding: 0 48rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.reward-title {
|
||||
font-size: 44rpx;
|
||||
font-weight: 900;
|
||||
color: #263238;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
/* Level Styles */
|
||||
.reward-level-tag {
|
||||
background: #FFF3E0;
|
||||
color: #EF6C00;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 32rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
border: 2rpx solid #FFE0B2;
|
||||
}
|
||||
|
||||
/* Description Box */
|
||||
.reward-desc-box {
|
||||
background: #FAFAFA;
|
||||
border: 2rpx solid #F5F5F5;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
width: 100%;
|
||||
color: #546E7A;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Button */
|
||||
.reward-btn-container {
|
||||
width: 100%;
|
||||
padding: 0 48rpx;
|
||||
margin-top: 40rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Custom Button Style override */
|
||||
.reward-btn {
|
||||
border-radius: 48rpx !important;
|
||||
font-weight: 700 !important;
|
||||
font-size: 32rpx !important;
|
||||
height: 96rpx !important;
|
||||
box-shadow: 0 12rpx 24rpx rgba(255, 152, 0, 0.3); /* Default orange shadow */
|
||||
}
|
||||
|
||||
.btn-level {
|
||||
background: linear-gradient(90deg, #FF9800, #F57C00) !important;
|
||||
color: white !important;
|
||||
box-shadow: 0 12rpx 24rpx rgba(245, 124, 0, 0.4) !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.btn-badge {
|
||||
background: linear-gradient(90deg, #42A5F5, #1976D2) !important;
|
||||
color: white !important;
|
||||
box-shadow: 0 12rpx 24rpx rgba(25, 118, 210, 0.4) !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
@@ -10,25 +10,19 @@ Page({
|
||||
|
||||
onLoad(options) {
|
||||
const eventChannel = this.getOpenerEventChannel();
|
||||
let loadedFromEvent = false;
|
||||
|
||||
if (eventChannel && eventChannel.on) {
|
||||
eventChannel.on('acceptDataFromOpenerPage', (res) => {
|
||||
if (res.data) {
|
||||
this.setPlantData(res.data);
|
||||
loadedFromEvent = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (options.id) {
|
||||
// Give event channel a chance to fire
|
||||
setTimeout(() => {
|
||||
if (!loadedFromEvent && !this.data.plant) {
|
||||
// Always fetch latest data (especially favorites status)
|
||||
this.loadPlantDetail(options.id);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
|
||||
loadPlantDetail(id) {
|
||||
@@ -99,7 +93,8 @@ Page({
|
||||
pestsDiseases: item.pestsDiseases || '',
|
||||
commonPests,
|
||||
classes: (item.classes || []).map(c => c.name),
|
||||
imgList: item.imgList || []
|
||||
imgList: item.imgList || [],
|
||||
isFavorited: (item.hasStar === 1 || item.hasStar === '1')
|
||||
};
|
||||
|
||||
this.setData({
|
||||
@@ -108,6 +103,23 @@ Page({
|
||||
});
|
||||
},
|
||||
|
||||
toggleFavorite() {
|
||||
if (!this.data.plant) return;
|
||||
|
||||
const { id, isFavorited } = this.data.plant;
|
||||
const type = isFavorited ? 2 : 1;
|
||||
|
||||
request.get('/wiki/star', { id, type }).then(() => {
|
||||
this.setData({
|
||||
'plant.isFavorited': !isFavorited
|
||||
});
|
||||
wx.showToast({ title: type === 1 ? '已收藏' : '已取消', icon: 'success' });
|
||||
}).catch(err => {
|
||||
console.error('Toggle favorite failed', err);
|
||||
wx.showToast({ title: '操作失败', icon: 'none' });
|
||||
});
|
||||
},
|
||||
|
||||
onSwiperChange(e) {
|
||||
this.setData({
|
||||
activeImageIndex: e.detail.current
|
||||
|
||||
@@ -13,14 +13,24 @@
|
||||
<view class="wd-counter" wx:if="{{swiperList.length > 0}}">
|
||||
<text>{{activeImageIndex + 1}} / {{swiperList.length}}</text>
|
||||
</view>
|
||||
<!-- Share Button -->
|
||||
<button class="header-share-btn" open-type="share">
|
||||
<t-icon name="share" size="40rpx" color="#FFF" />
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- Plant Name Card -->
|
||||
<view class="wd-name-card">
|
||||
<view class="wd-name-row">
|
||||
<text class="wd-name">{{plant.name}}</text>
|
||||
<text class="wd-name" style="padding-right: 60rpx;">{{plant.name}}</text>
|
||||
<text class="wd-scientific" wx:if="{{plant.latinName}}">{{plant.latinName}}</text>
|
||||
</view>
|
||||
|
||||
<!-- Collect Button -->
|
||||
<view class="card-collect-btn" bindtap="toggleFavorite">
|
||||
<t-icon name="{{plant.isFavorited ? 'heart-filled' : 'heart'}}" size="52rpx" color="{{plant.isFavorited ? '#EF4444' : '#D1D5DB'}}" />
|
||||
</view>
|
||||
|
||||
<view class="wd-badges" wx:if="{{plant.genus || plant.classes.length > 0}}">
|
||||
<text class="wd-badge" wx:if="{{plant.genus}}">{{plant.genus}}</text>
|
||||
<text class="wd-badge" wx:for="{{plant.classes}}" wx:key="*this">{{item}}</text>
|
||||
@@ -38,6 +48,8 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<!-- Basic Info -->
|
||||
<view class="wd-section">
|
||||
<view class="section-title">
|
||||
@@ -183,8 +195,10 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view style="height: 120rpx;"></view>
|
||||
<view style="height: 40rpx;"></view>
|
||||
</scroll-view>
|
||||
|
||||
|
||||
</view>
|
||||
|
||||
<!-- Loading -->
|
||||
|
||||
@@ -243,3 +243,46 @@ page {
|
||||
justify-content: center;
|
||||
background: #F4F6F0;
|
||||
}
|
||||
|
||||
/* Header Share Button */
|
||||
.header-share-btn {
|
||||
position: absolute;
|
||||
top: 24rpx;
|
||||
right: 24rpx;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
background: rgba(0,0,0,0.35);
|
||||
backdrop-filter: blur(4px);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 20;
|
||||
border: none;
|
||||
padding: 0;
|
||||
line-height: normal;
|
||||
margin: 0;
|
||||
}
|
||||
.header-share-btn::after { border: none; }
|
||||
.header-share-btn:active {
|
||||
transform: scale(0.9);
|
||||
background: rgba(0,0,0,0.5);
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
/* Card Collect Button */
|
||||
.card-collect-btn {
|
||||
position: absolute;
|
||||
top: 32rpx;
|
||||
right: 32rpx;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 20;
|
||||
}
|
||||
.card-collect-btn:active {
|
||||
transform: scale(0.9);
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ Page({
|
||||
pageSize: 10,
|
||||
isLoading: false,
|
||||
hasMore: true,
|
||||
scrollTop: 0,
|
||||
|
||||
// Modal State
|
||||
showIdentifyModal: false
|
||||
@@ -35,6 +36,10 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
onTabItemTap() {
|
||||
this.setData({ scrollTop: Math.random() * 0.01 });
|
||||
},
|
||||
|
||||
// Fetch categories from API
|
||||
fetchCategories() {
|
||||
request.get('/wiki-class/list').then(res => {
|
||||
@@ -84,6 +89,7 @@ Page({
|
||||
genus: item.genus || '',
|
||||
difficulty: item.difficulty || 0,
|
||||
isHot: item.isHot === 1,
|
||||
isFavorited: item.hasStar === 1,
|
||||
image: (item.imgList && item.imgList.length > 0) ? item.imgList[0].url : '',
|
||||
classes: (item.classes || []).map(c => c.name),
|
||||
// Pass the full item for detail navigation
|
||||
@@ -115,6 +121,31 @@ Page({
|
||||
});
|
||||
},
|
||||
|
||||
// Toggle Favorite
|
||||
async toggleFavorite(e) {
|
||||
const id = e.currentTarget.dataset.id;
|
||||
const index = this.data.displayedList.findIndex(i => i.id === id);
|
||||
if (index === -1) return;
|
||||
|
||||
const item = this.data.displayedList[index];
|
||||
const type = item.isFavorited ? 2 : 1;
|
||||
|
||||
try {
|
||||
// Attempting consistent API pattern
|
||||
await request.get('/wiki/star', { id, type });
|
||||
|
||||
const key = `displayedList[${index}].isFavorited`;
|
||||
this.setData({
|
||||
[key]: !item.isFavorited
|
||||
});
|
||||
|
||||
wx.showToast({ title: type === 1 ? '已收藏' : '已取消', icon: 'success' });
|
||||
} catch (err) {
|
||||
console.error('Toggle favorite failed', err);
|
||||
wx.showToast({ title: '操作失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
|
||||
// Search Input Handler (debounced)
|
||||
onSearchInput(e) {
|
||||
const value = e.detail.value;
|
||||
@@ -149,6 +180,7 @@ Page({
|
||||
|
||||
goToDetail(e) {
|
||||
const item = e.currentTarget.dataset.item;
|
||||
|
||||
wx.navigateTo({
|
||||
url: `/pages/wiki/detail/index?id=${item.id}`,
|
||||
success: (res) => {
|
||||
|
||||
@@ -16,38 +16,35 @@
|
||||
bindscrolltolower="onReachBottom"
|
||||
enhanced
|
||||
show-scrollbar="{{false}}"
|
||||
scroll-top="{{scrollTop}}"
|
||||
>
|
||||
<view class="search-section">
|
||||
<t-search placeholder="搜索植物名称,如:龟背竹" value="{{searchQuery}}" bind:change="onSearchInput" shape="round" />
|
||||
<view class="search-box-card">
|
||||
<t-search placeholder="搜索植物名称,如:龟背竹" value="{{searchQuery}}" bind:change="onSearchInput" shape="round" t-class-input-container="search-input-bg" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Dynamic Categories from API -->
|
||||
<view class="category-scroll">
|
||||
<t-tag
|
||||
variant="{{activeCategory === 'all' ? 'dark' : 'outline'}}"
|
||||
theme="{{activeCategory === 'all' ? 'primary' : 'default'}}"
|
||||
shape="mark"
|
||||
size="medium"
|
||||
style="margin-right: 16rpx;"
|
||||
bind:tap="setCategory"
|
||||
<scroll-view class="category-scroll" scroll-x enable-flex show-scrollbar="{{false}}">
|
||||
<view
|
||||
class="category-item {{activeCategory === 'all' ? 'active' : ''}}"
|
||||
bindtap="setCategory"
|
||||
data-cat="all"
|
||||
>
|
||||
全部
|
||||
</t-tag>
|
||||
<t-tag
|
||||
<text>全部</text>
|
||||
</view>
|
||||
<view
|
||||
wx:for="{{categories}}"
|
||||
wx:key="id"
|
||||
variant="{{activeCategory === item.id ? 'dark' : 'outline'}}"
|
||||
theme="{{activeCategory === item.id ? 'primary' : 'default'}}"
|
||||
shape="mark"
|
||||
size="medium"
|
||||
style="margin-right: 16rpx;"
|
||||
bind:tap="setCategory"
|
||||
class="category-item {{activeCategory === item.id ? 'active' : ''}}"
|
||||
bindtap="setCategory"
|
||||
data-cat="{{item.id}}"
|
||||
>
|
||||
{{item.name}}
|
||||
</t-tag>
|
||||
<text>{{item.name}}</text>
|
||||
</view>
|
||||
<!-- Right spacer for scroll -->
|
||||
<view style="width: 40rpx; flex-shrink: 0;"></view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="wiki-list">
|
||||
<view wx:for="{{displayedList}}" wx:key="id" class="wiki-card" bindtap="goToDetail" data-item="{{item}}">
|
||||
@@ -96,7 +93,11 @@
|
||||
</t-tag>
|
||||
</view>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="48rpx" color="#ccc" />
|
||||
|
||||
<!-- Favorite Heart Button -->
|
||||
<view class="fav-action-btn" catchtap="toggleFavorite" data-id="{{item.id}}" style="padding: 16rpx; margin-right: -16rpx;">
|
||||
<t-icon name="{{item.isFavorited ? 'heart-filled' : 'heart'}}" size="48rpx" color="{{item.isFavorited ? '#FA5151' : '#CCC'}}" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -19,15 +19,71 @@
|
||||
Usually easier to apply padding to the items or a wrapper inside scroll-view. */
|
||||
|
||||
.search-section {
|
||||
padding: 20rpx 40rpx 0;
|
||||
padding: 24rpx 40rpx 0;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.search-box-card {
|
||||
background: #fff;
|
||||
border-radius: 40rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(85, 139, 47, 0.08); /* Green-tinted shadow */
|
||||
}
|
||||
|
||||
/* Override TDesign Search */
|
||||
.search-input-bg {
|
||||
background: #F4F6F0 !important; /* Light green-grey */
|
||||
border-radius: 32rpx !important;
|
||||
height: 80rpx !important;
|
||||
}
|
||||
.t-search__input-container {
|
||||
height: 80rpx !important;
|
||||
}
|
||||
|
||||
/* Category Scroll */
|
||||
.category-scroll {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 40rpx;
|
||||
margin-bottom: 48rpx;
|
||||
white-space: nowrap;
|
||||
padding-left: 40rpx;
|
||||
margin-bottom: 40rpx;
|
||||
height: 88rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for webkit */
|
||||
.category-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 36rpx;
|
||||
height: 72rpx;
|
||||
background: #fff;
|
||||
border-radius: 36rpx;
|
||||
margin-right: 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #546E7A;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04);
|
||||
transition: all 0.2s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
flex-shrink: 0;
|
||||
border: 2rpx solid transparent; /* Reserve border space */
|
||||
}
|
||||
|
||||
.category-item:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.category-item.active {
|
||||
background: #558B2F;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8rpx 20rpx rgba(85, 139, 47, 0.3);
|
||||
border-color: #558B2F;
|
||||
}
|
||||
|
||||
.wiki-list {
|
||||
@@ -59,6 +115,28 @@
|
||||
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.1);
|
||||
flex-shrink: 0;
|
||||
background: #f0f0f0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fav-icon-box {
|
||||
position: absolute;
|
||||
top: 10rpx;
|
||||
right: 10rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(4px);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 5;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.fav-icon-box:active {
|
||||
transform: scale(0.9);
|
||||
transition: transform 0.1s;
|
||||
}
|
||||
.wiki-image image { width: 100%; height: 100%; display: block; }
|
||||
.wiki-image-placeholder {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"projectname": "plant-mp",
|
||||
"condition": {},
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"urlCheck": true,
|
||||
"coverView": true,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"skylineRenderEnable": false,
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
|
||||
// 预置养护图标 - 模拟从后端加载
|
||||
export const CARE_TASK_ICONS = [
|
||||
{ id: 'water', name: '浇水', icon: 'rain-medium', color: '#2196F3', bgColor: '#E3F2FD' },
|
||||
{ id: 'fertilize', name: '施肥', icon: 'gift', color: '#8BC34A', bgColor: '#F1F8E9' },
|
||||
{ id: 'prune', name: '修剪', icon: 'cut', color: '#FF9800', bgColor: '#FFF3E0' },
|
||||
{ id: 'repot', name: '换盆', icon: 'home', color: '#9C27B0', bgColor: '#F3E5F5' },
|
||||
{ id: 'resoil', name: '换土', icon: 'load', color: '#757575', bgColor: '#F5F5F5' },
|
||||
{ id: 'rotate', name: '转盆', icon: 'refresh', color: '#00BCD4', bgColor: '#E0F7FA' },
|
||||
{ id: 'spray', name: '喷雾', icon: 'cloud', color: '#03A9F4', bgColor: '#E1F5FE' },
|
||||
{ id: 'check', name: '检查', icon: 'search', color: '#4CAF50', bgColor: '#E8F5E9' },
|
||||
{ id: 'sun', name: '晒太阳', icon: 'sunny', color: '#FFC107', bgColor: '#FFFDE7' },
|
||||
{ id: 'move', name: '挪位置', icon: 'location', color: '#E91E63', bgColor: '#FCE4EC' },
|
||||
{ id: 'medicine', name: '用药', icon: 'heart', color: '#F44336', bgColor: '#FFEBEE' },
|
||||
{ id: 'other', name: '其他', icon: 'ellipsis', color: '#607D8B', bgColor: '#ECEFF1' },
|
||||
|
||||
{ id: 'water', name: '浇水', icon: 'rain-medium', color: '#2196F3', bgColor: '#E3F2FD', targetAction: 'ACT_WATER' },
|
||||
{ id: 'fertilize', name: '施肥', icon: 'gift', color: '#8BC34A', bgColor: '#F1F8E9', targetAction: 'ACT_FERTILIZE' },
|
||||
{ id: 'prune', name: '修剪', icon: 'cut', color: '#FF9800', bgColor: '#FFF3E0', targetAction: 'ACT_PRUNE' },
|
||||
{ id: 'repot', name: '换盆', icon: 'home', color: '#9C27B0', bgColor: '#F3E5F5', targetAction: 'ACT_REPOT' },
|
||||
{ id: 'resoil', name: '换土', icon: 'load', color: '#757575', bgColor: '#F5F5F5', targetAction: 'ACT_RESOIL' },
|
||||
{ id: 'rotate', name: '转盆', icon: 'refresh', color: '#00BCD4', bgColor: '#E0F7FA', targetAction: 'ACT_ROTATE' },
|
||||
{ id: 'spray', name: '喷雾', icon: 'cloud', color: '#03A9F4', bgColor: '#E1F5FE', targetAction: 'ACT_SPRAY' },
|
||||
{ id: 'check', name: '检查', icon: 'search', color: '#4CAF50', bgColor: '#E8F5E9', targetAction: 'ACT_CHECK' },
|
||||
{ id: 'sun', name: '晒太阳', icon: 'sunny', color: '#FFC107', bgColor: '#FFFDE7', targetAction: 'ACT_SUN' },
|
||||
{ id: 'move', name: '挪位置', icon: 'location', color: '#E91E63', bgColor: '#FCE4EC', targetAction: 'ACT_MOVE' },
|
||||
{ id: 'medicine', name: '用药', icon: 'heart', color: '#F44336', bgColor: '#FFEBEE', targetAction: 'ACT_MEDICINE' },
|
||||
{ id: 'other', name: '其他', icon: 'ellipsis', color: '#607D8B', bgColor: '#ECEFF1', targetAction: 'ACT_OTHER' },
|
||||
];
|
||||
|
||||
export const MOCK_BADGES = [
|
||||
@@ -270,8 +270,8 @@ class WxRequest {
|
||||
|
||||
// Initialize with default instance
|
||||
const request = new WxRequest({
|
||||
baseUrl: 'http://192.168.0.184:8889',
|
||||
//baseUrl: 'https://go.sundynix.cn/api',
|
||||
//baseUrl: 'http://192.168.0.184:8889',
|
||||
baseUrl: 'https://go.sundynix.cn/api',
|
||||
header: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||