在多个页面中通过骨架屏优化加载状态。

在章节、礼物代付详情、阅读和搜索结果页面,用骨架屏替换传统加载指示器,以提升数据获取过程中的用户体验。
更新骨架屏样式,使加载状态更加美观。
实现章节和配置信息的缓存策略,以优化性能并减少冷启动问题。
This commit is contained in:
Alex-larget
2026-03-18 12:56:34 +08:00
parent 1fa20756a8
commit 46f94a9c81
23 changed files with 841 additions and 138 deletions

View File

@@ -17,10 +17,21 @@
<!-- 导航栏占位 -->
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
<!-- 目录加载中 -->
<view class="parts-loading" wx:if="{{partsLoading}}">
<view class="parts-loading-spinner"></view>
<text class="parts-loading-text">加载目录中...</text>
<!-- 目录骨架屏:加载中时展示 -->
<view class="parts-skeleton" wx:if="{{partsLoading}}">
<view class="skeleton-book-card">
<view class="skeleton-book-icon"></view>
<view class="skeleton-book-info">
<view class="skeleton-line skeleton-title"></view>
<view class="skeleton-line skeleton-subtitle"></view>
</view>
<view class="skeleton-count"></view>
</view>
<view class="skeleton-part-list">
<view class="skeleton-part-item" wx:for="{{[1,2,3,4,5]}}" wx:key="*this">
<view class="skeleton-part-header"></view>
</view>
</view>
</view>
<!-- 书籍信息卡 -->

View File

