refactor: store based md-pins
This commit is contained in:
parent
b83e884f6f
commit
fdecd0fb6e
|
|
@ -1,78 +1,22 @@
|
||||||
import { customElement, noShadowDOM } from "solid-element";
|
import { customElement, noShadowDOM } from "solid-element";
|
||||||
import { createSignal, onMount, onCleanup, Show, For, createResource, createMemo } from "solid-js";
|
import {Show, For, createResource, createMemo, createSignal} from "solid-js";
|
||||||
import { resolvePath } from "./utils/path";
|
import { resolvePath } from "./utils/path";
|
||||||
|
import { createPinsStore } from "./stores/pinsStore";
|
||||||
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('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析 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-pins", { pins: "", fixed: false }, (props, { element }) => {
|
customElement("md-pins", { pins: "", fixed: false }, (props, { element }) => {
|
||||||
noShadowDOM();
|
noShadowDOM();
|
||||||
|
|
||||||
const [pins, setPins] = createSignal<Pin[]>([]);
|
|
||||||
const [showToast, setShowToast] = createSignal(false);
|
const [showToast, setShowToast] = createSignal(false);
|
||||||
let editorContainer: HTMLDivElement | undefined;
|
|
||||||
|
// 创建 store
|
||||||
|
const store = createPinsStore(props.pins, props.fixed);
|
||||||
|
|
||||||
// 从 element 的 textContent 获取图片路径
|
// 从 element 的 textContent 获取图片路径
|
||||||
const rawSrc = element?.textContent?.trim() || '';
|
const rawSrc = element?.textContent?.trim() || '';
|
||||||
|
|
||||||
|
// 设置 rawSrc 到 store
|
||||||
|
store.setRawSrc(rawSrc);
|
||||||
|
|
||||||
// 隐藏原始文本内容
|
// 隐藏原始文本内容
|
||||||
if (element) {
|
if (element) {
|
||||||
element.textContent = "";
|
element.textContent = "";
|
||||||
|
|
@ -98,22 +42,12 @@ customElement("md-pins", { pins: "", fixed: false }, (props, { element }) => {
|
||||||
const [image] = createResource(resolvedSrc, loadImage);
|
const [image] = createResource(resolvedSrc, loadImage);
|
||||||
const visible = createMemo(() => !image.loading && !!image());
|
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
|
// 添加 pin
|
||||||
const addPin = (e: MouseEvent) => {
|
const addPin = (e: MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (isFixed()) return;
|
if (props.fixed) return;
|
||||||
|
|
||||||
const imgRect = (e.target as Element).getBoundingClientRect();
|
const imgRect = (e.target as Element).getBoundingClientRect();
|
||||||
const clickX = ((e.clientX - imgRect.left) / imgRect.width) * 100;
|
const clickX = ((e.clientX - imgRect.left) / imgRect.width) * 100;
|
||||||
|
|
@ -121,9 +55,8 @@ customElement("md-pins", { pins: "", fixed: false }, (props, { element }) => {
|
||||||
|
|
||||||
const x = Math.round(clickX);
|
const x = Math.round(clickX);
|
||||||
const y = Math.round(clickY);
|
const y = Math.round(clickY);
|
||||||
const label = findNextUnusedLabel(pins());
|
|
||||||
|
|
||||||
setPins([...pins(), { x, y, label }]);
|
store.addPin(x, y);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除 pin
|
// 删除 pin
|
||||||
|
|
@ -131,15 +64,14 @@ customElement("md-pins", { pins: "", fixed: false }, (props, { element }) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (isFixed()) return;
|
if (props.fixed) return;
|
||||||
|
|
||||||
setPins(pins().filter((_, i) => i !== index));
|
store.removePin(index);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 复制所有 pin 为 :md-editor-pin 格式
|
// 复制所有 pin 为 :md-editor-pin 格式
|
||||||
const copyPins = () => {
|
const copyPins = () => {
|
||||||
const pinsStr = formatPins(pins());
|
const text = store.getCopyText();
|
||||||
const text = `:md-pins[${rawSrc}]{pins="${pinsStr}" fixed}`;
|
|
||||||
|
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
setShowToast(true);
|
setShowToast(true);
|
||||||
|
|
@ -149,26 +81,24 @@ customElement("md-pins", { pins: "", fixed: false }, (props, { element }) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFixed = () => props.fixed;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={editorContainer}>
|
<div>
|
||||||
<Show when={visible() && image()}>
|
<Show when={visible()}>
|
||||||
{/* 图片容器 */}
|
{/* 图片容器 */}
|
||||||
<div class="relative" onClick={addPin}>
|
<div class="relative" onClick={addPin}>
|
||||||
{/* 显示图片 */}
|
{/* 显示图片 */}
|
||||||
<img src={resolvedSrc} alt="" class="inset-0" />
|
<img src={resolvedSrc} alt="" class="inset-0" />
|
||||||
|
|
||||||
{/* 透明遮罩层 */}
|
{/* 透明遮罩层 */}
|
||||||
<Show when={!isFixed()}>
|
<Show when={!props.fixed}>
|
||||||
<div class="absolute inset-0 bg-transparent hover:bg-black/10 transition-colors cursor-crosshair" />
|
<div class="absolute inset-0 bg-transparent hover:bg-black/10 transition-colors cursor-crosshair" />
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={isFixed()}>
|
<Show when={props.fixed}>
|
||||||
<div class="absolute inset-0 pointer-events-none" />
|
<div class="absolute inset-0 pointer-events-none" />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{/* 复制按钮 HUD */}
|
{/* 复制按钮 HUD */}
|
||||||
<Show when={!isFixed()}>
|
<Show when={!props.fixed}>
|
||||||
<div class="absolute top-2 right-2 z-20">
|
<div class="absolute top-2 right-2 z-20">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|
@ -184,19 +114,19 @@ customElement("md-pins", { pins: "", fixed: false }, (props, { element }) => {
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{/* Pin 列表 */}
|
{/* Pin 列表 */}
|
||||||
<For each={pins()}>
|
<For each={store.state.pins}>
|
||||||
{(pin, index) => (
|
{(pin, index) => (
|
||||||
<span
|
<span
|
||||||
onClick={(e) => removePin(index(), e)}
|
onClick={(e) => removePin(index(), e)}
|
||||||
class={`absolute transform -translate-x-1/2 -translate-y-1/2 pointer-events-auto
|
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
|
bg-red-500 text-white text-xs font-bold rounded-full w-6 h-6
|
||||||
flex items-center justify-center shadow-lg
|
flex items-center justify-center shadow-lg
|
||||||
${!isFixed() ? 'cursor-pointer hover:bg-red-600 hover:scale-110 transition-all z-10' : 'cursor-default z-10'}`}
|
${!props.fixed ? 'cursor-pointer hover:bg-red-600 hover:scale-110 transition-all z-10' : 'cursor-default z-10'}`}
|
||||||
style={{
|
style={{
|
||||||
left: `${pin.x}%`,
|
left: `${pin.x}%`,
|
||||||
top: `${pin.y}%`
|
top: `${pin.y}%`
|
||||||
}}
|
}}
|
||||||
title={isFixed() ? `(${pin.x}, ${pin.y})` : `点击删除 (${pin.x}, ${pin.y})`}
|
title={props.fixed ? `(${pin.x}, ${pin.y})` : `点击删除 (${pin.x}, ${pin.y})`}
|
||||||
>
|
>
|
||||||
{pin.label}
|
{pin.label}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -208,7 +138,7 @@ customElement("md-pins", { pins: "", fixed: false }, (props, { element }) => {
|
||||||
{/* Toast 提示 */}
|
{/* Toast 提示 */}
|
||||||
<Show when={showToast()}>
|
<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">
|
<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 坐标
|
已复制 {store.state.pins.length} 个 pin 坐标
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { createStore } from "solid-js/store";
|
||||||
|
|
||||||
|
export interface Pin {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PinsState {
|
||||||
|
pins: Pin[];
|
||||||
|
rawSrc: string;
|
||||||
|
isFixed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成标签 A-Z, AA-ZZ, AAA-ZZZ ...
|
||||||
|
export 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('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 pins 字符串 "A:30,40 B:10,30" -> Pin[]
|
||||||
|
export 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"
|
||||||
|
export function formatPins(pins: Pin[]): string {
|
||||||
|
return pins.map(pin => `${pin.label}:${pin.x},${pin.y}`).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到最早未使用的标签
|
||||||
|
export 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 store 实例
|
||||||
|
export function createPinsStore(initialPinsStr: string = "", initialFixed: boolean = false) {
|
||||||
|
const [state, setState] = createStore<PinsState>({
|
||||||
|
pins: parsePins(initialPinsStr),
|
||||||
|
rawSrc: "",
|
||||||
|
isFixed: initialFixed
|
||||||
|
});
|
||||||
|
|
||||||
|
const setRawSrc = (src: string) => {
|
||||||
|
setState("rawSrc", src);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addPin = (x: number, y: number) => {
|
||||||
|
const label = findNextUnusedLabel(state.pins);
|
||||||
|
setState("pins", [...state.pins, { x, y, label }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removePin = (index: number) => {
|
||||||
|
setState("pins", state.pins.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrentPins = () => formatPins(state.pins);
|
||||||
|
|
||||||
|
const getCopyText = () => {
|
||||||
|
const pinsStr = formatCurrentPins();
|
||||||
|
return `:md-pins[${state.rawSrc}]{pins="${pinsStr}" fixed}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
setState,
|
||||||
|
setRawSrc,
|
||||||
|
addPin,
|
||||||
|
removePin,
|
||||||
|
formatCurrentPins,
|
||||||
|
getCopyText
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue