优化导览预览与讲解展示层

This commit is contained in:
lyf
2026-06-25 02:48:33 +08:00
parent 19516a507b
commit 33641e75df
17 changed files with 1224 additions and 793 deletions

View File

@@ -0,0 +1,339 @@
<template>
<view class="explain-hall-select">
<view class="explain-page-header">
<view class="header-back" @tap="handleBack">
<text class="header-back-icon"></text>
<text class="header-back-text">返回</text>
</view>
<text class="header-title">讲解</text>
</view>
<scroll-view
class="explain-scroll"
scroll-y
:show-scrollbar="false"
>
<view v-if="loading" class="state-block">
<text class="state-title">正在加载展厅讲解</text>
<text class="state-desc">稍后将展示可选择的展厅</text>
</view>
<view v-else-if="error" class="state-block error">
<text class="state-title">讲解内容加载失败</text>
<text class="state-desc">{{ error }}</text>
</view>
<template v-else>
<view class="section-heading">
<text class="section-title">请选择展厅</text>
<text class="section-count">{{ filteredHalls.length }} 个展厅</text>
</view>
<view v-if="filteredHalls.length" class="hall-list">
<view
v-for="hall in filteredHalls"
:key="hall.id"
class="hall-card"
@tap="handleHallClick(hall.id)"
>
<image
v-if="hall.image"
class="hall-thumb"
:src="hall.image"
mode="aspectFill"
/>
<view v-else class="hall-thumb placeholder">
<text class="hall-thumb-text">{{ hallIconText(hall.name) }}</text>
</view>
<view class="hall-main">
<text class="hall-name">{{ hall.name }}</text>
<view class="hall-meta-row">
<view class="floor-badge">
<text class="floor-badge-text">{{ hall.floorLabel || '楼层待补充' }}</text>
</view>
<text class="hall-meta-text">{{ hall.explainCount > 0 ? `${hall.explainCount} 条讲解` : '暂无讲解' }}</text>
</view>
</view>
<text class="hall-arrow"></text>
</view>
</view>
<view v-else class="state-block empty">
<text class="state-title">未找到相关展厅</text>
<text class="state-desc">暂无可选择的展厅讲解</text>
</view>
</template>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { computed } from 'vue'
export interface ExplainHallSelectItem {
id: string
name: string
floorLabel?: string
image?: string
explainCount: number
searchText: string
}
const props = withDefaults(defineProps<{
halls?: ExplainHallSelectItem[]
loading?: boolean
error?: string
}>(), {
halls: () => [],
loading: false,
error: ''
})
const emit = defineEmits<{
hallClick: [hallId: string]
back: []
}>()
const filteredHalls = computed(() => props.halls)
const hallIconText = (name: string) => name.trim().slice(0, 1) || '讲'
const handleHallClick = (hallId: string) => {
emit('hallClick', hallId)
}
const handleBack = () => {
emit('back')
}
</script>
<style scoped lang="scss">
.explain-hall-select {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
background: #f9fafb;
}
.explain-scroll {
height: 100%;
padding: 76px 20px calc(28px + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.section-title,
.section-count,
.state-title,
.state-desc,
.hall-name,
.hall-meta-text {
display: block;
}
.explain-page-header {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 20px;
box-sizing: border-box;
background: #ffffff;
border-bottom: 1px solid #eceee8;
z-index: 2001;
}
.header-back {
position: absolute;
left: 12px;
top: 0;
height: 56px;
min-width: 74px;
display: flex;
align-items: center;
gap: 2px;
}
.header-back-icon {
font-size: 27px;
line-height: 27px;
color: #151713;
}
.header-back-text {
font-size: 15px;
line-height: 21px;
font-weight: 500;
color: #151713;
}
.header-title {
font-size: 18px;
line-height: 25px;
font-weight: 800;
color: #151713;
}
.section-heading {
margin-top: 0;
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.section-title {
font-size: 19px;
line-height: 27px;
font-weight: 800;
color: #151713;
}
.section-count {
flex-shrink: 0;
font-size: 12px;
line-height: 18px;
color: #7b8275;
}
.hall-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.hall-card {
min-height: 112px;
padding: 14px 13px 14px 16px;
display: flex;
align-items: center;
gap: 14px;
box-sizing: border-box;
background: #ffffff;
border: 1px solid #e4e6df;
border-radius: 8px;
box-shadow: 0 8px 18px rgba(36, 49, 42, 0.05);
}
.hall-card:active {
background: #f7f8f3;
}
.hall-thumb {
flex-shrink: 0;
width: 68px;
height: 68px;
border-radius: 8px;
background: #f1f2ee;
}
.hall-thumb.placeholder {
display: flex;
align-items: center;
justify-content: center;
}
.hall-thumb-text {
font-size: 24px;
line-height: 32px;
font-weight: 800;
color: #83927a;
}
.hall-main {
flex: 1;
min-width: 0;
}
.hall-name {
font-size: 17px;
line-height: 24px;
font-weight: 800;
color: #151713;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hall-meta-row {
margin-top: 10px;
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.floor-badge {
flex-shrink: 0;
min-width: 28px;
height: 24px;
padding: 0 6px;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
background: #ffffff;
border: 1px solid #e0df00;
border-radius: 6px;
}
.floor-badge-text {
font-size: 13px;
line-height: 17px;
font-weight: 600;
color: #aaa900;
}
.hall-meta-text {
min-width: 0;
font-size: 15px;
line-height: 21px;
color: #555c51;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hall-arrow {
flex-shrink: 0;
font-size: 30px;
line-height: 30px;
color: #151713;
}
.state-block {
margin: 34px 0;
padding: 22px 16px;
box-sizing: border-box;
text-align: center;
background: #ffffff;
border: 1px solid #e4e6df;
border-radius: 8px;
}
.state-title {
font-size: 15px;
line-height: 21px;
font-weight: 700;
color: #151713;
}
.state-desc {
margin-top: 6px;
font-size: 12px;
line-height: 18px;
color: #68725d;
}
.state-block.error {
border-color: #e5c2c2;
background: #fff7f7;
}
</style>

View File

@@ -64,6 +64,7 @@ import type {
type ViewMode = 'overview' | 'floor' | 'multi'
type CameraPreset = 'top' | 'oblique'
type TouchGestureMode = 'orbit' | 'pan'
type FloorIndexItem = GuideModelFloorAsset
@@ -79,12 +80,23 @@ type PoiVisibilityTier = 'tight' | 'balanced' | 'full'
interface PoiSpriteUserData {
poi?: RenderPoi
baseScale?: number
labelBaseScaleX?: number
labelBaseScaleY?: number
isPoiLabel?: boolean
isPoiPulse?: boolean
isPoiBase?: boolean
isCorePoi?: boolean
}
interface FocusHallMaterialState {
material: THREE.Material
color?: THREE.Color
emissive?: THREE.Color
emissiveIntensity?: number
opacity: number
transparent: boolean
}
interface TargetPoiFocusRequest {
requestId: number | string
poiId: string
@@ -93,6 +105,7 @@ interface TargetPoiFocusRequest {
floorLabel?: string
primaryCategoryZh?: string
positionGltf?: [number, number, number]
sourceObjectName?: string
}
interface TargetPoiFocusResult {
@@ -120,6 +133,8 @@ const props = withDefaults(defineProps<{
showControls?: boolean
showPoi?: boolean
targetFocus?: TargetPoiFocusRequest | null
touchGestureMode?: TouchGestureMode
targetFocusDistanceFactor?: number
routePreview?: GuideRouteResult | null
showRoute?: boolean
autoSwitch?: boolean
@@ -133,6 +148,8 @@ const props = withDefaults(defineProps<{
showControls: true,
showPoi: false,
targetFocus: null,
touchGestureMode: 'orbit',
targetFocusDistanceFactor: 0.22,
routePreview: null,
showRoute: false,
autoSwitch: true,
@@ -183,6 +200,8 @@ let routeGroup: THREE.Group | null = null
let activeFocusLabelSprite: THREE.Sprite | null = null
let activeFocusPulseSprite: THREE.Sprite | null = null
let activeFocusBaseSprite: THREE.Sprite | null = null
let activeFocusHallGlowMesh: THREE.Mesh | null = null
let activeFocusHallMaterialStates: FocusHallMaterialState[] = []
let animationId = 0
let resizeObserver: ResizeObserver | null = null
let isDisposed = false
@@ -201,6 +220,15 @@ let autoSwitchDisableTimer: ReturnType<typeof setTimeout> | null = null
let isProgrammaticCameraChange = false
let programmaticCameraTimer: ReturnType<typeof setTimeout> | null = null
const syncControlInteractionOptions = () => {
if (!controls) return
const shouldEnablePan = props.showControls || props.touchGestureMode === 'pan'
controls.enablePan = shouldEnablePan
controls.touches.ONE = props.touchGestureMode === 'pan' ? THREE.TOUCH.PAN : THREE.TOUCH.ROTATE
controls.touches.TWO = THREE.TOUCH.DOLLY_PAN
}
const corePoiCategories = new Set([
'basic_service_facility',
'transport_circulation',
@@ -276,6 +304,61 @@ const getModelNodeNames = (object: THREE.Object3D) => {
return names
}
const normalizeModelMatchKey = (value: string) => (
value
.trim()
.toLowerCase()
.replace(/[\s_\-./\\]/g, '')
)
const getPoiModelMatchKeys = (poi: RenderPoi) => (
[
poi.sourceObjectName,
`${poi.floorId}_${poi.name}`,
poi.name
]
.filter((value): value is string => Boolean(value))
.map(normalizeModelMatchKey)
.filter(Boolean)
)
const isPoiModelNodeMatch = (object: THREE.Object3D, poi: RenderPoi) => {
const keys = getPoiModelMatchKeys(poi)
if (!keys.length) return false
return getModelNodeNames(object)
.map(normalizeModelMatchKey)
.some((name) => keys.some((key) => name === key || name.includes(key)))
}
const findPoiModelRoot = (poi: RenderPoi): THREE.Object3D | null => {
if (!activeModel) return null
let matchedRoot: THREE.Object3D | null = null
activeModel.traverse((child) => {
if (matchedRoot || child === activeModel) return
const childName = child.name ? normalizeModelMatchKey(child.name) : ''
const keys = getPoiModelMatchKeys(poi)
if (childName && keys.some((key) => childName === key || childName.includes(key))) {
matchedRoot = child
}
})
if (matchedRoot) return matchedRoot
activeModel.traverse((child) => {
if (matchedRoot || !(child instanceof THREE.Mesh) || !child.visible) return
if (isPoiModelNodeMatch(child, poi)) {
matchedRoot = child.parent && child.parent !== activeModel ? child.parent : child
}
})
return matchedRoot
}
const isExteriorModelNode = (object: THREE.Object3D) => (
getModelNodeNames(object).some((name) => (
floorExteriorNameKeywords.some((keyword) => name.includes(keyword))
@@ -591,12 +674,12 @@ const initThree = async () => {
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.08
controls.enablePan = props.showControls
controls.enableZoom = true
controls.minDistance = 6
controls.maxDistance = 1200
controls.minPolarAngle = 0.08
controls.maxPolarAngle = Math.PI * 0.48
syncControlInteractionOptions()
loader = new GLTFLoader()
poiGroup = new THREE.Group()
@@ -636,6 +719,7 @@ const startRenderLoop = () => {
if (isDisposed || !renderer || !scene || !camera) return
controls?.update()
updateFocusLabelScale()
renderer.render(scene, camera)
animationId = window.requestAnimationFrame(render)
}
@@ -675,6 +759,174 @@ const disposeObject = (object: THREE.Object3D) => {
})
}
const getMaterialColor = (material: THREE.Material) => {
const candidate = material as THREE.Material & { color?: unknown }
return candidate.color instanceof THREE.Color ? candidate.color : undefined
}
const getMaterialEmissive = (material: THREE.Material) => {
const candidate = material as THREE.Material & { emissive?: unknown }
return candidate.emissive instanceof THREE.Color ? candidate.emissive : undefined
}
const getMaterialEmissiveIntensity = (material: THREE.Material) => {
const candidate = material as THREE.Material & { emissiveIntensity?: unknown }
return typeof candidate.emissiveIntensity === 'number'
? candidate.emissiveIntensity
: undefined
}
const setMaterialEmissiveIntensity = (material: THREE.Material, value: number) => {
const candidate = material as THREE.Material & { emissiveIntensity?: number }
if (typeof candidate.emissiveIntensity === 'number') {
candidate.emissiveIntensity = value
}
}
const clearFocusHallHighlight = () => {
activeFocusHallMaterialStates.forEach((state) => {
const color = getMaterialColor(state.material)
const emissive = getMaterialEmissive(state.material)
if (color && state.color) {
color.copy(state.color)
}
if (emissive && state.emissive) {
emissive.copy(state.emissive)
}
if (typeof state.emissiveIntensity === 'number') {
setMaterialEmissiveIntensity(state.material, state.emissiveIntensity)
}
state.material.opacity = state.opacity
state.material.transparent = state.transparent
state.material.needsUpdate = true
})
activeFocusHallMaterialStates = []
if (activeFocusHallGlowMesh) {
activeFocusHallGlowMesh.parent?.remove(activeFocusHallGlowMesh)
const material = activeFocusHallGlowMesh.material
if (!Array.isArray(material)) {
const glowMaterial = material as THREE.MeshBasicMaterial
glowMaterial.map?.dispose()
}
disposeObject(activeFocusHallGlowMesh)
activeFocusHallGlowMesh = null
}
}
const applyFocusHallMaterial = (material: THREE.Material, seenMaterials: Set<THREE.Material>) => {
if (seenMaterials.has(material)) return
seenMaterials.add(material)
const color = getMaterialColor(material)
const emissive = getMaterialEmissive(material)
const emissiveIntensity = getMaterialEmissiveIntensity(material)
activeFocusHallMaterialStates.push({
material,
color: color?.clone(),
emissive: emissive?.clone(),
emissiveIntensity,
opacity: material.opacity,
transparent: material.transparent
})
const glowColor = new THREE.Color('#e0df00')
if (color) {
color.lerp(glowColor, 0.34)
}
if (emissive) {
emissive.copy(glowColor)
setMaterialEmissiveIntensity(material, Math.max(emissiveIntensity || 0, 0.76))
}
material.opacity = Math.max(material.opacity, 0.96)
material.transparent = true
material.needsUpdate = true
}
const createFocusHallGlowMesh = (poi: RenderPoi) => {
if (!poi.positionGltf) return null
const canvas = document.createElement('canvas')
canvas.width = 256
canvas.height = 256
const context = canvas.getContext('2d')
if (context) {
const gradient = context.createRadialGradient(128, 128, 16, 128, 128, 118)
gradient.addColorStop(0, 'rgba(224, 223, 0, 0.52)')
gradient.addColorStop(0.45, 'rgba(224, 223, 0, 0.34)')
gradient.addColorStop(0.78, 'rgba(224, 223, 0, 0.16)')
gradient.addColorStop(1, 'rgba(224, 223, 0, 0)')
context.clearRect(0, 0, canvas.width, canvas.height)
context.fillStyle = gradient
context.fillRect(0, 0, canvas.width, canvas.height)
}
const texture = new THREE.CanvasTexture(canvas)
texture.colorSpace = THREE.SRGBColorSpace
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
opacity: 0.78,
depthTest: false,
depthWrite: false,
side: THREE.DoubleSide
})
const hallGlowSize = Math.max(getPoiMarkerSize() * 14, getActiveModelSpan() * 0.14)
const geometry = new THREE.PlaneGeometry(hallGlowSize, hallGlowSize * 0.72)
const mesh = new THREE.Mesh(geometry, material)
const [x, y, z] = poi.positionGltf
mesh.name = 'GuideFocusHallGlow'
mesh.position.set(x, y + 0.08, z)
mesh.rotation.x = -Math.PI / 2
mesh.renderOrder = 7
return mesh
}
const showFocusHallHighlight = (poi: RenderPoi) => {
clearFocusHallHighlight()
if (!activeModel) return
activeFocusHallGlowMesh = createFocusHallGlowMesh(poi)
if (activeFocusHallGlowMesh) {
poiGroup?.add(activeFocusHallGlowMesh)
}
const modelRoot = findPoiModelRoot(poi)
if (!modelRoot) return
const seenMaterials = new Set<THREE.Material>()
let matchedMeshCount = 0
modelRoot.traverse((child) => {
if (!(child instanceof THREE.Mesh) || !child.visible) return
matchedMeshCount += 1
const materials = Array.isArray(child.material) ? child.material : [child.material]
materials.forEach((material) => {
if (material) {
applyFocusHallMaterial(material, seenMaterials)
}
})
})
if (!matchedMeshCount) {
clearFocusHallHighlight()
}
}
const disposeCachedOverviewModel = () => {
if (!cachedOverviewModel) return
@@ -840,6 +1092,7 @@ const renderRoutePreview = () => {
const clearSceneData = () => {
if (activeModel && scene) {
clearFocusHallHighlight()
scene.remove(activeModel)
if (activeModel === cachedOverviewModel) {
cachedOverviewModel.visible = false
@@ -1390,8 +1643,10 @@ const createPoiLabelSprite = (poi: RenderPoi, markerSize: number) => {
depthWrite: false
}))
sprite.userData.isPoiLabel = true
sprite.userData.labelBaseScaleX = markerSize * 5.6
sprite.userData.labelBaseScaleY = markerSize * 1.68
sprite.renderOrder = 30
sprite.scale.set(markerSize * 5.6, markerSize * 1.68, 1)
sprite.scale.set(sprite.userData.labelBaseScaleX, sprite.userData.labelBaseScaleY, 1)
return sprite
}
@@ -1501,6 +1756,7 @@ const showFocusPoiLabel = (poi: RenderPoi) => {
const [x, y, z] = poi.positionGltf
activeFocusLabelSprite = createPoiLabelSprite(poi, markerSize)
activeFocusLabelSprite.position.set(x, y + markerSize * 2.35, z)
updateFocusLabelScale()
poiGroup.add(activeFocusLabelSprite)
}
@@ -1529,6 +1785,7 @@ const showFocusPoiPulse = (poi: RenderPoi) => {
}
const showFocusPoiAffordances = (poi: RenderPoi) => {
showFocusHallHighlight(poi)
showFocusPoiBase(poi)
showFocusPoiPulse(poi)
showFocusPoiLabel(poi)
@@ -1552,6 +1809,28 @@ const getPoiMarkerSize = () => (
: 3
)
const getFocusLabelScaleBoost = () => {
if (!controls || !activeModel) return 1
const distanceRatio = controls.getDistance() / getActiveModelSpan()
return Math.min(2.25, Math.max(1, distanceRatio / 0.36))
}
const updateFocusLabelScale = () => {
if (!activeFocusLabelSprite) return
const userData = getPoiSpriteUserData(activeFocusLabelSprite)
const baseScaleX = typeof userData.labelBaseScaleX === 'number'
? userData.labelBaseScaleX
: activeFocusLabelSprite.scale.x
const baseScaleY = typeof userData.labelBaseScaleY === 'number'
? userData.labelBaseScaleY
: activeFocusLabelSprite.scale.y
const scaleBoost = getFocusLabelScaleBoost()
activeFocusLabelSprite.scale.set(baseScaleX * scaleBoost, baseScaleY * scaleBoost, 1)
}
const setPoiSpriteFocusStyle = (sprite: THREE.Sprite, focused: boolean) => {
const userData = getPoiSpriteUserData(sprite)
const baseScale = typeof userData.baseScale === 'number'
@@ -1603,7 +1882,8 @@ const createPoiFromFocusRequest = (request: TargetPoiFocusRequest): RenderPoi |
primaryCategory: 'target_preview',
primaryCategoryZh: request.primaryCategoryZh || '位置预览',
iconType: 'target_preview',
positionGltf: request.positionGltf
positionGltf: request.positionGltf,
sourceObjectName: request.sourceObjectName
}
}
@@ -1654,6 +1934,7 @@ const loadFloorPOIs = async (floor: FloorIndexItem, loadToken?: number) => {
const clearMapSelection = (shouldEmit = true) => {
activeFocusPoiId.value = ''
selectedPOI.value = null
clearFocusHallHighlight()
disposeFocusLabel()
disposeFocusPulse()
disposeFocusBase()
@@ -1779,7 +2060,12 @@ const focusCameraOnPoi = (poi: RenderPoi) => {
? new THREE.Box3().setFromObject(activeModel).getSize(new THREE.Vector3())
: new THREE.Vector3(80, 80, 80)
const maxDim = Math.max(modelSize.x, modelSize.y, modelSize.z, 1)
const distance = Math.min(Math.max(maxDim * 0.22, 28), Math.max(maxDim * 0.55, 80))
const focusDistanceFactor = Math.max(props.targetFocusDistanceFactor, 0.1)
const focusMaxDistanceFactor = Math.max(0.55, focusDistanceFactor * 1.4)
const distance = Math.min(
Math.max(maxDim * focusDistanceFactor, 28),
Math.max(maxDim * focusMaxDistanceFactor, 80)
)
const direction = new THREE.Vector3(0.72, 0.58, 1).normalize()
camera.position.copy(target).add(direction.multiplyScalar(distance))
@@ -2036,6 +2322,10 @@ watch(() => props.modelSource, () => {
init3DScene()
})
watch(() => [props.showControls, props.touchGestureMode], () => {
syncControlInteractionOptions()
})
watch(() => props.targetFocus, (request) => {
queueTargetFocus(request || null)
}, {

View File

@@ -25,6 +25,8 @@
:show-controls="false"
:show-poi="shouldShowIndoorPois"
:target-focus="targetFocusRequest"
:touch-gesture-mode="touchGestureMode"
:target-focus-distance-factor="targetFocusDistanceFactor"
:route-preview="routePreview"
:show-route="showRoute"
@floor-change="handleThreeFloorChange"
@@ -216,6 +218,7 @@ interface TargetPoiFocusRequest {
floorLabel?: string
primaryCategoryZh?: string
positionGltf?: [number, number, number]
sourceObjectName?: string
}
interface TargetPoiFocusResult {
@@ -228,6 +231,7 @@ interface TargetPoiFocusResult {
type IndoorViewMode = 'overview' | 'floor' | 'multi'
type LayerDisplayMode = 'single' | 'multi'
type TouchGestureMode = 'orbit' | 'pan'
const props = withDefaults(defineProps<{
searchText?: string
@@ -267,6 +271,8 @@ const props = withDefaults(defineProps<{
normalizeFloorId?: (labelOrId: string) => string
dimmed?: boolean
targetFocusRequest?: TargetPoiFocusRequest | null
touchGestureMode?: TouchGestureMode
targetFocusDistanceFactor?: number
routePreview?: GuideRouteResult | null
showRoute?: boolean
}>(), {
@@ -307,6 +313,8 @@ const props = withDefaults(defineProps<{
normalizeFloorId: (labelOrId: string) => labelOrId,
dimmed: false,
targetFocusRequest: null,
touchGestureMode: 'orbit',
targetFocusDistanceFactor: 0.22,
routePreview: null,
showRoute: false
})

View File

@@ -1,6 +1,10 @@
<template>
<view class="guide-page-frame" :class="`variant-${variant}`">
<view
class="guide-page-frame"
:class="[`variant-${variant}`, { 'no-top-tabs': !showTopTabs }]"
>
<GuideTopTabs
v-if="showTopTabs"
:active-tab="activeTab"
:variant="topTabsVariant"
:top="topTabsTop"
@@ -40,6 +44,7 @@ withDefaults(defineProps<{
variant?: 'overlay' | 'static'
topTabsVariant?: 'underline' | 'segmented'
topTabsTop?: string
showTopTabs?: boolean
showBack?: boolean
showCancel?: boolean
backLabel?: string
@@ -49,6 +54,7 @@ withDefaults(defineProps<{
variant: 'overlay',
topTabsVariant: 'underline',
topTabsTop: '0',
showTopTabs: true,
showBack: false,
showCancel: false,
backLabel: '返回',
@@ -102,6 +108,10 @@ const handleCancelTap = () => {
background: var(--museum-bg-light);
}
.variant-static.no-top-tabs .guide-page-frame-body {
top: 0;
}
.guide-page-frame-actions {
position: absolute;
top: 0;