feat(platform): streamline vehicle service dashboard
This commit is contained in:
@@ -1416,102 +1416,6 @@ export function Dashboard({
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Card bordered title="客户常用服务" style={{ marginBottom: 16 }}>
|
||||
<div className="vp-customer-workflow-grid">
|
||||
{customerHeroActions.map((item) => (
|
||||
<button key={item.title} type="button" className="vp-customer-workflow-item" onClick={item.onClick} aria-label={`客户工作流 ${item.title}`}>
|
||||
<div>
|
||||
<Typography.Title heading={6} style={{ margin: 0 }}>{item.title}</Typography.Title>
|
||||
<Typography.Text type="secondary">{item.detail}</Typography.Text>
|
||||
</div>
|
||||
<strong>{item.value}</strong>
|
||||
<Tag color="blue">{item.action}</Tag>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Card bordered title="今日车辆服务任务板" style={{ marginBottom: 16 }}>
|
||||
<div className="vp-customer-task-grid">
|
||||
{customerTaskBoard.map((item) => (
|
||||
<div key={item.title} className="vp-customer-task-item">
|
||||
<div className="vp-customer-task-head">
|
||||
<Tag color={item.color}>{item.title}</Tag>
|
||||
<strong>{item.value}</strong>
|
||||
</div>
|
||||
<Typography.Text type="secondary">{item.detail}</Typography.Text>
|
||||
<Space wrap>
|
||||
<Button
|
||||
size="small"
|
||||
theme="solid"
|
||||
type="primary"
|
||||
disabled={item.disabled}
|
||||
aria-label={`今日任务 ${item.title} ${item.primaryAction}`}
|
||||
onClick={item.onPrimary}
|
||||
>
|
||||
{item.primaryAction}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
theme="light"
|
||||
type="primary"
|
||||
disabled={item.disabled && item.secondaryAction !== '复制处置卡'}
|
||||
aria-label={`今日任务 ${item.title} ${item.secondaryAction}`}
|
||||
onClick={item.onSecondary}
|
||||
>
|
||||
{item.secondaryAction}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<div className="vp-kpi-grid">
|
||||
{kpis.map((item) => (
|
||||
<Card key={item.label} bordered className="vp-kpi-card" bodyStyle={{ padding: 0 }}>
|
||||
<button className="vp-kpi-button" type="button" onClick={() => onOpenVehicles(item.filters)} aria-label={`${item.label} ${item.value}`}>
|
||||
<div className="vp-kpi-value">{item.value}</div>
|
||||
<div className="vp-kpi-label">{item.label}</div>
|
||||
</button>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<Card bordered title="车辆服务入口" style={{ marginBottom: 16 }}>
|
||||
<div className="vp-service-entry-board">
|
||||
<div className="vp-service-entry-summary">
|
||||
<Space wrap>
|
||||
<Tag color="blue">客户服务路径</Tag>
|
||||
<Tag color="green">{vehicleServiceOnlineText(serviceSummary, summary)}</Tag>
|
||||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警事件</Tag>
|
||||
</Space>
|
||||
<Typography.Title heading={5} style={{ margin: 0 }}>从车辆出发,完成监控、复盘、统计、导出和告警闭环</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
客户看到的是车辆服务结果;接入来源只在需要追溯时作为证据通道出现。
|
||||
</Typography.Text>
|
||||
<Space wrap>
|
||||
<Button size="small" theme="solid" type="primary" onClick={() => onOpenVehicles({ online: 'online' })}>查看在线车辆</Button>
|
||||
<Button size="small" theme="light" type="primary" onClick={copyCustomerServiceGuide}>复制客户服务说明</Button>
|
||||
<Button size="small" theme="light" type="primary" onClick={exportDashboardSnapshot}>导出驾驶舱 CSV</Button>
|
||||
<Button size="small" theme="light" type="tertiary" onClick={copyOperationsHandoff}>复制运营交接摘要</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div className="vp-service-entry-grid">
|
||||
{customerServicePathItems.map((item) => (
|
||||
<button
|
||||
key={item.title}
|
||||
type="button"
|
||||
className="vp-service-entry-item"
|
||||
onClick={item.onClick}
|
||||
aria-label={`车辆服务入口 ${item.title} ${item.action}`}
|
||||
>
|
||||
<Tag color={item.color}>{item.title}</Tag>
|
||||
<strong>{item.value}</strong>
|
||||
<span>{item.detail}</span>
|
||||
<em>{item.action}</em>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
bordered
|
||||
title={<Space><span>自定义时间监控</span><Tag color="blue">{timeMonitorSummary}</Tag></Space>}
|
||||
@@ -1542,7 +1446,7 @@ export function Dashboard({
|
||||
<div className="vp-time-monitor-actions">
|
||||
<Button theme="solid" type="primary" onClick={openTimeMonitorHistory}>查看轨迹</Button>
|
||||
<Button theme="light" type="primary" onClick={openTimeMonitorMileage}>统计里程</Button>
|
||||
<Button theme="light" type="primary" onClick={openTimeMonitorRaw}>导出原始记录</Button>
|
||||
<Button theme="light" type="primary" onClick={openTimeMonitorRaw}>历史查询导出</Button>
|
||||
<Button theme="light" type="warning" onClick={openTimeMonitorAlerts}>查看告警</Button>
|
||||
</div>
|
||||
<Typography.Text type="secondary">
|
||||
@@ -1638,6 +1542,110 @@ export function Dashboard({
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<details className="vp-dashboard-secondary-details">
|
||||
<summary>
|
||||
<span>更多车辆服务入口</span>
|
||||
<small>常用服务、今日任务、KPI 和车辆入口默认收起,主页面优先保留地图、指挥台和时间窗监控。</small>
|
||||
</summary>
|
||||
<div className="vp-dashboard-secondary-body">
|
||||
<Card bordered title="客户常用服务" style={{ marginBottom: 16 }}>
|
||||
<div className="vp-customer-workflow-grid">
|
||||
{customerHeroActions.map((item) => (
|
||||
<button key={item.title} type="button" className="vp-customer-workflow-item" onClick={item.onClick} aria-label={`客户工作流 ${item.title}`}>
|
||||
<div>
|
||||
<Typography.Title heading={6} style={{ margin: 0 }}>{item.title}</Typography.Title>
|
||||
<Typography.Text type="secondary">{item.detail}</Typography.Text>
|
||||
</div>
|
||||
<strong>{item.value}</strong>
|
||||
<Tag color="blue">{item.action}</Tag>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Card bordered title="今日车辆服务任务板" style={{ marginBottom: 16 }}>
|
||||
<div className="vp-customer-task-grid">
|
||||
{customerTaskBoard.map((item) => (
|
||||
<div key={item.title} className="vp-customer-task-item">
|
||||
<div className="vp-customer-task-head">
|
||||
<Tag color={item.color}>{item.title}</Tag>
|
||||
<strong>{item.value}</strong>
|
||||
</div>
|
||||
<Typography.Text type="secondary">{item.detail}</Typography.Text>
|
||||
<Space wrap>
|
||||
<Button
|
||||
size="small"
|
||||
theme="solid"
|
||||
type="primary"
|
||||
disabled={item.disabled}
|
||||
aria-label={`今日任务 ${item.title} ${item.primaryAction}`}
|
||||
onClick={item.onPrimary}
|
||||
>
|
||||
{item.primaryAction}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
theme="light"
|
||||
type="primary"
|
||||
disabled={item.disabled && item.secondaryAction !== '复制处置卡'}
|
||||
aria-label={`今日任务 ${item.title} ${item.secondaryAction}`}
|
||||
onClick={item.onSecondary}
|
||||
>
|
||||
{item.secondaryAction}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<div className="vp-kpi-grid">
|
||||
{kpis.map((item) => (
|
||||
<Card key={item.label} bordered className="vp-kpi-card" bodyStyle={{ padding: 0 }}>
|
||||
<button className="vp-kpi-button" type="button" onClick={() => onOpenVehicles(item.filters)} aria-label={`${item.label} ${item.value}`}>
|
||||
<div className="vp-kpi-value">{item.value}</div>
|
||||
<div className="vp-kpi-label">{item.label}</div>
|
||||
</button>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<Card bordered title="车辆服务入口" style={{ marginBottom: 16 }}>
|
||||
<div className="vp-service-entry-board">
|
||||
<div className="vp-service-entry-summary">
|
||||
<Space wrap>
|
||||
<Tag color="blue">客户服务路径</Tag>
|
||||
<Tag color="green">{vehicleServiceOnlineText(serviceSummary, summary)}</Tag>
|
||||
<Tag color={(summary?.issueVehicles ?? 0) > 0 ? 'orange' : 'green'}>{formatCount(summary?.issueVehicles)} 告警事件</Tag>
|
||||
</Space>
|
||||
<Typography.Title heading={5} style={{ margin: 0 }}>从车辆出发,完成监控、复盘、统计、导出和告警闭环</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
客户看到的是车辆服务结果;接入来源只在需要追溯时作为数据通道出现。
|
||||
</Typography.Text>
|
||||
<Space wrap>
|
||||
<Button size="small" theme="solid" type="primary" onClick={() => onOpenVehicles({ online: 'online' })}>查看在线车辆</Button>
|
||||
<Button size="small" theme="light" type="primary" onClick={copyCustomerServiceGuide}>复制客户服务说明</Button>
|
||||
<Button size="small" theme="light" type="primary" onClick={exportDashboardSnapshot}>导出驾驶舱 CSV</Button>
|
||||
<Button size="small" theme="light" type="tertiary" onClick={copyOperationsHandoff}>复制运营交接摘要</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div className="vp-service-entry-grid">
|
||||
{customerServicePathItems.map((item) => (
|
||||
<button
|
||||
key={item.title}
|
||||
type="button"
|
||||
className="vp-service-entry-item"
|
||||
onClick={item.onClick}
|
||||
aria-label={`车辆服务入口 ${item.title} ${item.action}`}
|
||||
>
|
||||
<Tag color={item.color}>{item.title}</Tag>
|
||||
<strong>{item.value}</strong>
|
||||
<span>{item.detail}</span>
|
||||
<em>{item.action}</em>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</details>
|
||||
<details className="vp-dashboard-evidence-details">
|
||||
<summary>
|
||||
<span>运维和证据层</span>
|
||||
|
||||
@@ -303,10 +303,12 @@ body {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.vp-dashboard-secondary-details,
|
||||
.vp-dashboard-evidence-details {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.vp-dashboard-secondary-details > summary,
|
||||
.vp-dashboard-evidence-details > summary {
|
||||
min-height: 56px;
|
||||
padding: 12px 14px;
|
||||
@@ -318,10 +320,17 @@ body {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.vp-dashboard-secondary-details > summary {
|
||||
border-color: rgba(22, 100, 255, 0.18);
|
||||
background: #f7fbff;
|
||||
}
|
||||
|
||||
.vp-dashboard-secondary-details > summary::marker,
|
||||
.vp-dashboard-evidence-details > summary::marker {
|
||||
color: var(--vp-primary);
|
||||
}
|
||||
|
||||
.vp-dashboard-secondary-details > summary span,
|
||||
.vp-dashboard-evidence-details > summary span {
|
||||
color: var(--vp-text);
|
||||
font-size: 15px;
|
||||
@@ -329,12 +338,14 @@ body {
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.vp-dashboard-secondary-details > summary small,
|
||||
.vp-dashboard-evidence-details > summary small {
|
||||
color: var(--vp-text-muted);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.vp-dashboard-secondary-body,
|
||||
.vp-dashboard-evidence-body {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
@@ -822,10 +822,11 @@ test('dashboard exposes vehicle data center capability matrix', async () => {
|
||||
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('客户看到的是车辆服务结果;接入来源只在需要追溯时作为证据通道出现。')).toBeInTheDocument();
|
||||
expect(screen.getByText('客户看到的是车辆服务结果;接入来源只在需要追溯时作为数据通道出现。')).toBeInTheDocument();
|
||||
expect(screen.getByText('找车')).toBeInTheDocument();
|
||||
expect(screen.getByText('看位置')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('复盘时间').length).toBeGreaterThanOrEqual(1);
|
||||
@@ -853,7 +854,7 @@ test('dashboard exposes vehicle data center capability matrix', async () => {
|
||||
expect(screen.getByText('这个范围会同步带入轨迹、里程、历史证据和告警复盘,适合客户问询、BI 核对和异常解释。')).toBeInTheDocument();
|
||||
expect(screen.getByText('查看轨迹')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('统计里程').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByText('导出原始记录')).toBeInTheDocument();
|
||||
expect(screen.getByText('历史查询导出')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('轨迹回放').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByText('里程核对')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('历史证据').length).toBeGreaterThanOrEqual(1);
|
||||
@@ -967,7 +968,7 @@ test('dashboard exposes vehicle data center capability matrix', async () => {
|
||||
cleanup();
|
||||
|
||||
await renderDashboard();
|
||||
fireEvent.click(screen.getByRole('button', { name: '导出原始记录' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '历史查询导出' }));
|
||||
expect(window.location.hash.startsWith('#/history-query')).toBe(true);
|
||||
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('tab')).toBe('raw');
|
||||
expect(new URLSearchParams(window.location.hash.split('?')[1] ?? '').get('includeFields')).toBe('true');
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# 车辆数据中台产品化参考
|
||||
|
||||
## 参考产品共性
|
||||
|
||||
- Samsara、Geotab、Verizon Connect、Motive 这类车队平台都把实时车辆位置放在主入口,地图是调度、监控和异常处置的第一视角。
|
||||
- 主导航通常围绕车辆业务组织:实时地图、车辆/资产、轨迹回放、告警、安全或维护、报表统计,而不是围绕接入协议组织。
|
||||
- 历史数据查询通常服务于 trip history、route replay、reports 和 alerts 的复核,原始接入数据不应成为客户主路径。
|
||||
- 告警和通知需要从“发现异常”延伸到“责任人、升级、闭环”,而不是只展示技术链路状态。
|
||||
- 运维链路、消息队列、存储状态适合放在内部视图,作为客户问题排查的支撑层。
|
||||
|
||||
## 本项目采用的产品原则
|
||||
|
||||
- 三个接入来源只是数据通道,最终服务对象是车辆。
|
||||
- 第一屏必须回答客户最关心的问题:有多少车在线、车在哪里、哪些车异常、如何按时间复盘。
|
||||
- 车辆服务主路径固定为:车辆地图 -> 实时监控 -> 轨迹回放 -> 里程统计 -> 历史查询导出 -> 告警通知。
|
||||
- 自定义时间窗要贯穿轨迹、里程、历史查询和告警,避免用户在多个页面重复录入条件。
|
||||
- 协议、来源覆盖、Kafka、Redis、TDEngine、MySQL 等信息只在内部运维或复核层展示。
|
||||
|
||||
## 当前迭代落点
|
||||
|
||||
- 首页保留客户主路径:实时地图总览、车辆服务指挥台、自定义时间监控。
|
||||
- 重复入口、KPI 和辅助服务收进“更多车辆服务入口”。
|
||||
- 来源覆盖、地图配置、告警预览和链路状态继续放在“运维和依据层”折叠区。
|
||||
Reference in New Issue
Block a user