1490 lines
38 KiB
Vue
1490 lines
38 KiB
Vue
<template>
|
||
<GuidePageFrame
|
||
:active-tab="currentTab"
|
||
variant="overlay"
|
||
:top-tabs-variant="currentTab === 'guide' ? 'segmented' : 'underline'"
|
||
:top-tabs-top="currentTab === 'guide' ? '80px' : '0'"
|
||
:show-top-tabs="currentTab === 'guide'"
|
||
@tab-change="handleTabChange"
|
||
>
|
||
<GuideMapShell
|
||
v-if="currentTab === 'guide'"
|
||
:search-text="guideSearchText"
|
||
:active-mode="guideShellMode"
|
||
:mode-layout="'status'"
|
||
:mode-status="guideStatusLabel"
|
||
mode-status-tone="glass"
|
||
search-top="20px"
|
||
search-variant="home"
|
||
: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="floor"
|
||
:indoor-view="indoorView"
|
||
:layer-mode="indoorLayerMode"
|
||
:active-floor="activeGuideFloor"
|
||
layer-mode-toggle-top="154px"
|
||
floor-top="420px"
|
||
floor-side="left"
|
||
show-floor-header
|
||
:show-layer-mode-toggle="false"
|
||
:show-floor="is3DMode"
|
||
tools-top="130px"
|
||
show-zoom-controls
|
||
zoom-controls-top="560px"
|
||
show-location-control
|
||
location-control-bottom="54px"
|
||
show-more-control
|
||
more-control-bottom="54px"
|
||
:tools="guide3DTools"
|
||
:route-preview="activeRoutePreview"
|
||
:show-route="Boolean(activeRoutePreview)"
|
||
@search-tap="handleGuideSearchTap"
|
||
@mode-change="handleModeChange"
|
||
@floor-change="handleFloorChange"
|
||
@indoor-view-change="handleIndoorViewChange"
|
||
@poi-click="handleGuidePoiClick"
|
||
@selection-clear="handleGuideSelectionClear"
|
||
@tool-click="handleGuideToolClick"
|
||
@more-click="handleGuideMoreClick"
|
||
@auto-switch="handleAutoSwitch"
|
||
>
|
||
<template #overlay>
|
||
<view v-if="guideOutdoorState === 'entrance' && !is3DMode" class="entrance-tip">
|
||
<text class="entrance-tip-text">主入口 约 4 分钟</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">位置预览 · 点按展开操作</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="handleGuideSelectionClear">
|
||
<text class="poi-card-close-text">×</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="poi-card-actions">
|
||
<view class="poi-action primary" @tap="handlePreviewSelectedPoi">
|
||
<text class="poi-action-text">查看位置</text>
|
||
</view>
|
||
<view class="poi-action" @tap="handleSetSelectedPoiAsStart">
|
||
<text class="poi-action-text">从这里出发</text>
|
||
</view>
|
||
<view class="poi-action" @tap="handleSetSelectedPoiAsEnd">
|
||
<text class="poi-action-text">去这里</text>
|
||
</view>
|
||
<view class="poi-action" @tap="handleSelectedPoiRelated">
|
||
<text class="poi-action-text">相关讲解</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
</view>
|
||
|
||
<view v-if="showGuideMoreMenu && !showRoutePlanner" class="guide-more-popover">
|
||
<view class="more-popover-handle"></view>
|
||
<view class="more-popover-grid">
|
||
<view class="more-action" @tap="handleMoreRouteGuide">
|
||
<text class="more-action-title">线路预览</text>
|
||
<text class="more-action-desc">室内到室内</text>
|
||
</view>
|
||
<view class="more-action" @tap="handleMoreOutdoorReference">
|
||
<text class="more-action-title">室外参考</text>
|
||
<text class="more-action-desc">入口位置</text>
|
||
</view>
|
||
<view class="more-action" @tap="handleMoreRecommendedEntrance">
|
||
<text class="more-action-title">推荐入口</text>
|
||
<text class="more-action-desc">入口参考</text>
|
||
</view>
|
||
<view class="more-action" @tap="handleEnter3DMode">
|
||
<text class="more-action-title">室内3D</text>
|
||
<text class="more-action-desc">位置预览</text>
|
||
</view>
|
||
<view class="more-action" @tap="handleMoreToggleLayer">
|
||
<text class="more-action-title">{{ indoorLayerMode === 'multi' ? '单层' : '多层' }}</text>
|
||
<text class="more-action-desc">切换楼层</text>
|
||
</view>
|
||
</view>
|
||
</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"
|
||
@update:start-point="handleRouteStartPointChange"
|
||
@update:end-point="handleRouteEndPointChange"
|
||
@picker-open="handleRoutePickerOpen"
|
||
@search="handleRoutePickerSearch"
|
||
@swap="handleRouteSwap"
|
||
@clear="handleRouteClear"
|
||
@view-route="handleRouteView"
|
||
@simulate-guide="handleRouteSimulate"
|
||
/>
|
||
|
||
<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="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>
|
||
|
||
<view v-else-if="currentTab === 'explain'" class="explain-page">
|
||
<ExplainHallSelect
|
||
:halls="explainHallItems"
|
||
:loading="explainLoading"
|
||
:error="explainError"
|
||
@hall-click="handleExplainHallClick"
|
||
@back="handleExplainBack"
|
||
/>
|
||
</view>
|
||
</GuidePageFrame>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||
import { onLoad } 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 type {
|
||
RoutePointOption
|
||
} from '@/components/navigation/RoutePointPicker.vue'
|
||
import ExplainHallSelect, {
|
||
type ExplainHallSelectItem
|
||
} from '@/components/explain/ExplainHallSelect.vue'
|
||
import {
|
||
getLastGuideLocationPreviewUrl,
|
||
isGuideTopTab,
|
||
type GuideTopTab
|
||
} from '@/utils/guideTopTabs'
|
||
import {
|
||
guideUseCase
|
||
} from '@/usecases/guideUseCase'
|
||
import {
|
||
guideRouteUseCase
|
||
} from '@/usecases/guideRouteUseCase'
|
||
import {
|
||
explainUseCase
|
||
} from '@/usecases/explainUseCase'
|
||
import type {
|
||
GuideRenderPoi
|
||
} from '@/domain/guideModel'
|
||
import type {
|
||
GuideRouteResult,
|
||
GuideRouteTarget,
|
||
MuseumExhibit,
|
||
MuseumFloor
|
||
} from '@/domain/museum'
|
||
|
||
type GuideIndoorView = 'overview' | 'floor' | 'multi'
|
||
type LayerDisplayMode = 'single' | 'multi'
|
||
|
||
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 initialTopTab = (): GuideTopTab => topTabFromHash() || 'guide'
|
||
|
||
// 3D 模式状态
|
||
const is3DMode = ref(true)
|
||
const guideOutdoorState = ref<'home' | 'entrance'>('home')
|
||
const indoorView = ref<GuideIndoorView>('floor')
|
||
const activeGuideFloor = ref('1F')
|
||
const selectedGuidePoi = ref<GuideRenderPoi | null>(null)
|
||
|
||
// 状态
|
||
const currentTab = ref<GuideTopTab>(initialTopTab())
|
||
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 activeRoutePreview = ref<GuideRouteResult | null>(null)
|
||
const showGuideMoreMenu = ref(false)
|
||
const isSimulatingRoute = ref(false)
|
||
const isPoiCardCollapsed = ref(false)
|
||
|
||
const indoorNavAssetBaseUrl = guideUseCase.getAssetBaseUrl()
|
||
const indoorModelSource = guideUseCase.getModelSource()
|
||
const guideFloors = ref<MuseumFloor[]>([])
|
||
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
|
||
const indoorLayerMode = computed<LayerDisplayMode>(() => (
|
||
indoorView.value === 'multi' ? 'multi' : 'single'
|
||
))
|
||
const guide3DTools = computed(() => {
|
||
return is3DMode.value ? ['重置', '俯视', '斜视', '清除', '清路线'] : []
|
||
})
|
||
|
||
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 || routePreviewUnavailableMessage.value)
|
||
|
||
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 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 url = `#/pages/index/index?tab=${tabId}`
|
||
if (window.location.hash !== url) {
|
||
window.history.replaceState(null, '', url)
|
||
}
|
||
}
|
||
|
||
const loadTopTabData = (tabId: GuideTopTab) => {
|
||
if (tabId === 'explain') {
|
||
void loadExplainExhibits()
|
||
} 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)
|
||
|
||
// 关闭可能打开的面板
|
||
showGuideMoreMenu.value = false
|
||
showRoutePlanner.value = false
|
||
isSimulatingRoute.value = false
|
||
isPoiCardCollapsed.value = false
|
||
}
|
||
|
||
const syncTopTabFromHash = () => {
|
||
if (!isIndexHashRoute()) return
|
||
|
||
const tab = topTabFromHash()
|
||
if (tab) {
|
||
const tabChanged = currentTab.value !== tab
|
||
currentTab.value = tab
|
||
loadTopTabData(tab)
|
||
if (tabChanged) {
|
||
showGuideMoreMenu.value = false
|
||
showRoutePlanner.value = false
|
||
isSimulatingRoute.value = false
|
||
isPoiCardCollapsed.value = false
|
||
}
|
||
syncTopTabToUrl(tab)
|
||
return
|
||
}
|
||
|
||
syncTopTabToUrl(currentTab.value)
|
||
loadTopTabData(currentTab.value)
|
||
}
|
||
|
||
onMounted(() => {
|
||
if (typeof window === 'undefined') return
|
||
|
||
syncTopTabFromHash()
|
||
window.addEventListener('hashchange', syncTopTabFromHash)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
if (typeof window === 'undefined') return
|
||
|
||
window.removeEventListener('hashchange', syncTopTabFromHash)
|
||
})
|
||
|
||
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
|
||
activeGuideFloor.value = floors.find((floor) => floor.label === activeGuideFloor.value)?.label
|
||
|| floors.find((floor) => floor.id === 'L1')?.label
|
||
|| floors[0]?.label
|
||
|| activeGuideFloor.value
|
||
} catch (error) {
|
||
console.error('加载导览楼层失败:', error)
|
||
guideFloors.value = []
|
||
}
|
||
}
|
||
|
||
const loadExplainExhibits = async () => {
|
||
if (explainHallItems.value.length) return
|
||
if (explainLoadPromise) return explainLoadPromise
|
||
|
||
explainLoading.value = true
|
||
explainError.value = ''
|
||
|
||
explainLoadPromise = (async () => {
|
||
try {
|
||
const [halls, exhibits] = await Promise.all([
|
||
explainUseCase.listHalls(),
|
||
explainUseCase.listExplainExhibits()
|
||
])
|
||
explainHallItems.value = buildExplainHallItems(halls, exhibits)
|
||
} 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.listHalls>>,
|
||
exhibits: MuseumExhibit[]
|
||
): ExplainHallSelectItem[] => {
|
||
const countsByHallId = new Map<string, number>()
|
||
const countsByHallName = new Map<string, number>()
|
||
const keywordsByHallId = new Map<string, Set<string>>()
|
||
const keywordsByHallName = new Map<string, Set<string>>()
|
||
|
||
exhibits.forEach((exhibit) => {
|
||
const keywordParts = [
|
||
exhibit.name,
|
||
exhibit.hallName,
|
||
exhibit.floorLabel,
|
||
exhibit.description,
|
||
exhibit.guideTitle,
|
||
exhibit.guideText,
|
||
...(exhibit.tags || [])
|
||
].filter(Boolean) as string[]
|
||
|
||
if (exhibit.hallId) {
|
||
countsByHallId.set(exhibit.hallId, (countsByHallId.get(exhibit.hallId) || 0) + 1)
|
||
const keywords = keywordsByHallId.get(exhibit.hallId) || new Set<string>()
|
||
keywordParts.forEach((part) => keywords.add(part))
|
||
keywordsByHallId.set(exhibit.hallId, keywords)
|
||
}
|
||
|
||
if (exhibit.hallName) {
|
||
countsByHallName.set(exhibit.hallName, (countsByHallName.get(exhibit.hallName) || 0) + 1)
|
||
const keywords = keywordsByHallName.get(exhibit.hallName) || new Set<string>()
|
||
keywordParts.forEach((part) => keywords.add(part))
|
||
keywordsByHallName.set(exhibit.hallName, keywords)
|
||
}
|
||
})
|
||
|
||
return halls.map((hall) => {
|
||
const explainCount = countsByHallId.get(hall.id)
|
||
|| countsByHallName.get(hall.name)
|
||
|| hall.exhibitCount
|
||
|| 0
|
||
const keywordParts = [
|
||
hall.name,
|
||
hall.floorLabel,
|
||
hall.description,
|
||
hall.area,
|
||
...Array.from(keywordsByHallId.get(hall.id) || []),
|
||
...Array.from(keywordsByHallName.get(hall.name) || [])
|
||
].filter(Boolean) as string[]
|
||
|
||
return {
|
||
id: hall.id,
|
||
name: hall.name,
|
||
floorLabel: hall.floorLabel,
|
||
image: hall.image,
|
||
explainCount,
|
||
searchText: normalizeExplainKeyword(keywordParts.join(' '))
|
||
}
|
||
})
|
||
}
|
||
|
||
const handleGuideSearchTap = () => {
|
||
uni.navigateTo({
|
||
url: '/pages/search/index'
|
||
})
|
||
}
|
||
|
||
const handleFloorChange = (floor: string) => {
|
||
activeGuideFloor.value = floor
|
||
indoorView.value = 'floor'
|
||
selectedGuidePoi.value = null
|
||
isPoiCardCollapsed.value = false
|
||
console.log('切换楼层:', floor)
|
||
}
|
||
|
||
const handleIndoorViewChange = (view: GuideIndoorView) => {
|
||
indoorView.value = view
|
||
if (view !== 'floor') {
|
||
selectedGuidePoi.value = null
|
||
isPoiCardCollapsed.value = false
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|
||
|
||
const handleGuidePoiClick = (poi: GuideRenderPoi) => {
|
||
selectedGuidePoi.value = poi
|
||
isPoiCardCollapsed.value = false
|
||
indoorView.value = 'floor'
|
||
showGuideMoreMenu.value = false
|
||
isSimulatingRoute.value = false
|
||
const matchedFloor = guideFloors.value.find((floor) => floor.id === poi.floorId)
|
||
activeGuideFloor.value = matchedFloor?.label || poi.floorId
|
||
}
|
||
|
||
const handleGuideSelectionClear = () => {
|
||
selectedGuidePoi.value = null
|
||
isPoiCardCollapsed.value = false
|
||
}
|
||
|
||
const collapsePoiCard = () => {
|
||
isPoiCardCollapsed.value = true
|
||
}
|
||
|
||
const expandPoiCard = () => {
|
||
isPoiCardCollapsed.value = false
|
||
}
|
||
|
||
const togglePoiCardCollapsed = () => {
|
||
isPoiCardCollapsed.value = !isPoiCardCollapsed.value
|
||
}
|
||
|
||
const findRouteOptionForPoi = async (poi: GuideRenderPoi) => {
|
||
if (!routePointOptions.value.length) {
|
||
await loadRouteTargets()
|
||
}
|
||
|
||
return routePointOptions.value.find((option) => option.poiId === poi.id) || null
|
||
}
|
||
|
||
const setSelectedPoiAsRoutePoint = async (mode: 'start' | 'end') => {
|
||
if (!selectedGuidePoi.value) return
|
||
|
||
const option = await findRouteOptionForPoi(selectedGuidePoi.value)
|
||
if (!option) {
|
||
uni.showToast({
|
||
title: '该点位暂无线路数据,可先查看三维位置',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
if (mode === 'start') {
|
||
routeStartPoint.value = option
|
||
} else {
|
||
routeEndPoint.value = option
|
||
}
|
||
|
||
clearRoutePreviewState()
|
||
openRoutePlanner()
|
||
}
|
||
|
||
const handlePreviewSelectedPoi = () => {
|
||
if (!selectedGuidePoi.value) return
|
||
indoorView.value = 'floor'
|
||
showRoutePlanner.value = false
|
||
showGuideMoreMenu.value = false
|
||
isSimulatingRoute.value = false
|
||
}
|
||
|
||
const handleSetSelectedPoiAsStart = () => {
|
||
void setSelectedPoiAsRoutePoint('start')
|
||
}
|
||
|
||
const handleSetSelectedPoiAsEnd = () => {
|
||
void setSelectedPoiAsRoutePoint('end')
|
||
}
|
||
|
||
const handleSelectedPoiRelated = () => {
|
||
if (!selectedGuidePoi.value) return
|
||
uni.navigateTo({
|
||
url: `/pages/search/index?keyword=${encodeURIComponent(selectedGuidePoi.value.name)}`
|
||
})
|
||
}
|
||
|
||
const handleGuideToolClick = (tool: string) => {
|
||
if (tool === '清除') {
|
||
selectedGuidePoi.value = null
|
||
} else if (tool === '清路线') {
|
||
clearRoutePreviewState()
|
||
isSimulatingRoute.value = false
|
||
}
|
||
}
|
||
|
||
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()
|
||
routePreviewUnavailableMessage.value = readiness.ready
|
||
? ''
|
||
: readiness.message || '当前暂不支持正式导航,可查看位置预览和路线示意'
|
||
} catch (error) {
|
||
console.warn('读取路线能力状态失败:', error)
|
||
routePreviewUnavailableMessage.value = '当前暂不支持正式导航,可查看位置预览和路线示意'
|
||
}
|
||
}
|
||
|
||
const openRoutePlanner = () => {
|
||
showRoutePlanner.value = true
|
||
is3DMode.value = true
|
||
indoorView.value = indoorView.value === 'overview' ? 'floor' : indoorView.value
|
||
guideOutdoorState.value = 'home'
|
||
showGuideMoreMenu.value = false
|
||
isPoiCardCollapsed.value = false
|
||
isSimulatingRoute.value = false
|
||
if (!routePointOptions.value.length) {
|
||
void loadRouteTargets()
|
||
}
|
||
void refreshRouteReadinessState()
|
||
}
|
||
|
||
const clearRoutePreviewState = () => {
|
||
activeRoutePreview.value = null
|
||
routePlanError.value = ''
|
||
isSimulatingRoute.value = false
|
||
}
|
||
|
||
const handleRouteClear = () => {
|
||
routeStartPoint.value = null
|
||
routeEndPoint.value = null
|
||
showRoutePlanner.value = false
|
||
clearRoutePreviewState()
|
||
}
|
||
|
||
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 floor = guideFloors.value.find((item) => item.id === route.start.floorId)
|
||
const routeFloorCount = new Set(route.floorSegments.map((segment) => segment.floorId)).size
|
||
activeGuideFloor.value = floor?.label || route.start.floorLabel
|
||
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
|
||
showGuideMoreMenu.value = false
|
||
isSimulatingRoute.value = true
|
||
}
|
||
|
||
// 进入 3D 室内模式
|
||
const handleEnter3DMode = () => {
|
||
console.log('进入 3D 室内模式')
|
||
indoorView.value = 'floor'
|
||
is3DMode.value = true
|
||
guideOutdoorState.value = 'home'
|
||
showGuideMoreMenu.value = false
|
||
if (!routePointOptions.value.length) {
|
||
void loadRouteTargets()
|
||
}
|
||
}
|
||
|
||
const handleModeChange = (mode: '2d' | '3d') => {
|
||
is3DMode.value = mode === '3d'
|
||
|
||
if (mode === '2d') {
|
||
indoorView.value = 'overview'
|
||
guideOutdoorState.value = 'home'
|
||
selectedGuidePoi.value = null
|
||
isPoiCardCollapsed.value = false
|
||
showRoutePlanner.value = false
|
||
showGuideMoreMenu.value = false
|
||
isSimulatingRoute.value = false
|
||
clearRoutePreviewState()
|
||
} else {
|
||
indoorView.value = 'floor'
|
||
if (!routePointOptions.value.length) {
|
||
void loadRouteTargets()
|
||
}
|
||
}
|
||
}
|
||
|
||
const guideStatusLabel = computed(() => {
|
||
if (is3DMode.value) {
|
||
if (indoorView.value === 'overview') return '建筑外观'
|
||
if (indoorView.value === 'multi') return '室内多层'
|
||
return '室内单层'
|
||
}
|
||
|
||
if (guideOutdoorState.value === 'entrance') {
|
||
return '推荐入口'
|
||
}
|
||
|
||
return '室外参考'
|
||
})
|
||
|
||
const handleSwitchEntrance = () => {
|
||
guideOutdoorState.value = 'home'
|
||
}
|
||
|
||
const handleGuideMoreClick = () => {
|
||
showGuideMoreMenu.value = !showGuideMoreMenu.value
|
||
selectedGuidePoi.value = null
|
||
}
|
||
|
||
const handleMoreRouteGuide = () => {
|
||
openRoutePlanner()
|
||
}
|
||
|
||
const openOutdoorReference = (state: 'home' | 'entrance') => {
|
||
is3DMode.value = false
|
||
guideOutdoorState.value = state
|
||
indoorView.value = 'overview'
|
||
selectedGuidePoi.value = null
|
||
isPoiCardCollapsed.value = false
|
||
showRoutePlanner.value = false
|
||
showGuideMoreMenu.value = false
|
||
clearRoutePreviewState()
|
||
}
|
||
|
||
const handleMoreOutdoorReference = () => {
|
||
openOutdoorReference('home')
|
||
}
|
||
|
||
const handleMoreRecommendedEntrance = () => {
|
||
openOutdoorReference('entrance')
|
||
}
|
||
|
||
const handleMoreToggleLayer = () => {
|
||
is3DMode.value = true
|
||
guideOutdoorState.value = 'home'
|
||
indoorView.value = indoorLayerMode.value === 'multi' ? 'floor' : 'multi'
|
||
showGuideMoreMenu.value = false
|
||
}
|
||
|
||
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 = (hallId: string) => {
|
||
uni.navigateTo({
|
||
url: `/pages/hall/detail?id=${encodeURIComponent(hallId)}&tab=explain`
|
||
})
|
||
}
|
||
|
||
const handleExplainBack = () => {
|
||
currentTab.value = 'guide'
|
||
syncTopTabToUrl('guide')
|
||
loadTopTabData('guide')
|
||
showGuideMoreMenu.value = false
|
||
showRoutePlanner.value = false
|
||
isSimulatingRoute.value = false
|
||
isPoiCardCollapsed.value = false
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.explain-page {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: #f6f7f4;
|
||
z-index: 10;
|
||
}
|
||
|
||
.guide-poi-card,
|
||
.guide-more-popover {
|
||
position: absolute;
|
||
left: 12px;
|
||
right: 12px;
|
||
bottom: calc(env(safe-area-inset-bottom) + 30px);
|
||
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,
|
||
.more-popover-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(4, 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-more-popover {
|
||
padding: 10px 12px 12px;
|
||
}
|
||
|
||
.more-popover-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 8px;
|
||
}
|
||
|
||
.more-action {
|
||
min-height: 64px;
|
||
padding: 10px 8px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
gap: 4px;
|
||
box-sizing: border-box;
|
||
background: #f6f7f4;
|
||
border: 1px solid #e5e6de;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.more-action-title {
|
||
font-size: 14px;
|
||
line-height: 20px;
|
||
font-weight: 700;
|
||
color: #151713;
|
||
text-align: center;
|
||
}
|
||
|
||
.more-action-desc {
|
||
font-size: 11px;
|
||
line-height: 15px;
|
||
color: #696962;
|
||
text-align: center;
|
||
}
|
||
|
||
.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) + 30px);
|
||
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;
|
||
}
|
||
|
||
.home-shortcuts-card {
|
||
position: absolute;
|
||
left: 12px;
|
||
right: 12px;
|
||
bottom: calc(env(safe-area-inset-bottom) + 134px);
|
||
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) + 134px);
|
||
z-index: 1003;
|
||
}
|
||
|
||
.guide-task-card {
|
||
position: absolute;
|
||
left: 12px;
|
||
right: 12px;
|
||
bottom: calc(env(safe-area-inset-bottom) + 30px);
|
||
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) + 34px);
|
||
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>
|