diff --git a/src/components/index.ts b/src/components/index.ts index 2b26e1a..bfe2764 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -3,6 +3,7 @@ import './dice'; import './table'; import './md-link'; import './md-pin'; +import './md-pin-editor'; // 导出组件 export { Article } from './Article'; diff --git a/src/components/md-pin-editor.tsx b/src/components/md-pin-editor.tsx new file mode 100644 index 0000000..f1aca1b --- /dev/null +++ b/src/components/md-pin-editor.tsx @@ -0,0 +1,151 @@ +import { customElement, noShadowDOM } from "solid-element"; +import { createSignal, onMount, onCleanup, Show, For, createResource, createMemo } from "solid-js"; +import { resolvePath } from "../utils/path"; + +interface Pin { + x: number; + y: number; + label: string; +} + +// 生成标签 A-Z, AA-ZZ, AAA-ZZZ ... +function generateLabel(index: number): string { + const labels: string[] = []; + let num = index; + + do { + const remainder = num % 26; + labels.unshift(String.fromCharCode(65 + remainder)); + num = Math.floor(num / 26) - 1; + } while (num >= 0); + + return labels.join(''); +} + +customElement("md-pin-editor", {}, (props, { element }) => { + noShadowDOM(); + + const [pins, setPins] = createSignal([]); + const [showToast, setShowToast] = createSignal(false); + let editorContainer: HTMLDivElement | undefined; + + // 从 element 的 textContent 获取图片路径 + const rawSrc = element?.textContent?.trim() || ''; + + // 隐藏原始文本内容 + if (element) { + element.textContent = ""; + } + + // 从父节点 article 的 data-src 获取当前 markdown 文件完整路径 + const articleEl = element?.closest('article[data-src]'); + const articlePath = articleEl?.getAttribute('data-src') || ''; + + // 解析相对路径 + const resolvedSrc = resolvePath(articlePath, rawSrc); + + // 加载图片 + const loadImage = (src: string): Promise => { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = src; + }); + }; + + const [image] = createResource(resolvedSrc, loadImage); + const visible = createMemo(() => !image.loading && !!image()); + + // 添加 pin + const addPin = (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + 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); + + setPins([...pins(), { x, y, label }]); + }; + + // 删除 pin + const removePin = (index: number, e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setPins(pins().filter((_, i) => i !== index)); + }; + + // 复制所有 pin 为 md-pin 文本 + const copyPins = () => { + const pinTexts = pins().map(pin => `:md-pin[${pin.label}]{x=${pin.x} y=${pin.y}}`); + const text = pinTexts.join('\n'); + + navigator.clipboard.writeText(text).then(() => { + setShowToast(true); + setTimeout(() => setShowToast(false), 2000); + }).catch(err => { + console.error('复制失败:', err); + }); + }; + + return ( +
+ + {/* 图片容器 */} +
+ {/* 显示图片 */} + + + {/* 透明遮罩层 */} +
+ + {/* 复制按钮 HUD */} +
+ +
+ + {/* Pin 列表 */} + + {(pin, index) => ( + removePin(index(), e)} + class="absolute transform -translate-x-1/2 -translate-y-1/2 pointer-events-auto cursor-pointer + 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" + style={{ + left: `${pin.x}%`, + top: `${pin.y}%` + }} + title={`点击删除 (${pin.x}, ${pin.y})`} + > + {pin.label} + + )} + +
+ + + {/* Toast 提示 */} + +
+ 已复制 {pins().length} 个 pin 坐标 +
+
+
+ ); +});