refine Semi UI platform shell and states
This commit is contained in:
@@ -19,6 +19,8 @@ describe('route recovery boundary', () => {
|
||||
render(<PlatformErrorBoundary><BrokenShell /></PlatformErrorBoundary>);
|
||||
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('平台外壳运行异常');
|
||||
expect(document.querySelector('.v2-route-error-card.semi-card')).toBeInTheDocument();
|
||||
expect(screen.getByText('查看技术信息')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /重新加载平台/ })).toHaveClass('semi-button');
|
||||
expect(screen.getByRole('button', { name: /返回全局监控/ })).toHaveClass('semi-button');
|
||||
});
|
||||
@@ -35,6 +37,7 @@ describe('route recovery boundary', () => {
|
||||
const BrokenPage = () => { throw new Error('render exploded'); };
|
||||
render(<MemoryRouter future={ROUTER_FUTURE} initialEntries={['/history']}><RoutePage page={BrokenPage} label="历史数据" /></MemoryRouter>);
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('当前模块暂时无法显示');
|
||||
expect(document.querySelector('.v2-route-error.is-runtime .v2-route-error-card.semi-card')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /刷新当前页面/ })).toHaveClass('semi-button');
|
||||
expect(screen.getByRole('button', { name: /返回全局监控/ })).toHaveClass('semi-button');
|
||||
});
|
||||
@@ -83,7 +86,7 @@ describe('route recovery boundary', () => {
|
||||
const PendingPage = lazy(() => new Promise<never>(() => undefined));
|
||||
const { container } = render(<MemoryRouter future={ROUTER_FUTURE} initialEntries={['/access']}><RoutePage page={PendingPage} label="接入管理" /></MemoryRouter>);
|
||||
expect(screen.getByRole('status')).toHaveTextContent('正在加载接入管理');
|
||||
expect(screen.getByText('导航与筛选状态已保留')).toBeInTheDocument();
|
||||
expect(screen.getByText(/导航与筛选状态已保留/)).toBeInTheDocument();
|
||||
expect(container.querySelectorAll('.v2-page-skeleton > i')).toHaveLength(4);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,53 @@
|
||||
import { IconAlertTriangle } from '@douyinfe/semi-icons';
|
||||
import { Card, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import { Component, type ElementType, type ErrorInfo, type ReactNode, Suspense, useCallback, useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { PageLoading } from '../shared/AsyncState';
|
||||
import { RecoveryActions } from '../shared/RecoveryActions';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const CHUNK_RELOAD_KEY = 'vehicle-platform:route-chunk-reload';
|
||||
const CHUNK_RELOAD_COOLDOWN_MS = 2 * 60_000;
|
||||
type RecoveryTone = 'timeout' | 'update' | 'runtime' | 'shell';
|
||||
|
||||
function RouteRecoveryView({
|
||||
tone,
|
||||
eyebrow,
|
||||
title,
|
||||
description,
|
||||
error,
|
||||
children,
|
||||
root = false
|
||||
}: {
|
||||
tone: RecoveryTone;
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
description: string;
|
||||
error: Error;
|
||||
children: ReactNode;
|
||||
root?: boolean;
|
||||
}) {
|
||||
const tagColor = tone === 'update' ? 'blue' : tone === 'timeout' ? 'orange' : 'red';
|
||||
return <section className={`v2-route-error is-${tone}${root ? ' v2-root-error' : ''}`} role="alert">
|
||||
<Card className="v2-route-error-card" bodyStyle={{ padding: 0 }}>
|
||||
<div className="v2-route-error-content">
|
||||
<header>
|
||||
<span className="v2-route-error-icon"><IconAlertTriangle /></span>
|
||||
<Tag color={tagColor}>{eyebrow}</Tag>
|
||||
</header>
|
||||
<div className="v2-route-error-copy">
|
||||
<Title heading={2}>{title}</Title>
|
||||
<Text type="secondary">{description}</Text>
|
||||
</div>
|
||||
<details className="v2-route-error-detail">
|
||||
<summary>查看技术信息</summary>
|
||||
<code>{error.message || error.name}</code>
|
||||
</details>
|
||||
{children}
|
||||
</div>
|
||||
</Card>
|
||||
</section>;
|
||||
}
|
||||
|
||||
export function isRouteChunkError(error: unknown) {
|
||||
const message = error instanceof Error ? `${error.name} ${error.message}` : String(error);
|
||||
@@ -58,21 +100,21 @@ class RecoverableRouteBoundary extends Component<{ children: ReactNode; routeKey
|
||||
if (!error) return this.props.children;
|
||||
const chunkError = isRouteChunkError(error);
|
||||
const chunkTimeout = isRouteChunkTimeout(error);
|
||||
return <section className="v2-route-error" role="alert">
|
||||
<span><IconAlertTriangle /></span>
|
||||
<div>
|
||||
<small>{chunkTimeout ? '页面资源加载超时' : chunkError ? '检测到页面版本更新' : '页面运行异常'}</small>
|
||||
<h2>{chunkTimeout ? '当前模块没有在预期时间内加载完成' : chunkError ? '正在等待加载最新页面资源' : '当前模块暂时无法显示'}</h2>
|
||||
<p>{chunkTimeout ? '系统已等待并尝试恢复,可能是网络暂时不稳定。刷新后会重新加载当前模块,不会丢失服务端数据。' : chunkError ? '通常是发布后旧标签页仍引用上一版本资源。刷新后会恢复,不会丢失服务端数据。' : '页面已被安全隔离,侧栏和其他模块仍可使用。更换筛选条件会自动重试,也可刷新当前页面。'}</p>
|
||||
<code>{error.message || error.name}</code>
|
||||
<RecoveryActions
|
||||
primaryLabel="重试加载模块"
|
||||
onPrimary={this.props.onRetry}
|
||||
secondaryLabel="刷新当前页面"
|
||||
onSecondary={() => window.location.reload()}
|
||||
/>
|
||||
</div>
|
||||
</section>;
|
||||
const tone: RecoveryTone = chunkTimeout ? 'timeout' : chunkError ? 'update' : 'runtime';
|
||||
return <RouteRecoveryView
|
||||
tone={tone}
|
||||
eyebrow={chunkTimeout ? '页面资源加载超时' : chunkError ? '检测到页面版本更新' : '页面运行异常'}
|
||||
title={chunkTimeout ? '当前模块没有在预期时间内加载完成' : chunkError ? '正在等待加载最新页面资源' : '当前模块暂时无法显示'}
|
||||
description={chunkTimeout ? '系统已等待并尝试恢复,可能是网络暂时不稳定。刷新后会重新加载当前模块,不会丢失服务端数据。' : chunkError ? '通常是发布后旧标签页仍引用上一版本资源。刷新后会恢复,不会丢失服务端数据。' : '页面已被安全隔离,侧栏和其他模块仍可使用。更换筛选条件会自动重试,也可刷新当前页面。'}
|
||||
error={error}
|
||||
>
|
||||
<RecoveryActions
|
||||
primaryLabel="重试加载模块"
|
||||
onPrimary={this.props.onRetry}
|
||||
secondaryLabel="刷新当前页面"
|
||||
onSecondary={() => window.location.reload()}
|
||||
/>
|
||||
</RouteRecoveryView>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,16 +128,16 @@ export class PlatformErrorBoundary extends Component<{ children: ReactNode }, {
|
||||
render() {
|
||||
const { error } = this.state;
|
||||
if (!error) return this.props.children;
|
||||
return <section className="v2-route-error v2-root-error" role="alert">
|
||||
<span><IconAlertTriangle /></span>
|
||||
<div>
|
||||
<small>平台外壳运行异常</small>
|
||||
<h2>工作台暂时无法继续显示</h2>
|
||||
<p>系统已阻止异常扩散。请重新加载最新版本;服务端数据不会因页面恢复操作而改变。</p>
|
||||
<code>{error.message || error.name}</code>
|
||||
<RecoveryActions primaryLabel="重新加载平台" onPrimary={() => window.location.reload()} />
|
||||
</div>
|
||||
</section>;
|
||||
return <RouteRecoveryView
|
||||
tone="shell"
|
||||
eyebrow="平台外壳运行异常"
|
||||
title="工作台暂时无法继续显示"
|
||||
description="系统已阻止异常扩散。请重新加载最新版本;服务端数据不会因页面恢复操作而改变。"
|
||||
error={error}
|
||||
root
|
||||
>
|
||||
<RecoveryActions primaryLabel="重新加载平台" onPrimary={() => window.location.reload()} />
|
||||
</RouteRecoveryView>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user