init commit
This commit is contained in:
312
src/components/DataImporter.tsx
Normal file
312
src/components/DataImporter.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { ImportData, RecordRow } from '../types';
|
||||
import { FileSpreadsheet, Upload, Download, Trash2 } from 'lucide-react';
|
||||
import { useAppDialog } from './AppDialog';
|
||||
|
||||
export function downloadDataTemplate(templateName: string, templateVariables: string[]) {
|
||||
const columns = templateVariables.length > 0 ? templateVariables : ['数据'];
|
||||
const ws = XLSX.utils.aoa_to_sheet([columns, columns.map(() => '')]);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, '数据');
|
||||
const safeName = templateName.replace(/[^\w\u4e00-\u9fa5-]+/g, '_').slice(0, 30) || '标签';
|
||||
XLSX.writeFile(wb, `${safeName}_数据模板.xlsx`);
|
||||
}
|
||||
|
||||
interface DataImporterProps {
|
||||
importData: ImportData | null;
|
||||
onDataLoaded: (data: ImportData) => void;
|
||||
onClear: () => void;
|
||||
templateName: string;
|
||||
templateVariables: string[];
|
||||
variant?: 'default' | 'ps' | 'mobile';
|
||||
}
|
||||
|
||||
export const DataImporter: React.FC<DataImporterProps> = ({
|
||||
importData,
|
||||
onDataLoaded,
|
||||
onClear,
|
||||
templateName,
|
||||
templateVariables,
|
||||
variant = 'default',
|
||||
}) => {
|
||||
const { showAlert } = useAppDialog();
|
||||
const isPs = variant === 'ps';
|
||||
const isMobile = variant === 'mobile';
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [dragActive, setDragActive] = useState<boolean>(false);
|
||||
|
||||
const handleDrag = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.type === 'dragenter' || e.type === 'dragover') {
|
||||
setDragActive(true);
|
||||
} else if (e.type === 'dragleave') {
|
||||
setDragActive(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragActive(false);
|
||||
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||||
processFile(e.dataTransfer.files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
processFile(e.target.files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const processFile = (file: File) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
const data = new Uint8Array(e.target?.result as ArrayBuffer);
|
||||
const workbook = XLSX.read(data, { type: 'array' });
|
||||
const firstSheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[firstSheetName];
|
||||
const jsonData = XLSX.utils.sheet_to_json<any[]>(worksheet, { header: 1, defval: '' });
|
||||
|
||||
if (jsonData.length === 0) {
|
||||
await showAlert('表格为空或格式不正确', { variant: 'warning' });
|
||||
return;
|
||||
}
|
||||
|
||||
const rawColumns = jsonData[0] as string[];
|
||||
const columns = rawColumns.map((c, idx) => (c ? String(c).trim() : `列_${idx + 1}`)).filter(Boolean);
|
||||
const rows: RecordRow[] = [];
|
||||
for (let i = 1; i < jsonData.length; i++) {
|
||||
const rawRow = jsonData[i] as any[];
|
||||
if (rawRow.every((cell) => cell === undefined || cell === null || String(cell).trim() === '')) {
|
||||
continue;
|
||||
}
|
||||
const row: RecordRow = {};
|
||||
columns.forEach((col, colIdx) => {
|
||||
row[col] = rawRow[colIdx] !== undefined ? String(rawRow[colIdx]).trim() : '';
|
||||
});
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
onDataLoaded({
|
||||
fileName: file.name,
|
||||
sheets: workbook.SheetNames,
|
||||
currentSheet: firstSheetName,
|
||||
columns,
|
||||
rows,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
await showAlert('解析文件出错,请确保是标准的 Excel 或 CSV 文件', { variant: 'error' });
|
||||
}
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
};
|
||||
|
||||
const handleSheetChange = (sheetName: string) => {
|
||||
if (!fileInputRef.current?.files?.[0]) return;
|
||||
const file = fileInputRef.current.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
const data = new Uint8Array(e.target?.result as ArrayBuffer);
|
||||
const workbook = XLSX.read(data, { type: 'array' });
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
const jsonData = XLSX.utils.sheet_to_json<any[]>(worksheet, { header: 1, defval: '' });
|
||||
if (jsonData.length === 0) return;
|
||||
|
||||
const rawColumns = jsonData[0] as string[];
|
||||
const columns = rawColumns.map((c, idx) => (c ? String(c).trim() : `列_${idx + 1}`)).filter(Boolean);
|
||||
const rows: RecordRow[] = [];
|
||||
for (let i = 1; i < jsonData.length; i++) {
|
||||
const rawRow = jsonData[i] as any[];
|
||||
if (rawRow.every((cell) => cell === undefined || cell === null || String(cell).trim() === '')) {
|
||||
continue;
|
||||
}
|
||||
const row: RecordRow = {};
|
||||
columns.forEach((col, colIdx) => {
|
||||
row[col] = rawRow[colIdx] !== undefined ? String(rawRow[colIdx]).trim() : '';
|
||||
});
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
onDataLoaded({
|
||||
fileName: file.name,
|
||||
sheets: workbook.SheetNames,
|
||||
currentSheet: sheetName,
|
||||
columns,
|
||||
rows,
|
||||
});
|
||||
} catch {
|
||||
await showAlert('切换工作表时出错', { variant: 'error' });
|
||||
}
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
};
|
||||
|
||||
const rows = importData?.rows || [];
|
||||
const columns = importData?.columns || [];
|
||||
|
||||
const wrapperClass = isMobile
|
||||
? ''
|
||||
: isPs
|
||||
? 'space-y-3'
|
||||
: 'bg-white border border-gray-200 rounded-2xl p-6 shadow-sm flex flex-col gap-5';
|
||||
|
||||
return (
|
||||
<div className={wrapperClass}>
|
||||
{/* {!isPs && (
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-gray-900 leading-none">批量数据源导入</h3>
|
||||
<p className="text-[11px] text-gray-400 mt-1">上传 Excel 或 CSV,绑定当前模板变量</p>
|
||||
</div>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{!isMobile && (
|
||||
<div className={`flex flex-wrap gap-2 ${isPs ? '' : 'justify-end'}`}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => downloadDataTemplate(templateName, templateVariables)}
|
||||
className={
|
||||
isMobile
|
||||
? 'mobile-secondary-btn w-full'
|
||||
: isPs
|
||||
? 'ps-btn text-[10px] w-full justify-center'
|
||||
: 'inline-flex items-center gap-1.5 px-3 py-1.5 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-lg text-xs font-semibold cursor-pointer'
|
||||
}
|
||||
>
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
下载数据模板
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!importData ? (
|
||||
<div
|
||||
onDragEnter={handleDrag}
|
||||
onDragOver={handleDrag}
|
||||
onDragLeave={handleDrag}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className={
|
||||
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'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".xlsx,.xls,.csv"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
<Upload
|
||||
className={`${isMobile ? 'w-8 h-8' : 'w-6 h-6'} ${
|
||||
isPs ? 'text-[#777]' : isMobile ? 'text-indigo-400' : 'text-gray-400'
|
||||
}`}
|
||||
/>
|
||||
<div>
|
||||
<p
|
||||
className={`${
|
||||
isMobile
|
||||
? 'text-[15px] font-semibold text-slate-700'
|
||||
: isPs
|
||||
? 'text-xs text-[#ccc]'
|
||||
: 'text-xs text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{isMobile ? '点击选择 Excel / CSV' : '点击上传或拖拽文件到此处'}
|
||||
</p>
|
||||
{isMobile && (
|
||||
<p className="text-xs text-slate-400 leading-relaxed">
|
||||
支持 .xlsx、.xls、.csv
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={
|
||||
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'
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden min-w-0">
|
||||
<FileSpreadsheet
|
||||
className={`w-5 h-5 shrink-0 ${isPs ? 'text-emerald-400' : 'text-emerald-600'}`}
|
||||
/>
|
||||
<div className="overflow-hidden min-w-0">
|
||||
<p
|
||||
className={`truncate ${
|
||||
isMobile ? 'text-sm text-slate-800' : isPs ? 'text-xs text-[#e8e8e8]' : 'text-gray-800'
|
||||
}`}
|
||||
>
|
||||
{importData.fileName}
|
||||
</p>
|
||||
<p
|
||||
className={`mt-0.5 ${
|
||||
isMobile ? 'text-xs text-slate-500' : isPs ? 'text-[10px] text-[#777]' : 'text-[10px] text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{rows.length} 条记录 · {columns.length} 列
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{importData.sheets.length > 1 && (
|
||||
<select
|
||||
value={importData.currentSheet}
|
||||
onChange={(e) => handleSheetChange(e.target.value)}
|
||||
className={
|
||||
isMobile
|
||||
? 'mobile-field text-sm py-2'
|
||||
: isPs
|
||||
? 'ps-field text-[10px] py-1'
|
||||
: 'px-2.5 py-1 text-xs bg-white border border-gray-100 rounded-lg'
|
||||
}
|
||||
>
|
||||
{importData.sheets.map((sheet) => (
|
||||
<option key={sheet} value={sheet}>
|
||||
{sheet}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClear}
|
||||
className={
|
||||
isMobile
|
||||
? 'mobile-text-btn danger'
|
||||
: isPs
|
||||
? 'ps-btn text-[10px] text-rose-300 hover:text-rose-200'
|
||||
: 'flex items-center gap-1 text-xs font-semibold text-rose-600 hover:bg-rose-50 px-2.5 py-1.5 rounded-lg cursor-pointer'
|
||||
}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user