Compare commits

...

2 Commits

Author SHA1 Message Date
lyf
19d048dd66 配置前端三环境变量
Some checks failed
CI / verify (push) Has been cancelled
2026-07-06 11:59:54 +08:00
lyf
4a6a7f9c29 同步加载页与首页模型加载状态 2026-07-06 10:11:20 +08:00
17 changed files with 324 additions and 96 deletions

14
.env
View File

@@ -1,14 +0,0 @@
# 腾讯地图 API Key
# 请到 https://lbs.qq.com/index.html 申请
VITE_DATA_SOURCE_MODE=sdk
VITE_GUIDE_CONTENT_SOURCE_MODE=static
VITE_API_BASE_URL=/app-api
VITE_SGS_SDK_TIMEOUT_MS=15000
VITE_TENCENT_MAP_KEY=EJPBZ-DQEEQ-PDN5U-4ZDVX-F4I3F-6MBJC
# SGS SDK 配置SDK 模式下必需)
VITE_SGS_API_BASE_URL=/app-api
VITE_SGS_MAP_ID=1
VITE_SGS_SDK_SCRIPT_URL=/static/sgs-map-sdk/index.global.js
VITE_SGS_H5_ENGINE_URL=/h5-sdk
VITE_SGS_SDK_ORIGIN=https://guide.whaoyue.com

27
.env.development Normal file
View File

@@ -0,0 +1,27 @@
# uni-app + Vite 开发环境默认配置。
# 真实本地接口地址或 key 请放入 .env.development.local。
VITE_APP_PUBLIC_BASE=/
VITE_DATA_SOURCE_MODE=sdk
VITE_GUIDE_CONTENT_SOURCE_MODE=static
VITE_GUIDE_STATIC_DATA_BASE_URL=/static/guide-data
VITE_API_BASE_URL=/app-api
VITE_AUDIO_API_BASE_URL=/nhm/audio
VITE_AUDIO_LANGUAGE=zh-CN
VITE_SGS_API_BASE_URL=/app-api
VITE_SGS_MAP_ID=1
VITE_SGS_SDK_SCRIPT_URL=/static/sgs-map-sdk/index.global.js
VITE_SGS_H5_ENGINE_URL=/h5-sdk
VITE_SGS_SDK_ORIGIN=
VITE_SGS_SDK_TIMEOUT_MS=15000
VITE_TENCENT_MAP_KEY=__REPLACE_WITH_TENCENT_MAP_WEB_KEY__
VITE_PUBLIC_SAME_ORIGIN_ASSET_HOST=
VITE_PUBLIC_LEGACY_AUDIO_HOST=
# Vite 开发服务器代理目标,只由 vite.config.ts 读取。
DEV_PROXY_APP_API_TARGET=http://localhost:3001
DEV_PROXY_H5_SDK_TARGET=http://localhost:3001
DEV_PROXY_SDK_TARGET=http://localhost:3001
DEV_PROXY_MUSEUM_ASSETS_TARGET=http://localhost:9000
DEV_PROXY_MINIO_TARGET=http://localhost:3001
DEV_PROXY_AUDIO_TARGET=http://localhost:19000

View File

@@ -1,15 +1,27 @@
# 腾讯地图 API Key # 复制到 .env.development.local、.env.test.local 或 .env.production.local 后填写真实部署值。
# 请到 https://lbs.qq.com/index.html 申请 # VITE_ 前缀变量会被打包进前端产物,不应存放后端密钥或私有令牌。
VITE_TENCENT_MAP_KEY=your_tencent_map_key_here
# 数据源模式sdk / mock VITE_APP_PUBLIC_BASE=/
VITE_DATA_SOURCE_MODE=sdk VITE_DATA_SOURCE_MODE=sdk
VITE_GUIDE_CONTENT_SOURCE_MODE=static
VITE_GUIDE_STATIC_DATA_BASE_URL=/static/guide-data
VITE_API_BASE_URL=/app-api VITE_API_BASE_URL=/app-api
VITE_SGS_SDK_TIMEOUT_MS=15000 VITE_AUDIO_API_BASE_URL=/yudao-server
VITE_AUDIO_LANGUAGE=zh-CN
# SGS SDK 配置(生产部署使用同源路径,避免 HTTPS 页面请求 HTTP 后端被浏览器拦截)
VITE_SGS_API_BASE_URL=/app-api VITE_SGS_API_BASE_URL=/app-api
VITE_SGS_MAP_ID=1 VITE_SGS_MAP_ID=1
VITE_SGS_SDK_SCRIPT_URL=/static/sgs-map-sdk/index.global.js VITE_SGS_SDK_SCRIPT_URL=/static/sgs-map-sdk/index.global.js
VITE_SGS_H5_ENGINE_URL=/h5-sdk VITE_SGS_H5_ENGINE_URL=/h5-sdk
VITE_SGS_SDK_ORIGIN=https://guide.whaoyue.com VITE_SGS_SDK_ORIGIN=
VITE_SGS_SDK_TIMEOUT_MS=15000
VITE_TENCENT_MAP_KEY=__REPLACE_WITH_TENCENT_MAP_WEB_KEY__
VITE_PUBLIC_SAME_ORIGIN_ASSET_HOST=
VITE_PUBLIC_LEGACY_AUDIO_HOST=
# 仅开发环境使用的 Vite 代理目标,只由 vite.config.ts 读取。
DEV_PROXY_APP_API_TARGET=http://localhost:3001
DEV_PROXY_H5_SDK_TARGET=http://localhost:3001
DEV_PROXY_SDK_TARGET=http://localhost:3001
DEV_PROXY_MUSEUM_ASSETS_TARGET=http://localhost:9000
DEV_PROXY_MINIO_TARGET=http://localhost:3001
DEV_PROXY_AUDIO_TARGET=http://localhost:19000

