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

396 lines
9.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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'
import { getCheckInProfile } from '@/services/check-in'
const state = ref<'loading' | 'ready' | 'invalid' | 'error'>('loading')
const poi = ref<PoiResolved | null>(null)
const hasCheckedIn = ref(false)
const checkInStorageError = ref(false)
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
try {
hasCheckedIn.value = getCheckInProfile().checkIns.some(record => record.poiId === result.id)
checkInStorageError.value = false
}
catch (error) {
console.error('Failed to load POI check-in state', error)
hasCheckedIn.value = false
checkInStorageError.value = true
}
state.value = 'ready'
}
catch (error) {
console.error('Failed to load POI detail', error)
state.value = 'error'
}
}
function openCheckIn() {
if (!poi.value)
return
uni.navigateTo({ url: `/pages/check-in/index?poiId=${encodeURIComponent(poi.value.id)}` })
}
function openCheckInRecords() {
uni.navigateTo({ url: '/pages/check-in/records' })
}
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'
}
})
onShow(() => {
if (!poi.value)
return
try {
hasCheckedIn.value = getCheckInProfile().checkIns.some(record => record.poiId === poi.value?.id)
checkInStorageError.value = false
}
catch (error) {
console.error('Failed to refresh POI check-in state', error)
hasCheckedIn.value = false
checkInStorageError.value = true
}
})
</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="check-in-entry" :class="{ 'check-in-entry--done': hasCheckedIn }">
<view class="check-in-entry__mark">{{ hasCheckedIn ? '✓' : '◎' }}</view>
<view class="check-in-entry__copy">
<text class="check-in-entry__title">{{ checkInStorageError ? '打卡数据需要处理' : hasCheckedIn ? '已留下光明足迹' : '到点打卡' }}</text>
<text>{{ checkInStorageError ? '本机打卡数据异常,请先处理' : hasCheckedIn ? '该点位已在本机完成打卡' : '到达点位附近后,使用本次真实定位打卡' }}</text>
</view>
<button @click="checkInStorageError ? openCheckInRecords() : openCheckIn()">{{ checkInStorageError ? '去处理' : hasCheckedIn ? '查看' : '去打卡' }}</button>
</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,
.check-in-entry,
.description-section,
.poc-notice {
padding: 28rpx;
margin-top: 24rpx;
background: #fff;
border: 1rpx solid #dde5e1;
border-radius: 20rpx;
}
.check-in-entry {
display: flex;
gap: 18rpx;
align-items: center;
padding: 24rpx;
}
.check-in-entry--done {
background: #e8f4ee;
border-color: #cfe4d9;
}
.check-in-entry__mark {
display: flex;
flex: none;
align-items: center;
justify-content: center;
width: 62rpx;
height: 62rpx;
font-size: 28rpx;
font-weight: 800;
color: #fff;
background: #167a5b;
border-radius: 50%;
}
.check-in-entry__copy {
display: flex;
flex: 1;
flex-direction: column;
min-width: 0;
font-size: 21rpx;
line-height: 32rpx;
color: #5e6b65;
}
.check-in-entry__title {
margin-bottom: 3rpx;
font-size: 27rpx;
font-weight: 750;
color: #18201d;
}
.check-in-entry button {
flex: none;
width: auto;
height: 60rpx;
padding: 0 22rpx;
margin: 0;
font-size: 23rpx;
font-weight: 700;
line-height: 60rpx;
color: #fff;
background: #167a5b;
border: 0;
border-radius: 30rpx;
}
.check-in-entry button::after {
border: 0;
}
.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>