forked from zhouruizhe/gmTouringMiniApp
Add check-in feature and update travel planner UI
This commit is contained in:
+127
-9
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user