2635 lines
70 KiB
Vue
2635 lines
70 KiB
Vue
<template>
|
||
<GuidePageFrame
|
||
:active-tab="currentTab"
|
||
variant="overlay"
|
||
:show-top-tabs="false"
|
||
@tab-change="handleTabChange"
|
||
>
|
||
<GuideMapShell
|
||
v-if="currentTab === 'guide'"
|
||
ref="guideMapShellRef"
|
||
:search-text="guideSearchText"
|
||
:active-mode="guideShellMode"
|
||
:mode-layout="'status'"
|
||
:mode-status="guideStatusLabel"
|
||
mode-status-tone="glass"
|
||
search-top="20px"
|
||
search-variant="home"
|
||
:show-search="false"
|
||
:show-mode-row="false"
|
||
mode-top="80px"
|
||
:map-type="guideMapType"
|
||
:outdoor-variant="guideOutdoorVariant"
|
||
:indoor-asset-base-url="indoorNavAssetBaseUrl"
|
||
:indoor-model-source="indoorModelSource"
|
||
:floors="guideFloors"
|
||
:normalize-floor-id="normalizeGuideFloorId"
|
||
indoor-initial-view="overview"
|
||
:indoor-view="indoorView"
|
||
:layer-mode="indoorLayerMode"
|
||
:active-floor="activeGuideFloor"
|
||
:visible-poi-ids="searchVisiblePoiIds"
|
||
:target-focus-request="searchTargetFocusRequest"
|
||
layer-mode-toggle-top="154px"
|
||
floor-bottom="193px"
|
||
floor-max-height="206px"
|
||
floor-side="left"
|
||
show-floor-header
|
||
:show-layer-mode-toggle="false"
|
||
:show-floor="is3DMode && !showRoutePlanner"
|
||
:show-zoom-controls="is3DMode"
|
||
zoom-controls-top="calc(100vh - 292px)"
|
||
:route-preview="activeRoutePreview"
|
||
:show-route="Boolean(activeRoutePreview)"
|
||
:outdoor-markers="homeArrivalFeatureEnabled ? arrivalOutdoorMarkers : []"
|
||
:active-outdoor-marker-id="homeArrivalFeatureEnabled ? activeArrivalMarkerId : ''"
|
||
:outdoor-focus-offset-y="homeArrivalFeatureEnabled ? arrivalOutdoorFocusOffsetY : 0"
|
||
:disable-auto-exit="disableIndoorAutoExit"
|
||
@mode-change="handleModeChange"
|
||
@floor-request="handleFloorRequest"
|
||
@floor-change="handleFloorChange"
|
||
@floor-switch-failed="handleFloorSwitchFailed"
|
||
@indoor-view-change="handleIndoorViewChange"
|
||
@poi-click="handleGuidePoiClick"
|
||
@selection-clear="handleGuideSelectionClear"
|
||
@auto-switch="handleAutoSwitch"
|
||
@initial-model-progress="handleInitialModelProgress"
|
||
@initial-model-ready="handleInitialModelReady"
|
||
@initial-model-failed="handleInitialModelFailed"
|
||
@tool-click="handleIndoorToolClick"
|
||
@outdoor-marker-click="handleArrivalMarkerClick"
|
||
>
|
||
<template #overlay>
|
||
<view v-if="homeArrivalFeatureEnabled && guideOutdoorState === 'entrance' && !is3DMode" class="entrance-tip">
|
||
<text class="entrance-tip-text">主入口 约 4 分钟</text>
|
||
</view>
|
||
<view v-if="is3DMode && showIndoorGestureHint" class="indoor-gesture-hint">
|
||
<text class="indoor-gesture-text">{{ indoorGestureHintText }}</text>
|
||
</view>
|
||
</template>
|
||
|
||
<view
|
||
v-if="selectedGuidePoi && !showRoutePlanner && !isSimulatingRoute"
|
||
class="guide-poi-card"
|
||
:class="{ collapsed: isPoiCardCollapsed }"
|
||
>
|
||
<view class="poi-card-handle" @tap.stop="togglePoiCardCollapsed"></view>
|
||
<view v-if="isPoiCardCollapsed" class="poi-card-collapsed" @tap="expandPoiCard">
|
||
<view class="poi-card-copy">
|
||
<text class="poi-card-title">{{ selectedGuidePoi.name }}</text>
|
||
<text class="poi-card-subtitle">{{ selectedGuidePoiCollapsedSubtitle }}</text>
|
||
</view>
|
||
<view class="poi-card-mini-action">
|
||
<text class="poi-card-mini-text">展开</text>
|
||
</view>
|
||
</view>
|
||
<template v-else>
|
||
<view class="poi-card-header">
|
||
<view class="poi-card-copy">
|
||
<text class="poi-card-title">{{ selectedGuidePoi.name }}</text>
|
||
<text class="poi-card-subtitle">{{ selectedGuidePoiSubtitle }}</text>
|
||
</view>
|
||
<view class="poi-card-header-actions">
|
||
<view class="poi-card-soft-action" @tap="collapsePoiCard">
|
||
<text class="poi-card-soft-text">收起</text>
|
||
</view>
|
||
<view class="poi-card-close" @tap="handleGuidePoiCardClose">
|
||
<text class="poi-card-close-text">×</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view
|
||
v-if="selectedGuidePoiExplainHall"
|
||
class="poi-card-actions"
|
||
>
|
||
<view class="poi-action primary" @tap="handleViewSelectedHall">
|
||
<text class="poi-action-text">查看展厅</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
</view>
|
||
|
||
<view
|
||
v-if="showGuideFloatingActions"
|
||
class="guide-quick-actions"
|
||
>
|
||
<view
|
||
v-if="homeArrivalFeatureEnabled"
|
||
class="guide-quick-action"
|
||
:class="{ active: guideQuickActiveAction === 'arrival' }"
|
||
@tap="handleMoreOutdoorNav"
|
||
>
|
||
<view class="guide-quick-icon arrival">
|
||
<view class="guide-quick-pin"></view>
|
||
</view>
|
||
<text class="guide-quick-action-text">来馆</text>
|
||
</view>
|
||
<view
|
||
class="guide-quick-action"
|
||
:class="{ active: guideQuickActiveAction === 'indoor' }"
|
||
@tap="handleMoreRouteGuide"
|
||
>
|
||
<view class="guide-quick-icon indoor">
|
||
<view class="guide-quick-icon-floor top"></view>
|
||
<view class="guide-quick-icon-floor bottom"></view>
|
||
</view>
|
||
<text class="guide-quick-action-text">导览</text>
|
||
</view>
|
||
<view
|
||
class="guide-quick-action"
|
||
:class="{ active: guideQuickActiveAction === 'explain' }"
|
||
@tap="handleExplainQuickTap"
|
||
>
|
||
<view class="guide-quick-icon explain">
|
||
<view class="guide-quick-explain-wave top"></view>
|
||
<view class="guide-quick-explain-wave middle"></view>
|
||
<view class="guide-quick-explain-wave bottom"></view>
|
||
</view>
|
||
<text class="guide-quick-action-text">讲解</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view
|
||
v-if="showGuideHomeDock"
|
||
v-show="homeSearchExpanded"
|
||
class="guide-home-search-scrim"
|
||
@mousedown.stop.prevent="handleHomeSearchOutsideTap"
|
||
@click.stop="handleHomeSearchOutsideTap"
|
||
@touchstart.stop.prevent="handleHomeSearchOutsideTap"
|
||
@tap.stop="handleHomeSearchOutsideTap"
|
||
></view>
|
||
|
||
<view
|
||
v-show="showGuideHomeDock"
|
||
class="guide-home-dock"
|
||
:class="{ expanded: homeSearchExpanded }"
|
||
>
|
||
<PoiSearchPanel
|
||
ref="homeSearchPanelRef"
|
||
variant="home"
|
||
:current-floor-id="activeGuideFloor"
|
||
:current-floor-label="getGuideFloorLabel(activeGuideFloor)"
|
||
@expanded-change="handleHomeSearchExpandedChange"
|
||
@category-mode-change="handleHomeCategoryModeChange"
|
||
@results-change="handleHomeSearchResultsChange"
|
||
@result-tap="handleHomeSearchResultTap"
|
||
@cancel="handleHomeSearchCancel"
|
||
/>
|
||
</view>
|
||
|
||
<RoutePlannerPanel
|
||
v-if="is3DMode"
|
||
:visible="showRoutePlanner"
|
||
:start-point="routeStartPoint"
|
||
:end-point="routeEndPoint"
|
||
:point-options="routePointOptions"
|
||
:has-route-preview="Boolean(activeRoutePreview)"
|
||
:loading="routePlanning"
|
||
:error="routePanelError"
|
||
:summary="routePlanSummary"
|
||
:picker-loading="routeTargetsLoading"
|
||
:picker-error="routeTargetsError"
|
||
:route-ready="routeReady"
|
||
@update:start-point="handleRouteStartPointChange"
|
||
@update:end-point="handleRouteEndPointChange"
|
||
@picker-open="handleRoutePickerOpen"
|
||
@search="handleRoutePickerSearch"
|
||
@swap="handleRouteSwap"
|
||
@clear="handleRouteClear"
|
||
@view-route="handleRouteView"
|
||
@simulate-guide="handleRouteSimulate"
|
||
@back="handleRouteBack"
|
||
/>
|
||
|
||
<view v-if="isSimulatingRoute && activeRoutePreview" class="route-sim-top-pill">
|
||
<text class="route-sim-top-text">
|
||
{{ activeRoutePreview.start.name }} → {{ activeRoutePreview.end.name }}
|
||
</text>
|
||
</view>
|
||
|
||
<view v-if="isSimulatingRoute && activeRoutePreview" class="route-sim-step-bar">
|
||
<text class="route-sim-step-text">{{ routeSimulationStepText }}</text>
|
||
<view class="route-sim-expand" @tap="showRoutePlanner = true">
|
||
<text class="route-sim-expand-text">展开预览</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="homeArrivalFeatureEnabled && guideOutdoorState === 'entrance' && !is3DMode" class="entrance-bottom-card">
|
||
<text class="entrance-card-title">主入口</text>
|
||
<text class="entrance-card-desc">
|
||
可参考主入口位置,也可手动进入馆内3D并选择楼层/区域。
|
||
</text>
|
||
<view class="entrance-actions">
|
||
<view class="entrance-btn secondary" @tap="handleSwitchEntrance">
|
||
<text class="entrance-btn-text">切换入口</text>
|
||
</view>
|
||
<view class="entrance-btn primary" @tap="handleEnter3DMode">
|
||
<text class="entrance-btn-text">进入馆内3D</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</GuideMapShell>
|
||
|
||
<ArrivalPanel
|
||
ref="arrivalPanelRef"
|
||
v-if="homeArrivalFeatureEnabled && currentTab === 'guide'"
|
||
:visible="showArrivalPanel"
|
||
:active-type="activeArrivalType"
|
||
:targets="activeArrivalTargets"
|
||
:selected-target-id="selectedArrivalTargetId"
|
||
:selected-target="selectedArrivalTarget"
|
||
@update:active-type="handleArrivalTypeChange"
|
||
@select-target="handleArrivalTargetSelect"
|
||
@collapsed-change="handleArrivalPanelCollapsedChange"
|
||
@layout-change="handleArrivalPanelLayoutChange"
|
||
/>
|
||
|
||
<view v-else-if="currentTab === 'explain'" class="explain-page">
|
||
<ExplainHallSelect
|
||
:halls="explainHallItems"
|
||
stage="hall"
|
||
:loading="explainLoading"
|
||
:error="explainError"
|
||
@hall-click="handleExplainHallClick"
|
||
@retry="loadExplainHalls"
|
||
@back="handleExplainBack"
|
||
/>
|
||
</view>
|
||
|
||
<view
|
||
v-if="launchOverlayVisible"
|
||
class="app-launch-overlay"
|
||
:class="{ leaving: launchOverlayLeaving }"
|
||
>
|
||
<view class="app-launch-stage">
|
||
<view class="app-launch-content">
|
||
<image
|
||
class="app-launch-logo"
|
||
src="/static/guide/museum-brand-icons.webp"
|
||
mode="widthFix"
|
||
/>
|
||
<text class="app-launch-name-en">SHENZHEN NATURAL HISTORY MUSEUM</text>
|
||
<text class="app-launch-name-cn">深圳自然博物馆</text>
|
||
<progress
|
||
class="app-launch-progress"
|
||
:percent="launchProgress"
|
||
active
|
||
active-mode="forwards"
|
||
:stroke-width="3"
|
||
active-color="#e0e100"
|
||
background-color="#eceee7"
|
||
:show-info="false"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</GuidePageFrame>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||
import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
|
||
import RoutePlannerPanel from '@/components/navigation/RoutePlannerPanel.vue'
|
||
import ArrivalPanel from '@/components/navigation/ArrivalPanel.vue'
|
||
import PoiSearchPanel from '@/components/search/PoiSearchPanel.vue'
|
||
import type {
|
||
RoutePointOption
|
||
} from '@/components/navigation/RoutePointPicker.vue'
|
||
import ExplainHallSelect, {
|
||
type ExplainHallSelectItem
|
||
} from '@/components/explain/ExplainHallSelect.vue'
|
||
import { explainGuideStopListUrl } from '@/utils/explainNavigation'
|
||
import {
|
||
getLastGuideLocationPreviewUrl,
|
||
isGuideTopTab,
|
||
type GuideTopTab
|
||
} from '@/utils/guideTopTabs'
|
||
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
|
||
import {
|
||
guideUseCase
|
||
} from '@/usecases/guideUseCase'
|
||
import {
|
||
guideRouteUseCase
|
||
} from '@/usecases/guideRouteUseCase'
|
||
import {
|
||
explainUseCase
|
||
} from '@/usecases/explainUseCase'
|
||
import type {
|
||
GuideRenderPoi
|
||
} from '@/domain/guideModel'
|
||
import type {
|
||
PoiCategoryResultState,
|
||
PoiSearchContext
|
||
} from '@/domain/poiSearch'
|
||
import {
|
||
type TargetPoiFocusRequestViewModel
|
||
} from '@/view-models/guideViewModels'
|
||
import {
|
||
useGuideModelState,
|
||
type GuideModelResetReason,
|
||
type GuideModelResetRequest
|
||
} from '@/composables/useGuideModelState'
|
||
import type { ResetViewBaselineResult } from '@/components/map/ThreeMap.vue'
|
||
import {
|
||
ARRIVAL_TARGETS_BY_TYPE,
|
||
type ArrivalSearchTarget,
|
||
type ArrivalTargetType
|
||
} from '@/data/arrivalSearchTargets'
|
||
import type {
|
||
GuideRouteResult,
|
||
GuideRouteTarget,
|
||
MuseumFloor,
|
||
MuseumHall,
|
||
MuseumPoi
|
||
} from '@/domain/museum'
|
||
import { getFloorSortLevel } from '@/domain/guideFloor'
|
||
|
||
type GuideIndoorView = 'overview' | 'floor' | 'multi'
|
||
type LayerDisplayMode = 'single' | 'multi'
|
||
|
||
const guideModelState = useGuideModelState()
|
||
|
||
const isIndexHashRoute = () => {
|
||
if (typeof window === 'undefined') return false
|
||
|
||
const hash = window.location.hash || ''
|
||
return (
|
||
!hash
|
||
|| hash === '#'
|
||
|| hash === '#/'
|
||
|| hash.startsWith('#/?')
|
||
|| hash.startsWith('#/pages/index/index')
|
||
)
|
||
}
|
||
|
||
const topTabFromHash = () => {
|
||
if (typeof window === 'undefined') return null
|
||
if (!isIndexHashRoute()) return null
|
||
|
||
const queryString = window.location.hash.split('?')[1] || ''
|
||
const tab = new URLSearchParams(queryString).get('tab')
|
||
return isGuideTopTab(tab) ? tab : null
|
||
}
|
||
|
||
const launchOverlayMinDisplayMs = 700
|
||
const launchOverlayGuideTimeoutMs = 12000
|
||
const launchOverlayNonGuideTimeoutMs = 900
|
||
|
||
const shouldShowLaunchOverlay = () => {
|
||
if (typeof window === 'undefined') return false
|
||
return isIndexHashRoute()
|
||
}
|
||
|
||
const initialTopTab = (): GuideTopTab => topTabFromHash() || 'guide'
|
||
// 到馆服务暂不对外开放;保留实现,后续仅需恢复此开关即可。
|
||
const homeArrivalFeatureEnabled = false
|
||
|
||
// 3D 模式状态
|
||
const is3DMode = ref(true)
|
||
const guideOutdoorState = ref<'home' | 'entrance'>('home')
|
||
const indoorView = ref<GuideIndoorView>('overview')
|
||
const activeGuideFloor = ref('')
|
||
const requestedFloorId = ref('')
|
||
const requestedFloorLabel = ref('')
|
||
const loadingFloorId = ref('')
|
||
const renderedFloorId = ref('')
|
||
const failedFloorId = ref('')
|
||
const selectedGuidePoi = ref<GuideRenderPoi | null>(null)
|
||
const selectedGuidePoiExplainHall = ref<MuseumHall | null>(null)
|
||
let selectedGuidePoiExplainHallRequestId = 0
|
||
|
||
// 状态
|
||
const currentTab = ref<GuideTopTab>(initialTopTab())
|
||
const launchOverlayVisible = ref(shouldShowLaunchOverlay())
|
||
const launchOverlayLeaving = ref(false)
|
||
const launchProgress = ref(8)
|
||
const launchInitialModelSettled = ref(false)
|
||
const showRoutePlanner = ref(false)
|
||
const routeStartPoint = ref<RoutePointOption | null>(null)
|
||
const routeEndPoint = ref<RoutePointOption | null>(null)
|
||
const routePointOptions = ref<RoutePointOption[]>([])
|
||
const routeTargetsLoading = ref(false)
|
||
const routeTargetsError = ref('')
|
||
const routePlanning = ref(false)
|
||
const routePlanError = ref('')
|
||
const routePreviewUnavailableMessage = ref('')
|
||
const routeReady = ref(false)
|
||
const activeRoutePreview = ref<GuideRouteResult | null>(null)
|
||
const isSimulatingRoute = ref(false)
|
||
const isPoiCardCollapsed = ref(false)
|
||
const returningToOverview = ref(false)
|
||
|
||
// GuideMapShell 组件引用,用于显式调用路线清除方法
|
||
const guideMapShellRef = ref<{
|
||
clearRoute?: () => void
|
||
switchFloor?: (floorId: string) => Promise<void> | void
|
||
showOverview?: () => Promise<void> | void
|
||
resetToViewBaseline?: (options: {
|
||
view: 'overview' | 'floor'
|
||
floorId?: string
|
||
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
||
}) => Promise<ResetViewBaselineResult> | ResetViewBaselineResult
|
||
resetToInitialState?: () => Promise<boolean> | boolean
|
||
} | null>(null)
|
||
const homeSearchPanelRef = ref<{
|
||
collapseHomePanel?: () => void
|
||
resetSearchState?: () => Promise<void> | void
|
||
returnToCollapsedAfterDetail?: () => Promise<void>
|
||
restoreResultListScroll?: () => Promise<void>
|
||
} | null>(null)
|
||
const homeSearchExpanded = ref(false)
|
||
const homeCategoryModeActive = ref(false)
|
||
const searchVisiblePoiIds = ref<string[] | null>(null)
|
||
const searchTargetFocusRequest = ref<TargetPoiFocusRequestViewModel | null>(null)
|
||
type PoiSearchOverlayHistoryState = {
|
||
museumGuideOverlay?: 'poi-search'
|
||
[key: string]: unknown
|
||
}
|
||
|
||
let poiSearchHistoryPushed = false
|
||
let removingPoiSearchHistory = false
|
||
|
||
let launchOverlayFallbackTimer: ReturnType<typeof setTimeout> | null = null
|
||
let launchOverlayFadeTimer: ReturnType<typeof setTimeout> | null = null
|
||
let launchOverlayHideDelayTimer: ReturnType<typeof setTimeout> | null = null
|
||
let launchOverlayStartedAt = 0
|
||
let launchOverlayRunId = 0
|
||
|
||
const clearLaunchTimers = () => {
|
||
if (launchOverlayFallbackTimer) {
|
||
clearTimeout(launchOverlayFallbackTimer)
|
||
launchOverlayFallbackTimer = null
|
||
}
|
||
|
||
if (launchOverlayFadeTimer) {
|
||
clearTimeout(launchOverlayFadeTimer)
|
||
launchOverlayFadeTimer = null
|
||
}
|
||
|
||
if (launchOverlayHideDelayTimer) {
|
||
clearTimeout(launchOverlayHideDelayTimer)
|
||
launchOverlayHideDelayTimer = null
|
||
}
|
||
}
|
||
|
||
const completeHideLaunchOverlay = () => {
|
||
if (!launchOverlayVisible.value) return
|
||
|
||
const runId = launchOverlayRunId
|
||
launchProgress.value = 100
|
||
launchOverlayLeaving.value = true
|
||
clearLaunchTimers()
|
||
|
||
launchOverlayFadeTimer = setTimeout(() => {
|
||
if (runId !== launchOverlayRunId) return
|
||
launchOverlayVisible.value = false
|
||
launchOverlayLeaving.value = false
|
||
launchOverlayFadeTimer = null
|
||
}, 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)
|
||
const runId = launchOverlayRunId
|
||
launchOverlayHideDelayTimer = setTimeout(() => {
|
||
if (runId !== launchOverlayRunId) return
|
||
launchOverlayHideDelayTimer = null
|
||
completeHideLaunchOverlay()
|
||
}, waitMs)
|
||
return
|
||
}
|
||
|
||
completeHideLaunchOverlay()
|
||
}
|
||
|
||
const shouldWaitForLaunchModel = () => (
|
||
currentTab.value === 'guide'
|
||
&& is3DMode.value
|
||
)
|
||
|
||
// The launch overlay is independent from the selected map business mode. Releasing it
|
||
// must leave ThreeMap mounted so its loading/error/retry lifecycle can continue.
|
||
const releaseLaunchOverlayWithoutInitialModel = (reason: string, event?: { view: GuideIndoorView; floorId?: string; message?: string; elapsedMs?: number }) => {
|
||
if (!launchOverlayVisible.value || launchOverlayLeaving.value) return
|
||
|
||
console.warn('首页前置加载页未等待初始三维模型,已关闭启动遮罩:', {
|
||
reason,
|
||
event
|
||
})
|
||
|
||
launchInitialModelSettled.value = true
|
||
launchProgress.value = Math.max(launchProgress.value, 96)
|
||
hideLaunchOverlay()
|
||
}
|
||
|
||
const startLaunchOverlay = () => {
|
||
if (!launchOverlayVisible.value) return
|
||
|
||
clearLaunchTimers()
|
||
const runId = ++launchOverlayRunId
|
||
launchInitialModelSettled.value = false
|
||
launchOverlayStartedAt = Date.now()
|
||
launchProgress.value = shouldWaitForLaunchModel() ? 8 : 68
|
||
|
||
const timeoutMs = shouldWaitForLaunchModel()
|
||
? launchOverlayGuideTimeoutMs
|
||
: launchOverlayNonGuideTimeoutMs
|
||
|
||
launchOverlayFallbackTimer = setTimeout(() => {
|
||
if (runId !== launchOverlayRunId) return
|
||
if (shouldWaitForLaunchModel() && !launchInitialModelSettled.value) {
|
||
releaseLaunchOverlayWithoutInitialModel('timeout')
|
||
return
|
||
}
|
||
hideLaunchOverlay()
|
||
}, timeoutMs)
|
||
}
|
||
|
||
const handleInitialModelProgress = (event: { progress: number; message: string; view: GuideIndoorView; floorId?: string; elapsedMs?: number }) => {
|
||
if (!launchOverlayVisible.value || launchInitialModelSettled.value || !shouldWaitForLaunchModel()) return
|
||
|
||
launchProgress.value = Math.min(99, Math.max(8, event.progress))
|
||
}
|
||
|
||
const settleLaunchOverlayByModel = (status: 'ready' | 'failed', event?: { view: GuideIndoorView; floorId?: string; message?: string; elapsedMs?: number }) => {
|
||
launchInitialModelSettled.value = true
|
||
|
||
if (status === 'failed') {
|
||
releaseLaunchOverlayWithoutInitialModel('failed', event)
|
||
return
|
||
} else {
|
||
launchProgress.value = 100
|
||
console.log('首页前置加载页收到初始模型加载完成:', event)
|
||
}
|
||
|
||
hideLaunchOverlay()
|
||
}
|
||
|
||
const showArrivalPanel = ref(false)
|
||
const showArrivalPanelCollapsed = ref(false)
|
||
const arrivalPanelRef = ref<InstanceType<typeof ArrivalPanel> | null>(null)
|
||
const activeArrivalType = ref<ArrivalTargetType>('bus')
|
||
const selectedArrivalTargetId = ref('')
|
||
const arrivalPanelHeight = ref(0)
|
||
|
||
const indoorNavAssetBaseUrl = guideUseCase.getAssetBaseUrl()
|
||
const indoorModelSource = guideUseCase.getModelSource()
|
||
const guideFloors = ref<MuseumFloor[]>([])
|
||
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
|
||
const getPreferredInitialGuideFloorId = (floors: MuseumFloor[]) => (
|
||
floors.find((floor) => (
|
||
getFloorSortLevel(floor) === 1
|
||
|| normalizeGuideFloorId(floor.id) === 'L1'
|
||
|| normalizeGuideFloorId(floor.label) === 'L1'
|
||
))?.id
|
||
|| floors[0]?.id
|
||
|| ''
|
||
)
|
||
const getGuideFloorById = (floorId: string) => (
|
||
guideFloors.value.find((floor) => floor.id === floorId)
|
||
)
|
||
const getGuideFloorLabel = (floorId: string) => (
|
||
getGuideFloorById(floorId)?.label || floorId
|
||
)
|
||
const indoorLayerMode = computed<LayerDisplayMode>(() => (
|
||
indoorView.value === 'multi' ? 'multi' : 'single'
|
||
))
|
||
const showIndoorGestureHint = ref(true)
|
||
const indoorGestureHintText = ref('单指平移 · 双指缩放/平移 · 点选标记查看位置')
|
||
let indoorGestureHintTimer: ReturnType<typeof setTimeout> | null = null
|
||
|
||
const showIndoorHint = (text = '单指平移 · 双指缩放/平移 · 点选标记查看位置', durationMs = 5200) => {
|
||
indoorGestureHintText.value = text
|
||
showIndoorGestureHint.value = is3DMode.value
|
||
|
||
if (indoorGestureHintTimer) {
|
||
clearTimeout(indoorGestureHintTimer)
|
||
}
|
||
|
||
indoorGestureHintTimer = setTimeout(() => {
|
||
showIndoorGestureHint.value = false
|
||
indoorGestureHintTimer = null
|
||
}, durationMs)
|
||
}
|
||
|
||
const routePlanSummary = computed(() => {
|
||
if (activeRoutePreview.value) {
|
||
const floorCount = new Set(activeRoutePreview.value.floorSegments.map((segment) => segment.floorId)).size
|
||
const floorText = floorCount > 1 ? `跨 ${floorCount} 层` : activeRoutePreview.value.start.floorLabel
|
||
return `${floorText} · 约 ${Math.round(activeRoutePreview.value.distanceMeters)} 米`
|
||
}
|
||
|
||
if (routeStartPoint.value && routeEndPoint.value) {
|
||
return `${routeStartPoint.value.floorLabel} 到 ${routeEndPoint.value.floorLabel}`
|
||
}
|
||
|
||
return ''
|
||
})
|
||
|
||
const routePanelError = computed(() => routePlanError.value)
|
||
|
||
const disableIndoorAutoExit = computed(() => (
|
||
Boolean(selectedGuidePoi.value)
|
||
|| Boolean(activeRoutePreview.value)
|
||
|| showRoutePlanner.value
|
||
|| isSimulatingRoute.value
|
||
))
|
||
|
||
const showGuideHomeDock = computed(() => (
|
||
currentTab.value === 'guide'
|
||
&& is3DMode.value
|
||
&& !showRoutePlanner.value
|
||
&& !showArrivalPanel.value
|
||
&& !selectedGuidePoi.value
|
||
&& !isSimulatingRoute.value
|
||
&& !(guideOutdoorState.value === 'entrance' && !is3DMode.value)
|
||
))
|
||
|
||
const showGuideFloatingActions = computed(() => (
|
||
currentTab.value === 'guide'
|
||
&& !showRoutePlanner.value
|
||
&& (!showArrivalPanel.value || showArrivalPanelCollapsed.value)
|
||
&& !selectedGuidePoi.value
|
||
&& !isSimulatingRoute.value
|
||
&& (!showGuideHomeDock.value || !homeSearchExpanded.value)
|
||
))
|
||
|
||
const activeArrivalTargets = computed(() => ARRIVAL_TARGETS_BY_TYPE[activeArrivalType.value])
|
||
|
||
const selectedArrivalTarget = computed(() => (
|
||
activeArrivalTargets.value.find((target) => target.id === selectedArrivalTargetId.value)
|
||
|| activeArrivalTargets.value[0]
|
||
|| null
|
||
))
|
||
|
||
const arrivalOutdoorMarkers = computed(() => {
|
||
if (!showArrivalPanel.value || is3DMode.value) return []
|
||
|
||
return activeArrivalTargets.value.map((target) => ({
|
||
id: target.id,
|
||
title: target.title,
|
||
subtitle: target.subtitle,
|
||
latitude: target.latitude,
|
||
longitude: target.longitude
|
||
}))
|
||
})
|
||
|
||
const activeArrivalMarkerId = computed(() => (
|
||
showArrivalPanel.value && !is3DMode.value ? selectedArrivalTarget.value?.id || '' : ''
|
||
))
|
||
|
||
const arrivalOutdoorFocusOffsetY = computed(() => {
|
||
if (!showArrivalPanel.value || showArrivalPanelCollapsed.value || is3DMode.value) return 0
|
||
return Math.min(260, Math.max(0, Math.round(arrivalPanelHeight.value / 2 + 24)))
|
||
})
|
||
|
||
const guideQuickActiveAction = computed<'indoor' | 'arrival' | 'explain'>(() => {
|
||
if (currentTab.value === 'explain') return 'explain'
|
||
if (showArrivalPanel.value) return 'arrival'
|
||
return is3DMode.value ? 'indoor' : 'arrival'
|
||
})
|
||
|
||
const toRoutePointOption = (target: GuideRouteTarget): RoutePointOption => ({
|
||
poiId: target.poiId,
|
||
name: target.name,
|
||
floorId: target.floorId,
|
||
floorLabel: target.floorLabel,
|
||
categoryLabel: target.categoryLabel
|
||
})
|
||
|
||
const selectedGuidePoiSubtitle = computed(() => {
|
||
const poi = selectedGuidePoi.value
|
||
if (!poi) return ''
|
||
const floor = guideFloors.value.find((item) => item.id === poi.floorId)
|
||
const floorLabel = floor?.label || poi.floorId
|
||
return `${floorLabel} · ${poi.primaryCategoryZh || '位置预览'}`
|
||
})
|
||
|
||
const isGuidePoiHallLike = (poi: GuideRenderPoi | null) => Boolean(
|
||
poi
|
||
&& (
|
||
poi.kind === 'hall'
|
||
|| poi.primaryCategory === 'touring_poi'
|
||
|| poi.primaryCategory === 'exhibition_hall'
|
||
|| poi.primaryCategory === 'exhibition_hall_entrance'
|
||
)
|
||
)
|
||
|
||
const selectedGuidePoiIsHall = computed(() => isGuidePoiHallLike(selectedGuidePoi.value))
|
||
|
||
const selectedGuidePoiCollapsedSubtitle = computed(() => (
|
||
selectedGuidePoiExplainHall.value ? '点按展开查看展厅' : selectedGuidePoiSubtitle.value
|
||
))
|
||
|
||
const routeSimulationStepText = computed(() => {
|
||
if (!activeRoutePreview.value) return '位置预览 · 查看位置关系'
|
||
const hasConnector = activeRoutePreview.value.connectorPoints.length > 0
|
||
return hasConnector ? '位置预览 · 跨楼层位置关系' : '位置预览 · 同层位置关系'
|
||
})
|
||
|
||
const explainHallItems = ref<ExplainHallSelectItem[]>([])
|
||
const explainLoading = ref(false)
|
||
const explainError = ref('')
|
||
let explainLoadPromise: Promise<void> | null = null
|
||
|
||
// Tab 切换处理
|
||
const syncTopTabToUrl = (tabId: GuideTopTab) => {
|
||
if (typeof window === 'undefined') return
|
||
if (!isIndexHashRoute()) return
|
||
|
||
const currentHash = window.location.hash || ''
|
||
const queryStart = currentHash.indexOf('?')
|
||
const params = new URLSearchParams(queryStart >= 0 ? currentHash.slice(queryStart + 1) : '')
|
||
params.set('tab', tabId)
|
||
const url = `#/pages/index/index?${params.toString()}`
|
||
if (window.location.hash !== url) {
|
||
window.history.replaceState(window.history.state, '', url)
|
||
}
|
||
}
|
||
|
||
const isPoiSearchOverlayHistoryState = (state: unknown): state is PoiSearchOverlayHistoryState => (
|
||
Boolean(
|
||
state
|
||
&& typeof state === 'object'
|
||
&& (state as PoiSearchOverlayHistoryState).museumGuideOverlay === 'poi-search'
|
||
)
|
||
)
|
||
|
||
const canManagePoiSearchOverlayHistory = () => (
|
||
typeof window !== 'undefined'
|
||
&& isIndexHashRoute()
|
||
&& isEmbeddedInWechatMiniProgram()
|
||
)
|
||
|
||
const removePoiSearchOverlayFromCurrentHistoryState = () => {
|
||
if (!canManagePoiSearchOverlayHistory()) return
|
||
if (!isPoiSearchOverlayHistoryState(window.history.state)) return
|
||
|
||
const nextState = { ...window.history.state }
|
||
delete nextState.museumGuideOverlay
|
||
window.history.replaceState(nextState, '', window.location.href)
|
||
}
|
||
|
||
const pushPoiSearchOverlayHistory = () => {
|
||
if (!canManagePoiSearchOverlayHistory() || !homeSearchExpanded.value) return
|
||
if (poiSearchHistoryPushed) return
|
||
|
||
if (isPoiSearchOverlayHistoryState(window.history.state)) {
|
||
poiSearchHistoryPushed = true
|
||
return
|
||
}
|
||
|
||
window.history.pushState({
|
||
...(window.history.state || {}),
|
||
museumGuideOverlay: 'poi-search'
|
||
} satisfies PoiSearchOverlayHistoryState, '', window.location.href)
|
||
poiSearchHistoryPushed = true
|
||
}
|
||
|
||
const removePoiSearchOverlayHistory = () => {
|
||
if (!canManagePoiSearchOverlayHistory() || !poiSearchHistoryPushed) return
|
||
|
||
if (!isPoiSearchOverlayHistoryState(window.history.state)) {
|
||
poiSearchHistoryPushed = false
|
||
return
|
||
}
|
||
|
||
poiSearchHistoryPushed = false
|
||
removingPoiSearchHistory = true
|
||
window.history.back()
|
||
}
|
||
|
||
const collapsePoiSearchAfterHistoryBack = async () => {
|
||
homeSearchPanelRef.value?.collapseHomePanel?.()
|
||
await homeSearchPanelRef.value?.resetSearchState?.()
|
||
clearHomeSearchMapState()
|
||
selectedGuidePoi.value = null
|
||
isPoiCardCollapsed.value = false
|
||
homeSearchExpanded.value = false
|
||
}
|
||
|
||
const handlePoiSearchHistoryPopState = (event: PopStateEvent) => {
|
||
if (!canManagePoiSearchOverlayHistory()) return
|
||
|
||
if (removingPoiSearchHistory) {
|
||
removingPoiSearchHistory = false
|
||
poiSearchHistoryPushed = false
|
||
if (homeSearchExpanded.value) pushPoiSearchOverlayHistory()
|
||
return
|
||
}
|
||
|
||
if (!homeSearchExpanded.value) return
|
||
if (isPoiSearchOverlayHistoryState(event.state)) {
|
||
poiSearchHistoryPushed = true
|
||
return
|
||
}
|
||
|
||
poiSearchHistoryPushed = false
|
||
void collapsePoiSearchAfterHistoryBack()
|
||
}
|
||
|
||
const loadTopTabData = (tabId: GuideTopTab) => {
|
||
if (tabId === 'explain') {
|
||
void loadExplainHalls()
|
||
} else {
|
||
void loadGuideFloors()
|
||
}
|
||
}
|
||
|
||
const handleTabChange = (tabId: GuideTopTab) => {
|
||
if (tabId === 'guide' && currentTab.value === 'explain') {
|
||
const lastLocationPreviewUrl = getLastGuideLocationPreviewUrl()
|
||
if (lastLocationPreviewUrl) {
|
||
uni.reLaunch({
|
||
url: lastLocationPreviewUrl
|
||
})
|
||
return
|
||
}
|
||
}
|
||
|
||
currentTab.value = tabId
|
||
syncTopTabToUrl(tabId)
|
||
loadTopTabData(tabId)
|
||
|
||
console.log('切换标签:', tabId)
|
||
|
||
// 关闭可能打开的面板
|
||
showRoutePlanner.value = false
|
||
isSimulatingRoute.value = false
|
||
isPoiCardCollapsed.value = false
|
||
closeArrivalPanel()
|
||
}
|
||
|
||
const syncTopTabFromHash = () => {
|
||
if (!isIndexHashRoute()) return
|
||
|
||
const tab = topTabFromHash()
|
||
if (tab) {
|
||
const tabChanged = currentTab.value !== tab
|
||
currentTab.value = tab
|
||
loadTopTabData(tab)
|
||
if (tabChanged) {
|
||
showRoutePlanner.value = false
|
||
isSimulatingRoute.value = false
|
||
isPoiCardCollapsed.value = false
|
||
closeArrivalPanel()
|
||
}
|
||
syncTopTabToUrl(tab)
|
||
return
|
||
}
|
||
|
||
syncTopTabToUrl(currentTab.value)
|
||
loadTopTabData(currentTab.value)
|
||
}
|
||
|
||
onMounted(() => {
|
||
startLaunchOverlay()
|
||
showIndoorHint()
|
||
|
||
if (typeof window === 'undefined') return
|
||
|
||
syncTopTabFromHash()
|
||
removePoiSearchOverlayFromCurrentHistoryState()
|
||
window.addEventListener('hashchange', syncTopTabFromHash)
|
||
window.addEventListener('popstate', handlePoiSearchHistoryPopState)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
launchOverlayRunId += 1
|
||
clearLaunchTimers()
|
||
|
||
if (indoorGestureHintTimer) {
|
||
clearTimeout(indoorGestureHintTimer)
|
||
indoorGestureHintTimer = null
|
||
}
|
||
|
||
if (typeof window === 'undefined') return
|
||
|
||
window.removeEventListener('hashchange', syncTopTabFromHash)
|
||
window.removeEventListener('popstate', handlePoiSearchHistoryPopState)
|
||
})
|
||
|
||
onLoad((options: any = {}) => {
|
||
const routeTab = Array.isArray(options.tab) ? options.tab[0] : options.tab
|
||
const tab = topTabFromHash() || routeTab
|
||
if (isGuideTopTab(tab)) {
|
||
currentTab.value = tab
|
||
} else {
|
||
syncTopTabToUrl(currentTab.value)
|
||
}
|
||
|
||
loadTopTabData(currentTab.value)
|
||
})
|
||
|
||
const loadGuideFloors = async () => {
|
||
try {
|
||
const floors = await guideUseCase.getFloors()
|
||
guideFloors.value = floors
|
||
const normalizedActiveFloorId = activeGuideFloor.value
|
||
? normalizeGuideFloorId(activeGuideFloor.value)
|
||
: ''
|
||
activeGuideFloor.value = floors.find((floor) => floor.id === normalizedActiveFloorId)?.id
|
||
|| getPreferredInitialGuideFloorId(floors)
|
||
|| activeGuideFloor.value
|
||
renderedFloorId.value = activeGuideFloor.value
|
||
} catch (error) {
|
||
console.error('加载导览楼层失败:', error)
|
||
guideFloors.value = []
|
||
}
|
||
}
|
||
|
||
const loadExplainHalls = async () => {
|
||
if (explainHallItems.value.length) return
|
||
if (explainLoadPromise) return explainLoadPromise
|
||
|
||
explainLoading.value = true
|
||
explainError.value = ''
|
||
|
||
explainLoadPromise = (async () => {
|
||
try {
|
||
const halls = await explainUseCase.loadExplainHalls()
|
||
const summaries = await explainUseCase.loadExplainHallSummaries(halls.map((hall) => hall.id))
|
||
explainHallItems.value = buildExplainHallItems(halls, summaries)
|
||
} catch (error) {
|
||
console.error('加载讲解内容失败:', error)
|
||
explainError.value = '讲解内容加载失败,请稍后重试'
|
||
explainHallItems.value = []
|
||
} finally {
|
||
explainLoading.value = false
|
||
explainLoadPromise = null
|
||
}
|
||
})()
|
||
|
||
return explainLoadPromise
|
||
}
|
||
|
||
const normalizeExplainKeyword = (value?: string) => (value || '').trim().toLowerCase()
|
||
|
||
const buildExplainHallItems = (
|
||
halls: Awaited<ReturnType<typeof explainUseCase.loadExplainHalls>>,
|
||
summaries: Awaited<ReturnType<typeof explainUseCase.loadExplainHallSummaries>> = {}
|
||
): ExplainHallSelectItem[] => {
|
||
return halls.map((hall) => {
|
||
const keywordParts = [
|
||
hall.name,
|
||
hall.floorLabel,
|
||
hall.description,
|
||
hall.area
|
||
].filter(Boolean) as string[]
|
||
|
||
return {
|
||
id: hall.id,
|
||
name: hall.name,
|
||
floorLabel: hall.floorLabel,
|
||
image: hall.image,
|
||
explainCount: hall.exhibitCount || 0,
|
||
guideStopCount: summaries[hall.id]?.guideStopCount,
|
||
searchText: normalizeExplainKeyword(keywordParts.join(' '))
|
||
}
|
||
})
|
||
}
|
||
|
||
const handleFloorRequest = ({ floorId, floorLabel }: { floorId: string; floorLabel: string }) => {
|
||
requestedFloorId.value = floorId
|
||
requestedFloorLabel.value = floorLabel
|
||
loadingFloorId.value = floorId
|
||
failedFloorId.value = ''
|
||
}
|
||
|
||
const handleFloorChange = (floorId: string) => {
|
||
activeGuideFloor.value = floorId
|
||
renderedFloorId.value = floorId
|
||
if (loadingFloorId.value === floorId) {
|
||
loadingFloorId.value = ''
|
||
}
|
||
if (failedFloorId.value === floorId) {
|
||
failedFloorId.value = ''
|
||
}
|
||
indoorView.value = 'floor'
|
||
selectedGuidePoi.value = null
|
||
isPoiCardCollapsed.value = false
|
||
showIndoorHint(`已切换到 ${getGuideFloorLabel(floorId)},单指平移、双指可缩放`, 3200)
|
||
console.log('楼层渲染完成:', floorId)
|
||
}
|
||
|
||
const handleFloorSwitchFailed = ({ floorId, floorLabel }: { floorId: string; floorLabel: string }) => {
|
||
if (loadingFloorId.value === floorId) {
|
||
loadingFloorId.value = ''
|
||
}
|
||
failedFloorId.value = floorId
|
||
showIndoorHint(`${floorLabel || getGuideFloorLabel(floorId)} 加载失败,请稍后重试`, 3600)
|
||
console.warn('楼层渲染失败:', floorId)
|
||
}
|
||
|
||
const handleIndoorViewChange = (view: GuideIndoorView) => {
|
||
indoorView.value = view
|
||
if (view !== 'floor') {
|
||
selectedGuidePoi.value = null
|
||
isPoiCardCollapsed.value = false
|
||
}
|
||
|
||
if (view === 'multi') {
|
||
showIndoorHint('已切换多层视图,可点选楼层查看', 3200)
|
||
} else if (view === 'floor') {
|
||
showIndoorHint('已切换单层视图,可点选标记查看位置', 3200)
|
||
}
|
||
}
|
||
|
||
const handleAutoSwitch = (event: { from: 'overview' | 'floor'; to: 'overview' | 'floor'; trigger: string; distance: number }) => {
|
||
console.log('自动切换视图:', event)
|
||
indoorView.value = event.to
|
||
if (event.to !== 'floor') {
|
||
selectedGuidePoi.value = null
|
||
isPoiCardCollapsed.value = false
|
||
}
|
||
showIndoorHint(event.to === 'floor' ? '已进入单层视图,可点选标记查看位置' : '已返回建筑外观', 3600)
|
||
}
|
||
|
||
const handleInitialModelReady = (event: { view: GuideIndoorView; floorId?: string; elapsedMs?: number }) => {
|
||
const readyFloorId = event.floorId || activeGuideFloor.value
|
||
if (event.view === 'overview' && readyFloorId) {
|
||
activeGuideFloor.value = readyFloorId
|
||
renderedFloorId.value = readyFloorId
|
||
guideModelState.initializeModel({ view: event.view, floorId: readyFloorId })
|
||
}
|
||
settleLaunchOverlayByModel('ready', event)
|
||
const context = guideModelState.consumePendingViewBaselineReset()
|
||
if (context) {
|
||
void resetGuideModelToFloorBaseline({
|
||
rendererReason: context.rendererReason,
|
||
floorId: context.floorId,
|
||
request: context
|
||
})
|
||
}
|
||
}
|
||
|
||
const handleInitialModelFailed = (event: { view: GuideIndoorView; floorId?: string; message: string; elapsedMs?: number }) => {
|
||
settleLaunchOverlayByModel('failed', event)
|
||
}
|
||
|
||
const handleIndoorToolClick = (tool: string) => {
|
||
if (!is3DMode.value) return
|
||
|
||
if (tool === '复位') {
|
||
showIndoorHint('已复位视角,单指平移、双指可缩放', 2800)
|
||
} else if (tool === '俯视') {
|
||
showIndoorHint('已切换俯视视角,可点选标记查看位置', 2800)
|
||
} else if (tool === '斜视') {
|
||
showIndoorHint('已切换斜视视角,可查看空间层次', 2800)
|
||
}
|
||
}
|
||
|
||
const handleGuidePoiClick = (poi: GuideRenderPoi) => {
|
||
selectedGuidePoi.value = poi
|
||
isPoiCardCollapsed.value = !selectedGuidePoiIsHall.value
|
||
indoorView.value = 'floor'
|
||
isSimulatingRoute.value = false
|
||
activeGuideFloor.value = poi.floorId
|
||
renderedFloorId.value = poi.floorId
|
||
showIndoorHint(`${poi.name} · ${getGuideFloorLabel(poi.floorId)}`, 3000)
|
||
}
|
||
|
||
const handleGuideSelectionClear = () => {
|
||
selectedGuidePoi.value = null
|
||
isPoiCardCollapsed.value = false
|
||
}
|
||
|
||
const handleGuidePoiCardClose = () => {
|
||
const floorId = selectedGuidePoi.value?.floorId
|
||
if (!floorId) {
|
||
handleGuideSelectionClear()
|
||
return
|
||
}
|
||
|
||
void resetGuideModelToFloorBaseline({
|
||
rendererReason: 'floor-reset',
|
||
floorId
|
||
})
|
||
}
|
||
|
||
const collapsePoiCard = () => {
|
||
isPoiCardCollapsed.value = true
|
||
}
|
||
|
||
const expandPoiCard = () => {
|
||
isPoiCardCollapsed.value = false
|
||
}
|
||
|
||
const togglePoiCardCollapsed = () => {
|
||
isPoiCardCollapsed.value = !isPoiCardCollapsed.value
|
||
}
|
||
|
||
const normalizeHallMatchText = (value?: string) => (value || '')
|
||
.trim()
|
||
.replace(/[\s()()【】[\]_-]/g, '')
|
||
.replace(/^展厅\d+/, '')
|
||
.toLowerCase()
|
||
|
||
const resolvePoiExplainHall = async (poi: GuideRenderPoi): Promise<MuseumHall | null> => {
|
||
const halls = await explainUseCase.listHalls()
|
||
const matchedByPoiId = halls.find((hall) => (
|
||
hall.poiId === poi.id
|
||
|| hall.location?.poiId === poi.id
|
||
))
|
||
if (matchedByPoiId) return matchedByPoiId
|
||
|
||
const hallId = poi.hallId
|
||
if (hallId) {
|
||
const matchedById = halls.find((hall) => hall.id === hallId)
|
||
if (matchedById) return matchedById
|
||
}
|
||
|
||
const matchedByEntrance = halls.find((hall) => (
|
||
(poi.entrances || []).some((entrance) => (
|
||
hall.poiId === entrance.sourcePlaceId
|
||
|| hall.poiId === entrance.id
|
||
))
|
||
))
|
||
if (matchedByEntrance) return matchedByEntrance
|
||
|
||
const poiHallName = normalizeHallMatchText(poi.hallName || poi.name)
|
||
return halls.find((hall) => {
|
||
const hallName = normalizeHallMatchText(hall.name)
|
||
return hallName && poiHallName && hallName === poiHallName
|
||
}) || null
|
||
}
|
||
|
||
watch(selectedGuidePoi, (poi) => {
|
||
const requestId = ++selectedGuidePoiExplainHallRequestId
|
||
selectedGuidePoiExplainHall.value = null
|
||
if (!poi || !isGuidePoiHallLike(poi)) return
|
||
|
||
void resolvePoiExplainHall(poi)
|
||
.then((hall) => {
|
||
if (
|
||
requestId !== selectedGuidePoiExplainHallRequestId
|
||
|| selectedGuidePoi.value?.id !== poi.id
|
||
) return
|
||
|
||
selectedGuidePoiExplainHall.value = hall
|
||
})
|
||
.catch((error) => {
|
||
if (requestId !== selectedGuidePoiExplainHallRequestId) return
|
||
console.warn('点位讲解展厅关联解析失败:', error)
|
||
})
|
||
}, { flush: 'sync' })
|
||
|
||
const navigateToPoiDetail = ({
|
||
url,
|
||
floorId,
|
||
failureMessage
|
||
}: {
|
||
url: string
|
||
floorId: string
|
||
failureMessage: string
|
||
}) => {
|
||
const returnContext = guideModelState.beginPoiDetailReturn(floorId)
|
||
uni.navigateTo({
|
||
url,
|
||
success: () => {
|
||
guideModelState.confirmPoiDetailReturn(returnContext.requestId)
|
||
},
|
||
fail: () => {
|
||
guideModelState.cancelPoiDetailReturn(returnContext.requestId)
|
||
uni.showToast({
|
||
title: failureMessage,
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
const navigateFromSelectedPoiToDetail = (url: string) => {
|
||
const poi = selectedGuidePoi.value
|
||
if (!poi) return
|
||
|
||
// Detail pages do not own the mounted renderer. The homepage consumes this once on return.
|
||
navigateToPoiDetail({
|
||
url,
|
||
floorId: poi.floorId,
|
||
failureMessage: '讲解详情打开失败,请重试'
|
||
})
|
||
}
|
||
|
||
const navigateToHallExplainObjects = (hallId: string, hallName = '讲解') => {
|
||
navigateFromSelectedPoiToDetail(
|
||
explainGuideStopListUrl(hallId, hallName)
|
||
)
|
||
}
|
||
|
||
const handleViewSelectedHall = () => {
|
||
const hall = selectedGuidePoiExplainHall.value
|
||
if (!hall) return
|
||
navigateToHallExplainObjects(hall.id, hall.name)
|
||
}
|
||
|
||
const loadRouteTargets = async (keyword = '') => {
|
||
routeTargetsLoading.value = true
|
||
routeTargetsError.value = ''
|
||
|
||
try {
|
||
const targets = keyword
|
||
? await guideRouteUseCase.searchTargets(keyword)
|
||
: await guideRouteUseCase.listTargets()
|
||
const options = targets.map(toRoutePointOption)
|
||
routePointOptions.value = options
|
||
} catch (error) {
|
||
console.error('加载路线目标失败:', error)
|
||
routeTargetsError.value = error instanceof Error ? error.message : '路线目标加载失败'
|
||
routePointOptions.value = []
|
||
} finally {
|
||
routeTargetsLoading.value = false
|
||
}
|
||
}
|
||
|
||
const refreshRouteReadinessState = async () => {
|
||
try {
|
||
const readiness = await guideRouteUseCase.getRouteReadiness()
|
||
routeReady.value = readiness.ready
|
||
routePreviewUnavailableMessage.value = readiness.ready
|
||
? ''
|
||
: readiness.message || '当前可查看位置预览和位置关系,暂不提供正式室内导航'
|
||
} catch (error) {
|
||
console.warn('读取路线能力状态失败:', error)
|
||
routeReady.value = false
|
||
routePreviewUnavailableMessage.value = '当前可查看位置预览和位置关系,暂不提供正式室内导航'
|
||
}
|
||
}
|
||
|
||
const clearRoutePreviewState = () => {
|
||
guideMapShellRef.value?.clearRoute?.()
|
||
activeRoutePreview.value = null
|
||
routePlanError.value = ''
|
||
isSimulatingRoute.value = false
|
||
}
|
||
|
||
const handleRouteClear = () => {
|
||
routeStartPoint.value = null
|
||
routeEndPoint.value = null
|
||
showRoutePlanner.value = false
|
||
clearRoutePreviewState()
|
||
}
|
||
|
||
const handleRouteBack = () => {
|
||
if (activeRoutePreview.value) {
|
||
activeRoutePreview.value = null
|
||
routePlanError.value = ''
|
||
isSimulatingRoute.value = false
|
||
showRoutePlanner.value = true
|
||
return
|
||
}
|
||
|
||
showRoutePlanner.value = false
|
||
isSimulatingRoute.value = false
|
||
}
|
||
|
||
const handleRouteStartPointChange = (point: RoutePointOption | null) => {
|
||
routeStartPoint.value = point
|
||
clearRoutePreviewState()
|
||
}
|
||
|
||
const handleRouteEndPointChange = (point: RoutePointOption | null) => {
|
||
routeEndPoint.value = point
|
||
clearRoutePreviewState()
|
||
}
|
||
|
||
const handleRouteSwap = () => {
|
||
const nextStart = routeEndPoint.value
|
||
routeEndPoint.value = routeStartPoint.value
|
||
routeStartPoint.value = nextStart
|
||
clearRoutePreviewState()
|
||
}
|
||
|
||
const handleRoutePickerOpen = () => {
|
||
if (!routePointOptions.value.length) {
|
||
void loadRouteTargets()
|
||
}
|
||
}
|
||
|
||
const handleRoutePickerSearch = ({ keyword }: { mode: 'start' | 'end'; keyword: string }) => {
|
||
void loadRouteTargets(keyword)
|
||
}
|
||
|
||
const focusRouteFloor = (route: GuideRouteResult) => {
|
||
const routeFloorCount = new Set(route.floorSegments.map((segment) => segment.floorId)).size
|
||
activeGuideFloor.value = route.start.floorId
|
||
renderedFloorId.value = route.start.floorId
|
||
indoorView.value = routeFloorCount > 1 ? 'multi' : 'floor'
|
||
}
|
||
|
||
const handleRouteView = async ({ startPoint, endPoint }: { startPoint: RoutePointOption; endPoint: RoutePointOption }) => {
|
||
routePlanning.value = true
|
||
routePlanError.value = ''
|
||
isSimulatingRoute.value = false
|
||
|
||
try {
|
||
await refreshRouteReadinessState()
|
||
if (routePreviewUnavailableMessage.value) {
|
||
activeRoutePreview.value = null
|
||
routePlanError.value = routePreviewUnavailableMessage.value
|
||
return
|
||
}
|
||
|
||
const result = await guideRouteUseCase.planRoute({
|
||
startPoiId: startPoint.poiId,
|
||
endPoiId: endPoint.poiId
|
||
})
|
||
|
||
if (!result.route) {
|
||
activeRoutePreview.value = null
|
||
routePlanError.value = result.error || '馆内导览位置关系生成失败'
|
||
return
|
||
}
|
||
|
||
activeRoutePreview.value = result.route
|
||
focusRouteFloor(result.route)
|
||
showRoutePlanner.value = true
|
||
} catch (error) {
|
||
console.error('馆内导览位置关系生成失败:', error)
|
||
activeRoutePreview.value = null
|
||
routePlanError.value = error instanceof Error ? error.message : '馆内导览位置关系生成失败'
|
||
} finally {
|
||
routePlanning.value = false
|
||
}
|
||
}
|
||
|
||
const handleRouteSimulate = () => {
|
||
if (!activeRoutePreview.value) return
|
||
showRoutePlanner.value = false
|
||
selectedGuidePoi.value = null
|
||
isPoiCardCollapsed.value = false
|
||
isSimulatingRoute.value = true
|
||
}
|
||
|
||
// 进入 3D 馆内模式
|
||
const handleEnter3DMode = () => {
|
||
console.log('进入 3D 馆内模式')
|
||
closeArrivalPanel()
|
||
indoorView.value = 'overview'
|
||
is3DMode.value = true
|
||
guideOutdoorState.value = 'home'
|
||
showIndoorHint()
|
||
if (!routePointOptions.value.length) {
|
||
void loadRouteTargets()
|
||
}
|
||
}
|
||
|
||
const handleModeChange = (mode: '2d' | '3d') => {
|
||
is3DMode.value = mode === '3d'
|
||
|
||
if (mode === '2d') {
|
||
closeHomeSearchDock()
|
||
indoorView.value = 'overview'
|
||
guideOutdoorState.value = 'home'
|
||
selectedGuidePoi.value = null
|
||
isPoiCardCollapsed.value = false
|
||
showRoutePlanner.value = false
|
||
isSimulatingRoute.value = false
|
||
clearRoutePreviewState()
|
||
} else {
|
||
closeArrivalPanel()
|
||
indoorView.value = 'overview'
|
||
showIndoorHint()
|
||
if (!routePointOptions.value.length) {
|
||
void loadRouteTargets()
|
||
}
|
||
}
|
||
}
|
||
|
||
const guideStatusLabel = computed(() => {
|
||
if (is3DMode.value) {
|
||
if (indoorView.value === 'overview') return '建筑外观'
|
||
if (indoorView.value === 'multi') return '馆内多层'
|
||
if (loadingFloorId.value) {
|
||
return `切换${requestedFloorLabel.value || getGuideFloorLabel(requestedFloorId.value || loadingFloorId.value)}`
|
||
}
|
||
if (failedFloorId.value) {
|
||
return `${getGuideFloorLabel(failedFloorId.value)}失败`
|
||
}
|
||
return getGuideFloorLabel(renderedFloorId.value || activeGuideFloor.value) || '馆内单层'
|
||
}
|
||
|
||
if (guideOutdoorState.value === 'entrance') {
|
||
return '推荐入口'
|
||
}
|
||
|
||
return '馆外参考'
|
||
})
|
||
|
||
const handleSwitchEntrance = () => {
|
||
guideOutdoorState.value = 'home'
|
||
}
|
||
|
||
const handleMoreRouteGuide = async () => {
|
||
if (returningToOverview.value) return
|
||
|
||
if (is3DMode.value && indoorView.value !== 'overview') {
|
||
returningToOverview.value = true
|
||
showRoutePlanner.value = false
|
||
selectedGuidePoi.value = null
|
||
isPoiCardCollapsed.value = false
|
||
isSimulatingRoute.value = false
|
||
requestedFloorId.value = ''
|
||
requestedFloorLabel.value = ''
|
||
loadingFloorId.value = ''
|
||
failedFloorId.value = ''
|
||
clearRoutePreviewState()
|
||
|
||
try {
|
||
await guideMapShellRef.value?.showOverview?.()
|
||
indoorView.value = 'overview'
|
||
guideOutdoorState.value = 'home'
|
||
closeHomeSearchDock()
|
||
showIndoorHint('已返回建筑外观,可放大再次进入馆内', 3200)
|
||
} catch (error) {
|
||
console.error('返回建筑外观失败:', error)
|
||
showIndoorHint('建筑外观加载失败,请稍后重试', 3200)
|
||
} finally {
|
||
returningToOverview.value = false
|
||
}
|
||
return
|
||
}
|
||
|
||
closeArrivalPanel()
|
||
showRoutePlanner.value = false
|
||
is3DMode.value = true
|
||
guideOutdoorState.value = 'home'
|
||
selectedGuidePoi.value = null
|
||
isPoiCardCollapsed.value = false
|
||
isSimulatingRoute.value = false
|
||
const floorId = activeGuideFloor.value
|
||
const floorLabel = getGuideFloorLabel(floorId) || '默认楼层'
|
||
if (!floorId) {
|
||
showIndoorHint('暂未获取到可进入的楼层,请稍后重试', 3200)
|
||
return
|
||
}
|
||
|
||
requestedFloorId.value = floorId
|
||
requestedFloorLabel.value = floorLabel
|
||
loadingFloorId.value = floorId
|
||
failedFloorId.value = ''
|
||
// 渲染器提交 floor-change 前仍保持建筑外观,避免楼层控件显示假状态。
|
||
showIndoorHint(`正在进入 ${floorLabel}…`, 3200)
|
||
await guideMapShellRef.value?.switchFloor?.(floorId)
|
||
}
|
||
|
||
const selectFirstArrivalTarget = () => {
|
||
selectedArrivalTargetId.value = activeArrivalTargets.value[0]?.id || ''
|
||
}
|
||
|
||
const resetArrivalPanelState = () => {
|
||
activeArrivalType.value = 'bus'
|
||
selectFirstArrivalTarget()
|
||
}
|
||
|
||
const closeArrivalPanel = () => {
|
||
showArrivalPanel.value = false
|
||
showArrivalPanelCollapsed.value = false
|
||
selectedArrivalTargetId.value = ''
|
||
arrivalPanelHeight.value = 0
|
||
}
|
||
|
||
const handleArrivalPanelCollapsedChange = (collapsed: boolean) => {
|
||
showArrivalPanelCollapsed.value = collapsed
|
||
}
|
||
|
||
const handleArrivalPanelLayoutChange = (layout: { height: number; collapsed: boolean }) => {
|
||
arrivalPanelHeight.value = layout.height
|
||
showArrivalPanelCollapsed.value = layout.collapsed
|
||
}
|
||
|
||
const handleArrivalTypeChange = (type: ArrivalTargetType) => {
|
||
activeArrivalType.value = type
|
||
selectFirstArrivalTarget()
|
||
}
|
||
|
||
const handleArrivalTargetSelect = (target: ArrivalSearchTarget) => {
|
||
selectedArrivalTargetId.value = target.id
|
||
}
|
||
|
||
const handleArrivalMarkerClick = (markerId: string) => {
|
||
if (!homeArrivalFeatureEnabled) return
|
||
const target = activeArrivalTargets.value.find((item) => item.id === markerId)
|
||
if (!target) return
|
||
selectedArrivalTargetId.value = target.id
|
||
}
|
||
|
||
const closeHomeSearchDock = () => {
|
||
homeSearchPanelRef.value?.collapseHomePanel?.()
|
||
homeSearchExpanded.value = false
|
||
}
|
||
|
||
const resetGuideModelToFloorBaseline = async ({
|
||
rendererReason,
|
||
floorId,
|
||
request
|
||
}: {
|
||
rendererReason: GuideModelResetReason
|
||
floorId: string
|
||
request?: GuideModelResetRequest
|
||
}) => {
|
||
const initial = guideModelState.initialState.value
|
||
const resetRequest = request || guideModelState.beginViewBaselineReset({ floorId, rendererReason })
|
||
searchTargetFocusRequest.value = null
|
||
searchVisiblePoiIds.value = null
|
||
homeCategoryModeActive.value = false
|
||
selectedGuidePoi.value = null
|
||
isPoiCardCollapsed.value = false
|
||
showRoutePlanner.value = false
|
||
activeRoutePreview.value = null
|
||
routeStartPoint.value = null
|
||
routeEndPoint.value = null
|
||
routePlanError.value = ''
|
||
isSimulatingRoute.value = false
|
||
requestedFloorId.value = ''
|
||
requestedFloorLabel.value = ''
|
||
loadingFloorId.value = ''
|
||
failedFloorId.value = ''
|
||
is3DMode.value = true
|
||
guideOutdoorState.value = 'home'
|
||
indoorView.value = 'floor'
|
||
activeGuideFloor.value = floorId || initial.defaultActiveFloorId || activeGuideFloor.value
|
||
closeArrivalPanel()
|
||
if (!guideModelState.isCurrentRequest(resetRequest.requestId)) return
|
||
|
||
const resetResult = await guideMapShellRef.value?.resetToViewBaseline?.({
|
||
view: 'floor',
|
||
floorId: activeGuideFloor.value,
|
||
reason: rendererReason
|
||
})
|
||
if (!guideModelState.isCurrentRequest(resetRequest.requestId)) return
|
||
if (resetResult === 'applied') {
|
||
renderedFloorId.value = activeGuideFloor.value
|
||
guideModelState.completeViewBaselineReset(resetRequest)
|
||
console.info('馆内三维模型已恢复楼层视觉基线:', { rendererReason })
|
||
} else if (resetResult === 'not-ready' || resetResult === undefined) {
|
||
guideModelState.queueViewBaselineReset(resetRequest)
|
||
}
|
||
}
|
||
|
||
onShow(async () => {
|
||
await nextTick()
|
||
const detailReturnContext = guideModelState.consumePoiDetailReturn()
|
||
if (detailReturnContext) {
|
||
await resetGuideModelToFloorBaseline({
|
||
rendererReason: 'poi-detail-close',
|
||
floorId: detailReturnContext.floorId
|
||
})
|
||
await homeSearchPanelRef.value?.restoreResultListScroll?.()
|
||
return
|
||
}
|
||
|
||
await homeSearchPanelRef.value?.restoreResultListScroll?.()
|
||
})
|
||
|
||
const handleMoreOutdoorNav = () => {
|
||
if (!homeArrivalFeatureEnabled) return
|
||
if (showArrivalPanel.value && showArrivalPanelCollapsed.value) {
|
||
arrivalPanelRef.value?.expandPanel()
|
||
showArrivalPanelCollapsed.value = false
|
||
return
|
||
}
|
||
|
||
resetArrivalPanelState()
|
||
showArrivalPanel.value = true
|
||
showArrivalPanelCollapsed.value = false
|
||
showRoutePlanner.value = false
|
||
is3DMode.value = false
|
||
guideOutdoorState.value = 'home'
|
||
selectedGuidePoi.value = null
|
||
isPoiCardCollapsed.value = false
|
||
isSimulatingRoute.value = false
|
||
closeHomeSearchDock()
|
||
}
|
||
|
||
const handleExplainQuickTap = () => {
|
||
handleTabChange('explain')
|
||
}
|
||
|
||
const handleHomeSearchExpandedChange = (expanded: boolean) => {
|
||
homeSearchExpanded.value = expanded
|
||
if (expanded) {
|
||
pushPoiSearchOverlayHistory()
|
||
return
|
||
}
|
||
|
||
removePoiSearchOverlayHistory()
|
||
}
|
||
|
||
const clearHomeSearchMapState = () => {
|
||
homeCategoryModeActive.value = false
|
||
searchVisiblePoiIds.value = null
|
||
searchTargetFocusRequest.value = null
|
||
}
|
||
|
||
const handleHomeCategoryModeChange = (active: boolean) => {
|
||
homeCategoryModeActive.value = active
|
||
if (!active) {
|
||
searchVisiblePoiIds.value = null
|
||
return
|
||
}
|
||
|
||
closeArrivalPanel()
|
||
showRoutePlanner.value = false
|
||
isSimulatingRoute.value = false
|
||
selectedGuidePoi.value = null
|
||
isPoiCardCollapsed.value = false
|
||
is3DMode.value = true
|
||
indoorView.value = 'floor'
|
||
}
|
||
|
||
const handleHomeSearchResultsChange = (state: PoiCategoryResultState) => {
|
||
if (!state.active) {
|
||
searchVisiblePoiIds.value = null
|
||
return
|
||
}
|
||
|
||
searchVisiblePoiIds.value = [...state.visiblePoiIds]
|
||
if (state.floorId) {
|
||
activeGuideFloor.value = state.floorId
|
||
}
|
||
}
|
||
|
||
const createSearchDetailUrl = (poi: MuseumPoi, context: PoiSearchContext) => {
|
||
const resultCount = homeCategoryModeActive.value
|
||
? context.floorResultCount
|
||
: context.resultCount
|
||
const params = new URLSearchParams({
|
||
source: 'search',
|
||
searchOrigin: context.origin,
|
||
searchKeyword: context.keyword,
|
||
searchCategoryId: context.categoryId,
|
||
searchFloorId: context.floorId,
|
||
searchFloorLabel: context.floorLabel,
|
||
resultCount: String(Math.max(1, resultCount)),
|
||
floorResultCount: String(context.floorResultCount),
|
||
visiblePoiIds: context.visiblePoiIds.join(','),
|
||
listScrollTop: String(context.listScrollTop),
|
||
id: poi.id,
|
||
target: poi.name,
|
||
floorId: poi.floorId,
|
||
floorLabel: poi.floorLabel
|
||
})
|
||
|
||
return `/pages/facility/detail?${params.toString()}`
|
||
}
|
||
|
||
const handleHomeSearchResultTap = async (poi: MuseumPoi, context: PoiSearchContext) => {
|
||
// The homepage keeps its renderer mounted; detail close is consumed by onShow once.
|
||
searchTargetFocusRequest.value = null
|
||
navigateToPoiDetail({
|
||
url: createSearchDetailUrl(poi, context),
|
||
floorId: poi.floorId,
|
||
failureMessage: '点位详情打开失败,请重试'
|
||
})
|
||
}
|
||
|
||
const handleHomeSearchCancel = () => {
|
||
clearHomeSearchMapState()
|
||
selectedGuidePoi.value = null
|
||
isPoiCardCollapsed.value = false
|
||
homeSearchExpanded.value = false
|
||
}
|
||
|
||
const handleHomeSearchOutsideTap = () => {
|
||
closeHomeSearchDock()
|
||
}
|
||
|
||
const guideShellMode = computed(() => (is3DMode.value ? '3d' : '2d'))
|
||
const guideMapType = computed(() => (is3DMode.value ? 'indoor' : 'outdoor'))
|
||
const guideOutdoorVariant = computed(() => (
|
||
guideOutdoorState.value === 'entrance' ? 'entrance' : 'home'
|
||
))
|
||
const guideSearchText = computed(() => {
|
||
return '请输入地点进行搜索'
|
||
})
|
||
|
||
// 讲解页面处理
|
||
const handleExplainHallClick = async (hallId: string) => {
|
||
const hall = explainHallItems.value.find((item) => item.id === hallId)
|
||
uni.navigateTo({
|
||
url: explainGuideStopListUrl(hallId, hall?.name || '讲解')
|
||
})
|
||
}
|
||
|
||
const handleExplainBack = () => {
|
||
currentTab.value = 'guide'
|
||
syncTopTabToUrl('guide')
|
||
loadTopTabData('guide')
|
||
showRoutePlanner.value = false
|
||
isSimulatingRoute.value = false
|
||
isPoiCardCollapsed.value = false
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.app-launch-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 5000;
|
||
display: flex;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
background: #ffffff;
|
||
opacity: 1;
|
||
transition: opacity 0.36s ease, transform 0.36s ease;
|
||
}
|
||
|
||
.app-launch-overlay.leaving {
|
||
opacity: 0;
|
||
transform: scale(1.015);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.app-launch-stage {
|
||
width: 100%;
|
||
max-width: 430px;
|
||
height: 100vh;
|
||
height: 100dvh;
|
||
min-height: 480px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.app-launch-content {
|
||
width: 100%;
|
||
padding: 0 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
flex-direction: column;
|
||
box-sizing: border-box;
|
||
transform: translateY(-3vh);
|
||
}
|
||
|
||
.app-launch-logo {
|
||
width: min(84vw, 350px);
|
||
height: auto;
|
||
}
|
||
|
||
.app-launch-name-en {
|
||
margin-top: 20px;
|
||
white-space: nowrap;
|
||
font-family: 'HarmonyOS Sans SC', 'HarmonyOS Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
|
||
font-size: 13px;
|
||
line-height: 18px;
|
||
font-weight: 500;
|
||
letter-spacing: 0;
|
||
color: #262421;
|
||
}
|
||
|
||
.app-launch-name-cn {
|
||
margin-top: 8px;
|
||
white-space: nowrap;
|
||
font-family: 'Songti SC', STSong, SimSun, serif;
|
||
font-size: 36px;
|
||
line-height: 48px;
|
||
font-weight: 500;
|
||
letter-spacing: 0;
|
||
color: #000000;
|
||
}
|
||
|
||
.app-launch-progress {
|
||
width: 112px;
|
||
margin-top: 32px;
|
||
}
|
||
|
||
@media (max-width: 360px) {
|
||
.app-launch-content {
|
||
padding: 0 24px;
|
||
}
|
||
|
||
.app-launch-logo {
|
||
width: calc(100vw - 52px);
|
||
}
|
||
|
||
.app-launch-name-cn {
|
||
font-size: 32px;
|
||
line-height: 43px;
|
||
}
|
||
}
|
||
|
||
@media (max-height: 680px) {
|
||
.app-launch-content {
|
||
transform: translateY(-1vh);
|
||
}
|
||
|
||
.app-launch-progress {
|
||
margin-top: 26px;
|
||
}
|
||
}
|
||
|
||
.explain-page {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: #f6f7f4;
|
||
z-index: 10;
|
||
}
|
||
|
||
.guide-poi-card {
|
||
position: absolute;
|
||
left: 12px;
|
||
right: 12px;
|
||
bottom: calc(env(safe-area-inset-bottom) + 24px);
|
||
box-sizing: border-box;
|
||
background: rgba(255, 255, 255, 0.96);
|
||
border: 1px solid #e5e6de;
|
||
border-radius: 8px;
|
||
box-shadow: 0 10px 24px rgba(36, 49, 42, 0.12);
|
||
z-index: 1003;
|
||
}
|
||
|
||
.guide-poi-card {
|
||
padding: 10px 14px 14px;
|
||
}
|
||
|
||
.guide-poi-card.collapsed {
|
||
padding: 8px 10px 10px;
|
||
}
|
||
|
||
.poi-card-handle {
|
||
width: 38px;
|
||
height: 4px;
|
||
margin: 0 auto 10px;
|
||
background: #d8dbd2;
|
||
border-radius: 999px;
|
||
}
|
||
|
||
.poi-card-collapsed {
|
||
min-height: 44px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
|
||
.poi-card-mini-action {
|
||
flex: 0 0 auto;
|
||
height: 30px;
|
||
padding: 0 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #151713;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.poi-card-mini-text {
|
||
font-size: 12px;
|
||
line-height: 16px;
|
||
font-weight: 500;
|
||
color: var(--museum-accent);
|
||
}
|
||
|
||
.poi-card-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
}
|
||
|
||
.poi-card-copy {
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 3px;
|
||
}
|
||
|
||
.poi-card-title {
|
||
font-size: 17px;
|
||
line-height: 24px;
|
||
font-weight: 700;
|
||
color: #151713;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.poi-card-subtitle {
|
||
font-size: 12px;
|
||
line-height: 17px;
|
||
color: #696962;
|
||
}
|
||
|
||
.poi-card-header-actions {
|
||
flex: 0 0 auto;
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
|
||
.poi-card-soft-action {
|
||
height: 28px;
|
||
padding: 0 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-sizing: border-box;
|
||
border: 1px solid #e4e5df;
|
||
border-radius: 8px;
|
||
background: #f5f7f2;
|
||
}
|
||
|
||
.poi-card-soft-text {
|
||
font-size: 12px;
|
||
line-height: 16px;
|
||
font-weight: 500;
|
||
color: #545861;
|
||
}
|
||
|
||
.poi-card-close {
|
||
width: 28px;
|
||
height: 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 8px;
|
||
background: #f5f7f2;
|
||
}
|
||
|
||
.poi-card-close-text {
|
||
font-size: 18px;
|
||
line-height: 20px;
|
||
color: #545861;
|
||
}
|
||
|
||
.poi-card-actions {
|
||
margin-top: 12px;
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 8px;
|
||
}
|
||
|
||
.poi-action {
|
||
min-width: 0;
|
||
height: 38px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0 6px;
|
||
box-sizing: border-box;
|
||
background: #f6f7f4;
|
||
border: 1px solid #e5e6de;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.poi-action.primary {
|
||
background: #151713;
|
||
border-color: #151713;
|
||
}
|
||
|
||
.poi-action-text {
|
||
font-size: 12px;
|
||
line-height: 16px;
|
||
font-weight: 500;
|
||
color: #151713;
|
||
text-align: center;
|
||
}
|
||
|
||
.poi-action.primary .poi-action-text {
|
||
color: var(--museum-accent);
|
||
}
|
||
|
||
.guide-home-dock {
|
||
position: absolute;
|
||
--museum-h5-page-width: min(100vw, 430px);
|
||
left: 50%;
|
||
right: auto;
|
||
width: calc(var(--museum-h5-page-width) - 20px);
|
||
bottom: calc(env(safe-area-inset-bottom) + 10px);
|
||
padding: 7px;
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
background:
|
||
linear-gradient(180deg, rgba(224, 225, 0, 0.14) 0, rgba(224, 225, 0, 0) 46px),
|
||
#443c2d;
|
||
border: 1px solid #a66a42;
|
||
border-radius: 8px;
|
||
box-shadow: 0 12px 28px rgba(58, 50, 35, 0.26);
|
||
transform: translateX(-50%);
|
||
z-index: 1003;
|
||
}
|
||
|
||
.guide-home-dock::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 7px;
|
||
right: 7px;
|
||
top: 0;
|
||
height: 3px;
|
||
background: var(--museum-accent);
|
||
}
|
||
|
||
.guide-home-dock.expanded {
|
||
position: fixed;
|
||
left: 50%;
|
||
right: auto;
|
||
top: 0;
|
||
bottom: auto;
|
||
width: var(--museum-h5-page-width);
|
||
height: 100vh;
|
||
height: 100dvh;
|
||
height: var(--home-search-viewport-height, 100dvh);
|
||
max-height: none;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: calc(env(safe-area-inset-top) + 12px) 14px calc(env(safe-area-inset-bottom) + 14px);
|
||
background: #f7f8f2;
|
||
border: 0;
|
||
border-radius: 0;
|
||
box-shadow: none;
|
||
transform: translateX(-50%);
|
||
overflow: hidden;
|
||
overscroll-behavior: contain;
|
||
touch-action: pan-x pan-y;
|
||
z-index: 1100;
|
||
}
|
||
|
||
.guide-home-dock.expanded::before {
|
||
display: none;
|
||
}
|
||
|
||
/* #ifdef H5 */
|
||
.guide-home-dock.expanded {
|
||
min-height: 100vh;
|
||
min-height: 100dvh;
|
||
max-width: var(--museum-h5-page-width);
|
||
}
|
||
/* #endif */
|
||
|
||
.guide-home-search-scrim {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
background: transparent;
|
||
z-index: 1002;
|
||
}
|
||
|
||
.guide-quick-actions {
|
||
position: absolute;
|
||
top: clamp(88px, 13vh, 112px);
|
||
right: 14px;
|
||
width: 52px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 7px;
|
||
z-index: 1002;
|
||
}
|
||
|
||
.guide-quick-action {
|
||
width: 52px;
|
||
min-height: 58px;
|
||
padding: 7px 4px 6px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 4px;
|
||
box-sizing: border-box;
|
||
background: rgba(245, 245, 237, 0.94);
|
||
border: 1px solid rgba(255, 255, 255, 0.72);
|
||
border-radius: 8px;
|
||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.2);
|
||
backdrop-filter: blur(10px);
|
||
transition:
|
||
background 0.18s ease,
|
||
border-color 0.18s ease,
|
||
box-shadow 0.18s ease,
|
||
transform 0.16s ease;
|
||
}
|
||
|
||
.guide-quick-action.active {
|
||
background: var(--museum-accent);
|
||
border-color: var(--museum-accent);
|
||
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.24), 0 0 0 1px rgba(224, 225, 0, 0.35);
|
||
animation: guide-quick-active-in 0.22s ease-out both;
|
||
}
|
||
|
||
.guide-quick-action:active {
|
||
transform: translateY(1px) scale(0.98);
|
||
}
|
||
|
||
.guide-quick-icon {
|
||
width: 22px;
|
||
height: 22px;
|
||
flex: 0 0 22px;
|
||
position: relative;
|
||
color: #151713;
|
||
}
|
||
|
||
.guide-quick-action.active .guide-quick-icon {
|
||
color: #0a0b08;
|
||
}
|
||
|
||
.guide-quick-icon-floor {
|
||
position: absolute;
|
||
left: 3px;
|
||
width: 16px;
|
||
height: 9px;
|
||
box-sizing: border-box;
|
||
border: 1.7px solid currentColor;
|
||
border-radius: 2px;
|
||
transform: skewY(-18deg);
|
||
}
|
||
|
||
.guide-quick-icon-floor.top {
|
||
top: 2px;
|
||
}
|
||
|
||
.guide-quick-icon-floor.bottom {
|
||
top: 10px;
|
||
}
|
||
|
||
.guide-quick-pin {
|
||
position: absolute;
|
||
left: 5px;
|
||
top: 2px;
|
||
width: 12px;
|
||
height: 16px;
|
||
box-sizing: border-box;
|
||
border: 1.8px solid currentColor;
|
||
border-radius: 7px 7px 7px 0;
|
||
transform: rotate(-45deg);
|
||
}
|
||
|
||
.guide-quick-pin::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 3px;
|
||
top: 3px;
|
||
width: 4px;
|
||
height: 4px;
|
||
background: currentColor;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.guide-quick-explain-wave {
|
||
position: absolute;
|
||
left: 2px;
|
||
right: 2px;
|
||
height: 4px;
|
||
border: 1.8px solid currentColor;
|
||
border-left: 0;
|
||
border-right: 0;
|
||
border-bottom: 0;
|
||
border-radius: 999px 999px 0 0;
|
||
}
|
||
|
||
.guide-quick-explain-wave.top {
|
||
top: 3px;
|
||
}
|
||
|
||
.guide-quick-explain-wave.middle {
|
||
top: 9px;
|
||
left: 5px;
|
||
}
|
||
|
||
.guide-quick-explain-wave.bottom {
|
||
top: 15px;
|
||
right: 6px;
|
||
}
|
||
|
||
.guide-quick-action-text {
|
||
width: 100%;
|
||
font-size: 12px;
|
||
line-height: 16px;
|
||
font-weight: 700;
|
||
color: #151713;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.guide-quick-action.active .guide-quick-action-text {
|
||
color: #0a0b08;
|
||
}
|
||
|
||
@keyframes guide-quick-active-in {
|
||
from {
|
||
transform: scale(0.96);
|
||
}
|
||
|
||
to {
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
.route-sim-top-pill {
|
||
position: absolute;
|
||
top: 126px;
|
||
left: 50%;
|
||
max-width: calc(100% - 40px);
|
||
height: 34px;
|
||
padding: 0 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
box-sizing: border-box;
|
||
background: rgba(21, 23, 19, 0.86);
|
||
border-radius: 8px;
|
||
transform: translateX(-50%);
|
||
z-index: 1003;
|
||
}
|
||
|
||
.route-sim-top-text {
|
||
min-width: 0;
|
||
font-size: 12px;
|
||
line-height: 16px;
|
||
font-weight: 500;
|
||
color: #ffffff;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.route-sim-step-bar {
|
||
position: absolute;
|
||
left: 12px;
|
||
right: 12px;
|
||
bottom: calc(env(safe-area-inset-bottom) + 24px);
|
||
min-height: 54px;
|
||
padding: 10px 10px 10px 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
box-sizing: border-box;
|
||
background: rgba(255, 255, 255, 0.96);
|
||
border: 1px solid #e5e6de;
|
||
border-radius: 8px;
|
||
box-shadow: 0 10px 24px rgba(36, 49, 42, 0.12);
|
||
z-index: 1003;
|
||
}
|
||
|
||
.route-sim-step-text {
|
||
min-width: 0;
|
||
flex: 1;
|
||
font-size: 14px;
|
||
line-height: 20px;
|
||
font-weight: 700;
|
||
color: #151713;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.route-sim-expand {
|
||
height: 34px;
|
||
padding: 0 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-sizing: border-box;
|
||
background: #000000;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.route-sim-expand-text {
|
||
font-size: 12px;
|
||
line-height: 16px;
|
||
font-weight: 500;
|
||
color: var(--museum-accent);
|
||
}
|
||
|
||
.entrance-tip {
|
||
position: absolute;
|
||
top: 126px;
|
||
left: 30px;
|
||
width: 168px;
|
||
height: 42px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-sizing: border-box;
|
||
background: rgba(255, 255, 255, 0.96);
|
||
border: 1px solid #e5e6de;
|
||
border-radius: 12px;
|
||
z-index: 45;
|
||
}
|
||
|
||
.entrance-tip-text {
|
||
font-size: 14px;
|
||
line-height: 19px;
|
||
font-weight: 500;
|
||
color: #1f2329;
|
||
}
|
||
|
||
.indoor-gesture-hint {
|
||
position: absolute;
|
||
top: clamp(136px, 19vh, 190px);
|
||
left: 50%;
|
||
max-width: calc(100% - 112px);
|
||
min-height: 38px;
|
||
padding: 8px 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-sizing: border-box;
|
||
background: rgba(9, 15, 10, 0.9);
|
||
border: 1px solid rgba(255, 235, 59, 0.36);
|
||
border-radius: 999px;
|
||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.2);
|
||
transform: translateX(-50%);
|
||
pointer-events: none;
|
||
z-index: 58;
|
||
}
|
||
|
||
.indoor-gesture-text {
|
||
min-width: 0;
|
||
font-size: 13px;
|
||
line-height: 18px;
|
||
font-weight: 700;
|
||
color: #f4ff2f;
|
||
text-align: center;
|
||
white-space: normal;
|
||
}
|
||
|
||
.home-shortcuts-card {
|
||
position: absolute;
|
||
left: 12px;
|
||
right: 12px;
|
||
bottom: calc(env(safe-area-inset-bottom) + 128px);
|
||
height: 84px;
|
||
padding: 16px 10px 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
box-sizing: border-box;
|
||
background: rgba(255, 255, 255, 0.96);
|
||
border: 1px solid #ffffff;
|
||
border-radius: 16px;
|
||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
|
||
z-index: 45;
|
||
}
|
||
|
||
.home-shortcut-item {
|
||
width: 62px;
|
||
height: 60px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: 4px;
|
||
}
|
||
|
||
.home-shortcut-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-sizing: border-box;
|
||
background: #ffffff;
|
||
border: 1px solid #d9dcd2;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.home-shortcut-abbr {
|
||
font-size: 15px;
|
||
line-height: 20px;
|
||
font-weight: 700;
|
||
color: #1f2329;
|
||
}
|
||
|
||
.home-shortcut-label {
|
||
font-size: 12px;
|
||
line-height: 16px;
|
||
color: #1f2329;
|
||
text-align: center;
|
||
}
|
||
|
||
.home-shortcut-item.active {
|
||
.home-shortcut-icon {
|
||
background: #000000;
|
||
border-color: #000000;
|
||
}
|
||
|
||
.home-shortcut-abbr {
|
||
color: var(--museum-accent);
|
||
}
|
||
}
|
||
|
||
.search-bar-wrapper {
|
||
position: absolute;
|
||
top: 44px;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 1003;
|
||
}
|
||
|
||
.guide-mode-row {
|
||
position: absolute;
|
||
top: 104px;
|
||
left: 16px;
|
||
right: 16px;
|
||
height: 34px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
z-index: 1003;
|
||
}
|
||
|
||
.mode-switch {
|
||
width: 220px;
|
||
height: 34px;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 4px;
|
||
box-sizing: border-box;
|
||
background: rgba(255, 255, 255, 0.72);
|
||
border: 1px solid rgba(230, 230, 230, 0.5);
|
||
border-radius: 17px;
|
||
}
|
||
|
||
.mode-option {
|
||
width: 106px;
|
||
height: 26px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 13px;
|
||
}
|
||
|
||
.mode-option.active {
|
||
background: #000000;
|
||
}
|
||
|
||
.mode-label {
|
||
font-size: 12px;
|
||
line-height: 16px;
|
||
font-weight: 500;
|
||
color: #1f2329;
|
||
}
|
||
|
||
.mode-option.active .mode-label {
|
||
color: var(--museum-accent);
|
||
}
|
||
|
||
.mode-status-chip {
|
||
width: 111px;
|
||
height: 28px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(255, 255, 255, 0.42);
|
||
border-radius: 14px;
|
||
}
|
||
|
||
.mode-status-text {
|
||
font-size: 12px;
|
||
line-height: 16px;
|
||
font-weight: 500;
|
||
color: #696962;
|
||
}
|
||
|
||
.area-selector-wrapper {
|
||
position: absolute;
|
||
left: 12px;
|
||
right: 12px;
|
||
bottom: calc(env(safe-area-inset-bottom) + 128px);
|
||
z-index: 1003;
|
||
}
|
||
|
||
.guide-task-card {
|
||
position: absolute;
|
||
left: 12px;
|
||
right: 12px;
|
||
bottom: calc(env(safe-area-inset-bottom) + 24px);
|
||
height: 94px;
|
||
padding: 16px 16px 10px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-sizing: border-box;
|
||
background: #ffffff;
|
||
border: 1px solid #ffffff;
|
||
border-radius: 16px;
|
||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
|
||
z-index: 1003;
|
||
}
|
||
|
||
.guide-task-card.has-subtitle {
|
||
height: auto;
|
||
min-height: 128px;
|
||
}
|
||
|
||
.task-title {
|
||
flex-shrink: 0;
|
||
font-size: 17px;
|
||
line-height: 24px;
|
||
font-weight: 700;
|
||
color: #000000;
|
||
}
|
||
|
||
.task-subtitle {
|
||
margin-top: 4px;
|
||
min-height: 20px;
|
||
font-size: 12px;
|
||
line-height: 18px;
|
||
color: #696962;
|
||
}
|
||
|
||
.task-primary-btn {
|
||
flex-shrink: 0;
|
||
margin-top: 4px;
|
||
height: 38px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #000000;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.task-primary-text {
|
||
font-size: 14px;
|
||
line-height: 19px;
|
||
font-weight: 500;
|
||
color: var(--museum-accent);
|
||
}
|
||
|
||
.entrance-bottom-card {
|
||
position: absolute;
|
||
left: 12px;
|
||
right: 12px;
|
||
bottom: calc(env(safe-area-inset-bottom) + 24px);
|
||
height: 158px;
|
||
padding: 18px 16px 14px;
|
||
box-sizing: border-box;
|
||
background: #ffffff;
|
||
border: 1px solid #ffffff;
|
||
border-radius: 16px;
|
||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
|
||
z-index: 45;
|
||
}
|
||
|
||
.entrance-card-title {
|
||
display: block;
|
||
font-size: 17px;
|
||
line-height: 24px;
|
||
font-weight: 700;
|
||
color: #000000;
|
||
}
|
||
|
||
.entrance-card-desc {
|
||
display: block;
|
||
width: 305px;
|
||
margin-top: 10px;
|
||
font-size: 13px;
|
||
line-height: 18px;
|
||
color: #6b7178;
|
||
}
|
||
|
||
.entrance-actions {
|
||
position: absolute;
|
||
left: 16px;
|
||
right: 16px;
|
||
bottom: 14px;
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.entrance-btn {
|
||
height: 40px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-sizing: border-box;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.entrance-btn.secondary {
|
||
width: 126px;
|
||
background: #ffffff;
|
||
border: 1px solid #000000;
|
||
}
|
||
|
||
.entrance-btn.primary {
|
||
width: 181px;
|
||
background: #000000;
|
||
border: 1px solid #000000;
|
||
}
|
||
|
||
.entrance-btn-text {
|
||
font-size: 14px;
|
||
line-height: 19px;
|
||
font-weight: 500;
|
||
color: #000000;
|
||
}
|
||
|
||
.entrance-btn.primary .entrance-btn-text {
|
||
color: var(--museum-accent);
|
||
}
|
||
|
||
</style>
|