feat: 任务和社区页面

This commit is contained in:
Blizzard
2026-02-06 17:27:35 +08:00
parent d42471e1d5
commit b800ea03b5
30 changed files with 1777 additions and 551 deletions
+64 -109
View File
@@ -1,16 +1,15 @@
// pages/community/create/index.js
import { MOCK_POSTS } from '../../../utils/mockData';
import request from '../../../utils/request';
Page({
data: {
content: '',
images: [],
canPublish: false,
canPublish: false, // Only depends on content
autoFocus: true,
location: '',
selectedTopics: [],
suggestedTopics: ['植物养护', '多肉日记', '绿植分享', '花卉美照', '阳台花园', '新手入门'],
hasDraft: false,
showImageSheet: false,
imageSheetItems: [
{ label: '拍照', value: 'camera' },
@@ -19,60 +18,18 @@ Page({
},
onLoad() {
// Check for saved draft
this.loadDraft();
// No draft loading
},
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');
}
// No draft saving
},
onContentInput(e) {
const content = e.detail.value;
this.setData({
content,
canPublish: content.trim().length > 0,
hasDraft: false
canPublish: content.trim().length > 0
});
},
@@ -109,8 +66,7 @@ Page({
success: (res) => {
const newImages = res.tempFiles.map(f => f.tempFilePath);
this.setData({
images: [...this.data.images, ...newImages],
hasDraft: false
images: [...this.data.images, ...newImages]
});
}
});
@@ -130,8 +86,7 @@ Page({
success: (res) => {
const newImage = res.tempFiles[0].tempFilePath;
this.setData({
images: [...this.data.images, newImage],
hasDraft: false
images: [...this.data.images, newImage]
});
}
});
@@ -200,74 +155,74 @@ Page({
});
},
insertEmoji() {
// Simple emoji picker simulation
const emojis = ['🌱', '🌿', '🍀', '🌵', '🌻', '🌺', '🌸', '🌼', '🪴', '🌲'];
wx.showActionSheet({
itemList: emojis,
success: (res) => {
const emoji = emojis[res.tapIndex];
this.setData({
content: this.data.content + emoji,
canPublish: true
});
}
});
},
handleCancel() {
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();
}
wx.navigateBack();
},
handlePublish() {
if (!this.data.canPublish) {
async handlePublish() {
if (!this.data.content || !this.data.content.trim()) {
wx.showToast({ title: '请输入内容', icon: 'none' });
return;
}
// Content already includes topics as hashtags
const finalContent = this.data.content.trim();
wx.showLoading({ title: '发布中...', mask: true });
// Create new post
const newPost = {
id: Date.now().toString(),
user: '我的花园',
content: finalContent,
images: this.data.images,
time: '刚刚',
likes: [],
comments: []
};
try {
// 1. Upload Images
const ossIds = [];
const images = this.data.images;
// Add to global mock data (at the beginning)
MOCK_POSTS.unshift(newPost);
if (images.length > 0) {
const uploadPromises = images.map(filePath => {
return request.upload(filePath).then(res => {
// Res structure: { file: { id: "...", url: "..." } }
return res && res.file ? res.file.id : null;
});
});
// Clear draft
this.clearDraft();
const uploadedIds = await Promise.all(uploadPromises);
wx.showToast({ title: '发布成功', icon: 'success' });
uploadedIds.forEach(id => {
if (id) ossIds.push(id);
});
setTimeout(() => {
wx.navigateBack();
}, 1000);
if (images.length > 0 && ossIds.length === 0) {
throw new Error('图片上传失败');
}
}
// 2. Publish Post
// Title is removed from UI. Using content snippet or default title.
const content = this.data.content.trim();
const title = content.length > 20 ? content.substring(0, 20) + '...' : content;
const payload = {
title: title || '新动态', // Fallback title
content: content,
location: this.data.location || '',
ossIds: ossIds
};
await request.post('/post/publish', payload);
wx.hideLoading();
wx.showToast({ title: '发布成功', icon: 'success' });
// Refresh previous page
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2];
if (prevPage && prevPage.onRefresh) {
prevPage.onRefresh();
}
setTimeout(() => {
wx.navigateBack();
}, 1000);
} catch (err) {
wx.hideLoading();
console.error('Publish failed', err);
wx.showToast({ title: '发布失败', icon: 'none' });
}
}
})
+3 -1
View File
@@ -11,6 +11,8 @@
</view>
</view>
<!-- Text Input -->
<textarea
class="post-textarea"
@@ -33,7 +35,7 @@
<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%" />
<image src="{{item}}" mode="aspectFill" style="width: 100%; height: 100%; display: block;" />
<view class="remove-btn" catchtap="removeImage" data-index="{{index}}">
<t-icon name="close" size="24rpx" color="#fff" />
</view>
+20
View File
@@ -314,3 +314,23 @@ page {
font-size: 24rpx;
color: #999;
}
/* Title Input */
.title-section {
padding: 16rpx 0;
border-bottom: 2rpx solid #f5f5f5;
margin-bottom: 16rpx;
}
.post-title-input {
font-size: 36rpx;
font-weight: 700;
color: #333;
width: 100%;
height: 64rpx; /* Height for single line */
min-height: 64rpx;
}
.title-placeholder {
color: #bbb;
font-weight: 400;
}
+168 -57
View File
@@ -1,5 +1,5 @@
// pages/community/index.js
import { MOCK_POSTS } from '../../utils/mockData';
import request from '../../utils/request';
Page({
data: {
@@ -8,31 +8,110 @@ Page({
activePostId: null, // For showing action popup
showCommentBar: false,
commentingPostId: null,
commentText: ''
commentText: '',
isLoading: false,
current: 1,
pageSize: 10,
hasMore: true
},
onLoad() {
this.setData({ posts: MOCK_POSTS });
this.updateDisplayedPosts();
this.fetchPosts(true);
},
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();
},
// Called by create post page
onRefresh() {
this.fetchPosts(true);
},
onPullDownRefresh() {
this.fetchPosts(true).then(() => {
wx.stopPullDownRefresh();
});
},
onReachBottom() {
if (this.data.hasMore && !this.data.isLoading) {
this.fetchPosts(false);
}
},
async fetchPosts(reset = false) {
if (this.data.isLoading) return;
this.setData({ isLoading: true });
const current = reset ? 1 : this.data.current;
const pageSize = this.data.pageSize;
try {
// Correct API Endpoint and Params
const res = await request.post('/post/page', { current, pageSize });
// Handle response structure: { code: 200, data: { list: [], ... } }
// OR if request.js unwraps it: { list: [], ... }
const data = res.data || res || {};
const rawList = data.list || [];
// Map backend data to UI model
const newPosts = rawList.map(item => {
const publisher = item.publisher || {};
const avatarObj = publisher.avatar || {};
return {
id: item.id,
user: publisher.nickName || publisher.name || '花友',
avatar: avatarObj.url || '/assets/default_avatar.png',
content: item.content,
images: (item.imgList || []).map(img => img.url),
time: item.createdAtStr || '刚刚',
likes: (item.likeList || []).map(l => l.liker ? (l.liker.nickName || l.liker.name) : '花友'),
comments: (item.commentList || []).map(c => ({
id: c.id,
user: c.commentator ? (c.commentator.nickName || c.commentator.name) : '花友',
content: c.content
})),
likedByMe: item.hasLiked === 1,
likeCount: item.likeCount || 0,
commentCount: item.commentCount || 0,
isExpanded: false
};
});
if (reset) {
this.setData({
posts: newPosts,
displayedPosts: newPosts,
current: 2,
hasMore: newPosts.length >= pageSize
});
} else {
const combined = [...this.data.posts, ...newPosts];
this.setData({
posts: combined,
displayedPosts: combined,
current: current + 1,
hasMore: newPosts.length >= pageSize
});
}
} catch (err) {
console.error('Fetch posts failed', err);
wx.showToast({ title: '加载失败', icon: 'none' });
} finally {
this.setData({ isLoading: false });
}
},
updateDisplayedPosts() {
const { posts } = this.data;
// Show all posts with likedByMe flag
const displayed = posts.map(post => ({
...post,
likedByMe: post.likes.includes('我的花园')
}));
this.setData({ displayedPosts: displayed });
// Just sync posts to displayedPosts
this.setData({ displayedPosts: this.data.posts });
},
// Preview image in full screen
@@ -72,31 +151,37 @@ Page({
},
// Like post
likePost(e) {
async 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;
});
const post = this.data.posts.find(p => p.id === postId);
if (!post) return;
this.setData({
posts,
activePostId: null // Hide popup after action
}, () => {
this.updateDisplayedPosts();
});
const type = post.likedByMe ? 2 : 1;
try {
await request.get('/post/like', { id: postId, type });
// Optimistic Update: Only toggle button state. Do NOT modify likes list text.
const updatedPosts = this.data.posts.map(p => {
if (p.id === postId) {
return { ...p, likedByMe: !p.likedByMe };
}
return p;
});
this.setData({
posts: updatedPosts,
displayedPosts: updatedPosts,
activePostId: null
});
// Call page API to refresh list data (including Like List text)
this.fetchPosts(true);
} catch (err) {
console.error('Like failed', err);
wx.showToast({ title: '操作失败', icon: 'none' });
}
},
// Show comment input bar
@@ -122,39 +207,65 @@ Page({
this.setData({ commentText: e.detail.value });
},
submitComment() {
async 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;
});
try {
await request.post('/post/comment', {
postId: commentingPostId,
content: commentText.trim()
});
this.setData({
posts,
showCommentBar: false,
commentingPostId: null,
commentText: ''
}, () => {
this.updateDisplayedPosts();
wx.showToast({ title: '评论成功', icon: 'success' });
});
// Optimistic update
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,
displayedPosts: posts,
showCommentBar: false,
commentingPostId: null,
commentText: ''
});
// Silent refresh from server
this.fetchPosts(true);
} catch (err) {
console.error('Comment failed', err);
wx.showToast({ title: '评论失败', icon: 'none' });
}
},
goToCreatePost() {
wx.navigateTo({
url: '/pages/community/create/index'
});
},
toggleCommentExpand(e) {
const postId = e.currentTarget.dataset.id;
const posts = this.data.posts.map(p => {
if (p.id === postId) {
return { ...p, isExpanded: !p.isExpanded };
}
return p;
});
this.setData({ posts, displayedPosts: posts });
}
})
+32 -11
View File
@@ -22,9 +22,7 @@
<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>
<image class="avatar-square" src="{{item.avatar}}" mode="aspectFill" />
</view>
<!-- Content -->
@@ -34,12 +32,11 @@
<!-- 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)}}"
<view wx:for="{{item.images}}" wx:for-item="img" wx:key="*this" class="post-image-item" catchtap="previewImage" data-url="{{img}}" data-urls="{{item.images}}">
<image
src="{{img}}"
mode="aspectFill"
width="100%"
height="100%"
style="width: 100%; height: 100%; display: block;"
lazy-load
/>
</view>
@@ -76,7 +73,7 @@
<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" />
<t-icon name="heart-filled" size="28rpx" color="#FA5151" />
<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>
@@ -87,10 +84,26 @@
<!-- 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">
<view
wx:for="{{item.comments}}"
wx:for-item="comment"
wx:key="id"
class="comment-item"
wx:if="{{index < 3 || item.isExpanded}}"
>
<text class="comment-user">{{comment.user}}</text>
<text class="comment-content">{{comment.content}}</text>
</view>
<!-- Expand/Collapse Button -->
<view
wx:if="{{item.comments.length > 3}}"
class="comment-expand-btn"
catchtap="toggleCommentExpand"
data-id="{{item.id}}"
>
<text>{{item.isExpanded ? '收起' : '展开更多'}}</text>
</view>
</view>
</view>
@@ -116,9 +129,16 @@
<t-icon name="add" size="48rpx" color="#fff" />
</view>
<!-- Comment Input Mask -->
<view
class="comment-input-mask"
wx:if="{{showCommentBar}}"
bindtap="hideCommentBar"
catchtouchmove="stopPropagation"
></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"
@@ -129,6 +149,7 @@
focus="{{showCommentBar}}"
confirm-type="send"
adjust-position="{{true}}"
cursor-spacing="20"
/>
<view class="send-btn {{commentText ? 'active' : ''}}" bindtap="submitComment">
<text>发送</text>
+7
View File
@@ -429,3 +429,10 @@ page {
.send-btn:active {
opacity: 0.8;
}
.comment-expand-btn {
font-size: 26rpx;
color: #576b95;
margin-top: 8rpx;
padding: 4rpx 0;
}