2026-02-26 18:11:33 +08:00
|
|
|
|
import { customElement, noShadowDOM } from "solid-element";
|
|
|
|
|
|
import { createSignal, onMount, onCleanup, Show } from "solid-js";
|
|
|
|
|
|
|
|
|
|
|
|
customElement("md-pin", { x: 0, y: 0 }, (props, { element }) => {
|
|
|
|
|
|
noShadowDOM();
|
|
|
|
|
|
|
|
|
|
|
|
const [position, setPosition] = createSignal<{ top: string; left: string }>({ top: "0", left: "0" });
|
|
|
|
|
|
const [visible, setVisible] = createSignal(false);
|
2026-02-26 23:10:13 +08:00
|
|
|
|
const [transformStyle, setTransformStyle] = createSignal<string>("");
|
2026-02-26 18:11:33 +08:00
|
|
|
|
let pinContainer: HTMLSpanElement | undefined;
|
|
|
|
|
|
let targetImage: HTMLImageElement | undefined;
|
|
|
|
|
|
let resizeObserver: ResizeObserver | undefined;
|
|
|
|
|
|
|
|
|
|
|
|
// 从 element 的 textContent 获取 pin 标签内容
|
|
|
|
|
|
const label = element?.textContent?.trim() || "";
|
|
|
|
|
|
|
|
|
|
|
|
// 隐藏原始文本内容
|
|
|
|
|
|
if (element) {
|
|
|
|
|
|
element.textContent = "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查找上方最近的图片
|
|
|
|
|
|
const findNearestImage = (): HTMLImageElement | null => {
|
|
|
|
|
|
if (!element) return null;
|
|
|
|
|
|
|
|
|
|
|
|
// 从当前元素向上查找
|
|
|
|
|
|
let current: Element | null = element;
|
|
|
|
|
|
while (current) {
|
|
|
|
|
|
// 在当前元素的之前兄弟节点中查找图片
|
|
|
|
|
|
let sibling: Element | null = current.previousElementSibling;
|
|
|
|
|
|
while (sibling) {
|
|
|
|
|
|
// 检查是否是图片元素
|
|
|
|
|
|
const img = sibling.querySelector('img');
|
|
|
|
|
|
if (img) return img;
|
|
|
|
|
|
|
|
|
|
|
|
// 检查元素本身是否有图片相关的类或标签
|
|
|
|
|
|
if (sibling.tagName === 'IMG') return sibling as HTMLImageElement;
|
|
|
|
|
|
|
|
|
|
|
|
sibling = sibling.previousElementSibling;
|
|
|
|
|
|
}
|
|
|
|
|
|
current = current.parentElement;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 更新 pin 位置和容器样式
|
|
|
|
|
|
const updatePosition = () => {
|
|
|
|
|
|
if (!targetImage || !pinContainer) return;
|
|
|
|
|
|
|
|
|
|
|
|
const imgRect = targetImage.getBoundingClientRect();
|
2026-02-26 23:12:26 +08:00
|
|
|
|
const containerRect = pinContainer.parentElement.getBoundingClientRect();
|
2026-02-26 23:10:13 +08:00
|
|
|
|
|
|
|
|
|
|
// 计算图片左上角相对于容器原始位置的偏移
|
|
|
|
|
|
const offsetX = imgRect.left - containerRect.left;
|
|
|
|
|
|
const offsetY = imgRect.top - containerRect.top;
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 transform 将容器移动到图片位置
|
|
|
|
|
|
setTransformStyle(`translate(${offsetX}px, ${offsetY}px)`);
|
2026-02-26 18:11:33 +08:00
|
|
|
|
|
|
|
|
|
|
// 计算 pin 在图片内的相对位置(x/y 是百分比)
|
|
|
|
|
|
const x = typeof props.x === 'number' ? props.x : parseFloat(props.x) || 0;
|
|
|
|
|
|
const y = typeof props.y === 'number' ? props.y : parseFloat(props.y) || 0;
|
2026-02-26 23:10:13 +08:00
|
|
|
|
|
2026-02-26 18:11:33 +08:00
|
|
|
|
const left = (x / 100) * imgRect.width;
|
|
|
|
|
|
const top = (y / 100) * imgRect.height;
|
|
|
|
|
|
|
|
|
|
|
|
setPosition({
|
|
|
|
|
|
left: `${left}px`,
|
|
|
|
|
|
top: `${top}px`
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onMount(() => {
|
|
|
|
|
|
// 查找目标图片
|
|
|
|
|
|
targetImage = findNearestImage();
|
|
|
|
|
|
|
2026-02-26 23:10:13 +08:00
|
|
|
|
if (targetImage) {
|
2026-02-26 18:11:33 +08:00
|
|
|
|
// 初始定位
|
|
|
|
|
|
updatePosition();
|
2026-02-26 23:10:13 +08:00
|
|
|
|
|
2026-02-26 18:11:33 +08:00
|
|
|
|
// 使用 ResizeObserver 监听图片大小变化
|
|
|
|
|
|
resizeObserver = new ResizeObserver(() => {
|
|
|
|
|
|
updatePosition();
|
|
|
|
|
|
});
|
|
|
|
|
|
resizeObserver.observe(targetImage);
|
|
|
|
|
|
|
|
|
|
|
|
// 延迟显示以等待位置计算完成
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
setVisible(true);
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.warn('md-pin: 未找到目标图片');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
onCleanup(() => {
|
|
|
|
|
|
if (resizeObserver && targetImage) {
|
|
|
|
|
|
resizeObserver.unobserve(targetImage);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-02-26 23:12:26 +08:00
|
|
|
|
<div
|
2026-02-26 23:10:13 +08:00
|
|
|
|
ref={pinContainer}
|
|
|
|
|
|
class="md-pin-container"
|
2026-02-26 23:12:26 +08:00
|
|
|
|
style={{ transform: transformStyle(), 'transform-origin': 'top left' }}
|
2026-02-26 18:20:52 +08:00
|
|
|
|
>
|
2026-02-26 18:11:33 +08:00
|
|
|
|
<Show when={visible() && targetImage}>
|
|
|
|
|
|
<span
|
2026-02-26 23:26:45 +08:00
|
|
|
|
class="md-pin absolute transform -translate-x-1/2 -translate-y-1/2 pointer-events-auto
|
2026-02-26 18:20:52 +08:00
|
|
|
|
bg-red-500 text-white text-xs font-bold rounded-full w-6 h-6
|
2026-02-26 18:11:33 +08:00
|
|
|
|
flex items-center justify-center shadow-lg
|
2026-02-26 23:26:45 +08:00
|
|
|
|
z-10"
|
2026-02-26 18:11:33 +08:00
|
|
|
|
style={{
|
|
|
|
|
|
left: position().left,
|
|
|
|
|
|
top: position().top
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{label || '📍'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</Show>
|
2026-02-26 23:12:26 +08:00
|
|
|
|
</div>
|
2026-02-26 18:11:33 +08:00
|
|
|
|
);
|
|
|
|
|
|
});
|