feat: md-pins allow numbers
This commit is contained in:
parent
6f57970711
commit
617fd17def
|
|
@ -145,22 +145,33 @@
|
||||||
```markdown
|
```markdown
|
||||||
:md-pins[./images/map.png]{pins="A:30,40 B:10,30" fixed}
|
:md-pins[./images/map.png]{pins="A:30,40 B:10,30" fixed}
|
||||||
:md-pins[./images/city-map.png]
|
: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` 模式:仅显示标记,不可编辑
|
||||||
- 非 fixed 模式:点击图片添加标记,点击标记删除
|
- 非 fixed 模式:点击图片添加标记,点击标记删除
|
||||||
- 提供复制按钮生成标记代码
|
- 提供复制按钮生成标记代码
|
||||||
|
- 支持自定义标签起始值(字母或数字)
|
||||||
|
|
||||||
**属性:**
|
**属性:**
|
||||||
|
|
||||||
| 属性 | 类型 | 说明 |
|
| 属性 | 类型 | 默认值 | 说明 |
|
||||||
|------|------|------|
|
|------|------|--------|------|
|
||||||
| `pins` | string | 标记列表,格式 `"A:30,40 B:10,30"` (标签:x,y) |
|
| `pins` | string | `""` | 标记列表,格式 `"A:30,40 B:10,30"` (标签:x,y) |
|
||||||
| `fixed` | boolean | 是否为固定模式(只读) |
|
| `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
|
- 0-25: A-Z
|
||||||
- 26-51: AA-ZZ
|
- 26-51: AA-ZZ
|
||||||
- 52-77: AAA-ZZZ
|
- 52-77: AAA-ZZZ
|
||||||
|
|
@ -168,12 +179,24 @@
|
||||||
**示例:**
|
**示例:**
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
<!-- 固定标记模式 -->
|
<!-- 固定标记模式 - 字母标签(默认从 A 开始) -->
|
||||||
:md-pins[./images/battle-map.png]{pins="A:25,50 B:75,30 C:50,80" fixed}
|
:md-pins[./images/battle-map.png]{pins="A:25,50 B:75,30 C:50,80" fixed}
|
||||||
|
|
||||||
<!-- 可编辑模式 - 点击添加标记 -->
|
<!-- 可编辑模式 - 点击添加标记 - 从 A 开始 -->
|
||||||
:md-pins[./images/blank-map.png]
|
:md-pins[./images/blank-map.png]
|
||||||
|
|
||||||
|
<!-- 数字标签 - 从 1 开始 -->
|
||||||
|
:md-pins[./images/dungeon.png]{labelStart="1"}
|
||||||
|
|
||||||
|
<!-- 数字标签 - 从 7 开始 -->
|
||||||
|
:md-pins[./images/cave.png]{labelStart="7"}
|
||||||
|
|
||||||
|
<!-- 字母标签 - 从 C 开始 -->
|
||||||
|
: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}
|
:md-pins[./images/map.png]{pins="A:30,40 B:10,30" fixed}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@ import {Show, For, createResource, createMemo, createSignal} from "solid-js";
|
||||||
import { resolvePath } from "./utils/path";
|
import { resolvePath } from "./utils/path";
|
||||||
import { createPinsStore } from "./stores/pinsStore";
|
import { createPinsStore } from "./stores/pinsStore";
|
||||||
|
|
||||||
customElement("md-pins", { pins: "", fixed: false }, (props, { element }) => {
|
customElement("md-pins", { pins: "", fixed: false, labelStart: "A" }, (props, { element }) => {
|
||||||
noShadowDOM();
|
noShadowDOM();
|
||||||
|
|
||||||
const [showToast, setShowToast] = createSignal(false);
|
const [showToast, setShowToast] = createSignal(false);
|
||||||
|
|
||||||
// 创建 store
|
// 创建 store
|
||||||
const store = createPinsStore(props.pins, props.fixed);
|
const store = createPinsStore(props.pins, props.fixed, props.labelStart);
|
||||||
|
|
||||||
// 从 element 的 textContent 获取图片路径
|
// 从 element 的 textContent 获取图片路径
|
||||||
const rawSrc = element?.textContent?.trim() || '';
|
const rawSrc = element?.textContent?.trim() || '';
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,31 @@ interface PinsState {
|
||||||
pins: Pin[];
|
pins: Pin[];
|
||||||
rawSrc: string;
|
rawSrc: string;
|
||||||
isFixed: boolean;
|
isFixed: boolean;
|
||||||
|
labelStart: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成标签 A-Z, AA-ZZ, AAA-ZZZ ...
|
// 判断 labelStart 是否为数字
|
||||||
export function generateLabel(index: number): string {
|
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[] = [];
|
const labels: string[] = [];
|
||||||
let num = index;
|
let num = index;
|
||||||
|
|
||||||
|
|
@ -26,12 +47,18 @@ export function generateLabel(index: number): string {
|
||||||
return labels.join('');
|
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[] {
|
export function parsePins(pinsStr: string): Pin[] {
|
||||||
if (!pinsStr) return [];
|
if (!pinsStr) return [];
|
||||||
|
|
||||||
const pins: Pin[] = [];
|
const pins: Pin[] = [];
|
||||||
const regex = /([A-Z]+):(\d+),(\d+)/g;
|
// 支持任意非空白字符作为标签(不仅限于大写字母)
|
||||||
|
const regex = /([^:\s]+):(\d+),(\d+)/g;
|
||||||
let match;
|
let match;
|
||||||
|
|
||||||
while ((match = regex.exec(pinsStr)) !== null) {
|
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));
|
const usedLabels = new Set(pins.map(p => p.label));
|
||||||
|
|
||||||
let index = 0;
|
if (isNumericLabelStart(labelStart)) {
|
||||||
while (true) {
|
// 数字标签模式
|
||||||
const label = generateLabel(index);
|
const startNum = parseInt(labelStart);
|
||||||
if (!usedLabels.has(label)) {
|
let index = 0;
|
||||||
return label;
|
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 实例
|
// 创建 store 实例
|
||||||
export function createPinsStore(initialPinsStr: string = "", initialFixed: boolean = false) {
|
export function createPinsStore(
|
||||||
|
initialPinsStr: string = "",
|
||||||
|
initialFixed: boolean = false,
|
||||||
|
initialLabelStart: string = "A"
|
||||||
|
) {
|
||||||
const [state, setState] = createStore<PinsState>({
|
const [state, setState] = createStore<PinsState>({
|
||||||
pins: parsePins(initialPinsStr),
|
pins: parsePins(initialPinsStr),
|
||||||
rawSrc: "",
|
rawSrc: "",
|
||||||
isFixed: initialFixed
|
isFixed: initialFixed,
|
||||||
|
labelStart: initialLabelStart
|
||||||
});
|
});
|
||||||
|
|
||||||
const setRawSrc = (src: string) => {
|
const setRawSrc = (src: string) => {
|
||||||
|
|
@ -80,7 +131,7 @@ export function createPinsStore(initialPinsStr: string = "", initialFixed: boole
|
||||||
};
|
};
|
||||||
|
|
||||||
const addPin = (x: number, y: number) => {
|
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 }]);
|
setState("pins", [...state.pins, { x, y, label }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -92,7 +143,8 @@ export function createPinsStore(initialPinsStr: string = "", initialFixed: boole
|
||||||
|
|
||||||
const getCopyText = () => {
|
const getCopyText = () => {
|
||||||
const pinsStr = formatCurrentPins();
|
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 {
|
return {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue