Files
mihomo-patchbay/web/components/ToastHost.vue
T
im0o f761d13579 反馈改右上角轻提示;侧栏加撤回重做等配置动作
设置页的横幅换成 toast(右滑入、几秒后上移淡出,web/toast.ts 单例):
动作结果看一眼就行,不值得在页面里占一块常驻位置。表单校验类的错误
仍留在各自表单里。

撤回栈就是备份链:撤回把最新备份应用回去并消费掉它,撤回前的状态进
重做栈(.patchbay-redo,同样留 10 份);重做反向;新的正常保存清空
重做栈,不留分叉历史。这两条路复用 applyConfig 的保护链路,用
restoreFrom 参数跳过自建备份——否则备份链会被撤回本身污染,撤两次
等于原地打转。侧栏左下角一排图标:撤回、重做、历史记录(备份列表
弹窗)、更新配置(POST /api/kernel/reload,给 SSH 手改后用)、重启
内核;收拢时容器变窄自动折成一列。

设置页顺手修三处:GEO 行的编辑按钮在输入框开着时再点会闪(blur 关了
click 又开),mousedown.prevent 保焦点、click 显式开关;上传按钮改
「选择或拖放文件」,投放区拖入高亮;恢复策略挪到恢复行下面。
2026-08-12 01:59:22 +08:00

122 lines
2.6 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'
</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" />
<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);
}
.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>