19
.env.production Normal file
View File

@@ -0,0 +1,19 @@
# uni-app + Vite 生产环境默认配置。
# 真实生产值请通过部署平台环境变量或 .env.production.local 覆盖。
VITE_APP_PUBLIC_BASE=/
VITE_DATA_SOURCE_MODE=sdk
VITE_GUIDE_CONTENT_SOURCE_MODE=static
VITE_GUIDE_STATIC_DATA_BASE_URL=/static/guide-data
VITE_API_BASE_URL=/app-api
VITE_AUDIO_API_BASE_URL=/yudao-server
VITE_AUDIO_LANGUAGE=zh-CN
VITE_SGS_API_BASE_URL=/app-api
VITE_SGS_MAP_ID=1
VITE_SGS_SDK_SCRIPT_URL=/static/sgs-map-sdk/index.global.js
VITE_SGS_H5_ENGINE_URL=/h5-sdk
VITE_SGS_SDK_ORIGIN=https://guide.sznhmuseum.org.cn
VITE_SGS_SDK_TIMEOUT_MS=15000
VITE_TENCENT_MAP_KEY=__REPLACE_WITH_TENCENT_MAP_WEB_KEY__
VITE_PUBLIC_SAME_ORIGIN_ASSET_HOST=
VITE_PUBLIC_LEGACY_AUDIO_HOST=

19
.env.test Normal file
View File

@@ -0,0 +1,19 @@
# uni-app + Vite 测试环境默认配置。
# 真实测试环境接口地址或 key 请通过部署平台环境变量或 .env.test.local 覆盖。
VITE_APP_PUBLIC_BASE=/
VITE_DATA_SOURCE_MODE=sdk
VITE_GUIDE_CONTENT_SOURCE_MODE=static
VITE_GUIDE_STATIC_DATA_BASE_URL=/static/guide-data
VITE_API_BASE_URL=/app-api
VITE_AUDIO_API_BASE_URL=/yudao-server
VITE_AUDIO_LANGUAGE=zh-CN
VITE_SGS_API_BASE_URL=/app-api
VITE_SGS_MAP_ID=1
VITE_SGS_SDK_SCRIPT_URL=/static/sgs-map-sdk/index.global.js
VITE_SGS_H5_ENGINE_URL=/h5-sdk
VITE_SGS_SDK_ORIGIN=https://guide.whaoyue.com
VITE_SGS_SDK_TIMEOUT_MS=15000
VITE_TENCENT_MAP_KEY=__REPLACE_WITH_TENCENT_MAP_WEB_KEY__
VITE_PUBLIC_SAME_ORIGIN_ASSET_HOST=
VITE_PUBLIC_LEGACY_AUDIO_HOST=

4
.gitignore vendored
View File

@@ -14,6 +14,10 @@ coverage/
.env .env
.env.local .env.local
.env.*.local .env.*.local
!.env.example
!.env.development
!.env.test
!.env.production
.claude/*.local.json .claude/*.local.json
.claude/worktrees/ .claude/worktrees/
.codex-artifacts/ .codex-artifacts/

View File

@@ -8,10 +8,14 @@
"node": ">=20 <25" "node": ">=20 <25"
}, },
"scripts": { "scripts": {
"dev:h5": "uni -p h5", "dev:h5": "uni -p h5 --mode development",
"build:h5": "uni build -p h5 && node scripts/copy-h5-nav-assets.cjs", "dev:test:h5": "uni -p h5 --mode test",
"dev:mp-weixin": "uni -p mp-weixin", "build:test:h5": "uni build -p h5 --mode test && node scripts/copy-h5-nav-assets.cjs",
"build:mp-weixin": "uni build -p mp-weixin", "build:h5": "uni build -p h5 --mode production && node scripts/copy-h5-nav-assets.cjs",
"dev:mp-weixin": "uni -p mp-weixin --mode development",
"dev:test:mp-weixin": "uni -p mp-weixin --mode test",
"build:test:mp-weixin": "uni build -p mp-weixin --mode test",
"build:mp-weixin": "uni build -p mp-weixin --mode production",
"type-check": "vue-tsc --noEmit", "type-check": "vue-tsc --noEmit",
"lint": "eslint \"src/**/*.{ts,vue}\"" "lint": "eslint \"src/**/*.{ts,vue}\""
}, },

