feat: 任务和社区页面
This commit is contained in:
+64
-109
@@ -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' });
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user