侧栏动作排撤掉重启图标,内核状态(圆点加版本号)本身变成按钮:悬停 换层显示「重启内核」,不在线时只当状态看。行高压成 1 修掉圆点和等宽 字体不居中的问题。 四个页面的一次性反馈(保存、删除、更新订阅、断开连接、测延迟结论) 全部改走 toast,commit() 改返回成败给「失败不关对话框」用;持续状态 (连接页轮询失败、请求页日志流失败)留在页内,会自动消失的提示装不下 需要一直可见的信息。toast 加 info 类别给中性结论用。 GEO 每行的更新改成单独下载那一个文件:内核只有全量更新的口子,单文件 只能服务端自己下(POST /api/geo/sync),代价是只有形状校验(MaxMind 末尾标记、geodata 首字节,shared/geo.ts 带测试)且内核重启后才换用新 文件;「更新全部」文字按钮保留内核的全量口子,有完整校验和热加载。 上传两行的按钮改「选择或拖放文件」,投放区拖入高亮,恢复策略挪到 恢复行下方。
128 lines
2.7 KiB
Vue
128 lines
2.7 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* 轻提示的渲染宿主,挂在外壳根部。进场从右滑入,退场上移淡出;
|
|
* 点一下立刻消掉。内容来自 web/toast.ts 的单例队列。
|
|
*/
|
|
import { dismiss, toasts } from '../toast'
|
|
import IconOk from '~icons/ri/checkbox-circle-line'
|
|
import IconErr from '~icons/ri/error-warning-line'
|
|
import IconInfo from '~icons/ri/information-line'
|
|
</script>
|
|
|
|
<template>
|
|
<div class="host" aria-live="polite">
|
|
<TransitionGroup name="tst">
|
|
<div
|
|
v-for="t in toasts"
|
|
:key="t.id"
|
|
:class="['tst', t.kind === 'error' && 'is-error']"
|
|
@click="dismiss(t.id)"
|
|
>
|
|
<IconErr v-if="t.kind === 'error'" class="tico" />
|
|
<IconInfo v-else-if="t.kind === 'info'" class="tico is-info" />
|
|
<IconOk v-else class="tico" />
|
|
<div class="tbody">
|
|
<strong class="tmsg">{{ t.message }}</strong>
|
|
<span v-if="t.hint" class="thint">{{ t.hint }}</span>
|
|
</div>
|
|
</div>
|
|
</TransitionGroup>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.host {
|
|
position: fixed;
|
|
top: var(--s-4);
|
|
right: var(--s-4);
|
|
z-index: 300;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-end;
|
|
gap: var(--s-2);
|
|
/* 宿主自己不挡点击,字条才挡 */
|
|
pointer-events: none;
|
|
}
|
|
|
|
.tst {
|
|
pointer-events: auto;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: var(--s-3);
|
|
min-width: 240px;
|
|
max-width: 380px;
|
|
padding: var(--s-3) var(--s-4);
|
|
border: 1px solid var(--c-border-strong);
|
|
border-radius: var(--r-md);
|
|
background: var(--c-surface);
|
|
box-shadow: 0 6px 24px rgb(0 0 0 / 0.16);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.tst.is-error {
|
|
border-color: color-mix(in srgb, var(--c-danger) 35%, var(--c-border));
|
|
}
|
|
|
|
.tico {
|
|
flex: none;
|
|
width: 18px;
|
|
height: 18px;
|
|
margin-top: 1px;
|
|
color: var(--c-ok);
|
|
}
|
|
|
|
.is-error .tico {
|
|
color: var(--c-danger);
|
|
}
|
|
|
|
.tico.is-info {
|
|
color: var(--c-text-soft);
|
|
}
|
|
|
|
.tbody {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--s-1);
|
|
min-width: 0;
|
|
}
|
|
|
|
.tmsg {
|
|
font-size: var(--t-sm);
|
|
font-weight: 500;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.thint {
|
|
font-size: var(--t-xs);
|
|
color: var(--c-text-soft);
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
/* 进场:从右滑入;退场:上移淡出;队列压缩时其余字条平滑归位 */
|
|
.tst-enter-active {
|
|
transition:
|
|
transform 0.28s cubic-bezier(0.21, 0.8, 0.35, 1),
|
|
opacity 0.28s ease;
|
|
}
|
|
.tst-enter-from {
|
|
transform: translateX(120%);
|
|
opacity: 0;
|
|
}
|
|
.tst-leave-active {
|
|
transition:
|
|
transform 0.4s ease,
|
|
opacity 0.4s ease;
|
|
/* 退场中脱离布局,后面的字条立刻开始归位 */
|
|
position: absolute;
|
|
right: 0;
|
|
}
|
|
.tst-leave-to {
|
|
transform: translateY(-14px);
|
|
opacity: 0;
|
|
}
|
|
.tst-move {
|
|
transition: transform 0.3s ease;
|
|
}
|
|
</style>
|