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
+ })
+}