chore: initialize frontend miniapp repository
This commit is contained in:
937
src/components/explain/ExplainList.vue
Normal file
937
src/components/explain/ExplainList.vue
Normal file
@@ -0,0 +1,937 @@
|
||||
<template>
|
||||
<view class="explain-list">
|
||||
<!-- 顶部筛选器 -->
|
||||
<view class="explain-header">
|
||||
<!-- 搜索栏触发按钮 -->
|
||||
<view class="search-bar-trigger" @tap="handleOpenSearch">
|
||||
<view class="search-input-wrapper">
|
||||
<svg class="search-icon" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<circle cx="8.5" cy="8.5" r="5.5" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M12.5 12.5L17 17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<text class="search-placeholder">{{ searchKeyword || '搜索展品、展厅、设施...' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选按钮 -->
|
||||
<view class="filter-buttons">
|
||||
<view
|
||||
class="filter-btn"
|
||||
:class="{ active: activeFilter === 'hall' }"
|
||||
@tap="handleFilterChange('hall')"
|
||||
>
|
||||
<text class="filter-text">按展厅</text>
|
||||
</view>
|
||||
<view
|
||||
class="filter-btn"
|
||||
:class="{ active: activeFilter === 'theme' }"
|
||||
@tap="handleFilterChange('theme')"
|
||||
>
|
||||
<text class="filter-text">按主题</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 抽屉遮罩层 -->
|
||||
<view
|
||||
v-if="drawerVisible && !searchPanelOpen"
|
||||
class="drawer-overlay"
|
||||
@tap="handleCloseDrawer"
|
||||
></view>
|
||||
|
||||
<!-- 底部可拖拽抽屉 -->
|
||||
<view
|
||||
class="drawer-container"
|
||||
:class="{
|
||||
'search-mode': searchPanelOpen,
|
||||
'drawer-visible': drawerVisible,
|
||||
'drawer-collapsed': !drawerVisible && !searchPanelOpen
|
||||
}"
|
||||
:style="drawerStyle"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchmove="handleTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
>
|
||||
<!-- 抽屉把手 -->
|
||||
<view class="drawer-handle" @tap="handleToggleDrawer">
|
||||
<view class="handle-bar"></view>
|
||||
</view>
|
||||
|
||||
<!-- 抽屉内容 -->
|
||||
<scroll-view
|
||||
class="drawer-content"
|
||||
:scroll-y="true"
|
||||
:scroll-top="searchScrollTop"
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<!-- 搜索模式内容 -->
|
||||
<view v-if="searchPanelOpen" class="search-content">
|
||||
<!-- 搜索框 -->
|
||||
<view class="search-box">
|
||||
<svg class="search-box-icon" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<circle cx="8.5" cy="8.5" r="5.5" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M12.5 12.5L17 17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<input
|
||||
class="search-box-input"
|
||||
type="text"
|
||||
placeholder="搜索展品、展厅、设施..."
|
||||
v-model="searchInputValue"
|
||||
@input="handleSearchInput"
|
||||
@confirm="handleSearchConfirm"
|
||||
/>
|
||||
<view v-if="searchInputValue" class="clear-btn" @tap="handleClearSearch">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
||||
<circle cx="7" cy="7" r="6" fill="#999"/>
|
||||
<path d="M4.5 4.5L9.5 9.5M9.5 4.5L4.5 9.5" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索结果 -->
|
||||
<view v-if="searchInputValue" class="results-section">
|
||||
<view v-if="searchResults.length" class="results-list">
|
||||
<view
|
||||
v-for="item in searchResults"
|
||||
:key="item.id"
|
||||
class="result-item"
|
||||
@tap="handleResultClick(item)"
|
||||
>
|
||||
<view class="result-icon" :class="item.type">
|
||||
<text>{{ getResultIcon(item.type) }}</text>
|
||||
</view>
|
||||
<view class="result-info">
|
||||
<text class="result-name">{{ item.name }}</text>
|
||||
<text class="result-desc">{{ item.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="no-results">
|
||||
<text>未找到 "{{ searchInputValue }}" 相关结果</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 热门搜索 (无关键词时显示) -->
|
||||
<view v-if="!searchInputValue" class="hot-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">热门搜索</text>
|
||||
</view>
|
||||
<view class="hot-tags">
|
||||
<view
|
||||
v-for="tag in hotSearches"
|
||||
:key="tag"
|
||||
class="hot-tag"
|
||||
@tap="handleHotTagClick(tag)"
|
||||
>
|
||||
<text class="hot-tag-text">{{ tag }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索历史 (无关键词时显示) -->
|
||||
<view v-if="!searchInputValue && searchHistory.length" class="history-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">搜索历史</text>
|
||||
<text class="clear-history" @tap="handleClearHistory">清除</text>
|
||||
</view>
|
||||
<view class="history-list">
|
||||
<view
|
||||
v-for="(item, index) in searchHistory"
|
||||
:key="index"
|
||||
class="history-item"
|
||||
@tap="handleHistoryClick(item)"
|
||||
>
|
||||
<text class="history-text">{{ item }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 关闭搜索按钮 -->
|
||||
<view class="close-search" @tap="handleCloseSearch">
|
||||
<text>关闭</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 正常模式内容 - 展品列表 -->
|
||||
<view v-else class="exhibits-list">
|
||||
<view
|
||||
v-for="exhibit in filteredExhibits"
|
||||
:key="exhibit.id"
|
||||
class="exhibit-card"
|
||||
@tap="handleExhibitClick(exhibit)"
|
||||
>
|
||||
<!-- 左侧展品图片 -->
|
||||
<view class="exhibit-image">
|
||||
<image
|
||||
v-if="exhibit.image"
|
||||
class="image"
|
||||
:src="exhibit.image"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="image-placeholder">
|
||||
<svg class="placeholder-icon" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="3" y="3" width="18" height="18" rx="3" stroke="currentColor" stroke-width="1.5"/>
|
||||
<circle cx="8.5" cy="8.5" r="2" fill="currentColor"/>
|
||||
<path d="M3 16L7 12L10 15L14 11L21 18" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 中间展品信息 -->
|
||||
<view class="exhibit-info">
|
||||
<text class="exhibit-name">{{ exhibit.name }}</text>
|
||||
<text class="exhibit-floor">{{ exhibit.hall || exhibit.floor }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 右侧音频指示器 -->
|
||||
<view v-if="exhibit.hasAudio" class="audio-indicator" @tap.stop="handleAudioClick(exhibit)">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 3C12 3 14 5 14 9C14 13 12 15 12 15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M12 15C12 15 14 17 14 21C14 21 10 21 10 21C10 21 10 17 12 15Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 9C6.5 9 5 10.5 5 12C5 13.5 6.5 15 8 15C9 15 9.5 14.5 9.5 14.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M16 7C13.5 7 11.5 9 11.5 11.5C11.5 14 13.5 16 16 16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部加载完毕提示 -->
|
||||
<view class="loading-end">
|
||||
<text class="end-text">— 已加载全部 —</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export interface Exhibit {
|
||||
id: string
|
||||
name: string
|
||||
artist?: string
|
||||
hall?: string
|
||||
floor?: string
|
||||
image?: string
|
||||
hasAudio?: boolean
|
||||
audioUrl?: string
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
id: string
|
||||
name: string
|
||||
desc: string
|
||||
type: 'exhibit' | 'hall' | 'facility'
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
exhibitClick: [exhibit: Exhibit]
|
||||
audioClick: [exhibit: Exhibit]
|
||||
filterChange: [filter: 'hall' | 'theme']
|
||||
search: [keyword: string]
|
||||
}>()
|
||||
|
||||
// 状态
|
||||
const searchKeyword = ref('')
|
||||
const searchPanelOpen = ref(false)
|
||||
const searchInputValue = ref('')
|
||||
const searchScrollTop = ref(0)
|
||||
const activeFilter = ref<'hall' | 'theme'>('hall') // 默认选中"按展厅"
|
||||
const drawerVisible = ref(false) // 抽屉默认收起
|
||||
const drawerOffset = ref(0)
|
||||
|
||||
// 抽屉拖拽样式 - 将 drawerOffset 绑定到 transform
|
||||
const drawerStyle = computed(() => {
|
||||
if (!drawerVisible.value && !searchPanelOpen.value) {
|
||||
return {} // 收起状态不需要 transform
|
||||
}
|
||||
return {
|
||||
transform: `translateY(${drawerOffset.value}px)`
|
||||
}
|
||||
})
|
||||
|
||||
// 抽屉拖拽相关
|
||||
const DRAWER_MIN_OFFSET = 0
|
||||
const DRAWER_MAX_OFFSET = 200
|
||||
const DRAWER_SNAP_THRESHOLD = 100
|
||||
let touchStartY = 0
|
||||
let touchCurrentY = 0
|
||||
|
||||
// 热门搜索
|
||||
const hotSearches = ['恐龙化石', '银杏', '海洋生物', '霸王龙', '古植物', '矿石标本', '恐龙', '化石']
|
||||
|
||||
// 搜索历史
|
||||
const searchHistory = ref<string[]>(['霸王龙', '化石', '恐龙'])
|
||||
|
||||
// 搜索数据
|
||||
const allData: SearchResult[] = [
|
||||
{ id: '1', name: '霸王龙化石骨架', desc: '恐龙厅 · 2F', type: 'exhibit' },
|
||||
{ id: '2', name: '银杏化石', desc: '演化厅 · B1', type: 'exhibit' },
|
||||
{ id: '3', name: '三叶虫化石', desc: '演化厅 · B1', type: 'exhibit' },
|
||||
{ id: '4', name: '腕龙骨骼', desc: '恐龙厅 · 2F', type: 'exhibit' },
|
||||
{ id: '5', name: '猛犸象象牙', desc: '人类厅 · 3F', type: 'exhibit' },
|
||||
{ id: 'h1', name: '恐龙厅', desc: '2F · 25件展品', type: 'hall' },
|
||||
{ id: 'h2', name: '演化厅', desc: 'B1 · 30件展品', type: 'hall' },
|
||||
{ id: 'h3', name: '生物厅', desc: '3F · 18件展品', type: 'hall' },
|
||||
{ id: 'f1', name: '洗手间', desc: '1F', type: 'facility' },
|
||||
{ id: 'f2', name: '咖啡厅', desc: '1F', type: 'facility' },
|
||||
]
|
||||
|
||||
// 搜索结果计算
|
||||
const searchResults = computed(() => {
|
||||
if (!searchInputValue.value.trim()) return []
|
||||
const kw = searchInputValue.value.toLowerCase()
|
||||
return allData.filter(item =>
|
||||
item.name.toLowerCase().includes(kw) ||
|
||||
item.desc.toLowerCase().includes(kw)
|
||||
)
|
||||
})
|
||||
|
||||
// 展品列表
|
||||
const exhibits = ref<Exhibit[]>([
|
||||
{ id: '1', name: '银杏化石', hall: '演化厅 B1', hasAudio: true, audioUrl: '' },
|
||||
{ id: '2', name: '三叶虫化石', hall: '演化厅 B1', hasAudio: true, audioUrl: '' },
|
||||
{ id: '3', name: '腕龙骨骼', hall: '恐龙厅 2F', hasAudio: true, audioUrl: '' },
|
||||
{ id: '4', name: '始祖鸟标本', hall: '演化厅 B1', hasAudio: false, audioUrl: '' },
|
||||
{ id: '5', name: '猛犸象象牙', hall: '人类厅 3F', hasAudio: true, audioUrl: '' },
|
||||
{ id: '6', name: '珊瑚礁标本', hall: '生物厅 3F', hasAudio: false, audioUrl: '' },
|
||||
{ id: '7', name: '剑龙化石', hall: '恐龙厅 2F', hasAudio: true, audioUrl: '' },
|
||||
{ id: '8', name: '鹦鹉螺化石', hall: '演化厅 B1', hasAudio: true, audioUrl: '' }
|
||||
])
|
||||
|
||||
// 过滤展品
|
||||
const filteredExhibits = computed(() => {
|
||||
if (!searchKeyword.value.trim()) {
|
||||
return exhibits.value
|
||||
}
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
return exhibits.value.filter(exhibit =>
|
||||
exhibit.name.toLowerCase().includes(keyword) ||
|
||||
(exhibit.hall && exhibit.hall.toLowerCase().includes(keyword))
|
||||
)
|
||||
})
|
||||
|
||||
// 事件处理
|
||||
const handleExhibitClick = (exhibit: Exhibit) => {
|
||||
emit('exhibitClick', exhibit)
|
||||
}
|
||||
|
||||
const handleAudioClick = (exhibit: Exhibit) => {
|
||||
emit('audioClick', exhibit)
|
||||
}
|
||||
|
||||
// 搜索相关方法
|
||||
const handleOpenSearch = () => {
|
||||
searchPanelOpen.value = true
|
||||
searchScrollTop.value = 0
|
||||
}
|
||||
|
||||
const handleCloseSearch = () => {
|
||||
searchPanelOpen.value = false
|
||||
searchInputValue.value = ''
|
||||
}
|
||||
|
||||
const handleSearchInput = () => {
|
||||
// 实时搜索
|
||||
}
|
||||
|
||||
const handleSearchConfirm = () => {
|
||||
if (searchInputValue.value.trim()) {
|
||||
addToHistory(searchInputValue.value)
|
||||
searchKeyword.value = searchInputValue.value
|
||||
emit('search', searchInputValue.value)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClearSearch = () => {
|
||||
searchInputValue.value = ''
|
||||
}
|
||||
|
||||
const handleHotTagClick = (tag: string) => {
|
||||
searchInputValue.value = tag
|
||||
handleSearchConfirm()
|
||||
}
|
||||
|
||||
const handleHistoryClick = (history: string) => {
|
||||
searchInputValue.value = history
|
||||
handleSearchConfirm()
|
||||
}
|
||||
|
||||
const handleClearHistory = () => {
|
||||
searchHistory.value = []
|
||||
}
|
||||
|
||||
const handleResultClick = (item: SearchResult) => {
|
||||
if (item.type === 'exhibit') {
|
||||
const exhibit = exhibits.value.find(e => e.id === item.id)
|
||||
if (exhibit) {
|
||||
emit('exhibitClick', exhibit)
|
||||
}
|
||||
}
|
||||
searchPanelOpen.value = false
|
||||
searchInputValue.value = ''
|
||||
}
|
||||
|
||||
const addToHistory = (keyword: string) => {
|
||||
if (!searchHistory.value.includes(keyword)) {
|
||||
searchHistory.value.unshift(keyword)
|
||||
if (searchHistory.value.length > 10) {
|
||||
searchHistory.value.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getResultIcon = (type: string): string => {
|
||||
const icons: Record<string, string> = {
|
||||
exhibit: '🖼️',
|
||||
hall: '🏛️',
|
||||
facility: '📍'
|
||||
}
|
||||
return icons[type] || '📍'
|
||||
}
|
||||
|
||||
const handleFilterChange = (filter: 'hall' | 'theme') => {
|
||||
if (activeFilter.value === filter && drawerVisible.value) {
|
||||
// 再次点击同一筛选项,收起抽屉
|
||||
handleCloseDrawer()
|
||||
} else {
|
||||
// 切换筛选项或展开抽屉
|
||||
activeFilter.value = filter
|
||||
drawerVisible.value = true
|
||||
drawerOffset.value = 0
|
||||
emit('filterChange', filter)
|
||||
}
|
||||
}
|
||||
|
||||
// 收起抽屉
|
||||
const handleCloseDrawer = () => {
|
||||
drawerVisible.value = false
|
||||
drawerOffset.value = 0
|
||||
}
|
||||
|
||||
// 切换抽屉展开/收起
|
||||
const handleToggleDrawer = () => {
|
||||
if (drawerVisible.value) {
|
||||
handleCloseDrawer()
|
||||
} else {
|
||||
drawerVisible.value = true
|
||||
drawerOffset.value = 0
|
||||
emit('filterChange', activeFilter.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 抽屉拖拽处理
|
||||
const handleTouchStart = (e: TouchEvent) => {
|
||||
// 搜索模式下或收起状态禁用抽屉拖拽
|
||||
if (searchPanelOpen.value || !drawerVisible.value) return
|
||||
touchStartY = e.touches[0].clientY
|
||||
touchCurrentY = touchStartY
|
||||
}
|
||||
|
||||
const handleTouchMove = (e: TouchEvent) => {
|
||||
// 搜索模式下或收起状态禁用抽屉拖拽
|
||||
if (searchPanelOpen.value || !drawerVisible.value) return
|
||||
touchCurrentY = e.touches[0].clientY
|
||||
const deltaY = touchCurrentY - touchStartY
|
||||
|
||||
let newOffset = drawerOffset.value + deltaY
|
||||
newOffset = Math.max(DRAWER_MIN_OFFSET, Math.min(DRAWER_MAX_OFFSET, newOffset))
|
||||
|
||||
drawerOffset.value = newOffset
|
||||
touchStartY = touchCurrentY
|
||||
}
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
// 搜索模式下或收起状态禁用抽屉拖拽
|
||||
if (searchPanelOpen.value || !drawerVisible.value) return
|
||||
|
||||
if (drawerOffset.value < DRAWER_SNAP_THRESHOLD) {
|
||||
drawerOffset.value = DRAWER_MIN_OFFSET
|
||||
} else {
|
||||
// 向下拖拽超过阈值,收起抽屉
|
||||
handleCloseDrawer()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.explain-list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
background: transparent;
|
||||
// 非交互区域不透光穿透,让触摸事件落到地图上
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.explain-header {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 56px 16px 12px;
|
||||
background-color: transparent;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.search-bar-trigger {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background: var(--museum-bg-cream);
|
||||
border: 1px solid white;
|
||||
border-radius: var(--radius-mini);
|
||||
padding: 9px 12px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
font-size: 14px;
|
||||
color: var(--museum-text-disabled);
|
||||
}
|
||||
|
||||
.filter-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
z-index: 11;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
flex: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(0, 0, 0, 0.06);
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background-color: #1a1a1a;
|
||||
border-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.filter-text {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: #333333;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.filter-btn.active .filter-text {
|
||||
color: #E0E100;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// 抽屉遮罩层
|
||||
.drawer-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
z-index: 99;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
// 抽屉容器
|
||||
.drawer-container {
|
||||
position: absolute;
|
||||
// 默认展开高度:屏幕一半
|
||||
top: 50vh;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 24px 24px 0 0;
|
||||
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.08);
|
||||
transition: top 0.35s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
opacity 0.3s ease;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
// 收起状态:隐藏到屏幕底部
|
||||
.drawer-container.drawer-collapsed {
|
||||
top: 100vh;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// 展开状态
|
||||
.drawer-container.drawer-visible {
|
||||
top: 50vh;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.drawer-container.search-mode {
|
||||
top: 44px;
|
||||
border-radius: 0;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.drawer-handle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.handle-bar {
|
||||
width: 66px;
|
||||
height: 5px;
|
||||
background-color: #E5E5E5;
|
||||
border-radius: 2.5px;
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
// 移除 overflow-y: auto,避免与 scroll-view 自身滚动机制冲突
|
||||
padding: 12px 16px 0;
|
||||
height: 0;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索内容样式
|
||||
.search-content {
|
||||
padding-bottom: calc(20px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.search-box {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background: var(--museum-bg-cream);
|
||||
border: 1px solid white;
|
||||
border-radius: var(--radius-mini);
|
||||
padding: 9px 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-box-icon {
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.search-box-input {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: var(--museum-text-primary);
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--museum-text-disabled);
|
||||
}
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.results-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.results-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
|
||||
&:active {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
.result-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.result-icon.exhibit {
|
||||
background-color: rgba(224, 225, 0, 0.15);
|
||||
}
|
||||
|
||||
.result-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.result-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #1A1A1A;
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.result-desc {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: #999999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.hot-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
|
||||
.clear-history {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.hot-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.hot-tag {
|
||||
padding: 8px 14px;
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 18px;
|
||||
|
||||
&:active {
|
||||
background-color: rgba(224, 225, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.hot-tag-text {
|
||||
font-size: 13px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.history-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
padding: 8px 14px;
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 18px;
|
||||
|
||||
&:active {
|
||||
background-color: rgba(224, 225, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.history-text {
|
||||
font-size: 13px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.close-search {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
margin-top: 8px;
|
||||
|
||||
text {
|
||||
font-size: 14px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
&:active {
|
||||
text {
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 展品列表样式
|
||||
.exhibits-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.exhibit-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
background-color: #F3F3F3;
|
||||
border-radius: 12px;
|
||||
padding: 0 12px;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.exhibit-image {
|
||||
flex-shrink: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background-color: #F0F0F0;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #F0F0F0;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
color: #CCCCCC;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.exhibit-info {
|
||||
flex: 1;
|
||||
margin-left: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.exhibit-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #1A1A1A;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.exhibit-floor {
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.audio-indicator {
|
||||
flex-shrink: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #E0E100;
|
||||
border-radius: 50%;
|
||||
margin-left: 12px;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #1A1A1A;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-end {
|
||||
padding: 24px 16px calc(24px + env(safe-area-inset-bottom));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.end-text {
|
||||
font-size: 12px;
|
||||
color: #CCCCCC;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user