feat(platform): add customer vehicle lookup path
This commit is contained in:
@@ -273,6 +273,43 @@ function vehicleIdentityMaintenanceText({
|
|||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function vehicleCustomerLookupText({
|
||||||
|
filters,
|
||||||
|
rows,
|
||||||
|
total,
|
||||||
|
summary
|
||||||
|
}: {
|
||||||
|
filters: Record<string, string>;
|
||||||
|
rows: VehicleCoverageRow[];
|
||||||
|
total: number;
|
||||||
|
summary: VehicleCoverageSummary | null;
|
||||||
|
}) {
|
||||||
|
const serviceMode = filters.keyword?.trim() ? '单车服务' : '车辆池';
|
||||||
|
const scopeHash = buildAppHash({ page: 'vehicles', keyword: filters.keyword, protocol: filters.protocol, filters });
|
||||||
|
const singleVehicleFilters = filters.keyword?.trim()
|
||||||
|
? { keyword: filters.keyword.trim(), ...(filters.protocol ? { protocol: filters.protocol } : {}) }
|
||||||
|
: {};
|
||||||
|
const mapFilters = { ...filters, online: filters.online || 'online' };
|
||||||
|
const lines = [
|
||||||
|
'【客户查车路径】',
|
||||||
|
`服务模式:${serviceMode}`,
|
||||||
|
`当前筛选:${vehicleFilterSummary(filters).join(';') || '全部车辆'}`,
|
||||||
|
`当前页:${rows.length.toLocaleString()} 辆 / 当前筛选 ${total.toLocaleString()} 辆`,
|
||||||
|
`在线车辆:${(summary?.onlineVehicles ?? 0).toLocaleString()};待绑定:${(summary?.unboundVehicles ?? 0).toLocaleString()};暂无来源:${(summary?.noDataVehicles ?? 0).toLocaleString()};档案不完整:${(summary?.archiveIncompleteVehicles ?? 0).toLocaleString()}`,
|
||||||
|
'',
|
||||||
|
serviceMode === '单车服务'
|
||||||
|
? '推荐路径:先看车辆档案和实时监控,再按需要进入轨迹回放、里程统计、历史数据导出。'
|
||||||
|
: '推荐路径:先筛在线/离线/无来源/待绑定车辆,再进入地图态势或导出处置清单。',
|
||||||
|
`车辆服务:${appURL(scopeHash)}`,
|
||||||
|
`实时监控:${appURL(buildAppHash({ page: 'realtime', ...singleVehicleFilters }))}`,
|
||||||
|
`车辆地图:${appURL(buildAppHash({ page: 'map', keyword: filters.keyword, protocol: filters.protocol, filters: mapFilters }))}`,
|
||||||
|
`轨迹回放:${appURL(buildAppHash({ page: 'history', ...singleVehicleFilters, filters: { tab: 'location' } }))}`,
|
||||||
|
`里程统计:${appURL(buildAppHash({ page: 'mileage', ...singleVehicleFilters }))}`,
|
||||||
|
`历史数据:${appURL(buildAppHash({ page: 'history', ...singleVehicleFilters, filters: { tab: 'raw', includeFields: 'true' } }))}`
|
||||||
|
];
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
type VehicleActionRecommendation = {
|
type VehicleActionRecommendation = {
|
||||||
label: string;
|
label: string;
|
||||||
color: 'green' | 'orange' | 'red';
|
color: 'green' | 'orange' | 'red';
|
||||||
@@ -557,6 +594,31 @@ export function Vehicles({
|
|||||||
}
|
}
|
||||||
copyText(vehicleIdentityMaintenanceText({ filters, rows, total: pagination.total }), '车辆身份维护清单');
|
copyText(vehicleIdentityMaintenanceText({ filters, rows, total: pagination.total }), '车辆身份维护清单');
|
||||||
};
|
};
|
||||||
|
const copyCustomerLookupPath = () => {
|
||||||
|
copyText(vehicleCustomerLookupText({ filters, rows, total: pagination.total, summary }), '客户查车路径');
|
||||||
|
};
|
||||||
|
const customerLookupSteps = [
|
||||||
|
{
|
||||||
|
label: '车辆身份',
|
||||||
|
value: filters.keyword?.trim() || 'VIN / 车牌 / 手机号',
|
||||||
|
detail: '确认车辆归属、车牌、手机号和 OEM,客户问题先落到一辆车。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '在线状态',
|
||||||
|
value: `${(summary?.onlineVehicles ?? 0).toLocaleString()} 在线`,
|
||||||
|
detail: '判断车辆是否仍在上报,离线车辆进入断链和来源证据排查。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '轨迹与位置',
|
||||||
|
value: filters.keyword?.trim() ? '可回放' : '先选车辆',
|
||||||
|
detail: '单车进入轨迹回放,车辆池进入地图态势查看位置分布。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '数据交付',
|
||||||
|
value: `${rows.length.toLocaleString()} 当前页`,
|
||||||
|
detail: '导出车辆清单,再进入历史页交付轨迹、RAW 和字段证据。'
|
||||||
|
}
|
||||||
|
];
|
||||||
const serviceDeskActions = useMemo(() => [
|
const serviceDeskActions = useMemo(() => [
|
||||||
{
|
{
|
||||||
label: '实时监控',
|
label: '实时监控',
|
||||||
@@ -789,6 +851,34 @@ export function Vehicles({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
<Card bordered title="客户查车路径" style={{ marginTop: 16 }}>
|
||||||
|
<div className="vp-vehicle-lookup-strip">
|
||||||
|
<div className="vp-vehicle-lookup-head">
|
||||||
|
<div>
|
||||||
|
<Space wrap>
|
||||||
|
<Tag color={filters.keyword?.trim() ? 'green' : 'blue'}>{filters.keyword?.trim() ? '单车服务' : '车辆池服务'}</Tag>
|
||||||
|
<Tag color="grey">{vehicleFilterSummary(filters).join(' / ') || '全部车辆'}</Tag>
|
||||||
|
</Space>
|
||||||
|
<strong>{filters.keyword?.trim() ? '围绕一辆车完成监控、回放、统计和导出' : '从车辆池筛出客户要看的车,再进入单车服务'}</strong>
|
||||||
|
<span>客户侧只关心车辆服务结果,32960、808、MQTT 作为证据来源参与筛选和可信度判断。</span>
|
||||||
|
</div>
|
||||||
|
<Space wrap>
|
||||||
|
<Button size="small" theme="solid" type="primary" disabled={!onOpenMap} onClick={() => onOpenMap?.({ ...serviceScopeFilters, online: 'online' })}>查看地图</Button>
|
||||||
|
<Button size="small" disabled={!onOpenHistory || !filters.keyword?.trim()} onClick={() => onOpenHistory?.({ ...serviceScopeFilters, tab: 'location' })}>轨迹回放</Button>
|
||||||
|
<Button size="small" icon={<IconCopy />} onClick={copyCustomerLookupPath}>复制客户查车路径</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<div className="vp-vehicle-lookup-grid">
|
||||||
|
{customerLookupSteps.map((step) => (
|
||||||
|
<div key={step.label} className="vp-vehicle-lookup-item">
|
||||||
|
<Tag color="blue">{step.label}</Tag>
|
||||||
|
<strong>{step.value}</strong>
|
||||||
|
<span>{step.detail}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
<Card bordered title="车辆服务决策板" style={{ marginTop: 16 }}>
|
<Card bordered title="车辆服务决策板" style={{ marginTop: 16 }}>
|
||||||
<div className="vp-vehicle-decision-grid">
|
<div className="vp-vehicle-decision-grid">
|
||||||
{vehicleDecisionItems.map((item) => (
|
{vehicleDecisionItems.map((item) => (
|
||||||
|
|||||||
@@ -1003,6 +1003,72 @@ button.vp-realtime-command-item:focus-visible {
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vp-vehicle-lookup-strip {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-vehicle-lookup-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid rgba(22, 100, 255, 0.16);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f6f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-vehicle-lookup-head > div {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-vehicle-lookup-head strong {
|
||||||
|
color: var(--vp-text);
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-vehicle-lookup-head span {
|
||||||
|
color: var(--vp-text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-vehicle-lookup-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-vehicle-lookup-item {
|
||||||
|
min-height: 128px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--vp-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fbfcff;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-vehicle-lookup-item strong {
|
||||||
|
color: var(--vp-text);
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-vehicle-lookup-item span {
|
||||||
|
color: var(--vp-text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.vp-vehicle-decision-grid {
|
.vp-vehicle-decision-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
@@ -4035,6 +4101,8 @@ button.vp-realtime-command-item:focus-visible {
|
|||||||
.vp-current-service-grid,
|
.vp-current-service-grid,
|
||||||
.vp-vehicle-service-desk,
|
.vp-vehicle-service-desk,
|
||||||
.vp-vehicle-service-grid,
|
.vp-vehicle-service-grid,
|
||||||
|
.vp-vehicle-lookup-head,
|
||||||
|
.vp-vehicle-lookup-grid,
|
||||||
.vp-vehicle-decision-grid,
|
.vp-vehicle-decision-grid,
|
||||||
.vp-time-review-package,
|
.vp-time-review-package,
|
||||||
.vp-time-review-grid,
|
.vp-time-review-grid,
|
||||||
|
|||||||
@@ -2007,6 +2007,15 @@ test('shows vehicle service result summary on vehicle list filters', async () =>
|
|||||||
expect(await screen.findByText('当前车辆结果')).toBeInTheDocument();
|
expect(await screen.findByText('当前车辆结果')).toBeInTheDocument();
|
||||||
expect(screen.getByText('车辆服务台')).toBeInTheDocument();
|
expect(screen.getByText('车辆服务台')).toBeInTheDocument();
|
||||||
expect(screen.getByText('客户视角')).toBeInTheDocument();
|
expect(screen.getByText('客户视角')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('客户查车路径')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('车辆池服务')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('从车辆池筛出客户要看的车,再进入单车服务')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('客户侧只关心车辆服务结果,32960、808、MQTT 作为证据来源参与筛选和可信度判断。')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('车辆身份')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('VIN / 车牌 / 手机号')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('轨迹与位置')).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByText('数据交付').length).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(screen.getByRole('button', { name: /复制客户查车路径/ })).toBeInTheDocument();
|
||||||
expect(screen.getByText('车辆服务决策板')).toBeInTheDocument();
|
expect(screen.getByText('车辆服务决策板')).toBeInTheDocument();
|
||||||
expect(screen.getAllByText('异常车辆').length).toBeGreaterThan(0);
|
expect(screen.getAllByText('异常车辆').length).toBeGreaterThan(0);
|
||||||
expect(screen.getByText('4 无来源')).toBeInTheDocument();
|
expect(screen.getByText('4 无来源')).toBeInTheDocument();
|
||||||
|
|||||||
Reference in New Issue
Block a user