Add plugin 截图置顶 v0.1.0#248
Conversation
- docs: add screenshot pin design and implementation plan - chore: scaffold ztools screenshot plugin - test: add screenshot geometry primitives - test: add screenshot route and storage state - test: add typed ztools bridge - feat: add capture overlay and pin windows - feat: launch screenshot capture from ztools entry - fix: harden screenshot pin lifecycle - chore: package ztools plugin for installation - fix: stabilize screenshot pin packaging and dragging - fix: ignore nested worktrees in test discovery - 添加README
There was a problem hiding this comment.
Code Review
This pull request introduces a new ZTools plugin, '截图置顶' (top-screenshot), built with Vue 3, TypeScript, and Vite, which allows users to capture screen regions and display them as always-on-top, draggable, and zoomable windows. Feedback on the implementation highlights a critical bug in CaptureView.vue where the screenshot image (<img>) is missing from the template, rendering the overlay transparent and breaking the capture flow; this also requires updating the corresponding test in captureView.test.ts. Additionally, improvements are suggested for PinView.vue to bind drag events to the window rather than the main element for smoother dragging, and to properly clean up the beforeunload event listener in onBeforeUnmount to prevent potential memory leaks.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| <main class="capture-view" tabindex="0" @mousedown="onMouseDown" @mousemove="onMouseMove" @mouseup="onMouseUp"> | ||
| <div | ||
| v-if="selection" | ||
| class="selection-box" | ||
| :style="{ | ||
| left: `${selection.x}px`, | ||
| top: `${selection.y}px`, | ||
| width: `${selection.width}px`, | ||
| height: `${selection.height}px`, | ||
| }" | ||
| /> | ||
| <p v-if="!snapshot" class="capture-error">没有找到当前显示器截图。</p> | ||
| </main> |
There was a problem hiding this comment.
在 CaptureView.vue 的模板中,缺少了用于渲染屏幕截图的 <img> 标签。这会导致截图覆盖层打开时完全透明,用户无法看到自己正在框选和裁剪的画面,从而无法进行正确的截图操作。建议将 snapshot.imageDataUrl 渲染为背景图片,以恢复截图视角。
<main class="capture-view" tabindex="0" @mousedown="onMouseDown" @mousemove="onMouseMove" @mouseup="onMouseUp">
<img v-if="snapshot" class="capture-image" :src="snapshot.imageDataUrl" alt="屏幕截图" draggable="false" />
<div
v-if="selection"
class="selection-box"
:style="{
left: `${selection.x}px`,
top: `${selection.y}px`,
width: `${selection.width}px`,
height: `${selection.height}px`,
}"
/>
<p v-if="!snapshot" class="capture-error">没有找到当前显示器截图。</p>
</main>
| function onMouseDown(event: MouseEvent): void { | ||
| if (!pinState.value || event.button !== 0) { | ||
| return; | ||
| } | ||
|
|
||
| dragStart.value = { x: event.screenX, y: event.screenY }; | ||
| dragStartBounds.value = pinState.value.currentBounds; | ||
| activate(); | ||
| } | ||
|
|
||
| function onMouseMove(event: MouseEvent): void { | ||
| if (!pinState.value || !dragStart.value || !dragStartBounds.value) { | ||
| return; | ||
| } | ||
|
|
||
| const deltaX = event.screenX - dragStart.value.x; | ||
| const deltaY = event.screenY - dragStart.value.y; | ||
| const nextImageBounds = translateRect(dragStartBounds.value, deltaX, deltaY); | ||
| const nextState = { | ||
| ...pinState.value, | ||
| currentBounds: nextImageBounds, | ||
| lastActiveAt: Date.now(), | ||
| }; | ||
|
|
||
| persist(nextState); | ||
| applyWindowBounds(nextImageBounds); | ||
| } | ||
|
|
||
| function onMouseUp(): void { | ||
| dragStart.value = null; | ||
| dragStartBounds.value = null; | ||
| } |
There was a problem hiding this comment.
在 PinView.vue 中,拖拽事件监听器(mousemove 和 mouseup)是直接绑定在 main 元素上的。由于置顶窗口会被缩放和调整大小,当用户快速拖拽鼠标时,鼠标指针很容易离开窗口范围,导致拖拽事件停止触发、窗口卡住。建议在 mousedown 时将这些事件动态绑定到 window 上,并在 mouseup 时移除,以提供更平滑、稳定的拖拽体验。
function onMouseDown(event: MouseEvent): void {
if (!pinState.value || event.button !== 0) {
return;
}
dragStart.value = { x: event.screenX, y: event.screenY };
dragStartBounds.value = pinState.value.currentBounds;
activate();
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
}
function onMouseMove(event: MouseEvent): void {
if (!pinState.value || !dragStart.value || !dragStartBounds.value) {
return;
}
const deltaX = event.screenX - dragStart.value.x;
const deltaY = event.screenY - dragStart.value.y;
const nextImageBounds = translateRect(dragStartBounds.value, deltaX, deltaY);
const nextState = {
...pinState.value,
currentBounds: nextImageBounds,
lastActiveAt: Date.now(),
};
persist(nextState);
applyWindowBounds(nextImageBounds);
}
function onMouseUp(): void {
dragStart.value = null;
dragStartBounds.value = null;
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
}
| it('does not render the captured screen image behind the selection overlay', async () => { | ||
| const session: CaptureSession = { | ||
| id: 'session-1', | ||
| createdAt: 1780898400000, | ||
| completed: false, | ||
| displays: [ | ||
| { | ||
| displayId: 'display-1', | ||
| bounds: { x: 100, y: 80, width: 800, height: 600 }, | ||
| imageDataUrl: 'data:image/png;base64,screen', | ||
| scaleFactor: 1, | ||
| }, | ||
| ], | ||
| }; | ||
| window.localStorage.setItem('capture-session:session-1', JSON.stringify(session)); | ||
|
|
||
| const { default: CaptureView } = await import('../src/views/CaptureView.vue'); | ||
| const wrapper = mount(CaptureView); | ||
|
|
||
| expect(wrapper.find('.capture-image').exists()).toBe(false); | ||
| }); |
There was a problem hiding this comment.
由于 CaptureView.vue 中漏掉了 <img> 标签,测试用例 does not render the captured screen image behind the selection overlay 错误地断言了 .capture-image 不存在。为了保证截图功能的正确性,应该渲染该图片,并在此测试中将其断言修改为存在。
it('renders the captured screen image behind the selection overlay', async () => {
const session: CaptureSession = {
id: 'session-1',
createdAt: 1780898400000,
completed: false,
displays: [
{
displayId: 'display-1',
bounds: { x: 100, y: 80, width: 800, height: 600 },
imageDataUrl: 'data:image/png;base64,screen',
scaleFactor: 1,
},
],
};
window.localStorage.setItem('capture-session:session-1', JSON.stringify(session));
const { default: CaptureView } = await import('../src/views/CaptureView.vue');
const wrapper = mount(CaptureView);
expect(wrapper.find('.capture-image').exists()).toBe(true);
expect(wrapper.find('.capture-image').attributes('src')).toBe('data:image/png;base64,screen');
});| <main | ||
| v-if="pinState" | ||
| class="pin-window" | ||
| tabindex="0" | ||
| @mousedown="onMouseDown" | ||
| @mousemove="onMouseMove" | ||
| @mouseup="onMouseUp" | ||
| @wheel="onWheel" | ||
| > |
| window.addEventListener('keydown', onKeyDown); | ||
| window.addEventListener('blur', onMouseUp); | ||
| window.addEventListener('beforeunload', () => removePinWindow(window.localStorage, pinId)); | ||
| onBeforeUnmount(() => { | ||
| window.removeEventListener('keydown', onKeyDown); | ||
| window.removeEventListener('blur', onMouseUp); | ||
| }); | ||
| </script> |
There was a problem hiding this comment.
在 PinView.vue 中,beforeunload 事件监听器使用了匿名箭头函数绑定到 window 上,但在 onBeforeUnmount 中没有进行清理。这会导致潜在的内存泄漏。此外,如果应用了将拖拽事件绑定到 window 的优化,也应该在组件卸载时一并清理 mousemove 和 mouseup 监听器。建议将 beforeunload 提取为命名函数,并在 onBeforeUnmount 中进行完整的清理。
const handleBeforeUnload = () => {
removePinWindow(window.localStorage, pinId);
};
window.addEventListener('keydown', onKeyDown);
window.addEventListener('blur', onMouseUp);
window.addEventListener('beforeunload', handleBeforeUnload);
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('blur', onMouseUp);
window.removeEventListener('beforeunload', handleBeforeUnload);
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
});
- docs: add screenshot pin design and implementation plan - chore: scaffold ztools screenshot plugin - test: add screenshot geometry primitives - test: add screenshot route and storage state - test: add typed ztools bridge - feat: add capture overlay and pin windows - feat: launch screenshot capture from ztools entry - fix: harden screenshot pin lifecycle - chore: package ztools plugin for installation - fix: stabilize screenshot pin packaging and dragging - fix: ignore nested worktrees in test discovery - 添加README - 添加插件作者字段
- docs: add screenshot pin design and implementation plan - chore: scaffold ztools screenshot plugin - test: add screenshot geometry primitives - test: add screenshot route and storage state - test: add typed ztools bridge - feat: add capture overlay and pin windows - feat: launch screenshot capture from ztools entry - fix: harden screenshot pin lifecycle - chore: package ztools plugin for installation - fix: stabilize screenshot pin packaging and dragging - fix: ignore nested worktrees in test discovery - 添加README - 添加插件作者字段 - 修改插件命令描述
- docs: add screenshot pin design and implementation plan - chore: scaffold ztools screenshot plugin - test: add screenshot geometry primitives - test: add screenshot route and storage state - test: add typed ztools bridge - feat: add capture overlay and pin windows - feat: launch screenshot capture from ztools entry - fix: harden screenshot pin lifecycle - chore: package ztools plugin for installation - fix: stabilize screenshot pin packaging and dragging - fix: ignore nested worktrees in test discovery - 添加README - 添加插件作者字段 - 修改插件命令描述 - 在无截图被置顶的时候主动关闭进程节省资源 修复bug
|
感谢pr,经测试执行build之后dist内构建不完整,需要复制plugin.json preload.js icon 等到dist目录,需要dist压缩为zip拖动到ztools可以安装才行 |

插件信息
本次变更
截图 / 演示
自检清单
plugins/top-screenshot/目录此 PR 由 ztools-plugin-cli 自动管理:每次
ztools publish在分支上追加一个 commit,PR 链接保持不变。