137 lines
5.0 KiB
TypeScript
137 lines
5.0 KiB
TypeScript
import { IconAlertTriangle, IconRefresh } from '@douyinfe/semi-icons';
|
|
import { Button, Card, CardGroup, Empty, Spin, Typography } from '@douyinfe/semi-ui';
|
|
import type { ReactNode } from 'react';
|
|
|
|
type StateScope = 'page' | 'panel' | 'inline';
|
|
type StateKind = 'loading' | 'empty' | 'error';
|
|
|
|
const PAGE_LOADING_SKELETON = <div className="v2-state-skeleton v2-page-skeleton" aria-hidden="true"><i /><i /><i /><i /></div>;
|
|
const PANEL_LOADING_SKELETON = <div className="v2-state-skeleton" aria-hidden="true"><i /><i /><i /></div>;
|
|
|
|
function stateRole(kind: StateKind) {
|
|
return kind === 'error' ? 'alert' : 'status';
|
|
}
|
|
|
|
function StateSurface({
|
|
kind,
|
|
scope,
|
|
title,
|
|
description,
|
|
className = '',
|
|
action,
|
|
compact = false
|
|
}: {
|
|
kind: StateKind;
|
|
scope: StateScope;
|
|
title: string;
|
|
description?: string;
|
|
className?: string;
|
|
action?: ReactNode;
|
|
compact?: boolean;
|
|
}) {
|
|
const classes = ['v2-state-surface', `is-${kind}`, `is-${scope}`, compact ? 'is-compact' : '', className].filter(Boolean).join(' ');
|
|
|
|
return <section className={classes} role={stateRole(kind)} aria-live={kind === 'error' ? 'assertive' : 'polite'}>
|
|
{kind === 'empty'
|
|
? <Empty className={`v2-state-empty${className ? ` ${className}` : ''}`} title={title} description={description}>{action}</Empty>
|
|
: <div className="v2-state-message">
|
|
<span className="v2-state-icon">{kind === 'loading' ? <Spin size={scope === 'page' ? 'large' : 'middle'} /> : <IconAlertTriangle />}</span>
|
|
<div className="v2-state-copy"><Typography.Text strong>{title}</Typography.Text>{description ? <Typography.Text type={kind === 'error' ? 'danger' : 'tertiary'} size="small">{description}</Typography.Text> : null}</div>
|
|
{action ? <div className="v2-state-action">{action}</div> : null}
|
|
</div>}
|
|
{kind === 'loading' ? (scope === 'page' ? PAGE_LOADING_SKELETON : PANEL_LOADING_SKELETON) : null}
|
|
</section>;
|
|
}
|
|
|
|
export function PageLoading({ label = '正在加载车辆数据' }: { label?: string }) {
|
|
return <StateSurface className="v2-page-state" kind="loading" scope="page" title={label} description="导航与筛选状态已保留,页面就绪后会自动显示。" />;
|
|
}
|
|
|
|
export function PanelLoading({
|
|
title = '正在读取数据',
|
|
description = '当前结果返回后会自动更新。',
|
|
className = '',
|
|
compact = false
|
|
}: {
|
|
title?: string;
|
|
description?: string;
|
|
className?: string;
|
|
compact?: boolean;
|
|
}) {
|
|
return <StateSurface className={className} kind="loading" scope="panel" title={title} description={description} compact={compact} />;
|
|
}
|
|
|
|
export function PanelEmpty({
|
|
title,
|
|
description,
|
|
className = '',
|
|
action
|
|
}: {
|
|
title: string;
|
|
description?: string;
|
|
className?: string;
|
|
action?: ReactNode;
|
|
}) {
|
|
return <StateSurface className={className} kind="empty" scope="panel" title={title} description={description} action={action} />;
|
|
}
|
|
|
|
export type WorkspaceEmptyGuideStep = {
|
|
title: string;
|
|
description: string;
|
|
icon?: ReactNode;
|
|
};
|
|
|
|
export function WorkspaceEmptyGuide({
|
|
title,
|
|
description,
|
|
steps,
|
|
icon,
|
|
eyebrow,
|
|
action,
|
|
secondaryAction,
|
|
className = ''
|
|
}: {
|
|
title: string;
|
|
description: string;
|
|
steps: WorkspaceEmptyGuideStep[];
|
|
icon?: ReactNode;
|
|
eyebrow?: string;
|
|
action?: ReactNode;
|
|
secondaryAction?: ReactNode;
|
|
className?: string;
|
|
}) {
|
|
const classes = ['v2-state-surface', 'is-empty', 'is-panel', 'v2-workspace-empty-guide', className].filter(Boolean).join(' ');
|
|
|
|
return <section className={classes} role="status" aria-live="polite">
|
|
{eyebrow ? <Typography.Text className="v2-workspace-empty-guide-eyebrow" type="tertiary">{eyebrow}</Typography.Text> : null}
|
|
<Empty
|
|
className="v2-workspace-empty-guide-message"
|
|
image={icon ? <span className="v2-workspace-empty-guide-icon">{icon}</span> : undefined}
|
|
title={title}
|
|
description={description}
|
|
/>
|
|
<CardGroup className="v2-workspace-empty-guide-steps" type="grid" spacing={0}>
|
|
{steps.map((step, index) => <Card key={`${step.title}-${index}`} className="v2-workspace-empty-guide-step" bodyStyle={{ padding: 0 }}>
|
|
<span className="v2-workspace-empty-guide-step-index">{step.icon ?? index + 1}</span>
|
|
<span><Typography.Text strong>{step.title}</Typography.Text><Typography.Text type="tertiary" size="small">{step.description}</Typography.Text></span>
|
|
</Card>)}
|
|
</CardGroup>
|
|
{action || secondaryAction ? <div className="v2-workspace-empty-guide-actions">{action}{secondaryAction}</div> : null}
|
|
</section>;
|
|
}
|
|
|
|
export function InlineError({ message, onRetry }: { message: string; onRetry?: () => void }) {
|
|
return <StateSurface
|
|
className="v2-inline-state"
|
|
kind="error"
|
|
scope="inline"
|
|
title="数据暂时无法加载"
|
|
description={message}
|
|
action={onRetry ? <Button theme="light" type="danger" icon={<IconRefresh />} aria-label="重试" onClick={onRetry}>重试</Button> : null}
|
|
/>;
|
|
}
|
|
|
|
export function EmptyState({ title = '暂无符合条件的车辆' }: { title?: string }) {
|
|
return <StateSurface className="v2-inline-state" kind="empty" scope="inline" title={title} />;
|
|
}
|