Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceSideSheet.tsx
2026-07-19 05:38:17 +08:00

125 lines
4.1 KiB
TypeScript

import { Button, SideSheet, Tag } from '@douyinfe/semi-ui';
import { useId, useLayoutEffect, type ReactNode } from 'react';
export type WorkspaceSideSheetAction = {
label: string;
onClick: () => void;
disabled?: boolean;
loading?: boolean;
theme?: 'solid' | 'light' | 'borderless';
type?: 'primary' | 'secondary' | 'tertiary' | 'warning' | 'danger';
};
export type WorkspaceSideSheetSummaryItem = {
label: string;
value: ReactNode;
detail?: ReactNode;
tone?: 'neutral' | 'primary' | 'success' | 'warning' | 'danger';
};
export type WorkspaceSideSheetBadgeColor = 'blue' | 'green' | 'orange' | 'red' | 'grey';
export type WorkspaceSideSheetProps = {
className?: string;
variant?: 'config' | 'detail';
visible: boolean;
ariaLabel: string;
closeLabel?: string;
title: ReactNode;
description: ReactNode;
badge?: ReactNode;
badgeColor?: WorkspaceSideSheetBadgeColor;
placement?: 'left' | 'right' | 'top' | 'bottom';
width?: string | number;
height?: string | number;
summaryItems?: WorkspaceSideSheetSummaryItem[];
footerNote?: ReactNode;
secondaryActions?: WorkspaceSideSheetAction[];
primaryAction?: WorkspaceSideSheetAction;
onCancel: () => void;
children: ReactNode;
};
export function WorkspaceSideSheet({
className = '',
variant = 'config',
visible,
ariaLabel,
closeLabel = `关闭${ariaLabel}`,
title,
description,
badge,
badgeColor = 'blue',
placement = 'right',
width = 460,
height,
summaryItems = [],
footerNote,
secondaryActions = [],
primaryAction,
onCancel,
children
}: WorkspaceSideSheetProps) {
const sheetId = useId();
const sheetClassName = [`v2-workspace-${variant}-sidesheet`, className].filter(Boolean).join(' ');
const hasFooter = Boolean(footerNote || secondaryActions.length || primaryAction);
useLayoutEffect(() => {
if (!visible) return;
const apply = () => {
const dialog = document.querySelector(`[data-workspace-sheet-id="${sheetId}"] .semi-sidesheet-inner`);
dialog?.setAttribute('aria-label', ariaLabel);
dialog?.querySelector('.semi-sidesheet-close')?.setAttribute('aria-label', closeLabel);
};
apply();
const frame = window.requestAnimationFrame(apply);
return () => window.cancelAnimationFrame(frame);
}, [ariaLabel, closeLabel, sheetId, visible]);
return <SideSheet
className={sheetClassName}
data-workspace-sheet-id={sheetId}
visible={visible}
aria-label={ariaLabel}
placement={placement}
width={placement === 'left' || placement === 'right' ? width : undefined}
height={placement === 'top' || placement === 'bottom' ? height : undefined}
title={<div className="v2-workspace-config-title">
<span><strong>{title}</strong><small>{description}</small></span>
{badge ? <Tag color={badgeColor} type="light" size="small">{badge}</Tag> : null}
</div>}
onCancel={onCancel}
footer={hasFooter ? <div className="v2-workspace-config-footer">
<span>{footerNote}</span>
<div>
{secondaryActions.map((action) => <Button
key={action.label}
theme={action.theme ?? 'light'}
type={action.type}
disabled={action.disabled}
loading={action.loading}
onClick={action.onClick}
>{action.label}</Button>)}
{primaryAction ? <Button
theme={primaryAction.theme ?? 'solid'}
type={primaryAction.type ?? 'primary'}
disabled={primaryAction.disabled}
loading={primaryAction.loading}
onClick={primaryAction.onClick}
>{primaryAction.label}</Button> : null}
</div>
</div> : null}
>
<div className="v2-workspace-config-body">
{summaryItems.length ? <div className="v2-workspace-config-summary" role="list" aria-label={`${ariaLabel}摘要`}>
{summaryItems.map((item) => <span className={`is-${item.tone ?? 'neutral'}`} role="listitem" key={item.label}>
<small>{item.label}</small>
<strong>{item.value}</strong>
{item.detail ? <em>{item.detail}</em> : null}
</span>)}
</div> : null}
<div className="v2-workspace-config-content">{children}</div>
</div>
</SideSheet>;
}