fix: resolve Pascal toolbar icons under base path
This commit is contained in:
parent
4c1b97f210
commit
3d35858c05
@ -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>
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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',
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -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}`
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user