feat: unify global action workbenches

This commit is contained in:
lingniu
2026-07-19 07:13:09 +08:00
parent 3b5a66b687
commit af608b35e0
8 changed files with 295 additions and 34 deletions

View File

@@ -99,6 +99,11 @@ test('opens contextual help for the active module and closes it without navigati
fireEvent.click(help);
expect(screen.getByRole('dialog', { name: '全局监控' })).toHaveTextContent('筛选会自动生效');
expect(document.querySelector('.v2-help-sheet')).toHaveClass('v2-workspace-help-sidesheet');
const helpSummary = screen.getByRole('list', { name: '全局监控摘要' });
expect(helpSummary).toHaveTextContent('页面目标快速完成');
expect(helpSummary).toHaveTextContent('建议步骤3 项');
expect(helpSummary).toHaveTextContent('快捷关闭Esc');
expect(document.querySelector('.v2-help-overview.semi-card')).toBeInTheDocument();
expect(screen.getByText('建议操作')).toBeInTheDocument();
expect(help).toHaveAttribute('aria-expanded', 'true');
@@ -186,7 +191,8 @@ test('uses the compact mobile navigation and opens secondary modules in a Semi S
fireEvent.click(screen.getByRole('button', { name: '更多功能' }));
const moreSheet = screen.getByRole('dialog', { name: '更多功能' });
expect(moreSheet).toHaveTextContent('数据分析与系统管理');
expect(moreSheet.closest('.semi-sidesheet')).toHaveClass('v2-workspace-action-sidesheet', 'v2-mobile-more-sidesheet');
expect(moreSheet).toHaveTextContent('数据分析、业务处置与平台治理');
expect(screen.getByRole('navigation', { name: '分析与处置' })).toBeInTheDocument();
expect(screen.getByRole('navigation', { name: '平台治理' })).toBeInTheDocument();
expect(moreSheet).toHaveTextContent('历史证据与业务告警');

View File

@@ -15,16 +15,16 @@ import {
IconUser,
IconExit
} from '@douyinfe/semi-icons';
import { Avatar, Banner, Button, Card, Dropdown, Layout, Modal, Nav, SideSheet, Tag, Typography } from '@douyinfe/semi-ui';
import { Avatar, Banner, Button, Card, Dropdown, Layout, Modal, Nav, Tag, Typography } from '@douyinfe/semi-ui';
import { type CSSProperties, FormEvent, type MouseEvent, useEffect, useLayoutEffect, useState } from 'react';
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { api } from '../../api/client';
import { usePlatformSession } from '../auth/AuthGate';
import { hasMenu } from '../auth/session';
import { useMobileLayout } from '../hooks/useMobileLayout';
import { useSideSheetA11y } from '../hooks/useSideSheetA11y';
import { preloadRoute, scheduleIdleRoutePreloads, shouldPreloadRouteOnIntent } from '../routing/routeModules';
import { PasswordInput } from '../shared/PasswordInput';
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
const { Header, Sider, Content } = Layout;
const { Text, Title } = Typography;
@@ -64,29 +64,42 @@ const pageHelp: Record<string, { summary: string; tips: string[] }> = {
users: { summary: '创建客户账号并分配菜单和车辆数据范围。', tips: ['客户只能使用四个对外菜单中的已分配项。', '停用账号或重置密码会立即撤销其旧会话。', '车辆权限修改最多在 30 秒内对活跃会话生效。'] }
};
function ContextHelp({ section, visible, onClose }: { section: string; visible: boolean; onClose: () => void }) {
function ContextHelp({ section, visible, mobile, onClose }: { section: string; visible: boolean; mobile: boolean; onClose: () => void }) {
const content = pageHelp[section] ?? { summary: '查看当前模块的操作说明。', tips: ['通过左侧导航切换模块,页面状态会在当前任务中保留。'] };
const title = pageNames[section] ?? '车辆数据中台';
useSideSheetA11y(visible, '.v2-help-sheet', 'v2-context-help', title, '关闭帮助');
return <SideSheet
return <WorkspaceSideSheet
className="v2-help-sheet"
variant="help"
visible={visible}
aria-label={title}
ariaLabel={title}
closeLabel="关闭帮助"
dialogId="v2-context-help"
placement={mobile ? 'bottom' : 'right'}
width={410}
title={<div className="v2-help-sheet-title"><span><IconHelpCircle /></span><div><Text type="tertiary" size="small"></Text><Title heading={4}>{title}</Title></div></div>}
height={mobile ? 'min(70dvh, 620px)' : undefined}
icon={<IconHelpCircle />}
title={title}
description="当前页面帮助与推荐操作"
badge={`${content.tips.length} 项建议`}
summaryItems={[
{ label: '页面目标', value: '快速完成', detail: '保留当前任务状态', tone: 'primary' },
{ label: '建议步骤', value: `${content.tips.length}`, detail: '按顺序执行' },
{ label: '快捷关闭', value: 'Esc', detail: '随时返回页面' }
]}
footerNote="关闭帮助不会改变当前筛选、地图或任务状态"
primaryAction={{ label: '返回当前页面', onClick: onClose }}
onCancel={onClose}
footer={<Button block theme="light" type="primary" onClick={onClose}></Button>}
>
<div className="v2-help-panel">
<Card className="v2-help-overview" bodyStyle={{ padding: 0 }}>
<Tag color="blue"></Tag>
<Tag color="blue"></Tag>
<Text type="secondary">{content.summary}</Text>
</Card>
<div className="v2-help-section-label"><Text strong></Text><Text type="tertiary" size="small">{content.tips.length} </Text></div>
<ol>{content.tips.map((tip, index) => <li key={tip}><span>{index + 1}</span>{tip}</li>)}</ol>
<footer><kbd>Esc</kbd><span></span></footer>
</div>
</SideSheet>;
</WorkspaceSideSheet>;
}
function AccountMenu({
@@ -189,7 +202,7 @@ export function AppShell() {
</Header>
<Content key={location.pathname} className={`v2-content${section === 'tracks' ? ' is-track-workspace' : ''}`}><Outlet /></Content>
</Layout>
<ContextHelp section={section} visible={helpOpen} onClose={() => setHelpOpen(false)} />
<ContextHelp section={section} visible={helpOpen} mobile={mobileLayout} onClose={() => setHelpOpen(false)} />
{passwordOpen ? <PasswordDialog onClose={() => setPasswordOpen(false)} onChanged={logout} /> : null}
</Layout>
);
@@ -281,18 +294,20 @@ function MobileNavigation() {
const itemCount = primary.length + (more.length ? 1 : 0);
const warmRoute = (path: string) => { if (shouldPreloadRouteOnIntent()) void preloadRoute(path); };
useEffect(() => setMoreOpen(false), [location.pathname]);
useSideSheetA11y(moreOpen, '.v2-mobile-more-sidesheet', 'v2-mobile-more', '更多功能', '关闭更多功能');
const link = ({ to, label, icon: Icon }: (typeof mobileMoreNavigation)[number]) => <NavLink key={to} to={to} aria-label={label} onPointerDown={() => warmRoute(to)} className={({ isActive }) => `v2-mobile-nav-item ${isActive ? 'is-active' : ''}`}><Icon size="large" /><span>{label}</span></NavLink>;
return <>
{more.length ? <SideSheet
{more.length ? <WorkspaceSideSheet
className="v2-mobile-more-sidesheet"
variant="action"
visible={moreOpen}
placement="bottom"
height="auto"
title={<div><strong></strong><span></span></div>}
aria-label="更多功能"
footer={null}
height="min(54dvh, 460px)"
icon={<IconMore />}
title="更多功能"
description="数据分析、业务处置与平台治理"
badge={`${more.length} 个入口`}
ariaLabel="更多功能"
dialogId="v2-mobile-more"
onCancel={() => setMoreOpen(false)}
>
<div className="v2-mobile-more-groups">
@@ -304,7 +319,7 @@ function MobileNavigation() {
<nav aria-label={group.title}>{group.items.map(link)}</nav>
</section>)}
</div>
</SideSheet> : null}
</WorkspaceSideSheet> : null}
<nav className="v2-mobile-navigation" aria-label="主导航" style={{ '--v2-mobile-nav-count': Math.max(itemCount, 1) } as CSSProperties}>
{primary.map(link)}
{more.length ? <Button theme="borderless" className={`v2-mobile-nav-item${moreActive ? ' is-active' : ''}`} aria-label="更多功能" aria-expanded={moreOpen} aria-controls="v2-mobile-more" icon={<IconMore size="large" />} onClick={() => setMoreOpen((value) => !value)}><span></span></Button> : null}