AssetManager.UniApp/pages/me/me.vue
claw_bot b9f9c8c9f6 feat: P6 UI/UX 优化完成
P1:
- 收益曲线 Y 轴刻度(5档刻度线+标签)

P2:
- 交易记录时间格式合并为一行
- 组合名称长度限制(max-width + ellipsis)
- 去掉无意义标签("账本追踪中"、"NV")

P3:
- 我的页面会员等级改为运行天数

改进:
- 信息密度提升
- 图表可读性增强
- 去除冗余信息
2026-03-24 08:19:33 +00:00

268 lines
8.0 KiB
Vue
Executable File
Raw Permalink 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.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>