Files
gmTouringMiniApp/src/pages/poi/detail.vue
T

288 lines
6.8 KiB
Vue
Raw Normal View History

2026-07-30 16:04:34 +08:00
<script setup lang="ts">
import type { PoiResolved } from '@/domain/poi'
import PageState from '@/components/poi/PageState.vue'
import PoiHeroGallery from '@/components/poi/PoiHeroGallery.vue'
import PoiTagList from '@/components/poi/PoiTagList.vue'
import { getPoiRepository } from '@/data/poi'
import { formatRecommendationLabel } from '@/domain/poi'
const state = ref<'loading' | 'ready' | 'invalid' | 'error'>('loading')
const poi = ref<PoiResolved | null>(null)
function loadPoi(poiId: string) {
state.value = 'loading'
poi.value = null
try {
if (!poiId.trim()) {
state.value = 'invalid'
return
}
const result = getPoiRepository().getPoiById(poiId)
if (!result) {
state.value = 'invalid'
return
}
poi.value = result
state.value = 'ready'
}
catch (error) {
console.error('Failed to load POI detail', error)
state.value = 'error'
}
}
function returnToMap() {
uni.navigateBack({
fail: () => {
uni.reLaunch({ url: '/pages/map/index' })
},
})
}
function formatCoordinate(value: number): string {
return value.toFixed(6)
}
function formatUpdatedAt(value: string): string {
const date = new Date(value)
if (Number.isNaN(date.getTime()))
return '待确认'
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
onLoad((query) => {
const rawPoiId = typeof query?.poiId === 'string' ? query.poiId : ''
try {
loadPoi(decodeURIComponent(rawPoiId))
}
catch {
state.value = 'invalid'
}
})
</script>
<template>
<view class="poi-detail-page">
<PageState v-if="state === 'loading'" state="loading" title="正在加载点位信息…" />
<PageState
v-else-if="state === 'invalid'"
state="empty"
title="点位不存在或已下线"
description="该点位可能已更新,请返回地图重新选择"
action-label="返回地图"
@action="returnToMap"
/>
<PageState
v-else-if="state === 'error'"
state="error"
title="点位信息加载失败"
description="暂时无法读取该点位,请稍后返回重试"
action-label="返回地图"
@action="returnToMap"
/>
<template v-else-if="poi">
<PoiHeroGallery :images="poi.images" :name="poi.name" />
<view class="poi-detail-page__content">
<view class="poi-detail-page__header">
<text class="poi-detail-page__category">{{ poi.category.name }}</text>
<text class="poi-detail-page__name">{{ poi.name }}</text>
<view class="poi-detail-page__recommendation">
{{ formatRecommendationLabel(poi.recommendationSource, poi.recommendationIndex) }}
</view>
<PoiTagList class="poi-detail-page__tags" :tags="poi.tags" />
</view>
<view class="info-card">
<view class="info-row">
<text class="info-row__label">开放时间</text>
<text class="info-row__value">{{ poi.openingHoursText }}</text>
</view>
<view class="info-row">
<text class="info-row__label">地址</text>
<text class="info-row__value">{{ poi.address }}</text>
</view>
<view class="info-row info-row--coordinates">
<text class="info-row__label">经纬度</text>
<view class="info-row__value info-row__coordinates">
<text>经度 {{ formatCoordinate(poi.longitude) }}</text>
<text>纬度 {{ formatCoordinate(poi.latitude) }}</text>
<text class="info-row__coordinate-system">坐标系 {{ poi.coordinateSystem }}</text>
</view>
</view>
<view class="info-row">
<text class="info-row__label">数据更新</text>
<text class="info-row__value">{{ formatUpdatedAt(poi.updatedAt) }}</text>
</view>
</view>
<view class="description-section">
<text class="description-section__title">点位简介</text>
<text class="description-section__body">{{ poi.description }}</text>
</view>
<view class="poc-notice">
<text class="poc-notice__title">POC 数据说明</text>
<text class="poc-notice__body">当前内容用于产品和技术可行性验证正式开放信息请以后续审核版本为准</text>
</view>
</view>
</template>
</view>
</template>
<route lang="yaml">
layout: map
style:
navigationBarTitleText: 点位详情
navigationBarBackgroundColor: '#ffffff'
navigationBarTextStyle: black
backgroundColor: '#f5f7f6'
</route>
<style scoped lang="scss">
.poi-detail-page {
min-height: 100vh;
background: #f5f7f6;
}
.poi-detail-page__content {
padding: 32rpx 32rpx calc(32rpx + env(safe-area-inset-bottom));
}
.poi-detail-page__header {
display: flex;
flex-direction: column;
}
.poi-detail-page__category {
align-self: flex-start;
padding: 8rpx 18rpx;
font-size: 24rpx;
font-weight: 600;
line-height: 34rpx;
color: #0e6247;
background: #e8f4ee;
border-radius: 24rpx;
}
.poi-detail-page__name {
margin-top: 16rpx;
font-size: 40rpx;
font-weight: 700;
line-height: 56rpx;
color: #18201d;
}
.poi-detail-page__recommendation {
align-self: flex-start;
padding: 8rpx 16rpx;
margin-top: 16rpx;
font-size: 26rpx;
font-weight: 600;
line-height: 38rpx;
color: #934b00;
background: #fff3e0;
border-radius: 10rpx;
}
.poi-detail-page__tags {
margin-top: 18rpx;
}
.info-card,
.description-section,
.poc-notice {
padding: 28rpx;
margin-top: 24rpx;
background: #fff;
border: 1rpx solid #dde5e1;
border-radius: 20rpx;
}
.info-row {
display: flex;
gap: 24rpx;
align-items: flex-start;
padding: 20rpx 0;
border-bottom: 1rpx solid #edf1ef;
}
.info-row:first-child {
padding-top: 0;
}
.info-row:last-child {
padding-bottom: 0;
border-bottom: 0;
}
.info-row__label {
flex: none;
width: 128rpx;
font-size: 26rpx;
line-height: 40rpx;
color: #5e6b65;
}
.info-row__value {
flex: 1;
font-size: 28rpx;
line-height: 40rpx;
color: #18201d;
}
.info-row__coordinates {
display: flex;
flex-direction: column;
min-width: 0;
}
.info-row__coordinate-system {
margin-top: 6rpx;
font-size: 24rpx;
color: #8a9690;
}
.description-section__title,
.poc-notice__title {
display: block;
font-size: 30rpx;
font-weight: 700;
line-height: 42rpx;
color: #18201d;
}
.description-section__body,
.poc-notice__body {
display: block;
margin-top: 16rpx;
font-size: 28rpx;
line-height: 44rpx;
color: #425049;
white-space: pre-wrap;
}
.poc-notice {
background: #e8f4ee;
border-color: #cfe4d9;
}
.poc-notice__title {
font-size: 26rpx;
color: #0e6247;
}
.poc-notice__body {
font-size: 24rpx;
line-height: 38rpx;
color: #3d5a4d;
}
</style>