Initial commit

This commit is contained in:
2026-06-05 17:46:51 +08:00
commit c04c890186
96 changed files with 6477 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
{
"navigationStyle": "custom",
"usingComponents": {
"post-card": "/components/post-card/post-card"
}
}

View File

@@ -0,0 +1,167 @@
import { Post } from '../../types/index'
import { api } from '../../utils/api'
import { getAvatarGradient, getPetEmoji, formatCount } from '../../utils/format'
const app = getApp<{ userInfo: any; isLoggedIn: boolean }>()
Page({
data: {
statusBarHeight: 0,
tabBarHeight: 56,
userInfo: {
nickName: '',
handle: '',
bio: '',
location: '',
stats: { posts: 0, friends: 0, likes: 0 },
pets: [],
} as any,
formattedLikes: '0',
avatarGradient: 'linear-gradient(135deg, #ffd6e8, #fff1a6)',
posts: [] as Post[],
postView: 'grid' as 'grid' | 'list',
loading: false,
refreshing: false,
},
onLoad() {
const info = wx.getSystemInfoSync()
this.setData({ statusBarHeight: info.statusBarHeight })
this.loadProfile()
},
onShow() {
const tabBar = this.getTabBar() as any
tabBar?.setSelected?.(3)
this.syncFromGlobal()
},
syncFromGlobal() {
const user = app.globalData?.userInfo
if (!user) {
wx.redirectTo({ url: '/pages/login/login' })
return
}
const pets = (user.pets || []).map((p: any) => ({
...p,
emoji: getPetEmoji(p.species, p.breed),
gradient: getAvatarGradient(p._id),
}))
this.setData({
userInfo: { ...user, pets },
formattedLikes: formatCount(user.stats?.likes || 0),
avatarGradient: getAvatarGradient(user.openid || user._id || ''),
})
},
async loadProfile() {
this.setData({ loading: true })
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 })),
])
if (_user) {
app.globalData.userInfo = _user
this.syncFromGlobal()
}
this.setData({ posts: postsRes.list })
} catch {
} finally {
this.setData({ loading: false, refreshing: false })
}
},
setPostView(e: WechatMiniprogram.CustomEvent) {
this.setData({ postView: e.currentTarget.dataset.v as 'grid' | 'list' })
},
onStatTap(e: WechatMiniprogram.CustomEvent) {
const type = e.currentTarget.dataset.type
if (type === 'following') {
wx.switchTab({ url: '/pages/friends/friends' })
}
},
onGridItemTap(e: WechatMiniprogram.CustomEvent) {
const id = e.currentTarget.dataset.id
wx.navigateTo({ url: `/pages/post-detail/post-detail?id=${id}` })
},
onPetTap(e: WechatMiniprogram.CustomEvent) {
const pid = e.currentTarget.dataset.pid
wx.navigateTo({ url: `/pages/pet-edit/pet-edit?id=${pid}` })
},
onAddPet() {
wx.navigateTo({ url: '/pages/pet-edit/pet-edit' })
},
onEditProfile() {
wx.navigateTo({ url: '/pages/edit-profile/edit-profile' })
},
onEditAvatar() {
wx.showActionSheet({
itemList: ['从相册选取', '拍照'],
success: res => {
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: [res.tapIndex === 0 ? 'album' : 'camera'],
success: async mediaRes => {
const path = mediaRes.tempFiles[0].tempFilePath
const cloudPath = `avatars/${app.globalData.userInfo?._id || Date.now()}.jpg`
wx.showLoading({ title: '上传中...' })
try {
const r = await wx.cloud.uploadFile({ cloudPath, filePath: path })
wx.hideLoading()
wx.showToast({ title: '头像已更新', icon: 'success' })
} catch {
wx.hideLoading()
wx.showToast({ title: '上传失败', icon: 'none' })
}
},
})
},
})
},
onSettings() {
wx.showActionSheet({
itemList: ['账号设置', '隐私设置', '退出登录'],
success: res => {
if (res.tapIndex === 2) {
wx.showModal({
title: '退出登录',
content: '确定要退出汪圈吗?',
success: modal => {
if (modal.confirm) {
wx.clearStorageSync()
app.globalData.userInfo = null
app.globalData.isLoggedIn = false
wx.redirectTo({ url: '/pages/login/login' })
}
},
})
}
},
})
},
onGoPost() {
wx.navigateTo({ url: '/pages/post/post' })
},
async onRefresh() {
this.setData({ refreshing: true })
await this.loadProfile()
},
onShareAppMessage() {
return {
title: `${this.data.userInfo.nickName}的汪圈主页`,
path: `/pages/pet-detail/pet-detail?userId=${app.globalData?.userInfo?._id}`,
}
},
})

View File

