pet/pages/profile/profile.vue

1175 lines
23 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="profile-container page-container-unified">
<!-- 用户信息卡片 -->
<view class="user-info-card">
<view class="user-avatar-section" :class="{ 'logged-in': userInfo.nickName }" @click="handleUserAction">
<view class="avatar-wrapper">
<u-avatar
:src="userInfo.avatarUrl || ''"
:text="userInfo.nickName ? userInfo.nickName.charAt(0) : '👤'"
size="100"
shape="circle"
bg-color="linear-gradient(135deg, #FF8A80, #FFB6C1)"
color="white"
font-size="40"
></u-avatar>
<view class="online-status" v-if="userInfo.nickName">
<view class="status-dot"></view>
</view>
<!-- 登录提示图标仅在未登录时显示 -->
<view class="login-hint" v-if="!userInfo.nickName">
<text class="hint-icon">👋</text>
</view>
</view>
</view>
<view class="user-info-section">
<view class="user-name">{{ userInfo.nickName || '点击头像登录' }}</view>
<view class="user-status">{{ userInfo.nickName ? '已登录' : '未登录' }}</view>
<view class="login-days" v-if="userInfo.nickName">已使用 {{ loginDays }} </view>
</view>
</view>
<!-- 宠物概览统计卡片 -->
<view class="stats-card">
<view class="stats-header">
<view class="header-left">
<text class="card-icon">🐱</text>
<text class="card-title">我的宠物</text>
</view>
</view>
<view class="stats-grid">
<view class="stat-item" @click="navigateTo('/pages/pets/pets')">
<view class="stat-number">{{ petStats.petCount }}</view>
<view class="stat-label">宠物</view>
<view class="stat-icon">🐱</view>
</view>
<view class="stat-item" @click="navigateTo('/pages/pets/pet-records')">
<view class="stat-number">{{ petStats.recordCount }}</view>
<view class="stat-label">记录</view>
<view class="stat-icon">📝</view>
</view>
<view class="stat-item" @click="showReminders">
<view class="stat-number">{{ petStats.reminderCount }}</view>
<view class="stat-label">提醒</view>
<view class="stat-icon"></view>
</view>
</view>
</view>
<!-- 家庭管理卡片 -->
<view class="family-card">
<view class="card-header">
<view class="header-left">
<text class="card-icon">👨👩👧👦</text>
<text class="card-title">我的家庭</text>
</view>
</view>
<view class="family-stats">
<view class="stat-item" @click="navigateToFamily">
<view class="stat-number">{{ familyStats.memberCount }}</view>
<view class="stat-label">成员</view>
<view class="stat-icon">👥</view>
</view>
<view class="stat-item" @click="navigateTo('/pages/pets/pets')">
<view class="stat-number">{{ familyStats.sharedPets || petStats.petCount }}</view>
<view class="stat-label">共享宠物</view>
<view class="stat-icon">🐾</view>
</view>
<view class="stat-item" @click="navigateToFamily">
<view class="stat-number">{{ familyStats.activityCount || 0 }}</view>
<view class="stat-label">动态</view>
<view class="stat-icon">📱</view>
</view>
</view>
<view class="family-actions">
<view class="action-btn" @click="inviteFamily">
<text class="btn-icon"></text>
<text class="btn-text">邀请成员</text>
</view>
<view class="action-btn" @click="navigateToFamily">
<text class="btn-icon">👥</text>
<text class="btn-text">家庭设置</text>
</view>
</view>
</view>
<!-- 领养管理卡片 -->
<view class="adoption-card">
<view class="card-header">
<view class="header-left">
<text class="card-icon">🏠</text>
<text class="card-title">领养管理</text>
</view>
</view>
<view class="adoption-stats">
<view class="adoption-item" @click="navigateTo('/pages/adoption/my-published')">
<view class="adoption-number">{{ adoptionStats.publishedCount }}</view>
<view class="adoption-label">已发布</view>
</view>
<view class="adoption-item" @click="navigateTo('/pages/adoption/my-applications')">
<view class="adoption-number">{{ adoptionStats.applicationCount }}</view>
<view class="adoption-label">已申请</view>
</view>
</view>
<view class="publish-btn" @click="navigateTo('/pages/adoption/publish')">
<text class="publish-icon">📝</text>
<text class="publish-text">发布领养信息</text>
</view>
</view>
<!-- 设置与帮助卡片 -->
<view class="settings-card">
<view class="card-header">
<view class="header-left">
<text class="card-icon"></text>
<text class="card-title">设置与帮助</text>
</view>
</view>
<view class="settings-list">
<view class="setting-item floating-indicator" @click="navigateTo('/pages/profile/feedback')">
<view class="setting-left">
<text class="setting-icon">💬</text>
<text class="setting-text">意见反馈</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { reactive, ref, onMounted, computed } from 'vue'
import { uploadImage } from '@/http/api/common.js'
import { updateUserInfo } from '@/http/api/profile.js'
import {
getCurrentLoginStep,
isLoginCompleted,
checkAndResumeLogin,
saveWxLoginData,
clearTempLoginData
} from '@/utils/loginState.js'
export default {
name: 'ProfilePage',
setup() {
// 响应式数据
const userInfo = ref({
nickName: '',
avatarUrl: ''
})
const loginDays = ref(0)
// 宠物统计数据
const petStats = reactive({
petCount: 0,
recordCount: 0,
reminderCount: 0
})
// 家庭统计数据
const familyStats = reactive({
memberCount: 1,
sharedPets: 0,
activityCount: 0
})
// 领养统计数据
const adoptionStats = reactive({
publishedCount: 0,
applicationCount: 0
})
// 方法定义
const initPage = () => {
checkLogin()
loadUserStats()
}
const checkLogin = () => {
// 检查登录状态
console.log('Profile页面登录状态检查')
// 检查是否需要继续未完成的登录流程
const nextPage = checkAndResumeLogin()
if (nextPage) {
console.log('检测到未完成的登录流程,跳转到:', nextPage)
// 显示提示信息
uni.showToast({
title: '继续完成登录流程',
icon: 'none',
duration: 1500
})
setTimeout(() => {
uni.navigateTo({
url: nextPage
})
}, 1500)
return
}
// 检查是否已完成登录
if (isLoginCompleted()) {
const savedUserInfo = uni.getStorageSync('userInfo')
if (savedUserInfo) {
userInfo.value = savedUserInfo
calculateLoginDays()
// 清理临时登录数据
clearTempLoginData()
}
}
}
const calculateLoginDays = () => {
const loginDate = uni.getStorageSync('loginDate')
if (loginDate) {
const now = new Date()
const login = new Date(loginDate)
const diffTime = Math.abs(now - login)
loginDays.value = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
}
}
const loadUserStats = () => {
try {
// 加载宠物统计数据
const pets = uni.getStorageSync('pets') || []
petStats.petCount = pets.length
// 加载记录统计数据 - 统计所有宠物的记录
let totalRecords = 0
pets.forEach(pet => {
const petRecords = uni.getStorageSync(`petRecords_${pet.id}`) || []
totalRecords += petRecords.length
})
petStats.recordCount = totalRecords
// 计算待处理提醒数量
const today = new Date()
const reminders = uni.getStorageSync('reminders') || []
const pendingReminders = reminders.filter(reminder => {
const reminderDate = new Date(reminder.date)
return reminderDate <= today && !reminder.completed
})
petStats.reminderCount = pendingReminders.length
// 加载家庭数据
const familyData = uni.getStorageSync('familyData') || { members: [], activities: [] }
familyStats.memberCount = familyData.members.length || 1
familyStats.sharedPets = Math.min(petStats.petCount, familyStats.memberCount > 1 ? petStats.petCount : 0)
familyStats.activityCount = familyData.activities ? familyData.activities.length : 0
// 加载领养数据
const adoptionData = uni.getStorageSync('adoptionData') || {
published: [],
applications: []
}
adoptionStats.publishedCount = adoptionData.published.length
adoptionStats.applicationCount = adoptionData.applications.length
} catch (error) {
console.error('加载用户统计数据失败:', error)
// 重置为默认值
resetStatsToDefault()
}
}
// 重置统计数据为默认值
const resetStatsToDefault = () => {
Object.assign(petStats, {
petCount: 0,
recordCount: 0,
reminderCount: 0
})
Object.assign(familyStats, {
memberCount: 1,
sharedPets: 0,
activityCount: 0
})
Object.assign(adoptionStats, {
publishedCount: 0,
applicationCount: 0
})
}
const handleUserAction = () => {
if (userInfo.value.nickName) {
// 已登录,跳转到个人信息设置
navigateTo('/pages/profile/user-info')
} else {
// 未登录,执行登录
wxLogin()
}
}
const wxLogin = () => {
uni.login({
provider: 'weixin',
success: (res) => {
console.log('登录成功', res)
// 使用状态管理工具保存微信登录数据
saveWxLoginData(res)
calculateLoginDays()
uni.showToast({
title: '登录成功',
icon: 'success',
duration: 1500
})
// 跳转到手机号授权页面
setTimeout(() => {
uni.navigateTo({
url: '/pages/auth/phone-auth'
})
}, 1500)
},
fail: (err) => {
console.error('登录失败', err)
uni.showToast({
title: '登录失败,请重试',
icon: 'none'
})
}
})
}
// 统一的页面导航方法
const navigateTo = (url, options = {}) => {
const { fallbackMessage = '页面开发中' } = options
uni.navigateTo({
url,
fail: (error) => {
console.warn('页面导航失败:', url, error)
uni.showToast({
title: fallbackMessage,
icon: 'none'
})
}
})
}
const navigateToFamily = () => {
navigateTo('/pages/profile/family', {
fallbackMessage: '家庭功能开发中'
})
}
const inviteFamily = () => {
uni.showModal({
title: '邀请家庭成员',
content: '通过微信分享邀请码邀请家人加入',
confirmText: '分享邀请',
success: (res) => {
if (res.confirm) {
uni.showToast({
title: '功能开发中',
icon: 'none'
})
}
}
})
}
const showReminders = () => {
if (petStats.reminderCount > 0) {
navigateTo('/pages/profile/reminders')
} else {
uni.showToast({
title: '暂无提醒事项',
icon: 'none'
})
}
}
// 刷新页面数据
const refreshData = () => {
loadUserStats()
}
// 页面显示时刷新数据
const onShow = () => {
refreshData()
}
// 下拉刷新
const onPullDownRefresh = () => {
refreshData()
setTimeout(() => {
uni.stopPullDownRefresh()
}, 1000)
}
// 格式化数字显示
const formatNumber = (num) => {
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'k'
}
return num.toString()
}
// 页面初始化
onMounted(() => {
initPage()
})
return {
userInfo,
loginDays,
petStats,
familyStats,
adoptionStats,
handleUserAction,
navigateTo,
navigateToFamily,
inviteFamily,
showReminders,
refreshData,
formatNumber,
resetStatsToDefault,
onShow,
onPullDownRefresh
}
},
// 页面生命周期
onShow() {
this.onShow()
},
onPullDownRefresh() {
this.onPullDownRefresh()
}
}
</script>
<style lang="scss" scoped>
.profile-container {
/* 边距由外层page-container-unified统一管理 */
}
/* 用户信息卡片 */
.user-info-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
margin: 0 0 24rpx 0;
border-radius: 24rpx;
padding: 32rpx;
display: flex;
align-items: center;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3);
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
}
.user-avatar-section {
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 50%;
padding: 12rpx;
position: relative;
// 未登录时的可点击提示
&:not(.logged-in) {
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 2rpx dashed rgba(255, 138, 128, 0.3);
border-radius: 50%;
animation: breathe 3s ease-in-out infinite;
}
}
&:hover {
background: rgba(255, 138, 128, 0.15);
transform: scale(1.08);
box-shadow: 0 8rpx 24rpx rgba(255, 138, 128, 0.25);
}
&:active {
transform: scale(0.92);
transition: all 0.1s ease;
}
.avatar-wrapper {
position: relative;
.online-status {
position: absolute;
bottom: 8rpx;
right: 8rpx;
width: 24rpx;
height: 24rpx;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.status-dot {
width: 16rpx;
height: 16rpx;
background: #4CAF50;
border-radius: 50%;
}
}
.login-hint {
position: absolute;
bottom: -8rpx;
right: -8rpx;
width: 40rpx;
height: 40rpx;
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 16rpx rgba(255, 138, 128, 0.4);
animation: pulse 2s infinite;
border: 3rpx solid white;
z-index: 10;
&::before {
content: '';
position: absolute;
top: -6rpx;
left: -6rpx;
right: -6rpx;
bottom: -6rpx;
border: 2rpx solid rgba(255, 138, 128, 0.3);
border-radius: 50%;
animation: ripple 2s infinite;
}
.hint-icon {
font-size: 22rpx;
color: white;
font-weight: bold;
}
}
}
}
.user-info-section {
flex: 1;
margin-left: 24rpx;
.user-name {
font-size: 36rpx;
font-weight: 600;
color: #333333;
margin-bottom: 8rpx;
}
.user-status {
font-size: 24rpx;
color: #FF8A80;
margin-bottom: 4rpx;
}
.login-days {
font-size: 22rpx;
color: #999999;
}
}
.user-action {
.action-icon {
font-size: 32rpx;
}
}
}
/* 宠物概览统计卡片 */
.stats-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
margin: 0 0 24rpx 0;
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3);
.stats-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.header-left {
display: flex;
align-items: center;
.card-icon {
font-size: 36rpx;
margin-right: 16rpx;
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
}
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 24rpx;
.stat-item {
text-align: center;
padding: 24rpx 16rpx;
border-radius: 16rpx;
background: rgba(255, 138, 128, 0.05);
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
background: rgba(255, 138, 128, 0.1);
}
.stat-number {
font-size: 40rpx;
font-weight: 700;
color: #FF8A80;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 22rpx;
color: #666666;
margin-bottom: 8rpx;
}
.stat-icon {
font-size: 28rpx;
}
}
}
}
/* 家庭管理卡片 */
.family-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
margin: 0 0 24rpx 0;
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3);
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.header-left {
display: flex;
align-items: center;
.card-icon {
font-size: 32rpx;
margin-right: 12rpx;
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
}
.header-right {
display: flex;
align-items: center;
.member-count {
font-size: 24rpx;
color: #FF8A80;
margin-right: 8rpx;
}
}
}
.family-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
margin-bottom: 32rpx;
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 24rpx 16rpx;
background: rgba(255, 138, 128, 0.05);
border-radius: 16rpx;
transition: all 0.3s ease;
cursor: pointer;
&:active {
transform: scale(0.95);
background: rgba(255, 138, 128, 0.1);
}
.stat-number {
font-size: 36rpx;
font-weight: 600;
color: #FF8A80;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 24rpx;
color: #666666;
margin-bottom: 8rpx;
}
.stat-icon {
font-size: 28rpx;
}
}
}
.family-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16rpx;
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
padding: 24rpx 16rpx;
border-radius: 16rpx;
background: rgba(255, 138, 128, 0.05);
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
background: rgba(255, 138, 128, 0.1);
}
.btn-icon {
font-size: 32rpx;
margin-bottom: 8rpx;
}
.btn-text {
font-size: 22rpx;
color: #666666;
}
}
}
}
/* 领养管理卡片 */
.adoption-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
margin: 0 0 24rpx 0;
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3);
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.header-left {
display: flex;
align-items: center;
.card-icon {
font-size: 32rpx;
margin-right: 12rpx;
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
}
.header-right {
/* 悬浮指示器样式已在通用样式中定义 */
}
}
.adoption-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16rpx;
margin-bottom: 24rpx;
.adoption-item {
text-align: center;
padding: 20rpx 16rpx;
border-radius: 16rpx;
background: rgba(255, 138, 128, 0.05);
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
background: rgba(255, 138, 128, 0.1);
}
.adoption-number {
font-size: 36rpx;
font-weight: 700;
color: #FF8A80;
margin-bottom: 8rpx;
}
.adoption-label {
font-size: 22rpx;
color: #666666;
}
}
}
.publish-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx;
border-radius: 16rpx;
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
}
.publish-icon {
font-size: 24rpx;
margin-right: 8rpx;
}
.publish-text {
font-size: 26rpx;
color: white;
font-weight: 500;
}
}
}
/* 响应式设计 */
@media (max-width: 375px) {
.profile-container {
.user-info-card {
margin: 0 0 20rpx 0;
padding: 24rpx;
}
.stats-card,
.family-card,
.adoption-card,
.settings-card {
margin: 0 0 20rpx 0;
padding: 24rpx;
}
.stats-grid {
gap: 16rpx;
.stat-item {
padding: 20rpx 12rpx;
.stat-number {
font-size: 36rpx;
}
}
}
.family-stats {
gap: 16rpx;
margin-bottom: 24rpx;
.stat-item {
padding: 20rpx 12rpx;
.stat-number {
font-size: 32rpx;
}
.stat-label {
font-size: 22rpx;
}
.stat-icon {
font-size: 24rpx;
}
}
}
.family-actions {
gap: 12rpx;
.action-btn {
padding: 20rpx 12rpx;
}
}
}
}
/* 加载状态 */
.loading-state {
display: flex;
justify-content: center;
align-items: center;
padding: 60rpx;
.loading-text {
font-size: 24rpx;
color: #999999;
margin-left: 16rpx;
}
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60rpx 40rpx;
.empty-icon {
font-size: 80rpx;
margin-bottom: 16rpx;
opacity: 0.5;
}
.empty-text {
font-size: 24rpx;
color: #999999;
line-height: 1.5;
}
}
/* 悬浮指示器通用样式 */
.floating-indicator {
position: relative;
cursor: pointer;
&::after {
content: '';
position: absolute;
right: 16rpx;
top: 50%;
transform: translateY(-50%);
width: 12rpx;
height: 12rpx;
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
border-radius: 50%;
animation: pulse 2s infinite;
transition: all 0.3s ease;
opacity: 0.8;
}
&:hover::after {
width: 16rpx;
height: 16rpx;
box-shadow: 0 0 0 6rpx rgba(255, 138, 128, 0.2);
opacity: 1;
}
&:active::after {
transform: translateY(-50%) scale(0.8);
}
}
/* 设置与帮助卡片 */
.settings-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
margin: 0 0 24rpx 0;
border-radius: 24rpx;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3);
overflow: hidden;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
.header-left {
display: flex;
align-items: center;
.card-icon {
font-size: 36rpx;
margin-right: 16rpx;
}
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
}
}
.settings-list {
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
transition: all 0.3s ease;
&:active {
background: rgba(255, 138, 128, 0.05);
}
.setting-left {
display: flex;
align-items: center;
.setting-icon {
font-size: 32rpx;
margin-right: 16rpx;
}
.setting-text {
font-size: 30rpx;
color: #333333;
font-weight: 500;
}
}
.setting-right {
/* 悬浮指示器样式已在通用样式中定义 */
}
}
}
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
100% {
transform: scale(1);
opacity: 1;
}
}
/* 悬浮指示器脉冲动画 */
@keyframes pulse {
0%, 100% {
opacity: 0.7;
transform: translateY(-50%) scale(1);
}
50% {
opacity: 1;
transform: translateY(-50%) scale(1.2);
}
}
@keyframes breathe {
0%, 100% {
opacity: 0.3;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.02);
}
}
@keyframes ripple {
0% {
transform: scale(1);
opacity: 0.8;
}
100% {
transform: scale(1.4);
opacity: 0;
}
}
.profile-container > view {
animation: fadeIn 0.5s ease-out;
}
/* 响应式设计 */
@media screen and (max-width: 750rpx) {
}
@media screen and (min-width: 1200rpx) {
}
.profile-container > view:nth-child(1) { animation-delay: 0.1s; }
.profile-container > view:nth-child(2) { animation-delay: 0.2s; }
.profile-container > view:nth-child(3) { animation-delay: 0.3s; }
.profile-container > view:nth-child(4) { animation-delay: 0.4s; }
.profile-container > view:nth-child(5) { animation-delay: 0.5s; }
</style>