Compare commits
5 Commits
c04c890186
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| d55ed60907 | |||
| 2c3c2653fa | |||
| b02e26f602 | |||
| 3d21a9d297 | |||
| 24891d9004 |
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# 依赖
|
||||
node_modules/
|
||||
**/node_modules/
|
||||
|
||||
# 云函数锁文件(可选提交,这里统一忽略)
|
||||
cloudfunctions/**/package-lock.json
|
||||
|
||||
# 微信开发者工具私有配置(含 token,不入库)
|
||||
project.private.config.json
|
||||
**/project.private.config.json
|
||||
|
||||
# 开发者工具在 srcMiniprogramRoot 内生成的重复配置
|
||||
miniprogram/project.config.json
|
||||
|
||||
# 编译产物
|
||||
miniprogram/**/*.js.map
|
||||
|
||||
# 系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
175
cloudbaserc.json
Normal file
175
cloudbaserc.json
Normal file
@@ -0,0 +1,175 @@
|
||||
{
|
||||
"$schema": "https://static.cloudbase.net/cli/cloudbaserc.schema.json",
|
||||
"envId": "cloud1-d4g697lte499543d8",
|
||||
"functionRoot": "cloudfunctions",
|
||||
"functions": [
|
||||
{
|
||||
"name": "login",
|
||||
"timeout": 10,
|
||||
"runtime": "Nodejs20.19",
|
||||
"handler": "index.main",
|
||||
"description": "微信登录 & 用户注册"
|
||||
},
|
||||
{
|
||||
"name": "createPost",
|
||||
"timeout": 15,
|
||||
"runtime": "Nodejs20.19",
|
||||
"handler": "index.main",
|
||||
"description": "发布帖子"
|
||||
},
|
||||
{
|
||||
"name": "getPosts",
|
||||
"timeout": 10,
|
||||
"runtime": "Nodejs20.19",
|
||||
"handler": "index.main",
|
||||
"description": "获取信息流帖子列表"
|
||||
},
|
||||
{
|
||||
"name": "getPost",
|
||||
"timeout": 10,
|
||||
"runtime": "Nodejs20.19",
|
||||
"handler": "index.main",
|
||||
"description": "获取单条帖子详情"
|
||||
},
|
||||
{
|
||||
"name": "likePost",
|
||||
"timeout": 10,
|
||||
"runtime": "Nodejs20.19",
|
||||
"handler": "index.main",
|
||||
"description": "点赞 / 取消点赞"
|
||||
},
|
||||
{
|
||||
"name": "getNearbyPets",
|
||||
"timeout": 15,
|
||||
"runtime": "Nodejs20.19",
|
||||
"handler": "index.main",
|
||||
"description": "附近宠物地理位置查询"
|
||||
},
|
||||
{
|
||||
"name": "updateLocation",
|
||||
"timeout": 10,
|
||||
"runtime": "Nodejs20.19",
|
||||
"handler": "index.main",
|
||||
"description": "更新用户地理位置(GeoPoint)"
|
||||
},
|
||||
{
|
||||
"name": "sendGreeting",
|
||||
"timeout": 10,
|
||||
"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": "删除宠物档案"
|
||||
},
|
||||
{
|
||||
"name": "getMessages",
|
||||
"timeout": 10,
|
||||
"runtime": "Nodejs20.19",
|
||||
"handler": "index.main",
|
||||
"description": "获取与某用户的聊天消息列表"
|
||||
},
|
||||
{
|
||||
"name": "sendMessage",
|
||||
"timeout": 10,
|
||||
"runtime": "Nodejs20.19",
|
||||
"handler": "index.main",
|
||||
"description": "发送私信消息"
|
||||
},
|
||||
{
|
||||
"name": "getHotTopics",
|
||||
"timeout": 10,
|
||||
"runtime": "Nodejs20.19",
|
||||
"handler": "index.main",
|
||||
"description": "获取热门话题标签"
|
||||
},
|
||||
{
|
||||
"name": "searchPosts",
|
||||
"timeout": 10,
|
||||
"runtime": "Nodejs20.19",
|
||||
"handler": "index.main",
|
||||
"description": "搜索帖子"
|
||||
},
|
||||
{
|
||||
"name": "searchUsers",
|
||||
"timeout": 10,
|
||||
"runtime": "Nodejs20.19",
|
||||
"handler": "index.main",
|
||||
"description": "搜索用户"
|
||||
},
|
||||
{
|
||||
"name": "getStory",
|
||||
"timeout": 10,
|
||||
"runtime": "Nodejs20.19",
|
||||
"handler": "index.main",
|
||||
"description": "获取用户故事(最近带图动态)"
|
||||
},
|
||||
{
|
||||
"name": "getNotifications",
|
||||
"timeout": 10,
|
||||
"runtime": "Nodejs20.19",
|
||||
"handler": "index.main",
|
||||
"description": "获取通知列表(点赞/评论聚合)"
|
||||
},
|
||||
{
|
||||
"name": "markAllRead",
|
||||
"timeout": 10,
|
||||
"runtime": "Nodejs20.19",
|
||||
"handler": "index.main",
|
||||
"description": "标记全部通知已读"
|
||||
}
|
||||
]
|
||||
}
|
||||
43
cloudfunctions/addComment/index.js
Normal file
43
cloudfunctions/addComment/index.js
Normal file
@@ -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 }
|
||||
}
|
||||
}
|
||||
1
cloudfunctions/addComment/package.json
Normal file
1
cloudfunctions/addComment/package.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "name": "addComment", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } }
|
||||
@@ -27,6 +27,11 @@ exports.main = async (event, context) => {
|
||||
updatedAt: db.serverDate(),
|
||||
}
|
||||
|
||||
// geo.near 距离查询字段;无定位时整个字段省略(写 null 会让地理索引构建失败)
|
||||
if (location && typeof location.latitude === 'number' && typeof location.longitude === 'number') {
|
||||
post.geo = db.Geo.Point(location.longitude, location.latitude)
|
||||
}
|
||||
|
||||
const res = await db.collection('posts').add({ data: post })
|
||||
|
||||
// 更新用户帖子统计
|
||||
|
||||
30
cloudfunctions/deletePet/index.js
Normal file
30
cloudfunctions/deletePet/index.js
Normal file
@@ -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 }
|
||||
}
|
||||
}
|
||||
1
cloudfunctions/deletePet/package.json
Normal file
1
cloudfunctions/deletePet/package.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "name": "deletePet", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } }
|
||||
40
cloudfunctions/followUser/index.js
Normal file
40
cloudfunctions/followUser/index.js
Normal file
@@ -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 }
|
||||
}
|
||||
}
|
||||
1
cloudfunctions/followUser/package.json
Normal file
1
cloudfunctions/followUser/package.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "name": "followUser", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } }
|
||||
45
cloudfunctions/getComments/index.js
Normal file
45
cloudfunctions/getComments/index.js
Normal file
@@ -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 }
|
||||
}
|
||||
}
|
||||
1
cloudfunctions/getComments/package.json
Normal file
1
cloudfunctions/getComments/package.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "name": "getComments", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } }
|
||||
51
cloudfunctions/getFollowList/index.js
Normal file
51
cloudfunctions/getFollowList/index.js
Normal file
@@ -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 }
|
||||
}
|
||||
}
|
||||
1
cloudfunctions/getFollowList/package.json
Normal file
1
cloudfunctions/getFollowList/package.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "name": "getFollowList", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } }
|
||||
32
cloudfunctions/getHotTopics/index.js
Normal file
32
cloudfunctions/getHotTopics/index.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const cloud = require('wx-server-sdk')
|
||||
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
const db = cloud.database()
|
||||
const SCAN_LIMIT = 200
|
||||
const TOP_N = 10
|
||||
|
||||
exports.main = async (event, context) => {
|
||||
try {
|
||||
const res = await db.collection('posts')
|
||||
.orderBy('createdAt', 'desc')
|
||||
.limit(SCAN_LIMIT)
|
||||
.field({ hashtags: true })
|
||||
.get()
|
||||
|
||||
const counts = {}
|
||||
res.data.forEach(p => {
|
||||
;(p.hashtags || []).forEach(tag => {
|
||||
if (!tag) return
|
||||
counts[tag] = (counts[tag] || 0) + 1
|
||||
})
|
||||
})
|
||||
|
||||
const topics = Object.keys(counts)
|
||||
.map(tag => ({ tag, count: counts[tag] }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, TOP_N)
|
||||
|
||||
return { code: 0, data: topics }
|
||||
} catch (e) {
|
||||
return { code: -1, message: e.message }
|
||||
}
|
||||
}
|
||||
1
cloudfunctions/getHotTopics/package.json
Normal file
1
cloudfunctions/getHotTopics/package.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "name": "getHotTopics", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } }
|
||||
46
cloudfunctions/getMessages/index.js
Normal file
46
cloudfunctions/getMessages/index.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const cloud = require('wx-server-sdk')
|
||||
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
const db = cloud.database()
|
||||
const _ = db.command
|
||||
const PAGE_SIZE = 30
|
||||
|
||||
exports.main = async (event, context) => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
const { peerId, page = 0 } = event || {}
|
||||
|
||||
try {
|
||||
const meRes = await db.collection('users').where({ openid: OPENID }).get()
|
||||
if (meRes.data.length === 0) return { code: -1, message: '用户不存在' }
|
||||
const myId = meRes.data[0]._id
|
||||
|
||||
let list = []
|
||||
if (peerId) {
|
||||
const skip = page * PAGE_SIZE
|
||||
const msgRes = await db.collection('messages')
|
||||
.where(_.or([
|
||||
{ fromId: myId, toId: peerId },
|
||||
{ fromId: peerId, toId: myId },
|
||||
]))
|
||||
.orderBy('createdAt', 'desc')
|
||||
.skip(skip)
|
||||
.limit(PAGE_SIZE)
|
||||
.get()
|
||||
|
||||
list = msgRes.data.reverse().map(m => ({ ...m, isMe: m.fromId === myId }))
|
||||
|
||||
// 把对方发来的未读消息标记为已读
|
||||
const unreadIds = msgRes.data.filter(m => m.toId === myId && !m.isRead).map(m => m._id)
|
||||
if (unreadIds.length) {
|
||||
await db.collection('messages').where({ _id: _.in(unreadIds) }).update({ data: { isRead: true } })
|
||||
}
|
||||
}
|
||||
|
||||
const unreadRes = await db.collection('messages')
|
||||
.where({ toId: myId, isRead: _.neq(true) })
|
||||
.count()
|
||||
|
||||
return { code: 0, data: { list, unreadCount: unreadRes.total } }
|
||||
} catch (e) {
|
||||
return { code: -1, message: e.message }
|
||||
}
|
||||
}
|
||||
1
cloudfunctions/getMessages/package.json
Normal file
1
cloudfunctions/getMessages/package.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "name": "getMessages", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } }
|
||||
@@ -3,74 +3,64 @@ cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
const db = cloud.database()
|
||||
const _ = db.command
|
||||
|
||||
// 粗略经纬度范围换算(1度≈111km)
|
||||
function latDelta(km) { return km / 111 }
|
||||
function lngDelta(km, lat) { return km / (111 * Math.cos(lat * Math.PI / 180)) }
|
||||
|
||||
function calcDistance(lat1, lng1, lat2, lng2) {
|
||||
const R = 6371
|
||||
const dLat = (lat2 - lat1) * Math.PI / 180
|
||||
const dLng = (lng2 - lng1) * Math.PI / 180
|
||||
const a = Math.sin(dLat / 2) ** 2 +
|
||||
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng / 2) ** 2
|
||||
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) * 1000 // meters
|
||||
}
|
||||
|
||||
exports.main = async (event, context) => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
const { latitude, longitude, radiusKm = 5 } = event
|
||||
const {
|
||||
latitude,
|
||||
longitude,
|
||||
maxDistance = 5000, // 默认 5km,单位:米
|
||||
page = 0,
|
||||
pageSize = 20,
|
||||
} = event
|
||||
|
||||
if (!latitude || !longitude) {
|
||||
return { code: -1, message: '缺少位置信息' }
|
||||
}
|
||||
|
||||
// 在线判断:60秒内更新过位置的用户为在线
|
||||
const onlineThreshold = new Date(Date.now() - 60 * 1000).toISOString()
|
||||
if (!latitude || !longitude) return { code: -1, message: '缺少位置' }
|
||||
|
||||
try {
|
||||
const dLat = latDelta(radiusKm)
|
||||
const dLng = lngDelta(radiusKm, latitude)
|
||||
|
||||
const usersRes = await db.collection('users')
|
||||
.where(
|
||||
_.and([
|
||||
{ openid: _.neq(OPENID) }, // 排除自己
|
||||
{ 'lastLocation.latitude': _.gt(latitude - dLat).and(_.lt(latitude + dLat)) },
|
||||
{ 'lastLocation.longitude': _.gt(longitude - dLng).and(_.lt(longitude + dLng)) },
|
||||
{ pets: _.exists(true) },
|
||||
])
|
||||
)
|
||||
.field({
|
||||
openid: true, nickName: true, avatarUrl: true,
|
||||
lastLocation: true, lastSeen: true, isOnline: true,
|
||||
pets: true, location: true, bio: true,
|
||||
// ✅ geoNear 聚合管道:依赖 users.lastLocation 字段上的 2dsphere 地理位置索引
|
||||
const { list } = await db.collection('users')
|
||||
.aggregate()
|
||||
.geoNear({
|
||||
near: db.Geo.Point(longitude, latitude), // 注意:CloudBase GeoPoint 是 (lng, lat) 顺序
|
||||
spherical: true,
|
||||
distanceField: 'dist', // 在每条记录上附加实际距离(米)
|
||||
maxDistance,
|
||||
query: {
|
||||
openid: _.neq(OPENID), // 排除自己
|
||||
'pets.0': _.exists(true), // 至少有一只宠物
|
||||
},
|
||||
})
|
||||
.limit(30)
|
||||
.get()
|
||||
|
||||
const nearby = usersRes.data
|
||||
.map(user => {
|
||||
const userLat = user.lastLocation?.latitude
|
||||
const userLng = user.lastLocation?.longitude
|
||||
if (!userLat || !userLng) return null
|
||||
|
||||
const distance = calcDistance(latitude, longitude, userLat, userLng)
|
||||
if (distance > radiusKm * 1000) return null
|
||||
|
||||
return {
|
||||
userId: user.openid,
|
||||
user,
|
||||
pet: user.pets?.[0] || null,
|
||||
distance,
|
||||
isOnline: user.isOnline && user.lastSeen > onlineThreshold,
|
||||
location: { latitude: userLat, longitude: userLng },
|
||||
}
|
||||
.skip(page * pageSize)
|
||||
.limit(pageSize)
|
||||
.project({
|
||||
openid: false,
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.distance - b.distance)
|
||||
.end()
|
||||
|
||||
return { code: 0, data: nearby }
|
||||
// 在线状态:5 分钟内活跃
|
||||
const fiveMin = 5 * 60 * 1000
|
||||
const now = Date.now()
|
||||
|
||||
const result = list.map(u => ({
|
||||
userId: u._id,
|
||||
user: u,
|
||||
pet: u.pets?.[0] || null,
|
||||
distance: Math.round(u.dist),
|
||||
distanceText: formatDist(u.dist),
|
||||
isOnline: u.lastSeen && (now - new Date(u.lastSeen).getTime()) < fiveMin,
|
||||
location: {
|
||||
latitude: u.lastLocation?.coordinates?.[1],
|
||||
longitude: u.lastLocation?.coordinates?.[0],
|
||||
},
|
||||
}))
|
||||
|
||||
return { code: 0, data: result }
|
||||
} catch (e) {
|
||||
return { code: -1, message: e.message }
|
||||
}
|
||||
}
|
||||
|
||||
function formatDist(meters) {
|
||||
if (!meters && meters !== 0) return '未知'
|
||||
if (meters < 1000) return Math.round(meters) + 'm'
|
||||
return (meters / 1000).toFixed(1) + 'km'
|
||||
}
|
||||
|
||||
80
cloudfunctions/getNotifications/index.js
Normal file
80
cloudfunctions/getNotifications/index.js
Normal file
@@ -0,0 +1,80 @@
|
||||
const cloud = require('wx-server-sdk')
|
||||
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
const db = cloud.database()
|
||||
const _ = db.command
|
||||
|
||||
// 项目暂未建立独立的通知集合,这里通过聚合"我的动态"收到的点赞 / 评论
|
||||
// 实时合成通知列表;已读状态通过用户文档上的 lastReadNotificationsAt 时间戳比对得出
|
||||
exports.main = async (event, context) => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
|
||||
try {
|
||||
const meRes = await db.collection('users').where({ openid: OPENID }).get()
|
||||
if (meRes.data.length === 0) return { code: 0, data: [] }
|
||||
const me = meRes.data[0]
|
||||
const lastRead = me.lastReadNotificationsAt ? new Date(me.lastReadNotificationsAt).getTime() : 0
|
||||
|
||||
const myPostsRes = await db.collection('posts')
|
||||
.where({ authorId: OPENID })
|
||||
.field({ _id: true })
|
||||
.orderBy('createdAt', 'desc')
|
||||
.limit(50)
|
||||
.get()
|
||||
const myPostIds = myPostsRes.data.map(p => p._id)
|
||||
|
||||
const items = []
|
||||
|
||||
if (myPostIds.length) {
|
||||
const [likesRes, commentsRes] = await Promise.all([
|
||||
db.collection('likes')
|
||||
.where({ postId: _.in(myPostIds), userId: _.neq(OPENID) })
|
||||
.orderBy('createdAt', 'desc')
|
||||
.limit(20)
|
||||
.get(),
|
||||
db.collection('comments')
|
||||
.where({ postId: _.in(myPostIds), authorId: _.neq(OPENID) })
|
||||
.orderBy('createdAt', 'desc')
|
||||
.limit(20)
|
||||
.get(),
|
||||
])
|
||||
|
||||
likesRes.data.forEach(l => items.push({
|
||||
_id: `like_${l._id}`,
|
||||
fromId: l.userId,
|
||||
postId: l.postId,
|
||||
action: '点赞',
|
||||
target: '动态',
|
||||
createdAt: l.createdAt,
|
||||
}))
|
||||
|
||||
commentsRes.data.forEach(c => items.push({
|
||||
_id: `comment_${c._id}`,
|
||||
fromId: c.authorId,
|
||||
postId: c.postId,
|
||||
action: '评论',
|
||||
target: '动态',
|
||||
createdAt: c.createdAt,
|
||||
}))
|
||||
}
|
||||
|
||||
items.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
const sliced = items.slice(0, 30)
|
||||
|
||||
const fromIds = [...new Set(sliced.map(i => i.fromId).filter(Boolean))]
|
||||
const usersRes = fromIds.length
|
||||
? await db.collection('users').where({ openid: _.in(fromIds) }).field({ openid: true, nickName: true }).get()
|
||||
: { data: [] }
|
||||
const nickMap = {}
|
||||
usersRes.data.forEach(u => { nickMap[u.openid] = u.nickName })
|
||||
|
||||
const result = sliced.map(i => ({
|
||||
...i,
|
||||
fromNick: nickMap[i.fromId] || '一位用户',
|
||||
isRead: new Date(i.createdAt).getTime() <= lastRead,
|
||||
}))
|
||||
|
||||
return { code: 0, data: result }
|
||||
} catch (e) {
|
||||
return { code: -1, message: e.message }
|
||||
}
|
||||
}
|
||||
1
cloudfunctions/getNotifications/package.json
Normal file
1
cloudfunctions/getNotifications/package.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "name": "getNotifications", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } }
|
||||
@@ -2,49 +2,119 @@ const cloud = require('wx-server-sdk')
|
||||
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
const db = cloud.database()
|
||||
const _ = db.command
|
||||
const $ = db.command.aggregate
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
// 按 where 条件取一页,时间倒序(关注流 / 个人主页 / 附近兜底共用)
|
||||
async function listByWhere(where, skip) {
|
||||
const col = db.collection('posts').where(where)
|
||||
const countRes = await col.count()
|
||||
const res = await col
|
||||
.orderBy('createdAt', 'desc')
|
||||
.skip(skip)
|
||||
.limit(PAGE_SIZE)
|
||||
.get()
|
||||
return {
|
||||
posts: res.data,
|
||||
total: countRes.total,
|
||||
hasMore: skip + res.data.length < countRes.total,
|
||||
}
|
||||
}
|
||||
|
||||
// 热门:热度分 = (点赞×3 + 评论×5 + 1) / (帖龄小时数 + 2)^1.5
|
||||
// 类 Hacker News 的重力衰减,互动加分、随时间下沉;同分按发布时间倒序
|
||||
async function listHot(skip) {
|
||||
const countRes = await db.collection('posts').count()
|
||||
const res = await db.collection('posts').aggregate()
|
||||
.addFields({
|
||||
hotScore: $.divide([
|
||||
$.add([
|
||||
$.multiply([$.ifNull(['$likeCount', 0]), 3]),
|
||||
$.multiply([$.ifNull(['$commentCount', 0]), 5]),
|
||||
1,
|
||||
]),
|
||||
$.pow([
|
||||
$.add([
|
||||
$.divide([$.subtract([new Date(), '$createdAt']), 3600000]),
|
||||
2,
|
||||
]),
|
||||
1.5,
|
||||
]),
|
||||
]),
|
||||
})
|
||||
.sort({ hotScore: -1, createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(PAGE_SIZE)
|
||||
.end()
|
||||
return {
|
||||
posts: res.list,
|
||||
total: countRes.total,
|
||||
hasMore: skip + res.list.length < countRes.total,
|
||||
}
|
||||
}
|
||||
|
||||
// 附近:geo.near 按距离由近到远,依赖 posts.geo 的地理位置索引;
|
||||
// 索引未建或查询失败时回退为矩形围栏 + 时间倒序。
|
||||
// $near 不支持 count,用多取一条判断 hasMore,total 为近似值
|
||||
async function listNearby(latitude, longitude, skip) {
|
||||
try {
|
||||
const res = await db.collection('posts')
|
||||
.where({
|
||||
geo: _.geoNear({
|
||||
geometry: db.Geo.Point(longitude, latitude),
|
||||
maxDistance: 10000,
|
||||
}),
|
||||
})
|
||||
.skip(skip)
|
||||
.limit(PAGE_SIZE + 1)
|
||||
.get()
|
||||
const hasMore = res.data.length > PAGE_SIZE
|
||||
const posts = hasMore ? res.data.slice(0, PAGE_SIZE) : res.data
|
||||
return { posts, total: skip + posts.length + (hasMore ? 1 : 0), hasMore }
|
||||
} catch (e) {
|
||||
return listByWhere(
|
||||
_.and([
|
||||
{ 'location.latitude': _.gt(latitude - 0.1) },
|
||||
{ 'location.latitude': _.lt(latitude + 0.1) },
|
||||
{ 'location.longitude': _.gt(longitude - 0.15) },
|
||||
{ 'location.longitude': _.lt(longitude + 0.15) },
|
||||
]),
|
||||
skip
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
exports.main = async (event, context) => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
const { page = 0, type = 'hot', userId, latitude, longitude } = event
|
||||
const skip = page * PAGE_SIZE
|
||||
|
||||
try {
|
||||
let query = db.collection('posts')
|
||||
const skip = page * PAGE_SIZE
|
||||
let result
|
||||
|
||||
if (userId) {
|
||||
// 某用户的帖子
|
||||
query = query.where({ authorId: userId })
|
||||
result = await listByWhere({ authorId: userId }, skip)
|
||||
} else if (type === 'feed') {
|
||||
// 关注的用户帖子
|
||||
// 关注流:我关注的人 + 自己
|
||||
const followsRes = await db.collection('follows')
|
||||
.where({ followerId: OPENID })
|
||||
.field({ followeeId: true })
|
||||
.get()
|
||||
const followeeIds = followsRes.data.map(f => f.followeeId)
|
||||
followeeIds.push(OPENID)
|
||||
query = query.where({ authorId: _.in(followeeIds) })
|
||||
result = await listByWhere({ authorId: _.in(followeeIds) }, skip)
|
||||
} else if (type === 'nearby' && latitude && longitude) {
|
||||
// 附近帖子:使用地理围栏查询(简化版,用 geo_near)
|
||||
query = query.where(
|
||||
_.and([
|
||||
{ 'location.latitude': _.gt(latitude - 0.1) },
|
||||
{ 'location.latitude': _.lt(latitude + 0.1) },
|
||||
{ 'location.longitude': _.gt(longitude - 0.15) },
|
||||
{ 'location.longitude': _.lt(longitude + 0.15) },
|
||||
])
|
||||
)
|
||||
result = await listNearby(latitude, longitude, skip)
|
||||
} else {
|
||||
result = await listHot(skip)
|
||||
}
|
||||
// type === 'hot' 全量按热度排序
|
||||
|
||||
const total = await query.count()
|
||||
const postsRes = await query
|
||||
.orderBy('createdAt', 'desc')
|
||||
.skip(skip)
|
||||
.limit(PAGE_SIZE)
|
||||
.get()
|
||||
const { posts, total, hasMore } = result
|
||||
|
||||
const posts = postsRes.data
|
||||
if (posts.length === 0) {
|
||||
return { code: 0, data: { list: [], total, hasMore: false } }
|
||||
}
|
||||
|
||||
// 批量获取作者信息
|
||||
const authorIds = [...new Set(posts.map(p => p.authorId))]
|
||||
@@ -71,11 +141,7 @@ exports.main = async (event, context) => {
|
||||
|
||||
return {
|
||||
code: 0,
|
||||
data: {
|
||||
list: enriched,
|
||||
total: total.total,
|
||||
hasMore: skip + posts.length < total.total,
|
||||
},
|
||||
data: { list: enriched, total, hasMore },
|
||||
}
|
||||
} catch (e) {
|
||||
return { code: -1, message: e.message }
|
||||
|
||||
51
cloudfunctions/getStory/index.js
Normal file
51
cloudfunctions/getStory/index.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const cloud = require('wx-server-sdk')
|
||||
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
const db = cloud.database()
|
||||
const _ = db.command
|
||||
const ONE_DAY_MS = 24 * 3600 * 1000
|
||||
|
||||
// 项目暂未建立独立的"故事"集合,这里把指定用户最近(24小时内)发布的
|
||||
// 带图片动态包装成"故事"展示,作为 Stories 功能的轻量实现
|
||||
exports.main = async (event, context) => {
|
||||
const id = (event && event.id) || ''
|
||||
if (!id) return { code: -1, message: '缺少用户ID' }
|
||||
|
||||
try {
|
||||
const since = new Date(Date.now() - ONE_DAY_MS)
|
||||
let postsRes = await db.collection('posts')
|
||||
.where({ authorId: id, createdAt: _.gte(since) })
|
||||
.orderBy('createdAt', 'desc')
|
||||
.limit(20)
|
||||
.get()
|
||||
|
||||
let posts = postsRes.data.filter(p => Array.isArray(p.images) && p.images.length > 0)
|
||||
|
||||
if (posts.length === 0) {
|
||||
// 兜底:取该用户最近一条带图动态
|
||||
const fallbackRes = await db.collection('posts')
|
||||
.where({ authorId: id })
|
||||
.orderBy('createdAt', 'desc')
|
||||
.limit(20)
|
||||
.get()
|
||||
posts = fallbackRes.data.filter(p => Array.isArray(p.images) && p.images.length > 0).slice(0, 1)
|
||||
}
|
||||
|
||||
if (posts.length === 0) return { code: 0, data: [] }
|
||||
|
||||
const userRes = await db.collection('users').where({ openid: id }).field({ nickName: true }).get()
|
||||
const authorName = (userRes.data[0] && userRes.data[0].nickName) || '宠物爱好者'
|
||||
|
||||
const stories = posts.map(p => ({
|
||||
_id: p._id,
|
||||
authorId: p.authorId,
|
||||
authorName,
|
||||
imageUrl: p.images[0],
|
||||
text: p.content || '',
|
||||
createdAt: p.createdAt,
|
||||
}))
|
||||
|
||||
return { code: 0, data: stories }
|
||||
} catch (e) {
|
||||
return { code: -1, message: e.message }
|
||||
}
|
||||
}
|
||||
1
cloudfunctions/getStory/package.json
Normal file
1
cloudfunctions/getStory/package.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "name": "getStory", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } }
|
||||
40
cloudfunctions/getUser/index.js
Normal file
40
cloudfunctions/getUser/index.js
Normal file
@@ -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 }
|
||||
}
|
||||
}
|
||||
1
cloudfunctions/getUser/package.json
Normal file
1
cloudfunctions/getUser/package.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "name": "getUser", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } }
|
||||
19
cloudfunctions/markAllRead/index.js
Normal file
19
cloudfunctions/markAllRead/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const cloud = require('wx-server-sdk')
|
||||
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
const db = cloud.database()
|
||||
|
||||
// 与 getNotifications 配套:把"已读截止时间"写到用户文档上,
|
||||
// 之后凡是早于该时间的通知都视为已读
|
||||
exports.main = async (event, context) => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
|
||||
try {
|
||||
await db.collection('users')
|
||||
.where({ openid: OPENID })
|
||||
.update({ data: { lastReadNotificationsAt: db.serverDate() } })
|
||||
|
||||
return { code: 0, data: null }
|
||||
} catch (e) {
|
||||
return { code: -1, message: e.message }
|
||||
}
|
||||
}
|
||||
1
cloudfunctions/markAllRead/package.json
Normal file
1
cloudfunctions/markAllRead/package.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "name": "markAllRead", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } }
|
||||
52
cloudfunctions/searchPosts/index.js
Normal file
52
cloudfunctions/searchPosts/index.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const cloud = require('wx-server-sdk')
|
||||
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
const db = cloud.database()
|
||||
const _ = db.command
|
||||
const RESULT_LIMIT = 20
|
||||
|
||||
function escapeRegExp(s) {
|
||||
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
exports.main = async (event, context) => {
|
||||
const { OPENID } = cloud.getWXContext()
|
||||
const keyword = String((event && event.keyword) || '').trim()
|
||||
if (!keyword) return { code: 0, data: [] }
|
||||
|
||||
try {
|
||||
const re = db.RegExp({ regexp: escapeRegExp(keyword), options: 'i' })
|
||||
const postsRes = await db.collection('posts')
|
||||
.where(_.or([{ content: re }, { hashtags: keyword }]))
|
||||
.orderBy('createdAt', 'desc')
|
||||
.limit(RESULT_LIMIT)
|
||||
.get()
|
||||
|
||||
const posts = postsRes.data
|
||||
if (posts.length === 0) return { code: 0, data: [] }
|
||||
|
||||
const authorIds = [...new Set(posts.map(p => p.authorId))]
|
||||
const authorsRes = await db.collection('users')
|
||||
.where({ openid: _.in(authorIds) })
|
||||
.field({ nickName: true, avatarUrl: true, openid: true, pets: true, location: true })
|
||||
.get()
|
||||
const authorMap = {}
|
||||
authorsRes.data.forEach(u => { authorMap[u.openid] = u })
|
||||
|
||||
const postIds = posts.map(p => p._id)
|
||||
const likesRes = await db.collection('likes')
|
||||
.where({ userId: OPENID, postId: _.in(postIds) })
|
||||
.field({ postId: true })
|
||||
.get()
|
||||
const likedSet = new Set(likesRes.data.map(l => l.postId))
|
||||
|
||||
const enriched = posts.map(p => ({
|
||||
...p,
|
||||
author: authorMap[p.authorId] || null,
|
||||
isLiked: likedSet.has(p._id),
|
||||
}))
|
||||
|
||||
return { code: 0, data: enriched }
|
||||
} catch (e) {
|
||||
return { code: -1, message: e.message }
|
||||
}
|
||||
}
|
||||
1
cloudfunctions/searchPosts/package.json
Normal file
1
cloudfunctions/searchPosts/package.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "name": "searchPosts", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } }
|
||||
27
cloudfunctions/searchUsers/index.js
Normal file
27
cloudfunctions/searchUsers/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const cloud = require('wx-server-sdk')
|
||||
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })
|
||||
const db = cloud.database()
|
||||
const _ = db.command
|
||||
const RESULT_LIMIT = 20
|
||||
|
||||
function escapeRegExp(s) {
|
||||
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
exports.main = async (event, context) => {
|
||||
const keyword = String((event && event.keyword) || '').trim()
|
||||
if (!keyword) return { code: 0, data: [] }
|
||||
|
||||
try {
|
||||
const re = db.RegExp({ regexp: escapeRegExp(keyword), options: 'i' })
|
||||
const res = await db.collection('users')
|
||||
.where(_.or([{ nickName: re }, { handle: re }, { location: re }]))
|
||||
.field({ nickName: true, avatarUrl: true, bio: true, handle: true, location: true })
|
||||
.limit(RESULT_LIMIT)
|
||||
.get()
|
||||
|
||||
return { code: 0, data: res.data }
|
||||
} catch (e) {
|
||||
return { code: -1, message: e.message }
|
||||
}
|
||||
}
|
||||
1
cloudfunctions/searchUsers/package.json
Normal file
1
cloudfunctions/searchUsers/package.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "name": "searchUsers", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } }
|
||||
32
cloudfunctions/sendMessage/index.js
Normal file
32
cloudfunctions/sendMessage/index.js
Normal file
@@ -0,0 +1,32 @@
|
||||
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 { toId, content, type = 'text' } = event || {}
|
||||
|
||||
if (!toId || !String(content || '').trim()) {
|
||||
return { code: -1, message: '参数错误' }
|
||||
}
|
||||
|
||||
try {
|
||||
const meRes = await db.collection('users').where({ openid: OPENID }).get()
|
||||
if (meRes.data.length === 0) return { code: -1, message: '用户不存在' }
|
||||
const myId = meRes.data[0]._id
|
||||
|
||||
const message = {
|
||||
fromId: myId,
|
||||
toId,
|
||||
content: String(content).trim(),
|
||||
type,
|
||||
isRead: false,
|
||||
createdAt: db.serverDate(),
|
||||
}
|
||||
const res = await db.collection('messages').add({ data: message })
|
||||
|
||||
return { code: 0, data: { _id: res._id, ...message } }
|
||||
} catch (e) {
|
||||
return { code: -1, message: e.message }
|
||||
}
|
||||
}
|
||||
1
cloudfunctions/sendMessage/package.json
Normal file
1
cloudfunctions/sendMessage/package.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "name": "sendMessage", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } }
|
||||
@@ -13,9 +13,10 @@ exports.main = async (event, context) => {
|
||||
.where({ openid: OPENID })
|
||||
.update({
|
||||
data: {
|
||||
lastLocation: { latitude, longitude },
|
||||
isOnline: true,
|
||||
// ✅ 改用 GeoPoint,才能用地理位置索引做距离查询
|
||||
lastLocation: db.Geo.Point(longitude, latitude), // 注意:Geo.Point 是 (lng, lat) 顺序
|
||||
lastSeen: db.serverDate(),
|
||||
isOnline: true,
|
||||
},
|
||||
})
|
||||
return { code: 0, data: null }
|
||||
|
||||
72
cloudfunctions/updatePet/index.js
Normal file
72
cloudfunctions/updatePet/index.js
Normal file
@@ -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 }
|
||||
}
|
||||
}
|
||||
1
cloudfunctions/updatePet/package.json
Normal file
1
cloudfunctions/updatePet/package.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "name": "updatePet", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } }
|
||||
27
cloudfunctions/updateProfile/index.js
Normal file
27
cloudfunctions/updateProfile/index.js
Normal file
@@ -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 }
|
||||
}
|
||||
}
|
||||
1
cloudfunctions/updateProfile/package.json
Normal file
1
cloudfunctions/updateProfile/package.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "name": "updateProfile", "version": "1.0.0", "main": "index.js", "dependencies": { "wx-server-sdk": "~2.1.2" } }
|
||||
11
i18n/base.json
Normal file
11
i18n/base.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ios": {
|
||||
"name": "汪圈"
|
||||
},
|
||||
"android": {
|
||||
"name": "汪圈"
|
||||
},
|
||||
"common": {
|
||||
"name": "汪圈"
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,8 @@
|
||||
],
|
||||
"tabBar": {
|
||||
"custom": true,
|
||||
"color": "#9b8fa8",
|
||||
"selectedColor": "#ff4f91",
|
||||
"color": "#b9bdc6",
|
||||
"selectedColor": "#2b2b30",
|
||||
"backgroundColor": "#ffffff",
|
||||
"list": [
|
||||
{ "pagePath": "pages/feed/feed", "text": "广场" },
|
||||
@@ -31,7 +31,7 @@
|
||||
"window": {
|
||||
"navigationStyle": "custom",
|
||||
"backgroundTextStyle": "dark",
|
||||
"backgroundColor": "#fff9fb"
|
||||
"backgroundColor": "#f1f2f6"
|
||||
},
|
||||
"permission": {
|
||||
"scope.userLocation": {
|
||||
@@ -41,7 +41,6 @@
|
||||
"requiredPrivateInfos": ["getLocation", "chooseLocation"],
|
||||
"sitemapLocation": "sitemap.json",
|
||||
"style": "v2",
|
||||
"componentFramework": "glass-easel",
|
||||
"lazyCodeLoading": "requiredComponents",
|
||||
"networkTimeout": {
|
||||
"request": 10000,
|
||||
|
||||
5
miniprogram/app.miniapp.json
Normal file
5
miniprogram/app.miniapp.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"adapteByMiniprogram": {
|
||||
"userName": "gh_994b4ce3ce01"
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,15 @@
|
||||
import { UserProfile, GeoPoint } from './types/index'
|
||||
import { getCachedUser, login } from './utils/auth'
|
||||
import { AppGlobalData } from './types/index'
|
||||
import { getCachedUser } 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
|
||||
@@ -39,16 +44,6 @@ App<{
|
||||
this._stopLocationHeartbeat()
|
||||
},
|
||||
|
||||
async _initLogin() {
|
||||
try {
|
||||
const userInfo = await login()
|
||||
this.globalData.userInfo = userInfo
|
||||
this.globalData.isLoggedIn = true
|
||||
} catch (e) {
|
||||
console.error('Login failed', e)
|
||||
}
|
||||
},
|
||||
|
||||
_initLocation() {
|
||||
wx.getLocation({
|
||||
type: 'gcj02',
|
||||
|
||||
@@ -1,38 +1,63 @@
|
||||
/* ── 全局设计令牌 ── */
|
||||
/* ── 全局设计令牌:Vital 风格(参考 vital_style_reference.md)──
|
||||
单一紫色主色 + 黑白灰层级:强调动作用黑,品牌用紫,其余靠灰度撑层级 */
|
||||
page {
|
||||
--pink: #ff4f91;
|
||||
--pink-light: #ffe5f0;
|
||||
--orange: #ff9f1c;
|
||||
--yellow: #ffe15a;
|
||||
--green: #2fd37a;
|
||||
--mint: #67e8c9;
|
||||
--blue: #4d8dff;
|
||||
--violet: #8c5cff;
|
||||
--ink: #272235;
|
||||
/* 主色板 */
|
||||
--purple: #8b7cf6;
|
||||
--purple-light: #a99bf8;
|
||||
--purple-deep: #7563e0;
|
||||
--accent-dark: #2b2b30;
|
||||
|
||||
--bg-primary: #fff9fb;
|
||||
--bg-page: linear-gradient(180deg, #fff8f2 0%, #fff4fb 50%, #f5f0ff 100%);
|
||||
/* 兼容旧变量名:六彩体系全部收敛到 Vital 色板,
|
||||
旧渐变 coral→tangerine 自动变为 紫→浅紫 */
|
||||
--coral: var(--purple);
|
||||
--tangerine: var(--purple-light);
|
||||
--lemon: var(--purple-light);
|
||||
--mint: #4bbe9a;
|
||||
--sky: var(--purple);
|
||||
--lilac: var(--purple);
|
||||
|
||||
--text-primary: #272235;
|
||||
--text-secondary: #6a6178;
|
||||
--text-tertiary: #9b8fa8;
|
||||
--pink: var(--purple);
|
||||
--pink-light: rgba(139, 124, 246, 0.12);
|
||||
--orange: var(--purple-light);
|
||||
--yellow: var(--purple-light);
|
||||
--green: var(--mint);
|
||||
--blue: var(--purple);
|
||||
--violet: var(--purple);
|
||||
--ink: #1b1b20;
|
||||
|
||||
--border-card: rgba(255, 255, 255, 0.72);
|
||||
--border-subtle: rgba(43, 37, 61, 0.08);
|
||||
--bg-primary: #f1f2f6;
|
||||
--bg-page: #f1f2f6;
|
||||
|
||||
--shadow-soft: 0 18rpx 40rpx rgba(95, 49, 104, 0.12);
|
||||
--shadow-card: 0 10rpx 28rpx rgba(78, 56, 96, 0.10);
|
||||
--shadow-pop: 0 10rpx 22rpx rgba(255, 79, 145, 0.28);
|
||||
--text-primary: #1b1b20;
|
||||
--text-secondary: #6e7280;
|
||||
--text-tertiary: #b9bdc6;
|
||||
|
||||
--radius-sm: 16rpx;
|
||||
--radius-md: 28rpx;
|
||||
--radius-lg: 44rpx;
|
||||
/* 紫底上的半透明白(头部胶囊、次级卡片) */
|
||||
--on-primary: rgba(255, 255, 255, 0.22);
|
||||
--on-primary-strong: rgba(255, 255, 255, 0.32);
|
||||
|
||||
/* 旧玻璃变量 → 实底白卡 */
|
||||
--glass: #ffffff;
|
||||
--glass-strong: #ffffff;
|
||||
--glass-edge: rgba(255, 255, 255, 0);
|
||||
|
||||
--border-card: rgba(255, 255, 255, 0);
|
||||
--border-subtle: rgba(27, 27, 32, 0.06);
|
||||
|
||||
/* 中性阴影:大模糊、低不透明度,光源一致来自上方 */
|
||||
--shadow-soft: 0 16rpx 48rpx rgba(27, 27, 32, 0.06);
|
||||
--shadow-card: 0 16rpx 48rpx rgba(27, 27, 32, 0.06);
|
||||
--shadow-pop: 0 12rpx 32rpx rgba(43, 43, 48, 0.22);
|
||||
|
||||
--radius-sm: 20rpx;
|
||||
--radius-md: 32rpx;
|
||||
--radius-lg: 48rpx;
|
||||
--radius-full: 9999rpx;
|
||||
|
||||
--tab-h: 112rpx;
|
||||
|
||||
font-family: "PingFang SC", "Helvetica Neue", Arial, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
background: var(--bg-page);
|
||||
color: var(--text-primary);
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
@@ -57,26 +82,92 @@ button::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* ── 通用卡片 ── */
|
||||
/* ── 通用卡片:纯白实底、大圆角、柔和中性阴影 ── */
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
border: 1rpx solid var(--border-card);
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-soft);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── 渐变按钮 ── */
|
||||
/* ── 旧玻璃工具类 → 实底白卡(Vital 无毛玻璃,靠层叠和阴影做深度) ── */
|
||||
.glass {
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.glass-strong {
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
/* ── 标签:白色小胶囊,不再旋转 ── */
|
||||
.sticker-tag {
|
||||
position: absolute;
|
||||
background: #ffffff;
|
||||
color: var(--text-primary);
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
padding: 10rpx 22rpx;
|
||||
border-radius: var(--radius-full);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.sticker-tag.tilt-r {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* ── 主按钮:黑色胶囊(Vital 的 CTA 用强调黑,不用主题色) ── */
|
||||
.btn-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--pink), var(--orange));
|
||||
background: var(--accent-dark);
|
||||
color: #fff;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
box-shadow: var(--shadow-pop);
|
||||
transition: transform 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: scale(0.98);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* ── 数字分级排版:整数超大加粗 + 小数/单位缩小变灰 ── */
|
||||
.num-xl {
|
||||
font-size: 96rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: -2rpx;
|
||||
line-height: 1;
|
||||
color: var(--text-primary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.num-sub {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-tertiary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ── 信息行:左数字、右说明 + chevron,中间渐隐细线 ── */
|
||||
.row-line {
|
||||
flex: 1;
|
||||
height: 2rpx;
|
||||
margin: 0 20rpx;
|
||||
background: linear-gradient(90deg, rgba(27, 27, 32, 0.10), rgba(27, 27, 32, 0.02));
|
||||
}
|
||||
|
||||
.chevron {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-top: 4rpx solid var(--text-tertiary);
|
||||
border-right: 4rpx solid var(--text-tertiary);
|
||||
transform: rotate(45deg);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── 页面顶部状态栏占位 ── */
|
||||
@@ -84,39 +175,44 @@ button::after {
|
||||
height: env(safe-area-inset-top, 44rpx);
|
||||
}
|
||||
|
||||
/* ── 紫色渐变淡出头部:从顶部主色向下融入页面背景,无分界线 ── */
|
||||
.hero-fade {
|
||||
background: linear-gradient(180deg, #9181f4 0%, #a99bf8 55%, rgba(169, 155, 248, 0) 100%);
|
||||
}
|
||||
|
||||
/* ── 滚动区通用 ── */
|
||||
.scroll-view {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ── Hashtag 标签 ── */
|
||||
/* ── Hashtag 标签:主色淡紫底 ── */
|
||||
.hashtag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 22rpx;
|
||||
font-weight: 800;
|
||||
color: #8b3cff;
|
||||
background: #f0e8ff;
|
||||
font-weight: 600;
|
||||
color: var(--purple-deep);
|
||||
background: var(--pink-light);
|
||||
border-radius: var(--radius-full);
|
||||
padding: 6rpx 14rpx;
|
||||
}
|
||||
|
||||
/* ── 在线绿点 ── */
|
||||
/* ── 在线绿点(语义色,保留但降饱和) ── */
|
||||
.online-dot {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--green);
|
||||
background: var(--mint);
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 0 5rpx rgba(47, 211, 122, 0.18);
|
||||
box-shadow: 0 0 0 5rpx rgba(75, 190, 154, 0.16);
|
||||
}
|
||||
|
||||
/* ── 骨架屏占位 ── */
|
||||
/* ── 骨架屏占位:中性冷灰 ── */
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #f0eef5 25%, #e8e4f0 50%, #f0eef5 75%);
|
||||
background: linear-gradient(90deg, #ebecf1 25%, #e2e4ea 50%, #ebecf1 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-shine 1.4s infinite;
|
||||
border-radius: 8rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
@keyframes skeleton-shine {
|
||||
@@ -124,6 +220,84 @@ button::after {
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* ── 固定顶栏:液态玻璃,置于图层顶层,高度固定(状态栏 + 导航栏) ──
|
||||
下层内容滚动/下拉时始终钉在顶部 */
|
||||
.glass-topbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background:
|
||||
radial-gradient(120% 180% at 18% -50%, rgba(255, 255, 255, 0.42), transparent 60%),
|
||||
rgba(255, 255, 255, 0.52);
|
||||
backdrop-filter: blur(40rpx) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(40rpx) saturate(180%);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.55);
|
||||
box-shadow:
|
||||
0 8rpx 28rpx rgba(27, 27, 32, 0.05),
|
||||
inset 0 1rpx 0 rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* 顶栏上的标题:玻璃上用深色文字 */
|
||||
.glass-topbar .topbar-logo,
|
||||
.glass-topbar .navbar-title {
|
||||
color: #1b1b20;
|
||||
}
|
||||
|
||||
.glass-topbar .logo-accent {
|
||||
color: var(--purple);
|
||||
}
|
||||
|
||||
/* ── 顶栏图标按钮:线条化图标,右侧紧贴小程序胶囊按钮 ── */
|
||||
.navbar-action {
|
||||
margin-left: auto;
|
||||
margin-right: 8rpx;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.navbar-action:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 线条图标:单色 alpha 蒙版 + background-color 上色 */
|
||||
.icon-line {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
background-color: #1b1b20;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
-webkit-mask-size: contain;
|
||||
}
|
||||
|
||||
/* 搜索:线条放大镜 */
|
||||
.icon-search-line {
|
||||
-webkit-mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OCA0OCI+PGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjMuNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIj48Y2lyY2xlIGN4PSIyMSIgY3k9IjIxIiByPSIxMi41Ii8+PHBhdGggZD0iTTMwLjUgMzAuNUw0MSA0MSIvPjwvZz48L3N2Zz4=");
|
||||
}
|
||||
|
||||
/* 通知:线条铃铛 */
|
||||
.icon-bell-line {
|
||||
-webkit-mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OCA0OCI+PGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjMuNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMjQgN2ExMyAxMyAwIDAgMC0xMyAxM3Y3LjVMNyAzNWgzNGwtNC03LjVWMjBBMTMgMTMgMCAwIDAgMjQgN1oiLz48cGF0aCBkPSJNMTkuNSAzOC41YTQuOCA0LjggMCAwIDAgOSAwIi8+PC9nPjwvc3ZnPg==");
|
||||
}
|
||||
|
||||
/* 图标按钮上的未读红点 */
|
||||
.navbar-action.has-badge::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 6rpx;
|
||||
right: 6rpx;
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--purple);
|
||||
border: 2rpx solid #fff;
|
||||
}
|
||||
|
||||
/* ── 安全区 ── */
|
||||
.safe-top {
|
||||
background: transparent;
|
||||
|
||||
@@ -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'] })
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<view class="post-card" bindtap="onCardTap">
|
||||
<!-- 作者头部 -->
|
||||
<view class="post-header">
|
||||
<view class="post-avatar" style="background: {{avatarGradient}}" bindtap="onAuthorTap" catchtap="onAuthorTap">
|
||||
<text>{{post.author.nickName[0] || '?'}}</text>
|
||||
<view class="post-avatar" style="background: {{avatarGradient}}" catchtap="onAuthorTap">
|
||||
<text>{{post.author && post.author.nickName ? post.author.nickName[0] : '?'}}</text>
|
||||
</view>
|
||||
<view class="post-meta">
|
||||
<text class="post-username">{{post.author.nickName}}</text>
|
||||
<text class="post-username">{{post.author && post.author.nickName ? post.author.nickName : '匿名用户'}}</text>
|
||||
<view class="post-loc" wx:if="{{post.location}}">
|
||||
<text class="loc-icon">📍</text>
|
||||
<text class="loc-text">{{post.location.name}}</text>
|
||||
@@ -17,19 +17,19 @@
|
||||
<!-- 图片 -->
|
||||
<view wx:if="{{post.images && post.images.length > 0}}" class="post-images-wrap">
|
||||
<!-- 单图全宽 -->
|
||||
<view wx:if="{{post.images.length === 1}}" class="post-img-single">
|
||||
<view wx:if="{{post.images && post.images.length === 1}}" class="post-img-single">
|
||||
<image
|
||||
class="img-single"
|
||||
src="{{post.images[0]}}"
|
||||
mode="aspectFill"
|
||||
lazy-load="true"
|
||||
bindtap="onImageTap"
|
||||
catchtap="onImageTap"
|
||||
data-idx="0"
|
||||
/>
|
||||
<view wx:if="{{post.mood}}" class="post-tag-chip">{{post.mood}}</view>
|
||||
</view>
|
||||
<!-- 多图网格 -->
|
||||
<view wx:else class="post-img-grid post-img-grid-{{post.images.length > 3 ? 'multi' : post.images.length}}">
|
||||
<view wx:else class="post-img-grid {{imageGridClass}}">
|
||||
<image
|
||||
wx:for="{{post.images}}"
|
||||
wx:key="index"
|
||||
@@ -38,7 +38,7 @@
|
||||
src="{{item}}"
|
||||
mode="aspectFill"
|
||||
lazy-load="true"
|
||||
bindtap="onImageTap"
|
||||
catchtap="onImageTap"
|
||||
data-idx="{{index}}"
|
||||
/>
|
||||
</view>
|
||||
@@ -55,15 +55,15 @@
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<view class="post-actions">
|
||||
<view class="action-btn {{post.isLiked ? 'liked' : ''}}" bindtap="onLike" catchtap="onLike">
|
||||
<view class="action-btn {{post.isLiked ? 'liked' : ''}}" catchtap="onLike">
|
||||
<text class="action-icon">{{post.isLiked ? '❤️' : '🤍'}}</text>
|
||||
<text class="action-count">{{likeCount}}</text>
|
||||
</view>
|
||||
<view class="action-btn" bindtap="onComment" catchtap="onComment">
|
||||
<view class="action-btn" catchtap="onComment">
|
||||
<text class="action-icon">💬</text>
|
||||
<text class="action-count">{{post.commentCount}}</text>
|
||||
<text class="action-count">{{post.commentCount || 0}}</text>
|
||||
</view>
|
||||
<view class="action-btn" bindtap="onShare" catchtap="onShare">
|
||||
<view class="action-btn" catchtap="onShare">
|
||||
<text class="action-icon">↗</text>
|
||||
<text class="action-count">转发</text>
|
||||
</view>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* ── 帖子卡片:纯白实底、大圆角、中性柔和阴影(Vital 风格) ── */
|
||||
.post-card {
|
||||
margin: 0 28rpx 32rpx;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
background: #ffffff;
|
||||
border-radius: 48rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 18rpx 40rpx rgba(95, 49, 104, 0.12);
|
||||
box-shadow: 0 16rpx 48rpx rgba(27, 27, 32, 0.06);
|
||||
}
|
||||
|
||||
/* 头部 */
|
||||
@@ -16,17 +16,16 @@
|
||||
}
|
||||
|
||||
.post-avatar {
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
border-radius: 28rpx;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 800;
|
||||
color: rgba(39, 34, 53, 0.72);
|
||||
font-weight: 700;
|
||||
color: rgba(27, 27, 32, 0.65);
|
||||
flex-shrink: 0;
|
||||
box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
@@ -37,8 +36,8 @@
|
||||
.post-username {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
color: #272235;
|
||||
font-weight: 600;
|
||||
color: #1b1b20;
|
||||
}
|
||||
|
||||
.post-loc {
|
||||
@@ -52,7 +51,7 @@
|
||||
|
||||
.loc-text {
|
||||
font-size: 22rpx;
|
||||
color: #9b8fa8;
|
||||
color: #b9bdc6;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -60,7 +59,7 @@
|
||||
|
||||
.post-time {
|
||||
font-size: 22rpx;
|
||||
color: #9b8fa8;
|
||||
color: #b9bdc6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -73,19 +72,21 @@
|
||||
width: 100%;
|
||||
height: 360rpx;
|
||||
display: block;
|
||||
background: #f0eef5;
|
||||
background: #eef0f5;
|
||||
}
|
||||
|
||||
/* 心情标签:白色小胶囊,不旋转 */
|
||||
.post-tag-chip {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
left: 20rpx;
|
||||
background: rgba(39, 34, 53, 0.88);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
font-weight: 800;
|
||||
padding: 8rpx 18rpx;
|
||||
top: 22rpx;
|
||||
left: 22rpx;
|
||||
background: #ffffff;
|
||||
color: #1b1b20;
|
||||
font-size: 21rpx;
|
||||
font-weight: 600;
|
||||
padding: 10rpx 22rpx;
|
||||
border-radius: 999rpx;
|
||||
box-shadow: 0 10rpx 26rpx rgba(27, 27, 32, 0.12);
|
||||
}
|
||||
|
||||
.post-img-grid {
|
||||
@@ -116,7 +117,7 @@
|
||||
.img-grid-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f0eef5;
|
||||
background: #eef0f5;
|
||||
}
|
||||
|
||||
/* 正文区 */
|
||||
@@ -127,7 +128,7 @@
|
||||
.post-caption {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #272235;
|
||||
color: #1b1b20;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
@@ -143,9 +144,9 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 22rpx;
|
||||
font-weight: 800;
|
||||
color: #8b3cff;
|
||||
background: #f0e8ff;
|
||||
font-weight: 600;
|
||||
color: #7563e0;
|
||||
background: rgba(139, 124, 246, 0.12);
|
||||
border-radius: 999rpx;
|
||||
padding: 6rpx 14rpx;
|
||||
}
|
||||
@@ -153,9 +154,9 @@
|
||||
/* 操作栏 */
|
||||
.post-actions {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
padding-top: 18rpx;
|
||||
border-top: 1rpx solid rgba(43, 37, 61, 0.07);
|
||||
gap: 14rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid rgba(27, 27, 32, 0.06);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
@@ -163,16 +164,23 @@
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
color: #6a6178;
|
||||
background: #f7f3ff;
|
||||
font-weight: 600;
|
||||
color: #6e7280;
|
||||
background: #f1f2f6;
|
||||
border-radius: 999rpx;
|
||||
padding: 10rpx 18rpx;
|
||||
padding: 12rpx 22rpx;
|
||||
transition: transform 0.18s ease, background 0.18s ease, color 0.18s ease;
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 已赞:强调黑胶囊(动作强调不用主题色) */
|
||||
.action-btn.liked {
|
||||
color: #ff4f91;
|
||||
background: #ffe5f0;
|
||||
color: #fff;
|
||||
background: #2b2b30;
|
||||
box-shadow: 0 10rpx 24rpx rgba(43, 43, 48, 0.24);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
@@ -182,4 +190,5 @@
|
||||
|
||||
.action-count {
|
||||
font-size: 24rpx;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ Component({
|
||||
|
||||
methods: {
|
||||
onTab(e: WechatMiniprogram.CustomEvent) {
|
||||
const index = e.currentTarget.dataset.index as number
|
||||
// dataset 取出的是字符串,必须转数字,否则 WXML 中 selected === n 严格比较失败
|
||||
const index = Number(e.currentTarget.dataset.index)
|
||||
const path = e.currentTarget.dataset.path as string
|
||||
if (index === this.data.selected) return
|
||||
this.setData({ selected: index })
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
<view class="tab-bar">
|
||||
|
||||
<view class="tab-dock">
|
||||
<!-- 广场 -->
|
||||
<view class="tab-item {{selected === 0 ? 'active' : ''}}" bindtap="onTab" data-index="0" data-path="/pages/feed/feed">
|
||||
<view class="tab-pill {{selected === 0 ? 'active' : ''}}" bindtap="onTab" data-index="0" data-path="/pages/feed/feed">
|
||||
<view class="icon-wrap">
|
||||
<view class="icon-grid">
|
||||
<view class="sq"></view><view class="sq"></view>
|
||||
<view class="sq"></view><view class="sq"></view>
|
||||
</view>
|
||||
<view class="tab-icon icon-feed"></view>
|
||||
</view>
|
||||
<text class="tab-label">广场</text>
|
||||
</view>
|
||||
|
||||
<!-- 附近 -->
|
||||
<view class="tab-item {{selected === 1 ? 'active' : ''}}" bindtap="onTab" data-index="1" data-path="/pages/nearby/nearby">
|
||||
<view class="tab-pill {{selected === 1 ? 'active' : ''}}" bindtap="onTab" data-index="1" data-path="/pages/nearby/nearby">
|
||||
<view class="icon-wrap">
|
||||
<view class="icon-pin">
|
||||
<view class="pin-head"></view>
|
||||
<view class="pin-tail"></view>
|
||||
</view>
|
||||
<view class="tab-icon icon-nearby"></view>
|
||||
</view>
|
||||
<text class="tab-label">附近</text>
|
||||
</view>
|
||||
@@ -25,30 +18,23 @@
|
||||
<!-- 发布 FAB -->
|
||||
<view class="tab-fab-wrap">
|
||||
<view class="tab-fab" bindtap="onPost">
|
||||
<text class="fab-plus">+</text>
|
||||
<view class="fab-paw"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 汪友 -->
|
||||
<view class="tab-item {{selected === 2 ? 'active' : ''}}" bindtap="onTab" data-index="2" data-path="/pages/friends/friends">
|
||||
<view class="tab-pill {{selected === 2 ? 'active' : ''}}" bindtap="onTab" data-index="2" data-path="/pages/friends/friends">
|
||||
<view class="icon-wrap" style="position:relative">
|
||||
<view class="icon-ppl">
|
||||
<view class="ppl-head ppl-head-l"></view>
|
||||
<view class="ppl-head ppl-head-r"></view>
|
||||
<view class="ppl-body"></view>
|
||||
</view>
|
||||
<view class="tab-icon icon-friends"></view>
|
||||
<view wx:if="{{unreadCount > 0}}" class="badge">{{unreadCount > 99 ? '99+' : unreadCount}}</view>
|
||||
</view>
|
||||
<text class="tab-label">汪友</text>
|
||||
</view>
|
||||
|
||||
<!-- 我的 -->
|
||||
<view class="tab-item {{selected === 3 ? 'active' : ''}}" bindtap="onTab" data-index="3" data-path="/pages/profile/profile">
|
||||
<view class="tab-pill {{selected === 3 ? 'active' : ''}}" bindtap="onTab" data-index="3" data-path="/pages/profile/profile">
|
||||
<view class="icon-wrap">
|
||||
<view class="icon-user">
|
||||
<view class="user-head"></view>
|
||||
<view class="user-body"></view>
|
||||
</view>
|
||||
<view class="tab-icon icon-profile"></view>
|
||||
</view>
|
||||
<text class="tab-label">我的</text>
|
||||
</view>
|
||||
|
||||
@@ -1,204 +1,157 @@
|
||||
/* ── 底部导航栏 ── */
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-top: 1rpx solid rgba(255, 255, 255, 0.65);
|
||||
background: rgba(255, 255, 255, 0.90);
|
||||
padding: 10rpx 0 calc(10rpx + env(safe-area-inset-bottom));
|
||||
box-shadow: 0 -10rpx 24rpx rgba(110, 78, 131, 0.08);
|
||||
/* ── 底部导航:白色悬浮浮岛(Vital 风格)──
|
||||
纯白实底 + 中性柔和阴影,激活态用强调黑胶囊,不用主题色 */
|
||||
.tab-dock {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
left: 16rpx;
|
||||
right: 16rpx;
|
||||
bottom: calc(28rpx + env(safe-area-inset-bottom) * 0.5);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* ── Tab 项 ── */
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5rpx;
|
||||
border-radius: 22rpx;
|
||||
padding: 8rpx 0 6rpx;
|
||||
justify-content: space-around;
|
||||
border-radius: 56rpx;
|
||||
padding: 12rpx 10rpx;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 20rpx 50rpx rgba(27, 27, 32, 0.10);
|
||||
}
|
||||
|
||||
/* ── Tab 项:展开胶囊 ──
|
||||
未选中 = 浅灰图标;选中 = 横向展开为黑色胶囊(白图标 + 白文字) */
|
||||
.tab-pill {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
border-radius: 999rpx;
|
||||
padding: 10rpx 16rpx;
|
||||
transition:
|
||||
background 0.32s cubic-bezier(.34, 1.4, .64, 1),
|
||||
padding 0.32s cubic-bezier(.34, 1.4, .64, 1),
|
||||
box-shadow 0.32s ease;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
background: linear-gradient(180deg, rgba(255, 79, 145, 0.12), rgba(255, 225, 90, 0.08));
|
||||
.tab-pill.active {
|
||||
background: #2b2b30;
|
||||
padding: 10rpx 26rpx;
|
||||
box-shadow: 0 12rpx 26rpx rgba(43, 43, 48, 0.28);
|
||||
}
|
||||
|
||||
.tab-pill:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
|
||||
/* 文字标签:默认折叠隐藏,选中时从图标右侧平滑展开 */
|
||||
.tab-label {
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
color: #9b8fa8;
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: transparent;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
margin-left: 0;
|
||||
transition:
|
||||
max-width 0.32s cubic-bezier(.34, 1.4, .64, 1),
|
||||
opacity 0.24s ease,
|
||||
margin-left 0.32s cubic-bezier(.34, 1.4, .64, 1);
|
||||
}
|
||||
|
||||
.tab-item.active .tab-label {
|
||||
color: #ff4f91;
|
||||
font-weight: 900;
|
||||
.tab-pill.active .tab-label {
|
||||
max-width: 140rpx;
|
||||
opacity: 1;
|
||||
color: #fff;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
/* ── 图标容器 ── */
|
||||
.icon-wrap {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
width: 66rpx;
|
||||
height: 66rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* ── 广场图标:2×2 圆角方块网格 ── */
|
||||
.icon-grid {
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
gap: 5rpx;
|
||||
/* ── Tab 图标:狗主题线性描边 SVG(base64 蒙版)──
|
||||
单色 alpha 蒙版 + background-color 上色,
|
||||
选中时浅灰→白平滑过渡,任意尺寸保持清晰 */
|
||||
.tab-icon {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
background-color: #b9bdc6;
|
||||
transition: background-color 0.2s;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
-webkit-mask-size: contain;
|
||||
}
|
||||
|
||||
.icon-grid .sq {
|
||||
background: #9b8fa8;
|
||||
border-radius: 4rpx;
|
||||
transition: background 0.2s;
|
||||
.tab-pill.active .tab-icon {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.tab-item.active .icon-grid .sq {
|
||||
background: #ff4f91;
|
||||
/* 广场:狗窝(拱形门 + 门上小骨头) */
|
||||
.icon-feed {
|
||||
-webkit-mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OCA0OCI+PGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjMuNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNNi41IDIxLjVWMzZhNS41IDUuNSAwIDAgMCA1LjUgNS41aDI0YTUuNSA1LjUgMCAwIDAgNS41LTUuNVYyMS41Ii8+PHBhdGggZD0iTTQgMTkuNUwyNCA0LjVMNDQgMTkuNSIvPjxwYXRoIGQ9Ik0xNy41IDQxLjV2LTcuNWE2LjUgNi41IDAgMCAxIDEzIDB2Ny41Ii8+PHBhdGggZD0iTTE5LjUgMTUuMmg5IiBzdHJva2Utd2lkdGg9IjQuNiIvPjwvZz48L3N2Zz4=");
|
||||
}
|
||||
|
||||
/* ── 附近图标:地图 Pin ── */
|
||||
.icon-pin {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
/* 附近:定位水滴 + 爪印 */
|
||||
.icon-nearby {
|
||||
-webkit-mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OCA0OCI+PHBhdGggZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjMuNCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZD0iTTI0IDQ0QzEzLjUgMzQgOCAyNi44IDggMjBhMTYgMTYgMCAwIDEgMzIgMEM0MCAyNi44IDM0LjUgMzQgMjQgNDRaIi8+PGcgZmlsbD0iIzAwMCI+PGNpcmNsZSBjeD0iMTcuNiIgY3k9IjE1LjQiIHI9IjIuMyIvPjxjaXJjbGUgY3g9IjI0IiBjeT0iMTMiIHI9IjIuNSIvPjxjaXJjbGUgY3g9IjMwLjQiIGN5PSIxNS40IiByPSIyLjMiLz48cGF0aCBkPSJNMjQgMTkuMmMyLjkgMCA1LjIgMS45IDUuMiA0LjNjMCAyLjQtMi4zIDQuMi01LjIgNC4yYy0yLjkgMC01LjItMS44LTUuMi00LjJjMC0yLjQgMi4zLTQuMyA1LjItNC4zWiIvPjwvZz48L3N2Zz4=");
|
||||
}
|
||||
|
||||
.pin-head {
|
||||
width: 22rpx;
|
||||
height: 22rpx;
|
||||
border-radius: 50% 50% 50% 0;
|
||||
background: #9b8fa8;
|
||||
transform: rotate(-45deg);
|
||||
transition: background 0.2s;
|
||||
/* 汪友:交叉双骨头(前后交叠,交叠处留缝) */
|
||||
.icon-friends {
|
||||
-webkit-mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OCA0OCI+PGRlZnM+PG1hc2sgaWQ9Im0iPjxyZWN0IHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgZmlsbD0iI2ZmZiIvPjxwYXRoIGQ9Ik0tNC4zNCAtMi42QTQuMiA0LjIgMCAxIDAgLTExLjIgMEE0LjIgNC4yIDAgMSAwIC00LjM0IDIuNkg0LjM0QTQuMiA0LjIgMCAxIDAgMTEuMiAwQTQuMiA0LjIgMCAxIDAgNC4zNCAtMi42WiIgZmlsbD0iIzAwMCIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjYuNSIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjQgMjQpIHJvdGF0ZSgtNDUpIHNjYWxlKDEuMykiLz48L21hc2s+PC9kZWZzPjxnIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIyLjUiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxnIG1hc2s9InVybCgjbSkiPjxwYXRoIGQ9Ik0tNC4zNCAtMi42QTQuMiA0LjIgMCAxIDAgLTExLjIgMEE0LjIgNC4yIDAgMSAwIC00LjM0IDIuNkg0LjM0QTQuMiA0LjIgMCAxIDAgMTEuMiAwQTQuMiA0LjIgMCAxIDAgNC4zNCAtMi42WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjQgMjQpIHJvdGF0ZSg0NSkgc2NhbGUoMS4zKSIvPjwvZz48cGF0aCBkPSJNLTQuMzQgLTIuNkE0LjIgNC4yIDAgMSAwIC0xMS4yIDBBNC4yIDQuMiAwIDEgMCAtNC4zNCAyLjZINC4zNEE0LjIgNC4yIDAgMSAwIDExLjIgMEE0LjIgNC4yIDAgMSAwIDQuMzQgLTIuNloiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI0IDI0KSByb3RhdGUoLTQ1KSBzY2FsZSgxLjMpIi8+PC9nPjwvc3ZnPg==");
|
||||
}
|
||||
|
||||
.pin-tail {
|
||||
width: 4rpx;
|
||||
height: 8rpx;
|
||||
background: #9b8fa8;
|
||||
border-radius: 0 0 4rpx 4rpx;
|
||||
margin-top: -2rpx;
|
||||
transition: background 0.2s;
|
||||
/* 我的:项圈 + 爱心狗牌 */
|
||||
.icon-profile {
|
||||
-webkit-mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OCA0OCI+PGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjMuNCI+PGNpcmNsZSBjeD0iMjQiIGN5PSIxNy41IiByPSIxMS40Ii8+PGNpcmNsZSBjeD0iMjQiIGN5PSIzNi41IiByPSI2LjQiLz48L2c+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTI0IDQwYy0yLjItMS42LTMuMy0yLjctMy4zLTRhMS45IDEuOSAwIDAgMSAzLjMtMS4yYTEuOSAxLjkgMCAwIDEgMy4zIDEuMmMwIDEuMy0xLjEgMi40LTMuMyA0WiIvPjwvc3ZnPg==");
|
||||
}
|
||||
|
||||
.tab-item.active .pin-head,
|
||||
.tab-item.active .pin-tail {
|
||||
background: #ff4f91;
|
||||
}
|
||||
|
||||
/* ── 汪友图标:双人轮廓 ── */
|
||||
.icon-ppl {
|
||||
width: 36rpx;
|
||||
height: 30rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ppl-head {
|
||||
position: absolute;
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
border-radius: 50%;
|
||||
background: #9b8fa8;
|
||||
top: 0;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.ppl-head-l { left: 2rpx; }
|
||||
.ppl-head-r { right: 2rpx; }
|
||||
|
||||
.ppl-body {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 14rpx;
|
||||
background: #9b8fa8;
|
||||
border-radius: 8rpx 8rpx 0 0;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.tab-item.active .ppl-head,
|
||||
.tab-item.active .ppl-body {
|
||||
background: #ff4f91;
|
||||
}
|
||||
|
||||
/* ── 我的图标:单人轮廓 ── */
|
||||
.icon-user {
|
||||
width: 28rpx;
|
||||
height: 34rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.user-head {
|
||||
width: 18rpx;
|
||||
height: 18rpx;
|
||||
border-radius: 50%;
|
||||
background: #9b8fa8;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.user-body {
|
||||
width: 28rpx;
|
||||
height: 14rpx;
|
||||
border-radius: 8rpx 8rpx 0 0;
|
||||
background: #9b8fa8;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.tab-item.active .user-head,
|
||||
.tab-item.active .user-body {
|
||||
background: #ff4f91;
|
||||
}
|
||||
|
||||
/* ── 发布 FAB ── */
|
||||
/* ── 发布按钮:黑色圆底 + 白色爪印(CTA 用强调黑) ── */
|
||||
.tab-fab-wrap {
|
||||
flex: 1;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 4rpx;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
.tab-fab {
|
||||
width: 92rpx;
|
||||
height: 92rpx;
|
||||
position: relative;
|
||||
width: 84rpx;
|
||||
height: 84rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #ff4f91, #ff9f1c 55%, #ffe15a);
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.90);
|
||||
box-shadow: 0 10rpx 22rpx rgba(255, 79, 145, 0.28);
|
||||
background: #2b2b30;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 4rpx;
|
||||
box-shadow: 0 10rpx 24rpx rgba(43, 43, 48, 0.30);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.fab-plus {
|
||||
font-size: 52rpx;
|
||||
color: #fff;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
margin-top: -4rpx;
|
||||
.tab-fab:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
/* ── 未读角标 ── */
|
||||
/* 白色爪印(base64 SVG),任意尺寸保持清晰 */
|
||||
.fab-paw {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OCA0OCI+PGcgZmlsbD0iI2ZmZiI+PGNpcmNsZSBjeD0iMTEuNSIgY3k9IjE4LjUiIHI9IjMuNiIvPjxjaXJjbGUgY3g9IjE5LjUiIGN5PSIxMy41IiByPSIzLjkiLz48Y2lyY2xlIGN4PSIyOC41IiBjeT0iMTMuNSIgcj0iMy45Ii8+PGNpcmNsZSBjeD0iMzYuNSIgY3k9IjE4LjUiIHI9IjMuNiIvPjxwYXRoIGQ9Ik0yNCAyMmM1IDAgOSAzLjQgOSA3LjZjMCA0LjItNCA3LjQtOSA3LjRjLTUgMC05LTMuMi05LTcuNGMwLTQuMiA0LTcuNiA5LTcuNloiLz48L2c+PC9zdmc+");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
/* ── 未读角标:主色紫 ── */
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -4rpx;
|
||||
@@ -206,10 +159,10 @@
|
||||
min-width: 30rpx;
|
||||
height: 30rpx;
|
||||
border-radius: 15rpx;
|
||||
background: #ff4f91;
|
||||
background: #8b7cf6;
|
||||
color: #fff;
|
||||
font-size: 18rpx;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { api } from '../../utils/api'
|
||||
import { AppGlobalData } from '../../types/index'
|
||||
|
||||
const app = getApp<{ userInfo: any }>()
|
||||
const app = getApp<{ globalData: AppGlobalData }>()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -17,6 +18,7 @@ Page({
|
||||
onLoad(query: { userId?: string }) {
|
||||
const info = wx.getSystemInfoSync()
|
||||
this.setData({ statusBarHeight: info.statusBarHeight, peerId: query.userId || '' })
|
||||
this.loadPeerInfo()
|
||||
this.loadMessages()
|
||||
this.subscribeRealtime()
|
||||
},
|
||||
@@ -25,9 +27,18 @@ Page({
|
||||
this._watcher?.close()
|
||||
},
|
||||
|
||||
async loadMessages() {
|
||||
async loadPeerInfo() {
|
||||
if (!this.data.peerId) return
|
||||
try {
|
||||
const res = await api.getMessages()
|
||||
const user = await api.getUserProfile(this.data.peerId)
|
||||
this.setData({ peerName: user.nickName || '宠物爱好者' })
|
||||
} catch {}
|
||||
},
|
||||
|
||||
async loadMessages() {
|
||||
if (!this.data.peerId) return
|
||||
try {
|
||||
const res = await api.getMessages(this.data.peerId)
|
||||
const msgs = res.list.map((m: any) => ({
|
||||
...m,
|
||||
isMe: m.fromId === app.globalData?.userInfo?._id,
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
.chat-page { display: flex; flex-direction: column; height: 100vh; background: #f5f0ff; }
|
||||
.chat-nav { display: flex; align-items: center; padding: 16rpx 28rpx; background: rgba(255,255,255,0.90); border-bottom: 1rpx solid rgba(43,37,61,0.08); }
|
||||
.nav-back { font-size: 60rpx; color: #272235; font-weight: 300; padding: 0 10rpx; line-height: 1; }
|
||||
.nav-title { flex: 1; font-size: 32rpx; font-weight: 900; color: #272235; text-align: center; }
|
||||
.nav-more { font-size: 36rpx; color: #9b8fa8; padding: 0 10rpx; }
|
||||
/* ── 聊天页:Vital 风格 ──
|
||||
我的气泡 = 主色紫,对方 = 白卡;发送按钮 = 强调黑 */
|
||||
.chat-page { display: flex; flex-direction: column; height: 100vh; background: #f1f2f6; }
|
||||
.chat-nav { display: flex; align-items: center; padding: 16rpx 28rpx; background: #ffffff; border-bottom: 1rpx solid rgba(27, 27, 32, 0.06); }
|
||||
.nav-back { font-size: 60rpx; color: #1b1b20; font-weight: 300; padding: 0 10rpx; line-height: 1; }
|
||||
.nav-title { flex: 1; font-size: 32rpx; font-weight: 700; color: #1b1b20; text-align: center; }
|
||||
.nav-more { font-size: 36rpx; color: #b9bdc6; padding: 0 10rpx; }
|
||||
.msg-scroll { flex: 1; padding: 20rpx 28rpx; }
|
||||
.msg-row { display: flex; margin-bottom: 20rpx; }
|
||||
.msg-right { justify-content: flex-end; }
|
||||
.msg-left { justify-content: flex-start; }
|
||||
.msg-bubble { max-width: 75%; padding: 18rpx 24rpx; border-radius: 28rpx; font-size: 28rpx; line-height: 1.5; }
|
||||
.bubble-me { background: linear-gradient(135deg, #ff4f91, #ff9f1c); color: #fff; border-bottom-right-radius: 8rpx; }
|
||||
.bubble-peer { background: rgba(255,255,255,0.90); color: #272235; border-bottom-left-radius: 8rpx; box-shadow: 0 6rpx 14rpx rgba(78,56,96,0.09); }
|
||||
.input-bar { display: flex; align-items: center; gap: 14rpx; padding: 16rpx 28rpx; padding-bottom: calc(16rpx + env(safe-area-inset-bottom)); background: rgba(255,255,255,0.92); border-top: 1rpx solid rgba(43,37,61,0.08); }
|
||||
.msg-input { flex: 1; height: 80rpx; background: rgba(255,255,255,0.80); border: 1rpx solid rgba(43,37,61,0.10); border-radius: 999rpx; padding: 0 24rpx; font-size: 28rpx; color: #272235; }
|
||||
.send-btn { background: linear-gradient(135deg, #ff4f91, #ff9f1c); color: #fff; border-radius: 999rpx; padding: 18rpx 30rpx; font-size: 28rpx; font-weight: 800; flex-shrink: 0; }
|
||||
.send-disabled { background: #e8e4f0; color: #9b8fa8; }
|
||||
.bubble-me { background: linear-gradient(135deg, #8b7cf6, #a99bf8); color: #fff; border-bottom-right-radius: 8rpx; box-shadow: 0 10rpx 24rpx rgba(139, 124, 246, 0.22); }
|
||||
.bubble-peer { background: #ffffff; color: #1b1b20; border-bottom-left-radius: 8rpx; box-shadow: 0 6rpx 14rpx rgba(27, 27, 32, 0.06); }
|
||||
.input-bar { display: flex; align-items: center; gap: 14rpx; padding: 16rpx 28rpx; padding-bottom: calc(16rpx + env(safe-area-inset-bottom)); background: #ffffff; border-top: 1rpx solid rgba(27, 27, 32, 0.06); }
|
||||
.msg-input { flex: 1; height: 80rpx; background: #f1f2f6; border-radius: 999rpx; padding: 0 24rpx; font-size: 28rpx; color: #1b1b20; }
|
||||
.send-btn { background: #2b2b30; color: #fff; border-radius: 999rpx; padding: 18rpx 30rpx; font-size: 28rpx; font-weight: 600; flex-shrink: 0; }
|
||||
.send-disabled { background: #e7e9ef; color: #b9bdc6; }
|
||||
|
||||
@@ -1,22 +1,45 @@
|
||||
const app = getApp<{ userInfo: any }>()
|
||||
import { AppGlobalData } from '../../types/index'
|
||||
|
||||
const PROVINCES = [
|
||||
'北京', '天津', '上海', '重庆',
|
||||
'河北', '山西', '辽宁', '吉林', '黑龙江',
|
||||
'江苏', '浙江', '安徽', '福建', '江西', '山东',
|
||||
'河南', '湖北', '湖南', '广东', '海南',
|
||||
'四川', '贵州', '云南', '陕西', '甘肃', '青海',
|
||||
'内蒙古', '广西', '西藏', '宁夏', '新疆',
|
||||
'香港', '澳门', '台湾',
|
||||
]
|
||||
|
||||
const app = getApp<{ globalData: AppGlobalData }>()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 0,
|
||||
navbarHeight: 44,
|
||||
navbarPaddingRight: 0,
|
||||
form: { nickName: '', bio: '', location: '' },
|
||||
provinces: PROVINCES,
|
||||
locationIndex: -1,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const info = wx.getSystemInfoSync()
|
||||
this.setData({ statusBarHeight: info.statusBarHeight })
|
||||
const { statusBarHeight, navbarHeight, navbarPaddingRight } = app.globalData.navBarInfo
|
||||
this.setData({ statusBarHeight, navbarHeight, navbarPaddingRight })
|
||||
const user = app.globalData?.userInfo
|
||||
if (user) {
|
||||
const locationIndex = PROVINCES.indexOf(user.location || '')
|
||||
this.setData({
|
||||
form: { nickName: user.nickName || '', bio: user.bio || '', location: user.location || '' },
|
||||
locationIndex,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
onLocationChange(e: WechatMiniprogram.CustomEvent) {
|
||||
const idx = Number(e.detail.value)
|
||||
this.setData({ locationIndex: idx, 'form.location': PROVINCES[idx] })
|
||||
},
|
||||
|
||||
onField(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ [`form.${e.currentTarget.dataset.key}`]: e.detail.value })
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<view class="page">
|
||||
<view class="safe-top" style="height: {{statusBarHeight}}px"></view>
|
||||
<view class="nav">
|
||||
<view class="nav" style="height: {{navbarHeight}}px; padding-right: {{navbarPaddingRight}}px; box-sizing: border-box;">
|
||||
<view class="nav-back" bindtap="onBack">取消</view>
|
||||
<view class="nav-title">编辑资料</view>
|
||||
<view class="nav-save" bindtap="onSave">保存</view>
|
||||
@@ -17,8 +17,12 @@
|
||||
<textarea class="field-textarea" value="{{form.bio}}" bindinput="onField" data-key="bio" placeholder="介绍一下你和你的宠物..." maxlength="80" auto-height="true" show-confirm-bar="false" />
|
||||
</view>
|
||||
<view class="field-item">
|
||||
<text class="field-label">所在城市</text>
|
||||
<input class="field-input" value="{{form.location}}" bindinput="onField" data-key="location" placeholder="所在城市" maxlength="20" />
|
||||
<text class="field-label">所在省份</text>
|
||||
<picker mode="selector" range="{{provinces}}" value="{{locationIndex}}" bindchange="onLocationChange">
|
||||
<view class="field-picker-val {{locationIndex < 0 ? 'placeholder' : ''}}">
|
||||
{{locationIndex >= 0 ? provinces[locationIndex] : '请选择省份'}}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
<view style="height: 60rpx;"></view>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
.page { min-height: 100vh; background: linear-gradient(180deg, #fff8f2 0%, #f5f0ff 100%); }
|
||||
/* ── 资料编辑:Vital 风格表单页 ── */
|
||||
.page { min-height: 100vh; background: #f1f2f6; }
|
||||
.nav { display: flex; align-items: center; padding: 14rpx 28rpx; gap: 16rpx; }
|
||||
.nav-back { font-size: 28rpx; font-weight: 700; color: #9b8fa8; }
|
||||
.nav-title { flex: 1; font-size: 32rpx; font-weight: 900; color: #272235; text-align: center; }
|
||||
.nav-save { font-size: 28rpx; font-weight: 900; color: #ff4f91; }
|
||||
.nav-back { font-size: 28rpx; font-weight: 600; color: #6e7280; }
|
||||
.nav-title { flex: 1; font-size: 32rpx; font-weight: 700; color: #1b1b20; text-align: center; }
|
||||
.nav-save { font-size: 28rpx; font-weight: 700; color: #7563e0; }
|
||||
.form { padding: 24rpx 28rpx; }
|
||||
.field-group { background: rgba(255,255,255,0.90); border-radius: 36rpx; border: 1rpx solid rgba(255,255,255,0.72); overflow: hidden; box-shadow: 0 12rpx 26rpx rgba(78,56,96,0.08); }
|
||||
.field-item { display: flex; align-items: center; justify-content: space-between; padding: 24rpx 28rpx; border-bottom: 1rpx solid rgba(43,37,61,0.06); }
|
||||
.field-group { background: #ffffff; border-radius: 36rpx; overflow: hidden; box-shadow: var(--shadow-soft); }
|
||||
.field-item { display: flex; align-items: center; justify-content: space-between; padding: 24rpx 28rpx; border-bottom: 1rpx solid rgba(27, 27, 32, 0.05); }
|
||||
.field-item:last-child { border-bottom: none; }
|
||||
.field-item-col { flex-direction: column; align-items: flex-start; gap: 14rpx; }
|
||||
.field-label { font-size: 28rpx; font-weight: 700; color: #272235; flex-shrink: 0; margin-right: 20rpx; }
|
||||
.field-input { flex: 1; font-size: 28rpx; color: #272235; text-align: right; }
|
||||
.field-textarea { width: 100%; min-height: 80rpx; font-size: 28rpx; color: #272235; line-height: 1.6; }
|
||||
.field-label { font-size: 28rpx; font-weight: 600; color: #1b1b20; flex-shrink: 0; margin-right: 20rpx; }
|
||||
.field-input { flex: 1; font-size: 28rpx; color: #1b1b20; text-align: right; }
|
||||
.field-textarea { width: 100%; min-height: 80rpx; font-size: 28rpx; color: #1b1b20; line-height: 1.6; }
|
||||
.field-picker-val { flex: 1; font-size: 28rpx; color: #1b1b20; text-align: right; }
|
||||
.field-picker-val.placeholder { color: #b9bdc6; }
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Post } from '../../types/index'
|
||||
import { Post, AppGlobalData } from '../../types/index'
|
||||
import { api } from '../../utils/api'
|
||||
import { getAvatarGradient, getPetEmoji } from '../../utils/format'
|
||||
import { PAGE_SIZE } from '../../utils/constants'
|
||||
|
||||
interface FeedTab { key: 'feed' | 'nearby' | 'hot'; label: string }
|
||||
|
||||
const app = getApp<{ userInfo: any; currentLocation: any }>()
|
||||
const app = getApp<{ globalData: AppGlobalData }>()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 0,
|
||||
navbarHeight: 44,
|
||||
navbarPaddingRight: 0,
|
||||
tabBarHeight: 56,
|
||||
posts: [] as Post[],
|
||||
stories: [] as any[],
|
||||
@@ -24,19 +26,20 @@ Page({
|
||||
refreshing: false,
|
||||
hasMore: true,
|
||||
page: 0,
|
||||
hasUnread: false,
|
||||
myAvatarGradient: 'linear-gradient(135deg, #ffd6e8, #fff1a6)',
|
||||
myAvatarGradient: 'linear-gradient(135deg, #d9d2fb, #efedfd)',
|
||||
myEmoji: '🐶',
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const info = wx.getSystemInfoSync()
|
||||
const { statusBarHeight, navbarHeight, navbarPaddingRight } = app.globalData.navBarInfo
|
||||
this.setData({
|
||||
statusBarHeight: info.statusBarHeight,
|
||||
tabBarHeight: info.windowHeight - info.safeArea.bottom + 56,
|
||||
statusBarHeight,
|
||||
navbarHeight,
|
||||
navbarPaddingRight,
|
||||
tabBarHeight: info.windowHeight - (info.safeArea?.bottom ?? info.windowHeight) + 56,
|
||||
})
|
||||
this.loadFeed(true)
|
||||
this.loadStories()
|
||||
this.syncMyInfo()
|
||||
},
|
||||
|
||||
@@ -45,10 +48,6 @@ Page({
|
||||
tabBar?.setSelected?.(0)
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.onRefresh()
|
||||
},
|
||||
|
||||
async onRefresh() {
|
||||
this.setData({ refreshing: true, page: 0, hasMore: true })
|
||||
await this.loadFeed(true)
|
||||
@@ -84,18 +83,6 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
async loadStories() {
|
||||
try {
|
||||
const res = await wx.cloud.callFunction({
|
||||
name: 'getStories',
|
||||
data: {},
|
||||
}) as any
|
||||
if (res.result?.code === 0) {
|
||||
this.setData({ stories: res.result.data || [] })
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
|
||||
syncMyInfo() {
|
||||
const user = app.globalData?.userInfo
|
||||
if (!user) return
|
||||
@@ -129,10 +116,6 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/search/search' })
|
||||
},
|
||||
|
||||
onNotify() {
|
||||
wx.navigateTo({ url: '/pages/notifications/notifications' })
|
||||
},
|
||||
|
||||
onMyStory() {
|
||||
wx.navigateTo({ url: '/pages/post/post?type=story' })
|
||||
},
|
||||
|
||||
@@ -1,64 +1,71 @@
|
||||
<view class="feed-page">
|
||||
<!-- 状态栏占位 -->
|
||||
<view class="safe-top" style="height: {{statusBarHeight}}px"></view>
|
||||
|
||||
<!-- 顶部栏 -->
|
||||
<view class="topbar">
|
||||
<view class="topbar-logo">汪<text class="logo-accent">圈</text></view>
|
||||
<view class="topbar-tabs">
|
||||
<view
|
||||
wx:for="{{feedTabs}}"
|
||||
wx:key="key"
|
||||
class="feed-tab {{activeTab === item.key ? 'feed-tab-active' : ''}}"
|
||||
bindtap="onSwitchTab"
|
||||
data-key="{{item.key}}"
|
||||
>{{item.label}}</view>
|
||||
</view>
|
||||
<view class="topbar-actions">
|
||||
<view class="topbar-btn" bindtap="onSearch">🔍</view>
|
||||
<view class="topbar-btn {{hasUnread ? 'has-badge' : ''}}" bindtap="onNotify">🔔</view>
|
||||
<!-- 固定液态玻璃顶栏:标题 + 搜索图标(紧贴胶囊按钮) -->
|
||||
<view class="glass-topbar">
|
||||
<view style="height: {{statusBarHeight}}px;"></view>
|
||||
<view class="navbar" style="height: {{navbarHeight}}px; padding-right: {{navbarPaddingRight}}px;">
|
||||
<view class="topbar-logo">汪<text class="logo-accent">圈</text></view>
|
||||
<view class="navbar-action" bindtap="onSearch">
|
||||
<view class="icon-line icon-search-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 故事圈 -->
|
||||
<scroll-view class="stories-scroll" scroll-x="true" enhanced="true" show-scrollbar="false">
|
||||
<!-- 我的故事 -->
|
||||
<view class="story" bindtap="onMyStory">
|
||||
<view class="story-ring story-ring-me">
|
||||
<view class="story-inner" style="background: {{myAvatarGradient}}">
|
||||
<text class="story-emoji">{{myEmoji}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="story-name">我的</text>
|
||||
</view>
|
||||
<!-- 关注的宠物故事 -->
|
||||
<view
|
||||
wx:for="{{stories}}"
|
||||
wx:key="_id"
|
||||
class="story"
|
||||
bindtap="onStoryTap"
|
||||
data-id="{{item._id}}"
|
||||
>
|
||||
<view class="story-ring {{item.hasNew ? '' : 'story-ring-seen'}}">
|
||||
<view class="story-inner" style="background: {{item.gradient}}">
|
||||
<text class="story-emoji">{{item.emoji}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="story-name">{{item.name}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<!-- 顶栏占位 -->
|
||||
<view style="height: {{statusBarHeight + navbarHeight}}px; flex-shrink: 0;"></view>
|
||||
|
||||
<!-- 信息流 -->
|
||||
<!-- 信息流:tabs、故事圈与帖子同在滚动区,下拉刷新只在真正顶部触发 -->
|
||||
<scroll-view
|
||||
class="feed-scroll"
|
||||
scroll-y="true"
|
||||
enhanced="true"
|
||||
bounces="{{false}}"
|
||||
show-scrollbar="false"
|
||||
bindscrolltolower="onLoadMore"
|
||||
refresher-enabled="true"
|
||||
bindrefresherrefresh="onRefresh"
|
||||
refresher-triggered="{{refreshing}}"
|
||||
>
|
||||
<!-- 次级菜单栏:信息流 tabs -->
|
||||
<view class="subbar">
|
||||
<view class="topbar-tabs">
|
||||
<view
|
||||
wx:for="{{feedTabs}}"
|
||||
wx:key="key"
|
||||
class="feed-tab {{activeTab === item.key ? 'feed-tab-active' : ''}}"
|
||||
bindtap="onSwitchTab"
|
||||
data-key="{{item.key}}"
|
||||
>{{item.label}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 故事圈 -->
|
||||
<scroll-view class="stories-scroll" scroll-x="true" enhanced="true" show-scrollbar="false">
|
||||
<!-- 我的故事 -->
|
||||
<view class="story" bindtap="onMyStory">
|
||||
<view class="story-ring story-ring-me">
|
||||
<view class="story-inner" style="background: {{myAvatarGradient}}">
|
||||
<text class="story-emoji">{{myEmoji}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="story-name">我的</text>
|
||||
</view>
|
||||
<!-- 关注的宠物故事 -->
|
||||
<view
|
||||
wx:for="{{stories}}"
|
||||
wx:key="_id"
|
||||
class="story"
|
||||
bindtap="onStoryTap"
|
||||
data-id="{{item._id}}"
|
||||
>
|
||||
<view class="story-ring {{item.hasNew ? '' : 'story-ring-seen'}}">
|
||||
<view class="story-inner" style="background: {{item.gradient}}">
|
||||
<text class="story-emoji">{{item.emoji}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="story-name">{{item.name}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 骨架屏 -->
|
||||
<view wx:if="{{loading && posts.length === 0}}" class="skeleton-wrap">
|
||||
<view wx:for="{{[1,2,3]}}" wx:key="index" class="skeleton-card">
|
||||
|
||||
@@ -1,28 +1,41 @@
|
||||
/* ── 广场页:Vital 风格 ──
|
||||
顶部紫色渐变向下淡出融入灰底,白卡信息流叠压其上 */
|
||||
.feed-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #fff8f2 0%, #fff4fb 50%, #f5f0ff 100%);
|
||||
height: 100vh;
|
||||
background-color: #f1f2f6;
|
||||
background-image: linear-gradient(180deg, #9181f4 0%, #a99bf8 52%, rgba(241, 242, 246, 0) 100%);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 520rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部栏 */
|
||||
.topbar {
|
||||
/* 主导航栏:与胶囊按钮同高,右侧避让胶囊 */
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 28rpx 12rpx;
|
||||
padding-left: 28rpx;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 次级菜单栏:胶囊按钮下方,全宽 */
|
||||
.subbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 28rpx 12rpx;
|
||||
gap: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* logo:玻璃顶栏上的深色标题(颜色由 .glass-topbar 统一控制) */
|
||||
.topbar-logo {
|
||||
font-size: 44rpx;
|
||||
font-weight: 900;
|
||||
color: #272235;
|
||||
font-size: 46rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-accent { color: #ff4f91; }
|
||||
|
||||
.topbar-tabs {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -30,50 +43,21 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 紫底上的 tab:半透明白文字,激活态为白色胶囊 + 黑字 */
|
||||
.feed-tab {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #9b8fa8;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.feed-tab-active {
|
||||
color: #ff4f91;
|
||||
background: rgba(255, 79, 145, 0.10);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.topbar-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
box-shadow: 0 8rpx 18rpx rgba(78, 56, 96, 0.10);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.has-badge::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 6rpx;
|
||||
right: 6rpx;
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
border-radius: 50%;
|
||||
background: #ff4f91;
|
||||
border: 2rpx solid #fff;
|
||||
color: #1b1b20;
|
||||
background: #ffffff;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8rpx 20rpx rgba(27, 27, 32, 0.10);
|
||||
}
|
||||
|
||||
/* 故事圈 */
|
||||
@@ -92,43 +76,45 @@
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* 故事环:未读 = 紫色渐变环(单一主色家族),已读 = 浅灰 */
|
||||
.story-ring {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 50%;
|
||||
padding: 5rpx;
|
||||
width: 116rpx;
|
||||
height: 116rpx;
|
||||
border-radius: 40rpx;
|
||||
padding: 6rpx;
|
||||
box-sizing: border-box;
|
||||
background: conic-gradient(from 120deg, #ff4f91, #ff9f1c, #ffe15a, #67e8c9, #4d8dff, #8c5cff, #ff4f91);
|
||||
box-shadow: 0 10rpx 18rpx rgba(96, 60, 115, 0.16);
|
||||
background: linear-gradient(135deg, #7563e0, #a99bf8);
|
||||
box-shadow: 0 16rpx 36rpx rgba(27, 27, 32, 0.10);
|
||||
}
|
||||
|
||||
.story-ring-seen {
|
||||
background: #e8e4f0;
|
||||
background: rgba(27, 27, 32, 0.10);
|
||||
}
|
||||
|
||||
/* 我的故事:强调黑(与发布 CTA 呼应) */
|
||||
.story-ring-me {
|
||||
background: linear-gradient(135deg, #ff4f91, #ff9f1c);
|
||||
background: #2b2b30;
|
||||
}
|
||||
|
||||
.story-inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.90);
|
||||
border: 5rpx solid rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.story-emoji {
|
||||
font-size: 44rpx;
|
||||
font-size: 46rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.story-name {
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #6a6178;
|
||||
font-size: 21rpx;
|
||||
font-weight: 600;
|
||||
color: #6e7280;
|
||||
}
|
||||
|
||||
/* 信息流 */
|
||||
@@ -144,8 +130,9 @@
|
||||
|
||||
.skeleton-card {
|
||||
margin: 0 28rpx 32rpx;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
background: #ffffff;
|
||||
border-radius: 48rpx;
|
||||
box-shadow: 0 16rpx 48rpx rgba(27, 27, 32, 0.06);
|
||||
overflow: hidden;
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
@@ -187,7 +174,7 @@
|
||||
.w40 { width: 40%; }
|
||||
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #f0eef5 25%, #e8e4f0 50%, #f0eef5 75%);
|
||||
background: linear-gradient(90deg, #ebecf1 25%, #e2e4ea 50%, #ebecf1 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shine 1.4s infinite;
|
||||
}
|
||||
@@ -209,7 +196,7 @@
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: #ff4f91;
|
||||
background: var(--purple);
|
||||
animation: bounce 1.2s infinite;
|
||||
}
|
||||
|
||||
@@ -225,7 +212,7 @@
|
||||
.end-tip {
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #c4b8d0;
|
||||
color: var(--text-tertiary);
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
@@ -245,13 +232,13 @@
|
||||
|
||||
.empty-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 900;
|
||||
color: #272235;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: #9b8fa8;
|
||||
color: var(--text-tertiary);
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
@@ -260,5 +247,5 @@
|
||||
margin-top: 24rpx;
|
||||
padding: 22rpx 60rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { UserProfile } from '../../types/index'
|
||||
import { UserProfile, AppGlobalData } from '../../types/index'
|
||||
import { api } from '../../utils/api'
|
||||
import { getAvatarGradient } from '../../utils/format'
|
||||
|
||||
const app = getApp<{ userInfo: any }>()
|
||||
const app = getApp<{ globalData: AppGlobalData }>()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 0,
|
||||
navbarHeight: 44,
|
||||
navbarPaddingRight: 0,
|
||||
tabBarHeight: 56,
|
||||
activeTab: 'following' as 'following' | 'followers',
|
||||
list: [] as any[],
|
||||
@@ -14,11 +16,18 @@ Page({
|
||||
followersCount: 0,
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
hasUnread: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const info = wx.getSystemInfoSync()
|
||||
this.setData({ statusBarHeight: info.statusBarHeight })
|
||||
const { statusBarHeight, navbarHeight, navbarPaddingRight } = app.globalData.navBarInfo
|
||||
this.setData({
|
||||
statusBarHeight,
|
||||
navbarHeight,
|
||||
navbarPaddingRight,
|
||||
tabBarHeight: info.windowHeight - (info.safeArea?.bottom ?? info.windowHeight) + 56,
|
||||
})
|
||||
this.loadList()
|
||||
},
|
||||
|
||||
@@ -76,6 +85,10 @@ Page({
|
||||
wx.switchTab({ url: '/pages/nearby/nearby' })
|
||||
},
|
||||
|
||||
onNotify() {
|
||||
wx.navigateTo({ url: '/pages/notifications/notifications' })
|
||||
},
|
||||
|
||||
onGoNearby() {
|
||||
wx.switchTab({ url: '/pages/nearby/nearby' })
|
||||
},
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
<view class="friends-page">
|
||||
<view class="safe-top" style="height: {{statusBarHeight}}px"></view>
|
||||
<!-- 固定液态玻璃顶栏:标题 + 通知铃铛(紧贴胶囊按钮) -->
|
||||
<view class="glass-topbar">
|
||||
<view style="height: {{statusBarHeight}}px;"></view>
|
||||
<view class="navbar" style="height: {{navbarHeight}}px; padding-right: {{navbarPaddingRight}}px;">
|
||||
<view class="topbar-logo">汪<text class="logo-accent">友</text></view>
|
||||
<view class="navbar-action {{hasUnread ? 'has-badge' : ''}}" bindtap="onNotify">
|
||||
<view class="icon-line icon-bell-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="topbar">
|
||||
<view class="topbar-logo">汪<text class="logo-accent">友</text></view>
|
||||
<!-- 顶栏占位 -->
|
||||
<view style="height: {{statusBarHeight + navbarHeight}}px; flex-shrink: 0;"></view>
|
||||
|
||||
<!-- 次级菜单栏:位于胶囊下方,全宽可用 -->
|
||||
<view class="subbar">
|
||||
<view class="topbar-tabs">
|
||||
<view class="ftab {{activeTab === 'following' ? 'ftab-active' : ''}}" bindtap="switchTab" data-tab="following">
|
||||
关注 <text class="ftab-count">{{followingCount}}</text>
|
||||
@@ -30,6 +42,7 @@
|
||||
scroll-y="true"
|
||||
class="friends-scroll"
|
||||
enhanced="true"
|
||||
bounces="{{false}}"
|
||||
show-scrollbar="false"
|
||||
refresher-enabled="true"
|
||||
bindrefresherrefresh="onRefresh"
|
||||
|
||||
@@ -1,47 +1,70 @@
|
||||
/* ── 汪友页:Vital 风格 ──
|
||||
紫色渐变淡出头部 + 白卡列表叠压 */
|
||||
.friends-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #fff8f2 0%, #f5f0ff 100%);
|
||||
height: 100vh;
|
||||
background-color: #f1f2f6;
|
||||
background-image: linear-gradient(180deg, #9181f4 0%, #a99bf8 52%, rgba(241, 242, 246, 0) 100%);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 420rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 28rpx 12rpx;
|
||||
gap: 20rpx;
|
||||
padding-left: 28rpx;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.topbar-logo { font-size: 44rpx; font-weight: 900; color: #272235; }
|
||||
.subbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 28rpx 12rpx;
|
||||
gap: 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-accent { color: #ff4f91; }
|
||||
/* logo:玻璃顶栏上的深色标题(颜色由 .glass-topbar 统一控制) */
|
||||
.topbar-logo {
|
||||
font-size: 46rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5rpx;
|
||||
}
|
||||
|
||||
.topbar-tabs { flex: 1; display: flex; gap: 6rpx; }
|
||||
|
||||
/* 紫底 tab:激活态为白色胶囊 + 黑字 */
|
||||
.ftab {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #9b8fa8;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.ftab-active { color: #ff4f91; background: rgba(255, 79, 145, 0.10); font-weight: 900; }
|
||||
.ftab-active {
|
||||
color: #1b1b20;
|
||||
background: #ffffff;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8rpx 20rpx rgba(27, 27, 32, 0.10);
|
||||
}
|
||||
|
||||
.ftab-count { font-size: 22rpx; font-weight: 800; }
|
||||
.ftab-count { font-size: 22rpx; font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* 添加按钮:紫底上的半透明白 */
|
||||
.topbar-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #ff4f91, #ff9f1c);
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 26rpx;
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
color: #fff;
|
||||
font-size: 40rpx;
|
||||
font-weight: 200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 16rpx rgba(255, 79, 145, 0.28);
|
||||
}
|
||||
|
||||
/* 空态 */
|
||||
@@ -56,40 +79,54 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 0 60rpx;
|
||||
gap: 18rpx;
|
||||
padding: 48rpx 60rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-emoji { font-size: 80rpx; }
|
||||
.empty-emoji {
|
||||
font-size: 64rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 40rpx;
|
||||
padding: 28rpx;
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.empty-title { font-size: 34rpx; font-weight: 900; color: #272235; }
|
||||
.empty-title { font-size: 34rpx; font-weight: 700; color: #1b1b20; }
|
||||
|
||||
.empty-desc { font-size: 26rpx; color: #9b8fa8; text-align: center; line-height: 1.6; }
|
||||
.empty-desc { font-size: 26rpx; color: #6e7280; text-align: center; line-height: 1.6; }
|
||||
|
||||
.go-btn { margin-top: 20rpx; padding: 22rpx 60rpx; font-size: 28rpx; font-weight: 800; }
|
||||
.go-btn {
|
||||
margin-top: 12rpx;
|
||||
padding: 22rpx 60rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 好友列表 */
|
||||
/* 好友列表:白卡 */
|
||||
.friends-scroll { flex: 1; }
|
||||
|
||||
.friend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 20rpx 28rpx;
|
||||
background: rgba(255, 255, 255, 0.80);
|
||||
border-bottom: 1rpx solid rgba(43, 37, 61, 0.05);
|
||||
padding: 22rpx;
|
||||
margin: 0 28rpx 20rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 44rpx;
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.friend-avatar {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 28rpx;
|
||||
width: 92rpx;
|
||||
height: 92rpx;
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36rpx;
|
||||
font-weight: 800;
|
||||
color: rgba(39, 34, 53, 0.72);
|
||||
font-weight: 700;
|
||||
color: rgba(27, 27, 32, 0.65);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -102,38 +139,45 @@
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.friend-name { font-size: 28rpx; font-weight: 800; color: #272235; }
|
||||
.friend-name { font-size: 28rpx; font-weight: 600; color: #1b1b20; }
|
||||
|
||||
.online-tag {
|
||||
font-size: 20rpx;
|
||||
font-weight: 800;
|
||||
color: #2fd37a;
|
||||
background: rgba(47, 211, 122, 0.12);
|
||||
font-weight: 600;
|
||||
color: #2f9c7d;
|
||||
background: rgba(75, 190, 154, 0.14);
|
||||
border-radius: 999rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
}
|
||||
|
||||
.friend-bio {
|
||||
font-size: 24rpx;
|
||||
color: #9b8fa8;
|
||||
color: #b9bdc6;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 关注:强调黑胶囊;已关注:浅灰静默态 */
|
||||
.follow-btn {
|
||||
background: linear-gradient(135deg, #ff4f91, #ff9f1c);
|
||||
background: #2b2b30;
|
||||
color: #fff;
|
||||
border-radius: 999rpx;
|
||||
padding: 14rpx 28rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 12rpx 24rpx rgba(43, 43, 48, 0.22);
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
|
||||
.follow-btn:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
|
||||
.follow-btn.following {
|
||||
background: rgba(255, 255, 255, 0.80);
|
||||
border: 1rpx solid rgba(43, 37, 61, 0.12);
|
||||
color: #9b8fa8;
|
||||
background: #f1f2f6;
|
||||
color: #6e7280;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { login } from '../../utils/auth'
|
||||
import { AppGlobalData } from '../../types/index'
|
||||
|
||||
const app = getApp<{ userInfo: any; isLoggedIn: boolean }>()
|
||||
const app = getApp<{ globalData: AppGlobalData }>()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* ── 登录页:品牌主场(Vital 风格) ──
|
||||
整屏紫色渐变向下淡出,白卡特性列表 + 黑色 CTA */
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(160deg, #fff8bd 0%, #ffd6e8 34%, #cff7ff 68%, #e9ddff 100%);
|
||||
background: linear-gradient(180deg, #9181f4 0%, #a99bf8 55%, #f1f2f6 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -8,32 +10,11 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 背景光晕 */
|
||||
/* 旧版彩色光晕已停用 */
|
||||
.login-bg { position: absolute; inset: 0; pointer-events: none; }
|
||||
|
||||
.bg-blob {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(60rpx);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.blob1 {
|
||||
width: 400rpx; height: 400rpx;
|
||||
background: rgba(255, 79, 145, 0.25);
|
||||
top: -80rpx; left: -80rpx;
|
||||
}
|
||||
|
||||
.blob2 {
|
||||
width: 360rpx; height: 360rpx;
|
||||
background: rgba(103, 232, 201, 0.30);
|
||||
bottom: -60rpx; right: -60rpx;
|
||||
}
|
||||
|
||||
.blob3 {
|
||||
width: 300rpx; height: 300rpx;
|
||||
background: rgba(255, 225, 90, 0.30);
|
||||
bottom: 20%; left: 10%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 内容 */
|
||||
@@ -47,7 +28,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Logo */
|
||||
/* Logo:紫底白字 */
|
||||
.app-logo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -59,23 +40,24 @@
|
||||
font-size: 120rpx;
|
||||
line-height: 1;
|
||||
margin-bottom: 20rpx;
|
||||
filter: drop-shadow(0 20rpx 30rpx rgba(95, 49, 104, 0.25));
|
||||
filter: drop-shadow(0 20rpx 30rpx rgba(27, 27, 32, 0.18));
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 80rpx;
|
||||
font-weight: 900;
|
||||
color: #272235;
|
||||
font-weight: 700;
|
||||
letter-spacing: -2rpx;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.logo-accent { color: #ff4f91; }
|
||||
.logo-accent { color: rgba(255, 255, 255, 0.72); }
|
||||
|
||||
.logo-tagline {
|
||||
font-size: 28rpx;
|
||||
color: #6a6178;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 宠物 emoji 装饰 */
|
||||
@@ -83,18 +65,16 @@
|
||||
font-size: 52rpx;
|
||||
letter-spacing: 16rpx;
|
||||
margin-bottom: 56rpx;
|
||||
text-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* 特性列表 */
|
||||
/* 特性列表:白卡叠压在渐变上 */
|
||||
.feature-list {
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
background: #ffffff;
|
||||
border-radius: 40rpx;
|
||||
padding: 32rpx 36rpx;
|
||||
margin-bottom: 48rpx;
|
||||
box-shadow: 0 18rpx 40rpx rgba(95, 49, 104, 0.10);
|
||||
box-shadow: 0 20rpx 50rpx rgba(27, 27, 32, 0.12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
@@ -110,20 +90,20 @@
|
||||
|
||||
.f-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #272235;
|
||||
font-weight: 600;
|
||||
color: #1b1b20;
|
||||
}
|
||||
|
||||
/* 登录按钮 */
|
||||
/* 登录按钮:强调黑胶囊 CTA */
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
background: linear-gradient(135deg, #ff4f91, #ff9f1c 55%, #ffe15a) !important;
|
||||
background: #2b2b30 !important;
|
||||
border-radius: 999rpx !important;
|
||||
font-size: 32rpx !important;
|
||||
font-weight: 900 !important;
|
||||
font-weight: 600 !important;
|
||||
color: #fff !important;
|
||||
box-shadow: 0 16rpx 32rpx rgba(255, 79, 145, 0.35) !important;
|
||||
box-shadow: 0 16rpx 36rpx rgba(43, 43, 48, 0.30) !important;
|
||||
border: none !important;
|
||||
margin-bottom: 28rpx !important;
|
||||
display: flex;
|
||||
@@ -137,12 +117,12 @@
|
||||
/* 隐私 */
|
||||
.privacy-tip {
|
||||
font-size: 22rpx;
|
||||
color: #9b8fa8;
|
||||
color: #6e7280;
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: #4d8dff;
|
||||
font-weight: 700;
|
||||
color: #7563e0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NearbyUser } from '../../types/index'
|
||||
import { NearbyUser, AppGlobalData } from '../../types/index'
|
||||
import { api } from '../../utils/api'
|
||||
import { formatDistance, formatLastSeen, getAvatarGradient, getPetEmoji } from '../../utils/format'
|
||||
import { requireLocation } from '../../utils/auth'
|
||||
@@ -7,11 +7,13 @@ import { NEARBY_RADIUS_KM } from '../../utils/constants'
|
||||
const DEFAULT_LAT = 31.2304
|
||||
const DEFAULT_LNG = 121.4737
|
||||
|
||||
const app = getApp<{ userInfo: any; currentLocation: any }>()
|
||||
const app = getApp<{ globalData: AppGlobalData }>()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 0,
|
||||
navbarHeight: 44,
|
||||
navbarPaddingRight: 0,
|
||||
tabBarHeight: 56,
|
||||
viewMode: 'map' as 'map' | 'list',
|
||||
mapCenter: { latitude: DEFAULT_LAT, longitude: DEFAULT_LNG },
|
||||
@@ -29,7 +31,13 @@ Page({
|
||||
|
||||
onLoad() {
|
||||
const info = wx.getSystemInfoSync()
|
||||
this.setData({ statusBarHeight: info.statusBarHeight })
|
||||
const { statusBarHeight, navbarHeight, navbarPaddingRight } = app.globalData.navBarInfo
|
||||
this.setData({
|
||||
statusBarHeight,
|
||||
navbarHeight,
|
||||
navbarPaddingRight,
|
||||
tabBarHeight: info.windowHeight - (info.safeArea?.bottom ?? info.windowHeight) + 56,
|
||||
})
|
||||
this.initLocation()
|
||||
},
|
||||
|
||||
@@ -49,8 +57,8 @@ Page({
|
||||
longitude: loc.longitude,
|
||||
radius: NEARBY_RADIUS_KM * 1000,
|
||||
strokeWidth: 2,
|
||||
strokeColor: 'rgba(255, 79, 145, 0.3)',
|
||||
fillColor: 'rgba(255, 79, 145, 0.05)',
|
||||
strokeColor: 'rgba(139, 124, 246, 0.3)',
|
||||
fillColor: 'rgba(139, 124, 246, 0.05)',
|
||||
}],
|
||||
})
|
||||
app.globalData.currentLocation = loc
|
||||
@@ -109,7 +117,7 @@ Page({
|
||||
color: '#ffffff',
|
||||
fontSize: 11,
|
||||
borderRadius: 12,
|
||||
bgColor: '#ff4f91',
|
||||
bgColor: '#8b7cf6',
|
||||
padding: 6,
|
||||
display: 'ALWAYS',
|
||||
},
|
||||
@@ -125,7 +133,7 @@ Page({
|
||||
height: 36,
|
||||
callout: {
|
||||
content: item.petName,
|
||||
color: '#272235',
|
||||
color: '#1b1b20',
|
||||
fontSize: 11,
|
||||
borderRadius: 12,
|
||||
bgColor: 'rgba(255,255,255,0.92)',
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
<view class="nearby-page">
|
||||
<view class="safe-top" style="height: {{statusBarHeight}}px"></view>
|
||||
<!-- 固定液态玻璃顶栏:标题 -->
|
||||
<view class="glass-topbar">
|
||||
<view style="height: {{statusBarHeight}}px;"></view>
|
||||
<view class="navbar" style="height: {{navbarHeight}}px; padding-right: {{navbarPaddingRight}}px;">
|
||||
<view class="topbar-logo">附<text class="logo-accent">近</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 顶栏 -->
|
||||
<view class="topbar">
|
||||
<view class="topbar-logo">附<text class="logo-accent">近</text></view>
|
||||
<!-- 顶栏占位 -->
|
||||
<view style="height: {{statusBarHeight + navbarHeight}}px; flex-shrink: 0;"></view>
|
||||
|
||||
<!-- 次级菜单栏:位于胶囊下方,全宽可用 -->
|
||||
<view class="subbar">
|
||||
<view class="view-toggle">
|
||||
<view class="toggle-btn {{viewMode === 'map' ? 'toggle-active' : ''}}" bindtap="setViewMode" data-mode="map">🗺 地图</view>
|
||||
<view class="toggle-btn {{viewMode === 'list' ? 'toggle-active' : ''}}" bindtap="setViewMode" data-mode="list">📋 列表</view>
|
||||
|
||||
@@ -1,67 +1,78 @@
|
||||
/* ── 附近页:Vital 风格工具页 ──
|
||||
浅灰底 + 白卡,地图浮层全部实底白卡,动作按钮用强调黑 */
|
||||
.nearby-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: linear-gradient(180deg, #c9f7df 0%, #d8efff 40%, #fff1a6 100%);
|
||||
background: #f1f2f6;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 顶栏 */
|
||||
.topbar {
|
||||
/* 主导航栏:与胶囊等高,右侧避让胶囊 */
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 28rpx 12rpx;
|
||||
padding-left: 28rpx;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 次级菜单栏:胶囊下方,全宽 */
|
||||
.subbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 28rpx 16rpx;
|
||||
gap: 16rpx;
|
||||
flex-shrink: 0;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
/* logo:玻璃顶栏上的深色标题(颜色由 .glass-topbar 统一控制) */
|
||||
.topbar-logo {
|
||||
font-size: 44rpx;
|
||||
font-weight: 900;
|
||||
color: #272235;
|
||||
font-size: 46rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-accent { color: #ff4f91; }
|
||||
|
||||
/* 视图切换:灰底分段控件,激活为白色胶囊 */
|
||||
.view-toggle {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
background: rgba(255, 255, 255, 0.60);
|
||||
background: #e7e9ef;
|
||||
border-radius: 999rpx;
|
||||
padding: 4rpx;
|
||||
padding: 6rpx;
|
||||
gap: 4rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.80);
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #9b8fa8;
|
||||
font-weight: 600;
|
||||
color: #6e7280;
|
||||
padding: 12rpx 0;
|
||||
border-radius: 999rpx;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle-active {
|
||||
background: rgba(255, 255, 255, 0.90);
|
||||
color: #272235;
|
||||
font-weight: 900;
|
||||
box-shadow: 0 4rpx 10rpx rgba(78, 56, 96, 0.10);
|
||||
background: #ffffff;
|
||||
color: #1b1b20;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8rpx 18rpx rgba(27, 27, 32, 0.08);
|
||||
}
|
||||
|
||||
.topbar-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
box-shadow: 0 8rpx 18rpx rgba(78, 56, 96, 0.10);
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 26rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow-soft);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 30rpx;
|
||||
font-size: 28rpx;
|
||||
color: #6e7280;
|
||||
}
|
||||
|
||||
/* 地图 */
|
||||
@@ -88,18 +99,17 @@
|
||||
}
|
||||
|
||||
.map-ctrl-btn {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
background: rgba(255, 255, 255, 0.90);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.80);
|
||||
border-radius: 22rpx;
|
||||
box-shadow: 0 10rpx 20rpx rgba(82, 66, 105, 0.16);
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 26rpx;
|
||||
box-shadow: 0 12rpx 32rpx rgba(27, 27, 32, 0.12);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
color: #4d8dff;
|
||||
font-weight: 700;
|
||||
color: #2b2b30;
|
||||
}
|
||||
|
||||
/* 宠物弹窗 */
|
||||
@@ -108,14 +118,13 @@
|
||||
bottom: 300rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
border-radius: 30rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 36rpx;
|
||||
padding: 22rpx 26rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
box-shadow: 0 18rpx 40rpx rgba(95, 49, 104, 0.18);
|
||||
box-shadow: 0 20rpx 50rpx rgba(27, 27, 32, 0.14);
|
||||
min-width: 520rpx;
|
||||
z-index: 20;
|
||||
}
|
||||
@@ -136,21 +145,21 @@
|
||||
.popup-name {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
color: #272235;
|
||||
font-weight: 600;
|
||||
color: #1b1b20;
|
||||
}
|
||||
|
||||
.popup-sub {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #9b8fa8;
|
||||
color: #b9bdc6;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.popup-hi {
|
||||
padding: 16rpx 24rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
font-weight: 600;
|
||||
border-radius: 999rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -160,13 +169,13 @@
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
left: 20rpx;
|
||||
background: rgba(255, 255, 255, 0.90);
|
||||
background: #ffffff;
|
||||
border-radius: 999rpx;
|
||||
padding: 10rpx 20rpx;
|
||||
padding: 10rpx 22rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 800;
|
||||
color: #ff4f91;
|
||||
box-shadow: 0 8rpx 16rpx rgba(78, 56, 96, 0.12);
|
||||
font-weight: 600;
|
||||
color: #7563e0;
|
||||
box-shadow: var(--shadow-soft);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@@ -177,8 +186,8 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 15;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border-top: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
background: #ffffff;
|
||||
border-top: 1rpx solid rgba(27, 27, 32, 0.06);
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
|
||||
@@ -191,12 +200,10 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
background: rgba(255, 255, 255, 0.80);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
border-radius: 24rpx;
|
||||
background: #f1f2f6;
|
||||
border-radius: 26rpx;
|
||||
padding: 14rpx 18rpx;
|
||||
margin-right: 16rpx;
|
||||
box-shadow: 0 8rpx 16rpx rgba(78, 56, 96, 0.08);
|
||||
}
|
||||
|
||||
.preview-avatar {
|
||||
@@ -218,20 +225,21 @@
|
||||
|
||||
.preview-name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 800;
|
||||
color: #272235;
|
||||
font-weight: 600;
|
||||
color: #1b1b20;
|
||||
}
|
||||
|
||||
.preview-dist {
|
||||
font-size: 20rpx;
|
||||
color: #4d8dff;
|
||||
font-weight: 700;
|
||||
color: #7563e0;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* 列表视图 */
|
||||
.list-scroll {
|
||||
flex: 1;
|
||||
background: linear-gradient(180deg, #fff9fb 0%, #f5f0ff 100%);
|
||||
background: #f1f2f6;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
@@ -242,37 +250,37 @@
|
||||
}
|
||||
|
||||
.list-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 900;
|
||||
color: #272235;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1b1b20;
|
||||
}
|
||||
|
||||
/* 在线数:淡紫静默胶囊 */
|
||||
.online-chip {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #ff4f91;
|
||||
background: #ffe5f0;
|
||||
font-weight: 600;
|
||||
color: #7563e0;
|
||||
background: rgba(139, 124, 246, 0.12);
|
||||
border-radius: 999rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
padding: 10rpx 22rpx;
|
||||
}
|
||||
|
||||
/* 列表项 */
|
||||
/* 列表项:白卡 */
|
||||
.nearby-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
background: rgba(255, 255, 255, 0.80);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
border-radius: 40rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 48rpx;
|
||||
margin: 0 28rpx 20rpx;
|
||||
padding: 20rpx;
|
||||
box-shadow: 0 12rpx 26rpx rgba(80, 58, 108, 0.09);
|
||||
padding: 22rpx;
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.nearby-avatar {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 30rpx;
|
||||
width: 92rpx;
|
||||
height: 92rpx;
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -293,17 +301,17 @@
|
||||
|
||||
.nearby-pet-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 900;
|
||||
color: #272235;
|
||||
font-weight: 600;
|
||||
color: #1b1b20;
|
||||
}
|
||||
|
||||
.breed-chip {
|
||||
font-size: 20rpx;
|
||||
font-weight: 800;
|
||||
background: #ffe5f0;
|
||||
color: #a91d5b;
|
||||
font-weight: 600;
|
||||
color: #7563e0;
|
||||
background: rgba(139, 124, 246, 0.12);
|
||||
border-radius: 999rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
}
|
||||
|
||||
.nearby-sub {
|
||||
@@ -311,16 +319,16 @@
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #9b8fa8;
|
||||
color: #b9bdc6;
|
||||
}
|
||||
|
||||
.online-dot {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
border-radius: 50%;
|
||||
background: #2fd37a;
|
||||
background: #4bbe9a;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 0 4rpx rgba(47, 211, 122, 0.18);
|
||||
box-shadow: 0 0 0 4rpx rgba(75, 190, 154, 0.16);
|
||||
}
|
||||
|
||||
.nearby-right {
|
||||
@@ -333,25 +341,31 @@
|
||||
|
||||
.dist-text {
|
||||
font-size: 22rpx;
|
||||
font-weight: 800;
|
||||
color: #4d8dff;
|
||||
font-weight: 600;
|
||||
color: #7563e0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* 打招呼:强调黑胶囊 */
|
||||
.say-hi {
|
||||
background: linear-gradient(135deg, #fff, #fff4b7);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.8);
|
||||
background: #2b2b30;
|
||||
border-radius: 999rpx;
|
||||
padding: 12rpx 22rpx;
|
||||
padding: 14rpx 26rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 800;
|
||||
color: #272235;
|
||||
box-shadow: 0 8rpx 16rpx rgba(255, 159, 28, 0.14);
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
box-shadow: 0 12rpx 28rpx rgba(43, 43, 48, 0.24);
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
|
||||
.say-hi:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
|
||||
/* 骨架 */
|
||||
.nearby-skeleton {
|
||||
height: 128rpx;
|
||||
background: linear-gradient(90deg, #f0eef5 25%, #e8e4f0 50%, #f0eef5 75%);
|
||||
background: linear-gradient(90deg, #ebecf1 25%, #e2e4ea 50%, #ebecf1 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shine 1.4s infinite;
|
||||
border-radius: 40rpx;
|
||||
@@ -376,13 +390,13 @@
|
||||
|
||||
.empty-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 900;
|
||||
color: #272235;
|
||||
font-weight: 700;
|
||||
color: #1b1b20;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: #9b8fa8;
|
||||
color: #b9bdc6;
|
||||
text-align: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
import { api } from '../../utils/api'
|
||||
import { getAvatarGradient, formatTime } from '../../utils/format'
|
||||
import { AppGlobalData } from '../../types/index'
|
||||
|
||||
const app = getApp<{ globalData: AppGlobalData }>()
|
||||
|
||||
Page({
|
||||
data: { statusBarHeight: 0, list: [] as any[] },
|
||||
data: {
|
||||
statusBarHeight: 0,
|
||||
navbarHeight: 44,
|
||||
navbarPaddingRight: 0,
|
||||
list: [] as any[],
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const info = wx.getSystemInfoSync()
|
||||
this.setData({ statusBarHeight: info.statusBarHeight })
|
||||
const { statusBarHeight, navbarHeight, navbarPaddingRight } = app.globalData.navBarInfo
|
||||
this.setData({ statusBarHeight, navbarHeight, navbarPaddingRight })
|
||||
this.loadNotifications()
|
||||
},
|
||||
|
||||
async loadNotifications() {
|
||||
try {
|
||||
const res = await wx.cloud.callFunction({ name: 'getNotifications', data: {} }) as any
|
||||
const list = (res.result?.data || []).map((n: any) => ({
|
||||
const list = await api.getNotifications()
|
||||
const enriched = (list || []).map((n: any) => ({
|
||||
...n,
|
||||
gradient: getAvatarGradient(n.fromId || ''),
|
||||
timeText: formatTime(n.createdAt),
|
||||
}))
|
||||
this.setData({ list })
|
||||
this.setData({ list: enriched })
|
||||
} catch {}
|
||||
},
|
||||
|
||||
@@ -27,7 +36,7 @@ Page({
|
||||
},
|
||||
|
||||
onReadAll() {
|
||||
wx.cloud.callFunction({ name: 'markAllRead', data: {} }).catch(() => {})
|
||||
api.markAllRead().catch(() => {})
|
||||
const list = this.data.list.map((n: any) => ({ ...n, isRead: true }))
|
||||
this.setData({ list })
|
||||
},
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
<view class="page">
|
||||
<view class="safe-top" style="height: {{statusBarHeight}}px"></view>
|
||||
<view class="nav">
|
||||
<view class="nav-back" bindtap="onBack">‹</view>
|
||||
<view class="nav-title">消息通知</view>
|
||||
<view class="nav-right" bindtap="onReadAll">全部已读</view>
|
||||
<!-- 固定液态玻璃顶栏:返回 + 标题 + 全部已读(右侧避让胶囊按钮) -->
|
||||
<view class="glass-topbar">
|
||||
<view style="height: {{statusBarHeight}}px;"></view>
|
||||
<view class="navbar" style="height: {{navbarHeight}}px; padding-right: {{navbarPaddingRight}}px;">
|
||||
<view class="nav-back" bindtap="onBack">‹</view>
|
||||
<view class="navbar-title">消息通知</view>
|
||||
<view class="nav-right" bindtap="onReadAll">全部已读</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 顶栏占位 -->
|
||||
<view style="height: {{statusBarHeight + navbarHeight}}px;"></view>
|
||||
|
||||
<scroll-view scroll-y="true" enhanced="true" show-scrollbar="false">
|
||||
<view wx:if="{{list.length === 0}}" class="empty">
|
||||
<text class="empty-emoji">🔔</text>
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
.page { min-height: 100vh; background: linear-gradient(180deg, #fff8f2 0%, #f5f0ff 100%); }
|
||||
.nav { display: flex; align-items: center; padding: 14rpx 28rpx; gap: 16rpx; }
|
||||
.nav-back { font-size: 60rpx; color: #272235; font-weight: 300; line-height: 1; }
|
||||
.nav-title { flex: 1; font-size: 32rpx; font-weight: 900; color: #272235; text-align: center; }
|
||||
.nav-right { font-size: 26rpx; color: #9b8fa8; }
|
||||
/* ── 通知页:Vital 风格 ── */
|
||||
.page { min-height: 100vh; background: #f1f2f6; }
|
||||
|
||||
/* 顶栏内导航行:返回 + 标题 + 全部已读(玻璃样式由 .glass-topbar 提供) */
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding-left: 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.navbar-title { font-size: 36rpx; font-weight: 700; }
|
||||
|
||||
.nav-back { font-size: 60rpx; color: #1b1b20; font-weight: 300; line-height: 1; padding: 0 10rpx; }
|
||||
|
||||
/* 全部已读:靠右紧贴胶囊 */
|
||||
.nav-right { margin-left: auto; margin-right: 8rpx; font-size: 26rpx; color: #6e7280; padding: 10rpx 0; }
|
||||
.nav-right:active { opacity: 0.6; }
|
||||
.empty { display: flex; flex-direction: column; align-items: center; padding: 120rpx 60rpx; gap: 20rpx; }
|
||||
.empty-emoji { font-size: 80rpx; }
|
||||
.empty-tip { font-size: 28rpx; color: #9b8fa8; }
|
||||
.notify-item { display: flex; align-items: center; gap: 20rpx; padding: 22rpx 28rpx; background: rgba(255,255,255,0.80); border-bottom: 1rpx solid rgba(43,37,61,0.05); }
|
||||
.notify-item.unread { background: rgba(255,229,240,0.40); }
|
||||
.n-avatar { width: 80rpx; height: 80rpx; border-radius: 26rpx; display: flex; align-items: center; justify-content: center; font-size: 34rpx; font-weight: 800; color: rgba(39,34,53,0.72); flex-shrink: 0; }
|
||||
.empty-tip { font-size: 28rpx; color: #b9bdc6; }
|
||||
.notify-item { display: flex; align-items: center; gap: 20rpx; padding: 22rpx 28rpx; background: #ffffff; border-bottom: 1rpx solid rgba(27, 27, 32, 0.04); }
|
||||
.notify-item.unread { background: rgba(139, 124, 246, 0.08); }
|
||||
.n-avatar { width: 80rpx; height: 80rpx; border-radius: 26rpx; display: flex; align-items: center; justify-content: center; font-size: 34rpx; font-weight: 700; color: rgba(27, 27, 32, 0.65); flex-shrink: 0; }
|
||||
.n-body { flex: 1; }
|
||||
.n-text { display: block; font-size: 28rpx; color: #272235; line-height: 1.5; }
|
||||
.n-time { display: block; font-size: 22rpx; color: #9b8fa8; margin-top: 6rpx; }
|
||||
.unread-dot { width: 14rpx; height: 14rpx; border-radius: 50%; background: #ff4f91; flex-shrink: 0; }
|
||||
.n-text { display: block; font-size: 28rpx; color: #1b1b20; line-height: 1.5; }
|
||||
.n-time { display: block; font-size: 22rpx; color: #b9bdc6; margin-top: 6rpx; }
|
||||
.unread-dot { width: 14rpx; height: 14rpx; border-radius: 50%; background: #8b7cf6; flex-shrink: 0; }
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { api } from '../../utils/api'
|
||||
import { getAvatarGradient, getPetEmoji } from '../../utils/format'
|
||||
import { AppGlobalData } from '../../types/index'
|
||||
|
||||
const app = getApp<{ userInfo: any }>()
|
||||
const app = getApp<{ globalData: AppGlobalData }>()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
.detail-page { min-height: 100vh; background: linear-gradient(180deg, #fff8f2 0%, #f5f0ff 100%); }
|
||||
/* ── 用户/宠物详情:Vital 风格 ── */
|
||||
.detail-page { min-height: 100vh; background: #f1f2f6; }
|
||||
.detail-nav { display: flex; align-items: center; padding: 16rpx 28rpx; gap: 16rpx; }
|
||||
.nav-back { font-size: 60rpx; color: #272235; font-weight: 300; padding: 0 10rpx; line-height: 1; }
|
||||
.nav-title { flex: 1; font-size: 32rpx; font-weight: 900; color: #272235; text-align: center; }
|
||||
.nav-more { font-size: 36rpx; color: #9b8fa8; padding: 0 10rpx; }
|
||||
.nav-back { font-size: 60rpx; color: #1b1b20; font-weight: 300; padding: 0 10rpx; line-height: 1; }
|
||||
.nav-title { flex: 1; font-size: 32rpx; font-weight: 700; color: #1b1b20; text-align: center; }
|
||||
.nav-more { font-size: 36rpx; color: #b9bdc6; padding: 0 10rpx; }
|
||||
.detail-scroll { min-height: 80vh; }
|
||||
|
||||
.user-card { margin: 0 28rpx 28rpx; background: rgba(255,255,255,0.88); border-radius: 48rpx; border: 1rpx solid rgba(255,255,255,0.72); padding: 28rpx; box-shadow: 0 18rpx 40rpx rgba(95,49,104,0.12); }
|
||||
.user-card { margin: 0 28rpx 28rpx; background: #ffffff; border-radius: 48rpx; padding: 28rpx; box-shadow: 0 16rpx 48rpx rgba(27, 27, 32, 0.06); }
|
||||
.card-top { display: flex; gap: 24rpx; margin-bottom: 24rpx; }
|
||||
.card-avatar { width: 120rpx; height: 120rpx; border-radius: 36rpx; display: flex; align-items: center; justify-content: center; font-size: 52rpx; font-weight: 800; color: rgba(39,34,53,0.72); flex-shrink: 0; }
|
||||
.card-avatar { width: 120rpx; height: 120rpx; border-radius: 36rpx; display: flex; align-items: center; justify-content: center; font-size: 52rpx; font-weight: 700; color: rgba(27, 27, 32, 0.65); flex-shrink: 0; }
|
||||
.card-meta { flex: 1; }
|
||||
.card-name { display: block; font-size: 34rpx; font-weight: 900; color: #272235; margin-bottom: 8rpx; }
|
||||
.card-location { display: block; font-size: 24rpx; color: #9b8fa8; margin-bottom: 10rpx; }
|
||||
.card-bio { display: block; font-size: 26rpx; color: #6a6178; line-height: 1.5; }
|
||||
.card-name { display: block; font-size: 34rpx; font-weight: 700; color: #1b1b20; margin-bottom: 8rpx; }
|
||||
.card-location { display: block; font-size: 24rpx; color: #b9bdc6; margin-bottom: 10rpx; }
|
||||
.card-bio { display: block; font-size: 26rpx; color: #6e7280; line-height: 1.5; }
|
||||
|
||||
.pets-row { display: flex; gap: 16rpx; flex-wrap: wrap; margin-bottom: 24rpx; }
|
||||
.pet-chip { display: flex; align-items: center; gap: 12rpx; background: linear-gradient(135deg, rgba(255,255,255,0.8), rgba(201,247,223,0.6)); border: 1rpx solid rgba(255,255,255,0.72); border-radius: 24rpx; padding: 16rpx 20rpx; }
|
||||
.pet-chip { display: flex; align-items: center; gap: 12rpx; background: #f6f7fa; border-radius: 24rpx; padding: 16rpx 20rpx; }
|
||||
.chip-emoji { font-size: 36rpx; }
|
||||
.chip-name { display: block; font-size: 26rpx; font-weight: 800; color: #272235; }
|
||||
.chip-breed { display: block; font-size: 22rpx; color: #9b8fa8; }
|
||||
.chip-name { display: block; font-size: 26rpx; font-weight: 600; color: #1b1b20; }
|
||||
.chip-breed { display: block; font-size: 22rpx; color: #b9bdc6; }
|
||||
|
||||
.action-row { display: flex; gap: 16rpx; }
|
||||
.follow-action { flex: 1; background: linear-gradient(135deg, #ff4f91, #ff9f1c); color: #fff; border-radius: 999rpx; padding: 20rpx; text-align: center; font-size: 28rpx; font-weight: 800; box-shadow: 0 10rpx 22rpx rgba(255,79,145,0.28); }
|
||||
.follow-action.following { background: rgba(255,255,255,0.80); border: 1rpx solid rgba(43,37,61,0.12); color: #9b8fa8; box-shadow: none; }
|
||||
.msg-action { flex: 1; background: rgba(255,255,255,0.80); border: 1rpx solid rgba(43,37,61,0.10); border-radius: 999rpx; padding: 20rpx; text-align: center; font-size: 28rpx; font-weight: 800; color: #272235; }
|
||||
.follow-action { flex: 1; background: #2b2b30; color: #fff; border-radius: 999rpx; padding: 20rpx; text-align: center; font-size: 28rpx; font-weight: 600; box-shadow: 0 12rpx 28rpx rgba(43, 43, 48, 0.24); }
|
||||
.follow-action.following { background: #f1f2f6; color: #6e7280; box-shadow: none; }
|
||||
.msg-action { flex: 1; background: #f1f2f6; border-radius: 999rpx; padding: 20rpx; text-align: center; font-size: 28rpx; font-weight: 600; color: #1b1b20; }
|
||||
|
||||
.section-hd { padding: 20rpx 28rpx 10rpx; }
|
||||
.section-title { font-size: 26rpx; font-weight: 900; color: #9b8fa8; }
|
||||
.empty-posts { text-align: center; font-size: 26rpx; color: #9b8fa8; padding: 60rpx; }
|
||||
.section-title { font-size: 26rpx; font-weight: 600; color: #b9bdc6; }
|
||||
.empty-posts { text-align: center; font-size: 26rpx; color: #b9bdc6; padding: 60rpx; }
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import { api } from '../../utils/api'
|
||||
import { BREED_OPTIONS } from '../../utils/constants'
|
||||
import { getPetEmoji } from '../../utils/format'
|
||||
import { AppGlobalData } from '../../types/index'
|
||||
|
||||
const SPECIES_LABELS: Record<string, string> = { dog: '狗狗', cat: '猫猫', other: '其他' }
|
||||
const PET_TAGS = ['活泼', '温顺', '黏人', '独立', '爱玩', '怕生', '聪明', '贪吃', '爱运动', '安静']
|
||||
|
||||
const app = getApp<{ userInfo: any }>()
|
||||
const PRESET_TAGS = ['活泼', '温顺', '黏人', '独立', '爱玩', '怕生', '聪明', '贪吃', '爱运动', '安静']
|
||||
|
||||
const app = getApp<{ globalData: AppGlobalData }>()
|
||||
|
||||
// 根据已选标签数组生成 {tag: boolean} 映射,供 WXML 直接判断选中态
|
||||
function buildTagSet(tags: string[]): Record<string, boolean> {
|
||||
const set: Record<string, boolean> = {}
|
||||
tags.forEach(t => { set[t] = true })
|
||||
return set
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 0,
|
||||
navbarHeight: 44,
|
||||
navbarPaddingRight: 0,
|
||||
isEdit: false,
|
||||
petId: '',
|
||||
form: {
|
||||
@@ -23,12 +34,24 @@ Page({
|
||||
emoji: '🐶',
|
||||
},
|
||||
speciesLabel: '狗狗',
|
||||
allTags: PET_TAGS,
|
||||
// 品种
|
||||
currentBreeds: BREED_OPTIONS['dog'],
|
||||
breedIndex: -1,
|
||||
// 标签
|
||||
allTags: [...PRESET_TAGS] as string[],
|
||||
selectedTagSet: {} as Record<string, boolean>,
|
||||
customTagInput: '',
|
||||
},
|
||||
|
||||
onLoad(query: { id?: string }) {
|
||||
const info = wx.getSystemInfoSync()
|
||||
this.setData({ statusBarHeight: info.statusBarHeight })
|
||||
const { statusBarHeight, navbarHeight, navbarPaddingRight } = app.globalData.navBarInfo
|
||||
this.setData({
|
||||
statusBarHeight,
|
||||
navbarHeight,
|
||||
navbarPaddingRight,
|
||||
currentBreeds: BREED_OPTIONS['dog'],
|
||||
})
|
||||
|
||||
if (query.id) {
|
||||
this.setData({ isEdit: true, petId: query.id })
|
||||
@@ -40,6 +63,15 @@ Page({
|
||||
const user = app.globalData?.userInfo
|
||||
const pet = user?.pets?.find((p: any) => p._id === id)
|
||||
if (!pet) return
|
||||
|
||||
const breeds = BREED_OPTIONS[pet.species] || BREED_OPTIONS['dog']
|
||||
const breedIndex = breeds.indexOf(pet.breed)
|
||||
const petTags: string[] = pet.tags || []
|
||||
|
||||
// 把宠物已有的自定义标签合并进 allTags
|
||||
const merged = [...PRESET_TAGS]
|
||||
petTags.forEach(t => { if (!merged.includes(t)) merged.push(t) })
|
||||
|
||||
this.setData({
|
||||
form: {
|
||||
name: pet.name,
|
||||
@@ -48,10 +80,14 @@ Page({
|
||||
gender: pet.gender,
|
||||
birthday: pet.birthday || '',
|
||||
bio: pet.bio || '',
|
||||
tags: pet.tags || [],
|
||||
tags: petTags,
|
||||
emoji: pet.emoji,
|
||||
},
|
||||
speciesLabel: SPECIES_LABELS[pet.species] || '狗狗',
|
||||
currentBreeds: breeds,
|
||||
breedIndex,
|
||||
allTags: merged,
|
||||
selectedTagSet: buildTagSet(petTags),
|
||||
})
|
||||
},
|
||||
|
||||
@@ -65,27 +101,27 @@ Page({
|
||||
itemList: ['狗狗', '猫猫', '其他'],
|
||||
success: res => {
|
||||
const species = (['dog', 'cat', 'other'] as const)[res.tapIndex]
|
||||
const breeds = BREED_OPTIONS[species]
|
||||
this.setData({
|
||||
'form.species': species,
|
||||
'form.breed': '',
|
||||
speciesLabel: SPECIES_LABELS[species],
|
||||
'form.emoji': getPetEmoji(species),
|
||||
speciesLabel: SPECIES_LABELS[species],
|
||||
currentBreeds: breeds,
|
||||
breedIndex: -1,
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
onBreed() {
|
||||
const breeds = BREED_OPTIONS[this.data.form.species] || BREED_OPTIONS.dog
|
||||
wx.showActionSheet({
|
||||
itemList: breeds,
|
||||
success: res => {
|
||||
const breed = breeds[res.tapIndex]
|
||||
this.setData({
|
||||
'form.breed': breed,
|
||||
'form.emoji': getPetEmoji(this.data.form.species, breed),
|
||||
})
|
||||
},
|
||||
// 品种 picker 选中回调
|
||||
onBreedChange(e: WechatMiniprogram.CustomEvent) {
|
||||
const idx = Number(e.detail.value)
|
||||
const breed = this.data.currentBreeds[idx]
|
||||
this.setData({
|
||||
breedIndex: idx,
|
||||
'form.breed': breed,
|
||||
'form.emoji': getPetEmoji(this.data.form.species, breed),
|
||||
})
|
||||
},
|
||||
|
||||
@@ -102,13 +138,64 @@ Page({
|
||||
this.setData({ 'form.birthday': dob, 'form.age': age })
|
||||
},
|
||||
|
||||
// 预设标签 — 点击切换
|
||||
onTagToggle(e: WechatMiniprogram.CustomEvent) {
|
||||
const tag = e.currentTarget.dataset.tag as string
|
||||
const tags = [...this.data.form.tags]
|
||||
const idx = tags.indexOf(tag)
|
||||
if (idx >= 0) tags.splice(idx, 1)
|
||||
else if (tags.length < 5) tags.push(tag)
|
||||
this.setData({ 'form.tags': tags })
|
||||
if (idx >= 0) {
|
||||
tags.splice(idx, 1)
|
||||
} else {
|
||||
if (tags.length >= 8) {
|
||||
wx.showToast({ title: '最多选8个标签', icon: 'none' })
|
||||
return
|
||||
}
|
||||
tags.push(tag)
|
||||
}
|
||||
this.setData({ 'form.tags': tags, selectedTagSet: buildTagSet(tags) })
|
||||
},
|
||||
|
||||
// 自定义标签输入
|
||||
onCustomTagInput(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ customTagInput: e.detail.value })
|
||||
},
|
||||
|
||||
// 添加自定义标签
|
||||
onAddCustomTag() {
|
||||
const raw = this.data.customTagInput.trim()
|
||||
if (!raw) return
|
||||
|
||||
// 仅允许中英文和数字,最多5个字符
|
||||
if ([...raw].length > 5) {
|
||||
wx.showToast({ title: '标签最多5个字', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!/^[一-龥a-zA-Z0-9]+$/.test(raw)) {
|
||||
wx.showToast({ title: '只能输入文字或字母', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const tags = [...this.data.form.tags]
|
||||
if (tags.includes(raw)) {
|
||||
this.setData({ customTagInput: '' })
|
||||
return
|
||||
}
|
||||
if (tags.length >= 8) {
|
||||
wx.showToast({ title: '最多选8个标签', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
tags.push(raw)
|
||||
const allTags = this.data.allTags.includes(raw)
|
||||
? this.data.allTags
|
||||
: [...this.data.allTags, raw]
|
||||
|
||||
this.setData({
|
||||
'form.tags': tags,
|
||||
selectedTagSet: buildTagSet(tags),
|
||||
allTags,
|
||||
customTagInput: '',
|
||||
})
|
||||
},
|
||||
|
||||
async onChoosePhoto() {
|
||||
@@ -140,27 +227,51 @@ Page({
|
||||
}
|
||||
wx.showLoading({ title: '保存中...' })
|
||||
try {
|
||||
await api.updatePet(isEdit ? { ...form, _id: petId } : form)
|
||||
const saved = await api.updatePet(isEdit ? { ...form, _id: petId } : form)
|
||||
// 同步本地缓存,让"我的"页面立即看到最新的宠物档案
|
||||
const user = app.globalData.userInfo
|
||||
if (user) {
|
||||
const pets = Array.isArray(user.pets) ? [...user.pets] : []
|
||||
const idx = pets.findIndex((p: any) => p._id === saved._id)
|
||||
if (idx >= 0) pets[idx] = saved
|
||||
else pets.push(saved)
|
||||
user.pets = pets
|
||||
}
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => wx.navigateBack(), 800)
|
||||
} catch {
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '保存失败', icon: 'none' })
|
||||
const message = e instanceof Error ? e.message : '保存失败'
|
||||
wx.showToast({ title: message, icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
onDelete() {
|
||||
const { petId } = this.data
|
||||
wx.showModal({
|
||||
title: '删除宠物档案',
|
||||
content: '确定要删除吗?',
|
||||
confirmText: '删除',
|
||||
confirmColor: '#ff4f91',
|
||||
confirmColor: '#d4596a',
|
||||
success: res => {
|
||||
if (res.confirm) {
|
||||
wx.showToast({ title: '已删除', icon: 'success' })
|
||||
setTimeout(() => wx.navigateBack(), 800)
|
||||
}
|
||||
if (!res.confirm) return
|
||||
wx.showLoading({ title: '删除中...' })
|
||||
api.deletePet(petId)
|
||||
.then(() => {
|
||||
const user = app.globalData.userInfo
|
||||
if (user && Array.isArray(user.pets)) {
|
||||
user.pets = user.pets.filter((p: any) => p._id !== petId)
|
||||
}
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '已删除', icon: 'success' })
|
||||
setTimeout(() => wx.navigateBack(), 800)
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
wx.hideLoading()
|
||||
const message = e instanceof Error ? e.message : '删除失败'
|
||||
wx.showToast({ title: message, icon: 'none' })
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<view class="page">
|
||||
<!-- 状态栏占位 -->
|
||||
<view class="safe-top" style="height: {{statusBarHeight}}px"></view>
|
||||
<view class="nav">
|
||||
|
||||
<!-- 导航栏:动态高度 + 右侧胶囊避让 -->
|
||||
<view class="nav" style="height: {{navbarHeight}}px; padding-right: {{navbarPaddingRight}}px; box-sizing: border-box;">
|
||||
<view class="nav-back" bindtap="onBack">取消</view>
|
||||
<view class="nav-title">{{isEdit ? '编辑宠物' : '添加宠物'}}</view>
|
||||
<view class="nav-save" bindtap="onSave">保存</view>
|
||||
@@ -11,14 +14,16 @@
|
||||
|
||||
<!-- 宠物头像 -->
|
||||
<view class="avatar-section" bindtap="onChoosePhoto">
|
||||
<view class="pet-avatar" style="background: linear-gradient(135deg, #c9f7df, #d9d2ff);">
|
||||
<view class="pet-avatar" style="background: linear-gradient(135deg, #d9d2fb, #efedfd);">
|
||||
<text class="pet-emoji-big">{{form.emoji || '🐾'}}</text>
|
||||
</view>
|
||||
<text class="avatar-tip">点击更换照片</text>
|
||||
</view>
|
||||
|
||||
<!-- 名字 -->
|
||||
<!-- 基础信息 -->
|
||||
<view class="field-group">
|
||||
|
||||
<!-- 名字 -->
|
||||
<view class="field-item">
|
||||
<text class="field-label">宠物名字 *</text>
|
||||
<input class="field-input" value="{{form.name}}" bindinput="onField" data-key="name" placeholder="给TA起个名字" maxlength="10" />
|
||||
@@ -33,13 +38,15 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 品种 -->
|
||||
<view class="field-item" bindtap="onBreed">
|
||||
<!-- 品种 — picker 滚轮 -->
|
||||
<view class="field-item">
|
||||
<text class="field-label">品种</text>
|
||||
<view class="field-select">
|
||||
<text>{{form.breed || '选择品种'}}</text>
|
||||
<text class="select-arrow">›</text>
|
||||
</view>
|
||||
<picker mode="selector" range="{{currentBreeds}}" value="{{breedIndex}}" bindchange="onBreedChange">
|
||||
<view class="field-select">
|
||||
<text class="{{form.breed ? '' : 'placeholder-text'}}">{{form.breed || '选择品种'}}</text>
|
||||
<text class="select-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 性别 -->
|
||||
@@ -56,7 +63,7 @@
|
||||
<text class="field-label">生日</text>
|
||||
<picker mode="date" value="{{form.birthday}}" bindchange="onBirthday">
|
||||
<view class="field-select">
|
||||
<text>{{form.birthday || '选择生日'}}</text>
|
||||
<text class="{{form.birthday ? '' : 'placeholder-text'}}">{{form.birthday || '选择生日'}}</text>
|
||||
<text class="select-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
@@ -80,22 +87,41 @@
|
||||
|
||||
<!-- 性格标签 -->
|
||||
<view class="tags-section">
|
||||
<text class="tags-title">性格标签</text>
|
||||
<view class="tags-header">
|
||||
<text class="tags-title">性格标签</text>
|
||||
<text class="tags-count">已选 {{form.tags.length}}/8</text>
|
||||
</view>
|
||||
|
||||
<!-- 预设 + 自定义标签网格 -->
|
||||
<view class="tag-grid">
|
||||
<view
|
||||
wx:for="{{allTags}}"
|
||||
wx:key="index"
|
||||
class="tag-chip {{form.tags.includes(item) ? 'tag-on' : ''}}"
|
||||
class="tag-chip {{selectedTagSet[item] ? 'tag-on' : ''}}"
|
||||
bindtap="onTagToggle"
|
||||
data-tag="{{item}}"
|
||||
>{{item}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 自定义标签输入 -->
|
||||
<view class="custom-tag-row">
|
||||
<input
|
||||
class="custom-tag-input"
|
||||
value="{{customTagInput}}"
|
||||
bindinput="onCustomTagInput"
|
||||
bindconfirm="onAddCustomTag"
|
||||
placeholder="自定义标签(最多5字)"
|
||||
maxlength="5"
|
||||
confirm-type="done"
|
||||
/>
|
||||
<view class="custom-tag-add {{customTagInput ? '' : 'add-disabled'}}" bindtap="onAddCustomTag">添加</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 删除 -->
|
||||
<view wx:if="{{isEdit}}" class="delete-btn" bindtap="onDelete">删除这只宠物</view>
|
||||
|
||||
<view style="height: 60rpx;"></view>
|
||||
<view style="height: 80rpx;"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
@@ -1,35 +1,46 @@
|
||||
.page { min-height: 100vh; background: linear-gradient(180deg, #fff8f2 0%, #f5f0ff 100%); }
|
||||
/* ── 宠物编辑:Vital 风格表单页 ── */
|
||||
.page { min-height: 100vh; background: #f1f2f6; }
|
||||
.nav { display: flex; align-items: center; padding: 14rpx 28rpx; gap: 16rpx; }
|
||||
.nav-back { font-size: 28rpx; font-weight: 700; color: #9b8fa8; }
|
||||
.nav-title { flex: 1; font-size: 32rpx; font-weight: 900; color: #272235; text-align: center; }
|
||||
.nav-save { font-size: 28rpx; font-weight: 900; color: #ff4f91; }
|
||||
.nav-back { font-size: 28rpx; font-weight: 600; color: #6e7280; }
|
||||
.nav-title { flex: 1; font-size: 32rpx; font-weight: 700; color: #1b1b20; text-align: center; }
|
||||
.nav-save { font-size: 28rpx; font-weight: 700; color: #7563e0; }
|
||||
.scroll { }
|
||||
|
||||
.form { padding: 0 28rpx; }
|
||||
|
||||
.avatar-section { display: flex; flex-direction: column; align-items: center; padding: 28rpx 0 32rpx; gap: 12rpx; }
|
||||
.pet-avatar { width: 140rpx; height: 140rpx; border-radius: 44rpx; display: flex; align-items: center; justify-content: center; box-shadow: 0 12rpx 24rpx rgba(78,56,96,0.15); }
|
||||
.pet-avatar { width: 140rpx; height: 140rpx; border-radius: 44rpx; display: flex; align-items: center; justify-content: center; box-shadow: 0 12rpx 28rpx rgba(27, 27, 32, 0.10); }
|
||||
.pet-emoji-big { font-size: 72rpx; line-height: 1; }
|
||||
.avatar-tip { font-size: 24rpx; color: #9b8fa8; }
|
||||
.avatar-tip { font-size: 24rpx; color: #b9bdc6; }
|
||||
|
||||
.field-group { background: rgba(255,255,255,0.90); border-radius: 36rpx; border: 1rpx solid rgba(255,255,255,0.72); overflow: hidden; box-shadow: 0 12rpx 26rpx rgba(78,56,96,0.08); margin-bottom: 24rpx; }
|
||||
.field-item { display: flex; align-items: center; justify-content: space-between; padding: 24rpx 28rpx; border-bottom: 1rpx solid rgba(43,37,61,0.06); }
|
||||
.field-group { background: #ffffff; border-radius: 36rpx; overflow: hidden; box-shadow: var(--shadow-soft); margin-bottom: 24rpx; }
|
||||
.field-item { display: flex; align-items: center; justify-content: space-between; padding: 24rpx 28rpx; border-bottom: 1rpx solid rgba(27, 27, 32, 0.05); }
|
||||
.field-item:last-child { border-bottom: none; }
|
||||
.field-item-col { flex-direction: column; align-items: flex-start; gap: 14rpx; }
|
||||
.field-label { font-size: 28rpx; font-weight: 700; color: #272235; flex-shrink: 0; }
|
||||
.field-input { flex: 1; font-size: 28rpx; color: #272235; text-align: right; }
|
||||
.field-select { display: flex; align-items: center; gap: 8rpx; font-size: 28rpx; color: #9b8fa8; }
|
||||
.select-arrow { font-size: 36rpx; color: #c4b8d0; font-weight: 300; }
|
||||
.field-textarea { width: 100%; min-height: 100rpx; font-size: 28rpx; color: #272235; line-height: 1.6; }
|
||||
.field-label { font-size: 28rpx; font-weight: 600; color: #1b1b20; flex-shrink: 0; }
|
||||
.field-input { flex: 1; font-size: 28rpx; color: #1b1b20; text-align: right; }
|
||||
.field-select { display: flex; align-items: center; gap: 8rpx; font-size: 28rpx; color: #6e7280; }
|
||||
.select-arrow { font-size: 36rpx; color: #b9bdc6; font-weight: 300; }
|
||||
.field-textarea { width: 100%; min-height: 100rpx; font-size: 28rpx; color: #1b1b20; line-height: 1.6; }
|
||||
|
||||
.gender-btns { display: flex; gap: 14rpx; }
|
||||
.gender-btn { background: rgba(255,255,255,0.80); border: 1rpx solid rgba(43,37,61,0.12); border-radius: 999rpx; padding: 12rpx 24rpx; font-size: 26rpx; color: #9b8fa8; font-weight: 700; }
|
||||
.g-active { background: #ffe5f0; border-color: #ffb6d0; color: #a91d5b; }
|
||||
.gender-btn { background: #f1f2f6; border-radius: 999rpx; padding: 12rpx 24rpx; font-size: 26rpx; color: #6e7280; font-weight: 600; }
|
||||
.g-active { background: #2b2b30; color: #fff; }
|
||||
|
||||
.placeholder-text { color: #b9bdc6; }
|
||||
|
||||
.tags-section { margin-bottom: 28rpx; }
|
||||
.tags-title { display: block; font-size: 26rpx; font-weight: 900; color: #9b8fa8; margin-bottom: 16rpx; }
|
||||
.tag-grid { display: flex; gap: 14rpx; flex-wrap: wrap; }
|
||||
.tag-chip { background: rgba(255,255,255,0.80); border: 1rpx solid rgba(43,37,61,0.10); border-radius: 999rpx; padding: 12rpx 24rpx; font-size: 26rpx; color: #6a6178; font-weight: 700; }
|
||||
.tag-on { background: #f0e8ff; border-color: #d4b8ff; color: #8c5cff; }
|
||||
.tags-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16rpx; }
|
||||
.tags-title { font-size: 26rpx; font-weight: 600; color: #b9bdc6; }
|
||||
.tags-count { font-size: 22rpx; color: #b9bdc6; font-variant-numeric: tabular-nums; }
|
||||
.tag-grid { display: flex; gap: 14rpx; flex-wrap: wrap; margin-bottom: 20rpx; }
|
||||
.tag-chip { background: #ffffff; border-radius: 999rpx; padding: 12rpx 24rpx; font-size: 26rpx; color: #6e7280; font-weight: 600; }
|
||||
.tag-on { background: rgba(139, 124, 246, 0.12); color: #7563e0; }
|
||||
|
||||
.delete-btn { text-align: center; font-size: 28rpx; color: #ff4f91; background: rgba(255,79,145,0.08); border-radius: 999rpx; padding: 24rpx; margin-top: 40rpx; }
|
||||
.custom-tag-row { display: flex; gap: 14rpx; align-items: center; margin-top: 4rpx; }
|
||||
.custom-tag-input { flex: 1; height: 68rpx; background: #ffffff; border-radius: 999rpx; padding: 0 24rpx; font-size: 26rpx; color: #1b1b20; }
|
||||
.custom-tag-add { background: #2b2b30; color: #fff; border-radius: 999rpx; padding: 14rpx 30rpx; font-size: 26rpx; font-weight: 600; flex-shrink: 0; }
|
||||
.add-disabled { background: #e7e9ef; color: #b9bdc6; }
|
||||
|
||||
/* 删除:唯一保留的警示色,降饱和处理 */
|
||||
.delete-btn { text-align: center; font-size: 28rpx; color: #d4596a; background: rgba(212, 89, 106, 0.08); border-radius: 999rpx; padding: 24rpx; margin-top: 40rpx; }
|
||||
|
||||
@@ -1,7 +1,43 @@
|
||||
import { api } from '../../utils/api'
|
||||
import { formatTime, getAvatarGradient } from '../../utils/format'
|
||||
import { AppGlobalData } from '../../types/index'
|
||||
|
||||
const app = getApp<{ userInfo: any }>()
|
||||
const app = getApp<{ globalData: AppGlobalData }>()
|
||||
|
||||
function normalizeAuthor(author: any, authorId = '') {
|
||||
return {
|
||||
...(author || {}),
|
||||
nickName: author?.nickName || '匿名用户',
|
||||
avatarUrl: author?.avatarUrl || '',
|
||||
openid: author?.openid || authorId,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePost(post: any) {
|
||||
return {
|
||||
authorId: '',
|
||||
...(post || {}),
|
||||
author: normalizeAuthor(post?.author, post?.authorId),
|
||||
content: post?.content || '',
|
||||
images: Array.isArray(post?.images) ? post.images : [],
|
||||
hashtags: Array.isArray(post?.hashtags) ? post.hashtags : [],
|
||||
likeCount: typeof post?.likeCount === 'number' ? post.likeCount : 0,
|
||||
commentCount: typeof post?.commentCount === 'number' ? post.commentCount : 0,
|
||||
createdAt: post?.createdAt || '',
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeComment(comment: any) {
|
||||
return {
|
||||
authorId: '',
|
||||
...(comment || {}),
|
||||
author: normalizeAuthor(comment?.author, comment?.authorId),
|
||||
content: comment?.content || '',
|
||||
createdAt: comment?.createdAt || '',
|
||||
gradient: getAvatarGradient(comment?.authorId || ''),
|
||||
timeText: comment?.createdAt ? formatTime(comment.createdAt) : '',
|
||||
}
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -14,15 +50,17 @@ Page({
|
||||
likeCount: 0,
|
||||
commentText: '',
|
||||
focusInput: false,
|
||||
pendingFocusComment: false,
|
||||
},
|
||||
|
||||
onLoad(query: { id?: string; focus?: string }) {
|
||||
const info = wx.getSystemInfoSync()
|
||||
this.setData({ statusBarHeight: info.statusBarHeight, postId: query.id || '' })
|
||||
this.setData({
|
||||
statusBarHeight: info.statusBarHeight,
|
||||
postId: query.id || '',
|
||||
pendingFocusComment: query.focus === 'comment',
|
||||
})
|
||||
this.loadPost(query.id || '')
|
||||
if (query.focus === 'comment') {
|
||||
this.setData({ focusInput: true })
|
||||
}
|
||||
},
|
||||
|
||||
async loadPost(id: string) {
|
||||
@@ -32,14 +70,19 @@ Page({
|
||||
name: 'getPost',
|
||||
data: { postId: id },
|
||||
}) as any
|
||||
const post = res.result?.data
|
||||
if (!post) return
|
||||
const post = normalizePost(res.result?.data)
|
||||
if (!post?._id) return
|
||||
this.setData({
|
||||
post,
|
||||
avatarGradient: getAvatarGradient(post.authorId),
|
||||
timeText: formatTime(post.createdAt),
|
||||
avatarGradient: getAvatarGradient(post.authorId || ''),
|
||||
timeText: post.createdAt ? formatTime(post.createdAt) : '',
|
||||
likeCount: post.likeCount,
|
||||
})
|
||||
if (this.data.pendingFocusComment) {
|
||||
wx.nextTick(() => {
|
||||
this.setData({ focusInput: true, pendingFocusComment: false })
|
||||
})
|
||||
}
|
||||
this.loadComments(id)
|
||||
} catch {}
|
||||
},
|
||||
@@ -47,11 +90,7 @@ Page({
|
||||
async loadComments(postId: string) {
|
||||
try {
|
||||
const res = await api.getComments(postId)
|
||||
const comments = res.list.map((c: any) => ({
|
||||
...c,
|
||||
gradient: getAvatarGradient(c.authorId),
|
||||
timeText: formatTime(c.createdAt),
|
||||
}))
|
||||
const comments = res.list.map(normalizeComment)
|
||||
this.setData({ comments })
|
||||
} catch {}
|
||||
},
|
||||
@@ -73,6 +112,10 @@ Page({
|
||||
this.setData({ commentText: e.detail.value })
|
||||
},
|
||||
|
||||
onCommentBlur() {
|
||||
this.setData({ focusInput: false })
|
||||
},
|
||||
|
||||
async onSend() {
|
||||
const text = this.data.commentText.trim()
|
||||
if (!text || !this.data.postId) return
|
||||
@@ -81,11 +124,7 @@ Page({
|
||||
await api.addComment(this.data.postId, text)
|
||||
const comments = await api.getComments(this.data.postId)
|
||||
this.setData({
|
||||
comments: comments.list.map((c: any) => ({
|
||||
...c,
|
||||
gradient: getAvatarGradient(c.authorId),
|
||||
timeText: formatTime(c.createdAt),
|
||||
})),
|
||||
comments: comments.list.map(normalizeComment),
|
||||
})
|
||||
} catch {
|
||||
wx.showToast({ title: '发送失败', icon: 'none' })
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
<!-- 作者 -->
|
||||
<view class="post-header" bindtap="onAuthorTap">
|
||||
<view class="avatar" style="background: {{avatarGradient}}">
|
||||
<text>{{post.author.nickName[0]}}</text>
|
||||
<text>{{post.author && post.author.nickName ? post.author.nickName[0] : '?'}}</text>
|
||||
</view>
|
||||
<view class="meta">
|
||||
<text class="uname">{{post.author.nickName}}</text>
|
||||
<text class="uname">{{post.author && post.author.nickName ? post.author.nickName : '匿名用户'}}</text>
|
||||
<text class="utime">{{timeText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -25,11 +25,11 @@
|
||||
<text class="content">{{post.content}}</text>
|
||||
|
||||
<!-- 图片 -->
|
||||
<view wx:if="{{post.images.length > 0}}" class="images">
|
||||
<view wx:if="{{post.images && post.images.length > 0}}" class="images">
|
||||
<image
|
||||
wx:for="{{post.images}}"
|
||||
wx:key="index"
|
||||
class="img {{post.images.length === 1 ? 'img-full' : 'img-grid'}}"
|
||||
class="img {{post.images && post.images.length === 1 ? 'img-full' : 'img-grid'}}"
|
||||
src="{{item}}"
|
||||
mode="aspectFill"
|
||||
lazy-load="true"
|
||||
@@ -73,10 +73,10 @@
|
||||
class="comment-item"
|
||||
>
|
||||
<view class="c-avatar" style="background: {{item.gradient}}">
|
||||
<text>{{item.author.nickName[0]}}</text>
|
||||
<text>{{item.author && item.author.nickName ? item.author.nickName[0] : '?'}}</text>
|
||||
</view>
|
||||
<view class="c-body">
|
||||
<text class="c-name">{{item.author.nickName}}</text>
|
||||
<text class="c-name">{{item.author && item.author.nickName ? item.author.nickName : '匿名用户'}}</text>
|
||||
<text class="c-text">{{item.content}}</text>
|
||||
<text class="c-time">{{item.timeText}}</text>
|
||||
</view>
|
||||
@@ -92,7 +92,9 @@
|
||||
class="comment-input"
|
||||
id="commentInput"
|
||||
value="{{commentText}}"
|
||||
focus="{{focusInput}}"
|
||||
bindinput="onCommentInput"
|
||||
bindblur="onCommentBlur"
|
||||
placeholder="写下你的想法..."
|
||||
confirm-type="send"
|
||||
bindconfirm="onSend"
|
||||
|
||||
@@ -1,46 +1,47 @@
|
||||
.page { display: flex; flex-direction: column; min-height: 100vh; background: linear-gradient(180deg, #fff8f2 0%, #f5f0ff 100%); }
|
||||
/* ── 帖子详情:Vital 风格 ── */
|
||||
.page { display: flex; flex-direction: column; min-height: 100vh; background: #f1f2f6; }
|
||||
.nav { display: flex; align-items: center; padding: 14rpx 28rpx; gap: 16rpx; }
|
||||
.nav-back { font-size: 60rpx; color: #272235; font-weight: 300; line-height: 1; padding-right: 10rpx; }
|
||||
.nav-title { flex: 1; font-size: 32rpx; font-weight: 900; color: #272235; text-align: center; }
|
||||
.nav-more { font-size: 36rpx; color: #9b8fa8; }
|
||||
.nav-back { font-size: 60rpx; color: #1b1b20; font-weight: 300; line-height: 1; padding-right: 10rpx; }
|
||||
.nav-title { flex: 1; font-size: 32rpx; font-weight: 700; color: #1b1b20; text-align: center; }
|
||||
.nav-more { font-size: 36rpx; color: #b9bdc6; }
|
||||
.scroll { flex: 1; }
|
||||
|
||||
.post-wrap { margin: 0 28rpx; background: rgba(255,255,255,0.90); border-radius: 48rpx; border: 1rpx solid rgba(255,255,255,0.72); padding: 28rpx; box-shadow: 0 18rpx 40rpx rgba(95,49,104,0.10); }
|
||||
.post-wrap { margin: 0 28rpx; background: #ffffff; border-radius: 48rpx; padding: 28rpx; box-shadow: 0 16rpx 48rpx rgba(27, 27, 32, 0.06); }
|
||||
.post-header { display: flex; align-items: center; gap: 20rpx; margin-bottom: 20rpx; }
|
||||
.avatar { width: 80rpx; height: 80rpx; border-radius: 28rpx; display: flex; align-items: center; justify-content: center; font-size: 34rpx; font-weight: 800; color: rgba(39,34,53,0.72); flex-shrink: 0; }
|
||||
.avatar { width: 80rpx; height: 80rpx; border-radius: 28rpx; display: flex; align-items: center; justify-content: center; font-size: 34rpx; font-weight: 700; color: rgba(27, 27, 32, 0.65); flex-shrink: 0; }
|
||||
.meta { flex: 1; }
|
||||
.uname { display: block; font-size: 28rpx; font-weight: 800; color: #272235; }
|
||||
.utime { display: block; font-size: 22rpx; color: #9b8fa8; margin-top: 4rpx; }
|
||||
.content { display: block; font-size: 30rpx; color: #272235; line-height: 1.65; margin-bottom: 20rpx; }
|
||||
.uname { display: block; font-size: 28rpx; font-weight: 600; color: #1b1b20; }
|
||||
.utime { display: block; font-size: 22rpx; color: #b9bdc6; margin-top: 4rpx; }
|
||||
.content { display: block; font-size: 30rpx; color: #1b1b20; line-height: 1.65; margin-bottom: 20rpx; }
|
||||
|
||||
.images { display: flex; flex-wrap: wrap; gap: 8rpx; margin-bottom: 20rpx; border-radius: 24rpx; overflow: hidden; }
|
||||
.img-full { width: 100%; height: 360rpx; border-radius: 24rpx; background: #f0eef5; }
|
||||
.img-grid { width: calc(50% - 4rpx); height: 240rpx; background: #f0eef5; }
|
||||
.img-full { width: 100%; height: 360rpx; border-radius: 24rpx; background: #eef0f5; }
|
||||
.img-grid { width: calc(50% - 4rpx); height: 240rpx; background: #eef0f5; }
|
||||
|
||||
.tags-row { display: flex; gap: 10rpx; flex-wrap: wrap; margin-bottom: 20rpx; }
|
||||
.hashtag { font-size: 22rpx; font-weight: 800; color: #8b3cff; background: #f0e8ff; border-radius: 999rpx; padding: 6rpx 14rpx; }
|
||||
.loc-tag { font-size: 22rpx; color: #9b8fa8; background: rgba(255,255,255,0.80); border: 1rpx solid rgba(43,37,61,0.10); border-radius: 999rpx; padding: 6rpx 14rpx; }
|
||||
.mood-tag { font-size: 22rpx; color: #6f4b00; background: #fff0a8; border-radius: 999rpx; padding: 6rpx 14rpx; }
|
||||
.hashtag { font-size: 22rpx; font-weight: 600; color: #7563e0; background: rgba(139, 124, 246, 0.12); border-radius: 999rpx; padding: 6rpx 14rpx; }
|
||||
.loc-tag { font-size: 22rpx; color: #6e7280; background: #f1f2f6; border-radius: 999rpx; padding: 6rpx 14rpx; }
|
||||
.mood-tag { font-size: 22rpx; color: #6e7280; background: #f1f2f6; border-radius: 999rpx; padding: 6rpx 14rpx; }
|
||||
|
||||
.actions { display: flex; gap: 16rpx; padding-top: 20rpx; border-top: 1rpx solid rgba(43,37,61,0.07); }
|
||||
.action-btn { display: flex; align-items: center; gap: 8rpx; font-size: 24rpx; font-weight: 800; color: #6a6178; background: #f7f3ff; border-radius: 999rpx; padding: 10rpx 18rpx; }
|
||||
.action-btn.liked { color: #ff4f91; background: #ffe5f0; }
|
||||
.act-count { font-size: 24rpx; }
|
||||
.actions { display: flex; gap: 16rpx; padding-top: 20rpx; border-top: 1rpx solid rgba(27, 27, 32, 0.06); }
|
||||
.action-btn { display: flex; align-items: center; gap: 8rpx; font-size: 24rpx; font-weight: 600; color: #6e7280; background: #f1f2f6; border-radius: 999rpx; padding: 10rpx 18rpx; }
|
||||
.action-btn.liked { color: #fff; background: #2b2b30; }
|
||||
.act-count { font-size: 24rpx; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.divider { height: 16rpx; background: rgba(43,37,61,0.04); margin: 16rpx 0; }
|
||||
.divider { height: 16rpx; background: rgba(27, 27, 32, 0.03); margin: 16rpx 0; }
|
||||
|
||||
.comments-section { padding: 0 28rpx; }
|
||||
.comments-title { display: block; font-size: 26rpx; font-weight: 900; color: #9b8fa8; margin-bottom: 20rpx; }
|
||||
.no-comment { text-align: center; font-size: 26rpx; color: #9b8fa8; padding: 40rpx 0; }
|
||||
.comments-title { display: block; font-size: 26rpx; font-weight: 600; color: #b9bdc6; margin-bottom: 20rpx; }
|
||||
.no-comment { text-align: center; font-size: 26rpx; color: #b9bdc6; padding: 40rpx 0; }
|
||||
|
||||
.comment-item { display: flex; gap: 16rpx; margin-bottom: 24rpx; }
|
||||
.c-avatar { width: 64rpx; height: 64rpx; border-radius: 20rpx; display: flex; align-items: center; justify-content: center; font-size: 28rpx; font-weight: 800; color: rgba(39,34,53,0.72); flex-shrink: 0; }
|
||||
.c-body { flex: 1; background: rgba(255,255,255,0.80); border-radius: 24rpx; padding: 16rpx 20rpx; }
|
||||
.c-name { display: block; font-size: 24rpx; font-weight: 800; color: #272235; margin-bottom: 6rpx; }
|
||||
.c-text { display: block; font-size: 26rpx; color: #272235; line-height: 1.5; margin-bottom: 8rpx; }
|
||||
.c-time { display: block; font-size: 20rpx; color: #9b8fa8; }
|
||||
.c-avatar { width: 64rpx; height: 64rpx; border-radius: 20rpx; display: flex; align-items: center; justify-content: center; font-size: 28rpx; font-weight: 700; color: rgba(27, 27, 32, 0.65); flex-shrink: 0; }
|
||||
.c-body { flex: 1; background: #ffffff; border-radius: 24rpx; padding: 16rpx 20rpx; }
|
||||
.c-name { display: block; font-size: 24rpx; font-weight: 600; color: #1b1b20; margin-bottom: 6rpx; }
|
||||
.c-text { display: block; font-size: 26rpx; color: #1b1b20; line-height: 1.5; margin-bottom: 8rpx; }
|
||||
.c-time { display: block; font-size: 20rpx; color: #b9bdc6; }
|
||||
|
||||
.input-bar { position: fixed; bottom: 0; left: 0; right: 0; display: flex; align-items: center; gap: 14rpx; padding: 16rpx 28rpx; padding-bottom: calc(16rpx + env(safe-area-inset-bottom)); background: rgba(255,255,255,0.92); border-top: 1rpx solid rgba(43,37,61,0.08); z-index: 100; }
|
||||
.comment-input { flex: 1; height: 80rpx; background: rgba(255,255,255,0.80); border: 1rpx solid rgba(43,37,61,0.12); border-radius: 999rpx; padding: 0 24rpx; font-size: 28rpx; color: #272235; }
|
||||
.send-btn { background: linear-gradient(135deg, #ff4f91, #ff9f1c); color: #fff; border-radius: 999rpx; padding: 18rpx 30rpx; font-size: 28rpx; font-weight: 800; flex-shrink: 0; }
|
||||
.send-off { background: #e8e4f0; color: #9b8fa8; }
|
||||
.input-bar { position: fixed; bottom: 0; left: 0; right: 0; display: flex; align-items: center; gap: 14rpx; padding: 16rpx 28rpx; padding-bottom: calc(16rpx + env(safe-area-inset-bottom)); background: #ffffff; border-top: 1rpx solid rgba(27, 27, 32, 0.06); z-index: 100; }
|
||||
.comment-input { flex: 1; height: 80rpx; background: #f1f2f6; border-radius: 999rpx; padding: 0 24rpx; font-size: 28rpx; color: #1b1b20; }
|
||||
.send-btn { background: #2b2b30; color: #fff; border-radius: 999rpx; padding: 18rpx 30rpx; font-size: 28rpx; font-weight: 600; flex-shrink: 0; }
|
||||
.send-off { background: #e7e9ef; color: #b9bdc6; }
|
||||
|
||||
@@ -3,11 +3,15 @@ import { chooseAndUploadMedia } from '../../utils/auth'
|
||||
import { MOOD_OPTIONS, HOT_HASHTAGS } from '../../utils/constants'
|
||||
import { getPetEmoji } from '../../utils/format'
|
||||
|
||||
const app = getApp<{ userInfo: any; currentLocation: any }>()
|
||||
import { AppGlobalData } from '../../types/index'
|
||||
|
||||
const app = getApp<{ globalData: AppGlobalData }>()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 0,
|
||||
navbarHeight: 44,
|
||||
navbarPaddingRight: 0,
|
||||
content: '',
|
||||
images: [] as any[],
|
||||
location: null as any,
|
||||
@@ -23,10 +27,9 @@ Page({
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const info = wx.getSystemInfoSync()
|
||||
this.setData({ statusBarHeight: info.statusBarHeight })
|
||||
const { statusBarHeight, navbarHeight, navbarPaddingRight } = app.globalData.navBarInfo
|
||||
this.setData({ statusBarHeight, navbarHeight, navbarPaddingRight })
|
||||
this.loadMyPets()
|
||||
this.autoGetLocation()
|
||||
},
|
||||
|
||||
loadMyPets() {
|
||||
@@ -42,24 +45,6 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
autoGetLocation() {
|
||||
const loc = app.globalData?.currentLocation
|
||||
if (!loc) return
|
||||
wx.reverseGeocoder({
|
||||
location: { longitude: loc.longitude, latitude: loc.latitude },
|
||||
success: (res: any) => {
|
||||
const name = res.result?.address || ''
|
||||
if (name) {
|
||||
this.setData({
|
||||
location: { name, latitude: loc.latitude, longitude: loc.longitude },
|
||||
})
|
||||
this.checkCanSubmit()
|
||||
}
|
||||
},
|
||||
fail: () => {},
|
||||
} as any)
|
||||
},
|
||||
|
||||
onContentInput(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ content: e.detail.value })
|
||||
this.checkCanSubmit()
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<view class="post-page">
|
||||
<view class="safe-top" style="height: {{statusBarHeight}}px"></view>
|
||||
<!-- 状态栏占位 -->
|
||||
<view style="height: {{statusBarHeight}}px; flex-shrink: 0;"></view>
|
||||
|
||||
<!-- 顶部导航 -->
|
||||
<view class="post-header">
|
||||
<!-- 顶部导航:高度与胶囊等高,右侧 padding 避让胶囊 -->
|
||||
<view class="post-header" style="height: {{navbarHeight}}px; padding-right: {{navbarPaddingRight}}px;">
|
||||
<view class="cancel-btn" bindtap="onCancel">取消</view>
|
||||
<view class="header-title">发布动态</view>
|
||||
<view class="submit-btn {{canSubmit ? '' : 'submit-disabled'}}" bindtap="onSubmit">
|
||||
|
||||
@@ -1,47 +1,52 @@
|
||||
/* ── 发布页:Vital 风格工具页 ──
|
||||
浅灰底 + 白卡输入区,选中态统一为强调黑胶囊 */
|
||||
.post-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #fff8f2 0%, #fff4fb 100%);
|
||||
background: #f1f2f6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部 */
|
||||
/* 顶部:高度和右侧 padding 由 JS 动态设置,避让系统胶囊按钮 */
|
||||
.post-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 28rpx;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
background: rgba(255, 255, 255, 0.60);
|
||||
padding-left: 28rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 0 0 40rpx 40rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(27, 27, 32, 0.04);
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #9b8fa8;
|
||||
font-weight: 600;
|
||||
color: #6e7280;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 900;
|
||||
color: #272235;
|
||||
font-weight: 700;
|
||||
color: #1b1b20;
|
||||
}
|
||||
|
||||
/* 发布:强调黑胶囊 */
|
||||
.submit-btn {
|
||||
background: linear-gradient(135deg, #ff4f91, #ff9f1c);
|
||||
background: #2b2b30;
|
||||
color: #fff;
|
||||
border-radius: 999rpx;
|
||||
padding: 14rpx 34rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 900;
|
||||
box-shadow: 0 10rpx 22rpx rgba(255, 79, 145, 0.28);
|
||||
font-weight: 600;
|
||||
box-shadow: 0 12rpx 28rpx rgba(43, 43, 48, 0.24);
|
||||
}
|
||||
|
||||
.submit-disabled {
|
||||
background: #e8e4f0;
|
||||
color: #9b8fa8;
|
||||
background: #e7e9ef;
|
||||
color: #b9bdc6;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -58,28 +63,33 @@
|
||||
.upload-placeholder {
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
background: linear-gradient(135deg, #fff0a8, #ffd6e8 48%, #c9f7df);
|
||||
border: 3rpx dashed rgba(39, 34, 53, 0.18);
|
||||
background: #ffffff;
|
||||
border: 3rpx dashed rgba(27, 27, 32, 0.14);
|
||||
border-radius: 48rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, 0.65), 0 16rpx 32rpx rgba(83, 62, 100, 0.09);
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.upload-icon { font-size: 64rpx; }
|
||||
.upload-icon {
|
||||
font-size: 56rpx;
|
||||
color: var(--purple);
|
||||
background: rgba(139, 124, 246, 0.10);
|
||||
border-radius: 28rpx;
|
||||
padding: 18rpx;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
color: #272235;
|
||||
font-weight: 600;
|
||||
color: #1b1b20;
|
||||
}
|
||||
|
||||
.upload-sub {
|
||||
font-size: 22rpx;
|
||||
color: #9b8fa8;
|
||||
color: #b9bdc6;
|
||||
}
|
||||
|
||||
/* 图片网格 */
|
||||
@@ -99,7 +109,7 @@
|
||||
.img-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f0eef5;
|
||||
background: #eef0f5;
|
||||
}
|
||||
|
||||
.img-remove {
|
||||
@@ -109,13 +119,13 @@
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(39, 34, 53, 0.72);
|
||||
background: rgba(27, 27, 32, 0.65);
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.img-uploading {
|
||||
@@ -130,8 +140,8 @@
|
||||
.upload-spinner {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 4rpx solid rgba(255, 79, 145, 0.2);
|
||||
border-top-color: #ff4f91;
|
||||
border: 4rpx solid rgba(139, 124, 246, 0.2);
|
||||
border-top-color: var(--purple);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@@ -141,8 +151,8 @@
|
||||
.image-add {
|
||||
aspect-ratio: 1;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 3rpx dashed rgba(39, 34, 53, 0.18);
|
||||
background: #ffffff;
|
||||
border: 3rpx dashed rgba(27, 27, 32, 0.14);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -150,48 +160,47 @@
|
||||
|
||||
.add-plus {
|
||||
font-size: 60rpx;
|
||||
color: #9b8fa8;
|
||||
color: #b9bdc6;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
/* 文字输入 */
|
||||
/* 文字输入:白卡 */
|
||||
.content-input {
|
||||
width: 100%;
|
||||
min-height: 160rpx;
|
||||
background: rgba(255, 255, 255, 0.80);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
border-radius: 36rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 40rpx;
|
||||
padding: 24rpx 28rpx;
|
||||
font-size: 30rpx;
|
||||
color: #272235;
|
||||
color: #1b1b20;
|
||||
font-family: "PingFang SC", sans-serif;
|
||||
line-height: 1.6;
|
||||
box-shadow: 0 10rpx 24rpx rgba(78, 56, 96, 0.07);
|
||||
box-shadow: var(--shadow-soft);
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.input-placeholder { color: #c4b8d0; }
|
||||
.input-placeholder { color: #b9bdc6; }
|
||||
|
||||
.word-count {
|
||||
text-align: right;
|
||||
font-size: 22rpx;
|
||||
color: #9b8fa8;
|
||||
color: #b9bdc6;
|
||||
margin-bottom: 28rpx;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* 通用 section */
|
||||
.section-title {
|
||||
font-size: 24rpx;
|
||||
font-weight: 900;
|
||||
color: #9b8fa8;
|
||||
letter-spacing: 0.5rpx;
|
||||
margin-bottom: 14rpx;
|
||||
text-transform: uppercase;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #b9bdc6;
|
||||
letter-spacing: 0.6rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.tag-row {
|
||||
display: flex;
|
||||
gap: 14rpx;
|
||||
gap: 16rpx;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 28rpx;
|
||||
align-items: center;
|
||||
@@ -201,32 +210,32 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
background: #ffffff;
|
||||
border-radius: 999rpx;
|
||||
padding: 14rpx 24rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 800;
|
||||
color: #6a6178;
|
||||
padding: 16rpx 26rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #6e7280;
|
||||
}
|
||||
|
||||
/* 选中:强调黑 */
|
||||
.tag-selected {
|
||||
background: #ffe5f0;
|
||||
border-color: #ffb6d0;
|
||||
color: #a91d5b;
|
||||
background: #2b2b30;
|
||||
color: #fff;
|
||||
box-shadow: 0 12rpx 28rpx rgba(43, 43, 48, 0.24);
|
||||
}
|
||||
|
||||
.tag-remove {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 50%;
|
||||
background: #ffe5f0;
|
||||
color: #ff4f91;
|
||||
background: rgba(139, 124, 246, 0.12);
|
||||
color: #7563e0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 话题 */
|
||||
@@ -241,22 +250,21 @@
|
||||
.hashtag-input {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
background: rgba(255, 255, 255, 0.80);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
background: #ffffff;
|
||||
border-radius: 999rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 26rpx;
|
||||
color: #272235;
|
||||
color: #1b1b20;
|
||||
}
|
||||
|
||||
.hashtag-add-btn {
|
||||
background: rgba(255, 255, 255, 0.80);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
background: #ffffff;
|
||||
border-radius: 999rpx;
|
||||
padding: 14rpx 28rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 800;
|
||||
color: #272235;
|
||||
font-weight: 600;
|
||||
color: #1b1b20;
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.hashtag-chip {
|
||||
@@ -264,17 +272,17 @@
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
color: #8b3cff;
|
||||
background: #f0e8ff;
|
||||
font-weight: 600;
|
||||
color: #7563e0;
|
||||
background: rgba(139, 124, 246, 0.12);
|
||||
border-radius: 999rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
}
|
||||
|
||||
.chip-remove {
|
||||
font-size: 26rpx;
|
||||
color: #8b3cff;
|
||||
font-weight: 700;
|
||||
color: #7563e0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hot-tags {
|
||||
@@ -287,14 +295,13 @@
|
||||
|
||||
.hot-tags-label {
|
||||
font-size: 22rpx;
|
||||
color: #9b8fa8;
|
||||
color: #b9bdc6;
|
||||
}
|
||||
|
||||
.hot-tag {
|
||||
font-size: 22rpx;
|
||||
color: #9b8fa8;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1rpx solid rgba(43, 37, 61, 0.10);
|
||||
color: #6e7280;
|
||||
background: #ffffff;
|
||||
border-radius: 999rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
}
|
||||
@@ -308,17 +315,22 @@
|
||||
}
|
||||
|
||||
.mood-pill {
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
background: #ffffff;
|
||||
border-radius: 999rpx;
|
||||
padding: 14rpx 26rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 800;
|
||||
color: #6a6178;
|
||||
padding: 16rpx 28rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #6e7280;
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
|
||||
.mood-on {
|
||||
background: #fff0a8;
|
||||
border-color: #ffe15a;
|
||||
color: #6f4b00;
|
||||
.mood-pill:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* 选中:强调黑(与标签选中态一致) */
|
||||
.mood-on {
|
||||
background: #2b2b30;
|
||||
color: #fff;
|
||||
box-shadow: 0 12rpx 28rpx rgba(43, 43, 48, 0.24);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Post } from '../../types/index'
|
||||
import { Post, AppGlobalData } from '../../types/index'
|
||||
import { api } from '../../utils/api'
|
||||
import { getAvatarGradient, getPetEmoji, formatCount } from '../../utils/format'
|
||||
|
||||
const app = getApp<{ userInfo: any; isLoggedIn: boolean }>()
|
||||
const app = getApp<{ globalData: AppGlobalData }>()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 0,
|
||||
navbarHeight: 44,
|
||||
navbarPaddingRight: 0,
|
||||
tabBarHeight: 56,
|
||||
userInfo: {
|
||||
nickName: '',
|
||||
@@ -17,7 +19,7 @@ Page({
|
||||
pets: [],
|
||||
} as any,
|
||||
formattedLikes: '0',
|
||||
avatarGradient: 'linear-gradient(135deg, #ffd6e8, #fff1a6)',
|
||||
avatarGradient: 'linear-gradient(135deg, #d9d2fb, #efedfd)',
|
||||
posts: [] as Post[],
|
||||
postView: 'grid' as 'grid' | 'list',
|
||||
loading: false,
|
||||
@@ -26,7 +28,13 @@ Page({
|
||||
|
||||
onLoad() {
|
||||
const info = wx.getSystemInfoSync()
|
||||
this.setData({ statusBarHeight: info.statusBarHeight })
|
||||
const { statusBarHeight, navbarHeight, navbarPaddingRight } = app.globalData.navBarInfo
|
||||
this.setData({
|
||||
statusBarHeight,
|
||||
navbarHeight,
|
||||
navbarPaddingRight,
|
||||
tabBarHeight: info.windowHeight - (info.safeArea?.bottom ?? info.windowHeight) + 56,
|
||||
})
|
||||
this.loadProfile()
|
||||
},
|
||||
|
||||
@@ -59,7 +67,7 @@ Page({
|
||||
try {
|
||||
const [_user, postsRes] = await Promise.all([
|
||||
api.getUserProfile(app.globalData?.userInfo?._id || '').catch(() => null),
|
||||
api.getUserPosts(app.globalData?.userInfo?._id || '').catch(() => ({ list: [], total: 0 })),
|
||||
api.getUserPosts(app.globalData?.userInfo?.openid || '').catch(() => ({ list: [], total: 0 })),
|
||||
])
|
||||
if (_user) {
|
||||
app.globalData.userInfo = _user
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
<view class="profile-page">
|
||||
<view class="safe-top" style="height: {{statusBarHeight}}px"></view>
|
||||
<!-- 固定液态玻璃顶栏:标题 + 设置按钮(紧贴胶囊按钮) -->
|
||||
<view class="glass-topbar">
|
||||
<view style="height: {{statusBarHeight}}px;"></view>
|
||||
<view class="navbar" style="height: {{navbarHeight}}px; padding-right: {{navbarPaddingRight}}px;">
|
||||
<view class="navbar-title">我的</view>
|
||||
<view class="navbar-action" bindtap="onSettings">⚙️</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 顶栏占位 -->
|
||||
<view style="height: {{statusBarHeight + navbarHeight}}px; flex-shrink: 0;"></view>
|
||||
|
||||
<scroll-view
|
||||
scroll-y="true"
|
||||
class="profile-scroll"
|
||||
enhanced="true"
|
||||
bounces="{{false}}"
|
||||
show-scrollbar="false"
|
||||
refresher-enabled="true"
|
||||
bindrefresherrefresh="onRefresh"
|
||||
refresher-triggered="{{refreshing}}"
|
||||
>
|
||||
<!-- 用户信息区 -->
|
||||
<!-- 用户信息区(移除原来的 header-topbar,已移至 navbar)-->
|
||||
<view class="profile-header">
|
||||
<!-- 头部操作栏 -->
|
||||
<view class="header-topbar">
|
||||
<view class="settings-btn" bindtap="onSettings">⚙️</view>
|
||||
</view>
|
||||
<view class="hero-blob hb1"></view>
|
||||
<view class="hero-blob hb2"></view>
|
||||
|
||||
<!-- 头像 + 基本信息 -->
|
||||
<view class="user-top">
|
||||
@@ -70,7 +79,7 @@
|
||||
bindtap="onPetTap"
|
||||
data-pid="{{item._id}}"
|
||||
>
|
||||
<view class="pet-avatar" style="background: {{item.gradient || 'linear-gradient(135deg, #c9f7df, #d9d2ff)'}}">
|
||||
<view class="pet-avatar" style="background: {{item.gradient || 'linear-gradient(135deg, #d9d2fb, #efedfd)'}}">
|
||||
<text class="pet-emoji">{{item.emoji || '🐾'}}</text>
|
||||
</view>
|
||||
<view class="pet-info">
|
||||
|
||||
@@ -1,35 +1,51 @@
|
||||
/* ── 我的页:Vital 风格 ──
|
||||
紫色渐变淡出头部,白色英雄卡叠压其上,统计用大数字锚点 */
|
||||
.profile-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #fff8f2 0%, #fff4fb 50%, #f5f0ff 100%);
|
||||
}
|
||||
|
||||
.profile-scroll { min-height: 100vh; }
|
||||
|
||||
/* 用户信息区 */
|
||||
.profile-header {
|
||||
padding: 0 28rpx 28rpx;
|
||||
background: linear-gradient(180deg, rgba(255, 225, 90, 0.18), transparent);
|
||||
}
|
||||
|
||||
.header-topbar {
|
||||
height: 100vh;
|
||||
background-color: #f1f2f6;
|
||||
background-image: linear-gradient(180deg, #9181f4 0%, #a99bf8 52%, rgba(241, 242, 246, 0) 100%);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 460rpx;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 16rpx 0 12rpx;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
box-shadow: 0 8rpx 18rpx rgba(78, 56, 96, 0.10);
|
||||
/* 主导航栏:与胶囊等高,右侧避让胶囊 */
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 30rpx;
|
||||
padding-left: 28rpx;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 标题颜色由 .glass-topbar 统一控制 */
|
||||
.navbar-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.profile-scroll { flex: 1; }
|
||||
|
||||
/* 用户信息区:白色英雄卡叠压在紫色头部上 */
|
||||
.profile-header {
|
||||
position: relative;
|
||||
margin: 8rpx 28rpx 20rpx;
|
||||
padding: 32rpx 28rpx 26rpx;
|
||||
overflow: hidden;
|
||||
border-radius: 48rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 20rpx 50rpx rgba(27, 27, 32, 0.10);
|
||||
}
|
||||
|
||||
/* 旧版渐变光斑已停用(Vital 用纯白卡面) */
|
||||
.hero-blob {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.user-top {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 24rpx;
|
||||
@@ -42,30 +58,30 @@
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 40rpx;
|
||||
width: 132rpx;
|
||||
height: 132rpx;
|
||||
border-radius: 44rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 12rpx 24rpx rgba(98, 60, 118, 0.18);
|
||||
box-shadow: 0 14rpx 32rpx rgba(27, 27, 32, 0.10);
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
font-size: 56rpx;
|
||||
font-weight: 800;
|
||||
color: rgba(39, 34, 53, 0.72);
|
||||
font-weight: 700;
|
||||
color: rgba(27, 27, 32, 0.65);
|
||||
}
|
||||
|
||||
.avatar-edit {
|
||||
position: absolute;
|
||||
bottom: -6rpx;
|
||||
right: -6rpx;
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
width: 46rpx;
|
||||
height: 46rpx;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 4rpx 10rpx rgba(78, 56, 96, 0.16);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8rpx 18rpx rgba(27, 27, 32, 0.14);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -86,40 +102,41 @@
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: 900;
|
||||
color: #272235;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #1b1b20;
|
||||
}
|
||||
|
||||
.user-handle {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #9b8fa8;
|
||||
color: #b9bdc6;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.user-bio {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #6a6178;
|
||||
line-height: 1.5;
|
||||
font-size: 24rpx;
|
||||
color: #6e7280;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 编辑资料:浅灰静默胶囊 */
|
||||
.edit-profile-btn {
|
||||
background: rgba(255, 255, 255, 0.80);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
background: #f1f2f6;
|
||||
border-radius: 999rpx;
|
||||
padding: 14rpx 28rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
color: #272235;
|
||||
font-weight: 600;
|
||||
color: #1b1b20;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 8rpx 18rpx rgba(78, 56, 96, 0.08);
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
/* 数据统计 */
|
||||
/* 数据统计:大数字锚点(整数大、标签小且灰) */
|
||||
.stats-row {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
@@ -127,26 +144,26 @@
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
padding: 24rpx 0;
|
||||
padding: 22rpx 0;
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
border-radius: 30rpx;
|
||||
box-shadow: 0 10rpx 22rpx rgba(78, 56, 96, 0.07);
|
||||
background: #f6f7fa;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.stat-num {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: 900;
|
||||
color: #ff4f91;
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: -1rpx;
|
||||
color: #1b1b20;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #9b8fa8;
|
||||
font-size: 21rpx;
|
||||
font-weight: 600;
|
||||
color: #b9bdc6;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
@@ -163,23 +180,22 @@
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 900;
|
||||
color: #9b8fa8;
|
||||
letter-spacing: 0.5rpx;
|
||||
text-transform: uppercase;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #b9bdc6;
|
||||
letter-spacing: 0.6rpx;
|
||||
}
|
||||
|
||||
.add-pet-btn {
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
color: #ff4f91;
|
||||
background: #ffe5f0;
|
||||
font-weight: 600;
|
||||
color: #7563e0;
|
||||
background: rgba(139, 124, 246, 0.12);
|
||||
border-radius: 999rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
}
|
||||
|
||||
/* 宠物卡片 */
|
||||
/* 宠物卡片:白卡 */
|
||||
.pets-scroll {
|
||||
white-space: nowrap;
|
||||
padding: 0 28rpx 16rpx;
|
||||
@@ -189,33 +205,33 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.82), rgba(201, 247, 223, 0.72));
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
border-radius: 36rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 40rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
margin-right: 20rpx;
|
||||
box-shadow: 0 12rpx 26rpx rgba(78, 56, 96, 0.09);
|
||||
box-shadow: var(--shadow-soft);
|
||||
white-space: normal;
|
||||
vertical-align: top;
|
||||
max-width: 560rpx;
|
||||
}
|
||||
|
||||
.pet-card-empty {
|
||||
background: rgba(255, 255, 255, 0.60);
|
||||
border: 3rpx dashed rgba(43, 37, 61, 0.16);
|
||||
background: transparent;
|
||||
border: 3rpx dashed rgba(27, 27, 32, 0.14);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.pet-avatar {
|
||||
width: 84rpx;
|
||||
height: 84rpx;
|
||||
border-radius: 28rpx;
|
||||
border-radius: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pet-avatar-add { background: rgba(255, 255, 255, 0.60); color: #9b8fa8; }
|
||||
.pet-avatar-add { background: #f1f2f6; border: 2.5rpx dashed rgba(27, 27, 32, 0.14); color: #b9bdc6; }
|
||||
|
||||
.pet-emoji { font-size: 44rpx; line-height: 1; }
|
||||
|
||||
@@ -224,61 +240,61 @@
|
||||
.pet-name {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 900;
|
||||
color: #272235;
|
||||
font-weight: 600;
|
||||
color: #1b1b20;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.pet-breed {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #9b8fa8;
|
||||
color: #b9bdc6;
|
||||
}
|
||||
|
||||
/* 宠物徽章:淡紫静默胶囊,不旋转 */
|
||||
.pet-badge {
|
||||
background: #ffe5f0;
|
||||
background: rgba(139, 124, 246, 0.12);
|
||||
color: #7563e0;
|
||||
border-radius: 999rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
padding: 10rpx 18rpx;
|
||||
font-size: 20rpx;
|
||||
color: #a91d5b;
|
||||
font-weight: 900;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 视图切换 */
|
||||
/* 视图切换:灰底分段控件 */
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
background: rgba(255, 255, 255, 0.60);
|
||||
border: 1rpx solid rgba(43, 37, 61, 0.10);
|
||||
border-radius: 14rpx;
|
||||
background: #e7e9ef;
|
||||
border-radius: 18rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toggle-item {
|
||||
padding: 10rpx 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: #9b8fa8;
|
||||
color: #6e7280;
|
||||
}
|
||||
|
||||
.toggle-item.active {
|
||||
background: rgba(255, 79, 145, 0.12);
|
||||
color: #ff4f91;
|
||||
font-weight: 800;
|
||||
background: #ffffff;
|
||||
color: #1b1b20;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 动态网格 */
|
||||
.post-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 6rpx;
|
||||
gap: 9rpx;
|
||||
padding: 0 28rpx 20rpx;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
aspect-ratio: 1;
|
||||
border-radius: 28rpx;
|
||||
border-radius: 26rpx;
|
||||
overflow: hidden;
|
||||
background: #f0eef5;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -289,7 +305,7 @@
|
||||
.grid-no-img {
|
||||
padding: 12rpx;
|
||||
font-size: 20rpx;
|
||||
color: #9b8fa8;
|
||||
color: #b9bdc6;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
@@ -305,6 +321,6 @@
|
||||
|
||||
.empty-emoji { font-size: 72rpx; }
|
||||
|
||||
.empty-tip { font-size: 26rpx; color: #9b8fa8; }
|
||||
.empty-tip { font-size: 26rpx; color: #b9bdc6; }
|
||||
|
||||
.post-btn { padding: 18rpx 48rpx; font-size: 26rpx; font-weight: 800; margin-top: 8rpx; }
|
||||
.post-btn { padding: 18rpx 48rpx; font-size: 26rpx; font-weight: 600; margin-top: 8rpx; }
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { api } from '../../utils/api'
|
||||
import { getAvatarGradient } from '../../utils/format'
|
||||
import { AppGlobalData } from '../../types/index'
|
||||
|
||||
const app = getApp<{ globalData: AppGlobalData }>()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 0,
|
||||
navbarHeight: 44,
|
||||
navbarPaddingRight: 0,
|
||||
keyword: '',
|
||||
resultTab: 'posts' as 'posts' | 'users',
|
||||
postResults: [] as any[],
|
||||
@@ -12,15 +18,15 @@ Page({
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const info = wx.getSystemInfoSync()
|
||||
this.setData({ statusBarHeight: info.statusBarHeight })
|
||||
const { statusBarHeight, navbarHeight, navbarPaddingRight } = app.globalData.navBarInfo
|
||||
this.setData({ statusBarHeight, navbarHeight, navbarPaddingRight })
|
||||
this.loadHotTopics()
|
||||
},
|
||||
|
||||
async loadHotTopics() {
|
||||
try {
|
||||
const res = await wx.cloud.callFunction({ name: 'getHotTopics', data: {} }) as any
|
||||
this.setData({ hotTopics: res.result?.data || [] })
|
||||
const topics = await api.getHotTopics()
|
||||
this.setData({ hotTopics: topics || [] })
|
||||
} catch {}
|
||||
},
|
||||
|
||||
@@ -38,12 +44,12 @@ Page({
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
const [posts, users] = await Promise.all([
|
||||
wx.cloud.callFunction({ name: 'searchPosts', data: { keyword: kw } }) as any,
|
||||
wx.cloud.callFunction({ name: 'searchUsers', data: { keyword: kw } }) as any,
|
||||
api.searchPosts(kw),
|
||||
api.searchUsers(kw),
|
||||
])
|
||||
this.setData({
|
||||
postResults: posts.result?.data || [],
|
||||
userResults: (users.result?.data || []).map((u: any) => ({
|
||||
postResults: posts || [],
|
||||
userResults: (users || []).map((u: any) => ({
|
||||
...u,
|
||||
gradient: getAvatarGradient(u._id),
|
||||
})),
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
<view class="page">
|
||||
<view class="safe-top" style="height: {{statusBarHeight}}px"></view>
|
||||
<!-- 固定液态玻璃顶栏:返回 + 标题,右侧避让小程序胶囊按钮 -->
|
||||
<view class="glass-topbar">
|
||||
<view style="height: {{statusBarHeight}}px;"></view>
|
||||
<view class="navbar" style="height: {{navbarHeight}}px; padding-right: {{navbarPaddingRight}}px;">
|
||||
<view class="back" bindtap="onBack">‹</view>
|
||||
<view class="navbar-title">搜索</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 顶栏占位 -->
|
||||
<view style="height: {{statusBarHeight + navbarHeight}}px;"></view>
|
||||
|
||||
<!-- 搜索框:位于标题栏下方,全宽可用,不受胶囊遮挡 -->
|
||||
<view class="search-bar">
|
||||
<view class="back" bindtap="onBack">‹</view>
|
||||
<view class="input-wrap">
|
||||
<text class="search-icon">🔍</text>
|
||||
<input
|
||||
|
||||
@@ -1,30 +1,45 @@
|
||||
.page { min-height: 100vh; background: linear-gradient(180deg, #fff8f2 0%, #f5f0ff 100%); }
|
||||
/* ── 搜索页:Vital 风格 ── */
|
||||
.page { min-height: 100vh; background: #f1f2f6; }
|
||||
|
||||
/* 顶栏内导航行:返回 + 标题(玻璃样式由 .glass-topbar 提供) */
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding-left: 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.navbar-title { font-size: 36rpx; font-weight: 700; }
|
||||
|
||||
.back { font-size: 60rpx; color: #1b1b20; font-weight: 300; line-height: 1; padding: 0 10rpx; }
|
||||
|
||||
/* 搜索框行:标题栏下方,全宽 */
|
||||
.search-bar { display: flex; align-items: center; gap: 14rpx; padding: 14rpx 28rpx; }
|
||||
.back { font-size: 60rpx; color: #272235; font-weight: 300; line-height: 1; }
|
||||
.input-wrap { flex: 1; display: flex; align-items: center; background: rgba(255,255,255,0.88); border: 1rpx solid rgba(255,255,255,0.72); border-radius: 999rpx; padding: 0 20rpx; height: 76rpx; gap: 10rpx; box-shadow: 0 8rpx 18rpx rgba(78,56,96,0.08); }
|
||||
.input-wrap { flex: 1; display: flex; align-items: center; background: #ffffff; border-radius: 999rpx; padding: 0 20rpx; height: 76rpx; gap: 10rpx; box-shadow: var(--shadow-soft); }
|
||||
.search-icon { font-size: 30rpx; }
|
||||
.search-input { flex: 1; font-size: 28rpx; color: #272235; }
|
||||
.clear { font-size: 36rpx; color: #9b8fa8; }
|
||||
.search-btn { font-size: 28rpx; font-weight: 800; color: #ff4f91; padding: 0 6rpx; }
|
||||
.search-input { flex: 1; font-size: 28rpx; color: #1b1b20; }
|
||||
.clear { font-size: 36rpx; color: #b9bdc6; }
|
||||
.search-btn { font-size: 28rpx; font-weight: 600; color: #7563e0; padding: 0 6rpx; }
|
||||
|
||||
.hot-section { padding: 20rpx 28rpx; }
|
||||
.section-title { display: block; font-size: 26rpx; font-weight: 900; color: #9b8fa8; margin-bottom: 20rpx; }
|
||||
.hot-tags { display: flex; flex-direction: column; gap: 0; background: rgba(255,255,255,0.88); border-radius: 36rpx; overflow: hidden; border: 1rpx solid rgba(255,255,255,0.72); box-shadow: 0 12rpx 26rpx rgba(78,56,96,0.08); }
|
||||
.hot-tag { display: flex; align-items: center; gap: 20rpx; padding: 22rpx 28rpx; border-bottom: 1rpx solid rgba(43,37,61,0.05); }
|
||||
.section-title { display: block; font-size: 26rpx; font-weight: 600; color: #b9bdc6; margin-bottom: 20rpx; }
|
||||
.hot-tags { display: flex; flex-direction: column; gap: 0; background: #ffffff; border-radius: 36rpx; overflow: hidden; box-shadow: var(--shadow-soft); }
|
||||
.hot-tag { display: flex; align-items: center; gap: 20rpx; padding: 22rpx 28rpx; border-bottom: 1rpx solid rgba(27, 27, 32, 0.04); }
|
||||
.hot-tag:last-child { border-bottom: none; }
|
||||
.tag-rank { font-size: 30rpx; font-weight: 900; color: #ff4f91; width: 32rpx; text-align: center; }
|
||||
.hot-tag:nth-child(n+4) .tag-rank { color: #9b8fa8; }
|
||||
.tag-text { flex: 1; font-size: 28rpx; font-weight: 700; color: #272235; }
|
||||
.tag-count { font-size: 24rpx; color: #9b8fa8; }
|
||||
.tag-rank { font-size: 30rpx; font-weight: 700; color: #7563e0; width: 32rpx; text-align: center; font-variant-numeric: tabular-nums; }
|
||||
.hot-tag:nth-child(n+4) .tag-rank { color: #b9bdc6; }
|
||||
.tag-text { flex: 1; font-size: 28rpx; font-weight: 600; color: #1b1b20; }
|
||||
.tag-count { font-size: 24rpx; color: #b9bdc6; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.results-tabs { display: flex; padding: 0 28rpx 20rpx; gap: 8rpx; }
|
||||
.rtab { font-size: 28rpx; font-weight: 700; color: #9b8fa8; padding: 10rpx 20rpx; border-radius: 999rpx; }
|
||||
.rtab-on { color: #ff4f91; background: rgba(255,79,145,0.10); font-weight: 900; }
|
||||
.rtab { font-size: 28rpx; font-weight: 600; color: #6e7280; padding: 10rpx 20rpx; border-radius: 999rpx; }
|
||||
.rtab-on { color: #1b1b20; background: #ffffff; font-weight: 700; box-shadow: 0 8rpx 18rpx rgba(27, 27, 32, 0.08); }
|
||||
|
||||
.user-result { display: flex; align-items: center; gap: 20rpx; padding: 20rpx 28rpx; background: rgba(255,255,255,0.80); border-bottom: 1rpx solid rgba(43,37,61,0.05); }
|
||||
.u-avatar { width: 88rpx; height: 88rpx; border-radius: 28rpx; display: flex; align-items: center; justify-content: center; font-size: 36rpx; font-weight: 800; color: rgba(39,34,53,0.72); flex-shrink: 0; }
|
||||
.u-name { display: block; font-size: 28rpx; font-weight: 800; color: #272235; }
|
||||
.u-bio { display: block; font-size: 24rpx; color: #9b8fa8; margin-top: 4rpx; }
|
||||
.user-result { display: flex; align-items: center; gap: 20rpx; padding: 20rpx 28rpx; background: #ffffff; border-bottom: 1rpx solid rgba(27, 27, 32, 0.04); }
|
||||
.u-avatar { width: 88rpx; height: 88rpx; border-radius: 28rpx; display: flex; align-items: center; justify-content: center; font-size: 36rpx; font-weight: 700; color: rgba(27, 27, 32, 0.65); flex-shrink: 0; }
|
||||
.u-name { display: block; font-size: 28rpx; font-weight: 600; color: #1b1b20; }
|
||||
.u-bio { display: block; font-size: 24rpx; color: #b9bdc6; margin-top: 4rpx; }
|
||||
|
||||
.empty { text-align: center; font-size: 26rpx; color: #9b8fa8; padding: 80rpx; }
|
||||
.empty { text-align: center; font-size: 26rpx; color: #b9bdc6; padding: 80rpx; }
|
||||
.results-scroll { }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { api } from '../../utils/api'
|
||||
import { getAvatarGradient, formatTime } from '../../utils/format'
|
||||
|
||||
const DURATION = 5000
|
||||
@@ -22,15 +23,18 @@ Page({
|
||||
|
||||
async loadStory(id: string) {
|
||||
try {
|
||||
const res = await wx.cloud.callFunction({ name: 'getStory', data: { id } }) as any
|
||||
const stories = (res.result?.data || []).map((s: any) => ({
|
||||
const list = await api.getStory(id)
|
||||
const stories = (list || []).map((s: any) => ({
|
||||
...s,
|
||||
gradient: getAvatarGradient(s.authorId),
|
||||
timeText: formatTime(s.createdAt),
|
||||
}))
|
||||
this.setData({ stories, currentStory: stories[0] || {} })
|
||||
this.startProgress()
|
||||
} catch {}
|
||||
if (stories.length) this.startProgress()
|
||||
else wx.navigateBack()
|
||||
} catch {
|
||||
wx.navigateBack()
|
||||
}
|
||||
},
|
||||
|
||||
startProgress() {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
.story-page { width: 100vw; height: 100vh; background: #111; position: relative; overflow: hidden; }
|
||||
.story-page { width: 100vw; height: 100vh; background: #121214; position: relative; overflow: hidden; }
|
||||
.progress-row { position: absolute; top: calc(env(safe-area-inset-top, 44rpx) + 16rpx); left: 16rpx; right: 16rpx; display: flex; gap: 6rpx; z-index: 10; }
|
||||
.progress-bar { flex: 1; height: 4rpx; background: rgba(255,255,255,0.35); border-radius: 2rpx; overflow: hidden; }
|
||||
.progress-fill { height: 100%; background: #fff; border-radius: 2rpx; transition: width 0.1s linear; }
|
||||
.story-nav { position: absolute; top: calc(env(safe-area-inset-top, 44rpx) + 28rpx); left: 20rpx; right: 20rpx; display: flex; align-items: center; gap: 14rpx; z-index: 10; }
|
||||
.story-avatar { width: 72rpx; height: 72rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; font-weight: 800; color: rgba(39,34,53,0.72); border: 3rpx solid rgba(255,255,255,0.72); }
|
||||
.story-author { display: block; font-size: 28rpx; font-weight: 800; color: #fff; }
|
||||
.story-avatar { width: 72rpx; height: 72rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; font-weight: 700; color: rgba(27, 27, 32, 0.65); border: 3rpx solid rgba(255,255,255,0.72); }
|
||||
.story-author { display: block; font-size: 28rpx; font-weight: 700; color: #fff; }
|
||||
.story-time { display: block; font-size: 22rpx; color: rgba(255,255,255,0.72); }
|
||||
.close-btn { margin-left: auto; font-size: 40rpx; color: rgba(255,255,255,0.80); padding: 10rpx; }
|
||||
.story-content { width: 100%; height: 100%; }
|
||||
|
||||
@@ -71,17 +71,6 @@ export interface Comment {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
_id: string
|
||||
fromId: string
|
||||
from?: UserProfile
|
||||
toId: string
|
||||
content: string
|
||||
type: 'greeting' | 'text' | 'image'
|
||||
isRead: boolean
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface NearbyUser {
|
||||
userId: string
|
||||
user: UserProfile
|
||||
@@ -93,16 +82,16 @@ export interface NearbyUser {
|
||||
location: GeoPoint
|
||||
}
|
||||
|
||||
export interface TabBarItem {
|
||||
pagePath: string
|
||||
text: string
|
||||
icon: string
|
||||
activeIcon: string
|
||||
selected: boolean
|
||||
export interface NavBarInfo {
|
||||
statusBarHeight: number
|
||||
navbarHeight: number
|
||||
navbarPaddingRight: number
|
||||
}
|
||||
|
||||
export interface AppGlobalData {
|
||||
userInfo: UserProfile | null
|
||||
isLoggedIn: boolean
|
||||
currentLocation: GeoPoint | null
|
||||
navBarInfo: NavBarInfo
|
||||
_locationTimer: number | null
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Post, UserProfile, NearbyUser, Pet } from '../types/index'
|
||||
import { Post, UserProfile, NearbyUser, Pet, Comment } from '../types/index'
|
||||
|
||||
type CloudResult<T> = { result: { code: number; data: T; message?: string } }
|
||||
|
||||
@@ -31,9 +31,6 @@ export const api = {
|
||||
petId?: string
|
||||
}) => call<Post>('createPost', params),
|
||||
|
||||
deletePost: (postId: string) =>
|
||||
call<void>('deletePost', { postId }),
|
||||
|
||||
likePost: (postId: string) =>
|
||||
call<{ liked: boolean; count: number }>('likePost', { postId }),
|
||||
|
||||
@@ -58,24 +55,33 @@ export const api = {
|
||||
getFollowList: (type: 'following' | 'followers', userId?: string) =>
|
||||
call<UserProfile[]>('getFollowList', { type, userId }),
|
||||
|
||||
getMessages: (page = 0) =>
|
||||
call<{ list: any[]; unreadCount: number }>('getMessages', { page }),
|
||||
getMessages: (peerId: string, page = 0) =>
|
||||
call<{ list: any[]; unreadCount: number }>('getMessages', { peerId, page }),
|
||||
|
||||
sendMessage: (toId: string, content: string, type = 'text') =>
|
||||
call<void>('sendMessage', { toId, content, type }),
|
||||
call<{ _id: string }>('sendMessage', { toId, content, type }),
|
||||
|
||||
updatePet: (pet: Partial<Pet> & { _id?: string }) =>
|
||||
call<Pet>('updatePet', { pet }),
|
||||
|
||||
uploadImage: async (filePath: string): Promise<string> => {
|
||||
const ext = filePath.split('.').pop() || 'jpg'
|
||||
const cloudPath = `posts/${Date.now()}_${Math.random().toString(36).slice(2)}.${ext}`
|
||||
const res = await wx.cloud.uploadFile({ cloudPath, filePath })
|
||||
return res.fileID
|
||||
},
|
||||
deletePet: (petId: string) =>
|
||||
call<void>('deletePet', { petId }),
|
||||
|
||||
getFileURL: async (fileID: string): Promise<string> => {
|
||||
const res = await wx.cloud.getTempFileURL({ fileList: [fileID] })
|
||||
return res.fileList[0]?.tempFileURL || fileID
|
||||
},
|
||||
getHotTopics: () =>
|
||||
call<{ tag: string; count: number }[]>('getHotTopics'),
|
||||
|
||||
searchPosts: (keyword: string) =>
|
||||
call<Post[]>('searchPosts', { keyword }),
|
||||
|
||||
searchUsers: (keyword: string) =>
|
||||
call<UserProfile[]>('searchUsers', { keyword }),
|
||||
|
||||
getStory: (id: string) =>
|
||||
call<any[]>('getStory', { id }),
|
||||
|
||||
getNotifications: () =>
|
||||
call<any[]>('getNotifications'),
|
||||
|
||||
markAllRead: () =>
|
||||
call<void>('markAllRead'),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { UserProfile } from '../types/index'
|
||||
import { api } from './api'
|
||||
|
||||
const TOKEN_KEY = 'wangquan_token'
|
||||
const USER_KEY = 'wangquan_user'
|
||||
|
||||
export async function login(): Promise<UserProfile> {
|
||||
@@ -19,11 +18,6 @@ export function getCachedUser(): UserProfile | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function clearAuth(): void {
|
||||
wx.removeStorageSync(TOKEN_KEY)
|
||||
wx.removeStorageSync(USER_KEY)
|
||||
}
|
||||
|
||||
export async function requireLocation(): Promise<{ latitude: number; longitude: number }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.getLocation({
|
||||
|
||||
@@ -1,27 +1,4 @@
|
||||
export const CLOUD_ENV = 'YOUR_CLOUD_ENV_ID'
|
||||
|
||||
export const COLORS = {
|
||||
pink: '#ff4f91',
|
||||
orange: '#ff9f1c',
|
||||
yellow: '#ffe15a',
|
||||
green: '#2fd37a',
|
||||
mint: '#67e8c9',
|
||||
blue: '#4d8dff',
|
||||
violet: '#8c5cff',
|
||||
ink: '#272235',
|
||||
bgPrimary: '#fff9fb',
|
||||
textPrimary: '#272235',
|
||||
textSecondary: '#6a6178',
|
||||
textTertiary: '#9b8fa8',
|
||||
}
|
||||
|
||||
export const AVATAR_GRADIENTS = [
|
||||
'linear-gradient(135deg, #ffd6e8, #fff1a6)',
|
||||
'linear-gradient(135deg, #ffe7a0, #b7f4d2)',
|
||||
'linear-gradient(135deg, #c9f7df, #d9d2ff)',
|
||||
'linear-gradient(135deg, #cfe8ff, #ffd6e8)',
|
||||
'linear-gradient(135deg, #e3d8ff, #ffd6e8)',
|
||||
]
|
||||
export const CLOUD_ENV = 'cloud1-d4g697lte499543d8'
|
||||
|
||||
export const MOOD_OPTIONS = [
|
||||
{ label: '开心 😄', value: 'happy' },
|
||||
@@ -45,10 +22,41 @@ export const HOT_HASHTAGS = [
|
||||
'#周末遛狗',
|
||||
]
|
||||
|
||||
export const BREED_OPTIONS = {
|
||||
dog: ['比熊', '柴犬', '金毛', '泰迪', '边牧', '法斗', '哈士奇', '萨摩耶', '拉布拉多', '其他'],
|
||||
cat: ['英短', '布偶', '美短', '加菲', '暹罗', '橘猫', '黑猫', '其他'],
|
||||
other: ['兔子', '仓鼠', '鹦鹉', '龟', '其他'],
|
||||
// 品种数据集 — 后期直接在对应数组末尾追加即可
|
||||
export const BREED_OPTIONS: Record<string, string[]> = {
|
||||
dog: [
|
||||
// 小型犬
|
||||
'比熊', '泰迪/贵宾', '博美', '吉娃娃', '约克夏', '马尔济斯', '西施', '迷你雪纳瑞',
|
||||
'柯基', '腊肠', '法斗', '巴哥', '迷你杜宾',
|
||||
// 中型犬
|
||||
'柴犬', '边牧', '萨摩耶', '哈士奇', '阿拉斯加', '秋田',
|
||||
// 大型犬
|
||||
'金毛', '拉布拉多', '德牧', '杜宾', '大白熊', '圣伯纳',
|
||||
// 中华/混种
|
||||
'田园犬(土狗)', '串串',
|
||||
// 兜底
|
||||
'其他',
|
||||
],
|
||||
cat: [
|
||||
// 短毛
|
||||
'英短', '美短', '苏格兰折耳', '孟买', '阿比西尼亚', '俄罗斯蓝猫', '暹罗', '东方短毛',
|
||||
// 长毛
|
||||
'布偶', '波斯', '加菲', '缅因', '挪威森林', '土耳其安哥拉',
|
||||
// 中华田园
|
||||
'橘猫', '狸花猫', '黑猫', '白猫', '三花', '玳瑁', '中华田园猫',
|
||||
// 兜底
|
||||
'其他',
|
||||
],
|
||||
other: [
|
||||
// 啮齿 & 小型哺乳
|
||||
'兔子', '仓鼠', '豚鼠', '龙猫', '花鼠', '刺猬',
|
||||
// 鸟类
|
||||
'虎皮鹦鹉', '牡丹鹦鹉', '玄凤鹦鹉', '文鸟', '金丝雀',
|
||||
// 爬行 & 水生
|
||||
'乌龟', '蜥蜴', '金鱼/锦鲤', '热带鱼',
|
||||
// 兜底
|
||||
'其他',
|
||||
],
|
||||
}
|
||||
|
||||
export const NEARBY_RADIUS_KM = 5
|
||||
|
||||
@@ -37,12 +37,13 @@ export function formatCount(n: number): string {
|
||||
}
|
||||
|
||||
export function getAvatarGradient(seed: string): string {
|
||||
// 紫/冷灰单色家族,深浅交错保证相邻头像可区分
|
||||
const gradients = [
|
||||
'linear-gradient(135deg, #ffd6e8, #fff1a6)',
|
||||
'linear-gradient(135deg, #ffe7a0, #b7f4d2)',
|
||||
'linear-gradient(135deg, #c9f7df, #d9d2ff)',
|
||||
'linear-gradient(135deg, #cfe8ff, #ffd6e8)',
|
||||
'linear-gradient(135deg, #e3d8ff, #c9f7df)',
|
||||
'linear-gradient(135deg, #d9d2fb, #efedfd)',
|
||||
'linear-gradient(135deg, #e3e5ec, #f4f5f8)',
|
||||
'linear-gradient(135deg, #cfc6fa, #e9e6fc)',
|
||||
'linear-gradient(135deg, #d8dbe4, #eef0f5)',
|
||||
'linear-gradient(135deg, #e6e1fd, #f3f1fe)',
|
||||
]
|
||||
let hash = 0
|
||||
for (let i = 0; i < seed.length; i++) hash = (hash * 31 + seed.charCodeAt(i)) & 0xffffffff
|
||||
@@ -61,12 +62,3 @@ export function getPetEmoji(species: string, breed?: string): string {
|
||||
}
|
||||
return (breed && dogBreedEmoji[breed]) || '🐶'
|
||||
}
|
||||
|
||||
export function calcDistance(lat1: number, lng1: number, lat2: number, lng2: number): number {
|
||||
const R = 6371000
|
||||
const dLat = (lat2 - lat1) * Math.PI / 180
|
||||
const dLng = (lng2 - lng1) * Math.PI / 180
|
||||
const a = Math.sin(dLat / 2) ** 2 +
|
||||
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng / 2) ** 2
|
||||
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
{
|
||||
"appid": "YOUR_APPID_HERE",
|
||||
"appid": "wx8b033e454024200b",
|
||||
"compileType": "miniprogram",
|
||||
"libVersion": "2.25.0",
|
||||
"libVersion": "3.16.1",
|
||||
"packOptions": {
|
||||
"ignore": [
|
||||
{ "type": "regexp", "value": "\\.eslintrc\\.js" },
|
||||
{ "type": "regexp", "value": "\\.gitignore" }
|
||||
]
|
||||
{
|
||||
"value": "\\.eslintrc\\.js",
|
||||
"type": "regexp"
|
||||
},
|
||||
{
|
||||
"value": "\\.gitignore",
|
||||
"type": "regexp"
|
||||
}
|
||||
],
|
||||
"include": []
|
||||
},
|
||||
"setting": {
|
||||
"coverView": true,
|
||||
@@ -31,19 +38,29 @@
|
||||
},
|
||||
"enableEngineNative": false,
|
||||
"bundle": false,
|
||||
"useIsolateContext": true,
|
||||
"userConfirmedBundleSwitch": false,
|
||||
"packNpmManually": false,
|
||||
"packNpmRelationList": [],
|
||||
"minifyWXSS": true,
|
||||
"disableUseStrict": false,
|
||||
"ignoreUploadUnusedFiles": true,
|
||||
"uploadWithSourceMap": true
|
||||
"compileWorklet": false,
|
||||
"minifyWXML": true,
|
||||
"localPlugins": false,
|
||||
"useCompilerPlugins": [
|
||||
"typescript"
|
||||
],
|
||||
"condition": true,
|
||||
"swc": false,
|
||||
"disableSWC": true
|
||||
},
|
||||
"condition": {},
|
||||
"editorSetting": {
|
||||
"tabIndent": "insertSpaces",
|
||||
"tabSize": 2
|
||||
},
|
||||
"srcMiniprogramRoot": "miniprogram/"
|
||||
}
|
||||
"miniprogramRoot": "miniprogram/",
|
||||
"srcMiniprogramRoot": "miniprogram/",
|
||||
"simulatorPluginLibVersion": {},
|
||||
"projectArchitecture": "multiPlatform"
|
||||
}
|
||||
68
project.miniapp.json
Normal file
68
project.miniapp.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"miniVersion": "v2",
|
||||
"name": "%name%",
|
||||
"version": "0.0.1",
|
||||
"versionCode": 100,
|
||||
"i18nFilePath": "i18n",
|
||||
"mini-ohos": {
|
||||
"sdkVersion": "0.5.1"
|
||||
},
|
||||
"mini-android": {
|
||||
"resourcePath": "miniapp/android/nativeResources",
|
||||
"sdkVersion": "1.6.24",
|
||||
"toolkitVersion": "0.11.0",
|
||||
"useExtendedSdk": {
|
||||
"media": false,
|
||||
"bluetooth": false,
|
||||
"network": false,
|
||||
"scanner": false,
|
||||
"xweb": false
|
||||
},
|
||||
"icons": {
|
||||
"hdpi": "",
|
||||
"xhdpi": "",
|
||||
"xxhdpi": "",
|
||||
"xxxhdpi": ""
|
||||
},
|
||||
"splashscreen": {
|
||||
"hdpi": "",
|
||||
"xhdpi": "",
|
||||
"xxhdpi": ""
|
||||
},
|
||||
"enableVConsole": "open",
|
||||
"privacy": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"mini-ios": {
|
||||
"sdkVersion": "1.7.0",
|
||||
"toolkitVersion": "0.0.9",
|
||||
"useExtendedSdk": {
|
||||
"WeAppOpenFuns": true,
|
||||
"WeAppNetwork": false,
|
||||
"WeAppBluetooth": false,
|
||||
"WeAppMedia": false,
|
||||
"WeAppLBS": false,
|
||||
"WeAppOthers": false
|
||||
},
|
||||
"enableVConsole": "open",
|
||||
"icons": {
|
||||
"mainIcon120": "",
|
||||
"mainIcon180": "",
|
||||
"spotlightIcon80": "",
|
||||
"spotlightIcon120": "",
|
||||
"settingsIcon58": "",
|
||||
"settingsIcon87": "",
|
||||
"notificationIcon40": "",
|
||||
"notificationIcon60": "",
|
||||
"appStore1024": ""
|
||||
},
|
||||
"splashScreen": {
|
||||
"customImage": ""
|
||||
},
|
||||
"privacy": {
|
||||
"enable": false
|
||||
},
|
||||
"enableOpenUrlNavigate": true
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,8 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["miniprogram/*"]
|
||||
},
|
||||
"typeRoots": ["./typings"]
|
||||
}
|
||||
|
||||
},
|
||||
"include": ["miniprogram/**/*.ts"],
|
||||
"exclude": ["node_modules", "miniprogram/**/*.d.ts"]
|
||||
|
||||
Reference in New Issue
Block a user