Files
admin-web/src/modules/overseas-master-data/components/TransportRouteDialog.vue
T

81 lines
5.1 KiB
Vue
Raw Normal View History

2026-10-09 17:36:32 +08:00
<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { resolveRouteLocation, RouteAddressError, type RouteLocation } from '../routeLocations'
const open = defineModel<boolean>({ default: false })
const props = defineProps<{ origin: RouteLocation; destinations: RouteLocation[] }>()
const mapRevision = ref(0)
const destinationIndex = ref(0)
const selected = computed(() => props.destinations[destinationIndex.value])
const origin = ref(''), destination = ref(''), loading = ref(false), error = ref<ApiErrorDetails | null>(null)
let generation = 0
let cache = new Map<string, Promise<string>>()
function resolve(location: RouteLocation) {
const key = JSON.stringify(location)
if (!cache.has(key)) cache.set(key, resolveRouteLocation(location))
return cache.get(key)!
}
async function load() {
const current = ++generation
origin.value = ''; destination.value = ''; error.value = null; loading.value = false
if (!open.value) return
const valid = (location?: RouteLocation) => location && !['', '—', '-', '仓库名称待补充'].includes(location.label.trim())
if (!valid(props.origin) || !valid(selected.value)) {
error.value = { message: '缺少起点或目的地,请先补充地点或详细地址' }; return
}
loading.value = true
try {
const [start, end] = await Promise.all([resolve(props.origin), resolve(selected.value!)])
if (current === generation && open.value) { origin.value = start; destination.value = end; mapRevision.value++ }
} catch (cause) {
if (current === generation) error.value = cause instanceof RouteAddressError ? { message: cause.message } : getApiErrorDetails(cause, '仓库地址查询失败,请确认具有仓库查看权限后重试')
} finally { if (current === generation) loading.value = false }
}
function retry() { cache = new Map(); void load() }
watch([open, () => props.origin, () => props.destinations], () => {
destinationIndex.value = 0; cache = new Map(); void load()
}, { immediate: true })
watch(destinationIndex, load)
onBeforeUnmount(() => { generation++ })
const ready = computed(() => !loading.value && !error.value && Boolean(origin.value && destination.value))
const mapUrl = computed(() => `https://maps.google.com/maps?${new URLSearchParams({ saddr: origin.value, daddr: destination.value, dirflg: 'd', output: 'embed' })}`)
const externalUrl = computed(() => `https://www.google.com/maps/dir/?${new URLSearchParams({ api: '1', origin: origin.value, destination: destination.value, travelmode: 'driving' })}`)
const tooLong = computed(() => mapUrl.value.length > 2048 || externalUrl.value.length > 2048)
</script>
<template>
<AppDialog v-model="open" class="admin-density-dialog" title="查看路线"
width="min(1100px, 96vw)" align-center :show-confirm="false" cancel-text="关闭">
<section v-if="open" class="route-map" aria-label="起点至目的地路线">
<strong>{{ props.origin.label || '起点未填写' }} → {{ selected?.label || '目的地未填写' }}</strong>
<el-select v-if="props.destinations.length > 1" v-model="destinationIndex" aria-label="选择目的仓">
<el-option v-for="(item, index) in props.destinations" :key="index" :label="item.label" :value="index" />
</el-select>
<p class="route-note">根据起点与目的地地址规划驾车路线,尚未接入 GPS,不显示车辆实时位置或实际行驶轨迹。</p>
<p v-if="loading" role="status">正在查询仓库详细地址…</p>
<LoadErrorState v-else-if="error" title="无法加载路线" :message="error.message" @retry="retry"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<p v-else-if="tooLong" role="status">地点信息过长,请核对起点和目的地地址后重试。</p>
<template v-else-if="ready"><div class="route-addresses"><div>起点地址:{{ origin }}</div><div>目的地地址:{{ destination }}</div></div>
<iframe :key="mapRevision" :src="mapUrl" :title="`${props.origin.label}至${selected?.label}路线地图`"
referrerpolicy="no-referrer" />
</template>
<div v-if="ready && !tooLong" class="route-help">
<span>地图需联网加载。如未显示,可重新加载或在新窗口打开。</span>
<el-button @click="mapRevision++">重新加载地图</el-button>
<el-link :href="externalUrl" target="_blank" rel="noopener noreferrer" type="primary">在新窗口打开地图</el-link>
</div>
</section>
</AppDialog>
</template>
<style scoped>
.route-map { display: grid; gap: var(--admin-space-3); }
.route-note { margin: 0; color: var(--cc-muted); font-size: var(--admin-font-caption); }
.route-map iframe { width: 100%; height: 58vh; min-height: 320px; border: 1px solid var(--cc-border); border-radius: var(--admin-radius); }
.route-help { display: flex; align-items: center; flex-wrap: wrap; gap: var(--admin-space-3); color: var(--cc-muted); }
</style>