Add check-in feature and update travel planner UI

This commit is contained in:
周瑞哲
2026-07-31 12:50:14 +08:00
parent ac4179086c
commit 3ef7266591
44 changed files with 2919 additions and 144 deletions
+127 -9
View File
@@ -1,11 +1,13 @@
<script setup lang="ts">
import type { PoiCategory, PoiDatasetMeta, PoiSummary } from '@/domain/poi'
import type { PlanResponse } from '@/domain/travel'
import type { PoiMapMarker } from '@/services/map'
import MapFilterHeader from '@/components/map/MapFilterHeader.vue'
import PoiSummaryCard from '@/components/map/PoiSummaryCard.vue'
import PageState from '@/components/poi/PageState.vue'
import { getPoiRepository } from '@/data/poi'
import { buildMarkerIdMap, createPoiMarkers } from '@/services/map'
import { getSessionPlanningOrigin, loadPlan } from '@/services/travel-assistant'
import { useLocationStore, useMapStore } from '@/stores'
const MAP_ID = 'guangming-cultural-map'
@@ -18,6 +20,7 @@ const mapError = ref(false)
const categories = ref<PoiCategory[]>([])
const allPoiSummaries = ref<PoiSummary[]>([])
const datasetMeta = ref<PoiDatasetMeta | null>(null)
const activePlan = shallowRef<PlanResponse | null>(null)
const mapContext = shallowRef<ReturnType<typeof uni.createMapContext> | null>(null)
const markerIdToPoiId = shallowRef(new Map<number, string>())
const lastMarkerTapAt = ref(0)
@@ -25,6 +28,8 @@ const navigating = ref(false)
let includePointsRequestVersion = 0
let locationListenerAttached = false
let locationUpdatesStarted = false
let locationUpdatesStarting = false
let locationUpdatesRequested = false
let locationRequestVersion = 0
const activeCategoryCode = computed(() => mapStore.categoryCode)
@@ -50,16 +55,84 @@ const selectedPoi = computed(() => {
return null
return allPoiSummaries.value.find(poi => poi.id === selectedPoiId.value) ?? null
})
const markers = computed<PoiMapMarker[]>(() => createPoiMarkers(
filteredPois.value,
categories.value,
markerIdToPoiId.value,
selectedPoiId.value,
))
const markers = computed<PoiMapMarker[]>(() => {
const baseMarkers = createPoiMarkers(
filteredPois.value,
categories.value,
markerIdToPoiId.value,
selectedPoiId.value,
)
if (!activePlan.value)
return baseMarkers
const routeOrder = new Map(activePlan.value.itinerary.items.map((item, index) => [item.placeId, index + 1]))
return baseMarkers.map((marker) => {
const poiId = markerIdToPoiId.value.get(marker.id)
const order = poiId ? routeOrder.get(poiId) : undefined
if (!order)
return marker
return {
...marker,
callout: {
...marker.callout,
content: selectedPoiId.value === poiId ? `${order} · ${marker.callout.content}` : `路线 ${order}`,
color: '#ffffff',
bgColor: '#167a5b',
display: 'ALWAYS',
},
zIndex: Math.max(marker.zIndex, 8),
}
})
})
const mapPoints = computed<Array<{ latitude: number, longitude: number }>>(() => filteredPois.value.map(poi => ({
latitude: poi.latitude,
longitude: poi.longitude,
})))
const plannedPois = computed(() => {
if (!activePlan.value)
return []
const repository = getPoiRepository()
return activePlan.value.itinerary.items.flatMap((item) => {
const poi = repository.getPoiById(item.placeId)
return poi ? [poi] : []
})
})
const plannedOrigin = computed(() => activePlan.value
? getSessionPlanningOrigin(activePlan.value.conversationId)
: null)
const plannedRoutePoints = computed(() => [
...(activePlan.value?.itinerary.startsFromCurrentLocation && plannedOrigin.value
? [{ latitude: plannedOrigin.value.latitude, longitude: plannedOrigin.value.longitude }]
: []),
...plannedPois.value.map(poi => ({ latitude: poi.latitude, longitude: poi.longitude })),
])
const plannedRoutePolyline = computed(() => plannedRoutePoints.value.length > 1
? [{
points: plannedRoutePoints.value,
color: '#167a5bcc',
width: 6,
dottedLine: false,
arrowLine: true,
borderColor: '#ffffffcc',
borderWidth: 2,
}]
: [])
function loadActivePlan() {
activePlan.value = mapStore.plannedRouteVisible ? loadPlan() : null
}
function includePlannedRoute() {
const context = mapContext.value
if (!context || !mapReady.value || plannedRoutePoints.value.length === 0)
return
nextTick(() => {
context.includePoints({
points: plannedRoutePoints.value,
padding: [uni.upx2px(70), uni.upx2px(70), uni.upx2px(150), uni.upx2px(70)],
})
})
}
function loadDataset() {
pageState.value = 'loading'
@@ -161,6 +234,8 @@ function selectCategory(categoryCode: string | null) {
return
mapStore.setCategory(categoryCode)
mapStore.hidePlannedRoute()
activePlan.value = null
if (selectedPoi.value && categoryCode && selectedPoi.value.categoryCode !== categoryCode)
mapStore.selectPoi(null)
@@ -244,6 +319,8 @@ function updateViewportWithScale(longitude: number, latitude: number, fallbackSc
function resetToAll() {
mapStore.setCategory(null)
mapStore.selectPoi(null)
mapStore.hidePlannedRoute()
activePlan.value = null
const target = datasetMeta.value?.defaultViewport
if (target)
mapStore.resetViewport(target)
@@ -279,16 +356,24 @@ function detachLocationListener() {
}
function startForegroundLocationUpdates() {
if (locationUpdatesStarted)
if (locationUpdatesStarted || locationUpdatesStarting || !locationUpdatesRequested)
return
locationUpdatesStarting = true
attachLocationListener()
uni.startLocationUpdate({
type: 'gcj02',
success: () => {
locationUpdatesStarting = false
if (!locationUpdatesRequested) {
uni.stopLocationUpdate()
detachLocationListener()
return
}
locationUpdatesStarted = true
},
fail: (error) => {
locationUpdatesStarting = false
detachLocationListener()
console.error('Failed to start foreground location updates', error)
},
@@ -296,6 +381,7 @@ function startForegroundLocationUpdates() {
}
function stopForegroundLocationUpdates() {
locationUpdatesRequested = false
locationRequestVersion += 1
detachLocationListener()
if (locationUpdatesStarted) {
@@ -324,6 +410,7 @@ function requestUserLocation() {
uni.openSetting({
success: (settings) => {
if (settings.authSetting['scope.userLocation']) {
locationUpdatesRequested = true
locationStore.startLocating()
requestCurrentLocation()
}
@@ -332,11 +419,13 @@ function requestUserLocation() {
return
}
if (locationReady.value) {
locationUpdatesRequested = true
centerOnUserLocation()
startForegroundLocationUpdates()
return
}
locationUpdatesRequested = true
locationStore.startLocating()
requestCurrentLocation()
}
@@ -395,6 +484,10 @@ function openDetail(poiId: string) {
})
}
function openCheckInRecords() {
uni.navigateTo({ url: '/pages/check-in/records' })
}
function initializeMap() {
includePointsRequestVersion += 1
mapReady.value = false
@@ -404,7 +497,10 @@ function initializeMap() {
try {
mapContext.value = uni.createMapContext(MAP_ID)
mapReady.value = true
includeFilteredPoints()
if (activePlan.value)
includePlannedRoute()
else
includeFilteredPoints()
}
catch (error) {
console.error('Failed to initialize map', error)
@@ -431,7 +527,10 @@ onReady(() => {
})
onShow(() => {
if (locationReady.value)
loadActivePlan()
if (activePlan.value)
includePlannedRoute()
if (locationReady.value && locationUpdatesRequested)
startForegroundLocationUpdates()
})
@@ -447,6 +546,7 @@ onUnload(stopForegroundLocationUpdates)
:result-count="filteredPois.length"
:coverage-label="datasetMeta?.coverageLabel ?? 'POC 示例数据'"
:loading="pageState === 'loading'"
@check-ins="openCheckInRecords"
@select="selectCategory"
/>
@@ -492,6 +592,7 @@ onUnload(stopForegroundLocationUpdates)
:latitude="viewport.latitude"
:scale="viewport.scale"
:markers="markers"
:polyline="plannedRoutePolyline"
:show-location="locationReady"
:enable-rotate="false"
:enable-overlooking="false"
@@ -511,6 +612,9 @@ onUnload(stopForegroundLocationUpdates)
>
{{ locationButtonText }}
</cover-view>
<cover-view v-if="activePlan" class="map-page__route" @tap="includePlannedRoute">
完整路线 · {{ plannedPois.length }}
</cover-view>
</map>
</view>
@@ -628,6 +732,20 @@ style:
background: #167a5b;
}
.map-page__route {
position: absolute;
bottom: 28rpx;
left: 24rpx;
padding: 14rpx 22rpx;
font-size: 24rpx;
font-weight: 700;
line-height: 36rpx;
color: #fff;
background: #167a5b;
border-radius: 28rpx;
box-shadow: 0 8rpx 24rpx rgb(24 32 29 / 16%);
}
.map-page__disclaimer {
flex: none;
padding: 8rpx 24rpx calc(8rpx + env(safe-area-inset-bottom));