View File

@@ -75,6 +75,14 @@ type CameraPreset = 'top' | 'oblique'
type TouchGestureMode = 'orbit' | 'pan' type TouchGestureMode = 'orbit' | 'pan'
type ZoomCameraSource = 'button' | 'gesture' type ZoomCameraSource = 'button' | 'gesture'
interface InitialModelProgressEvent {
progress: number
message: string
view: ViewMode
floorId?: string
elapsedMs?: number
}
const CANVAS_FONT_FAMILY = '"鸿蒙黑体", "HarmonyOS Sans SC", "HarmonyOS Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif' const CANVAS_FONT_FAMILY = '"鸿蒙黑体", "HarmonyOS Sans SC", "HarmonyOS Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
const FLOOR_CAMERA_DISTANCE_FACTOR = 0.78 const FLOOR_CAMERA_DISTANCE_FACTOR = 0.78
const SGS_VISUAL_RENDER_CONFIG = { const SGS_VISUAL_RENDER_CONFIG = {
@@ -300,6 +308,7 @@ const emit = defineEmits<{
targetFocus: [result: TargetPoiFocusResult] targetFocus: [result: TargetPoiFocusResult]
autoSwitch: [event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: 'zoom-in' | 'zoom-out'; distance: number }] autoSwitch: [event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: 'zoom-in' | 'zoom-out'; distance: number }]
autoSwitchBlocked: [reason: { reason: 'loading-locked' | 'cooldown' | 'disabled' }] autoSwitchBlocked: [reason: { reason: 'loading-locked' | 'cooldown' | 'disabled' }]
initialModelProgress: [event: InitialModelProgressEvent]
initialModelReady: [event: { view: ViewMode; floorId?: string; elapsedMs?: number }] initialModelReady: [event: { view: ViewMode; floorId?: string; elapsedMs?: number }]
initialModelFailed: [event: { view: ViewMode; floorId?: string; message: string; elapsedMs?: number }] initialModelFailed: [event: { view: ViewMode; floorId?: string; message: string; elapsedMs?: number }]
}>() }>()
@@ -385,6 +394,7 @@ let cameraTween: CameraTweenState | null = null
let adjacentPreloadSeq = 0 let adjacentPreloadSeq = 0
let firstModelLoadStartedAt = 0 let firstModelLoadStartedAt = 0
let firstModelVisibleReported = false let firstModelVisibleReported = false
let initialModelSettled = false
const cameraTweenEnabled = true const cameraTweenEnabled = true
const cameraTweenDurationMs = 480 const cameraTweenDurationMs = 480
@@ -1442,8 +1452,19 @@ const handleControlEnd = () => {
} }
const setProgress = (progress: number, message: string) => { const setProgress = (progress: number, message: string) => {
loadingProgress.value = Math.min(100, Math.max(0, progress)) const normalizedProgress = Math.min(100, Math.max(0, progress))
loadingProgress.value = normalizedProgress
loadingMessage.value = message loadingMessage.value = message
if (firstModelLoadStartedAt && !initialModelSettled) {
emit('initialModelProgress', {
progress: normalizedProgress,
message,
view: activeView.value,
floorId: currentFloor.value,
elapsedMs: Math.round(getNow() - firstModelLoadStartedAt)
})
}
} }
const markFirstModelVisible = ( const markFirstModelVisible = (
@@ -4713,6 +4734,7 @@ const init3DScene = async () => {
isDisposed = false isDisposed = false
firstModelLoadStartedAt = getNow() firstModelLoadStartedAt = getNow()
firstModelVisibleReported = false firstModelVisibleReported = false
initialModelSettled = false
isLoading.value = true isLoading.value = true
loadError.value = false loadError.value = false
selectedPOI.value = null selectedPOI.value = null
@@ -4727,6 +4749,7 @@ const init3DScene = async () => {
await loadModelPackage() await loadModelPackage()
setProgress(100, '馆内三维模型加载完成') setProgress(100, '馆内三维模型加载完成')
initialModelSettled = true
isLoading.value = false isLoading.value = false
emit('initialModelReady', { emit('initialModelReady', {
view: activeView.value, view: activeView.value,
@@ -4743,6 +4766,7 @@ const init3DScene = async () => {
isLoading.value = false isLoading.value = false
const message = error instanceof Error ? error.message : '请检查模型资源或网络状态后重试' const message = error instanceof Error ? error.message : '请检查模型资源或网络状态后重试'
setProgress(0, message) setProgress(0, message)
initialModelSettled = true
emit('initialModelFailed', { emit('initialModelFailed', {
view: activeView.value, view: activeView.value,
floorId: currentFloor.value, floorId: currentFloor.value,

View File

@@ -26,6 +26,7 @@
@selection-clear="handleSelectionClear" @selection-clear="handleSelectionClear"
@target-focus="handleTargetFocus" @target-focus="handleTargetFocus"
@auto-switch="handleAutoSwitch" @auto-switch="handleAutoSwitch"
@initial-model-progress="handleInitialModelProgress"
@initial-model-ready="handleInitialModelReady" @initial-model-ready="handleInitialModelReady"
@initial-model-failed="handleInitialModelFailed" @initial-model-failed="handleInitialModelFailed"
/> />
@@ -289,6 +290,14 @@ type IndoorViewMode = 'overview' | 'floor' | 'multi'
type LayerDisplayMode = 'single' | 'multi' type LayerDisplayMode = 'single' | 'multi'
type TouchGestureMode = 'orbit' | 'pan' type TouchGestureMode = 'orbit' | 'pan'
interface InitialModelProgressEvent {
progress: number
message: string
view: IndoorViewMode
floorId?: string
elapsedMs?: number
}
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
searchText?: string searchText?: string
activeMode?: '2d' | '3d' activeMode?: '2d' | '3d'
@@ -403,6 +412,7 @@ const emit = defineEmits<{
selectionClear: [] selectionClear: []
targetFocus: [result: TargetPoiFocusResult] targetFocus: [result: TargetPoiFocusResult]
autoSwitch: [event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: string; distance: number }] autoSwitch: [event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: string; distance: number }]
initialModelProgress: [event: InitialModelProgressEvent]
initialModelReady: [event: { view: IndoorViewMode; floorId?: string; elapsedMs?: number }] initialModelReady: [event: { view: IndoorViewMode; floorId?: string; elapsedMs?: number }]
initialModelFailed: [event: { view: IndoorViewMode; floorId?: string; message: string; elapsedMs?: number }] initialModelFailed: [event: { view: IndoorViewMode; floorId?: string; message: string; elapsedMs?: number }]
mapTap: [location: { latitude: number; longitude: number }] mapTap: [location: { latitude: number; longitude: number }]
@@ -710,6 +720,10 @@ const handleAutoSwitch = (event: { from: 'overview' | 'floor'; to: 'overview' |
emit('autoSwitch', event) emit('autoSwitch', event)
} }
const handleInitialModelProgress = (event: InitialModelProgressEvent) => {
emit('initialModelProgress', event)
}
const handleInitialModelReady = (event: { view: IndoorViewMode; floorId?: string; elapsedMs?: number }) => { const handleInitialModelReady = (event: { view: IndoorViewMode; floorId?: string; elapsedMs?: number }) => {
emit('initialModelReady', event) emit('initialModelReady', event)
} }

View File

@@ -68,7 +68,9 @@ export const dataSourceConfig = {
sgsSdkScriptUrl: normalizeUrl(import.meta.env.VITE_SGS_SDK_SCRIPT_URL, defaultSdkScriptUrl), sgsSdkScriptUrl: normalizeUrl(import.meta.env.VITE_SGS_SDK_SCRIPT_URL, defaultSdkScriptUrl),
sgsMapEngineUrl: normalizeUrl(import.meta.env.VITE_SGS_H5_ENGINE_URL, defaultSgsEngineUrl), sgsMapEngineUrl: normalizeUrl(import.meta.env.VITE_SGS_H5_ENGINE_URL, defaultSgsEngineUrl),
sgsSdkTargetOrigin: import.meta.env.VITE_SGS_SDK_ORIGIN?.trim() || '', sgsSdkTargetOrigin: import.meta.env.VITE_SGS_SDK_ORIGIN?.trim() || '',
sgsSdkTimeoutMs: normalizeTimeout(import.meta.env.VITE_SGS_SDK_TIMEOUT_MS) sgsSdkTimeoutMs: normalizeTimeout(import.meta.env.VITE_SGS_SDK_TIMEOUT_MS),
publicSameOriginAssetHost: import.meta.env.VITE_PUBLIC_SAME_ORIGIN_ASSET_HOST?.trim() || '',
publicLegacyAudioHost: import.meta.env.VITE_PUBLIC_LEGACY_AUDIO_HOST?.trim() || ''
} }
export const isSgsSdkMode = () => dataSourceConfig.mode === 'sdk' export const isSgsSdkMode = () => dataSourceConfig.mode === 'sdk'

3
src/env.d.ts vendored
View File

@@ -1,6 +1,7 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_APP_PUBLIC_BASE?: string
readonly VITE_DATA_SOURCE_MODE?: 'static' | 'api' | 'sdk' readonly VITE_DATA_SOURCE_MODE?: 'static' | 'api' | 'sdk'
readonly VITE_GUIDE_CONTENT_SOURCE_MODE?: 'static' | 'remote' | 'mock' readonly VITE_GUIDE_CONTENT_SOURCE_MODE?: 'static' | 'remote' | 'mock'
readonly VITE_GUIDE_STATIC_DATA_BASE_URL?: string readonly VITE_GUIDE_STATIC_DATA_BASE_URL?: string
@@ -14,6 +15,8 @@ interface ImportMetaEnv {
readonly VITE_SGS_SDK_ORIGIN?: string readonly VITE_SGS_SDK_ORIGIN?: string
readonly VITE_SGS_SDK_TIMEOUT_MS?: string readonly VITE_SGS_SDK_TIMEOUT_MS?: string
readonly VITE_TENCENT_MAP_KEY?: string readonly VITE_TENCENT_MAP_KEY?: string
readonly VITE_PUBLIC_SAME_ORIGIN_ASSET_HOST?: string
readonly VITE_PUBLIC_LEGACY_AUDIO_HOST?: string
} }
interface ImportMeta { interface ImportMeta {

View File

@@ -55,7 +55,7 @@
"sdkConfigs": { "sdkConfigs": {
"maps": { "maps": {
"qqmap": { "qqmap": {
"key": "EJPBZ-DQEEQ-PDN5U-4ZDVX-F4I3F-6MBJC" "key": "__REPLACE_WITH_TENCENT_MAP_WEB_KEY__"
} }
} }
} }

View File

@@ -51,6 +51,7 @@
@poi-click="handleGuidePoiClick" @poi-click="handleGuidePoiClick"
@selection-clear="handleGuideSelectionClear" @selection-clear="handleGuideSelectionClear"
@auto-switch="handleAutoSwitch" @auto-switch="handleAutoSwitch"
@initial-model-progress="handleInitialModelProgress"
@initial-model-ready="handleInitialModelReady" @initial-model-ready="handleInitialModelReady"
@initial-model-failed="handleInitialModelFailed" @initial-model-failed="handleInitialModelFailed"
@tool-click="handleIndoorToolClick" @tool-click="handleIndoorToolClick"
@@ -255,10 +256,18 @@
<text class="app-launch-subtitle">Shenzhen Natural History Museum</text> <text class="app-launch-subtitle">Shenzhen Natural History Museum</text>
</view> </view>
<view class="app-launch-status"> <view class="app-launch-status">
<text class="app-launch-loading-text">正在进入自然导览</text> <text class="app-launch-loading-text">{{ launchLoadingText }}</text>
<view class="app-launch-progress"> <view class="app-launch-progress">
<view class="app-launch-progress-bar" :style="launchProgressStyle"></view> <view class="app-launch-progress-bar" :style="launchProgressStyle"></view>
</view> </view>
<view v-if="launchOverlayRecoverable" class="app-launch-actions">
<view class="app-launch-action secondary" @tap="handleLaunchRetry">
<text class="app-launch-action-text">重新加载</text>
</view>
<view class="app-launch-action primary" @tap="handleLaunchContinue">
<text class="app-launch-action-text">继续进入</text>
</view>
</view>
</view> </view>
</view> </view>
</view> </view>
@@ -346,6 +355,7 @@ const topTabFromHash = () => {
return isGuideTopTab(tab) ? tab : null return isGuideTopTab(tab) ? tab : null
} }
const launchOverlayMinDisplayMs = 700
const launchOverlayGuideTimeoutMs = 12000 const launchOverlayGuideTimeoutMs = 12000
const launchOverlayNonGuideTimeoutMs = 900 const launchOverlayNonGuideTimeoutMs = 900
@@ -372,8 +382,10 @@ const selectedGuidePoi = ref<GuideRenderPoi | null>(null)
const currentTab = ref<GuideTopTab>(initialTopTab()) const currentTab = ref<GuideTopTab>(initialTopTab())
const launchOverlayVisible = ref(shouldShowLaunchOverlay()) const launchOverlayVisible = ref(shouldShowLaunchOverlay())
const launchOverlayLeaving = ref(false) const launchOverlayLeaving = ref(false)
const launchProgress = ref(18) const launchProgress = ref(8)
const launchLoadingText = ref('正在初始化三维场景')
const launchInitialModelSettled = ref(false) const launchInitialModelSettled = ref(false)
const launchOverlayRecoverable = ref(false)
const showRoutePlanner = ref(false) const showRoutePlanner = ref(false)
const routeStartPoint = ref<RoutePointOption | null>(null) const routeStartPoint = ref<RoutePointOption | null>(null)
const routeEndPoint = ref<RoutePointOption | null>(null) const routeEndPoint = ref<RoutePointOption | null>(null)
@@ -396,7 +408,8 @@ const homeSearchExpanded = ref(false)
let launchOverlayFallbackTimer: ReturnType<typeof setTimeout> | null = null let launchOverlayFallbackTimer: ReturnType<typeof setTimeout> | null = null
let launchOverlayFadeTimer: ReturnType<typeof setTimeout> | null = null let launchOverlayFadeTimer: ReturnType<typeof setTimeout> | null = null
let launchProgressTimer: ReturnType<typeof setInterval> | null = null let launchOverlayHideDelayTimer: ReturnType<typeof setTimeout> | null = null
let launchOverlayStartedAt = 0
const launchProgressStyle = computed(() => ({ const launchProgressStyle = computed(() => ({
width: `${launchProgress.value}%` width: `${launchProgress.value}%`
@@ -413,13 +426,13 @@ const clearLaunchTimers = () => {
launchOverlayFadeTimer = null launchOverlayFadeTimer = null
} }
if (launchProgressTimer) { if (launchOverlayHideDelayTimer) {
clearInterval(launchProgressTimer) clearTimeout(launchOverlayHideDelayTimer)
launchProgressTimer = null launchOverlayHideDelayTimer = null
} }
} }
const hideLaunchOverlay = () => { const completeHideLaunchOverlay = () => {
if (!launchOverlayVisible.value) return if (!launchOverlayVisible.value) return
launchProgress.value = 100 launchProgress.value = 100
@@ -433,6 +446,24 @@ const hideLaunchOverlay = () => {
}, 360) }, 360)
} }
const hideLaunchOverlay = () => {
if (!launchOverlayVisible.value || launchOverlayLeaving.value) return
const elapsedMs = launchOverlayStartedAt ? Date.now() - launchOverlayStartedAt : launchOverlayMinDisplayMs
const waitMs = Math.max(0, launchOverlayMinDisplayMs - elapsedMs)
if (waitMs > 0) {
if (launchOverlayHideDelayTimer) clearTimeout(launchOverlayHideDelayTimer)
launchOverlayHideDelayTimer = setTimeout(() => {
launchOverlayHideDelayTimer = null
completeHideLaunchOverlay()
}, waitMs)
return
}
completeHideLaunchOverlay()
}
const shouldWaitForLaunchModel = () => ( const shouldWaitForLaunchModel = () => (
currentTab.value === 'guide' currentTab.value === 'guide'
&& is3DMode.value && is3DMode.value
@@ -443,10 +474,12 @@ const startLaunchOverlay = () => {
clearLaunchTimers() clearLaunchTimers()
launchInitialModelSettled.value = false launchInitialModelSettled.value = false
launchOverlayRecoverable.value = false
launchProgressTimer = setInterval(() => { launchOverlayStartedAt = Date.now()
launchProgress.value = Math.min(92, launchProgress.value + 11) launchProgress.value = shouldWaitForLaunchModel() ? 8 : 68
}, 180) launchLoadingText.value = shouldWaitForLaunchModel()
? '正在初始化三维场景'
: '正在进入自然导览'
const timeoutMs = shouldWaitForLaunchModel() const timeoutMs = shouldWaitForLaunchModel()
? launchOverlayGuideTimeoutMs ? launchOverlayGuideTimeoutMs
@@ -454,24 +487,53 @@ const startLaunchOverlay = () => {
launchOverlayFallbackTimer = setTimeout(() => { launchOverlayFallbackTimer = setTimeout(() => {
if (shouldWaitForLaunchModel() && !launchInitialModelSettled.value) { if (shouldWaitForLaunchModel() && !launchInitialModelSettled.value) {
console.warn(`首页前置加载页等待模型超过 ${timeoutMs}ms交给页面自身加载态接管`) launchOverlayRecoverable.value = true
launchProgress.value = Math.min(96, Math.max(launchProgress.value, 92))
launchLoadingText.value = '三维场景加载较慢,仍在等待模型就绪'
console.warn(`首页前置加载页等待模型超过 ${timeoutMs}ms继续等待真实模型 ready`)
return
} }
hideLaunchOverlay() hideLaunchOverlay()
}, timeoutMs) }, timeoutMs)
} }
const handleInitialModelProgress = (event: { progress: number; message: string; view: GuideIndoorView; floorId?: string; elapsedMs?: number }) => {
if (!launchOverlayVisible.value || launchInitialModelSettled.value || !shouldWaitForLaunchModel()) return
launchOverlayRecoverable.value = false
launchProgress.value = Math.min(99, Math.max(8, event.progress))
launchLoadingText.value = event.message || '正在加载三维场景'
}
const settleLaunchOverlayByModel = (status: 'ready' | 'failed', event?: { view: GuideIndoorView; floorId?: string; message?: string; elapsedMs?: number }) => { const settleLaunchOverlayByModel = (status: 'ready' | 'failed', event?: { view: GuideIndoorView; floorId?: string; message?: string; elapsedMs?: number }) => {
launchInitialModelSettled.value = true launchInitialModelSettled.value = true
launchOverlayRecoverable.value = status === 'failed'
if (status === 'failed') { if (status === 'failed') {
console.warn('首页前置加载页收到初始模型加载失败,交给 ThreeMap 错误态接管:', event) launchProgress.value = Math.min(launchProgress.value, 96)
launchLoadingText.value = event?.message || '三维场景加载失败'
console.warn('首页前置加载页收到初始模型加载失败,等待用户选择重试或进入降级首页:', event)
return
} else { } else {
launchProgress.value = 100
launchLoadingText.value = '三维场景已就绪'
console.log('首页前置加载页收到初始模型加载完成:', event) console.log('首页前置加载页收到初始模型加载完成:', event)
} }
hideLaunchOverlay() hideLaunchOverlay()
} }
const handleLaunchRetry = () => {
if (typeof window !== 'undefined') {
window.location.reload()
}
}
const handleLaunchContinue = () => {
launchOverlayRecoverable.value = false
hideLaunchOverlay()
}
// 馆外导航相关状态 // 馆外导航相关状态
const isOutdoorGuidePanelEnabled = false const isOutdoorGuidePanelEnabled = false
const showOutdoorNavPanel = ref(false) const showOutdoorNavPanel = ref(false)
@@ -1588,6 +1650,43 @@ const handleExplainBack = () => {
transition: width 0.22s ease; transition: width 0.22s ease;
} }
.app-launch-actions {
margin-top: 14px;
display: flex;
gap: 10px;
}
.app-launch-action {
height: 36px;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border-radius: 8px;
}
.app-launch-action.secondary {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.38);
}
.app-launch-action.primary {
background: var(--museum-accent);
border: 1px solid var(--museum-accent);
}
.app-launch-action-text {
font-size: 13px;
line-height: 18px;
font-weight: 700;
color: #ffffff;
}
.app-launch-action.primary .app-launch-action-text {
color: #151713;
}
@media (max-width: 360px) { @media (max-width: 360px) {
.app-launch-brand { .app-launch-brand {
top: calc(env(safe-area-inset-top) + 48px); top: calc(env(safe-area-inset-top) + 48px);

View File

@@ -113,7 +113,8 @@ const resolveSgsAssetUrl = (url?: string | null) => {
try { try {
const assetUrl = new URL(normalizedUrl) const assetUrl = new URL(normalizedUrl)
if ( if (
assetUrl.hostname === '1.92.206.90' dataSourceConfig.publicSameOriginAssetHost
&& assetUrl.hostname === dataSourceConfig.publicSameOriginAssetHost
&& assetUrl.port === '9000' && assetUrl.port === '9000'
&& assetUrl.pathname.startsWith('/museum-assets/') && assetUrl.pathname.startsWith('/museum-assets/')
) { ) {

View File

@@ -3,8 +3,7 @@
* 用于 POI 搜索、地址解析、路线规划等 * 用于 POI 搜索、地址解析、路线规划等
*/ */
// 优先从环境变量读取 API Key回退到硬编码值仅用于开发环境 const TENCENT_MAP_KEY = import.meta.env.VITE_TENCENT_MAP_KEY?.trim() || ''
const TENCENT_MAP_KEY = import.meta.env.VITE_TENCENT_MAP_KEY || 'EJPBZ-DQEEQ-PDN5U-4ZDVX-F4I3F-6MBJC'
const TENCENT_MAP_BASE_URL = 'https://apis.map.qq.com/ws' const TENCENT_MAP_BASE_URL = 'https://apis.map.qq.com/ws'
const TENCENT_DIRECTION_BASE_URL = `${TENCENT_MAP_BASE_URL}/direction/v1` const TENCENT_DIRECTION_BASE_URL = `${TENCENT_MAP_BASE_URL}/direction/v1`

View File

@@ -1,5 +1,6 @@
const SAME_SERVER_HOST = '1.92.206.90' import {
const LEGACY_AUDIO_HOST = '47.120.48.148' dataSourceConfig
} from '@/config/dataSource'
const isAbsoluteHttpUrl = (url: string) => /^https?:\/\//i.test(url) const isAbsoluteHttpUrl = (url: string) => /^https?:\/\//i.test(url)
@@ -9,7 +10,8 @@ export const normalizeSameOriginPublicUrl = (url: string | null | undefined) =>
try { try {
const parsed = new URL(value) const parsed = new URL(value)
const isSameServer = parsed.hostname === SAME_SERVER_HOST const isSameServer = Boolean(dataSourceConfig.publicSameOriginAssetHost)
&& parsed.hostname === dataSourceConfig.publicSameOriginAssetHost
if ( if (
isSameServer isSameServer
@@ -31,7 +33,8 @@ export const normalizeSameOriginPublicUrl = (url: string | null | undefined) =>
} }
if ( if (
parsed.hostname === LEGACY_AUDIO_HOST dataSourceConfig.publicLegacyAudioHost
&& parsed.hostname === dataSourceConfig.publicLegacyAudioHost
&& parsed.port === '19000' && parsed.port === '19000'
&& parsed.pathname.startsWith('/nhm/audio/') && parsed.pathname.startsWith('/nhm/audio/')
) { ) {

View File

@@ -1,54 +1,66 @@
import { defineConfig } from 'vite' import { defineConfig, loadEnv } from 'vite'
import uni from '@dcloudio/vite-plugin-uni' import uni from '@dcloudio/vite-plugin-uni'
const sgsDevServer = 'http://1.92.206.90:3001' const normalizeEnvUrl = (value: string | undefined, fallback: string) => {
const sgsAssetServer = 'http://1.92.206.90:9000' const normalized = value?.trim()
const legacyAudioServer = 'http://47.120.48.148:19000' return normalized || fallback
}
export default defineConfig({ export default defineConfig(({ mode }) => {
plugins: [uni()], const env = loadEnv(mode, process.cwd(), '')
server: { const appApiProxyTarget = normalizeEnvUrl(env.DEV_PROXY_APP_API_TARGET, 'http://localhost:3001')
host: '0.0.0.0', const h5SdkProxyTarget = normalizeEnvUrl(env.DEV_PROXY_H5_SDK_TARGET, appApiProxyTarget)
proxy: { const sdkProxyTarget = normalizeEnvUrl(env.DEV_PROXY_SDK_TARGET, appApiProxyTarget)
'/app-api': { const museumAssetsProxyTarget = normalizeEnvUrl(env.DEV_PROXY_MUSEUM_ASSETS_TARGET, 'http://localhost:9000')
target: sgsDevServer, const minioProxyTarget = normalizeEnvUrl(env.DEV_PROXY_MINIO_TARGET, appApiProxyTarget)
changeOrigin: true const audioProxyTarget = normalizeEnvUrl(env.DEV_PROXY_AUDIO_TARGET, 'http://localhost:19000')
},
'/h5-sdk': { return {
target: sgsDevServer, base: normalizeEnvUrl(env.VITE_APP_PUBLIC_BASE, '/'),
changeOrigin: true plugins: [uni()],
}, server: {
'/sdk': { host: '0.0.0.0',
target: sgsDevServer, proxy: {
changeOrigin: true '/app-api': {
}, target: appApiProxyTarget,
'/museum-assets': { changeOrigin: true
target: sgsAssetServer, },
changeOrigin: true '/h5-sdk': {
}, target: h5SdkProxyTarget,
'/minio': { changeOrigin: true
target: sgsDevServer, },
changeOrigin: true '/sdk': {
}, target: sdkProxyTarget,
'/nhm/audio': { changeOrigin: true
target: legacyAudioServer, },
changeOrigin: true '/museum-assets': {
target: museumAssetsProxyTarget,
changeOrigin: true
},
'/minio': {
target: minioProxyTarget,
changeOrigin: true
},
'/nhm/audio': {
target: audioProxyTarget,
changeOrigin: true
}
} }
} },
}, resolve: {
resolve: { alias: {
alias: { '@': '/src'
'@': '/src' }
} },
}, optimizeDeps: {
optimizeDeps: { include: ['three']
include: ['three'] },
}, build: {
build: { rollupOptions: {
rollupOptions: { output: {
output: { manualChunks: {
manualChunks: { three: ['three']
three: ['three'] }
} }
} }
} }