init: initial commit

This commit is contained in:
Blizzard
2026-02-04 14:02:31 +08:00
commit 6ceda92e9d
2234 changed files with 38231 additions and 0 deletions
+273
View File
@@ -0,0 +1,273 @@
// pages/community/create/index.js
import { MOCK_POSTS } from '../../../utils/mockData';
Page({
data: {
content: '',
images: [],
canPublish: false,
autoFocus: true,
location: '',
selectedTopics: [],
suggestedTopics: ['植物养护', '多肉日记', '绿植分享', '花卉美照', '阳台花园', '新手入门'],
hasDraft: false,
showImageSheet: false,
imageSheetItems: [
{ label: '拍照', value: 'camera' },
{ label: '从相册选择', value: 'album' }
]
},
onLoad() {
// Check for saved draft
this.loadDraft();
},
onUnload() {
// Save draft if there's content
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) {
const content = e.detail.value;
this.setData({
content,
canPublish: content.trim().length > 0,
hasDraft: false
});
},
showImageSourceSheet() {
this.setData({ showImageSheet: true });
},
hideImageSheet() {
this.setData({ showImageSheet: false });
},
onImageSheetSelect(e) {
const { value } = e.detail.selected;
this.setData({ showImageSheet: false });
if (value === 'camera') {
this.takePhoto();
} else {
this.chooseImage();
}
},
chooseImage() {
const remaining = 9 - this.data.images.length;
if (remaining <= 0) {
wx.showToast({ title: '最多9张图片', icon: 'none' });
return;
}
wx.chooseMedia({
count: remaining,
mediaType: ['image'],
sourceType: ['album'],
success: (res) => {
const newImages = res.tempFiles.map(f => f.tempFilePath);
this.setData({
images: [...this.data.images, ...newImages],
hasDraft: false
});
}
});
},
takePhoto() {
if (this.data.images.length >= 9) {
wx.showToast({ title: '最多9张图片', icon: 'none' });
return;
}
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['camera'],
camera: 'back',
success: (res) => {
const newImage = res.tempFiles[0].tempFilePath;
this.setData({
images: [...this.data.images, newImage],
hasDraft: false
});
}
});
},
removeImage(e) {
const index = e.currentTarget.dataset.index;
const images = [...this.data.images];
images.splice(index, 1);
this.setData({ images });
},
showImageMenu(e) {
const index = e.currentTarget.dataset.index;
wx.showActionSheet({
itemList: ['设为封面', '删除'],
success: (res) => {
if (res.tapIndex === 0) {
// Move to first position
const images = [...this.data.images];
const [img] = images.splice(index, 1);
images.unshift(img);
this.setData({ images });
wx.showToast({ title: '已设为封面', icon: 'success' });
} else if (res.tapIndex === 1) {
this.removeImage({ currentTarget: { dataset: { index } } });
}
}
});
},
chooseLocation() {
wx.chooseLocation({
success: (res) => {
this.setData({ location: res.name || res.address });
},
fail: () => {
// User cancelled or no permission
}
});
},
toggleTopic(e) {
const topic = e.currentTarget.dataset.topic;
const hashtag = `#${topic} `;
let { content, selectedTopics } = this.data;
if (selectedTopics.includes(topic)) {
// Remove topic and hashtag from content
selectedTopics = selectedTopics.filter(t => t !== topic);
content = content.replace(hashtag, '');
} else {
if (selectedTopics.length >= 3) {
wx.showToast({ title: '最多选择3个话题', icon: 'none' });
return;
}
// Add topic and insert hashtag into content
selectedTopics.push(topic);
content = content + hashtag;
}
this.setData({
selectedTopics,
content,
canPublish: content.trim().length > 0
});
},
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() {
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();
}
}
});
} else {
wx.navigateBack();
}
},
handlePublish() {
if (!this.data.canPublish) {
wx.showToast({ title: '请输入内容', icon: 'none' });
return;
}
// Content already includes topics as hashtags
const finalContent = this.data.content.trim();
// Create new post
const newPost = {
id: Date.now().toString(),
user: '我的花园',
content: finalContent,
images: this.data.images,
time: '刚刚',
likes: [],
comments: []
};
// Add to global mock data (at the beginning)
MOCK_POSTS.unshift(newPost);
// Clear draft
this.clearDraft();
wx.showToast({ title: '发布成功', icon: 'success' });
setTimeout(() => {
wx.navigateBack();
}, 1000);
}
})
+9
View File
@@ -0,0 +1,9 @@
{
"navigationBarTitleText": "发布动态",
"navigationStyle": "custom",
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon",
"t-image": "tdesign-miniprogram/image/image",
"t-action-sheet": "tdesign-miniprogram/action-sheet/action-sheet"
}
}
+100
View File
@@ -0,0 +1,100 @@
<view class="create-post-page">
<!-- Content Area -->
<scroll-view scroll-y class="create-content" enhanced="{{true}}" show-scrollbar="{{false}}">
<!-- Action Row: Cancel + Publish -->
<view class="action-row">
<view class="cancel-btn" bindtap="handleCancel">
<text>取消</text>
</view>
<view class="publish-btn {{canPublish ? 'active' : ''}}" bindtap="handlePublish">
<text>发布</text>
</view>
</view>
<!-- Text Input -->
<textarea
class="post-textarea"
placeholder="分享你的植物养护心得..."
placeholder-class="textarea-placeholder"
value="{{content}}"
bindinput="onContentInput"
maxlength="500"
auto-height
focus="{{autoFocus}}"
/>
<!-- Character Counter -->
<view class="char-counter" wx:if="{{content.length > 0}}">
<text class="{{content.length >= 450 ? 'warning' : ''}}">{{content.length}}</text>
<text class="total">/500</text>
</view>
<!-- Image Preview Grid -->
<view wx:if="{{images.length > 0}}" class="image-section">
<view class="image-preview-grid">
<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%" />
<view class="remove-btn" catchtap="removeImage" data-index="{{index}}">
<t-icon name="close" size="24rpx" color="#fff" />
</view>
<view class="image-index">{{index + 1}}</view>
</view>
<!-- Add More Button -->
<view wx:if="{{images.length < 9}}" class="add-image-btn" bindtap="showImageSourceSheet">
<t-icon name="add" size="48rpx" color="#999" />
<text class="add-count">{{images.length}}/9</text>
</view>
</view>
</view>
<!-- Add Image Area (when no images) -->
<view wx:else class="add-first-image" bindtap="showImageSourceSheet">
<view class="image-upload-box">
<view class="upload-icon">
<t-icon name="photo" size="64rpx" color="#558B2F" />
</view>
<text class="upload-text">添加图片</text>
<text class="upload-hint">分享你的植物美照,最多9张</text>
</view>
</view>
<!-- Location Tag (Optional Feature) -->
<view class="location-section" bindtap="chooseLocation">
<view class="location-left">
<t-icon name="location" size="40rpx" color="#558B2F" />
<text class="location-text">{{location || '添加位置'}}</text>
</view>
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
</view>
<!-- Topic Tags -->
<view class="topic-section">
<view class="section-title">
<t-icon name="hashtag" size="36rpx" color="#558B2F" />
<text>添加话题</text>
</view>
<view class="topic-list">
<view class="topic-tag {{selectedTopics.includes(item) ? 'selected' : ''}}"
wx:for="{{suggestedTopics}}"
wx:key="*this"
bindtap="toggleTopic"
data-topic="{{item}}">
<text>#{{item}}</text>
</view>
</view>
</view>
<!-- Bottom Spacer -->
<view style="height: 60rpx;"></view>
</scroll-view>
<!-- Image Source Action Sheet -->
<t-action-sheet
visible="{{showImageSheet}}"
items="{{imageSheetItems}}"
bind:selected="onImageSheetSelect"
bind:cancel="hideImageSheet"
show-cancel
/>
</view>
+316
View File
@@ -0,0 +1,316 @@
/** pages/community/create/index.wxss **/
page {
height: 100%;
background: #fff;
}
.create-post-page {
height: 100vh;
display: flex;
flex-direction: column;
background: #fff;
}
/* Content Area */
.create-content {
flex: 1;
padding: 0 32rpx;
padding-top: calc(env(safe-area-inset-top) + 100rpx);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.create-content::-webkit-scrollbar {
display: none;
}
/* Action Row */
.action-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 0;
margin-bottom: 32rpx;
}
/* Cancel Button */
.cancel-btn {
font-size: 30rpx;
color: #666;
padding: 12rpx 0;
}
.cancel-btn:active {
opacity: 0.6;
}
/* Publish Button */
.publish-btn {
font-size: 28rpx;
color: #999;
background: #f0f0f0;
padding: 16rpx 40rpx;
border-radius: 32rpx;
transition: all 0.2s;
}
.publish-btn.active {
color: #fff;
background: linear-gradient(135deg, #689F38, #558B2F);
box-shadow: 0 8rpx 24rpx rgba(85, 139, 47, 0.3);
}
.publish-btn.active:active {
transform: scale(0.95);
}
/* Textarea */
.post-textarea {
width: 100%;
min-height: 200rpx;
font-size: 32rpx;
line-height: 1.7;
color: #333;
padding: 32rpx 0;
box-sizing: border-box;
}
.textarea-placeholder {
color: #bbb;
}
/* Character Counter */
.char-counter {
text-align: right;
font-size: 24rpx;
margin-bottom: 24rpx;
}
.char-counter text {
color: #999;
}
.char-counter .warning {
color: #FF9800;
}
.char-counter .total {
color: #ccc;
}
/* Image Section */
.image-section {
margin-bottom: 32rpx;
}
/* Image Preview Grid */
.image-preview-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12rpx;
}
.preview-item {
position: relative;
aspect-ratio: 1;
border-radius: 12rpx;
overflow: hidden;
background: #f5f5f5;
}
.remove-btn {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 44rpx;
height: 44rpx;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.remove-btn:active {
background: rgba(0, 0, 0, 0.7);
}
.image-index {
position: absolute;
bottom: 8rpx;
left: 8rpx;
width: 36rpx;
height: 36rpx;
background: rgba(0, 0, 0, 0.5);
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
color: #fff;
}
.add-image-btn {
aspect-ratio: 1;
border: 2rpx dashed #ddd;
border-radius: 12rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
background: #fafafa;
}
.add-image-btn:active {
background: #f0f0f0;
}
.add-count {
font-size: 22rpx;
color: #999;
}
/* Add First Image */
.add-first-image {
margin: 32rpx 0;
}
.image-upload-box {
padding: 64rpx 48rpx;
border: 2rpx dashed #C8E6C9;
border-radius: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
background: linear-gradient(180deg, #F8FCF8, #fff);
}
.image-upload-box:active {
background: #F0F7F0;
}
.upload-icon {
width: 120rpx;
height: 120rpx;
background: #E8F5E9;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24rpx;
}
.upload-text {
font-size: 32rpx;
color: #558B2F;
font-weight: 600;
margin-bottom: 8rpx;
}
.upload-hint {
font-size: 26rpx;
color: #90A4AE;
}
/* Location Section */
.location-section {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 0;
border-top: 2rpx solid #f5f5f5;
border-bottom: 2rpx solid #f5f5f5;
}
.location-left {
display: flex;
align-items: center;
gap: 16rpx;
}
.location-text {
font-size: 28rpx;
color: #666;
}
/* Topic Section */
.topic-section {
padding: 28rpx 0;
}
.section-title {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 20rpx;
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.topic-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.topic-tag {
padding: 12rpx 24rpx;
background: #f5f5f5;
border-radius: 32rpx;
font-size: 26rpx;
color: #666;
transition: all 0.2s;
}
.topic-tag.selected {
background: #E8F5E9;
color: #558B2F;
font-weight: 500;
}
.topic-tag:active {
transform: scale(0.95);
}
/* Bottom Toolbar */
.create-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 32rpx;
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
border-top: 2rpx solid #f5f5f5;
background: #fff;
flex-shrink: 0;
}
.toolbar-left {
display: flex;
gap: 24rpx;
}
.toolbar-btn {
width: 72rpx;
height: 72rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.toolbar-btn:active {
background: #f5f5f5;
}
.toolbar-right {
display: flex;
align-items: center;
}
.draft-hint {
font-size: 24rpx;
color: #999;
}
+160
View File
@@ -0,0 +1,160 @@
// pages/community/index.js
import { MOCK_POSTS } from '../../utils/mockData';
Page({
data: {
posts: [],
displayedPosts: [],
activePostId: null, // For showing action popup
showCommentBar: false,
commentingPostId: null,
commentText: ''
},
onLoad() {
this.setData({ posts: MOCK_POSTS });
this.updateDisplayedPosts();
},
onShow() {
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 2 });
}
// Refresh posts in case new ones were added
this.setData({ posts: MOCK_POSTS });
this.updateDisplayedPosts();
},
updateDisplayedPosts() {
const { posts } = this.data;
// Show all posts with likedByMe flag
const displayed = posts.map(post => ({
...post,
likedByMe: post.likes.includes('我的花园')
}));
this.setData({ displayedPosts: displayed });
},
// 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}`;
});
wx.previewImage({
current: url,
urls: resolvedUrls
});
},
// Toggle action popup (WeChat style)
toggleActionPopup(e) {
const postId = e.currentTarget.dataset.id;
if (this.data.activePostId === postId) {
this.setData({ activePostId: null });
} else {
this.setData({ activePostId: postId });
}
},
hideActionPopup() {
if (this.data.activePostId) {
this.setData({ activePostId: null });
}
},
stopPropagation() {
// Just stop event propagation, do nothing
},
// Like post
likePost(e) {
const postId = e.currentTarget.dataset.id;
const posts = this.data.posts.map(post => {
if (post.id === postId) {
const likes = [...post.likes];
const myName = '我的花园';
if (likes.includes(myName)) {
// Unlike
const idx = likes.indexOf(myName);
likes.splice(idx, 1);
} else {
// Like
likes.push(myName);
}
return { ...post, likes };
}
return post;
});
this.setData({
posts,
activePostId: null // Hide popup after action
}, () => {
this.updateDisplayedPosts();
});
},
// Show comment input bar
showCommentInput(e) {
const postId = e.currentTarget.dataset.id;
this.setData({
showCommentBar: true,
commentingPostId: postId,
commentText: '',
activePostId: null // Hide popup
});
},
hideCommentBar() {
this.setData({
showCommentBar: false,
commentingPostId: null,
commentText: ''
});
},
onCommentInput(e) {
this.setData({ commentText: e.detail.value });
},
submitComment() {
const { commentText, commentingPostId } = this.data;
if (!commentText.trim()) {
return;
}
const posts = this.data.posts.map(post => {
if (post.id === commentingPostId) {
const comments = [...post.comments, {
id: Date.now().toString(),
user: '我的花园',
content: commentText.trim()
}];
return { ...post, comments };
}
return post;
});
this.setData({
posts,
showCommentBar: false,
commentingPostId: null,
commentText: ''
}, () => {
this.updateDisplayedPosts();
wx.showToast({ title: '评论成功', icon: 'success' });
});
},
goToCreatePost() {
wx.navigateTo({
url: '/pages/community/create/index'
});
}
})
+12
View File
@@ -0,0 +1,12 @@
{
"navigationBarTitleText": "植友动态",
"usingComponents": {
"t-avatar": "tdesign-miniprogram/avatar/avatar",
"t-image": "tdesign-miniprogram/image/image",
"t-tabs": "tdesign-miniprogram/tabs/tabs",
"t-tab-panel": "tdesign-miniprogram/tab-panel/tab-panel",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-button": "tdesign-miniprogram/button/button",
"t-loading": "tdesign-miniprogram/loading/loading"
}
}
+138
View File
@@ -0,0 +1,138 @@
<wxs src="../../utils/tools.wxs" module="tools" />
<view class="community-page">
<!-- Header with User Info -->
<view class="community-header">
<view class="user-info">
<view class="user-avatar">
<text>我</text>
</view>
<text class="user-name">我的花园</text>
</view>
</view>
<!-- Posts Feed -->
<scroll-view
scroll-y
class="moments-feed"
enhanced="{{true}}"
show-scrollbar="{{false}}"
bindtap="hideActionPopup"
>
<block wx:if="{{displayedPosts.length > 0}}">
<view wx:for="{{displayedPosts}}" wx:key="id" class="moment-post">
<!-- Avatar -->
<view class="post-avatar">
<view class="avatar-square">
<text>{{item.user[0]}}</text>
</view>
</view>
<!-- Content -->
<view class="post-content">
<text class="post-user-name">{{item.user}}</text>
<text class="post-text">{{item.content}}</text>
<!-- 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: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}}">
<t-image
src="{{tools.resolvePath(img)}}"
mode="aspectFill"
width="100%"
height="100%"
lazy-load
/>
</view>
</view>
<!-- Meta: Time + Action -->
<view class="post-meta">
<text class="post-time">{{item.time}}</text>
<!-- Action Button & Popup Container -->
<view class="action-container">
<!-- 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" />
<text>{{item.likedByMe ? '取消' : '赞'}}</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>
</view>
</view>
<!-- Trigger Button -->
<view class="action-btn" catchtap="toggleActionPopup" data-id="{{item.id}}">
<view class="action-dot"></view>
<view class="action-dot"></view>
</view>
</view>
</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="#576b95" />
<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>
</view>
</view>
<!-- Divider -->
<view wx:if="{{item.likes.length > 0 && item.comments.length > 0}}" class="divider"></view>
<!-- Comments -->
<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">
<text class="comment-user">{{comment.user}}</text>
<text class="comment-content">{{comment.content}}</text>
</view>
</view>
</view>
</view>
</view>
</block>
<!-- Empty State -->
<view wx:else class="empty-feed">
<view class="empty-icon">
<t-icon name="chat" size="80rpx" color="#ccc" />
</view>
<text class="empty-text">暂无相关动态</text>
<text class="empty-hint">快来发布第一条动态吧</text>
</view>
<!-- Bottom Spacer -->
<view style="height: 160rpx;"></view>
</scroll-view>
<!-- Floating Action Button -->
<view class="fab" bindtap="goToCreatePost">
<t-icon name="add" size="48rpx" color="#fff" />
</view>
<!-- Comment Input Bar (WeChat Style) -->
<view class="comment-input-bar {{showCommentBar ? 'show' : ''}}">
<view class="comment-input-mask" bindtap="hideCommentBar"></view>
<view class="comment-input-content">
<input
class="comment-input"
placeholder="评论..."
value="{{commentText}}"
bindinput="onCommentInput"
bindconfirm="submitComment"
focus="{{showCommentBar}}"
confirm-type="send"
adjust-position="{{true}}"
/>
<view class="send-btn {{commentText ? 'active' : ''}}" bindtap="submitComment">
<text>发送</text>
</view>
</view>
</view>
</view>
+431
View File
@@ -0,0 +1,431 @@
/** pages/community/index.wxss **/
page {
height: 100%;
overflow: hidden;
}
.community-page {
background-color: #fff;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Header with User Info */
.community-header {
display: flex;
align-items: center;
padding: 24rpx 40rpx;
padding-top: calc(24rpx + env(safe-area-inset-top));
background: white;
flex-shrink: 0;
border-bottom: 2rpx solid rgba(0, 0, 0, 0.03);
}
.community-header .user-info {
display: flex;
align-items: center;
gap: 20rpx;
}
.community-header .user-avatar {
width: 80rpx;
height: 80rpx;
background: linear-gradient(135deg, #E8F5E9, #C8E6C9);
color: #558B2F;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 32rpx;
}
.community-header .user-name {
font-size: 34rpx;
font-weight: 700;
color: #333;
}
/* Feed Scroll Area */
.moments-feed {
flex: 1;
padding: 0 40rpx;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.moments-feed::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
/* Post Card */
.moment-post {
display: flex;
gap: 24rpx;
margin-top: 40rpx;
padding-bottom: 40rpx;
border-bottom: 2rpx solid #f5f5f5;
}
.moment-post:last-child {
border-bottom: none;
}
/* Avatar - WeChat Style Rounded Square */
.post-avatar {
flex-shrink: 0;
}
.avatar-square {
width: 88rpx;
height: 88rpx;
background: linear-gradient(135deg, #E8F5E9, #C8E6C9);
color: #558B2F;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 36rpx;
}
/* Post Content */
.post-content {
flex: 1;
overflow: hidden;
min-width: 0;
}
.post-user-name {
color: #576b95;
font-weight: 600;
font-size: 30rpx;
margin-bottom: 12rpx;
display: block;
}
.post-text {
font-size: 30rpx;
color: #333;
line-height: 1.6;
margin-bottom: 20rpx;
display: block;
word-wrap: break-word;
}
/* Image Grid - CSS Grid Layout */
.post-images-grid {
display: grid;
gap: 8rpx;
margin-bottom: 20rpx;
border-radius: 16rpx;
overflow: hidden;
}
.post-image-item {
position: relative;
background: #f0f0f0;
overflow: hidden;
}
/* Single Image */
.grid-1 {
display: flex;
width: 70%;
}
.grid-1 .post-image-item {
width: 100%;
border-radius: 16rpx;
}
.grid-1 .post-image-item t-image {
border-radius: 16rpx;
}
/* 2-4 Images (2 columns) */
.grid-2 {
grid-template-columns: repeat(2, 1fr);
width: 75%;
}
.grid-2 .post-image-item {
aspect-ratio: 1;
}
/* 5+ Images (3 columns) */
.grid-3 {
grid-template-columns: repeat(3, 1fr);
}
.grid-3 .post-image-item {
aspect-ratio: 1;
}
/* Post Meta */
.post-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
height: 44rpx;
}
.post-time {
font-size: 24rpx;
color: #a0a0a0;
}
.action-btn {
background: #f7f7f7;
border-radius: 8rpx;
width: 72rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
}
.action-btn:active {
background: #e8e8e8;
}
.action-dot {
width: 10rpx;
height: 10rpx;
background: #576b95;
border-radius: 50%;
}
/* Likes & Comments Box */
.likes-comments-box {
background: #f7f7f7;
border-radius: 8rpx;
padding: 20rpx 24rpx;
position: relative;
font-size: 26rpx;
color: #333;
}
.likes-comments-box::before {
content: '';
position: absolute;
top: -12rpx;
left: 24rpx;
border-width: 0 12rpx 12rpx;
border-style: solid;
border-color: transparent transparent #f7f7f7;
}
.likes-section {
display: flex;
align-items: flex-start;
gap: 12rpx;
color: #576b95;
line-height: 1.5;
}
.likes-list {
flex: 1;
font-weight: 500;
}
.like-name {
color: #576b95;
}
.divider {
height: 2rpx;
background: rgba(0, 0, 0, 0.05);
margin: 16rpx 0;
}
.comments-section {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.comment-item {
line-height: 1.5;
}
.comment-user {
color: #576b95;
font-weight: 500;
}
.comment-content {
color: #333;
}
/* Empty State */
.empty-feed {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx;
color: #999;
}
.empty-icon {
width: 160rpx;
height: 160rpx;
background: #f5f5f5;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 32rpx;
}
.empty-text {
font-size: 32rpx;
font-weight: 600;
color: #666;
margin-bottom: 12rpx;
}
.empty-hint {
font-size: 26rpx;
color: #999;
}
/* Floating Action Button */
.fab {
position: fixed;
right: 40rpx;
bottom: 200rpx;
width: 112rpx;
height: 112rpx;
background: linear-gradient(135deg, #689F38, #558B2F);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12rpx 32rpx rgba(85, 139, 47, 0.4);
z-index: 100;
transition: all 0.2s;
}
.fab:active {
transform: scale(0.92);
box-shadow: 0 6rpx 16rpx rgba(85, 139, 47, 0.3);
}
/* WeChat Style Action Container */
.action-container {
position: relative;
display: flex;
align-items: center;
}
/* WeChat Style Action Popup */
.action-popup {
position: absolute;
right: 80rpx;
top: 50%;
transform: translateY(-50%) scaleX(0);
transform-origin: right center;
display: flex;
align-items: center;
background: #4c4c4c;
border-radius: 8rpx;
overflow: hidden;
opacity: 0;
transition: all 0.2s ease-out;
z-index: 10;
}
.action-popup.show {
transform: translateY(-50%) scaleX(1);
opacity: 1;
}
.popup-btn {
display: flex;
align-items: center;
gap: 8rpx;
padding: 16rpx 24rpx;
color: #fff;
font-size: 26rpx;
white-space: nowrap;
}
.popup-btn:active {
background: rgba(255, 255, 255, 0.1);
}
.popup-divider {
width: 2rpx;
height: 32rpx;
background: rgba(255, 255, 255, 0.2);
}
/* Comment Input Bar */
.comment-input-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
transform: translateY(100%);
transition: transform 0.3s ease-out;
}
.comment-input-bar.show {
transform: translateY(0);
}
.comment-input-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
}
.comment-input-content {
position: relative;
display: flex;
align-items: center;
gap: 20rpx;
padding: 20rpx 32rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #f5f5f5;
border-top: 2rpx solid #e5e5e5;
}
.comment-input {
flex: 1;
height: 72rpx;
padding: 0 24rpx;
background: #fff;
border-radius: 36rpx;
font-size: 28rpx;
}
.send-btn {
padding: 16rpx 32rpx;
background: #e0e0e0;
border-radius: 8rpx;
font-size: 28rpx;
color: #999;
transition: all 0.2s;
}
.send-btn.active {
background: #558B2F;
color: #fff;
}
.send-btn:active {
opacity: 0.8;
}
+228
View File
@@ -0,0 +1,228 @@
// pages/garden/add/index.js
import { MOCK_PLANTS, CARE_TASK_ICONS } from '../../../utils/mockData';
Page({
data: {
newPlantName: '',
newPlantLocation: '',
newPlantDate: '',
newPlantImage: null,
isLocalImage: false,
newCareTasks: [],
scrollIntoViewId: '',
showActionSheet: false,
actionSheetItems: [
{ label: '拍摄', value: 'camera' },
{ label: '从手机相册选取', value: 'album' }
],
// Icon picker
careTaskIcons: [],
showIconPicker: false,
currentEditingTaskId: null
},
onLoad() {
const now = new Date();
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
// Default care task with water icon
const defaultIcon = CARE_TASK_ICONS.find(i => i.id === 'water');
this.setData({
newPlantDate: `${year}-${month}-${day}`,
newCareTasks: [{
id: Date.now().toString(),
taskName: '浇水',
frequencyValue: 7,
frequencyUnit: 'day',
iconId: 'water',
taskIcon: defaultIcon
}],
careTaskIcons: CARE_TASK_ICONS
});
},
handleBack() {
wx.navigateBack();
},
// Action Sheet Logic
showActionSheet() {
this.setData({ showActionSheet: true });
},
onActionSheetCancel() {
this.setData({ showActionSheet: false });
},
onActionSheetSelected(e) {
const { value } = e.detail.selected;
this.handleImageUpload(value);
this.setData({ showActionSheet: false });
},
handleImageUpload(sourceType) {
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: [sourceType], // 'camera' or 'album'
camera: 'back',
success: (res) => {
const tempFilePath = res.tempFiles[0].tempFilePath;
this.setData({
newPlantImage: tempFilePath,
isLocalImage: true
});
},
fail: (err) => {
console.log('User cancelled', err);
}
});
},
// Form Handlers
onNameInput(e) { this.setData({ newPlantName: e.detail.value }); },
onLocationInput(e) { this.setData({ newPlantLocation: e.detail.value }); },
onDateChange(e) { this.setData({ newPlantDate: e.detail.value }); },
handleAddCareTask() {
const tasks = this.data.newCareTasks;
const defaultIcon = CARE_TASK_ICONS.find(i => i.id === 'other');
tasks.push({
id: Date.now().toString(),
taskName: '',
frequencyValue: 1,
frequencyUnit: 'day',
iconId: 'other',
taskIcon: defaultIcon
});
// First clear the scrollIntoViewId, then set it to trigger scroll
this.setData({
newCareTasks: tasks,
scrollIntoViewId: ''
}, () => {
// Use setTimeout to ensure the DOM has updated before scrolling
setTimeout(() => {
this.setData({ scrollIntoViewId: 'care-list-bottom' });
}, 50);
});
},
handleRemoveCareTask(e) {
const id = e.currentTarget.dataset.id;
const tasks = this.data.newCareTasks.filter(t => t.id !== id);
this.setData({ newCareTasks: tasks });
},
onTaskNameInput(e) {
const { id } = e.currentTarget.dataset;
const tasks = this.data.newCareTasks.map(t => t.id === id ? { ...t, taskName: e.detail.value } : t);
this.setData({ newCareTasks: tasks });
},
onTaskFreqInput(e) {
const { id } = e.currentTarget.dataset;
const tasks = this.data.newCareTasks.map(t => t.id === id ? { ...t, frequencyValue: parseInt(e.detail.value) || 1 } : t);
this.setData({ newCareTasks: tasks });
},
// Icon Picker
showIconPickerForTask(e) {
const taskId = e.currentTarget.dataset.id;
this.setData({
showIconPicker: true,
currentEditingTaskId: taskId
});
},
hideIconPicker() {
this.setData({
showIconPicker: false,
currentEditingTaskId: null
});
},
selectIcon(e) {
const iconId = e.currentTarget.dataset.iconid;
const { currentEditingTaskId, careTaskIcons, newCareTasks } = this.data;
const selectedIcon = careTaskIcons.find(i => i.id === iconId);
if (selectedIcon && currentEditingTaskId) {
const updatedTasks = newCareTasks.map(t => {
if (t.id === currentEditingTaskId) {
return {
...t,
iconId: iconId,
taskIcon: selectedIcon,
// Auto-fill task name if empty
taskName: t.taskName || selectedIcon.name
};
}
return t;
});
this.setData({
newCareTasks: updatedTasks,
showIconPicker: false,
currentEditingTaskId: null
});
}
},
handleAddPlant() {
if (!this.data.newPlantName) {
wx.showToast({ title: '请输入植物名称', icon: 'none' });
return;
}
const newId = (MOCK_PLANTS.length + 1).toString();
const adoption = new Date(this.data.newPlantDate);
const today = new Date();
const diffTime = Math.abs(today.getTime() - adoption.getTime());
const daysPlanted = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
// Prepare care schedule with icon info
const careSchedule = this.data.newCareTasks.map(task => ({
id: task.id,
taskName: task.taskName,
frequencyValue: task.frequencyValue,
frequencyUnit: task.frequencyUnit,
iconId: task.iconId,
taskIcon: task.taskIcon
}));
const newPlant = {
id: newId,
name: this.data.newPlantName,
images: [this.data.newPlantImage || 'monstera_plant_1769757312755.png'],
daysPlanted: daysPlanted,
adoptionDate: this.data.newPlantDate,
location: this.data.newPlantLocation || '未分配位置',
scientificName: 'Unknown',
family: '未知科',
genus: '未知属',
description: '新添加的植物...',
difficulty: '⭐️',
toxicity: '未知',
flowerMsg: '充满希望',
careSchedule: careSchedule
};
// In a real app we would call an API or update global store
// For this mock, we append to the imported array (memory only)
MOCK_PLANTS.push(newPlant);
wx.showToast({ title: '添加成功', icon: 'success' });
setTimeout(() => {
wx.navigateBack();
}, 1000);
}
})
+13
View File
@@ -0,0 +1,13 @@
{
"navigationBarTitleText": "添加新植物",
"navigationBarBackgroundColor": "#FFFFFF",
"usingComponents": {
"t-input": "tdesign-miniprogram/input/input",
"t-button": "tdesign-miniprogram/button/button",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-image": "tdesign-miniprogram/image/image",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
"t-action-sheet": "tdesign-miniprogram/action-sheet/action-sheet"
}
}
+135
View File
@@ -0,0 +1,135 @@
<wxs src="../../../utils/tools.wxs" module="tools" />
<view class="add-plant-page">
<scroll-view
scroll-y
class="page-content"
show-scrollbar="{{false}}"
enhanced="{{true}}"
scroll-into-view="{{scrollIntoViewId}}"
scroll-with-animation="{{true}}"
>
<!-- Upload Area -->
<view class="upload-section">
<view class="image-upload-area {{newPlantImage ? 'has-image' : ''}}" bindtap="showActionSheet">
<t-image wx:if="{{newPlantImage}}" src="{{tools.resolvePath(newPlantImage)}}" mode="aspectFill" width="100%" height="100%" />
<view wx:else class="upload-placeholder">
<t-icon name="upload" size="64rpx" color="#999" />
<text>点击上传封面图</text>
</view>
</view>
</view>
<!-- Form Fields -->
<view class="form-group">
<text class="field-label">植物昵称</text>
<view class="custom-input-box">
<input class="native-input" placeholder="例如:小绿、旺财" placeholder-class="input-placeholder" value="{{newPlantName}}" bindinput="onNameInput" />
</view>
</view>
<view class="form-group">
<text class="field-label">摆放位置</text>
<view class="custom-input-box">
<input class="native-input" placeholder="例如:客厅、卧室" placeholder-class="input-placeholder" value="{{newPlantLocation}}" bindinput="onLocationInput" />
</view>
</view>
<view class="form-group">
<text class="field-label">入家日期</text>
<picker mode="date" value="{{newPlantDate}}" bindchange="onDateChange">
<view class="custom-input-box picker-box">
<text class="picker-text">{{newPlantDate}}</text>
<t-icon name="calendar" size="40rpx" color="#666" />
</view>
</picker>
</view>
<!-- Care Plan -->
<view class="care-section-group">
<view class="section-header-row">
<text class="field-label" style="margin-bottom: 0;">养护计划</text>
<view class="add-task-btn-small" bindtap="handleAddCareTask">
<t-icon name="add" size="28rpx" />
<text>添加</text>
</view>
</view>
<view class="care-list-styled">
<view wx:for="{{newCareTasks}}" wx:key="id" class="care-row-styled">
<!-- Icon Selector -->
<view
class="care-icon-btn"
bindtap="showIconPickerForTask"
data-id="{{item.id}}"
style="background: {{item.taskIcon ? item.taskIcon.bgColor : '#f5f5f5'}};"
>
<t-icon
name="{{item.taskIcon ? item.taskIcon.icon : 'ellipsis'}}"
size="40rpx"
color="{{item.taskIcon ? item.taskIcon.color : '#999'}}"
/>
</view>
<view class="care-input-col task-col">
<view class="custom-input-box small-box">
<input class="native-input" placeholder="事项名称" value="{{item.taskName}}" bindinput="onTaskNameInput" data-id="{{item.id}}" />
</view>
</view>
<view class="care-input-col freq-col">
<view class="custom-input-box small-box flex-row">
<input type="number" class="native-input center-text" style="width: 50rpx;" value="{{item.frequencyValue}}" bindinput="onTaskFreqInput" data-id="{{item.id}}" />
<text class="suffix-text">天</text>
</view>
</view>
<view class="delete-btn-pink" bindtap="handleRemoveCareTask" data-id="{{item.id}}">
<t-icon name="delete" size="36rpx" />
</view>
</view>
</view>
<!-- Scroll anchor for newly added care items -->
<view id="care-list-bottom"></view>
</view>
<!-- Spacer for bottom button -->
<view style="height: 180rpx;"></view>
</scroll-view>
<view class="page-footer">
<t-button theme="primary" block size="large" icon="check" shape="round" bind:tap="handleAddPlant">确认添加</t-button>
</view>
<t-action-sheet
visible="{{showActionSheet}}"
description="请选择图片来源"
items="{{actionSheetItems}}"
bind:selected="onActionSheetSelected"
bind:cancel="onActionSheetCancel"
show-cancel="{{true}}"
/>
<!-- Icon Picker Popup -->
<view class="icon-picker-mask {{showIconPicker ? 'show' : ''}}" bindtap="hideIconPicker"></view>
<view class="icon-picker-popup {{showIconPicker ? 'show' : ''}}">
<view class="icon-picker-header">
<text class="icon-picker-title">选择图标</text>
<view class="icon-picker-close" bindtap="hideIconPicker">
<t-icon name="close" size="40rpx" color="#999" />
</view>
</view>
<view class="icon-picker-grid">
<view
wx:for="{{careTaskIcons}}"
wx:key="id"
class="icon-picker-item"
bindtap="selectIcon"
data-iconid="{{item.id}}"
>
<view class="icon-circle" style="background: {{item.bgColor}};">
<t-icon name="{{item.icon}}" size="48rpx" color="{{item.color}}" />
</view>
<text class="icon-name">{{item.name}}</text>
</view>
</view>
</view>
</view>
+336
View File
@@ -0,0 +1,336 @@
/** pages/garden/add/index.wxss **/
page {
height: 100%;
overflow: hidden;
}
.add-plant-page {
background-color: #FFFFFF;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.page-content {
height: calc(100vh - 140rpx - env(safe-area-inset-bottom));
padding: 32rpx 40rpx;
background: #FFFFFF;
box-sizing: border-box;
}
/* Hide scrollbar - multiple approaches for compatibility */
.page-content::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
background: transparent !important;
}
/* For scroll-view component */
::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
}
scroll-view ::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
}
/* Upload Section */
.upload-section {
margin: 0 0 40rpx;
display: flex;
justify-content: center;
}
.image-upload-area {
width: 100%;
height: 240rpx;
border-radius: 32rpx;
border: 4rpx dashed #ddd; /* Match prototype dashed border */
background: #fafafa;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
transition: all 0.2s;
}
.image-upload-area:active {
border-color: #558B2F; /* var(--primary) */
background: #F1F8E9;
}
.image-upload-area.has-image {
border: none;
height: 360rpx; /* Taller when has image */
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
}
.upload-placeholder text {
color: #BDBDBD;
font-size: 26rpx;
}
/* Form Styles */
.form-group {
margin-bottom: 40rpx;
}
.field-label {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #263238;
margin-bottom: 16rpx;
}
.custom-input-box {
background: #f9f9f9;
border: 2rpx solid #e0e0e0;
border-radius: 24rpx;
padding: 24rpx 32rpx;
color: #263238;
font-size: 30rpx;
transition: all 0.2s;
}
.custom-input-box:active, .custom-input-box:focus-within {
background: #FFFFFF;
border-color: #558B2F;
}
.input-placeholder {
color: #999;
}
.native-input {
width: 100%;
height: 100%;
}
.picker-box {
display: flex;
justify-content: space-between;
align-items: center;
}
/* Care Section */
.care-section-group {
margin-top: 24rpx;
margin-bottom: 48rpx;
}
.section-header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.add-task-btn-small {
font-size: 26rpx;
color: #558B2F;
background: #F1F8E9;
border: 2rpx dashed #558B2F;
padding: 12rpx 20rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
gap: 8rpx;
}
.care-list-styled {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.care-row-styled {
display: flex;
align-items: center;
gap: 16rpx;
}
.care-input-col.task-col {
flex: 1;
}
.care-input-col.freq-col {
flex-shrink: 0;
}
.small-box {
padding: 24rpx;
background: #f9f9f9;
}
.flex-row {
display: flex;
align-items: center;
padding-right: 20rpx;
}
.center-text {
text-align: center;
}
.suffix-text {
color: #888;
font-size: 28rpx;
font-weight: 500;
}
.delete-btn-pink {
width: 84rpx;
height: 84rpx;
background: #FFEBEE;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
color: #EF5350;
flex-shrink: 0;
}
/* Footer */
.page-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 32rpx 40rpx calc(32rpx + env(safe-area-inset-bottom));
background: white;
z-index: 100;
box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.02);
}
/* Footer Button */
.page-footer t-button {
--td-button-font-weight: 600;
--td-button-primary-bg-color: #558B2F;
--td-button-primary-border-color: #558B2F;
box-shadow: 0 8rpx 32rpx rgba(85, 139, 47, 0.3);
}
/* Care Icon Button */
.care-icon-btn {
width: 84rpx;
height: 84rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s;
}
.care-icon-btn:active {
transform: scale(0.95);
}
/* Icon Picker Popup */
.icon-picker-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.3s;
}
.icon-picker-mask.show {
opacity: 1;
visibility: visible;
}
.icon-picker-popup {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
border-radius: 32rpx 32rpx 0 0;
z-index: 1001;
transform: translateY(100%);
transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1);
padding-bottom: env(safe-area-inset-bottom);
}
.icon-picker-popup.show {
transform: translateY(0);
}
.icon-picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 40rpx;
border-bottom: 2rpx solid #f5f5f5;
}
.icon-picker-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.icon-picker-close {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
}
.icon-picker-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24rpx;
padding: 32rpx 40rpx;
max-height: 60vh;
overflow-y: auto;
}
.icon-picker-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
padding: 20rpx 0;
}
.icon-picker-item:active {
opacity: 0.7;
}
.icon-circle {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.icon-name {
font-size: 24rpx;
color: #666;
}
+72
View File
@@ -0,0 +1,72 @@
// pages/garden/index.js
import { MOCK_PLANTS } from '../../utils/mockData';
Page({
data: {
plants: [],
dateString: '',
greeting: ''
},
onLoad(options) {
this.initTime();
this.loadPlants();
},
onShow() {
if (typeof this.getTabBar === 'function' &&
this.getTabBar()) {
this.getTabBar().setData({
selected: 0
})
}
// Refresh list in case new plant was added
this.loadPlants();
},
loadPlants() {
this.setData({ plants: MOCK_PLANTS });
},
initTime() {
const updateTime = () => {
const now = new Date();
const hour = now.getHours();
let greet = '晚上好';
if (hour >= 5 && hour < 12) greet = '上午好';
else if (hour >= 12 && hour < 14) greet = '中午好';
else if (hour >= 14 && hour < 19) greet = '下午好';
const month = now.getMonth() + 1;
const day = now.getDate();
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
const weekDay = weekDays[now.getDay()];
this.setData({
greeting: greet,
dateString: `${month}${day}${weekDay}`
});
};
updateTime();
},
navigateToDetail(e) {
const { id } = e.currentTarget.dataset;
wx.navigateTo({
url: `/pages/plant-detail/index?id=${id}`,
});
},
navigateToAdd() {
wx.navigateTo({
url: '/pages/garden/add/index'
});
},
onScrollLower() {
console.log('Scroll to lower - loading more plants...');
// In a real app, this would trigger pagination
}
})
+14
View File
@@ -0,0 +1,14 @@
{
"navigationBarTitleText": "我的花园",
"usingComponents": {
"t-fab": "tdesign-miniprogram/fab/fab",
"t-popup": "tdesign-miniprogram/popup/popup",
"t-input": "tdesign-miniprogram/input/input",
"t-button": "tdesign-miniprogram/button/button",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-image": "tdesign-miniprogram/image/image",
"t-upload": "tdesign-miniprogram/upload/upload",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group"
}
}
+45
View File
@@ -0,0 +1,45 @@
<wxs src="../../utils/tools.wxs" module="tools" />
<view class="garden-page">
<view class="page-header with-banner">
<view class="header-content">
<text class="date-text">{{dateString}}</text>
<view class="greeting-row">
<text class="greeting-main">{{greeting}},园长</text>
</view>
<text class="subtitle">今天也要好好照顾它们哦</text>
</view>
</view>
<view class="banner-container">
<image src="https://images.unsplash.com/photo-1585320806297-9794b3e4eeae?w=800" class="garden-banner" mode="aspectFill" />
<view class="banner-overlay">
<text class="count-tag">共养护 {{plants.length}} 盆植物</text>
</view>
</view>
<view class="garden-list-wrapper">
<scroll-view scroll-y class="garden-list-container" enhanced show-scrollbar="{{false}}" bindscrolltolower="onScrollLower">
<view class="plant-grid">
<view wx:for="{{plants}}" wx:key="id" class="plant-card" bindtap="navigateToDetail" data-id="{{item.id}}">
<view class="plant-image-container">
<t-image src="{{tools.resolvePath(item.images[0])}}" mode="aspectFill" width="100%" height="100%" />
<view class="days-badge">{{item.daysPlanted}}天</view>
</view>
<view class="plant-info">
<text class="plant-name">{{item.name}}</text>
<view class="status-wrap">
<text class="status">生长中</text>
</view>
</view>
</view>
</view>
<view style="height: 100rpx;"></view>
</scroll-view>
</view>
<!-- Custom Floating Action Button -->
<view class="floating-add-btn" bindtap="navigateToAdd">
<t-icon name="add" size="40rpx" color="#FFF" />
<text>添加植物</text>
</view>
</view>
+194
View File
@@ -0,0 +1,194 @@
/** pages/garden/index.wxss **/
.garden-page {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #FFFFFF;
position: relative;
overflow: hidden;
}
.page-header.with-banner {
padding: 40rpx 40rpx 20rpx;
background: white;
z-index: 10;
}
.date-text {
font-size: 26rpx;
color: #558B2F; /* var(--primary) */
margin-bottom: 8rpx;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1rpx;
}
.greeting-main {
font-size: 56rpx;
font-weight: 800;
background: linear-gradient(120deg, #33691E, #689F38);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
display: inline-block;
margin-bottom: 12rpx;
line-height: 1.2;
}
.subtitle {
font-size: 30rpx;
color: #90A4AE;
font-weight: 500;
}
.banner-container {
margin: 0 40rpx 48rpx;
height: 280rpx;
border-radius: 40rpx;
overflow: hidden;
position: relative;
box-shadow: 0 12rpx 32rpx rgba(85, 139, 47, 0.15);
flex-shrink: 0;
}
.garden-banner {
width: 100%;
height: 100%;
}
.banner-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 24rpx 32rpx;
background: linear-gradient(to top, rgba(0, 0, 0, 0.5) 0%, transparent 100%);
display: flex;
align-items: flex-end;
}
.count-tag {
color: white;
font-size: 24rpx;
font-weight: 600;
background: rgba(255, 255, 255, 0.2);
padding: 8rpx 20rpx;
border-radius: 24rpx;
backdrop-filter: blur(8rpx);
border: 1rpx solid rgba(255, 255, 255, 0.3);
}
.garden-list-wrapper {
flex: 1;
background: #F4F6F0;
border-top-left-radius: 60rpx;
border-top-right-radius: 60rpx;
padding: 48rpx 40rpx 0;
box-shadow: 0 -8rpx 40rpx rgba(0,0,0,0.05);
overflow: hidden;
display: flex;
flex-direction: column;
}
.garden-list-container {
height: 100%;
}
/* Plant Grid Layout */
.plant-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 32rpx;
padding-bottom: 40rpx;
}
.plant-card {
background: rgba(255, 255, 255, 0.9);
border-radius: 40rpx;
overflow: hidden;
box-shadow: 0 8rpx 24rpx rgba(85, 139, 47, 0.08), inset 0 0 0 2rpx rgba(255, 255, 255, 0.6);
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
display: flex;
flex-direction: column;
position: relative;
}
.plant-card:active {
transform: translateY(-8rpx);
box-shadow: 0 16rpx 32rpx rgba(85, 139, 47, 0.15);
}
.plant-image-container {
height: 300rpx;
position: relative;
background: #f0f0f0;
overflow: hidden;
}
.plant-image-container t-image {
width: 100%;
height: 100%;
transition: transform 0.6s ease;
}
.days-badge {
position: absolute;
top: 20rpx;
right: 20rpx;
background: rgba(255, 255, 255, 0.95);
color: #33691E;
padding: 8rpx 20rpx;
border-radius: 20rpx;
font-size: 20rpx;
font-weight: 800;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.plant-info {
padding: 28rpx;
display: flex;
flex-direction: column;
}
.plant-name {
font-size: 32rpx;
font-weight: 700;
color: #263238;
margin-bottom: 8rpx;
}
.status-wrap {
display: flex;
}
.status {
font-size: 22rpx;
color: #33691E;
background: #DCEDC8;
padding: 4rpx 16rpx;
border-radius: 12rpx;
font-weight: 600;
}
/* Custom Floating Button */
.floating-add-btn {
position: fixed;
right: 40rpx;
bottom: 60rpx;
background: #558B2F;
color: white;
padding: 24rpx 40rpx;
border-radius: 60rpx;
display: flex;
align-items: center;
gap: 12rpx;
box-shadow: 0 12rpx 32rpx rgba(85, 139, 47, 0.4);
z-index: 1000;
font-size: 28rpx;
font-weight: 700;
transition: all 0.2s ease;
}
.floating-add-btn:active {
transform: scale(0.92);
box-shadow: 0 4rpx 16rpx rgba(85, 139, 47, 0.2);
}
+248
View File
@@ -0,0 +1,248 @@
// pages/plant-detail/edit/index.js
import { MOCK_PLANTS, CARE_TASK_ICONS } from '../../../utils/mockData';
Page({
data: {
plantId: '',
newPlantName: '',
newPlantLocation: '',
newPlantDate: '',
newPlantImage: null,
isLocalImage: false,
newCareTasks: [],
scrollIntoViewId: '',
showActionSheet: false,
actionSheetItems: [
{ label: '拍摄', value: 'camera' },
{ label: '从手机相册选取', value: 'album' }
],
// Icon picker
careTaskIcons: [],
showIconPicker: false,
currentEditingTaskId: null
},
onLoad(options) {
const { id } = options;
if (!id) {
wx.navigateBack();
return;
}
const plant = MOCK_PLANTS.find(p => p.id === id);
if (!plant) {
wx.showToast({ title: '植物不存在', icon: 'error' });
setTimeout(() => wx.navigateBack(), 1500);
return;
}
const defaultIcon = CARE_TASK_ICONS.find(i => i.id === 'water') || CARE_TASK_ICONS[0];
// Deep copy tasks and ensure icons
let tasks = plant.careSchedule ? JSON.parse(JSON.stringify(plant.careSchedule)) : [];
tasks = tasks.map(task => {
const icon = CARE_TASK_ICONS.find(i => i.id === task.iconId) || defaultIcon;
return { ...task, taskIcon: icon };
});
// Set default date if not present (today)
let adoptionDate = plant.adoptionDate;
if (!adoptionDate) {
const now = new Date();
adoptionDate = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`;
}
this.setData({
plantId: id,
newPlantName: plant.name || '',
newPlantLocation: plant.location || '',
newPlantDate: adoptionDate,
newPlantImage: plant.images && plant.images.length > 0 ? plant.images[0] : null,
newCareTasks: tasks,
careTaskIcons: CARE_TASK_ICONS
});
},
handleBack() {
wx.navigateBack();
},
showActionSheet() {
this.setData({ showActionSheet: true });
},
onActionSheetCancel() {
this.setData({ showActionSheet: false });
},
onActionSheetSelected(e) {
const { value } = e.detail.selected;
this.handleImageUpload(value);
this.setData({ showActionSheet: false });
},
handleImageUpload(sourceType) {
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: [sourceType],
camera: 'back',
success: (res) => {
const tempFilePath = res.tempFiles[0].tempFilePath;
this.setData({
newPlantImage: tempFilePath,
isLocalImage: true
});
}
});
},
onNameInput(e) { this.setData({ newPlantName: e.detail.value }); },
onLocationInput(e) { this.setData({ newPlantLocation: e.detail.value }); },
onDateChange(e) { this.setData({ newPlantDate: e.detail.value }); },
handleAddCareTask() {
const tasks = this.data.newCareTasks;
const defaultIcon = CARE_TASK_ICONS.find(i => i.id === 'other') || CARE_TASK_ICONS[0];
tasks.push({
id: Date.now().toString(),
taskName: '',
frequencyValue: 1,
frequencyUnit: 'day',
iconId: 'other',
taskIcon: defaultIcon
});
this.setData({
newCareTasks: tasks,
scrollIntoViewId: ''
}, () => {
setTimeout(() => {
this.setData({ scrollIntoViewId: 'care-list-bottom' });
}, 50);
});
},
handleRemoveCareTask(e) {
const id = e.currentTarget.dataset.id;
const tasks = this.data.newCareTasks.filter(t => t.id !== id);
this.setData({ newCareTasks: tasks });
},
onTaskNameInput(e) {
const { id } = e.currentTarget.dataset;
const tasks = this.data.newCareTasks.map(t => t.id === id ? { ...t, taskName: e.detail.value } : t);
this.setData({ newCareTasks: tasks });
},
onTaskFreqInput(e) {
const { id } = e.currentTarget.dataset;
const tasks = this.data.newCareTasks.map(t => t.id === id ? { ...t, frequencyValue: parseInt(e.detail.value) || 1 } : t);
this.setData({ newCareTasks: tasks });
},
showIconPickerForTask(e) {
const taskId = e.currentTarget.dataset.id;
this.setData({
showIconPicker: true,
currentEditingTaskId: taskId
});
},
hideIconPicker() {
this.setData({
showIconPicker: false,
currentEditingTaskId: null
});
},
selectIcon(e) {
const iconId = e.currentTarget.dataset.iconid;
const { currentEditingTaskId, careTaskIcons, newCareTasks } = this.data;
const selectedIcon = careTaskIcons.find(i => i.id === iconId);
if (selectedIcon && currentEditingTaskId) {
const updatedTasks = newCareTasks.map(t => {
if (t.id === currentEditingTaskId) {
return {
...t,
iconId: iconId,
taskIcon: selectedIcon,
taskName: t.taskName || selectedIcon.name
};
}
return t;
});
this.setData({
newCareTasks: updatedTasks,
showIconPicker: false,
currentEditingTaskId: null
});
}
},
handleSavePlant() {
if (!this.data.newPlantName) {
wx.showToast({ title: '请输入植物名称', icon: 'none' });
return;
}
const plantIndex = MOCK_PLANTS.findIndex(p => p.id === this.data.plantId);
if (plantIndex === -1) return;
const adoption = new Date(this.data.newPlantDate);
const today = new Date();
const diffTime = Math.abs(today.getTime() - adoption.getTime());
const daysPlanted = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) || 0;
const updatedPlant = {
...MOCK_PLANTS[plantIndex],
name: this.data.newPlantName,
location: this.data.newPlantLocation || '',
adoptionDate: this.data.newPlantDate,
images: [this.data.newPlantImage],
daysPlanted: daysPlanted,
careSchedule: this.data.newCareTasks.map(task => ({
id: task.id,
taskName: task.taskName,
frequencyValue: task.frequencyValue,
frequencyUnit: task.frequencyUnit,
iconId: task.iconId,
taskIcon: task.taskIcon
}))
};
MOCK_PLANTS[plantIndex] = updatedPlant;
wx.showToast({ title: '修改成功', icon: 'success' });
setTimeout(() => {
wx.navigateBack();
}, 1000);
},
handleDeletePlant() {
wx.showModal({
title: '确认删除',
content: '确定要删除这个植物吗?删除后无法恢复。',
confirmColor: '#EF5350',
success: (res) => {
if (res.confirm) {
const idx = MOCK_PLANTS.findIndex(p => p.id === this.data.plantId);
if (idx > -1) {
MOCK_PLANTS.splice(idx, 1);
wx.showToast({ title: '已删除', icon: 'success' });
setTimeout(() => {
wx.switchTab({ url: '/pages/garden/index' });
}, 1000);
}
}
}
});
}
})
+13
View File
@@ -0,0 +1,13 @@
{
"navigationBarTitleText": "编辑植物",
"navigationBarBackgroundColor": "#FFFFFF",
"usingComponents": {
"t-input": "tdesign-miniprogram/input/input",
"t-button": "tdesign-miniprogram/button/button",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-image": "tdesign-miniprogram/image/image",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
"t-action-sheet": "tdesign-miniprogram/action-sheet/action-sheet"
}
}
+151
View File
@@ -0,0 +1,151 @@
<wxs src="../../../utils/tools.wxs" module="tools" />
<view class="add-plant-page">
<scroll-view
scroll-y
class="page-content"
show-scrollbar="{{false}}"
enhanced="{{true}}"
scroll-into-view="{{scrollIntoViewId}}"
scroll-with-animation="{{true}}"
>
<!-- Upload Area -->
<view class="upload-section">
<view class="image-upload-area {{newPlantImage ? 'has-image' : ''}}" bindtap="showActionSheet">
<t-image wx:if="{{newPlantImage}}" src="{{tools.resolvePath(newPlantImage)}}" mode="aspectFill" width="100%" height="100%" />
<!-- Placeholder shown when NO image -->
<view wx:if="{{!newPlantImage}}" class="upload-placeholder">
<t-icon name="upload" size="64rpx" color="#999" />
<text>点击设置封面图</text>
</view>
<!-- Update hint shown when HAS image (for edit UX) -->
<view wx:if="{{newPlantImage}}" class="edit-overlay">
<t-icon name="camera" size="32rpx" color="#FFF" />
<text>更换照片</text>
</view>
</view>
</view>
<!-- Form Fields -->
<view class="form-group">
<text class="field-label">植物昵称</text>
<view class="custom-input-box">
<input class="native-input" placeholder="例如:小绿、旺财" placeholder-class="input-placeholder" value="{{newPlantName}}" bindinput="onNameInput" />
</view>
</view>
<view class="form-group">
<text class="field-label">摆放位置</text>
<view class="custom-input-box">
<input class="native-input" placeholder="例如:客厅、卧室" placeholder-class="input-placeholder" value="{{newPlantLocation}}" bindinput="onLocationInput" />
</view>
</view>
<view class="form-group">
<text class="field-label">入家日期</text>
<picker mode="date" value="{{newPlantDate}}" bindchange="onDateChange">
<view class="custom-input-box picker-box">
<text class="picker-text">{{newPlantDate}}</text>
<t-icon name="calendar" size="40rpx" color="#666" />
</view>
</picker>
</view>
<!-- Care Plan -->
<view class="care-section-group">
<view class="section-header-row">
<text class="field-label" style="margin-bottom: 0;">养护计划</text>
<view class="add-task-btn-small" bindtap="handleAddCareTask">
<t-icon name="add" size="28rpx" />
<text>添加</text>
</view>
</view>
<view class="care-list-styled">
<view wx:for="{{newCareTasks}}" wx:key="id" class="care-row-styled">
<!-- Icon Selector -->
<view
class="care-icon-btn"
bindtap="showIconPickerForTask"
data-id="{{item.id}}"
style="background: {{item.taskIcon ? item.taskIcon.bgColor : '#f5f5f5'}};"
>
<t-icon
name="{{item.taskIcon ? item.taskIcon.icon : 'ellipsis'}}"
size="40rpx"
color="{{item.taskIcon ? item.taskIcon.color : '#999'}}"
/>
</view>
<view class="care-input-col task-col">
<view class="custom-input-box small-box">
<input class="native-input" placeholder="事项名称" value="{{item.taskName}}" bindinput="onTaskNameInput" data-id="{{item.id}}" />
</view>
</view>
<view class="care-input-col freq-col">
<view class="custom-input-box small-box flex-row">
<input type="number" class="native-input center-text" style="width: 50rpx;" value="{{item.frequencyValue}}" bindinput="onTaskFreqInput" data-id="{{item.id}}" />
<text class="suffix-text">天</text>
</view>
</view>
<view class="delete-btn-pink" bindtap="handleRemoveCareTask" data-id="{{item.id}}">
<t-icon name="delete" size="36rpx" />
</view>
</view>
</view>
<!-- Scroll anchor for newly added care items -->
<view id="care-list-bottom"></view>
</view>
<!-- Delete Button for Edit Page -->
<view class="delete-section" style="margin-top: 40rpx; padding-bottom: 40rpx;">
<view class="delete-page-btn" bindtap="handleDeletePlant">
<t-icon name="delete" size="32rpx" />
<text>删除植物档案</text>
</view>
</view>
<!-- Spacer for bottom button -->
<view style="height: 180rpx;"></view>
</scroll-view>
<view class="page-footer">
<t-button theme="primary" block size="large" icon="check" shape="round" bind:tap="handleSavePlant">确认保存</t-button>
</view>
<t-action-sheet
visible="{{showActionSheet}}"
description="请选择图片来源"
items="{{actionSheetItems}}"
bind:selected="onActionSheetSelected"
bind:cancel="onActionSheetCancel"
show-cancel="{{true}}"
/>
<!-- Icon Picker Popup -->
<view class="icon-picker-mask {{showIconPicker ? 'show' : ''}}" bindtap="hideIconPicker"></view>
<view class="icon-picker-popup {{showIconPicker ? 'show' : ''}}">
<view class="icon-picker-header">
<text class="icon-picker-title">选择图标</text>
<view class="icon-picker-close" bindtap="hideIconPicker">
<t-icon name="close" size="40rpx" color="#999" />
</view>
</view>
<view class="icon-picker-grid">
<view
wx:for="{{careTaskIcons}}"
wx:key="id"
class="icon-picker-item"
bindtap="selectIcon"
data-iconid="{{item.id}}"
>
<view class="icon-circle" style="background: {{item.bgColor}};">
<t-icon name="{{item.icon}}" size="48rpx" color="{{item.color}}" />
</view>
<text class="icon-name">{{item.name}}</text>
</view>
</view>
</view>
</view>
+356
View File
@@ -0,0 +1,356 @@
/** pages/plant-detail/edit/index.wxss **/
page {
height: 100%;
overflow: hidden;
}
.add-plant-page {
background-color: #FFFFFF;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.page-content {
height: calc(100vh - 140rpx - env(safe-area-inset-bottom));
padding: 32rpx 40rpx;
background: #FFFFFF;
box-sizing: border-box;
}
/* Hide scrollbar */
::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
}
/* Upload Section */
.upload-section {
margin: 0 0 40rpx;
display: flex;
justify-content: center;
}
.image-upload-area {
width: 100%;
height: 240rpx;
border-radius: 32rpx;
border: 4rpx dashed #ddd;
background: #fafafa;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
transition: all 0.2s;
}
.image-upload-area:active {
opacity: 0.9;
}
.image-upload-area.has-image {
border: none;
height: 360rpx;
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
}
.upload-placeholder text {
color: #BDBDBD;
font-size: 26rpx;
}
/* Edit Overlay Hint */
.edit-overlay {
position: absolute;
bottom: 24rpx;
right: 24rpx;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10rpx);
padding: 12rpx 24rpx;
border-radius: 30rpx;
display: flex;
align-items: center;
gap: 8rpx;
color: #FFFFFF;
font-size: 24rpx;
font-weight: 500;
}
/* Form Styles */
.form-group {
margin-bottom: 40rpx;
}
.field-label {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #263238;
margin-bottom: 16rpx;
}
.custom-input-box {
background: #f9f9f9;
border: 2rpx solid #e0e0e0;
border-radius: 24rpx;
padding: 24rpx 32rpx;
color: #263238;
font-size: 30rpx;
transition: all 0.2s;
}
.custom-input-box:focus-within {
background: #FFFFFF;
border-color: #558B2F;
}
.input-placeholder {
color: #999;
}
.native-input {
width: 100%;
height: 100%;
}
.picker-box {
display: flex;
justify-content: space-between;
align-items: center;
}
/* Care Section */
.care-section-group {
margin-top: 24rpx;
margin-bottom: 48rpx;
}
.section-header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.add-task-btn-small {
font-size: 26rpx;
color: #558B2F;
background: #F1F8E9;
border: 2rpx dashed #558B2F;
padding: 12rpx 20rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
gap: 8rpx;
}
.care-list-styled {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.care-row-styled {
display: flex;
align-items: center;
gap: 16rpx;
}
.care-input-col.task-col {
flex: 1;
}
.care-input-col.freq-col {
flex-shrink: 0;
}
.small-box {
padding: 24rpx;
background: #f9f9f9;
}
.flex-row {
display: flex;
align-items: center;
padding-right: 20rpx;
}
.center-text {
text-align: center;
}
.suffix-text {
color: #888;
font-size: 28rpx;
font-weight: 500;
}
.delete-btn-pink {
width: 84rpx;
height: 84rpx;
background: #FFEBEE;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
color: #EF5350;
flex-shrink: 0;
}
/* Delete Button for Edit Page */
.delete-page-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
color: #FF5252;
font-size: 28rpx;
font-weight: 500;
padding: 20rpx;
border: 2rpx solid #FFCDD2;
border-radius: 24rpx;
background: #FFF9F9;
}
.delete-page-btn:active {
background: #FFEBEE;
}
/* Footer */
.page-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 32rpx 40rpx calc(32rpx + env(safe-area-inset-bottom));
background: white;
z-index: 100;
box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.02);
}
.page-footer t-button {
--td-button-font-weight: 600;
--td-button-primary-bg-color: #558B2F;
--td-button-primary-border-color: #558B2F;
box-shadow: 0 8rpx 32rpx rgba(85, 139, 47, 0.3);
}
/* Care Icon Button */
.care-icon-btn {
width: 84rpx;
height: 84rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s;
}
.care-icon-btn:active {
transform: scale(0.95);
}
/* Icon Picker Popup */
.icon-picker-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.3s;
}
.icon-picker-mask.show {
opacity: 1;
visibility: visible;
}
.icon-picker-popup {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
border-radius: 32rpx 32rpx 0 0;
z-index: 1001;
transform: translateY(100%);
transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1);
padding-bottom: env(safe-area-inset-bottom);
}
.icon-picker-popup.show {
transform: translateY(0);
}
.icon-picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 40rpx;
border-bottom: 2rpx solid #f5f5f5;
}
.icon-picker-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.icon-picker-close {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
}
.icon-picker-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24rpx;
padding: 32rpx 40rpx;
max-height: 60vh;
overflow-y: auto;
}
.icon-picker-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
padding: 20rpx 0;
}
.icon-picker-item:active {
opacity: 0.7;
}
.icon-circle {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.icon-name {
font-size: 24rpx;
color: #666;
}
+240
View File
@@ -0,0 +1,240 @@
// pages/plant-detail/index.js
import { MOCK_PLANTS } from '../../utils/mockData';
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'
}
];
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({
data: {
currentPlant: null,
activeImageIndex: 0,
activeTab: 'care',
careLogs: [],
displayCareLogs: [],
displayCareLimit: 5,
records: [],
displayRecords: [],
displayRecordLimit: 5,
swiperImages: [],
// Growth Modal
showGrowthModal: false,
newRecordType: 'growth',
newRecordContent: ''
},
onLoad(options) {
this.initData(options.id);
},
onShow() {
if (this.data.currentPlant && this.data.currentPlant.id) {
this.initData(this.data.currentPlant.id);
}
},
initData(id) {
const plant = MOCK_PLANTS.find(p => p.id === id);
if (plant) {
this.setData({
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
});
this.updateDisplayLogs();
this.updateDisplayRecords();
}
},
processLogs(logs) {
return logs.map(log => {
const parts = log.date.split('-');
return {
...log,
day: parts[2],
month: parts[1],
typeLabel: this.getCareTypeLabel(log.type)
};
});
},
getCareTypeLabel(type) {
const map = {
water: '浇水',
fertilize: '施肥',
prune: '修剪',
repot: '换盆'
};
return map[type] || '养护';
},
updateDisplayLogs() {
this.setData({
displayCareLogs: this.data.careLogs.slice(0, this.data.displayCareLimit)
});
},
onSwiperChange(e) {
this.setData({ activeImageIndex: e.detail.current });
},
switchTab(e) {
const tab = e.currentTarget.dataset.tab;
if (tab) {
this.setData({ activeTab: tab });
}
},
// Prevent background scroll when modal is open
preventTouchMove() {
return false;
},
toggleCareLimit() {
const newLimit = this.data.displayCareLimit + 5;
this.setData({ displayCareLimit: newLimit });
this.updateDisplayLogs();
},
updateDisplayRecords() {
this.setData({
displayRecords: this.data.records.slice(0, this.data.displayRecordLimit)
});
},
toggleRecordLimit() {
const newLimit = this.data.displayRecordLimit + 5;
this.setData({ displayRecordLimit: newLimit });
this.updateDisplayRecords();
},
// Navigate to Edit Page
handleOpenEditModal() {
if (this.data.currentPlant && this.data.currentPlant.id) {
wx.navigateTo({
url: `/pages/plant-detail/edit/index?id=${this.data.currentPlant.id}`
});
}
},
// Growth Record Logic
openGrowthModal() { this.setData({ showGrowthModal: true, newRecordContent: '', newRecordType: 'growth' }); },
onGrowthPopupVisibleChange(e) { this.setData({ showGrowthModal: e.detail.visible }); },
closeGrowthModal() { this.setData({ showGrowthModal: false }); },
setRecordType(e) {
const type = e.currentTarget.dataset.type;
if (e.detail.checked) {
this.setData({ newRecordType: type });
}
},
setRecordTypeByTap(e) {
const type = e.currentTarget.dataset.type;
this.setData({ newRecordType: type });
},
onRecordContentInput(e) { this.setData({ newRecordContent: e.detail.value }); },
handleAddRecord() {
if (!this.data.newRecordContent.trim()) return;
const mapTitle = { growth: '生长记录', repot: '换盆记录', pest: '病虫害记录', other: '日常记录' };
const now = new Date();
const dateStr = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`;
const record = {
id: Date.now().toString(),
date: dateStr,
type: this.data.newRecordType,
title: mapTitle[this.data.newRecordType],
content: this.data.newRecordContent
};
this.setData({
records: [record, ...this.data.records],
showGrowthModal: false
});
this.updateDisplayRecords();
wx.showToast({ title: '记录成功', icon: 'success' });
}
})
+20
View File
@@ -0,0 +1,20 @@
{
"navigationBarTitleText": "植物详情",
"usingComponents": {
"t-swiper": "tdesign-miniprogram/swiper/swiper",
"t-tabs": "tdesign-miniprogram/tabs/tabs",
"t-tab-panel": "tdesign-miniprogram/tab-panel/tab-panel",
"t-popup": "tdesign-miniprogram/popup/popup",
"t-input": "tdesign-miniprogram/input/input",
"t-button": "tdesign-miniprogram/button/button",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-steps": "tdesign-miniprogram/steps/steps",
"t-step-item": "tdesign-miniprogram/step-item/step-item",
"t-tag": "tdesign-miniprogram/tag/tag",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
"t-image": "tdesign-miniprogram/image/image",
"t-textarea": "tdesign-miniprogram/textarea/textarea",
"t-chip": "tdesign-miniprogram/check-tag/check-tag"
}
}
+212
View File
@@ -0,0 +1,212 @@
<wxs src="../../utils/tools.wxs" module="tools" />
<view class="plant-detail">
<!-- Header -->
<view class="detail-header">
<view class="settings-btn-detail" bindtap="handleOpenEditModal">
<t-icon name="setting" size="44rpx" color="#263238" />
</view>
<view class="header-gallery">
<t-swiper
current="{{activeImageIndex}}"
autoplay="{{false}}"
navigation="{{ { type: '' } }}"
list="{{swiperImages}}"
bind:change="onSwiperChange"
height="500rpx"
/>
<view class="header-gradient"></view>
<!-- Custom Carousel Indicators -->
<view class="carousel-indicators">
<view wx:for="{{swiperImages}}" wx:key="index" class="carousel-dot {{index === activeImageIndex ? 'active' : ''}}"></view>
</view>
</view>
<view class="header-info">
<text class="header-title">{{currentPlant.name}}</text>
</view>
</view>
<!-- Content -->
<view class="detail-content">
<!-- Custom Tabs -->
<view class="pd-tabs">
<view class="pd-tab-btn {{activeTab === 'care' ? 'active' : ''}}" bindtap="switchTab" data-tab="care">
养护记录
</view>
<view class="pd-tab-btn {{activeTab === 'archive' ? 'active' : ''}}" bindtap="switchTab" data-tab="archive">
植物档案
</view>
</view>
<!-- Care Tab Scroll View -->
<scroll-view
scroll-y="{{!showGrowthModal}}"
class="pd-content"
enhanced="{{true}}"
show-scrollbar="{{false}}"
hidden="{{activeTab !== 'care'}}"
>
<view class="care-view fadeIn">
<view class="section-title">
<text class="h3">养护历史</text>
</view>
<view class="care-log-list">
<view wx:for="{{displayCareLogs}}" wx:key="id" class="care-log-item">
<view class="log-left">
<view class="log-date-v">
<text class="l-day">{{item.day}}</text>
<text class="l-month">{{item.month}}月</text>
</view>
<view class="log-type-icon {{item.type === 'water' ? 'icon-water' : (item.type === 'fertilize' ? 'icon-fertilize' : (item.type === 'prune' ? 'icon-prune' : 'icon-repot'))}}">
<t-icon wx:if="{{item.type === 'water'}}" name="heart" size="36rpx" color="#2196F3" />
<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 class="log-info">
<view class="log-header-row">
<text class="log-type-name">{{item.typeLabel}}</text>
<text class="log-time">{{item.time}}</text>
</view>
<text wx:if="{{item.remark}}" class="log-remark">{{item.remark}}</text>
</view>
</view>
</view>
</view>
<block wx:if="{{careLogs.length > displayCareLimit}}">
<view class="load-more-btn" bindtap="toggleCareLimit">
<text>加载更多 ({{displayCareLogs.length}}/{{careLogs.length}})</text>
<t-icon name="chevron-down" size="32rpx" />
</view>
</block>
</view>
</scroll-view>
<!-- Archive Tab Scroll View -->
<scroll-view
scroll-y="{{!showGrowthModal}}"
class="pd-content"
enhanced="{{true}}"
show-scrollbar="{{false}}"
hidden="{{activeTab !== 'archive'}}"
>
<view class="archive-view fadeIn">
<view class="archive-identity-card">
<view class="aic-header">
<view class="aic-badge">🏆 元老级植物</view>
<view class="aic-location">
<t-icon name="location" size="28rpx" color="#388E3C" />
<text>{{currentPlant.location || '位置未定'}}</text>
</view>
</view>
<view class="aic-stats">
<view class="aic-stat-item">
<text class="label">入家时间</text>
<text class="value">{{currentPlant.adoptionDate || '未知'}}</text>
</view>
<view class="aic-stat-item">
<text class="label">入住天数</text>
<text class="value">{{currentPlant.daysPlanted}} 天</text>
</view>
<view class="aic-stat-item">
<text class="label">养护次数</text>
<text class="value">{{careLogs.length}} 次</text>
</view>
</view>
</view>
<view class="section-header">
<text class="h3">成长记录</text>
<view class="add-btn" bindtap="openGrowthModal">
<t-icon name="edit" size="28rpx" color="#FFF" />
<text>记录</text>
</view>
</view>
<view class="archive-timeline">
<view wx:for="{{displayRecords}}" wx:key="id" class="timeline-item">
<view class="timeline-dot"></view>
<text class="timeline-date">{{item.date}}</text>
<view class="timeline-content-box">
<view class="timeline-title">
<t-icon wx:if="{{item.type === 'growth'}}" name="thumb-up" size="32rpx" color="#4CAF50" />
<t-icon wx:elif="{{item.type === 'repot'}}" name="swap" size="32rpx" color="#FF9800" />
<t-icon wx:elif="{{item.type === 'pest'}}" name="error-circle" size="32rpx" color="#F44336" />
<t-icon wx:else name="file" size="32rpx" color="#2196F3" />
<text>{{item.title}}</text>
</view>
<text class="timeline-desc">{{item.content}}</text>
<t-image wx:if="{{item.image}}" src="{{tools.resolvePath(item.image)}}" mode="widthFix" width="100%" class="timeline-img" />
</view>
</view>
</view>
<block wx:if="{{records.length > displayRecordLimit}}">
<view class="load-more-btn" bindtap="toggleRecordLimit">
<text>加载更多 ({{displayRecords.length}}/{{records.length}})</text>
<t-icon name="chevron-down" size="32rpx" />
</view>
</block>
</view>
</scroll-view>
</view>
<!-- Growth Record Popup -->
<t-popup visible="{{showGrowthModal}}" bind:visible-change="onGrowthPopupVisibleChange" placement="center" prevent-scroll-through>
<view class="edit-modal">
<view class="modal-header" catchtouchmove="preventTouchMove">
<text class="modal-title">添加成长记录</text>
<view class="close-btn" bindtap="closeGrowthModal">
<t-icon name="close" size="40rpx" color="#666" />
</view>
</view>
<view class="modal-body">
<!-- Record Type -->
<view class="form-group">
<text class="form-label">记录类型</text>
<view class="chip-group">
<view class="chip {{newRecordType === 'growth' ? 'active' : ''}}" bindtap="setRecordTypeByTap" data-type="growth">
<t-icon name="thumb-up" size="28rpx" />
<text>生长</text>
</view>
<view class="chip {{newRecordType === 'repot' ? 'active' : ''}}" bindtap="setRecordTypeByTap" data-type="repot">
<t-icon name="swap" size="28rpx" />
<text>换盆</text>
</view>
<view class="chip {{newRecordType === 'pest' ? 'active' : ''}}" bindtap="setRecordTypeByTap" data-type="pest">
<t-icon name="error-circle" size="28rpx" />
<text>病虫害</text>
</view>
<view class="chip {{newRecordType === 'other' ? 'active' : ''}}" bindtap="setRecordTypeByTap" data-type="other">
<t-icon name="file" size="28rpx" />
<text>其他</text>
</view>
</view>
</view>
<!-- Content -->
<view class="form-group">
<text class="form-label">备注</text>
<textarea
class="form-textarea"
placeholder="记录这一刻的变化..."
value="{{newRecordContent}}"
bindinput="onRecordContentInput"
auto-height
/>
</view>
</view>
<view class="modal-footer" catchtouchmove="preventTouchMove">
<view class="submit-btn" bindtap="handleAddRecord">
<text>保存记录</text>
</view>
</view>
</view>
</t-popup>
</view>
+716
View File
@@ -0,0 +1,716 @@
/** pages/plant-detail/index.wxss **/
page {
height: 100%;
overflow: hidden;
}
.plant-detail {
background-color: #F4F6F0;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Header */
.detail-header {
position: relative;
height: 500rpx;
overflow: hidden;
}
.settings-btn-detail {
position: absolute;
top: 88rpx;
right: 32rpx;
z-index: 100;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(10px);
border: 2rpx solid rgba(255, 255, 255, 0.5);
width: 88rpx;
height: 88rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
}
.settings-btn-detail:active {
transform: scale(0.92);
}
.header-gallery {
height: 500rpx;
position: relative;
}
.header-gradient {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 240rpx;
background: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.6));
pointer-events: none;
z-index: 10;
}
/* Carousel Indicators */
.carousel-indicators {
position: absolute;
bottom: 100rpx;
right: 48rpx;
display: flex;
gap: 12rpx;
z-index: 30;
}
.carousel-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.4);
transition: all 0.3s;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
}
.carousel-dot.active {
background: white;
width: 24rpx;
border-radius: 8rpx;
}
.header-info {
position: absolute;
bottom: 80rpx;
left: 48rpx;
right: 48rpx;
z-index: 20;
color: white;
}
.header-title {
font-size: 56rpx;
font-weight: 800;
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
display: block;
}
/* Content */
.detail-content {
flex: 1;
background: #F4F6F0;
border-top-left-radius: 64rpx;
border-top-right-radius: 64rpx;
margin-top: -64rpx;
position: relative;
z-index: 30;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Custom Tabs */
.pd-tabs {
display: flex;
padding: 48rpx 48rpx 0;
border-bottom: 2rpx solid rgba(0, 0, 0, 0.05);
background: rgba(255, 255, 255, 0.95);
flex-shrink: 0;
}
.pd-tab-btn {
flex: 1;
text-align: center;
padding: 32rpx 48rpx;
font-size: 30rpx;
font-weight: 500;
color: #6B7280;
background: none;
border: none;
position: relative;
}
.pd-tab-btn.active {
color: #558B2F;
font-weight: 600;
}
.pd-tab-btn.active::after {
content: '';
position: absolute;
bottom: -2rpx;
left: 25%;
right: 25%;
height: 6rpx;
background: #558B2F;
border-radius: 4rpx;
}
/* Content Area */
.pd-content {
flex: 1;
overflow-y: auto;
padding: 48rpx;
padding-bottom: 160rpx;
-webkit-overflow-scrolling: touch;
}
/* Hide scrollbar for pd-content */
.pd-content::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
.fadeIn {
animation: fadeIn 0.4s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Section Title */
.section-title {
margin-bottom: 32rpx;
}
.h3 {
font-size: 34rpx;
font-weight: 800;
color: #263238;
}
/* Care Log List - Matching Prototype */
.care-log-list {
display: flex;
flex-direction: column;
gap: 24rpx;
margin-bottom: 40rpx;
}
.care-log-item {
background: white;
border-radius: 32rpx;
padding: 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.log-left {
display: flex;
align-items: center;
gap: 24rpx;
}
.log-date-v {
display: flex;
flex-direction: column;
align-items: center;
min-width: 80rpx;
border-right: 2rpx solid #F0F0F0;
padding-right: 24rpx;
}
.l-day {
font-size: 36rpx;
font-weight: 700;
color: #333;
}
.l-month {
font-size: 22rpx;
color: #999;
}
.log-type-icon {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: #FAFAFA;
display: flex;
align-items: center;
justify-content: center;
}
.log-info {
flex: 1;
}
.log-header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8rpx;
}
.log-type-name {
font-weight: 600;
font-size: 30rpx;
color: #333;
}
.log-time {
font-size: 24rpx;
color: #999;
}
.log-remark {
font-size: 26rpx;
color: #666;
margin: 0;
line-height: 1.4;
}
/* Load More Button */
.load-more-btn {
width: 100%;
padding: 24rpx;
margin-top: 24rpx;
background: #f5f5f5;
border: 2rpx dashed #e0e0e0;
border-radius: 24rpx;
color: #666;
font-size: 28rpx;
font-weight: 500;
display: flex;
justify-content: center;
align-items: center;
gap: 8rpx;
}
.load-more-btn:active {
background: #EEEEEE;
color: #558B2F;
border-color: #558B2F;
}
/* Archive View */
.archive-view {
padding: 0;
}
/* Identity Card */
.archive-identity-card {
background: white;
border-radius: 40rpx;
padding: 48rpx;
margin-bottom: 48rpx;
box-shadow: 0 16rpx 40rpx rgba(0, 0, 0, 0.06);
border: 2rpx solid rgba(0, 0, 0, 0.02);
text-align: center;
position: relative;
overflow: hidden;
}
.archive-identity-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 12rpx;
background: linear-gradient(90deg, #AED581, #2E7D32);
}
.aic-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40rpx;
}
.aic-badge {
background: rgba(255, 255, 255, 0.9);
padding: 12rpx 24rpx;
border-radius: 40rpx;
font-size: 24rpx;
font-weight: 600;
color: #2E7D32;
display: inline-flex;
align-items: center;
gap: 8rpx;
}
.aic-location {
display: flex;
align-items: center;
font-size: 26rpx;
color: #388E3C;
background: rgba(255, 255, 255, 0.6);
padding: 8rpx 20rpx;
border-radius: 24rpx;
gap: 8rpx;
}
.aic-stats {
display: flex;
justify-content: space-between;
}
.aic-stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.aic-stat-item .label {
font-size: 22rpx;
color: #558B2F;
margin-bottom: 8rpx;
opacity: 0.8;
}
.aic-stat-item .value {
font-size: 32rpx;
font-weight: 700;
color: #1B5E20;
}
/* Section Header */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
}
/* Timeline */
.archive-timeline {
position: relative;
padding-left: 48rpx;
border-left: 4rpx solid #E0E0E0;
margin-left: 24rpx;
margin-bottom: 80rpx;
}
.timeline-item {
position: relative;
margin-bottom: 48rpx;
}
.timeline-item:last-child {
margin-bottom: 0;
}
.timeline-dot {
position: absolute;
left: -62rpx;
top: 0;
width: 24rpx;
height: 24rpx;
background: white;
border: 6rpx solid #558B2F;
border-radius: 50%;
z-index: 1;
box-shadow: 0 0 0 8rpx #F4F6F0;
}
.timeline-date {
font-size: 24rpx;
color: #9E9E9E;
margin-bottom: 16rpx;
font-weight: 500;
}
.timeline-content-box {
background: white;
border-radius: 32rpx;
padding: 32rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.04);
border: 2rpx solid rgba(0, 0, 0, 0.02);
}
.timeline-content-box:active {
transform: scale(0.98);
}
.timeline-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
display: flex;
align-items: center;
gap: 16rpx;
}
.timeline-desc {
font-size: 28rpx;
color: #546E7A;
line-height: 1.5;
display: block;
margin-bottom: 16rpx;
}
.timeline-img {
border-radius: 24rpx;
margin-top: 16rpx;
overflow: hidden;
}
/* Popup Styles */
.popup-content {
background: white;
border-radius: 48rpx 48rpx 0 0;
padding-bottom: env(safe-area-inset-bottom);
}
.popup-header {
padding: 48rpx;
text-align: center;
}
.popup-title {
font-size: 36rpx;
font-weight: 800;
color: #263238;
}
.form-section {
padding: 0 0 32rpx;
}
.chip-group-td {
padding: 0 32rpx;
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
/* Edit Modal - Matching Prototype */
.edit-modal {
background: white;
width: 680rpx;
max-width: 90vw;
border-radius: 48rpx;
padding: 48rpx;
box-shadow: 0 20rpx 80rpx rgba(0, 0, 0, 0.2);
animation: modalSlideUp 0.3s ease-out;
max-height: 85vh;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
@keyframes modalSlideUp {
from {
opacity: 0;
transform: translateY(40rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40rpx;
flex-shrink: 0;
}
.modal-title {
font-size: 40rpx;
font-weight: 700;
color: #333;
}
.close-btn {
width: 64rpx;
height: 64rpx;
background: #f5f5f5;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:active {
background: #e0e0e0;
}
.modal-body {
max-height: 55vh;
flex: 1;
overflow-y: auto;
padding-right: 0;
-webkit-overflow-scrolling: touch;
}
/* Hide scrollbar but keep scrollable */
.modal-body::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
/* Form Styles */
.form-group {
margin-bottom: 40rpx;
}
.form-label {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #666;
margin-bottom: 20rpx;
}
.form-input {
width: 100%;
height: 88rpx;
padding: 0 24rpx;
border: 2rpx solid #e0e0e0;
border-radius: 24rpx;
font-size: 30rpx;
box-sizing: border-box;
background: #FAFAFA;
transition: all 0.2s;
display: flex;
align-items: center;
}
.form-input:focus {
background: white;
border-color: #558B2F;
box-shadow: 0 0 0 6rpx rgba(85, 139, 47, 0.1);
}
.date-picker {
display: flex;
justify-content: space-between;
align-items: center;
}
.date-picker .placeholder {
color: #999;
}
/* Modal Footer */
.modal-footer {
margin-top: 40rpx;
flex-shrink: 0;
padding-top: 16rpx;
}
.submit-btn {
width: 100%;
padding: 28rpx;
background: linear-gradient(135deg, #689F38, #558B2F);
border-radius: 32rpx;
color: white;
text-align: center;
font-size: 32rpx;
font-weight: 600;
box-shadow: 0 8rpx 24rpx rgba(85, 139, 47, 0.4);
}
.submit-btn:active {
transform: scale(0.98);
filter: brightness(0.95);
}
/* Keep existing add-btn style */
.add-btn {
background: #558B2F;
color: white;
padding: 16rpx 32rpx;
border-radius: 40rpx;
font-size: 26rpx;
font-weight: 600;
display: flex;
align-items: center;
gap: 12rpx;
box-shadow: 0 8rpx 24rpx rgba(85, 139, 47, 0.3);
}
.add-btn:active {
transform: scale(0.96);
}
/* Care Type Icons */
.icon-water {
color: #2196F3;
background: #E1F5FE;
}
.icon-fertilize {
color: #FFD700;
background: #FFFDE7;
}
.icon-prune {
color: #757575;
background: #F5F5F5;
}
.icon-repot {
color: #8D6E63;
background: #EFEBE9;
}
/* Chip Group */
.chip-group {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.chip {
padding: 16rpx 24rpx;
background: #F5F5F5;
border-radius: 32rpx;
font-size: 26rpx;
color: #666;
display: flex;
align-items: center;
gap: 8rpx;
border: 2rpx solid transparent;
transition: all 0.2s;
}
.chip.active {
background: #E8F5E9;
color: #558B2F;
border-color: #558B2F;
font-weight: 500;
}
.chip:active {
transform: scale(0.96);
}
/* Form Textarea */
.form-textarea {
width: 100%;
min-height: 160rpx;
padding: 24rpx;
border: 2rpx solid #e0e0e0;
border-radius: 24rpx;
font-size: 30rpx;
box-sizing: border-box;
background: #FAFAFA;
line-height: 1.5;
}
.form-textarea:focus {
background: white;
border-color: #558B2F;
box-shadow: 0 0 0 6rpx rgba(85, 139, 47, 0.1);
}
+144
View File
@@ -0,0 +1,144 @@
// pages/profile/index.js
import { MOCK_FAVORITES, MOCK_BADGES, MOCK_POSTS } from '../../utils/mockData';
Page({
data: {
view: 'profile', // profile, favorites, posts, badges
favTab: 'all', // all, plant, article
postsTab: 'published', // published, drafts
favorites: [],
filteredFavorites: [],
myPublishedPosts: [],
myDrafts: [],
badges: []
},
onLoad(options) {
this.setData({
favorites: MOCK_FAVORITES,
badges: MOCK_BADGES
});
this.filterFavorites();
},
onShow() {
if (typeof this.getTabBar === 'function' &&
this.getTabBar()) {
this.getTabBar().setData({
selected: 4 // Index 4 is Profile
})
}
// Refresh posts data
this.loadMyPosts();
this.loadDrafts();
},
loadMyPosts() {
// Get published posts by current user
const myPosts = MOCK_POSTS.filter(p => p.user === '我的花园');
this.setData({ myPublishedPosts: myPosts });
},
loadDrafts() {
// Load drafts from storage
try {
const draft = wx.getStorageSync('post_draft');
if (draft && (draft.content || (draft.images && draft.images.length > 0))) {
// Convert single draft to array for consistency
this.setData({
myDrafts: [{
id: 'draft_1',
content: draft.content || '',
images: draft.images || [],
selectedTopics: draft.selectedTopics || []
}]
});
} else {
this.setData({ myDrafts: [] });
}
} catch (e) {
this.setData({ myDrafts: [] });
}
},
setView(e) {
const view = e.currentTarget.dataset.view;
this.setData({ view });
// Refresh data when entering posts view
if (view === 'posts') {
this.loadMyPosts();
this.loadDrafts();
}
},
onFavTabChange(e) {
const tab = e.detail.value;
this.setData({ favTab: tab }, () => {
this.filterFavorites();
});
},
onPostsTabChange(e) {
const tab = e.detail.value;
this.setData({ postsTab: tab });
},
filterFavorites() {
const { favorites, favTab } = this.data;
const filtered = favorites.filter(item => {
if (favTab === 'all') return true;
return item.type === favTab;
});
this.setData({ filteredFavorites: filtered });
},
// Delete a published post
deletePost(e) {
const postId = e.currentTarget.dataset.id;
wx.showModal({
title: '删除动态',
content: '确定要删除这条动态吗?',
confirmColor: '#EF5350',
success: (res) => {
if (res.confirm) {
// Remove from MOCK_POSTS
const idx = MOCK_POSTS.findIndex(p => p.id === postId);
if (idx > -1) {
MOCK_POSTS.splice(idx, 1);
}
this.loadMyPosts();
wx.showToast({ title: '已删除', icon: 'success' });
}
}
});
},
// Edit a draft
editDraft(e) {
// Navigate to create page, which will load the draft
wx.navigateTo({
url: '/pages/community/create/index'
});
},
// Delete a draft
deleteDraft(e) {
wx.showModal({
title: '删除草稿',
content: '确定要删除这份草稿吗?',
confirmColor: '#EF5350',
success: (res) => {
if (res.confirm) {
try {
wx.removeStorageSync('post_draft');
} catch (e) { }
this.setData({ myDrafts: [] });
wx.showToast({ title: '已删除', icon: 'success' });
}
}
});
}
})
+15
View File
@@ -0,0 +1,15 @@
{
"navigationBarTitleText": "个人中心",
"usingComponents": {
"t-grid": "tdesign-miniprogram/grid/grid",
"t-grid-item": "tdesign-miniprogram/grid-item/grid-item",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
"t-avatar": "tdesign-miniprogram/avatar/avatar",
"t-image": "tdesign-miniprogram/image/image",
"t-tag": "tdesign-miniprogram/tag/tag",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-badge": "tdesign-miniprogram/badge/badge",
"t-progress": "tdesign-miniprogram/progress/progress"
}
}
+200
View File
@@ -0,0 +1,200 @@
<wxs src="../../utils/tools.wxs" module="tools" />
<view class="profile-page">
<!-- Sub-views handled by conditional rendering to match prototype single-page feel -->
<!-- FAVORITES VIEW -->
<view wx:if="{{view === 'favorites'}}" class="favorites-page info-view-anim">
<view class="back-nav sticky-nav">
<t-button variant="text" icon="arrow-left" bind:tap="setView" data-view="profile">我的收藏</t-button>
</view>
<t-tabs value="{{favTab}}" bind:change="onFavTabChange" theme="card">
<t-tab-panel label="全部" value="all" />
<t-tab-panel label="植物" value="plant" />
<t-tab-panel label="文章" value="article" />
</t-tabs>
<view class="tab-content">
<view class="fav-grid">
<block wx:if="{{filteredFavorites.length > 0}}">
<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" />
<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="32rpx" color="#90A4AE" />
<text class="fav-type">{{item.meta}}</text>
</view>
</view>
</view>
</block>
<block wx:else>
<view class="empty-state">
<t-icon name="star" size="64rpx" color="#ccc" />
<text style="margin-top: 16rpx;">暂无收藏内容</text>
</view>
</block>
</view>
</view>
</view>
<!-- POSTS VIEW -->
<view wx:elif="{{view === 'posts'}}" class="posts-page-detail info-view-anim">
<view class="back-nav sticky-nav">
<t-button variant="text" icon="arrow-left" bind:tap="setView" data-view="profile">我的发布</t-button>
</view>
<!-- Tabs for Published / Drafts -->
<t-tabs value="{{postsTab}}" bind:change="onPostsTabChange" theme="card">
<t-tab-panel label="已发布" value="published" />
<t-tab-panel label="草稿箱" value="drafts" />
</t-tabs>
<!-- Published Posts -->
<view wx:if="{{postsTab === 'published'}}" class="my-posts-list">
<block wx:if="{{myPublishedPosts.length > 0}}">
<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="{{tools.resolvePath(img)}}" mode="aspectFill" width="160rpx" height="160rpx" style="margin-right: 16rpx; display: inline-block; border-radius: 8rpx;" />
</view>
<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="chat" size="32rpx" /> <text>{{item.comments.length}}</text></view>
<view class="footer-item delete-btn" bindtap="deletePost" data-id="{{item.id}}"><t-icon name="delete" size="32rpx" color="#EF5350" /></view>
</view>
</view>
</view>
</block>
<view wx:else class="empty-state">
<t-icon name="file-copy" size="64rpx" color="#ccc" />
<text style="margin-top: 16rpx;">暂无已发布的动态</text>
</view>
</view>
<!-- Draft Posts -->
<view wx:if="{{postsTab === 'drafts'}}" class="my-posts-list">
<block wx:if="{{myDrafts.length > 0}}">
<view wx:for="{{myDrafts}}" wx:key="id" class="my-post-card draft-card">
<view class="draft-badge">草稿</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="160rpx" height="160rpx" style="margin-right: 16rpx; display: inline-block; border-radius: 8rpx;" />
</view>
<view class="my-post-footer">
<view class="footer-item edit-btn" bindtap="editDraft" data-index="{{index}}">
<t-icon name="edit" size="32rpx" color="#558B2F" />
<text style="color: #558B2F;">编辑</text>
</view>
<view class="footer-item delete-btn" bindtap="deleteDraft" data-index="{{index}}">
<t-icon name="delete" size="32rpx" color="#EF5350" />
<text style="color: #EF5350;">删除</text>
</view>
</view>
</view>
</view>
</block>
<view wx:else class="empty-state">
<t-icon name="file-add" size="64rpx" color="#ccc" />
<text style="margin-top: 16rpx;">暂无草稿</text>
</view>
</view>
</view>
<!-- BADGES VIEW -->
<view wx:elif="{{view === 'badges'}}" class="badges-page info-view-anim">
<view class="back-nav sticky-nav">
<t-button variant="text" icon="arrow-left" bind:tap="setView" data-view="profile">成就徽章</t-button>
</view>
<scroll-view scroll-y class="badges-content">
<view class="level-card-large">
<view class="level-header">
<view class="level-info-large">
<text class="level-label">当前等级</text>
<text class="level-value">Lv.4 资深植人</text>
</view>
<t-icon name="trophy" size="80rpx" color="#FFD700" />
</view>
<view class="level-progress-section">
<view class="progress-text">
<text>经验值</text>
<text>350 / 500</text>
</view>
<t-progress percentage="70" theme="plump" color="#FFD700" track-color="rgba(255,255,255,0.3)" />
<text class="next-level-tip">距离 Lv.5 园艺大师 还需 150 经验</text>
</view>
</view>
<view class="section-title-badges">所有徽章 (3/6)</view>
<t-grid column="{{3}}" gutter="24rpx">
<t-grid-item wx:for="{{badges}}" wx:key="id" text="{{item.name}}" description="{{item.desc}}" image="{{item.unlocked ? '/assets/icons/'+item.icon+'.png' : '/assets/icons/lock.png'}}" />
<!-- TDesign grid item image might need full path or use slot for svg if needed, using png for now -->
</t-grid>
</scroll-view>
</view>
<!-- MAIN PROFILE VIEW -->
<view wx:else class="main-profile-view">
<view class="profile-header">
<view class="user-main">
<view class="user-avatar">
<t-avatar image="https://api.dicebear.com/7.x/avataaars/svg?seed=Lucky" size="large" />
</view>
<view class="user-text">
<text class="user-name">布偶猫园长</text>
<t-tag theme="warning" variant="light" size="small">Lv.4 资深植人</t-tag>
</view>
</view>
<t-icon name="setting" size="48rpx" />
</view>
<scroll-view scroll-y class="profile-content">
<view class="stats-grid">
<view class="stat-col">
<text class="stat-num">12</text>
<text class="stat-label">植物</text>
</view>
<view class="stat-col">
<text class="stat-num">328</text>
<text class="stat-label">养护</text>
</view>
<view class="stat-col">
<text class="stat-num">15</text>
<text class="stat-label">关注</text>
</view>
</view>
<view class="profile-menu">
<t-cell-group title="常用功能" theme="card">
<t-cell title="我的收藏" hover arrow bind:tap="setView" data-view="favorites">
<t-icon slot="left-icon" name="star" color="#FFA000" style="margin-right: 16rpx;" />
</t-cell>
<t-cell title="我的发布" hover arrow bind:tap="setView" data-view="posts">
<t-icon slot="left-icon" name="file-copy" color="#1976D2" style="margin-right: 16rpx;" />
</t-cell>
<t-cell title="识别记录" hover arrow>
<t-icon slot="left-icon" name="scan" color="#388E3C" style="margin-right: 16rpx;" />
</t-cell>
<t-cell title="成就徽章" note="已获 3 个" hover arrow bind:tap="setView" data-view="badges">
<t-icon slot="left-icon" name="control-platform" color="#AB47BC" style="margin-right: 16rpx;" />
</t-cell>
</t-cell-group>
<t-cell-group title="更多服务" theme="card" style="margin-top: 24rpx;">
<t-cell title="帮助与反馈" hover arrow>
<t-icon slot="left-icon" name="help-circle" color="#757575" style="margin-right: 16rpx;" />
</t-cell>
</t-cell-group>
</view>
<!-- Spacer -->
<view style="height: 40rpx;"></view>
</scroll-view>
</view>
</view>
+149
View File
@@ -0,0 +1,149 @@
/** pages/profile/index.wxss **/
.profile-page {
background-color: #F4F6F0;
min-height: 100vh;
position: relative;
overflow: hidden;
display: flex; flex-direction: column;
}
/* Animations */
.info-view-anim {
animation: slideInRight 0.3s cubic-bezier(0.25, 1, 0.5, 1);
}
@keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.sticky-nav {
position: sticky; top: 0; z-index: 100; background: white;
border-bottom: 2rpx solid #f0f0f0;
padding: 20rpx;
}
.tab-content { padding: 32rpx; }
/* Favorites Grid */
.fav-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24rpx;
margin-top: 24rpx;
}
.fav-card {
background: white;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
}
.fav-img { width: 100%; display: block; background: #f0f0f0; }
.fav-info { padding: 20rpx; }
.fav-name { font-size: 28rpx; font-weight: 700; color: #37474F; margin-bottom: 12rpx; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fav-meta-row { display: flex; align-items: center; gap: 8rpx; }
.fav-type { font-size: 20rpx; color: #90A4AE; }
.empty-state {
display: flex; flex-direction: column; align-items: center; justify-content: center;
padding: 80rpx 0; color: #B0BEC5; font-size: 28rpx;
}
/* Posts View */
.my-posts-list { padding: 40rpx; }
.my-post-card { display: flex; gap: 24rpx; margin-bottom: 48rpx; position: relative; }
.my-post-time { font-size: 24rpx; color: #B0BEC5; width: 140rpx; flex-shrink: 0; text-align: right; }
.my-post-content-wrap { flex: 1; border-left: 4rpx solid #ECEFF1; padding-left: 24rpx; padding-bottom: 24rpx; }
.my-post-images { margin: 16rpx 0; white-space: nowrap; overflow-x: auto; }
.my-post-footer { display: flex; gap: 32rpx; margin-top: 16rpx; }
.footer-item { display: flex; align-items: center; gap: 8rpx; font-size: 24rpx; color: #78909C; }
/* Draft Card */
.draft-card {
background: #FFFDE7;
border-radius: 16rpx;
padding: 20rpx;
margin-left: 0;
}
.draft-card .my-post-content-wrap {
border-left: 4rpx solid #FFC107;
}
.draft-badge {
position: absolute;
top: 0;
left: 0;
background: #FFC107;
color: #fff;
font-size: 20rpx;
padding: 4rpx 16rpx;
border-radius: 8rpx 0 8rpx 0;
font-weight: 600;
}
/* Action Buttons */
.edit-btn, .delete-btn {
cursor: pointer;
}
.edit-btn:active, .delete-btn:active {
opacity: 0.7;
}
/* Badges View */
.badges-content { padding: 40rpx; background: white; height: 100%; }
.level-card-large {
background: linear-gradient(135deg, #FFF3E0 0%, #FFE0B2 100%);
border-radius: 40rpx;
padding: 48rpx;
margin-bottom: 60rpx;
color: #E65100;
}
.level-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 40rpx; }
.level-label { font-size: 26rpx; opacity: 0.8; display: block; }
.level-value { font-size: 48rpx; font-weight: 800; display: block; }
.level-progress-section { }
.progress-text { display: flex; justify-content: space-between; font-size: 24rpx; font-weight: 600; margin-bottom: 12rpx; }
.next-level-tip { font-size: 22rpx; margin-top: 16rpx; display: block; opacity: 0.8; }
.section-title-badges { font-size: 32rpx; font-weight: 700; color: #333; margin-bottom: 32rpx; }
/* Basic TDesign Grid Item styling override if needed */
.t-grid-item__content { padding: 24rpx 0 !important; }
/* Main Profile */
.main-profile-view { display: flex; flex-direction: column; height: 100%; }
.profile-header {
padding: 40rpx 48rpx;
background: white;
display: flex; justify-content: space-between; align-items: flex-start;
}
.user-main { display: flex; align-items: center; gap: 32rpx; }
.user-text { display: flex; flex-direction: column; gap: 12rpx; }
.user-name { font-size: 40rpx; font-weight: 800; color: var(--text-main); }
.stats-grid {
display: flex; justify-content: space-around;
padding: 40rpx;
margin: 24rpx 40rpx;
background: white;
border-radius: 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.02);
}
.stat-col { display: flex; flex-direction: column; align-items: center; gap: 4rpx; }
.stat-num { font-size: 36rpx; font-weight: 800; color: var(--text-main); }
.stat-label { font-size: 22rpx; color: #90A4AE; }
.profile-menu {
padding: 0 32rpx;
}
+99
View File
@@ -0,0 +1,99 @@
// pages/tasks/index.js
import { MOCK_TASKS_DATA } from '../../utils/mockData';
Page({
data: {
tasks: [],
groupedTasks: [],
progress: 0,
completingTask: null,
remark: ''
},
onLoad() {
this.setData({ tasks: MOCK_TASKS_DATA });
this.processTasks();
},
onShow() {
if (typeof this.getTabBar === 'function' &&
this.getTabBar()) {
this.getTabBar().setData({ selected: 1 });
}
},
processTasks() {
const { tasks } = this.data;
// Calculate Progress (Simulated)
const completedCount = 3; // Mocked existing completed
const initialTotal = MOCK_TASKS_DATA.length + completedCount;
const currentCompleted = completedCount + (MOCK_TASKS_DATA.length - tasks.length);
const progress = Math.min(100, Math.round((currentCompleted / initialTotal) * 100));
// Grouping
const groups = {};
tasks.forEach(task => {
if (!groups[task.plantName]) {
groups[task.plantName] = {
plantName: task.plantName,
plantImage: task.plantImage,
tasks: [],
hasOverdue: false
};
}
groups[task.plantName].tasks.push(task);
if (task.isOverdue) groups[task.plantName].hasOverdue = true;
});
// Sorting
const sortedGroups = Object.values(groups).sort((a, b) => {
if (a.hasOverdue && !b.hasOverdue) return -1;
if (!a.hasOverdue && b.hasOverdue) return 1;
return 0;
});
this.setData({
groupedTasks: sortedGroups,
progress
});
},
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;
this.setData({
completingTask: task,
remark: ''
});
},
onPopupVisibleChange(e) {
// Handle both TDesign event and manual close tap
const visible = e.detail ? e.detail.visible : e.currentTarget.dataset.visible;
if (!visible) {
this.setData({ completingTask: null });
}
},
onRemarkInput(e) {
this.setData({ remark: e.detail.value });
},
handleConfirmComplete() {
if (!this.data.completingTask) return;
const taskId = this.data.completingTask.id;
// Filter out the completed task
const newTasks = this.data.tasks.filter(t => t.id !== taskId);
this.setData({
tasks: newTasks,
completingTask: null
}, () => {
this.processTasks();
wx.showToast({ title: '已完成', icon: 'success' });
});
}
})
+13
View File
@@ -0,0 +1,13 @@
{
"navigationBarTitleText": "我的任务",
"usingComponents": {
"t-fab": "tdesign-miniprogram/fab/fab",
"t-popup": "tdesign-miniprogram/popup/popup",
"t-progress": "tdesign-miniprogram/progress/progress",
"t-button": "tdesign-miniprogram/button/button",
"t-textarea": "tdesign-miniprogram/textarea/textarea",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-tag": "tdesign-miniprogram/tag/tag"
}
}
+105
View File
@@ -0,0 +1,105 @@
<!--pages/tasks/index.wxml-->
<view class="tasks-page">
<!-- Progress Card -->
<view class="progress-section">
<view class="progress-card">
<view class="progress-info">
<text class="progress-card-title">今日完成进度</text>
<text class="progress-card-desc">{{progress}}% - 继续保持!🌿</text>
</view>
<view class="progress-circle">
<view class="progress-ring" style="--progress-deg: calc({{progress}} * 3.6deg);"></view>
<text class="percentage">{{progress}}%</text>
</view>
</view>
</view>
<!-- Tasks Container -->
<view class="tasks-container">
<text class="section-title">今日待办</text>
<view wx:if="{{tasks.length === 0}}" class="empty-state">
<text>太棒了!所有任务都已完成 🎉</text>
</view>
<scroll-view wx:else scroll-y class="task-list" enhanced show-scrollbar="{{false}}">
<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">
<view class="plant-thumb-small">
<image wx:if="{{item.plantImage}}" src="/assets/{{item.plantImage}}" mode="aspectFill"></image>
<view wx:else class="thumb-placeholder">{{item.plantName[0]}}</view>
</view>
<text class="plant-name-title">{{item.plantName}}</text>
</view>
<text wx:if="{{item.hasOverdue}}" class="group-overdue-badge">有任务逾期</text>
</view>
<view class="plant-tasks-list">
<view wx:for="{{item.tasks}}" wx:key="id" wx:for-item="task" class="mini-task-row">
<view class="mini-task-left">
<!-- Task Icon with dynamic colors from backend -->
<view class="task-type-icon-circle" style="background: {{task.taskIcon ? task.taskIcon.bgColor : '#f5f5f5'}};">
<t-icon wx:if="{{task.taskIcon}}" name="{{task.taskIcon.icon}}" size="36rpx" color="{{task.taskIcon.color}}" />
<t-icon wx:else name="calendar-1" size="36rpx" color="#999" />
</view>
<view class="mini-task-text">
<!-- Use taskType name from taskIcon or fallback to computed name -->
<text class="task-label">{{task.taskIcon ? task.taskIcon.name : (task.taskType === 'water'?'浇水':(task.taskType==='fertilize'?'施肥':(task.taskType==='prune'?'修剪':(task.taskType==='repot'?'换盆':'任务'))))}}</text>
<text wx:if="{{task.isOverdue}}" class="task-overdue-text">{{task.overdueDays}}天前</text>
</view>
</view>
<view class="mini-check-btn {{task.isOverdue ? 'btn-urgent' : ''}}" bindtap="handleTaskClick" data-task="{{task}}">
<t-icon name="check" size="32rpx" color="{{task.isOverdue ? '#EF5350' : '#E0E0E0'}}" />
</view>
</view>
</view>
</view>
<view style="height: 120rpx;"></view>
</scroll-view>
</view>
<!-- Complete Task Popup -->
<t-popup visible="{{completingTask}}" bind:visible-change="onPopupVisibleChange" placement="center">
<view class="modal-card">
<view class="modal-header">
<text class="modal-title">确认完成任务</text>
<view class="close-btn" bindtap="onPopupVisibleChange" data-visible="{{false}}">
<t-icon name="close" size="40rpx" color="#90A4AE" />
</view>
</view>
<view class="task-confirm-content" wx:if="{{completingTask}}">
<view class="confirm-plant-info">
<view class="confirm-icon" style="background: {{completingTask.taskIcon ? completingTask.taskIcon.bgColor : '#f5f5f5'}};">
<t-icon wx:if="{{completingTask.taskIcon}}" name="{{completingTask.taskIcon.icon}}" size="48rpx" color="{{completingTask.taskIcon.color}}" />
<t-icon wx:else name="calendar-1" size="48rpx" color="#999" />
</view>
<view class="confirm-text">
<text class="cp-name">{{completingTask.plantName}}</text>
<text class="cp-task">{{completingTask.taskIcon ? completingTask.taskIcon.name : (completingTask.taskType === 'water'?'浇水':(completingTask.taskType==='fertilize'?'施肥':(completingTask.taskType==='prune'?'修剪':(completingTask.taskType==='repot'?'换盆':'日常任务'))))}}</text>
</view>
</view>
<view class="remark-section">
<text class="remark-label">添加记录备注 (可选)</text>
<textarea
class="remark-input"
placeholder="例如:浇了500ml水..."
value="{{remark}}"
bindinput="onRemarkInput"
fixed="{{true}}"
/>
</view>
<view class="confirm-btn-wrap">
<view class="confirm-complete-btn" bindtap="handleConfirmComplete">
<t-icon name="check" size="36rpx" color="#FFF" style="margin-right: 12rpx;" />
<text>确认完成并记录</text>
</view>
</view>
</view>
</view>
</t-popup>
</view>
+549
View File
@@ -0,0 +1,549 @@
/** pages/tasks/index.wxss **/
.tasks-page {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #F8F9FA;
position: relative;
overflow: hidden;
}
/* Progress Section */
.progress-section {
padding: 0;
margin: 40rpx 48rpx 40rpx;
}
.progress-card {
background: linear-gradient(135deg, #E8F5E9 0%, #C8E6C9 100%);
border-radius: 40rpx;
padding: 32rpx 40rpx;
display: flex;
align-items: center;
justify-content: space-between;
color: #2E7D32;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.02);
position: relative;
overflow: hidden;
min-height: 170rpx;
box-sizing: border-box;
}
.progress-card::after {
content: '';
position: absolute;
right: -50rpx;
top: -50rpx;
width: 200rpx;
height: 200rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
pointer-events: none;
}
.progress-info {
display: flex;
flex-direction: column;
gap: 8rpx;
position: relative;
z-index: 1;
flex: 1;
}
.progress-card-title {
font-size: 30rpx;
font-weight: 700;
}
.progress-card-desc {
font-size: 22rpx;
opacity: 0.85;
font-weight: 500;
}
/* Custom CSS Progress Ring */
.progress-ring-container {
width: 104rpx;
height: 104rpx;
position: relative;
margin-left: 20rpx;
z-index: 2;
border-radius: 50%;
background: rgba(255,255,255,0.3); /* Track color */
display: flex;
align-items: center;
justify-content: center;
}
.progress-ring-inner {
width: 100%;
height: 100%;
border-radius: 50%;
background: conic-gradient(#2E7D32 var(--progress), transparent 0deg);
position: absolute;
top: 0;
left: 0;
transform: rotate(-90deg); /* Start from top */
z-index: 1;
}
/* Mask circle to create ring effect */
.progress-ring-mask {
width: 88rpx; /* 104 - (8*2) = 88 */
height: 88rpx;
background: #E8F5E9; /* Match card gradient start roughly or transparent? */
/* Actually background needs to match parent. Since parent is gradient, solid color mask won't match perfectly.
BETTER approach: Use border-radius border trick.
*/
border-radius: 50%;
position: absolute;
z-index: 2;
/* This method has a flaw with gradient backgrounds.
Alternative: pure border with clip-path?
Or simpler:
.ring {
width: 104rpx; height: 104rpx;
border-radius: 50%;
padding: 8rpx;
background: conic-gradient(currentColor var(--val), transparent 0) content-box;
-webkit-mask: ...
}
*/
}
/* Updated Approach: CSS Border Ring using masks */
.css-progress-ring {
width: 104rpx;
height: 104rpx;
border-radius: 50%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background: conic-gradient(#FFFFFF var(--progress), rgba(255,255,255,0.3) 0deg);
/* The ring color is White vs Track transparent/white-alpha */
}
/* The inner hole */
.css-progress-ring::before {
content: "";
position: absolute;
inset: 10rpx; /* stroke width ~ 10rpx/2 ?? No. 104 wide. inset 8rpx -> 88 inner. stroke 8. */
border-radius: 50%;
background-color: transparent; /* We need to show the card gradient behind... difficult with this technique unless we use mask-image */
}
/* Let's try standard mask composite approach or just SVG image which is easiest.
But user wants style resolved. SVG support in WXML view is limited to background-image.
I will use the `t-progress` but FIX the overflow by making sure container is large enough and REMOVE OVERFLOW HIDDEN from the card if necessary, OR fix the container size.
Actually, the user said "Banner is too wide".
I will stick to `t-progress` but I will Wrap it in a `view` that has definitive size and `overflow: visible`.
However, user said "overflows banner".
Let's use a SIMPLIFIED CSS Ring that doesn't rely on transparency tricks that fail on gradients:
Use 4 borders rotated? No.
Let's go with the SVG Image solution in WXML. It is supported via <image> with svg data uri, or just careful CSS.
Wait, the user's "style not resolved" likely implies `t-progress` is just broken essentially.
I will implement a pure WXML/WXSS ring using `view` borders (two halves). This allows transparency center.
*/
.ring-container {
width: 104rpx;
height: 104rpx;
position: relative;
margin-left: 20rpx;
z-index: 5;
}
.ring-text {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
font-weight: 800;
color: #1B5E20;
z-index: 10;
}
/* Pure CSS Progress Circle (Left/Right spin method) */
.circle-wrap {
width: 104rpx;
height: 104rpx;
background: rgba(255,255,255,0.3); /* Track */
border-radius: 50%;
position: relative;
overflow: hidden; /* This clips the half-circles but center is filled? No. */
/* This method clears center by adding a smaller circle on top */
}
.circle-wrap .mask,
.circle-wrap .fill {
width: 104rpx;
height: 104rpx;
position: absolute;
border-radius: 50%;
}
.circle-wrap .mask {
clip: rect(0px, 104rpx, 104rpx, 52rpx);
}
.circle-wrap .fill {
clip: rect(0px, 52rpx, 104rpx, 0px);
background-color: #FFFFFF; /* Progress Color */
}
.circle-wrap .mask.full,
.circle-wrap .fill {
animation: fill ease-in-out 3s;
transform: rotate(126deg); /* Dynamic via style */
}
/* Inner hole */
.circle-inside {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
background: #D2E8D5; /* Approximated gradient mid-point or use transparent if specific layout allows */
/* Since gradient is complex, solid color looks bad.
The "Progress Circle" style issue is likely the 'cut off' or solid center blocking gradient.
BEST SOLUTION: Canvas 2D. But cumbersome.
Let's try t-progress one last time with correct constraints? No, user is annoyed.
Let's use a transparent PNG for the track and `conic-gradient` for the fill with a specific mix-blend-mode? No support.
Let's use `conic-gradient` with `mask-image`. Most modern webviews support it.
*/
}
/* Progress Circle - Matching Prototype */
.progress-circle {
position: relative;
width: 120rpx;
height: 120rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 20rpx;
z-index: 1;
}
.progress-ring {
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
background: conic-gradient(#2E7D32 var(--progress-deg), rgba(255, 255, 255, 0.3) 0deg);
-webkit-mask: radial-gradient(transparent 60%, black 61%);
mask: radial-gradient(transparent 60%, black 61%);
}
.percentage {
position: relative;
z-index: 2;
font-size: 28rpx;
font-weight: 700;
color: #1B5E20;
}
.tasks-container {
flex: 1;
background: white;
border-top-left-radius: 60rpx;
border-top-right-radius: 60rpx;
padding: 48rpx 40rpx 0;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 -8rpx 32rpx rgba(0,0,0,0.03);
min-height: 0; /* Critical for flex scrolling */
}
.section-title {
font-size: 36rpx;
font-weight: 800;
color: #263238;
margin-bottom: 32rpx;
padding-left: 8rpx;
flex-shrink: 0;
}
.task-list {
flex: 1;
height: 0; /* Force flex container to define height */
}
.plant-task-card {
background: white;
border-radius: 40rpx;
padding: 32rpx;
margin-bottom: 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.03);
border: 2rpx solid transparent;
transition: all 0.2s;
}
.plant-task-card.has-overdue {
border-color: rgba(239, 83, 80, 0.1);
background: #FFF8F8;
}
.card-header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
padding-bottom: 24rpx;
border-bottom: 2rpx solid rgba(0, 0, 0, 0.03);
}
.plant-info-brief {
display: flex;
align-items: center;
gap: 20rpx;
}
.plant-thumb-small {
width: 80rpx;
height: 80rpx;
border-radius: 24rpx;
overflow: hidden;
background: #f0f0f0;
}
.plant-thumb-small image {
width: 100%;
height: 100%;
}
.thumb-placeholder {
width: 100%;
height: 100%;
background: #E8F5E9;
color: #558B2F;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 32rpx;
}
.plant-name-title {
font-size: 30rpx;
font-weight: 700;
color: #263238;
}
.group-overdue-badge {
font-size: 20rpx;
color: #EF5350;
background: rgba(239, 83, 80, 0.1);
padding: 4rpx 16rpx;
border-radius: 20rpx;
font-weight: 600;
}
.plant-tasks-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.mini-task-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.mini-task-left {
display: flex;
align-items: center;
gap: 24rpx;
}
.task-type-icon-circle {
width: 64rpx;
height: 64rpx;
background: #F8F9FA;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #90A4AE;
}
.task-icon {
width: 32rpx;
height: 32rpx;
}
.mini-task-text {
display: flex;
flex-direction: column;
gap: 4rpx;
}
.task-label {
font-size: 28rpx;
font-weight: 500;
color: #37474F;
}
.task-overdue-text {
font-size: 22rpx;
color: #EF5350;
font-weight: 600;
}
.mini-check-btn {
width: 64rpx;
height: 64rpx;
border-radius: 20rpx;
border: 3rpx solid #E0E0E0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
box-sizing: border-box;
}
.mini-check-btn:active {
background: rgba(85, 139, 47, 0.05);
border-color: #558B2F;
}
.mini-check-btn.btn-urgent {
border-color: rgba(239, 83, 80, 0.3);
}
.mini-check-btn.btn-urgent:active {
background: rgba(239, 83, 80, 0.05);
border-color: #EF5350;
}
/* Modal Specifics */
.modal-card {
background: white;
width: 640rpx;
border-radius: 64rpx;
padding: 48rpx;
box-sizing: border-box;
position: relative;
overflow: hidden;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 48rpx;
}
.modal-title {
font-size: 36rpx;
font-weight: 800;
color: #263238;
}
.close-btn {
padding: 8rpx;
}
.task-confirm-content {
display: flex;
flex-direction: column;
gap: 40rpx;
}
.confirm-plant-info {
display: flex;
align-items: center;
gap: 24rpx;
background: #F4F6F0;
padding: 32rpx;
border-radius: 32rpx;
}
.confirm-icon {
width: 96rpx;
height: 96rpx;
background: white;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.05);
}
.icon-32 {
width: 48rpx;
height: 48rpx;
}
.confirm-text {
display: flex;
flex-direction: column;
gap: 8rpx;
flex: 1;
}
.cp-name {
font-size: 32rpx;
font-weight: 700;
color: #263238;
}
.cp-task {
font-size: 26rpx;
color: #90A4AE;
}
.remark-section {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.remark-label {
font-size: 26rpx;
font-weight: 600;
color: #263238;
}
.remark-input {
width: 100%;
height: 160rpx;
background: #F8F9FA;
border: 2rpx solid #ECEFF1;
border-radius: 24rpx;
padding: 24rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.confirm-btn-wrap {
margin-top: 16rpx;
}
.confirm-complete-btn {
width: 100%;
background: #558B2F;
color: white;
height: 100rpx;
border-radius: 50rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 600;
box-shadow: 0 8rpx 24rpx rgba(85, 139, 47, 0.4);
}
.confirm-complete-btn:active {
transform: scale(0.98);
}
+73
View File
@@ -0,0 +1,73 @@
// pages/wiki/index.js
import { MOCK_WIKI } from '../../utils/mockData';
Page({
data: {
wikiList: [],
displayedList: [],
searchQuery: '',
activeCategory: '全部',
showIdentifyModal: false
},
onLoad() {
this.setData({ wikiList: MOCK_WIKI });
this.filterList();
},
onShow() {
if (typeof this.getTabBar === 'function' &&
this.getTabBar()) {
this.getTabBar().setData({ selected: 3 });
}
},
onSearchInput(e) {
// TDesign search event: e.detail.value
this.setData({ searchQuery: e.detail.value }, () => {
this.filterList();
});
},
setCategory(e) {
this.setData({ activeCategory: e.currentTarget.dataset.cat }, () => {
this.filterList();
});
},
filterList() {
const { wikiList, searchQuery, activeCategory } = this.data;
let result = wikiList;
if (searchQuery) {
const q = searchQuery.toLowerCase();
result = result.filter(item =>
item.name.toLowerCase().includes(q) ||
item.scientificName.toLowerCase().includes(q)
);
}
if (activeCategory !== '全部') {
result = result.filter(item => item.category.includes(activeCategory));
}
this.setData({ displayedList: result });
},
goToDetail(e) {
const item = e.currentTarget.dataset.item;
wx.navigateTo({
url: `/pages/plant-detail/index?id=${item.id}&mode=wiki`
});
},
openIdentifyModal() { this.setData({ showIdentifyModal: true }); },
onPopupVisibleChange(e) {
this.setData({
showIdentifyModal: e.detail.visible
});
},
closeIdentifyModal() { this.setData({ showIdentifyModal: false }); }
})
+13
View File
@@ -0,0 +1,13 @@
{
"navigationBarTitleText": "植物百科",
"usingComponents": {
"t-search": "tdesign-miniprogram/search/search",
"t-tag": "tdesign-miniprogram/tag/tag",
"t-image": "tdesign-miniprogram/image/image",
"t-fab": "tdesign-miniprogram/fab/fab",
"t-popup": "tdesign-miniprogram/popup/popup",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
"t-icon": "tdesign-miniprogram/icon/icon"
}
}
+74
View File
@@ -0,0 +1,74 @@
<!--pages/wiki/index.wxml-->
<view class="wiki-page">
<view class="wiki-scroll-area">
<view class="search-section">
<t-search placeholder="搜索植物名称,如:龟背竹" value="{{searchQuery}}" bind:change="onSearchInput" shape="round" />
</view>
<view class="category-scroll">
<t-tag
wx:for="{{['全部', '观叶', '观花', '多肉']}}"
wx:key="*this"
variant="{{activeCategory === item ? 'dark' : 'outline'}}"
theme="{{activeCategory === item ? 'primary' : 'default'}}"
shape="mark"
size="medium"
style="margin-right: 16rpx;"
bind:tap="setCategory"
data-cat="{{item}}"
>
{{item}}
</t-tag>
</view>
<view class="wiki-list">
<view wx:for="{{displayedList}}" wx:key="id" class="wiki-card" bindtap="goToDetail" data-item="{{item}}">
<view class="wiki-image">
<t-image src="/assets/{{item.images[0]}}" mode="aspectFill" width="100%" height="100%" />
</view>
<view class="wiki-info">
<view class="wiki-top">
<text class="wiki-name">{{item.name}}</text>
<text class="scientific-name">{{item.scientificName}}</text>
</view>
<t-tag size="small" variant="light" theme="success">{{item.category}}</t-tag>
</view>
<t-icon name="chevron-right" size="48rpx" color="#ccc" />
</view>
</view>
<!-- Spacer -->
<view style="height: 160rpx;"></view>
</view>
<t-fab icon="scan" text="植物识别" bind:click="openIdentifyModal" aria-label="植物识别"></t-fab>
<!-- Identify Popup -->
<t-popup visible="{{showIdentifyModal}}" bind:visible-change="onPopupVisibleChange" placement="bottom">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">识别植物</text>
</view>
<view class="upload-options-grid">
<view class="upload-opt-item">
<view class="opt-icon-circle" style="background: #E8F5E9;">
<t-icon name="camera" size="64rpx" color="#2E7D32" />
</view>
<text>拍照识别</text>
</view>
<view class="upload-opt-item">
<view class="opt-icon-circle" style="background: #E3F2FD;">
<t-icon name="image" size="64rpx" color="#1565C0" />
</view>
<text>从相册上传</text>
</view>
</view>
<view class="popup-footer">
<t-button block variant="outline" bind:tap="closeIdentifyModal">取消</t-button>
</view>
</view>
</t-popup>
</view>
+98
View File
@@ -0,0 +1,98 @@
/** pages/wiki/index.wxss **/
.wiki-page {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #F9FAFB;
position: relative;
overflow: hidden;
}
.wiki-scroll-area {
flex: 1;
overflow-y: auto;
padding: 20rpx 40rpx;
}
.search-section {
margin-bottom: 32rpx;
}
.category-scroll {
display: flex;
flex-wrap: wrap; /* TDesign Tags might wrap nicely */
margin-bottom: 48rpx;
}
/* Old chips removed, replaced by t-tag */
.wiki-list {
display: flex;
flex-direction: column;
gap: 32rpx;
}
.wiki-card {
background: white;
padding: 32rpx;
border-radius: 48rpx;
display: flex;
align-items: center;
gap: 36rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.03);
}
.wiki-image {
width: 140rpx; height: 140rpx;
border-radius: 36rpx;
overflow: hidden;
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.1);
flex-shrink: 0;
background: #f0f0f0;
}
.wiki-image t-image { width: 100%; height: 100%; display: block; }
.wiki-info { flex: 1; }
.wiki-top { margin-bottom: 8rpx; }
.wiki-name { font-size: 34rpx; font-weight: 700; color: var(--text-main); display: block; margin-bottom: 4rpx; }
.scientific-name { font-size: 24rpx; color: #90A4AE; font-style: italic; font-family: serif; display: block; }
/* Popup Styles */
.popup-content {
background: white;
border-radius: 40rpx 40rpx 0 0;
padding: 40rpx;
padding-bottom: env(safe-area-inset-bottom);
}
.popup-header {
text-align: center;
margin-bottom: 48rpx;
}
.popup-title {
font-size: 36rpx;
font-weight: 800;
color: var(--text-main);
}
.upload-options-grid {
display: flex; gap: 40rpx; justify-content: center;
margin-bottom: 48rpx;
}
.upload-opt-item {
display: flex; flex-direction: column; align-items: center; gap: 16rpx;
}
.opt-icon-circle {
width: 120rpx; height: 120rpx; border-radius: 40rpx;
display: flex; align-items: center; justify-content: center;
}
.popup-footer {
display: flex;
justify-content: center;
}