/** * 全局数据状态(模块级 reactive 单例,多页面共享) * - packages:包列表(已排序:预警置顶 → 正常 → 忽略) * - thresholds:全局阈值 + 单包例外 * - logs:操作日志 */ import { reactive, computed } from 'vue' import api from '../api' const state = reactive({ packages: [], threshold: { versionThreshold: 15, sizeThreshold: 500, exceptions: [] }, logs: [], loading: false, scannedAt: null, storageDir: localStorage.getItem('vcm-storage-dir') || '', // 后端目录扫描总字节(所有文件,与 du 同口径;null 表示未知/演示模式) totalBytes: null, // 扫描进度(异步扫描期间实时更新,用于遮罩展示) scanProgress: { packages: 0, bytes: 0 }, }) // —— 派生统计 —— const stats = computed(() => { const valid = state.packages.filter(p => p.status !== 'broken') // 总占用统计全部物理缓存(含元数据异常的包,其大小按实际 .tgz 计算) const totalSize = state.packages.reduce((s, p) => s + p.size, 0) const warnCount = valid.filter(p => p.status === 'warn').length const cleanable = valid.filter(p => p.status === 'warn').reduce((s, p) => s + p.size, 0) const maxPkg = [...valid].sort((a, b) => b.size - a.size)[0] || null const oldestPkg = [...valid].sort((a, b) => (a.lastUpdated || '').localeCompare(b.lastUpdated || ''))[0] || null // 今日更新包数:最近更新时间等于今天的包数量 const d = new Date() const todayStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` const todayUpdated = valid.filter(p => (p.lastUpdated || '') === todayStr).length // 真实环境无访问统计(visits 全 0)时,Top20 降级为「缓存占用 Top 20」 const hasVisits = valid.some(p => (p.visits || 0) > 0) const top30 = [...valid] .sort((a, b) => (hasVisits ? (b.visits || 0) - (a.visits || 0) : b.size - a.size)) .slice(0, 20) const maxVisit = Math.max(...top30.map(p => p.visits || 0), 1) const maxSize = Math.max(...top30.map(p => p.size), 1) return { pkgCount: valid.length, totalVersions: state.packages.reduce((s, p) => s + (p.versions || 0), 0), totalSize, // 总占用优先用后端「目录所有文件字节和」(与 du 一致),否则回退到包大小之和 totalSizeText: state.totalBytes != null ? fmtBytes(state.totalBytes) : fmtSize(totalSize), warnCount, cleanable, cleanableText: fmtSize(cleanable), todayUpdated, maxPkg, oldestPkg, top30: top30.map(p => ({ ...p, heat: hasVisits ? Math.round(((p.visits || 0) / maxVisit) * 100) : Math.round((p.size / maxSize) * 100), })), top30By: hasVisits ? 'visits' : 'size', } }) function fmtSize(mb) { if (mb >= 1024) return `${(mb / 1024).toFixed(1)} GB` return `${mb.toFixed(1)} MB` } /** 字节 → 可读文本 */ function fmtBytes(b) { const mb = b / 1024 / 1024 if (mb >= 1024) return `${(mb / 1024).toFixed(1)} GB` return `${mb.toFixed(1)} MB` } const fmtSizeFn = fmtSize async function refreshPackages(keyword = '') { const { packages, scannedAt, scanning, scanPackages, scanBytes, totalBytes } = await api.getPackages(keyword) state.packages = packages || [] if (scannedAt) state.scannedAt = scannedAt if (scanPackages !== undefined) state.scanProgress.packages = scanPackages if (scanBytes !== undefined) state.scanProgress.bytes = scanBytes if (totalBytes !== undefined && totalBytes !== null) state.totalBytes = totalBytes return { scanning: !!scanning } } /** 轮询后端扫描进度,直到扫描完成 */ async function pollScanProgress() { for (;;) { await new Promise(r => setTimeout(r, 800)) let p try { p = await api.getScanProgress() } catch { break // 接口异常时退出,避免遮罩永久拦截点击 } state.scanProgress.packages = p.packages || 0 state.scanProgress.bytes = p.bytes || 0 if (!p.scanning) break } } /** 刷新数据:若后端仍在扫描则先轮询进度,扫完再拉取真实数据 */ async function doRefresh() { const first = await refreshPackages() if (first.scanning) { await pollScanProgress() await refreshPackages() // 扫描完成后重新拉取完整数据 } await Promise.all([refreshThresholds(), refreshLogs()]) } /** 手动重新扫描缓存目录(异步,遮罩实时显示进度) */ async function scanNow() { state.loading = true try { const res = await api.scanNow() if (!res.ok) return res await doRefresh() return res } finally { state.loading = false } } async function refreshThresholds() { const t = await api.getThresholds() state.threshold.versionThreshold = t.versionThreshold state.threshold.sizeThreshold = t.sizeThreshold state.threshold.exceptions = t.exceptions || [] } async function refreshLogs() { state.logs = await api.getLogs() } async function refreshAll() { state.loading = true try { await doRefresh() } finally { state.loading = false } } async function saveThresholds(payload) { const ok = await api.saveThresholds(payload) if (ok) { await refreshThresholds() await refreshPackages() } return ok } async function ignorePkg(name) { await api.ignore(name) await refreshPackages() } async function unignorePkg(name) { await api.unignore(name) await refreshPackages() } async function cleanPkg(name, keep) { const res = await api.clean(name, keep) if (res.ok) { await Promise.all([refreshPackages(), refreshLogs()]) } return res } async function deleteVersion(name, version) { const res = await api.deleteVersion(name, version) if (res.ok) { await Promise.all([refreshPackages(), refreshLogs()]) } return res } export function useStore() { return { state, stats, fmtSize: fmtSizeFn, refreshAll, refreshPackages, refreshThresholds, refreshLogs, scanNow, saveThresholds, ignorePkg, unignorePkg, cleanPkg, deleteVersion, } }