init: initial commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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' });
|
||||
}
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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' });
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 }); }
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user