refactor(web-platform): release V0.7.2 项目常规优化
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled

This commit is contained in:
2026-09-01 16:39:04 +08:00
parent 89d1c8cb57
commit 8ba83a8d0a
28 changed files with 993 additions and 932 deletions
+298
View File
@@ -0,0 +1,298 @@
import { useState } from 'react';
import { BadgeCheck, CheckCircle2, Mountain, Plus } from 'lucide-react';
import { Button, Select } from '../components/ui';
import {
CERTIFIED_MAP_ASSETS,
MAP_ASSET_DRAG_MIME,
MAP_ASSET_PLACEMENT_MIME,
} from './editor/assetCatalog';
import {
MAP_OBJECT_PLACEMENT_LABELS,
type EditableMapDocument,
type EditableMapObjectType,
type MapObjectPlacementMode,
} from './editor/types';
import {
PHYSICAL_MAP_PRESET_LABELS,
SYSTEM_TERRAIN_PRESETS,
type SystemTerrainPreset,
} from './types';
function AssetPreview({ type, color }: { type: EditableMapObjectType; color: string }) {
const shape =
type === 'cylinder'
? 'h-9 w-9 rounded-full'
: type === 'capsule'
? 'h-10 w-6 rounded-full'
: type === 'ramp'
? 'h-0 w-0 border-b-[34px] border-l-[48px] border-l-transparent'
: type === 'stairs'
? 'h-9 w-11 [clip-path:polygon(0_100%,0_66%,34%_66%,34%_33%,67%_33%,67%_0,100%_0,100%_100%)]'
: 'h-9 w-9 rounded-sm';
return (
<div className="flex h-16 items-center justify-center rounded bg-black/10" aria-hidden="true">
<div
className={`${shape} shadow-[0_6px_14px_rgba(0,0,0,0.22)]`}
style={type === 'ramp' ? { borderBottomColor: color } : { backgroundColor: color }}
/>
</div>
);
}
function TerrainPreview({ preset }: { preset: SystemTerrainPreset }) {
const grid = (
<g stroke="currentColor" strokeWidth="0.45" opacity="0.18">
<path d="M14 50 60 23l46 27-46 27z" />
<path d="m26 57 46-27m-34 34 46-27m-58 6 46 27m-34-34 46 27" />
</g>
);
let shape;
if (preset === 'discrete_obstacles')
shape = (
<g fill="currentColor" fillOpacity="0.16" stroke="currentColor">
<path d="m31 48 9-5 9 5-9 5zM40 53V39l9-5v14M40 39l-9-5v14" />
<path d="m55 39 8-5 8 5-8 5zM63 44V27l8-5v17M63 27l-8-5v17" />
<path d="m77 51 10-6 10 6-10 6zM87 57V38l10-6v19M87 38l-10-6v19" />
</g>
);
else if (preset === 'gap')
shape = (
<g fill="currentColor" fillOpacity="0.12" stroke="currentColor">
<path d="m21 48 31-18 15 9-31 18zM69 40l15 9 17-10-15-9z" />
<path d="m36 57 31-18v8L36 65zM69 40l17-10v8L69 48z" fillOpacity="0.24" />
</g>
);
else if (preset === 'inverted_pyramid_stairs' || preset === 'pit')
shape = (
<g fill="currentColor" fillOpacity="0.08" stroke="currentColor">
<path d="m22 47 38-22 38 22-38 22z" />
<path d="m32 47 28-16 28 16-28 16z" />
<path d="m43 47 17-10 17 10-17 10z" fillOpacity="0.2" />
{preset === 'inverted_pyramid_stairs' && (
<path d="M32 47v7m11-7v10m17-10v16m17-16v10m11-10v7" />
)}
</g>
);
else if (preset === 'pyramid_stairs')
shape = (
<g fill="currentColor" stroke="currentColor" strokeLinejoin="round">
<path d="m25 52 12 7 52-30-12-7z" fillOpacity="0.1" />
<path d="m37 45 12 7 40-23-12-7z" fillOpacity="0.17" />
<path d="m49 38 12 7 28-16-12-7z" fillOpacity="0.24" />
<path d="m61 31 12 7 16-9-12-7z" fillOpacity="0.32" />
</g>
);
else if (preset === 'rails')
shape = (
<g fill="none" stroke="currentColor" strokeLinecap="round">
<path d="m26 58 53-31m15 40 12-7" strokeWidth="2.3" opacity="0.25" />
<path d="m26 58 53-31m15 40 12-7M33 58V45m14 5V37m30 12V27m20 39V53" strokeWidth="1.2" />
</g>
);
else if (preset === 'rough')
shape = (
<path
d="m20 54 10-18 9 9 10-22 11 17 10-13 10 20 10-8 10 15-40 18z"
fill="currentColor"
fillOpacity="0.16"
stroke="currentColor"
strokeLinejoin="round"
/>
);
else if (preset === 'stepping_stones')
shape = (
<g fill="currentColor" fillOpacity="0.2" stroke="currentColor">
<path d="m27 52 10-6 10 6-10 6zM46 39l10-6 10 6-10 6zM66 54l10-6 10 6-10 6zM83 36l10-6 10 6-10 6z" />
</g>
);
else
shape = (
<g fill="none" stroke="currentColor" strokeLinecap="round">
<path d="M19 48c10-17 20-17 30 0s20 17 30 0 20-17 29-2" strokeWidth="5" opacity="0.16" />
<path d="M19 48c10-17 20-17 30 0s20 17 30 0 20-17 29-2" strokeWidth="1.4" />
<path d="M25 59c10-17 20-17 30 0s20 17 30 0 15-13 23-7" opacity="0.55" />
</g>
);
return (
<svg viewBox="0 0 120 80" className="h-[76px] w-full text-accent" aria-hidden="true">
{grid}
{shape}
</svg>
);
}
function AddAction({ label }: { label: string }) {
return (
<span className="inline-flex items-center gap-1 text-[11px] font-medium text-accent">
{label}
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-accent text-white">
<Plus className="h-3.5 w-3.5" aria-hidden="true" />
</span>
</span>
);
}
export function MapAssetLibrary({
disabled,
terrainSize,
document,
onAdd,
onSelectTerrain,
onSelectObject,
}: {
disabled: boolean;
terrainSize: number;
document?: EditableMapDocument | null;
onAdd: (
type: EditableMapObjectType,
placementMode: MapObjectPlacementMode,
) => void | Promise<void>;
onSelectTerrain: (preset: SystemTerrainPreset) => void;
onSelectObject?: (id: string) => void;
}) {
const [placementMode, setPlacementMode] = useState<MapObjectPlacementMode>('auto_ground');
return (
<section aria-label="认证资产" className="space-y-3 p-3">
<div className="rounded-lg border border-border-subtle bg-panel-muted/50 p-2.5">
<div className="flex items-center justify-between text-xs font-medium text-text-primary">
<span>场景结构</span>
<span className="technical-value text-[10px] text-text-tertiary">
{document?.objects.length ?? 0} 个对象
</span>
</div>
{document?.objects.length ? (
<div className="mt-2 max-h-36 space-y-0.5 overflow-auto panel-scroll">
{document.objects.map((object) => (
<button
key={object.id}
type="button"
className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-[11px] text-text-secondary hover:bg-element-hover hover:text-text-primary"
onClick={() => onSelectObject?.(object.id)}
>
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-accent" />
<span className="min-w-0 flex-1 truncate">{object.name}</span>
<span className="text-[9px] text-text-tertiary">{object.type}</span>
</button>
))}
</div>
) : (
<p className="mt-2 text-[10px] leading-relaxed text-text-tertiary">
选择工程地图后,这里显示场景对象层级;可从下方资产库创建场景。
</p>
)}
</div>
<div className="flex items-end justify-between gap-3">
<div className="min-w-0">
<div className="flex items-center gap-1.5 text-xs font-medium text-text-primary">
<BadgeCheck className="h-3.5 w-3.5 text-accent" aria-hidden="true" />
认证资产
</div>
<p className="mt-1 text-[10px] leading-relaxed text-text-tertiary">
点击添加,或按住资产拖到画布落位。
</p>
</div>
<label className="w-36 shrink-0 text-[10px] text-text-secondary">
放置方式
<Select
aria-label="新增资产放置方式"
className="mt-1 w-full"
value={placementMode}
disabled={disabled}
onChange={(event) => setPlacementMode(event.target.value as MapObjectPlacementMode)}
>
{(Object.keys(MAP_OBJECT_PLACEMENT_LABELS) as MapObjectPlacementMode[]).map((mode) => (
<option key={mode} value={mode}>
{MAP_OBJECT_PLACEMENT_LABELS[mode]}
</option>
))}
</Select>
</label>
</div>
<div className="grid grid-cols-2 gap-2">
{CERTIFIED_MAP_ASSETS.map((asset) => (
<article
key={asset.type}
draggable={!disabled}
data-map-asset={asset.type}
className={`overflow-hidden rounded-lg border border-border-subtle bg-panel-muted shadow-sm transition-colors ${disabled ? 'opacity-50' : 'cursor-grab hover:border-accent active:cursor-grabbing'}`}
onDragStart={(event) => {
if (disabled) {
event.preventDefault();
return;
}
event.dataTransfer.effectAllowed = 'copy';
event.dataTransfer.setData(MAP_ASSET_DRAG_MIME, asset.type);
event.dataTransfer.setData(MAP_ASSET_PLACEMENT_MIME, placementMode);
event.dataTransfer.setData('text/plain', asset.name);
}}
>
<div className="p-2 pb-0">
<AssetPreview type={asset.type} color={asset.color} />
</div>
<div className="border-t border-border-subtle p-2">
<div className="truncate text-[11px] font-semibold text-text-primary">
{asset.name}
</div>
<div className="mt-0.5 truncate text-[9px] text-text-tertiary">
{asset.description}
</div>
<Button
className="mt-2 w-full"
disabled={disabled}
aria-label={`添加${asset.name}`}
onClick={() => void onAdd(asset.type, placementMode)}
>
<Plus className="mr-1 h-3 w-3" aria-hidden="true" />
添加
</Button>
</div>
</article>
))}
</div>
<div className="border-t border-border-subtle pt-3">
<div className="flex items-center gap-1.5 text-xs font-medium text-text-primary">
<Mountain className="h-3.5 w-3.5 text-accent" aria-hidden="true" />
系统参数化地形
</div>
<p className="mt-1 text-[10px] leading-relaxed text-text-tertiary">
本地确定性生成;点击“添加”后在右侧“地图”调整尺寸、难度和随机种子。
</p>
</div>
<div className="grid grid-cols-2 gap-2">
{SYSTEM_TERRAIN_PRESETS.map((preset) => (
<article
key={preset}
data-system-terrain={preset}
className={`group relative overflow-hidden rounded-lg border border-border-subtle bg-panel-muted shadow-sm transition-colors ${disabled ? 'opacity-50' : 'hover:border-accent'}`}
>
<div className="absolute right-2 top-2 z-10 rounded-full bg-panel-bg/90 p-0.5 text-success">
<CheckCircle2 className="h-3.5 w-3.5" aria-hidden="true" />
</div>
<div className="px-1 pt-1">
<TerrainPreview preset={preset} />
</div>
<div className="border-t border-border-subtle px-2 pb-2 pt-1.5">
<div className="truncate text-[11px] font-semibold text-text-primary">
{PHYSICAL_MAP_PRESET_LABELS[preset]}
</div>
<div className="mt-0.5 text-[9px] text-text-tertiary">
{terrainSize.toFixed(2)} × {terrainSize.toFixed(2)} m
</div>
<button
type="button"
className="mt-1 flex w-full justify-end rounded outline-none focus-visible:ring-2 focus-visible:ring-accent/30"
disabled={disabled}
aria-label={`添加${PHYSICAL_MAP_PRESET_LABELS[preset]}`}
onClick={() => onSelectTerrain(preset)}
>
<AddAction label="添加" />
</button>
</div>
</article>
))}
</div>
</section>
);
}