feat(web): unify vehicle workspace identity
This commit is contained in:
@@ -553,8 +553,10 @@ function VehicleRecord({ detail, liveRealtime, telemetry, telemetryPending, tele
|
|||||||
<WorkspaceCommandBar
|
<WorkspaceCommandBar
|
||||||
className="v2-vehicle-command-header v2-vehicle-record-command"
|
className="v2-vehicle-command-header v2-vehicle-record-command"
|
||||||
ariaLabel="单车任务栏"
|
ariaLabel="单车任务栏"
|
||||||
|
eyebrow="车辆数字档案"
|
||||||
|
icon={<IconBox />}
|
||||||
title={<span className="v2-identity-primary">
|
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>
|
<Tag className={`v2-online-label ${realtime?.online ? 'is-online' : ''}`} color={realtime?.online ? 'green' : 'grey'} size="small">{realtime?.online ? '在线' : '离线'}</Tag>
|
||||||
</span>}
|
</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>}
|
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
|
{!mobileLayout || administrator ? <WorkspaceCommandBar
|
||||||
className={`v2-vehicle-command-bar${mobileLayout ? ' is-mobile-admin' : ''}`}
|
className={`v2-vehicle-command-bar${mobileLayout ? ' is-mobile-admin' : ''}`}
|
||||||
ariaLabel={mobileLayout ? '车辆主档操作' : '查车操作'}
|
ariaLabel={mobileLayout ? '车辆主档操作' : '查车操作'}
|
||||||
|
eyebrow={mobileLayout ? '车辆治理' : '车辆目录'}
|
||||||
|
icon={mobileLayout ? <IconBox /> : <IconSearch />}
|
||||||
title={mobileLayout ? '车辆主档' : '车辆快速定位'}
|
title={mobileLayout ? '车辆主档' : '车辆快速定位'}
|
||||||
description={mobileLayout ? '批量维护品牌、车型与运营属性' : '车牌、VIN 或手机号快速查车'}
|
description={mobileLayout ? '批量维护品牌、车型与运营属性' : '车牌、VIN 或手机号快速查车'}
|
||||||
status={mobileLayout ? undefined : candidates.isPending ? '正在读取授权范围' : vehicleScope}
|
status={mobileLayout ? undefined : candidates.isPending ? '正在读取授权范围' : vehicleScope}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { IconSearch } from '@douyinfe/semi-icons';
|
||||||
import { Button, Typography } from '@douyinfe/semi-ui';
|
import { Button, Typography } from '@douyinfe/semi-ui';
|
||||||
import { expect, test } from 'vitest';
|
import { expect, test } from 'vitest';
|
||||||
import { WorkspaceCommandBar } from './WorkspaceCommandBar';
|
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(screen.getByRole('button', { name: '执行' })).toHaveClass('semi-button');
|
||||||
expect(view.container.querySelector('.v2-workspace-command-copy')).toHaveTextContent('说明当前任务和数据范围');
|
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';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
@@ -6,6 +6,8 @@ const { Text, Title } = Typography;
|
|||||||
export function WorkspaceCommandBar({
|
export function WorkspaceCommandBar({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
eyebrow,
|
||||||
|
icon,
|
||||||
status,
|
status,
|
||||||
statusColor = 'blue',
|
statusColor = 'blue',
|
||||||
meta,
|
meta,
|
||||||
@@ -15,6 +17,8 @@ export function WorkspaceCommandBar({
|
|||||||
}: {
|
}: {
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
description: ReactNode;
|
description: ReactNode;
|
||||||
|
eyebrow?: ReactNode;
|
||||||
|
icon?: ReactNode;
|
||||||
status?: string;
|
status?: string;
|
||||||
statusColor?: React.ComponentProps<typeof Tag>['color'];
|
statusColor?: React.ComponentProps<typeof Tag>['color'];
|
||||||
meta?: ReactNode;
|
meta?: ReactNode;
|
||||||
@@ -22,13 +26,23 @@ export function WorkspaceCommandBar({
|
|||||||
className?: string;
|
className?: string;
|
||||||
ariaLabel?: 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 (
|
return (
|
||||||
<header className={rootClassName} aria-label={ariaLabel}>
|
<header className={rootClassName} aria-label={ariaLabel}>
|
||||||
<div className="v2-workspace-command-copy">
|
<div className="v2-workspace-command-copy">
|
||||||
<Title heading={5}>{title}</Title>
|
{hasIdentity ? <>
|
||||||
<Text type="tertiary">{description}</Text>
|
<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>
|
</div>
|
||||||
{status || meta || actions ? <div className="v2-workspace-command-actions">
|
{status || meta || actions ? <div className="v2-workspace-command-actions">
|
||||||
{status ? <Tag color={statusColor} type="light" size="small">{status}</Tag> : null}
|
{status ? <Tag color={statusColor} type="light" size="small">{status}</Tag> : null}
|
||||||
|
|||||||
@@ -26577,3 +26577,176 @@
|
|||||||
padding: 5px 9px;
|
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