@@ -0,0 +1,148 @@
<view class="profile-page">
<view class="safe-top" style="height: {{statusBarHeight}}px"></view>
<scroll-view
scroll-y="true"
class="profile-scroll"
enhanced="true"
show-scrollbar="false"
refresher-enabled="true"
bindrefresherrefresh="onRefresh"
refresher-triggered="{{refreshing}}"
>
<!-- 用户信息区 -->
<view class="profile-header">
<!-- 头部操作栏 -->
<view class="header-topbar">
<view class="settings-btn" bindtap="onSettings">⚙️</view>
</view>
<!-- 头像 + 基本信息 -->
<view class="user-top">
<view class="user-avatar-wrap">
<view class="user-avatar" style="background: {{avatarGradient}}">
<text class="avatar-text">{{userInfo.nickName[0] || '?'}}</text>
</view>
<view class="avatar-edit" bindtap="onEditAvatar">✏️</view>
</view>
<view class="user-meta">
<view class="user-name-row">
<text class="user-name">{{userInfo.nickName}}</text>
</view>
<text class="user-handle">{{userInfo.handle || '@' + userInfo.openid.slice(0,8)}} · {{userInfo.location || '上海'}}</text>
<text class="user-bio">{{userInfo.bio || '还没有介绍~'}}</text>
</view>
<view class="edit-profile-btn" bindtap="onEditProfile">编辑</view>
</view>
<!-- 数据统计 -->
<view class="stats-row">
<view class="stat-item" bindtap="onStatTap" data-type="posts">
<text class="stat-num">{{userInfo.stats.posts || 0}}</text>
<text class="stat-label">动态</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item" bindtap="onStatTap" data-type="following">
<text class="stat-num">{{userInfo.stats.friends || 0}}</text>
<text class="stat-label">汪友</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item" bindtap="onStatTap" data-type="likes">
<text class="stat-num">{{formattedLikes}}</text>
<text class="stat-label">获赞</text>
</view>
</view>
</view>
<!-- 宠物档案卡 -->
<view class="section-wrap">
<view class="section-header">
<text class="section-title">我的宝贝</text>
<view class="add-pet-btn" bindtap="onAddPet">+ 添加</view>
</view>
<!-- 宠物卡片横滑 -->
<scroll-view scroll-x="true" enhanced="true" show-scrollbar="false" class="pets-scroll">
<view
wx:for="{{userInfo.pets}}"
wx:key="_id"
class="pet-card"
bindtap="onPetTap"
data-pid="{{item._id}}"
>
<view class="pet-avatar" style="background: {{item.gradient || 'linear-gradient(135deg, #c9f7df, #d9d2ff)'}}">
<text class="pet-emoji">{{item.emoji || '🐾'}}</text>
</view>
<view class="pet-info">
<text class="pet-name">{{item.name}}</text>
<text class="pet-breed">{{item.breed}} · {{item.gender === 'female' ? '女生' : '男生'}} · {{item.age}}</text>
</view>
<view class="pet-badge">我的宝贝</view>
</view>
<!-- 空态时添加宠物 -->
<view wx:if="{{userInfo.pets.length === 0}}" class="pet-card pet-card-empty" bindtap="onAddPet">
<view class="pet-avatar pet-avatar-add">
<text style="font-size: 48rpx;">+</text>
</view>
<view class="pet-info">
<text class="pet-name">添加宠物</text>
<text class="pet-breed">建立您的宠物档案</text>
</view>
</view>
</scroll-view>
</view>
<!-- 动态网格 / 列表切换 -->
<view class="section-wrap">
<view class="section-header">
<text class="section-title">全部动态</text>
<view class="view-toggle">
<view class="toggle-item {{postView === 'grid' ? 'active' : ''}}" bindtap="setPostView" data-v="grid">▦</view>
<view class="toggle-item {{postView === 'list' ? 'active' : ''}}" bindtap="setPostView" data-v="list">☰</view>
</view>
</view>
<!-- 网格视图 -->
<view wx:if="{{postView === 'grid'}}" class="post-grid">
<view
wx:for="{{posts}}"
wx:key="_id"
class="grid-item"
bindtap="onGridItemTap"
data-id="{{item._id}}"
>
<image
wx:if="{{item.images && item.images.length > 0}}"
class="grid-img"
src="{{item.images[0]}}"
mode="aspectFill"
lazy-load="true"
/>
<view wx:else class="grid-no-img">
<text>{{item.content.slice(0, 20)}}</text>
</view>
</view>
<!-- 空态 -->
<view wx:if="{{posts.length === 0}}" class="grid-empty">
<text class="empty-emoji">📸</text>
<text class="empty-tip">还没有动态,去发一条吧</text>
<view class="btn-primary post-btn" bindtap="onGoPost">发动态</view>
</view>
</view>
<!-- 列表视图 -->
<view wx:else>
<post-card
wx:for="{{posts}}"
wx:key="_id"
post="{{item}}"
/>
</view>
</view>
<view style="height: {{tabBarHeight}}px"></view>
<view class="safe-bottom"></view>
</scroll-view>
</view>

