feat(platform): add customer history export wizard
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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('交付物:轨迹报告 / 位置历史 / 统计查询 / 明细证据 / 字段裁剪 / 质量提示'));
|
||||
|
||||
Reference in New Issue
Block a user