- 下拉刷新:index / strategies / me / detail 四个页面 - 上拉加载:detail 交易记录分页加载 - 空状态优化:index / strategies / detail 空状态提示 - pages.json 开启 enablePullDownRefresh 功能细节: - onPullDownRefresh / onReachBottom 生命周期 - 交易记录分页逻辑(logPage / logHasMore / logLoading) - 加载状态提示
268 lines
8.1 KiB
Vue
Executable File
268 lines
8.1 KiB
Vue
Executable File
<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>
|