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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user