Compare commits
No commits in common. "4c3d83922f8ed0996aa971bcbad8507262641402" and "f3b9078c45d34920b3230406ca7e2295315d6b45" have entirely different histories.
4c3d83922f
...
f3b9078c45
|
|
@ -48,7 +48,7 @@ export const Article: Component<ArticleProps> = (props) => {
|
||||||
<div class="text-red-500">加载失败:{content.error?.message}</div>
|
<div class="text-red-500">加载失败:{content.error?.message}</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!content.loading && !content.error && content()}>
|
<Show when={!content.loading && !content.error && content()}>
|
||||||
<div class="relative" innerHTML={parseMarkdown(content()!)} />
|
<div innerHTML={parseMarkdown(content()!)} />
|
||||||
</Show>
|
</Show>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
import './dice';
|
import './dice';
|
||||||
import './table';
|
import './table';
|
||||||
import './md-link';
|
import './md-link';
|
||||||
import './md-pin';
|
|
||||||
|
|
||||||
// 导出组件
|
// 导出组件
|
||||||
export { Article } from './Article';
|
export { Article } from './Article';
|
||||||
|
|
@ -14,3 +13,4 @@ export { FileTreeNode, HeadingNode } from './FileTree';
|
||||||
// 导出数据类型
|
// 导出数据类型
|
||||||
export type { DiceProps } from './dice';
|
export type { DiceProps } from './dice';
|
||||||
export type { TableProps } from './table';
|
export type { TableProps } from './table';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,188 +0,0 @@
|
||||||
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);
|
|
||||||
const [containerStyle, setContainerStyle] = createSignal<{ position: string; top: string; left: string; width: string; height: string }>({
|
|
||||||
position: "absolute",
|
|
||||||
top: "0",
|
|
||||||
left: "0",
|
|
||||||
width: "0",
|
|
||||||
height: "0"
|
|
||||||
});
|
|
||||||
const [showToast, setShowToast] = createSignal(false);
|
|
||||||
const [toastMessage, setToastMessage] = createSignal("");
|
|
||||||
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();
|
|
||||||
const articleEl = element?.closest('article[data-src]');
|
|
||||||
const articleRect = articleEl?.getBoundingClientRect();
|
|
||||||
|
|
||||||
if (!articleRect) return;
|
|
||||||
|
|
||||||
// 计算图片相对于 article 的位置
|
|
||||||
const relativeTop = imgRect.top - articleRect.top;
|
|
||||||
const relativeLeft = imgRect.left - articleRect.left;
|
|
||||||
|
|
||||||
// 设置容器样式,使其定位到图片位置
|
|
||||||
setContainerStyle({
|
|
||||||
position: "absolute",
|
|
||||||
top: `${relativeTop}px`,
|
|
||||||
left: `${relativeLeft}px`,
|
|
||||||
width: `${imgRect.width}px`,
|
|
||||||
height: `${imgRect.height}px`
|
|
||||||
});
|
|
||||||
|
|
||||||
// 计算 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;
|
|
||||||
|
|
||||||
const left = (x / 100) * imgRect.width;
|
|
||||||
const top = (y / 100) * imgRect.height;
|
|
||||||
|
|
||||||
setPosition({
|
|
||||||
left: `${left}px`,
|
|
||||||
top: `${top}px`
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
// 查找目标图片
|
|
||||||
targetImage = findNearestImage();
|
|
||||||
|
|
||||||
if (targetImage) {
|
|
||||||
// 确保图片容器是 relative 定位
|
|
||||||
const imgParent = targetImage.parentElement;
|
|
||||||
if (imgParent) {
|
|
||||||
const parentStyle = window.getComputedStyle(imgParent);
|
|
||||||
if (parentStyle.position === 'static') {
|
|
||||||
imgParent.style.position = 'relative';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始定位
|
|
||||||
updatePosition();
|
|
||||||
|
|
||||||
// 使用 ResizeObserver 监听图片大小变化
|
|
||||||
resizeObserver = new ResizeObserver(() => {
|
|
||||||
updatePosition();
|
|
||||||
});
|
|
||||||
resizeObserver.observe(targetImage);
|
|
||||||
|
|
||||||
// 延迟显示以等待位置计算完成
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
setVisible(true);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.warn('md-pin: 未找到目标图片');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
if (resizeObserver && targetImage) {
|
|
||||||
resizeObserver.unobserve(targetImage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理点击,报告坐标并复制到剪贴板
|
|
||||||
const handleClick = (e: MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (!targetImage) return;
|
|
||||||
|
|
||||||
const imgRect = targetImage.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 coordText = `:md-pin[${label || ''}]{x=${x} y=${y}}`;
|
|
||||||
|
|
||||||
// 复制到剪贴板
|
|
||||||
navigator.clipboard.writeText(coordText).then(() => {
|
|
||||||
setToastMessage(`已复制:${coordText}`);
|
|
||||||
setShowToast(true);
|
|
||||||
setTimeout(() => setShowToast(false), 2000);
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('复制失败:', err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
ref={pinContainer}
|
|
||||||
class="md-pin-container"
|
|
||||||
style={{ display: 'inline', position: containerStyle().position, top: containerStyle().top, left: containerStyle().left, width: containerStyle().width, height: containerStyle().height }}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<Show when={visible() && targetImage}>
|
|
||||||
<span
|
|
||||||
class="md-pin 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: position().left,
|
|
||||||
top: position().top
|
|
||||||
}}
|
|
||||||
title={label || '点击复制坐标'}
|
|
||||||
>
|
|
||||||
{label || '📍'}
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
<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">
|
|
||||||
{toastMessage()}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue