提交
This commit is contained in:
1 parent
8e70047f4c
commit
b4a49183db
33 files changed
+11558
-5889
No files matched your search
@@ -0,0 +1,427 @@
|
||||
<template>
|
||||
<div class="amap-address-picker">
|
||||
<el-input
|
||||
:id="inputId"
|
||||
v-model="innerAddress"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
clearable
|
||||
@input="handleAddressInput"
|
||||
>
|
||||
<template #append>
|
||||
<el-button :disabled="disabled" :loading="mapOpening" @click="openMapDialog">
|
||||
<el-icon><Location /></el-icon>地图选点
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<el-dialog
|
||||
v-model="mapVisible"
|
||||
title="地图选点"
|
||||
width="760px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
class="dialog-form-layout amap-address-picker-dialog"
|
||||
modal-class="order-module-dialog-mask"
|
||||
:close-on-click-modal="false"
|
||||
style="backdrop-filter: none"
|
||||
@opened="handleMapDialogOpened"
|
||||
@closed="handleMapDialogClosed"
|
||||
>
|
||||
<div class="amap-address-picker-body">
|
||||
<div class="amap-address-picker-search">
|
||||
<el-input
|
||||
:id="searchInputId"
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索地点、POI"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #append>
|
||||
<el-button :loading="searchLoading" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div ref="mapContainerRef" class="amap-address-picker-map" />
|
||||
|
||||
<p v-if="pickedAddress" class="amap-address-picker-result">
|
||||
已选:<span>{{ pickedAddress }}</span>
|
||||
</p>
|
||||
<p v-else class="amap-address-picker-hint">点击地图选点,或搜索后选择结果;可拖拽标记微调位置</p>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="mapVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :disabled="!pickedAddress" @click="confirmPick">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, Location, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
formatPoiAddress,
|
||||
isAmapConfigured,
|
||||
loadAmap,
|
||||
loadAmapPlugins,
|
||||
resolveLngLat,
|
||||
type AMapAutoComplete,
|
||||
type AMapGeocoder,
|
||||
type AMapGlobal,
|
||||
type AMapLngLat,
|
||||
type AMapMap,
|
||||
type AMapMarker,
|
||||
type AMapPlaceSearch,
|
||||
type AMapPoi
|
||||
} from '@/utils/amap/loader'
|
||||
|
||||
export interface AmapAddressPickResult {
|
||||
address: string
|
||||
lng?: number
|
||||
lat?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: string
|
||||
lng?: number | null
|
||||
lat?: number | null
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
city?: string
|
||||
}>(),
|
||||
{
|
||||
modelValue: '',
|
||||
lng: null,
|
||||
lat: null,
|
||||
placeholder: '请输入或地图选点',
|
||||
disabled: false,
|
||||
city: ''
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
'update:lng': [value: number | null]
|
||||
'update:lat': [value: number | null]
|
||||
change: [payload: AmapAddressPickResult]
|
||||
}>()
|
||||
|
||||
const inputId = `amap-address-input-${useId()}`
|
||||
const searchInputId = `amap-address-search-${useId()}`
|
||||
|
||||
const innerAddress = ref(props.modelValue || '')
|
||||
const mapVisible = ref(false)
|
||||
const mapOpening = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
const searchLoading = ref(false)
|
||||
const pickedAddress = ref('')
|
||||
const pickedLngLat = ref<[number, number] | null>(null)
|
||||
|
||||
const mapContainerRef = ref<HTMLDivElement>()
|
||||
let amapRef: AMapGlobal | null = null
|
||||
let mapInstance: AMapMap | null = null
|
||||
let markerInstance: AMapMarker | null = null
|
||||
let geocoder: AMapGeocoder | null = null
|
||||
let placeSearch: AMapPlaceSearch | null = null
|
||||
let autoComplete: AMapAutoComplete | null = null
|
||||
let mainAutoComplete: AMapAutoComplete | null = null
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
value => {
|
||||
if (value !== innerAddress.value) {
|
||||
innerAddress.value = value || ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
initMainAutoComplete()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
destroyMainAutoComplete()
|
||||
destroyMapResources()
|
||||
})
|
||||
|
||||
function setCoordinates(lng?: number | null, lat?: number | null) {
|
||||
const nextLng = lng ?? null
|
||||
const nextLat = lat ?? null
|
||||
emit('update:lng', nextLng)
|
||||
emit('update:lat', nextLat)
|
||||
}
|
||||
|
||||
function handleAddressInput(value: string) {
|
||||
emit('update:modelValue', value)
|
||||
setCoordinates(null, null)
|
||||
emit('change', { address: value })
|
||||
}
|
||||
|
||||
async function ensureAmapReady() {
|
||||
if (!isAmapConfigured()) {
|
||||
ElMessage.warning('未配置高德地图 Key,请在 .env.development.local 中设置 VITE_AMAP_KEY')
|
||||
return false
|
||||
}
|
||||
try {
|
||||
amapRef = await loadAmap()
|
||||
await loadAmapPlugins()
|
||||
return true
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '高德地图加载失败'
|
||||
ElMessage.error(message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function bindAutoComplete(
|
||||
target: AMapAutoComplete | null,
|
||||
onSelect: (poi?: AMapPoi) => void
|
||||
) {
|
||||
if (!target) return
|
||||
const handler = (event: { poi?: AMapPoi }) => onSelect(event.poi)
|
||||
target.on('select', handler)
|
||||
}
|
||||
|
||||
async function initMainAutoComplete() {
|
||||
const ready = await ensureAmapReady()
|
||||
if (!ready || !amapRef) return
|
||||
|
||||
amapRef.plugin(['AMap.AutoComplete'], () => {
|
||||
if (mainAutoComplete) return
|
||||
mainAutoComplete = new amapRef!.AutoComplete({
|
||||
input: inputId,
|
||||
city: props.city || '全国',
|
||||
citylimit: !!props.city
|
||||
})
|
||||
bindAutoComplete(mainAutoComplete, poi => {
|
||||
const address = formatPoiAddress(poi)
|
||||
if (!address) return
|
||||
innerAddress.value = address
|
||||
emit('update:modelValue', address)
|
||||
if (poi?.location) {
|
||||
const [lng, lat] = resolveLngLat(poi.location)
|
||||
setCoordinates(lng, lat)
|
||||
emit('change', { address, lng, lat })
|
||||
} else {
|
||||
setCoordinates(null, null)
|
||||
emit('change', { address })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function destroyMainAutoComplete() {
|
||||
mainAutoComplete = null
|
||||
}
|
||||
|
||||
async function openMapDialog() {
|
||||
if (props.disabled) return
|
||||
mapOpening.value = true
|
||||
try {
|
||||
const ready = await ensureAmapReady()
|
||||
if (!ready) return
|
||||
pickedAddress.value = innerAddress.value || ''
|
||||
pickedLngLat.value =
|
||||
props.lng != null && props.lat != null ? [Number(props.lng), Number(props.lat)] : null
|
||||
searchKeyword.value = innerAddress.value || ''
|
||||
mapVisible.value = true
|
||||
} finally {
|
||||
mapOpening.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMapDialogOpened() {
|
||||
await nextTick()
|
||||
await initMapPicker()
|
||||
if (pickedLngLat.value) {
|
||||
applyLngLat({ lng: pickedLngLat.value[0], lat: pickedLngLat.value[1] }, pickedAddress.value)
|
||||
} else if (searchKeyword.value.trim()) {
|
||||
handleSearch()
|
||||
}
|
||||
}
|
||||
|
||||
function handleMapDialogClosed() {
|
||||
destroyMapResources()
|
||||
}
|
||||
|
||||
async function initMapPicker() {
|
||||
if (!amapRef || !mapContainerRef.value) return
|
||||
|
||||
destroyMapResources()
|
||||
|
||||
mapInstance = new amapRef.Map(mapContainerRef.value, {
|
||||
zoom: 12,
|
||||
viewMode: '2D'
|
||||
})
|
||||
|
||||
geocoder = new amapRef.Geocoder()
|
||||
placeSearch = new amapRef.PlaceSearch({
|
||||
map: mapInstance,
|
||||
pageSize: 8,
|
||||
city: props.city || '全国',
|
||||
citylimit: !!props.city
|
||||
})
|
||||
|
||||
autoComplete = new amapRef.AutoComplete({
|
||||
input: searchInputId,
|
||||
city: props.city || '全国',
|
||||
citylimit: !!props.city
|
||||
})
|
||||
bindAutoComplete(autoComplete, poi => {
|
||||
if (!poi?.location) return
|
||||
applyPoiSelection(poi)
|
||||
})
|
||||
|
||||
mapInstance.on('click', event => {
|
||||
applyLngLat(event.lnglat)
|
||||
})
|
||||
|
||||
window.setTimeout(() => {
|
||||
mapInstance?.resize()
|
||||
}, 120)
|
||||
}
|
||||
|
||||
function applyPoiSelection(poi: AMapPoi) {
|
||||
if (!poi.location) return
|
||||
applyLngLat(poi.location, formatPoiAddress(poi))
|
||||
}
|
||||
|
||||
function applyLngLat(lnglat: AMapLngLat, presetAddress?: string) {
|
||||
if (!amapRef || !mapInstance) return
|
||||
const position = resolveLngLat(lnglat)
|
||||
pickedLngLat.value = position
|
||||
|
||||
if (!markerInstance) {
|
||||
markerInstance = new amapRef.Marker({
|
||||
position,
|
||||
draggable: true
|
||||
})
|
||||
markerInstance.on('dragend', () => {
|
||||
const pos = markerInstance?.getPosition()
|
||||
if (pos) reverseGeocode(pos)
|
||||
})
|
||||
mapInstance.add(markerInstance)
|
||||
} else {
|
||||
markerInstance.setPosition(position)
|
||||
}
|
||||
|
||||
mapInstance.setCenter(position)
|
||||
mapInstance.setZoom(16)
|
||||
|
||||
if (presetAddress) {
|
||||
pickedAddress.value = presetAddress
|
||||
return
|
||||
}
|
||||
reverseGeocode(lnglat)
|
||||
}
|
||||
|
||||
function reverseGeocode(lnglat: AMapLngLat) {
|
||||
if (!geocoder) return
|
||||
geocoder.getAddress(lnglat, (status, result) => {
|
||||
if (status !== 'complete') return
|
||||
const address = result.regeocode?.formattedAddress?.trim()
|
||||
if (address) pickedAddress.value = address
|
||||
})
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
const keyword = searchKeyword.value.trim()
|
||||
if (!keyword) {
|
||||
ElMessage.warning('请输入搜索关键词')
|
||||
return
|
||||
}
|
||||
if (!placeSearch) return
|
||||
|
||||
searchLoading.value = true
|
||||
placeSearch.search(keyword, (status, result) => {
|
||||
searchLoading.value = false
|
||||
if (status !== 'complete') {
|
||||
ElMessage.warning('未找到相关地点')
|
||||
return
|
||||
}
|
||||
const poi = result.poiList?.pois?.[0]
|
||||
if (!poi) {
|
||||
ElMessage.warning('未找到相关地点')
|
||||
return
|
||||
}
|
||||
applyPoiSelection(poi)
|
||||
})
|
||||
}
|
||||
|
||||
function confirmPick() {
|
||||
if (!pickedAddress.value) return
|
||||
innerAddress.value = pickedAddress.value
|
||||
emit('update:modelValue', pickedAddress.value)
|
||||
const payload: AmapAddressPickResult = { address: pickedAddress.value }
|
||||
if (pickedLngLat.value) {
|
||||
payload.lng = pickedLngLat.value[0]
|
||||
payload.lat = pickedLngLat.value[1]
|
||||
setCoordinates(payload.lng, payload.lat)
|
||||
} else {
|
||||
setCoordinates(null, null)
|
||||
}
|
||||
emit('change', payload)
|
||||
mapVisible.value = false
|
||||
}
|
||||
|
||||
function destroyMapResources() {
|
||||
autoComplete = null
|
||||
placeSearch = null
|
||||
geocoder = null
|
||||
markerInstance = null
|
||||
if (mapInstance) {
|
||||
mapInstance.destroy()
|
||||
mapInstance = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.amap-address-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.amap-address-picker-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.amap-address-picker-map {
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
border: 1px solid var(--border, #d9e8e2);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.amap-address-picker-result,
|
||||
.amap-address-picker-hint {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted, #64748b);
|
||||
line-height: 1.5;
|
||||
|
||||
span {
|
||||
color: var(--text, #1e293b);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.el-dialog.amap-address-picker-dialog.dialog-form-layout {
|
||||
width: min(760px, calc(100vw - 80px)) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="pl-address-with-library">
|
||||
<div class="pl-address-with-library__row">
|
||||
<AmapAddressPicker
|
||||
v-model="innerAddress"
|
||||
v-model:lng="innerLng"
|
||||
v-model:lat="innerLat"
|
||||
class="pl-address-with-library__picker"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
<el-button
|
||||
class="pl-address-with-library__library-btn btn-ghost"
|
||||
:disabled="disabled"
|
||||
@click="libraryVisible = true"
|
||||
>
|
||||
<el-icon><FolderOpened /></el-icon>从地址库选择
|
||||
</el-button>
|
||||
</div>
|
||||
<PlTransportAddressSelectDialog v-model="libraryVisible" @select="handleLibrarySelect" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FolderOpened } from '@element-plus/icons-vue'
|
||||
import AmapAddressPicker from '@/components/business/amap-address-picker/index.vue'
|
||||
import PlTransportAddressSelectDialog from '@/components/business/pl-transport-address-select-dialog/index.vue'
|
||||
import type { TransportAddress } from '@/api/pl/index'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
lng?: number | null
|
||||
lat?: number | null
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
'update:lng': [value: number | null]
|
||||
'update:lat': [value: number | null]
|
||||
}>()
|
||||
|
||||
const libraryVisible = ref(false)
|
||||
|
||||
const innerAddress = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: string) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const innerLng = computed({
|
||||
get: () => props.lng ?? null,
|
||||
set: (value: number | null) => emit('update:lng', value)
|
||||
})
|
||||
|
||||
const innerLat = computed({
|
||||
get: () => props.lat ?? null,
|
||||
set: (value: number | null) => emit('update:lat', value)
|
||||
})
|
||||
|
||||
function handleLibrarySelect(row: TransportAddress) {
|
||||
innerAddress.value = row.fullAddress
|
||||
innerLng.value = row.lng != null ? Number(row.lng) : null
|
||||
innerLat.value = row.lat != null ? Number(row.lat) : null
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pl-address-with-library {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pl-address-with-library__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pl-address-with-library__picker {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pl-address-with-library__library-btn {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -25,61 +25,64 @@
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="pl-plan-form-section">
|
||||
<section class="pl-plan-form-section pl-plan-freight-section">
|
||||
<h4 class="pl-plan-subtitle">运费信息</h4>
|
||||
<div class="form-grid-4">
|
||||
<el-form-item label="是否有运输费用">
|
||||
<el-switch v-model="formData.hasFreight" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
<template v-if="formData.hasFreight === 1">
|
||||
<el-form-item label="运输公里数">
|
||||
<el-input-number
|
||||
v-model="formData.distanceKm"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<el-form
|
||||
:model="formData"
|
||||
label-width="96px"
|
||||
label-position="right"
|
||||
class="pl-plan-freight-form"
|
||||
>
|
||||
<div class="form-grid-4">
|
||||
<el-form-item label="是否有运输费用">
|
||||
<el-switch v-model="formData.hasFreight" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="运费计算方式">
|
||||
<el-select v-model="formData.freightType">
|
||||
<el-option label="按重量" value="by_weight" />
|
||||
<el-option label="按趟" value="by_trip" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formData.freightType === 'by_weight'"
|
||||
:label="`运输量(${freightUnitLabel})`"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="freightCalc.transportQty.value"
|
||||
:min="0"
|
||||
:precision="3"
|
||||
style="width: 100%"
|
||||
@change="freightCalc.markTransportQtyTouched()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formData.freightType === 'by_weight'"
|
||||
:label="`运输单价(元/${freightUnitLabel})`"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="freightCalc.freightPrice.value"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="运费金额(元)">
|
||||
<el-input-number
|
||||
v-model="freightCalc.freightAmount.value"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
@change="freightCalc.markFreightAmountTouched()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
<template v-if="formData.hasFreight === 1">
|
||||
<el-form-item label="运输公里数">
|
||||
<el-input-number
|
||||
v-model="formData.distanceKm"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="运费计算方式">
|
||||
<el-select v-model="formData.freightType">
|
||||
<el-option label="按重量" value="by_weight" />
|
||||
<el-option label="按趟" value="by_trip" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formData.freightType === 'by_weight'"
|
||||
:label="`运输量(${freightUnitLabel})`"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="freightCalc.transportQty.value"
|
||||
:min="0"
|
||||
:precision="3"
|
||||
@change="freightCalc.markTransportQtyTouched()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formData.freightType === 'by_weight'"
|
||||
:label="`运输单价(元/${freightUnitLabel})`"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="freightCalc.freightPrice.value"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="运费金额(元)">
|
||||
<el-input-number
|
||||
v-model="freightCalc.freightAmount.value"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
@change="freightCalc.markFreightAmountTouched()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
@@ -384,10 +387,32 @@ async function handleSubmit() {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.pl-arrange-freight-muted {
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
.pl-plan-freight-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
height: auto !important;
|
||||
line-height: 1.4 !important;
|
||||
padding: 0 8px 0 0 !important;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__content) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.form-grid-4 {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.el-select),
|
||||
:deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-arrange-footer {
|
||||
@@ -396,6 +421,7 @@ async function handleSubmit() {
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -132,7 +132,12 @@
|
||||
<h4 class="pl-plan-subtitle">取货信息</h4>
|
||||
<div class="form-grid-3">
|
||||
<el-form-item label="取货地址">
|
||||
<el-input v-model="formData.pickupAddress" placeholder="取货地址" />
|
||||
<PlAddressWithLibrary
|
||||
v-model="formData.pickupAddress"
|
||||
v-model:lng="formData.pickupLng"
|
||||
v-model:lat="formData.pickupLat"
|
||||
placeholder="取货地址"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="取货时间">
|
||||
<el-date-picker
|
||||
@@ -174,7 +179,12 @@
|
||||
<h4 class="pl-plan-subtitle">送货信息</h4>
|
||||
<div class="form-grid-3">
|
||||
<el-form-item label="送货地址">
|
||||
<el-input v-model="formData.destinationAddress" placeholder="送货地址" />
|
||||
<PlAddressWithLibrary
|
||||
v-model="formData.destinationAddress"
|
||||
v-model:lng="formData.destinationLng"
|
||||
v-model:lat="formData.destinationLat"
|
||||
placeholder="送货地址"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="送达时间">
|
||||
<el-date-picker
|
||||
@@ -315,7 +325,11 @@
|
||||
@click="
|
||||
copyPlanSummary({
|
||||
pickupAddress: formData.pickupAddress,
|
||||
pickupLng: formData.pickupLng,
|
||||
pickupLat: formData.pickupLat,
|
||||
destinationAddress: formData.destinationAddress,
|
||||
destinationLng: formData.destinationLng,
|
||||
destinationLat: formData.destinationLat,
|
||||
pickupNotes: formData.pickupNotes,
|
||||
destinationNotes: formData.destinationNotes,
|
||||
licensePlate: vehicleList.find(v => v.id === formData.vehicleId)?.licensePlate,
|
||||
@@ -374,6 +388,7 @@ import {
|
||||
type VehiclePersonnelRow
|
||||
} from '@/api/pl'
|
||||
import DbPoundSlipQrcodeDialog from '@/components/business/db/DbPoundSlipQrcodeDialog.vue'
|
||||
import PlAddressWithLibrary from '@/components/business/pl-address-with-library/index.vue'
|
||||
import PlVehicleSummaryStrip from '@/components/business/pl-vehicle-summary-strip/index.vue'
|
||||
import PlVehicleViewDialog from '@/components/business/pl-vehicle-view-dialog/index.vue'
|
||||
import PlPlanViewTabs from '@/components/business/pl-plan-view-tabs/index.vue'
|
||||
@@ -449,9 +464,13 @@ const formData = reactive({
|
||||
driverId: '',
|
||||
carrierId: '',
|
||||
pickupAddress: '',
|
||||
pickupLng: null as number | null,
|
||||
pickupLat: null as number | null,
|
||||
pickupTime: '',
|
||||
pickupNotes: '',
|
||||
destinationAddress: '',
|
||||
destinationLng: null as number | null,
|
||||
destinationLat: null as number | null,
|
||||
deliveryTime: '',
|
||||
destinationNotes: '',
|
||||
hasFreight: 0,
|
||||
@@ -581,9 +600,13 @@ function applyVehiclePersonnel(personnelList: VehiclePersonnelRow[], preferredDr
|
||||
|
||||
async function applyPrefillFromPlan(data: Plan) {
|
||||
formData.pickupAddress = data.pickupAddress || ''
|
||||
formData.pickupLng = data.pickupLng != null ? Number(data.pickupLng) : null
|
||||
formData.pickupLat = data.pickupLat != null ? Number(data.pickupLat) : null
|
||||
formData.pickupTime = formatDate(data.pickupTime) || ''
|
||||
formData.pickupNotes = data.pickupNotes || ''
|
||||
formData.destinationAddress = data.destinationAddress || ''
|
||||
formData.destinationLng = data.destinationLng != null ? Number(data.destinationLng) : null
|
||||
formData.destinationLat = data.destinationLat != null ? Number(data.destinationLat) : null
|
||||
formData.deliveryTime = formatDate(data.deliveryTime) || ''
|
||||
formData.destinationNotes = data.destinationNotes || ''
|
||||
formData.hasFreight = data.hasFreight ?? 0
|
||||
@@ -657,9 +680,13 @@ function resetState() {
|
||||
driverId: '',
|
||||
carrierId: '',
|
||||
pickupAddress: '',
|
||||
pickupLng: null,
|
||||
pickupLat: null,
|
||||
pickupTime: '',
|
||||
pickupNotes: '',
|
||||
destinationAddress: '',
|
||||
destinationLng: null,
|
||||
destinationLat: null,
|
||||
deliveryTime: '',
|
||||
destinationNotes: '',
|
||||
hasFreight: 0,
|
||||
@@ -829,9 +856,13 @@ async function handleSubmit() {
|
||||
driverId: formData.driverId,
|
||||
carrierId: formData.carrierId || undefined,
|
||||
pickupAddress: formData.pickupAddress || undefined,
|
||||
pickupLng: formData.pickupLng,
|
||||
pickupLat: formData.pickupLat,
|
||||
pickupTime: formData.pickupTime || undefined,
|
||||
pickupNotes: formData.pickupNotes || undefined,
|
||||
destinationAddress: formData.destinationAddress || undefined,
|
||||
destinationLng: formData.destinationLng,
|
||||
destinationLat: formData.destinationLat,
|
||||
deliveryTime: formData.deliveryTime || undefined,
|
||||
destinationNotes: formData.destinationNotes || undefined,
|
||||
transportMedium: transportMediumText.value || undefined,
|
||||
|
||||
@@ -122,9 +122,14 @@
|
||||
<div class="pl-plan-logistics-row">
|
||||
<div class="pl-plan-logistics-block">
|
||||
<h4 class="pl-plan-subtitle">取货信息</h4>
|
||||
<div class="form-grid-3">
|
||||
<el-form-item label="取货地址">
|
||||
<el-input v-model="formData.pickupAddress" placeholder="取货地址" />
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="取货地址" class="form-col-span-2">
|
||||
<PlAddressWithLibrary
|
||||
v-model="formData.pickupAddress"
|
||||
v-model:lng="formData.pickupLng"
|
||||
v-model:lat="formData.pickupLat"
|
||||
placeholder="取货地址"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="取货时间">
|
||||
<el-date-picker
|
||||
@@ -164,9 +169,14 @@
|
||||
|
||||
<div class="pl-plan-logistics-block">
|
||||
<h4 class="pl-plan-subtitle">送货信息</h4>
|
||||
<div class="form-grid-3">
|
||||
<el-form-item label="送货地址">
|
||||
<el-input v-model="formData.destinationAddress" placeholder="送货地址" />
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="送货地址" class="form-col-span-2">
|
||||
<PlAddressWithLibrary
|
||||
v-model="formData.destinationAddress"
|
||||
v-model:lng="formData.destinationLng"
|
||||
v-model:lat="formData.destinationLat"
|
||||
placeholder="送货地址"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="送达时间">
|
||||
<el-date-picker
|
||||
@@ -273,6 +283,7 @@ import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vu
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import PlVehicleSummaryStrip from '@/components/business/pl-vehicle-summary-strip/index.vue'
|
||||
import PlVehicleViewDialog from '@/components/business/pl-vehicle-view-dialog/index.vue'
|
||||
import PlAddressWithLibrary from '@/components/business/pl-address-with-library/index.vue'
|
||||
import PlPlanFormProductTable from './PlPlanFormProductTable.vue'
|
||||
import {
|
||||
applyContractToPlanForm,
|
||||
@@ -373,9 +384,13 @@ const formData = reactive({
|
||||
settlementMethod: '',
|
||||
hasFreight: 0,
|
||||
pickupAddress: '',
|
||||
pickupLng: null as number | null,
|
||||
pickupLat: null as number | null,
|
||||
pickupTime: '',
|
||||
pickupNotes: '',
|
||||
destinationAddress: '',
|
||||
destinationLng: null as number | null,
|
||||
destinationLat: null as number | null,
|
||||
deliveryTime: '',
|
||||
destinationNotes: '',
|
||||
vehicleId: '',
|
||||
@@ -571,9 +586,13 @@ function buildPlanCommonPayload(options?: { allowClearTransport?: boolean }): Pl
|
||||
settlementMethod: formData.settlementMethod || undefined,
|
||||
hasFreight: formData.hasFreight,
|
||||
pickupAddress: formData.pickupAddress || undefined,
|
||||
pickupLng: formData.pickupLng,
|
||||
pickupLat: formData.pickupLat,
|
||||
pickupTime: formData.pickupTime || undefined,
|
||||
pickupNotes: formData.pickupNotes || undefined,
|
||||
destinationAddress: formData.destinationAddress || undefined,
|
||||
destinationLng: formData.destinationLng,
|
||||
destinationLat: formData.destinationLat,
|
||||
deliveryTime: formData.deliveryTime || undefined,
|
||||
destinationNotes: formData.destinationNotes || undefined,
|
||||
followUserId: formData.followUserId || undefined,
|
||||
@@ -650,9 +669,13 @@ function resetForm() {
|
||||
settlementMethod: '',
|
||||
hasFreight: 0,
|
||||
pickupAddress: '',
|
||||
pickupLng: null as number | null,
|
||||
pickupLat: null as number | null,
|
||||
pickupTime: '',
|
||||
pickupNotes: '',
|
||||
destinationAddress: '',
|
||||
destinationLng: null as number | null,
|
||||
destinationLat: null as number | null,
|
||||
deliveryTime: '',
|
||||
destinationNotes: '',
|
||||
vehicleId: '',
|
||||
@@ -705,9 +728,13 @@ async function loadPlanForEdit(planId: string) {
|
||||
settlementMethod: data.settlementMethod || '',
|
||||
hasFreight: data.hasFreight ?? 0,
|
||||
pickupAddress: data.pickupAddress || '',
|
||||
pickupLng: data.pickupLng != null ? Number(data.pickupLng) : null,
|
||||
pickupLat: data.pickupLat != null ? Number(data.pickupLat) : null,
|
||||
pickupTime: formatDate(data.pickupTime) || '',
|
||||
pickupNotes: data.pickupNotes || '',
|
||||
destinationAddress: data.destinationAddress || '',
|
||||
destinationLng: data.destinationLng != null ? Number(data.destinationLng) : null,
|
||||
destinationLat: data.destinationLat != null ? Number(data.destinationLat) : null,
|
||||
deliveryTime: formatDate(data.deliveryTime) || '',
|
||||
destinationNotes: data.destinationNotes || '',
|
||||
vehicleId: data.vehicleId || '',
|
||||
@@ -1084,7 +1111,7 @@ async function handleSubmit() {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-grid-3 {
|
||||
.form-grid-2 {
|
||||
gap: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
<ViewKvTable :fields="pickupViewFields" mode="quad" />
|
||||
</div>
|
||||
|
||||
<div class="party-flow-bridge" aria-hidden="true">
|
||||
<div class="party-flow-bridge">
|
||||
<span class="party-flow-bridge__label">运输</span>
|
||||
<span class="party-flow-bridge__arrow">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="innerVisible"
|
||||
title="从地址库选择"
|
||||
width="1000px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
class="dialog-form-layout pl-transport-address-select-dialog"
|
||||
modal-class="order-module-dialog-mask"
|
||||
:close-on-click-modal="false"
|
||||
style="backdrop-filter: none"
|
||||
@open="handleOpen"
|
||||
>
|
||||
<div class="filter-block pl-transport-address-select-filter">
|
||||
<el-form class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="名称/地址">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="搜索地址名称、详细地址、联系人或电话"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-start" />
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="panel dialog-list-panel">
|
||||
<div class="list-body list-wrap dialog-list-body-only">
|
||||
<el-table
|
||||
class="list-table pl-transport-address-select-table"
|
||||
:data="rows"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
highlight-current-row
|
||||
:max-height="480"
|
||||
empty-text="暂无可用地址"
|
||||
@row-dblclick="handleConfirm"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="index => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="addressName" label="地址名称" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="handleConfirm(row)">{{ row.addressName }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fullAddress" label="详细地址" min-width="260" show-overflow-tooltip />
|
||||
<el-table-column label="坐标" min-width="150" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.lng != null && row.lat != null">{{ row.lng }}, {{ row.lat }}</span>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="contactName"
|
||||
label="联系人"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="contactPhone"
|
||||
label="联系电话"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
fixed="right"
|
||||
class-name="col-actions"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleConfirm(row)">
|
||||
<el-icon><Select /></el-icon>选择
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<ListPagination
|
||||
v-show="pagination.total > 0"
|
||||
:total="pagination.total"
|
||||
v-model:page="pagination.page"
|
||||
v-model:limit="pagination.pageSize"
|
||||
@pagination="fetchList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="innerVisible = false">
|
||||
<el-icon><Close /></el-icon>关闭
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Close, RefreshLeft, Search, Select } from '@element-plus/icons-vue'
|
||||
import { transportAddressApi, type TransportAddress } from '@/api/pl/index'
|
||||
import ListPagination from '@/components/ListPagination/index.vue'
|
||||
import { pickBxPageResult } from '@/utils/http/bx'
|
||||
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
select: [row: TransportAddress]
|
||||
}>()
|
||||
|
||||
const innerVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const rows = ref<TransportAddress[]>([])
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: DEFAULT_LIST_PAGE_SIZE,
|
||||
total: 0
|
||||
})
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await transportAddressApi.list({
|
||||
q: keyword.value.trim() || undefined,
|
||||
status: '1',
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const { list, total } = pickBxPageResult<TransportAddress>(res)
|
||||
rows.value = list
|
||||
pagination.total = total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
keyword.value = ''
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleOpen() {
|
||||
keyword.value = ''
|
||||
pagination.page = 1
|
||||
pagination.pageSize = DEFAULT_LIST_PAGE_SIZE
|
||||
pagination.total = 0
|
||||
rows.value = []
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleConfirm(row: TransportAddress) {
|
||||
emit('select', row)
|
||||
innerVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pl-transport-address-select-filter {
|
||||
padding: 12px 16px 10px;
|
||||
background: var(--mint-surface, #fff);
|
||||
border: 1px solid var(--mint-border, #d9e8e2);
|
||||
border-radius: var(--mint-radius, 10px);
|
||||
margin-bottom: 12px;
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 10px 14px;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-actions-end {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.filter-actions-end .el-button + .el-button {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-transport-address-select-dialog {
|
||||
:deep(.el-dialog__body) {
|
||||
min-height: 620px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user