feat(platform): add vehicle service desk
This commit is contained in:
@@ -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} />,
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user