AssetManager.UniApp/pages/me/me.vue
claw_bot fa2fa98985 feat: TypeScript 迁移完成
- 新增 tsconfig.json 配置
- 新增 types/ 目录(7个类型定义文件,与后端 DTO 对齐)
- 迁移 vite.config.js → vite.config.ts
- 迁移 main.js → main.ts
- 迁移 utils/api.js → utils/api.ts(泛型化请求封装)
- 迁移 utils/currency.js → utils/currency.ts
- 迁移 6 个 Vue 页面组件(添加 lang="ts" 和类型注解)
- 新增 TYPESCRIPT_MIGRATION.md 迁移计划文档
- 更新 todo.md 进度

收益:完整类型提示、编译时错误检查、重构安全性提升
2026-03-24 05:53:29 +00:00

258 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 lang="ts">
import { ref, onMounted, getCurrentInstance } from 'vue';
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;
});
</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>