fix:存储空间页加载状态优化(进度条实时计算 + 详情页加载动画)

- 列表页进度条三段改为数据就绪(deviceTotalBytes>0)后才渲染,修复加载中 CSS min-width:2% 强制显示假蓝段的问题;占用百分比同步 gate,不再显示 0.00%
- 详情页加载动画与列表页一致:首屏转圈、数字位 loading 显示 spinner、加载中禁用清理按钮

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
zheng020 2026-08-03 18:27:12 +08:00
parent 7dc9aef5be
commit a6859a67d3
2 changed files with 91 additions and 28 deletions

View File

@ -6,21 +6,25 @@
<text v-if="category.description" class="header-desc">{{ category.description }}</text>
</view>
<!-- 加载占位 -->
<!-- 加载占位与列表页 cache-cleanup.vue 一致的 spinner 动画 -->
<view v-if="loading && !category.label" class="loading">
<text>加载中...</text>
<view class="value-spinner spinner-hero"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 简单模板preload / progress / sandbox-tmp / others -->
<view v-else-if="breakdown === null" class="simple-body">
<view class="big-stat">
<text class="big-value">{{ formatSize(category.sizeBytes) }}</text>
<text class="big-count"> {{ category.keyCount }} 项缓存</text>
<!-- 与列表页一致loading 时数字位置显示 spinner刷新/重新扫描时生效 -->
<view v-if="loading" class="value-spinner spinner-hero"></view>
<text v-else class="big-value">{{ formatSize(category.sizeBytes) }}</text>
<view v-if="loading" class="value-spinner spinner-sm"></view>
<text v-else class="big-count"> {{ category.keyCount }} 项缓存</text>
</view>
<view class="action-wrap">
<button
class="clean-btn"
:disabled="category.sizeBytes <= 0"
:disabled="loading || category.sizeBytes <= 0"
@click="confirmSimple"
>
@ -45,13 +49,17 @@
<text class="group-icon">{{ g.isCurrent ? '👤' : (g.global ? '🌐' : '👥') }}</text>
<text class="group-name">{{ g.displayUid }}</text>
</view>
<text class="group-size">{{ formatSize(g.sizeBytes) }} · {{ g.keyCount }} </text>
<view class="group-size">
<!-- 与列表页一致loading 时行内数字显示 spinner -->
<view v-if="loading" class="value-spinner spinner-sm"></view>
<text v-else>{{ formatSize(g.sizeBytes) }} · {{ g.keyCount }} </text>
</view>
<text v-if="g.warning" class="group-warn"></text>
</view>
<button
class="clean-btn small"
:class="{ 'strong': !g.isCurrent && !g.global }"
:disabled="!g.canClean"
:disabled="loading || !g.canClean"
@click="confirmGroup(g)"
>
{{ g.canClean ? '清 理' : disabledLabel(g.disabledReason) }}
@ -117,17 +125,17 @@ async function load() {
// - getCategoryBreakdown 1min TTLM2 < 50ms
// - handler 4s + breakdown walk 4s = 8s
// - / category
// 8s 4sM3 HANDLER_TIMEOUT_MS 4s
// 2026-08-03 4s 8sliberate
let info = null
let br = null
try {
info = await Promise.race([
getCacheInfo(),
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 4000)),
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 8000)),
])
br = await Promise.race([
getCategoryBreakdown(id.value),
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 4000)),
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 8000)),
])
const meta = (info.categories || []).find((c) => c.id === id.value)
if (meta) {
@ -337,6 +345,43 @@ function disabledLabel(reason) {
color: #999;
font-size: 28rpx;
}
.loading-text {
display: block;
margin-top: 16rpx;
color: #999;
font-size: 28rpx;
}
/* ── 加载 spinner与列表页 cache-cleanup.vue 一致的动画) ── */
.value-spinner {
display: inline-block;
border-style: solid;
border-color: #e6e6e6;
border-top-color: #1890ff;
border-radius: 50%;
vertical-align: middle;
animation: value-spin 0.8s linear infinite;
-webkit-animation: value-spin 0.8s linear infinite;
}
.spinner-hero {
/* big-value 数字位:占位高度 ≈ 72rpx 字号(与列表页一致的 margin 防布局抖动) */
width: 56rpx;
height: 56rpx;
border-width: 6rpx;
margin: 8rpx 0 24rpx;
}
.spinner-sm {
/* big-count / group-size内联在 24-26rpx 文字旁 */
width: 20rpx;
height: 20rpx;
border-width: 2rpx;
}
@keyframes value-spin {
to { transform: rotate(360deg); }
}
@-webkit-keyframes value-spin {
to { -webkit-transform: rotate(360deg); }
}
/* 简单模板 */
.simple-body {

View File

@ -16,22 +16,28 @@
<!-- 嵌套进度条Topfans 已用空间 其他 app 使用空间 剩余可用空间绿 -->
<view class="progress-bar">
<!-- 绿段剩余可用空间贴在右侧 -->
<view
class="seg-available"
:style="{ width: availablePct + '%' }"
></view>
<!-- 蓝段Topfans 已用空间从左 -->
<view
class="seg-topfans"
:style="{ width: topfansPct + '%' }"
></view>
<!-- 橙段其他 app 使用空间从蓝段"视觉右边界"开始用视觉宽度而非真实百分比
避免 0.04%2% 区间被橙段覆盖导致蓝段消失 -->
<view
class="seg-other"
:style="{ left: topfansVisualPct + '%', width: otherPct + '%' }"
></view>
<!-- 2026-08-03 三段只在真实数据就绪后渲染
加载中 info 为全 0 .seg-topfans CSS min-width:2% 会把蓝段强制显示成 2%
造成"数据未加载出来进度条先有值" hasDeviceData 统一 gate
加载中/加载失败时进度条只显示灰色轨道加载完再按计算值渲染 -->
<template v-if="hasDeviceData">
<!-- 绿段剩余可用空间贴在右侧 -->
<view
class="seg-available"
:style="{ width: availablePct + '%' }"
></view>
<!-- 蓝段Topfans 已用空间从左 -->
<view
class="seg-topfans"
:style="{ width: topfansPct + '%' }"
></view>
<!-- 橙段其他 app 使用空间从蓝段"视觉右边界"开始用视觉宽度而非真实百分比
避免 0.04%2% 区间被橙段覆盖导致蓝段消失 -->
<view
class="seg-other"
:style="{ left: topfansVisualPct + '%', width: otherPct + '%' }"
></view>
</template>
</view>
<view class="chips">
@ -64,7 +70,9 @@
</view>
</view>
<text class="hero-percent"
<!-- 与进度条同一 class bug加载中 deviceUsagePercent=0 会显示"占设备 0.00%"
同样用 hasDeviceData gate数据就绪前不渲染 -->
<text v-if="hasDeviceData" class="hero-percent"
>占设备 {{ info.deviceUsagePercent.toFixed(2) }}% 存储空间</text
>
</view>
@ -176,6 +184,15 @@ const availablePct = computed(() => {
return Math.max(0, 100 - topfansVisualPct.value - otherPct.value);
});
// 2026-08-03 ""
// loading info INITIAL_INFOdeviceTotalBytes=0 topfansPct=0
// .seg-topfans CSS min-width:2% 2%
// hasDeviceData gate +
//
const hasDeviceData = computed(
() => !loading.value && info.value.deviceTotalBytes > 0
);
// APP-PLUS
// #ifdef APP-PLUS
const isIos = (() => {
@ -202,7 +219,8 @@ async function load(force = false) {
const result = await Promise.race([
getCacheInfo(force),
new Promise((_, rej) =>
setTimeout(() => rej(new Error("timeout")), 8000),
// 2026-08-03 8s 20sliberate getSandboxTotalSize
setTimeout(() => rej(new Error("timeout")), 20000),
),
]);
info.value = result;