refine Semi UI vehicle flow

This commit is contained in:
lingniu
2026-07-18 21:33:32 +08:00
parent 22f1548f8a
commit 911addd891
6 changed files with 75 additions and 18 deletions

View File

@@ -89,6 +89,8 @@ test('polls lightweight single-vehicle realtime data and passes its report inter
expect(commandCard?.querySelector('.v2-live-grid > div:nth-child(2)')).toHaveTextContent('SOC'); expect(commandCard?.querySelector('.v2-live-grid > div:nth-child(2)')).toHaveTextContent('SOC');
expect(commandCard?.querySelector('.v2-live-grid > div:nth-child(4)')).toHaveTextContent('当日里程'); expect(commandCard?.querySelector('.v2-live-grid > div:nth-child(4)')).toHaveTextContent('当日里程');
expect(commandCard?.querySelector('.v2-live-grid > div:nth-child(5)')).toHaveTextContent('推荐来源JT/T 808'); expect(commandCard?.querySelector('.v2-live-grid > div:nth-child(5)')).toHaveTextContent('推荐来源JT/T 808');
expect(commandCard?.querySelector('.v2-live-primary-protocol')).toHaveClass('v2-protocol-tag', 'semi-tag-cyan-light');
expect(commandCard?.querySelector('.v2-identity-sources .v2-protocol-tag')).toHaveClass('semi-tag-cyan-light');
expect(screen.getByLabelText('粤A12345 车辆档案')).toHaveFocus(); expect(screen.getByLabelText('粤A12345 车辆档案')).toHaveFocus();
expect(screen.getByText('扫描 0 帧 · 上海时间 10:00:00')).toBeInTheDocument(); expect(screen.getByText('扫描 0 帧 · 上海时间 10:00:00')).toBeInTheDocument();
expect(screen.getByRole('heading', { name: '最新上报', level: 5 })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: '最新上报', level: 5 })).toBeInTheDocument();
@@ -117,6 +119,31 @@ test('polls lightweight single-vehicle realtime data and passes its report inter
await waitFor(() => expect(sourceEvidence).toHaveBeenCalledTimes(1)); await waitFor(() => expect(sourceEvidence).toHaveBeenCalledTimes(1));
}); });
test('holds the vehicle identity at the top through late layout shifts and yields to deliberate scrolling', async () => {
vi.spyOn(api, 'vehicleDetail').mockResolvedValue(detail);
vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [initialRealtime], total: 1, limit: 1, offset: 0 });
vi.spyOn(api, 'latestTelemetry').mockResolvedValue({ values: [], categories: [], scannedFrames: 0 } as never);
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const workspace = document.createElement('main');
workspace.className = 'v2-content';
document.body.appendChild(workspace);
render(<QueryClientProvider client={client}><MemoryRouter initialEntries={[`/vehicles/${initialRealtime.vin}`]} future={ROUTER_FUTURE}><Routes><Route path="/vehicles/:vin" element={<VehiclePage />} /></Routes></MemoryRouter></QueryClientProvider>, { container: workspace });
await screen.findByLabelText('粤A12345 车辆档案');
await new Promise((resolve) => window.setTimeout(resolve, 420));
workspace.scrollTop = 312;
fireEvent.scroll(workspace);
await waitFor(() => expect(workspace.scrollTop).toBe(0));
fireEvent.wheel(workspace);
workspace.scrollTop = 188;
fireEvent.scroll(workspace);
await new Promise((resolve) => window.setTimeout(resolve, 32));
expect(workspace.scrollTop).toBe(188);
workspace.remove();
});
test('uses the shared Semi empty recovery surface when a vehicle cannot be resolved', async () => { test('uses the shared Semi empty recovery surface when a vehicle cannot be resolved', async () => {
vi.spyOn(api, 'vehicleDetail').mockResolvedValue({ ...detail, lookupResolved: false }); vi.spyOn(api, 'vehicleDetail').mockResolvedValue({ ...detail, lookupResolved: false });
vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [], total: 0, limit: 1, offset: 0 }); vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [], total: 0, limit: 1, offset: 0 });
@@ -184,6 +211,7 @@ test('shows deduplicated Semi vehicle candidates and opens the selected VIN', as
expect(view.container.querySelector('.v2-vehicle-directory-table.semi-table-wrapper')).toBeInTheDocument(); expect(view.container.querySelector('.v2-vehicle-directory-table.semi-table-wrapper')).toBeInTheDocument();
expect(view.container.querySelector('.v2-vehicle-search-page')).toHaveClass('v2-vehicle-directory-v3'); expect(view.container.querySelector('.v2-vehicle-search-page')).toHaveClass('v2-vehicle-directory-v3');
expect(view.container.querySelector('.v2-vehicle-directory-profile')).toHaveTextContent('测试品牌13800000000'); expect(view.container.querySelector('.v2-vehicle-directory-profile')).toHaveTextContent('测试品牌13800000000');
expect(view.container.querySelector('.v2-vehicle-directory-protocols .v2-protocol-tag')).toHaveClass('semi-tag-cyan-light');
expect(view.container.querySelector('.v2-vehicle-discovery-shell')).toContainElement(screen.getByLabelText('查车操作')); expect(view.container.querySelector('.v2-vehicle-discovery-shell')).toContainElement(screen.getByLabelText('查车操作'));
expect(screen.getByRole('button', { name: `打开 ${initialRealtime.vin} 车辆档案` })).toBeInTheDocument(); expect(screen.getByRole('button', { name: `打开 ${initialRealtime.vin} 车辆档案` })).toBeInTheDocument();
fireEvent.focus(input); fireEvent.focus(input);
@@ -492,6 +520,9 @@ test('uses compact Semi telemetry evidence cards on mobile', async () => {
expect(screen.getByRole('group', { name: '车辆快捷操作' })).toHaveTextContent('切换车辆轨迹回放历史数据里程查询告警事件'); expect(screen.getByRole('group', { name: '车辆快捷操作' })).toHaveTextContent('切换车辆轨迹回放历史数据里程查询告警事件');
expect(document.querySelectorAll('.v2-identity-sources .semi-tag')).toHaveLength(3); expect(document.querySelectorAll('.v2-identity-sources .semi-tag')).toHaveLength(3);
expect(document.querySelector('.v2-identity-sources')).toHaveTextContent('GB/T 32960JT/T 808宇通 MQTT'); expect(document.querySelector('.v2-identity-sources')).toHaveTextContent('GB/T 32960JT/T 808宇通 MQTT');
expect(document.querySelector('.v2-identity-sources .semi-tag-blue-light')).toHaveTextContent('GB/T 32960');
expect(document.querySelector('.v2-identity-sources .semi-tag-cyan-light')).toHaveTextContent('JT/T 808');
expect(document.querySelector('.v2-identity-sources .semi-tag-violet-light')).toHaveTextContent('宇通 MQTT');
expect(document.querySelector('.v2-vehicle-command-card .v2-live-grid')).toHaveAttribute('aria-label', '车辆实时指标'); expect(document.querySelector('.v2-vehicle-command-card .v2-live-grid')).toHaveAttribute('aria-label', '车辆实时指标');
expect(document.querySelectorAll('.v2-vehicle-command-card .v2-live-grid > [role="listitem"]')).toHaveLength(6); expect(document.querySelectorAll('.v2-vehicle-command-card .v2-live-grid > [role="listitem"]')).toHaveLength(6);
expect(document.querySelector('.v2-live-metric.is-speed')).toHaveTextContent('速度'); expect(document.querySelector('.v2-live-metric.is-speed')).toHaveTextContent('速度');

View File

@@ -19,6 +19,7 @@ import { FleetMap } from '../map/FleetMap';
import { InlineError, PageLoading } from '../shared/AsyncState'; import { InlineError, PageLoading } from '../shared/AsyncState';
import { MonitorReturnBar } from '../shared/MonitorReturnBar'; import { MonitorReturnBar } from '../shared/MonitorReturnBar';
import { PlatformTime } from '../shared/PlatformTime'; import { PlatformTime } from '../shared/PlatformTime';
import { ProtocolTag } from '../shared/ProtocolTag';
import { SegmentedTabs } from '../shared/SegmentedTabs'; import { SegmentedTabs } from '../shared/SegmentedTabs';
import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel'; import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel';
import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar';
@@ -223,7 +224,7 @@ function TelemetryTimeCell({ item }: { item: LatestTelemetryValue }) {
} }
function TelemetrySourceCell({ item }: { item: LatestTelemetryValue }) { function TelemetrySourceCell({ item }: { item: LatestTelemetryValue }) {
return <div className="v2-telemetry-source"><Tag color="blue" type="light" size="small">{protocolDisplayLabel(item.protocol)}</Tag><small title={item.sourceEndpoint}>{item.sourceEndpoint || '协议默认来源'}</small></div>; return <div className="v2-telemetry-source"><ProtocolTag protocol={item.protocol} compact /><small title={item.sourceEndpoint}>{item.sourceEndpoint || '协议默认来源'}</small></div>;
} }
function telemetryRowKey(item?: LatestTelemetryValue) { function telemetryRowKey(item?: LatestTelemetryValue) {
@@ -431,27 +432,41 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
useLayoutEffect(() => { useLayoutEffect(() => {
const root = recordRootRef.current; const root = recordRootRef.current;
if (!root) return; if (!root) return;
let cancelled = false; const scrollOwner = root.closest<HTMLElement>('.v2-content') ?? document.querySelector<HTMLElement>('.v2-content');
let active = true;
let resetFrame = 0;
const timers: number[] = []; const timers: number[] = [];
const revealIdentity = () => { const revealIdentity = () => {
if (!cancelled) revealVehicleIdentity(root); if (active) revealVehicleIdentity(root);
}; };
const cancelEntryReset = () => { const stopEntryGuard = () => {
cancelled = true; if (!active) return;
active = false;
window.cancelAnimationFrame(resetFrame);
for (const timer of timers) window.clearTimeout(timer); for (const timer of timers) window.clearTimeout(timer);
resizeObserver?.disconnect();
scrollOwner?.removeEventListener('scroll', restoreEntryPosition);
for (const eventName of interactionEvents) window.removeEventListener(eventName, stopEntryGuard, true);
};
const restoreEntryPosition = () => {
if (!active || !scrollOwner || scrollOwner.scrollTop === 0) return;
window.cancelAnimationFrame(resetFrame);
resetFrame = window.requestAnimationFrame(revealIdentity);
}; };
const interactionEvents = ['wheel', 'touchstart', 'pointerdown', 'keydown'] as const; const interactionEvents = ['wheel', 'touchstart', 'pointerdown', 'keydown'] as const;
for (const eventName of interactionEvents) window.addEventListener(eventName, cancelEntryReset, { capture: true, passive: true }); for (const eventName of interactionEvents) window.addEventListener(eventName, stopEntryGuard, { capture: true, passive: true });
scrollOwner?.addEventListener('scroll', restoreEntryPosition, { passive: true });
const resizeObserver = typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(revealIdentity);
resizeObserver?.observe(root);
revealIdentity(); revealIdentity();
const frame = window.requestAnimationFrame(revealIdentity); const frame = window.requestAnimationFrame(revealIdentity);
// The map and live evidence arrive independently. Keep the identity band // Map tiles, source evidence and live data settle independently. Guard the
// anchored only during initial settlement, then immediately yield to input. // first viewport through those late layout changes, but yield immediately
for (const delay of [80, 180, 360, 720, 1200, 1800, 2400]) timers.push(window.setTimeout(revealIdentity, delay)); // as soon as the user starts scrolling or interacting.
for (const delay of [80, 180, 360, 720, 1200, 1800, 2400, 3600, 5200]) timers.push(window.setTimeout(revealIdentity, delay));
return () => { return () => {
cancelled = true;
window.cancelAnimationFrame(frame); window.cancelAnimationFrame(frame);
for (const timer of timers) window.clearTimeout(timer); stopEntryGuard();
for (const eventName of interactionEvents) window.removeEventListener(eventName, cancelEntryReset, true);
}; };
}, [detail.vin]); }, [detail.vin]);
return <div ref={recordRootRef} className={`v2-vehicle-record-page v2-vehicle-record-v3${mobileLayout ? ' is-mobile-layout' : ''}`} tabIndex={-1} aria-label={`${fmt(identity?.plate || realtime?.plate)} 车辆档案`}> return <div ref={recordRootRef} className={`v2-vehicle-record-page v2-vehicle-record-v3${mobileLayout ? ' is-mobile-layout' : ''}`} tabIndex={-1} aria-label={`${fmt(identity?.plate || realtime?.plate)} 车辆档案`}>
@@ -467,7 +482,7 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
description={<span className="v2-identity-vin"><small>VIN</small><b>{detail.vin}</b><Button theme="borderless" aria-label="复制 VIN" title="复制 VIN" icon={<IconCopy />} onClick={() => navigator.clipboard?.writeText(detail.vin)} /></span>} description={<span className="v2-identity-vin"><small>VIN</small><b>{detail.vin}</b><Button theme="borderless" aria-label="复制 VIN" title="复制 VIN" icon={<IconCopy />} onClick={() => navigator.clipboard?.writeText(detail.vin)} /></span>}
meta={<span className="v2-identity-meta"> meta={<span className="v2-identity-meta">
<span className="v2-identity-model"><small> / </small><strong>{[detail.profile?.brandName, detail.profile?.modelName].filter(Boolean).join(' / ') || fmt(identity?.oem || realtime?.oem)}</strong></span> <span className="v2-identity-model"><small> / </small><strong>{[detail.profile?.brandName, detail.profile?.modelName].filter(Boolean).join(' / ') || fmt(identity?.oem || realtime?.oem)}</strong></span>
<span className="v2-identity-source-context"><small></small><span className="v2-identity-sources">{detail.sources.length ? detail.sources.map((source) => <Tag key={source} color="blue" type="light" size="small">{protocolDisplayLabel(source)}</Tag>) : <Tag color="grey" type="light" size="small"></Tag>}</span></span> <span className="v2-identity-source-context"><small></small><span className="v2-identity-sources">{detail.sources.length ? detail.sources.map((source) => <ProtocolTag key={source} protocol={source} compact />) : <Tag color="grey" type="light" size="small"></Tag>}</span></span>
</span>} </span>}
actions={<div className="v2-identity-actions" role="group" aria-label="车辆快捷操作"> actions={<div className="v2-identity-actions" role="group" aria-label="车辆快捷操作">
{actions.map((action) => <Button key={action.key} theme="light" type={action.type} icon={action.icon} aria-label={action.label} onClick={() => navigate(action.to)}>{action.label}</Button>)} {actions.map((action) => <Button key={action.key} theme="light" type={action.type} icon={action.icon} aria-label={action.label} onClick={() => navigate(action.to)}>{action.label}</Button>)}
@@ -484,7 +499,7 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
<div className="v2-live-metric is-soc" role="listitem"><small>SOC</small><strong>{availableMetric(realtime?.socPercent, realtime?.socAvailable)}<em>%</em></strong><span></span></div> <div className="v2-live-metric is-soc" role="listitem"><small>SOC</small><strong>{availableMetric(realtime?.socPercent, realtime?.socAvailable)}<em>%</em></strong><span></span></div>
<div className="v2-live-metric is-mileage" role="listitem"><small></small><Button theme="borderless" type="tertiary" className="v2-live-source-link" title="展开全部里程来源" aria-label="查看总里程全部来源" onClick={() => setSourceEvidenceOpen(true)}><span>{availableMetric(realtime?.totalMileageKm, realtime?.mileageAvailable)}<em>km</em></span><IconChevronRight /></Button><span></span></div> <div className="v2-live-metric is-mileage" role="listitem"><small></small><Button theme="borderless" type="tertiary" className="v2-live-source-link" title="展开全部里程来源" aria-label="查看总里程全部来源" onClick={() => setSourceEvidenceOpen(true)}><span>{availableMetric(realtime?.totalMileageKm, realtime?.mileageAvailable)}<em>km</em></span><IconChevronRight /></Button><span></span></div>
<div className="v2-live-metric is-today-mileage" role="listitem"><small></small><Button theme="borderless" type="tertiary" className="v2-live-source-link" title="展开全部里程来源" aria-label="查看当日里程全部来源" onClick={() => setSourceEvidenceOpen(true)}><span>{availableMetric(realtime?.todayMileageKm, realtime?.todayMileageAvailable)}<em>km</em></span><IconChevronRight /></Button><span></span></div> <div className="v2-live-metric is-today-mileage" role="listitem"><small></small><Button theme="borderless" type="tertiary" className="v2-live-source-link" title="展开全部里程来源" aria-label="查看当日里程全部来源" onClick={() => setSourceEvidenceOpen(true)}><span>{availableMetric(realtime?.todayMileageKm, realtime?.todayMileageAvailable)}<em>km</em></span><IconChevronRight /></Button><span></span></div>
<div className="v2-live-metric is-source" role="listitem"><small></small><strong className="is-text" title={realtime?.primaryProtocol}>{protocolDisplayLabel(realtime?.primaryProtocol)}</strong><span title={realtime?.locationSource}>{protocolSourceLabel(realtime?.locationSource) || '当前最优来源'}</span></div> <div className="v2-live-metric is-source" role="listitem"><small></small><ProtocolTag protocol={realtime?.primaryProtocol} className="v2-live-primary-protocol" /><span title={realtime?.locationSource}>{protocolSourceLabel(realtime?.locationSource) || '当前最优来源'}</span></div>
<div className="v2-live-metric is-source-status" role="listitem"><small></small><strong>{realtime?.onlineSourceCount ?? 0}<em> / {detail.sourceStatus.length}</em></strong><span>线 / </span></div> <div className="v2-live-metric is-source-status" role="listitem"><small></small><strong>{realtime?.onlineSourceCount ?? 0}<em> / {detail.sourceStatus.length}</em></strong><span>线 / </span></div>
</div> </div>
<CurrentVehicleAddress vehicle={realtime} fallback={identity?.locationText} /> <CurrentVehicleAddress vehicle={realtime} fallback={identity?.locationText} />

View File

@@ -8,9 +8,9 @@ import type { Page, VehicleCoverageRow } from '../../api/types';
import { usePlatformSession } from '../auth/AuthGate'; import { usePlatformSession } from '../auth/AuthGate';
import { canAdminister } from '../auth/session'; import { canAdminister } from '../auth/session';
import { formatZhNumber } from '../domain/formatters'; import { formatZhNumber } from '../domain/formatters';
import { protocolDisplayLabel } from '../domain/protocols';
import { useMobileLayout } from '../hooks/useMobileLayout'; import { useMobileLayout } from '../hooks/useMobileLayout';
import { QUERY_MEMORY, queryScopeKey, retainPreviousPageWithinScope } from '../queryPolicy'; import { QUERY_MEMORY, queryScopeKey, retainPreviousPageWithinScope } from '../queryPolicy';
import { ProtocolTag } from '../shared/ProtocolTag';
import { TablePagination } from '../shared/TablePagination'; import { TablePagination } from '../shared/TablePagination';
import { VehicleCandidateList } from '../shared/VehicleCandidateList'; import { VehicleCandidateList } from '../shared/VehicleCandidateList';
import { mergeVehicleCandidates } from '../shared/vehicleCandidates'; import { mergeVehicleCandidates } from '../shared/vehicleCandidates';
@@ -194,7 +194,7 @@ export default function VehicleSearchWorkspace() {
<Tag color={vehicle.online ? 'green' : 'grey'} type="light" size="small">{vehicle.online ? '在线' : '离线'}</Tag> <Tag color={vehicle.online ? 'green' : 'grey'} type="light" size="small">{vehicle.online ? '在线' : '离线'}</Tag>
<small>{vehicleLastSeenLabel(vehicle.lastSeen)}</small> <small>{vehicleLastSeenLabel(vehicle.lastSeen)}</small>
</span> </span>
<span className="v2-vehicle-recent-protocols">{vehicle.protocols.slice(0, 2).map((protocol) => <Tag key={protocol} color="blue" type="light" size="small">{protocolDisplayLabel(protocol)}</Tag>)}</span> <span className="v2-vehicle-recent-protocols">{vehicle.protocols.slice(0, 2).map((protocol) => <ProtocolTag key={protocol} protocol={protocol} compact />)}</span>
<IconChevronRight className="v2-vehicle-recent-arrow" /> <IconChevronRight className="v2-vehicle-recent-arrow" />
</Button> </Button>
</div>)} </div>)}
@@ -222,7 +222,7 @@ export default function VehicleSearchWorkspace() {
{ {
title: '协议来源', title: '协议来源',
dataIndex: 'protocols', dataIndex: 'protocols',
render: (protocols: string[]) => <span className="v2-vehicle-directory-protocols">{protocols.length ? protocols.slice(0, 3).map((protocol) => <Tag key={protocol} color="blue" type="light" size="small">{protocolDisplayLabel(protocol)}</Tag>) : <span></span>}</span> render: (protocols: string[]) => <span className="v2-vehicle-directory-protocols">{protocols.length ? protocols.slice(0, 3).map((protocol) => <ProtocolTag key={protocol} protocol={protocol} compact />) : <span></span>}</span>
}, },
{ {
title: '', title: '',

View File

@@ -27,6 +27,8 @@ test('renders consistent Semi vehicle options and keeps plate first', () => {
const option = screen.getByRole('option', { name: '粤A00001 VIN001 GB/T 32960 JT/T 808 选择' }); const option = screen.getByRole('option', { name: '粤A00001 VIN001 GB/T 32960 JT/T 808 选择' });
expect(option).toHaveClass('semi-button', 'v2-vehicle-option'); expect(option).toHaveClass('semi-button', 'v2-vehicle-option');
expect(option).toHaveTextContent('粤A00001VIN001GB/T 32960JT/T 808选择'); expect(option).toHaveTextContent('粤A00001VIN001GB/T 32960JT/T 808选择');
expect(option.querySelector('.semi-tag-blue-light')).toHaveTextContent('GB/T 32960');
expect(option.querySelector('.semi-tag-cyan-light')).toHaveTextContent('JT/T 808');
expect(screen.getByRole('option', { name: '粤A00002 VIN002 JT/T 808 已分配' })).toHaveAttribute('aria-selected', 'true'); expect(screen.getByRole('option', { name: '粤A00002 VIN002 JT/T 808 已分配' })).toHaveAttribute('aria-selected', 'true');
fireEvent.click(option); fireEvent.click(option);

View File

@@ -1,6 +1,7 @@
import { Button, Spin, Tag } from '@douyinfe/semi-ui'; import { Button, Spin, Tag } from '@douyinfe/semi-ui';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { protocolDisplayLabel } from '../domain/protocols'; import { protocolDisplayLabel } from '../domain/protocols';
import { ProtocolTag } from './ProtocolTag';
import { VehicleOptionError } from './VehicleOptionError'; import { VehicleOptionError } from './VehicleOptionError';
export interface VehicleCandidateItem { export interface VehicleCandidateItem {
@@ -75,7 +76,7 @@ export function VehicleCandidateList<T extends VehicleCandidateItem>({
<strong>{vehicle.plate || '未绑定车牌'}</strong> <strong>{vehicle.plate || '未绑定车牌'}</strong>
<small>{vehicle.vin}</small> <small>{vehicle.vin}</small>
</span> </span>
{protocols.length ? <span className="v2-vehicle-option-protocols">{protocols.slice(0, 3).map((protocol) => <Tag key={protocol} color="grey" type="light" size="small">{protocolDisplayLabel(protocol)}</Tag>)}</span> : null} {protocols.length ? <span className="v2-vehicle-option-protocols">{protocols.slice(0, 3).map((protocol) => <ProtocolTag key={protocol} protocol={protocol} compact />)}</span> : null}
<Tag className="v2-vehicle-option-action" color={selected ? 'blue' : 'grey'} type="light" size="small">{label}</Tag> <Tag className="v2-vehicle-option-action" color={selected ? 'blue' : 'grey'} type="light" size="small">{label}</Tag>
</Button>; </Button>;
}) : null} }) : null}

View File

@@ -6513,6 +6513,14 @@
letter-spacing: 0; letter-spacing: 0;
} }
.v2-vehicle-command-card .v2-live-primary-protocol.semi-tag {
min-height: 24px;
margin: 7px 0 0;
border-radius: 7px;
padding-inline: 8px;
font-size: 10px;
}
.v2-vehicle-command-card .v2-live-source-link .semi-button-content { .v2-vehicle-command-card .v2-live-source-link .semi-button-content {
display: flex; display: flex;
min-width: 0; min-width: 0;