From e71452ae780d162de1be42ba0bb148f78a9f1825 Mon Sep 17 00:00:00 2001 From: iqudoo Date: Fri, 12 Jun 2026 21:39:00 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 48 ++- src/components/DataImporter.tsx | 377 ++++++++++++---------- src/components/DataRangeFields.tsx | 15 +- src/components/MobileExportPropsPanel.tsx | 35 +- src/components/MobileExportView.tsx | 119 ++----- src/components/PreviewGrid.tsx | 13 +- src/index.css | 11 +- 7 files changed, 296 insertions(+), 322 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7981b78..20b755c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useEffect } from 'react'; +import React, { useState, useMemo, useEffect, useLayoutEffect } from 'react'; import { LabelTemplate, PaperConfig, ImportData, RecordRow, ExportRangeConfig, CutLineConfig } from './types'; import { stripTemplateForStorage, @@ -442,6 +442,40 @@ export default function App() { return !exportRangeEquals(draftExportRange, appliedExportData.exportRange); }, [importData, variableColumnMapping, appliedExportData, draftExportRange]); + // 移动端导出页实时同步数据,无需手动「应用数据」 + useLayoutEffect(() => { + if (!isMobile || workflowStep !== 'print-view') return; + if (!importData || activeRows.length === 0) return; + + const nextExportRange = { ...draftExportRange }; + setAppliedExportData({ + fileName: importData.fileName, + currentSheet: importData.currentSheet, + rows: importData.rows, + mapping: { ...variableColumnMapping }, + exportRange: nextExportRange, + }); + setPreviewSnapshot({ + pages: layoutResult.pages, + gridCols: layoutResult.gridCols, + labelsPerPage: layoutResult.labelsPerPage, + totalPages: layoutResult.totalPages, + selectedCount: layoutResult.selectedCount, + paper, + exportRange: nextExportRange, + }); + setPreviewLayoutStale(false); + }, [ + isMobile, + workflowStep, + importData, + activeRows, + variableColumnMapping, + draftExportRange, + layoutResult, + paper, + ]); + const handleApplyData = async () => { if (!importData || activeRows.length === 0) { await showAlert('请先上传 Excel 或 CSV 数据。', { variant: 'warning' }); @@ -884,7 +918,7 @@ export default function App() {
- Shift/Ctrl+点击多选 · V 移动 · T 文本 · 图片工具栏 + Shift/Ctrl+点击多选 -
+
+ +
)} {!importData ? ( @@ -197,16 +214,16 @@ export const DataImporter: React.FC = ({ isMobile ? `mobile-upload-zone ${dragActive ? 'active' : ''}` : isPs - ? `border border-dashed rounded py-8 px-4 text-center cursor-pointer transition flex flex-col items-center gap-2 ${ - dragActive - ? 'border-[#31a8ff] bg-[#1a3a4f]/30' - : 'border-[#4a4a4a] hover:border-[#31a8ff]/60 hover:bg-[#1e1e1e]' - }` - : `border-2 border-dashed rounded-2xl py-10 px-6 text-center cursor-pointer transition flex flex-col items-center gap-3 ${ - dragActive - ? 'border-indigo-500 bg-indigo-50/20' - : 'border-gray-200 hover:border-indigo-400 hover:bg-gray-50/50' - }` + ? `border border-dashed rounded py-8 px-4 text-center cursor-pointer transition flex flex-col items-center gap-2 ${ + dragActive + ? 'border-[#31a8ff] bg-[#1a3a4f]/30' + : 'border-[#4a4a4a] hover:border-[#31a8ff]/60 hover:bg-[#1e1e1e]' + }` + : `border-2 border-dashed rounded-2xl py-10 px-6 text-center cursor-pointer transition flex flex-col items-center gap-3 ${ + dragActive + ? 'border-indigo-500 bg-indigo-50/20' + : 'border-gray-200 hover:border-indigo-400 hover:bg-gray-50/50' + }` } > = ({ isMobile ? 'text-[15px] font-semibold text-slate-700' : isPs - ? 'text-xs text-[#ccc]' - : 'text-xs text-gray-700' + ? 'text-xs text-[#ccc]' + : 'text-xs text-gray-700' }`} > {isMobile ? '点击选择 Excel / CSV' : '点击上传或拖拽文件到此处'}

{isMobile && ( -

- 支持 .xlsx、.xls、.csv -

+

支持 .xlsx、.xls、.csv

+ )} + {!isMobile && isPs && ( +

多工作表文件可在导入后切换 Sheet

)} @@ -246,43 +264,74 @@ export const DataImporter: React.FC = ({ isMobile ? 'mobile-file-card' : isPs - ? 'flex flex-wrap items-center justify-between p-2.5 bg-[#1e1e1e] border border-[#3a3a3a] rounded gap-3' - : 'flex flex-wrap items-center justify-between p-3.5 bg-gray-50 rounded-xl gap-4 border border-gray-100' + ? 'p-2.5 bg-[#1e1e1e] border border-[#3a3a3a] rounded space-y-2.5 min-w-0' + : 'p-3.5 bg-gray-50 rounded-xl border border-gray-100 space-y-3 min-w-0' } > -
- -
-

- {importData.fileName} -

-

- {rows.length} 条记录 · {columns.length} 列 -

+
+
+ +
+

+ {importData.fileName} +

+

+ {hasMultipleSheets && ( + + {importData.currentSheet} + {' · '} + + )} + {rows.length} 条记录 · {columns.length} 列 +

+
+
-
- {importData.sheets.length > 1 && ( + + {hasMultipleSheets && ( +
+ - )} - -
+ {sheetLoading && ( + + 切换中… + + )} +
+ )}
)}
diff --git a/src/components/DataRangeFields.tsx b/src/components/DataRangeFields.tsx index e7bba51..ae4592b 100644 --- a/src/components/DataRangeFields.tsx +++ b/src/components/DataRangeFields.tsx @@ -33,15 +33,6 @@ export const DataRangeFields: React.FC = ({ return (
{!isMobile &&
数据范围
} - {isMobile && ( -
- - 数据范围 -
- )} -

- 序号指数据行顺序(从 1 开始),应用数据后生效。 -

{isMobile && } {!isMobile && } @@ -51,9 +42,9 @@ export const DataRangeFields: React.FC = ({ onChange={(e) => patchRange({ mode: e.target.value as ExportRangeConfig['mode'] })} className={isMobile ? 'mobile-field' : 'ps-field text-[11px]'} > - - - + + +
diff --git a/src/components/MobileExportPropsPanel.tsx b/src/components/MobileExportPropsPanel.tsx index b138941..7a93774 100644 --- a/src/components/MobileExportPropsPanel.tsx +++ b/src/components/MobileExportPropsPanel.tsx @@ -32,46 +32,35 @@ export const MobileExportPropsPanel: React.FC = ({ }) => { const [open, setOpen] = useState(false); + if (locked) return null; + const paperLabel = paper.type === 'custom' ? `${paper.width}×${paper.height} mm` : paper.type; const summary = `${paperLabel} · ${paper.orientation === 'portrait' ? '纵向' : '横向'}${ cutLine.enabled ? ' · 裁切线' : '' }`; - const toggleOpen = () => { - if (locked) return; - setOpen((value) => !value); - }; - return ( -
+
- {open && !locked && ( + {open && (
void; cutLine: CutLineConfig; onChangeCutLine: (cutLine: CutLineConfig) => void; - dataStale: boolean; - dataApplied: boolean; - onApplyData: () => void; - draftSelectedCount: number; - appliedSelectedCount: number; - appliedTotalPages: number; + selectedCount: number; + totalPages: number; pdfGenerating: boolean; onBack: () => void; onExportPdf: () => void; } -function StepBadge({ - done, - active, - number, -}: { - done: boolean; - active: boolean; - number: number; -}) { - if (done) { - return ( - - - - ); - } - return ( - - {active ? number : } - - ); -} - export const MobileExportView: React.FC = ({ template, templateName, @@ -91,12 +62,8 @@ export const MobileExportView: React.FC = ({ onChangeDraftExportRange, cutLine, onChangeCutLine, - dataStale, - dataApplied, - onApplyData, - draftSelectedCount, - appliedSelectedCount, - appliedTotalPages, + selectedCount, + totalPages, pdfGenerating, onBack, onExportPdf, @@ -108,17 +75,13 @@ export const MobileExportView: React.FC = ({ const needsMapping = templateVariables.length > 0; const mappingComplete = !needsMapping || (hasData && mappedCount === templateVariables.length); - const currentStep = !hasData ? 1 : !mappingComplete ? 2 : 3; - const exportBlockedReason = !hasData ? '请先上传 Excel 数据' : !mappingComplete - ? `还有 ${templateVariables.length - mappedCount} 个变量未关联` - : dataStale || !dataApplied - ? '请先应用数据' - : appliedSelectedCount === 0 - ? '当前数据范围内无标签' - : null; + ? `还有 ${templateVariables.length - mappedCount} 个变量未关联` + : selectedCount === 0 + ? '当前数据范围内无标签' + : null; return (
@@ -130,34 +93,16 @@ export const MobileExportView: React.FC = ({

{templateName}

{template.width}×{template.height} mm - {hasData && dataApplied && !dataStale && ( + {hasData && selectedCount > 0 && ( {' '} - · {appliedSelectedCount} 枚 · {appliedTotalPages} 页 + · {selectedCount} 枚 · {totalPages} 页 )}

-
-
= 1 ? 'active' : ''} ${hasData ? 'done' : ''}`}> - - 数据 -
-
-
= 2 ? 'active' : ''} ${mappingComplete && hasData ? 'done' : ''}`} - > - - 关联 -
-
= 3 ? 'active' : ''}`}> - - 导出 -
-
-
@@ -165,7 +110,7 @@ export const MobileExportView: React.FC = ({
-

步骤 1 · 数据源

+

数据源

{hasData ? '已上传,可设置数据范围' : '上传 Excel 或 CSV 文件'}

-
- + )}
@@ -228,7 +151,7 @@ export const MobileExportView: React.FC = ({
-

步骤 2 · 关联变量

+

关联变量

{needsMapping ? `已关联 ${mappedCount}/${templateVariables.length}` @@ -281,7 +204,7 @@ export const MobileExportView: React.FC = ({ {exportBlockedReason ? '生成 PDF' - : `生成 PDF(${appliedSelectedCount} 枚 · ${appliedTotalPages} 页)`} + : `生成 PDF(${selectedCount} 枚 · ${totalPages} 页)`} )} diff --git a/src/components/PreviewGrid.tsx b/src/components/PreviewGrid.tsx index 18a86e7..d03351f 100644 --- a/src/components/PreviewGrid.tsx +++ b/src/components/PreviewGrid.tsx @@ -91,7 +91,7 @@ export const PaperSettingsPanel: React.FC = ({ const isPs = theme === 'ps'; const isMobile = theme === 'mobile'; const fieldClass = isMobile - ? 'mobile-field font-mono text-center' + ? 'mobile-field font-mono' : isPs ? 'ps-field font-mono text-center' : 'w-full px-2.5 py-1.5 bg-gray-50 border border-gray-100 rounded-lg text-xs text-center font-mono'; @@ -326,20 +326,19 @@ export const PaperSettingsPanel: React.FC = ({ />

))} -
- +
diff --git a/src/index.css b/src/index.css index 7353a44..10fe492 100644 --- a/src/index.css +++ b/src/index.css @@ -272,6 +272,7 @@ body, .ps-panel-body { color: var(--color-ps-text); overscroll-behavior: none; + min-width: 0; } .ps-panel-body input[type="number"], @@ -845,6 +846,7 @@ body, .mobile-export-card-body { padding: 18px 18px 20px; + min-width: 0; } .mobile-export-card-locked { @@ -927,10 +929,11 @@ body, .mobile-file-card { display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: space-between; - gap: 14px; + flex-direction: column; + align-items: stretch; + gap: 12px; + min-width: 0; + width: 100%; padding: 0; background: transparent; border: none;