feat: 添加骨架屏加载状态
- detail.vue: 添加资产卡片骨架屏 - strategies.vue: 添加策略列表骨架屏 - me.vue: 添加用户信息和统计卡片骨架屏 - config.vue: 添加表单骨架屏 - 所有页面统一loading状态控制
This commit is contained in:
parent
2d986dd855
commit
12057dc019
@ -3,7 +3,26 @@
|
||||
<!-- uView Toast 组件 -->
|
||||
<u-toast ref="uToastRef" />
|
||||
|
||||
<view class="section-card">
|
||||
<!-- 骨架屏 -->
|
||||
<view v-if="loading" class="section-card">
|
||||
<view class="skeleton-form-item" v-for="i in 3" :key="i">
|
||||
<view class="skeleton-label"></view>
|
||||
<view class="skeleton-input"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading" class="section-card">
|
||||
<view class="skeleton-form-item" v-for="i in 2" :key="i">
|
||||
<view class="skeleton-row">
|
||||
<view class="skeleton-label-sm"></view>
|
||||
<view class="skeleton-input-sm"></view>
|
||||
<view class="skeleton-label-sm"></view>
|
||||
<view class="skeleton-input-sm"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 真实内容 -->
|
||||
<view v-else class="section-card">
|
||||
<view class="card-header">
|
||||
<view class="header-icon bg-emerald-100">
|
||||
<uni-icons type="settings" size="18" color="#064E3B"></uni-icons>
|
||||
@ -164,6 +183,9 @@ import { api } from '../../utils/api';
|
||||
const { proxy } = getCurrentInstance();
|
||||
const uToastRef = ref();
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
const strategies = ref([]);
|
||||
const strategyIndex = ref(-1);
|
||||
// 币种选择
|
||||
@ -401,7 +423,9 @@ const submitForm = async () => {
|
||||
|
||||
onShow(async () => {
|
||||
isFetching = true;
|
||||
loading.value = true;
|
||||
await fetchStrategies();
|
||||
loading.value = false;
|
||||
isFetching = false;
|
||||
});
|
||||
</script>
|
||||
@ -412,7 +436,60 @@ onShow(async () => {
|
||||
min-height: 100vh;
|
||||
background-color: #F3F4F6;
|
||||
padding-bottom: 200rpx;
|
||||
/* 给底部按钮留空 */
|
||||
}
|
||||
|
||||
/* 骨架屏样式 */
|
||||
.skeleton-form-item {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.skeleton-label {
|
||||
width: 120rpx;
|
||||
height: 26rpx;
|
||||
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-label-sm {
|
||||
width: 80rpx;
|
||||
height: 22rpx;
|
||||
background: linear-gradient(90deg, #E5E7EB 25%, #F3F4F6 37%, #E5E7EB 50%);
|
||||
background-size: 400% 100%;
|
||||
animation: skeleton-loading 1.8s ease infinite;
|
||||
border-radius: 6rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.skeleton-input {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
background: linear-gradient(90deg, #F9FAFB 25%, #F3F4F6 37%, #F9FAFB 50%);
|
||||
background-size: 400% 100%;
|
||||
animation: skeleton-loading 1.8s ease infinite;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.skeleton-input-sm {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
background: linear-gradient(90deg, #F9FAFB 25%, #F3F4F6 37%, #F9FAFB 50%);
|
||||
background-size: 400% 100%;
|
||||
animation: skeleton-loading 1.8s ease infinite;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.skeleton-row {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% { background-position: 100% 50% }
|
||||
100% { background-position: 0 50% }
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
|
||||
@ -3,8 +3,25 @@
|
||||
<!-- uView Toast 组件 -->
|
||||
<u-toast ref="uToastRef" />
|
||||
|
||||
<!-- 骨架屏:资产卡片 -->
|
||||
<view class="header-section" v-if="loading">
|
||||
<view class="skeleton-card">
|
||||
<view class="skeleton-row">
|
||||
<view class="skeleton-text skeleton-label"></view>
|
||||
<view class="skeleton-text skeleton-badge"></view>
|
||||
</view>
|
||||
<view class="skeleton-row">
|
||||
<view class="skeleton-text skeleton-big"></view>
|
||||
</view>
|
||||
<view class="skeleton-row skeleton-bottom">
|
||||
<view class="skeleton-text skeleton-stat"></view>
|
||||
<view class="skeleton-text skeleton-stat"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="header-section">
|
||||
<!-- 真实内容:资产卡片 -->
|
||||
<view class="header-section" v-else>
|
||||
<view class="asset-card">
|
||||
<view class="card-watermark">
|
||||
<uni-icons type="vip-filled" size="120" color="rgba(255,255,255,0.05)"></uni-icons>
|
||||
@ -367,6 +384,9 @@ import { api } from '../../utils/api';
|
||||
const { proxy } = getCurrentInstance();
|
||||
const uToastRef = ref();
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
// 获取货币符号
|
||||
const getCurrencySymbol = (currency) => {
|
||||
const symbols = {
|
||||
@ -541,8 +561,10 @@ const fetchTransactions = async () => {
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
await fetchPortfolioData();
|
||||
await fetchTransactions();
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const goStrategyConfig = () => {
|
||||
@ -712,9 +734,62 @@ const deletePortfolio = async () => {
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background-color: #F9FAFB;
|
||||
/* 关键:底部留出空间,防止内容被固定按钮遮挡 */
|
||||
padding-bottom: 180rpx;
|
||||
}
|
||||
|
||||
/* 骨架屏样式 */
|
||||
.skeleton-card {
|
||||
background-color: #064E3B;
|
||||
border-radius: 40rpx;
|
||||
padding: 40rpx 48rpx;
|
||||
min-height: 320rpx;
|
||||
}
|
||||
|
||||
.skeleton-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.skeleton-bottom {
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.skeleton-text {
|
||||
background: linear-gradient(90deg, rgba(255,255,255,0.1) 25%, rgba(255,255,255,0.2) 37%, rgba(255,255,255,0.1) 50%);
|
||||
background-size: 400% 100%;
|
||||
animation: skeleton-loading 1.8s ease infinite;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.skeleton-label {
|
||||
width: 160rpx;
|
||||
height: 28rpx;
|
||||
}
|
||||
|
||||
.skeleton-badge {
|
||||
width: 120rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.skeleton-big {
|
||||
width: 350rpx;
|
||||
height: 68rpx;
|
||||
}
|
||||
|
||||
.skeleton-stat {
|
||||
width: 150rpx;
|
||||
height: 36rpx;
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% { background-position: 100% 50% }
|
||||
100% { background-position: 0 50% }
|
||||
}
|
||||
|
||||
/* 工具类 */
|
||||
.flex-row { display: flex; flex-direction: row; }
|
||||
.flex-col { display: flex; flex-direction: column; }
|
||||
.items-center { align-items: center; }
|
||||
|
||||
@ -1,6 +1,20 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="profile-header">
|
||||
<!-- 骨架屏 -->
|
||||
<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>
|
||||
@ -12,7 +26,13 @@
|
||||
<text v-if="userInfo.email" class="user-email">{{ userInfo.email }}</text>
|
||||
</view>
|
||||
|
||||
<view class="stats-grid">
|
||||
<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>
|
||||
@ -61,6 +81,9 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, getCurrentInstance } from 'vue';
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
// 用户信息
|
||||
const userInfo = ref({
|
||||
userName: '',
|
||||
@ -124,10 +147,12 @@ const fetchUserStats = async () => {
|
||||
onMounted(async () => {
|
||||
console.log('个人中心页面加载,开始获取数据...');
|
||||
|
||||
loading.value = true;
|
||||
await Promise.all([
|
||||
fetchUserInfo(),
|
||||
fetchUserStats()
|
||||
]);
|
||||
loading.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -137,8 +162,62 @@ onMounted(async () => {
|
||||
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); }
|
||||
|
||||
@ -17,8 +17,24 @@
|
||||
</view>
|
||||
|
||||
<view class="strategy-list">
|
||||
<!-- 骨架屏 -->
|
||||
<view v-if="loading" class="strategy-card" v-for="i in 2" :key="'skeleton-' + i">
|
||||
<view class="skeleton-row">
|
||||
<view class="skeleton-icon"></view>
|
||||
<view class="skeleton-content">
|
||||
<view class="skeleton-text skeleton-title"></view>
|
||||
<view class="skeleton-text skeleton-tag"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="skeleton-text skeleton-desc"></view>
|
||||
<view class="skeleton-footer">
|
||||
<view class="skeleton-text skeleton-btn"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 真实内容 -->
|
||||
<view
|
||||
v-else
|
||||
class="strategy-card"
|
||||
v-for="(item, index) in strategies"
|
||||
:key="index"
|
||||
@ -67,6 +83,7 @@ import { onShow } from '@dcloudio/uni-app';
|
||||
const { proxy } = getCurrentInstance();
|
||||
const api = proxy.$api;
|
||||
|
||||
const loading = ref(true);
|
||||
const strategies = ref([]);
|
||||
|
||||
// 防止重复请求的标志
|
||||
@ -97,7 +114,9 @@ const fetchStrategies = async () => {
|
||||
|
||||
onShow(async () => {
|
||||
isFetching = true;
|
||||
loading.value = true;
|
||||
await fetchStrategies();
|
||||
loading.value = false;
|
||||
isFetching = false;
|
||||
});
|
||||
|
||||
@ -149,6 +168,67 @@ const handleAction = (item) => {
|
||||
}
|
||||
.add-btn-box:active { background-color: #F3F4F6; }
|
||||
|
||||
/* 骨架屏样式 */
|
||||
.skeleton-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.skeleton-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 20rpx;
|
||||
background: linear-gradient(90deg, #F1F2F4 25%, #e6e6e6 37%, #F1F2F4 50%);
|
||||
background-size: 400% 100%;
|
||||
animation: skeleton-loading 1.8s ease infinite;
|
||||
}
|
||||
|
||||
.skeleton-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.skeleton-text {
|
||||
background: linear-gradient(90deg, #F1F2F4 25%, #e6e6e6 37%, #F1F2F4 50%);
|
||||
background-size: 400% 100%;
|
||||
animation: skeleton-loading 1.8s ease infinite;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.skeleton-title {
|
||||
width: 200rpx;
|
||||
height: 32rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.skeleton-tag {
|
||||
width: 100rpx;
|
||||
height: 22rpx;
|
||||
}
|
||||
|
||||
.skeleton-desc {
|
||||
width: 100%;
|
||||
height: 60rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.skeleton-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.skeleton-btn {
|
||||
width: 120rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% { background-position: 100% 50% }
|
||||
100% { background-position: 0 50% }
|
||||
}
|
||||
|
||||
/* 策略卡片 */
|
||||
.strategy-card {
|
||||
background-color: #FFFFFF;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user