refactor(web-platform): release V0.7.2 项目常规优化
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user