diff --git a/services/pascal-editor/packages/editor/src/components/ui/action-menu/camera-actions.tsx b/services/pascal-editor/packages/editor/src/components/ui/action-menu/camera-actions.tsx index 5455095f8..a30dc7687 100644 --- a/services/pascal-editor/packages/editor/src/components/ui/action-menu/camera-actions.tsx +++ b/services/pascal-editor/packages/editor/src/components/ui/action-menu/camera-actions.tsx @@ -2,6 +2,7 @@ import { emitter } from '@pascal-app/core' import Image from 'next/image' +import { withEditorBasePath } from '../../../lib/public-asset-path' import useEditor from '../../../store/use-editor' import { ActionButton } from './action-button' @@ -38,7 +39,7 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) { alt="Orbit Left" className="h-[28px] w-[28px] -scale-x-100 object-contain opacity-70 transition-opacity group-hover:opacity-100" height={28} - src="/icons/rotate.webp" + src={withEditorBasePath('/icons/rotate.webp')} width={28} /> @@ -55,7 +56,7 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) { alt="Orbit Right" className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100" height={28} - src="/icons/rotate.webp" + src={withEditorBasePath('/icons/rotate.webp')} width={28} /> @@ -75,7 +76,7 @@ export function CameraActions({ hideOrbit = false }: { hideOrbit?: boolean }) { alt="Top View" className="h-[28px] w-[28px] object-contain opacity-70 transition-opacity group-hover:opacity-100" height={28} - src="/icons/topview.webp" + src={withEditorBasePath('/icons/topview.webp')} width={28} /> diff --git a/services/pascal-editor/packages/editor/src/components/ui/action-menu/control-modes.tsx b/services/pascal-editor/packages/editor/src/components/ui/action-menu/control-modes.tsx index 6ffb9f606..12db51c7f 100644 --- a/services/pascal-editor/packages/editor/src/components/ui/action-menu/control-modes.tsx +++ b/services/pascal-editor/packages/editor/src/components/ui/action-menu/control-modes.tsx @@ -3,6 +3,7 @@ import { Icon } from '@iconify/react' import { type LucideIcon, Trash2 } from 'lucide-react' import Image from 'next/image' +import { withEditorBasePath } from './../../../lib/public-asset-path' import { cn } from './../../../lib/utils' import useEditor from './../../../store/use-editor' import { ActionButton } from './action-button' @@ -24,7 +25,7 @@ type ControlConfig = { const controls: ControlConfig[] = [ { id: 'select', - imageSrc: '/icons/select.webp', + imageSrc: withEditorBasePath('/icons/select.webp'), label: '选择', shortcut: 'V', color: 'hover:bg-blue-500/20 hover:text-blue-400', @@ -32,7 +33,7 @@ const controls: ControlConfig[] = [ }, { id: 'zone', - imageSrc: '/icons/zone.webp', + imageSrc: withEditorBasePath('/icons/zone.webp'), label: '区域', shortcut: 'Z', color: 'hover:bg-green-500/20 hover:text-green-400', diff --git a/services/pascal-editor/packages/editor/src/components/ui/action-menu/view-toggles.tsx b/services/pascal-editor/packages/editor/src/components/ui/action-menu/view-toggles.tsx index 4bfbc1be7..420cf53e7 100644 --- a/services/pascal-editor/packages/editor/src/components/ui/action-menu/view-toggles.tsx +++ b/services/pascal-editor/packages/editor/src/components/ui/action-menu/view-toggles.tsx @@ -14,6 +14,7 @@ import { useCallback, useRef, useState } from 'react' import { useShallow } from 'zustand/react/shallow' import { getLevelDisplayName } from '@pascal-app/core' import { createLocalGuideImage } from '../../../lib/local-guide-image' +import { withEditorBasePath } from '../../../lib/public-asset-path' import { cn } from '../../../lib/utils' import useEditor from '../../../store/use-editor' import { useUploadStore } from '../../../store/use-upload' @@ -25,6 +26,8 @@ const MAX_FILE_SIZE = 200 * 1024 * 1024 // 200MB const ACCEPTED_FILE_TYPES = '.glb,.gltf,image/jpeg,image/png,image/webp,image/gif' const REFERENCES_EMPTY_TEXT = '可上传 GLB 网格作为扫描参考,或上传蓝图图片作为导引参考。' +const FLOORPLAN_ICON = withEditorBasePath('/icons/floorplan.webp') +const MESH_ICON = withEditorBasePath('/icons/mesh.webp') // ── Helper: get guide images for the current level ────────────────────────── @@ -219,7 +222,7 @@ function GuidesControl() { Guides {guides.length} @@ -258,7 +261,7 @@ function GuidesControl() {
- +

Guide images

@@ -298,7 +301,7 @@ function GuidesControl() {

{guide.name || `Guide image ${index + 1}`} @@ -395,7 +398,7 @@ function ScansControl() { variant="ghost" >

- Scans + Scans {scans.length} @@ -433,7 +436,7 @@ function ScansControl() {
- +

Scans

@@ -473,7 +476,7 @@ function ScansControl() {

{scan.name || `Scan ${index + 1}`} @@ -693,7 +696,7 @@ function ReferencesControl() { 参考资源 {total} @@ -736,7 +739,7 @@ function ReferencesControl() { )} { + test('prefixes root-relative assets for a subpath deployment', () => { + expect(withEditorBasePath('/icons/select.webp', '/pascal-service')).toBe( + '/pascal-service/icons/select.webp', + ) + }) + + test('does not duplicate an existing base path', () => { + expect(withEditorBasePath('/pascal-service/icons/select.webp', '/pascal-service')).toBe( + '/pascal-service/icons/select.webp', + ) + }) + + test('leaves external assets unchanged', () => { + expect(withEditorBasePath('https://cdn.example/icon.webp', '/pascal-service')).toBe( + 'https://cdn.example/icon.webp', + ) + }) +}) diff --git a/services/pascal-editor/packages/editor/src/lib/public-asset-path.ts b/services/pascal-editor/packages/editor/src/lib/public-asset-path.ts new file mode 100644 index 000000000..948465db1 --- /dev/null +++ b/services/pascal-editor/packages/editor/src/lib/public-asset-path.ts @@ -0,0 +1,20 @@ +const configuredBasePath = process.env.NEXT_PUBLIC_PASCAL_BASE_PATH?.trim() ?? '' + +const EDITOR_BASE_PATH = configuredBasePath + ? `/${configuredBasePath.replace(/^\/+|\/+$/g, '')}` + : '' + +export function withEditorBasePath(pathname: string, basePath = EDITOR_BASE_PATH): string { + if (/^(?:[a-z]+:)?\/\//i.test(pathname) || pathname.startsWith('data:')) return pathname + + const path = pathname.startsWith('/') ? pathname : `/${pathname}` + const normalizedBasePath = basePath ? `/${basePath.replace(/^\/+|\/+$/g, '')}` : '' + if ( + !normalizedBasePath || + path === normalizedBasePath || + path.startsWith(`${normalizedBasePath}/`) + ) { + return path + } + return `${normalizedBasePath}${path}` +}