feat(md-deck): Add card list navigation and tabbed editor panels

- Move card tab selector to a left sidebar (CardList) with pagination
- Replace separate side toggle and layer panels with EditorTabs
- Move copy code button from layer panel to DeckHeader
- Simplify PropertiesEditorPanel by removing front/back toggle and
  using a select for shape
This commit is contained in:
hyper
2026-08-07 11:20:26 +08:00
parent 18cad20d2c
commit 6bcdca3a77
6 changed files with 203 additions and 107 deletions
+86
View File
@@ -0,0 +1,86 @@
import { For, Show, createEffect, createSignal, on } from "solid-js";
import type { DeckStore } from "./hooks/deckStore";
export interface CardListProps {
store: DeckStore;
}
const PAGE_SIZE = 10;
/**
* 卡牌列表:左侧垂直导航,每行高度一致,按页分页
*/
export function CardList(props: CardListProps) {
const { store } = props;
const [page, setPage] = createSignal(0);
const totalPages = () =>
Math.max(1, Math.ceil(store.state.cards.length / PAGE_SIZE));
const pageCards = () => {
const start = page() * PAGE_SIZE;
return store.state.cards.slice(start, start + PAGE_SIZE);
};
// 当活动卡牌变化时,自动跳转到其所在页(只响应 activeTab,避免与手动翻页互相干扰)
createEffect(
on(
() => store.state.activeTab,
(tab) => {
setPage(Math.floor(tab / PAGE_SIZE));
},
),
);
return (
<nav class="w-44 shrink-0 border-r border-gray-200 pr-3 flex flex-col">
<div class="flex flex-col gap-1 flex-1">
<For each={pageCards()}>
{(card, index) => {
const globalIndex = () => page() * PAGE_SIZE + index();
const active = store.state.activeTab === globalIndex();
return (
<button
onClick={() => store.actions.setActiveTab(globalIndex())}
class={`flex items-center gap-2 w-full text-left px-3 py-2 rounded text-sm font-medium transition-colors cursor-pointer ${
active
? "bg-blue-100 text-blue-600"
: "text-gray-600 hover:bg-gray-100"
}`}
>
<span class="shrink-0 text-xs text-gray-400 tabular-nums w-5">
{globalIndex() + 1}
</span>
<span class="truncate">
{card.label || card.name || `Card ${globalIndex() + 1}`}
</span>
</button>
);
}}
</For>
</div>
<Show when={totalPages() > 1}>
<div class="flex items-center justify-between mt-2 pt-2 border-t border-gray-200">
<button
onClick={() => setPage(Math.max(0, page() - 1))}
disabled={page() === 0}
class="px-2 py-1 rounded text-sm text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
>
</button>
<span class="text-xs text-gray-500 tabular-nums">
{page() + 1} / {totalPages()}
</span>
<button
onClick={() => setPage(Math.min(totalPages() - 1, page() + 1))}
disabled={page() >= totalPages() - 1}
class="px-2 py-1 rounded text-sm text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
>
</button>
</div>
</Show>
</nav>
);
}
+4 -15
View File
@@ -1,4 +1,3 @@
import { For } from "solid-js";
import type { DeckStore } from "./hooks/deckStore";
export interface DeckHeaderProps {
@@ -33,23 +32,13 @@ export function DeckHeader(props: DeckHeaderProps) {
📥 PDF
</button>
{/* Tab 选择器 */}
<div class="flex gap-1 overflow-x-auto flex-1 min-w-0 flex-wrap">
<For each={store.state.cards}>
{(card, index) => (
{/* 复制代码按钮 */}
<button
onClick={() => store.actions.setActiveTab(index())}
class={`font-medium transition-colors shrink-0 min-w-[1.6em] cursor-pointer px-2 py-1 rounded ${
store.state.activeTab === index()
? "bg-blue-100 text-blue-600 border-b-2 border-blue-600"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`}
onClick={() => store.actions.copyCode()}
class="px-2 py-1 rounded text-xs font-medium transition-colors cursor-pointer bg-purple-100 text-purple-600 hover:bg-purple-200"
>
{card.label || card.name || `Card ${index() + 1}`}
📋
</button>
)}
</For>
</div>
</div>
);
}
+59
View File
@@ -0,0 +1,59 @@
import { createSignal } from "solid-js";
import type { DeckStore } from "./hooks/deckStore";
import { LayerEditorPanel, PropertiesEditorPanel } from "./editor-panel";
export interface EditorTabsProps {
store: DeckStore;
}
type TabKey = "sizing" | "front" | "back";
/**
* 编辑面板:尺寸 / 正面图层 / 背面图层 用标签页切换,节省横向空间
*/
export function EditorTabs(props: EditorTabsProps) {
const { store } = props;
const [tab, setTab] = createSignal<TabKey>("sizing");
const tabs: { key: TabKey; label: string }[] = [
{ key: "sizing", label: "尺寸" },
{ key: "front", label: "正面图层" },
{ key: "back", label: "背面图层" },
];
const selectTab = (key: TabKey) => {
setTab(key);
// 切换正/背面标签时,同步预览的显示面
if (key === "front") store.actions.setActiveSide("front");
if (key === "back") store.actions.setActiveSide("back");
};
return (
<div class="w-64 shrink-0">
{/* 标签页切换 */}
<div class="flex gap-1 mb-3">
{tabs.map((t) => (
<button
onClick={() => selectTab(t.key)}
class={`flex-1 px-3 py-1.5 rounded text-sm font-medium cursor-pointer ${
tab() === t.key
? "bg-blue-600 text-white"
: "bg-gray-100 text-gray-600 hover:bg-gray-200"
}`}
>
{t.label}
</button>
))}
</div>
{tab() === "sizing" ? (
<PropertiesEditorPanel store={store} />
) : (
<LayerEditorPanel
store={store}
side={tab() === "front" ? "front" : "back"}
/>
)}
</div>
);
}
@@ -6,11 +6,13 @@ import {
closestCenter,
} from "@thisbeyond/solid-dnd";
import type { DeckStore } from "../hooks/deckStore";
import type { CardSide } from "../types";
import { toSortableId } from "../hooks/layer-crud";
import { LayerRow } from "./LayerRow";
export interface LayerEditorPanelProps {
store: DeckStore;
side?: CardSide;
}
function LayerEditorPanel(props: LayerEditorPanelProps) {
@@ -20,7 +22,7 @@ function LayerEditorPanel(props: LayerEditorPanelProps) {
const [addMenuOpen, setAddMenuOpen] = createSignal(false);
let addMenuRef: HTMLDivElement | undefined;
const side = () => store.state.activeSide;
const side = () => props.side || "front";
const layers = () =>
side() === "front"
? store.state.frontLayerConfigs
@@ -143,15 +145,6 @@ function LayerEditorPanel(props: LayerEditorPanelProps) {
</SortableProvider>
</DragDropSensors>
</DragDropProvider>
<hr class="my-4" />
<button
onClick={() => store.actions.copyCode()}
class="w-full bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded text-sm font-medium cursor-pointer flex items-center gap-2 justify-center"
>
<span>📋</span>
<span></span>
</button>
</div>
);
}
@@ -1,49 +1,32 @@
import type { DeckStore } from '../hooks/deckStore';
import type { CardShape } from '../types';
import type { DeckStore } from "../hooks/deckStore";
import type { CardShape } from "../types";
export interface PropertiesEditorPanelProps {
store: DeckStore;
}
/**
* 卡牌属性编辑面板:尺寸、网格、出血、内边距、正背面切换
* 卡牌尺寸编辑面板:尺寸、网格、出血、内边距、形状
*/
export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {
const { store } = props;
return (
<div class="w-64 flex-shrink-0">
<h3 class="font-bold mb-2 mt-0"></h3>
const shapeOptions: { value: CardShape; label: string }[] = [
{ value: "rectangle", label: "矩形" },
{ value: "circle", label: "圆形" },
{ value: "triangle", label: "三角形" },
{ value: "hexagon", label: "六边形" },
];
{/* 正面/背面切换标签页 */}
<div class="mb-4">
<div class="flex gap-1">
<button
onClick={() => store.actions.setActiveSide('front')}
class={`flex-1 px-3 py-1.5 rounded text-sm font-medium cursor-pointer border ${
store.state.activeSide === 'front'
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
}`}
>
</button>
<button
onClick={() => store.actions.setActiveSide('back')}
class={`flex-1 px-3 py-1.5 rounded text-sm font-medium cursor-pointer border ${
store.state.activeSide === 'back'
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
}`}
>
</button>
</div>
</div>
return (
<div class="w-64 shrink-0">
<h3 class="font-bold mb-2 mt-0"></h3>
<div class="space-y-3">
<div>
<label class="block text-sm font-medium text-gray-700"> (mm)</label>
<label class="block text-sm font-medium text-gray-700">
(mm)
</label>
<div class="flex gap-2">
<input
type="number"
@@ -83,7 +66,9 @@ export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {
</div>
<div>
<label class="block text-sm font-medium text-gray-700"> / (mm)</label>
<label class="block text-sm font-medium text-gray-700">
/ (mm)
</label>
<div class="flex gap-2">
<input
type="number"
@@ -103,24 +88,20 @@ export function PropertiesEditorPanel(props: PropertiesEditorPanelProps) {
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"></label>
<div class="grid grid-cols-2 gap-2">
{(['rectangle', 'circle', 'triangle', 'hexagon'] as CardShape[]).map((shape) => (
<button
onClick={() => store.actions.setShape(shape)}
class={`px-3 py-1.5 rounded text-sm font-medium cursor-pointer border ${
store.state.shape === shape
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
}`}
<label class="block text-sm font-medium text-gray-700 mb-2">
</label>
<select
value={store.state.shape}
onChange={(e) =>
store.actions.setShape(e.target.value as CardShape)
}
class="w-full border border-gray-300 rounded px-2 py-1 text-sm bg-white"
>
{shape === 'rectangle' && '矩形'}
{shape === 'circle' && '圆形'}
{shape === 'triangle' && '三角形'}
{shape === 'hexagon' && '六边形'}
</button>
{shapeOptions.map((s) => (
<option value={s.value}>{s.label}</option>
))}
</div>
</select>
</div>
</div>
</div>
+12 -24
View File
@@ -6,13 +6,11 @@ import { createDeckStore } from "./hooks/deckStore";
import { registerDeck, unregisterDeck } from "./hooks/deck-registry";
import type { CardShape } from "./types";
import { DeckHeader } from "./DeckHeader";
import { CardList } from "./CardList";
import { DeckContent } from "./DeckContent";
import { EditorTabs } from "./EditorTabs";
import { PrintPreview } from "./PrintPreview";
import {
DataEditorPanel,
LayerEditorPanel,
PropertiesEditorPanel,
} from "./editor-panel";
import { DataEditorPanel } from "./editor-panel";
interface DeckProps {
size?: string;
@@ -139,29 +137,19 @@ customElement<DeckProps>(
</Show>
<div class="flex gap-4">
{/* 内容区域:错误/加载/卡牌预览/空状态 */}
{/* 左侧:CSV 数据编辑 */}
{/*<Show when={store.state.isEditing && !store.state.fixed}>*/}
{/* <DataEditorPanel*/}
{/* activeTab={store.state.activeTab}*/}
{/* cards={store.state.cards}*/}
{/* updateCardData={store.actions.updateCardData}*/}
{/* />*/}
{/*</Show>*/}
<Show when={store.state.isEditing && !store.state.fixed}>
<div class="flex-1">
<PropertiesEditorPanel store={store} />
</div>
{/* 左侧:卡牌列表导航 */}
<Show when={store.state.cards.length > 0 && !store.state.error}>
<CardList store={store} />
</Show>
{/* 中间:内容区域(错误/加载/卡牌预览/空状态) */}
<div class="flex-1 min-w-0">
<DeckContent store={store} isLoading={store.state.isLoading} />
{/* 右侧:属性/图层编辑面板 */}
<Show when={store.state.isEditing && !store.state.fixed}>
<div class="flex-1">
<LayerEditorPanel store={store} />
</div>
{/* 右侧:属性/图层编辑面板(标签页切换) */}
<Show when={store.state.isEditing && !store.state.fixed}>
<EditorTabs store={store} />
</Show>
</div>
</div>