From f57eb55328827ac0907479c34147b6fa367a13c6 Mon Sep 17 00:00:00 2001 From: Jz0ojiang Date: Wed, 12 Aug 2026 13:17:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=96=E5=A3=B3=EF=BC=9A=E4=BE=A7=E6=A0=8F?= =?UTF-8?q?=E6=94=B9=20fixed=EF=BC=8C=E8=AF=B7=E6=B1=82=E9=A1=B5=E6=94=B9?= =?UTF-8?q?=E5=90=8D=E6=97=A5=E5=BF=97=EF=BC=8C=E9=94=99=E8=AF=AF=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E7=82=B9=E5=87=BB=E5=A4=8D=E5=88=B6=EF=BC=8C=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E9=94=81=E6=BB=9A=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 侧栏从 sticky 改 fixed:sticky 的位置跟着 scrollTop 走,滚到底时 scrollTop 是 scrollHeight 减 clientHeight,在 Windows 显示缩放下是 小数,部分图标会跳 1px;fixed 按视口摆放,不参与这个运算。网格第一列 只当占位,主区显式站到第二列。 错误提示是拿去粘贴排查的,点击改成复制而不是关闭,关闭交给自动消失; 局域网 http 下没有 clipboard API,退回 execCommand。撤回/重做各加 2.5 秒冷却,窗内连点算一次,顺带避开重做备份名的同秒覆盖。弹窗打开 时锁住背景滚动,滚动条位置由 scrollbar-gutter 常驻,不横向跳。 --- web/App.vue | 39 +++++++++++++++++++++++++------- web/components/ToastHost.vue | 43 ++++++++++++++++++++++++++++++++---- web/nav.ts | 4 ++-- web/styles/tokens.css | 6 +++++ 4 files changed, 78 insertions(+), 14 deletions(-) diff --git a/web/App.vue b/web/App.vue index caf0d74..81b6a40 100644 --- a/web/App.vue +++ b/web/App.vue @@ -100,8 +100,24 @@ const busyAct = ref(false) const historyOpen = ref(false) const askRestart = ref(false) +/** + * 撤回/重做各自的冷却:窗内的连点算一次。手抖不该多退一步;顺带避开 + * 重做的备份名同秒覆盖(备份名精确到秒,窗比一秒宽就撞不上)。 + * 两个方向分开冷却,撤回错了马上重做这条路不受影响。 + */ +const HISTORY_COOLDOWN_MS = 2_500 +const cooling = ref({ undo: false, redo: false }) + +function coolDown(kind: 'undo' | 'redo') { + cooling.value = { ...cooling.value, [kind]: true } + window.setTimeout(() => { + cooling.value = { ...cooling.value, [kind]: false } + }, HISTORY_COOLDOWN_MS) +} + async function undo() { - if (busyAct.value) return + if (busyAct.value || cooling.value.undo) return + coolDown('undo') busyAct.value = true try { const result = await api.undoConfig() @@ -115,7 +131,8 @@ async function undo() { } async function redo() { - if (busyAct.value) return + if (busyAct.value || cooling.value.redo) return + coolDown('redo') busyAct.value = true try { const result = await api.redoConfig() @@ -239,7 +256,7 @@ const current = computed(() => NAV.find((n) => n.key === active.value)) type="button" class="act" title="撤回" - :disabled="busyAct || !snapshot || history.backups.length === 0" + :disabled="busyAct || cooling.undo || !snapshot || history.backups.length === 0" @click="undo" > @@ -248,7 +265,7 @@ const current = computed(() => NAV.find((n) => n.key === active.value)) type="button" class="act" title="重做" - :disabled="busyAct || !snapshot || history.redo === 0" + :disabled="busyAct || cooling.redo || !snapshot || history.redo === 0" @click="redo" > @@ -411,14 +428,18 @@ const current = computed(() => NAV.find((n) => n.key === active.value)) /* ── 侧栏 ───────────────────────────────────── */ /* - * 钉在视口里。不加 sticky 的话它会被网格行撑到和主区一样高,页面一长, - * 导航就跟着滚出屏幕外,而它是全局导航,任何时候都该够得着。 + * 钉在视口里。用 fixed 而不是 sticky:sticky 元素的位置跟着 scrollTop 走, + * 而滚动到底时 scrollTop = scrollHeight - clientHeight,在 Windows 显示缩放下 + * 是小数——侧栏落在小数像素上,图标和文字取整方向不同,部分图标会跳 1px。 + * fixed 按视口摆放,不参与滚动偏移的运算。网格的第一列只当占位。 */ .side { - position: sticky; + position: fixed; top: 0; - align-self: start; + left: 0; + width: var(--side-w); height: 100vh; + transition: width 0.3s ease; display: flex; flex-direction: column; gap: var(--s-5); @@ -696,7 +717,9 @@ const current = computed(() => NAV.find((n) => n.key === active.value)) /* ── 主区 ───────────────────────────────────── */ +/* 侧栏是 fixed、不占网格位,主区要自己站到第二列,否则会顶进第一列 */ .main { + grid-column: 2; padding: var(--s-6) var(--s-6) var(--s-7); max-width: 1600px; } diff --git a/web/components/ToastHost.vue b/web/components/ToastHost.vue index 384238d..ee98ec1 100644 --- a/web/components/ToastHost.vue +++ b/web/components/ToastHost.vue @@ -1,12 +1,45 @@