修改bug

This commit is contained in:
2026-06-18 15:38:28 +08:00
parent 0e43ccec72
commit 702b578e1e
57 changed files with 1378 additions and 66 deletions

View File

@@ -5,9 +5,12 @@
"functions": [
{ "name": "login", "runtime": "Nodejs16.13", "handler": "index.main", "timeout": 10, "memorySize": 128 },
{ "name": "feedList", "runtime": "Nodejs16.13", "handler": "index.main", "timeout": 10, "memorySize": 128 },
{ "name": "userPosts", "runtime": "Nodejs16.13", "handler": "index.main", "timeout": 10, "memorySize": 128 },
{ "name": "postDetail", "runtime": "Nodejs16.13", "handler": "index.main", "timeout": 10, "memorySize": 128 },
{ "name": "postCreate", "runtime": "Nodejs16.13", "handler": "index.main", "timeout": 10, "memorySize": 128 },
{ "name": "postLike", "runtime": "Nodejs16.13", "handler": "index.main", "timeout": 10, "memorySize": 128 },
{ "name": "postFavorite", "runtime": "Nodejs16.13", "handler": "index.main", "timeout": 10, "memorySize": 128 },
{ "name": "commentCreate", "runtime": "Nodejs16.13", "handler": "index.main", "timeout": 10, "memorySize": 128 },
{ "name": "draftSave", "runtime": "Nodejs16.13", "handler": "index.main", "timeout": 10, "memorySize": 128 },
{ "name": "draftGet", "runtime": "Nodejs16.13", "handler": "index.main", "timeout": 10, "memorySize": 128 },
{ "name": "nearbyPets", "runtime": "Nodejs16.13", "handler": "index.main", "timeout": 10, "memorySize": 128 },

View File

@@ -0,0 +1,91 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const _ = db.command
const COMMENTS_COLLECTION = 'comments'
function isCollectionMissing(error) {
const text = String(error?.errMsg || error?.message || error || '')
return (
text.includes('collection not exist') ||
text.includes('collection not exists') ||
text.includes('collection is not exists') ||
text.includes('DATABASE_COLLECTION_NOT_EXIST') ||
text.includes('-502005')
)
}
function isCollectionAlreadyExists(error) {
const text = String(error?.errMsg || error?.message || error || '')
return text.includes('collection already exists') || text.includes('DATABASE_COLLECTION_ALREADY_EXIST')
}
async function ensureCommentsCollection() {
if (typeof db.createCollection !== 'function') {
throw new Error('comments collection does not exist')
}
try {
await db.createCollection(COMMENTS_COLLECTION)
} catch (error) {
if (!isCollectionAlreadyExists(error)) throw error
}
}
async function addComment(data) {
try {
return await db.collection(COMMENTS_COLLECTION).add({ data })
} catch (error) {
if (!isCollectionMissing(error)) throw error
await ensureCommentsCollection()
return db.collection(COMMENTS_COLLECTION).add({ data })
}
}
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
if (!OPENID) throw new Error('no openid in wx context')
const postId = String(event.postId || '').trim()
const content = String(event.content || '').trim()
if (!postId) throw new Error('postId is required')
if (!content) throw new Error('content is required')
const [postRes, userRes] = await Promise.all([
db.collection('posts').doc(postId).get(),
db.collection('users').where({ openid: OPENID }).limit(1).get()
])
if (!postRes.data) throw new Error('post not found')
if (!userRes.data.length) throw new Error('user not found')
const now = Date.now()
const user = userRes.data[0]
const result = await addComment({
postId,
openid: OPENID,
authorId: user._id,
authorSnapshot: {
name: user.nickname || '',
avatarKey: user.avatarKey || 'gradient-avatar-1',
avatarUrl: user.avatarUrl || ''
},
content: content.slice(0, 500),
createdAt: now,
updatedAt: now
})
await db.collection('posts').doc(postId).update({
data: {
'counts.comments': _.inc(1),
updatedAt: now
}
})
const updatedPost = await db.collection('posts').doc(postId).get()
return {
commentId: result._id,
comments: updatedPost.data.counts?.comments || 0
}
}

View File

