feat(web): unify vehicle workspace identity
This commit is contained in:
@@ -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>}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user