移动端适配
This commit is contained in:
203
src/App.tsx
203
src/App.tsx
@@ -17,6 +17,7 @@ import { LabelDesigner } from './components/LabelDesigner';
|
||||
import { PropSidebar } from './components/PropSidebar';
|
||||
import { ExportSidebar } from './components/ExportSidebar';
|
||||
import { MobileExportView } from './components/MobileExportView';
|
||||
import { MobileDesignView } from './components/MobileDesignView';
|
||||
import { LayoutPreview } from './components/PreviewGrid';
|
||||
import { CanvasLabelImage } from './components/CanvasLabelImage';
|
||||
import { renderLabelToDataUrl } from './labelRenderer';
|
||||
@@ -153,6 +154,9 @@ export default function App() {
|
||||
const [editTmplName, setEditTmplName] = useState<string>('');
|
||||
const [templateMenuId, setTemplateMenuId] = useState<string | null>(null);
|
||||
const templateMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const [headerTemplateMenuOpen, setHeaderTemplateMenuOpen] = useState(false);
|
||||
const headerTemplateMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const importTemplateInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const designHistory = useUndoRedo<LabelTemplate>();
|
||||
const skipDesignHistoryRef = useRef(false);
|
||||
const designHistorySessionRef = useRef<{ step: string; templateId: string | null }>({
|
||||
@@ -396,6 +400,16 @@ export default function App() {
|
||||
return () => document.removeEventListener('pointerdown', handlePointerDown);
|
||||
}, [templateMenuId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!headerTemplateMenuOpen) return;
|
||||
const handlePointerDown = (e: PointerEvent) => {
|
||||
if (headerTemplateMenuRef.current?.contains(e.target as Node)) return;
|
||||
setHeaderTemplateMenuOpen(false);
|
||||
};
|
||||
document.addEventListener('pointerdown', handlePointerDown);
|
||||
return () => document.removeEventListener('pointerdown', handlePointerDown);
|
||||
}, [headerTemplateMenuOpen]);
|
||||
|
||||
const handleEditTemplateSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!editingTemplateId) return;
|
||||
@@ -770,7 +784,7 @@ export default function App() {
|
||||
className={`flex flex-col font-sans h-dvh max-h-dvh overflow-hidden ${workflowStep === 'template-design' || (workflowStep === 'print-view' && !isMobile)
|
||||
? 'bg-[#323232] text-[#e8e8e8]'
|
||||
: workflowStep === 'print-view' && isMobile
|
||||
? 'bg-slate-100 text-slate-800'
|
||||
? 'bg-[#323232] text-[#e8e8e8]'
|
||||
: 'bg-slate-50 text-slate-800'
|
||||
}`}
|
||||
>
|
||||
@@ -779,7 +793,7 @@ export default function App() {
|
||||
SCREEN HEADER
|
||||
=========================================
|
||||
*/}
|
||||
{!(isMobile && workflowStep === 'print-view') && (
|
||||
{workflowStep === 'template-select' && (
|
||||
<header
|
||||
className={`no-print bg-indigo-700 border-b border-indigo-900 shrink-0 shadow flex flex-wrap items-center justify-between gap-4 mobile-template-header app-header-sticky ${isMobile && workflowStep === 'template-select' && hasTemplates
|
||||
? 'mobile-template-header-row'
|
||||
@@ -807,31 +821,83 @@ export default function App() {
|
||||
className={`flex items-center gap-2 shrink-0 ${isMobile ? 'ml-auto' : 'flex-wrap'
|
||||
}`}
|
||||
>
|
||||
{!isMobile && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddTmplForm(true)}
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 bg-indigo-600 text-white rounded-xl text-xs font-semibold whitespace-nowrap transition hover:bg-indigo-800 shadow-sm cursor-pointer"
|
||||
>
|
||||
<Plus className="w-4 h-4 shrink-0" />
|
||||
新建模板
|
||||
</button>
|
||||
{isMobile ? (
|
||||
<>
|
||||
<input
|
||||
ref={importTemplateInputRef}
|
||||
type="file"
|
||||
accept=".lad"
|
||||
onChange={handleImportTemplate}
|
||||
className="hidden"
|
||||
/>
|
||||
<div ref={headerTemplateMenuRef} className="relative shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setHeaderTemplateMenuOpen((open) => !open)}
|
||||
className="mobile-header-menu-btn"
|
||||
aria-expanded={headerTemplateMenuOpen}
|
||||
aria-haspopup="menu"
|
||||
aria-label="更多"
|
||||
>
|
||||
<MoreHorizontal className="w-5 h-5 shrink-0" />
|
||||
</button>
|
||||
{headerTemplateMenuOpen && (
|
||||
<div
|
||||
role="menu"
|
||||
className="absolute right-0 top-full mt-1 z-30 min-w-[148px] py-1 bg-white border border-gray-200 rounded-xl shadow-lg"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => {
|
||||
setHeaderTemplateMenuOpen(false);
|
||||
setShowAddTmplForm(true);
|
||||
}}
|
||||
className="inline-flex items-center gap-3 w-full px-3 py-2.5 text-left text-sm text-gray-700 hover:bg-gray-50 active:bg-gray-100 cursor-pointer"
|
||||
>
|
||||
<Plus className="w-4 h-4 shrink-0" />
|
||||
新建模板
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => {
|
||||
setHeaderTemplateMenuOpen(false);
|
||||
importTemplateInputRef.current?.click();
|
||||
}}
|
||||
className="inline-flex items-center gap-3 w-full px-3 py-2.5 text-left text-sm text-gray-700 hover:bg-gray-50 active:bg-gray-100 cursor-pointer"
|
||||
>
|
||||
<Upload className="w-4 h-4 shrink-0" />
|
||||
导入模板
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddTmplForm(true)}
|
||||
className="inline-flex items-center gap-1.5 px-4 py-2 bg-indigo-600 text-white rounded-xl text-xs font-semibold whitespace-nowrap transition hover:bg-indigo-800 shadow-sm cursor-pointer"
|
||||
>
|
||||
<Plus className="w-4 h-4 shrink-0" />
|
||||
新建模板
|
||||
</button>
|
||||
<label
|
||||
className="inline-flex items-center gap-1.5 bg-indigo-600 hover:bg-indigo-800 text-white rounded-xl font-semibold whitespace-nowrap transition shadow-sm cursor-pointer px-4 py-2 text-xs"
|
||||
>
|
||||
<Upload className="w-4 h-4 shrink-0" />
|
||||
导入模板
|
||||
<input
|
||||
type="file"
|
||||
accept=".lad"
|
||||
onChange={handleImportTemplate}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
<label
|
||||
className={`inline-flex items-center gap-1.5 bg-indigo-600 hover:bg-indigo-800 text-white rounded-xl font-semibold whitespace-nowrap transition shadow-sm cursor-pointer ${isMobile
|
||||
? 'min-h-[40px] px-3 py-2 text-[11px]'
|
||||
: 'px-4 py-2 text-xs'
|
||||
}`}
|
||||
>
|
||||
<Upload className={`${isMobile ? 'w-3.5 h-3.5' : 'w-4 h-4'} shrink-0`} />
|
||||
导入模板
|
||||
<input
|
||||
type="file"
|
||||
accept=".lad"
|
||||
onChange={handleImportTemplate}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
@@ -871,16 +937,14 @@ export default function App() {
|
||||
: '创建模板开始可视化设计,或导入已有的模板文件。'}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 mt-3">
|
||||
{!isMobile && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddTmplForm(true)}
|
||||
className="inline-flex items-center justify-center gap-2 px-5 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl text-sm font-semibold transition shadow-sm cursor-pointer"
|
||||
>
|
||||
<Plus className="w-4 h-4 shrink-0" />
|
||||
新建模板
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddTmplForm(true)}
|
||||
className="inline-flex items-center justify-center gap-2 px-5 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl text-sm font-semibold transition shadow-sm cursor-pointer"
|
||||
>
|
||||
<Plus className="w-4 h-4 shrink-0" />
|
||||
新建模板
|
||||
</button>
|
||||
<label className="inline-flex items-center justify-center gap-2 px-5 py-2.5 bg-white border border-gray-200 hover:border-indigo-300 text-gray-700 rounded-xl text-sm font-semibold transition shadow-sm cursor-pointer shrink-0">
|
||||
<Upload className="w-4 h-4 shrink-0" />
|
||||
导入模板
|
||||
@@ -975,6 +1039,20 @@ export default function App() {
|
||||
设计
|
||||
</button>
|
||||
)}
|
||||
{isMobile && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSelectTemplate(tmpl.id!);
|
||||
setWorkflowStep('template-design');
|
||||
}}
|
||||
className="mobile-secondary-btn flex-1 min-w-0 justify-center min-h-[44px] px-3 py-2.5 rounded-xl text-[13px] font-semibold"
|
||||
>
|
||||
<Code className="w-4 h-4 shrink-0" />
|
||||
设计
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -986,7 +1064,7 @@ export default function App() {
|
||||
}`}
|
||||
>
|
||||
<Printer className={`${isMobile ? 'w-4 h-4' : 'w-3.5 h-3.5'} shrink-0`} />
|
||||
{isMobile ? '排版生成标签' : '排版导出'}
|
||||
{isMobile ? '生成' : '排版导出'}
|
||||
</button>
|
||||
<div
|
||||
ref={templateMenuId === tmpl.id ? templateMenuRef : null}
|
||||
@@ -1067,27 +1145,20 @@ export default function App() {
|
||||
{/* ========================================================
|
||||
STEP 2: The visual drag-and-drop label designer
|
||||
======================================================== */}
|
||||
{workflowStep === 'template-design' && isMobile && hasTemplates && (
|
||||
<div className="mobile-design-block bg-[#292929] text-[#e8e8e8] flex-1">
|
||||
<p className="text-sm text-[#666666] max-w-[280px] leading-relaxed">
|
||||
请在电脑端进行标签设计
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openPrintView()}
|
||||
className="inline-flex items-center gap-2 px-5 py-2.5 bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white text-sm font-semibold rounded-lg cursor-pointer"
|
||||
>
|
||||
<Printer className="w-4 h-4" />
|
||||
去生成标签
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setWorkflowStep('template-select')}
|
||||
className="text-[12px] text-[#999] underline cursor-pointer"
|
||||
>
|
||||
返回模板列表
|
||||
</button>
|
||||
</div>
|
||||
{workflowStep === 'template-design' && isMobile && hasTemplates && activeTemplate && (
|
||||
<MobileDesignView
|
||||
template={activeTemplate}
|
||||
paper={paper}
|
||||
selectedElementIds={selectedElementIds}
|
||||
onSelectElements={setSelectedElementIds}
|
||||
onChangeTemplate={handleDesignTemplateChange}
|
||||
canUndo={designHistory.canUndo}
|
||||
canRedo={designHistory.canRedo}
|
||||
onUndo={handleDesignUndo}
|
||||
onRedo={handleDesignRedo}
|
||||
onBack={() => setWorkflowStep('template-select')}
|
||||
onExport={() => openPrintView()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{workflowStep === 'template-design' && !isMobile && hasTemplates && activeTemplate && (
|
||||
@@ -1175,15 +1246,25 @@ export default function App() {
|
||||
dataColumns={dataColumns}
|
||||
variableColumnMapping={variableColumnMapping}
|
||||
onChangeMapping={handleMappingChange}
|
||||
paper={paper}
|
||||
paper={previewSnapshot?.paper ?? paper}
|
||||
onChangePaper={handlePaperChange}
|
||||
rows={mappedRows}
|
||||
draftExportRange={draftExportRange}
|
||||
onChangeDraftExportRange={setDraftExportRange}
|
||||
cutLine={cutLine}
|
||||
onChangeCutLine={handleCutLineChange}
|
||||
selectedCount={selectedCount}
|
||||
totalPages={totalPages}
|
||||
printablePages={previewSnapshot?.pages ?? []}
|
||||
gridCols={previewSnapshot?.gridCols ?? gridInfo.cols}
|
||||
labelsPerPage={previewSnapshot?.labelsPerPage ?? labelsPerPage}
|
||||
totalPages={previewSnapshot?.totalPages ?? totalPages}
|
||||
selectedCount={previewSnapshot?.selectedCount ?? selectedCount}
|
||||
draftTotalPages={totalPages}
|
||||
draftSelectedCount={selectedCount}
|
||||
totalRowCount={activeRows.length}
|
||||
previewReady={previewSnapshot !== null}
|
||||
previewLayoutStale={previewLayoutStale}
|
||||
activeRowIndex={activeRowIndex}
|
||||
onSelectRowIndex={setActiveRowIndex}
|
||||
pdfGenerating={pdfGenerating}
|
||||
onBack={() => setWorkflowStep('template-select')}
|
||||
onExportPdf={exportPdf}
|
||||
|
||||
Reference in New Issue
Block a user