AssetManager.UniApp/pages/me/me.vue
claw_bot 762665bfd5 feat: P3 功能增强完成
- 下拉刷新:index / strategies / me / detail 四个页面
- 上拉加载:detail 交易记录分页加载
- 空状态优化:index / strategies / detail 空状态提示
- pages.json 开启 enablePullDownRefresh

功能细节:
- onPullDownRefresh / onReachBottom 生命周期
- 交易记录分页逻辑(logPage / logHasMore / logLoading)
- 加载状态提示
2026-03-24 07:53:14 +00:00

268 lines
8.1 KiB
Vue
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

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="page-container">
<!-- 骨架屏 -->
<view v-if="loading" class="profile-header">
<view class="skeleton-avatar"></view>
<view class="skeleton-name"></view>
<view class="skeleton-info"></view>
</view>
<view v-if="loading" class="stats-grid">
<view class="stat-card" v-for="i in 4" :key="i">
<view class="skeleton-text skeleton-label"></view>
<view class="skeleton-text skeleton-value"></view>
</view>
</view>
<!-- 真实内容 -->
<view v-if="!loading" class="profile-header">
<view class="avatar-container">
<!-- 如果有头像URL使用图片显示 -->
<image v-if="userInfo.avatar" :src="userInfo.avatar" class="avatar-image" mode="aspectFill"></image>
<view v-else class="avatar-circle"></view>
<view class="online-badge"></view>
</view>
<text class="user-name">{{ userInfo.userName || '加载中...' }}</text>
<text class="user-info">会员等级: {{ userInfo.memberLevel || '普通会员' }} | 连续运行 {{ userInfo.runningDays || 0 }}天</text>
<text v-if="userInfo.email" class="user-email">{{ userInfo.email }}</text>
</view>
<view v-if="loading" class="stats-grid">
<view class="stat-card" v-for="i in 4" :key="i">
<view class="skeleton-text skeleton-label"></view>
<view class="skeleton-text skeleton-value"></view>
</view>
</view>
<view v-else class="stats-grid">
<view class="stat-card">
<text class="stat-label">已记录事件</text>
<text class="stat-val">{{ userStats.signalsCaptured?.toLocaleString() || 0 }}</text>
</view>
<view class="stat-card">
<text class="stat-label">胜率</text>
<text class="stat-val">{{ userStats.winRate || 0 }}%</text>
</view>
<view class="stat-card">
<text class="stat-label">总交易数</text>
<text class="stat-val">{{ userStats.totalTrades || 0 }}</text>
</view>
<view class="stat-card">
<text class="stat-label">平均收益</text>
<text class="stat-val">{{ userStats.averageProfit || 0 }}%</text>
</view>
</view>
<view class="menu-list">
<view class="menu-item">
<view class="flex-row items-center gap-3">
<uni-icons type="checkbox" size="20" color="#9CA3AF"></uni-icons>
<text class="menu-text">账户安全中心</text>
</view>
<uni-icons type="right" size="16" color="#D1D5DB"></uni-icons>
</view>
<view class="menu-item">
<view class="flex-row items-center gap-3">
<uni-icons type="gear" size="20" color="#9CA3AF"></uni-icons>
<text class="menu-text">全局执行偏好</text>
</view>
<uni-icons type="right" size="16" color="#D1D5DB"></uni-icons>
</view>
<view class="menu-item">
<view class="flex-row items-center gap-3">
<uni-icons type="staff" size="20" color="#EF4444"></uni-icons>
<text class="menu-text text-red">退出登录</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, getCurrentInstance } from 'vue';
import { onPullDownRefresh } from '@dcloudio/uni-app';
import type { UserInfoResponse, UserStatsResponse, ApiResponse } from '@/types';
// 加载状态
const loading = ref<boolean>(true);
// 用户信息
const userInfo = ref<UserInfoResponse>({
userName: '',
memberLevel: '',
runningDays: 0
});
// 用户统计数据
const userStats = ref<UserStatsResponse>({
signalsCaptured: 0,
winRate: 0,
totalTrades: 0,
averageProfit: 0
});
// 获取全局api对象
const getApi = () => {
const instance = getCurrentInstance();
return instance?.appContext.config.globalProperties.$api;
};
// 从后端API获取用户信息
const fetchUserInfo = async (): Promise<void> => {
try {
const api = getApi();
if (!api?.user) {
console.error('API模块未加载');
return;
}
const response: ApiResponse<UserInfoResponse> = await api.user.getUserInfo();
if (response.code === 200) {
userInfo.value = response.data;
}
} catch (error) {
console.error('获取用户信息失败:', error);
}
};
// 从后端API获取用户统计数据
const fetchUserStats = async (): Promise<void> => {
try {
const api = getApi();
if (!api?.user) {
console.error('API模块未加载');
return;
}
const response: ApiResponse<UserStatsResponse> = await api.user.getUserStats();
if (response.code === 200) {
userStats.value = response.data;
}
} catch (error) {
console.error('获取用户统计数据失败:', error);
}
};
// 页面加载时获取数据
onMounted(async () => {
loading.value = true;
await Promise.all([
fetchUserInfo(),
fetchUserStats()
]);
loading.value = false;
});
// 下拉刷新
onPullDownRefresh(async () => {
await Promise.all([
fetchUserInfo(),
fetchUserStats()
]);
uni.stopPullDownRefresh();
});
</script>
<style scoped>
.page-container {
min-height: 100vh;
padding: 40rpx;
padding-top: 100rpx;
background-color: #F9FAFB;
}
/* 骨架屏样式 */
.skeleton-avatar {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background: linear-gradient(90deg, #E5E7EB 25%, #F3F4F6 37%, #E5E7EB 50%);
background-size: 400% 100%;
animation: skeleton-loading 1.8s ease infinite;
margin-bottom: 24rpx;
}
.skeleton-name {
width: 200rpx;
height: 40rpx;
background: linear-gradient(90deg, #E5E7EB 25%, #F3F4F6 37%, #E5E7EB 50%);
background-size: 400% 100%;
animation: skeleton-loading 1.8s ease infinite;
border-radius: 8rpx;
margin-bottom: 16rpx;
}
.skeleton-info {
width: 300rpx;
height: 24rpx;
background: linear-gradient(90deg, #E5E7EB 25%, #F3F4F6 37%, #E5E7EB 50%);
background-size: 400% 100%;
animation: skeleton-loading 1.8s ease infinite;
border-radius: 8rpx;
}
.skeleton-text {
background: linear-gradient(90deg, #E5E7EB 25%, #F3F4F6 37%, #E5E7EB 50%);
background-size: 400% 100%;
animation: skeleton-loading 1.8s ease infinite;
border-radius: 8rpx;
}
.skeleton-label {
width: 100rpx;
height: 20rpx;
margin: 0 auto 8rpx;
}
.skeleton-value {
width: 80rpx;
height: 36rpx;
margin: 0 auto;
}
@keyframes skeleton-loading {
0% { background-position: 100% 50% }
100% { background-position: 0 50% }
}
.profile-header { display: flex; flex-direction: column; align-items: center; margin-bottom: 60rpx; }
.avatar-container { position: relative; margin-bottom: 24rpx; }
.avatar-circle { width: 160rpx; height: 160rpx; border-radius: 50%; background-color: #E5E7EB; border: 4rpx solid #fff; box-shadow: 0 10rpx 20rpx rgba(0,0,0,0.1); }
.avatar-image { width: 160rpx; height: 160rpx; border-radius: 50%; border: 4rpx solid #fff; box-shadow: 0 10rpx 20rpx rgba(0,0,0,0.1); }
.online-badge { width: 32rpx; height: 32rpx; background-color: #10B981; border: 4rpx solid #fff; border-radius: 50%; position: absolute; bottom: 8rpx; right: 8rpx; }
.user-name { font-size: 40rpx; font-weight: 700; color: #111827; }
.user-info { font-size: 24rpx; color: #9CA3AF; margin-top: 8rpx; }
.user-email { font-size: 20rpx; color: #6B7280; margin-top: 4rpx; }
.stats-grid { display: flex; gap: 32rpx; margin-bottom: 64rpx; }
.stat-card {
flex: 1;
background: #FFFFFF;
padding: 32rpx;
border-radius: 32rpx;
text-align: center;
border: 1rpx solid #E5E7EB;
box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.06);
transition: all 0.2s ease;
}
.stat-card:active {
transform: translateY(2rpx);
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.04);
}
.stat-label { font-size: 20rpx; color: #9CA3AF; display: block; margin-bottom: 8rpx; }
.stat-val { font-size: 36rpx; font-weight: 700; color: #111827; }
.menu-list {
background: #FFFFFF;
border-radius: 32rpx;
padding: 0 32rpx;
border: 1rpx solid #E5E7EB;
box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.06);
}
.menu-item { display: flex; justify-content: space-between; align-items: center; padding: 32rpx 0; border-bottom: 1rpx solid #F3F4F6; }
.menu-item:last-child { border-bottom: none; }
.menu-text { font-size: 28rpx; font-weight: 500; color: #374151; margin-left: 20rpx; }
.text-red { color: #EF4444; }
</style>