feat(web): unify vehicle workspace identity

This commit is contained in:
lingniu
2026-07-20 02:02:06 +08:00
parent 6e9a170fbf
commit 537f8d9b45
5 changed files with 212 additions and 5 deletions

View File

@@ -553,8 +553,10 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
<WorkspaceCommandBar
className="v2-vehicle-command-header v2-vehicle-record-command"
ariaLabel="单车任务栏"
eyebrow="车辆数字档案"
icon={<IconBox />}
title={<span className="v2-identity-primary">
<span className="v2-plate"><IconBox />{fmt(identity?.plate || realtime?.plate)}</span>
<span className="v2-plate">{fmt(identity?.plate || realtime?.plate)}</span>
<Tag className={`v2-online-label ${realtime?.online ? 'is-online' : ''}`} color={realtime?.online ? 'green' : 'grey'} size="small">{realtime?.online ? '在线' : '离线'}</Tag>
</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>}

View File

@@ -182,6 +182,8 @@ export default function VehicleSearchWorkspace() {
{!mobileLayout || administrator ? <WorkspaceCommandBar
className={`v2-vehicle-command-bar${mobileLayout ? ' is-mobile-admin' : ''}`}
ariaLabel={mobileLayout ? '车辆主档操作' : '查车操作'}
eyebrow={mobileLayout ? '车辆治理' : '车辆目录'}
icon={mobileLayout ? <IconBox /> : <IconSearch />}
title={mobileLayout ? '车辆主档' : '车辆快速定位'}
description={mobileLayout ? '批量维护品牌、车型与运营属性' : '车牌、VIN 或手机号快速查车'}
status={mobileLayout ? undefined : candidates.isPending ? '正在读取授权范围' : vehicleScope}

View File

@@ -1,4 +1,5 @@
import { render, screen } from '@testing-library/react';
import { IconSearch } from '@douyinfe/semi-icons';
import { Button, Typography } from '@douyinfe/semi-ui';
import { expect, test } from 'vitest';
import { WorkspaceCommandBar } from './WorkspaceCommandBar';
@@ -20,3 +21,18 @@ test('renders a compact Semi workspace command hierarchy', () => {
expect(screen.getByRole('button', { name: '执行' })).toHaveClass('semi-button');
expect(view.container.querySelector('.v2-workspace-command-copy')).toHaveTextContent('说明当前任务和数据范围');
});
test('renders a Semi identity treatment for primary workspaces', () => {
const view = render(<WorkspaceCommandBar
ariaLabel="车辆目录操作"
eyebrow="车辆目录"
icon={<IconSearch />}
title="车辆快速定位"
description="车牌、VIN 或手机号快速查车"
/>);
expect(screen.getByLabelText('车辆目录操作')).toHaveClass('has-identity');
expect(screen.getByText('车辆目录')).toHaveClass('v2-workspace-command-eyebrow');
expect(screen.getByRole('heading', { name: '车辆快速定位', level: 5 })).toHaveClass('v2-workspace-command-title');
expect(view.container.querySelector('.v2-workspace-command-icon')).toHaveClass('semi-avatar');
});

View File

@@ -1,4 +1,4 @@
import { Tag, Typography } from '@douyinfe/semi-ui';
import { Avatar, Tag, Typography } from '@douyinfe/semi-ui';
import type { ReactNode } from 'react';
const { Text, Title } = Typography;
@@ -6,6 +6,8 @@ const { Text, Title } = Typography;
export function WorkspaceCommandBar({
title,
description,
eyebrow,
icon,
status,
statusColor = 'blue',
meta,
@@ -15,6 +17,8 @@ export function WorkspaceCommandBar({
}: {
title: ReactNode;
description: ReactNode;
eyebrow?: ReactNode;
icon?: ReactNode;
status?: string;
statusColor?: React.ComponentProps<typeof Tag>['color'];
meta?: ReactNode;
@@ -22,13 +26,23 @@ export function WorkspaceCommandBar({
className?: string;
ariaLabel?: string;
}) {
const rootClassName = ['v2-workspace-command-bar', className].filter(Boolean).join(' ');
const hasIdentity = eyebrow != null || icon != null;
const rootClassName = ['v2-workspace-command-bar', hasIdentity ? 'has-identity' : '', className].filter(Boolean).join(' ');
return (
<header className={rootClassName} aria-label={ariaLabel}>
<div className="v2-workspace-command-copy">
<Title heading={5}>{title}</Title>
<Text type="tertiary">{description}</Text>
{hasIdentity ? <>
<Avatar className="v2-workspace-command-icon" color="blue" size="small">{icon}</Avatar>
<span className="v2-workspace-command-stack">
{eyebrow ? <Text className="v2-workspace-command-eyebrow" type="tertiary">{eyebrow}</Text> : null}
<Title className="v2-workspace-command-title" heading={5}>{title}</Title>
<Text className="v2-workspace-command-description" type="tertiary">{description}</Text>
</span>
</> : <>
<Title className="v2-workspace-command-title" heading={5}>{title}</Title>
<Text className="v2-workspace-command-description" type="tertiary">{description}</Text>
</>}
</div>
{status || meta || actions ? <div className="v2-workspace-command-actions">
{status ? <Tag color={statusColor} type="light" size="small">{status}</Tag> : null}

View File

@@ -26577,3 +26577,176 @@
padding: 5px 9px;
}
}
/*
* Semi UI identity convergence.
* Give high-value workspaces one shared visual grammar: a semantic Semi
* avatar, a quiet eyebrow, a strong task title and supporting context.
*/
.v2-workspace-command-bar.has-identity .v2-workspace-command-copy {
display: flex;
min-width: 0;
align-items: center;
gap: 10px;
}
.v2-workspace-command-icon.semi-avatar {
width: 36px;
height: 36px;
flex: 0 0 36px;
border: 1px solid rgba(18, 104, 243, .12);
border-radius: 10px;
background: linear-gradient(145deg, #edf5ff 0%, #e4efff 100%);
color: var(--v2-blue);
box-shadow: inset 0 1px rgba(255, 255, 255, .86), 0 5px 14px rgba(18, 104, 243, .08);
}
.v2-workspace-command-stack {
display: grid;
min-width: 0;
gap: 1px;
}
.v2-workspace-command-eyebrow.semi-typography {
overflow: hidden;
color: #7d8c9f;
font-size: 8px;
font-weight: 700;
letter-spacing: .08em;
line-height: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.v2-workspace-command-stack > .v2-workspace-command-title.semi-typography {
overflow: hidden;
color: #21374f;
font-size: 14px;
font-weight: 750;
letter-spacing: -.015em;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
.v2-workspace-command-stack > .v2-workspace-command-description.semi-typography {
overflow: hidden;
color: #7b8a9d;
font-size: 9px;
font-weight: 450;
line-height: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
.v2-vehicle-discovery-shell > .v2-vehicle-command-bar.has-identity {
min-height: 58px;
background:
radial-gradient(circle at 88% -80%, rgba(18, 104, 243, .1), transparent 44%),
linear-gradient(105deg, #fff 0%, #fbfdff 100%);
padding: 8px 10px 8px 12px;
}
.v2-vehicle-discovery-shell > .v2-vehicle-command-bar.has-identity .v2-workspace-command-actions > .semi-tag {
border-color: rgba(18, 104, 243, .08);
background: #eef5ff;
color: #245da8;
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.has-identity .v2-workspace-command-copy {
display: flex;
min-height: 94px;
align-items: center;
align-content: normal;
gap: 11px;
background:
radial-gradient(circle at -8% 0%, rgba(18, 104, 243, .1), transparent 48%),
linear-gradient(112deg, #fff 0%, #fbfdff 100%);
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.has-identity .v2-workspace-command-icon {
width: 42px;
height: 42px;
flex-basis: 42px;
border-radius: 12px;
font-size: 18px;
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.has-identity .v2-workspace-command-stack {
gap: 3px;
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.has-identity .v2-workspace-command-eyebrow {
color: #6f8095;
font-size: 8px;
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.has-identity .v2-workspace-command-title,
.v2-vehicle-record-v3 .v2-vehicle-command-header.has-identity .v2-workspace-command-description {
overflow: visible;
line-height: normal;
white-space: normal;
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.has-identity .v2-plate {
min-height: 34px;
border-color: rgba(18, 104, 243, .16);
background: #f2f7ff;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .72);
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.has-identity .v2-identity-actions {
background: linear-gradient(180deg, #fbfcfe 0%, #f7f9fc 100%);
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.has-identity .v2-identity-actions > .semi-button {
border-color: #e3e9f1;
background: #fff;
box-shadow: 0 2px 7px rgba(34, 53, 78, .035);
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.has-identity .v2-identity-actions > .semi-button-primary {
border-color: #c9dcfb;
background: #edf4ff;
box-shadow: 0 3px 9px rgba(18, 104, 243, .07);
}
@media (max-width: 680px) {
.v2-workspace-command-bar.has-identity .v2-workspace-command-copy {
gap: 8px;
}
.v2-workspace-command-bar.has-identity .v2-workspace-command-icon.semi-avatar {
width: 34px;
height: 34px;
flex-basis: 34px;
border-radius: 9px;
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.has-identity .v2-workspace-command-copy {
min-height: 70px;
padding: 8px 10px;
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.has-identity .v2-workspace-command-icon {
width: 36px;
height: 36px;
flex-basis: 36px;
border-radius: 10px;
font-size: 15px;
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.has-identity .v2-workspace-command-eyebrow {
line-height: 10px;
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.has-identity .v2-plate {
min-height: 30px;
padding-inline: 8px;
font-size: 14px;
}
.v2-vehicle-record-v3 .v2-vehicle-command-header.has-identity .v2-online-label.semi-tag {
min-height: 21px;
}
}