feat: 实现命令面板、颜色取色、JSON格式化和系统信息功能
- 重构项目架构,采用四层架构模式 (Command → Service → Platform → Utils) - 实现命令面板功能,支持快捷搜索和特征分类 - 添加颜色取色功能,支持屏幕像素颜色获取 - 添加JSON格式化功能,支持JSON格式化和压缩 - 添加系统信息功能,显示操作系统和硬件信息 - 移除旧的状态文档和无用配置文件
This commit is contained in:
257
picker.html
Normal file
257
picker.html
Normal file
@@ -0,0 +1,257 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>取色遮罩</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: transparent;
|
||||
cursor: crosshair; /* 关键:CSS 十字光标 */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
/* 添加一个极其透明的背景,确保能捕获鼠标事件 */
|
||||
#overlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* 保持完全透明,避免影响底层颜色的合成结果 */
|
||||
background-color: transparent;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#magnifier {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
background: rgba(20, 20, 20, 0.75);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
padding: 10px;
|
||||
width: 260px;
|
||||
height: 300px;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
#mag-canvas {
|
||||
width: 240px;
|
||||
height: 240px;
|
||||
border-radius: 10px;
|
||||
display: block;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
#mag-info {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
"Courier New", monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
#mag-swatch {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="overlay"></div>
|
||||
<div id="magnifier">
|
||||
<canvas id="mag-canvas" width="240" height="240"></canvas>
|
||||
<div id="mag-info">
|
||||
<div id="mag-hex">#------</div>
|
||||
<div id="mag-swatch"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
注意:此页面是 Vite 的独立入口,会被打包到 dist/picker.html。
|
||||
因此可以直接使用 @tauri-apps/api 的 ESM 导入。
|
||||
-->
|
||||
<script type="module">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
|
||||
const overlay = document.getElementById("overlay");
|
||||
const magnifier = document.getElementById("magnifier");
|
||||
const magCanvas = document.getElementById("mag-canvas");
|
||||
const magHex = document.getElementById("mag-hex");
|
||||
const magSwatch = document.getElementById("mag-swatch");
|
||||
const magCtx = magCanvas.getContext("2d", { willReadFrequently: false });
|
||||
const tmpCanvas = document.createElement("canvas");
|
||||
const tmpCtx = tmpCanvas.getContext("2d");
|
||||
// 初始隐藏到屏幕外,避免一开始挡住视线
|
||||
magnifier.style.transform = "translate(-9999px, -9999px)";
|
||||
|
||||
const CAPTURE_SIZE = 21; // 必须是奇数,中心点用于取色
|
||||
const SCALE = 10; // 放大倍率(21*10=210,canvas 240 留边)
|
||||
const DRAW_SIZE = CAPTURE_SIZE * SCALE;
|
||||
const OFFSET_X = 26;
|
||||
const OFFSET_Y = 26;
|
||||
|
||||
let lastClientX = 0;
|
||||
let lastClientY = 0;
|
||||
let lastScreenX = 0;
|
||||
let lastScreenY = 0;
|
||||
let pending = false;
|
||||
let lastCaptureAt = 0;
|
||||
|
||||
function clamp(v, min, max) {
|
||||
return Math.max(min, Math.min(max, v));
|
||||
}
|
||||
|
||||
function positionMagnifier(clientX, clientY) {
|
||||
const w = magnifier.offsetWidth || 260;
|
||||
const h = magnifier.offsetHeight || 300;
|
||||
const maxX = window.innerWidth - w - 8;
|
||||
const maxY = window.innerHeight - h - 8;
|
||||
|
||||
// 放到鼠标右下角,避免把被采样区域遮住(防止递归镜像)
|
||||
const x = clamp(clientX + OFFSET_X, 8, Math.max(8, maxX));
|
||||
const y = clamp(clientY + OFFSET_Y, 8, Math.max(8, maxY));
|
||||
magnifier.style.transform = `translate(${x}px, ${y}px)`;
|
||||
}
|
||||
|
||||
async function captureAndRender() {
|
||||
pending = false;
|
||||
if (!magCtx || !tmpCtx) return;
|
||||
|
||||
const now = performance.now();
|
||||
// 节流:最多 30 FPS
|
||||
if (now - lastCaptureAt < 33) return;
|
||||
lastCaptureAt = now;
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const px = Math.round(lastScreenX * dpr);
|
||||
const py = Math.round(lastScreenY * dpr);
|
||||
const half = Math.floor(CAPTURE_SIZE / 2);
|
||||
|
||||
try {
|
||||
const res = await invoke("capture_screen_region_rgba", {
|
||||
x: px - half,
|
||||
y: py - half,
|
||||
width: CAPTURE_SIZE,
|
||||
height: CAPTURE_SIZE,
|
||||
});
|
||||
|
||||
// res.data 是 RGBA Uint8 数组(通过 JSON 传输会变成 number[])
|
||||
const u8 = new Uint8ClampedArray(res.data);
|
||||
const imageData = new ImageData(u8, res.width, res.height);
|
||||
|
||||
// 先画到临时 canvas(原尺寸),再按像素风格放大(避免每帧创建对象)
|
||||
tmpCanvas.width = res.width;
|
||||
tmpCanvas.height = res.height;
|
||||
tmpCtx.putImageData(imageData, 0, 0);
|
||||
|
||||
magCtx.clearRect(0, 0, magCanvas.width, magCanvas.height);
|
||||
magCtx.imageSmoothingEnabled = false;
|
||||
|
||||
// 居中绘制
|
||||
const dx = Math.floor((magCanvas.width - DRAW_SIZE) / 2);
|
||||
const dy = Math.floor((magCanvas.height - DRAW_SIZE) / 2);
|
||||
magCtx.drawImage(tmpCanvas, dx, dy, DRAW_SIZE, DRAW_SIZE);
|
||||
|
||||
// 网格线(轻微)
|
||||
magCtx.strokeStyle = "rgba(255,255,255,0.08)";
|
||||
magCtx.lineWidth = 1;
|
||||
for (let i = 0; i <= CAPTURE_SIZE; i++) {
|
||||
const gx = dx + i * SCALE + 0.5;
|
||||
const gy = dy + i * SCALE + 0.5;
|
||||
magCtx.beginPath();
|
||||
magCtx.moveTo(gx, dy);
|
||||
magCtx.lineTo(gx, dy + DRAW_SIZE);
|
||||
magCtx.stroke();
|
||||
magCtx.beginPath();
|
||||
magCtx.moveTo(dx, gy);
|
||||
magCtx.lineTo(dx + DRAW_SIZE, gy);
|
||||
magCtx.stroke();
|
||||
}
|
||||
|
||||
// 中心十字
|
||||
const cx = dx + half * SCALE;
|
||||
const cy = dy + half * SCALE;
|
||||
magCtx.strokeStyle = "rgba(255,255,255,0.9)";
|
||||
magCtx.lineWidth = 2;
|
||||
magCtx.strokeRect(cx, cy, SCALE, SCALE);
|
||||
|
||||
// 更新颜色信息
|
||||
magHex.textContent = res.center_hex;
|
||||
magSwatch.style.backgroundColor = res.center_hex;
|
||||
} catch (e) {
|
||||
// 静默失败,避免刷屏
|
||||
}
|
||||
}
|
||||
|
||||
overlay.addEventListener("mousemove", (e) => {
|
||||
lastClientX = e.clientX;
|
||||
lastClientY = e.clientY;
|
||||
lastScreenX = e.screenX;
|
||||
lastScreenY = e.screenY;
|
||||
|
||||
positionMagnifier(lastClientX, lastClientY);
|
||||
|
||||
if (!pending) {
|
||||
pending = true;
|
||||
requestAnimationFrame(captureAndRender);
|
||||
}
|
||||
});
|
||||
|
||||
overlay.addEventListener("click", async (e) => {
|
||||
try {
|
||||
// screenX/screenY 通常是 CSS 像素;后端 GetPixel 需要物理像素
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const x = Math.round(e.screenX * dpr);
|
||||
const y = Math.round(e.screenY * dpr);
|
||||
|
||||
// 取色前先隐藏遮罩窗口,确保拿到“最上层应用”的真实颜色
|
||||
const color = await invoke("pick_color_at_point_topmost", {
|
||||
x,
|
||||
y,
|
||||
});
|
||||
|
||||
await emit("color-picked", color);
|
||||
await invoke("close_picker_window");
|
||||
} catch (error) {
|
||||
console.error("取色失败:", error);
|
||||
await invoke("close_picker_window");
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", async (e) => {
|
||||
if (e.key === "Escape") {
|
||||
try {
|
||||
await invoke("close_picker_window");
|
||||
await emit("color-picker-cancelled");
|
||||
} catch (error) {
|
||||
console.error("关闭窗口失败:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
overlay.addEventListener("contextmenu", (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user