Compare commits
2 Commits
b99ae572da
...
19d048dd66
| Author | SHA1 | Date | |
|---|---|---|---|
| 19d048dd66 | |||
| 4a6a7f9c29 |
14
.env
14
.env
@@ -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
27
.env.development
Normal 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
|
||||||
28
.env.example
28
.env.example
@@ -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
19
.env.production
Normal 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
19
.env.test
Normal 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
4
.gitignore
vendored
@@ -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/
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -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}\""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
3
src/env.d.ts
vendored
@@ -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 {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
"sdkConfigs": {
|
"sdkConfigs": {
|
||||||
"maps": {
|
"maps": {
|
||||||
"qqmap": {
|
"qqmap": {
|
||||||
"key": "EJPBZ-DQEEQ-PDN5U-4ZDVX-F4I3F-6MBJC"
|
"key": "__REPLACE_WITH_TENCENT_MAP_WEB_KEY__"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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/')
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|
||||||
|
|||||||
@@ -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/')
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,37 +1,48 @@
|
|||||||
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 }) => {
|
||||||
|
const env = loadEnv(mode, process.cwd(), '')
|
||||||
|
const appApiProxyTarget = normalizeEnvUrl(env.DEV_PROXY_APP_API_TARGET, 'http://localhost:3001')
|
||||||
|
const h5SdkProxyTarget = normalizeEnvUrl(env.DEV_PROXY_H5_SDK_TARGET, appApiProxyTarget)
|
||||||
|
const sdkProxyTarget = normalizeEnvUrl(env.DEV_PROXY_SDK_TARGET, appApiProxyTarget)
|
||||||
|
const museumAssetsProxyTarget = normalizeEnvUrl(env.DEV_PROXY_MUSEUM_ASSETS_TARGET, 'http://localhost:9000')
|
||||||
|
const minioProxyTarget = normalizeEnvUrl(env.DEV_PROXY_MINIO_TARGET, appApiProxyTarget)
|
||||||
|
const audioProxyTarget = normalizeEnvUrl(env.DEV_PROXY_AUDIO_TARGET, 'http://localhost:19000')
|
||||||
|
|
||||||
|
return {
|
||||||
|
base: normalizeEnvUrl(env.VITE_APP_PUBLIC_BASE, '/'),
|
||||||
plugins: [uni()],
|
plugins: [uni()],
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
proxy: {
|
proxy: {
|
||||||
'/app-api': {
|
'/app-api': {
|
||||||
target: sgsDevServer,
|
target: appApiProxyTarget,
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
},
|
},
|
||||||
'/h5-sdk': {
|
'/h5-sdk': {
|
||||||
target: sgsDevServer,
|
target: h5SdkProxyTarget,
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
},
|
},
|
||||||
'/sdk': {
|
'/sdk': {
|
||||||
target: sgsDevServer,
|
target: sdkProxyTarget,
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
},
|
},
|
||||||
'/museum-assets': {
|
'/museum-assets': {
|
||||||
target: sgsAssetServer,
|
target: museumAssetsProxyTarget,
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
},
|
},
|
||||||
'/minio': {
|
'/minio': {
|
||||||
target: sgsDevServer,
|
target: minioProxyTarget,
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
},
|
},
|
||||||
'/nhm/audio': {
|
'/nhm/audio': {
|
||||||
target: legacyAudioServer,
|
target: audioProxyTarget,
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,4 +64,5 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user