@@ -0,0 +1,8 @@
{
"name": "commentCreate",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -2,11 +2,31 @@ const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const DRAFTS_COLLECTION = 'drafts'
function isCollectionMissing(error) {
const text = String(error?.errMsg || error?.message || error || '')
return (
text.includes('collection not exist') ||
text.includes('collection not exists') ||
text.includes('collection is not exists') ||
text.includes('DATABASE_COLLECTION_NOT_EXIST') ||
text.includes('-502005')
)
}
exports.main = async () => {
const { OPENID } = cloud.getWXContext()
if (!OPENID) throw new Error('no openid in wx context')
const res = await db.collection('drafts').where({ openid: OPENID }).limit(1).get()
let res
try {
res = await db.collection(DRAFTS_COLLECTION).where({ openid: OPENID }).limit(1).get()
} catch (error) {
if (isCollectionMissing(error)) return { draft: null }
throw error
}
if (!res.data.length) return { draft: null }
const record = res.data[0]
return { draft: record.draft || null, updatedAt: record.updatedAt }

View File

@@ -2,23 +2,64 @@ const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const DRAFTS_COLLECTION = 'drafts'
function isCollectionMissing(error) {
const text = String(error?.errMsg || error?.message || error || '')
return (
text.includes('collection not exist') ||
text.includes('collection not exists') ||
text.includes('collection is not exists') ||
text.includes('DATABASE_COLLECTION_NOT_EXIST') ||
text.includes('-502005')
)
}
function isCollectionAlreadyExists(error) {
const text = String(error?.errMsg || error?.message || error || '')
return text.includes('collection already exists') || text.includes('DATABASE_COLLECTION_ALREADY_EXIST')
}
async function ensureDraftsCollection() {
if (typeof db.createCollection !== 'function') {
throw new Error('drafts collection does not exist')
}
try {
await db.createCollection(DRAFTS_COLLECTION)
} catch (error) {
if (!isCollectionAlreadyExists(error)) throw error
}
}
async function getExistingDraft(openid) {
try {
return await db.collection(DRAFTS_COLLECTION).where({ openid }).limit(1).get()
} catch (error) {
if (!isCollectionMissing(error)) throw error
await ensureDraftsCollection()
return { data: [] }
}
}
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
if (!OPENID) throw new Error('no openid in wx context')
const now = Date.now()
const data = {
openid: OPENID,
draft: event.draft || {},
updatedAt: now
}
const existed = await db.collection('drafts').where({ openid: OPENID }).limit(1).get()
const existed = await getExistingDraft(OPENID)
const collection = db.collection(DRAFTS_COLLECTION)
if (existed.data.length) {
await db.collection('drafts').doc(existed.data[0]._id).update({ data })
await collection.doc(existed.data[0]._id).update({ data })
return { draftId: existed.data[0]._id, updatedAt: now }
}
const result = await db.collection('drafts').add({ data })
const result = await collection.add({ data })
return { draftId: result._id, updatedAt: now }
}

View File

@@ -74,12 +74,25 @@ exports.main = async event => {
favSet = new Set(favRes.data.map(f => f.postId))
}
const list = res.data.map(post => ({
// Resolve authors' current profile (real avatar image) so feed avatars load
// and stay fresh, even for posts whose snapshot predates avatar upload.
const authorIds = [...new Set(res.data.map(p => p.authorId).filter(Boolean))]
const authorMap = new Map()
for (let i = 0; i < authorIds.length; i += 100) {
const chunk = authorIds.slice(i, i + 100)
const usersRes = await safeGet('users', { _id: _.in(chunk) }, 100)
usersRes.data.forEach(u => authorMap.set(u._id, u))
}
const list = res.data.map(post => {
const author = authorMap.get(post.authorId)
return {
_id: post._id,
author: {
id: post.authorId || '',
name: post.authorSnapshot?.name || '用户',
avatarKey: post.authorSnapshot?.avatarKey || 'gradient-avatar-1'
name: (author && author.nickname) || post.authorSnapshot?.name || '用户',
avatarKey: (author && author.avatarKey) || post.authorSnapshot?.avatarKey || 'gradient-avatar-1',
avatarUrl: (author && author.avatarUrl) || post.authorSnapshot?.avatarUrl || ''
},
pet: post.petSnapshot || { name: '', breed: '' },
body: post.content || '',
@@ -98,7 +111,8 @@ exports.main = async event => {
},
likedByMe: likedSet.has(post._id),
favoritedByMe: favSet.has(post._id)
}))
}
})
const last = res.data[res.data.length - 1]
return { list, nextCursor: last ? String(last.createdAt) : '' }

View File

@@ -2,6 +2,18 @@ const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const _ = db.command
async function fetchUsers(ids) {
if (!ids.length) return []
const out = []
for (let i = 0; i < ids.length; i += 100) {
const chunk = ids.slice(i, i + 100)
const res = await db.collection('users').where({ _id: _.in(chunk) }).limit(100).get()
out.push(...res.data)
}
return out
}
function hashId(id) {
let h = 5381
@@ -46,19 +58,48 @@ function distanceText(petLoc, userLoc) {
}
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
const filter = event.filters?.type || 'all'
const userLoc = event.location || null
const query = {}
if (filter === 'dog' || filter === 'cat') query.species = filter
if (filter === 'online') query.online = true
let meId = ''
let followingSet = new Set()
let followerSet = new Set()
if (OPENID) {
const meRes = await db.collection('users').where({ openid: OPENID }).limit(1).get()
const me = meRes.data[0]
if (me) {
meId = me._id
const [followingRes, followerRes] = await Promise.all([
db.collection('follows').where({ followerId: meId }).limit(500).get(),
db.collection('follows').where({ followeeId: meId }).limit(500).get()
])
followingSet = new Set(followingRes.data.map(f => f.followeeId))
followerSet = new Set(followerRes.data.map(f => f.followerId))
}
}
const res = await db.collection('pets').where(query).limit(50).get()
const ownerIds = [...new Set(res.data.map(pet => pet.ownerId).filter(Boolean))]
const owners = await fetchUsers(ownerIds)
const ownerMap = new Map(owners.map(owner => [owner._id, owner]))
const list = res.data.map(pet => {
const realCoord = pet.location && typeof pet.location.latitude === 'number' ? pet.location : null
const coord = realCoord || (userLoc ? syntheticCoord(pet._id, userLoc) : null)
const owner = ownerMap.get(pet.ownerId)
const isFollowing = followingSet.has(pet.ownerId)
const isFollowedBy = followerSet.has(pet.ownerId)
return {
...pet,
ownerName: owner?.nickname || '',
isMine: Boolean(meId && pet.ownerId === meId),
isFollowing,
isFriend: isFollowing && isFollowedBy,
latitude: coord ? coord.latitude : undefined,
longitude: coord ? coord.longitude : undefined,
distanceText: distanceText(coord, userLoc) || pet.distanceText || null,

View File

@@ -0,0 +1,112 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const _ = db.command
// Query a collection that may not exist yet (created lazily on first write).
async function safeGet(collection, where, limit = 200, orderBy) {
try {
let query = db.collection(collection).where(where)
if (orderBy) query = query.orderBy(orderBy.field, orderBy.direction)
return await query.limit(limit).get()
} catch (e) {
return { data: [] }
}
}
function formatTimeText(ts) {
const diff = Date.now() - ts
const mins = Math.floor(diff / 60000)
if (mins < 1) return '刚刚'
if (mins < 60) return `${mins} 分钟前`
const hours = Math.floor(mins / 60)
if (hours < 24) return `${hours} 小时前`
const days = Math.floor(hours / 24)
if (days === 1) return '昨天'
if (days < 7) return `${days} 天前`
const d = new Date(ts)
return `${d.getMonth() + 1}${d.getDate()}`
}
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
const postId = String(event.postId || '').trim()
if (!postId) throw new Error('postId is required')
const postRes = await db.collection('posts').doc(postId).get().catch(() => null)
if (!postRes || !postRes.data) return { post: null, comments: [] }
const post = postRes.data
// Resolve the author's current profile (fresh avatar), liked/favorited state.
let author = null
if (post.authorId) {
const authorRes = await safeGet('users', { _id: post.authorId }, 1)
author = authorRes.data[0] || null
}
let likedByMe = false
let favoritedByMe = false
if (OPENID) {
const [likedRes, favRes] = await Promise.all([
safeGet('postLikes', { openid: OPENID, postId }, 1),
safeGet('postFavorites', { openid: OPENID, postId }, 1)
])
likedByMe = likedRes.data.length > 0
favoritedByMe = favRes.data.length > 0
}
const mappedPost = {
_id: post._id,
author: {
id: post.authorId || '',
name: (author && author.nickname) || post.authorSnapshot?.name || '用户',
avatarKey: (author && author.avatarKey) || post.authorSnapshot?.avatarKey || 'gradient-avatar-1',
avatarUrl: (author && author.avatarUrl) || post.authorSnapshot?.avatarUrl || ''
},
pet: post.petSnapshot || { name: '', breed: '' },
body: post.content || '',
topics: post.topics || [],
media: Array.isArray(post.media)
? post.media.map(m => (typeof m === 'string' ? m : m && (m.url || m.fileId))).filter(Boolean)
: [],
mediaTone: post.mediaTone || 'pet-1',
sticker: post.sticker || undefined,
locationText: post.locationText || '',
timeText: formatTimeText(post.createdAt || Date.now()),
counts: {
likes: post.counts?.likes || 0,
comments: post.counts?.comments || 0,
favorites: post.counts?.favorites || 0
},
likedByMe,
favoritedByMe
}
// Comments, oldest first. Resolve each author's current profile for avatars.
const commentsRes = await safeGet('comments', { postId }, 200, { field: 'createdAt', direction: 'asc' })
const commentAuthorIds = [...new Set(commentsRes.data.map(c => c.authorId).filter(Boolean))]
const authorMap = new Map()
for (let i = 0; i < commentAuthorIds.length; i += 100) {
const chunk = commentAuthorIds.slice(i, i + 100)
const usersRes = await safeGet('users', { _id: _.in(chunk) }, 100)
usersRes.data.forEach(u => authorMap.set(u._id, u))
}
const comments = commentsRes.data.map(c => {
const u = authorMap.get(c.authorId)
return {
_id: c._id,
author: {
id: c.authorId || '',
name: (u && u.nickname) || c.authorSnapshot?.name || '用户',
avatarKey: (u && u.avatarKey) || c.authorSnapshot?.avatarKey || 'gradient-avatar-1',
avatarUrl: (u && u.avatarUrl) || c.authorSnapshot?.avatarUrl || ''
},
content: c.content || '',
timeText: formatTimeText(c.createdAt || Date.now())
}
})
return { post: mappedPost, comments }
}

View File

@@ -0,0 +1,8 @@
{
"name": "postDetail",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

View File

@@ -3,22 +3,63 @@ const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const _ = db.command
const POST_LIKES_COLLECTION = 'postLikes'
function isCollectionMissing(error) {
const text = String(error?.errMsg || error?.message || error || '')
return (
text.includes('collection not exist') ||
text.includes('collection not exists') ||
text.includes('collection is not exists') ||
text.includes('DATABASE_COLLECTION_NOT_EXIST') ||
text.includes('-502005')
)
}
function isCollectionAlreadyExists(error) {
const text = String(error?.errMsg || error?.message || error || '')
return text.includes('collection already exists') || text.includes('DATABASE_COLLECTION_ALREADY_EXIST')
}
async function ensurePostLikesCollection() {
if (typeof db.createCollection !== 'function') {
throw new Error('postLikes collection does not exist')
}
try {
await db.createCollection(POST_LIKES_COLLECTION)
} catch (error) {
if (!isCollectionAlreadyExists(error)) throw error
}
}
async function getExistingLike(postId, openid) {
try {
return await db.collection(POST_LIKES_COLLECTION).where({ postId, openid }).limit(1).get()
} catch (error) {
if (!isCollectionMissing(error)) throw error
await ensurePostLikesCollection()
return { data: [] }
}
}
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
const { postId, liked } = event
if (!OPENID) throw new Error('no openid in wx context')
if (!postId) throw new Error('postId is required')
const existed = await db.collection('postLikes').where({ postId, openid: OPENID }).limit(1).get()
const existed = await getExistingLike(postId, OPENID)
const collection = db.collection(POST_LIKES_COLLECTION)
let delta = 0
if (liked && !existed.data.length) {
await db.collection('postLikes').add({ data: { postId, openid: OPENID, createdAt: Date.now() } })
await collection.add({ data: { postId, openid: OPENID, createdAt: Date.now() } })
delta = 1
}
if (!liked && existed.data.length) {
await db.collection('postLikes').doc(existed.data[0]._id).remove()
await collection.doc(existed.data[0]._id).remove()
delta = -1
}
@@ -29,4 +70,3 @@ exports.main = async event => {
const post = await db.collection('posts').doc(postId).get()
return { liked: Boolean(liked), likes: post.data.counts?.likes || 0 }
}

View File

@@ -0,0 +1,90 @@
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
const db = cloud.database()
const _ = db.command
async function safeGet(collection, where, limit = 200) {
try {
return await db.collection(collection).where(where).limit(limit).get()
} catch (e) {
return { data: [] }
}
}
function formatTimeText(ts) {
const diff = Date.now() - ts
const mins = Math.floor(diff / 60000)
if (mins < 1) return '刚刚'
if (mins < 60) return `${mins} 分钟前`
const hours = Math.floor(mins / 60)
if (hours < 24) return `${hours} 小时前`
const days = Math.floor(hours / 24)
if (days === 1) return '昨天'
if (days < 7) return `${days} 天前`
const d = new Date(ts)
return `${d.getMonth() + 1}${d.getDate()}`
}
// List a user's own posts. Defaults to the caller; pass authorId to view
// another user (only public posts are returned for others).
exports.main = async event => {
const { OPENID } = cloud.getWXContext()
if (!OPENID) throw new Error('no openid in wx context')
let authorOpenid = OPENID
let viewingSelf = true
if (event.authorId) {
const u = await db.collection('users').doc(event.authorId).get().catch(() => null)
if (!u || !u.data) return { list: [] }
authorOpenid = u.data.openid
viewingSelf = authorOpenid === OPENID
}
const where = { authorOpenid }
if (!viewingSelf) where.visibility = 'public'
const res = await db.collection('posts')
.where(where)
.orderBy('createdAt', 'desc')
.limit(50)
.get()
if (!res.data.length) return { list: [] }
const postIds = res.data.map(p => p._id)
const [likedRes, favRes] = await Promise.all([
safeGet('postLikes', { openid: OPENID, postId: _.in(postIds) }),
safeGet('postFavorites', { openid: OPENID, postId: _.in(postIds) })
])
const likedSet = new Set(likedRes.data.map(l => l.postId))
const favSet = new Set(favRes.data.map(f => f.postId))
const list = res.data.map(post => ({
_id: post._id,
author: {
id: post.authorId || '',
name: post.authorSnapshot?.name || '用户',
avatarKey: post.authorSnapshot?.avatarKey || 'gradient-avatar-1'
},
pet: post.petSnapshot || { name: '', breed: '' },
body: post.content || '',
topics: post.topics || [],
media: Array.isArray(post.media)
? post.media.map(m => (typeof m === 'string' ? m : m && (m.url || m.fileId))).filter(Boolean)
: [],
mediaTone: post.mediaTone || 'pet-1',
sticker: post.sticker || undefined,
locationText: post.locationText || '',
timeText: formatTimeText(post.createdAt || Date.now()),
counts: {
likes: post.counts?.likes || 0,
comments: post.counts?.comments || 0,
favorites: post.counts?.favorites || 0
},
likedByMe: likedSet.has(post._id),
favoritedByMe: favSet.has(post._id)
}))
return { list }
}

View File

@@ -0,0 +1,8 @@
{
"name": "userPosts",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"wx-server-sdk": "latest"
}
}

2
dist/app.js vendored

File diff suppressed because one or more lines are too long

2
dist/app.json vendored
View File

@@ -1 +1 @@
{"pages":["pages/plaza/index","pages/nearby/index","pages/publish/index","pages/messages/index","pages/profile/index","pages/profile-edit/index","pages/pet-edit/index","pages/chat/index","pages/contacts/index"],"window":{"navigationStyle":"custom","backgroundColor":"#14102b","backgroundTextStyle":"dark"},"tabBar":{"custom":true,"color":"#8a7aab","selectedColor":"#d8b4ff","backgroundColor":"#14102b","borderStyle":"black","list":[{"pagePath":"pages/plaza/index","text":"广场"},{"pagePath":"pages/nearby/index","text":"附近"},{"pagePath":"pages/messages/index","text":"消息"},{"pagePath":"pages/profile/index","text":"我的"}]},"permission":{"scope.userLocation":{"desc":"用于发现附近的毛孩子,以及在发布动态时标记位置"}},"requiredPrivateInfos":["chooseLocation","getLocation"],"lazyCodeLoading":"requiredComponents"}
{"pages":["pages/plaza/index","pages/nearby/index","pages/publish/index","pages/messages/index","pages/profile/index","pages/profile-edit/index","pages/pet-edit/index","pages/chat/index","pages/contacts/index","pages/my-posts/index","pages/post-detail/index"],"window":{"navigationStyle":"custom","backgroundColor":"#14102b","backgroundTextStyle":"dark"},"tabBar":{"custom":true,"color":"#8a7aab","selectedColor":"#d8b4ff","backgroundColor":"#14102b","borderStyle":"black","list":[{"pagePath":"pages/plaza/index","text":"广场"},{"pagePath":"pages/nearby/index","text":"附近"},{"pagePath":"pages/messages/index","text":"汪友"},{"pagePath":"pages/profile/index","text":"我的"}]},"permission":{"scope.userLocation":{"desc":"用于发现附近的毛孩子,以及在发布动态时标记位置"}},"requiredPrivateInfos":["chooseLocation","getLocation"],"lazyCodeLoading":"requiredComponents"}

2
dist/common.js vendored

File diff suppressed because one or more lines are too long

2
dist/common.wxss vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
"use strict";(wx["webpackJsonp"]=wx["webpackJsonp"]||[]).push([[781],{6430:function(e,s,a){var n=a(8870),t=a(1212),r=a(9379),i=a(467),c=a(5544),o=a(6540),l=a(758),u=a.n(l),m=a(118),v=a(7797),d=a(8324),_=a(6503),x=a(7377),h=a(326),p=a(7745),j=a(3401),f=a(4848);function g(e){var s=e.users,a=e.onSelect,n=e.onDiscover;return(0,f.jsx)(m.BM,{scrollX:!0,className:"active-user-row",showScrollbar:!1,children:(0,f.jsxs)(m.Ss,{className:"active-user-row__inner",children:[(0,f.jsxs)(m.Ss,{className:"active-user-row__item",onClick:n,children:[(0,f.jsx)(m.Ss,{className:"active-user-row__avatar-wrap active-user-row__add",children:(0,f.jsx)(j.A,{name:"plus",size:22})}),(0,f.jsx)(m.EY,{className:"active-user-row__name",children:"\u53d1\u73b0"})]}),s.map(function(e){return(0,f.jsxs)(m.Ss,{className:"active-user-row__item",onClick:function(){return null===a||void 0===a?void 0:a(e)},children:[(0,f.jsxs)(m.Ss,{className:"active-user-row__avatar-wrap",children:[(0,f.jsx)(p.A,{avatarKey:e.avatarKey,avatarUrl:e.avatarUrl,size:"lg",label:e.name}),e.live?(0,f.jsx)(m.EY,{className:"active-user-row__live",children:"LIVE"}):null]}),(0,f.jsx)(m.EY,{className:"active-user-row__name",children:e.name})]},e.id)})]})})}var w=g;function N(e){var s=e.conversation,a=e.onClick;return(0,f.jsxs)(m.Ss,{className:"conversation-item",onClick:a,children:[(0,f.jsx)(p.A,{avatarKey:s.avatarKey,avatarUrl:s.avatarUrl,size:"lg",online:s.online,petTag:"single"===s.type?"\ud83d\udc3e":void 0}),(0,f.jsxs)(m.Ss,{className:"conversation-item__main",children:[(0,f.jsxs)(m.Ss,{className:"conversation-item__top",children:[(0,f.jsxs)(m.Ss,{className:"conversation-item__name-row",children:[(0,f.jsx)(m.EY,{className:"conversation-item__name",children:s.title}),s.petName?(0,f.jsx)(m.EY,{className:"conversation-item__pet",children:s.petName}):null]}),(0,f.jsx)(m.EY,{className:"conversation-item__time",children:s.timeText})]}),(0,f.jsxs)(m.Ss,{className:"conversation-item__bottom",children:[(0,f.jsx)(m.EY,{className:"conversation-item__preview",children:s.preview}),s.pinned?(0,f.jsx)(m.Ss,{className:"conversation-item__pin",children:(0,f.jsx)(j.A,{name:"bookmark",size:12})}):s.unreadCount>0?(0,f.jsx)(m.EY,{className:"conversation-item__badge",children:s.unreadCount}):s.muted?(0,f.jsx)(m.Ss,{className:"conversation-item__muted"}):null]})]})]})}var S=N,A=a(9672),b=[{key:"all",label:"\u5168\u90e8"},{key:"unread",label:"\u672a\u8bfb"}];function C(){var e=(0,o.useState)("all"),s=(0,c.A)(e,2),a=s[0],n=s[1],p=(0,o.useState)([]),j=(0,c.A)(p,2),g=j[0],N=j[1],C=(0,o.useState)([]),E=(0,c.A)(C,2),k=E[0],y=E[1],Y=(0,o.useState)(""),U=(0,c.A)(Y,2),T=U[0],I=U[1],L=(0,o.useRef)(!0),z=function(){(0,A.VL)(a).then(function(e){N(e.activeUsers),y(e.conversations)})};(0,o.useEffect)(function(){z()},[a]),(0,l.useDidShow)(function(){L.current?L.current=!1:z()});var K=function(){var e=(0,i.A)((0,t.A)().m(function e(s){return(0,t.A)().w(function(e){while(1)switch(e.n){case 0:y(function(e){return e.map(function(e){return e._id===s._id?(0,r.A)((0,r.A)({},e),{},{unreadCount:0}):e})}),(0,A.vs)(s._id),u().navigateTo({url:"/pages/chat/index?conversationId=".concat(s._id,"&title=").concat(encodeURIComponent(s.title))});case 1:return e.a(2)}},e)}));return function(s){return e.apply(this,arguments)}}(),D=function(e){u().navigateTo({url:"/pages/chat/index?targetUserId=".concat(e.id,"&title=").concat(encodeURIComponent(e.name))})},R=function(){return u().navigateTo({url:"/pages/contacts/index"})},B=T.trim().toLowerCase(),J=B?k.filter(function(e){return e.title.toLowerCase().includes(B)||(e.preview||"").toLowerCase().includes(B)}):k;return(0,f.jsxs)(d.A,{className:"messages-page",children:[(0,f.jsx)(v.A,{title:"\u6d88\u606f"}),(0,f.jsx)(x.A,{placeholder:"\u641c\u7d22\u804a\u5929\u6216\u901a\u77e5",value:T,onInput:I}),(0,f.jsx)(h.A,{items:b,value:a,onChange:n}),B?null:(0,f.jsx)(w,{users:g,onSelect:D,onDiscover:R}),(0,f.jsx)(m.Ss,{className:"messages-page__divider",children:(0,f.jsx)(m.EY,{children:B?"\u641c\u7d22\u7ed3\u679c":"\u6700\u8fd1\u804a\u5929"})}),(0,f.jsxs)(m.Ss,{className:"messages-page__list",children:[J.map(function(e){return(0,f.jsx)(S,{conversation:e,onClick:function(){return K(e)}},e._id)}),0===J.length?(0,f.jsx)(m.EY,{className:"messages-page__empty",children:B?"\u6ca1\u6709\u5339\u914d\u7684\u804a\u5929":"unread"===a?"\u6ca1\u6709\u672a\u8bfb\u6d88\u606f":"\u8fd8\u6ca1\u6709\u804a\u5929\uff0c\u53bb\u300c\u53d1\u73b0\u300d\u627e\u6c6a\u53cb\u804a\u804a\u5427"}):null]}),(0,f.jsx)(_.A,{active:"messages"})]})}var E={navigationBarTitleText:"\u6d88\u606f",navigationStyle:"custom",disableScroll:!0},k=(0,n.eU)(C,"pages/messages/index",{root:{cn:[]}},E||{});C&&C.behaviors&&(k.behaviors=(k.behaviors||[]).concat(C.behaviors));Page(k)}},function(e){var s=function(s){return e(e.s=s)};e.O(0,[907,96,76],function(){return s(6430)});e.O()}]);
"use strict";(wx["webpackJsonp"]=wx["webpackJsonp"]||[]).push([[781],{6430:function(e,s,a){var n=a(8870),t=a(1212),r=a(9379),i=a(467),c=a(5544),o=a(6540),l=a(758),u=a.n(l),m=a(118),v=a(7797),d=a(8324),_=a(6503),x=a(7377),h=a(326),p=a(7745),j=a(3401),f=a(4848);function g(e){var s=e.users,a=e.onSelect,n=e.onDiscover;return(0,f.jsx)(m.BM,{scrollX:!0,className:"active-user-row",showScrollbar:!1,children:(0,f.jsxs)(m.Ss,{className:"active-user-row__inner",children:[(0,f.jsxs)(m.Ss,{className:"active-user-row__item",onClick:n,children:[(0,f.jsx)(m.Ss,{className:"active-user-row__avatar-wrap active-user-row__add",children:(0,f.jsx)(j.A,{name:"plus",size:22})}),(0,f.jsx)(m.EY,{className:"active-user-row__name",children:"\u627e\u6c6a\u53cb"})]}),s.map(function(e){return(0,f.jsxs)(m.Ss,{className:"active-user-row__item",onClick:function(){return null===a||void 0===a?void 0:a(e)},children:[(0,f.jsxs)(m.Ss,{className:"active-user-row__avatar-wrap",children:[(0,f.jsx)(p.A,{avatarKey:e.avatarKey,avatarUrl:e.avatarUrl,size:"lg",label:e.name}),e.live?(0,f.jsx)(m.EY,{className:"active-user-row__live",children:"LIVE"}):null]}),(0,f.jsx)(m.EY,{className:"active-user-row__name",children:e.name})]},e.id)})]})})}var w=g;function N(e){var s=e.conversation,a=e.onClick;return(0,f.jsxs)(m.Ss,{className:"conversation-item",onClick:a,children:[(0,f.jsx)(p.A,{avatarKey:s.avatarKey,avatarUrl:s.avatarUrl,size:"lg",online:s.online,petTag:"single"===s.type?"\ud83d\udc3e":void 0}),(0,f.jsxs)(m.Ss,{className:"conversation-item__main",children:[(0,f.jsxs)(m.Ss,{className:"conversation-item__top",children:[(0,f.jsxs)(m.Ss,{className:"conversation-item__name-row",children:[(0,f.jsx)(m.EY,{className:"conversation-item__name",children:s.title}),s.petName?(0,f.jsx)(m.EY,{className:"conversation-item__pet",children:s.petName}):null]}),(0,f.jsx)(m.EY,{className:"conversation-item__time",children:s.timeText})]}),(0,f.jsxs)(m.Ss,{className:"conversation-item__bottom",children:[(0,f.jsx)(m.EY,{className:"conversation-item__preview",children:s.preview}),s.pinned?(0,f.jsx)(m.Ss,{className:"conversation-item__pin",children:(0,f.jsx)(j.A,{name:"bookmark",size:12})}):s.unreadCount>0?(0,f.jsx)(m.EY,{className:"conversation-item__badge",children:s.unreadCount}):s.muted?(0,f.jsx)(m.Ss,{className:"conversation-item__muted"}):null]})]})]})}var S=N,A=a(9672),b=[{key:"all",label:"\u5168\u90e8"},{key:"unread",label:"\u672a\u8bfb"}];function C(){var e=(0,o.useState)("all"),s=(0,c.A)(e,2),a=s[0],n=s[1],p=(0,o.useState)([]),j=(0,c.A)(p,2),g=j[0],N=j[1],C=(0,o.useState)([]),E=(0,c.A)(C,2),k=E[0],y=E[1],Y=(0,o.useState)(""),U=(0,c.A)(Y,2),T=U[0],I=U[1],L=(0,o.useRef)(!0),z=function(){(0,A.VL)(a).then(function(e){N(e.activeUsers),y(e.conversations)})};(0,o.useEffect)(function(){z()},[a]),(0,l.useDidShow)(function(){L.current?L.current=!1:z()});var K=function(){var e=(0,i.A)((0,t.A)().m(function e(s){return(0,t.A)().w(function(e){while(1)switch(e.n){case 0:y(function(e){return e.map(function(e){return e._id===s._id?(0,r.A)((0,r.A)({},e),{},{unreadCount:0}):e})}),(0,A.vs)(s._id),u().navigateTo({url:"/pages/chat/index?conversationId=".concat(s._id,"&title=").concat(encodeURIComponent(s.title))});case 1:return e.a(2)}},e)}));return function(s){return e.apply(this,arguments)}}(),D=function(e){u().navigateTo({url:"/pages/chat/index?targetUserId=".concat(e.id,"&title=").concat(encodeURIComponent(e.name))})},R=function(){return u().navigateTo({url:"/pages/contacts/index?tab=discover"})},B=T.trim().toLowerCase(),J=B?k.filter(function(e){return e.title.toLowerCase().includes(B)||(e.preview||"").toLowerCase().includes(B)}):k;return(0,f.jsxs)(d.A,{className:"messages-page",children:[(0,f.jsx)(v.A,{title:"\u6c6a\u53cb"}),(0,f.jsx)(x.A,{placeholder:"\u641c\u7d22\u6c6a\u53cb\u6216\u804a\u5929",value:T,onInput:I}),(0,f.jsx)(h.A,{items:b,value:a,onChange:n}),B?null:(0,f.jsx)(w,{users:g,onSelect:D,onDiscover:R}),(0,f.jsx)(m.Ss,{className:"messages-page__divider",children:(0,f.jsx)(m.EY,{children:B?"\u641c\u7d22\u7ed3\u679c":"\u6700\u8fd1\u804a\u5929"})}),(0,f.jsxs)(m.Ss,{className:"messages-page__list",children:[J.map(function(e){return(0,f.jsx)(S,{conversation:e,onClick:function(){return K(e)}},e._id)}),0===J.length?(0,f.jsx)(m.EY,{className:"messages-page__empty",children:B?"\u6ca1\u6709\u5339\u914d\u7684\u804a\u5929":"unread"===a?"\u6ca1\u6709\u672a\u8bfb\u6d88\u606f":"\u8fd8\u6ca1\u6709\u804a\u5929\uff0c\u53bb\u300c\u9644\u8fd1\u300d\u52a0\u6c6a\u53cb\u5427"}):null]}),(0,f.jsx)(_.A,{active:"messages"})]})}var E={navigationBarTitleText:"\u6c6a\u53cb",navigationStyle:"custom",disableScroll:!0},k=(0,n.eU)(C,"pages/messages/index",{root:{cn:[]}},E||{});C&&C.behaviors&&(k.behaviors=(k.behaviors||[]).concat(C.behaviors));Page(k)}},function(e){var s=function(s){return e(e.s=s)};e.O(0,[907,96,76],function(){return s(6430)});e.O()}]);

View File

@@ -1 +1 @@
{"navigationBarTitleText":"消息","navigationStyle":"custom","disableScroll":true,"usingComponents":{"comp":"../../comp"}}
{"navigationBarTitleText":"汪友","navigationStyle":"custom","disableScroll":true,"usingComponents":{"comp":"../../comp"}}

1
dist/pages/my-posts/index.js vendored Normal file
View File

@@ -0,0 +1 @@
"use strict";(wx["webpackJsonp"]=wx["webpackJsonp"]||[]).push([[493],{8868:function(s,n,e){var t=e(8870),a=e(1212),c=e(9379),i=e(467),r=e(5544),o=e(6540),l=e(758),u=e.n(l),p=e(118),m=e(374),h=e(3401),d=e(3972),f=e(7896),_=e(4149),v=e(994),y=e(4848);function x(){var s=(0,m.v)(),n=s.statusBarHeight,e=(0,o.useState)([]),t=(0,r.A)(e,2),x=t[0],j=t[1],k=(0,o.useState)(!0),b=(0,r.A)(k,2),A=b[0],w=b[1],N=(0,v.d)(480),S=function(){w(!0),(0,_.hv)().then(j).finally(function(){return w(!1)})};(0,o.useEffect)(function(){S()},[]),(0,l.useDidShow)(S);var g=function(){var s=(0,i.A)((0,a.A)().m(function s(n){var e,t;return(0,a.A)().w(function(s){while(1)switch(s.n){case 0:if(e=x.map(function(s){if(s._id!==n)return s;var e=!s.likedByMe;return e&&N.show(),(0,c.A)((0,c.A)({},s),{},{likedByMe:e,counts:(0,c.A)((0,c.A)({},s.counts),{},{likes:Math.max(0,s.counts.likes+(e?1:-1))})})}),j(e),t=e.find(function(s){return s._id===n}),!t){s.n=1;break}return s.n=1,(0,_.O$)(n,t.likedByMe);case 1:return s.a(2)}},s)}));return function(n){return s.apply(this,arguments)}}();return(0,y.jsxs)(p.Ss,{className:"my-posts",children:[(0,y.jsx)(p.Ss,{className:"my-posts__nav",style:{paddingTop:"".concat(n,"px")},children:(0,y.jsxs)(p.Ss,{className:"my-posts__nav-row",children:[(0,y.jsx)(p.Ss,{className:"my-posts__back",onClick:function(){return u().navigateBack()},children:(0,y.jsx)(h.A,{name:"chev",size:22,className:"my-posts__back-icon"})}),(0,y.jsx)(p.EY,{className:"my-posts__title",children:"\u6211\u7684\u52a8\u6001"}),(0,y.jsx)(p.Ss,{className:"my-posts__nav-spacer"})]})}),(0,y.jsx)(p.BM,{scrollY:!0,className:"my-posts__scroll",enhanced:!0,showScrollbar:!1,children:(0,y.jsxs)(p.Ss,{className:"my-posts__feed",children:[x.map(function(s){return(0,y.jsx)(d.A,{post:s,onLike:g,onOpen:function(s){return u().navigateTo({url:"/pages/post-detail/index?postId=".concat(s)})}},s._id)}),A&&0===x.length?(0,y.jsx)(p.EY,{className:"my-posts__empty",children:"\u52a0\u8f7d\u4e2d\u2026"}):A||0!==x.length?null:(0,y.jsx)(p.EY,{className:"my-posts__empty",children:"\u8fd8\u6ca1\u6709\u53d1\u5e03\u8fc7\u52a8\u6001\uff0c\u53bb\u5e7f\u573a\u70b9 + \u53d1\u5e03\u7b2c\u4e00\u6761\u5427"})]})}),(0,y.jsx)(f.A,{visible:N.visible})]})}var j={},k=(0,t.eU)(x,"pages/my-posts/index",{root:{cn:[]}},j||{});x&&x.behaviors&&(k.behaviors=(k.behaviors||[]).concat(x.behaviors));Page(k)}},function(s){var n=function(n){return s(s.s=n)};s.O(0,[907,96,76],function(){return n(8868)});s.O()}]);

1
dist/pages/my-posts/index.json vendored Normal file
View File

@@ -0,0 +1 @@
{"usingComponents":{"comp":"../../comp"}}

2
dist/pages/my-posts/index.wxml vendored Normal file
View File

@@ -0,0 +1,2 @@
<import src="../../base.wxml"/>
<template is="taro_tmpl" data="{{root:root}}" />

1
dist/pages/my-posts/index.wxss vendored Normal file
View File

@@ -0,0 +1 @@
.my-posts{background:#14102b;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;height:100vh}.my-posts__nav{-ms-flex-negative:0;background:#14102b;flex-shrink:0}.my-posts__nav-row{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;height:100rpx;padding:0 24rpx}.my-posts__back{display:-ms-flexbox;display:flex;height:72rpx;width:72rpx;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;color:#f0e9ff;justify-content:center}.my-posts__back-icon{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.my-posts__title{color:#f0e9ff;-ms-flex:1;flex:1;font:700 32rpx/1 Space Grotesk,Inter,system-ui,sans-serif;text-align:center}.my-posts__nav-spacer{width:72rpx}.my-posts__scroll{-ms-flex:1;flex:1;min-height:0}.my-posts__feed{padding:16rpx 32rpx 64rpx}.my-posts__empty{color:#8a7aab;display:block;font:400 26rpx/1.6 Inter,system-ui,-apple-system,PingFang SC,Microsoft YaHei,sans-serif;padding:96rpx 48rpx;text-align:center}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
dist/pages/post-detail/index.js vendored Normal file
View File

@@ -0,0 +1 @@
"use strict";(wx["webpackJsonp"]=wx["webpackJsonp"]||[]).push([[741],{8680:function(e,t,s){var a=s(8870),n=s(436),c=s(1212),i=s(9379),o=s(467),l=s(5544),r=s(6540),u=s(758),d=s.n(u),m=s(118),p=s(374),h=s(3401),_=s(7745),x=s(3972),v=s(7896),f=s(4149),A=s(994),j=s(4848);function N(){var e=(0,p.v)(),t=e.statusBarHeight,s=e.safeBottom,a=(0,u.useRouter)(),N=a.params.postId||"",k=(0,r.useState)(null),b=(0,l.A)(k,2),S=b[0],w=b[1],y=(0,r.useState)([]),g=(0,l.A)(y,2),B=g[0],E=g[1],Y=(0,r.useState)(!0),M=(0,l.A)(Y,2),T=M[0],C=M[1],I=(0,r.useState)(""),K=(0,l.A)(I,2),O=K[0],U=K[1],z=(0,r.useState)(!1),J=(0,l.A)(z,2),P=J[0],H=J[1],L=(0,A.d)(480),R=function(){N?(C(!0),(0,f.aC)(N).then(function(e){w(e.post),E(e.comments)}).finally(function(){return C(!1)})):C(!1)};(0,r.useEffect)(function(){R()},[N]);var $=function(){var e=(0,o.A)((0,c.A)().m(function e(t){var s,a,n;return(0,c.A)().w(function(e){while(1)switch(e.p=e.n){case 0:if(S){e.n=1;break}return e.a(2);case 1:return s=!S.likedByMe,s&&L.show(),a=(0,i.A)((0,i.A)({},S),{},{likedByMe:s,counts:(0,i.A)((0,i.A)({},S.counts),{},{likes:Math.max(0,S.counts.likes+(s?1:-1))})}),w(a),e.p=2,e.n=3,(0,f.O$)(t,s);case 3:n=e.v,w(function(e){return e?(0,i.A)((0,i.A)({},e),{},{likedByMe:n.liked,counts:(0,i.A)((0,i.A)({},e.counts),{},{likes:n.likes})}):e}),e.n=5;break;case 4:e.p=4,e.v,w(S),d().showToast({title:"\u70b9\u8d5e\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5",icon:"none"});case 5:return e.a(2)}},e,null,[[2,4]])}));return function(t){return e.apply(this,arguments)}}(),q=function(){var e=(0,o.A)((0,c.A)().m(function e(){var t,s;return(0,c.A)().w(function(e){while(1)switch(e.p=e.n){case 0:if(t=O.trim(),t&&!P&&S){e.n=1;break}return e.a(2);case 1:return H(!0),U(""),e.p=2,e.n=3,(0,f.SI)(S._id,t);case 3:s=e.v,E(function(e){return[].concat((0,n.A)(e),[{_id:s.commentId,author:{id:"me",name:"\u6211",avatarKey:"gradient-avatar-1"},content:t,timeText:"\u521a\u521a"}])}),w(function(e){return e?(0,i.A)((0,i.A)({},e),{},{counts:(0,i.A)((0,i.A)({},e.counts),{},{comments:s.comments})}):e}),e.n=5;break;case 4:e.p=4,e.v,d().showToast({title:"\u8bc4\u8bba\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5",icon:"none"}),U(t);case 5:return e.p=5,H(!1),e.f(5);case 6:return e.a(2)}},e,null,[[2,4,5,6]])}));return function(){return e.apply(this,arguments)}}();return(0,j.jsxs)(m.Ss,{className:"post-detail",children:[(0,j.jsx)(m.Ss,{className:"post-detail__nav",style:{paddingTop:"".concat(t,"px")},children:(0,j.jsxs)(m.Ss,{className:"post-detail__nav-row",children:[(0,j.jsx)(m.Ss,{className:"post-detail__back",onClick:function(){return d().navigateBack()},children:(0,j.jsx)(h.A,{name:"chev",size:22,className:"post-detail__back-icon"})}),(0,j.jsx)(m.EY,{className:"post-detail__title",children:"\u52a8\u6001\u8be6\u60c5"}),(0,j.jsx)(m.Ss,{className:"post-detail__nav-spacer"})]})}),(0,j.jsxs)(m.BM,{scrollY:!0,className:"post-detail__scroll",enhanced:!0,showScrollbar:!1,children:[S?(0,j.jsx)(m.Ss,{className:"post-detail__post",children:(0,j.jsx)(x.A,{post:S,onLike:$,enableImagePreview:!0})}):null,(0,j.jsxs)(m.Ss,{className:"post-detail__comments",children:[(0,j.jsxs)(m.EY,{className:"post-detail__comments-title",children:["\u8bc4\u8bba",S?" ".concat(S.counts.comments):""]}),T&&!S?(0,j.jsx)(m.EY,{className:"post-detail__empty",children:"\u52a0\u8f7d\u4e2d\u2026"}):0===B.length?(0,j.jsx)(m.EY,{className:"post-detail__empty",children:"\u8fd8\u6ca1\u6709\u8bc4\u8bba\uff0c\u6765\u62a2\u6c99\u53d1\u5427"}):B.map(function(e){return(0,j.jsxs)(m.Ss,{className:"post-detail__comment",children:[(0,j.jsx)(_.A,{avatarKey:e.author.avatarKey,avatarUrl:e.author.avatarUrl,size:"sm"}),(0,j.jsxs)(m.Ss,{className:"post-detail__comment-body",children:[(0,j.jsxs)(m.Ss,{className:"post-detail__comment-head",children:[(0,j.jsx)(m.EY,{className:"post-detail__comment-name",children:e.author.name}),(0,j.jsx)(m.EY,{className:"post-detail__comment-time",children:e.timeText})]}),(0,j.jsx)(m.EY,{className:"post-detail__comment-text",children:e.content})]})]},e._id)})]})]}),(0,j.jsxs)(m.Ss,{className:"post-detail__input-bar",style:{paddingBottom:"".concat(Math.max(12,s),"px")},children:[(0,j.jsx)(m.pd,{className:"post-detail__input",value:O,placeholder:"\u5199\u8bc4\u8bba\u2026",placeholderClass:"post-detail__input-ph",confirmType:"send",onInput:function(e){return U(e.detail.value)},onConfirm:q}),(0,j.jsx)(m.Ss,{className:"post-detail__send ".concat(O.trim()?"":"post-detail__send--disabled"),onClick:q,children:"\u53d1\u9001"})]}),(0,j.jsx)(v.A,{visible:L.visible})]})}var k={},b=(0,a.eU)(N,"pages/post-detail/index",{root:{cn:[]}},k||{});N&&N.behaviors&&(b.behaviors=(b.behaviors||[]).concat(N.behaviors));Page(b)}},function(e){var t=function(t){return e(e.s=t)};e.O(0,[907,96,76],function(){return t(8680)});e.O()}]);

1
dist/pages/post-detail/index.json vendored Normal file
View File

@@ -0,0 +1 @@
{"usingComponents":{"comp":"../../comp"}}

2
dist/pages/post-detail/index.wxml vendored Normal file
View File

@@ -0,0 +1,2 @@
<import src="../../base.wxml"/>
<template is="taro_tmpl" data="{{root:root}}" />

1
dist/pages/post-detail/index.wxss vendored Normal file
View File

@@ -0,0 +1 @@
.post-detail{background:#14102b;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;height:100vh}.post-detail__nav{-ms-flex-negative:0;background:#14102b;border-bottom:2rpx solid rgba(216,180,255,.06);flex-shrink:0}.post-detail__nav-row{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;height:100rpx;padding:0 24rpx}.post-detail__back{display:-ms-flexbox;display:flex;height:72rpx;width:72rpx;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;color:#f0e9ff;justify-content:center}.post-detail__back-icon{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.post-detail__title{color:#f0e9ff;-ms-flex:1;flex:1;font:700 32rpx/1 Space Grotesk,Inter,system-ui,sans-serif;text-align:center}.post-detail__nav-spacer{width:72rpx}.post-detail__scroll{-ms-flex:1;flex:1;min-height:0}.post-detail__post{padding-top:24rpx}.post-detail__comments{padding:8rpx 32rpx 48rpx}.post-detail__comments-title{color:#f0e9ff;display:block;font:700 28rpx/1 Space Grotesk,Inter,system-ui,sans-serif;margin:16rpx 0 24rpx}.post-detail__empty{color:#8a7aab;display:block;font:400 26rpx/1.6 Inter,system-ui,-apple-system,PingFang SC,Microsoft YaHei,sans-serif;padding:72rpx 48rpx;text-align:center}.post-detail__comment{border-top:2rpx solid rgba(216,180,255,.06);display:-ms-flexbox;display:flex;gap:20rpx;padding:24rpx 0}.post-detail__comment-body{-ms-flex:1;flex:1;min-width:0}.post-detail__comment-head{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;gap:16rpx}.post-detail__comment-name{color:#f0e9ff;font:600 26rpx/1.2 Inter,system-ui,-apple-system,PingFang SC,Microsoft YaHei,sans-serif}.post-detail__comment-time{color:#8a7aab;font:400 22rpx/1 Inter,system-ui,-apple-system,PingFang SC,Microsoft YaHei,sans-serif}.post-detail__comment-text{color:#f0e9ff;display:block;font:400 28rpx/1.55 Inter,system-ui,-apple-system,PingFang SC,Microsoft YaHei,sans-serif;margin-top:12rpx;word-break:break-word}.post-detail__input-bar{-ms-flex-negative:0;display:-ms-flexbox;display:flex;flex-shrink:0;-ms-flex-align:center;align-items:center;background:#14102b;border-top:2rpx solid rgba(216,180,255,.08);gap:20rpx;padding:20rpx 32rpx 24rpx}.post-detail__input{background:rgba(31,23,66,.68);border:2rpx solid rgba(216,180,255,.1);border-radius:19998rpx;-webkit-box-sizing:border-box;box-sizing:border-box;color:#f0e9ff;-ms-flex:1;flex:1;font:400 28rpx/80rpx Inter,system-ui,-apple-system,PingFang SC,Microsoft YaHei,sans-serif;height:80rpx;padding:0 28rpx}.post-detail__input-ph{color:#8a7aab}.post-detail__send{-ms-flex-negative:0;background:linear-gradient(135deg,#d8b4ff,#ffb3c8);border-radius:19998rpx;color:#2a1d10;flex-shrink:0;font:700 28rpx/80rpx Inter,system-ui,-apple-system,PingFang SC,Microsoft YaHei,sans-serif;height:80rpx;padding:0 36rpx}.post-detail__send--disabled{opacity:.5}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,171 @@
# 汪圈小程序 — 功能逻辑缺陷审查报告
> 审查日期2026-06-18
> 审查范围:全部 10 个页面、10 个 Service、23 个云函数、3 个类型文件、关键组件
> 审查分支:`master`
---
## 一、广场页 `pages/plaza/index.tsx`
| # | 缺陷 | 严重程度 | 位置 | 说明 |
|---|------|----------|------|------|
| 1 | **Feed 无分页 / 无限滚动** | 🔴 高 | `loadFeed()` (L34-38) | `getFeedList` 云函数返回 `CursorResponse<T>``nextCursor` 分页字段但页面完全没有使用分页机制。只加载第一页数据用户无法浏览更多帖子。Feed 是社交应用的核心功能,此为致命缺陷。 |
| 2 | **话题列表硬编码** | 🟡 中 | `topics` (L24) | `['全部', '滨江公园', '金毛日常', ...]` 写死在组件中,不会从云端动态获取热门话题,运营新增的话题无法展示。 |
| 3 | **广告卡片位置固定** | 🟡 中 | `PostCard` 渲染逻辑 (L130-133) | `FeedAdCard` 写死在 `index === 0` 的位置。每次 `loadFeed` 刷新后数据重置,广告位置永远在第一条帖子后,缺乏真实广告系统的随机插入和频次控制。 |
| 4 | **收藏功能存在但入口缺失** | 🟡 中 | `PostCard` + `PlazaPage` | `feed.service.ts` 导出了 `setPostLike` / `setPostFavorite``Post` 类型有 `favoritedByMe` 字段,但 `PostCard``PlazaPage` 均没有收藏操作的 UI 入口,收藏功能全链路断裂。 |
| 5 | **没有帖子详情页** | 🟡 中 | `commentPost()` (L86-113) | 评论通过 `Taro.showModal` 弹窗实现,无法查看评论列表、历史记录,也无法点击图片查看大图预览。社交类帖子的完整交互(评论链、转发、分享)均缺失。 |
| 6 | **点赞乐观更新存在竞态** | 🟡 中 | `likePost()` (L54-84) | 连续两次 `setPosts`(乐观更新 → 服务端返回覆盖),期间若 `useDidShow` 触发 `loadFeed`,可能覆盖掉刚点赞的中间状态。应考虑用 `postId` 粒度的状态管理或防抖。 |
| 7 | **搜索栏无功能** | 🟡 中 | `<SearchBar>` (L118) | `SearchBar` 组件已渲染但没有绑定 `onInput` / `onSubmit` 回调,搜索框为纯装饰。 |
---
## 二、附近页 `pages/nearby/index.tsx`
| # | 缺陷 | 严重程度 | 位置 | 说明 |
|---|------|----------|------|------|
| 1 | **没有调用 `updateLocation` 上报自身位置** | 🔴 高 | 整个页面 | `nearby.service.ts` 导出了 `updateLocation(location, visible)`,用户位置变化**从未上报云端**。其他用户看不到"我"在附近地图上出现,双向发现功能完全失效。应在获取到位置后调用上报,并在 relocate 时重复上报。 |
| 2 | **没有尊重 `preferences.nearbyVisible` 偏好** | 🔴 高 | 整个页面 | 用户在个人资料中可关闭"附近可见"(`preferences.nearbyVisible`),但附近页完全不考虑此设置。即使关闭,仍请求并展示附近宠物,自身位置也没有根据偏好控制是否上报。应在 `useEffect` 中读取偏好,决定是否请求附近数据。 |
| 3 | **`likePet` 没有乐观更新和错误回滚** | 🟡 中 | `likePet()` (L89-92) | 只调用了 `likeNearbyPet(id)`,没有先更新 UI 状态(不像 `PlazaPage.likePost` 做了乐观更新 + 回滚)。网络慢时用户看不到反馈,快速点击可能重复请求。 |
| 4 | **`filter: 'online'` 后端过滤能力不确定** | 🟡 中 | `loadPets()` (L41-46) | 前端传 `{ type: 'online' }`,但 `nearbyPets` 云函数是否支持此过滤逻辑取决于后端实现。如果后端不识别此 filter返回的数据未做二次前端过滤展示可能不准确。 |
| 5 | **搜索仅前端过滤** | 🟡 中 | `visiblePets` (L139-142) | 只按 `name``breed` 过滤,不支持按距离排序或更丰富的筛选组合。筛选器仅有 all/dog/cat/online 四种,缺少按距离、年龄段、标签等维度。 |
---
## 三、发布页 `pages/publish/index.tsx`
| # | 缺陷 | 严重程度 | 位置 | 说明 |
|---|------|----------|------|------|
| 1 | **没有恢复草稿的逻辑** | 🔴 高 | `useEffect` (L44-50) | `publish.service.ts` 导出了 `getDraft()`,但发布页 `mount` 时**从未调用它**。用户保存的草稿在下次进入发布页时不会被恢复。应添加逻辑:进入时检测未提交内容 → 提示恢复草稿 → 调用 `getDraft()` 回填表单。 |
| 2 | **没有内容字数上限预警** | 🟡 中 | `ComposerToolbar` `count` prop | `isValidPostContent` 限制 12000 字,但 `content.length` 仅展示数字,没有在接近上限(如 >1800 字)时给出警告样式或禁用输入。 |
| 3 | **图片上传失败静默跳过** | 🟡 中 | `uploadPostMedia()` (`publish.service.ts` L24-38) | 单张图片上传失败时 `catch` 返回原 `item`,用户不知道哪张图没上传成功。帖子可能携带本地临时路径 `wxfile://...` 发布,其他用户无法查看。应在 UI 上展示上传失败提示。 |
| 4 | **返回没有未保存提示** | 🟡 中 | 关闭按钮 (L148-150) | 点击 `navigateBack()` 直接返回,如果用户已输入内容但未发布,不会提醒"是否保存为草稿"。应检测 `hasContent`,有内容时弹窗确认。 |
---
## 四、消息页 `pages/messages/index.tsx`
| # | 缺陷 | 严重程度 | 位置 | 说明 |
|---|------|----------|------|------|
| 1 | **`markConversationRead` 是 fire-and-forget** | 🟡 中 | `openConversation()` (L48-54) | 调用了 `markConversationRead` 但没有 `await`,且 `openConversation``async` 函数却没有 `try-catch`。如果 `navigateTo` 失败(如页面栈满),异常不会被捕获。 |
| 2 | **没有区分 single / system 会话交互** | 🟡 中 | `openConversation()` (L48-54) | `Conversation``type: 'single' \| 'system'`,系统消息(如"汪圈小助手")不应有"发起聊天"行为,但 UI 上没有区分处理——点击系统消息也会跳转到 chat 页面。 |
| 3 | **没有消息通知 / 推送机制** | 🟡 中 | 整个页面 | 没有接入微信订阅消息或任何推送通知机制。`preferences.notifications` 偏好存在但无实际功能接入,用户即使打开通知设置也不会收到新消息提醒。 |
| 4 | **搜索不会过滤 activeUsers** | 🟡 中 | `kw` 过滤逻辑 (L64-69) | 搜索只过滤 `conversations`,不搜索 `activeUsers`。有搜索关键词时 `activeUsers` 被隐藏(`kw ? null : <ActiveUserRow>`),但清空搜索后立即恢复,交互上不连贯。 |
---
## 五、聊天页 `pages/chat/index.tsx`
| # | 缺陷 | 严重程度 | 位置 | 说明 |
|---|------|----------|------|------|
| 1 | **消息列表没有分页加载历史** | 🔴 高 | `load()` (L26-35) | 只调用一次 `getThread()` 获取当前消息,没有向上滚动加载更早消息的机制。长时间对话的历史消息无法查看。应实现 `ScrollView` 到顶触发加载上一页。 |
| 2 | **轮询频率固定 5 秒,无智能调节** | 🟡 中 | `useDidShow` (L43-45) | 不管页面是否在前台活跃、消息密度如何,都固定 5 秒轮询一次。高频轮询消耗资源,低频则延迟高。对于聊天场景,建议使用 WebSocket 或根据消息活跃度动态调整轮询间隔。 |
| 3 | **发送失败时输入框闪空** | 🟡 中 | `send()` (L53-71) | 发送时先 `setDraft('')` 清空输入框,失败后 `setDraft(text)` 恢复。在发送到失败的间隙,输入框会先空再恢复,视觉闪烁。建议先不清空,成功后再清空。 |
| 4 | **没有消息发送状态指示** | 🟡 中 | 聊天气泡 UI (L92-103) | `sending` 状态变量存在但没有用在 UI 上。用户发送消息后没有 loading / 发送中指示器,不知道消息是否正在发送。 |
| 5 | **`scrollTop` 递增 hack 不可靠** | 🟡 中 | `setScrollTop(prev => prev + 100000)` (L34) | 通过不断递增 `scrollTop` 强制滚动到底部。`Number.MAX_SAFE_INTEGER``9007199254740991`,每次 +100000 约可调用 9×10¹⁰ 次才溢出——虽然实际不太可能触发,但设计上应改用 ref 记录并 toggle 一个标志位。 |
| 6 | **不支持图片 / 表情消息** | 🟡 中 | 输入栏 UI (L108-119) | 只支持纯文本输入,没有图片、表情等富媒体消息功能。`ChatMessage.type` 字段存在但没有被使用,输入区域也没有附件按钮。 |
---
## 六、个人资料页 `pages/profile/index.tsx`
| # | 缺陷 | 严重程度 | 位置 | 说明 |
|---|------|----------|------|------|
| 1 | **偏好 `changePreference` 无错误回滚** | 🟡 中 | `changePreference()` (L92-102) | 先乐观更新 UI 再 `await updateProfile`,但整个函数没有 `try-catch`。如果云端更新失败UI 状态已改但数据未持久化,下次加载会回滚,造成用户困惑(以为保存了但实际没有)。 |
| 2 | **"关于"功能是空壳** | 🟢 低 | `handleAction('about')` (L89) | 只弹 `toast: '敬请期待新功能'`。MVP 阶段可接受,但应在版本规划中补充。 |
---
## 七、资料编辑页 `pages/profile-edit/index.tsx`
| # | 缺陷 | 严重程度 | 位置 | 说明 |
|---|------|----------|------|------|
| 1 | **`type='nickname'` 仅真机生效** | 🟡 中 | `<Input type='nickname'>` (L116) | `type='nickname'` 是微信小程序获取微信昵称的特殊能力仅在真机上有效。H5 预览 / 开发工具中用户可手动输入任意昵称(绕过微信身份验证)。应在非真机环境增加校验提示。 |
| 2 | **`avatarUrl``avatarKey` 共存冲突** | 🟡 中 | `submit()` (L56-82) | 编辑后 `uploadAvatar` 返回 `fileID` 赋给 `patch.avatarUrl`,但 `avatarKey` 没有被更新或清除。如果 `Avatar` 组件优先读 `avatarUrl`,则 `avatarKey` 多余;若优先读 `avatarKey`,可能显示旧头像。需要明确两者的优先级关系。 |
| 3 | **没有未保存离开提示** | 🟡 中 | 关闭按钮 (L88-90) | 直接 `navigateBack()` 没有检测是否有未保存修改。应比较当前表单值与初始值的差异,有差异时弹窗确认。 |
---
## 八、宠物编辑页 `pages/pet-edit/index.tsx`
| # | 缺陷 | 严重程度 | 位置 | 说明 |
|---|------|----------|------|------|
| 1 | **没有宠物数量上限限制** | 🟡 中 | `submit()` (L132-168) | 没有检查用户已添加的宠物数量。用户可以无限添加宠物,可能导致数据膨胀。建议在后端限制(如最多 5 只),前端也做预检提示。 |
| 2 | **照片隐式非必填但交互暗示必填** | 🟡 中 | `choosePhoto()` + 验证逻辑 | 提交验证只检查 `name``breed`,无照片也能保存。但展示逻辑依赖 `photoKey` 的兜底图,用户可能以为照片是必须的。建议明确标注照片为"可选",或在空照片时展示更友好的占位。 |
---
## 九、联系人页 `pages/contacts/index.tsx`
| # | 缺陷 | 严重程度 | 位置 | 说明 |
|---|------|----------|------|------|
| 1 | **"发现"搜索需手动提交,不支持实时搜索** | 🟡 中 | `<Input onConfirm>` (L101-102) | 搜索需要点击键盘"搜索"按钮才触发 (`onConfirm`),没有 `onInput` 实时触发搜索。对于"发现陌生人"场景,用户期望输入即搜索,应增加防抖实时搜索。 |
| 2 | **点击用户只能发起聊天,无个人主页入口** | 🟡 中 | `openChat()` (L63-67) | 点击用户头像/名称只能跳转到聊天页,没有查看对方个人主页(宠物列表、帖子等)的选项。用户想了解对方更多信息需要退出再找,交互断裂。 |
| 3 | **互关后 `isFriend` 未即时更新** | 🟡 中 | `onFollow()` (L51-61) | `toggleFollow` 返回 `{ mutual }` 但只更新了 `isFollowing`,没有同步更新 `isFriend`。互关后列表中不会立即显示"汪友"标签,需手动刷新页面才能看到。 |
---
## 十、我的动态页 `pages/my-posts/index.tsx`
| # | 缺陷 | 严重程度 | 位置 | 说明 |
|---|------|----------|------|------|
| 1 | **没有分页加载** | 🔴 高 | `load()` (L19-23) | 只加载一次 `getUserPosts()`,没有翻页。如果用户帖子很多,无法查看全部历史帖子。应实现 `ScrollView` 到底触发加载下一页。 |
| 2 | **`useDidShow` 首次触发导致重复加载** | 🔴 高 | `useDidShow(load)` (L31) | `useDidShow` 没有用 `firstShow` ref 跳过首次加载(对比 `PlazaPage``MessagesPage` 有此保护),首次进入页面时 `load()` 会被调用**两次**`useEffect` + `useDidShow`),浪费一次网络请求。 |
| 3 | **`likePost` 没有错误回滚** | 🟡 中 | `likePost()` (L33-47) | 乐观更新后调用 `setPostLike`,但如果失败不会回滚到之前的状态。对比 `PlazaPage.likePost``previousPosts` 回滚逻辑,此处遗漏了。 |
---
## 十一、跨页面系统性问题
| # | 问题 | 影响范围 | 说明 |
|---|------|----------|------|
| 1 | **全应用没有全局登录态守卫** | 所有页面 | 所有 service 通过 `canUseCloud()` 判断是否可用,但没有统一的"未登录则拦截/跳转"逻辑。只有 `ProfilePage` 单独处理了登录态,其他页面(广场、附近、消息、发布等)均假定已登录,未登录用户可能看到空数据或报错。 |
| 2 | **`Post.favoritedByMe` 收藏能力全链路断裂** | PlazaPage, PostCard | 类型定义有 `favoritedByMe` 字段,`feed.service.ts` 导出 `setPostFavorite` 方法,但 `PostCard` 组件和 `PlazaPage` 均没有收藏操作的 UI 入口,前端从未调用过该 service 方法。 |
| 3 | **草稿系统断裂** | PublishPage | `draftSave``draftGet` 云函数完整存在,`publish.service.ts` 导出了 `getDraft()`,但发布页 `mount` 时从不调用它。草稿保存后永远无法恢复,草稿功能形同虚设。 |
| 4 | **`StatsRow` 数据可能与子页面不同步** | ProfilePage → ContactsPage, MyPostsPage | 个人资料页 `stats.posts/following/followers` 来自 `getProfile()`,跳转到 `contacts?tab=following``getFollowList()` 可能因缓存延迟返回不同计数,造成数字前后不一致。 |
| 5 | **没有全局错误边界** | 全应用 | 任何 service 层未处理的异常会直接在页面崩溃。没有 React ErrorBoundary 或全局 Toast 兜底机制,用户体验差。 |
| 6 | **`CloudFunctionName` 联合类型缺少 `commentCreate`** | feed.service.ts L43 | `createPostComment` 调用了 `callCloud('commentCreate', ...)`,但 `CloudFunctionName` 联合类型中是否包含 `commentCreate` 取决于 `cloud.ts` 的定义。如果未声明TypeScript 不会报错但运行时可能失败。 |
---
## 十二、修复优先级建议
### P0 — 必须立即修复(影响核心功能)
1. **Feed / 我的动态分页** — 社交应用内容流的基础能力
2. **发布页草稿恢复**`getDraft()` 已存在,只需在页面 mount 时调用
3. **附近页上报自身位置**`updateLocation()` 已存在,只需在获取位置后调用
### P1 — 短期内修复(影响用户体验)
4. 我的动态页 `useDidShow` 重复加载 → 添加 `firstShow` ref
5. 我的动态页 `likePost` 错误回滚
6. 附近页 `likePet` 添加乐观更新
7. 附近页尊重 `nearbyVisible` 偏好
8. 聊天页消息分页加载
9. 个人资料页 `changePreference` 添加 `try-catch`
10. 收藏功能 UI 入口补全
### P2 — 中期迭代(完善产品体验)
11. 搜索栏功能实现(广场页)
12. 联系人页实时搜索
13. 聊天页发送状态指示 + 发送失败 UX
14. 未保存离开提示(发布页、编辑页)
15. 全局登录态守卫
16. 全局错误边界
17. 消息通知 / 推送接入
### P3 — 长期优化(性能和扩展性)
18. 话题列表动态化
19. 广告系统正规化
20. 图片 / 富媒体消息
21. 帖子详情页 + 评论列表
22. 聊天页轮询策略优化WebSocket
23. 宠物数量上限
24. 用户个人主页入口(从联系人页)
---
> 本报告基于源码静态审查生成,所有问题均指向代码层面的逻辑缺陷,不涉及 UI/UX 视觉设计层面的评审。

View File

@@ -8,7 +8,9 @@ export default defineAppConfig({
'pages/profile-edit/index',
'pages/pet-edit/index',
'pages/chat/index',
'pages/contacts/index'
'pages/contacts/index',
'pages/my-posts/index',
'pages/post-detail/index'
],
window: {
navigationStyle: 'custom',
@@ -24,7 +26,7 @@ export default defineAppConfig({
list: [
{ pagePath: 'pages/plaza/index', text: '广场' },
{ pagePath: 'pages/nearby/index', text: '附近' },
{ pagePath: 'pages/messages/index', text: '消息' },
{ pagePath: 'pages/messages/index', text: '汪友' },
{ pagePath: 'pages/profile/index', text: '我的' }
]
},

View File

@@ -10,6 +10,6 @@ export interface TabConfig {
export const tabs: TabConfig[] = [
{ key: 'plaza', label: '广场', path: '/pages/plaza/index' },
{ key: 'nearby', label: '附近', path: '/pages/nearby/index' },
{ key: 'messages', label: '消息', path: '/pages/messages/index', dot: true },
{ key: 'messages', label: '汪友', path: '/pages/messages/index', dot: true },
{ key: 'profile', label: '我的', path: '/pages/profile/index' }
]

View File

@@ -1,3 +1,4 @@
import Taro from '@tarojs/taro'
import { Image, Text, View } from '@tarojs/components'
import Avatar from '@/components/ui/Avatar'
import Icon from '@/components/ui/Icon'
@@ -7,17 +8,28 @@ import './index.scss'
interface PostCardProps {
post: Post
onLike: (postId: string) => void
onComment?: (postId: string) => void
// Opens the post detail page (triggered by tapping the body, photos or the
// comment button). When provided it takes precedence over onComment.
onOpen?: (postId: string) => void
// When true, tapping a photo opens the native full-screen image preview
// instead of navigating. Used on the detail page where there's nowhere to go.
enableImagePreview?: boolean
}
function PostActionIcon({ type, onClick }: { type: 'heart' | 'comment'; onClick?: () => void }) {
return <View className={`icon icon--${type}`} onClick={onClick} />
}
export function PostCard({ post, onLike }: PostCardProps) {
export function PostCard({ post, onLike, onComment, onOpen, enableImagePreview }: PostCardProps) {
const openDetail = onOpen ? () => onOpen(post._id) : undefined
const handleComment = onOpen ? openDetail : onComment ? () => onComment(post._id) : undefined
const urls = post.media || []
const previewImage = (current: string) => Taro.previewImage({ current, urls })
return (
<View className='post-card'>
<View className='post-card__head'>
<Avatar avatarKey={post.author.avatarKey} size='md' online />
<Avatar avatarKey={post.author.avatarKey} avatarUrl={post.author.avatarUrl} size='md' online />
<View className='post-card__meta'>
<View className='post-card__name-row'>
<Text className='post-card__name'>{post.author.name}</Text>
@@ -36,16 +48,22 @@ export function PostCard({ post, onLike }: PostCardProps) {
</View>
</View>
<Text className='post-card__body'>{post.body}</Text>
<Text className='post-card__body' onClick={openDetail}>{post.body}</Text>
{post.media && post.media.length ? (
<View className={`post-card__photos post-card__photos--${Math.min(post.media.length, 3)}`}>
{post.media.slice(0, 9).map((src, i) => (
<Image key={i} className='post-card__photo' src={src} mode='aspectFill' />
<Image
key={i}
className='post-card__photo'
src={src}
mode='aspectFill'
onClick={enableImagePreview ? () => previewImage(src) : openDetail}
/>
))}
</View>
) : (
<View className={`post-card__media post-card__media--${post.mediaTone}`}>
<View className={`post-card__media post-card__media--${post.mediaTone}`} onClick={openDetail}>
<View className='post-card__silhouette' />
{post.sticker ? <Text className='post-card__sticker'>{post.sticker}</Text> : null}
</View>
@@ -54,8 +72,8 @@ export function PostCard({ post, onLike }: PostCardProps) {
<View className={`post-card__actions ${post.likedByMe ? 'post-card__actions--liked' : ''}`}>
<PostActionIcon type='heart' onClick={() => onLike(post._id)} />
<Text onClick={() => onLike(post._id)}>{post.counts.likes}</Text>
<PostActionIcon type='comment' />
<Text>{post.counts.comments}</Text>
<PostActionIcon type='comment' onClick={handleComment} />
<Text onClick={handleComment}>{post.counts.comments}</Text>
</View>
</View>
)

View File

@@ -18,7 +18,7 @@ export function ActiveUserRow({ users, onSelect, onDiscover }: ActiveUserRowProp
<View className='active-user-row__avatar-wrap active-user-row__add'>
<Icon name='plus' size={22} />
</View>
<Text className='active-user-row__name'></Text>
<Text className='active-user-row__name'></Text>
</View>
{users.map(user => (
<View key={user.id} className='active-user-row__item' onClick={() => onSelect?.(user)}>
@@ -35,4 +35,3 @@ export function ActiveUserRow({ users, onSelect, onDiscover }: ActiveUserRowProp
}
export default ActiveUserRow

View File

@@ -77,6 +77,7 @@ export function NearbyMap({ pets, center, scale, activeId, onSelect, onRegionCha
onRegionChange={(e: any) => {
if (e?.type === 'end') onRegionChange?.(e.causedBy || '')
}}
onError={() => undefined}
/>
)
}

View File

@@ -114,13 +114,55 @@
.nearby-pet-card__actions {
display: flex;
gap: 6px;
flex-direction: column;
gap: 8px;
align-items: center;
flex-shrink: 0;
width: 78px;
}
.nearby-pet-card__friend {
width: 76px;
height: 30px;
border-radius: $radius-pill;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
box-sizing: border-box;
color: $primary-on;
background: linear-gradient(135deg, $accent, $accent-2);
box-shadow: 0 6px 16px rgba(216, 180, 255, 0.22);
transition: transform 180ms, opacity 180ms, background 180ms;
}
.nearby-pet-card__friend:active {
transform: scale(0.98);
}
.nearby-pet-card__friend--friend {
background: rgba(143, 229, 181, 0.16);
border: 1px solid rgba(143, 229, 181, 0.32);
color: $success;
box-shadow: none;
}
.nearby-pet-card__friend--pending,
.nearby-pet-card__friend--mine {
background: rgba(216, 180, 255, 0.08);
border: 1px solid rgba(216, 180, 255, 0.18);
color: $muted;
box-shadow: none;
}
.nearby-pet-card__friend-text {
font: 600 10px / 1 $font-body;
white-space: nowrap;
}
.nearby-pet-card__btn {
width: 36px;
height: 36px;
width: 34px;
height: 34px;
border-radius: 50%;
background: rgba(20, 16, 43, 0.5);
border: 1px solid rgba(216, 180, 255, 0.08);
@@ -136,4 +178,3 @@
color: #fff;
box-shadow: 0 6px 16px rgba(255, 138, 171, 0.35);
}

View File

@@ -7,10 +7,19 @@ interface NearbyPetCardProps {
pet: NearbyPet
active: boolean
onSelect: () => void
onFriendAction: () => void
onLike: () => void
}
export function NearbyPetCard({ pet, active, onSelect, onLike }: NearbyPetCardProps) {
export function NearbyPetCard({ pet, active, onSelect, onFriendAction, onLike }: NearbyPetCardProps) {
const relation = pet.isMine
? { key: 'mine', label: '我的', icon: 'user' as const }
: pet.isFriend
? { key: 'friend', label: '聊天', icon: 'message' as const }
: pet.isFollowing
? { key: 'pending', label: '待回关', icon: 'check' as const }
: { key: 'add', label: '加汪友', icon: 'plus' as const }
return (
<View className={`nearby-pet-card ${active ? 'nearby-pet-card--active' : ''}`} onClick={onSelect}>
<View className={`nearby-pet-card__photo ${pet.photoKey}`}>
@@ -34,8 +43,15 @@ export function NearbyPetCard({ pet, active, onSelect, onLike }: NearbyPetCardPr
</View>
</View>
<View className='nearby-pet-card__actions'>
<View className='nearby-pet-card__btn'>
<Icon name='message' size={16} />
<View
className={`nearby-pet-card__friend nearby-pet-card__friend--${relation.key}`}
onClick={event => {
event.stopPropagation()
onFriendAction()
}}
>
<Icon name={relation.icon} size={13} />
<Text className='nearby-pet-card__friend-text'>{relation.label}</Text>
</View>
<View
className='nearby-pet-card__btn nearby-pet-card__btn--like'
@@ -52,4 +68,3 @@ export function NearbyPetCard({ pet, active, onSelect, onLike }: NearbyPetCardPr
}
export default NearbyPetCard

View File

@@ -11,6 +11,7 @@ interface NearbySheetProps {
filter: string
onFilterChange: (filter: string) => void
onSelect: (id: string) => void
onFriendAction: (pet: NearbyPet) => void
onLike: (id: string) => void
}
@@ -21,7 +22,7 @@ const filters = [
{ key: 'online', label: '在线' }
]
export function NearbySheet({ pets, activeId, filter, onFilterChange, onSelect, onLike }: NearbySheetProps) {
export function NearbySheet({ pets, activeId, filter, onFilterChange, onSelect, onFriendAction, onLike }: NearbySheetProps) {
const sheet = useDragSheet(220)
return (
@@ -62,6 +63,7 @@ export function NearbySheet({ pets, activeId, filter, onFilterChange, onSelect,
pet={pet}
active={pet._id === activeId}
onSelect={() => onSelect(pet._id)}
onFriendAction={() => onFriendAction(pet)}
onLike={() => onLike(pet._id)}
/>
))}
@@ -71,4 +73,3 @@ export function NearbySheet({ pets, activeId, filter, onFilterChange, onSelect,
}
export default NearbySheet

View File

@@ -3,17 +3,18 @@ import { formatCount } from '@/utils/date'
import type { UserProfile } from '@/types/domain'
import './index.scss'
type StatKey = 'posts' | 'following' | 'followers'
interface StatsRowProps {
stats: UserProfile['stats']
onSelect?: (key: 'posts' | 'following' | 'followers' | 'favorites') => void
onSelect?: (key: StatKey) => void
}
const labels = [
const labels: Array<[StatKey, string]> = [
['posts', '动态'],
['following', '关注'],
['followers', '粉丝'],
['favorites', '收藏']
] as const
['followers', '粉丝']
]
export function StatsRow({ stats, onSelect }: StatsRowProps) {
const safeStats = stats || { posts: 0, following: 0, followers: 0, favorites: 0 }

View File

@@ -1,6 +1,5 @@
export default definePageConfig({
navigationBarTitleText: '消息',
navigationBarTitleText: '汪友',
navigationStyle: 'custom',
disableScroll: true
})

View File

@@ -59,7 +59,7 @@ export default function MessagesPage() {
})
}
const goContacts = () => Taro.navigateTo({ url: '/pages/contacts/index' })
const goContacts = () => Taro.navigateTo({ url: '/pages/contacts/index?tab=discover' })
const kw = keyword.trim().toLowerCase()
const visibleConversations = kw
@@ -70,8 +70,8 @@ export default function MessagesPage() {
return (
<PageShell className='messages-page'>
<CustomNavBar title='消息' />
<SearchBar placeholder='搜索聊天或通知' value={keyword} onInput={setKeyword} />
<CustomNavBar title='汪友' />
<SearchBar placeholder='搜索汪友或聊天' value={keyword} onInput={setKeyword} />
<SegmentedTabs items={tabs as any} value={tab} onChange={setTab} />
{kw ? null : <ActiveUserRow users={activeUsers} onSelect={openChatWith} onDiscover={goContacts} />}
<View className='messages-page__divider'>
@@ -87,7 +87,7 @@ export default function MessagesPage() {
? '没有匹配的聊天'
: tab === 'unread'
? '没有未读消息'
: '还没有聊天,去「发现」找汪友聊聊吧'}
: '还没有聊天,去「附近」加汪友吧'}
</Text>
) : null}
</View>

View File

@@ -0,0 +1,59 @@
.my-posts {
display: flex;
flex-direction: column;
height: 100vh;
background: $bg;
}
.my-posts__nav {
flex-shrink: 0;
background: $bg;
}
.my-posts__nav-row {
display: flex;
align-items: center;
height: 50px;
padding: 0 12px;
}
.my-posts__back {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
color: $fg;
}
.my-posts__back-icon {
transform: rotate(180deg);
}
.my-posts__title {
flex: 1;
text-align: center;
color: $fg;
font: 700 16px / 1 $font-display;
}
.my-posts__nav-spacer {
width: 36px;
}
.my-posts__scroll {
flex: 1;
min-height: 0;
}
.my-posts__feed {
padding: 8px 16px 32px;
}
.my-posts__empty {
display: block;
text-align: center;
color: $muted;
font: 400 13px / 1.6 $font-body;
padding: 48px 24px;
}

View File

@@ -0,0 +1,82 @@
import { useEffect, useState } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { ScrollView, Text, View } from '@tarojs/components'
import { useSystemLayout } from '@/hooks/useSystemLayout'
import Icon from '@/components/ui/Icon'
import PostCard from '@/features/feed/components/PostCard'
import ToastOverlay from '@/components/feedback/ToastOverlay'
import { getUserPosts, setPostLike } from '@/services/feed.service'
import { useToast } from '@/hooks/useToast'
import type { Post } from '@/types/domain'
import './index.scss'
export default function MyPostsPage() {
const { statusBarHeight } = useSystemLayout()
const [posts, setPosts] = useState<Post[]>([])
const [loading, setLoading] = useState(true)
const toast = useToast(480)
const load = () => {
setLoading(true)
getUserPosts()
.then(setPosts)
.finally(() => setLoading(false))
}
useEffect(() => {
load()
}, [])
// Refresh on return (e.g. after publishing a new post).
useDidShow(load)
const likePost = async (postId: string) => {
const next = posts.map(post => {
if (post._id !== postId) return post
const likedByMe = !post.likedByMe
if (likedByMe) toast.show()
return {
...post,
likedByMe,
counts: { ...post.counts, likes: Math.max(0, post.counts.likes + (likedByMe ? 1 : -1)) }
}
})
setPosts(next)
const target = next.find(p => p._id === postId)
if (target) await setPostLike(postId, target.likedByMe)
}
return (
<View className='my-posts'>
<View className='my-posts__nav' style={{ paddingTop: `${statusBarHeight}px` }}>
<View className='my-posts__nav-row'>
<View className='my-posts__back' onClick={() => Taro.navigateBack()}>
<Icon name='chev' size={22} className='my-posts__back-icon' />
</View>
<Text className='my-posts__title'></Text>
<View className='my-posts__nav-spacer' />
</View>
</View>
<ScrollView scrollY className='my-posts__scroll' enhanced showScrollbar={false}>
<View className='my-posts__feed'>
{posts.map(post => (
<PostCard
key={post._id}
post={post}
onLike={likePost}
onOpen={id => Taro.navigateTo({ url: `/pages/post-detail/index?postId=${id}` })}
/>
))}
{loading && posts.length === 0 ? (
<Text className='my-posts__empty'></Text>
) : !loading && posts.length === 0 ? (
<Text className='my-posts__empty'>广 + </Text>
) : null}
</View>
</ScrollView>
<ToastOverlay visible={toast.visible} />
</View>
)
}

View File

@@ -10,6 +10,7 @@ import MapControls from '@/features/nearby/components/MapControls'
import NearbySheet from '@/features/nearby/components/NearbySheet'
import { useSystemLayout } from '@/hooks/useSystemLayout'
import { getNearbyPets, likeNearbyPet } from '@/services/nearby.service'
import { toggleFollow } from '@/services/social.service'
import { mockNearbyPets } from '@/services/mock'
import { useToast } from '@/hooks/useToast'
import type { GeoPoint, NearbyPet } from '@/types/domain'
@@ -90,6 +91,51 @@ export default function NearbyPage() {
await likeNearbyPet(id)
}
const updateOwnerRelation = (
ownerId: string,
patch: Pick<NearbyPet, 'isFollowing' | 'isFriend'>
) => {
setPets(current => current.map(pet => (pet.ownerId === ownerId ? { ...pet, ...patch } : pet)))
}
const openChatWithPet = (pet: NearbyPet) => {
if (!pet.ownerId) return
const title = pet.ownerName || pet.name
Taro.navigateTo({
url: `/pages/chat/index?targetUserId=${pet.ownerId}&title=${encodeURIComponent(title)}`
})
}
const handleFriendAction = async (pet: NearbyPet) => {
if (pet.isMine) {
Taro.showToast({ title: '这是你自己的毛孩子', icon: 'none' })
return
}
if (pet.isFriend) {
openChatWithPet(pet)
return
}
if (pet.isFollowing) {
Taro.showToast({ title: '已关注,等对方回关', icon: 'none' })
return
}
updateOwnerRelation(pet.ownerId, { isFollowing: true, isFriend: false })
try {
const result = await toggleFollow(pet.ownerId, true)
updateOwnerRelation(pet.ownerId, { isFollowing: true, isFriend: result.mutual })
Taro.showToast({
title: result.mutual ? '已成为汪友' : '已关注,等对方回关',
icon: 'none'
})
} catch {
updateOwnerRelation(pet.ownerId, { isFollowing: false, isFriend: false })
Taro.showToast({ title: '添加失败,请稍后再试', icon: 'none' })
}
}
const q = query.trim().toLowerCase()
const visiblePets = q
? pets.filter(p => p.name.toLowerCase().includes(q) || (p.breed || '').toLowerCase().includes(q))
@@ -129,6 +175,7 @@ export default function NearbyPage() {
filter={filter}
onFilterChange={setFilter}
onSelect={setActiveId}
onFriendAction={handleFriendAction}
onLike={likePet}
/>
<ToastOverlay visible={toast.visible} />

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react'
import { useDidShow } from '@tarojs/taro'
import Taro, { useDidShow } from '@tarojs/taro'
import { ScrollView, Text, View } from '@tarojs/components'
import CustomNavBar from '@/components/layout/CustomNavBar'
import PageShell from '@/components/layout/PageShell'
@@ -52,6 +52,7 @@ export default function PlazaPage() {
})
const likePost = async (postId: string) => {
const previousPosts = posts
const nextPosts = posts.map(post => {
if (post._id !== postId) return post
const likedByMe = !post.likedByMe
@@ -67,7 +68,23 @@ export default function PlazaPage() {
})
setPosts(nextPosts)
const next = nextPosts.find(post => post._id === postId)
if (next) await setPostLike(postId, next.likedByMe)
if (!next) return
try {
const result = await setPostLike(postId, next.likedByMe)
setPosts(current => current.map(post => (
post._id === postId
? { ...post, likedByMe: result.liked, counts: { ...post.counts, likes: result.likes } }
: post
)))
} catch {
setPosts(previousPosts)
Taro.showToast({ title: '点赞失败,请重试', icon: 'none' })
}
}
const openPost = (postId: string) => {
Taro.navigateTo({ url: `/pages/post-detail/index?postId=${postId}` })
}
return (
@@ -87,7 +104,7 @@ export default function PlazaPage() {
<View className='plaza-page__feed'>
{posts.map((post, index) => (
<View key={post._id}>
<PostCard post={post} onLike={likePost} />
<PostCard post={post} onLike={likePost} onOpen={openPost} />
{index === 0 ? <FeedAdCard /> : null}
</View>
))}

View File

@@ -0,0 +1,147 @@
.post-detail {
display: flex;
flex-direction: column;
height: 100vh;
background: $bg;
}
.post-detail__nav {
flex-shrink: 0;
background: $bg;
border-bottom: 1px solid rgba(216, 180, 255, 0.06);
}
.post-detail__nav-row {
display: flex;
align-items: center;
height: 50px;
padding: 0 12px;
}
.post-detail__back {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
color: $fg;
}
.post-detail__back-icon {
transform: rotate(180deg);
}
.post-detail__title {
flex: 1;
text-align: center;
color: $fg;
font: 700 16px / 1 $font-display;
}
.post-detail__nav-spacer {
width: 36px;
}
.post-detail__scroll {
flex: 1;
min-height: 0;
}
.post-detail__post {
padding-top: 12px;
}
.post-detail__comments {
padding: 4px 16px 24px;
}
.post-detail__comments-title {
display: block;
margin: 8px 0 12px;
color: $fg;
font: 700 14px / 1 $font-display;
}
.post-detail__empty {
display: block;
text-align: center;
color: $muted;
font: 400 13px / 1.6 $font-body;
padding: 36px 24px;
}
.post-detail__comment {
display: flex;
gap: 10px;
padding: 12px 0;
border-top: 1px solid rgba(216, 180, 255, 0.06);
}
.post-detail__comment-body {
flex: 1;
min-width: 0;
}
.post-detail__comment-head {
display: flex;
align-items: center;
gap: 8px;
}
.post-detail__comment-name {
color: $fg;
font: 600 13px / 1.2 $font-body;
}
.post-detail__comment-time {
color: $muted;
font: 400 11px / 1 $font-body;
}
.post-detail__comment-text {
display: block;
margin-top: 6px;
color: $fg;
font: 400 14px / 1.55 $font-body;
word-break: break-word;
}
.post-detail__input-bar {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px 12px;
background: $bg;
border-top: 1px solid rgba(216, 180, 255, 0.08);
}
.post-detail__input {
flex: 1;
height: 40px;
padding: 0 14px;
box-sizing: border-box;
border-radius: $radius-pill;
background: rgba(31, 23, 66, 0.68);
border: 1px solid rgba(216, 180, 255, 0.1);
color: $fg;
font: 400 14px / 40px $font-body;
}
.post-detail__input-ph {
color: $muted;
}
.post-detail__send {
flex-shrink: 0;
padding: 0 18px;
height: 40px;
border-radius: $radius-pill;
background: linear-gradient(135deg, $accent, $accent-2);
color: $primary-on;
font: 700 14px / 40px $font-body;
}
.post-detail__send--disabled {
opacity: 0.5;
}

View File

@@ -0,0 +1,163 @@
import { useEffect, useState } from 'react'
import Taro, { useRouter } from '@tarojs/taro'
import { Input, ScrollView, Text, View } from '@tarojs/components'
import { useSystemLayout } from '@/hooks/useSystemLayout'
import Icon from '@/components/ui/Icon'
import Avatar from '@/components/ui/Avatar'
import PostCard from '@/features/feed/components/PostCard'
import ToastOverlay from '@/components/feedback/ToastOverlay'
import { createPostComment, getPostDetail, setPostLike } from '@/services/feed.service'
import { useToast } from '@/hooks/useToast'
import type { Post, PostComment } from '@/types/domain'
import './index.scss'
export default function PostDetailPage() {
const { statusBarHeight, safeBottom } = useSystemLayout()
const router = useRouter()
const postId = router.params.postId || ''
const [post, setPost] = useState<Post | null>(null)
const [comments, setComments] = useState<PostComment[]>([])
const [loading, setLoading] = useState(true)
const [draft, setDraft] = useState('')
const [sending, setSending] = useState(false)
const toast = useToast(480)
const load = () => {
if (!postId) {
setLoading(false)
return
}
setLoading(true)
getPostDetail(postId)
.then(res => {
setPost(res.post)
setComments(res.comments)
})
.finally(() => setLoading(false))
}
useEffect(() => {
load()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [postId])
const likePost = async (id: string) => {
if (!post) return
const likedByMe = !post.likedByMe
if (likedByMe) toast.show()
const next = {
...post,
likedByMe,
counts: { ...post.counts, likes: Math.max(0, post.counts.likes + (likedByMe ? 1 : -1)) }
}
setPost(next)
try {
const result = await setPostLike(id, likedByMe)
setPost(current =>
current ? { ...current, likedByMe: result.liked, counts: { ...current.counts, likes: result.likes } } : current
)
} catch {
setPost(post)
Taro.showToast({ title: '点赞失败,请重试', icon: 'none' })
}
}
const send = async () => {
const content = draft.trim()
if (!content || sending || !post) return
setSending(true)
setDraft('')
try {
const result = await createPostComment(post._id, content)
setComments(current => [
...current,
{
_id: result.commentId,
author: {
id: 'me',
name: '我',
avatarKey: 'gradient-avatar-1'
},
content,
timeText: '刚刚'
}
])
setPost(current =>
current ? { ...current, counts: { ...current.counts, comments: result.comments } } : current
)
} catch {
Taro.showToast({ title: '评论失败,请重试', icon: 'none' })
setDraft(content)
} finally {
setSending(false)
}
}
return (
<View className='post-detail'>
<View className='post-detail__nav' style={{ paddingTop: `${statusBarHeight}px` }}>
<View className='post-detail__nav-row'>
<View className='post-detail__back' onClick={() => Taro.navigateBack()}>
<Icon name='chev' size={22} className='post-detail__back-icon' />
</View>
<Text className='post-detail__title'></Text>
<View className='post-detail__nav-spacer' />
</View>
</View>
<ScrollView scrollY className='post-detail__scroll' enhanced showScrollbar={false}>
{post ? (
<View className='post-detail__post'>
<PostCard post={post} onLike={likePost} enableImagePreview />
</View>
) : null}
<View className='post-detail__comments'>
<Text className='post-detail__comments-title'>
{post ? ` ${post.counts.comments}` : ''}
</Text>
{loading && !post ? (
<Text className='post-detail__empty'></Text>
) : comments.length === 0 ? (
<Text className='post-detail__empty'></Text>
) : (
comments.map(comment => (
<View key={comment._id} className='post-detail__comment'>
<Avatar avatarKey={comment.author.avatarKey} avatarUrl={comment.author.avatarUrl} size='sm' />
<View className='post-detail__comment-body'>
<View className='post-detail__comment-head'>
<Text className='post-detail__comment-name'>{comment.author.name}</Text>
<Text className='post-detail__comment-time'>{comment.timeText}</Text>
</View>
<Text className='post-detail__comment-text'>{comment.content}</Text>
</View>
</View>
))
)}
</View>
</ScrollView>
<View className='post-detail__input-bar' style={{ paddingBottom: `${Math.max(12, safeBottom)}px` }}>
<Input
className='post-detail__input'
value={draft}
placeholder='写评论…'
placeholderClass='post-detail__input-ph'
confirmType='send'
onInput={e => setDraft(e.detail.value)}
onConfirm={send}
/>
<View
className={`post-detail__send ${draft.trim() ? '' : 'post-detail__send--disabled'}`}
onClick={send}
>
</View>
</View>
<ToastOverlay visible={toast.visible} />
</View>
)
}

View File

@@ -118,7 +118,8 @@ export default function ProfilePage() {
<StatsRow
stats={user.stats}
onSelect={key => {
if (key === 'following') Taro.navigateTo({ url: '/pages/contacts/index?tab=following' })
if (key === 'posts') Taro.navigateTo({ url: '/pages/my-posts/index' })
else if (key === 'following') Taro.navigateTo({ url: '/pages/contacts/index?tab=following' })
else if (key === 'followers') Taro.navigateTo({ url: '/pages/contacts/index?tab=followers' })
}}
/>

View File

@@ -80,7 +80,7 @@ export default function PublishPage() {
}
}
try {
const res = await Taro.chooseLocation()
const res = await Taro.chooseLocation({})
if (res && (res.name || res.address)) {
setLocation(res.name || res.address)
setLocationGeo({ latitude: res.latitude, longitude: res.longitude })

View File

@@ -1,6 +1,25 @@
import { callCloud, canUseCloud } from './cloud'
import { mockPosts } from './mock'
import type { FeedChannel, Post } from '@/types/domain'
import type { FeedChannel, Post, PostComment } from '@/types/domain'
export async function getUserPosts(authorId?: string): Promise<Post[]> {
if (!canUseCloud()) return mockPosts
const result = await callCloud<object, { list: Post[] }>('userPosts', authorId ? { authorId } : {})
return result.list || []
}
export async function getPostDetail(
postId: string
): Promise<{ post: Post | null; comments: PostComment[] }> {
if (!canUseCloud()) {
return { post: mockPosts.find(item => item._id === postId) || null, comments: [] }
}
const result = await callCloud<{ postId: string }, { post: Post | null; comments: PostComment[] }>(
'postDetail',
{ postId }
)
return { post: result.post || null, comments: result.comments || [] }
}
export async function getFeedList(channel: FeedChannel, topic?: string): Promise<Post[]> {
// Only fall back to sample posts in non-cloud previews (e.g. H5 dev).
@@ -28,3 +47,11 @@ export async function setPostFavorite(
return callCloud('postFavorite', { postId, favorited })
}
export async function createPostComment(postId: string, content: string): Promise<{ commentId: string; comments: number }> {
if (!canUseCloud()) {
const post = mockPosts.find(item => item._id === postId)
return { commentId: `local_comment_${Date.now()}`, comments: (post?.counts.comments || 0) + 1 }
}
return callCloud('commentCreate', { postId, content })
}

View File

@@ -103,6 +103,10 @@ export const mockPosts: Post[] = [
export const mockNearbyPets: NearbyPet[] = [
{
...mockPets[0],
ownerName: '阿布的家长',
isMine: true,
isFollowing: false,
isFriend: false,
distanceText: '260m',
online: true,
pin: { left: 24, top: 28 }
@@ -117,6 +121,9 @@ export const mockNearbyPets: NearbyPet[] = [
distanceText: '420m',
avatarKey: 'gradient-avatar-3',
photoKey: 'photo-green',
ownerName: 'Lucky 之家',
isFollowing: true,
isFriend: true,
tags: ['精力旺盛', '会接飞盘'],
mood: '找玩伴',
online: true,
@@ -132,6 +139,9 @@ export const mockNearbyPets: NearbyPet[] = [
distanceText: '530m',
avatarKey: 'gradient-avatar-6',
photoKey: 'photo-pink',
ownerName: '豆包妈',
isFollowing: false,
isFriend: false,
tags: ['慢热', '爱追球'],
mood: '想闻新朋友',
pin: { left: 42, top: 56 }
@@ -146,6 +156,9 @@ export const mockNearbyPets: NearbyPet[] = [
distanceText: '780m',
avatarKey: 'gradient-avatar-2',
photoKey: 'photo-blue',
ownerName: '白桃家',
isFollowing: true,
isFriend: false,
tags: ['温柔', '适合新手'],
mood: '公园散步中',
pin: { left: 76, top: 62 }
@@ -160,6 +173,9 @@ export const mockNearbyPets: NearbyPet[] = [
distanceText: '960m',
avatarKey: 'gradient-avatar-5',
photoKey: 'photo-gold',
ownerName: '摩卡家',
isFollowing: false,
isFriend: false,
tags: ['安静围观', '亲人'],
mood: '正在车里看热闹',
pin: { left: 18, top: 70 }

View File

@@ -1,9 +1,12 @@
export type CloudFunctionName =
| 'login'
| 'feedList'
| 'userPosts'
| 'postDetail'
| 'postCreate'
| 'postLike'
| 'postFavorite'
| 'commentCreate'
| 'draftSave'
| 'draftGet'
| 'nearbyPets'
@@ -32,4 +35,3 @@ export interface CloudResult<T> {
data?: T
error?: string
}

View File

@@ -64,6 +64,7 @@ export interface Post {
id: string
name: string
avatarKey: string
avatarUrl?: string
}
pet: {
name: string
@@ -86,7 +87,23 @@ export interface Post {
favoritedByMe?: boolean
}
export interface PostComment {
_id: string
author: {
id: string
name: string
avatarKey: string
avatarUrl?: string
}
content: string
timeText: string
}
export interface NearbyPet extends Pet {
ownerName?: string
isMine?: boolean
isFollowing?: boolean
isFriend?: boolean
pin: {
left: number
top: number