feat(web-platform): release V0.8.3 常规界面优化
web-platform-release / Build and publish release (push) Has been cancelled
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
web-platform-release / Build and publish release (push) Has been cancelled
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:
@@ -0,0 +1,202 @@
|
||||
import {
|
||||
ArrowDownToLine,
|
||||
Check,
|
||||
Grid3X3,
|
||||
LockKeyhole,
|
||||
MapPinned,
|
||||
Move3d,
|
||||
Rotate3d,
|
||||
Scaling,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Select,
|
||||
ToolbarToggleGroup,
|
||||
type ToolbarItem,
|
||||
} from '../../components/ui';
|
||||
import {
|
||||
MAP_OBJECT_PLACEMENT_LABELS,
|
||||
type MapEditorTransformMode,
|
||||
type MapObjectPlacementMode,
|
||||
} from '../../map/editor/types';
|
||||
|
||||
const TRANSFORM_TOOLS: ToolbarItem<MapEditorTransformMode>[] = [
|
||||
{ value: 'translate', label: '移动工具 W', icon: Move3d },
|
||||
{ value: 'rotate', label: '旋转工具 E', icon: Rotate3d },
|
||||
{ value: 'scale', label: '缩放工具 R', icon: Scaling },
|
||||
];
|
||||
|
||||
export function MapViewportToolbar({
|
||||
visible,
|
||||
interactionActive,
|
||||
mode,
|
||||
snapping,
|
||||
placementMode,
|
||||
hasSelectedObject,
|
||||
allowScale,
|
||||
loading,
|
||||
onActivate,
|
||||
onModeChange,
|
||||
onSnappingChange,
|
||||
onPlacementModeChange,
|
||||
onAlignToSurface,
|
||||
}: {
|
||||
visible: boolean;
|
||||
interactionActive: boolean;
|
||||
mode: MapEditorTransformMode;
|
||||
snapping: boolean;
|
||||
placementMode: MapObjectPlacementMode;
|
||||
hasSelectedObject: boolean;
|
||||
allowScale: boolean;
|
||||
loading: boolean;
|
||||
onActivate: () => void;
|
||||
onModeChange: (mode: MapEditorTransformMode) => void;
|
||||
onSnappingChange: (value: boolean) => void;
|
||||
onPlacementModeChange: (mode: MapObjectPlacementMode) => void;
|
||||
onAlignToSurface: () => void;
|
||||
}) {
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<div
|
||||
aria-label="地图视口工具"
|
||||
className="engineering-glass map-tool-enter absolute left-1/2 top-3 z-20 flex max-w-[calc(100%-24px)] -translate-x-1/2 items-center gap-1.5 rounded-xl border p-1.5"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="激活地图编辑"
|
||||
aria-pressed={interactionActive}
|
||||
disabled={loading}
|
||||
onClick={onActivate}
|
||||
className={`hidden h-8 items-center gap-1.5 rounded-lg px-2 text-[10px] font-semibold transition-colors sm:flex ${interactionActive ? 'bg-accent-soft text-accent' : 'bg-surface/80 text-text-secondary hover:bg-element-hover'}`}
|
||||
>
|
||||
<MapPinned className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
地图编辑
|
||||
</button>
|
||||
<ToolbarToggleGroup
|
||||
items={TRANSFORM_TOOLS.filter((item) => allowScale || item.value !== 'scale')}
|
||||
value={mode}
|
||||
onChange={onModeChange}
|
||||
label="地图变换模式"
|
||||
/>
|
||||
<IconButton
|
||||
active={snapping}
|
||||
tooltip={snapping ? '关闭网格吸附(当前 0.1 m / 5°)' : '开启网格吸附'}
|
||||
aria-label="网格吸附"
|
||||
aria-pressed={snapping}
|
||||
disabled={loading}
|
||||
onClick={() => onSnappingChange(!snapping)}
|
||||
>
|
||||
<Grid3X3 className="h-3.5 w-3.5" />
|
||||
</IconButton>
|
||||
<span aria-hidden="true" className="mx-0.5 h-5 w-px bg-border" />
|
||||
<label className="flex items-center gap-1 text-[10px] text-text-tertiary">
|
||||
<span className="hidden lg:inline">贴地</span>
|
||||
<Select
|
||||
aria-label="贴地检测模式"
|
||||
className="w-[112px]"
|
||||
value={placementMode}
|
||||
disabled={loading}
|
||||
onChange={(event) => onPlacementModeChange(event.target.value as MapObjectPlacementMode)}
|
||||
>
|
||||
{(Object.keys(MAP_OBJECT_PLACEMENT_LABELS) as MapObjectPlacementMode[]).map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value === 'auto_ground' ? '地面 z=0' : value === 'gravity' ? '表面检测' : '锁定位姿'}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</label>
|
||||
<IconButton
|
||||
tooltip={hasSelectedObject ? '重新检测并贴合承载面' : '选中地图物体后可贴合表面'}
|
||||
aria-label="立即贴合承载面"
|
||||
disabled={loading || !hasSelectedObject || placementMode === 'locked'}
|
||||
onClick={onAlignToSurface}
|
||||
>
|
||||
{placementMode === 'locked' ? (
|
||||
<LockKeyhole className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<ArrowDownToLine className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MapDraftStatusOverlay({
|
||||
visible,
|
||||
changeCount,
|
||||
loading,
|
||||
onCommit,
|
||||
onDiscard,
|
||||
}: {
|
||||
visible: boolean;
|
||||
changeCount: number;
|
||||
loading: boolean;
|
||||
onCommit: () => void;
|
||||
onDiscard: () => void;
|
||||
}) {
|
||||
if (!visible) return null;
|
||||
const dirty = changeCount > 0;
|
||||
return (
|
||||
<div
|
||||
aria-label="地图草稿状态"
|
||||
role="status"
|
||||
className="engineering-glass absolute bottom-3 left-1/2 z-20 flex max-w-[calc(100%-24px)] -translate-x-1/2 items-center gap-2 rounded-xl border px-2 py-1.5"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`h-2 w-2 shrink-0 rounded-full ${dirty ? 'draft-dirty-dot bg-warning' : 'bg-success'}`}
|
||||
/>
|
||||
<span className="min-w-0 whitespace-nowrap text-[10px] font-medium text-text-secondary sm:text-[11px]">
|
||||
{dirty ? `${changeCount} 项未保存改动` : '地图草稿已同步'}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
aria-label="丢弃地图草稿"
|
||||
disabled={loading || !dirty}
|
||||
icon={<Trash2 className="h-3 w-3" />}
|
||||
onClick={onDiscard}
|
||||
>
|
||||
<span className="hidden sm:inline">丢弃</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
aria-label="提交地图草稿"
|
||||
disabled={loading || !dirty}
|
||||
icon={<Check className="h-3 w-3" />}
|
||||
onClick={onCommit}
|
||||
>
|
||||
<span className="hidden sm:inline">提交</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface MapAssetDropTarget {
|
||||
left: number;
|
||||
top: number;
|
||||
position: readonly [number, number, number] | null;
|
||||
}
|
||||
|
||||
export function MapAssetDropIndicator({ target }: { target?: MapAssetDropTarget }) {
|
||||
if (!target) return null;
|
||||
const valid = Boolean(target.position);
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-30" aria-label="地图资产放置预览">
|
||||
<div
|
||||
className={`absolute -translate-x-1/2 -translate-y-1/2 rounded-full border-2 p-1 shadow-2xl ${valid ? 'border-accent bg-accent/25 text-accent' : 'border-danger bg-danger/20 text-danger'}`}
|
||||
style={{ left: target.left, top: target.top }}
|
||||
>
|
||||
<span className="grid h-8 w-8 place-items-center rounded-full border border-current bg-panel/85 backdrop-blur">
|
||||
<MapPinned className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="absolute left-1/2 top-full mt-2 -translate-x-1/2 whitespace-nowrap rounded-lg border border-border-strong bg-panel/90 px-2 py-1 text-[10px] font-medium shadow-xl backdrop-blur">
|
||||
{valid
|
||||
? `释放放置 · ${target.position![0].toFixed(1)}, ${target.position![1].toFixed(1)}`
|
||||
: '请拖到 3D 地面'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user