963 lines
23 KiB
Vue
963 lines
23 KiB
Vue
<template>
|
||
<GuidePageFrame
|
||
active-tab="guide"
|
||
variant="overlay"
|
||
show-back
|
||
:show-cancel="isManualLocationPanelOpen"
|
||
@tab-change="handleTopTabChange"
|
||
@back="handlePageBack"
|
||
@cancel="handleManualLocationCancel"
|
||
>
|
||
<GuideMapShell
|
||
v-bind="shellConfig"
|
||
:indoor-model-source="indoorModelSource"
|
||
:floors="guideFloors"
|
||
:normalize-floor-id="normalizeGuideFloorId"
|
||
:target-focus-request="targetFocusRequest"
|
||
@search-tap="handleSearchTap"
|
||
@mode-change="handleModeChange"
|
||
@floor-change="handleFloorChange"
|
||
@tool-click="handleToolClick"
|
||
@target-focus="handleTargetFocus"
|
||
>
|
||
<template #overlay>
|
||
<view
|
||
v-if="routeViewState === 'location-error' && !isManualLocationPanelOpen"
|
||
class="location-error-dialog"
|
||
>
|
||
<view class="error-icon">
|
||
<text class="error-icon-mark">!</text>
|
||
</view>
|
||
<text class="error-title">无法自动获取起点</text>
|
||
<text class="error-desc">
|
||
可以返回位置预览,或手动选择预览起点所在楼层与区域。
|
||
</text>
|
||
<view class="error-actions">
|
||
<view class="dialog-btn secondary" @tap="handleRelocate">
|
||
<text class="dialog-btn-text">返回预览</text>
|
||
</view>
|
||
<view class="dialog-btn primary" @tap="handleManualSelect">
|
||
<text class="dialog-btn-text">手动选择起点</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view
|
||
v-if="routeViewState === 'location-error' && isManualLocationPanelOpen"
|
||
class="manual-location-sheet"
|
||
>
|
||
<text class="manual-title">选择楼层与区域</text>
|
||
<view class="chip-row floor-chip-row">
|
||
<view
|
||
v-for="floor in manualFloors"
|
||
:key="floor"
|
||
class="manual-chip"
|
||
:class="{ active: floor === selectedManualFloor }"
|
||
@tap="handleManualFloorChange(floor)"
|
||
>
|
||
<text class="manual-chip-text">{{ floor }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="chip-row area-chip-row">
|
||
<view
|
||
v-for="area in manualAreas"
|
||
:key="area"
|
||
class="manual-chip area"
|
||
:class="{ active: area === selectedManualArea }"
|
||
@tap="handleManualAreaChange(area)"
|
||
>
|
||
<text class="manual-chip-text">{{ area }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="manual-location-actions">
|
||
<view class="manual-location-btn secondary" @tap="handleManualLocationCancel">
|
||
<text class="manual-location-btn-text">取消</text>
|
||
</view>
|
||
<view class="manual-location-btn primary" @tap="handleConfirmLocation">
|
||
<text class="manual-location-btn-text">
|
||
确认预览起点:{{ selectedManualFloor }} {{ selectedManualArea }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="routeViewState === 'outdoor-preview'" class="entrance-tip">
|
||
<text class="entrance-tip-text">主入口 约 4 分钟</text>
|
||
</view>
|
||
</template>
|
||
|
||
<view v-if="!hasRouteTarget" class="target-required-card">
|
||
<text class="target-required-title">请选择目标位置</text>
|
||
<text class="target-required-desc">需要先选择展厅或设施,才能查看馆内三维位置。</text>
|
||
<view class="target-required-btn" @tap="handleSelectTarget">
|
||
<text class="target-required-btn-text">选择目标地点</text>
|
||
</view>
|
||
</view>
|
||
|
||
<LocationPreview
|
||
v-else-if="routeViewState === 'preview'"
|
||
:summary="route.summary"
|
||
:steps="route.steps"
|
||
:target-location="route.targetLocation"
|
||
@view-outdoor-map="handleViewOutdoorMap"
|
||
@show-target-location="handleShowTargetLocation"
|
||
/>
|
||
|
||
<view v-else-if="routeViewState === 'outdoor-preview'" class="outdoor-preview-card">
|
||
<text class="outdoor-preview-title">室外地图预览:{{ route.target }}</text>
|
||
<text class="outdoor-preview-desc">
|
||
室外地图仅作入口参考;馆内当前支持三维位置预览。
|
||
</text>
|
||
<view class="outdoor-preview-actions">
|
||
<view class="outdoor-preview-btn secondary" @tap="handleReturnToPreview">
|
||
<text class="outdoor-preview-btn-text">返回预览</text>
|
||
</view>
|
||
<view class="outdoor-preview-btn primary" @tap="handleReturnToPreview">
|
||
<text class="outdoor-preview-btn-text">返回室内3D</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</GuideMapShell>
|
||
</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 LocationPreview from '@/components/navigation/LocationPreview.vue'
|
||
import {
|
||
guideUseCase
|
||
} from '@/usecases/guideUseCase'
|
||
import {
|
||
initialLocationPreviewPlan,
|
||
toTargetFocusRequestViewModel
|
||
} from '@/view-models/guideViewModels'
|
||
import type {
|
||
GuideLocationPreview,
|
||
GuideLocationPreviewPlan,
|
||
GuideStartLocation,
|
||
MuseumFloor
|
||
} from '@/domain/museum'
|
||
import {
|
||
navigateToGuideTopTab,
|
||
saveLastGuideLocationPreviewUrl,
|
||
type GuideTopTab
|
||
} from '@/utils/guideTopTabs'
|
||
|
||
type RouteViewState = 'preview' | 'location-error' | 'outdoor-preview'
|
||
type GuideMode = '2d' | '3d'
|
||
type RouteTargetLocation = GuideLocationPreview
|
||
type RouteStartLocation = GuideStartLocation
|
||
|
||
interface TargetPoiFocusRequest extends RouteTargetLocation {
|
||
requestId: number | string
|
||
}
|
||
|
||
interface TargetPoiFocusResult {
|
||
requestId: number | string
|
||
poiId: string
|
||
floorId: string
|
||
status: 'focused' | 'missing' | 'error'
|
||
message?: string
|
||
}
|
||
|
||
type RoutePlan = GuideLocationPreviewPlan
|
||
|
||
const routeDeepLinkStates: RouteViewState[] = ['preview', 'outdoor-preview']
|
||
const indoorModelSource = guideUseCase.getModelSource()
|
||
const guideFloors = ref<MuseumFloor[]>([])
|
||
const normalizeGuideFloorId = (labelOrId: string) => guideUseCase.normalizeFloorId(labelOrId)
|
||
|
||
const route = ref<RoutePlan>(initialLocationPreviewPlan())
|
||
const hasRouteTarget = ref(false)
|
||
const routeViewState = ref<RouteViewState>('preview')
|
||
const activeFloor = ref('1F')
|
||
const targetFocusRequest = ref<TargetPoiFocusRequest | null>(null)
|
||
const targetFocusRequestId = ref(0)
|
||
const manualFloors = computed(() => guideFloors.value.map((floor) => floor.label))
|
||
const guideFloorLabel = (floorId: string) => (
|
||
guideFloors.value.find((floor) => floor.id === floorId)?.label || floorId
|
||
)
|
||
const defaultGuideFloorLabel = () => (
|
||
guideFloors.value.find((floor) => floor.id === 'L1')?.label
|
||
|| guideFloors.value[0]?.label
|
||
|| '1F'
|
||
)
|
||
const loadGuideFloors = async () => {
|
||
if (guideFloors.value.length) return guideFloors.value
|
||
|
||
try {
|
||
guideFloors.value = await guideUseCase.getFloors()
|
||
} catch (error) {
|
||
console.error('加载导览楼层失败:', error)
|
||
guideFloors.value = []
|
||
}
|
||
|
||
return guideFloors.value
|
||
}
|
||
const manualAreas = ['主入口', '服务台附近', '停车场入口']
|
||
const isManualLocationPanelOpen = ref(false)
|
||
const selectedManualFloor = ref('1F')
|
||
const selectedManualArea = ref('服务台附近')
|
||
const normalizeRouteOption = (value: unknown) => {
|
||
if (Array.isArray(value)) {
|
||
return value[0]
|
||
}
|
||
|
||
return value
|
||
}
|
||
|
||
const isRouteViewState = (state: unknown): state is RouteViewState => {
|
||
const normalizedState = normalizeRouteOption(state)
|
||
return typeof normalizedState === 'string' && routeDeepLinkStates.includes(normalizedState as RouteViewState)
|
||
}
|
||
|
||
const shellConfig = computed(() => {
|
||
if (!hasRouteTarget.value) {
|
||
return {
|
||
searchText: '选择目标位置',
|
||
searchTop: '60px',
|
||
activeMode: '3d' as GuideMode,
|
||
activeFloor: activeFloor.value,
|
||
floorTop: '198px',
|
||
toolsTop: '452px',
|
||
tools: [],
|
||
showSearch: true,
|
||
showFloor: false,
|
||
modeTop: '104px',
|
||
modeLayout: 'full' as const,
|
||
mapType: 'indoor' as const
|
||
}
|
||
}
|
||
|
||
if (routeViewState.value === 'location-error') {
|
||
return {
|
||
searchText: '搜索设施、展厅、入口',
|
||
searchTop: '60px',
|
||
activeMode: '3d' as GuideMode,
|
||
activeFloor: activeFloor.value,
|
||
floorTop: '198px',
|
||
toolsTop: '452px',
|
||
tools: [],
|
||
showSearch: true,
|
||
showFloor: false,
|
||
modeTop: '104px',
|
||
modeLayout: 'full' as const,
|
||
mapType: 'indoor' as const,
|
||
dimmed: true
|
||
}
|
||
}
|
||
|
||
if (routeViewState.value === 'outdoor-preview') {
|
||
return {
|
||
searchText: `室外地图参考:${route.value.target}`,
|
||
searchTop: '60px',
|
||
activeMode: '2d' as GuideMode,
|
||
activeFloor: activeFloor.value,
|
||
floorTop: '198px',
|
||
toolsTop: '452px',
|
||
tools: [],
|
||
showSearch: true,
|
||
showFloor: false,
|
||
modeTop: '104px',
|
||
modeLayout: 'status' as const,
|
||
modeStatus: '室外参考',
|
||
mapType: 'outdoor' as const,
|
||
outdoorVariant: 'entrance' as const
|
||
}
|
||
}
|
||
|
||
return {
|
||
searchText: `位置预览:${route.value.target}`,
|
||
searchTop: '60px',
|
||
activeMode: '3d' as GuideMode,
|
||
activeFloor: activeFloor.value,
|
||
floorTop: '198px',
|
||
toolsTop: '452px',
|
||
tools: ['回正'],
|
||
showSearch: true,
|
||
showFloor: true,
|
||
modeTop: '104px',
|
||
modeLayout: 'full' as const,
|
||
mapType: 'indoor' as const
|
||
}
|
||
})
|
||
|
||
const safeDecode = (value: string) => {
|
||
try {
|
||
return decodeURIComponent(value)
|
||
} catch (error) {
|
||
return value
|
||
}
|
||
}
|
||
|
||
const normalizeStringOption = (value: unknown) => {
|
||
const normalizedValue = normalizeRouteOption(value)
|
||
return typeof normalizedValue === 'string' ? safeDecode(normalizedValue).trim() : ''
|
||
}
|
||
|
||
const setLocationPreviewTitle = () => {
|
||
if (typeof uni !== 'undefined') {
|
||
uni.setNavigationBarTitle({
|
||
title: '位置预览'
|
||
})
|
||
}
|
||
|
||
if (typeof document !== 'undefined') {
|
||
document.title = '位置预览'
|
||
}
|
||
}
|
||
|
||
const saveCurrentLocationPreviewUrl = () => {
|
||
if (!route.value.facilityId) return
|
||
|
||
const params = new URLSearchParams({
|
||
facilityId: route.value.facilityId,
|
||
target: route.value.target,
|
||
state: routeViewState.value
|
||
})
|
||
|
||
if (route.value.startLocation?.label) {
|
||
params.set('startLabel', route.value.startLocation.label)
|
||
}
|
||
if (route.value.startLocation?.floor) {
|
||
params.set('startFloor', route.value.startLocation.floor)
|
||
}
|
||
if (route.value.startLocation?.source) {
|
||
params.set('startSource', route.value.startLocation.source)
|
||
}
|
||
|
||
saveLastGuideLocationPreviewUrl(`/pages/route/detail?${params.toString()}`)
|
||
}
|
||
|
||
const createStartLocationFromOptions = (options: Record<string, unknown>) => {
|
||
const label = normalizeStringOption(options.startLabel)
|
||
|
||
if (!label) return null
|
||
|
||
const floor = normalizeStringOption(options.startFloor)
|
||
const source = normalizeStringOption(options.startSource)
|
||
|
||
return {
|
||
label,
|
||
floor: floor || undefined,
|
||
source: source || undefined
|
||
}
|
||
}
|
||
|
||
const requestTargetFocus = (target: Partial<RouteTargetLocation> | null, fallbackName = route.value.target) => {
|
||
if (!target?.poiId || !target.floorId) {
|
||
targetFocusRequest.value = null
|
||
return false
|
||
}
|
||
|
||
const floorLabel = target.floorLabel || guideFloorLabel(target.floorId)
|
||
const focusTarget: RouteTargetLocation = {
|
||
poiId: target.poiId,
|
||
name: target.name || fallbackName,
|
||
floorId: target.floorId,
|
||
floorLabel,
|
||
primaryCategoryZh: target.primaryCategoryZh,
|
||
positionGltf: target.positionGltf
|
||
}
|
||
|
||
activeFloor.value = floorLabel
|
||
routeViewState.value = 'preview'
|
||
targetFocusRequestId.value += 1
|
||
targetFocusRequest.value = toTargetFocusRequestViewModel(
|
||
focusTarget,
|
||
targetFocusRequestId.value
|
||
)
|
||
|
||
return true
|
||
}
|
||
|
||
const applyRouteOptions = async (options: Record<string, unknown> = {}) => {
|
||
await loadGuideFloors()
|
||
|
||
const facilityId = normalizeStringOption(options.facilityId)
|
||
const target = normalizeStringOption(options.target)
|
||
const state = normalizeRouteOption(options.state)
|
||
const startLocation = createStartLocationFromOptions(options)
|
||
|
||
if (!facilityId && !target) {
|
||
hasRouteTarget.value = false
|
||
route.value = initialLocationPreviewPlan()
|
||
routeViewState.value = 'preview'
|
||
activeFloor.value = defaultGuideFloorLabel()
|
||
targetFocusRequest.value = null
|
||
isManualLocationPanelOpen.value = false
|
||
return false
|
||
}
|
||
|
||
const matchedPoi = facilityId ? await guideUseCase.getPoiById(facilityId) : null
|
||
|
||
if (!matchedPoi) {
|
||
hasRouteTarget.value = false
|
||
route.value = await guideUseCase.createLocationPreviewPlan('', target || '目标地点', startLocation)
|
||
routeViewState.value = 'preview'
|
||
activeFloor.value = startLocation?.floor || defaultGuideFloorLabel()
|
||
targetFocusRequest.value = null
|
||
isManualLocationPanelOpen.value = false
|
||
return false
|
||
}
|
||
|
||
route.value = await guideUseCase.createLocationPreviewPlan(
|
||
facilityId,
|
||
target || matchedPoi.name,
|
||
startLocation
|
||
)
|
||
|
||
hasRouteTarget.value = true
|
||
routeViewState.value = isRouteViewState(state) ? state : 'preview'
|
||
isManualLocationPanelOpen.value = false
|
||
|
||
if (routeViewState.value === 'preview') {
|
||
requestTargetFocus(route.value.targetLocation, matchedPoi.name)
|
||
} else {
|
||
activeFloor.value = matchedPoi.floorLabel || guideFloorLabel(matchedPoi.floorId)
|
||
}
|
||
|
||
setLocationPreviewTitle()
|
||
saveCurrentLocationPreviewUrl()
|
||
|
||
return true
|
||
}
|
||
|
||
onLoad((options: any) => {
|
||
setLocationPreviewTitle()
|
||
void applyRouteOptions(options)
|
||
})
|
||
|
||
// #ifdef H5
|
||
const readH5RouteOptions = () => {
|
||
const hash = window.location.hash || ''
|
||
const queryStart = hash.indexOf('?')
|
||
const query = queryStart >= 0 ? hash.slice(queryStart + 1) : window.location.search.slice(1)
|
||
const params = new URLSearchParams(query)
|
||
const options: Record<string, string> = {}
|
||
|
||
params.forEach((value, key) => {
|
||
options[key] = value
|
||
})
|
||
|
||
return options
|
||
}
|
||
|
||
const syncH5RouteOptions = () => {
|
||
void applyRouteOptions(readH5RouteOptions())
|
||
}
|
||
|
||
onMounted(() => {
|
||
syncH5RouteOptions()
|
||
window.addEventListener('hashchange', syncH5RouteOptions)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
window.removeEventListener('hashchange', syncH5RouteOptions)
|
||
})
|
||
// #endif
|
||
|
||
const handleSelectTarget = () => {
|
||
uni.redirectTo({
|
||
url: '/pages/search/index',
|
||
fail: () => {
|
||
uni.navigateTo({
|
||
url: '/pages/search/index'
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
const handleSearchTap = () => {
|
||
uni.navigateTo({
|
||
url: `/pages/search/index?keyword=${encodeURIComponent(route.value.target)}`
|
||
})
|
||
}
|
||
|
||
const handleViewOutdoorMap = () => {
|
||
isManualLocationPanelOpen.value = false
|
||
routeViewState.value = 'outdoor-preview'
|
||
saveCurrentLocationPreviewUrl()
|
||
}
|
||
|
||
const handleShowTargetLocation = (target: Partial<RouteTargetLocation> | null) => {
|
||
const focused = requestTargetFocus(target || route.value.targetLocation)
|
||
|
||
if (!focused) {
|
||
uni.showToast({
|
||
title: '目标暂无三维位置数据',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
uni.showToast({
|
||
title: '已显示三维位置',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
|
||
const handleTargetFocus = (result: TargetPoiFocusResult) => {
|
||
if (result.status === 'focused') return
|
||
|
||
uni.showToast({
|
||
title: result.message || '目标位置暂时无法聚焦',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
|
||
const handleReturnToPreview = () => {
|
||
isManualLocationPanelOpen.value = false
|
||
routeViewState.value = 'preview'
|
||
requestTargetFocus(route.value.targetLocation)
|
||
saveCurrentLocationPreviewUrl()
|
||
}
|
||
|
||
const handleRelocate = () => {
|
||
isManualLocationPanelOpen.value = false
|
||
routeViewState.value = 'preview'
|
||
}
|
||
|
||
const handleManualSelect = () => {
|
||
isManualLocationPanelOpen.value = true
|
||
}
|
||
|
||
const handleManualFloorChange = (floor: string) => {
|
||
selectedManualFloor.value = floor
|
||
}
|
||
|
||
const handleManualAreaChange = (area: string) => {
|
||
selectedManualArea.value = area
|
||
}
|
||
|
||
const handleManualLocationCancel = () => {
|
||
isManualLocationPanelOpen.value = false
|
||
}
|
||
|
||
const createManualStartLocation = (): RouteStartLocation => ({
|
||
label: `${selectedManualFloor.value} ${selectedManualArea.value}`,
|
||
floor: selectedManualFloor.value,
|
||
source: 'manual'
|
||
})
|
||
|
||
const handleConfirmLocation = async () => {
|
||
const startLocation = createManualStartLocation()
|
||
|
||
route.value = await guideUseCase.createLocationPreviewPlan(
|
||
route.value.facilityId,
|
||
route.value.target,
|
||
startLocation
|
||
)
|
||
activeFloor.value = startLocation.floor || activeFloor.value
|
||
isManualLocationPanelOpen.value = false
|
||
routeViewState.value = 'preview'
|
||
requestTargetFocus(route.value.targetLocation)
|
||
saveCurrentLocationPreviewUrl()
|
||
}
|
||
|
||
const handleModeChange = (mode: GuideMode) => {
|
||
if (routeViewState.value === 'outdoor-preview' && mode === '3d') {
|
||
routeViewState.value = 'preview'
|
||
requestTargetFocus(route.value.targetLocation)
|
||
saveCurrentLocationPreviewUrl()
|
||
return
|
||
}
|
||
|
||
if (routeViewState.value !== 'outdoor-preview' && mode === '2d') {
|
||
routeViewState.value = 'outdoor-preview'
|
||
saveCurrentLocationPreviewUrl()
|
||
return
|
||
}
|
||
|
||
console.log('位置预览切换导览模式:', mode)
|
||
}
|
||
|
||
const handleFloorChange = (floor: string) => {
|
||
activeFloor.value = floor
|
||
console.log('位置预览切换楼层:', floor)
|
||
}
|
||
|
||
const handleToolClick = (tool: string) => {
|
||
if (tool === '重定') {
|
||
routeViewState.value = 'location-error'
|
||
return
|
||
}
|
||
|
||
uni.showToast({
|
||
title: tool === '回正' ? '已回到三维总览' : guideUseCase.getRouteReadinessSnapshot().message,
|
||
icon: 'none'
|
||
})
|
||
}
|
||
|
||
const handleTopTabChange = (tab: GuideTopTab) => {
|
||
navigateToGuideTopTab(tab)
|
||
}
|
||
|
||
const handlePageBack = () => {
|
||
if (isManualLocationPanelOpen.value) {
|
||
handleManualLocationCancel()
|
||
return
|
||
}
|
||
|
||
uni.navigateBack({
|
||
delta: 1,
|
||
fail: () => {
|
||
uni.reLaunch({
|
||
url: '/pages/index/index'
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.target-required-card {
|
||
position: absolute;
|
||
left: 16px;
|
||
right: 16px;
|
||
bottom: calc(env(safe-area-inset-bottom) + 34px);
|
||
min-height: 170px;
|
||
padding: 22px 20px 20px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-sizing: border-box;
|
||
background: #ffffff;
|
||
border: 0;
|
||
border-radius: 20px;
|
||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.14);
|
||
z-index: 45;
|
||
}
|
||
|
||
.target-required-title {
|
||
font-size: 18px;
|
||
line-height: 24px;
|
||
font-weight: 700;
|
||
color: #000000;
|
||
}
|
||
|
||
.target-required-desc {
|
||
margin-top: 10px;
|
||
font-size: 13px;
|
||
line-height: 19px;
|
||
color: #5f666d;
|
||
}
|
||
|
||
.target-required-btn {
|
||
margin-top: 22px;
|
||
height: 42px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-sizing: border-box;
|
||
background: #000000;
|
||
border: 1px solid #000000;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.target-required-btn-text {
|
||
font-size: 14px;
|
||
line-height: 19px;
|
||
font-weight: 500;
|
||
color: var(--museum-accent);
|
||
}
|
||
|
||
.outdoor-preview-actions,
|
||
.error-actions {
|
||
display: flex;
|
||
}
|
||
|
||
.outdoor-preview-btn,
|
||
.dialog-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-sizing: border-box;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.outdoor-preview-btn-text,
|
||
.dialog-btn-text {
|
||
font-size: 14px;
|
||
line-height: 19px;
|
||
font-weight: 500;
|
||
color: #000000;
|
||
}
|
||
|
||
.outdoor-preview-btn.primary .outdoor-preview-btn-text,
|
||
.dialog-btn.primary .dialog-btn-text {
|
||
color: var(--museum-accent);
|
||
}
|
||
|
||
.location-error-dialog {
|
||
position: absolute;
|
||
top: 200px;
|
||
left: 24px;
|
||
width: 327px;
|
||
height: 288px;
|
||
padding: 28px 24px 36px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
box-sizing: border-box;
|
||
background: #ffffff;
|
||
border-radius: 18px;
|
||
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.18);
|
||
z-index: 55;
|
||
}
|
||
|
||
.error-icon {
|
||
width: 64px;
|
||
height: 64px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #000000;
|
||
border-radius: 32px;
|
||
}
|
||
|
||
.error-icon-mark {
|
||
font-size: 34px;
|
||
line-height: 40px;
|
||
font-weight: 700;
|
||
color: var(--museum-accent);
|
||
}
|
||
|
||
.error-title {
|
||
margin-top: 18px;
|
||
font-size: 20px;
|
||
line-height: 27px;
|
||
font-weight: 700;
|
||
color: #000000;
|
||
}
|
||
|
||
.error-desc {
|
||
width: 258px;
|
||
margin-top: 10px;
|
||
font-size: 13px;
|
||
line-height: 20px;
|
||
color: #626970;
|
||
text-align: center;
|
||
}
|
||
|
||
.error-actions {
|
||
position: absolute;
|
||
left: 24px;
|
||
right: 24px;
|
||
bottom: 36px;
|
||
gap: 16px;
|
||
}
|
||
|
||
.dialog-btn {
|
||
width: 132px;
|
||
height: 40px;
|
||
}
|
||
|
||
.dialog-btn.secondary {
|
||
background: #ffffff;
|
||
border: 1px solid #000000;
|
||
}
|
||
|
||
.dialog-btn.primary {
|
||
background: #000000;
|
||
border: 1px solid #000000;
|
||
}
|
||
|
||
.manual-location-sheet {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: calc(env(safe-area-inset-bottom) + 34px);
|
||
height: 196px;
|
||
padding: 21px 20px 14px;
|
||
box-sizing: border-box;
|
||
background: #ffffff;
|
||
border-radius: 20px;
|
||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.14);
|
||
z-index: 54;
|
||
}
|
||
|
||
.manual-title {
|
||
display: block;
|
||
font-size: 18px;
|
||
line-height: 24px;
|
||
font-weight: 700;
|
||
color: #000000;
|
||
}
|
||
|
||
.chip-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.floor-chip-row {
|
||
margin-top: 17px;
|
||
}
|
||
|
||
.area-chip-row {
|
||
margin-top: 14px;
|
||
}
|
||
|
||
.manual-chip {
|
||
height: 28px;
|
||
min-width: 48px;
|
||
padding: 0 13px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-sizing: border-box;
|
||
background: #f4f4ef;
|
||
border: 1px solid #e7e8e2;
|
||
border-radius: 14px;
|
||
}
|
||
|
||
.manual-chip.area {
|
||
min-width: 72px;
|
||
}
|
||
|
||
.manual-chip.active {
|
||
background: #000000;
|
||
border-color: #000000;
|
||
}
|
||
|
||
.manual-chip-text {
|
||
font-size: 12px;
|
||
line-height: 16px;
|
||
font-weight: 500;
|
||
color: #60666d;
|
||
}
|
||
|
||
.manual-chip.active .manual-chip-text {
|
||
color: var(--museum-accent);
|
||
}
|
||
|
||
.manual-location-actions {
|
||
position: absolute;
|
||
left: 20px;
|
||
right: 20px;
|
||
bottom: 14px;
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.manual-location-btn {
|
||
height: 40px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-sizing: border-box;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.manual-location-btn.secondary {
|
||
width: 82px;
|
||
background: #ffffff;
|
||
border: 1px solid #000000;
|
||
}
|
||
|
||
.manual-location-btn.primary {
|
||
flex: 1;
|
||
min-width: 0;
|
||
background: #000000;
|
||
border: 1px solid #000000;
|
||
}
|
||
|
||
.manual-location-btn-text {
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-size: 14px;
|
||
line-height: 19px;
|
||
font-weight: 500;
|
||
color: #000000;
|
||
}
|
||
|
||
.manual-location-btn.primary .manual-location-btn-text {
|
||
color: var(--museum-accent);
|
||
}
|
||
|
||
.entrance-tip {
|
||
position: absolute;
|
||
top: 154px;
|
||
left: 30px;
|
||
width: 168px;
|
||
height: 42px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-sizing: border-box;
|
||
background: #ffffff;
|
||
border: 1px solid #e3e6df;
|
||
border-radius: 10px;
|
||
box-shadow: 0 8px 18px rgba(80, 98, 88, 0.12);
|
||
z-index: 45;
|
||
}
|
||
|
||
.entrance-tip-text {
|
||
font-size: 14px;
|
||
line-height: 19px;
|
||
font-weight: 600;
|
||
color: #1f2329;
|
||
}
|
||
|
||
.outdoor-preview-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-radius: 18px;
|
||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.14);
|
||
z-index: 45;
|
||
}
|
||
|
||
.outdoor-preview-title {
|
||
display: block;
|
||
font-size: 18px;
|
||
line-height: 24px;
|
||
font-weight: 700;
|
||
color: #000000;
|
||
}
|
||
|
||
.outdoor-preview-desc {
|
||
display: block;
|
||
margin-top: 10px;
|
||
font-size: 13px;
|
||
line-height: 20px;
|
||
color: #626970;
|
||
}
|
||
|
||
.outdoor-preview-actions {
|
||
position: absolute;
|
||
left: 16px;
|
||
right: 16px;
|
||
bottom: 14px;
|
||
gap: 12px;
|
||
}
|
||
|
||
.outdoor-preview-btn {
|
||
height: 40px;
|
||
}
|
||
|
||
.outdoor-preview-btn.secondary {
|
||
width: 126px;
|
||
background: #ffffff;
|
||
border: 1px solid #000000;
|
||
}
|
||
|
||
.outdoor-preview-btn.primary {
|
||
width: 181px;
|
||
background: #000000;
|
||
border: 1px solid #000000;
|
||
}
|
||
</style>
|