diff --git a/cloudbaserc.json b/cloudbaserc.json index 178416d..946afb1 100644 --- a/cloudbaserc.json +++ b/cloudbaserc.json @@ -58,6 +58,62 @@ "runtime": "Nodejs20.19", "handler": "index.main", "description": "打招呼 / 发起私信" + }, + { + "name": "addComment", + "timeout": 10, + "runtime": "Nodejs20.19", + "handler": "index.main", + "description": "发表评论" + }, + { + "name": "getComments", + "timeout": 10, + "runtime": "Nodejs20.19", + "handler": "index.main", + "description": "获取帖子评论列表" + }, + { + "name": "getFollowList", + "timeout": 10, + "runtime": "Nodejs20.19", + "handler": "index.main", + "description": "获取关注/粉丝列表" + }, + { + "name": "followUser", + "timeout": 10, + "runtime": "Nodejs20.19", + "handler": "index.main", + "description": "关注/取消关注用户" + }, + { + "name": "getUser", + "timeout": 10, + "runtime": "Nodejs20.19", + "handler": "index.main", + "description": "获取用户信息" + }, + { + "name": "updateProfile", + "timeout": 10, + "runtime": "Nodejs20.19", + "handler": "index.main", + "description": "更新用户资料" + }, + { + "name": "updatePet", + "timeout": 10, + "runtime": "Nodejs20.19", + "handler": "index.main", + "description": "新增或更新宠物档案" + }, + { + "name": "deletePet", + "timeout": 10, + "runtime": "Nodejs20.19", + "handler": "index.main", + "description": "删除宠物档案" } ] } diff --git a/cloudfunctions/addComment/index.js b/cloudfunctions/addComment/index.js new file mode 100644 index 0000000..3963db3 --- /dev/null +++ b/cloudfunctions/addComment/index.js @@ -0,0 +1,43 @@ +const cloud = require('wx-server-sdk') +cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) +const db = cloud.database() +const _ = db.command + +exports.main = async (event, context) => { + const { OPENID } = cloud.getWXContext() + const { postId, content } = event + + if (!postId || !content?.trim()) { + return { code: -1, message: '参数错误' } + } + + try { + const postRes = await db.collection('posts').doc(postId).get().catch(() => null) + if (!postRes?.data) return { code: -1, message: '帖子不存在' } + + const comment = { + postId, + authorId: OPENID, + content: content.trim(), + likeCount: 0, + createdAt: db.serverDate(), + } + const res = await db.collection('comments').add({ data: comment }) + + await db.collection('posts').doc(postId).update({ + data: { commentCount: _.inc(1) }, + }) + + const authorRes = await db.collection('users') + .where({ openid: OPENID }) + .field({ nickName: true, avatarUrl: true, openid: true }) + .get() + + return { + code: 0, + data: { _id: res._id, ...comment, author: authorRes.data[0] || null }, + } + } catch (e) { + return { code: -1, message: e.message } + } +} diff --git a/cloudfunctions/addComment/package.json b/cloudfunctions/addComment/package.json new file mode 100644 index 0000000..dd4d669 --- /dev/null +++ b/cloudfunctions/addComment/package.json @@ -0,0 +1 @@ +{ "name": "addComment", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } } diff --git a/cloudfunctions/deletePet/index.js b/cloudfunctions/deletePet/index.js new file mode 100644 index 0000000..4aa066a --- /dev/null +++ b/cloudfunctions/deletePet/index.js @@ -0,0 +1,30 @@ +const cloud = require('wx-server-sdk') +cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) +const db = cloud.database() + +exports.main = async (event, context) => { + const { OPENID } = cloud.getWXContext() + const { petId } = event || {} + + if (!petId) return { code: -1, message: '缺少宠物ID' } + + try { + const userRes = await db.collection('users').where({ openid: OPENID }).get() + if (userRes.data.length === 0) { + return { code: -1, message: '用户不存在' } + } + const user = userRes.data[0] + const pets = (Array.isArray(user.pets) ? user.pets : []).filter(p => p._id !== petId) + + await db.collection('users').doc(user._id).update({ + data: { + pets, + updatedAt: db.serverDate(), + }, + }) + + return { code: 0, data: null } + } catch (e) { + return { code: -1, message: e.message } + } +} diff --git a/cloudfunctions/deletePet/package.json b/cloudfunctions/deletePet/package.json new file mode 100644 index 0000000..add5faa --- /dev/null +++ b/cloudfunctions/deletePet/package.json @@ -0,0 +1 @@ +{ "name": "deletePet", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } } diff --git a/cloudfunctions/followUser/index.js b/cloudfunctions/followUser/index.js new file mode 100644 index 0000000..5e8d4c7 --- /dev/null +++ b/cloudfunctions/followUser/index.js @@ -0,0 +1,40 @@ +const cloud = require('wx-server-sdk') +cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) +const db = cloud.database() +const _ = db.command + +exports.main = async (event, context) => { + const { OPENID } = cloud.getWXContext() + const { targetId } = event + if (!targetId || targetId === OPENID) return { code: -1, message: '参数错误' } + + try { + const existRes = await db.collection('follows') + .where({ followerId: OPENID, followeeId: targetId }) + .count() + + if (existRes.total > 0) { + // unfollow + await db.collection('follows') + .where({ followerId: OPENID, followeeId: targetId }) + .remove() + await Promise.all([ + db.collection('users').where({ openid: OPENID }).update({ data: { 'stats.friends': _.inc(-1) } }), + db.collection('users').where({ openid: targetId }).update({ data: { 'stats.friends': _.inc(-1) } }), + ]) + return { code: 0, data: { following: false } } + } else { + // follow + await db.collection('follows').add({ + data: { followerId: OPENID, followeeId: targetId, createdAt: db.serverDate() }, + }) + await Promise.all([ + db.collection('users').where({ openid: OPENID }).update({ data: { 'stats.friends': _.inc(1) } }), + db.collection('users').where({ openid: targetId }).update({ data: { 'stats.friends': _.inc(1) } }), + ]) + return { code: 0, data: { following: true } } + } + } catch (e) { + return { code: -1, message: e.message } + } +} diff --git a/cloudfunctions/followUser/package.json b/cloudfunctions/followUser/package.json new file mode 100644 index 0000000..e3a4281 --- /dev/null +++ b/cloudfunctions/followUser/package.json @@ -0,0 +1 @@ +{ "name": "followUser", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } } diff --git a/cloudfunctions/getComments/index.js b/cloudfunctions/getComments/index.js new file mode 100644 index 0000000..9307e14 --- /dev/null +++ b/cloudfunctions/getComments/index.js @@ -0,0 +1,45 @@ +const cloud = require('wx-server-sdk') +cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) +const db = cloud.database() +const _ = db.command +const PAGE_SIZE = 20 + +exports.main = async (event, context) => { + const { postId, page = 0 } = event + if (!postId) return { code: -1, message: '缺少 postId' } + + try { + const skip = page * PAGE_SIZE + const [totalRes, commentsRes] = await Promise.all([ + db.collection('comments').where({ postId }).count(), + db.collection('comments') + .where({ postId }) + .orderBy('createdAt', 'asc') + .skip(skip) + .limit(PAGE_SIZE) + .get(), + ]) + + const comments = commentsRes.data + const authorIds = [...new Set(comments.map(c => c.authorId))] + + let authorMap = {} + if (authorIds.length > 0) { + const authorsRes = await db.collection('users') + .where({ openid: _.in(authorIds) }) + .field({ nickName: true, avatarUrl: true, openid: true }) + .get() + authorsRes.data.forEach(u => { authorMap[u.openid] = u }) + } + + return { + code: 0, + data: { + list: comments.map(c => ({ ...c, author: authorMap[c.authorId] || null })), + total: totalRes.total, + }, + } + } catch (e) { + return { code: -1, message: e.message } + } +} diff --git a/cloudfunctions/getComments/package.json b/cloudfunctions/getComments/package.json new file mode 100644 index 0000000..d480d32 --- /dev/null +++ b/cloudfunctions/getComments/package.json @@ -0,0 +1 @@ +{ "name": "getComments", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } } diff --git a/cloudfunctions/getFollowList/index.js b/cloudfunctions/getFollowList/index.js new file mode 100644 index 0000000..8466784 --- /dev/null +++ b/cloudfunctions/getFollowList/index.js @@ -0,0 +1,51 @@ +const cloud = require('wx-server-sdk') +cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) +const db = cloud.database() +const _ = db.command + +exports.main = async (event, context) => { + const { OPENID } = cloud.getWXContext() + const { type = 'following', userId } = event + const targetId = userId || OPENID + + try { + let followIds = [] + + if (type === 'following') { + const res = await db.collection('follows') + .where({ followerId: targetId }) + .field({ followeeId: true }) + .get() + followIds = res.data.map(f => f.followeeId) + } else { + const res = await db.collection('follows') + .where({ followeeId: targetId }) + .field({ followerId: true }) + .get() + followIds = res.data.map(f => f.followerId) + } + + if (followIds.length === 0) { + return { code: 0, data: [] } + } + + const usersRes = await db.collection('users') + .where({ openid: _.in(followIds) }) + .field({ nickName: true, openid: true, avatarUrl: true, bio: true, location: true, isOnline: true, lastSeen: true, pets: true, stats: true }) + .get() + + // check if current user follows each + const myFollowsRes = await db.collection('follows') + .where({ followerId: OPENID, followeeId: _.in(followIds) }) + .field({ followeeId: true }) + .get() + const myFollowSet = new Set(myFollowsRes.data.map(f => f.followeeId)) + + return { + code: 0, + data: usersRes.data.map(u => ({ ...u, _id: u._id, isFollowing: myFollowSet.has(u.openid) })), + } + } catch (e) { + return { code: -1, message: e.message } + } +} diff --git a/cloudfunctions/getFollowList/package.json b/cloudfunctions/getFollowList/package.json new file mode 100644 index 0000000..23d1710 --- /dev/null +++ b/cloudfunctions/getFollowList/package.json @@ -0,0 +1 @@ +{ "name": "getFollowList", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } } diff --git a/cloudfunctions/getUser/index.js b/cloudfunctions/getUser/index.js new file mode 100644 index 0000000..9f884c0 --- /dev/null +++ b/cloudfunctions/getUser/index.js @@ -0,0 +1,40 @@ +const cloud = require('wx-server-sdk') +cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) +const db = cloud.database() + +exports.main = async (event, context) => { + const { OPENID } = cloud.getWXContext() + const { userId } = event + + try { + let userRes + if (userId) { + // Look up by DB _id + userRes = await db.collection('users').doc(userId).get().catch(() => null) + if (!userRes?.data) { + // Fallback: look up by openid + const res = await db.collection('users').where({ openid: userId }).get() + userRes = res.data.length > 0 ? { data: res.data[0] } : null + } + } else { + const res = await db.collection('users').where({ openid: OPENID }).get() + userRes = res.data.length > 0 ? { data: res.data[0] } : null + } + + if (!userRes?.data) return { code: -1, message: '用户不存在' } + + const user = userRes.data + + // Check follow status + const followRes = await db.collection('follows') + .where({ followerId: OPENID, followeeId: user.openid }) + .count() + + return { + code: 0, + data: { ...user, isFollowing: followRes.total > 0 }, + } + } catch (e) { + return { code: -1, message: e.message } + } +} diff --git a/cloudfunctions/getUser/package.json b/cloudfunctions/getUser/package.json new file mode 100644 index 0000000..45d4c1a --- /dev/null +++ b/cloudfunctions/getUser/package.json @@ -0,0 +1 @@ +{ "name": "getUser", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } } diff --git a/cloudfunctions/updatePet/index.js b/cloudfunctions/updatePet/index.js new file mode 100644 index 0000000..b96c731 --- /dev/null +++ b/cloudfunctions/updatePet/index.js @@ -0,0 +1,72 @@ +const cloud = require('wx-server-sdk') +cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) +const db = cloud.database() + +// 根据生日计算年龄展示文案 +function calcAge(birthday) { + if (!birthday) return '' + const dob = new Date(birthday).getTime() + if (isNaN(dob)) return '' + const diffMs = Date.now() - dob + if (diffMs < 0) return '' + const years = Math.floor(diffMs / (365.25 * 24 * 3600 * 1000)) + const months = Math.floor((diffMs % (365.25 * 24 * 3600 * 1000)) / (30.44 * 24 * 3600 * 1000)) + return years > 0 ? `${years}岁${months > 0 ? months + '个月' : ''}` : `${months}个月` +} + +exports.main = async (event, context) => { + const { OPENID } = cloud.getWXContext() + const { pet } = event || {} + + if (!pet || !pet.name || !String(pet.name).trim()) { + return { code: -1, message: '宠物信息不完整' } + } + + try { + const userRes = await db.collection('users').where({ openid: OPENID }).get() + if (userRes.data.length === 0) { + return { code: -1, message: '用户不存在' } + } + const user = userRes.data[0] + const pets = Array.isArray(user.pets) ? [...user.pets] : [] + + const petData = { + name: String(pet.name).trim(), + species: pet.species || 'dog', + breed: pet.breed || '', + gender: pet.gender || 'female', + birthday: pet.birthday || '', + age: calcAge(pet.birthday), + bio: (pet.bio || '').trim(), + tags: Array.isArray(pet.tags) ? pet.tags.slice(0, 8) : [], + emoji: pet.emoji || '🐾', + photos: pet.avatarUrl ? [pet.avatarUrl] : (Array.isArray(pet.photos) ? pet.photos : []), + ownerId: OPENID, + } + + let resultPet + if (pet._id) { + const idx = pets.findIndex(p => p._id === pet._id) + if (idx === -1) { + return { code: -1, message: '宠物档案不存在' } + } + resultPet = { ...pets[idx], ...petData, _id: pet._id } + pets[idx] = resultPet + } else { + const newId = `pet_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` + resultPet = { _id: newId, ...petData } + pets.push(resultPet) + } + + await db.collection('users').doc(user._id).update({ + data: { + pets, + updatedAt: db.serverDate(), + }, + }) + + return { code: 0, data: resultPet } + } catch (e) { + return { code: -1, message: e.message } + } +} diff --git a/cloudfunctions/updatePet/package.json b/cloudfunctions/updatePet/package.json new file mode 100644 index 0000000..ea38a5b --- /dev/null +++ b/cloudfunctions/updatePet/package.json @@ -0,0 +1 @@ +{ "name": "updatePet", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } } diff --git a/cloudfunctions/updateProfile/index.js b/cloudfunctions/updateProfile/index.js new file mode 100644 index 0000000..69f437d --- /dev/null +++ b/cloudfunctions/updateProfile/index.js @@ -0,0 +1,27 @@ +const cloud = require('wx-server-sdk') +cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) +const db = cloud.database() + +exports.main = async (event, context) => { + const { OPENID } = cloud.getWXContext() + const { nickName, bio, location } = event + + if (!nickName?.trim()) return { code: -1, message: '昵称不能为空' } + + try { + await db.collection('users') + .where({ openid: OPENID }) + .update({ + data: { + nickName: nickName.trim(), + bio: (bio || '').trim(), + location: (location || '').trim(), + updatedAt: db.serverDate(), + }, + }) + + return { code: 0, data: null } + } catch (e) { + return { code: -1, message: e.message } + } +} diff --git a/cloudfunctions/updateProfile/package.json b/cloudfunctions/updateProfile/package.json new file mode 100644 index 0000000..7540fea --- /dev/null +++ b/cloudfunctions/updateProfile/package.json @@ -0,0 +1 @@ +{ "name": "updateProfile", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } } diff --git a/miniprogram/app.ts b/miniprogram/app.ts index 748cf23..1d09648 100644 --- a/miniprogram/app.ts +++ b/miniprogram/app.ts @@ -1,19 +1,15 @@ -import { UserProfile, GeoPoint } from './types/index' +import { UserProfile, GeoPoint, AppGlobalData } from './types/index' import { getCachedUser, login } from './utils/auth' import { CLOUD_ENV, HEARTBEAT_INTERVAL_MS } from './utils/constants' import { api } from './utils/api' -App<{ - userInfo: UserProfile | null - isLoggedIn: boolean - currentLocation: GeoPoint | null - _locationTimer: number | null -}>({ +App<{ globalData: AppGlobalData }>({ globalData: { userInfo: null, isLoggedIn: false, currentLocation: null, _locationTimer: null, + navBarInfo: { statusBarHeight: 0, navbarHeight: 44, navbarPaddingRight: 0 }, }, onLaunch() { @@ -22,6 +18,15 @@ App<{ traceUser: true, }) + // 测量系统胶囊按钮位置,供所有页面共用 + const sysInfo = wx.getSystemInfoSync() + const capsule = wx.getMenuButtonBoundingClientRect() + this.globalData.navBarInfo = { + statusBarHeight: sysInfo.statusBarHeight, + navbarHeight: (capsule.top - sysInfo.statusBarHeight) * 2 + capsule.height, + navbarPaddingRight: sysInfo.windowWidth - capsule.left, + } + const cached = getCachedUser() if (cached) { this.globalData.userInfo = cached diff --git a/miniprogram/components/post-card/post-card.ts b/miniprogram/components/post-card/post-card.ts index d00b8c4..14d4499 100644 --- a/miniprogram/components/post-card/post-card.ts +++ b/miniprogram/components/post-card/post-card.ts @@ -14,6 +14,7 @@ Component({ avatarGradient: '', timeText: '', likeCount: 0, + imageGridClass: 'post-img-grid-0', }, observers: { @@ -22,20 +23,25 @@ Component({ this.setData({ avatarGradient: getAvatarGradient(val.authorId || ''), timeText: formatTime(val.createdAt), - likeCount: val.likeCount, + likeCount: typeof val.likeCount === 'number' ? val.likeCount : 0, + imageGridClass: this.getImageGridClass(val.images), }) }, }, methods: { + getImageGridClass(images?: string[]) { + if (!Array.isArray(images)) return 'post-img-grid-0' + return `post-img-grid-${images.length > 3 ? 'multi' : images.length}` + }, + onCardTap() { const post = this.data.post as Post if (!post) return wx.navigateTo({ url: `/pages/post-detail/post-detail?id=${post._id}` }) }, - onAuthorTap(e: WechatMiniprogram.CustomEvent) { - e.stopPropagation() + onAuthorTap() { const post = this.data.post as Post if (!post?.authorId) return wx.navigateTo({ url: `/pages/pet-detail/pet-detail?userId=${post.authorId}` }) @@ -50,8 +56,7 @@ Component({ }) }, - async onLike(e: WechatMiniprogram.CustomEvent) { - e.stopPropagation() + async onLike() { const post = this.data.post as Post if (!post) return const wasLiked = post.isLiked @@ -72,15 +77,13 @@ Component({ } }, - onComment(e: WechatMiniprogram.CustomEvent) { - e.stopPropagation() + onComment() { const post = this.data.post as Post if (!post) return wx.navigateTo({ url: `/pages/post-detail/post-detail?id=${post._id}&focus=comment` }) }, - onShare(e: WechatMiniprogram.CustomEvent) { - e.stopPropagation() + onShare() { wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] }) }, }, diff --git a/miniprogram/components/post-card/post-card.wxml b/miniprogram/components/post-card/post-card.wxml index e55e1f0..765776c 100644 --- a/miniprogram/components/post-card/post-card.wxml +++ b/miniprogram/components/post-card/post-card.wxml @@ -1,11 +1,11 @@ - - {{post.author.nickName[0] || '?'}} + + {{post.author && post.author.nickName ? post.author.nickName[0] : '?'}}