feat(platform): add vehicle service desk

This commit is contained in:
lingniu
2026-07-05 01:36:46 +08:00
parent f5d38253fe
commit f45cf90b70
4 changed files with 171 additions and 2 deletions

View File

@@ -454,7 +454,7 @@ export default function App() {
const pages: Record<PageKey, JSX.Element> = {
dashboard: <Dashboard onOpenVehicle={openVehicle} onOpenQuality={openQuality} onOpenMap={openMap} onOpenRealtime={openRealtime} onOpenVehicles={openVehicles} onOpenHistory={openHistoryWithFilters} onOpenMileage={openMileageWithFilters} />,
vehicles: <Vehicles onOpenVehicle={openVehicle} onOpenQuality={openQuality} onOpenRealtime={openRealtime} onOpenHistory={openHistoryWithFilters} onFiltersChange={updateVehicleFilters} initialFilters={vehicleFilters} />,
vehicles: <Vehicles onOpenVehicle={openVehicle} onOpenQuality={openQuality} onOpenMap={openMap} onOpenRealtime={openRealtime} onOpenHistory={openHistoryWithFilters} onOpenMileage={openMileageWithFilters} onFiltersChange={updateVehicleFilters} initialFilters={vehicleFilters} />,
map: <Realtime mode="map" title="车辆实时地图" description="以车辆为中心查看位置分布、在线状态、关注车辆和轨迹入口" onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateMapFilters} initialFilters={realtimeFilters} />,
realtime: <Realtime onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateRealtimeFilters} initialFilters={realtimeFilters} />,
detail: <VehicleDetail vin={activeVin} protocol={activeProtocol} platformRelease={platformRelease} onOpenRealtime={openRealtimeForVehicle} onOpenHistory={openHistoryForVehicle} onOpenRaw={openRawForVehicle} onOpenMileage={openMileageForVehicle} onOpenVehicles={openVehicles} onOpenQuality={openQuality} onOpenHistoryEvidence={openHistoryWithFilters} onOpenRawEvidence={openRawWithFilters} onQueryChange={updateVehicleDetailQuery} />,

View File

@@ -383,15 +383,19 @@ async function copyVehicleShareURL() {
export function Vehicles({
onOpenVehicle,
onOpenQuality,
onOpenMap,
onOpenRealtime,
onOpenHistory,
onOpenMileage,
onFiltersChange,
initialFilters = {}
}: {
onOpenVehicle: (vin: string, protocol?: string) => void;
onOpenQuality?: (filters: Record<string, string>) => void;
onOpenMap?: (filters: Record<string, string>) => void;
onOpenRealtime?: (filters: Record<string, string>) => void;
onOpenHistory?: (filters: Record<string, string>) => void;
onOpenMileage?: (filters: Record<string, string>) => void;
onFiltersChange?: (filters: Record<string, string>) => void;
initialFilters?: Record<string, string>;
}) {
@@ -490,6 +494,12 @@ export function Vehicles({
const filterSummary = vehicleFilterSummary(filters);
const pageVerdictStats = useMemo(() => currentPageVerdictStats(rows), [rows]);
const pageActionStats = useMemo(() => currentPageActionStats(rows), [rows]);
const serviceScopeFilters = useMemo(() => {
const nextFilters: Record<string, string> = {};
if (filters.keyword?.trim()) nextFilters.keyword = filters.keyword.trim();
if (filters.protocol?.trim()) nextFilters.protocol = filters.protocol.trim();
return nextFilters;
}, [filters.keyword, filters.protocol]);
const load = (values: Record<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => {
setLoading(true);
@@ -547,6 +557,48 @@ export function Vehicles({
}
copyText(vehicleIdentityMaintenanceText({ filters, rows, total: pagination.total }), '车辆身份维护清单');
};
const serviceDeskActions = useMemo(() => [
{
label: '实时监控',
value: `${(summary?.onlineVehicles ?? 0).toLocaleString()} 在线`,
detail: '查看车辆在线、速度、最后上报和来源状态。',
color: 'green' as const,
disabled: !onOpenRealtime,
onClick: () => onOpenRealtime?.({ ...serviceScopeFilters, online: 'online' })
},
{
label: '车辆地图',
value: '地图态势',
detail: '把当前筛选车辆带入实时地图,优先看位置分布。',
color: 'blue' as const,
disabled: !onOpenMap,
onClick: () => onOpenMap?.({ ...serviceScopeFilters, online: 'online' })
},
{
label: '轨迹回放',
value: filters.keyword?.trim() ? '单车轨迹' : '选择车辆',
detail: filters.keyword?.trim() ? '按当前车辆进入历史轨迹复盘。' : '先选择车辆后进入精确轨迹回放。',
color: filters.keyword?.trim() ? 'blue' as const : 'grey' as const,
disabled: !onOpenHistory || !filters.keyword?.trim(),
onClick: () => onOpenHistory?.({ ...serviceScopeFilters, tab: 'location' })
},
{
label: '里程统计',
value: filters.keyword?.trim() ? '区间核对' : '车辆口径',
detail: filters.keyword?.trim() ? '按当前车辆核对区间里程和日里程。' : '输入车辆后查看里程闭合统计。',
color: filters.keyword?.trim() ? 'green' as const : 'grey' as const,
disabled: !onOpenMileage || !filters.keyword?.trim(),
onClick: () => onOpenMileage?.(serviceScopeFilters)
},
{
label: '数据导出',
value: `${rows.length.toLocaleString()} 当前页`,
detail: '导出当前页车辆清单,后续可继续导出轨迹和原始记录。',
color: rows.length > 0 ? 'blue' as const : 'grey' as const,
disabled: rows.length === 0,
onClick: exportVehicles
}
], [exportVehicles, filters.keyword, onOpenHistory, onOpenMap, onOpenMileage, onOpenRealtime, rows.length, serviceScopeFilters, summary?.onlineVehicles]);
useEffect(() => {
setFilters(initialFilters);
@@ -654,7 +706,42 @@ export function Vehicles({
return (
<div className="vp-page">
<PageHeader title="车辆服务" description="以 VIN 为主对象维护车辆身份、车辆覆盖、在线状态和绑定状态" />
<PageHeader title="车辆服务" description="围绕车辆完成实时监控、地图查看、轨迹回放、里程统计和数据导出,协议来源只作为证据过滤条件" />
<Card bordered title="车辆服务台">
<div className="vp-vehicle-service-desk">
<div className="vp-vehicle-service-summary">
<Space wrap>
<Tag color="blue"></Tag>
<Tag color={filters.keyword?.trim() ? 'green' : 'grey'}>{filters.keyword?.trim() ? '单车服务' : '车辆池'}</Tag>
</Space>
<strong>{filters.keyword?.trim() || '全部车辆'}</strong>
<span>
{(summary?.totalVehicles ?? pagination.total).toLocaleString()} 线 {(summary?.onlineVehicles ?? 0).toLocaleString()}
</span>
<Space wrap>
<Button size="small" theme="solid" type="primary" disabled={!onOpenRealtime} onClick={() => onOpenRealtime?.({ ...serviceScopeFilters, online: 'online' })}>线</Button>
<Button size="small" disabled={!onOpenMap} onClick={() => onOpenMap?.({ ...serviceScopeFilters, online: 'online' })}></Button>
<Button size="small" onClick={copyDispatchList}></Button>
</Space>
</div>
<div className="vp-vehicle-service-grid">
{serviceDeskActions.map((item) => (
<button
key={item.label}
className="vp-vehicle-service-action"
type="button"
disabled={item.disabled}
onClick={item.onClick}
>
<Tag color={item.color}>{item.label}</Tag>
<strong>{item.value}</strong>
<span>{item.detail}</span>
</button>
))}
</div>
</div>
</Card>
<Card bordered>
<Form key={JSON.stringify(filters)} initValues={filters} layout="horizontal" onSubmit={(values) => {
const nextFilters = values as Record<string, string>;

View File

@@ -654,6 +654,81 @@ button.vp-realtime-command-item:focus-visible {
line-height: 18px;
}
.vp-vehicle-service-desk {
display: grid;
grid-template-columns: minmax(280px, 0.7fr) minmax(0, 1.3fr);
gap: 16px;
}
.vp-vehicle-service-summary {
min-height: 168px;
padding: 12px;
border: 1px solid rgba(10, 168, 107, 0.28);
border-radius: var(--vp-radius);
background: #f4fbf8;
display: grid;
gap: 10px;
align-content: start;
}
.vp-vehicle-service-summary strong {
color: var(--vp-text);
font-size: 20px;
line-height: 26px;
word-break: break-word;
}
.vp-vehicle-service-summary span {
color: var(--vp-text-muted);
font-size: 13px;
line-height: 20px;
}
.vp-vehicle-service-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
}
.vp-vehicle-service-action {
min-height: 168px;
padding: 12px;
border: 1px solid var(--vp-border);
border-radius: var(--vp-radius);
background: #fbfcff;
display: grid;
gap: 10px;
align-content: start;
text-align: left;
font: inherit;
cursor: pointer;
}
.vp-vehicle-service-action:disabled {
cursor: not-allowed;
opacity: 0.62;
}
.vp-vehicle-service-action:not(:disabled):hover,
.vp-vehicle-service-action:not(:disabled):focus-visible {
border-color: rgba(22, 100, 255, 0.42);
background: #f5f9ff;
outline: none;
}
.vp-vehicle-service-action strong {
color: var(--vp-text);
font-size: 20px;
line-height: 26px;
word-break: break-word;
}
.vp-vehicle-service-action span {
color: var(--vp-text-muted);
font-size: 13px;
line-height: 20px;
}
.vp-result-summary-item {
min-height: 72px;
padding: 12px;
@@ -2957,6 +3032,8 @@ button.vp-realtime-command-item:focus-visible {
.vp-time-monitor-grid,
.vp-current-service-board,
.vp-current-service-grid,
.vp-vehicle-service-desk,
.vp-vehicle-service-grid,
.vp-realtime-command-board,
.vp-realtime-command-grid,
.vp-realtime-impact-board,

View File

@@ -1937,6 +1937,11 @@ test('shows vehicle service result summary on vehicle list filters', async () =>
render(<App />);
expect(await screen.findByText('当前车辆结果')).toBeInTheDocument();
expect(screen.getByText('车辆服务台')).toBeInTheDocument();
expect(screen.getByText('客户视角')).toBeInTheDocument();
expect(screen.getByText('车辆地图')).toBeInTheDocument();
expect(screen.getAllByText('里程统计').length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText('数据导出').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('当前页服务判读')).toBeInTheDocument();
expect(screen.getByText('建议下一步')).toBeInTheDocument();
expect(screen.getAllByText('降级可服务').length).toBeGreaterThanOrEqual(1);