feat(web-platform): release V0.9.4 前端设计优化
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled

This commit is contained in:
2026-09-09 13:15:46 +08:00
parent 13e35be98b
commit cb3fb47561
31 changed files with 563 additions and 182 deletions
@@ -27,7 +27,12 @@ describe('第二批工作台组件', () => {
it('空工作区解释导入到仿真的三步流程', () => {
render(<EmptyWorkspace />);
expect(screen.getByRole('region', { name: '导入模型工程' })).toBeVisible();
fireEvent.click(screen.getByText('导入与仿真流程'));
expect(screen.getByText('Local Simulation Workspace')).toBeVisible();
expect(screen.getByText('本地解析、编译并运行机器人模型,无需上传资源')).toBeVisible();
expect(screen.getByLabelText('支持格式')).toHaveTextContent('OBJ / STL / DAE');
expect(screen.getByRole('button', { name: '选择文件' })).toBeVisible();
expect(screen.getByRole('button', { name: '选择文件夹' })).toBeVisible();
expect(screen.queryByText('导入与仿真流程')).not.toBeInTheDocument();
expect(screen.getByRole('list', { name: '仿真工作流程' })).toHaveTextContent('导入');
expect(screen.getByRole('list', { name: '仿真工作流程' })).toHaveTextContent('检查与配置');
expect(screen.getByRole('list', { name: '仿真工作流程' })).toHaveTextContent('运行与调试');
@@ -18,7 +18,7 @@ export function SidebarPanel({
return (
<ResizablePanel side={side} storageKey={`mujoco-${side}-sidebar-width`} visible={visible}>
<aside
className={`flex h-full w-full min-w-0 flex-col overflow-hidden bg-panel ${side === 'left' ? 'border-r' : 'border-l'} border-border-subtle `}
className={`cyber-sidebar flex h-full w-full min-w-0 flex-col overflow-hidden ${side === 'left' ? 'border-r' : 'border-l'} border-border-subtle `}
>
<h2 className="flex h-9 shrink-0 items-center gap-2 border-b border-border-subtle px-3 text-xs font-semibold text-text-primary">
<span className="text-accent [&>svg]:h-4 [&>svg]:w-4">
@@ -146,7 +146,7 @@ export function SourceEditorDialog({
height: 'min(650px, calc(100vh - 32px))',
}
}
className={`source-editor-window pointer-events-auto fixed flex min-h-64 min-w-0 max-w-[calc(100vw-32px)] max-h-[calc(100vh-32px)] flex-col overflow-hidden border border-border-strong bg-panel shadow-2xl ${maximized ? 'inset-0 h-full w-full' : 'resize'}`}
className={`source-editor-window pointer-events-auto fixed flex min-h-64 min-w-0 max-w-[calc(100vw-32px)] max-h-[calc(100vh-32px)] flex-col overflow-hidden border border-border-strong ${maximized ? 'inset-0 h-full w-full' : 'resize'}`}
>
<header
className="flex min-h-11 flex-wrap shrink-0 py-2 cursor-move select-none items-center gap-3 border-b border-border bg-surface px-3"
@@ -9,9 +9,10 @@ export function ViewportHUD() {
return (
<div
aria-label="视口状态"
className="flex flex-wrap items-center gap-2 rounded-lg border border-border bg-panel/90 px-2 py-1 text-xs shadow-sm"
className="engineering-glass flex flex-wrap items-center gap-2 rounded-lg border border-border px-2 py-1 text-xs"
>
<Badge tone={!ready || paused ? 'neutral' : 'success'}>
<span aria-hidden="true" className="hud-signal" data-running={ready && !paused} />
{!ready || paused ? (
<CirclePause className="h-3.5 w-3.5" />
) : (
@@ -2,7 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { WorkbenchHeader } from './WorkbenchHeader';
const fn = () => {};
describe('WorkbenchHeader', () => {
it('保留导入、工程菜单、命令与侧栏入口,不再重复仿真控制', () => {
it('保留导入、源代码直达、命令与侧栏入口,不再重复仿真控制', () => {
const source = vi.fn(),
files = vi.fn(),
left = vi.fn();
@@ -26,8 +26,8 @@ describe('WorkbenchHeader', () => {
});
expect(files).toHaveBeenCalledOnce();
expect(screen.getByLabelText('打开文件夹')).toHaveAttribute('webkitdirectory');
expect(screen.queryByRole('button', { name: '源代码' })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '工程' }));
expect(screen.getByRole('button', { name: '源代码' })).toBeVisible();
expect(screen.queryByRole('button', { name: '工程' })).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '源代码' }));
expect(source).toHaveBeenCalledOnce();
fireEvent.click(screen.getByRole('button', { name: '隐藏工程面板' }));
@@ -1,8 +1,8 @@
import type { ChangeEvent, ReactNode } from 'react';
import { Boxes, Code2, FolderOpen, PanelLeft, PanelRight, Search, Upload } from 'lucide-react';
import { IconButton, Popover } from '../../components/ui';
import { IconButton } from '../../components/ui';
const fileActionClass =
'inline-flex h-8 cursor-pointer items-center gap-1.5 rounded-md border border-border bg-surface/80 px-2 text-xs font-medium text-text-primary shadow-sm transition-[background-color,border-color,transform] hover:-translate-y-px hover:border-border-strong hover:bg-element-hover focus-within:ring-2 focus-within:ring-accent/30';
'cyber-action inline-flex h-8 cursor-pointer items-center gap-1.5 rounded-md border border-border bg-surface/80 px-2 text-xs font-medium text-text-primary shadow-sm transition-[background-color,border-color,transform] hover:-translate-y-px hover:border-border-strong hover:bg-element-hover focus-within:ring-2 focus-within:ring-accent/30';
const fileActionLabelClass = 'whitespace-nowrap';
export function WorkbenchHeader({
loading,
@@ -32,7 +32,7 @@ export function WorkbenchHeader({
onCommands: () => void;
}) {
return (
<header className="workbench-header relative z-40 flex h-11 shrink-0 justify-between items-center gap-2 border-b border-border px-2.5 shadow-[0_1px_0_rgba(255,255,255,0.02)] backdrop-blur-md">
<header className="workbench-header relative z-40 flex h-11 shrink-0 justify-between items-center gap-2 border-b border-border px-2.5 ">
<div className="flex min-w-0 items-center gap-1">
<div className="mr-2 flex items-center gap-2">
<span className="grid h-7 w-7 place-items-center rounded-lg border border-accent/25 bg-accent-soft text-accent shadow-sm">
@@ -76,32 +76,15 @@ export function WorkbenchHeader({
onChange={onFolder}
/>
</label>
<Popover
label="工程菜单"
placement="bottom-left"
trigger={({ toggle }) => (
<button type="button" className={fileActionClass} onClick={toggle}>
工程
</button>
)}
<button
type="button"
className={`${fileActionClass} disabled:pointer-events-none disabled:opacity-40`}
disabled={!hasProject || loading}
onClick={onOpenSource}
>
{({ close }) => (
<div className="w-52 rounded-lg border border-border bg-surface-elevated p-1 shadow-xl">
<button
type="button"
className="flex h-8 w-full items-center gap-2 rounded px-2 text-xs hover:bg-element-hover disabled:opacity-40"
disabled={!hasProject || loading}
onClick={() => {
close();
onOpenSource?.();
}}
>
<Code2 className="h-4 w-4" />
源代码
</button>
</div>
)}
</Popover>
<Code2 aria-hidden="true" className="h-3.5 w-3.5" />
<span className={fileActionLabelClass}>源代码</span>
</button>
</div>
<div className="flex min-w-0 items-center justify-end gap-0.5">
<IconButton
@@ -6,6 +6,8 @@ import {
LoaderCircle,
PlayCircle,
Settings2,
ShieldCheck,
Sparkles,
Upload,
UploadCloud,
} from 'lucide-react';
@@ -39,36 +41,78 @@ export function EmptyWorkspace({ compact = false }: { compact?: boolean }) {
return (
<section
aria-label="导入模型工程"
className="mx-auto w-full max-w-md rounded-lg border border-border bg-panel p-6 text-center"
className="workspace-welcome relative w-full max-w-[600px] overflow-hidden rounded-[22px] border border-border-strong px-6 py-6 text-center sm:px-7"
>
<h2 className="text-base font-semibold">拖放模型工程到此处</h2>
<p className="mt-2 text-xs text-text-tertiary">支持 MJCF/XML、URDF、文件夹和 ZIP</p>
<div className="mt-4 flex flex-wrap justify-center gap-2">
<Button
variant="primary"
icon={<Upload className="h-4 w-4" />}
onClick={() => document.getElementById('mujoco-project-files')?.click()}
<div aria-hidden="true" className="welcome-glow" />
<div className="relative">
<div className="mb-4 flex items-center justify-center gap-2 text-[10px] font-semibold uppercase tracking-[0.18em] text-accent">
<Sparkles className="h-3.5 w-3.5" />
Local Simulation Workspace
</div>
<span className="mx-auto mb-4 grid h-14 w-14 place-items-center rounded-2xl border border-accent/25 bg-accent-soft text-accent tool-active-glow">
<UploadCloud className="h-6 w-6" />
</span>
<h2 className="text-lg font-semibold tracking-tight text-text-primary">
拖放模型工程到此处
</h2>
<p className="mt-1.5 text-xs leading-5 text-text-tertiary">
本地解析、编译并运行机器人模型,无需上传资源
</p>
<div
className="mt-4 flex flex-wrap items-center justify-center gap-1.5"
aria-label="支持格式"
>
选择文件
</Button>
<Button
icon={<FolderOpen className="h-4 w-4" />}
onClick={() => document.getElementById('mujoco-project-folder')?.click()}
{['MJCF / XML', 'URDF', 'ZIP', 'OBJ / STL / DAE'].map((format) => (
<span
key={format}
className="rounded-full border border-border bg-surface/80 px-2.5 py-1 text-[10px] font-medium text-text-secondary"
>
{format}
</span>
))}
</div>
<div className="mt-5 flex flex-wrap items-center justify-center gap-2">
<Button
variant="primary"
icon={<Upload className="h-4 w-4" />}
onClick={() => document.getElementById('mujoco-project-files')?.click()}
>
选择文件
</Button>
<Button
icon={<FolderOpen className="h-4 w-4" />}
onClick={() => document.getElementById('mujoco-project-folder')?.click()}
>
选择文件夹
</Button>
</div>
<ol
aria-label="仿真工作流程"
className="mt-6 hidden grid-cols-3 gap-2 border-t border-border-subtle pt-4 sm:grid"
>
选择文件夹
</Button>
</div>
<details className="domain-details mt-4 text-left">
<summary>导入与仿真流程</summary>
<ol aria-label="仿真工作流程" className="space-y-2 text-xs text-text-secondary">
{workflow.map((item) => (
<li key={item.label}>
{item.label} · {item.detail}
{workflow.map((item, index) => (
<li
key={item.label}
className="engineering-card rounded-xl border border-border/70 px-3 py-3 text-left"
>
<div className="flex items-center gap-2">
<span className="technical-value text-[10px] font-semibold text-accent">
0{index + 1}
</span>
<item.icon aria-hidden="true" className="h-3.5 w-3.5 text-text-secondary" />
<span className="text-xs font-semibold text-text-primary">{item.label}</span>
</div>
<span className="mt-1 block text-[10px] leading-4 text-text-tertiary">
{item.detail}
</span>
</li>
))}
</ol>
</details>
<p className="mt-3 text-xs text-text-tertiary">模型与资源仅在当前浏览器会话中处理</p>
<p className="mt-4 flex items-center justify-center gap-1.5 text-[10px] text-text-tertiary">
<ShieldCheck aria-hidden="true" className="h-3.5 w-3.5 text-success" />
模型与资源仅在当前浏览器会话中处理
</p>
</div>
</section>
);
}
@@ -135,10 +135,14 @@ export function WorkspaceToolsPanel({
keepMounted
badge={<ConsoleSectionBadges count={snapshot.actuators.length} />}
>
{snapshot.actuators.some((actuator) => ['motor', 'position'].includes(actuator.kind)) && (
<p className="mb-2 text-xs text-warning">参数立即作用于模型,可随 MJCF 导出。</p>
)}
{snapshot.actuators.length ? (
snapshot.actuators.map((actuator) => (
<ActuatorControl
key={actuator.id}
showParameterNotice={false}
actuator={actuator}
onControl={(value) => onActuator(actuator.id, value)}
onParameters={(parameters) => onActuatorParameters(actuator.id, parameters)}