View File

@@ -0,0 +1,310 @@
.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 {
display: flex;
justify-content: flex-end;
padding: 16rpx 0 12rpx;
}
.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);
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
}
.user-top {
display: flex;
align-items: flex-start;
gap: 24rpx;
margin-bottom: 28rpx;
}
.user-avatar-wrap {
position: relative;
flex-shrink: 0;
}
.user-avatar {
width: 128rpx;
height: 128rpx;
border-radius: 40rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12rpx 24rpx rgba(98, 60, 118, 0.18);
}
.avatar-text {
font-size: 56rpx;
font-weight: 800;
color: rgba(39, 34, 53, 0.72);
}
.avatar-edit {
position: absolute;
bottom: -6rpx;
right: -6rpx;
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background: #fff;
box-shadow: 0 4rpx 10rpx rgba(78, 56, 96, 0.16);
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
}
.user-meta {
flex: 1;
padding-top: 6rpx;
min-width: 0;
}
.user-name-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 4rpx;
}
.user-name {
font-size: 36rpx;
font-weight: 900;
color: #272235;
}
.user-handle {
display: block;
font-size: 24rpx;
color: #9b8fa8;
margin-bottom: 10rpx;
}
.user-bio {
display: block;
font-size: 26rpx;
color: #6a6178;
line-height: 1.5;
}
.edit-profile-btn {
background: rgba(255, 255, 255, 0.80);
border: 1rpx solid rgba(255, 255, 255, 0.72);
border-radius: 999rpx;
padding: 14rpx 28rpx;
font-size: 24rpx;
font-weight: 800;
color: #272235;
flex-shrink: 0;
box-shadow: 0 8rpx 18rpx rgba(78, 56, 96, 0.08);
align-self: flex-start;
}
/* 数据统计 */
.stats-row {
display: flex;
gap: 16rpx;
align-items: center;
}
.stat-item {
flex: 1;
padding: 24rpx 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);
}
.stat-num {
display: block;
font-size: 40rpx;
font-weight: 900;
color: #ff4f91;
}
.stat-label {
display: block;
font-size: 22rpx;
font-weight: 700;
color: #9b8fa8;
margin-top: 4rpx;
}
/* 区块 */
.section-wrap {
margin-bottom: 12rpx;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 28rpx 14rpx;
}
.section-title {
font-size: 26rpx;
font-weight: 900;
color: #9b8fa8;
letter-spacing: 0.5rpx;
text-transform: uppercase;
}
.add-pet-btn {
font-size: 24rpx;
font-weight: 800;
color: #ff4f91;
background: #ffe5f0;
border-radius: 999rpx;
padding: 8rpx 20rpx;
}
/* 宠物卡片 */
.pets-scroll {
white-space: nowrap;
padding: 0 28rpx 16rpx;
}
.pet-card {
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;
padding: 20rpx 24rpx;
margin-right: 20rpx;
box-shadow: 0 12rpx 26rpx rgba(78, 56, 96, 0.09);
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);
}
.pet-avatar {
width: 84rpx;
height: 84rpx;
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.pet-avatar-add { background: rgba(255, 255, 255, 0.60); color: #9b8fa8; }
.pet-emoji { font-size: 44rpx; line-height: 1; }
.pet-info { min-width: 0; }
.pet-name {
display: block;
font-size: 28rpx;
font-weight: 900;
color: #272235;
margin-bottom: 6rpx;
}
.pet-breed {
display: block;
font-size: 22rpx;
color: #9b8fa8;
}
.pet-badge {
background: #ffe5f0;
border-radius: 999rpx;
padding: 8rpx 16rpx;
font-size: 20rpx;
color: #a91d5b;
font-weight: 900;
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;
overflow: hidden;
}
.toggle-item {
padding: 10rpx 20rpx;
font-size: 24rpx;
color: #9b8fa8;
}
.toggle-item.active {
background: rgba(255, 79, 145, 0.12);
color: #ff4f91;
font-weight: 800;
}
/* 动态网格 */
.post-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6rpx;
padding: 0 28rpx 20rpx;
}
.grid-item {
aspect-ratio: 1;
border-radius: 28rpx;
overflow: hidden;
background: #f0eef5;
display: flex;
align-items: center;
justify-content: center;
}
.grid-img { width: 100%; height: 100%; }
.grid-no-img {
padding: 12rpx;
font-size: 20rpx;
color: #9b8fa8;
text-align: center;
line-height: 1.4;
}
.grid-empty {
grid-column: span 3;
display: flex;
flex-direction: column;
align-items: center;
padding: 60rpx 0;
gap: 16rpx;
}
.empty-emoji { font-size: 72rpx; }
.empty-tip { font-size: 26rpx; color: #9b8fa8; }
.post-btn { padding: 18rpx 48rpx; font-size: 26rpx; font-weight: 800; margin-top: 8rpx; }