chore: initialize frontend miniapp repository
This commit is contained in:
356
src/pages/search/index.vue
Normal file
356
src/pages/search/index.vue
Normal file
@@ -0,0 +1,356 @@
|
||||
<template>
|
||||
<GuideMapShell
|
||||
:search-text="searchKeyword || '搜索设施、展厅、入口'"
|
||||
floor-top="164px"
|
||||
@search-tap="handleSearchTap"
|
||||
@mode-change="handleModeChange"
|
||||
@floor-change="handleFloorChange"
|
||||
>
|
||||
<template #overlay>
|
||||
<view class="filter-row">
|
||||
<view
|
||||
v-for="filter in filters"
|
||||
:key="filter.id"
|
||||
class="filter-chip"
|
||||
:class="[{ active: currentFilter === filter.id }, `filter-${filter.id}`]"
|
||||
@tap="handleFilterChange(filter.id)"
|
||||
>
|
||||
<text class="filter-text">{{ filter.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="results-sheet">
|
||||
<view class="sheet-header">
|
||||
<text class="sheet-title">{{ resultTitle }}</text>
|
||||
<text class="sort-label">按距离排序</text>
|
||||
</view>
|
||||
|
||||
<view class="result-list">
|
||||
<view
|
||||
v-for="item in visibleFacilities"
|
||||
:key="item.id"
|
||||
class="result-card"
|
||||
@tap="handleFacilityClick(item)"
|
||||
>
|
||||
<view class="facility-mark">
|
||||
<text class="facility-mark-text">卫</text>
|
||||
</view>
|
||||
<view class="facility-info">
|
||||
<text class="facility-name">{{ item.name }}</text>
|
||||
<text class="facility-meta">{{ item.meta }}</text>
|
||||
</view>
|
||||
<view
|
||||
class="result-action"
|
||||
:class="{ secondary: item.action === '查看' }"
|
||||
@tap.stop="handleResultAction(item)"
|
||||
>
|
||||
<text class="result-action-text">{{ item.action }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</GuideMapShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
|
||||
import {
|
||||
formatNavFloorLabel,
|
||||
isPoiAccessible,
|
||||
searchCleanNavPois,
|
||||
type CleanNavPoi
|
||||
} from '@/services/navAssets'
|
||||
|
||||
interface FilterItem {
|
||||
id: 'all' | 'accessible' | 'floor' | 'nearest'
|
||||
label: string
|
||||
}
|
||||
|
||||
interface FacilityResult {
|
||||
id: string
|
||||
name: string
|
||||
floor: string
|
||||
meta: string
|
||||
accessible: boolean
|
||||
action: '查看'
|
||||
}
|
||||
|
||||
const searchKeyword = ref('卫生间')
|
||||
const currentFilter = ref<FilterItem['id']>('all')
|
||||
const isLoading = ref(false)
|
||||
|
||||
const filters: FilterItem[] = [
|
||||
{ id: 'all', label: '全部' },
|
||||
{ id: 'accessible', label: '无障碍' },
|
||||
{ id: 'floor', label: '本楼层' },
|
||||
{ id: 'nearest', label: '最近' }
|
||||
]
|
||||
|
||||
const facilities = ref<FacilityResult[]>([])
|
||||
|
||||
const toFacilityResult = (poi: CleanNavPoi): FacilityResult => {
|
||||
const floor = formatNavFloorLabel(poi.floorId)
|
||||
return {
|
||||
id: poi.id,
|
||||
name: poi.name,
|
||||
floor,
|
||||
meta: `${floor}|${poi.primaryCategoryZh}|${poi.navigationReadiness || '展示点位'}`,
|
||||
accessible: isPoiAccessible(poi),
|
||||
action: '查看'
|
||||
}
|
||||
}
|
||||
|
||||
const visibleFacilities = computed(() => {
|
||||
if (currentFilter.value === 'accessible') {
|
||||
return facilities.value.filter((item) => item.accessible)
|
||||
}
|
||||
if (currentFilter.value === 'floor') {
|
||||
return facilities.value.filter((item) => item.floor === '1F')
|
||||
}
|
||||
return facilities.value
|
||||
})
|
||||
|
||||
const resultTitle = computed(() => {
|
||||
if (isLoading.value) return '正在读取 clean 导览数据'
|
||||
return `${searchKeyword.value || '设施'} ${visibleFacilities.value.length} 个结果`
|
||||
})
|
||||
|
||||
const loadFacilityResults = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const pois = await searchCleanNavPois(searchKeyword.value)
|
||||
facilities.value = pois
|
||||
.filter((poi) => poi.primaryCategory !== 'touring_poi')
|
||||
.map(toFacilityResult)
|
||||
} catch (error) {
|
||||
console.error('加载 clean 导览搜索结果失败:', error)
|
||||
facilities.value = []
|
||||
uni.showToast({
|
||||
title: '导览数据读取失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((options: any) => {
|
||||
if (options.keyword) {
|
||||
searchKeyword.value = decodeURIComponent(options.keyword)
|
||||
}
|
||||
void loadFacilityResults()
|
||||
})
|
||||
|
||||
const handleSearchTap = () => {
|
||||
console.log('保持搜索结果页')
|
||||
}
|
||||
|
||||
const handleFilterChange = (filterId: FilterItem['id']) => {
|
||||
currentFilter.value = filterId
|
||||
}
|
||||
|
||||
const handleFacilityClick = (facility: FacilityResult) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/facility/detail?id=${facility.id}&target=${encodeURIComponent(facility.name)}`
|
||||
})
|
||||
}
|
||||
|
||||
const handleResultAction = (facility: FacilityResult) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/route/detail?facilityId=${facility.id}&target=${encodeURIComponent(facility.name)}&state=planning`
|
||||
})
|
||||
}
|
||||
|
||||
const handleModeChange = (mode: '2d' | '3d') => {
|
||||
console.log('切换搜索页导览模式:', mode)
|
||||
}
|
||||
|
||||
const handleFloorChange = (floor: string) => {
|
||||
console.log('搜索页切换楼层:', floor)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.filter-row {
|
||||
position: absolute;
|
||||
top: 102px;
|
||||
left: 23px;
|
||||
right: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 42;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.filter-chip.active {
|
||||
background: #000000;
|
||||
border-color: #000000;
|
||||
}
|
||||
|
||||
.filter-all {
|
||||
width: 68px;
|
||||
}
|
||||
|
||||
.filter-accessible,
|
||||
.filter-floor {
|
||||
width: 82px;
|
||||
}
|
||||
|
||||
.filter-nearest {
|
||||
width: 74px;
|
||||
}
|
||||
|
||||
.filter-text {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.filter-chip.active .filter-text {
|
||||
color: var(--museum-accent);
|
||||
}
|
||||
|
||||
.results-sheet {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: calc(env(safe-area-inset-bottom) + 34px);
|
||||
height: 428px;
|
||||
padding: 22px 16px;
|
||||
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;
|
||||
}
|
||||
|
||||
.sheet-header {
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.sheet-title {
|
||||
font-size: 17px;
|
||||
line-height: 23px;
|
||||
font-weight: 700;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.sort-label {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: #6b7178;
|
||||
}
|
||||
|
||||
.result-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
height: 76px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 14px 0 12px;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border: 1px solid #ecede8;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.facility-mark {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
margin-right: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
background: rgba(224, 223, 0, 0.14);
|
||||
border: 1px solid rgba(224, 223, 0, 0.72);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.facility-mark-text {
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
font-weight: 700;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.facility-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.facility-name {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
color: #000000;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.facility-meta {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: #6b7178;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.result-action {
|
||||
width: 48px;
|
||||
height: 30px;
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
background: #000000;
|
||||
border: 1px solid #000000;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.result-action.secondary {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.result-action-text {
|
||||
font-size: 14px;
|
||||
line-height: 19px;
|
||||
font-weight: 500;
|
||||
color: var(--museum-accent);
|
||||
}
|
||||
|
||||
.result-action.secondary .result-action-text {
|
||||
color: #000000;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user