init commit

This commit is contained in:
iqudoo
2026-06-12 19:34:27 +08:00
commit 89b59ecc1b
38 changed files with 17719 additions and 0 deletions

View 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>
);
};