外壳:侧栏改 fixed,请求页改名日志,错误提示点击复制,弹窗锁滚动

侧栏从 sticky 改 fixed:sticky 的位置跟着 scrollTop 走,滚到底时
scrollTop 是 scrollHeight 减 clientHeight,在 Windows 显示缩放下是
小数,部分图标会跳 1px;fixed 按视口摆放,不参与这个运算。网格第一列
只当占位,主区显式站到第二列。

错误提示是拿去粘贴排查的,点击改成复制而不是关闭,关闭交给自动消失;
局域网 http 下没有 clipboard API,退回 execCommand。撤回/重做各加
2.5 秒冷却,窗内连点算一次,顺带避开重做备份名的同秒覆盖。弹窗打开
时锁住背景滚动,滚动条位置由 scrollbar-gutter 常驻,不横向跳。
This commit is contained in:
2026-08-12 13:17:35 +08:00
parent 129a76696b
commit f57eb55328
4 changed files with 78 additions and 14 deletions
+31 -8
View File
@@ -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"
>
<IconUndo />
@@ -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"
>
<IconRedo />
@@ -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;
}