217 lines
6.7 KiB
TypeScript
217 lines
6.7 KiB
TypeScript
import React from 'react';
|
||
import {
|
||
ChevronLeft,
|
||
Printer,
|
||
Loader2,
|
||
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;
|
||
selectedCount: number;
|
||
totalPages: number;
|
||
pdfGenerating: boolean;
|
||
onBack: () => void;
|
||
onExportPdf: () => void;
|
||
}
|
||
|
||
export const MobileExportView: React.FC<MobileExportViewProps> = ({
|
||
template,
|
||
templateName,
|
||
templateVariables,
|
||
importData,
|
||
onDataLoaded,
|
||
onClear,
|
||
dataColumns,
|
||
variableColumnMapping,
|
||
onChangeMapping,
|
||
paper,
|
||
onChangePaper,
|
||
rows,
|
||
draftExportRange,
|
||
onChangeDraftExportRange,
|
||
cutLine,
|
||
onChangeCutLine,
|
||
selectedCount,
|
||
totalPages,
|
||
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 exportBlockedReason = !hasData
|
||
? '请先上传 Excel 数据'
|
||
: !mappingComplete
|
||
? `还有 ${templateVariables.length - mappedCount} 个变量未关联`
|
||
: selectedCount === 0
|
||
? '当前数据范围内无标签'
|
||
: null;
|
||
|
||
return (
|
||
<div className="mobile-export-view">
|
||
<header className="no-print bg-indigo-700 border-b border-indigo-900 shrink-0 shadow mobile-export-header px-4">
|
||
<div className="flex items-center w-full min-h-[44px]">
|
||
<button
|
||
type="button"
|
||
onClick={onBack}
|
||
className="mobile-export-back shrink-0"
|
||
aria-label="返回"
|
||
>
|
||
<ChevronLeft className="w-5 h-5" />
|
||
</button>
|
||
<p
|
||
className="flex-1 min-w-0 px-2 text-center text-sm font-bold tracking-wide text-slate-100 truncate"
|
||
title={templateName}
|
||
>
|
||
{templateName}
|
||
</p>
|
||
<div className="w-10 shrink-0" aria-hidden />
|
||
</div>
|
||
</header>
|
||
|
||
<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>数据源</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>
|
||
</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>关联变量</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 生成中
|
||
</>
|
||
) : (
|
||
<>
|
||
<Printer className="w-5 h-5 shrink-0" />
|
||
{exportBlockedReason
|
||
? '生成标签PDF文件'
|
||
: `生成标签PDF文件(${selectedCount} 枚 · ${totalPages} 页)`}
|
||
</>
|
||
)}
|
||
</button>
|
||
</footer>
|
||
</div>
|
||
);
|
||
};
|