This commit is contained in:
zlwu committed 2026-07-04 08:21:06 +08:00
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>