feat: pin editor
This commit is contained in:
parent
a00e84da7f
commit
9a858918fe
|
|
@ -3,6 +3,7 @@ import './dice';
|
||||||
import './table';
|
import './table';
|
||||||
import './md-link';
|
import './md-link';
|
||||||
import './md-pin';
|
import './md-pin';
|
||||||
|
import './md-pin-editor';
|
||||||
|
|
||||||
// 导出组件
|
// 导出组件
|
||||||
export { Article } from './Article';
|
export { Article } from './Article';
|
||||||
|
|
|
||||||
|
|
@ -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<Pin[]>([]);
|
||||||
|
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<HTMLImageElement> => {
|
||||||
|
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 (
|
||||||
|
<div ref={editorContainer}>
|
||||||
|
<Show when={visible()}>
|
||||||
|
{/* 图片容器 */}
|
||||||
|
<div class="relative" onClick={addPin}>
|
||||||
|
{/* 显示图片 */}
|
||||||
|
<img src={resolvedSrc} alt="" class="inset-0" />
|
||||||
|
|
||||||
|
{/* 透明遮罩层 */}
|
||||||
|
<div class="absolute inset-0 bg-transparent hover:bg-black/10 transition-colors cursor-crosshair" />
|
||||||
|
|
||||||
|
{/* 复制按钮 HUD */}
|
||||||
|
<div class="absolute top-2 right-2 z-20">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
copyPins();
|
||||||
|
}}
|
||||||
|
class="bg-gray-800 hover:bg-gray-700 text-white px-3 py-1.5 rounded shadow-lg text-sm flex items-center gap-1 transition-colors"
|
||||||
|
title="复制所有 pin 坐标"
|
||||||
|
>
|
||||||
|
📋 复制
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pin 列表 */}
|
||||||
|
<For each={pins()}>
|
||||||
|
{(pin, index) => (
|
||||||
|
<span
|
||||||
|
onClick={(e) => 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}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Toast 提示 */}
|
||||||
|
<Show when={showToast()}>
|
||||||
|
<div class="fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white px-4 py-2 rounded shadow-lg text-sm z-50">
|
||||||
|
已复制 {pins().length} 个 pin 坐标
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue