chore: initialize frontend miniapp repository
This commit is contained in:
270
src/pages/exhibit/detail.vue
Normal file
270
src/pages/exhibit/detail.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<view class="detail-page">
|
||||
<scroll-view class="content" scroll-y>
|
||||
<!-- 展品图片 -->
|
||||
<view class="exhibit-hero">
|
||||
<image class="hero-image" :src="exhibit.image" mode="aspectFill" />
|
||||
<view class="audio-control" @tap="handlePlayAudio">
|
||||
<text class="audio-icon">{{ isPlaying ? '⏸' : '▶' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 展品信息 -->
|
||||
<view class="exhibit-info">
|
||||
<text class="exhibit-title">{{ exhibit.name }}</text>
|
||||
<text v-if="exhibit.artist" class="exhibit-artist">{{ exhibit.artist }}</text>
|
||||
|
||||
<view class="info-grid">
|
||||
<view v-if="exhibit.year" class="info-item">
|
||||
<text class="info-label">创作年代</text>
|
||||
<text class="info-value">{{ exhibit.year }}</text>
|
||||
</view>
|
||||
<view v-if="exhibit.material" class="info-item">
|
||||
<text class="info-label">材质</text>
|
||||
<text class="info-value">{{ exhibit.material }}</text>
|
||||
</view>
|
||||
<view v-if="exhibit.size" class="info-item">
|
||||
<text class="info-label">尺寸</text>
|
||||
<text class="info-value">{{ exhibit.size }}</text>
|
||||
</view>
|
||||
<view v-if="exhibit.hall" class="info-item">
|
||||
<text class="info-label">展厅位置</text>
|
||||
<text class="info-value">{{ exhibit.hall }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="description-section">
|
||||
<text class="section-title">作品介绍</text>
|
||||
<text class="description-text">{{ exhibit.description }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="action-bar">
|
||||
<view class="action-btn-group">
|
||||
<view class="action-btn" @tap="handleNavigate">
|
||||
<text class="btn-icon">🗺️</text>
|
||||
<text class="btn-text">导航</text>
|
||||
</view>
|
||||
<view class="action-btn" @tap="handleCollect">
|
||||
<text class="btn-icon">{{ isCollected ? '❤️' : '🤍' }}</text>
|
||||
<text class="btn-text">收藏</text>
|
||||
</view>
|
||||
<view class="action-btn" @tap="handleShare">
|
||||
<text class="btn-icon">📤</text>
|
||||
<text class="btn-text">分享</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
|
||||
const exhibit = ref({
|
||||
id: '1',
|
||||
name: '蒙娜丽莎',
|
||||
artist: '列奥纳多·达·芬奇',
|
||||
year: '1503-1519',
|
||||
material: '木板油画',
|
||||
size: '77cm × 53cm',
|
||||
hall: '1号展厅 1F',
|
||||
image: '/static/exhibit-placeholder.jpg',
|
||||
description: '《蒙娜丽莎》是意大利文艺复兴时期画家列奥纳多·达·芬奇创作的油画,现收藏于法国卢浮宫博物馆。该画作主要表现了女性的典雅和恬静的典型形象,塑造了资本主义上升时期一位城市有产阶级的妇女形象。'
|
||||
})
|
||||
|
||||
const isPlaying = ref(false)
|
||||
const isCollected = ref(false)
|
||||
|
||||
onLoad((options: any) => {
|
||||
if (options.id) {
|
||||
// 根据 ID 加载展品数据
|
||||
console.log('加载展品:', options.id)
|
||||
}
|
||||
})
|
||||
|
||||
const handlePlayAudio = () => {
|
||||
isPlaying.value = !isPlaying.value
|
||||
console.log('播放/暂停音频')
|
||||
}
|
||||
|
||||
const handleNavigate = () => {
|
||||
console.log('导航到展品位置')
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
const handleCollect = () => {
|
||||
isCollected.value = !isCollected.value
|
||||
uni.showToast({
|
||||
title: isCollected.value ? '已收藏' : '已取消收藏',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleShare = () => {
|
||||
uni.showShareMenu()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: var(--museum-bg-light);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.exhibit-hero {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 450px;
|
||||
background-color: var(--museum-bg-map);
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.audio-control {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-color: var(--museum-accent);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.audio-control:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.audio-icon {
|
||||
font-size: 28px;
|
||||
color: var(--museum-text-primary);
|
||||
}
|
||||
|
||||
.exhibit-info {
|
||||
padding: 20px 16px 32px;
|
||||
background-color: var(--museum-bg-surface);
|
||||
}
|
||||
|
||||
.exhibit-title {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: var(--museum-text-primary);
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.exhibit-artist {
|
||||
font-size: 15px;
|
||||
color: var(--museum-text-secondary);
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px 16px;
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background-color: var(--museum-bg-light);
|
||||
border-radius: var(--radius-card);
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 12px;
|
||||
color: var(--museum-text-disabled);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
color: var(--museum-text-primary);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.description-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: var(--museum-text-primary);
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: var(--museum-text-secondary);
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
background-color: var(--museum-bg-surface);
|
||||
border-top: 1px solid var(--museum-border-light);
|
||||
padding: 8px 16px;
|
||||
padding-bottom: calc(8px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.action-btn-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 8px;
|
||||
background-color: var(--museum-bg-light);
|
||||
border-radius: var(--radius-button);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
background-color: var(--museum-border-light);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 11px;
|
||||
color: var(--museum-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
268
src/pages/facility/detail.vue
Normal file
268
src/pages/facility/detail.vue
Normal file
@@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<GuideMapShell
|
||||
:search-text="facility.name"
|
||||
floor-top="164px"
|
||||
tools-top="406px"
|
||||
:tools="['回正', '2D']"
|
||||
@search-tap="handleSearchTap"
|
||||
@mode-change="handleModeChange"
|
||||
@floor-change="handleFloorChange"
|
||||
@tool-click="handleToolClick"
|
||||
>
|
||||
<view class="detail-sheet">
|
||||
<view class="detail-title-row">
|
||||
<text class="detail-title">{{ facility.name }}</text>
|
||||
<text class="open-status">{{ facility.status }}</text>
|
||||
</view>
|
||||
|
||||
<view class="detail-lines">
|
||||
<text class="detail-line">所在区域:{{ facility.location }}</text>
|
||||
<text class="detail-line">最近垂直交通:{{ facility.traffic }}</text>
|
||||
</view>
|
||||
|
||||
<view class="tag-row">
|
||||
<view
|
||||
v-for="(tag, index) in facility.tags"
|
||||
:key="tag"
|
||||
class="detail-tag"
|
||||
:class="{ active: index === 0 }"
|
||||
>
|
||||
<text class="detail-tag-text">{{ tag }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="action-row">
|
||||
<view class="action-btn secondary" @tap="handleChooseStart">
|
||||
<text class="action-text">选择起点</text>
|
||||
</view>
|
||||
<view class="action-btn primary" @tap="handleStartNavigation">
|
||||
<text class="action-text">查看位置</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</GuideMapShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
|
||||
import {
|
||||
NAV_ROUTE_UNAVAILABLE_MESSAGE,
|
||||
formatNavFloorLabel,
|
||||
isPoiAccessible,
|
||||
loadCleanNavPoiById
|
||||
} from '@/services/navAssets'
|
||||
|
||||
const facility = ref({
|
||||
id: '',
|
||||
name: '目标位置',
|
||||
status: '可预览',
|
||||
location: 'clean 导览数据点位',
|
||||
traffic: NAV_ROUTE_UNAVAILABLE_MESSAGE,
|
||||
tags: ['三维位置', 'clean 数据']
|
||||
})
|
||||
|
||||
onLoad(async (options: any) => {
|
||||
if (options.id) {
|
||||
facility.value.id = options.id
|
||||
}
|
||||
|
||||
if (options.target) {
|
||||
facility.value.name = decodeURIComponent(options.target)
|
||||
}
|
||||
|
||||
if (!facility.value.id) return
|
||||
|
||||
try {
|
||||
const poi = await loadCleanNavPoiById(facility.value.id)
|
||||
if (!poi) return
|
||||
|
||||
const floor = formatNavFloorLabel(poi.floorId)
|
||||
facility.value = {
|
||||
id: poi.id,
|
||||
name: poi.name,
|
||||
status: '可预览',
|
||||
location: `${floor} · ${poi.primaryCategoryZh}`,
|
||||
traffic: NAV_ROUTE_UNAVAILABLE_MESSAGE,
|
||||
tags: [
|
||||
floor,
|
||||
poi.primaryCategoryZh,
|
||||
isPoiAccessible(poi) ? '无障碍相关' : '展示点位'
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载 clean 设施详情失败:', error)
|
||||
}
|
||||
})
|
||||
|
||||
const handleSearchTap = () => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/search/index?keyword=${encodeURIComponent(facility.value.name)}`
|
||||
})
|
||||
}
|
||||
|
||||
const handleChooseStart = () => {
|
||||
uni.showToast({
|
||||
title: '请选择当前位置',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleStartNavigation = () => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/route/detail?facilityId=${facility.value.id}&target=${encodeURIComponent(facility.value.name)}&state=planning`
|
||||
})
|
||||
}
|
||||
|
||||
const handleModeChange = (mode: '2d' | '3d') => {
|
||||
console.log('设施详情切换导览模式:', mode)
|
||||
}
|
||||
|
||||
const handleFloorChange = (floor: string) => {
|
||||
console.log('设施详情切换楼层:', floor)
|
||||
}
|
||||
|
||||
const handleToolClick = (tool: string) => {
|
||||
console.log('设施详情工具:', tool)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-sheet {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: calc(env(safe-area-inset-bottom) + 34px);
|
||||
height: 252px;
|
||||
padding: 22px 20px 20px;
|
||||
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;
|
||||
}
|
||||
|
||||
.detail-title-row {
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
color: #000000;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.open-status {
|
||||
width: 60px;
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
font-weight: 500;
|
||||
color: #1a7f37;
|
||||
}
|
||||
|
||||
.detail-lines {
|
||||
margin-top: 17px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-line {
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
color: #6b7178;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tag-row {
|
||||
margin-top: 21px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-tag {
|
||||
height: 28px;
|
||||
min-width: 88px;
|
||||
padding: 0 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.detail-tag.active {
|
||||
background: #000000;
|
||||
border-color: #000000;
|
||||
}
|
||||
|
||||
.detail-tag-text {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.detail-tag.active .detail-tag-text {
|
||||
color: var(--museum-accent);
|
||||
}
|
||||
|
||||
.action-row {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
width: 151px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
width: 168px;
|
||||
background: #000000;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 14px;
|
||||
line-height: 19px;
|
||||
font-weight: 500;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.action-btn.primary .action-text {
|
||||
color: var(--museum-accent);
|
||||
}
|
||||
</style>
|
||||
235
src/pages/hall/detail.vue
Normal file
235
src/pages/hall/detail.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<view class="detail-page">
|
||||
<scroll-view class="content" scroll-y>
|
||||
<!-- 展厅封面 -->
|
||||
<view class="hall-hero">
|
||||
<image class="hero-image" :src="hall.image" mode="aspectFill" />
|
||||
<view class="hall-badge">
|
||||
<text class="badge-text">{{ hall.floor }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 展厅信息 -->
|
||||
<view class="hall-info">
|
||||
<text class="hall-title">{{ hall.name }}</text>
|
||||
<text class="hall-description">{{ hall.description }}</text>
|
||||
|
||||
<view class="stats-row">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ hall.exhibitCount }}</text>
|
||||
<text class="stat-label">展品数量</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{ hall.area }}</text>
|
||||
<text class="stat-label">展厅面积</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 展品列表 -->
|
||||
<view class="exhibits-section">
|
||||
<text class="section-title">展厅展品</text>
|
||||
<view class="exhibits-grid">
|
||||
<ExhibitCard
|
||||
v-for="exhibit in exhibits"
|
||||
:key="exhibit.id"
|
||||
:exhibit="exhibit"
|
||||
@click="handleExhibitClick"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="action-bar">
|
||||
<view class="action-btn primary" @tap="handleNavigate">
|
||||
<text class="btn-text">导航到展厅</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import ExhibitCard from '@/components/content/ExhibitCard.vue'
|
||||
|
||||
const hall = ref({
|
||||
id: '1',
|
||||
name: '1号展厅',
|
||||
floor: '1F',
|
||||
description: '文艺复兴时期艺术作品展厅,展示了欧洲文艺复兴时期最具代表性的绘画和雕塑作品。',
|
||||
image: '/static/hall-placeholder.jpg',
|
||||
exhibitCount: 25,
|
||||
area: '500㎡'
|
||||
})
|
||||
|
||||
const exhibits = ref([
|
||||
{ id: '1', name: '蒙娜丽莎', artist: '达芬奇', hasAudio: true },
|
||||
{ id: '2', name: '最后的晚餐', artist: '达芬奇', hasAudio: true }
|
||||
])
|
||||
|
||||
onLoad((options: any) => {
|
||||
if (options.id) {
|
||||
console.log('加载展厅:', options.id)
|
||||
}
|
||||
})
|
||||
|
||||
const handleExhibitClick = (exhibit: any) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/exhibit/detail?id=${exhibit.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const handleNavigate = () => {
|
||||
console.log('导航到展厅')
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow-x: hidden;
|
||||
background-color: var(--museum-bg-light);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.hall-hero {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 240px;
|
||||
background-color: var(--museum-bg-map);
|
||||
}
|
||||
|
||||
.hero-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hall-badge {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: var(--blur-medium);
|
||||
padding: 6px 12px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
font-size: 14px;
|
||||
color: var(--museum-bg-surface);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hall-info {
|
||||
padding: 24px 16px;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--museum-bg-surface);
|
||||
}
|
||||
|
||||
.hall-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--museum-text-primary);
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hall-description {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--museum-text-secondary);
|
||||
margin-bottom: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--museum-bg-light);
|
||||
border-radius: var(--radius-card);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--museum-text-primary);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: var(--museum-text-disabled);
|
||||
}
|
||||
|
||||
.exhibits-section {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--museum-text-primary);
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.exhibits-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
background-color: var(--museum-bg-surface);
|
||||
border-top: 1px solid var(--museum-border-light);
|
||||
padding: 12px 16px;
|
||||
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--radius-button);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background-color: var(--museum-accent);
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--museum-text-primary);
|
||||
}
|
||||
</style>
|
||||
819
src/pages/index/index.vue
Normal file
819
src/pages/index/index.vue
Normal file
@@ -0,0 +1,819 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="content-tabs">
|
||||
<view
|
||||
v-for="tab in contentTabs"
|
||||
:key="tab.id"
|
||||
class="content-tab"
|
||||
:class="{ active: currentTab === tab.id }"
|
||||
@tap="handleTabChange(tab.id)"
|
||||
>
|
||||
<view v-if="currentTab === tab.id" class="content-tab-indicator"></view>
|
||||
<text class="content-tab-label">{{ tab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<GuideMapShell
|
||||
v-if="currentTab === 'guide'"
|
||||
:search-text="guideSearchText"
|
||||
:active-mode="guideShellMode"
|
||||
:mode-layout="'status'"
|
||||
:mode-status="guideStatusLabel"
|
||||
mode-status-tone="glass"
|
||||
search-top="60px"
|
||||
mode-top="104px"
|
||||
:map-type="guideMapType"
|
||||
:outdoor-variant="guideOutdoorVariant"
|
||||
:indoor-asset-base-url="indoorNavAssetBaseUrl"
|
||||
:show-floor="false"
|
||||
:tools="[]"
|
||||
@search-tap="handleGuideSearchTap"
|
||||
@mode-change="handleModeChange"
|
||||
>
|
||||
<template #overlay>
|
||||
<view v-if="guideOutdoorState === 'entrance' && !is3DMode" class="entrance-tip">
|
||||
<text class="entrance-tip-text">主入口 约 4 分钟</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-if="guideOutdoorState === 'home' && !is3DMode" class="home-shortcuts-card">
|
||||
<view
|
||||
v-for="item in shortcutItems"
|
||||
:key="item.id"
|
||||
class="home-shortcut-item"
|
||||
:class="{ active: selectedAreaId === item.id }"
|
||||
@tap="handleShortcutSelect(item)"
|
||||
>
|
||||
<view class="home-shortcut-icon">
|
||||
<text class="home-shortcut-abbr">{{ item.abbr }}</text>
|
||||
</view>
|
||||
<text class="home-shortcut-label">{{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-if="guideOutdoorState === 'home' || is3DMode"
|
||||
class="guide-task-card"
|
||||
:class="{ 'has-subtitle': guideTaskSubtitle }"
|
||||
>
|
||||
<text class="task-title">{{ guideTaskTitle }}</text>
|
||||
<text v-if="guideTaskSubtitle" class="task-subtitle">{{ guideTaskSubtitle }}</text>
|
||||
<view class="task-primary-btn" @tap="handleGuidePrimaryAction">
|
||||
<text class="task-primary-text">{{ guidePrimaryAction }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="guideOutdoorState === 'entrance' && !is3DMode" class="entrance-bottom-card">
|
||||
<text class="entrance-card-title">主入口</text>
|
||||
<text class="entrance-card-desc">
|
||||
可到达入口后自动切换,也可手动进入室内3D并选择楼层/区域。
|
||||
</text>
|
||||
<view class="entrance-actions">
|
||||
<view class="entrance-btn secondary" @tap="handleSwitchEntrance">
|
||||
<text class="entrance-btn-text">切换入口</text>
|
||||
</view>
|
||||
<view class="entrance-btn primary" @tap="handleEnter3DMode">
|
||||
<text class="entrance-btn-text">进入室内3D</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</GuideMapShell>
|
||||
|
||||
<view v-else-if="currentTab === 'explain'" class="explain-page">
|
||||
<ExplainList
|
||||
@exhibit-click="handleExplainExhibitClick"
|
||||
@featured-click="handleExplainFeaturedClick"
|
||||
@audio-click="handleAudioClick"
|
||||
/>
|
||||
|
||||
<FloatingAudioButton
|
||||
:is-playing="isAudioPlaying"
|
||||
@click="handleFloatingButtonClick"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 音频播放器 -->
|
||||
<AudioPlayer
|
||||
:visible="showAudioPlayer"
|
||||
:audio="currentAudio"
|
||||
:auto-play="true"
|
||||
@update:visible="showAudioPlayer = $event"
|
||||
@play="handleAudioPlay"
|
||||
@pause="handleAudioPause"
|
||||
@ended="handleAudioEnded"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
|
||||
import ExplainList from '@/components/explain/ExplainList.vue'
|
||||
import FloatingAudioButton from '@/components/audio/FloatingAudioButton.vue'
|
||||
import AudioPlayer, { type AudioItem } from '@/components/audio/AudioPlayer.vue'
|
||||
import {
|
||||
NAV_ASSET_BASE_URL,
|
||||
NAV_ROUTE_UNAVAILABLE_MESSAGE
|
||||
} from '@/services/navAssets'
|
||||
|
||||
// 3D 模式状态
|
||||
const is3DMode = ref(false)
|
||||
const guideOutdoorState = ref<'home' | 'entrance'>('home')
|
||||
|
||||
// 状态
|
||||
const searchKeyword = ref('')
|
||||
const currentTab = ref('guide')
|
||||
const searchBarFocused = ref(false)
|
||||
const showMarkerDetail = ref(false)
|
||||
const showAreaSelector = ref(false)
|
||||
const selectedAreaId = ref('service')
|
||||
|
||||
const contentTabs = [
|
||||
{ id: 'guide', label: '导览' },
|
||||
{ id: 'explain', label: '讲解' }
|
||||
]
|
||||
|
||||
const indoorNavAssetBaseUrl = NAV_ASSET_BASE_URL
|
||||
|
||||
const shortcutItems = [
|
||||
{ id: 'toilet', name: '卫生间', abbr: '卫' },
|
||||
{ id: 'elevator', name: '电梯', abbr: '电' },
|
||||
{ id: 'service', name: '服务台', abbr: '服' },
|
||||
{ id: 'entrance', name: '入口', abbr: '入' },
|
||||
{ id: 'accessible', name: '无障碍', abbr: '无' }
|
||||
]
|
||||
|
||||
// 音频播放器状态
|
||||
const showAudioPlayer = ref(false)
|
||||
const isAudioPlaying = ref(false)
|
||||
const currentAudio = ref<AudioItem | null>(null)
|
||||
|
||||
// 选中的标记详情
|
||||
interface MarkerDetail {
|
||||
id: string | number
|
||||
name: string
|
||||
subtitle?: string
|
||||
floor?: string
|
||||
hall?: string
|
||||
exhibitCount?: number
|
||||
image?: string
|
||||
type: 'exhibit' | 'hall' | 'facility'
|
||||
audioUrl?: string
|
||||
}
|
||||
|
||||
const selectedMarker = ref<MarkerDetail | null>(null)
|
||||
|
||||
// 地图标记数据 (与 TencentMap.vue 中的 markers 对应)
|
||||
const markerDataMap: Record<string, MarkerDetail> = {
|
||||
0: { id: 0, name: '深圳自然博物馆', type: 'hall', floor: '全馆' },
|
||||
1: { id: 1, name: '主入口', type: 'facility', floor: '1F' },
|
||||
'entrance-1': { id: 'entrance-1', name: '主入口', type: 'facility', floor: '1F' }
|
||||
}
|
||||
|
||||
// Tab 切换处理
|
||||
const handleTabChange = (tabId: string) => {
|
||||
currentTab.value = tabId
|
||||
console.log('切换标签:', tabId)
|
||||
|
||||
// 关闭可能打开的面板
|
||||
closeAreaSelector()
|
||||
closeMarkerDetail()
|
||||
searchBarFocused.value = false
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearchFocus = () => {
|
||||
searchBarFocused.value = true
|
||||
}
|
||||
|
||||
const handleSearchBlur = () => {
|
||||
searchBarFocused.value = false
|
||||
}
|
||||
|
||||
const handleSearch = (keyword: string) => {
|
||||
console.log('搜索:', keyword)
|
||||
}
|
||||
|
||||
const handleGuideSearchTap = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/search/index'
|
||||
})
|
||||
}
|
||||
|
||||
const closeSearchPanel = () => {
|
||||
searchBarFocused.value = false
|
||||
showAreaSelector.value = false
|
||||
}
|
||||
|
||||
// 区域选择处理
|
||||
const handleAreaClick = (area: any) => {
|
||||
console.log('选择区域:', area)
|
||||
}
|
||||
|
||||
const handleShortcutSelect = (area: { id: string; name: string; abbr: string }) => {
|
||||
selectedAreaId.value = area.id
|
||||
uni.navigateTo({
|
||||
url: `/pages/search/index?keyword=${encodeURIComponent(area.name)}`
|
||||
})
|
||||
}
|
||||
|
||||
const handleAreaChange = (areaId: string) => {
|
||||
console.log('切换设施快捷入口:', areaId)
|
||||
}
|
||||
|
||||
const handleAreaFloorChange = (floor: string) => {
|
||||
console.log('切换楼层:', floor)
|
||||
}
|
||||
|
||||
const closeAreaSelector = () => {
|
||||
showAreaSelector.value = false
|
||||
searchBarFocused.value = false
|
||||
}
|
||||
|
||||
// 地图标记处理
|
||||
const handleMarkerClick = (markerId: number) => {
|
||||
console.log('点击地图标记:', markerId)
|
||||
|
||||
// 获取标记详情数据
|
||||
const markerInfo = markerDataMap[markerId]
|
||||
if (markerInfo) {
|
||||
selectedMarker.value = markerInfo
|
||||
showMarkerDetail.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const closeMarkerDetail = () => {
|
||||
showMarkerDetail.value = false
|
||||
selectedMarker.value = null
|
||||
}
|
||||
|
||||
const handleNavigateToMarker = (marker: MarkerDetail) => {
|
||||
console.log('导航到标记位置:', marker)
|
||||
uni.showToast({
|
||||
title: NAV_ROUTE_UNAVAILABLE_MESSAGE,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleViewMarkerDetail = (marker: MarkerDetail) => {
|
||||
console.log('查看标记详情:', marker)
|
||||
|
||||
if (marker.type === 'exhibit') {
|
||||
uni.navigateTo({
|
||||
url: `/pages/exhibit/detail?id=${marker.id}`
|
||||
})
|
||||
} else if (marker.type === 'hall') {
|
||||
uni.navigateTo({
|
||||
url: `/pages/hall/detail?id=${marker.id}`
|
||||
})
|
||||
}
|
||||
|
||||
closeMarkerDetail()
|
||||
}
|
||||
|
||||
const handlePlayMarkerAudio = (marker: MarkerDetail) => {
|
||||
console.log('播放标记音频:', marker)
|
||||
uni.showToast({
|
||||
title: '开始播放讲解',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleCollectMarker = (marker: MarkerDetail) => {
|
||||
console.log('收藏标记:', marker)
|
||||
}
|
||||
|
||||
const handleFloorChange = (floor: string) => {
|
||||
console.log('切换楼层:', floor)
|
||||
}
|
||||
|
||||
// 进入 3D 室内模式
|
||||
const handleEnter3DMode = () => {
|
||||
console.log('进入 3D 室内模式')
|
||||
is3DMode.value = true
|
||||
guideOutdoorState.value = 'home'
|
||||
}
|
||||
|
||||
const handleModeChange = (mode: '2d' | '3d') => {
|
||||
is3DMode.value = mode === '3d'
|
||||
|
||||
if (mode === '2d') {
|
||||
guideOutdoorState.value = 'home'
|
||||
}
|
||||
}
|
||||
|
||||
const guideStatusLabel = computed(() => {
|
||||
if (is3DMode.value) {
|
||||
return '馆内定位'
|
||||
}
|
||||
|
||||
if (guideOutdoorState.value === 'entrance') {
|
||||
return '推荐入口'
|
||||
}
|
||||
|
||||
return '馆外定位'
|
||||
})
|
||||
|
||||
const guideTaskTitle = computed(() => {
|
||||
if (is3DMode.value) {
|
||||
return '规划馆内路线'
|
||||
}
|
||||
return '从室外进入馆内'
|
||||
})
|
||||
|
||||
const guideTaskSubtitle = computed(() => {
|
||||
if (is3DMode.value) {
|
||||
return '选择目标设施后,系统会优先推荐最近路径'
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
|
||||
const guidePrimaryAction = computed(() => {
|
||||
if (is3DMode.value) {
|
||||
return '选择目标地点'
|
||||
}
|
||||
|
||||
return '查看推荐入口'
|
||||
})
|
||||
|
||||
const handleGuidePrimaryAction = () => {
|
||||
if (is3DMode.value) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/search/index'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
guideOutdoorState.value = 'entrance'
|
||||
}
|
||||
|
||||
const handleSwitchEntrance = () => {
|
||||
guideOutdoorState.value = 'home'
|
||||
}
|
||||
|
||||
const guideShellMode = computed(() => (is3DMode.value ? '3d' : '2d'))
|
||||
const guideMapType = computed(() => (is3DMode.value ? 'indoor' : 'outdoor'))
|
||||
const guideOutdoorVariant = computed(() => (
|
||||
guideOutdoorState.value === 'entrance' ? 'entrance' : 'home'
|
||||
))
|
||||
const guideSearchText = computed(() => {
|
||||
if (is3DMode.value) {
|
||||
return '搜索设施、展厅、入口'
|
||||
}
|
||||
|
||||
if (guideOutdoorState.value === 'entrance') {
|
||||
return '选择入口或停车位置'
|
||||
}
|
||||
|
||||
return '搜索设施、展厅、入口'
|
||||
})
|
||||
|
||||
// 音频相关处理
|
||||
const handleAudioClick = (exhibit: any) => {
|
||||
console.log('点击音频播放:', exhibit)
|
||||
currentAudio.value = {
|
||||
id: exhibit.id,
|
||||
name: exhibit.name,
|
||||
audioUrl: exhibit.audioUrl || 'https://example.com/audio.mp3',
|
||||
image: exhibit.image
|
||||
}
|
||||
showAudioPlayer.value = true
|
||||
}
|
||||
|
||||
const handleFloatingButtonClick = () => {
|
||||
if (showAudioPlayer.value) {
|
||||
showAudioPlayer.value = false
|
||||
} else if (currentAudio.value) {
|
||||
showAudioPlayer.value = true
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '请先选择一个展品',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleAudioPlay = (audio: AudioItem) => {
|
||||
console.log('开始播放:', audio)
|
||||
isAudioPlaying.value = true
|
||||
}
|
||||
|
||||
const handleAudioPause = (audio: AudioItem) => {
|
||||
console.log('暂停播放:', audio)
|
||||
isAudioPlaying.value = false
|
||||
}
|
||||
|
||||
const handleAudioEnded = (audio: AudioItem) => {
|
||||
console.log('播放结束:', audio)
|
||||
isAudioPlaying.value = false
|
||||
}
|
||||
|
||||
// 讲解页面处理
|
||||
const handleExplainExhibitClick = (exhibit: any) => {
|
||||
console.log('点击讲解展品:', exhibit)
|
||||
uni.navigateTo({
|
||||
url: `/pages/exhibit/detail?id=${exhibit.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const handleExplainFeaturedClick = (exhibit: any) => {
|
||||
console.log('点击推荐讲解:', exhibit)
|
||||
// 播放音频讲解
|
||||
if (exhibit.hasAudio) {
|
||||
currentAudio.value = {
|
||||
id: exhibit.id,
|
||||
name: exhibit.name,
|
||||
audioUrl: exhibit.audioUrl || 'https://example.com/audio.mp3',
|
||||
image: exhibit.image
|
||||
}
|
||||
showAudioPlayer.value = true
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '该展品暂无讲解音频',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
background: var(--museum-bg-map);
|
||||
}
|
||||
|
||||
.explain-page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #f6f7f4;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.entrance-tip {
|
||||
position: absolute;
|
||||
top: 154px;
|
||||
left: 30px;
|
||||
width: 168px;
|
||||
height: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border: 1px solid #e5e6de;
|
||||
border-radius: 12px;
|
||||
z-index: 45;
|
||||
}
|
||||
|
||||
.entrance-tip-text {
|
||||
font-size: 14px;
|
||||
line-height: 19px;
|
||||
font-weight: 500;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.home-shortcuts-card {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
bottom: calc(env(safe-area-inset-bottom) + 134px);
|
||||
height: 84px;
|
||||
padding: 16px 10px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border: 1px solid #ffffff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
|
||||
z-index: 45;
|
||||
}
|
||||
|
||||
.home-shortcut-item {
|
||||
width: 62px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.home-shortcut-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border: 1px solid #d9dcd2;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.home-shortcut-abbr {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
font-weight: 700;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.home-shortcut-label {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: #1f2329;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.home-shortcut-item.active {
|
||||
.home-shortcut-icon {
|
||||
background: #000000;
|
||||
border-color: #000000;
|
||||
}
|
||||
|
||||
.home-shortcut-abbr {
|
||||
color: var(--museum-accent);
|
||||
}
|
||||
}
|
||||
|
||||
.content-tabs {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border-bottom: 1px solid #eceee8;
|
||||
box-sizing: border-box;
|
||||
z-index: 2000;
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.content-tab {
|
||||
position: relative;
|
||||
width: 75px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content-tab-label {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.content-tab.active .content-tab-label {
|
||||
font-weight: 500;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.content-tab-indicator {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 26px;
|
||||
width: 39px;
|
||||
height: 6px;
|
||||
background: var(--museum-accent);
|
||||
border-radius: 3px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.search-bar-wrapper {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1003;
|
||||
}
|
||||
|
||||
.guide-mode-row {
|
||||
position: absolute;
|
||||
top: 104px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 1003;
|
||||
}
|
||||
|
||||
.mode-switch {
|
||||
width: 220px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px solid rgba(230, 230, 230, 0.5);
|
||||
border-radius: 17px;
|
||||
}
|
||||
|
||||
.mode-option {
|
||||
width: 106px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 13px;
|
||||
}
|
||||
|
||||
.mode-option.active {
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
.mode-label {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.mode-option.active .mode-label {
|
||||
color: var(--museum-accent);
|
||||
}
|
||||
|
||||
.mode-status-chip {
|
||||
width: 111px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.42);
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.mode-status-text {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
color: #696962;
|
||||
}
|
||||
|
||||
.area-selector-wrapper {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
bottom: calc(env(safe-area-inset-bottom) + 134px);
|
||||
z-index: 1003;
|
||||
}
|
||||
|
||||
.guide-task-card {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
bottom: calc(env(safe-area-inset-bottom) + 30px);
|
||||
height: 94px;
|
||||
padding: 16px 16px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border: 1px solid #ffffff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
|
||||
z-index: 1003;
|
||||
}
|
||||
|
||||
.guide-task-card.has-subtitle {
|
||||
height: auto;
|
||||
min-height: 128px;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
flex-shrink: 0;
|
||||
font-size: 17px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.task-subtitle {
|
||||
margin-top: 4px;
|
||||
min-height: 20px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #696962;
|
||||
}
|
||||
|
||||
.task-primary-btn {
|
||||
flex-shrink: 0;
|
||||
margin-top: 4px;
|
||||
height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #000000;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.task-primary-text {
|
||||
font-size: 14px;
|
||||
line-height: 19px;
|
||||
font-weight: 500;
|
||||
color: var(--museum-accent);
|
||||
}
|
||||
|
||||
.entrance-bottom-card {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
bottom: calc(env(safe-area-inset-bottom) + 34px);
|
||||
height: 158px;
|
||||
padding: 18px 16px 14px;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border: 1px solid #ffffff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
|
||||
z-index: 45;
|
||||
}
|
||||
|
||||
.entrance-card-title {
|
||||
display: block;
|
||||
font-size: 17px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.entrance-card-desc {
|
||||
display: block;
|
||||
width: 305px;
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
color: #6b7178;
|
||||
}
|
||||
|
||||
.entrance-actions {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
bottom: 14px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.entrance-btn {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.entrance-btn.secondary {
|
||||
width: 126px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.entrance-btn.primary {
|
||||
width: 181px;
|
||||
background: #000000;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
.entrance-btn-text {
|
||||
font-size: 14px;
|
||||
line-height: 19px;
|
||||
font-weight: 500;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.entrance-btn.primary .entrance-btn-text {
|
||||
color: var(--museum-accent);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.page-container {
|
||||
max-width: 430px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 12px 36px rgba(36, 49, 42, 0.16);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1098
src/pages/route/detail.vue
Normal file
1098
src/pages/route/detail.vue
Normal file
File diff suppressed because it is too large
Load Diff
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