feat: 实现命令面板、颜色取色、JSON格式化和系统信息功能

- 重构项目架构,采用四层架构模式 (Command → Service → Platform → Utils)
  - 实现命令面板功能,支持快捷搜索和特征分类
  - 添加颜色取色功能,支持屏幕像素颜色获取
  - 添加JSON格式化功能,支持JSON格式化和压缩
  - 添加系统信息功能,显示操作系统和硬件信息
  - 移除旧的状态文档和无用配置文件
This commit is contained in:
2026-02-10 18:46:11 +08:00
parent db4978e349
commit 927eaa1e03
62 changed files with 7536 additions and 1958 deletions

257
picker.html Normal file
View 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=210canvas 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>