FurBall/pages/index/mine.vue

775 lines
19 KiB
Vue

<template>
<scroll-view class="mine-page" scroll-y :scroll-with-animation="true"
:style="{ paddingBottom: safeAreaBottom + 'px' }">
<view class="mine-container">
<!-- 用户信息头部 -->
<view class="profile-card">
<view class="profile-bg" />
<view class="profile-content">
<view class="profile-avatar-wrap" @click="handleEditProfile">
<image v-if="displayAvatarUrl" class="avatar-img" :src="displayAvatarUrl" mode="aspectFill" />
<view v-else class="avatar-placeholder">
<TnIcon name="my-circle" :size="80" color="#FFFFFF" />
</view>
<view class="avatar-edit-badge">
<TnIcon name="edit-write-fill" :size="24" color="#fff" />
</view>
</view>
<text class="profile-name" @click="handleEditProfile">
{{ userInfo.nickName || '铲屎官' }}
</text>
<view class="profile-member-row">
<view class="member-type-card" @click="handleGoMember">
<TnIcon :name="userInfo.membershipType === 'premium' ? 'vip-fill' : 'vip'" :size="36"
:color="userInfo.membershipType === 'premium' ? '#F59E0B' : '#94A3B8'" />
<view class="member-type-info">
<text class="member-type-name">
{{ userInfo.membershipType === 'premium' ? '高级会员' : '免费用户' }}
</text>
<text class="member-type-hint">
{{ userInfo.membershipType === 'premium'
? (userInfo.memberExpireAt ? '有效期至 ' + formatDate(userInfo.memberExpireAt) : '')
: '立即升级享更多权益' }}
</text>
</view>
<TnIcon name="right" :size="28" color="rgba(255,255,255,0.6)" />
</view>
</view>
</view>
</view>
<!-- 数据统计卡片 -->
<view class="stats-section">
<view class="stats-card" @click="handleGoMyPets">
<view class="stats-icon-wrap">
<TnIcon name="floral" :size="36" color="#F97316" />
</view>
<text class="stats-value">{{ statsData.petCount }}</text>
<text class="stats-label">我的萌宠</text>
</view>
<view class="stats-card" @click="handleGoRecords">
<view class="stats-icon-wrap">
<TnIcon name="write" :size="36" color="#14B8A6" />
</view>
<text class="stats-value">{{ statsData.recordCount }}</text>
<text class="stats-label">健康记录</text>
</view>
<view class="stats-card" @click="handleGoSchedules">
<view class="stats-icon-wrap">
<TnIcon name="calendar" :size="36" color="#8B5CF6" />
</view>
<text class="stats-value">{{ statsData.scheduleCount }}</text>
<text class="stats-label">待办提醒</text>
</view>
</view>
<!-- 快捷操作 -->
<view class="section-block">
<text class="section-title">快捷操作</text>
<view class="quick-grid">
<view class="quick-item" @click="handleAddPet">
<view class="quick-icon" style="background: linear-gradient(135deg, #F97316, #FB923C)">
<TnIcon name="add-lack" :size="44" color="#FFFFFF" />
</view>
<text class="quick-label">添加宠物</text>
</view>
<view class="quick-item" @click="handleAddRecord">
<view class="quick-icon" style="background: linear-gradient(135deg, #2DD4BF, #0D9488)">
<TnIcon name="write-feather" :size="44" color="#FFFFFF" />
</view>
<text class="quick-label">健康记录</text>
</view>
<view class="quick-item" @click="handleAddSchedule">
<view class="quick-icon" style="background: linear-gradient(135deg, #8B5CF6, #7C3AED)">
<TnIcon name="notice-fill" :size="44" color="#FFFFFF" />
</view>
<text class="quick-label">添加提醒</text>
</view>
<view class="quick-item" @click="handleNearbyHospital">
<view class="quick-icon" style="background: linear-gradient(135deg, #EC4899, #DB2777)">
<TnIcon name="safe-fill" :size="44" color="#FFFFFF" />
</view>
<text class="quick-label">附近医院</text>
</view>
</view>
</view>
<!-- 宠物管理 -->
<view class="section-block">
<text class="section-title">宠物管理</text>
<view class="menu-card">
<view class="menu-row" @click="handleGoMyPets">
<view class="menu-icon-wrap" style="background: #FFF7ED">
<TnIcon name="floral" :size="38" color="#F97316" />
</view>
<text class="menu-text">我的宠物</text>
<TnIcon name="right" :size="28" color="#CBD5E1" />
</view>
<view class="menu-divider" />
<view class="menu-row" @click="handleGoPetAlbum">
<view class="menu-icon-wrap" style="background: #FFF7ED">
<TnIcon name="eye-fill" :size="38" color="#FB923C" />
</view>
<text class="menu-text">宠物相册</text>
<TnIcon name="right" :size="28" color="#CBD5E1" />
</view>
</view>
</view>
<!-- 数据服务 -->
<view class="section-block">
<text class="section-title">数据服务</text>
<view class="menu-card">
<view class="menu-row" @click="handleGoHealthData">
<view class="menu-icon-wrap" style="background: #F0FDFA">
<TnIcon name="data-fill" :size="38" color="#14B8A6" />
</view>
<text class="menu-text">健康数据总览</text>
<text class="menu-badge">NEW</text>
<TnIcon name="right" :size="28" color="#CBD5E1" />
</view>
<view class="menu-divider" />
<view class="menu-row" @click="handleExportData">
<view class="menu-icon-wrap" style="background: #ECFEFF">
<TnIcon name="folder-upload" :size="38" color="#06B6D4" />
</view>
<text class="menu-text">数据导出</text>
<TnTag type="warning" size="sm" shape="round">会员</TnTag>
<TnIcon name="right" :size="28" color="#CBD5E1" />
</view>
<view class="menu-divider" />
<view class="menu-row" @click="handleCloudBackup">
<view class="menu-icon-wrap" style="background: #EFF6FF">
<TnIcon name="cloud-fill" :size="38" color="#3B82F6" />
</view>
<text class="menu-text">云端备份</text>
<text class="menu-extra">已备份</text>
<TnIcon name="right" :size="28" color="#CBD5E1" />
</view>
</view>
</view>
<!-- 会员服务 -->
<view class="section-block">
<text class="section-title">会员服务</text>
<view class="menu-card">
<view class="menu-row" @click="handleGoMember">
<view class="menu-icon-wrap" style="background: #FFFBEB">
<TnIcon name="vip-fill" :size="38" color="#F59E0B" />
</view>
<text class="menu-text">会员中心</text>
<TnTag type="danger" size="sm" shape="round">限时优惠</TnTag>
<TnIcon name="right" :size="28" color="#CBD5E1" />
</view>
<view class="menu-divider" />
<view class="menu-row" @click="handleShare">
<view class="menu-icon-wrap" style="background: #F5F3FF">
<TnIcon name="share" :size="38" color="#8B5CF6" />
</view>
<text class="menu-text">推荐给朋友</text>
<TnIcon name="right" :size="28" color="#CBD5E1" />
</view>
</view>
</view>
<!-- 其他 -->
<view class="section-block">
<text class="section-title">其他</text>
<view class="menu-card">
<view class="menu-row" @click="handleGoSettings">
<view class="menu-icon-wrap" style="background: #F8FAFC">
<TnIcon name="menu-set" :size="38" color="#475569" />
</view>
<text class="menu-text">设置</text>
<TnIcon name="right" :size="28" color="#CBD5E1" />
</view>
<view class="menu-divider" />
<view class="menu-row" @click="handleGoAbout">
<view class="menu-icon-wrap" style="background: #F8FAFC">
<TnIcon name="copyright" :size="38" color="#64748B" />
</view>
<text class="menu-text">关于毛球</text>
<TnIcon name="right" :size="28" color="#CBD5E1" />
</view>
<view class="menu-divider" />
<view class="menu-row" @click="handleFeedback">
<view class="menu-icon-wrap" style="background: #F8FAFC">
<TnIcon name="service" :size="38" color="#64748B" />
</view>
<text class="menu-text">帮助与反馈</text>
<TnIcon name="right" :size="28" color="#CBD5E1" />
</view>
<view class="menu-divider" />
<view class="menu-row" @click="handleGoLogin">
<view class="menu-icon-wrap" style="background: #FFF7ED">
<TnIcon name="my-circle" :size="38" color="#F97316" />
</view>
<text class="menu-text">登录 / 退出登录</text>
<TnIcon name="right" :size="28" color="#CBD5E1" />
</view>
</view>
</view>
<!-- 底部版权 -->
<view class="footer-section">
<TnIcon name="floral" :size="44" color="#F97316" />
<text class="footer-name">毛球手账 v1.0.0</text>
<text class="footer-slogan">让宠物健康管理像记账一样简单</text>
</view>
</view>
</scroll-view>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import TnIcon from '@/uni_modules/tuniaoui-vue3/components/icon/src/icon.vue'
interface UserInfo {
avatarUrl: string
nickName: string
membershipType: 'free' | 'premium'
memberExpireAt: string
}
const safeAreaBottom = ref(0)
const displayAvatarUrl = ref('')
const userInfo = reactive<UserInfo>({
avatarUrl: '',
nickName: '',
membershipType: 'free',
memberExpireAt: '',
})
const statsData = reactive({
petCount: 0,
recordCount: 0,
scheduleCount: 0,
})
onMounted(() => {
loadUserInfo()
loadStats()
calcSafeArea()
})
onShow(() => {
loadUserInfo()
loadStats()
})
function calcSafeArea() {
const systemInfo = uni.getSystemInfoSync()
safeAreaBottom.value = (systemInfo.safeAreaInsets?.bottom || 0) + 78
}
function formatDate(dateStr: string) {
if (!dateStr) return ''
const d = new Date(dateStr)
if (Number.isNaN(d.getTime())) return dateStr
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
async function loadUserInfo() {
try {
const uid = uni.getStorageSync('uid')
const res = await uniCloud.callFunction({
name: 'userManager',
data: { action: 'profile', uid },
})
if (res.result?.code === 0 && res.result.data) {
const data = res.result.data
userInfo.avatarUrl = data.avatarUrl || ''
userInfo.nickName = data.nickName || ''
userInfo.membershipType = data.membership?.type || 'free'
userInfo.memberExpireAt = data.membership?.expireAt || ''
await resolveMineAvatar()
return
}
} catch (e) {
console.warn('云函数加载用户信息失败,使用本地缓存', e)
}
const cached = uni.getStorageSync('userInfo')
if (cached) {
userInfo.avatarUrl = cached.avatarUrl || ''
userInfo.nickName = cached.nickName || '铲屎官'
userInfo.membershipType = cached.membership?.type || 'free'
userInfo.memberExpireAt = cached.membership?.expireAt || ''
await resolveMineAvatar()
}
}
async function resolveMineAvatar() {
const url = userInfo.avatarUrl
if (!url) {
displayAvatarUrl.value = ''
return
}
if (url.startsWith('cloud://')) {
try {
const res = await uniCloud.getTempFileURL({ fileList: [url] })
if (res.fileList && res.fileList[0]?.tempFileURL) {
displayAvatarUrl.value = res.fileList[0].tempFileURL
}
} catch (e) {
console.warn('获取头像临时URL失败', e)
displayAvatarUrl.value = ''
}
} else {
displayAvatarUrl.value = url
}
}
async function loadStats() {
try {
const uid = uni.getStorageSync('uid')
const res = await uniCloud.callFunction({
name: 'userManager',
data: { action: 'stats', uid },
})
if (res.result?.code === 0 && res.result.data) {
const data = res.result.data
statsData.petCount = data.petCount ?? 0
statsData.recordCount = data.recordCount ?? 0
statsData.scheduleCount = data.pendingScheduleCount ?? 0
}
} catch (e) {
console.warn('加载统计数据失败,使用默认值', e)
}
}
function handleEditProfile() {
uni.navigateTo({ url: '/pages/mine/profile-edit' })
}
function handleGoMember() {
uni.navigateTo({ url: '/pages/mine/member' })
}
function handleGoMyPets() {
uni.switchTab({ url: '/pages/index/index' })
}
function handleGoRecords() {
uni.switchTab({ url: '/pages/index/index' })
}
function handleGoSchedules() {
uni.switchTab({ url: '/pages/index/index' })
}
function handleAddPet() {
uni.navigateTo({ url: '/pages/pet/edit' })
}
function handleAddRecord() {
uni.navigateTo({ url: '/pages/record/type-select' })
}
function handleAddSchedule() {
uni.navigateTo({ url: '/pages/schedule/edit' })
}
function handleNearbyHospital() {
uni.navigateTo({ url: '/pages/discovery/hospital-list' })
}
function handleGoPetAlbum() {
uni.showToast({ title: '即将上线,敬请期待', icon: 'none' })
}
function handleGoHealthData() {
uni.showToast({ title: '即将上线,敬请期待', icon: 'none' })
}
function handleExportData() {
if (userInfo.membershipType !== 'premium') {
uni.showModal({
title: '会员功能',
content: '数据导出为高级会员专属功能,是否前往开通?',
success: (res) => {
if (res.confirm) {
handleGoMember()
}
},
})
return
}
uni.showToast({ title: '即将上线,敬请期待', icon: 'none' })
}
function handleCloudBackup() {
uni.showToast({ title: '数据已自动备份至云端', icon: 'none' })
}
function handleShare() {
uni.showToast({ title: '即将上线,敬请期待', icon: 'none' })
}
function handleGoSettings() {
uni.navigateTo({ url: '/pages/mine/settings' })
}
function handleGoAbout() {
uni.navigateTo({ url: '/pages/mine/about' })
}
function handleFeedback() {
uni.navigateTo({ url: '/pages/mine/feedback' })
}
function handleGoLogin() {
const token = uni.getStorageSync('token')
if (token) {
uni.showModal({
title: '提示',
content: '确定要退出当前账号吗?',
success: (res) => {
if (res.confirm) {
uni.removeStorageSync('token')
uni.removeStorageSync('uid')
uni.removeStorageSync('userInfo')
uni.reLaunch({ url: '/pages/login/login' })
}
},
})
} else {
uni.reLaunch({ url: '/pages/login/login' })
}
}
</script>
<style lang="scss" scoped>
$primary: #F97316;
$primary-light: #FB923C;
$primary-bg: #FFF7ED;
$text-dark: #1E293B;
$text-body: #475569;
$text-muted: #94A3B8;
$card-bg: #FFFFFF;
$border-color: #F1F5F9;
$shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
$radius-xl: 32rpx;
$radius-lg: 24rpx;
.mine-page {
height: 100vh;
background-color: #F8FAFC;
box-sizing: border-box;
}
.mine-container {
padding: 0 28rpx 28rpx;
}
// === 个人资料卡 ===
.profile-card {
position: relative;
margin-top: 24rpx;
border-radius: $radius-xl;
overflow: hidden;
box-shadow: 0 8rpx 32rpx rgba(249, 115, 22, 0.12);
}
.profile-bg {
position: absolute;
inset: 0;
background: linear-gradient(145deg, #F97316 0%, #FB923C 60%, #FDBA74 100%);
&::after {
content: '';
position: absolute;
top: -40rpx;
right: -60rpx;
width: 260rpx;
height: 260rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
}
&::before {
content: '';
position: absolute;
bottom: -30rpx;
left: -30rpx;
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.06);
}
}
.profile-content {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 48rpx 32rpx 40rpx;
gap: 16rpx;
}
.profile-avatar-wrap {
position: relative;
width: 160rpx;
height: 160rpx;
}
.avatar-img {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
border: 6rpx solid rgba(255, 255, 255, 0.6);
box-shadow: 0 8rpx 20rpx rgba(124, 45, 18, 0.2);
}
.avatar-placeholder {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
border: 6rpx solid rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.15);
display: flex;
align-items: center;
justify-content: center;
}
.avatar-edit-badge {
position: absolute;
bottom: 0;
right: 0;
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background: linear-gradient(135deg, #FB923C, #F97316);
border: 4rpx solid #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 8rpx rgba(124, 45, 18, 0.2);
}
.profile-name {
font-size: 40rpx;
font-weight: 800;
color: #FFFFFF;
line-height: 1.3;
text-shadow: 0 2rpx 8rpx rgba(124, 45, 18, 0.15);
}
.profile-member-row {
width: 100%;
margin-top: 8rpx;
}
.member-type-card {
display: flex;
align-items: center;
gap: 16rpx;
padding: 18rpx 24rpx;
background: rgba(255, 255, 255, 0.18);
backdrop-filter: blur(10rpx);
border-radius: 20rpx;
border: 1rpx solid rgba(255, 255, 255, 0.25);
}
.member-type-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4rpx;
}
.member-type-name {
font-size: 26rpx;
font-weight: 700;
color: #FFFFFF;
}
.member-type-hint {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.8);
}
// === 数据统计 ===
.stats-section {
display: flex;
gap: 16rpx;
margin-top: 24rpx;
}
.stats-card {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
padding: 24rpx 12rpx;
background-color: $card-bg;
border-radius: $radius-lg;
box-shadow: $shadow;
transition: transform 0.2s ease;
&:active {
transform: scale(0.97);
}
}
.stats-icon-wrap {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: $primary-bg;
}
.stats-value {
font-size: 40rpx;
font-weight: 800;
color: $text-dark;
line-height: 1;
}
.stats-label {
font-size: 22rpx;
font-weight: 500;
color: $text-muted;
}
// === 快捷操作 & 分区 ===
.section-block {
margin-top: 36rpx;
}
.section-title {
font-size: 30rpx;
font-weight: 800;
color: $text-dark;
margin-bottom: 16rpx;
padding-left: 4rpx;
}
.quick-grid {
display: flex;
gap: 16rpx;
}
.quick-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
padding: 24rpx 8rpx;
background-color: $card-bg;
border-radius: $radius-lg;
box-shadow: $shadow;
transition: transform 0.2s ease;
&:active {
transform: scale(0.96);
}
}
.quick-icon {
width: 80rpx;
height: 80rpx;
border-radius: 22rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.1);
}
.quick-label {
font-size: 22rpx;
font-weight: 600;
color: $text-body;
text-align: center;
}
// === 菜单卡片 ===
.menu-card {
background-color: $card-bg;
border-radius: $radius-lg;
box-shadow: $shadow;
overflow: hidden;
padding: 0 24rpx;
}
.menu-row {
display: flex;
align-items: center;
gap: 20rpx;
padding: 24rpx 0;
&:active {
opacity: 0.7;
}
}
.menu-divider {
height: 1rpx;
background: $border-color;
margin-left: 88rpx;
}
.menu-icon-wrap {
width: 64rpx;
height: 64rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.menu-text {
flex: 1;
font-size: 28rpx;
font-weight: 500;
color: $text-dark;
}
.menu-badge {
font-size: 18rpx;
font-weight: 700;
color: #FFFFFF;
background: linear-gradient(135deg, #F97316, #FB923C);
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
.menu-extra {
font-size: 24rpx;
color: #22C55E;
font-weight: 500;
}
// === 底部 ===
.footer-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
margin-top: 40rpx;
padding-bottom: 20rpx;
}
.footer-name {
font-size: 22rpx;
font-weight: 600;
color: $text-muted;
}
.footer-slogan {
font-size: 20rpx;
color: #CBD5E1;
}
</style>