forked from zhouruizhe/gmTouringMiniApp
Add check-in feature and update travel planner UI
This commit is contained in:
+132
-18
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { PoiResolved } from '@/domain/poi'
|
||||
import type { ItineraryItem, PlanResponse, TravelPreferences } from '@/domain/travel'
|
||||
import type { ItineraryItem, PlanningOrigin, PlanResponse, TravelPreferences } from '@/domain/travel'
|
||||
import TravelPoiCard from '@/components/travel/TravelPoiCard.vue'
|
||||
import { getPoiRepository } from '@/data/poi'
|
||||
import { adjustPlan, loadPlan, loadPreferences, savePlan } from '@/services/travel-assistant'
|
||||
import { adjustPlan, getSessionPlanningOrigin, loadPlan, loadPreferences, savePlan } from '@/services/travel-assistant'
|
||||
import { useMapStore } from '@/stores'
|
||||
|
||||
interface ResolvedItineraryItem {
|
||||
@@ -31,6 +31,11 @@ interface RouteMarker {
|
||||
anchor: { x: number, y: number }
|
||||
}
|
||||
|
||||
interface RoutePoint {
|
||||
latitude: number
|
||||
longitude: number
|
||||
}
|
||||
|
||||
type DurationFitStatus = PlanResponse['itinerary']['durationFitStatus']
|
||||
|
||||
interface DurationFitNotice {
|
||||
@@ -47,7 +52,10 @@ const adjusting = ref(false)
|
||||
const mapReady = ref(false)
|
||||
const mapError = ref(false)
|
||||
const mapContext = shallowRef<ReturnType<typeof uni.createMapContext> | null>(null)
|
||||
const quickPrompts = ['节奏慢一点', '换成室内场馆', '增加亲子体验']
|
||||
const planningOrigin = shallowRef<PlanningOrigin | null>(null)
|
||||
const quickPrompts = computed(() => plan.value?.itinerary.planningMode === 'custom'
|
||||
? ['节奏慢一点', '改为步行', '改成半日游']
|
||||
: ['节奏慢一点', '换成室内场馆', '增加亲子体验'])
|
||||
|
||||
const resolvedItems = computed<ResolvedItineraryItem[]>(() => {
|
||||
if (!plan.value)
|
||||
@@ -59,7 +67,7 @@ const resolvedItems = computed<ResolvedItineraryItem[]>(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const routeMarkers = computed<RouteMarker[]>(() => resolvedItems.value.map(({ poi }, index) => ({
|
||||
const poiRouteMarkers = computed<RouteMarker[]>(() => resolvedItems.value.map(({ poi }, index) => ({
|
||||
id: index + 1,
|
||||
latitude: poi.latitude,
|
||||
longitude: poi.longitude,
|
||||
@@ -68,7 +76,7 @@ const routeMarkers = computed<RouteMarker[]>(() => resolvedItems.value.map(({ po
|
||||
height: uni.upx2px(76),
|
||||
anchor: { x: 0.5, y: 1 },
|
||||
callout: {
|
||||
content: `${index + 1} · ${poi.name}`,
|
||||
content: String(index + 1),
|
||||
color: '#18201d',
|
||||
fontSize: 11,
|
||||
borderRadius: 7,
|
||||
@@ -79,11 +87,47 @@ const routeMarkers = computed<RouteMarker[]>(() => resolvedItems.value.map(({ po
|
||||
},
|
||||
})))
|
||||
|
||||
const routePolyline = computed(() => [{
|
||||
points: resolvedItems.value.map(({ poi }) => ({
|
||||
const originMarker = computed<RouteMarker | null>(() => {
|
||||
if (!plan.value?.itinerary.startsFromCurrentLocation || !planningOrigin.value)
|
||||
return null
|
||||
return {
|
||||
id: 900000001,
|
||||
latitude: planningOrigin.value.latitude,
|
||||
longitude: planningOrigin.value.longitude,
|
||||
iconPath: '/static/markers/default-selected.png',
|
||||
width: uni.upx2px(56),
|
||||
height: uni.upx2px(66),
|
||||
anchor: { x: 0.5, y: 1 },
|
||||
callout: {
|
||||
content: '起点',
|
||||
color: '#ffffff',
|
||||
fontSize: 11,
|
||||
borderRadius: 7,
|
||||
bgColor: '#b45a1b',
|
||||
padding: 5,
|
||||
display: 'ALWAYS',
|
||||
textAlign: 'center',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const routeMarkers = computed<RouteMarker[]>(() => [
|
||||
...(originMarker.value ? [originMarker.value] : []),
|
||||
...poiRouteMarkers.value,
|
||||
])
|
||||
|
||||
const routePoints = computed<RoutePoint[]>(() => [
|
||||
...(originMarker.value
|
||||
? [{ latitude: originMarker.value.latitude, longitude: originMarker.value.longitude }]
|
||||
: []),
|
||||
...resolvedItems.value.map(({ poi }) => ({
|
||||
latitude: poi.latitude,
|
||||
longitude: poi.longitude,
|
||||
})),
|
||||
])
|
||||
|
||||
const routePolyline = computed(() => [{
|
||||
points: routePoints.value,
|
||||
color: '#167a5bcc',
|
||||
width: 6,
|
||||
dottedLine: false,
|
||||
@@ -115,13 +159,23 @@ const planningSourceLabel = computed(() => {
|
||||
return '真实 AI 推荐'
|
||||
if (plan.value?.source === 'remote_mock')
|
||||
return '远端演示规划'
|
||||
return '本地智能规划'
|
||||
return '本地 POC 规划'
|
||||
})
|
||||
const planningBasisLabel = computed(() => {
|
||||
const location = plan.value?.itinerary.startsFromCurrentLocation
|
||||
? planningOrigin.value ? '已结合本次当前位置' : '已结合定位(起点坐标未保留)'
|
||||
: '光明区全域'
|
||||
const selection = plan.value?.itinerary.planningMode === 'custom'
|
||||
? '完整保留自选点位并优化顺序'
|
||||
: '按偏好、时长和推荐指数自动选点'
|
||||
return `${location} · ${selection}`
|
||||
})
|
||||
const durationFitNotice = computed(() => plan.value ? getDurationFitNotice(plan.value.itinerary.durationFitStatus) : null)
|
||||
|
||||
function loadStoredPlan() {
|
||||
plan.value = loadPlan()
|
||||
preferences.value = loadPreferences()
|
||||
planningOrigin.value = plan.value ? getSessionPlanningOrigin(plan.value.conversationId) : null
|
||||
}
|
||||
|
||||
function formatMinutes(minutes: number): string {
|
||||
@@ -186,7 +240,7 @@ function formatDate(value: string): string {
|
||||
}
|
||||
|
||||
function fitRoutePoints() {
|
||||
if (!mapReady.value || !mapContext.value || resolvedItems.value.length === 0)
|
||||
if (!mapReady.value || !mapContext.value || routePoints.value.length === 0)
|
||||
return
|
||||
|
||||
const context = mapContext.value
|
||||
@@ -195,7 +249,7 @@ function fitRoutePoints() {
|
||||
return
|
||||
try {
|
||||
context.includePoints({
|
||||
points: resolvedItems.value.map(({ poi }) => ({ latitude: poi.latitude, longitude: poi.longitude })),
|
||||
points: routePoints.value,
|
||||
padding: [uni.upx2px(70), uni.upx2px(70), uni.upx2px(70), uni.upx2px(70)],
|
||||
fail: handleMapError,
|
||||
})
|
||||
@@ -243,10 +297,11 @@ function focusOnMap(poiId: string) {
|
||||
uni.switchTab({ url: '/pages/map/index' })
|
||||
}
|
||||
|
||||
function focusFirstOnMap() {
|
||||
const poi = resolvedItems.value[0]?.poi
|
||||
if (poi)
|
||||
focusOnMap(poi.id)
|
||||
function openFullRouteOnMap() {
|
||||
mapStore.setCategory(null)
|
||||
mapStore.selectPoi(null)
|
||||
mapStore.showPlannedRoute()
|
||||
uni.switchTab({ url: '/pages/map/index' })
|
||||
}
|
||||
|
||||
function saveRoute() {
|
||||
@@ -273,6 +328,7 @@ async function sendAdjustment(quick?: string) {
|
||||
const response = await adjustPlan(plan.value.conversationId, content)
|
||||
savePlan(response)
|
||||
plan.value = response
|
||||
planningOrigin.value = getSessionPlanningOrigin(response.conversationId)
|
||||
preferences.value = loadPreferences()
|
||||
message.value = ''
|
||||
await showDurationFitNotice(response.itinerary.durationFitStatus)
|
||||
@@ -315,6 +371,11 @@ onReady(initializeMap)
|
||||
<view><text>行程点位</text><text>{{ resolvedItems.length }} 个</text></view>
|
||||
</view>
|
||||
<text class="route-hero__summary">{{ plan.itinerary.summary }}</text>
|
||||
<view class="route-hero__basis">
|
||||
<text>本次规划依据</text>
|
||||
<text>{{ planningBasisLabel }}</text>
|
||||
<text>{{ plan.assistantMessage }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="plan.changeSummary" class="change-notice">
|
||||
@@ -335,7 +396,9 @@ onReady(initializeMap)
|
||||
<view class="route-map-card__heading">
|
||||
<view>
|
||||
<text class="route-map-card__title">路线点位分布</text>
|
||||
<text class="route-map-card__description">按推荐顺序直线连接,并非实际道路导航</text>
|
||||
<text class="route-map-card__description">
|
||||
{{ originMarker ? '含当前位置起点及全部游览节点' : '展示全部游览节点' }},按规划顺序直线连接
|
||||
</text>
|
||||
</view>
|
||||
<button @click="fitRoutePoints">查看全程</button>
|
||||
</view>
|
||||
@@ -357,7 +420,11 @@ onReady(initializeMap)
|
||||
:show-compass="false"
|
||||
@error="handleMapError"
|
||||
/>
|
||||
<view class="route-map-card__notice">推荐顺序 · 非实时导航</view>
|
||||
<view class="route-node-sequence">
|
||||
<text v-if="originMarker" class="route-node-sequence__origin">起点 · 当前位置</text>
|
||||
<text v-for="({ poi }, index) in resolvedItems" :key="poi.id">{{ index + 1 }} · {{ poi.name }}</text>
|
||||
</view>
|
||||
<view class="route-map-card__notice">完整规划顺序 · 直线示意,非道路级实时导航</view>
|
||||
</view>
|
||||
|
||||
<view class="route-section">
|
||||
@@ -401,7 +468,7 @@ onReady(initializeMap)
|
||||
v-model="message"
|
||||
:disabled="adjusting"
|
||||
maxlength="500"
|
||||
placeholder="例如:第二个地点换成室内项目"
|
||||
:placeholder="plan.itinerary.planningMode === 'custom' ? '例如:节奏慢一点,或改为步行' : '例如:换成室内场馆'"
|
||||
placeholder-class="composer__placeholder"
|
||||
confirm-type="send"
|
||||
@confirm="sendAdjustment()"
|
||||
@@ -427,7 +494,7 @@ onReady(initializeMap)
|
||||
|
||||
<view class="bottom-actions">
|
||||
<button class="bottom-actions__save" @click="saveRoute">保存路线</button>
|
||||
<button class="bottom-actions__map" @click="focusFirstOnMap">在全域地图查看</button>
|
||||
<button class="bottom-actions__map" @click="openFullRouteOnMap">在全域地图查看完整路线</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -559,6 +626,29 @@ style:
|
||||
color: rgb(238 255 245 / 75%);
|
||||
}
|
||||
|
||||
.route-hero__basis {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5rpx;
|
||||
padding: 16rpx 18rpx;
|
||||
margin-top: 18rpx;
|
||||
background: rgb(255 255 255 / 9%);
|
||||
border: 1rpx solid rgb(255 255 255 / 15%);
|
||||
border-radius: 14rpx;
|
||||
}
|
||||
|
||||
.route-hero__basis text {
|
||||
font-size: 19rpx;
|
||||
line-height: 30rpx;
|
||||
color: rgb(238 255 245 / 78%);
|
||||
}
|
||||
|
||||
.route-hero__basis text:first-child {
|
||||
font-size: 18rpx;
|
||||
font-weight: 800;
|
||||
color: #b9f0d0;
|
||||
}
|
||||
|
||||
.change-notice {
|
||||
padding: 18rpx 28rpx;
|
||||
font-size: 21rpx;
|
||||
@@ -676,6 +766,30 @@ style:
|
||||
background: #eef3f0;
|
||||
}
|
||||
|
||||
.route-node-sequence {
|
||||
display: flex;
|
||||
gap: 10rpx;
|
||||
padding: 16rpx 20rpx 4rpx;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
background: #f8faf9;
|
||||
}
|
||||
|
||||
.route-node-sequence text {
|
||||
flex: none;
|
||||
padding: 7rpx 13rpx;
|
||||
font-size: 18rpx;
|
||||
line-height: 28rpx;
|
||||
color: #285542;
|
||||
background: #e8f4ee;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.route-node-sequence .route-node-sequence__origin {
|
||||
color: #8a431d;
|
||||
background: #fff0e5;
|
||||
}
|
||||
|
||||
.route-map-card__notice {
|
||||
padding: 12rpx 20rpx;
|
||||
font-size: 18rpx;
|
||||
|
||||
Reference in New Issue
Block a user