topfans/frontend/pages/profile/cache-cleanup.vue
2026-07-30 15:07:56 +08:00

438 lines
10 KiB
Vue
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="cache-cleanup">
<!-- Hero 卡片核心占用展示 -->
<view class="hero-card">
<!-- iOS 受沙盒限制无法读 .app bundle 大小提示用户到系统设置查看真实值 -->
<!-- #ifdef APP-PLUS -->
<view v-if="isIos" class="hero-tip"
>准确值请查看 iPhone「设置-通用-iPhone 储存空间」</view
>
<!-- #endif -->
<text class="hero-label">Topfans 已用空间</text>
<text class="hero-value">{{ formatSize(info.appUsedBytes) }}</text>
<!-- 嵌套进度条Topfans 已用空间(蓝)→ 内存缓存空间(紫,叠在蓝内)→ 总内存空间剩余(绿) -->
<view class="progress-bar">
<!-- 绿段:剩余配额(贴在右侧) -->
<view
class="seg-available"
:style="{ width: availablePct + '%' }"
></view>
<!-- 蓝段:完整 Topfans 已用(从左) -->
<view
class="seg-app"
:style="{ width: appPct + '%' }"
></view>
<!-- 紫段:缓存(从左、叠在蓝段内;不超过蓝段右边界) -->
<view
class="seg-cache"
:style="{ width: cachePct + '%' }"
></view>
</view>
<view class="chips">
<view class="chip">
<view class="chip-dot dot-app"></view>
<text>Topfans 已用空间</text>
</view>
<view class="chip">
<view class="chip-dot dot-cache"></view>
<text>内存缓存空间</text>
</view>
<view class="chip">
<view class="chip-dot dot-available"></view>
<text>总内存空间</text>
</view>
</view>
<text class="hero-percent"
>占手机 {{ info.usagePercent.toFixed(1) }}% 存储空间</text
>
</view>
<!-- 缓存分类卡片 -->
<view class="section-card">
<text class="section-title">缓存分类</text>
<!-- 全局空状态:失败时不显示(避免冲突显示) -->
<view
v-if="
!loadFailed &&
info.totalBytes === 0 &&
info.categories.every((c) => c.sizeBytes <= 0)
"
class="empty"
>
<view class="empty-icon">✓</view>
<text class="empty-text">当前无缓存可清理</text>
</view>
<view
v-for="(cat, index) in info.categories"
:key="cat.id"
class="row"
:class="{ 'row-last': index === info.categories.length - 1 }"
@click="goDetail(cat.id)"
>
<view class="row-main">
<text class="row-label">{{ cat.label }}</text>
<text v-if="cat.warning" class="row-warn">⚠ 按账号分组清理</text>
</view>
<view class="row-sub">
<text v-if="cat.sizeBytes === -1" class="error">? · 加载失败</text>
<text v-else class="size"
>{{ formatSize(cat.sizeBytes) }} · {{ cat.keyCount }} 项</text
>
<text class="chevron"></text>
</view>
</view>
</view>
<!-- "其他" 卡片 -->
<view class="section-card others-section">
<text class="section-title">其他</text>
<view class="row others-row row-last">
<view class="row-main">
<text class="row-label">其他</text>
<text class="row-desc"
>包含运行 Topfans 的必要数据、账号会话、其他账号的数据等</text
>
</view>
<text class="size">{{ formatSize(info.othersBytes) }}</text>
</view>
</view>
<!-- 整体加载失败状态reviewer P0 修正加点击重试 -->
<view
v-if="loadFailed && info.totalBytes === 0"
class="load-failed"
@click="load"
>
<text>加载失败点此重试</text>
</view>
</view>
</template>
<script setup>
import { ref, computed } from "vue";
import { onPullDownRefresh, onShow } from "@dcloudio/uni-app";
import { getCacheInfo, formatSize } from "@/utils/cacheManager";
const info = ref({
appUsedBytes: 0,
quotaTotalBytes: 0,
quotaAvailableBytes: 0,
usagePercent: 0,
othersBytes: 0,
totalBytes: 0,
categories: [],
});
const loadFailed = ref(false);
// ── 进度条三段宽度(基于 quotaTotalBytes = 100%)──
// 三段相加 ≤ 100%,不出现负数;任一字段缺失时安全降级为 0
// 语义:蓝 = Topfans 全部已用;紫 = 蓝内的缓存(必须 ≤ 蓝);绿 = 蓝右侧剩余
const appPct = computed(() => {
const total = info.value.quotaTotalBytes;
if (!total) return 0;
return Math.min(100, Math.max(0, (info.value.appUsedBytes / total) * 100));
});
const cachePct = computed(() => {
const total = info.value.quotaTotalBytes;
if (!total) return 0;
// 缓存必须落在 Topfans 已用区间内cache 是 used 的子集)
const cache = Math.min(info.value.totalBytes, info.value.appUsedBytes);
return Math.min(appPct.value, Math.max(0, (cache / total) * 100));
});
const availablePct = computed(() => {
const total = info.value.quotaTotalBytes;
if (!total) return 0;
return Math.max(0, 100 - appPct.value);
});
// ── 平台判断(仅 APP-PLUS 编译时计算)──
// #ifdef APP-PLUS
const isIos = (() => {
try {
return uni.getSystemInfoSync().platform === "ios";
} catch (e) {
return false;
}
})();
// #endif
async function load() {
loadFailed.value = false;
try {
const result = await Promise.race([
getCacheInfo(),
new Promise((_, rej) =>
setTimeout(() => rej(new Error("timeout")), 5000),
),
]);
info.value = result;
} catch (e) {
console.warn("[cache-cleanup] load failed:", e.message);
loadFailed.value = true;
}
}
function goDetail(id) {
const cat = info.value.categories.find((c) => c.id === id);
if (!cat || cat.sizeBytes <= 0) {
uni.showToast({ title: "该分类暂无缓存", icon: "none" });
return;
}
uni.navigateTo({ url: `/pages/profile/cache-cleanup-detail?id=${id}` });
}
// reviewer P0使用 onShow 统一触发 load进页面 + 从详情页返回时都触发)
// 不再使用 onMounted 避免重复触发
onShow(load);
onPullDownRefresh(async () => {
await load();
uni.stopPullDownRefresh();
});
</script>
<style scoped>
/* ── 容器 ── */
.cache-cleanup {
padding: 24rpx 24rpx 40rpx;
background: #f5f7fa;
min-height: 100vh;
box-sizing: border-box;
}
/* ── Hero 卡片 ── */
.hero-card {
background: #fff;
border-radius: 16rpx;
padding: 40rpx 32rpx 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.hero-label {
font-size: 28rpx;
color: #8c8c8c;
display: block;
}
.hero-value {
font-size: 88rpx;
font-weight: 700;
color: #1890ff;
letter-spacing: -1rpx;
display: block;
margin: 8rpx 0 24rpx;
line-height: 1.1;
}
.hero-percent {
font-size: 24rpx;
color: #8c8c8c;
display: block;
margin-top: 24rpx;
}
.hero-tip {
display: flex;
align-items: center;
font-size: 24rpx;
color: #faad14;
background: #fffbe6;
border-left: 4rpx solid #faad14;
border-radius: 8rpx;
padding: 16rpx 20rpx;
margin-bottom: 24rpx;
line-height: 1.5;
}
/* ── 进度条 ── */
.progress-bar {
position: relative;
height: 16rpx;
background: #f5f7fa;
border-radius: 8rpx;
overflow: hidden;
}
/* 三段都用 absolute 定位(蓝、紫从左贴齐;绿从右贴齐),
紫叠在蓝之上 → 视觉上"缓存占用 Topfans 已用的一部分" */
.seg-app,
.seg-cache,
.seg-available {
position: absolute;
top: 0;
height: 100%;
min-width: 0;
box-shadow: inset 0 0 0 1rpx rgba(255, 255, 255, 0.2);
}
.seg-app {
left: 0;
background: #1890ff;
}
.seg-cache {
left: 0;
background: #722ed1;
}
.seg-available {
right: 0;
background: #52c41a;
}
/* ── 图例 chips ── */
.chips {
display: flex;
gap: 12rpx;
margin-top: 20rpx;
}
.chip {
/* 不再 flex: 1按内容自适应宽度white-space 防中文被挤压换行 */
padding: 12rpx 16rpx;
background: #fafbfc;
border: 1rpx solid #f0f0f0;
border-radius: 12rpx;
font-size: 22rpx;
color: #595959;
display: inline-flex;
align-items: center;
gap: 8rpx;
box-sizing: border-box;
white-space: nowrap;
flex-shrink: 0;
}
.chip-dot {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
flex-shrink: 0;
}
.dot-app {
background: #1890ff;
}
.dot-cache {
background: #722ed1;
}
.dot-available {
background: #52c41a;
}
/* ── 分组卡片(缓存分类、其他) ── */
.section-card {
background: #fff;
border-radius: 16rpx;
padding: 0 24rpx;
margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.03);
}
.section-title {
font-size: 24rpx;
color: #8c8c8c;
font-weight: 500;
padding: 24rpx 0 16rpx;
display: block;
}
/* ── Row ── */
.row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx 0;
border-bottom: 1rpx solid #f5f5f5;
transition: background-color 0.15s ease;
}
.row:active {
background: #fafafa;
}
.row-last {
border-bottom: none;
}
.row-main {
display: flex;
flex-direction: column;
gap: 6rpx;
flex: 1;
min-width: 0;
padding-right: 24rpx;
}
.row-label {
font-size: 30rpx;
font-weight: 500;
color: #262626;
}
.row-warn {
font-size: 22rpx;
color: #faad14;
}
.row-sub {
display: flex;
align-items: center;
gap: 12rpx;
flex-shrink: 0;
}
.row-sub .size {
font-size: 26rpx;
color: #8c8c8c;
}
.row-sub .chevron {
font-size: 28rpx;
color: #bfbfbf;
line-height: 1;
}
.row-desc {
font-size: 22rpx;
color: #8c8c8c;
margin-top: 6rpx;
line-height: 1.5;
}
.others-row {
align-items: flex-start;
}
.others-row .size {
padding-top: 4rpx;
flex-shrink: 0;
white-space: nowrap;
color: #8c8c8c;
font-size: 26rpx;
}
.error {
color: #ff4d4f;
font-size: 26rpx;
}
/* ── 空状态 ── */
.empty {
display: flex;
flex-direction: column;
align-items: center;
padding: 80rpx 0;
gap: 16rpx;
}
.empty-icon {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
background: #f6ffed;
color: #52c41a;
font-size: 48rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.empty-text {
color: #8c8c8c;
font-size: 28rpx;
}
/* ── 加载失败按钮 ── */
.load-failed {
margin: 0 24rpx;
text-align: center;
padding: 24rpx 0;
background: #e6f7ff;
color: #1890ff;
font-size: 28rpx;
font-weight: 500;
border-radius: 16rpx;
cursor: pointer;
}
</style>