feat(platform): add vehicle service delivery strip

This commit is contained in:
lingniu
2026-07-05 22:19:20 +08:00
parent fc86119857
commit aa286b810e
3 changed files with 133 additions and 0 deletions

View File

@@ -188,6 +188,8 @@ export function AppShell({
const dateFrom = customerTimeWindow?.dateFrom ?? '';
const dateTo = customerTimeWindow?.dateTo ?? '';
const savedViews = customerViews ?? [];
const serviceObjectText = currentVehicleLabel ? `车辆:${currentVehicleLabel}` : '未选择车辆';
const serviceTimeText = dateFrom || dateTo ? `${dateFrom || '未指定'}${dateTo || '未指定'}` : '实时范围';
const applyTimeWindowPreset = (days: number) => {
onCustomerTimeWindowChange?.({
@@ -214,6 +216,7 @@ export function AppShell({
{ label: '导出', ariaLabel: '顶部数据导出', page: 'history-query' as const },
{ label: '告警', ariaLabel: '顶部告警事件', page: 'alert-events' as const }
];
const deliveryActions = quickActions.filter((item) => item.page !== 'realtime');
return (
<div className="vp-app">
@@ -299,6 +302,35 @@ export function AppShell({
<Button theme="solid" type="primary" loading={searching} onClick={search}></Button>
</Space>
<div className="vp-topbar-context">
<div className="vp-service-delivery-strip" aria-label="车辆服务交付条">
<div className="vp-service-delivery-strip-head">
<strong></strong>
<span></span>
</div>
<div className="vp-service-delivery-strip-fact">
<span></span>
<strong>{serviceObjectText}</strong>
</div>
<div className="vp-service-delivery-strip-fact">
<span></span>
<strong>{serviceTimeText}</strong>
</div>
<div className="vp-service-delivery-strip-actions">
<span></span>
<div>
{deliveryActions.map((item) => (
<button
key={item.page}
type="button"
aria-label={`车辆服务交付条 ${item.label}`}
onClick={() => (onCustomerTask ?? onChange)(item.page)}
>
{item.label}
</button>
))}
</div>
</div>
</div>
<div className="vp-topbar-time-window" aria-label="客户时间窗">
<div className="vp-topbar-time-window-copy">
<strong></strong>

View File

@@ -372,6 +372,81 @@ body {
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16);
}
.vp-service-delivery-strip {
min-width: min(100%, 560px);
max-width: 100%;
min-height: 44px;
padding: 6px;
border: 1px solid rgba(22, 100, 255, 0.18);
border-radius: 8px;
background: #ffffff;
display: grid;
grid-template-columns: minmax(138px, 0.9fr) minmax(112px, 0.65fr) minmax(150px, 0.85fr) minmax(150px, 1fr);
gap: 6px;
align-items: stretch;
}
.vp-service-delivery-strip-head,
.vp-service-delivery-strip-fact,
.vp-service-delivery-strip-actions {
min-width: 0;
padding: 6px 8px;
border-radius: 6px;
background: #f8fafc;
display: grid;
align-content: center;
gap: 2px;
}
.vp-service-delivery-strip-head strong,
.vp-service-delivery-strip-fact strong {
color: var(--vp-text);
font-size: 13px;
line-height: 18px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.vp-service-delivery-strip-head span,
.vp-service-delivery-strip-fact span,
.vp-service-delivery-strip-actions > span {
color: var(--vp-text-muted);
font-size: 11px;
line-height: 15px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.vp-service-delivery-strip-actions > div {
min-width: 0;
display: flex;
align-items: center;
gap: 3px;
overflow: hidden;
}
.vp-service-delivery-strip-actions button {
height: 22px;
min-width: 34px;
padding: 0 6px;
border: 0;
border-radius: 5px;
background: #ffffff;
color: var(--vp-primary);
cursor: pointer;
font-size: 12px;
font-weight: 700;
line-height: 20px;
}
.vp-service-delivery-strip-actions button:hover,
.vp-service-delivery-strip-actions button:focus-visible {
background: rgba(22, 100, 255, 0.1);
outline: none;
}
.vp-topbar-quick-actions {
height: 32px;
display: inline-flex;
@@ -10942,6 +11017,7 @@ button.vp-realtime-command-item:focus-visible {
.vp-customer-service-package,
.vp-customer-service-grid,
.vp-vehicle-task-grid,
.vp-service-delivery-strip,
.vp-dispatch-operations-home,
.vp-dispatch-operations-highlights,
.vp-customer-delivery-workbench,

View File

@@ -52,6 +52,31 @@ test('renders vehicle platform shell', () => {
expect(screen.getAllByText('运营总览').length).toBeGreaterThanOrEqual(1);
});
test('topbar frames customer work as a vehicle service delivery strip', () => {
vi.spyOn(globalThis, 'fetch').mockImplementation(async () => ({
ok: true,
json: async () => ({
data: { items: [], total: 0, limit: 20, offset: 0, linkHealth: [], runtime: { requestTimeoutMs: 5000 } },
traceId: 'trace-test',
timestamp: 1783094400000
})
}) as Response);
render(<App />);
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.getByRole('button', { name: '车辆服务交付条 地图' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务交付条 轨迹' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务交付条 统计' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务交付条 导出' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: '车辆服务交付条 告警' })).toBeInTheDocument();
});
test('exposes AMap operations shortcuts when map key is configured', async () => {
window.__LINGNIU_APP_CONFIG__ = { amapWebJsKey: 'amap-web-key' };
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {