feat: unify global action workbenches
This commit is contained in:
@@ -99,6 +99,11 @@ test('opens contextual help for the active module and closes it without navigati
|
|||||||
fireEvent.click(help);
|
fireEvent.click(help);
|
||||||
|
|
||||||
expect(screen.getByRole('dialog', { name: '全局监控' })).toHaveTextContent('筛选会自动生效');
|
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(document.querySelector('.v2-help-overview.semi-card')).toBeInTheDocument();
|
||||||
expect(screen.getByText('建议操作')).toBeInTheDocument();
|
expect(screen.getByText('建议操作')).toBeInTheDocument();
|
||||||
expect(help).toHaveAttribute('aria-expanded', 'true');
|
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: '更多功能' }));
|
fireEvent.click(screen.getByRole('button', { name: '更多功能' }));
|
||||||
|
|
||||||
const moreSheet = screen.getByRole('dialog', { 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(screen.getByRole('navigation', { name: '平台治理' })).toBeInTheDocument();
|
expect(screen.getByRole('navigation', { name: '平台治理' })).toBeInTheDocument();
|
||||||
expect(moreSheet).toHaveTextContent('历史证据与业务告警');
|
expect(moreSheet).toHaveTextContent('历史证据与业务告警');
|
||||||
|
|||||||
@@ -15,16 +15,16 @@ import {
|
|||||||
IconUser,
|
IconUser,
|
||||||
IconExit
|
IconExit
|
||||||
} from '@douyinfe/semi-icons';
|
} 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 { type CSSProperties, FormEvent, type MouseEvent, useEffect, useLayoutEffect, useState } from 'react';
|
||||||
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
import { NavLink, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { api } from '../../api/client';
|
import { api } from '../../api/client';
|
||||||
import { usePlatformSession } from '../auth/AuthGate';
|
import { usePlatformSession } from '../auth/AuthGate';
|
||||||
import { hasMenu } from '../auth/session';
|
import { hasMenu } from '../auth/session';
|
||||||
import { useMobileLayout } from '../hooks/useMobileLayout';
|
import { useMobileLayout } from '../hooks/useMobileLayout';
|
||||||
import { useSideSheetA11y } from '../hooks/useSideSheetA11y';
|
|
||||||
import { preloadRoute, scheduleIdleRoutePreloads, shouldPreloadRouteOnIntent } from '../routing/routeModules';
|
import { preloadRoute, scheduleIdleRoutePreloads, shouldPreloadRouteOnIntent } from '../routing/routeModules';
|
||||||
import { PasswordInput } from '../shared/PasswordInput';
|
import { PasswordInput } from '../shared/PasswordInput';
|
||||||
|
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
|
||||||
|
|
||||||
const { Header, Sider, Content } = Layout;
|
const { Header, Sider, Content } = Layout;
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
@@ -64,29 +64,42 @@ const pageHelp: Record<string, { summary: string; tips: string[] }> = {
|
|||||||
users: { summary: '创建客户账号并分配菜单和车辆数据范围。', tips: ['客户只能使用四个对外菜单中的已分配项。', '停用账号或重置密码会立即撤销其旧会话。', '车辆权限修改最多在 30 秒内对活跃会话生效。'] }
|
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 content = pageHelp[section] ?? { summary: '查看当前模块的操作说明。', tips: ['通过左侧导航切换模块,页面状态会在当前任务中保留。'] };
|
||||||
const title = pageNames[section] ?? '车辆数据中台';
|
const title = pageNames[section] ?? '车辆数据中台';
|
||||||
useSideSheetA11y(visible, '.v2-help-sheet', 'v2-context-help', title, '关闭帮助');
|
return <WorkspaceSideSheet
|
||||||
return <SideSheet
|
|
||||||
className="v2-help-sheet"
|
className="v2-help-sheet"
|
||||||
|
variant="help"
|
||||||
visible={visible}
|
visible={visible}
|
||||||
aria-label={title}
|
ariaLabel={title}
|
||||||
|
closeLabel="关闭帮助"
|
||||||
|
dialogId="v2-context-help"
|
||||||
|
placement={mobile ? 'bottom' : 'right'}
|
||||||
width={410}
|
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}
|
onCancel={onClose}
|
||||||
footer={<Button block theme="light" type="primary" onClick={onClose}>关闭帮助</Button>}
|
|
||||||
>
|
>
|
||||||
<div className="v2-help-panel">
|
<div className="v2-help-panel">
|
||||||
<Card className="v2-help-overview" bodyStyle={{ padding: 0 }}>
|
<Card className="v2-help-overview" bodyStyle={{ padding: 0 }}>
|
||||||
<Tag color="blue">当前模块</Tag>
|
<Tag color="blue">页面目标</Tag>
|
||||||
<Text type="secondary">{content.summary}</Text>
|
<Text type="secondary">{content.summary}</Text>
|
||||||
</Card>
|
</Card>
|
||||||
<div className="v2-help-section-label"><Text strong>建议操作</Text><Text type="tertiary" size="small">{content.tips.length} 项</Text></div>
|
<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>
|
<ol>{content.tips.map((tip, index) => <li key={tip}><span>{index + 1}</span>{tip}</li>)}</ol>
|
||||||
<footer><kbd>Esc</kbd><span>关闭帮助</span></footer>
|
<footer><kbd>Esc</kbd><span>关闭帮助</span></footer>
|
||||||
</div>
|
</div>
|
||||||
</SideSheet>;
|
</WorkspaceSideSheet>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AccountMenu({
|
function AccountMenu({
|
||||||
@@ -189,7 +202,7 @@ export function AppShell() {
|
|||||||
</Header>
|
</Header>
|
||||||
<Content key={location.pathname} className={`v2-content${section === 'tracks' ? ' is-track-workspace' : ''}`}><Outlet /></Content>
|
<Content key={location.pathname} className={`v2-content${section === 'tracks' ? ' is-track-workspace' : ''}`}><Outlet /></Content>
|
||||||
</Layout>
|
</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}
|
{passwordOpen ? <PasswordDialog onClose={() => setPasswordOpen(false)} onChanged={logout} /> : null}
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
@@ -281,18 +294,20 @@ function MobileNavigation() {
|
|||||||
const itemCount = primary.length + (more.length ? 1 : 0);
|
const itemCount = primary.length + (more.length ? 1 : 0);
|
||||||
const warmRoute = (path: string) => { if (shouldPreloadRouteOnIntent()) void preloadRoute(path); };
|
const warmRoute = (path: string) => { if (shouldPreloadRouteOnIntent()) void preloadRoute(path); };
|
||||||
useEffect(() => setMoreOpen(false), [location.pathname]);
|
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>;
|
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 <>
|
return <>
|
||||||
{more.length ? <SideSheet
|
{more.length ? <WorkspaceSideSheet
|
||||||
className="v2-mobile-more-sidesheet"
|
className="v2-mobile-more-sidesheet"
|
||||||
|
variant="action"
|
||||||
visible={moreOpen}
|
visible={moreOpen}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
height="auto"
|
height="min(54dvh, 460px)"
|
||||||
title={<div><strong>更多功能</strong><span>数据分析与系统管理</span></div>}
|
icon={<IconMore />}
|
||||||
aria-label="更多功能"
|
title="更多功能"
|
||||||
footer={null}
|
description="数据分析、业务处置与平台治理"
|
||||||
|
badge={`${more.length} 个入口`}
|
||||||
|
ariaLabel="更多功能"
|
||||||
|
dialogId="v2-mobile-more"
|
||||||
onCancel={() => setMoreOpen(false)}
|
onCancel={() => setMoreOpen(false)}
|
||||||
>
|
>
|
||||||
<div className="v2-mobile-more-groups">
|
<div className="v2-mobile-more-groups">
|
||||||
@@ -304,7 +319,7 @@ function MobileNavigation() {
|
|||||||
<nav aria-label={group.title}>{group.items.map(link)}</nav>
|
<nav aria-label={group.title}>{group.items.map(link)}</nav>
|
||||||
</section>)}
|
</section>)}
|
||||||
</div>
|
</div>
|
||||||
</SideSheet> : null}
|
</WorkspaceSideSheet> : null}
|
||||||
<nav className="v2-mobile-navigation" aria-label="主导航" style={{ '--v2-mobile-nav-count': Math.max(itemCount, 1) } as CSSProperties}>
|
<nav className="v2-mobile-navigation" aria-label="主导航" style={{ '--v2-mobile-nav-count': Math.max(itemCount, 1) } as CSSProperties}>
|
||||||
{primary.map(link)}
|
{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}
|
{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}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { IconChevronRight, IconMore } from '@douyinfe/semi-icons';
|
import { IconChevronRight, IconMore } from '@douyinfe/semi-icons';
|
||||||
import { Button, SideSheet } from '@douyinfe/semi-ui';
|
import { Button } from '@douyinfe/semi-ui';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useSideSheetA11y } from '../hooks/useSideSheetA11y';
|
import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet';
|
||||||
|
|
||||||
export type VehicleAction = {
|
export type VehicleAction = {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -28,8 +28,6 @@ export default function VehicleActions({
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
onSelect(to);
|
onSelect(to);
|
||||||
};
|
};
|
||||||
useSideSheetA11y(open, '.v2-vehicle-mobile-actions-sidesheet', 'v2-vehicle-mobile-actions', '更多车辆操作', '关闭更多车辆操作');
|
|
||||||
|
|
||||||
if (!mobile) return <div className="v2-identity-actions" role="group" aria-label="车辆快捷操作">
|
if (!mobile) return <div className="v2-identity-actions" role="group" aria-label="车辆快捷操作">
|
||||||
{actions.map((action) => <Button key={action.key} theme="light" type={action.type} icon={action.icon} aria-label={action.label} onClick={() => onSelect(action.to)}>{action.label}</Button>)}
|
{actions.map((action) => <Button key={action.key} theme="light" type={action.type} icon={action.icon} aria-label={action.label} onClick={() => onSelect(action.to)}>{action.label}</Button>)}
|
||||||
</div>;
|
</div>;
|
||||||
@@ -39,19 +37,25 @@ export default function VehicleActions({
|
|||||||
{primaryActions.map((action) => <Button key={action.key} theme="light" type={action.type} icon={action.icon} aria-label={action.label} onClick={() => selectAction(action.to)}>{action.label}</Button>)}
|
{primaryActions.map((action) => <Button key={action.key} theme="light" type={action.type} icon={action.icon} aria-label={action.label} onClick={() => selectAction(action.to)}>{action.label}</Button>)}
|
||||||
{overflowActions.length ? <Button className="v2-vehicle-more-trigger" theme="light" type="tertiary" icon={<IconMore />} aria-label="更多车辆操作" aria-haspopup="dialog" aria-controls="v2-vehicle-mobile-actions" aria-expanded={open} onClick={() => setOpen(true)}>更多</Button> : null}
|
{overflowActions.length ? <Button className="v2-vehicle-more-trigger" theme="light" type="tertiary" icon={<IconMore />} aria-label="更多车辆操作" aria-haspopup="dialog" aria-controls="v2-vehicle-mobile-actions" aria-expanded={open} onClick={() => setOpen(true)}>更多</Button> : null}
|
||||||
</div>
|
</div>
|
||||||
<SideSheet
|
<WorkspaceSideSheet
|
||||||
className="v2-vehicle-mobile-actions-sidesheet"
|
className="v2-vehicle-mobile-actions-sidesheet"
|
||||||
|
variant="action"
|
||||||
visible={open}
|
visible={open}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
height="min(48dvh, 360px)"
|
height="min(48dvh, 380px)"
|
||||||
aria-label="更多车辆操作"
|
icon={<IconMore />}
|
||||||
title={<div className="v2-vehicle-mobile-actions-title"><strong>更多车辆操作</strong><span>继续查看这辆车的历史、里程与告警</span></div>}
|
ariaLabel="更多车辆操作"
|
||||||
|
dialogId="v2-vehicle-mobile-actions"
|
||||||
|
title="更多车辆操作"
|
||||||
|
description="继续查看这辆车的历史、里程与告警"
|
||||||
|
badge={`${overflowActions.length} 项`}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
footer={<Button block theme="solid" type="primary" onClick={() => setOpen(false)}>完成</Button>}
|
footerNote="选择操作后将自动进入对应车辆页面"
|
||||||
|
primaryAction={{ label: '完成', onClick: () => setOpen(false) }}
|
||||||
>
|
>
|
||||||
<div className="v2-vehicle-mobile-actions-list">
|
<div className="v2-vehicle-mobile-actions-list">
|
||||||
{overflowActions.map((action) => <Button key={action.key} block theme="light" type="tertiary" icon={action.icon} iconPosition="left" aria-label={action.label} onClick={() => selectAction(action.to)}>{action.label}<IconChevronRight /></Button>)}
|
{overflowActions.map((action) => <Button key={action.key} block theme="light" type="tertiary" icon={action.icon} iconPosition="left" aria-label={action.label} onClick={() => selectAction(action.to)}>{action.label}<IconChevronRight /></Button>)}
|
||||||
</div>
|
</div>
|
||||||
</SideSheet>
|
</WorkspaceSideSheet>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -552,7 +552,7 @@ test('uses compact Semi telemetry evidence cards on mobile', async () => {
|
|||||||
expect(moreActions).toHaveAttribute('aria-expanded', 'false');
|
expect(moreActions).toHaveAttribute('aria-expanded', 'false');
|
||||||
fireEvent.click(moreActions);
|
fireEvent.click(moreActions);
|
||||||
expect(await screen.findByRole('dialog', { name: '更多车辆操作' })).toBeVisible();
|
expect(await screen.findByRole('dialog', { name: '更多车辆操作' })).toBeVisible();
|
||||||
expect(document.querySelector('.v2-vehicle-mobile-actions-sidesheet')).toHaveClass('semi-sidesheet-bottom');
|
expect(document.querySelector('.v2-vehicle-mobile-actions-sidesheet')).toHaveClass('v2-workspace-action-sidesheet', 'semi-sidesheet-bottom');
|
||||||
expect(screen.getByRole('button', { name: '历史数据' })).toBeVisible();
|
expect(screen.getByRole('button', { name: '历史数据' })).toBeVisible();
|
||||||
expect(screen.getByRole('button', { name: '里程查询' })).toBeVisible();
|
expect(screen.getByRole('button', { name: '里程查询' })).toBeVisible();
|
||||||
expect(screen.getByRole('button', { name: '告警事件' })).toBeVisible();
|
expect(screen.getByRole('button', { name: '告警事件' })).toBeVisible();
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const vehicleCandidateListSource = readFileSync(resolve(process.cwd(), 'src/v2/s
|
|||||||
const recoveryActionsSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/RecoveryActions.tsx'), 'utf8');
|
const recoveryActionsSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/RecoveryActions.tsx'), 'utf8');
|
||||||
const detailTriggerRowSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/detailTriggerRow.ts'), 'utf8');
|
const detailTriggerRowSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/detailTriggerRow.ts'), 'utf8');
|
||||||
const appShellSource = readFileSync(resolve(process.cwd(), 'src/v2/layout/AppShell.tsx'), 'utf8');
|
const appShellSource = readFileSync(resolve(process.cwd(), 'src/v2/layout/AppShell.tsx'), 'utf8');
|
||||||
|
const vehicleActionsSource = readFileSync(resolve(process.cwd(), 'src/v2/pages/VehicleActions.tsx'), 'utf8');
|
||||||
const authGateSource = readFileSync(resolve(process.cwd(), 'src/v2/auth/AuthGate.tsx'), 'utf8');
|
const authGateSource = readFileSync(resolve(process.cwd(), 'src/v2/auth/AuthGate.tsx'), 'utf8');
|
||||||
const routeBoundarySource = readFileSync(resolve(process.cwd(), 'src/v2/routing/RouteBoundary.tsx'), 'utf8');
|
const routeBoundarySource = readFileSync(resolve(process.cwd(), 'src/v2/routing/RouteBoundary.tsx'), 'utf8');
|
||||||
const fleetMapSource = readFileSync(resolve(process.cwd(), 'src/v2/map/FleetMap.tsx'), 'utf8');
|
const fleetMapSource = readFileSync(resolve(process.cwd(), 'src/v2/map/FleetMap.tsx'), 'utf8');
|
||||||
@@ -581,6 +582,13 @@ describe('V2 production entry', () => {
|
|||||||
expect(appShellSource).toContain('<Layout className="v2-shell">');
|
expect(appShellSource).toContain('<Layout className="v2-shell">');
|
||||||
expect(appShellSource).toContain('<Nav className="v2-navigation"');
|
expect(appShellSource).toContain('<Nav className="v2-navigation"');
|
||||||
expect(appShellSource).toContain('className="v2-mobile-more-sidesheet"');
|
expect(appShellSource).toContain('className="v2-mobile-more-sidesheet"');
|
||||||
|
expect(appShellSource.match(/<WorkspaceSideSheet/g)).toHaveLength(2);
|
||||||
|
expect(appShellSource).not.toContain('<SideSheet');
|
||||||
|
expect(appShellSource).not.toContain('useSideSheetA11y');
|
||||||
|
expect(vehicleActionsSource).toContain('<WorkspaceSideSheet');
|
||||||
|
expect(vehicleActionsSource).toContain('variant="action"');
|
||||||
|
expect(vehicleActionsSource).not.toContain('<SideSheet');
|
||||||
|
expect(vehicleActionsSource).not.toContain('useSideSheetA11y');
|
||||||
expect(appShellSource).toContain('className="v2-password-modal"');
|
expect(appShellSource).toContain('className="v2-password-modal"');
|
||||||
expect(appShellSource.match(/<PasswordInput/g)).toHaveLength(3);
|
expect(appShellSource.match(/<PasswordInput/g)).toHaveLength(3);
|
||||||
expect(authGateSource.match(/<PasswordInput/g)).toHaveLength(2);
|
expect(authGateSource.match(/<PasswordInput/g)).toHaveLength(2);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ test('renders one accessible Semi configuration sheet with shared summary and ac
|
|||||||
className="test-config-sheet"
|
className="test-config-sheet"
|
||||||
visible
|
visible
|
||||||
ariaLabel="测试策略配置"
|
ariaLabel="测试策略配置"
|
||||||
|
dialogId="test-strategy-sheet"
|
||||||
title="策略设置"
|
title="策略设置"
|
||||||
description="统一维护优先级与启停状态"
|
description="统一维护优先级与启停状态"
|
||||||
badge="2 项已启用"
|
badge="2 项已启用"
|
||||||
@@ -28,6 +29,7 @@ test('renders one accessible Semi configuration sheet with shared summary and ac
|
|||||||
</WorkspaceSideSheet>);
|
</WorkspaceSideSheet>);
|
||||||
|
|
||||||
const dialog = screen.getByRole('dialog', { name: '测试策略配置' });
|
const dialog = screen.getByRole('dialog', { name: '测试策略配置' });
|
||||||
|
expect(dialog).toHaveAttribute('id', 'test-strategy-sheet');
|
||||||
expect(dialog.closest('.semi-sidesheet')).toHaveClass('v2-workspace-config-sidesheet', 'test-config-sheet');
|
expect(dialog.closest('.semi-sidesheet')).toHaveClass('v2-workspace-config-sidesheet', 'test-config-sheet');
|
||||||
expect(screen.getByText('统一维护优先级与启停状态')).toBeInTheDocument();
|
expect(screen.getByText('统一维护优先级与启停状态')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('list', { name: '测试策略配置摘要' })).toHaveTextContent('当前首选来源 A健康状态正常实时上报');
|
expect(screen.getByRole('list', { name: '测试策略配置摘要' })).toHaveTextContent('当前首选来源 A健康状态正常实时上报');
|
||||||
@@ -39,7 +41,7 @@ test('renders one accessible Semi configuration sheet with shared summary and ac
|
|||||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('exposes task and editor variants without changing the accessible dialog contract', () => {
|
test('exposes task, editor, help and action variants without changing the accessible dialog contract', () => {
|
||||||
const view = render(<>
|
const view = render(<>
|
||||||
<WorkspaceSideSheet
|
<WorkspaceSideSheet
|
||||||
visible
|
visible
|
||||||
@@ -61,12 +63,38 @@ test('exposes task and editor variants without changing the accessible dialog co
|
|||||||
>
|
>
|
||||||
<span>编辑内容</span>
|
<span>编辑内容</span>
|
||||||
</WorkspaceSideSheet>
|
</WorkspaceSideSheet>
|
||||||
|
<WorkspaceSideSheet
|
||||||
|
visible
|
||||||
|
variant="help"
|
||||||
|
ariaLabel="页面帮助"
|
||||||
|
title="帮助"
|
||||||
|
description="查看当前模块说明"
|
||||||
|
icon={<span data-testid="help-icon">?</span>}
|
||||||
|
onCancel={() => undefined}
|
||||||
|
>
|
||||||
|
<span>帮助内容</span>
|
||||||
|
</WorkspaceSideSheet>
|
||||||
|
<WorkspaceSideSheet
|
||||||
|
visible
|
||||||
|
variant="action"
|
||||||
|
ariaLabel="快捷操作"
|
||||||
|
title="操作"
|
||||||
|
description="选择下一步"
|
||||||
|
onCancel={() => undefined}
|
||||||
|
>
|
||||||
|
<span>操作内容</span>
|
||||||
|
</WorkspaceSideSheet>
|
||||||
</>);
|
</>);
|
||||||
|
|
||||||
expect(screen.getByRole('dialog', { name: '后台任务' }).closest('.semi-sidesheet')).toHaveClass('v2-workspace-task-sidesheet');
|
expect(screen.getByRole('dialog', { name: '后台任务' }).closest('.semi-sidesheet')).toHaveClass('v2-workspace-task-sidesheet');
|
||||||
expect(screen.getByRole('dialog', { name: '策略编辑' }).closest('.semi-sidesheet')).toHaveClass('v2-workspace-editor-sidesheet');
|
expect(screen.getByRole('dialog', { name: '策略编辑' }).closest('.semi-sidesheet')).toHaveClass('v2-workspace-editor-sidesheet');
|
||||||
|
expect(screen.getByRole('dialog', { name: '页面帮助' }).closest('.semi-sidesheet')).toHaveClass('v2-workspace-help-sidesheet');
|
||||||
|
expect(screen.getByRole('dialog', { name: '快捷操作' }).closest('.semi-sidesheet')).toHaveClass('v2-workspace-action-sidesheet');
|
||||||
expect(screen.getByText('任务内容')).toBeInTheDocument();
|
expect(screen.getByText('任务内容')).toBeInTheDocument();
|
||||||
expect(screen.getByText('编辑内容')).toBeInTheDocument();
|
expect(screen.getByText('编辑内容')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('帮助内容')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('操作内容')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('help-icon').closest('.v2-workspace-config-title-icon')).toHaveAttribute('aria-hidden', 'true');
|
||||||
expect(view.baseElement).toHaveTextContent('查看生成进度');
|
expect(view.baseElement).toHaveTextContent('查看生成进度');
|
||||||
expect(view.baseElement).toHaveTextContent('保存后生效');
|
expect(view.baseElement).toHaveTextContent('保存后生效');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,12 +21,14 @@ export type WorkspaceSideSheetBadgeColor = 'blue' | 'green' | 'orange' | 'red' |
|
|||||||
|
|
||||||
export type WorkspaceSideSheetProps = {
|
export type WorkspaceSideSheetProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
variant?: 'config' | 'detail' | 'task' | 'editor';
|
variant?: 'config' | 'detail' | 'task' | 'editor' | 'help' | 'action';
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
ariaLabel: string;
|
ariaLabel: string;
|
||||||
closeLabel?: string;
|
closeLabel?: string;
|
||||||
|
dialogId?: string;
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
description: ReactNode;
|
description: ReactNode;
|
||||||
|
icon?: ReactNode;
|
||||||
badge?: ReactNode;
|
badge?: ReactNode;
|
||||||
badgeColor?: WorkspaceSideSheetBadgeColor;
|
badgeColor?: WorkspaceSideSheetBadgeColor;
|
||||||
placement?: 'left' | 'right' | 'top' | 'bottom';
|
placement?: 'left' | 'right' | 'top' | 'bottom';
|
||||||
@@ -46,8 +48,10 @@ export function WorkspaceSideSheet({
|
|||||||
visible,
|
visible,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
closeLabel = `关闭${ariaLabel}`,
|
closeLabel = `关闭${ariaLabel}`,
|
||||||
|
dialogId,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
icon,
|
||||||
badge,
|
badge,
|
||||||
badgeColor = 'blue',
|
badgeColor = 'blue',
|
||||||
placement = 'right',
|
placement = 'right',
|
||||||
@@ -69,12 +73,13 @@ export function WorkspaceSideSheet({
|
|||||||
const apply = () => {
|
const apply = () => {
|
||||||
const dialog = document.querySelector(`[data-workspace-sheet-id="${sheetId}"] .semi-sidesheet-inner`);
|
const dialog = document.querySelector(`[data-workspace-sheet-id="${sheetId}"] .semi-sidesheet-inner`);
|
||||||
dialog?.setAttribute('aria-label', ariaLabel);
|
dialog?.setAttribute('aria-label', ariaLabel);
|
||||||
|
if (dialogId) dialog?.setAttribute('id', dialogId);
|
||||||
dialog?.querySelector('.semi-sidesheet-close')?.setAttribute('aria-label', closeLabel);
|
dialog?.querySelector('.semi-sidesheet-close')?.setAttribute('aria-label', closeLabel);
|
||||||
};
|
};
|
||||||
apply();
|
apply();
|
||||||
const frame = window.requestAnimationFrame(apply);
|
const frame = window.requestAnimationFrame(apply);
|
||||||
return () => window.cancelAnimationFrame(frame);
|
return () => window.cancelAnimationFrame(frame);
|
||||||
}, [ariaLabel, closeLabel, sheetId, visible]);
|
}, [ariaLabel, closeLabel, dialogId, sheetId, visible]);
|
||||||
|
|
||||||
return <SideSheet
|
return <SideSheet
|
||||||
className={sheetClassName}
|
className={sheetClassName}
|
||||||
@@ -85,6 +90,7 @@ export function WorkspaceSideSheet({
|
|||||||
width={placement === 'left' || placement === 'right' ? width : undefined}
|
width={placement === 'left' || placement === 'right' ? width : undefined}
|
||||||
height={placement === 'top' || placement === 'bottom' ? height : undefined}
|
height={placement === 'top' || placement === 'bottom' ? height : undefined}
|
||||||
title={<div className="v2-workspace-config-title">
|
title={<div className="v2-workspace-config-title">
|
||||||
|
{icon ? <i className="v2-workspace-config-title-icon" aria-hidden="true">{icon}</i> : null}
|
||||||
<span><strong>{title}</strong><small>{description}</small></span>
|
<span><strong>{title}</strong><small>{description}</small></span>
|
||||||
{badge ? <Tag color={badgeColor} type="light" size="small">{badge}</Tag> : null}
|
{badge ? <Tag color={badgeColor} type="light" size="small">{badge}</Tag> : null}
|
||||||
</div>}
|
</div>}
|
||||||
|
|||||||
@@ -21777,3 +21777,197 @@
|
|||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Global help and action workbenches.
|
||||||
|
* These high-frequency shell surfaces now use the same Semi UI identity,
|
||||||
|
* scrolling and footer hierarchy as configuration, evidence and editor sheets.
|
||||||
|
*/
|
||||||
|
:is(.v2-workspace-help-sidesheet, .v2-workspace-action-sidesheet) .semi-sidesheet-inner {
|
||||||
|
overflow: hidden;
|
||||||
|
border-left: 1px solid #d9e3ef;
|
||||||
|
background: #f4f7fb;
|
||||||
|
box-shadow: -22px 0 64px rgba(24, 43, 68, .16);
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.v2-workspace-help-sidesheet, .v2-workspace-action-sidesheet) .semi-sidesheet-header {
|
||||||
|
min-height: 78px;
|
||||||
|
border-bottom: 1px solid #dfe7f0;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 10% 0%, rgba(18, 104, 243, .08), transparent 36%),
|
||||||
|
linear-gradient(180deg, #fff 0%, #fbfcfe 100%);
|
||||||
|
padding: 13px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.v2-workspace-help-sidesheet, .v2-workspace-action-sidesheet) .semi-sidesheet-title {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-config-title-icon {
|
||||||
|
display: grid;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid #d7e5f8;
|
||||||
|
border-radius: 11px;
|
||||||
|
background: linear-gradient(145deg, #f2f7ff 0%, #e8f1ff 100%);
|
||||||
|
color: #1268f3;
|
||||||
|
font-size: 18px;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .9);
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.v2-workspace-help-sidesheet, .v2-workspace-action-sidesheet) .semi-sidesheet-close {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.v2-workspace-help-sidesheet, .v2-workspace-action-sidesheet) .semi-sidesheet-body {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f4f7fb;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.v2-workspace-help-sidesheet, .v2-workspace-action-sidesheet) .v2-workspace-config-content {
|
||||||
|
padding: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.v2-workspace-help-sidesheet, .v2-workspace-action-sidesheet) .semi-sidesheet-footer {
|
||||||
|
min-height: 64px;
|
||||||
|
border-top: 1px solid #e1e8f1;
|
||||||
|
background: rgba(255, 255, 255, .97);
|
||||||
|
padding: 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-help-sidesheet .v2-workspace-config-summary {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-help-sidesheet .v2-workspace-config-summary strong {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-help-sidesheet .v2-help-panel {
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-help-sidesheet .v2-help-overview.semi-card {
|
||||||
|
min-height: 112px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-action-sidesheet .v2-workspace-config-title strong {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-action-sidesheet .v2-workspace-config-title > .semi-tag {
|
||||||
|
color: #1268f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-mobile-more-sidesheet .v2-workspace-config-content {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-mobile-more-sidesheet .semi-sidesheet-title > .v2-workspace-config-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-mobile-more-sidesheet .v2-mobile-more-groups {
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-vehicle-mobile-actions-sidesheet .v2-workspace-config-content {
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
:is(.v2-workspace-help-sidesheet, .v2-workspace-action-sidesheet).semi-sidesheet-bottom .semi-sidesheet-inner {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #d8e2ed;
|
||||||
|
border-bottom: 0;
|
||||||
|
border-radius: 18px 18px 0 0;
|
||||||
|
background: #f4f7fb;
|
||||||
|
box-shadow: 0 -18px 48px rgba(23, 43, 68, .18);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.v2-workspace-help-sidesheet, .v2-workspace-action-sidesheet).semi-sidesheet-bottom .semi-sidesheet-header {
|
||||||
|
min-height: 70px;
|
||||||
|
padding: 10px 12px 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.v2-workspace-help-sidesheet, .v2-workspace-action-sidesheet) .v2-workspace-config-title {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.v2-workspace-help-sidesheet, .v2-workspace-action-sidesheet) .v2-workspace-config-title-icon {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.v2-workspace-help-sidesheet, .v2-workspace-action-sidesheet) .v2-workspace-config-title strong {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.v2-workspace-help-sidesheet, .v2-workspace-action-sidesheet) .v2-workspace-config-title small {
|
||||||
|
max-width: 210px;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.v2-workspace-help-sidesheet, .v2-workspace-action-sidesheet) .v2-workspace-config-content {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.v2-workspace-help-sidesheet, .v2-workspace-action-sidesheet).semi-sidesheet-bottom .semi-sidesheet-footer {
|
||||||
|
min-height: 58px;
|
||||||
|
padding: 7px 9px calc(7px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-help-sidesheet .v2-workspace-config-summary {
|
||||||
|
min-height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-help-sidesheet .v2-workspace-config-summary > span {
|
||||||
|
padding: 7px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-help-sidesheet .v2-workspace-config-summary strong {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-help-sidesheet .v2-help-overview.semi-card {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-help-sidesheet .v2-help-panel li {
|
||||||
|
min-height: 54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-mobile-more-sidesheet .v2-workspace-config-content {
|
||||||
|
padding: 8px 9px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-vehicle-mobile-actions-sidesheet .v2-workspace-config-content {
|
||||||
|
padding: 9px 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) and (max-height: 480px) and (orientation: landscape) {
|
||||||
|
.v2-workspace-action-sidesheet.v2-vehicle-mobile-actions-sidesheet.semi-sidesheet-bottom .semi-sidesheet-inner {
|
||||||
|
height: min(100dvh, 380px) !important;
|
||||||
|
max-height: 100dvh;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-workspace-action-sidesheet.v2-vehicle-mobile-actions-sidesheet .v2-vehicle-mobile-actions-list {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user