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:
parent
eb1027b040
commit
888824ba85
|
|
@ -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 / 文件预览缩放加固 + 双击复位提示
|
### 2026-06-15 / 文件预览缩放加固 + 双击复位提示
|
||||||
|
|
||||||
- 图片 load 完即量基准尺寸(`_captureBase`,免首次缩放时还没渲染量到 0px 导致塌成 0);基准未量到时本次缩放跳过不破坏;双击复位时徽标显式提示「已复位 · 100%」(停留 1.4s)。bump 0.12.11 → 0.12.12。
|
- 图片 load 完即量基准尺寸(`_captureBase`,免首次缩放时还没渲染量到 0px 导致塌成 0);基准未量到时本次缩放跳过不破坏;双击复位时徽标显式提示「已复位 · 100%」(停留 1.4s)。bump 0.12.11 → 0.12.12。
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||||
# 改版本只动这一行。
|
# 改版本只动这一行。
|
||||||
__version__ = "0.12.12"
|
__version__ = "0.12.13"
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ function _applyZoom(z, hint) {
|
||||||
img.style.maxHeight = "";
|
img.style.maxHeight = "";
|
||||||
img.style.width = "";
|
img.style.width = "";
|
||||||
img.style.height = "";
|
img.style.height = "";
|
||||||
img.style.cursor = "zoom-in";
|
img.style.cursor = ""; // 100%:普通箭头(左键不缩放,放大镜会误导)
|
||||||
} else {
|
} else {
|
||||||
// 以 scale=1 时的"贴合显示"尺寸为基准,给显式像素尺寸。
|
// 以 scale=1 时的"贴合显示"尺寸为基准,给显式像素尺寸。
|
||||||
// 不用 CSS zoom:图片是带 max-width/height:100% 的 flex item,zoom 放大后会被
|
// 不用 CSS zoom:图片是带 max-width/height:100% 的 flex item,zoom 放大后会被
|
||||||
|
|
@ -94,7 +94,7 @@ function _applyZoom(z, hint) {
|
||||||
img.style.maxHeight = "none";
|
img.style.maxHeight = "none";
|
||||||
img.style.width = Math.round(z.baseW * z.scale) + "px";
|
img.style.width = Math.round(z.baseW * z.scale) + "px";
|
||||||
img.style.height = Math.round(z.baseH * 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.textContent = hint || (Math.round(z.scale * 100) + "%");
|
||||||
z.badge.classList.add("show");
|
z.badge.classList.add("show");
|
||||||
|
|
@ -111,7 +111,7 @@ function _makeImageZoomable(bodyEl, img) {
|
||||||
card.appendChild(badge);
|
card.appendChild(badge);
|
||||||
const z = { img, scale: 1, badge, timer: null, baseW: 0, baseH: 0 };
|
const z = { img, scale: 1, badge, timer: null, baseW: 0, baseH: 0 };
|
||||||
_zoomState.set(bodyEl, z);
|
_zoomState.set(bodyEl, z);
|
||||||
img.style.cursor = "zoom-in";
|
img.draggable = false; // 关掉浏览器原生图片拖拽(否则拖出 ghost image)
|
||||||
// 图片一加载完就量好贴合尺寸做基准,避免首次缩放时还没渲染量到 0
|
// 图片一加载完就量好贴合尺寸做基准,避免首次缩放时还没渲染量到 0
|
||||||
if (img.complete) _captureBase(z);
|
if (img.complete) _captureBase(z);
|
||||||
else img.addEventListener("load", () => _captureBase(z), { once: true });
|
else img.addEventListener("load", () => _captureBase(z), { once: true });
|
||||||
|
|
@ -120,12 +120,37 @@ function _makeImageZoomable(bodyEl, img) {
|
||||||
z.scale = 1;
|
z.scale = 1;
|
||||||
_applyZoom(z, "已复位 · 100%");
|
_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) {
|
function _clearZoom(bodyEl) {
|
||||||
const z = _zoomState.get(bodyEl);
|
const z = _zoomState.get(bodyEl);
|
||||||
if (!z) return;
|
if (!z) return;
|
||||||
if (z.timer) clearTimeout(z.timer);
|
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();
|
z.badge.remove();
|
||||||
_zoomState.delete(bodyEl);
|
_zoomState.delete(bodyEl);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue