From f1a55bf83e26f2af1d65dd057f72edd6291d2adc Mon Sep 17 00:00:00 2001 From: hypercross Date: Fri, 27 Feb 2026 00:05:05 +0800 Subject: [PATCH] fix: pin editor --- src/components/md-pin-editor.tsx | 109 ++++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 22 deletions(-) diff --git a/src/components/md-pin-editor.tsx b/src/components/md-pin-editor.tsx index f1aca1b..512ad59 100644 --- a/src/components/md-pin-editor.tsx +++ b/src/components/md-pin-editor.tsx @@ -22,7 +22,48 @@ function generateLabel(index: number): string { return labels.join(''); } -customElement("md-pin-editor", {}, (props, { element }) => { +// 解析 pins 字符串 "A:30,40 B:10,30" -> Pin[] +function parsePins(pinsStr: string): Pin[] { + if (!pinsStr) return []; + + const pins: Pin[] = []; + const regex = /([A-Z]+):(\d+),(\d+)/g; + let match; + + while ((match = regex.exec(pinsStr)) !== null) { + pins.push({ + label: match[1], + x: parseInt(match[2]), + y: parseInt(match[3]) + }); + } + + return pins; +} + +// 格式化 pins 为字符串 "A:30,40 B:10,30" +function formatPins(pins: Pin[]): string { + return pins.map(pin => `${pin.label}:${pin.x},${pin.y}`).join(' '); +} + +// 找到最早未使用的标签 +function findNextUnusedLabel(pins: Pin[]): string { + const usedLabels = new Set(pins.map(p => p.label)); + + let index = 0; + while (true) { + const label = generateLabel(index); + if (!usedLabels.has(label)) { + return label; + } + index++; + if (index > 10000) break; // 安全限制 + } + + return generateLabel(pins.length); +} + +customElement("md-pin-editor", { pins: "", fixed: false }, (props, { element }) => { noShadowDOM(); const [pins, setPins] = createSignal([]); @@ -57,18 +98,30 @@ customElement("md-pin-editor", {}, (props, { element }) => { const [image] = createResource(resolvedSrc, loadImage); const visible = createMemo(() => !image.loading && !!image()); + // 从 props.pins 初始化 pins + onMount(() => { + if (props.pins) { + const parsed = parsePins(props.pins); + if (parsed.length > 0) { + setPins(parsed); + } + } + }); + // 添加 pin const addPin = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); + if (isFixed()) return; + const imgRect = (e.target as Element).getBoundingClientRect(); const clickX = ((e.clientX - imgRect.left) / imgRect.width) * 100; const clickY = ((e.clientY - imgRect.top) / imgRect.height) * 100; const x = Math.round(clickX); const y = Math.round(clickY); - const label = generateLabel(pins().length); + const label = findNextUnusedLabel(pins()); setPins([...pins(), { x, y, label }]); }; @@ -77,13 +130,16 @@ customElement("md-pin-editor", {}, (props, { element }) => { const removePin = (index: number, e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); + + if (isFixed()) return; + setPins(pins().filter((_, i) => i !== index)); }; - // 复制所有 pin 为 md-pin 文本 + // 复制所有 pin 为 :md-editor-pin 格式 const copyPins = () => { - const pinTexts = pins().map(pin => `:md-pin[${pin.label}]{x=${pin.x} y=${pin.y}}`); - const text = pinTexts.join('\n'); + const pinsStr = formatPins(pins()); + const text = `:md-pin-editor[${rawSrc}]{pins="${pinsStr}" fixed}`; navigator.clipboard.writeText(text).then(() => { setShowToast(true); @@ -93,45 +149,54 @@ customElement("md-pin-editor", {}, (props, { element }) => { }); }; + const isFixed = () => props.fixed; + return (
- + {/* 图片容器 */}
{/* 显示图片 */} {/* 透明遮罩层 */} -
+ +
+ + +
+ {/* 复制按钮 HUD */} -
- -
+ +
+ +
+
{/* Pin 列表 */} {(pin, index) => ( removePin(index(), e)} - class="absolute transform -translate-x-1/2 -translate-y-1/2 pointer-events-auto cursor-pointer + class={`absolute transform -translate-x-1/2 -translate-y-1/2 pointer-events-auto bg-red-500 text-white text-xs font-bold rounded-full w-6 h-6 flex items-center justify-center shadow-lg - hover:bg-red-600 hover:scale-110 transition-all z-10" + ${!isFixed() ? 'cursor-pointer hover:bg-red-600 hover:scale-110 transition-all z-10' : 'cursor-default z-10'}`} style={{ left: `${pin.x}%`, top: `${pin.y}%` }} - title={`点击删除 (${pin.x}, ${pin.y})`} + title={isFixed() ? `(${pin.x}, ${pin.y})` : `点击删除 (${pin.x}, ${pin.y})`} > {pin.label}