176 lines
6.1 KiB
TypeScript
176 lines
6.1 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { ArrowLeft, Upload, Zap } from 'lucide-react';
|
|
import { fetchJson } from '../../auth/api-client';
|
|
import { useAuth } from '../../auth/useAuth';
|
|
import FeedbackFab from '../../components/FeedbackFab';
|
|
import RotatingFooterHint from '../../components/RotatingFooterHint';
|
|
import { PageFrame } from '../../components/ui/surface';
|
|
import { BatchTable } from './import-page/BatchTable';
|
|
import { RecordFilters } from './import-page/RecordFilters';
|
|
import { RecordTable } from './import-page/RecordTable';
|
|
import { SummaryKpis } from './import-page/SummaryKpis';
|
|
import { UploadDropzone } from './import-page/UploadDropzone';
|
|
import { UploadFeedback } from './import-page/UploadFeedback';
|
|
import type {
|
|
BatchRow,
|
|
ListItem,
|
|
OverallRow,
|
|
UploadResult,
|
|
VehicleKindFilter,
|
|
} from './import-page/types';
|
|
|
|
function getJwt(): string | null {
|
|
return sessionStorage.getItem('bi_jwt');
|
|
}
|
|
|
|
async function uploadFile(file: File): Promise<UploadResult> {
|
|
const fd = new FormData();
|
|
fd.append('file', file);
|
|
const token = getJwt();
|
|
const res = await fetch('/api/ele/import', {
|
|
method: 'POST',
|
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
body: fd,
|
|
});
|
|
const json = await res.json();
|
|
if (!res.ok || !json.ok) throw new Error(json.message || `上传失败 (${res.status})`);
|
|
return json as UploadResult;
|
|
}
|
|
|
|
export default function EleImportPage() {
|
|
const { user } = useAuth();
|
|
const [file, setFile] = useState<File | null>(null);
|
|
const [uploading, setUploading] = useState(false);
|
|
const [result, setResult] = useState<UploadResult | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [items, setItems] = useState<ListItem[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [overall, setOverall] = useState<OverallRow[]>([]);
|
|
const [batches, setBatches] = useState<BatchRow[]>([]);
|
|
const [filter, setFilter] = useState<VehicleKindFilter>('');
|
|
const [batchFilter, setBatchFilter] = useState('');
|
|
const [search, setSearch] = useState('');
|
|
const [searchInput, setSearchInput] = useState('');
|
|
const [dragOver, setDragOver] = useState(false);
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const reload = useCallback(async () => {
|
|
const params = new URLSearchParams({ page: '1', limit: '50' });
|
|
if (filter) params.set('kind', filter);
|
|
if (batchFilter) params.set('batchId', batchFilter);
|
|
if (search) params.set('search', search);
|
|
const [list, agg, b] = await Promise.all([
|
|
fetchJson<{ items: ListItem[]; total: number }>(`/api/ele/list?${params.toString()}`),
|
|
fetchJson<{ overall: OverallRow[] }>(`/api/ele/aggregate`),
|
|
fetchJson<{ items: BatchRow[] }>(`/api/ele/batches`),
|
|
]);
|
|
setItems(list.items);
|
|
setTotal(list.total);
|
|
setOverall(agg.overall);
|
|
setBatches(b.items);
|
|
}, [filter, batchFilter, search]);
|
|
|
|
useEffect(() => {
|
|
reload().catch(e => console.error(e));
|
|
}, [reload]);
|
|
|
|
const handleUpload = async (selectedFile: File) => {
|
|
setUploading(true);
|
|
setError(null);
|
|
setResult(null);
|
|
try {
|
|
const nextResult = await uploadFile(selectedFile);
|
|
setResult(nextResult);
|
|
await reload();
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : String(e));
|
|
} finally {
|
|
setUploading(false);
|
|
}
|
|
};
|
|
|
|
const onPick = (selectedFile: File | null) => {
|
|
setFile(selectedFile);
|
|
if (selectedFile) handleUpload(selectedFile);
|
|
};
|
|
|
|
return (
|
|
<PageFrame
|
|
title="充电记录导入"
|
|
subtitle="每日上传 xlsx,按订单编号去重,并自动匹配内部/外部车辆归属。"
|
|
icon={Zap}
|
|
eyebrow="ELECTRIC IMPORT"
|
|
meta={user?.userName || '导入工作台'}
|
|
actions={(
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => {
|
|
if (window.history.length > 1) window.history.back();
|
|
else { window.location.hash = '#mileage'; }
|
|
}}
|
|
className="flex h-9 w-9 items-center justify-center rounded-xl border border-slate-100 bg-white text-slate-500 transition-colors hover:border-blue-200 hover:bg-blue-50 hover:text-blue-600"
|
|
title="返回"
|
|
>
|
|
<ArrowLeft size={16} />
|
|
</button>
|
|
<button
|
|
onClick={() => inputRef.current?.click()}
|
|
className="inline-flex h-9 items-center gap-1.5 rounded-xl bg-slate-900 px-3 text-xs font-black text-white shadow-sm transition-colors hover:bg-slate-800"
|
|
>
|
|
<Upload size={14} />
|
|
上传文件
|
|
</button>
|
|
</div>
|
|
)}
|
|
>
|
|
<UploadDropzone
|
|
inputRef={inputRef}
|
|
file={file}
|
|
uploading={uploading}
|
|
dragOver={dragOver}
|
|
onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
|
|
onDragLeave={() => setDragOver(false)}
|
|
onDrop={(e) => {
|
|
e.preventDefault();
|
|
setDragOver(false);
|
|
const droppedFile = e.dataTransfer.files?.[0];
|
|
if (droppedFile) onPick(droppedFile);
|
|
}}
|
|
onClick={() => inputRef.current?.click()}
|
|
onInputChange={(e) => onPick(e.target.files?.[0] || null)}
|
|
/>
|
|
|
|
<UploadFeedback
|
|
result={result}
|
|
error={error}
|
|
onCloseResult={() => setResult(null)}
|
|
onCloseError={() => setError(null)}
|
|
/>
|
|
|
|
<SummaryKpis overall={overall} />
|
|
|
|
<BatchTable
|
|
batches={batches}
|
|
batchFilter={batchFilter}
|
|
onClearFilter={() => setBatchFilter('')}
|
|
onSelectBatch={(batchId) => setBatchFilter(batchFilter === batchId ? '' : batchId)}
|
|
/>
|
|
|
|
<section className="bg-white rounded-2xl border border-slate-100 shadow-sm overflow-hidden">
|
|
<RecordFilters
|
|
total={total}
|
|
searchInput={searchInput}
|
|
filter={filter}
|
|
onSearchInputChange={(e) => setSearchInput(e.target.value)}
|
|
onSearchKeyDown={(e) => { if (e.key === 'Enter') setSearch(searchInput); }}
|
|
onFilterChange={setFilter}
|
|
/>
|
|
<RecordTable items={items} />
|
|
</section>
|
|
|
|
<RotatingFooterHint className="pb-4" />
|
|
<FeedbackFab module="ele" />
|
|
</PageFrame>
|
|
);
|
|
}
|