feat: Verdaccio 缓存可视化管理面板(前端 + 后端 + Docker)
- 前端 Vue3+Vite+Element Plus 按需引入,双主题(bright/dark)CSS 变量 - 三端响应式(桌面/平板/手机),登录/概览/检索/阈值/忽略/动态 6 页面 - 后端 Express + better-sqlite3,目录扫描 + demo 模式,清理原子写入 - Dockerfile 多阶段构建 + docker-compose(storage 只读挂载)
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user