feat: unify configuration side sheets

This commit is contained in:
lingniu
2026-07-19 05:16:55 +08:00
parent 2bae87f6a2
commit 7eca5ae352
8 changed files with 597 additions and 23 deletions

View File

@@ -0,0 +1,120 @@
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';
};
type WorkspaceSideSheetProps = {
className?: string;
visible: boolean;
ariaLabel: string;
closeLabel?: string;
title: ReactNode;
description: ReactNode;
badge?: ReactNode;
badgeColor?: 'blue' | 'green' | 'orange' | 'red' | 'grey';
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 = '',
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-config-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>;
}