轻提示铺开到全部页面;内核状态悬停变重启;GEO 支持单文件下载

侧栏动作排撤掉重启图标,内核状态(圆点加版本号)本身变成按钮:悬停
换层显示「重启内核」,不在线时只当状态看。行高压成 1 修掉圆点和等宽
字体不居中的问题。

四个页面的一次性反馈(保存、删除、更新订阅、断开连接、测延迟结论)
全部改走 toast,commit() 改返回成败给「失败不关对话框」用;持续状态
(连接页轮询失败、请求页日志流失败)留在页内,会自动消失的提示装不下
需要一直可见的信息。toast 加 info 类别给中性结论用。

GEO 每行的更新改成单独下载那一个文件:内核只有全量更新的口子,单文件
只能服务端自己下(POST /api/geo/sync),代价是只有形状校验(MaxMind
末尾标记、geodata 首字节,shared/geo.ts 带测试)且内核重启后才换用新
文件;「更新全部」文字按钮保留内核的全量口子,有完整校验和热加载。
上传两行的按钮改「选择或拖放文件」,投放区拖入高亮,恢复策略挪到
恢复行下方。
This commit is contained in:
2026-08-12 02:20:46 +08:00
parent f761d13579
commit b276c4a8fe
13 changed files with 366 additions and 304 deletions
+75 -7
View File
@@ -123,9 +123,12 @@ function geoMeta(g: GeoFileStatus): string {
return `${formatBytes(g.size)} · ${formatAgo(new Date(g.updatedAt), new Date())}`
}
/** GEO 更新是运行时动作,不写配置文件。内核只有全量更新的口子,四行共用一个忙态。 */
/**
* 更新全部:让内核自己更新(运行时动作,带内核的完整校验和热加载,
* 但只更新配置实际用到的类型)。
*/
async function updateGeo() {
if (geoBusy.value || !kernel.value.online) return
if (geoBusy.value || geoSyncing.value !== null || !kernel.value.online) return
geoBusy.value = true
try {
await api.updateGeo()
@@ -138,6 +141,24 @@ async function updateGeo() {
}
}
/** 正在单独下载的那一行(configKey) */
const geoSyncing = ref<string | null>(null)
/** 单独更新一个文件:服务端自己下载,内核重启后换用新文件。 */
async function syncOne(g: GeoFileStatus) {
if (geoBusy.value || geoSyncing.value !== null) return
geoSyncing.value = g.configKey
try {
await api.syncGeoFile(g.configKey)
toast(`${g.key} 已下载`, { hint: '内核还在用旧数据,重启内核后换用新文件。' })
await loadGeo()
} catch (error) {
toastError(error)
} finally {
geoSyncing.value = null
}
}
/** 正在编辑下载地址的那一行(configKey),一次只开一行 */
const geoEditing = ref<string | null>(null)
const geoDraft = ref('')
@@ -433,7 +454,18 @@ const backend = location.origin
</section>
<section class="group">
<h2 class="title">资源</h2>
<div class="g-head">
<h2 class="title">资源</h2>
<button
type="button"
class="tbtn"
:title="kernel.online ? '让内核更新它用到的全部 GEO 文件' : '内核未连接'"
:disabled="geoBusy || geoSyncing !== null || !kernel.online"
@click="updateGeo"
>
<IconSync :class="geoBusy && 'spin'" /> 更新全部
</button>
</div>
<p v-if="geoFiles === null" class="faint">载入中</p>
<p v-else-if="geoFiles.length === 0" class="faint">读不到 GEO 文件的状态。</p>
<template v-else>
@@ -455,11 +487,11 @@ const backend = location.origin
<button
type="button"
class="ico"
:title="kernel.online ? '更新(有变化的 GEO 文件会一并更新)' : '内核未连接'"
:disabled="geoBusy || !kernel.online"
@click="updateGeo"
title="单独下载这一个文件(重启内核后生效)"
:disabled="geoBusy || geoSyncing !== null"
@click="syncOne(g)"
>
<IconSync :class="geoBusy && 'spin'" />
<IconSync :class="geoSyncing === g.configKey && 'spin'" />
</button>
</span>
</div>
@@ -673,6 +705,42 @@ const backend = location.origin
}
/* ── 资源 ────────────────────────────────────── */
.g-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.g-head .title {
margin-bottom: var(--s-3);
}
/* 文字按钮:图标加一个词,不占按钮的份量 */
.tbtn {
display: inline-flex;
align-items: center;
gap: var(--s-2);
padding: var(--s-1) var(--s-2);
border: none;
border-radius: var(--r-sm);
background: none;
color: var(--c-text-soft);
font-size: var(--t-sm);
cursor: pointer;
}
.tbtn:hover:not(:disabled) {
background: var(--c-surface-sunken);
color: var(--c-text);
}
.tbtn:disabled {
color: var(--c-text-faint);
cursor: not-allowed;
}
.tbtn svg {
width: 14px;
height: 14px;
}
.g-right {
display: inline-flex;
align-items: center;