反馈改右上角轻提示;侧栏加撤回重做等配置动作

设置页的横幅换成 toast(右滑入、几秒后上移淡出,web/toast.ts 单例):
动作结果看一眼就行,不值得在页面里占一块常驻位置。表单校验类的错误
仍留在各自表单里。

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

设置页顺手修三处:GEO 行的编辑按钮在输入框开着时再点会闪(blur 关了
click 又开),mousedown.prevent 保焦点、click 显式开关;上传按钮改
「选择或拖放文件」,投放区拖入高亮;恢复策略挪到恢复行下面。
This commit is contained in:
2026-08-12 01:59:22 +08:00
parent 2580c03bfa
commit f761d13579
9 changed files with 798 additions and 106 deletions
+181
View File
@@ -0,0 +1,181 @@
<script setup lang="ts">
/**
* 备份链(历史记录)的弹窗:列出每次改动前留下的备份,点还原直接跳回去。
* 还原走服务端同一条保护链路,当前状态会先自动备份,所以不需要再套一层确认。
*/
import { ref, watch } from 'vue'
import { backupTime } from '@shared/backups.ts'
import { api } from '../api'
import { toast, toastError } from '../toast'
import IconClose from '~icons/ri/close-line'
const props = defineProps<{ open: boolean }>()
/** restored:还原成功,外壳该重读配置了。 */
const emit = defineEmits<{ close: []; restored: [] }>()
const dialog = ref<HTMLDialogElement | null>(null)
const backups = ref<string[] | null>(null)
const busy = ref(false)
watch(
() => props.open,
(open) => {
if (open) {
dialog.value?.showModal()
backups.value = null
void api
.listBackups()
.then((r) => (backups.value = r.backups))
.catch(() => (backups.value = []))
} else {
dialog.value?.close()
}
},
)
async function restore(name: string) {
if (busy.value) return
busy.value = true
try {
const result = await api.restoreBackup(name)
const time = backupTime(name) ?? name
toast(result.reloaded ? `已还原到 ${time}` : `已还原到 ${time}(${result.reloadMessage})`)
emit('restored')
emit('close')
} catch (error) {
toastError(error)
} finally {
busy.value = false
}
}
/** 点在遮罩上就关闭。内容区的点击 target 是里面的元素,到不了 dialog 本身。 */
function onBackdrop(event: MouseEvent) {
if (event.target === dialog.value) emit('close')
}
</script>
<template>
<dialog
ref="dialog"
class="dlg"
@click="onBackdrop"
@close="emit('close')"
@cancel.prevent="emit('close')"
>
<div class="body">
<header class="head">
<h2>历史记录</h2>
<button type="button" class="x" title="关闭" @click="emit('close')"><IconClose /></button>
</header>
<p v-if="backups === null" class="faint">载入中</p>
<p v-else-if="backups.length === 0" class="faint">
没有备份。每次保存都会把改动前的状态留在这里,最多 10 份。
</p>
<ul v-else class="list">
<li v-for="name in backups" :key="name" class="item" :title="name">
<span class="mono time">{{ backupTime(name) ?? name }}</span>
<button type="button" class="btn" :disabled="busy" @click="restore(name)">还原</button>
</li>
</ul>
</div>
</dialog>
</template>
<style scoped>
.dlg {
width: min(420px, calc(100vw - 2 * var(--s-5)));
padding: 0;
border: 1px solid var(--c-border-strong);
border-radius: var(--r-lg);
background: var(--c-surface);
color: var(--c-text);
}
.dlg::backdrop {
background: rgb(0 0 0 / 0.45);
}
.body {
padding: var(--s-5);
}
.head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--s-3);
}
.head h2 {
font-size: var(--t-lg);
font-weight: 600;
}
.x {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 1px solid transparent;
border-radius: var(--r-sm);
background: none;
color: var(--c-text-faint);
cursor: pointer;
}
.x:hover {
background: var(--c-surface-sunken);
color: var(--c-text);
}
.faint {
color: var(--c-text-faint);
font-size: var(--t-sm);
}
.list {
list-style: none;
margin: 0;
padding: 0;
max-height: 50vh;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--c-border-strong) transparent;
}
.item {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--s-4);
padding: var(--s-2) 0;
}
.item + .item {
border-top: 1px solid var(--c-border);
}
.time {
font-size: var(--t-sm);
}
.btn {
padding: var(--s-1) var(--s-3);
border: 1px solid var(--c-border-strong);
border-radius: var(--r-sm);
background: var(--c-surface);
font-size: var(--t-sm);
cursor: pointer;
}
.btn:hover:not(:disabled) {
border-color: var(--c-accent);
color: var(--c-accent-text);
}
.btn:disabled {
color: var(--c-text-faint);
cursor: not-allowed;
}
</style>