From 561d647bce980c93d3025b783f1c9f666f45ffd8 Mon Sep 17 00:00:00 2001 From: hypercross Date: Fri, 27 Feb 2026 01:06:05 +0800 Subject: [PATCH] feat: md-deck editor??? --- src/components/md-deck.tsx | 466 +++++++++++++++++++++++++++++++------ todo.md | 18 ++ 2 files changed, 411 insertions(+), 73 deletions(-) diff --git a/src/components/md-deck.tsx b/src/components/md-deck.tsx index b3d14a5..ad58e5b 100644 --- a/src/components/md-deck.tsx +++ b/src/components/md-deck.tsx @@ -1,5 +1,5 @@ import { customElement, noShadowDOM } from 'solid-element'; -import { createSignal, For, Show, createEffect, createMemo, createResource } from 'solid-js'; +import { createSignal, For, Show, createEffect, createMemo, createResource, onMount } from 'solid-js'; import { parse } from 'csv-parse/browser/esm/sync'; import { marked } from '../markdown'; import { resolvePath } from '../utils/path'; @@ -16,14 +16,23 @@ interface Layer { y2: number; } +interface LayerConfig { + prop: string; + visible: boolean; + x1: number; + y1: number; + x2: number; + y2: number; +} + // 解析 layers 字符串 "body:1,7-5,8 title:1,1-5,1" function parseLayers(layersStr: string): Layer[] { if (!layersStr) return []; - + const layers: Layer[] = []; const regex = /(\w+):(\d+),(\d+)-(\d+),(\d+)/g; let match; - + while ((match = regex.exec(layersStr)) !== null) { layers.push({ prop: match[1], @@ -33,10 +42,18 @@ function parseLayers(layersStr: string): Layer[] { y2: parseInt(match[5]) }); } - + return layers; } +// 格式化 layers 为字符串 +function formatLayers(layers: LayerConfig[]): string { + return layers + .filter(l => l.visible) + .map(l => `${l.prop}:${l.x1},${l.y1}-${l.x2},${l.y2}`) + .join(' '); +} + // 全局缓存已加载的 CSV 内容 const csvCache = new Map(); @@ -45,14 +62,31 @@ customElement('md-deck', { grid: '5x8', bleed: '1', padding: '2', - layers: '' + layers: '', + fixed: false }, (props, { element }) => { noShadowDOM(); - + const [cards, setCards] = createSignal([]); const [activeTab, setActiveTab] = createSignal(0); let tabsContainer: HTMLDivElement | undefined; + // 编辑器状态 + const [isEditing, setIsEditing] = createSignal(false); + const [editingLayer, setEditingLayer] = createSignal(null); + const [layerConfigs, setLayerConfigs] = createSignal([]); + + // 框选状态 + const [isSelecting, setIsSelecting] = createSignal(false); + const [selectStart, setSelectStart] = createSignal<{ x: number; y: number } | null>(null); + const [selectEnd, setSelectEnd] = createSignal<{ x: number; y: number } | null>(null); + + // 本地编辑的属性 + const [localSize, setLocalSize] = createSignal(props.size as string || '54x86'); + const [localGrid, setLocalGrid] = createSignal(props.grid as string || '5x8'); + const [localBleed, setLocalBleed] = createSignal(props.bleed as string || '1'); + const [localPadding, setLocalPadding] = createSignal(props.padding as string || '2'); + // 从 element 的 textContent 获取 CSV 路径 const src = element?.textContent?.trim() || ''; @@ -92,18 +126,36 @@ customElement('md-deck', { const data = csvData(); if (data) { setCards(data); + // 初始化 layer configs + const parsed = parseLayers(props.layers as string || ''); + const allProps = Object.keys(data[0] || {}).filter(k => k !== 'label'); + const configs: LayerConfig[] = allProps.map(prop => { + const existing = parsed.find(l => l.prop === prop); + return { + prop, + visible: !!existing, + x1: existing?.x1 || 1, + y1: existing?.y1 || 1, + x2: existing?.x2 || 2, + y2: existing?.y2 || 2 + }; + }); + setLayerConfigs(configs); } }); + // 检查是否 fixed + const isFixed = () => props.fixed === true || props.fixed === 'true'; + // 解析尺寸 const dimensions = createMemo(() => { - const [width, height] = (props.size as string).split('x').map(Number); - const [bleedW, bleedH] = (props.bleed as string).includes('x') - ? (props.bleed as string).split('x').map(Number) - : [Number(props.bleed), Number(props.bleed)]; - const [padW, padH] = (props.padding as string).includes('x') - ? (props.padding as string).split('x').map(Number) - : [Number(props.padding), Number(props.padding)]; + const [width, height] = localSize().split('x').map(Number); + const [bleedW, bleedH] = localBleed().includes('x') + ? localBleed().split('x').map(Number) + : [Number(localBleed()), Number(localBleed())]; + const [padW, padH] = localPadding().includes('x') + ? localPadding().split('x').map(Number) + : [Number(localPadding()), Number(localPadding())]; // 实际卡牌尺寸(含出血) const cardWidth = width + bleedW * 2; @@ -114,7 +166,7 @@ customElement('md-deck', { const gridAreaHeight = height - padH * 2; // 解析网格 - const [gridW, gridH] = (props.grid as string).split('x').map(Number); + const [gridW, gridH] = localGrid().split('x').map(Number); // 每个网格单元的尺寸(mm) const cellWidth = gridAreaWidth / gridW; @@ -138,9 +190,6 @@ customElement('md-deck', { }; }); - // 解析 layers - const layers = createMemo(() => parseLayers(props.layers as string)); - // 渲染 layer 内容 const renderLayer = (layer: Layer, cardData: CardData): string => { const content = cardData[layer.prop] || ''; @@ -149,11 +198,8 @@ customElement('md-deck', { // 计算 layer 位置样式(单位:mm) const getLayerStyle = (layer: Layer, dims: ReturnType) => { - // layer 坐标是网格坐标(1-based) - // 计算相对于网格区域起点的偏移(mm) const left = (layer.x1 - 1) * dims.cellWidth; const top = (layer.y1 - 1) * dims.cellHeight; - // 计算尺寸(mm) const width = (layer.x2 - layer.x1 + 1) * dims.cellWidth; const height = (layer.y2 - layer.y1 + 1) * dims.cellHeight; @@ -165,72 +211,346 @@ customElement('md-deck', { }; }; - return ( -
- {/* Tab 选择器 */} -
-
- - {(card, index) => ( - - )} - -
-
+ // 开始框选 + const handleCardMouseDown = (e: MouseEvent) => { + if (!isEditing() || !editingLayer()) return; + + const cardEl = e.currentTarget as HTMLElement; + const rect = cardEl.getBoundingClientRect(); + const dims = dimensions(); + + // 计算相对于网格区域起点的坐标(网格单位) + const offsetX = (e.clientX - rect.left) / rect.width * dims.cardWidth; + const offsetY = (e.clientY - rect.top) / rect.height * dims.cardHeight; + + const gridX = Math.max(1, Math.floor((offsetX - dims.gridOriginX) / dims.cellWidth) + 1); + const gridY = Math.max(1, Math.floor((offsetY - dims.gridOriginY) / dims.cellHeight) + 1); + + setSelectStart({ x: gridX, y: gridY }); + setSelectEnd({ x: gridX, y: gridY }); + setIsSelecting(true); + }; - {/* 卡牌预览 */} - 0}> -
- - {(() => { - const currentCard = cards()[activeTab()]; - const dims = dimensions(); - - return ( -
{ + if (!isSelecting()) return; + + const cardEl = e.currentTarget as HTMLElement; + const rect = cardEl.getBoundingClientRect(); + const dims = dimensions(); + + const offsetX = (e.clientX - rect.left) / rect.width * dims.cardWidth; + const offsetY = (e.clientY - rect.top) / rect.height * dims.cardHeight; + + const gridX = Math.max(1, Math.min(dims.gridW, Math.floor((offsetX - dims.gridOriginX) / dims.cellWidth) + 1)); + const gridY = Math.max(1, Math.min(dims.gridH, Math.floor((offsetY - dims.gridOriginY) / dims.cellHeight) + 1)); + + setSelectEnd({ x: gridX, y: gridY }); + }; + + // 结束框选 + const handleCardMouseUp = () => { + if (!isSelecting() || !editingLayer()) return; + + const start = selectStart()!; + const end = selectEnd()!; + + const x1 = Math.min(start.x, end.x); + const y1 = Math.min(start.y, end.y); + const x2 = Math.max(start.x, end.x); + const y2 = Math.max(start.y, end.y); + + setLayerConfigs(configs => configs.map(c => + c.prop === editingLayer() ? { ...c, x1, y1, x2, y2 } : c + )); + + setIsSelecting(false); + setSelectStart(null); + setSelectEnd(null); + setEditingLayer(null); + }; + + // 切换图层可见性 + const toggleLayerVisible = (prop: string) => { + setLayerConfigs(configs => configs.map(c => + c.prop === prop ? { ...c, visible: !c.visible } : c + )); + }; + + // 开始编辑图层位置 + const startEditingLayer = (prop: string) => { + setEditingLayer(prop); + }; + + // 生成 md-deck 代码 + const generateCode = () => { + const layersStr = formatLayers(layerConfigs()); + return `:md-deck[${src}]{size="${localSize()}" grid="${localGrid()}" bleed="${localBleed()}" padding="${localPadding()}" layers="${layersStr}"}`; + }; + + // 复制代码 + const copyCode = () => { + const code = generateCode(); + navigator.clipboard.writeText(code).then(() => { + alert('已复制到剪贴板!'); + }).catch(err => { + console.error('复制失败:', err); + }); + }; + + // 更新 CSV 数据 + const updateCardData = (key: string, value: string) => { + setCards(cards => cards.map((card, i) => + i === activeTab() ? { ...card, [key]: value } : card + )); + }; + + return ( +
+ {/* 左侧:CSV 数据编辑 */} + +
+

卡牌数据

+
+ + {(key) => ( +
+ +