feat: Verdaccio 缓存可视化管理面板(前端 + 后端 + Docker)

- 前端 Vue3+Vite+Element Plus 按需引入,双主题(bright/dark)CSS 变量
- 三端响应式(桌面/平板/手机),登录/概览/检索/阈值/忽略/动态 6 页面
- 后端 Express + better-sqlite3,目录扫描 + demo 模式,清理原子写入
- Dockerfile 多阶段构建 + docker-compose(storage 只读挂载)
This commit is contained in:
陈银军
2026-09-19 15:00:04 +08:00
parent 338c1a0a95
commit 0a9ca8894a
43 changed files with 7036 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
/**
* 全局数据状态(模块级 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,
storageDir: localStorage.getItem('vcm-storage-dir') || '',
})
// —— 派生统计 ——
const stats = computed(() => {
const valid = state.packages.filter(p => p.status !== 'broken')
const totalSize = valid.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 top30 = [...valid].sort((a, b) => (b.visits || 0) - (a.visits || 0)).slice(0, 30)
const maxVisit = Math.max(...top30.map(p => p.visits || 0), 1)
return {
pkgCount: valid.length,
totalSize,
totalSizeText: fmtSize(totalSize),
warnCount,
cleanable,
cleanableText: fmtSize(cleanable),
maxPkg,
oldestPkg,
top30: top30.map(p => ({ ...p, heat: Math.round(((p.visits || 0) / maxVisit) * 100) })),
}
})
function fmtSize(mb) {
if (mb >= 1024) return `${(mb / 1024).toFixed(1)} GB`
return `${mb.toFixed(1)} MB`
}
const fmtSizeFn = fmtSize
async function refreshPackages(keyword = '') {
state.packages = await api.getPackages(keyword)
}
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 Promise.all([refreshPackages(), refreshThresholds(), refreshLogs()])
} 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,
saveThresholds,
ignorePkg,
unignorePkg,
cleanPkg,
deleteVersion,
}
}