2026-07-30 16:04:34 +08:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import type { PoiResolved } from '@/domain/poi'
|
2026-07-31 12:50:14 +08:00
|
|
|
|
import type { ItineraryItem, PlanningOrigin, PlanResponse, TravelPreferences } from '@/domain/travel'
|
2026-07-30 16:04:34 +08:00
|
|
|
|
import TravelPoiCard from '@/components/travel/TravelPoiCard.vue'
|
|
|
|
|
|
import { getPoiRepository } from '@/data/poi'
|
2026-07-31 12:50:14 +08:00
|
|
|
|
import { adjustPlan, getSessionPlanningOrigin, loadPlan, loadPreferences, savePlan } from '@/services/travel-assistant'
|
2026-07-30 16:04:34 +08:00
|
|
|
|
import { useMapStore } from '@/stores'
|
|
|
|
|
|
|
|
|
|
|
|
interface ResolvedItineraryItem {
|
|
|
|
|
|
item: ItineraryItem
|
|
|
|
|
|
poi: PoiResolved
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface RouteMarker {
|
|
|
|
|
|
id: number
|
|
|
|
|
|
latitude: number
|
|
|
|
|
|
longitude: number
|
|
|
|
|
|
iconPath: string
|
|
|
|
|
|
width: number
|
|
|
|
|
|
height: number
|
|
|
|
|
|
callout: {
|
|
|
|
|
|
content: string
|
|
|
|
|
|
color: string
|
|
|
|
|
|
fontSize: number
|
|
|
|
|
|
borderRadius: number
|
|
|
|
|
|
bgColor: string
|
|
|
|
|
|
padding: number
|
|
|
|
|
|
display: 'ALWAYS'
|
|
|
|
|
|
textAlign: 'center'
|
|
|
|
|
|
}
|
|
|
|
|
|
anchor: { x: number, y: number }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-31 12:50:14 +08:00
|
|
|
|
interface RoutePoint {
|
|
|
|
|
|
latitude: number
|
|
|
|
|
|
longitude: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 16:04:34 +08:00
|
|
|
|
type DurationFitStatus = PlanResponse['itinerary']['durationFitStatus']
|
|
|
|
|
|
|
|
|
|
|
|
interface DurationFitNotice {
|
|
|
|
|
|
title: string
|
|
|
|
|
|
content: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const MAP_ID = 'guangming-itinerary-map'
|
|
|
|
|
|
const mapStore = useMapStore()
|
|
|
|
|
|
const plan = ref<PlanResponse | null>(null)
|
|
|
|
|
|
const preferences = ref<TravelPreferences | null>(null)
|
|
|
|
|
|
const message = ref('')
|
|
|
|
|
|
const adjusting = ref(false)
|
|
|
|
|
|
const mapReady = ref(false)
|
|
|
|
|
|
const mapError = ref(false)
|
|
|
|
|
|
const mapContext = shallowRef<ReturnType<typeof uni.createMapContext> | null>(null)
|
2026-07-31 12:50:14 +08:00
|
|
|
|
const planningOrigin = shallowRef<PlanningOrigin | null>(null)
|
|
|
|
|
|
const quickPrompts = computed(() => plan.value?.itinerary.planningMode === 'custom'
|
|
|
|
|
|
? ['节奏慢一点', '改为步行', '改成半日游']
|
|
|
|
|
|
: ['节奏慢一点', '换成室内场馆', '增加亲子体验'])
|
2026-07-30 16:04:34 +08:00
|
|
|
|
|
|
|
|
|
|
const resolvedItems = computed<ResolvedItineraryItem[]>(() => {
|
|
|
|
|
|
if (!plan.value)
|
|
|
|
|
|
return []
|
|
|
|
|
|
const repository = getPoiRepository()
|
|
|
|
|
|
return plan.value.itinerary.items.flatMap((item) => {
|
|
|
|
|
|
const poi = repository.getPoiById(item.placeId)
|
|
|
|
|
|
return poi ? [{ item, poi }] : []
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-31 12:50:14 +08:00
|
|
|
|
const poiRouteMarkers = computed<RouteMarker[]>(() => resolvedItems.value.map(({ poi }, index) => ({
|
2026-07-30 16:04:34 +08:00
|
|
|
|
id: index + 1,
|
|
|
|
|
|
latitude: poi.latitude,
|
|
|
|
|
|
longitude: poi.longitude,
|
|
|
|
|
|
iconPath: '/static/markers/default-selected.png',
|
|
|
|
|
|
width: uni.upx2px(64),
|
|
|
|
|
|
height: uni.upx2px(76),
|
|
|
|
|
|
anchor: { x: 0.5, y: 1 },
|
|
|
|
|
|
callout: {
|
2026-07-31 12:50:14 +08:00
|
|
|
|
content: String(index + 1),
|
2026-07-30 16:04:34 +08:00
|
|
|
|
color: '#18201d',
|
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
|
borderRadius: 7,
|
|
|
|
|
|
bgColor: '#ffffff',
|
|
|
|
|
|
padding: 5,
|
|
|
|
|
|
display: 'ALWAYS',
|
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
|
},
|
|
|
|
|
|
})))
|
|
|
|
|
|
|
2026-07-31 12:50:14 +08:00
|
|
|
|
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 }) => ({
|
2026-07-30 16:04:34 +08:00
|
|
|
|
latitude: poi.latitude,
|
|
|
|
|
|
longitude: poi.longitude,
|
|
|
|
|
|
})),
|
2026-07-31 12:50:14 +08:00
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
const routePolyline = computed(() => [{
|
|
|
|
|
|
points: routePoints.value,
|
2026-07-30 16:04:34 +08:00
|
|
|
|
color: '#167a5bcc',
|
|
|
|
|
|
width: 6,
|
|
|
|
|
|
dottedLine: false,
|
|
|
|
|
|
arrowLine: true,
|
|
|
|
|
|
borderColor: '#ffffffcc',
|
|
|
|
|
|
borderWidth: 2,
|
|
|
|
|
|
}])
|
|
|
|
|
|
|
|
|
|
|
|
const mapCenter = computed(() => {
|
|
|
|
|
|
const first = resolvedItems.value[0]?.poi
|
|
|
|
|
|
return {
|
|
|
|
|
|
longitude: first?.longitude ?? 113.9275,
|
|
|
|
|
|
latitude: first?.latitude ?? 22.76,
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const routeTags = computed(() => {
|
|
|
|
|
|
const current = preferences.value
|
|
|
|
|
|
if (!current)
|
|
|
|
|
|
return ['光明文旅', 'POC 路线']
|
|
|
|
|
|
const duration = current.duration === 'half_day' ? '半日游' : '一日游'
|
|
|
|
|
|
const pace = { relaxed: '轻松', moderate: '适中', compact: '紧凑' }[current.pace]
|
|
|
|
|
|
return [duration, current.themes[0], pace, current.interests[0]].filter((tag): tag is string => Boolean(tag))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const planningModeLabel = computed(() => plan.value?.itinerary.planningMode === 'custom' ? '自选规划' : '快速规划')
|
2026-07-30 16:33:39 +08:00
|
|
|
|
const planningSourceLabel = computed(() => {
|
|
|
|
|
|
if (plan.value?.source === 'remote_ai')
|
|
|
|
|
|
return '真实 AI 推荐'
|
|
|
|
|
|
if (plan.value?.source === 'remote_mock')
|
|
|
|
|
|
return '远端演示规划'
|
2026-07-31 12:50:14 +08:00
|
|
|
|
return '本地 POC 规划'
|
|
|
|
|
|
})
|
|
|
|
|
|
const planningBasisLabel = computed(() => {
|
|
|
|
|
|
const location = plan.value?.itinerary.startsFromCurrentLocation
|
|
|
|
|
|
? planningOrigin.value ? '已结合本次当前位置' : '已结合定位(起点坐标未保留)'
|
|
|
|
|
|
: '光明区全域'
|
|
|
|
|
|
const selection = plan.value?.itinerary.planningMode === 'custom'
|
|
|
|
|
|
? '完整保留自选点位并优化顺序'
|
|
|
|
|
|
: '按偏好、时长和推荐指数自动选点'
|
|
|
|
|
|
return `${location} · ${selection}`
|
2026-07-30 16:33:39 +08:00
|
|
|
|
})
|
2026-07-30 16:04:34 +08:00
|
|
|
|
const durationFitNotice = computed(() => plan.value ? getDurationFitNotice(plan.value.itinerary.durationFitStatus) : null)
|
|
|
|
|
|
|
|
|
|
|
|
function loadStoredPlan() {
|
|
|
|
|
|
plan.value = loadPlan()
|
|
|
|
|
|
preferences.value = loadPreferences()
|
2026-07-31 12:50:14 +08:00
|
|
|
|
planningOrigin.value = plan.value ? getSessionPlanningOrigin(plan.value.conversationId) : null
|
2026-07-30 16:04:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatMinutes(minutes: number): string {
|
|
|
|
|
|
const hours = Math.floor(minutes / 60)
|
|
|
|
|
|
const rest = minutes % 60
|
|
|
|
|
|
if (!hours)
|
|
|
|
|
|
return `${rest} 分钟`
|
|
|
|
|
|
return rest ? `${hours} 小时 ${rest} 分` : `${hours} 小时`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatItemTime(item: ItineraryItem, index: number): string {
|
|
|
|
|
|
const startsFromCurrentLocation = index === 0
|
|
|
|
|
|
&& plan.value?.itinerary.startsFromCurrentLocation
|
|
|
|
|
|
&& item.transferFromPreviousMinutes !== null
|
|
|
|
|
|
const transfer = startsFromCurrentLocation
|
|
|
|
|
|
? ` · 从当前位置出发约${item.transferFromPreviousMinutes}分钟`
|
|
|
|
|
|
: index > 0
|
|
|
|
|
|
? item.transferFromPreviousMinutes === null
|
|
|
|
|
|
? ' · 交通待确认'
|
|
|
|
|
|
: ` · 转场约 ${item.transferFromPreviousMinutes} 分钟`
|
|
|
|
|
|
: ''
|
|
|
|
|
|
return `${item.startTime}–${item.endTime}${transfer}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getDurationFitNotice(status: DurationFitStatus): DurationFitNotice | null {
|
|
|
|
|
|
if (status === 'overflow') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
title: '行程可能超出所选时长',
|
|
|
|
|
|
content: '当前安排较为充实,建议调整点位、节奏或出行时间后再出发。',
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (status === 'underfilled') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
title: '行程安排较为宽松',
|
|
|
|
|
|
content: '当前安排少于所选时长,可继续添加点位或保留机动时间。',
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showDurationFitNotice(status: DurationFitStatus): Promise<void> {
|
|
|
|
|
|
const notice = getDurationFitNotice(status)
|
|
|
|
|
|
if (!notice)
|
|
|
|
|
|
return Promise.resolve()
|
|
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
...notice,
|
|
|
|
|
|
showCancel: false,
|
|
|
|
|
|
confirmText: '我知道了',
|
|
|
|
|
|
complete: () => resolve(),
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatDate(value: string): string {
|
|
|
|
|
|
const timestamp = Date.parse(value)
|
|
|
|
|
|
if (!Number.isFinite(timestamp))
|
|
|
|
|
|
return '待确认'
|
|
|
|
|
|
const date = new Date(timestamp)
|
|
|
|
|
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function fitRoutePoints() {
|
2026-07-31 12:50:14 +08:00
|
|
|
|
if (!mapReady.value || !mapContext.value || routePoints.value.length === 0)
|
2026-07-30 16:04:34 +08:00
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
const context = mapContext.value
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
if (!mapReady.value || mapContext.value !== context)
|
|
|
|
|
|
return
|
|
|
|
|
|
try {
|
|
|
|
|
|
context.includePoints({
|
2026-07-31 12:50:14 +08:00
|
|
|
|
points: routePoints.value,
|
2026-07-30 16:04:34 +08:00
|
|
|
|
padding: [uni.upx2px(70), uni.upx2px(70), uni.upx2px(70), uni.upx2px(70)],
|
|
|
|
|
|
fail: handleMapError,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
|
|
|
|
|
handleMapError(error)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function initializeMap() {
|
|
|
|
|
|
mapError.value = false
|
|
|
|
|
|
try {
|
|
|
|
|
|
mapContext.value = uni.createMapContext(MAP_ID)
|
|
|
|
|
|
mapReady.value = true
|
|
|
|
|
|
fitRoutePoints()
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
|
|
|
|
|
handleMapError(error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleMapError(error: unknown) {
|
|
|
|
|
|
console.error('Failed to render itinerary map', error)
|
|
|
|
|
|
mapReady.value = false
|
|
|
|
|
|
mapError.value = true
|
|
|
|
|
|
mapContext.value = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openPlanner() {
|
|
|
|
|
|
uni.switchTab({ url: '/pages/planner/index' })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openDetail(poiId: string) {
|
|
|
|
|
|
uni.navigateTo({ url: `/pages/poi/detail?poiId=${encodeURIComponent(poiId)}` })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function focusOnMap(poiId: string) {
|
|
|
|
|
|
const poi = getPoiRepository().getPoiById(poiId)
|
|
|
|
|
|
if (!poi)
|
|
|
|
|
|
return
|
|
|
|
|
|
mapStore.setCategory(null)
|
|
|
|
|
|
mapStore.selectPoi(poi.id)
|
|
|
|
|
|
mapStore.updateViewport({ longitude: poi.longitude, latitude: poi.latitude, scale: 14 })
|
|
|
|
|
|
uni.switchTab({ url: '/pages/map/index' })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-31 12:50:14 +08:00
|
|
|
|
function openFullRouteOnMap() {
|
|
|
|
|
|
mapStore.setCategory(null)
|
|
|
|
|
|
mapStore.selectPoi(null)
|
|
|
|
|
|
mapStore.showPlannedRoute()
|
|
|
|
|
|
uni.switchTab({ url: '/pages/map/index' })
|
2026-07-30 16:04:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function saveRoute() {
|
|
|
|
|
|
if (!plan.value)
|
|
|
|
|
|
return
|
|
|
|
|
|
try {
|
|
|
|
|
|
savePlan(plan.value)
|
|
|
|
|
|
uni.showToast({ title: '已保存到本机', icon: 'success' })
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
|
|
|
|
|
uni.showModal({ title: '保存失败', content: error instanceof Error ? error.message : '请稍后重试', showCancel: false })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function sendAdjustment(quick?: string) {
|
|
|
|
|
|
if (!plan.value || adjusting.value)
|
|
|
|
|
|
return
|
|
|
|
|
|
const content = (quick ?? message.value).trim()
|
|
|
|
|
|
if (!content)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
adjusting.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await adjustPlan(plan.value.conversationId, content)
|
|
|
|
|
|
savePlan(response)
|
|
|
|
|
|
plan.value = response
|
2026-07-31 12:50:14 +08:00
|
|
|
|
planningOrigin.value = getSessionPlanningOrigin(response.conversationId)
|
2026-07-30 16:04:34 +08:00
|
|
|
|
preferences.value = loadPreferences()
|
|
|
|
|
|
message.value = ''
|
|
|
|
|
|
await showDurationFitNotice(response.itinerary.durationFitStatus)
|
|
|
|
|
|
fitRoutePoints()
|
|
|
|
|
|
uni.pageScrollTo({ scrollTop: 0, duration: 300 })
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (error) {
|
|
|
|
|
|
uni.showModal({
|
|
|
|
|
|
title: '调整未完成',
|
|
|
|
|
|
content: error instanceof Error ? error.message : '上一次路线已为你保留',
|
|
|
|
|
|
showCancel: false,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
finally {
|
|
|
|
|
|
adjusting.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onLoad(loadStoredPlan)
|
|
|
|
|
|
onReady(initializeMap)
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<view v-if="plan" class="itinerary-page">
|
|
|
|
|
|
<view class="route-hero">
|
|
|
|
|
|
<view class="route-hero__top">
|
|
|
|
|
|
<view class="route-hero__label">
|
|
|
|
|
|
<text class="route-hero__spark">✦</text>
|
2026-07-30 16:33:39 +08:00
|
|
|
|
{{ planningModeLabel }} · {{ planningSourceLabel }}
|
2026-07-30 16:04:34 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<button @click="openPlanner">重新规划</button>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<text class="route-hero__title">{{ plan.itinerary.title }}</text>
|
|
|
|
|
|
<view class="route-hero__tags">
|
|
|
|
|
|
<text v-for="tag in routeTags" :key="tag">{{ tag }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="route-hero__meta">
|
|
|
|
|
|
<view><text>用户选择时长</text><text>{{ formatMinutes(plan.itinerary.requestedMinutes) }}</text></view>
|
|
|
|
|
|
<view><text>预计总时长</text><text>{{ formatMinutes(plan.itinerary.totalMinutes) }}</text></view>
|
|
|
|
|
|
<view><text>行程点位</text><text>{{ resolvedItems.length }} 个</text></view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<text class="route-hero__summary">{{ plan.itinerary.summary }}</text>
|
2026-07-31 12:50:14 +08:00
|
|
|
|
<view class="route-hero__basis">
|
|
|
|
|
|
<text>本次规划依据</text>
|
|
|
|
|
|
<text>{{ planningBasisLabel }}</text>
|
|
|
|
|
|
<text>{{ plan.assistantMessage }}</text>
|
|
|
|
|
|
</view>
|
2026-07-30 16:04:34 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view v-if="plan.changeSummary" class="change-notice">
|
|
|
|
|
|
<text>已更新</text>{{ plan.changeSummary }}
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view
|
|
|
|
|
|
v-if="durationFitNotice"
|
|
|
|
|
|
class="duration-fit-notice"
|
|
|
|
|
|
:class="`duration-fit-notice--${plan.itinerary.durationFitStatus}`"
|
|
|
|
|
|
>
|
|
|
|
|
|
<text class="duration-fit-notice__title">{{ durationFitNotice.title }}</text>
|
|
|
|
|
|
<text class="duration-fit-notice__content">{{ durationFitNotice.content }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="itinerary-content">
|
|
|
|
|
|
<view class="route-map-card">
|
|
|
|
|
|
<view class="route-map-card__heading">
|
|
|
|
|
|
<view>
|
|
|
|
|
|
<text class="route-map-card__title">路线点位分布</text>
|
2026-07-31 12:50:14 +08:00
|
|
|
|
<text class="route-map-card__description">
|
|
|
|
|
|
{{ originMarker ? '含当前位置起点及全部游览节点' : '展示全部游览节点' }},按规划顺序直线连接
|
|
|
|
|
|
</text>
|
2026-07-30 16:04:34 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
<button @click="fitRoutePoints">查看全程</button>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view v-if="mapError" class="route-map-card__error">
|
|
|
|
|
|
<text>地图暂时无法显示</text>
|
|
|
|
|
|
<button @click="initializeMap">重试地图</button>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<map
|
|
|
|
|
|
v-else
|
|
|
|
|
|
:id="MAP_ID"
|
|
|
|
|
|
class="route-map-card__map"
|
|
|
|
|
|
:longitude="mapCenter.longitude"
|
|
|
|
|
|
:latitude="mapCenter.latitude"
|
|
|
|
|
|
:scale="11"
|
|
|
|
|
|
:markers="routeMarkers"
|
|
|
|
|
|
:polyline="routePolyline"
|
|
|
|
|
|
:enable-rotate="false"
|
|
|
|
|
|
:enable-overlooking="false"
|
|
|
|
|
|
:show-compass="false"
|
|
|
|
|
|
@error="handleMapError"
|
|
|
|
|
|
/>
|
2026-07-31 12:50:14 +08:00
|
|
|
|
<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>
|
2026-07-30 16:04:34 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="route-section">
|
|
|
|
|
|
<view class="route-section__heading">
|
|
|
|
|
|
<view>
|
|
|
|
|
|
<text class="route-section__kicker">ITINERARY</text>
|
|
|
|
|
|
<text class="route-section__title">行程安排</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<text class="route-section__count">{{ String(resolvedItems.length).padStart(2, '0') }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="route-list">
|
|
|
|
|
|
<TravelPoiCard
|
|
|
|
|
|
v-for="({ item, poi }, index) in resolvedItems"
|
|
|
|
|
|
:key="item.placeId"
|
|
|
|
|
|
:poi="poi"
|
|
|
|
|
|
:index="index + 1"
|
|
|
|
|
|
:time-text="formatItemTime(item, index)"
|
|
|
|
|
|
:activity="item.activity"
|
|
|
|
|
|
:reason="item.reason"
|
|
|
|
|
|
@detail="openDetail"
|
|
|
|
|
|
@map="focusOnMap"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="cost-card">
|
|
|
|
|
|
<view class="cost-card__icon">¥</view>
|
|
|
|
|
|
<view><text>费用提示</text><text>{{ plan.itinerary.estimatedCostText }}</text></view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="adjust-card">
|
|
|
|
|
|
<text class="route-section__kicker">ROUTE ADJUSTMENT</text>
|
|
|
|
|
|
<text class="adjust-card__title">还想怎么调整?</text>
|
|
|
|
|
|
<view class="quick-list">
|
|
|
|
|
|
<button v-for="prompt in quickPrompts" :key="prompt" :disabled="adjusting" @click="sendAdjustment(prompt)">
|
|
|
|
|
|
<text>{{ prompt }}</text><text>›</text>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="composer">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="message"
|
|
|
|
|
|
:disabled="adjusting"
|
|
|
|
|
|
maxlength="500"
|
2026-07-31 12:50:14 +08:00
|
|
|
|
:placeholder="plan.itinerary.planningMode === 'custom' ? '例如:节奏慢一点,或改为步行' : '例如:换成室内场馆'"
|
2026-07-30 16:04:34 +08:00
|
|
|
|
placeholder-class="composer__placeholder"
|
|
|
|
|
|
confirm-type="send"
|
|
|
|
|
|
@confirm="sendAdjustment()"
|
|
|
|
|
|
>
|
|
|
|
|
|
<button :disabled="adjusting || !message.trim()" @click="sendAdjustment()">{{ adjusting ? '调整中' : '发送' }}</button>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="details-card">
|
|
|
|
|
|
<view class="details-card__section">
|
|
|
|
|
|
<text class="route-section__kicker">出行提醒</text>
|
|
|
|
|
|
<text v-for="note in plan.itinerary.notes" :key="note" class="details-card__note">· {{ note }}</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="details-card__section">
|
|
|
|
|
|
<text class="route-section__kicker">资料来源</text>
|
|
|
|
|
|
<view v-for="source in plan.itinerary.sources" :key="source.placeId" class="source-row">
|
|
|
|
|
|
<text>{{ source.sourceName }}</text>
|
|
|
|
|
|
<text>{{ formatDate(source.updatedAt) }} 更新</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="bottom-actions">
|
|
|
|
|
|
<button class="bottom-actions__save" @click="saveRoute">保存路线</button>
|
2026-07-31 12:50:14 +08:00
|
|
|
|
<button class="bottom-actions__map" @click="openFullRouteOnMap">在全域地图查看完整路线</button>
|
2026-07-30 16:04:34 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view v-else class="empty-route">
|
|
|
|
|
|
<view class="empty-route__icon">⌁</view>
|
|
|
|
|
|
<text class="empty-route__title">还没有生成路线</text>
|
|
|
|
|
|
<text class="empty-route__copy">先选择出行主题、时长和兴趣偏好,助手会基于本地点位为你安排。</text>
|
|
|
|
|
|
<button @click="openPlanner">开始规划</button>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<route lang="yaml">
|
|
|
|
|
|
layout: map
|
|
|
|
|
|
style:
|
|
|
|
|
|
navigationBarTitleText: 推荐路线
|
|
|
|
|
|
navigationBarBackgroundColor: '#ffffff'
|
|
|
|
|
|
navigationBarTextStyle: black
|
|
|
|
|
|
backgroundColor: '#f4f8f6'
|
|
|
|
|
|
</route>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.itinerary-page {
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
|
|
|
|
|
|
color: #17221d;
|
|
|
|
|
|
background: #f4f8f6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-hero {
|
|
|
|
|
|
padding: 34rpx 32rpx 38rpx;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
background: linear-gradient(145deg, #123d35, #08724d);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-hero__top {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-hero__label {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8rpx;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
font-size: 21rpx;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: #b9f0d0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-hero__spark {
|
|
|
|
|
|
font-size: 30rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-hero__top button {
|
|
|
|
|
|
width: auto;
|
|
|
|
|
|
height: 58rpx;
|
|
|
|
|
|
padding: 0 20rpx;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 20rpx;
|
|
|
|
|
|
line-height: 58rpx;
|
|
|
|
|
|
color: #eafff2;
|
|
|
|
|
|
background: rgb(255 255 255 / 11%);
|
|
|
|
|
|
border: 1rpx solid rgb(255 255 255 / 24%);
|
|
|
|
|
|
border-radius: 29rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-hero__top button::after,
|
|
|
|
|
|
.route-map-card button::after,
|
|
|
|
|
|
.quick-list button::after,
|
|
|
|
|
|
.composer button::after,
|
|
|
|
|
|
.bottom-actions button::after,
|
|
|
|
|
|
.empty-route button::after {
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-hero__title {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-top: 24rpx;
|
|
|
|
|
|
font-size: 42rpx;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
line-height: 58rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-hero__tags {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 10rpx;
|
|
|
|
|
|
margin-top: 18rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-hero__tags text {
|
|
|
|
|
|
padding: 7rpx 13rpx;
|
|
|
|
|
|
font-size: 18rpx;
|
|
|
|
|
|
line-height: 28rpx;
|
|
|
|
|
|
color: #eafff2;
|
|
|
|
|
|
background: rgb(255 255 255 / 10%);
|
|
|
|
|
|
border: 1rpx solid rgb(255 255 255 / 22%);
|
|
|
|
|
|
border-radius: 999rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-hero__meta {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
|
|
gap: 20rpx;
|
|
|
|
|
|
margin-top: 25rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-hero__meta view {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-hero__meta text:first-child {
|
|
|
|
|
|
font-size: 18rpx;
|
|
|
|
|
|
color: rgb(238 255 245 / 65%);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-hero__meta text:last-child {
|
|
|
|
|
|
margin-top: 5rpx;
|
|
|
|
|
|
font-size: 29rpx;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-hero__summary {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-top: 22rpx;
|
|
|
|
|
|
font-size: 21rpx;
|
|
|
|
|
|
line-height: 35rpx;
|
|
|
|
|
|
color: rgb(238 255 245 / 75%);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-31 12:50:14 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 16:04:34 +08:00
|
|
|
|
.change-notice {
|
|
|
|
|
|
padding: 18rpx 28rpx;
|
|
|
|
|
|
font-size: 21rpx;
|
|
|
|
|
|
line-height: 32rpx;
|
|
|
|
|
|
color: #486656;
|
|
|
|
|
|
background: #e4f5ec;
|
|
|
|
|
|
border-bottom: 1rpx solid #cce6d8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.change-notice text {
|
|
|
|
|
|
padding: 4rpx 9rpx;
|
|
|
|
|
|
margin-right: 10rpx;
|
|
|
|
|
|
font-size: 17rpx;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
background: #16805c;
|
|
|
|
|
|
border-radius: 8rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.duration-fit-notice {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 6rpx;
|
|
|
|
|
|
padding: 20rpx 28rpx;
|
|
|
|
|
|
color: #78551f;
|
|
|
|
|
|
background: #fff8e8;
|
|
|
|
|
|
border-bottom: 1rpx solid #f0dfb8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.duration-fit-notice--overflow {
|
|
|
|
|
|
color: #8a432f;
|
|
|
|
|
|
background: #fff1ec;
|
|
|
|
|
|
border-bottom-color: #f1d1c6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.duration-fit-notice__title {
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
line-height: 32rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.duration-fit-notice__content {
|
|
|
|
|
|
font-size: 19rpx;
|
|
|
|
|
|
line-height: 30rpx;
|
|
|
|
|
|
opacity: 0.85;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.itinerary-content {
|
|
|
|
|
|
padding: 24rpx 20rpx 48rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-map-card,
|
|
|
|
|
|
.adjust-card,
|
|
|
|
|
|
.details-card {
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1rpx solid #d9e7e0;
|
|
|
|
|
|
border-radius: 24rpx;
|
|
|
|
|
|
box-shadow: 0 12rpx 34rpx rgb(20 78 53 / 6%);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-map-card__heading {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: 22rpx 24rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-map-card__heading > view {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-map-card__title {
|
|
|
|
|
|
font-size: 27rpx;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
line-height: 38rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-map-card__description {
|
|
|
|
|
|
margin-top: 3rpx;
|
|
|
|
|
|
font-size: 17rpx;
|
|
|
|
|
|
line-height: 28rpx;
|
|
|
|
|
|
color: #85928b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-map-card__heading button,
|
|
|
|
|
|
.route-map-card__error button {
|
|
|
|
|
|
width: auto;
|
|
|
|
|
|
height: 54rpx;
|
|
|
|
|
|
padding: 0 17rpx;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 19rpx;
|
|
|
|
|
|
line-height: 54rpx;
|
|
|
|
|
|
color: #167a5b;
|
|
|
|
|
|
background: #eaf5f0;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
border-radius: 27rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-map-card__map {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 430rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-map-card__error {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 20rpx;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
height: 430rpx;
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #637069;
|
|
|
|
|
|
background: #eef3f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-31 12:50:14 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 16:04:34 +08:00
|
|
|
|
.route-map-card__notice {
|
|
|
|
|
|
padding: 12rpx 20rpx;
|
|
|
|
|
|
font-size: 18rpx;
|
|
|
|
|
|
line-height: 28rpx;
|
|
|
|
|
|
color: #718078;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
background: #f8faf9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-section {
|
|
|
|
|
|
margin-top: 44rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-section__heading {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
margin-bottom: 22rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-section__kicker {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: 17rpx;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
line-height: 27rpx;
|
|
|
|
|
|
color: #16805c;
|
|
|
|
|
|
letter-spacing: 2rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-section__title {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-top: 6rpx;
|
|
|
|
|
|
font-size: 34rpx;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
line-height: 48rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-section__count {
|
|
|
|
|
|
font-family: Georgia, serif;
|
|
|
|
|
|
font-size: 50rpx;
|
|
|
|
|
|
font-style: italic;
|
|
|
|
|
|
color: rgb(23 56 47 / 18%);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.route-list {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 18rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cost-card {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 18rpx;
|
|
|
|
|
|
padding: 24rpx;
|
|
|
|
|
|
margin-top: 22rpx;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
background: linear-gradient(135deg, #0a965b, #077647);
|
|
|
|
|
|
border-radius: 20rpx;
|
|
|
|
|
|
box-shadow: 0 12rpx 28rpx rgb(5 118 69 / 17%);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cost-card__icon {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex: none;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 52rpx;
|
|
|
|
|
|
height: 52rpx;
|
|
|
|
|
|
font-size: 23rpx;
|
|
|
|
|
|
border: 1rpx solid rgb(255 255 255 / 50%);
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cost-card > view:last-child {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 7rpx;
|
|
|
|
|
|
font-size: 18rpx;
|
|
|
|
|
|
line-height: 30rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cost-card > view:last-child text:first-child {
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.adjust-card,
|
|
|
|
|
|
.details-card {
|
|
|
|
|
|
padding: 26rpx;
|
|
|
|
|
|
margin-top: 22rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.adjust-card__title {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-top: 7rpx;
|
|
|
|
|
|
font-size: 30rpx;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
line-height: 42rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.quick-list {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
margin-top: 18rpx;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
border: 1rpx solid #e1eae5;
|
|
|
|
|
|
border-radius: 14rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.quick-list button {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 76rpx;
|
|
|
|
|
|
padding: 0 19rpx;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 21rpx;
|
|
|
|
|
|
line-height: 76rpx;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
border-bottom: 1rpx solid #e8eeeb;
|
|
|
|
|
|
border-radius: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.quick-list button:last-child {
|
|
|
|
|
|
border-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.quick-list button text:last-child {
|
|
|
|
|
|
font-size: 30rpx;
|
|
|
|
|
|
color: #078e55;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
margin-top: 17rpx;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
border: 1rpx solid #dce7e1;
|
|
|
|
|
|
border-radius: 14rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer input {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
height: 82rpx;
|
|
|
|
|
|
padding: 0 18rpx;
|
|
|
|
|
|
font-size: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer__placeholder {
|
|
|
|
|
|
color: #a5afaa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer button {
|
|
|
|
|
|
width: 120rpx;
|
|
|
|
|
|
height: 82rpx;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 20rpx;
|
|
|
|
|
|
line-height: 82rpx;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
background: #078e55;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
border-radius: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.composer button[disabled],
|
|
|
|
|
|
.quick-list button[disabled] {
|
|
|
|
|
|
opacity: 0.55;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.details-card__section + .details-card__section {
|
|
|
|
|
|
padding-top: 24rpx;
|
|
|
|
|
|
margin-top: 25rpx;
|
|
|
|
|
|
border-top: 1rpx solid #e5ece8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.details-card__note {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-top: 12rpx;
|
|
|
|
|
|
font-size: 19rpx;
|
|
|
|
|
|
line-height: 31rpx;
|
|
|
|
|
|
color: #68766f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.source-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 18rpx;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: 16rpx 0;
|
|
|
|
|
|
font-size: 17rpx;
|
|
|
|
|
|
line-height: 27rpx;
|
|
|
|
|
|
color: #34473d;
|
|
|
|
|
|
border-bottom: 1rpx solid #e8eeeb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.source-row text:first-child {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.source-row text:last-child {
|
|
|
|
|
|
flex: none;
|
|
|
|
|
|
color: #919b96;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bottom-actions {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
z-index: 20;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 16rpx;
|
|
|
|
|
|
padding: 18rpx 22rpx calc(18rpx + env(safe-area-inset-bottom));
|
|
|
|
|
|
background: rgb(255 255 255 / 96%);
|
|
|
|
|
|
border-top: 1rpx solid #dfe8e3;
|
|
|
|
|
|
box-shadow: 0 -10rpx 30rpx rgb(26 70 49 / 7%);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bottom-actions button {
|
|
|
|
|
|
height: 82rpx;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
line-height: 82rpx;
|
|
|
|
|
|
border-radius: 18rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bottom-actions__save {
|
|
|
|
|
|
width: 220rpx;
|
|
|
|
|
|
color: #087d4d;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1rpx solid #11915b;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.bottom-actions__map {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
background: linear-gradient(100deg, #079c5c, #067d4a);
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
box-shadow: 0 10rpx 22rpx rgb(3 129 75 / 20%);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-route {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
padding: 48rpx;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
background: #f4f8f6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-route__icon {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 104rpx;
|
|
|
|
|
|
height: 104rpx;
|
|
|
|
|
|
font-size: 58rpx;
|
|
|
|
|
|
color: #078e55;
|
|
|
|
|
|
background: #e4f5ec;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-route__title {
|
|
|
|
|
|
margin-top: 26rpx;
|
|
|
|
|
|
font-size: 31rpx;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
line-height: 44rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-route__copy {
|
|
|
|
|
|
max-width: 560rpx;
|
|
|
|
|
|
margin-top: 12rpx;
|
|
|
|
|
|
font-size: 21rpx;
|
|
|
|
|
|
line-height: 34rpx;
|
|
|
|
|
|
color: #718078;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-route button {
|
|
|
|
|
|
width: 300rpx;
|
|
|
|
|
|
height: 82rpx;
|
|
|
|
|
|
margin-top: 28rpx;
|
|
|
|
|
|
font-size: 23rpx;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
line-height: 82rpx;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
background: #078e55;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
border-radius: 18rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|