@@ -75,32 +75,75 @@
width: 100%;
}
/* ===== 目录加载中 ===== */
.parts-loading {
/* ===== 目录骨架屏 ===== */
.parts-skeleton {
padding: 32rpx;
}
.skeleton-book-card {
display: flex;
align-items: center;
gap: 24rpx;
padding: 32rpx;
background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%);
border-radius: 32rpx;
margin-bottom: 32rpx;
}
.skeleton-book-icon {
width: 96rpx;
height: 96rpx;
border-radius: 24rpx;
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
flex-shrink: 0;
}
.skeleton-book-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.skeleton-line {
height: 32rpx;
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
border-radius: 8rpx;
}
.skeleton-title { width: 70%; }
.skeleton-subtitle { width: 50%; }
.skeleton-count {
width: 80rpx;
height: 64rpx;
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
border-radius: 16rpx;
}
.skeleton-part-list {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
gap: 24rpx;
}
.parts-loading-spinner {
width: 64rpx;
height: 64rpx;
border: 6rpx solid rgba(255, 255, 255, 0.1);
border-top-color: #00CED1;
border-radius: 50%;
animation: parts-spin 0.8s linear infinite;
.skeleton-part-item .skeleton-part-header {
height: 100rpx;
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
border-radius: 16rpx;
}
.parts-loading-text {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.5);
}
@keyframes parts-spin {
to { transform: rotate(360deg); }
@keyframes skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* ===== 书籍信息卡 ===== */

View File

@@ -14,9 +14,20 @@
<view class="content" style="padding-top: calc({{statusBarHeight}}px + 88rpx);">
<block wx:if="{{loading}}">
<view class="loading-box">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
<view class="skeleton-wrap">
<view class="skeleton-hero">
<view class="skeleton-hero-badge"></view>
<view class="skeleton-hero-title"></view>
<view class="skeleton-hero-desc"></view>
<view class="skeleton-hero-amount"></view>
</view>
<view class="skeleton-card">
<view class="skeleton-avatar"></view>
<view class="skeleton-info">
<view class="skeleton-line"></view>
<view class="skeleton-line short"></view>
</view>
</view>
</view>
</block>
<block wx:elif="{{detail}}">

View File

@@ -56,32 +56,97 @@
padding: 24rpx 24rpx 200rpx;
}
/* 加载 */
.loading-box {
/* 骨架屏 */
.skeleton-wrap {
padding: 24rpx 0;
}
.skeleton-hero {
background: rgba(24, 24, 27, 0.8);
border-radius: 32rpx;
padding: 40rpx;
margin-bottom: 32rpx;
}
.skeleton-hero-badge {
width: 120rpx;
height: 40rpx;
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
border-radius: 8rpx;
margin-bottom: 24rpx;
}
.skeleton-hero-title {
width: 80%;
height: 48rpx;
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
border-radius: 8rpx;
margin-bottom: 16rpx;
}
.skeleton-hero-desc {
width: 60%;
height: 32rpx;
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
border-radius: 8rpx;
margin-bottom: 32rpx;
}
.skeleton-hero-amount {
width: 200rpx;
height: 64rpx;
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
border-radius: 12rpx;
}
.skeleton-card {
display: flex;
align-items: center;
gap: 24rpx;
padding: 32rpx;
background: rgba(24, 24, 27, 0.6);
border-radius: 24rpx;
}
.skeleton-avatar {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
flex-shrink: 0;
}
.skeleton-info {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
gap: 16rpx;
}
.loading-spinner {
width: 48rpx;
height: 48rpx;
border: 4rpx solid rgba(20, 184, 166, 0.2);
border-top-color: #14b8a6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
.skeleton-info .skeleton-line {
height: 32rpx;
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
border-radius: 8rpx;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.skeleton-info .skeleton-line { width: 70%; }
.skeleton-info .skeleton-line.short { width: 45%; }
.loading-text {
margin-top: 24rpx;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.45);
@keyframes skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* 产品 Hero 卡片 */

View File

@@ -52,19 +52,6 @@
<view class="banner-action"><text class="banner-action-text">开始阅读</text><view class="banner-arrow">→</view></view>
</view>
<!-- 阅读进度(设计稿:最新更新→阅读进度→超级个体) -->
<view class="progress-card" wx:if="{{isLoggedIn}}" bindtap="goToChapters">
<view class="progress-header">
<text class="progress-title">阅读进度</text>
<text class="progress-count">已读 {{readCount}}/{{totalSections}}</text>
</view>
<view class="progress-bar-wrapper">
<view class="progress-bar-bg">
<view class="progress-bar-fill" style="width: {{readCount && totalSections ? (readCount / totalSections * 100) : 0}}%;"></view>
</view>
</view>
</view>
<!-- 超级个体(横向滚动,已去掉「查看全部」;审核模式隐藏) -->
<view class="section" wx:if="{{!auditMode}}">
<view class="section-header">

View File

@@ -4,9 +4,7 @@
<!-- 自定义导航栏 -->
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
<view class="nav-content">
<view class="nav-settings" bindtap="openSettings">
<text class="settings-icon">⚙️</text>
</view>
<view class="nav-left-placeholder"></view>
<text class="nav-title">找伙伴</text>
<view class="nav-right-placeholder"></view>
</view>

View File

@@ -27,15 +27,9 @@
padding: 0 32rpx;
}
.nav-settings {
.nav-left-placeholder {
width: 80rpx;
height: 80rpx;
flex-shrink: 0;
border-radius: 50%;
background: #1c1c1e;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
@@ -51,10 +45,6 @@
flex-shrink: 0;
}
.settings-icon {
font-size: 36rpx;
}
.nav-placeholder {
width: 100%;
}

View File

@@ -24,8 +24,26 @@
<!-- 阅读内容 -->
<view class="read-content">
<!-- 章节标题 -->
<view class="chapter-header">
<!-- 骨架屏:加载中时展示,模拟章节标题+正文布局 -->
<view class="skeleton-wrap" wx:if="{{accessState === 'unknown' && loading}}">
<view class="skeleton-header">
<view class="skeleton-meta"></view>
<view class="skeleton-title"></view>
</view>
<view class="skeleton-lines">
<view class="skeleton skeleton-1"></view>
<view class="skeleton skeleton-2"></view>
<view class="skeleton skeleton-3"></view>
<view class="skeleton skeleton-4"></view>
<view class="skeleton skeleton-5"></view>
<view class="skeleton skeleton-6"></view>
<view class="skeleton skeleton-7"></view>
<view class="skeleton skeleton-8"></view>
</view>
</view>
<!-- 章节标题(加载完成后) -->
<view class="chapter-header" wx:elif="{{!loading}}">
<view class="chapter-meta">
<text class="chapter-id">{{section.id}}</text>
<text class="tag tag-free" wx:if="{{section.isFree}}">免费</text>
@@ -33,15 +51,6 @@
<text class="chapter-title" user-select>{{section.title}}</text>
</view>
<!-- 加载状态 -->
<view class="loading-state" wx:if="{{accessState === 'unknown' && loading}}">
<view class="skeleton skeleton-1"></view>
<view class="skeleton skeleton-2"></view>
<view class="skeleton skeleton-3"></view>
<view class="skeleton skeleton-4"></view>
<view class="skeleton skeleton-5"></view>
</view>
<!-- 完整内容 - 免费或已购买(支持 @ mention / #linkTag / 图片) -->
<view class="article" wx:if="{{accessState === 'free' || accessState === 'unlocked_purchased'}}">
<view class="paragraph" wx:for="{{contentSegments}}" wx:key="index">

View File

@@ -144,8 +144,35 @@
line-height: 1.4;
}
/* ===== 加载状态 ===== */
.loading-state {
/* ===== 骨架屏 ===== */
.skeleton-wrap {
padding-top: 24rpx;
}
.skeleton-header {
margin-bottom: 40rpx;
}
.skeleton-meta {
width: 120rpx;
height: 48rpx;
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
border-radius: 32rpx;
margin-bottom: 24rpx;
}
.skeleton-title {
width: 85%;
height: 52rpx;
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
border-radius: 8rpx;
}
.skeleton-lines {
display: flex;
flex-direction: column;
gap: 32rpx;
@@ -164,6 +191,9 @@
.skeleton-3 { width: 65%; }
.skeleton-4 { width: 85%; }
.skeleton-5 { width: 70%; }
.skeleton-6 { width: 80%; }
.skeleton-7 { width: 60%; }
.skeleton-8 { width: 88%; }
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
@@ -439,21 +469,24 @@
.action-row-inline {
display: flex;
flex-wrap: nowrap;
gap: 16rpx;
}
.action-btn-inline {
flex: 1;
flex: 1 1 0;
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
padding: 24rpx 16rpx;
padding: 24rpx 12rpx;
border-radius: 16rpx;
border: none;
background: transparent;
line-height: normal;
box-sizing: border-box;
overflow: hidden;
}
.action-btn-inline::after {
@@ -473,12 +506,18 @@
.action-icon-small {
font-size: 28rpx;
flex-shrink: 0;
}
.action-text-small {
font-size: 24rpx;
color: #ffffff;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.share-tip-inline {

View File

@@ -65,10 +65,15 @@
<!-- 搜索结果 -->
<view class="results-section" wx:if="{{searched}}">
<!-- 加载中 -->
<view class="loading-wrap" wx:if="{{loading}}">
<view class="loading-spinner"></view>
<text class="loading-text">搜索中...</text>
<!-- 搜索结果骨架屏 -->
<view class="skeleton-results" wx:if="{{loading}}">
<view class="skeleton-result-item" wx:for="{{[1,2,3,4,5]}}" wx:key="*this">
<view class="skeleton-result-rank"></view>
<view class="skeleton-result-content">
<view class="skeleton-result-title"></view>
<view class="skeleton-result-meta"></view>
</view>
</view>
</view>
<!-- 结果列表 -->

View File

@@ -284,30 +284,57 @@
}
/* 加载状态 */
.loading-wrap {
/* 搜索结果骨架屏 */
.skeleton-results {
padding: 24rpx 0;
}
.skeleton-result-item {
display: flex;
align-items: center;
gap: 24rpx;
padding: 28rpx 0;
border-bottom: 1rpx solid rgba(255,255,255,0.06);
}
.skeleton-result-rank {
width: 56rpx;
height: 56rpx;
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
border-radius: 12rpx;
flex-shrink: 0;
}
.skeleton-result-content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx 0;
gap: 16rpx;
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid rgba(0, 206, 209, 0.3);
border-top-color: #00CED1;
border-radius: 50%;
animation: spin 1s linear infinite;
.skeleton-result-title {
width: 85%;
height: 36rpx;
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
border-radius: 8rpx;
}
@keyframes spin {
to { transform: rotate(360deg); }
.skeleton-result-meta {
width: 50%;
height: 28rpx;
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s ease-in-out infinite;
border-radius: 8rpx;
}
.loading-text {
margin-top: 24rpx;
font-size: 28rpx;
color: rgba(255,255,255,0.5);
@keyframes skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* 空状态 */