AssetManager.UniApp/pages/me/me.vue
claw_bot 12057dc019 feat: 添加骨架屏加载状态
- detail.vue: 添加资产卡片骨架屏
- strategies.vue: 添加策略列表骨架屏
- me.vue: 添加用户信息和统计卡片骨架屏
- config.vue: 添加表单骨架屏
- 所有页面统一loading状态控制
2026-03-13 13:42:15 +00:00

260 lines
7.9 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>
import { ref, onMounted, getCurrentInstance } from 'vue';
// 加载状态
const loading = ref(true);
// 用户信息
const userInfo = ref({
userName: '',
memberLevel: '',
runningDays: 0
});
// 用户统计数据
const userStats = ref({
signalsCaptured: 0,
winRate: 0
});
// 获取全局api对象
const getApi = () => {
const instance = getCurrentInstance();
return instance?.appContext.config.globalProperties.$api;
};
// 从后端API获取用户信息的函数
const fetchUserInfo = async () => {
try {
const api = getApi();
if (!api || !api.user) {
console.error('API模块未加载');
return;
}
const response = await api.user.getUserInfo();
if (response.code === 200) {
userInfo.value = response.data;
console.log('用户信息获取成功:', response.data);
}
} catch (error) {
console.error('获取用户信息失败:', error);
}
};
// 从后端API获取用户统计数据的函数
const fetchUserStats = async () => {
try {
const api = getApi();
if (!api || !api.user) {
console.error('API模块未加载');
return;
}
const response = await api.user.getUserStats();
if (response.code === 200) {
userStats.value = response.data;
console.log('用户统计数据获取成功:', response.data);
}
} catch (error) {
console.error('获取用户统计数据失败:', error);
}
};
// 页面加载时获取数据
onMounted(async () => {
console.log('个人中心页面加载,开始获取数据...');
loading.value = true;
await Promise.all([
fetchUserInfo(),
fetchUserStats()
]);
loading.value = false;
});
</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>