47 lines
4.6 KiB
TypeScript
47 lines
4.6 KiB
TypeScript
import { useMutation } from '@tanstack/react-query';
|
||
import { Button, Card, Input, Select, Upload } from '@douyinfe/semi-ui';
|
||
import { useState } from 'react';
|
||
import { api } from '../../api/client';
|
||
import type { VehicleProfileSyncItem, VehicleProfileSyncResult } from '../../api/types';
|
||
import { parseVehicleProfileSyncCSV, vehicleProfileSyncCSVHeader } from '../domain/profileSync';
|
||
|
||
export default function VehicleProfileSyncPanel({ onClose }: { onClose: () => void }) {
|
||
const [sourceSystem, setSourceSystem] = useState('');
|
||
const [sourceVersion, setSourceVersion] = useState('');
|
||
const [conflictPolicy, setConflictPolicy] = useState<'preserve' | 'overwrite'>('preserve');
|
||
const [items, setItems] = useState<VehicleProfileSyncItem[]>([]);
|
||
const [fileName, setFileName] = useState('');
|
||
const [parseError, setParseError] = useState('');
|
||
const sync = useMutation<VehicleProfileSyncResult, Error, boolean>({
|
||
mutationFn: (dryRun) => api.syncVehicleProfiles({ sourceSystem: sourceSystem.trim(), sourceVersion: sourceVersion.trim(), conflictPolicy, dryRun, items })
|
||
});
|
||
const readFile = async (file?: File) => {
|
||
sync.reset(); setItems([]); setFileName(file?.name ?? ''); setParseError('');
|
||
if (!file) return;
|
||
try { setItems(parseVehicleProfileSyncCSV(await file.text())); } catch (error) { setParseError(error instanceof Error ? error.message : 'CSV 解析失败'); }
|
||
};
|
||
const ready = sourceSystem.trim() !== '' && sourceVersion.trim() !== '' && items.length > 0 && !sync.isPending;
|
||
const issues = sync.data?.items.filter((item) => item.status.startsWith('conflict_') || item.status === 'missing_vehicle').slice(0, 20) ?? [];
|
||
const applied = sync.data && !sync.data.dryRun;
|
||
|
||
return <Card className="v2-profile-sync-panel" aria-label="车辆主档批量同步">
|
||
<header><div><strong>批量同步车辆主档</strong><p>CSV 最多 500 辆;先预演,再写入。默认保留人工档案和其他来源。</p></div><Button theme="borderless" type="tertiary" onClick={onClose}>关闭</Button></header>
|
||
<div className="v2-profile-sync-fields">
|
||
<label><span>来源系统标识</span><Input value={sourceSystem} onChange={(value) => { setSourceSystem(value); sync.reset(); }} placeholder="例如 oem-tsp" maxLength={64} /></label>
|
||
<label><span>来源版本</span><Input value={sourceVersion} onChange={(value) => { setSourceVersion(value); sync.reset(); }} placeholder="例如 snapshot-20260714-01" maxLength={128} /></label>
|
||
<label><span>冲突策略</span><Select value={conflictPolicy} onChange={(value) => { setConflictPolicy(String(value) as 'preserve' | 'overwrite'); sync.reset(); }} optionList={[{ value: 'preserve', label: '保护现有来源' }, { value: 'overwrite', label: '显式覆盖现有来源' }]} /></label>
|
||
<label className="is-file"><span>CSV 文件</span><Upload action="" accept=".csv,text/csv" limit={1} uploadTrigger="custom" showUploadList={false} onFileChange={(files) => { void readFile(files[0]); }}><Button theme="light">选择 CSV 文件</Button></Upload></label>
|
||
</div>
|
||
<p className="v2-profile-sync-format">表头:<code>{vehicleProfileSyncCSVHeader}</code></p>
|
||
{fileName ? <p className="v2-profile-sync-file">{fileName} · 已读取 {items.length} 辆</p> : null}
|
||
{parseError ? <p className="v2-profile-sync-error">{parseError}</p> : null}
|
||
{sync.isError ? <p className="v2-profile-sync-error">{sync.error.message}</p> : null}
|
||
{sync.data ? <div className="v2-profile-sync-result">
|
||
<div><span>收到<strong>{sync.data.received}</strong></span><span>新增<strong>{sync.data.created}</strong></span><span>更新<strong>{sync.data.updated}</strong></span><span>未变化<strong>{sync.data.unchanged}</strong></span><span>冲突<strong>{sync.data.conflicted}</strong></span><span>身份缺失<strong>{sync.data.missing}</strong></span></div>
|
||
{issues.length ? <ul>{issues.map((item) => <li key={item.vin}><b>{item.vin}</b><span>{item.status === 'missing_vehicle' ? '网关身份不存在' : item.status === 'conflict_source_version' ? '同来源版本内容不一致' : `现有来源 ${item.previousSource || '未知'} 已保护`}</span></li>)}</ul> : <p>未发现来源冲突或身份缺失。</p>}
|
||
</div> : null}
|
||
{conflictPolicy === 'overwrite' ? <p className="v2-profile-sync-warning">覆盖模式会接管人工或其他系统维护的补充主档,请先确认预演结果。</p> : null}
|
||
<footer><Button theme="light" onClick={() => sync.mutate(true)} disabled={!ready} loading={sync.isPending}>预演同步</Button><Button theme="solid" onClick={() => sync.mutate(false)} disabled={!ready || !sync.data?.dryRun}>{applied ? '已完成写入' : '确认写入'}</Button></footer>
|
||
</Card>;
|
||
}
|