fix(logs): 忽略/取消忽略操作写入操作日志
原 /api/ignore 与 /api/ignore/:pkgName 只写 ignored_packages 表不记日志, 导致操作日志页查不到忽略记录。logs 表新增 action 列区分操作类型 (clean/ignore/unignore,旧库运行时 ALTER TABLE 迁移),忽略与取消忽略 接口补写 addLog;日志页按操作类型展示徽标与详情,删除数量/释放空间对 非清理操作显示占位符。
This commit is contained in:
+10
-3
@@ -26,6 +26,7 @@ CREATE TABLE IF NOT EXISTS ignored_packages (
|
|||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS logs (
|
CREATE TABLE IF NOT EXISTS logs (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
action TEXT DEFAULT 'clean',
|
||||||
pkg_name TEXT,
|
pkg_name TEXT,
|
||||||
deleted_versions TEXT,
|
deleted_versions TEXT,
|
||||||
deleted_count INTEGER,
|
deleted_count INTEGER,
|
||||||
@@ -34,6 +35,12 @@ CREATE TABLE IF NOT EXISTS logs (
|
|||||||
);
|
);
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
// 旧库迁移:早期 logs 表无 action 列,运行时补齐(忽略/取消忽略等操作依赖该列区分类型)
|
||||||
|
const logCols = db.prepare('PRAGMA table_info(logs)').all().map(c => c.name)
|
||||||
|
if (!logCols.includes('action')) {
|
||||||
|
db.exec("ALTER TABLE logs ADD COLUMN action TEXT DEFAULT 'clean'")
|
||||||
|
}
|
||||||
|
|
||||||
// —— settings ——
|
// —— settings ——
|
||||||
export function getSetting(key, def = null) {
|
export function getSetting(key, def = null) {
|
||||||
const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key)
|
const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key)
|
||||||
@@ -70,10 +77,10 @@ export function removeIgnored(pkgName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// —— logs ——
|
// —— logs ——
|
||||||
export function addLog({ pkgName, deletedVersions, deletedCount, freedSpace }) {
|
export function addLog({ action = 'clean', pkgName, deletedVersions = null, deletedCount = 0, freedSpace = null }) {
|
||||||
const info = db.prepare(
|
const info = db.prepare(
|
||||||
'INSERT INTO logs (pkg_name, deleted_versions, deleted_count, freed_space) VALUES (?, ?, ?, ?)',
|
'INSERT INTO logs (action, pkg_name, deleted_versions, deleted_count, freed_space) VALUES (?, ?, ?, ?, ?)',
|
||||||
).run(pkgName, deletedVersions, deletedCount, freedSpace)
|
).run(action, pkgName, deletedVersions, deletedCount, freedSpace)
|
||||||
return info.lastInsertRowid
|
return info.lastInsertRowid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -208,10 +208,13 @@ app.post('/api/ignore', (req, res) => {
|
|||||||
const { pkgName } = req.body || {}
|
const { pkgName } = req.body || {}
|
||||||
if (!pkgName) return res.status(400).json({ ok: false, message: '缺少 pkgName' })
|
if (!pkgName) return res.status(400).json({ ok: false, message: '缺少 pkgName' })
|
||||||
addIgnored(String(pkgName))
|
addIgnored(String(pkgName))
|
||||||
|
addLog({ action: 'ignore', pkgName: String(pkgName) })
|
||||||
res.json({ ok: true })
|
res.json({ ok: true })
|
||||||
})
|
})
|
||||||
app.delete('/api/ignore/:pkgName', (req, res) => {
|
app.delete('/api/ignore/:pkgName', (req, res) => {
|
||||||
removeIgnored(req.params.pkgName)
|
const pkgName = decodeURIComponent(req.params.pkgName)
|
||||||
|
removeIgnored(pkgName)
|
||||||
|
addLog({ action: 'unignore', pkgName })
|
||||||
res.json({ ok: true })
|
res.json({ ok: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,12 @@ const api = {
|
|||||||
mockState.ignored.add(pkgName)
|
mockState.ignored.add(pkgName)
|
||||||
const p = mockState.packages.find(x => x.name === pkgName)
|
const p = mockState.packages.find(x => x.name === pkgName)
|
||||||
if (p) { p.status = 'ignored'; p.ignoredAt = new Date().toISOString().slice(0, 10) }
|
if (p) { p.status = 'ignored'; p.ignoredAt = new Date().toISOString().slice(0, 10) }
|
||||||
|
mockState.logs.unshift({
|
||||||
|
id: Date.now(),
|
||||||
|
action: 'ignore',
|
||||||
|
pkgName,
|
||||||
|
created_at: new Date().toISOString().slice(0, 16).replace('T', ' '),
|
||||||
|
})
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
}
|
}
|
||||||
const r = await fetch('/api/ignore', {
|
const r = await fetch('/api/ignore', {
|
||||||
@@ -175,6 +181,12 @@ const api = {
|
|||||||
p.status = computeStatus(p, mockState.versionThreshold)
|
p.status = computeStatus(p, mockState.versionThreshold)
|
||||||
delete p.ignoredAt
|
delete p.ignoredAt
|
||||||
}
|
}
|
||||||
|
mockState.logs.unshift({
|
||||||
|
id: Date.now(),
|
||||||
|
action: 'unignore',
|
||||||
|
pkgName,
|
||||||
|
created_at: new Date().toISOString().slice(0, 16).replace('T', ' '),
|
||||||
|
})
|
||||||
return { ok: true }
|
return { ok: true }
|
||||||
}
|
}
|
||||||
const r = await fetch(`/api/ignore/${encodeURIComponent(pkgName)}`, { method: 'DELETE' })
|
const r = await fetch(`/api/ignore/${encodeURIComponent(pkgName)}`, { method: 'DELETE' })
|
||||||
|
|||||||
@@ -143,11 +143,11 @@ function topMax(list) {
|
|||||||
return Math.max(...list.map(p => p.visits), 1)
|
return Math.max(...list.map(p => p.visits), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 日志
|
// 日志(action: clean=清理 / ignore=忽略 / unignore=取消忽略)
|
||||||
const logs = [
|
const logs = [
|
||||||
{ id: 3, pkgName: '@antv/g2', deletedVersions: '1.0.0, 1.1.0, 1.2.0', deletedCount: 3, freedSpace: '36.5 MB', created_at: dateStr(0) + ' 09:12:04' },
|
{ id: 3, action: 'clean', pkgName: '@antv/g2', deletedVersions: '1.0.0, 1.1.0, 1.2.0', deletedCount: 3, freedSpace: '36.5 MB', created_at: dateStr(0) + ' 09:12:04' },
|
||||||
{ id: 2, pkgName: 'lodash', deletedVersions: '4.0.0, 4.1.0, 4.2.0, 4.3.0', deletedCount: 4, freedSpace: '15.2 MB', created_at: dateStr(1) + ' 18:40:22' },
|
{ id: 2, action: 'ignore', pkgName: 'moment', created_at: dateStr(1) + ' 18:40:22' },
|
||||||
{ id: 1, pkgName: 'axios', deletedVersions: '0.19.0', deletedCount: 1, freedSpace: '1.1 MB', created_at: dateStr(2) + ' 11:05:47' },
|
{ id: 1, action: 'clean', pkgName: 'axios', deletedVersions: '0.19.0', deletedCount: 1, freedSpace: '1.1 MB', created_at: dateStr(2) + ' 11:05:47' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 状态统计(与 overview 对齐)
|
// 状态统计(与 overview 对齐)
|
||||||
|
|||||||
@@ -10,13 +10,20 @@ onMounted(() => refreshLogs())
|
|||||||
function fmtTime(t) {
|
function fmtTime(t) {
|
||||||
return (t || '').replace('T', ' ').slice(0, 19)
|
return (t || '').replace('T', ' ').slice(0, 19)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 操作类型 → 徽标文案/class/emoji
|
||||||
|
function actionMeta(action) {
|
||||||
|
if (action === 'ignore') return { emo: '💤', text: '忽略', cls: 'ignore' }
|
||||||
|
if (action === 'unignore') return { emo: '↩️', text: '取消忽略', cls: 'unignore' }
|
||||||
|
return { emo: '🗑', text: '清理', cls: 'clean' }
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="page-inner">
|
<div class="page-inner">
|
||||||
<div class="page-title"><span class="emo">🕒</span>最近动态</div>
|
<div class="page-title"><span class="emo">🕒</span>最近动态</div>
|
||||||
<div class="page-sub">
|
<div class="page-sub">
|
||||||
共 <b>{{ state.logs.length }}</b> 条清理记录 · 按时间倒序
|
共 <b>{{ state.logs.length }}</b> 条操作记录 · 按时间倒序
|
||||||
<span class="log-live"><i></i>LIVE</span>
|
<span class="log-live"><i></i>LIVE</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -26,8 +33,9 @@ function fmtTime(t) {
|
|||||||
<tr>
|
<tr>
|
||||||
<th>时间</th>
|
<th>时间</th>
|
||||||
<th>包名</th>
|
<th>包名</th>
|
||||||
<th style="width: 120px">删除数量</th>
|
<th style="width: 110px">操作类型</th>
|
||||||
<th style="width: 120px">释放空间</th>
|
<th style="width: 100px">删除数量</th>
|
||||||
|
<th style="width: 100px">释放空间</th>
|
||||||
<th style="width: 90px">操作</th>
|
<th style="width: 90px">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -35,13 +43,24 @@ function fmtTime(t) {
|
|||||||
<tr v-for="log in state.logs" :key="log.id" @click="detail = log">
|
<tr v-for="log in state.logs" :key="log.id" @click="detail = log">
|
||||||
<td><span class="date-cell">{{ fmtTime(log.created_at) }}</span></td>
|
<td><span class="date-cell">{{ fmtTime(log.created_at) }}</span></td>
|
||||||
<td><span class="pkg-cell">{{ log.pkgName }}</span></td>
|
<td><span class="pkg-cell">{{ log.pkgName }}</span></td>
|
||||||
<td><span class="chip n">− {{ log.deletedCount }} 版</span></td>
|
<td>
|
||||||
<td><span class="chip s">🟢 {{ log.freedSpace }}</span></td>
|
<span class="act-badge" :class="actionMeta(log.action).cls">
|
||||||
|
{{ actionMeta(log.action).emo }} {{ actionMeta(log.action).text }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="log.action === 'clean'" class="chip n">− {{ log.deletedCount }} 版</span>
|
||||||
|
<span v-else class="dash">—</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="log.action === 'clean'" class="chip s">🟢 {{ log.freedSpace }}</span>
|
||||||
|
<span v-else class="dash">—</span>
|
||||||
|
</td>
|
||||||
<td><button class="btn ghost" @click.stop="detail = log">详情</button></td>
|
<td><button class="btn ghost" @click.stop="detail = log">详情</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div v-if="!state.logs.length" class="empty-tip">暂无清理记录,清理操作会自动记录在这里</div>
|
<div v-if="!state.logs.length" class="empty-tip">暂无操作记录,清理、忽略等操作会自动记录在这里</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -53,7 +72,9 @@ function fmtTime(t) {
|
|||||||
<Transition name="pop">
|
<Transition name="pop">
|
||||||
<div v-if="detail" class="detail-dialog">
|
<div v-if="detail" class="detail-dialog">
|
||||||
<div class="dd-head">
|
<div class="dd-head">
|
||||||
<span class="dd-title">📋 清理详情</span>
|
<span class="dd-title">
|
||||||
|
{{ detail.action === 'ignore' ? '💤 忽略' : detail.action === 'unignore' ? '↩️ 取消忽略' : '📋 清理详情' }}
|
||||||
|
</span>
|
||||||
<div class="dd-close" @click="detail = null">✕</div>
|
<div class="dd-close" @click="detail = null">✕</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dd-body">
|
<div class="dd-body">
|
||||||
@@ -65,19 +86,25 @@ function fmtTime(t) {
|
|||||||
<span class="k">操作时间</span>
|
<span class="k">操作时间</span>
|
||||||
<span class="v">{{ fmtTime(detail.created_at) }}</span>
|
<span class="v">{{ fmtTime(detail.created_at) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="dd-row">
|
<template v-if="detail.action === 'clean'">
|
||||||
<span class="k">删除数量</span>
|
<div class="dd-row">
|
||||||
<span class="v red">− {{ detail.deletedCount }} 个版本</span>
|
<span class="k">删除数量</span>
|
||||||
</div>
|
<span class="v red">− {{ detail.deletedCount }} 个版本</span>
|
||||||
<div class="dd-row">
|
|
||||||
<span class="k">释放空间</span>
|
|
||||||
<span class="v green">🟢 {{ detail.freedSpace }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="dd-row">
|
|
||||||
<span class="k">删除版本</span>
|
|
||||||
<div class="dd-tags">
|
|
||||||
<span v-for="v in (detail.deletedVersions || '').split(',').map(s => s.trim()).filter(Boolean)" :key="v" class="dd-tag">{{ v }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="dd-row">
|
||||||
|
<span class="k">释放空间</span>
|
||||||
|
<span class="v green">🟢 {{ detail.freedSpace }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dd-row">
|
||||||
|
<span class="k">删除版本</span>
|
||||||
|
<div class="dd-tags">
|
||||||
|
<span v-for="v in (detail.deletedVersions || '').split(',').map(s => s.trim()).filter(Boolean)" :key="v" class="dd-tag">{{ v }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="dd-row">
|
||||||
|
<span class="k">说明</span>
|
||||||
|
<span class="v">{{ detail.action === 'ignore' ? '已将包加入忽略列表,不再提示超阈值警告' : '已取消忽略,恢复超阈值提醒' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,9 +129,14 @@ function fmtTime(t) {
|
|||||||
}
|
}
|
||||||
.log-live i { width: 7px; height: 7px; border-radius: 50%; background: var(--green-deep); box-shadow: 0 0 8px var(--green-deep); animation: blink 1.6s infinite; }
|
.log-live i { width: 7px; height: 7px; border-radius: 50%; background: var(--green-deep); box-shadow: 0 0 8px var(--green-deep); animation: blink 1.6s infinite; }
|
||||||
.pkg-cell { font-weight: 600; word-break: break-all; }
|
.pkg-cell { font-weight: 600; word-break: break-all; }
|
||||||
|
.act-badge { font-family: var(--num); font-size: 11.5px; font-weight: 700; padding: 4px 10px; border-radius: 9px; display: inline-block; }
|
||||||
|
.act-badge.clean { color: var(--red); background: var(--red-soft); }
|
||||||
|
.act-badge.ignore { color: var(--ink-dim); background: var(--num-bg); }
|
||||||
|
.act-badge.unignore { color: var(--green-deep); background: var(--green-soft); }
|
||||||
.chip { font-family: var(--num); font-size: 12px; font-weight: 700; padding: 4px 12px; border-radius: 9px; display: inline-block; }
|
.chip { font-family: var(--num); font-size: 12px; font-weight: 700; padding: 4px 12px; border-radius: 9px; display: inline-block; }
|
||||||
.chip.n { color: var(--red); background: var(--red-soft); }
|
.chip.n { color: var(--red); background: var(--red-soft); }
|
||||||
.chip.s { color: var(--green-deep); background: var(--green-soft); }
|
.chip.s { color: var(--green-deep); background: var(--green-soft); }
|
||||||
|
.dash { color: var(--ink-faint); }
|
||||||
.empty-tip { padding: 50px 0; text-align: center; color: var(--ink-faint); font-size: 13px; }
|
.empty-tip { padding: 50px 0; text-align: center; color: var(--ink-faint); font-size: 13px; }
|
||||||
|
|
||||||
.ov { position: fixed; inset: 0; background: rgba(60, 45, 30, .4); z-index: 140; }
|
.ov { position: fixed; inset: 0; background: rgba(60, 45, 30, .4); z-index: 140; }
|
||||||
|
|||||||
Reference in New Issue
Block a user