refactor: converge semi metric rail

This commit is contained in:
lingniu
2026-07-19 12:32:01 +08:00
parent 1e57fdfff1
commit f825c9f417
3 changed files with 45 additions and 111 deletions

View File

@@ -346,6 +346,10 @@ describe('V2 production entry', () => {
expect(corePageSources.OperationsPage).toContain('<Card className="v2-ops-overview"'); expect(corePageSources.OperationsPage).toContain('<Card className="v2-ops-overview"');
expect(corePageSources.OperationsPage).toContain("from '../shared/WorkspaceMetricRail'"); expect(corePageSources.OperationsPage).toContain("from '../shared/WorkspaceMetricRail'");
expect(corePageSources.OperationsPage).toContain('<WorkspaceMetricRail'); expect(corePageSources.OperationsPage).toContain('<WorkspaceMetricRail');
expect(workspaceMetricRailSource).toContain("variant: 'queue'");
expect(workspaceMetricRailSource).not.toContain('CardGroup');
expect(workspaceMetricRailSource).not.toContain('primaryValueClassName');
expect(workspaceMetricRailSource).not.toContain('secondaryClassName');
expect(corePageSources.OperationsPage).not.toContain('<OpsMetric'); expect(corePageSources.OperationsPage).not.toContain('<OpsMetric');
expect(corePageSources.OperationsPage).toContain('<Card className="v2-ops-panel v2-ops-links"'); expect(corePageSources.OperationsPage).toContain('<Card className="v2-ops-panel v2-ops-links"');
expect(corePageSources.OperationsPage).toContain('<article className={`v2-ops-link-card'); expect(corePageSources.OperationsPage).toContain('<article className={`v2-ops-link-card');
@@ -484,9 +488,9 @@ describe('V2 production entry', () => {
expect(corePageSources.StatisticsPage).toContain('<PanelEmpty className="v2-mileage-empty"'); expect(corePageSources.StatisticsPage).toContain('<PanelEmpty className="v2-mileage-empty"');
expect(corePageSources.StatisticsPage).not.toContain('<table className="v2-mileage-table"'); expect(corePageSources.StatisticsPage).not.toContain('<table className="v2-mileage-table"');
expect(corePageSources.StatisticsPage).not.toContain('<input'); expect(corePageSources.StatisticsPage).not.toContain('<input');
expect(workspaceMetricRailSource).toContain("import { Button, Card, CardGroup } from '@douyinfe/semi-ui'"); expect(workspaceMetricRailSource).toContain("import { Button, Card } from '@douyinfe/semi-ui'");
expect(workspaceMetricRailSource).toContain('<Card className={className}'); expect(workspaceMetricRailSource).not.toContain('CardGroup');
expect(workspaceMetricRailSource).toContain('<CardGroup className={secondaryClassName}'); expect(workspaceMetricRailSource).not.toContain('secondaryClassName');
expect(workspaceMetricRailSource).toContain("variant: 'queue'"); expect(workspaceMetricRailSource).toContain("variant: 'queue'");
expect(workspaceMetricRailSource).toContain('className={`v2-workspace-metric-rail is-queue'); expect(workspaceMetricRailSource).toContain('className={`v2-workspace-metric-rail is-queue');
expect(workspaceMetricRailSource).toContain('className="v2-workspace-metric-action"'); expect(workspaceMetricRailSource).toContain('className="v2-workspace-metric-action"');

View File

@@ -3,28 +3,7 @@ import { describe, expect, it, vi } from 'vitest';
import { WorkspaceMetricRail } from './WorkspaceMetricRail'; import { WorkspaceMetricRail } from './WorkspaceMetricRail';
describe('WorkspaceMetricRail', () => { describe('WorkspaceMetricRail', () => {
it('renders one primary outcome and supporting facts as accessible Semi cards', () => { it('renders one accessible, list-first Semi metric rail', () => {
const view = render(<WorkspaceMetricRail
ariaLabel="查询统计"
className="test-rail"
cardClassName="test-card"
secondaryClassName="test-secondary"
primaryValueClassName="test-primary-value"
primary={{ label: '结果行数', value: '1,024', note: '来自 20 辆车辆' }}
secondary={[
{ label: '车辆数', value: '20', note: '当前授权范围', className: 'is-vehicles' },
{ label: '数据来源', value: 'GB32960', note: '全部协议来源', className: 'is-sources' }
]}
/>);
expect(view.container.querySelector('.test-rail.semi-card')).toHaveAttribute('aria-label', '查询统计');
expect(view.container.querySelector('.test-card.is-primary .test-primary-value')).toHaveTextContent('1,024');
expect(view.container.querySelector('.test-secondary.semi-card-group')).toBeInTheDocument();
expect(view.container.querySelectorAll('.test-card.semi-card')).toHaveLength(3);
expect(screen.getByLabelText('车辆数20当前授权范围')).toHaveClass('is-vehicles');
});
it('renders a list-first queue variant inside one Semi card', () => {
const view = render(<WorkspaceMetricRail const view = render(<WorkspaceMetricRail
variant="queue" variant="queue"
ariaLabel="处置队列概览" ariaLabel="处置队列概览"

View File

@@ -1,13 +1,6 @@
import { Button, Card, CardGroup } from '@douyinfe/semi-ui'; import { Button, Card } from '@douyinfe/semi-ui';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
export type WorkspaceMetricRailItem = {
label: string;
value: ReactNode;
note: string;
className?: string;
};
export type WorkspaceQueueMetricRailItem = { export type WorkspaceQueueMetricRailItem = {
label: string; label: string;
value: ReactNode; value: ReactNode;
@@ -19,16 +12,6 @@ export type WorkspaceQueueMetricRailItem = {
onClick?: () => void; onClick?: () => void;
}; };
type WorkspaceSummaryMetricRailProps = {
ariaLabel: string;
primary: WorkspaceMetricRailItem;
secondary: WorkspaceMetricRailItem[];
className: string;
cardClassName: string;
secondaryClassName: string;
primaryValueClassName?: string;
};
type WorkspaceQueueMetricRailProps = { type WorkspaceQueueMetricRailProps = {
variant: 'queue'; variant: 'queue';
ariaLabel: string; ariaLabel: string;
@@ -37,73 +20,41 @@ type WorkspaceQueueMetricRailProps = {
context?: ReactNode; context?: ReactNode;
}; };
export function WorkspaceMetricRail(props: WorkspaceSummaryMetricRailProps | WorkspaceQueueMetricRailProps) { export function WorkspaceMetricRail(props: WorkspaceQueueMetricRailProps) {
if ('items' in props) { const hasContext = props.context != null;
const hasContext = props.context != null; return <Card className={`v2-workspace-metric-rail is-queue${hasContext ? ' has-context' : ''} ${props.className}`} bodyStyle={{ padding: 0 }}>
return <Card className={`v2-workspace-metric-rail is-queue${hasContext ? ' has-context' : ''} ${props.className}`} bodyStyle={{ padding: 0 }}> <div className="v2-workspace-metric-list" role="list" aria-label={props.ariaLabel}>
<div className="v2-workspace-metric-list" role="list" aria-label={props.ariaLabel}> {props.items.map((item, index) => {
{props.items.map((item, index) => { const toneClassName = `is-${item.tone ?? 'neutral'}`;
const toneClassName = `is-${item.tone ?? 'neutral'}`; const emphasisClassName = `is-${item.emphasis ?? (index < 2 ? 'primary' : 'secondary')}`;
const emphasisClassName = `is-${item.emphasis ?? (index < 2 ? 'primary' : 'secondary')}`; const classes = [
const classes = [ toneClassName,
toneClassName, emphasisClassName !== toneClassName ? emphasisClassName : '',
emphasisClassName !== toneClassName ? emphasisClassName : '', item.onClick ? 'is-action' : '',
item.onClick ? 'is-action' : '', item.active ? 'is-active' : ''
item.active ? 'is-active' : '' ].filter(Boolean).join(' ');
].filter(Boolean).join(' '); const content = <>
const content = <> <small>{item.label}</small>
<small>{item.label}</small> <strong>{item.value}</strong>
<strong>{item.value}</strong> {item.note ? <em>{item.note}</em> : null}
{item.note ? <em>{item.note}</em> : null} </>;
</>; return <span
return <span className={classes}
className={classes} role="listitem"
role="listitem" key={item.label}
key={item.label} >
> {item.onClick ? <Button
{item.onClick ? <Button className="v2-workspace-metric-action"
className="v2-workspace-metric-action" theme="borderless"
theme="borderless" type="tertiary"
type="tertiary" htmlType="button"
htmlType="button" aria-label={item.ariaLabel}
aria-label={item.ariaLabel} aria-pressed={item.active}
aria-pressed={item.active} onClick={item.onClick}
onClick={item.onClick} >{content}</Button> : content}
>{content}</Button> : content} </span>;
</span>; })}
})} </div>
</div> {hasContext ? <div className="v2-workspace-metric-context">{props.context}</div> : null}
{hasContext ? <div className="v2-workspace-metric-context">{props.context}</div> : null}
</Card>;
}
const {
ariaLabel,
primary,
secondary,
className,
cardClassName,
secondaryClassName,
primaryValueClassName
} = props;
const cardClass = (item: WorkspaceMetricRailItem, primaryCard = false) => [
cardClassName,
primaryCard ? 'is-primary' : '',
item.className ?? ''
].filter(Boolean).join(' ');
return <Card className={className} aria-label={ariaLabel} bodyStyle={{ padding: 0 }}>
<Card className={cardClass(primary, true)} bodyStyle={{ padding: 0 }} aria-label={`${primary.label}${primary.value}${primary.note}`}>
<small>{primary.label}</small>
<strong className={primaryValueClassName}>{primary.value}</strong>
<span title={primary.note}>{primary.note}</span>
</Card>
<CardGroup className={secondaryClassName} type="grid" spacing={0}>
{secondary.map((item) => <Card className={cardClass(item)} bodyStyle={{ padding: 0 }} aria-label={`${item.label}${item.value}${item.note}`} key={item.label}>
<small>{item.label}</small>
<strong>{item.value}</strong>
<span title={item.note}>{item.note}</span>
</Card>)}
</CardGroup>
</Card>; </Card>;
} }