fix(logs): 忽略/取消忽略操作写入操作日志

原 /api/ignore 与 /api/ignore/:pkgName 只写 ignored_packages 表不记日志,
导致操作日志页查不到忽略记录。logs 表新增 action 列区分操作类型
(clean/ignore/unignore,旧库运行时 ALTER TABLE 迁移),忽略与取消忽略
接口补写 addLog;日志页按操作类型展示徽标与详情,删除数量/释放空间对
非清理操作显示占位符。
This commit is contained in:
陈银军
2026-09-19 21:58:44 +08:00
parent d5074d0ff4
commit a0f0c27c44
5 changed files with 81 additions and 27 deletions
+10 -3
View File
@@ -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
View File
@@ -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 })
}) })
+12
View File
@@ -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' })
+4 -4
View File
@@ -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 对齐)
+51 -19
View File
@@ -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; }