chore: initialize frontend miniapp repository

This commit is contained in:
lyf
2026-06-09 21:08:45 +08:00
commit a90f63cef0
107 changed files with 60454 additions and 0 deletions

356
src/pages/search/index.vue Normal file
View 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>