From 2f672920d992e05fc6242b67270c1aa1fe26a0f4 Mon Sep 17 00:00:00 2001 From: zheng020 Date: Thu, 30 Jul 2026 15:34:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E5=AE=9E=E9=99=85?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/specs/2026-07-28-cache-cleanup-design.md | 27 +++++++--- frontend/pages/profile/cache-cleanup.vue | 53 ++++++++++++++++++- frontend/utils/cacheManager.js | 19 ++++++- frontend/utils/ioPath.js | 35 ++++++++++++ 4 files changed, 125 insertions(+), 9 deletions(-) diff --git a/docs/specs/2026-07-28-cache-cleanup-design.md b/docs/specs/2026-07-28-cache-cleanup-design.md index e686b9c..55214e7 100644 --- a/docs/specs/2026-07-28-cache-cleanup-design.md +++ b/docs/specs/2026-07-28-cache-cleanup-design.md @@ -161,6 +161,9 @@ const info = await cacheManager.getCacheInfo() // quotaTotalBytes, // 配额总量 = uni.getStorageInfoSync().limitSize*1024 // quotaAvailableBytes, // 配额可用 = quotaTotalBytes - appUsedBytes // usagePercent, // 百分比(0-100,1 位小数)= appUsedBytes / quotaTotalBytes * 100 +// deviceTotalBytes, // 设备总存储 = plus.io.getStorageInfo().totalSize * 1024(HTML5+ API,MVP 已支持) +// deviceFreeBytes, // 设备可用存储 = plus.io.getStorageInfo().availableSize * 1024 +// deviceUsagePercent, // 设备占比(0-100,2 位小数)= appUsedBytes / deviceTotalBytes * 100 // othersBytes, // "其他" section 大小 = 所有黑名单 keys + 不可清理文件大小 // categories: [{id, label, description, sizeBytes, keyCount, warning}, ...] // } @@ -265,13 +268,18 @@ await invalidateAll() // 清空 memoryMap + inFlightMap │ │ │ 49.7 MB │ ← 大字号主指标(主色) │ │ -│ 占据配额 16% 存储空间 │ ← 百分比副文字(次要色) -│ │ │ ▓▓▓▓░░░░░░░░░░░░░░░░░░░░░ 16% │ ← 进度条(当前用量占配额比例) │ │ │ ┌────────────┬─────────────┬────────────┐ │ │ │ Topfans 已用空间 │ 内存缓存空间 │ 总内存空间 │ │ ← 一行 3 个 chip 标签(无数值) │ └────────────┴─────────────┴────────────┘ │ 数值已分别显示在:大字 / 缓存分类 sum / 总配额 +│ │ +│ ┌──────────────────────────────────────┐ │ +│ │ 设备总空间 │ 设备可用 │ │ ← 新增:HTML5+ 设备级信息行 +│ │ 128.0 GB │ 45.2 GB │ │ (plus.io.getStorageInfo;非 APP-PLUS 显示 "—") +│ └──────────────────────────────────────┘ │ +│ │ +│ 占设备 0.04% 存储空间 │ ← 百分比副文字(分母=设备总存储,2 位小数) ├─────────────────────────────────────────────┤ │ 缓存分类 │ ← section 标题(次要色) ├─────────────────────────────────────────────┤ @@ -314,8 +322,9 @@ await invalidateAll() // 清空 memoryMap + inFlightMap - 该行**不可点击进入详情**(没有可清理内容) **关于"磁盘" vs "配额"**: -- MVP 用 `uni.getStorageInfoSync().limitSize`(app 自己的 SQLite 配额),文案写"配额"而非"磁盘",不误导用户 -- 设备级"磁盘总空间/可用空间"需要 native plugin(iOS `NSFileManager` / Android `StatFs`),**MVP 不引入**,列为后续优化(§2) +- **进度条**仍以 `uni.getStorageInfoSync().limitSize`(app 自己的 SQLite 配额)为 100%,表达"app 缓存压力"(用户的清理决策依据) +- **百分比分母** = `plus.io.getStorageInfo().totalSize`(设备总存储),即"占设备 X% 存储空间"——HTML5+ 标准 API,Android/iOS 均原生支持,**不需要 native plugin** +- **设备信息行** = `plus.io.getStorageInfo()` 直接拿 `{ totalSize, availableSize }`,无权限要求;非 APP-PLUS 平台或调用失败时显示 `—` **草稿/引导的二级提示**:行右侧副文字"按账号分组清理",警示用户详情页会有分组。 @@ -416,15 +425,19 @@ cacheManager.getCacheInfo() │ └─ others.computeSize → 上述未匹配的 key 汇总 ├─ [并行 2] 读取存储配额 │ ├─ uni.getStorageInfoSync() → { currentSize (KB), limitSize (KB) } - │ └─ ioPath.getSandboxTotalSize() → 沙盒 doc 目录下所有文件总字节数(新增 ioPath 只读 API;与 scanSandboxTmpFiles 不同,前者含白名单 preload/share/image 所有文件,后者仅统计 tmp/) + │ ├─ ioPath.getSandboxTotalSize() → 沙盒 doc 目录下所有文件总字节数(新增 ioPath 只读 API;与 scanSandboxTmpFiles 不同,前者含白名单 preload/share/image 所有文件,后者仅统计 tmp/) + │ └─ ioPath.getDeviceStorageInfo() → 设备级 { totalSize (KB), availableSize (KB) },HTML5+ API,非 APP-PLUS 返回 {0,0} ↓ 聚合: totalBytes = sum(categories.sizeBytes) // 可清理总量(=内存缓存空间 chip) appUsedBytes = currentSize * 1024 + sandboxBytes // 软件占用(大字 + Topfans已用空间 chip) // 注:currentSize 是 uni storage 全量(含黑名单),这是 WeChat "已用空间" 口径 - quotaTotalBytes = limitSize * 1024 // 配额总量(=总内存空间 chip) + quotaTotalBytes = limitSize * 1024 // 配额总量(=总内存空间 chip,进度条 100% 基准) quotaAvailableBytes = quotaTotalBytes - appUsedBytes // 配额可用 - usagePercent = (appUsedBytes / quotaTotalBytes) * 100 // 百分比 + usagePercent = (appUsedBytes / quotaTotalBytes) * 100 // 配额百分比(进度条用) + deviceTotalBytes = deviceTotalKB * 1024 // 设备总存储(设备信息行"设备总空间") + deviceFreeBytes = deviceFreeKB * 1024 // 设备可用(设备信息行"设备可用") + deviceUsagePercent = (appUsedBytes / deviceTotalBytes) * 100 // 设备百分比(百分比副文字"占设备 X%"用) othersBytes = sum(黑名单 keys size) + 不可清理文件大小 // "其他" section ↓ 渲染页面(大字 + 百分比 + 进度条 + 3 chip 标签 + 6 缓存分类行 + 1 其他 section) diff --git a/frontend/pages/profile/cache-cleanup.vue b/frontend/pages/profile/cache-cleanup.vue index 8edecee..ace9d81 100644 --- a/frontend/pages/profile/cache-cleanup.vue +++ b/frontend/pages/profile/cache-cleanup.vue @@ -46,8 +46,21 @@ + + + + 设备总空间 + {{ formatSize(info.deviceTotalBytes) }} + + + + 设备可用 + {{ formatSize(info.deviceFreeBytes) }} + + + 占手机 {{ info.usagePercent.toFixed(1) }}% 存储空间占设备 {{ info.deviceUsagePercent.toFixed(2) }}% 存储空间 @@ -122,6 +135,9 @@ const info = ref({ quotaTotalBytes: 0, quotaAvailableBytes: 0, usagePercent: 0, + deviceTotalBytes: 0, + deviceFreeBytes: 0, + deviceUsagePercent: 0, othersBytes: 0, totalBytes: 0, categories: [], @@ -231,6 +247,41 @@ onPullDownRefresh(async () => { display: block; margin-top: 24rpx; } + +/* ── 设备级信息行(总空间 / 可用空间)── */ +.device-info { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 24rpx; + padding: 16rpx 20rpx; + background: #fafbfc; + border-radius: 12rpx; + box-sizing: border-box; +} +.device-info-item { + display: flex; + flex-direction: column; + gap: 4rpx; + flex: 1; + min-width: 0; +} +.device-info-divider { + width: 1rpx; + height: 32rpx; + background: #e8e8e8; + margin: 0 16rpx; + flex-shrink: 0; +} +.device-info-label { + font-size: 22rpx; + color: #8c8c8c; +} +.device-info-value { + font-size: 28rpx; + font-weight: 500; + color: #262626; +} .hero-tip { display: flex; align-items: center; diff --git a/frontend/utils/cacheManager.js b/frontend/utils/cacheManager.js index 1721ce3..c2cced2 100644 --- a/frontend/utils/cacheManager.js +++ b/frontend/utils/cacheManager.js @@ -9,7 +9,7 @@ import othersHandler from './handlers/othersHandler' import preloadHandler from './handlers/preloadHandler' import guideHandler from './handlers/guideHandler' import draftHandler from './handlers/draftHandler' -import { getSandboxTotalSize } from './ioPath' +import { getSandboxTotalSize, getDeviceStorageInfo } from './ioPath' // ── 黑名单(§3.4.1)── // 全等匹配 @@ -105,6 +105,16 @@ export async function getCacheInfo() { } catch (e) { console.warn('[cacheManager] getSandboxTotalSize failed:', e.message) } + let deviceTotalKB = 0, deviceFreeKB = 0 + try { + const dev = await getDeviceStorageInfo() + deviceTotalKB = dev.totalBytes || 0 + deviceFreeKB = dev.freeBytes || 0 + // 调试日志:把 getDeviceStorageInfo 的原始返回和 cacheManager 解析后的值都打出来 + console.log('[cacheManager][debug] getDeviceStorageInfo raw =', JSON.stringify(dev), 'parsed totalKB =', deviceTotalKB, 'freeKB =', deviceFreeKB) + } catch (e) { + console.warn('[cacheManager] getDeviceStorageInfo failed:', e.message) + } const totalBytes = categories.reduce((sum, c) => sum + (c.sizeBytes > 0 ? c.sizeBytes : 0), 0) const appUsedBytes = currentSizeKB * 1024 + sandboxBytes @@ -113,9 +123,16 @@ export async function getCacheInfo() { const quotaAvailableBytes = Math.max(0, raw) const quotaExceeded = raw < 0 const usagePercent = quotaTotalBytes > 0 ? (appUsedBytes / quotaTotalBytes) * 100 : 0 + // 设备级数据:plus.io.getStorageInfo 返回 KB,转字节 + const deviceTotalBytes = deviceTotalKB * 1024 + const deviceFreeBytes = deviceFreeKB * 1024 + const deviceUsagePercent = deviceTotalBytes > 0 + ? (appUsedBytes / deviceTotalBytes) * 100 + : 0 return { totalBytes, appUsedBytes, quotaTotalBytes, quotaAvailableBytes, quotaExceeded, usagePercent, + deviceTotalBytes, deviceFreeBytes, deviceUsagePercent, othersBytes: blacklistBytes + sandboxBytes, categories, } diff --git a/frontend/utils/ioPath.js b/frontend/utils/ioPath.js index ebbbe56..7164f52 100644 --- a/frontend/utils/ioPath.js +++ b/frontend/utils/ioPath.js @@ -522,3 +522,38 @@ async function _listDir(dirEntry) { return [] // #endif } + +/** + * 获取设备级存储信息(HTML5+ 标准 API,Android/iOS 均支持) + * 不需要任何 native plugin;非 APP-PLUS 平台或失败时返回安全零值 + * 注意:单位为 KB(plus.io 原始返回),调用方按需 * 1024 转字节 + * @returns {Promise<{totalBytes:number, freeBytes:number}>} + */ +export function getDeviceStorageInfo() { + return new Promise((resolve) => { + // #ifdef APP-PLUS + try { + plus.io.getStorageInfo({ + success: (res) => { + // 调试日志:打印 plus.io.getStorageInfo 的真实返回 shape + console.log('[ioPath][debug] plus.io.getStorageInfo res =', JSON.stringify(res), 'keys =', res ? Object.keys(res) : null) + resolve({ + totalBytes: Number(res?.totalSize) || 0, + freeBytes: Number(res?.availableSize) || 0, + }) + }, + fail: (e) => { + console.warn('[ioPath] getStorageInfo failed:', e?.message) + resolve({ totalBytes: 0, freeBytes: 0 }) + }, + }) + } catch (e) { + console.warn('[ioPath] getStorageInfo threw:', e?.message) + resolve({ totalBytes: 0, freeBytes: 0 }) + } + // #endif + // #ifndef APP-PLUS + resolve({ totalBytes: 0, freeBytes: 0 }) + // #endif + }) +}