From 617fd17defdcb3086a99682f5d4b051e4ff976ed Mon Sep 17 00:00:00 2001 From: hypercross Date: Sat, 21 Mar 2026 18:08:21 +0800 Subject: [PATCH] feat: md-pins allow numbers --- docs/markdown.md | 39 +++++++++++--- src/components/md-pins.tsx | 4 +- src/components/stores/pinsStore.ts | 86 ++++++++++++++++++++++++------ 3 files changed, 102 insertions(+), 27 deletions(-) diff --git a/docs/markdown.md b/docs/markdown.md index 0d04d26..ce2e596 100644 --- a/docs/markdown.md +++ b/docs/markdown.md @@ -145,22 +145,33 @@ ```markdown :md-pins[./images/map.png]{pins="A:30,40 B:10,30" fixed} :md-pins[./images/city-map.png] +:md-pins[./images/dungeon.png]{labelStart="1"} +:md-pins[./images/quest.png]{labelStart="C"} ``` **功能:** -- 在图片上显示标记点(A-Z, AA-ZZ, AAA-ZZZ...) +- 在图片上显示标记点(A-Z, AA-ZZ, AAA-ZZZ... 或数字) - `fixed` 模式:仅显示标记,不可编辑 - 非 fixed 模式:点击图片添加标记,点击标记删除 - 提供复制按钮生成标记代码 +- 支持自定义标签起始值(字母或数字) **属性:** -| 属性 | 类型 | 说明 | -|------|------|------| -| `pins` | string | 标记列表,格式 `"A:30,40 B:10,30"` (标签:x,y) | -| `fixed` | boolean | 是否为固定模式(只读) | +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `pins` | string | `""` | 标记列表,格式 `"A:30,40 B:10,30"` (标签:x,y) | +| `fixed` | boolean | `false` | 是否为固定模式(只读) | +| `labelStart` | string | `"A"` | 标签起始值,支持字母(如 `"A"`, `"C"`)或数字(如 `"1"`, `"7"`) | -**标记标签生成规则:** +**标签生成规则:** + +| 起始值类型 | 说明 | 示例 | +|------|------|------| +| 字母(默认) | 从起始字母开始,按 A-Z, AA-ZZ, AAA-ZZZ 顺序 | `A` → A, B, C...; `C` → C, D, E... | +| 数字 | 从起始数字开始递增 | `1` → 1, 2, 3...; `7` → 7, 8, 9... | + +**字母标签序列(从 A 开始):** - 0-25: A-Z - 26-51: AA-ZZ - 52-77: AAA-ZZZ @@ -168,12 +179,24 @@ **示例:** ```markdown - + :md-pins[./images/battle-map.png]{pins="A:25,50 B:75,30 C:50,80" fixed} - + :md-pins[./images/blank-map.png] + +:md-pins[./images/dungeon.png]{labelStart="1"} + + +:md-pins[./images/cave.png]{labelStart="7"} + + +:md-pins[./images/quest-map.png]{labelStart="C"} + + +:md-pins[./images/map.png]{pins="5:30,40 6:75,30" labelStart="5" fixed} + :md-pins[./images/map.png]{pins="A:30,40 B:10,30" fixed} ``` diff --git a/src/components/md-pins.tsx b/src/components/md-pins.tsx index f566837..3c83cc6 100644 --- a/src/components/md-pins.tsx +++ b/src/components/md-pins.tsx @@ -3,13 +3,13 @@ import {Show, For, createResource, createMemo, createSignal} from "solid-js"; import { resolvePath } from "./utils/path"; import { createPinsStore } from "./stores/pinsStore"; -customElement("md-pins", { pins: "", fixed: false }, (props, { element }) => { +customElement("md-pins", { pins: "", fixed: false, labelStart: "A" }, (props, { element }) => { noShadowDOM(); const [showToast, setShowToast] = createSignal(false); // 创建 store - const store = createPinsStore(props.pins, props.fixed); + const store = createPinsStore(props.pins, props.fixed, props.labelStart); // 从 element 的 textContent 获取图片路径 const rawSrc = element?.textContent?.trim() || ''; diff --git a/src/components/stores/pinsStore.ts b/src/components/stores/pinsStore.ts index 841bfcc..355b70f 100644 --- a/src/components/stores/pinsStore.ts +++ b/src/components/stores/pinsStore.ts @@ -10,10 +10,31 @@ interface PinsState { pins: Pin[]; rawSrc: string; isFixed: boolean; + labelStart: string; } -// 生成标签 A-Z, AA-ZZ, AAA-ZZZ ... -export function generateLabel(index: number): string { +// 判断 labelStart 是否为数字 +function isNumericLabelStart(start: string): boolean { + return /^\d+$/.test(start); +} + +// 生成字母标签 A-Z, AA-ZZ, AAA-ZZZ ... 从指定起始值开始 +export function generateAlphaLabel(index: number, startLabel: string = 'A'): string { + const startOffset = alphaLabelToIndex(startLabel); + return indexToAlphaLabel(index + startOffset); +} + +// 将字母标签转换为索引 (A=0, B=1, ..., Z=25, AA=26, ...) +export function alphaLabelToIndex(label: string): number { + let index = 0; + for (let i = 0; i < label.length; i++) { + index = index * 26 + (label.charCodeAt(i) - 64); + } + return index - 1; +} + +// 将索引转换为字母标签 (0=A, 1=B, ..., 25=Z, 26=AA, ...) +export function indexToAlphaLabel(index: number): string { const labels: string[] = []; let num = index; @@ -26,12 +47,18 @@ export function generateLabel(index: number): string { return labels.join(''); } -// 解析 pins 字符串 "A:30,40 B:10,30" -> Pin[] +// 生成数字标签,从指定起始值开始 +export function generateNumberLabel(index: number, startNum: number): string { + return String(startNum + index); +} + +// 解析 pins 字符串 "A:30,40 B:10,30" 或 "1:30,40 2:10,30" -> Pin[] export function parsePins(pinsStr: string): Pin[] { if (!pinsStr) return []; const pins: Pin[] = []; - const regex = /([A-Z]+):(\d+),(\d+)/g; + // 支持任意非空白字符作为标签(不仅限于大写字母) + const regex = /([^:\s]+):(\d+),(\d+)/g; let match; while ((match = regex.exec(pinsStr)) !== null) { @@ -51,28 +78,52 @@ export function formatPins(pins: Pin[]): string { } // 找到最早未使用的标签 -export function findNextUnusedLabel(pins: Pin[]): string { +export function findNextUnusedLabel(pins: Pin[], labelStart: string): 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; + if (isNumericLabelStart(labelStart)) { + // 数字标签模式 + const startNum = parseInt(labelStart); + let index = 0; + while (true) { + const label = generateNumberLabel(index, startNum); + if (!usedLabels.has(label)) { + return label; + } + index++; + if (index > 10000) break; // 安全限制 + } + } else { + // 字母标签模式 + let index = 0; + while (true) { + const label = generateAlphaLabel(index, labelStart); + if (!usedLabels.has(label)) { + return label; + } + index++; + if (index > 10000) break; // 安全限制 } - index++; - if (index > 10000) break; // 安全限制 } - return generateLabel(pins.length); + // 回退值 + if (isNumericLabelStart(labelStart)) { + return generateNumberLabel(pins.length, parseInt(labelStart)); + } + return generateAlphaLabel(pins.length, labelStart); } // 创建 store 实例 -export function createPinsStore(initialPinsStr: string = "", initialFixed: boolean = false) { +export function createPinsStore( + initialPinsStr: string = "", + initialFixed: boolean = false, + initialLabelStart: string = "A" +) { const [state, setState] = createStore({ pins: parsePins(initialPinsStr), rawSrc: "", - isFixed: initialFixed + isFixed: initialFixed, + labelStart: initialLabelStart }); const setRawSrc = (src: string) => { @@ -80,7 +131,7 @@ export function createPinsStore(initialPinsStr: string = "", initialFixed: boole }; const addPin = (x: number, y: number) => { - const label = findNextUnusedLabel(state.pins); + const label = findNextUnusedLabel(state.pins, state.labelStart); setState("pins", [...state.pins, { x, y, label }]); }; @@ -92,7 +143,8 @@ export function createPinsStore(initialPinsStr: string = "", initialFixed: boole const getCopyText = () => { const pinsStr = formatCurrentPins(); - return `:md-pins[${state.rawSrc}]{pins="${pinsStr}" fixed}`; + const labelStartAttr = state.labelStart !== 'A' ? ` labelStart="${state.labelStart}"` : ''; + return `:md-pins[${state.rawSrc}]{pins="${pinsStr}" fixed${labelStartAttr}`; }; return {