1744 lines
39 KiB
Vue
1744 lines
39 KiB
Vue
<template>
|
||
<view
|
||
class="poi-search-panel"
|
||
:class="[`variant-${variant}`, { expanded: showSearchContent, 'is-collapsed': isHomeCollapsed }]"
|
||
@touchstart="handlePanelTouchStart"
|
||
@touchend="handlePanelTouchEnd"
|
||
@touchcancel="resetPanelTouch"
|
||
@mousedown="handlePanelMouseStart"
|
||
@mousemove="handlePanelMouseMove"
|
||
@mouseup="handlePanelMouseEnd"
|
||
@mouseleave="resetPanelMouse"
|
||
>
|
||
<view v-if="variant === 'home' && showSearchContent" class="home-fullscreen-nav">
|
||
<view class="home-back-button" @tap.stop="collapseHomePanel">
|
||
<text class="home-back-icon">‹</text>
|
||
</view>
|
||
<text class="home-fullscreen-title">点位搜索</text>
|
||
<view class="home-nav-spacer"></view>
|
||
</view>
|
||
|
||
<view class="search-box" @tap="handleSearchBoxTap" @click="handleSearchBoxTap">
|
||
<view class="search-icon"></view>
|
||
<input
|
||
class="search-input"
|
||
type="text"
|
||
confirm-type="search"
|
||
:focus="searchInputFocused"
|
||
placeholder="请输入地点进行搜索"
|
||
placeholder-class="search-placeholder"
|
||
:value="searchDraftKeyword"
|
||
@mousedown="handleSearchBoxTap"
|
||
@click="handleSearchBoxTap"
|
||
@tap="handleSearchBoxTap"
|
||
@focus="handleSearchFocus"
|
||
@input="handleSearchInput"
|
||
@confirm="handleSearchConfirm"
|
||
/>
|
||
<view v-if="searchDraftKeyword" class="clear-button" @tap.stop="handleSearchClear">
|
||
<text class="clear-text">×</text>
|
||
</view>
|
||
<view v-else class="voice-icon"></view>
|
||
</view>
|
||
|
||
<view v-if="variant === 'home' && !showSearchContent" class="home-category-strip">
|
||
<view
|
||
v-for="item in primaryFacilities"
|
||
:key="item.id"
|
||
class="home-category-chip"
|
||
@tap="handleFacilityShortcut(item)"
|
||
>
|
||
<view class="home-category-icon line-icon" :class="`icon-${item.icon}`">
|
||
<view class="icon-part a"></view>
|
||
<view class="icon-part b"></view>
|
||
<view class="icon-part c"></view>
|
||
</view>
|
||
<text class="home-category-label">{{ item.label }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="showSearchContent" class="poi-search-content">
|
||
<view v-if="variant === 'home'" class="home-collapse-handle" @tap.stop="collapseHomePanel">
|
||
<view class="home-collapse-bar"></view>
|
||
</view>
|
||
|
||
<view class="category-scroll">
|
||
<view class="category-grid">
|
||
<view
|
||
v-for="item in primaryFacilityColumns"
|
||
:key="item.id"
|
||
class="category-item"
|
||
:class="{ active: searchKeyword === item.label }"
|
||
@tap="handleFacilityShortcut(item)"
|
||
>
|
||
<view class="line-icon" :class="`icon-${item.icon}`">
|
||
<view class="icon-part a"></view>
|
||
<view class="icon-part b"></view>
|
||
<view class="icon-part c"></view>
|
||
</view>
|
||
<text class="category-label">{{ item.label }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section-title-row">
|
||
<text class="section-title">服务设施</text>
|
||
</view>
|
||
|
||
<view class="facility-chip-row">
|
||
<view
|
||
v-for="item in serviceFacilities"
|
||
:key="item.id"
|
||
class="facility-chip"
|
||
:class="{ active: searchKeyword === item.label }"
|
||
@tap="handleFacilityShortcut(item)"
|
||
>
|
||
<view class="chip-icon" :class="`chip-${item.icon}`"></view>
|
||
<text class="facility-chip-text">{{ item.label }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="result-card">
|
||
<view class="result-card-header">
|
||
<text class="museum-title">深圳自然博物馆</text>
|
||
<text class="result-count">{{ floorResults.length }} 个点位</text>
|
||
</view>
|
||
|
||
<view class="result-body">
|
||
<view class="floor-tabs">
|
||
<view
|
||
v-for="floor in displayFloors"
|
||
:key="floor.label"
|
||
class="floor-tab"
|
||
:class="{ active: activeFloor === floor.label }"
|
||
@tap="handleFloorChange(floor.label)"
|
||
>
|
||
<text class="floor-tab-text">{{ floor.label }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="result-list">
|
||
<view
|
||
v-for="item in floorResults"
|
||
:key="item.id"
|
||
class="result-row"
|
||
@tap="handleResultTap(item)"
|
||
>
|
||
<view class="pin-icon"></view>
|
||
<view class="result-copy">
|
||
<text class="result-name">{{ item.name }}</text>
|
||
<text class="result-meta">{{ item.floorLabel }} · {{ item.primaryCategory.label }}</text>
|
||
</view>
|
||
<text class="result-arrow">›</text>
|
||
</view>
|
||
|
||
<view v-if="!floorResults.length" class="empty-state">
|
||
<text class="empty-title">{{ emptyTitle }}</text>
|
||
<text class="empty-desc">{{ emptyDesc }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||
import type {
|
||
MuseumFloor,
|
||
MuseumPoi
|
||
} from '@/domain/museum'
|
||
import {
|
||
compareFloorsTopToBottom,
|
||
isIndoorNavigableFloor,
|
||
isPoiOnIndoorNavigableFloor
|
||
} from '@/domain/guideFloor'
|
||
import {
|
||
guideUseCase
|
||
} from '@/usecases/guideUseCase'
|
||
|
||
export interface PoiSearchShortcut {
|
||
label: string
|
||
keywords: string[]
|
||
}
|
||
|
||
interface ShortcutItem extends PoiSearchShortcut {
|
||
id: string
|
||
icon: string
|
||
}
|
||
|
||
const props = withDefaults(defineProps<{
|
||
initialKeyword?: string
|
||
variant?: 'home' | 'page'
|
||
autofocus?: boolean
|
||
}>(), {
|
||
initialKeyword: '',
|
||
variant: 'page',
|
||
autofocus: false
|
||
})
|
||
|
||
const emit = defineEmits<{
|
||
resultTap: [poi: MuseumPoi]
|
||
'expanded-change': [expanded: boolean]
|
||
}>()
|
||
|
||
const primaryFacilities: ShortcutItem[] = [
|
||
{ id: 'exhibition', label: '展览', icon: 'exhibition', keywords: ['展览', '展厅', '展馆', 'exhibition_hall', 'temp_exhibition'] },
|
||
{ id: 'cinema', label: '影院', icon: 'cinema', keywords: ['影院', '剧场', '影厅', 'cinema', 'theater'] },
|
||
{ id: 'restroom', label: '洗手间', icon: 'restroom', keywords: ['洗手间', '卫生间', 'toilet', 'accessible_toilet'] },
|
||
{ id: 'restaurant', label: '餐厅', icon: 'restaurant', keywords: ['餐厅', '餐饮', '咖啡', 'restaurant', 'commercial'] },
|
||
{ id: 'ticket-office', label: '售票处', icon: 'ticket', keywords: ['售票处', '售票', '票务', 'ticket_office'] },
|
||
{ id: 'nursing', label: '母婴室', icon: 'nursing', keywords: ['母婴室', 'mother_baby_room'] },
|
||
{ id: 'service-center', label: '服务中心', icon: 'service', keywords: ['服务中心', '服务台', '咨询台', 'service_desk', 'service_space'] },
|
||
{ id: 'elevator', label: '电梯', icon: 'elevator', keywords: ['电梯', 'elevator'] },
|
||
{ id: 'escalator', label: '扶梯', icon: 'escalator', keywords: ['扶梯', 'escalator'] },
|
||
{ id: 'stairs', label: '楼梯', icon: 'stairs', keywords: ['楼梯', 'stairs', 'stair'] }
|
||
]
|
||
|
||
const primaryFacilityVisibleColumnCount = 4
|
||
const primaryFacilityRowCount = 2
|
||
|
||
const primaryFacilityColumns = computed(() => {
|
||
const firstScreenItems = primaryFacilities.slice(0, primaryFacilityVisibleColumnCount * primaryFacilityRowCount)
|
||
const overflowItems = primaryFacilities.slice(primaryFacilityVisibleColumnCount * primaryFacilityRowCount)
|
||
const orderedItems: ShortcutItem[] = []
|
||
|
||
for (let columnIndex = 0; columnIndex < primaryFacilityVisibleColumnCount; columnIndex += 1) {
|
||
const topItem = firstScreenItems[columnIndex]
|
||
const bottomItem = firstScreenItems[columnIndex + primaryFacilityVisibleColumnCount]
|
||
if (topItem) orderedItems.push(topItem)
|
||
if (bottomItem) orderedItems.push(bottomItem)
|
||
}
|
||
|
||
return orderedItems.concat(overflowItems)
|
||
})
|
||
|
||
const serviceFacilities: ShortcutItem[] = [
|
||
{ id: 'drink', label: '饮料售卖机', icon: 'drink', keywords: ['饮料售卖机', '饮料', '售卖机', 'vending'] },
|
||
{ id: 'ticket', label: '票务售卖机', icon: 'ticket', keywords: ['票务售卖机', '票务', '售票', 'ticket_office'] },
|
||
{ id: 'souvenir', label: '纪念品售卖机', icon: 'bag', keywords: ['纪念品售卖机', '纪念品', '商店', 'souvenir'] }
|
||
]
|
||
|
||
const searchKeyword = ref('')
|
||
const searchDraftKeyword = ref('')
|
||
const searchInputFocused = ref(false)
|
||
const homeExpanded = ref(false)
|
||
const touchStartX = ref(0)
|
||
const touchStartY = ref(0)
|
||
const isPanelTouching = ref(false)
|
||
const isPanelMouseDown = ref(false)
|
||
const homeContentTapBlockedUntil = ref(0)
|
||
const floors = ref<MuseumFloor[]>([])
|
||
const activeFloor = ref('1F')
|
||
const pois = ref<MuseumPoi[]>([])
|
||
const isLoading = ref(false)
|
||
const initialSpaceLoadError = ref('')
|
||
let searchRequestSeq = 0
|
||
|
||
const showSearchContent = computed(() => props.variant === 'page' || homeExpanded.value)
|
||
const isHomeCollapsed = computed(() => props.variant === 'home' && !showSearchContent.value)
|
||
const collapseDragThreshold = 48
|
||
const homeExpandTapGuardMs = 360
|
||
|
||
const displayFloors = computed(() => [...floors.value]
|
||
.filter(isIndoorNavigableFloor)
|
||
.sort(compareFloorsTopToBottom))
|
||
|
||
const floorResults = computed(() => {
|
||
const matched = pois.value.filter((poi) => poi.floorLabel === activeFloor.value)
|
||
return matched.slice(0, props.variant === 'home' ? 8 : 12)
|
||
})
|
||
|
||
const emptyTitle = computed(() => (
|
||
isLoading.value ? '正在读取点位' : initialSpaceLoadError.value || '暂无匹配点位'
|
||
))
|
||
|
||
const emptyDesc = computed(() => (
|
||
initialSpaceLoadError.value
|
||
? '请检查 SGS 空间点位数据接口后重试'
|
||
: '可切换楼层或搜索服务设施'
|
||
))
|
||
|
||
const isVisiblePoi = (poi: MuseumPoi) => poi.primaryCategory.id !== 'touring_poi'
|
||
&& isPoiOnIndoorNavigableFloor(poi)
|
||
|
||
const dedupePois = (items: MuseumPoi[]) => {
|
||
const seenIds = new Set<string>()
|
||
return items.filter((poi) => {
|
||
if (!poi.id || seenIds.has(poi.id)) return false
|
||
seenIds.add(poi.id)
|
||
return true
|
||
})
|
||
}
|
||
|
||
const isPoiMatchingKeywords = (poi: MuseumPoi, keywords: string[]) => {
|
||
const searchableText = [
|
||
poi.name,
|
||
poi.floorId,
|
||
poi.floorLabel,
|
||
poi.primaryCategory.id,
|
||
poi.primaryCategory.label,
|
||
poi.primaryCategory.iconType,
|
||
...poi.categories.flatMap((category) => [
|
||
category.id,
|
||
category.label,
|
||
category.iconType
|
||
])
|
||
]
|
||
.filter(Boolean)
|
||
.join(' ')
|
||
.toLowerCase()
|
||
|
||
return keywords.some((keyword) => searchableText.includes(keyword.trim().toLowerCase()))
|
||
}
|
||
|
||
const findShortcutByKeyword = (keyword: string) => {
|
||
const normalized = keyword.trim().toLowerCase()
|
||
if (!normalized) return null
|
||
|
||
return [...primaryFacilities, ...serviceFacilities].find((shortcut) => (
|
||
shortcut.label.toLowerCase() === normalized
|
||
|| shortcut.keywords.some((item) => item.trim().toLowerCase() === normalized)
|
||
)) || null
|
||
}
|
||
|
||
const syncActiveFloor = () => {
|
||
if (!displayFloors.value.length) {
|
||
activeFloor.value = ''
|
||
return
|
||
}
|
||
|
||
if (displayFloors.value.some((floor) => floor.label === activeFloor.value)) return
|
||
|
||
activeFloor.value = displayFloors.value.find((floor) => floor.label === '1F')?.label
|
||
|| displayFloors.value[0].label
|
||
}
|
||
|
||
const syncActiveFloorToResults = (items: MuseumPoi[]) => {
|
||
if (!displayFloors.value.length) {
|
||
activeFloor.value = ''
|
||
return
|
||
}
|
||
|
||
const resultFloorLabels = new Set(items.map((poi) => poi.floorLabel).filter(Boolean))
|
||
if (activeFloor.value && resultFloorLabels.has(activeFloor.value)) return
|
||
|
||
const firstFloorWithResult = displayFloors.value.find((floor) => resultFloorLabels.has(floor.label))
|
||
if (firstFloorWithResult) {
|
||
activeFloor.value = firstFloorWithResult.label
|
||
return
|
||
}
|
||
|
||
syncActiveFloor()
|
||
}
|
||
|
||
const loadFloors = async () => {
|
||
try {
|
||
floors.value = await guideUseCase.getFloors()
|
||
syncActiveFloor()
|
||
} catch (error) {
|
||
console.error('加载点位搜索楼层失败:', error)
|
||
floors.value = []
|
||
activeFloor.value = ''
|
||
}
|
||
}
|
||
|
||
const loadPois = async (keyword = '') => {
|
||
const requestSeq = ++searchRequestSeq
|
||
isLoading.value = true
|
||
initialSpaceLoadError.value = ''
|
||
pois.value = []
|
||
try {
|
||
const result = await guideUseCase.searchPois(keyword)
|
||
if (requestSeq !== searchRequestSeq) return
|
||
const visiblePois = result.filter(isVisiblePoi)
|
||
pois.value = visiblePois
|
||
syncActiveFloorToResults(visiblePois)
|
||
} catch (error) {
|
||
if (requestSeq !== searchRequestSeq) return
|
||
console.error('加载点位搜索结果失败:', error)
|
||
pois.value = []
|
||
uni.showToast({
|
||
title: '点位数据读取失败',
|
||
icon: 'none'
|
||
})
|
||
} finally {
|
||
if (requestSeq === searchRequestSeq) {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
}
|
||
|
||
const loadInitialSpacePoints = async () => {
|
||
const requestSeq = ++searchRequestSeq
|
||
isLoading.value = true
|
||
initialSpaceLoadError.value = ''
|
||
pois.value = []
|
||
|
||
try {
|
||
const result = await guideUseCase.getInitialSearchSpacePoints()
|
||
if (requestSeq !== searchRequestSeq) return
|
||
const visiblePois = result.filter(isVisiblePoi)
|
||
pois.value = visiblePois
|
||
syncActiveFloorToResults(visiblePois)
|
||
} catch (error) {
|
||
if (requestSeq !== searchRequestSeq) return
|
||
console.error('加载 SDK 空间点位失败:', error)
|
||
pois.value = []
|
||
initialSpaceLoadError.value = '空间点位读取失败'
|
||
uni.showToast({
|
||
title: '空间点位读取失败',
|
||
icon: 'none'
|
||
})
|
||
} finally {
|
||
if (requestSeq === searchRequestSeq) {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
}
|
||
|
||
const focusSearchInput = async () => {
|
||
searchInputFocused.value = false
|
||
await nextTick()
|
||
searchInputFocused.value = true
|
||
}
|
||
|
||
const expandHomePanel = () => {
|
||
if (props.variant !== 'home') return
|
||
|
||
if (!homeExpanded.value) {
|
||
homeContentTapBlockedUntil.value = Date.now() + homeExpandTapGuardMs
|
||
}
|
||
homeExpanded.value = true
|
||
}
|
||
|
||
const shouldBlockHomeContentTap = () => (
|
||
props.variant === 'home'
|
||
&& Date.now() < homeContentTapBlockedUntil.value
|
||
)
|
||
|
||
// #ifdef H5
|
||
const homeSearchLockClass = 'guide-home-search-fullscreen-lock'
|
||
let removeViewportResizeListener: (() => void) | null = null
|
||
|
||
const updateHomeSearchViewportHeight = () => {
|
||
if (typeof document === 'undefined') return
|
||
|
||
const viewportHeight = window.visualViewport?.height || window.innerHeight
|
||
document.documentElement.style.setProperty('--home-search-viewport-height', `${viewportHeight}px`)
|
||
}
|
||
|
||
const setH5HomeSearchLock = (locked: boolean) => {
|
||
if (typeof document === 'undefined') return
|
||
|
||
document.documentElement.classList.toggle(homeSearchLockClass, locked)
|
||
document.body?.classList.toggle(homeSearchLockClass, locked)
|
||
|
||
if (!locked) {
|
||
document.documentElement.style.removeProperty('--home-search-viewport-height')
|
||
removeViewportResizeListener?.()
|
||
removeViewportResizeListener = null
|
||
return
|
||
}
|
||
|
||
updateHomeSearchViewportHeight()
|
||
|
||
if (!removeViewportResizeListener) {
|
||
const target = window.visualViewport || window
|
||
target.addEventListener('resize', updateHomeSearchViewportHeight)
|
||
removeViewportResizeListener = () => {
|
||
target.removeEventListener('resize', updateHomeSearchViewportHeight)
|
||
}
|
||
}
|
||
}
|
||
// #endif
|
||
|
||
const handleSearchFocus = () => {
|
||
if (props.variant === 'home') {
|
||
expandHomePanel()
|
||
}
|
||
}
|
||
|
||
const handleSearchBoxTap = () => {
|
||
if (props.variant !== 'home' || homeExpanded.value) return
|
||
|
||
expandHomePanel()
|
||
void focusSearchInput()
|
||
}
|
||
|
||
const collapseHomePanel = () => {
|
||
if (props.variant !== 'home') return
|
||
|
||
homeExpanded.value = false
|
||
searchInputFocused.value = false
|
||
uni.hideKeyboard()
|
||
}
|
||
|
||
const resetPanelTouch = () => {
|
||
isPanelTouching.value = false
|
||
touchStartX.value = 0
|
||
touchStartY.value = 0
|
||
}
|
||
|
||
const resetPanelMouse = () => {
|
||
isPanelMouseDown.value = false
|
||
touchStartX.value = 0
|
||
touchStartY.value = 0
|
||
}
|
||
|
||
const getGesturePoint = (event: any) => {
|
||
const touchPoint = event.changedTouches?.[0] || event.touches?.[0]
|
||
if (touchPoint) return touchPoint
|
||
|
||
if (typeof event.clientX === 'number' && typeof event.clientY === 'number') {
|
||
return event
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
const handlePanelTouchStart = (event: any) => {
|
||
if (props.variant !== 'home' || !homeExpanded.value) return
|
||
|
||
const point = getGesturePoint(event)
|
||
if (!point) return
|
||
|
||
touchStartX.value = point.clientX
|
||
touchStartY.value = point.clientY
|
||
isPanelTouching.value = true
|
||
}
|
||
|
||
const handlePanelTouchEnd = (event: any) => {
|
||
if (!isPanelTouching.value || props.variant !== 'home' || !homeExpanded.value) {
|
||
resetPanelTouch()
|
||
return
|
||
}
|
||
|
||
const point = getGesturePoint(event)
|
||
if (!point) {
|
||
resetPanelTouch()
|
||
return
|
||
}
|
||
|
||
const deltaX = point.clientX - touchStartX.value
|
||
const deltaY = point.clientY - touchStartY.value
|
||
const isIntentionalDownSwipe = deltaY > collapseDragThreshold
|
||
&& deltaY > Math.abs(deltaX) * 1.2
|
||
|
||
if (isIntentionalDownSwipe) {
|
||
collapseHomePanel()
|
||
}
|
||
|
||
resetPanelTouch()
|
||
}
|
||
|
||
const handlePanelMouseStart = (event: any) => {
|
||
if (props.variant !== 'home' || !homeExpanded.value) return
|
||
|
||
const point = getGesturePoint(event)
|
||
if (!point) return
|
||
|
||
touchStartX.value = point.clientX
|
||
touchStartY.value = point.clientY
|
||
isPanelMouseDown.value = true
|
||
}
|
||
|
||
const handlePanelMouseMove = (event: any) => {
|
||
if (!isPanelMouseDown.value || props.variant !== 'home' || !homeExpanded.value) return
|
||
const point = getGesturePoint(event)
|
||
if (!point) return
|
||
|
||
const deltaX = point.clientX - touchStartX.value
|
||
const deltaY = point.clientY - touchStartY.value
|
||
const isIntentionalDownDrag = deltaY > collapseDragThreshold
|
||
&& deltaY > Math.abs(deltaX) * 1.2
|
||
|
||
if (isIntentionalDownDrag) {
|
||
collapseHomePanel()
|
||
resetPanelMouse()
|
||
}
|
||
}
|
||
|
||
const handlePanelMouseEnd = (event: any) => {
|
||
if (!isPanelMouseDown.value || props.variant !== 'home' || !homeExpanded.value) {
|
||
resetPanelMouse()
|
||
return
|
||
}
|
||
|
||
const point = getGesturePoint(event)
|
||
if (!point) {
|
||
resetPanelMouse()
|
||
return
|
||
}
|
||
|
||
const deltaX = point.clientX - touchStartX.value
|
||
const deltaY = point.clientY - touchStartY.value
|
||
const isIntentionalDownDrag = deltaY > collapseDragThreshold
|
||
&& deltaY > Math.abs(deltaX) * 1.2
|
||
|
||
if (isIntentionalDownDrag) {
|
||
collapseHomePanel()
|
||
}
|
||
|
||
resetPanelMouse()
|
||
}
|
||
|
||
const handleSearchInput = (event: any) => {
|
||
searchDraftKeyword.value = event.detail.value
|
||
if (props.variant === 'home') {
|
||
expandHomePanel()
|
||
}
|
||
}
|
||
|
||
const handleSearchClear = async () => {
|
||
searchDraftKeyword.value = ''
|
||
searchKeyword.value = ''
|
||
await loadInitialSpacePoints()
|
||
await focusSearchInput()
|
||
}
|
||
|
||
const handleSearchConfirm = async (event?: any) => {
|
||
const value = typeof event?.detail?.value === 'string'
|
||
? event.detail.value
|
||
: searchDraftKeyword.value
|
||
|
||
searchKeyword.value = value.trim()
|
||
searchDraftKeyword.value = searchKeyword.value
|
||
expandHomePanel()
|
||
|
||
const matchedShortcut = findShortcutByKeyword(searchKeyword.value)
|
||
if (matchedShortcut) {
|
||
await searchShortcut(matchedShortcut)
|
||
return
|
||
}
|
||
|
||
await loadPois(searchKeyword.value)
|
||
}
|
||
|
||
const searchShortcut = async (shortcut: PoiSearchShortcut) => {
|
||
const requestSeq = ++searchRequestSeq
|
||
searchKeyword.value = shortcut.label
|
||
searchDraftKeyword.value = shortcut.label
|
||
expandHomePanel()
|
||
isLoading.value = true
|
||
initialSpaceLoadError.value = ''
|
||
pois.value = []
|
||
|
||
try {
|
||
const groups = await Promise.all(shortcut.keywords.map((keyword) => guideUseCase.searchPois(keyword)))
|
||
if (requestSeq !== searchRequestSeq) return
|
||
const matchedPois = dedupePois(groups.flat())
|
||
.filter(isVisiblePoi)
|
||
.filter((poi) => isPoiMatchingKeywords(poi, shortcut.keywords))
|
||
pois.value = matchedPois
|
||
syncActiveFloorToResults(matchedPois)
|
||
} catch (error) {
|
||
if (requestSeq !== searchRequestSeq) return
|
||
console.error('加载点位分类搜索结果失败:', error)
|
||
pois.value = []
|
||
uni.showToast({
|
||
title: '点位数据读取失败',
|
||
icon: 'none'
|
||
})
|
||
} finally {
|
||
if (requestSeq === searchRequestSeq) {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleFacilityShortcut = (shortcut: ShortcutItem) => {
|
||
void searchShortcut(shortcut)
|
||
}
|
||
|
||
const handleFloorChange = (floor: string) => {
|
||
activeFloor.value = floor
|
||
}
|
||
|
||
const encodeQueryValue = (value: string) => encodeURIComponent(value)
|
||
|
||
const createPoiLocationQuery = (poi: MuseumPoi) => (
|
||
`id=${encodeQueryValue(poi.id)}`
|
||
+ `&target=${encodeQueryValue(poi.name)}`
|
||
+ `&floorId=${encodeQueryValue(poi.floorId)}`
|
||
+ `&floorLabel=${encodeQueryValue(poi.floorLabel)}`
|
||
)
|
||
|
||
const handleResultTap = (poi: MuseumPoi) => {
|
||
if (shouldBlockHomeContentTap()) return
|
||
|
||
emit('resultTap', poi)
|
||
|
||
if (poi.primaryCategory.id === 'operation_experience') {
|
||
uni.navigateTo({
|
||
url: `/pages/route/detail?facilityId=${encodeQueryValue(poi.id)}&target=${encodeQueryValue(poi.name)}&floorId=${encodeQueryValue(poi.floorId)}&state=preview`
|
||
})
|
||
return
|
||
}
|
||
|
||
uni.navigateTo({
|
||
url: `/pages/facility/detail?${createPoiLocationQuery(poi)}`
|
||
})
|
||
}
|
||
|
||
const applyInitialKeyword = async (keyword: string) => {
|
||
const normalizedKeyword = keyword.trim()
|
||
searchKeyword.value = normalizedKeyword
|
||
searchDraftKeyword.value = normalizedKeyword
|
||
if (props.variant === 'page') {
|
||
homeExpanded.value = true
|
||
} else if (normalizedKeyword) {
|
||
expandHomePanel()
|
||
} else {
|
||
homeExpanded.value = false
|
||
}
|
||
|
||
if (normalizedKeyword) {
|
||
await loadPois(normalizedKeyword)
|
||
return
|
||
}
|
||
|
||
await loadInitialSpacePoints()
|
||
}
|
||
|
||
watch(() => props.initialKeyword, (keyword) => {
|
||
void applyInitialKeyword(keyword || '')
|
||
})
|
||
|
||
watch(showSearchContent, (expanded) => {
|
||
emit('expanded-change', expanded)
|
||
// #ifdef H5
|
||
setH5HomeSearchLock(props.variant === 'home' && expanded)
|
||
// #endif
|
||
}, { immediate: true })
|
||
|
||
onMounted(async () => {
|
||
await loadFloors()
|
||
await applyInitialKeyword(props.initialKeyword)
|
||
if (props.autofocus) {
|
||
void focusSearchInput()
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
// #ifdef H5
|
||
setH5HomeSearchLock(false)
|
||
// #endif
|
||
})
|
||
|
||
defineExpose({
|
||
expandHomePanel,
|
||
collapseHomePanel,
|
||
focusSearchInput,
|
||
searchShortcut
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
/* #ifdef H5 */
|
||
:global(html.guide-home-search-fullscreen-lock),
|
||
:global(body.guide-home-search-fullscreen-lock) {
|
||
height: 100%;
|
||
overflow: hidden;
|
||
overscroll-behavior: none;
|
||
}
|
||
/* #endif */
|
||
|
||
.poi-search-panel {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
box-sizing: border-box;
|
||
color: #262421;
|
||
font-family: '鸿蒙黑体', 'HarmonyOS Sans SC', 'HarmonyOS Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
.variant-home.expanded {
|
||
height: 100%;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
overscroll-behavior: contain;
|
||
}
|
||
|
||
.variant-page {
|
||
height: 100%;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.home-fullscreen-nav {
|
||
height: 38px;
|
||
flex: 0 0 38px;
|
||
display: grid;
|
||
grid-template-columns: 38px minmax(0, 1fr) 38px;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.home-back-button,
|
||
.home-nav-spacer {
|
||
width: 38px;
|
||
height: 38px;
|
||
}
|
||
|
||
.home-back-button {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 8px;
|
||
background: #ffffff;
|
||
border: 1px solid #e1e4da;
|
||
}
|
||
|
||
.home-back-icon {
|
||
display: flex;
|
||
width: 18px;
|
||
height: 18px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 0;
|
||
line-height: 0;
|
||
font-weight: 600;
|
||
color: #151713;
|
||
}
|
||
|
||
.home-back-icon::before {
|
||
content: '';
|
||
width: 10px;
|
||
height: 10px;
|
||
border-left: 2px solid currentColor;
|
||
border-bottom: 2px solid currentColor;
|
||
transform: translateX(2px) rotate(45deg);
|
||
}
|
||
|
||
.home-fullscreen-title {
|
||
min-width: 0;
|
||
font-size: 17px;
|
||
line-height: 22px;
|
||
font-weight: 800;
|
||
color: #151713;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.search-box {
|
||
height: 48px;
|
||
flex: 0 0 48px;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 14px;
|
||
box-sizing: border-box;
|
||
background: rgba(255, 255, 255, 0.96);
|
||
border: 1px solid #d8dbd0;
|
||
border-radius: 8px;
|
||
box-shadow: 0 8px 18px rgba(36, 49, 42, 0.08);
|
||
}
|
||
|
||
.variant-home .search-box {
|
||
background: #f5f5ed;
|
||
border-color: rgba(224, 225, 0, 0.72);
|
||
box-shadow: none;
|
||
}
|
||
|
||
.variant-home.is-collapsed .search-box {
|
||
height: 38px;
|
||
flex-basis: 38px;
|
||
padding: 0 12px;
|
||
border-radius: 7px;
|
||
}
|
||
|
||
.home-category-strip {
|
||
margin-top: 10px;
|
||
display: flex;
|
||
gap: 8px;
|
||
overflow-x: auto;
|
||
padding-bottom: 2px;
|
||
scrollbar-width: none;
|
||
overscroll-behavior-x: contain;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
.variant-home.is-collapsed .home-category-strip {
|
||
margin-top: 7px;
|
||
gap: 7px;
|
||
padding-bottom: 1px;
|
||
}
|
||
|
||
.home-category-strip::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
|
||
.home-category-chip {
|
||
min-width: 112px;
|
||
height: 58px;
|
||
flex: 0 0 112px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
padding: 0 11px;
|
||
box-sizing: border-box;
|
||
background: #e0df00;
|
||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||
border-radius: 8px;
|
||
background-image:
|
||
linear-gradient(116deg, rgba(255, 255, 255, 0.22) 0 32%, rgba(255, 255, 255, 0) 33%),
|
||
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(0, 0, 0, 0.02));
|
||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.16);
|
||
}
|
||
|
||
.variant-home.is-collapsed .home-category-chip {
|
||
min-width: 76px;
|
||
height: 42px;
|
||
flex-basis: 76px;
|
||
justify-content: center;
|
||
gap: 5px;
|
||
padding: 0 7px;
|
||
border-radius: 7px;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.home-category-chip:nth-child(2) {
|
||
background-color: #18c2b6;
|
||
}
|
||
|
||
.home-category-chip:nth-child(3) {
|
||
background-color: #b99cf0;
|
||
}
|
||
|
||
.home-category-chip:nth-child(4) {
|
||
background-color: #8ad9ae;
|
||
}
|
||
|
||
.home-category-chip:nth-child(5) {
|
||
background-color: #ffc0a1;
|
||
}
|
||
|
||
.home-category-chip:nth-child(6) {
|
||
background-color: #5ed0e4;
|
||
}
|
||
|
||
.home-category-chip:nth-child(7) {
|
||
background-color: #c9a463;
|
||
}
|
||
|
||
.home-category-chip:nth-child(8) {
|
||
background-color: #f4f5ef;
|
||
}
|
||
|
||
.home-category-chip:active {
|
||
transform: translateY(1px);
|
||
}
|
||
|
||
.home-category-icon {
|
||
width: 24px;
|
||
height: 23px;
|
||
flex: 0 0 24px;
|
||
order: 2;
|
||
color: #050704;
|
||
}
|
||
|
||
.home-category-icon.line-icon {
|
||
width: 24px;
|
||
height: 23px;
|
||
}
|
||
|
||
.home-category-icon .icon-part {
|
||
transform: scale(0.8);
|
||
transform-origin: center;
|
||
}
|
||
|
||
.variant-home.is-collapsed .home-category-icon,
|
||
.variant-home.is-collapsed .home-category-icon.line-icon {
|
||
width: 20px;
|
||
height: 19px;
|
||
flex-basis: 20px;
|
||
}
|
||
|
||
.variant-home.is-collapsed .home-category-icon .icon-part {
|
||
transform: scale(0.68);
|
||
}
|
||
|
||
.home-category-label {
|
||
min-width: 0;
|
||
max-width: 70px;
|
||
font-size: 14px;
|
||
line-height: 18px;
|
||
font-weight: 800;
|
||
color: #050704;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.variant-home.is-collapsed .home-category-label {
|
||
max-width: 42px;
|
||
font-size: 11px;
|
||
line-height: 14px;
|
||
}
|
||
|
||
.search-icon {
|
||
width: 19px;
|
||
height: 19px;
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
margin-right: 14px;
|
||
border: 2px solid #151713;
|
||
border-radius: 50%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.search-icon::after {
|
||
content: '';
|
||
position: absolute;
|
||
right: -7px;
|
||
bottom: -5px;
|
||
width: 10px;
|
||
height: 2px;
|
||
background: #151713;
|
||
border-radius: 2px;
|
||
transform: rotate(45deg);
|
||
}
|
||
|
||
.variant-home.is-collapsed .search-icon {
|
||
width: 17px;
|
||
height: 17px;
|
||
margin-right: 11px;
|
||
border-width: 2px;
|
||
}
|
||
|
||
.variant-home.is-collapsed .search-icon::after {
|
||
right: -6px;
|
||
bottom: -4px;
|
||
width: 9px;
|
||
}
|
||
|
||
.search-input {
|
||
min-width: 0;
|
||
flex: 1;
|
||
height: 100%;
|
||
font-size: 16px;
|
||
line-height: 22px;
|
||
font-weight: 500;
|
||
color: #262421;
|
||
}
|
||
|
||
.search-input :deep(input),
|
||
.search-input :deep(.uni-input-input) {
|
||
font-size: 16px;
|
||
line-height: 22px;
|
||
}
|
||
|
||
.variant-home.is-collapsed .search-input,
|
||
.variant-home.is-collapsed .search-input :deep(input),
|
||
.variant-home.is-collapsed .search-input :deep(.uni-input-input) {
|
||
font-size: 14px;
|
||
line-height: 19px;
|
||
}
|
||
|
||
.search-placeholder {
|
||
color: #8b8d85;
|
||
}
|
||
|
||
.variant-home.is-collapsed .search-placeholder {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.clear-button,
|
||
.voice-icon {
|
||
width: 26px;
|
||
height: 26px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.clear-button {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
background: #eef0e8;
|
||
}
|
||
|
||
.variant-home.is-collapsed .clear-button,
|
||
.variant-home.is-collapsed .voice-icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
}
|
||
|
||
.variant-home.is-collapsed .clear-text {
|
||
font-size: 17px;
|
||
line-height: 18px;
|
||
}
|
||
|
||
.clear-text {
|
||
font-size: 18px;
|
||
line-height: 20px;
|
||
color: #5b6058;
|
||
}
|
||
|
||
.voice-icon {
|
||
position: relative;
|
||
}
|
||
|
||
.variant-home.is-collapsed .voice-icon::before {
|
||
left: 11px;
|
||
top: 7px;
|
||
width: 10px;
|
||
height: 13px;
|
||
}
|
||
|
||
.variant-home.is-collapsed .voice-icon::after {
|
||
left: 10px;
|
||
bottom: 6px;
|
||
width: 12px;
|
||
height: 7px;
|
||
}
|
||
|
||
.voice-icon::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 8px;
|
||
top: 4px;
|
||
width: 10px;
|
||
height: 14px;
|
||
border: 2px solid #151713;
|
||
border-radius: 999px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.voice-icon::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 7px;
|
||
bottom: 3px;
|
||
width: 12px;
|
||
height: 7px;
|
||
border-bottom: 2px solid #151713;
|
||
border-left: 2px solid transparent;
|
||
border-right: 2px solid transparent;
|
||
border-radius: 0 0 999px 999px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.poi-search-content {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.variant-page .poi-search-content {
|
||
margin-top: 14px;
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
padding-bottom: env(safe-area-inset-bottom);
|
||
}
|
||
|
||
.home-collapse-handle {
|
||
height: 18px;
|
||
margin: -4px 0 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.home-collapse-bar {
|
||
width: 42px;
|
||
height: 4px;
|
||
background: #d5d8cf;
|
||
border-radius: 999px;
|
||
}
|
||
|
||
.variant-home .poi-search-content {
|
||
max-height: min(50vh, 430px);
|
||
overflow-y: auto;
|
||
padding-right: 2px;
|
||
}
|
||
|
||
.variant-home.expanded .poi-search-content {
|
||
flex: 1;
|
||
min-height: 0;
|
||
max-height: none;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
padding-right: 0;
|
||
}
|
||
|
||
.category-scroll {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
padding-bottom: 4px;
|
||
scrollbar-width: none;
|
||
overscroll-behavior-x: contain;
|
||
-webkit-overflow-scrolling: touch;
|
||
touch-action: pan-x;
|
||
}
|
||
|
||
.category-scroll::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
|
||
.category-grid {
|
||
--category-gap: 8px;
|
||
display: grid;
|
||
grid-auto-flow: column;
|
||
grid-template-rows: repeat(2, minmax(70px, auto));
|
||
grid-auto-columns: calc((100% - 32px) / 5);
|
||
gap: var(--category-gap);
|
||
width: calc(125% + 2px);
|
||
min-width: calc(125% + 2px);
|
||
}
|
||
|
||
.category-item {
|
||
min-width: 0;
|
||
min-height: 70px;
|
||
position: relative;
|
||
padding: 9px 6px 10px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 7px;
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
background: #fffef2;
|
||
border: 1px solid #ecebd2;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.category-item.active {
|
||
background: #fffddc;
|
||
border-color: #d8d900;
|
||
}
|
||
|
||
.category-item.active::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 14px;
|
||
right: 14px;
|
||
bottom: 0;
|
||
height: 4px;
|
||
background: var(--museum-accent);
|
||
}
|
||
|
||
.line-icon {
|
||
width: 26px;
|
||
height: 24px;
|
||
position: relative;
|
||
color: #151713;
|
||
}
|
||
|
||
.category-item.active .line-icon {
|
||
color: #151713;
|
||
}
|
||
|
||
.icon-part {
|
||
position: absolute;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.icon-service .icon-part.a,
|
||
.icon-exhibition .icon-part.a,
|
||
.icon-cinema .icon-part.a,
|
||
.icon-ticket .icon-part.a,
|
||
.icon-drink .icon-part.a,
|
||
.icon-bag .icon-part.a {
|
||
left: 4px;
|
||
right: 4px;
|
||
top: 5px;
|
||
bottom: 5px;
|
||
border: 2px solid currentColor;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.icon-service .icon-part.b,
|
||
.icon-exhibition .icon-part.b,
|
||
.icon-cinema .icon-part.b,
|
||
.icon-ticket .icon-part.b,
|
||
.icon-drink .icon-part.b,
|
||
.icon-bag .icon-part.b {
|
||
left: 9px;
|
||
right: 9px;
|
||
top: 1px;
|
||
height: 5px;
|
||
border: 2px solid currentColor;
|
||
border-bottom: 0;
|
||
border-radius: 6px 6px 0 0;
|
||
}
|
||
|
||
.icon-elevator .icon-part.a,
|
||
.icon-escalator .icon-part.a,
|
||
.icon-restroom .icon-part.a,
|
||
.icon-nursing .icon-part.a,
|
||
.icon-restaurant .icon-part.a,
|
||
.icon-stairs .icon-part.a,
|
||
.icon-entrance .icon-part.a {
|
||
left: 4px;
|
||
right: 4px;
|
||
top: 3px;
|
||
bottom: 3px;
|
||
border: 2px solid currentColor;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.icon-elevator .icon-part.b {
|
||
left: 9px;
|
||
top: 7px;
|
||
width: 8px;
|
||
height: 10px;
|
||
border-left: 2px solid currentColor;
|
||
border-right: 2px solid currentColor;
|
||
}
|
||
|
||
.icon-escalator .icon-part.b,
|
||
.icon-entrance .icon-part.b {
|
||
left: 7px;
|
||
top: 12px;
|
||
width: 13px;
|
||
height: 2px;
|
||
background: currentColor;
|
||
transform: rotate(-22deg);
|
||
}
|
||
|
||
.icon-restroom .icon-part.b,
|
||
.icon-nursing .icon-part.b {
|
||
left: 8px;
|
||
top: 8px;
|
||
width: 4px;
|
||
height: 4px;
|
||
background: currentColor;
|
||
border-radius: 50%;
|
||
box-shadow: 8px 0 0 currentColor;
|
||
}
|
||
|
||
.icon-exhibition .icon-part.c {
|
||
left: 6px;
|
||
right: 6px;
|
||
bottom: 2px;
|
||
height: 2px;
|
||
background: currentColor;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.icon-cinema .icon-part.b {
|
||
left: 2px;
|
||
right: 2px;
|
||
top: 4px;
|
||
height: 4px;
|
||
border: 0;
|
||
border-top: 2px solid currentColor;
|
||
border-bottom: 2px solid currentColor;
|
||
border-radius: 0;
|
||
}
|
||
|
||
.icon-restaurant .icon-part.b {
|
||
left: 7px;
|
||
top: 6px;
|
||
width: 2px;
|
||
height: 12px;
|
||
background: currentColor;
|
||
box-shadow: 5px 0 0 currentColor, 10px 0 0 currentColor;
|
||
}
|
||
|
||
.icon-restaurant .icon-part.c {
|
||
right: 3px;
|
||
top: 5px;
|
||
width: 7px;
|
||
height: 15px;
|
||
border-left: 2px solid currentColor;
|
||
border-bottom: 2px solid currentColor;
|
||
border-radius: 0 0 0 5px;
|
||
}
|
||
|
||
.icon-stairs .icon-part.b {
|
||
left: 6px;
|
||
top: 7px;
|
||
width: 14px;
|
||
height: 12px;
|
||
border-left: 2px solid currentColor;
|
||
border-bottom: 2px solid currentColor;
|
||
}
|
||
|
||
.icon-stairs .icon-part.c {
|
||
left: 10px;
|
||
top: 11px;
|
||
width: 10px;
|
||
height: 8px;
|
||
border-left: 2px solid currentColor;
|
||
border-bottom: 2px solid currentColor;
|
||
}
|
||
|
||
.category-label {
|
||
max-width: 100%;
|
||
font-size: 12px;
|
||
line-height: 16px;
|
||
font-weight: 700;
|
||
color: #151713;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.category-item.active .category-label {
|
||
color: #151713;
|
||
}
|
||
|
||
.section-title-row {
|
||
position: relative;
|
||
margin-top: 16px;
|
||
margin-bottom: 10px;
|
||
padding-left: 12px;
|
||
}
|
||
|
||
.section-title-row::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 3px;
|
||
width: 4px;
|
||
height: 16px;
|
||
background: var(--museum-accent);
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 14px;
|
||
line-height: 20px;
|
||
font-weight: 800;
|
||
color: #151713;
|
||
}
|
||
|
||
.facility-chip-row {
|
||
display: flex;
|
||
gap: 8px;
|
||
overflow-x: auto;
|
||
padding-bottom: 2px;
|
||
scrollbar-width: none;
|
||
overscroll-behavior-x: contain;
|
||
}
|
||
|
||
.facility-chip-row::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
|
||
.facility-chip {
|
||
height: 34px;
|
||
flex: 0 0 auto;
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
padding: 0 12px;
|
||
box-sizing: border-box;
|
||
background: #fffef6;
|
||
border: 1px solid #e8e7d2;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.facility-chip.active {
|
||
background: #fffddc;
|
||
border-color: #d6d700;
|
||
}
|
||
|
||
.facility-chip.active::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 12px;
|
||
right: 12px;
|
||
bottom: -1px;
|
||
height: 3px;
|
||
background: var(--museum-accent);
|
||
}
|
||
|
||
.chip-icon {
|
||
width: 14px;
|
||
height: 14px;
|
||
flex: 0 0 14px;
|
||
position: relative;
|
||
border: 2px solid currentColor;
|
||
border-radius: 3px;
|
||
color: #151713;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.facility-chip.active .chip-icon {
|
||
color: #151713;
|
||
}
|
||
|
||
.facility-chip-text {
|
||
font-size: 12px;
|
||
line-height: 16px;
|
||
font-weight: 600;
|
||
color: #151713;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.facility-chip.active .facility-chip-text {
|
||
color: #151713;
|
||
}
|
||
|
||
.result-card {
|
||
margin-top: 14px;
|
||
overflow: hidden;
|
||
background: #fffef7;
|
||
border: 1px solid #ecebd8;
|
||
border-radius: 8px;
|
||
box-shadow: 0 10px 24px rgba(75, 78, 60, 0.08);
|
||
}
|
||
|
||
.result-card-header {
|
||
height: 46px;
|
||
padding: 0 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
box-sizing: border-box;
|
||
border-bottom: 1px solid #efeedf;
|
||
}
|
||
|
||
.museum-title,
|
||
.result-count {
|
||
font-size: 12px;
|
||
line-height: 16px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.museum-title {
|
||
color: #151713;
|
||
}
|
||
|
||
.result-count {
|
||
color: #6e746c;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.result-body {
|
||
display: flex;
|
||
min-height: 168px;
|
||
}
|
||
|
||
.floor-tabs {
|
||
width: 62px;
|
||
flex: 0 0 62px;
|
||
padding: 9px 0;
|
||
box-sizing: border-box;
|
||
background: #f6f5e8;
|
||
}
|
||
|
||
.floor-tab {
|
||
position: relative;
|
||
height: 31px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #676c64;
|
||
}
|
||
|
||
.floor-tab.active {
|
||
color: #151713;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.floor-tab.active::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 8px;
|
||
right: 8px;
|
||
bottom: 2px;
|
||
height: 3px;
|
||
background: var(--museum-accent);
|
||
}
|
||
|
||
.floor-tab-text {
|
||
font-size: 12px;
|
||
line-height: 16px;
|
||
}
|
||
|
||
.result-list {
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.variant-page .category-grid,
|
||
.variant-page .category-scroll,
|
||
.variant-page .section-title-row,
|
||
.variant-page .facility-chip-row {
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.variant-page .result-card {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.variant-page .result-body {
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
.variant-page .floor-tabs,
|
||
.variant-page .result-list {
|
||
overflow-y: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
overscroll-behavior: contain;
|
||
}
|
||
|
||
.variant-page .result-list {
|
||
padding-bottom: calc(env(safe-area-inset-bottom) + 10px);
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.variant-home.expanded .category-grid,
|
||
.variant-home.expanded .category-scroll,
|
||
.variant-home.expanded .section-title-row,
|
||
.variant-home.expanded .facility-chip-row {
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.variant-home.expanded .result-card {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.variant-home.expanded .result-body {
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
.variant-home.expanded .floor-tabs,
|
||
.variant-home.expanded .result-list {
|
||
overflow-y: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
overscroll-behavior: contain;
|
||
}
|
||
|
||
.result-row {
|
||
min-height: 58px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 9px;
|
||
padding: 10px 12px;
|
||
box-sizing: border-box;
|
||
border-bottom: 1px solid #efefe1;
|
||
}
|
||
|
||
.result-row:last-child {
|
||
border-bottom: 0;
|
||
}
|
||
|
||
.pin-icon {
|
||
width: 15px;
|
||
height: 18px;
|
||
flex: 0 0 15px;
|
||
position: relative;
|
||
border: 2px solid #151713;
|
||
border-radius: 8px 8px 8px 0;
|
||
transform: rotate(-45deg);
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.pin-icon::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 3px;
|
||
top: 3px;
|
||
width: 4px;
|
||
height: 4px;
|
||
background: #151713;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.result-copy {
|
||
min-width: 0;
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.result-name,
|
||
.result-meta {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.result-name {
|
||
font-size: 13px;
|
||
line-height: 18px;
|
||
font-weight: 700;
|
||
color: #151713;
|
||
}
|
||
|
||
.result-meta {
|
||
font-size: 11px;
|
||
line-height: 15px;
|
||
color: #6c716a;
|
||
}
|
||
|
||
.result-arrow {
|
||
font-size: 20px;
|
||
line-height: 20px;
|
||
color: #8b9188;
|
||
}
|
||
|
||
.empty-state {
|
||
min-height: 112px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
padding: 20px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.empty-title {
|
||
font-size: 14px;
|
||
line-height: 20px;
|
||
font-weight: 700;
|
||
color: #151713;
|
||
}
|
||
|
||
.empty-desc {
|
||
font-size: 12px;
|
||
line-height: 17px;
|
||
color: #7a7f78;
|
||
}
|
||
|
||
@media (max-width: 360px) {
|
||
.category-grid {
|
||
grid-template-rows: repeat(2, minmax(62px, auto));
|
||
}
|
||
|
||
.variant-page .category-item,
|
||
.variant-home.expanded .category-item {
|
||
min-height: 62px;
|
||
padding: 7px 4px;
|
||
gap: 5px;
|
||
}
|
||
|
||
.floor-tabs {
|
||
width: 52px;
|
||
flex-basis: 52px;
|
||
}
|
||
}
|
||
|
||
</style>
|