feat(web): 图片预览放大后左键拖动平移 + 光标语义改正 + bump 0.12.13

- 光标:100% 改普通箭头(原 zoom-in 放大镜误导,左键不缩放);放大后 grab、
  拖动中 grabbing。
- 左键拖动平移:放大态 mousedown 记起点+滚动位,mousemove 改 body
  scrollLeft/Top 平移;img.draggable=false 关原生拖拽。document move/up
  监听存 z._onMove/_onUp,_clearZoom 时移除避免泄漏。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-06-15 10:00:15 +08:00
parent eb1027b040
commit 888824ba85
3 changed files with 34 additions and 4 deletions

View File

@ -21,6 +21,11 @@
## 已完成关键能力
### 2026-06-15 / 图片预览:左键拖动平移 + 光标语义改正
- 光标:100% 时改回普通箭头(原 `zoom-in` 放大镜误导 —— 左键不缩放,缩放是 Ctrl+滚轮);放大后改 `grab`、拖动中 `grabbing`,贴合"可拖"语义。
- 左键拖动平移:放大态下 mousedown 记起点 + body 滚动位,mousemove 改 `bodyEl.scrollLeft/Top` 平移看局部(替代拖滚动条);`img.draggable=false` 关原生 ghost 拖拽。document 上的 move/up 监听存 `z._onMove/_onUp`,`_clearZoom` 时移除避免泄漏。bump 0.12.12 → 0.12.13。
### 2026-06-15 / 文件预览缩放加固 + 双击复位提示
- 图片 load 完即量基准尺寸(`_captureBase`,免首次缩放时还没渲染量到 0px 导致塌成 0);基准未量到时本次缩放跳过不破坏;双击复位时徽标显式提示「已复位 · 100%」(停留 1.4s)。bump 0.12.11 → 0.12.12。

View File

@ -1,3 +1,3 @@
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
# 改版本只动这一行。
__version__ = "0.12.12"
__version__ = "0.12.13"

View File

@ -84,7 +84,7 @@ function _applyZoom(z, hint) {
img.style.maxHeight = "";
img.style.width = "";
img.style.height = "";
img.style.cursor = "zoom-in";
img.style.cursor = ""; // 100%:普通箭头(左键不缩放,放大镜会误导)
} else {
// 以 scale=1 时的"贴合显示"尺寸为基准,给显式像素尺寸。
// 不用 CSS zoom:图片是带 max-width/height:100% 的 flex item,zoom 放大后会被
@ -94,7 +94,7 @@ function _applyZoom(z, hint) {
img.style.maxHeight = "none";
img.style.width = Math.round(z.baseW * z.scale) + "px";
img.style.height = Math.round(z.baseH * z.scale) + "px";
img.style.cursor = "zoom-out";
img.style.cursor = "grab"; // 放大后可左键拖动平移
}
z.badge.textContent = hint || (Math.round(z.scale * 100) + "%");
z.badge.classList.add("show");
@ -111,7 +111,7 @@ function _makeImageZoomable(bodyEl, img) {
card.appendChild(badge);
const z = { img, scale: 1, badge, timer: null, baseW: 0, baseH: 0 };
_zoomState.set(bodyEl, z);
img.style.cursor = "zoom-in";
img.draggable = false; // 关掉浏览器原生图片拖拽(否则拖出 ghost image)
// 图片一加载完就量好贴合尺寸做基准,避免首次缩放时还没渲染量到 0
if (img.complete) _captureBase(z);
else img.addEventListener("load", () => _captureBase(z), { once: true });
@ -120,12 +120,37 @@ function _makeImageZoomable(bodyEl, img) {
z.scale = 1;
_applyZoom(z, "已复位 · 100%");
});
// 放大后左键拖动平移:本质是改 body 的滚动位置(图片此时已撑大、body 可滚)
let dragging = false, sx = 0, sy = 0, sl = 0, st = 0;
img.addEventListener("mousedown", (e) => {
if (e.button !== 0 || z.scale === 1) return; // 仅左键、仅放大态
e.preventDefault();
dragging = true;
sx = e.clientX; sy = e.clientY;
sl = bodyEl.scrollLeft; st = bodyEl.scrollTop;
img.style.cursor = "grabbing";
});
z._onMove = (e) => {
if (!dragging) return;
bodyEl.scrollLeft = sl - (e.clientX - sx);
bodyEl.scrollTop = st - (e.clientY - sy);
};
z._onUp = () => {
if (!dragging) return;
dragging = false;
img.style.cursor = z.scale === 1 ? "" : "grab";
};
document.addEventListener("mousemove", z._onMove);
document.addEventListener("mouseup", z._onUp);
}
function _clearZoom(bodyEl) {
const z = _zoomState.get(bodyEl);
if (!z) return;
if (z.timer) clearTimeout(z.timer);
if (z._onMove) document.removeEventListener("mousemove", z._onMove);
if (z._onUp) document.removeEventListener("mouseup", z._onUp);
z.badge.remove();
_zoomState.delete(bodyEl);
}