fix: resolve Pascal toolbar icons under base path

This commit is contained in:
134 2026-07-12 10:23:21 +08:00
parent 4c1b97f210
commit 3d35858c05
5 changed files with 61 additions and 14 deletions

View File

@ -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}
/>
</ActionButton>
@ -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}
/>
</ActionButton>
@ -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}
/>
</ActionButton>

View File

@ -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',

View File

@ -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() {
<img
alt="Guides"
className="h-[28px] w-[28px] object-contain"
src="/icons/floorplan.webp"
src={FLOORPLAN_ICON}
/>
<span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
{guides.length}
@ -258,7 +261,7 @@ function GuidesControl() {
<div className="space-y-3">
<div className="flex items-center gap-2">
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-background/80">
<img alt="" className="h-4 w-4 object-contain" src="/icons/floorplan.webp" />
<img alt="" className="h-4 w-4 object-contain" src={FLOORPLAN_ICON} />
</span>
<div className="min-w-0 flex-1">
<p className="font-medium text-foreground text-sm">Guide images</p>
@ -298,7 +301,7 @@ function GuidesControl() {
<img
alt=""
className="h-3.5 w-3.5 shrink-0 object-contain opacity-70"
src="/icons/floorplan.webp"
src={FLOORPLAN_ICON}
/>
<p className="truncate font-medium text-foreground text-sm">
{guide.name || `Guide image ${index + 1}`}
@ -395,7 +398,7 @@ function ScansControl() {
variant="ghost"
>
<div className="relative">
<img alt="Scans" className="h-[28px] w-[28px] object-contain" src="/icons/mesh.webp" />
<img alt="Scans" className="h-[28px] w-[28px] object-contain" src={MESH_ICON} />
<span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
{scans.length}
</span>
@ -433,7 +436,7 @@ function ScansControl() {
<div className="space-y-3">
<div className="flex items-center gap-2">
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-background/80">
<img alt="" className="h-4 w-4 object-contain" src="/icons/mesh.webp" />
<img alt="" className="h-4 w-4 object-contain" src={MESH_ICON} />
</span>
<div className="min-w-0 flex-1">
<p className="font-medium text-foreground text-sm">Scans</p>
@ -473,7 +476,7 @@ function ScansControl() {
<img
alt=""
className="h-3.5 w-3.5 shrink-0 object-contain opacity-70"
src="/icons/mesh.webp"
src={MESH_ICON}
/>
<p className="truncate font-medium text-foreground text-sm">
{scan.name || `Scan ${index + 1}`}
@ -693,7 +696,7 @@ function ReferencesControl() {
<img
alt="参考资源"
className="h-[28px] w-[28px] object-contain"
src="/icons/floorplan.webp"
src={FLOORPLAN_ICON}
/>
<span className="absolute -right-1.5 -bottom-1 min-w-[14px] rounded-full bg-white/20 px-[3px] text-center font-medium text-[9px] text-white/70 leading-[14px]">
{total}
@ -736,7 +739,7 @@ function ReferencesControl() {
)}
<ReferenceListSection
emptyText={REFERENCES_EMPTY_TEXT}
iconSrc="/icons/mesh.webp"
iconSrc={MESH_ICON}
nodes={scans}
noun="扫描"
onError={setUploadError}
@ -747,7 +750,7 @@ function ReferencesControl() {
<div className="h-px bg-border/45" />
<ReferenceListSection
emptyText={REFERENCES_EMPTY_TEXT}
iconSrc="/icons/floorplan.webp"
iconSrc={FLOORPLAN_ICON}
nodes={guides}
noun="导引图片"
onError={setUploadError}

View File

@ -0,0 +1,22 @@
import { describe, expect, test } from 'bun:test'
import { withEditorBasePath } from './public-asset-path'
describe('withEditorBasePath', () => {
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',
)
})
})

View File

@ -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}`
}