init commit
This commit is contained in:
291
src/components/MobileExportView.tsx
Normal file
291
src/components/MobileExportView.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
ChevronLeft,
|
||||
FileDown,
|
||||
Loader2,
|
||||
CheckCircle2,
|
||||
Circle,
|
||||
Database,
|
||||
Link2,
|
||||
Download,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
ImportData,
|
||||
LabelTemplate,
|
||||
RecordRow,
|
||||
ExportRangeConfig,
|
||||
PaperConfig,
|
||||
CutLineConfig,
|
||||
} from '../types';
|
||||
import { isVariableMappingConfigured } from '../utils';
|
||||
import { DataImporter, downloadDataTemplate } from './DataImporter';
|
||||
import { VariableMappingPanel } from './VariableMappingPanel';
|
||||
import { MobileExportPropsPanel } from './MobileExportPropsPanel';
|
||||
import { DataRangeFields } from './DataRangeFields';
|
||||
|
||||
interface MobileExportViewProps {
|
||||
template: LabelTemplate;
|
||||
templateName: string;
|
||||
templateVariables: string[];
|
||||
importData: ImportData | null;
|
||||
onDataLoaded: (data: ImportData) => void;
|
||||
onClear: () => void;
|
||||
dataColumns: string[];
|
||||
variableColumnMapping: Record<string, string>;
|
||||
onChangeMapping: (mapping: Record<string, string>) => void;
|
||||
paper: PaperConfig;
|
||||
onChangePaper: (paper: PaperConfig) => void;
|
||||
rows: RecordRow[];
|
||||
draftExportRange: ExportRangeConfig;
|
||||
onChangeDraftExportRange: (range: ExportRangeConfig) => void;
|
||||
cutLine: CutLineConfig;
|
||||
onChangeCutLine: (cutLine: CutLineConfig) => void;
|
||||
dataStale: boolean;
|
||||
dataApplied: boolean;
|
||||
onApplyData: () => void;
|
||||
draftSelectedCount: number;
|
||||
appliedSelectedCount: number;
|
||||
appliedTotalPages: number;
|
||||
pdfGenerating: boolean;
|
||||
onBack: () => void;
|
||||
onExportPdf: () => void;
|
||||
}
|
||||
|
||||
function StepBadge({
|
||||
done,
|
||||
active,
|
||||
number,
|
||||
}: {
|
||||
done: boolean;
|
||||
active: boolean;
|
||||
number: number;
|
||||
}) {
|
||||
if (done) {
|
||||
return (
|
||||
<span className="mobile-step-badge done">
|
||||
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className={`mobile-step-badge ${active ? 'active' : ''}`}>
|
||||
{active ? number : <Circle className="w-3 h-3 opacity-50" />}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export const MobileExportView: React.FC<MobileExportViewProps> = ({
|
||||
template,
|
||||
templateName,
|
||||
templateVariables,
|
||||
importData,
|
||||
onDataLoaded,
|
||||
onClear,
|
||||
dataColumns,
|
||||
variableColumnMapping,
|
||||
onChangeMapping,
|
||||
paper,
|
||||
onChangePaper,
|
||||
rows,
|
||||
draftExportRange,
|
||||
onChangeDraftExportRange,
|
||||
cutLine,
|
||||
onChangeCutLine,
|
||||
dataStale,
|
||||
dataApplied,
|
||||
onApplyData,
|
||||
draftSelectedCount,
|
||||
appliedSelectedCount,
|
||||
appliedTotalPages,
|
||||
pdfGenerating,
|
||||
onBack,
|
||||
onExportPdf,
|
||||
}) => {
|
||||
const hasData = rows.length > 0;
|
||||
const mappedCount = templateVariables.filter((v) =>
|
||||
isVariableMappingConfigured(variableColumnMapping, v, dataColumns)
|
||||
).length;
|
||||
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;
|
||||
|
||||
return (
|
||||
<div className="mobile-export-view">
|
||||
<header className="mobile-export-header">
|
||||
<button type="button" onClick={onBack} className="mobile-export-back" aria-label="返回">
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="mobile-export-header-text min-w-0 flex-1">
|
||||
<p className="mobile-export-title truncate">{templateName}</p>
|
||||
<p className="mobile-export-subtitle">
|
||||
{template.width}×{template.height} mm
|
||||
{hasData && dataApplied && !dataStale && (
|
||||
<span>
|
||||
{' '}
|
||||
· {appliedSelectedCount} 枚 · {appliedTotalPages} 页
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="mobile-export-steps" aria-label="导出步骤">
|
||||
<div className={`mobile-export-step ${currentStep >= 1 ? 'active' : ''} ${hasData ? 'done' : ''}`}>
|
||||
<StepBadge done={hasData} active={currentStep === 1} number={1} />
|
||||
<span>数据</span>
|
||||
</div>
|
||||
<div className="mobile-export-step-line" />
|
||||
<div
|
||||
className={`mobile-export-step ${currentStep >= 2 ? 'active' : ''} ${mappingComplete && hasData ? 'done' : ''}`}
|
||||
>
|
||||
<StepBadge done={mappingComplete && hasData} active={currentStep === 2} number={2} />
|
||||
<span>关联</span>
|
||||
</div>
|
||||
<div className={`mobile-export-step ${currentStep >= 3 ? 'active' : ''}`}>
|
||||
<StepBadge done={dataApplied && !dataStale && mappingComplete} active={currentStep === 3} number={3} />
|
||||
<span>导出</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="mobile-export-content">
|
||||
<section className="mobile-export-card">
|
||||
<div className="mobile-export-card-head">
|
||||
<Database className="w-4 h-4 text-indigo-500 shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<h2>步骤 1 · 数据源</h2>
|
||||
<p>{hasData ? '已上传,可设置数据范围' : '上传 Excel 或 CSV 文件'}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => downloadDataTemplate(templateName, templateVariables)}
|
||||
className="mobile-export-icon-btn"
|
||||
aria-label="下载数据模板"
|
||||
>
|
||||
<Download className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mobile-export-card-body space-y-5">
|
||||
<DataImporter
|
||||
variant="mobile"
|
||||
importData={importData}
|
||||
onDataLoaded={onDataLoaded}
|
||||
onClear={onClear}
|
||||
templateName={templateName}
|
||||
templateVariables={templateVariables}
|
||||
/>
|
||||
{hasData && (
|
||||
<>
|
||||
<DataRangeFields
|
||||
variant="mobile"
|
||||
exportRange={draftExportRange}
|
||||
onChangeExportRange={onChangeDraftExportRange}
|
||||
rows={rows}
|
||||
paper={paper}
|
||||
template={template}
|
||||
/>
|
||||
<div className="space-y-2 pt-1 border-t border-slate-100">
|
||||
{dataStale && (
|
||||
<p className="text-xs text-amber-600">数据或数据范围已变更,请先应用数据</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onApplyData}
|
||||
disabled={draftSelectedCount === 0}
|
||||
className={`w-full inline-flex items-center justify-center gap-2 min-h-[44px] px-4 rounded-xl text-sm font-semibold transition cursor-pointer ${
|
||||
draftSelectedCount > 0
|
||||
? dataStale || !dataApplied
|
||||
? 'bg-indigo-600 text-white active:bg-indigo-700'
|
||||
: 'bg-slate-100 text-slate-600 border border-slate-200'
|
||||
: 'bg-slate-100 text-slate-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
<Database className="w-4 h-4 shrink-0" />
|
||||
应用数据
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{hasData && (
|
||||
<section className="mobile-export-card">
|
||||
<div className="mobile-export-card-head">
|
||||
<Link2 className="w-4 h-4 text-indigo-500 shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2>步骤 2 · 关联变量</h2>
|
||||
<p>
|
||||
{needsMapping
|
||||
? `已关联 ${mappedCount}/${templateVariables.length}`
|
||||
: '此模板无需变量映射'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mobile-export-card-body">
|
||||
<VariableMappingPanel
|
||||
variant="mobile"
|
||||
variables={templateVariables}
|
||||
columns={dataColumns}
|
||||
mapping={variableColumnMapping}
|
||||
onChangeMapping={onChangeMapping}
|
||||
variableDefaults={template.variableDefaults}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<MobileExportPropsPanel
|
||||
template={template}
|
||||
paper={paper}
|
||||
onChangePaper={onChangePaper}
|
||||
rows={rows}
|
||||
exportRange={draftExportRange}
|
||||
cutLine={cutLine}
|
||||
onChangeCutLine={onChangeCutLine}
|
||||
locked={!hasData}
|
||||
/>
|
||||
</main>
|
||||
|
||||
<footer className="mobile-export-footer">
|
||||
{exportBlockedReason && !pdfGenerating && (
|
||||
<p className="mobile-export-footer-hint">{exportBlockedReason}</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onExportPdf}
|
||||
disabled={!!exportBlockedReason || pdfGenerating}
|
||||
className="mobile-export-submit"
|
||||
>
|
||||
{pdfGenerating ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin shrink-0" />
|
||||
PDF 生成中
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileDown className="w-5 h-5 shrink-0" />
|
||||
{exportBlockedReason
|
||||
? '生成 PDF'
|
||||
: `生成 PDF(${appliedSelectedCount} 枚 · ${appliedTotalPages} 页)`}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user