feat(platform): add customer history export wizard

This commit is contained in:
lingniu
2026-07-06 05:58:05 +08:00
parent 8b7614d2d6
commit 55a65d2bc6
3 changed files with 190 additions and 0 deletions

View File

@@ -1684,6 +1684,13 @@ export function History({
const evidenceDateFrom = filters.dateFrom?.trim()
|| dateOnly(rawFieldRows[0]?.deviceTime || rawItems[0]?.deviceTime || firstLocation?.deviceTime || firstLocation?.serverTime);
const evidenceDateTo = filters.dateTo?.trim() || (evidenceDateFrom ? nextDate(evidenceDateFrom) : '');
const exportWizardEvidenceLabel = rawFieldRows.length > 0
? '字段证据'
: validLocations.length > 0
? '轨迹报告'
: (rawFrames.total ?? 0) > 0
? '明细证据'
: '待查询证据';
const copyDeliveryCover = () => {
copyText(
deliveryCoverText({
@@ -1700,6 +1707,65 @@ export function History({
'客户数据交付封面包'
);
};
const copyExportWizardPackage = () => {
const evidenceFilters = {
...(evidenceDateFrom ? { dateFrom: evidenceDateFrom } : {}),
...(evidenceDateTo ? { dateTo: evidenceDateTo } : {})
};
const fieldFilters = {
tab: 'fields',
...evidenceFilters,
includeFields: 'true',
...(filters.fields?.trim() ? { fields: filters.fields.trim() } : {})
};
const lines = [
'【客户导出向导包】',
`步骤1 锁定范围:${currentVehicleKeyword || '全部车辆'} / ${currentProtocol || '全部来源证据'} / ${evidenceDateFrom || '-'}${evidenceDateTo || '-'}`,
`步骤2 选择证据:${exportWizardEvidenceLabel}`,
`步骤3 质量复核:${deliveryState}`,
'步骤4 导出交付:复制交付包',
`位置历史:${appURL(buildAppHash({ page: 'history-query', keyword: currentVehicleKeyword, protocol: currentProtocol, filters: { ...evidenceFilters, tab: 'location' } }))}`,
`明细证据:${appURL(buildAppHash({ page: 'history-query', keyword: currentVehicleKeyword, protocol: currentProtocol, filters: { ...evidenceFilters, tab: 'raw', includeFields: 'true' } }))}`,
`字段裁剪:${appURL(buildAppHash({ page: 'history-query', keyword: currentVehicleKeyword, protocol: currentProtocol, filters: fieldFilters }))}`,
`轨迹回放:${appURL(buildAppHash({ page: 'history', keyword: currentVehicleKeyword, protocol: currentProtocol, filters: evidenceFilters }))}`,
`统计查询:${appURL(buildAppHash({ page: 'mileage', keyword: currentVehicleKeyword, protocol: currentProtocol, filters: evidenceFilters }))}`
];
copyText(lines.join('\n'), '客户导出向导包');
};
const exportWizardSteps = [
{
step: '1',
title: '锁定范围',
value: `${currentVehicleKeyword || '全部车辆'} / ${currentProtocol || '全部来源证据'}`,
detail: `${evidenceDateFrom || '-'}${evidenceDateTo || '-'},先确认客户要查的车辆和时间窗。`,
color: currentVehicleKeyword ? 'green' as const : 'orange' as const,
onClick: () => document.querySelector<HTMLInputElement>('input[name="keyword"]')?.focus()
},
{
step: '2',
title: '选择证据',
value: exportWizardEvidenceLabel,
detail: rawFieldRows.length > 0 ? `${rawFieldRows.length.toLocaleString()} 个字段可按客户口径导出。` : `${validLocations.length.toLocaleString()} 个有效坐标,${(rawFrames.total ?? 0).toLocaleString()} 帧明细。`,
color: rawFieldRows.length > 0 || validLocations.length > 0 ? 'blue' as const : 'orange' as const,
onClick: () => rawFieldRows.length > 0 ? openQueryTab('fields') : validLocations.length > 0 ? openQueryTab('location') : openQueryTab('raw')
},
{
step: '3',
title: '质量复核',
value: deliveryState,
detail: anomalyTotal > 0 ? `存在 ${anomalyTotal.toLocaleString()} 项质量提示,交付时附说明。` : '当前范围可生成客户可读交付说明。',
color: deliveryStateColor,
onClick: () => copyDeliveryCover()
},
{
step: '4',
title: '导出交付',
value: '复制交付包',
detail: '把位置、明细、字段、轨迹和统计链接打包给客户或内部复核。',
color: 'blue' as const,
onClick: () => copyExportWizardPackage()
}
];
const copyScheduledHistoryReportPlan = () => {
copyText(
scheduledHistoryReportPlanText({
@@ -2295,6 +2361,40 @@ export function History({
<Tag color={currentProtocol ? 'blue' : 'green'}>{currentProtocol || '全部来源证据'}</Tag>
<Typography.Text type="tertiary">{scopeDescription}</Typography.Text>
</div>
{mode === 'query' ? (
<section className="vp-history-export-wizard" aria-label="客户导出向导">
<div className="vp-history-export-wizard-copy">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={deliveryStateColor}>{deliveryState}</Tag>
<Tag color={rawFieldRows.length > 0 ? 'green' : 'orange'}>{exportWizardEvidenceLabel}</Tag>
</Space>
<Typography.Title heading={5} style={{ margin: 0 }}>
</Typography.Title>
<Typography.Text type="secondary">
</Typography.Text>
<Button size="small" theme="solid" type="primary" icon={<IconCopy />} onClick={copyExportWizardPackage}></Button>
</div>
<div className="vp-history-export-wizard-steps">
{exportWizardSteps.map((item) => (
<button
key={item.step}
type="button"
className="vp-history-export-wizard-step"
onClick={item.onClick}
aria-label={`客户导出向导 ${item.step} ${item.title} ${item.value}`}
>
<Tag color={item.color}>{item.step}</Tag>
<strong>{item.title}</strong>
<span>{item.value}</span>
<small>{item.detail}</small>
</button>
))}
</div>
</section>
) : null}
{mode === 'query' ? (
<section className="vp-history-delivery-conclusion-strip" aria-label="客户数据交付封面">
<div className="vp-history-delivery-conclusion-copy">

View File

@@ -11657,6 +11657,78 @@ button.vp-realtime-command-item:focus-visible {
line-height: 18px;
}
.vp-history-export-wizard {
margin-top: 16px;
border: 1px solid rgba(22, 100, 255, 0.18);
border-radius: var(--vp-radius);
background: #f7fbff;
display: grid;
grid-template-columns: minmax(280px, 0.42fr) minmax(0, 1fr);
overflow: hidden;
box-shadow: var(--vp-shadow-sm);
}
.vp-history-export-wizard-copy {
min-width: 0;
padding: 18px;
border-right: 1px solid rgba(22, 100, 255, 0.12);
display: grid;
gap: 10px;
align-content: start;
}
.vp-history-export-wizard-copy .semi-button {
width: fit-content;
}
.vp-history-export-wizard-steps {
min-width: 0;
padding: 14px;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.vp-history-export-wizard-step {
min-height: 142px;
padding: 12px;
border: 1px solid rgba(22, 100, 255, 0.14);
border-radius: 8px;
background: #ffffff;
color: inherit;
cursor: pointer;
font: inherit;
text-align: left;
display: grid;
gap: 8px;
align-content: start;
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
}
.vp-history-export-wizard-step:hover,
.vp-history-export-wizard-step:focus-visible {
border-color: rgba(22, 100, 255, 0.42);
box-shadow: 0 10px 24px rgba(24, 39, 75, 0.08);
outline: none;
transform: translateY(-1px);
}
.vp-history-export-wizard-step strong {
color: var(--vp-text);
font-size: 17px;
line-height: 23px;
font-weight: 800;
word-break: break-word;
}
.vp-history-export-wizard-step span,
.vp-history-export-wizard-step small {
color: var(--vp-text-muted);
font-size: 12px;
line-height: 18px;
word-break: break-word;
}
.vp-history-delivery-conclusion-strip {
margin-top: 16px;
border: 1px solid rgba(22, 100, 255, 0.18);
@@ -15150,6 +15222,8 @@ button.vp-realtime-command-item:focus-visible {
.vp-trajectory-anomaly-grid,
.vp-trajectory-impact-board,
.vp-trajectory-impact-grid,
.vp-history-export-wizard,
.vp-history-export-wizard-steps,
.vp-history-delivery-conclusion-strip,
.vp-history-delivery-conclusion-metrics,
.vp-history-next-export-strip,

View File

@@ -8221,6 +8221,13 @@ test('shows parsed field history as a flattened evidence table', async () => {
render(<App />);
expect(await screen.findByRole('heading', { name: '历史查询导出' })).toBeInTheDocument();
expect(screen.getAllByText('客户导出向导').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('按客户交付顺序完成:先锁定车辆和时间窗,再选择轨迹、明细、字段或统计证据,最后复核质量并复制交付包。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户导出向导 1 锁定范围 VIN-FIELDS-001 / GB32960' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户导出向导 2 选择证据 字段证据' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户导出向导 3 质量复核 可交付' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户导出向导 4 导出交付 复制交付包' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /复制导出向导包/ })).toBeInTheDocument();
expect(screen.getByText('客户数据交付封面')).toBeInTheDocument();
expect(screen.getByText('先给客户一个可读封面:交付结论、车辆范围、时间窗、交付物、质量提示和下一步动作。')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '客户数据交付封面 交付结论 可交付 复制封面' })).toBeInTheDocument();
@@ -8364,6 +8371,15 @@ test('shows parsed field history as a flattened evidence table', async () => {
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付物:轨迹报告 / 位置历史 CSV / 明细证据 CSV / 字段裁剪 CSV / 统计查询链接 / 质量提示'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('当前证据:位置 0 条 / 有效坐标 0 点 / 明细 1 帧 / 字段 2 个'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('建议动作:优先导出字段证据,并补查轨迹位置。'));
fireEvent.click(screen.getByRole('button', { name: /复制导出向导包/ }));
await waitFor(() => {
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【客户导出向导包】'));
});
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('步骤1 锁定范围VIN-FIELDS-001 / GB32960 / 2026-07-03 至 2026-07-04'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('步骤2 选择证据:字段证据'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('步骤3 质量复核:可交付'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('步骤4 导出交付:复制交付包'));
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('字段裁剪http://localhost:3000/#/history-query?keyword=VIN-FIELDS-001&protocol=GB32960&tab=fields&dateFrom=2026-07-03&dateTo=2026-07-04&includeFields=true&fields=gb32960.vehicle.speed_kmh'));
fireEvent.click(screen.getByRole('button', { name: /复制交付清单/ }));
await waitFor(() => {
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('交付物:轨迹报告 / 位置历史 / 统计查询 / 明细证据 / 字段裁剪 / 质量提示'));