提交
This commit is contained in:
1 parent
2716c2b6f8
commit
f3693be913
140 files changed
+799199
-11262
No files matched your search
@@ -31,6 +31,7 @@ const props = defineProps<{
|
||||
modelValue: string
|
||||
lng?: number | null
|
||||
lat?: number | null
|
||||
contact?: string
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
}>()
|
||||
@@ -39,6 +40,7 @@ const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
'update:lng': [value: number | null]
|
||||
'update:lat': [value: number | null]
|
||||
'update:contact': [value: string]
|
||||
}>()
|
||||
|
||||
const libraryVisible = ref(false)
|
||||
@@ -58,10 +60,21 @@ const innerLat = computed({
|
||||
set: (value: number | null) => emit('update:lat', value)
|
||||
})
|
||||
|
||||
function formatAddressContact(row: TransportAddress): string {
|
||||
const name = (row.contactName || '').trim()
|
||||
const phone = (row.contactPhone || '').trim()
|
||||
if (name && phone) return `${name} ${phone}`
|
||||
return name || phone
|
||||
}
|
||||
|
||||
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
|
||||
const contactText = formatAddressContact(row)
|
||||
if (contactText) {
|
||||
emit('update:contact', contactText)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogTitle"
|
||||
width="960px"
|
||||
align-center
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
class="dialog-form-layout"
|
||||
modal-class="order-module-dialog-mask"
|
||||
style="backdrop-filter: none"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
v-loading="dialogLoading"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="right"
|
||||
label-width="100px"
|
||||
class="pl-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="承运商名称" prop="carrierName">
|
||||
<el-input v-model="formData.carrierName" placeholder="请输入承运商名称" maxlength="200" />
|
||||
</el-form-item>
|
||||
<el-form-item label="合作状态" prop="status">
|
||||
<el-select v-model="formData.status" placeholder="请选择">
|
||||
<el-option label="合作中" value="1" />
|
||||
<el-option label="已终止" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人" prop="contactName">
|
||||
<el-input v-model="formData.contactName" placeholder="请输入联系人" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话" prop="contactPhone">
|
||||
<el-input v-model="formData.contactPhone" placeholder="请输入联系电话" maxlength="32" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
maxlength="512"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-divider content-position="left">资质附件</el-divider>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="营业执照" prop="businessLicenseUrls" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
ref="businessLicenseUploadRef"
|
||||
v-model="formData.businessLicenseUrls"
|
||||
:limit="5"
|
||||
accept="image/*,.pdf"
|
||||
folder="pl/carrier/license"
|
||||
hint="支持 JPG、PNG、PDF,最多 5 个;可拖拽或在此区域粘贴上传"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="道路经营许可证" prop="roadTransportLicenseUrls" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
ref="roadTransportLicenseUploadRef"
|
||||
v-model="formData.roadTransportLicenseUrls"
|
||||
:limit="5"
|
||||
accept="image/*,.pdf"
|
||||
folder="pl/carrier/permit"
|
||||
hint="支持 JPG、PNG、PDF,最多 5 个;可拖拽或在此区域粘贴上传"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="运输协议" prop="transportAgreementUrls" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
ref="transportAgreementUploadRef"
|
||||
v-model="formData.transportAgreementUrls"
|
||||
:limit="5"
|
||||
accept="image/*,.pdf"
|
||||
folder="pl/carrier/agreement"
|
||||
hint="支持 JPG、PNG、PDF,最多 5 个;可拖拽或在此区域粘贴上传"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="协议有效期" prop="transportAgreementExpireDate">
|
||||
<el-date-picker
|
||||
v-model="formData.transportAgreementExpireDate"
|
||||
type="date"
|
||||
placeholder="请选择协议有效期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="visible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { carrierApi, type Carrier } from '@/api/pl'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
|
||||
defineOptions({ name: 'PlCarrierFormDialog' })
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
/** 有值则为编辑,无值则为新增 */
|
||||
carrierId?: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
saved: [payload: { mode: 'create' | 'edit'; carrier: Carrier }]
|
||||
}>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const isEdit = computed(() => !!props.carrierId)
|
||||
|
||||
const dialogTitle = computed(() => (isEdit.value ? '编辑承运商' : '新增承运商'))
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const dialogLoading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const businessLicenseUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
const roadTransportLicenseUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
const transportAgreementUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
carrierName: '',
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
status: '1',
|
||||
remark: '',
|
||||
businessLicenseUrls: '',
|
||||
roadTransportLicenseUrls: '',
|
||||
transportAgreementUrls: '',
|
||||
transportAgreementExpireDate: ''
|
||||
})
|
||||
|
||||
const formRules: FormRules = {
|
||||
carrierName: [{ required: true, message: '请输入承运商名称', trigger: 'blur' }],
|
||||
contactName: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
|
||||
contactPhone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
function hasAttachmentUploading() {
|
||||
return [businessLicenseUploadRef, roadTransportLicenseUploadRef, transportAgreementUploadRef].some(
|
||||
item => item.value?.hasUploading?.()
|
||||
)
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
Object.assign(formData, {
|
||||
id: '',
|
||||
carrierName: '',
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
status: '1',
|
||||
remark: '',
|
||||
businessLicenseUrls: '',
|
||||
roadTransportLicenseUrls: '',
|
||||
transportAgreementUrls: '',
|
||||
transportAgreementExpireDate: ''
|
||||
})
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
async function openDialog() {
|
||||
if (!props.carrierId) {
|
||||
resetForm()
|
||||
return
|
||||
}
|
||||
dialogLoading.value = true
|
||||
try {
|
||||
const res = await carrierApi.get(props.carrierId)
|
||||
const detail = pickBxData<Carrier>(res) || ({} as Carrier)
|
||||
Object.assign(formData, {
|
||||
id: detail.id || '',
|
||||
carrierName: detail.carrierName || '',
|
||||
contactName: detail.contactName || '',
|
||||
contactPhone: detail.contactPhone || '',
|
||||
status: detail.status || '1',
|
||||
remark: detail.remark || '',
|
||||
businessLicenseUrls: detail.businessLicenseUrls || '',
|
||||
roadTransportLicenseUrls: detail.roadTransportLicenseUrls || '',
|
||||
transportAgreementUrls: detail.transportAgreementUrls || '',
|
||||
transportAgreementExpireDate: detail.transportAgreementExpireDate || ''
|
||||
})
|
||||
} catch {
|
||||
ElMessage.error('加载承运商详情失败')
|
||||
visible.value = false
|
||||
} finally {
|
||||
dialogLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDialogClose() {
|
||||
formRef.value?.resetFields()
|
||||
resetForm()
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
if (hasAttachmentUploading()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
carrierName: formData.carrierName,
|
||||
contactName: formData.contactName,
|
||||
contactPhone: formData.contactPhone,
|
||||
status: formData.status,
|
||||
remark: formData.remark,
|
||||
businessLicenseUrls: formData.businessLicenseUrls,
|
||||
roadTransportLicenseUrls: formData.roadTransportLicenseUrls,
|
||||
transportAgreementUrls: formData.transportAgreementUrls,
|
||||
...(formData.transportAgreementExpireDate
|
||||
? { transportAgreementExpireDate: formData.transportAgreementExpireDate }
|
||||
: {})
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await carrierApi.update(formData.id, payload)
|
||||
const res = await carrierApi.get(formData.id)
|
||||
const detail = pickBxData<Carrier>(res)
|
||||
if (!detail?.id) {
|
||||
ElMessage.error('编辑成功但未返回承运商信息')
|
||||
return
|
||||
}
|
||||
ElMessage.success('编辑成功')
|
||||
emit('saved', { mode: 'edit', carrier: detail })
|
||||
visible.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const res = await carrierApi.create(payload)
|
||||
const created = pickBxData<Carrier>(res)
|
||||
if (!created?.id) {
|
||||
ElMessage.error('新增成功但未返回承运商信息')
|
||||
return
|
||||
}
|
||||
ElMessage.success('新增成功')
|
||||
emit('saved', { mode: 'create', carrier: created })
|
||||
visible.value = false
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
value => {
|
||||
if (value) void openDialog()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="innerVisible"
|
||||
title="从承运商库选择"
|
||||
width="1200px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
class="dialog-form-layout pl-carrier-library-select-dialog"
|
||||
modal-class="order-module-dialog-mask"
|
||||
:close-on-click-modal="false"
|
||||
style="backdrop-filter: none"
|
||||
@open="handleOpen"
|
||||
>
|
||||
<div class="panel dialog-list-panel">
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row pl-carrier-library-select-toolbar">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
class="pl-carrier-library-select-search"
|
||||
placeholder="搜索承运商名称、联系人或电话"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button class="btn-main" type="primary" @click="handleOpenCreate">
|
||||
<el-icon><Plus /></el-icon>新增承运商
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap dialog-list-body-only">
|
||||
<ListTable
|
||||
table-key="business-pl-carrier-library-select-dialog"
|
||||
class="list-table pl-carrier-library-select-table"
|
||||
:data="rows"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
highlight-current-row
|
||||
:max-height="560"
|
||||
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="carrierName" label="承运商名称" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="handleConfirm(row)">{{ row.carrierName }}</el-link>
|
||||
</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">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">
|
||||
{{ row.status === '1' ? '合作中' : '已终止' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
fixed="right"
|
||||
class-name="col-actions"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleOpenEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="primary" @click="handleConfirm(row)">
|
||||
<el-icon><Select /></el-icon>选择
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
|
||||
<ListPagination
|
||||
v-show="pagination.total > 0"
|
||||
:total="pagination.total"
|
||||
v-model:page="pagination.page"
|
||||
v-model:limit="pagination.pageSize"
|
||||
@pagination="fetchList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PlCarrierFormDialog
|
||||
v-model="formVisible"
|
||||
:carrier-id="editingId"
|
||||
@saved="handleFormSaved"
|
||||
/>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Edit, Plus, Search, Select } from '@element-plus/icons-vue'
|
||||
import { carrierApi, type Carrier } from '@/api/pl/index'
|
||||
import PlCarrierFormDialog from '@/components/business/pl-carrier-form-dialog/index.vue'
|
||||
import ListPagination from '@/components/ListPagination/index.vue'
|
||||
import { pickBxPageResult } from '@/utils/http/bx'
|
||||
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
||||
|
||||
defineOptions({ name: 'PlCarrierLibrarySelectDialog' })
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
select: [row: Carrier]
|
||||
}>()
|
||||
|
||||
const innerVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const rows = ref<Carrier[]>([])
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: DEFAULT_LIST_PAGE_SIZE,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const formVisible = ref(false)
|
||||
const editingId = ref<string | null>(null)
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await carrierApi.list({
|
||||
q: keyword.value.trim() || undefined,
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const { list, total } = pickBxPageResult<Carrier>(res)
|
||||
rows.value = list
|
||||
pagination.total = total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleOpen() {
|
||||
keyword.value = ''
|
||||
pagination.page = 1
|
||||
pagination.pageSize = DEFAULT_LIST_PAGE_SIZE
|
||||
pagination.total = 0
|
||||
rows.value = []
|
||||
formVisible.value = false
|
||||
editingId.value = null
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleOpenCreate() {
|
||||
editingId.value = null
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
function handleOpenEdit(row: Carrier) {
|
||||
editingId.value = row.id
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
function handleFormSaved(payload: { mode: 'create' | 'edit'; carrier: Carrier }) {
|
||||
formVisible.value = false
|
||||
editingId.value = null
|
||||
if (payload.mode === 'create') {
|
||||
handleConfirm(payload.carrier)
|
||||
return
|
||||
}
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleConfirm(row: Carrier) {
|
||||
emit('select', row)
|
||||
innerVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pl-carrier-library-select-dialog {
|
||||
:deep(.el-dialog__body) {
|
||||
min-height: 560px;
|
||||
}
|
||||
|
||||
.dialog-list-panel {
|
||||
.list-toolbar {
|
||||
margin-bottom: 0;
|
||||
padding: 10px 0 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pl-carrier-library-select-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pl-carrier-library-select-search {
|
||||
width: 320px;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -39,7 +39,11 @@
|
||||
v-model="carrierId"
|
||||
placeholder="请选择承运商"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
:remote-method="searchCarriers"
|
||||
:loading="carrierSelectLoading"
|
||||
@focus="onCarrierSelectFocus"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
@@ -130,7 +134,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, MagicStick } from '@element-plus/icons-vue'
|
||||
import {
|
||||
carrierApi,
|
||||
pasteRecognizeApi,
|
||||
personnelApi,
|
||||
vehicleApi,
|
||||
@@ -138,6 +141,7 @@
|
||||
type PasteConflictAction,
|
||||
type PasteConflictItem
|
||||
} from '@/api/pl'
|
||||
import { usePlRemoteCarrierOptions } from '@/composables/usePlRemoteOptions'
|
||||
import { pickBxPageResult } from '@/utils/http/bx'
|
||||
import {
|
||||
PL_PASTE_SAMPLE_TEXT,
|
||||
@@ -175,7 +179,14 @@
|
||||
const carrierId = ref('')
|
||||
const createVehicle = ref(true)
|
||||
const submitLoading = ref(false)
|
||||
const localCarriers = ref<Carrier[]>([])
|
||||
const {
|
||||
list: remoteCarriers,
|
||||
loading: carrierSelectLoading,
|
||||
search: searchCarriers,
|
||||
onFocus: onCarrierSelectFocus
|
||||
} = usePlRemoteCarrierOptions({
|
||||
getSelectedId: () => carrierId.value
|
||||
})
|
||||
|
||||
const vehicleForm = reactive({
|
||||
licensePlate: '',
|
||||
@@ -202,9 +213,10 @@
|
||||
}
|
||||
const conflictRows = ref<ConflictRow[]>([])
|
||||
|
||||
const carrierOptions = computed(() =>
|
||||
props.carrierOptions.length ? props.carrierOptions : localCarriers.value
|
||||
)
|
||||
const carrierOptions = computed(() => {
|
||||
if (remoteCarriers.value.length) return remoteCarriers.value
|
||||
return props.carrierOptions
|
||||
})
|
||||
|
||||
const hasPlate = computed(() => Boolean(vehicleForm.licensePlate.trim()))
|
||||
const showVehicleFields = computed(() => {
|
||||
@@ -213,11 +225,7 @@
|
||||
})
|
||||
|
||||
async function ensureCarriers() {
|
||||
if (props.carrierOptions.length) return
|
||||
if (localCarriers.value.length) return
|
||||
const res = await carrierApi.list({ page: 1, pageSize: 100 })
|
||||
const page = pickBxPageResult<Carrier>(res)
|
||||
localCarriers.value = page.list || []
|
||||
await searchCarriers('')
|
||||
}
|
||||
|
||||
async function handleParse() {
|
||||
|
||||
@@ -45,7 +45,11 @@
|
||||
class="pl-arrange-vehicle-select"
|
||||
placeholder="请选择车辆"
|
||||
filterable
|
||||
:loading="vehicleLoading"
|
||||
remote
|
||||
clearable
|
||||
:remote-method="searchVehicles"
|
||||
:loading="vehicleSelectLoading"
|
||||
@focus="onVehicleSelectFocus"
|
||||
@change="handleVehicleChange"
|
||||
>
|
||||
<el-option
|
||||
@@ -69,8 +73,9 @@
|
||||
<el-form-item label="选择司机" prop="driverId">
|
||||
<el-select
|
||||
v-model="formData.driverId"
|
||||
placeholder="请先选择车辆"
|
||||
placeholder="请先选择车辆(可选)"
|
||||
filterable
|
||||
clearable
|
||||
:disabled="!formData.vehicleId || vehicleDriverList.length === 0"
|
||||
>
|
||||
<el-option
|
||||
@@ -86,7 +91,11 @@
|
||||
v-model="formData.carrierId"
|
||||
placeholder="请选择运输公司"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
:remote-method="searchCarriers"
|
||||
:loading="carrierSelectLoading"
|
||||
@focus="onCarrierSelectFocus"
|
||||
>
|
||||
<el-option v-for="c in carrierList" :key="c.id" :label="c.carrierName" :value="c.id" />
|
||||
</el-select>
|
||||
@@ -104,6 +113,7 @@
|
||||
v-model="formData.pickupAddress"
|
||||
v-model:lng="formData.pickupLng"
|
||||
v-model:lat="formData.pickupLat"
|
||||
v-model:contact="formData.pickupContact"
|
||||
placeholder="取货地址"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -117,10 +127,13 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="注意事项">
|
||||
<el-input v-model="formData.pickupNotes" placeholder="取货注意事项" />
|
||||
<el-form-item label="取货联系方式">
|
||||
<el-input v-model="formData.pickupContact" placeholder="取货联系方式" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="注意事项">
|
||||
<el-input v-model="formData.pickupNotes" placeholder="取货注意事项" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -152,23 +165,27 @@
|
||||
v-model="formData.destinationAddress"
|
||||
v-model:lng="formData.destinationLng"
|
||||
v-model:lat="formData.destinationLat"
|
||||
v-model:contact="formData.deliveryContact"
|
||||
placeholder="送货地址"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div class="form-grid-2 pl-logistics-meta-row">
|
||||
<el-form-item label="送达时间">
|
||||
<el-form-item label="送货时间">
|
||||
<el-date-picker
|
||||
v-model="formData.deliveryTime"
|
||||
type="date"
|
||||
placeholder="选择送达时间"
|
||||
placeholder="选择送货时间"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="注意事项">
|
||||
<el-input v-model="formData.destinationNotes" placeholder="送货注意事项" />
|
||||
<el-form-item label="送货联系方式">
|
||||
<el-input v-model="formData.deliveryContact" placeholder="送货联系方式" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="注意事项">
|
||||
<el-input v-model="formData.destinationNotes" placeholder="送货注意事项" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -251,7 +268,9 @@
|
||||
destinationAddress: formData.destinationAddress,
|
||||
destinationLng: formData.destinationLng,
|
||||
destinationLat: formData.destinationLat,
|
||||
pickupContact: formData.pickupContact,
|
||||
pickupNotes: formData.pickupNotes,
|
||||
deliveryContact: formData.deliveryContact,
|
||||
destinationNotes: formData.destinationNotes,
|
||||
licensePlate: vehicleList.find(v => v.id === formData.vehicleId)?.licensePlate,
|
||||
driverUrl: arrangeResult?.driverUrl
|
||||
@@ -322,16 +341,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowDown, Check, Close, CopyDocument, Printer, View } from '@element-plus/icons-vue'
|
||||
import {
|
||||
carrierApi,
|
||||
planApi,
|
||||
transportApi,
|
||||
vehicleApi,
|
||||
type Carrier,
|
||||
type Plan,
|
||||
type PlanProduct,
|
||||
type Transport,
|
||||
type TransportArrangeResult,
|
||||
type Vehicle,
|
||||
type VehiclePersonnelRow
|
||||
} from '@/api/pl'
|
||||
import DbPoundSlipQrcodeDialog from '@/components/business/db/DbPoundSlipQrcodeDialog.vue'
|
||||
@@ -343,13 +358,17 @@ import PlVehicleViewDialog from '@/components/business/pl-vehicle-view-dialog/in
|
||||
import PlPlanViewTabs from '@/components/business/pl-plan-view-tabs/index.vue'
|
||||
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
|
||||
import { usePlFreightCalc } from '@/composables/usePlFreightCalc'
|
||||
import {
|
||||
usePlRemoteCarrierOptions,
|
||||
usePlRemoteVehicleOptions
|
||||
} from '@/composables/usePlRemoteOptions'
|
||||
import { loadVehicleViewBundle, type VehicleViewBundle } from '@/composables/usePlVehicleViewDetail'
|
||||
import { buildMissingHazardCertReadyMessage } from '@/composables/usePlToxicProductCertCheck'
|
||||
import { usePlPlanArrangeActions } from '@/composables/usePlPlanArrangeActions'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { isTonProductUnit } from '@/utils/crm-product-display'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { pickBxData, pickBxList } from '@/utils/http/bx'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
import { canPrintSlipCommand, formatPlVehiclePlateDisplay, type PlPrintSlipCommand } from '@/utils/pl-display'
|
||||
|
||||
const dictStore = useDictStore()
|
||||
@@ -399,8 +418,28 @@ const subpackSlipPrintRef = ref<InstanceType<typeof PlSubpackSlipPrintDialog> |
|
||||
const formRef = ref()
|
||||
const plan = ref<Partial<Plan>>({})
|
||||
const products = ref<PlanProduct[]>([])
|
||||
const vehicleList = ref<Vehicle[]>([])
|
||||
const carrierList = ref<Carrier[]>([])
|
||||
const {
|
||||
list: vehicleList,
|
||||
loading: vehicleSelectLoading,
|
||||
search: searchVehicles,
|
||||
onFocus: onVehicleSelectFocus,
|
||||
reset: resetVehicleOptions,
|
||||
ensureById: ensureSelectedVehicleOption
|
||||
} = usePlRemoteVehicleOptions({
|
||||
getSelectedId: () => formData.vehicleId,
|
||||
listParams: { vehicleStatus: 'available' }
|
||||
})
|
||||
const {
|
||||
list: carrierList,
|
||||
loading: carrierSelectLoading,
|
||||
search: searchCarriers,
|
||||
onFocus: onCarrierSelectFocus,
|
||||
reset: resetCarrierOptions,
|
||||
ensureById: ensureSelectedCarrierOption
|
||||
} = usePlRemoteCarrierOptions({
|
||||
getSelectedId: () => formData.carrierId,
|
||||
listParams: { status: '1' }
|
||||
})
|
||||
const vehicleDriverList = ref<VehiclePersonnelRow[]>([])
|
||||
const prefilledDriverId = ref('')
|
||||
const vehicleBundle = ref<VehicleViewBundle | null>(null)
|
||||
@@ -416,11 +455,13 @@ const formData = reactive({
|
||||
pickupLng: null as number | null,
|
||||
pickupLat: null as number | null,
|
||||
pickupTime: '',
|
||||
pickupContact: '',
|
||||
pickupNotes: '',
|
||||
destinationAddress: '',
|
||||
destinationLng: null as number | null,
|
||||
destinationLat: null as number | null,
|
||||
deliveryTime: '',
|
||||
deliveryContact: '',
|
||||
destinationNotes: '',
|
||||
hasFreight: 0,
|
||||
distanceKm: null as number | null,
|
||||
@@ -545,11 +586,13 @@ async function applyPrefillFromPlan(data: Plan) {
|
||||
formData.pickupLng = data.pickupLng != null ? Number(data.pickupLng) : null
|
||||
formData.pickupLat = data.pickupLat != null ? Number(data.pickupLat) : null
|
||||
formData.pickupTime = formatDate(data.pickupTime) || ''
|
||||
formData.pickupContact = data.pickupContact || ''
|
||||
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.deliveryContact = data.deliveryContact || ''
|
||||
formData.destinationNotes = data.destinationNotes || ''
|
||||
formData.hasFreight = data.hasFreight ?? 0
|
||||
formData.remark = data.remark || ''
|
||||
@@ -610,6 +653,8 @@ function resetState() {
|
||||
plan.value = {}
|
||||
products.value = []
|
||||
vehicleDriverList.value = []
|
||||
resetVehicleOptions()
|
||||
resetCarrierOptions()
|
||||
prefilledDriverId.value = ''
|
||||
vehicleBundle.value = null
|
||||
vehicleViewVisible.value = false
|
||||
@@ -623,11 +668,13 @@ function resetState() {
|
||||
pickupLng: null,
|
||||
pickupLat: null,
|
||||
pickupTime: '',
|
||||
pickupContact: '',
|
||||
pickupNotes: '',
|
||||
destinationAddress: '',
|
||||
destinationLng: null,
|
||||
destinationLat: null,
|
||||
deliveryTime: '',
|
||||
deliveryContact: '',
|
||||
destinationNotes: '',
|
||||
hasFreight: 0,
|
||||
distanceKm: null,
|
||||
@@ -640,12 +687,7 @@ function resetState() {
|
||||
}
|
||||
|
||||
async function loadMasterData() {
|
||||
const [vehicleRes, carrierRes] = await Promise.all([
|
||||
vehicleApi.list({ vehicleStatus: 'available' }),
|
||||
carrierApi.list({ status: '1', page: 1, pageSize: 100 })
|
||||
])
|
||||
vehicleList.value = pickBxList<Vehicle>(vehicleRes)
|
||||
carrierList.value = pickBxList<Carrier>(carrierRes)
|
||||
await Promise.all([searchVehicles(''), searchCarriers('')])
|
||||
}
|
||||
|
||||
async function handleVehicleChange(vehicleId: string, preferredDriverId?: string) {
|
||||
@@ -672,6 +714,9 @@ async function handleVehicleChange(vehicleId: string, preferredDriverId?: string
|
||||
if (vehicleBundle.value.detail?.carrierId) {
|
||||
formData.carrierId = vehicleBundle.value.detail.carrierId
|
||||
}
|
||||
if (formData.carrierId) {
|
||||
await ensureSelectedCarrierOption(formData.carrierId)
|
||||
}
|
||||
applyVehiclePersonnel(vehicleBundle.value.personnelList || [], preferredDriverId)
|
||||
} finally {
|
||||
vehicleLoading.value = false
|
||||
@@ -693,8 +738,12 @@ async function handleOpened() {
|
||||
products.value = Array.isArray(data.products) ? data.products : []
|
||||
await applyPrefillFromPlan(data)
|
||||
if (formData.vehicleId) {
|
||||
await ensureSelectedVehicleOption(formData.vehicleId)
|
||||
await handleVehicleChange(formData.vehicleId, prefilledDriverId.value || data.driverId)
|
||||
}
|
||||
if (formData.carrierId) {
|
||||
await ensureSelectedCarrierOption(formData.carrierId)
|
||||
}
|
||||
} catch (error) {
|
||||
innerVisible.value = false
|
||||
const message = error instanceof Error ? error.message : '加载运输计划失败'
|
||||
@@ -754,17 +803,19 @@ async function handleSubmit() {
|
||||
|
||||
const payload = {
|
||||
vehicleId: formData.vehicleId,
|
||||
driverId: formData.driverId,
|
||||
driverId: formData.driverId || undefined,
|
||||
carrierId: formData.carrierId || undefined,
|
||||
pickupAddress: formData.pickupAddress || undefined,
|
||||
pickupLng: formData.pickupLng,
|
||||
pickupLat: formData.pickupLat,
|
||||
pickupTime: formData.pickupTime || undefined,
|
||||
pickupContact: formData.pickupContact || undefined,
|
||||
pickupNotes: formData.pickupNotes || undefined,
|
||||
destinationAddress: formData.destinationAddress || undefined,
|
||||
destinationLng: formData.destinationLng,
|
||||
destinationLat: formData.destinationLat,
|
||||
deliveryTime: formData.deliveryTime || undefined,
|
||||
deliveryContact: formData.deliveryContact || undefined,
|
||||
destinationNotes: formData.destinationNotes || undefined,
|
||||
transportMedium: transportMediumText.value || undefined,
|
||||
distanceKm: formData.distanceKm ?? undefined,
|
||||
|
||||
@@ -108,31 +108,61 @@
|
||||
<el-form :model="formData" label-position="right" label-width="100px" class="pl-dialog-form">
|
||||
<div class="form-grid-4">
|
||||
<el-form-item label="车牌号">
|
||||
<el-select
|
||||
v-model="formData.vehicleId"
|
||||
placeholder="请选择车牌号"
|
||||
filterable
|
||||
clearable
|
||||
:loading="vehicleLoading"
|
||||
@change="handleVehicleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in vehicleList"
|
||||
:key="item.id"
|
||||
:label="item.licensePlate"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="pl-plan-vehicle-with-library">
|
||||
<el-select
|
||||
v-model="formData.vehicleId"
|
||||
class="pl-plan-vehicle-with-library__select"
|
||||
placeholder="请选择车牌号"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
:remote-method="searchVehicles"
|
||||
:loading="vehicleSelectLoading || vehicleLoading"
|
||||
@focus="onVehicleSelectFocus"
|
||||
@change="handleVehicleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in vehicleList"
|
||||
:key="item.id"
|
||||
:label="item.licensePlate"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button
|
||||
class="pl-plan-vehicle-with-library__btn btn-ghost"
|
||||
@click="vehicleLibraryVisible = true"
|
||||
>
|
||||
<el-icon><FolderOpened /></el-icon>从车辆库选择
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="运输公司">
|
||||
<el-select
|
||||
v-model="formData.carrierId"
|
||||
placeholder="请选择运输公司"
|
||||
filterable
|
||||
clearable
|
||||
>
|
||||
<el-option v-for="c in carrierList" :key="c.id" :label="c.carrierName" :value="c.id" />
|
||||
</el-select>
|
||||
<div class="pl-plan-carrier-with-library">
|
||||
<el-select
|
||||
v-model="formData.carrierId"
|
||||
class="pl-plan-carrier-with-library__select"
|
||||
placeholder="请选择运输公司"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
:remote-method="searchCarriers"
|
||||
:loading="carrierSelectLoading"
|
||||
@focus="onCarrierSelectFocus"
|
||||
>
|
||||
<el-option
|
||||
v-for="c in carrierList"
|
||||
:key="c.id"
|
||||
:label="c.carrierName"
|
||||
:value="c.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button
|
||||
class="pl-plan-carrier-with-library__btn btn-ghost"
|
||||
@click="carrierLibraryVisible = true"
|
||||
>
|
||||
<el-icon><FolderOpened /></el-icon>从承运商库选择
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
@@ -153,6 +183,7 @@
|
||||
v-model="formData.pickupAddress"
|
||||
v-model:lng="formData.pickupLng"
|
||||
v-model:lat="formData.pickupLat"
|
||||
v-model:contact="formData.pickupContact"
|
||||
placeholder="取货地址"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -165,7 +196,10 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="注意事项">
|
||||
<el-form-item label="取货联系方式">
|
||||
<el-input v-model="formData.pickupContact" placeholder="取货联系方式" />
|
||||
</el-form-item>
|
||||
<el-form-item label="注意事项" class="form-col-span-2">
|
||||
<el-input v-model="formData.pickupNotes" placeholder="取货注意事项" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
@@ -200,19 +234,23 @@
|
||||
v-model="formData.destinationAddress"
|
||||
v-model:lng="formData.destinationLng"
|
||||
v-model:lat="formData.destinationLat"
|
||||
v-model:contact="formData.deliveryContact"
|
||||
placeholder="送货地址"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="送达时间">
|
||||
<el-form-item label="送货时间">
|
||||
<el-date-picker
|
||||
v-model="formData.deliveryTime"
|
||||
type="date"
|
||||
placeholder="选择送达时间"
|
||||
placeholder="选择送货时间"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="注意事项">
|
||||
<el-form-item label="送货联系方式">
|
||||
<el-input v-model="formData.deliveryContact" placeholder="送货联系方式" />
|
||||
</el-form-item>
|
||||
<el-form-item label="注意事项" class="form-col-span-2">
|
||||
<el-input v-model="formData.destinationNotes" placeholder="送货注意事项" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
@@ -263,19 +301,42 @@
|
||||
:vehicle-id="formData.vehicleId"
|
||||
:bundle="vehicleBundle"
|
||||
/>
|
||||
<PlVehicleLibrarySelectDialog
|
||||
v-model="vehicleLibraryVisible"
|
||||
@select="handleVehicleLibrarySelect"
|
||||
/>
|
||||
<PlCarrierLibrarySelectDialog
|
||||
v-model="carrierLibraryVisible"
|
||||
@select="handleCarrierLibrarySelect"
|
||||
/>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, Search, Van, View } from '@element-plus/icons-vue'
|
||||
import { Check, Close, FolderOpened, Search, Van, View } from '@element-plus/icons-vue'
|
||||
import { getContract } from '@/api/crm/contract'
|
||||
import type { ContractVO } from '@/api/crm/contract/types'
|
||||
import { carrierApi, planApi, transportApi, vehicleApi, type Carrier, type Plan, type PlanProduct, type PlanUpdatePayload, type Vehicle, type VehiclePersonnelRow } from '@/api/pl'
|
||||
import {
|
||||
planApi,
|
||||
transportApi,
|
||||
type Plan,
|
||||
type PlanProduct,
|
||||
type PlanUpdatePayload,
|
||||
type Carrier,
|
||||
type Vehicle,
|
||||
type VehiclePersonnelRow
|
||||
} from '@/api/pl'
|
||||
import {
|
||||
usePlRemoteCarrierOptions,
|
||||
usePlRemoteVehicleOptions
|
||||
} from '@/composables/usePlRemoteOptions'
|
||||
import ContractSelector from '@/components/business/contract-selector/index.vue'
|
||||
import ViewKvGrid from '@/components/business/system-view-dialog/ViewKvGrid.vue'
|
||||
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 PlVehicleLibrarySelectDialog from '@/components/business/pl-vehicle-library-select-dialog/index.vue'
|
||||
import PlCarrierLibrarySelectDialog from '@/components/business/pl-carrier-library-select-dialog/index.vue'
|
||||
import PlAddressWithLibrary from '@/components/business/pl-address-with-library/index.vue'
|
||||
import PlPlanFormProductTable from './PlPlanFormProductTable.vue'
|
||||
import {
|
||||
@@ -294,7 +355,7 @@ import { loadVehicleViewBundle, type VehicleViewBundle } from '@/composables/use
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { pickBxData, pickBxList } from '@/utils/http/bx'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
|
||||
const ContractDetailDialog = defineAsyncComponent(
|
||||
() => import('@/views/crm/contract/components/ContractDetailDialog.vue')
|
||||
@@ -365,11 +426,31 @@ const contractSelectorVisible = ref(false)
|
||||
const contractDetailDialogActive = ref(false)
|
||||
const contractDetailRef = ref<InstanceType<typeof ContractDetailDialog>>()
|
||||
const vehicleViewVisible = ref(false)
|
||||
const vehicleLibraryVisible = ref(false)
|
||||
const carrierLibraryVisible = ref(false)
|
||||
const vehicleLoading = ref(false)
|
||||
const basicFormRef = ref()
|
||||
const contractSnapshot = ref<ContractVO | null>(null)
|
||||
const vehicleList = ref<Vehicle[]>([])
|
||||
const carrierList = ref<Carrier[]>([])
|
||||
const {
|
||||
list: vehicleList,
|
||||
loading: vehicleSelectLoading,
|
||||
search: searchVehicles,
|
||||
onFocus: onVehicleSelectFocus,
|
||||
ensureById: ensureSelectedVehicleOption
|
||||
} = usePlRemoteVehicleOptions({
|
||||
getSelectedId: () => formData.vehicleId,
|
||||
listParams: { vehicleStatus: 'available' }
|
||||
})
|
||||
const {
|
||||
list: carrierList,
|
||||
loading: carrierSelectLoading,
|
||||
search: searchCarriers,
|
||||
onFocus: onCarrierSelectFocus,
|
||||
ensureById: ensureSelectedCarrierOption
|
||||
} = usePlRemoteCarrierOptions({
|
||||
getSelectedId: () => formData.carrierId,
|
||||
listParams: { status: '1' }
|
||||
})
|
||||
const vehicleBundle = ref<VehicleViewBundle | null>(null)
|
||||
const selectedDriverId = ref('')
|
||||
|
||||
@@ -400,11 +481,13 @@ const formData = reactive({
|
||||
pickupLng: null as number | null,
|
||||
pickupLat: null as number | null,
|
||||
pickupTime: '',
|
||||
pickupContact: '',
|
||||
pickupNotes: '',
|
||||
destinationAddress: '',
|
||||
destinationLng: null as number | null,
|
||||
destinationLat: null as number | null,
|
||||
deliveryTime: '',
|
||||
deliveryContact: '',
|
||||
destinationNotes: '',
|
||||
vehicleId: '',
|
||||
carrierId: '',
|
||||
@@ -615,11 +698,13 @@ function buildPlanCommonPayload(options?: { allowClearTransport?: boolean }): Pl
|
||||
pickupLng: formData.pickupLng,
|
||||
pickupLat: formData.pickupLat,
|
||||
pickupTime: formData.pickupTime || undefined,
|
||||
pickupContact: formData.pickupContact || undefined,
|
||||
pickupNotes: formData.pickupNotes || undefined,
|
||||
destinationAddress: formData.destinationAddress || undefined,
|
||||
destinationLng: formData.destinationLng,
|
||||
destinationLat: formData.destinationLat,
|
||||
deliveryTime: formData.deliveryTime || undefined,
|
||||
deliveryContact: formData.deliveryContact || undefined,
|
||||
destinationNotes: formData.destinationNotes || undefined,
|
||||
followUserId: formData.followUserId || undefined,
|
||||
followUserName: formData.followUserName || undefined,
|
||||
@@ -702,11 +787,13 @@ function resetForm() {
|
||||
pickupLng: null as number | null,
|
||||
pickupLat: null as number | null,
|
||||
pickupTime: '',
|
||||
pickupContact: '',
|
||||
pickupNotes: '',
|
||||
destinationAddress: '',
|
||||
destinationLng: null as number | null,
|
||||
destinationLat: null as number | null,
|
||||
deliveryTime: '',
|
||||
deliveryContact: '',
|
||||
destinationNotes: '',
|
||||
vehicleId: '',
|
||||
carrierId: '',
|
||||
@@ -778,11 +865,13 @@ async function loadPlanForEdit(planId: string) {
|
||||
pickupLng: data.pickupLng != null ? Number(data.pickupLng) : null,
|
||||
pickupLat: data.pickupLat != null ? Number(data.pickupLat) : null,
|
||||
pickupTime: formatDate(data.pickupTime) || '',
|
||||
pickupContact: data.pickupContact || '',
|
||||
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) || '',
|
||||
deliveryContact: data.deliveryContact || '',
|
||||
destinationNotes: data.destinationNotes || '',
|
||||
vehicleId: data.vehicleId || '',
|
||||
remark: data.remark || '',
|
||||
@@ -806,10 +895,14 @@ async function loadPlanForEdit(planId: string) {
|
||||
await refreshContractProductRows()
|
||||
|
||||
if (data.vehicleId) {
|
||||
await ensureSelectedVehicleOption(data.vehicleId)
|
||||
await handleVehicleChange(data.vehicleId)
|
||||
}
|
||||
freightCalc.resetFreightCalc()
|
||||
await restoreFreightFromTransport(data)
|
||||
if (formData.carrierId) {
|
||||
await ensureSelectedCarrierOption(formData.carrierId)
|
||||
}
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
@@ -866,17 +959,7 @@ function handleClosed() {
|
||||
}
|
||||
|
||||
async function loadMasterData() {
|
||||
try {
|
||||
const [vehicleRes, carrierRes] = await Promise.all([
|
||||
vehicleApi.list({ vehicleStatus: 'available' }),
|
||||
carrierApi.list({ status: '1', page: 1, pageSize: 100 })
|
||||
])
|
||||
vehicleList.value = pickBxList<Vehicle>(vehicleRes)
|
||||
carrierList.value = pickBxList<Carrier>(carrierRes)
|
||||
} catch {
|
||||
vehicleList.value = []
|
||||
carrierList.value = []
|
||||
}
|
||||
await Promise.all([searchVehicles(''), searchCarriers('')])
|
||||
}
|
||||
|
||||
async function handleViewContract() {
|
||||
@@ -957,6 +1040,22 @@ async function handleVehicleChange(vehicleId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleVehicleLibrarySelect(vehicle: Vehicle) {
|
||||
if (!vehicle?.id) return
|
||||
await ensureSelectedVehicleOption(vehicle.id)
|
||||
formData.vehicleId = vehicle.id
|
||||
await handleVehicleChange(vehicle.id)
|
||||
if (vehicle.carrierId) {
|
||||
await ensureSelectedCarrierOption(vehicle.carrierId)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCarrierLibrarySelect(carrier: Carrier) {
|
||||
if (!carrier?.id) return
|
||||
await ensureSelectedCarrierOption(carrier.id)
|
||||
formData.carrierId = carrier.id
|
||||
}
|
||||
|
||||
function validateBusinessRules() {
|
||||
if (!formData.contractId) {
|
||||
ElMessage.warning('请选择合同')
|
||||
@@ -1046,6 +1145,26 @@ async function handleSubmit(mode: 'save' | 'saveAndArrange' = 'save') {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pl-plan-vehicle-with-library,
|
||||
.pl-plan-carrier-with-library {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pl-plan-vehicle-with-library__select,
|
||||
.pl-plan-carrier-with-library__select {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pl-plan-vehicle-with-library__btn,
|
||||
.pl-plan-carrier-with-library__btn {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pl-plan-form-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
<span
|
||||
class="pl-plan-list-product-cell__line"
|
||||
:class="{
|
||||
'pl-plan-list-product-cell__line--qty': field === 'qty',
|
||||
'pl-plan-list-product-cell__line--center': field === 'spec' || field === 'unit'
|
||||
'pl-plan-list-product-cell__line--qty':
|
||||
field === 'qty' || field === 'planQty' || field === 'actualQty',
|
||||
'pl-plan-list-product-cell__line--center':
|
||||
field === 'spec' || field === 'model' || field === 'packageSpec' || field === 'unit'
|
||||
}"
|
||||
>
|
||||
<template v-if="!field">
|
||||
@@ -88,11 +90,23 @@ import {
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import {
|
||||
formatPlanProductListText,
|
||||
getPlanProductActualQuantity,
|
||||
getPlanProductModel,
|
||||
getPlanProductPackageSpec,
|
||||
getPlanProductPlannedQuantity,
|
||||
getPlanProductQuantity,
|
||||
getPlanProductSpec
|
||||
} from '@/utils/pl-display'
|
||||
|
||||
export type PlPlanListProductCellField = 'name' | 'spec' | 'qty' | 'unit'
|
||||
export type PlPlanListProductCellField =
|
||||
| 'name'
|
||||
| 'spec'
|
||||
| 'model'
|
||||
| 'packageSpec'
|
||||
| 'qty'
|
||||
| 'planQty'
|
||||
| 'actualQty'
|
||||
| 'unit'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -122,9 +136,21 @@ function formatFieldValue(product: PlanProduct) {
|
||||
if (props.field === 'spec') {
|
||||
return getPlanProductSpec(product)
|
||||
}
|
||||
if (props.field === 'model') {
|
||||
return getPlanProductModel(product)
|
||||
}
|
||||
if (props.field === 'packageSpec') {
|
||||
return getPlanProductPackageSpec(product)
|
||||
}
|
||||
if (props.field === 'qty') {
|
||||
return getPlanProductQuantity(product)
|
||||
}
|
||||
if (props.field === 'planQty') {
|
||||
return getPlanProductPlannedQuantity(product)
|
||||
}
|
||||
if (props.field === 'actualQty') {
|
||||
return getPlanProductActualQuantity(product)
|
||||
}
|
||||
if (props.field === 'unit') {
|
||||
const unitRaw = product.unit || ''
|
||||
if (!unitRaw) return '—'
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
@tab-change="handleTabChange"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="item in PL_PLAN_TYPE_TABS"
|
||||
v-for="item in resolvedTabs"
|
||||
:key="item.name"
|
||||
:label="item.label"
|
||||
:name="item.name"
|
||||
@@ -14,19 +14,23 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PL_PLAN_TYPE_TABS, type PlPlanTypeTab } from '@/utils/pl-display'
|
||||
import { PL_PLAN_TYPE_TABS } from '@/utils/pl-display'
|
||||
|
||||
defineProps<{
|
||||
modelValue: PlPlanTypeTab
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
/** 覆盖默认单据类型 Tab(如危化品证上传页追加「未上传证书」) */
|
||||
tabs?: { label: string; name: string }[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: PlPlanTypeTab]
|
||||
change: [value: PlPlanTypeTab]
|
||||
'update:modelValue': [value: string]
|
||||
change: [value: string]
|
||||
}>()
|
||||
|
||||
const resolvedTabs = computed(() => props.tabs ?? PL_PLAN_TYPE_TABS)
|
||||
|
||||
function handleTabChange(name: string | number) {
|
||||
const value = String(name) as PlPlanTypeTab
|
||||
const value = String(name)
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
}
|
||||
|
||||
@@ -1131,7 +1131,8 @@
|
||||
const pickupViewFields = computed<ViewField[]>(() => [
|
||||
{ label: '取货地址', value: plan.value?.pickupAddress, span: 'full' },
|
||||
{ label: '取货时间', value: formatDate(plan.value?.pickupTime) || '—', long: false },
|
||||
{ label: '注意事项', value: plan.value?.pickupNotes, long: false, fullSpan: false }
|
||||
{ label: '取货联系方式', value: plan.value?.pickupContact, long: false, fullSpan: false },
|
||||
{ label: '注意事项', value: plan.value?.pickupNotes, span: 'full' }
|
||||
])
|
||||
|
||||
const deliveryViewFields = computed<ViewField[]>(() => [
|
||||
@@ -1140,8 +1141,9 @@
|
||||
value: plan.value?.destinationAddress || plan.value?.destinationName,
|
||||
span: 'full'
|
||||
},
|
||||
{ label: '送达时间', value: formatDate(plan.value?.deliveryTime) || '—', long: false },
|
||||
{ label: '注意事项', value: plan.value?.destinationNotes, long: false, fullSpan: false }
|
||||
{ label: '送货时间', value: formatDate(plan.value?.deliveryTime) || '—', long: false },
|
||||
{ label: '送货联系方式', value: plan.value?.deliveryContact, long: false, fullSpan: false },
|
||||
{ label: '注意事项', value: plan.value?.destinationNotes, span: 'full' }
|
||||
])
|
||||
|
||||
const freightViewFields = computed<ViewField[]>(() => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<el-dialog
|
||||
v-model="innerVisible"
|
||||
title="从地址库选择"
|
||||
width="1000px"
|
||||
width="1200px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
@@ -12,35 +12,21 @@
|
||||
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-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-row pl-transport-address-select-toolbar">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
class="pl-transport-address-select-search"
|
||||
placeholder="搜索地址名称、详细地址、联系人或电话"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button class="btn-main" type="primary" @click="handleOpenCreate">
|
||||
<el-icon><Plus /></el-icon>新增地址
|
||||
</el-button>
|
||||
@@ -48,7 +34,8 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap dialog-list-body-only">
|
||||
<ListTable table-key="business-pl-transport-address-select-dialog"
|
||||
<ListTable
|
||||
table-key="business-pl-transport-address-select-dialog"
|
||||
class="list-table pl-transport-address-select-table"
|
||||
:data="rows"
|
||||
v-loading="loading"
|
||||
@@ -72,13 +59,7 @@
|
||||
<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="fullAddress" label="详细地址" min-width="280" show-overflow-tooltip />
|
||||
<el-table-column
|
||||
prop="contactName"
|
||||
label="联系人"
|
||||
@@ -95,13 +76,16 @@
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="100"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
fixed="right"
|
||||
class-name="col-actions"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleOpenEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="primary" @click="handleConfirm(row)">
|
||||
<el-icon><Select /></el-icon>选择
|
||||
</el-button>
|
||||
@@ -120,22 +104,22 @@
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="createVisible"
|
||||
title="新增地址"
|
||||
v-model="formVisible"
|
||||
:title="formDialogTitle"
|
||||
width="640px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
class="dialog-form-layout pl-transport-address-create-dialog"
|
||||
class="dialog-form-layout pl-transport-address-form-dialog"
|
||||
modal-class="order-module-dialog-mask"
|
||||
:close-on-click-modal="false"
|
||||
style="backdrop-filter: none"
|
||||
@closed="handleCreateClosed"
|
||||
@closed="handleFormClosed"
|
||||
>
|
||||
<el-form
|
||||
ref="createFormRef"
|
||||
:model="createForm"
|
||||
:rules="createRules"
|
||||
ref="formRef"
|
||||
:model="addressForm"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
class="pl-dialog-form"
|
||||
scroll-to-error
|
||||
@@ -143,33 +127,33 @@
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="地址名称" prop="addressName">
|
||||
<el-input v-model="createForm.addressName" placeholder="如:安庆仓库、某某化工厂" />
|
||||
<el-input v-model="addressForm.addressName" placeholder="如:安庆仓库、某某化工厂" />
|
||||
</el-form-item>
|
||||
<el-form-item label="详细地址" prop="fullAddress" class="form-col-span-2">
|
||||
<AmapAddressPicker
|
||||
v-model="createForm.fullAddress"
|
||||
v-model:lng="createForm.lng"
|
||||
v-model:lat="createForm.lat"
|
||||
v-model="addressForm.fullAddress"
|
||||
v-model:lng="addressForm.lng"
|
||||
v-model:lat="addressForm.lat"
|
||||
placeholder="请地图选点或搜索 POI"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人">
|
||||
<el-input v-model="createForm.contactName" placeholder="联系人(可选)" />
|
||||
<el-input v-model="addressForm.contactName" placeholder="联系人(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="createForm.contactPhone" placeholder="联系电话(可选)" />
|
||||
<el-input v-model="addressForm.contactPhone" placeholder="联系电话(可选)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="createForm.remark" type="textarea" :rows="2" placeholder="备注(可选)" />
|
||||
<el-input v-model="addressForm.remark" type="textarea" :rows="2" placeholder="备注(可选)" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="createVisible = false">
|
||||
<el-button @click="formVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="createLoading" @click="handleCreateSubmit">
|
||||
<el-button type="primary" :loading="formLoading" @click="handleFormSubmit">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -179,7 +163,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, Plus, RefreshLeft, Search, Select } from '@element-plus/icons-vue'
|
||||
import { Check, Close, Edit, Plus, Search, Select } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import {
|
||||
transportAddressApi,
|
||||
@@ -215,11 +199,12 @@ const pagination = reactive({
|
||||
total: 0
|
||||
})
|
||||
|
||||
const createVisible = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const createFormRef = ref<FormInstance>()
|
||||
const formVisible = ref(false)
|
||||
const formLoading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const editingId = ref<string | null>(null)
|
||||
|
||||
const createForm = reactive({
|
||||
const addressForm = reactive({
|
||||
addressName: '',
|
||||
fullAddress: '',
|
||||
lng: null as number | null,
|
||||
@@ -229,11 +214,13 @@ const createForm = reactive({
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const createRules: FormRules = {
|
||||
const formRules: FormRules = {
|
||||
addressName: [{ required: true, message: '请输入地址名称', trigger: 'blur' }],
|
||||
fullAddress: [{ required: true, message: '请填写详细地址', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const formDialogTitle = computed(() => (editingId.value ? '编辑地址' : '新增地址'))
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -256,24 +243,19 @@ function handleSearch() {
|
||||
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 = []
|
||||
createVisible.value = false
|
||||
formVisible.value = false
|
||||
editingId.value = null
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function resetCreateForm() {
|
||||
Object.assign(createForm, {
|
||||
function resetAddressForm() {
|
||||
Object.assign(addressForm, {
|
||||
addressName: '',
|
||||
fullAddress: '',
|
||||
lng: null,
|
||||
@@ -282,45 +264,78 @@ function resetCreateForm() {
|
||||
contactPhone: '',
|
||||
remark: ''
|
||||
})
|
||||
createFormRef.value?.resetFields()
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
function fillAddressForm(row: TransportAddress) {
|
||||
Object.assign(addressForm, {
|
||||
addressName: row.addressName || '',
|
||||
fullAddress: row.fullAddress || '',
|
||||
lng: row.lng != null ? Number(row.lng) : null,
|
||||
lat: row.lat != null ? Number(row.lat) : null,
|
||||
contactName: row.contactName || '',
|
||||
contactPhone: row.contactPhone || '',
|
||||
remark: row.remark || ''
|
||||
})
|
||||
}
|
||||
|
||||
function handleOpenCreate() {
|
||||
resetCreateForm()
|
||||
createVisible.value = true
|
||||
editingId.value = null
|
||||
resetAddressForm()
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
function handleCreateClosed() {
|
||||
resetCreateForm()
|
||||
function handleOpenEdit(row: TransportAddress) {
|
||||
editingId.value = row.id
|
||||
resetAddressForm()
|
||||
fillAddressForm(row)
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
async function handleCreateSubmit() {
|
||||
const valid = await createFormRef.value?.validate().catch(() => false)
|
||||
function handleFormClosed() {
|
||||
editingId.value = null
|
||||
resetAddressForm()
|
||||
}
|
||||
|
||||
function buildPayload(): TransportAddressPayload {
|
||||
return {
|
||||
addressName: addressForm.addressName.trim(),
|
||||
fullAddress: addressForm.fullAddress.trim(),
|
||||
lng: addressForm.lng as number,
|
||||
lat: addressForm.lat as number,
|
||||
contactName: addressForm.contactName.trim() || undefined,
|
||||
contactPhone: addressForm.contactPhone.trim() || undefined,
|
||||
status: '1',
|
||||
remark: addressForm.remark.trim() || undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFormSubmit() {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
if (createForm.lng == null || createForm.lat == null) {
|
||||
if (addressForm.lng == null || addressForm.lat == null) {
|
||||
ElMessage.warning('请通过地图选点填写经纬度')
|
||||
return
|
||||
}
|
||||
|
||||
createLoading.value = true
|
||||
formLoading.value = true
|
||||
try {
|
||||
const payload: TransportAddressPayload = {
|
||||
addressName: createForm.addressName.trim(),
|
||||
fullAddress: createForm.fullAddress.trim(),
|
||||
lng: createForm.lng,
|
||||
lat: createForm.lat,
|
||||
contactName: createForm.contactName.trim() || undefined,
|
||||
contactPhone: createForm.contactPhone.trim() || undefined,
|
||||
status: '1',
|
||||
remark: createForm.remark.trim() || undefined
|
||||
const payload = buildPayload()
|
||||
if (editingId.value) {
|
||||
await transportAddressApi.update(editingId.value, payload)
|
||||
ElMessage.success('编辑成功')
|
||||
formVisible.value = false
|
||||
await fetchList()
|
||||
return
|
||||
}
|
||||
|
||||
const res = await transportAddressApi.create(payload)
|
||||
const created = (res.data ?? res) as TransportAddress
|
||||
ElMessage.success('新增成功')
|
||||
createVisible.value = false
|
||||
formVisible.value = false
|
||||
handleConfirm(created)
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,43 +346,9 @@ function handleConfirm(row: TransportAddress) {
|
||||
</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;
|
||||
min-height: 560px;
|
||||
}
|
||||
|
||||
.dialog-list-panel {
|
||||
@@ -377,4 +358,16 @@ function handleConfirm(row: TransportAddress) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pl-transport-address-select-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pl-transport-address-select-search {
|
||||
width: 360px;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,708 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogTitle"
|
||||
width="1360px"
|
||||
align-center
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
class="dialog-form-layout"
|
||||
modal-class="order-module-dialog-mask"
|
||||
style="backdrop-filter: none"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
v-loading="dialogLoading"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="right"
|
||||
label-width="100px"
|
||||
class="pl-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-3">
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-input v-model="formData.licensePlate" placeholder="请输入车牌号" maxlength="20" />
|
||||
</el-form-item>
|
||||
<el-form-item label="挂车号" prop="trailerNo">
|
||||
<el-input
|
||||
v-model="formData.trailerNo"
|
||||
placeholder="请输入挂车号"
|
||||
maxlength="20"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="载重(吨)" prop="loadCapacity">
|
||||
<el-input-number
|
||||
v-model="formData.loadCapacity"
|
||||
:precision="2"
|
||||
:min="0"
|
||||
placeholder="请输入载重"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="承运商" prop="carrierId">
|
||||
<el-select
|
||||
v-model="formData.carrierId"
|
||||
placeholder="请选择承运商"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
:remote-method="searchCarriers"
|
||||
:loading="carrierSelectLoading"
|
||||
@focus="onCarrierSelectFocus"
|
||||
>
|
||||
<el-option v-for="c in carrierList" :key="c.id" :label="c.carrierName" :value="c.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="车辆状态" prop="vehicleStatus">
|
||||
<el-select v-model="formData.vehicleStatus" placeholder="请选择">
|
||||
<el-option label="可用" value="available" />
|
||||
<el-option label="维修中" value="maintaining" />
|
||||
<el-option label="报废" value="scrapped" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="备注"
|
||||
prop="remark"
|
||||
:class="{ 'form-col-span-2': props.vehicleType === 'self' }"
|
||||
>
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入备注"
|
||||
maxlength="512"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="车辆照片" style="grid-column: 1 / -1">
|
||||
<AttachmentUploadField
|
||||
ref="photoUploadRef"
|
||||
v-model="formData.photoUrls"
|
||||
:limit="1"
|
||||
accept="image/*"
|
||||
folder="pl/vehicle-photo"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="sub-table-section">
|
||||
<div class="sub-table-header">
|
||||
<span class="sub-table-title">关联人员</span>
|
||||
<el-button type="primary" size="small" @click="handleAddPersonnel">
|
||||
<el-icon><Plus /></el-icon>添加人员
|
||||
</el-button>
|
||||
</div>
|
||||
<ListTable
|
||||
table-key="pl-vehicle-form-dialog-personnel"
|
||||
:data="personnelList"
|
||||
stripe
|
||||
class="list-table sub-table"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column label="姓名" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.personnelId"
|
||||
placeholder="请选择人员"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
:remote-method="searchPersonnel"
|
||||
:loading="personnelSelectLoading"
|
||||
@focus="onPersonnelSelectFocus"
|
||||
@change="(id) => handlePersonnelSelect(row, id)"
|
||||
>
|
||||
<el-option
|
||||
v-for="p in getPersonnelOptionsForRow(row)"
|
||||
:key="p.id"
|
||||
:label="formatPersonnelLabel(p)"
|
||||
:value="p.id"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="手机号" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.phoneNo || '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="年龄" min-width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.age ?? '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="人员类型" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.role" placeholder="类型*" @change="handleRoleChange(row)">
|
||||
<el-option label="司机" value="driver" />
|
||||
<el-option label="押运员" value="escort" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.remark" placeholder="备注" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="60" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" @click="handleRemovePersonnel($index)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
</div>
|
||||
|
||||
<PlVehicleCertPartTabs
|
||||
ref="certFormListRef"
|
||||
v-model="certList"
|
||||
:license-plate="formData.licensePlate"
|
||||
:trailer-no="formData.trailerNo"
|
||||
title="车辆证书"
|
||||
upload-folder="pl/vehicle-cert"
|
||||
:cert-name-options="certNameOptions"
|
||||
layout="table"
|
||||
:enforce-self-vehicle-required="props.vehicleType === 'self'"
|
||||
:enforce-carrier-vehicle-required="false"
|
||||
/>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="visible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import PlVehicleCertPartTabs from '@/components/business/pl-vehicle-cert-part-tabs/index.vue'
|
||||
import { Check, Close, Delete, Plus } from '@element-plus/icons-vue'
|
||||
import {
|
||||
vehicleApi,
|
||||
type VehicleDetail,
|
||||
type VehiclePersonnelRow,
|
||||
type CertRow,
|
||||
type CertRead,
|
||||
type Personnel
|
||||
} from '@/api/pl'
|
||||
import {
|
||||
usePlRemoteCarrierOptions,
|
||||
usePlRemotePersonnelOptions
|
||||
} from '@/composables/usePlRemoteOptions'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
import {
|
||||
collectSelfVehicleRequiredCertErrors,
|
||||
ensureSelfVehicleCertSlots,
|
||||
ensureVehicleCertSlots,
|
||||
formatRequiredCertErrors
|
||||
} from '@/utils/pl-cert-slots'
|
||||
import { PL_VEHICLE_CERT_NAME_OPTIONS } from '@/utils/pl-vehicle-cert'
|
||||
import {
|
||||
mapPersonnelTypeToVehicleRole,
|
||||
validatePersonnelTypeRoleCompat,
|
||||
validatePersonnelAffiliationVehicleCompat
|
||||
} from '@/utils/pl-personnel-cert'
|
||||
|
||||
type CertListItem = CertRow & { _key?: string }
|
||||
type CertFormListRef = InstanceType<typeof PlVehicleCertPartTabs>
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
/** carrier=三方,self=自有;锁定类型,不可切换 */
|
||||
vehicleType: 'carrier' | 'self'
|
||||
/** 有值则为编辑,无值则为新增 */
|
||||
vehicleId?: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
/** 新增成功:返回详情;编辑成功:返回详情(父级刷新列表用) */
|
||||
saved: [payload: { mode: 'create' | 'edit'; vehicle: VehicleDetail }]
|
||||
}>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const isEdit = computed(() => Boolean(props.vehicleId))
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
if (isEdit.value) {
|
||||
return props.vehicleType === 'self' ? '编辑自有车辆' : '编辑车辆'
|
||||
}
|
||||
return props.vehicleType === 'self' ? '新增自有车辆' : '新增车辆'
|
||||
})
|
||||
|
||||
const submitLoading = ref(false)
|
||||
const dialogLoading = ref(false)
|
||||
const formRef = ref()
|
||||
const photoUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
const personnelList = ref<VehiclePersonnelRow[]>([])
|
||||
const certList = ref<CertListItem[]>([])
|
||||
const certFormListRef = ref<CertFormListRef>()
|
||||
const certNameOptions = PL_VEHICLE_CERT_NAME_OPTIONS
|
||||
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
licensePlate: '',
|
||||
trailerNo: '',
|
||||
vehicleType: props.vehicleType as 'carrier' | 'self',
|
||||
carrierId: '',
|
||||
carrierName: '',
|
||||
loadCapacity: null as number | null,
|
||||
vehicleStatus: 'available',
|
||||
remark: '',
|
||||
photoUrls: ''
|
||||
})
|
||||
|
||||
const formRules = computed(() => {
|
||||
const rules: Record<string, unknown[]> = {
|
||||
licensePlate: [{ required: true, message: '请输入车牌号', trigger: 'blur' }]
|
||||
}
|
||||
if (props.vehicleType === 'self') {
|
||||
rules.vehicleStatus = [{ required: true, message: '请选择车辆状态', trigger: 'change' }]
|
||||
rules.carrierId = [{ required: true, message: '请选择承运商', trigger: 'change' }]
|
||||
}
|
||||
return rules
|
||||
})
|
||||
|
||||
const {
|
||||
list: carrierList,
|
||||
loading: carrierSelectLoading,
|
||||
search: searchCarriers,
|
||||
onFocus: onCarrierSelectFocus,
|
||||
ensureById: ensureSelectedCarrierOption
|
||||
} = usePlRemoteCarrierOptions({
|
||||
getSelectedId: () => formData.carrierId
|
||||
})
|
||||
|
||||
const {
|
||||
list: personnelOptions,
|
||||
loading: personnelSelectLoading,
|
||||
search: searchPersonnel,
|
||||
onFocus: onPersonnelSelectFocus,
|
||||
invalidate: invalidatePersonnelOptions
|
||||
} = usePlRemotePersonnelOptions({
|
||||
listParams: () => ({
|
||||
status: '1',
|
||||
personnelAffiliation: props.vehicleType === 'carrier' ? 'carrier' : 'self'
|
||||
})
|
||||
})
|
||||
|
||||
function createEmptyPersonnel(): VehiclePersonnelRow {
|
||||
return {
|
||||
personnelId: undefined,
|
||||
personnelName: '',
|
||||
phoneNo: '',
|
||||
age: null,
|
||||
role: 'driver',
|
||||
isDefault: 0,
|
||||
remark: ''
|
||||
}
|
||||
}
|
||||
|
||||
function formatPersonnelLabel(p: Personnel) {
|
||||
return p.phoneNo ? `${p.personnelName}(${p.phoneNo})` : p.personnelName
|
||||
}
|
||||
|
||||
function getPersonnelOptionsForRow(row: VehiclePersonnelRow) {
|
||||
const selectedIds = new Set(
|
||||
personnelList.value
|
||||
.filter((item) => item !== row && item.personnelId)
|
||||
.map((item) => item.personnelId as string)
|
||||
)
|
||||
const options = personnelOptions.value.filter(
|
||||
(p) => !selectedIds.has(p.id) || p.id === row.personnelId
|
||||
)
|
||||
if (row.personnelId && !options.some((p) => p.id === row.personnelId) && row.personnelName) {
|
||||
return [
|
||||
{
|
||||
id: row.personnelId,
|
||||
personnelName: row.personnelName,
|
||||
phoneNo: row.phoneNo,
|
||||
age: row.age ?? undefined,
|
||||
status: '0',
|
||||
createTime: ''
|
||||
} as Personnel,
|
||||
...options
|
||||
]
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
function handlePersonnelSelect(row: VehiclePersonnelRow, personnelId?: string) {
|
||||
if (!personnelId) {
|
||||
row.personnelId = undefined
|
||||
row.personnelName = ''
|
||||
row.phoneNo = ''
|
||||
row.age = null
|
||||
row.role = ''
|
||||
return
|
||||
}
|
||||
const person = personnelOptions.value.find((p) => p.id === personnelId)
|
||||
if (!person) return
|
||||
row.personnelId = person.id
|
||||
row.personnelName = person.personnelName
|
||||
row.phoneNo = person.phoneNo || ''
|
||||
row.age = person.age ?? null
|
||||
const mappedRole = mapPersonnelTypeToVehicleRole(person.personnelType)
|
||||
if (mappedRole) {
|
||||
row.role = mappedRole
|
||||
handleRoleChange(row)
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePersonnelRows(rows: VehiclePersonnelRow[]) {
|
||||
return rows.map((row) => {
|
||||
if (row.personnelId) {
|
||||
handlePersonnelSelect(row, row.personnelId)
|
||||
return row
|
||||
}
|
||||
const matched = personnelOptions.value.find(
|
||||
(p) =>
|
||||
p.personnelName === row.personnelName &&
|
||||
(!row.phoneNo || !p.phoneNo || p.phoneNo === row.phoneNo)
|
||||
)
|
||||
if (matched) {
|
||||
handlePersonnelSelect(row, matched.id)
|
||||
}
|
||||
return row
|
||||
})
|
||||
}
|
||||
|
||||
async function loadPersonnelOptions() {
|
||||
invalidatePersonnelOptions()
|
||||
await searchPersonnel('')
|
||||
}
|
||||
|
||||
async function loadCarrierOptions() {
|
||||
await searchCarriers('')
|
||||
}
|
||||
|
||||
function handleAddPersonnel() {
|
||||
const row = createEmptyPersonnel()
|
||||
if (!personnelList.value.some((p) => p.role === 'driver' && p.isDefault === 1)) {
|
||||
row.isDefault = 1
|
||||
}
|
||||
personnelList.value.push(row)
|
||||
}
|
||||
|
||||
function handleRemovePersonnel(index: number) {
|
||||
const wasDefault = personnelList.value[index]?.isDefault === 1
|
||||
personnelList.value.splice(index, 1)
|
||||
if (wasDefault) {
|
||||
const firstDriver = personnelList.value.find((p) => p.role === 'driver')
|
||||
if (firstDriver) firstDriver.isDefault = 1
|
||||
}
|
||||
}
|
||||
|
||||
function handleRoleChange(row: VehiclePersonnelRow) {
|
||||
if (row.role !== 'driver') {
|
||||
row.isDefault = 0
|
||||
if (!personnelList.value.some((p) => p.role === 'driver' && p.isDefault === 1)) {
|
||||
const firstDriver = personnelList.value.find((p) => p.role === 'driver')
|
||||
if (firstDriver) firstDriver.isDefault = 1
|
||||
}
|
||||
} else if (!personnelList.value.some((p) => p.role === 'driver' && p.isDefault === 1)) {
|
||||
row.isDefault = 1
|
||||
}
|
||||
}
|
||||
|
||||
function resetSubTables() {
|
||||
personnelList.value = []
|
||||
certList.value = []
|
||||
certFormListRef.value?.clearUploadRefs()
|
||||
}
|
||||
|
||||
function resetFormData() {
|
||||
Object.assign(formData, {
|
||||
id: '',
|
||||
licensePlate: '',
|
||||
trailerNo: '',
|
||||
vehicleType: props.vehicleType,
|
||||
carrierId: '',
|
||||
carrierName: '',
|
||||
loadCapacity: null,
|
||||
vehicleStatus: 'available',
|
||||
remark: '',
|
||||
photoUrls: ''
|
||||
})
|
||||
}
|
||||
|
||||
function ensureEmptyCertSlots() {
|
||||
if (props.vehicleType === 'self') {
|
||||
certList.value = ensureSelfVehicleCertSlots([], formData.trailerNo)
|
||||
} else {
|
||||
certList.value = ensureVehicleCertSlots([], formData.trailerNo, 'carrier')
|
||||
}
|
||||
}
|
||||
|
||||
function mapCertRowsForForm(certs: CertRead[] | undefined | null) {
|
||||
return (certs || []).map((c) => ({
|
||||
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
|
||||
certId: c.id || c.certId,
|
||||
vehiclePart: c.vehiclePart,
|
||||
certName: c.certName,
|
||||
certNo: c.certNo,
|
||||
expireDate: c.expireDate ? String(c.expireDate).slice(0, 10) : '',
|
||||
remindDays: c.remindDays ?? 30,
|
||||
attachmentUrls: c.attachmentUrls || ''
|
||||
}))
|
||||
}
|
||||
|
||||
function validateSubTables(): boolean {
|
||||
for (const row of personnelList.value) {
|
||||
if (!row.personnelId) {
|
||||
ElMessage.warning('请选择关联人员')
|
||||
return false
|
||||
}
|
||||
if (!row.role) {
|
||||
ElMessage.warning('请选择人员类型')
|
||||
return false
|
||||
}
|
||||
const person = personnelOptions.value.find((p) => p.id === row.personnelId)
|
||||
if (
|
||||
!validatePersonnelTypeRoleCompat(person?.personnelType, row.role, (message) =>
|
||||
ElMessage.warning(message)
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
!validatePersonnelAffiliationVehicleCompat(
|
||||
person?.personnelAffiliation,
|
||||
props.vehicleType,
|
||||
(message) => ElMessage.warning(message)
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (props.vehicleType === 'self') {
|
||||
const certErrors = collectSelfVehicleRequiredCertErrors(certList.value, formData.trailerNo)
|
||||
if (certErrors.length) {
|
||||
ElMessage.warning(formatRequiredCertErrors(certErrors))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function openCreate() {
|
||||
resetFormData()
|
||||
resetSubTables()
|
||||
ensureEmptyCertSlots()
|
||||
await Promise.all([loadPersonnelOptions(), loadCarrierOptions()])
|
||||
}
|
||||
|
||||
async function openEdit() {
|
||||
const vehicleId = props.vehicleId
|
||||
if (!vehicleId) return
|
||||
|
||||
dialogLoading.value = true
|
||||
try {
|
||||
await loadPersonnelOptions()
|
||||
const res = await vehicleApi.get(vehicleId)
|
||||
const detail = pickBxData<VehicleDetail>(res) || ({} as VehicleDetail)
|
||||
|
||||
if (props.vehicleType === 'self' && detail.vehicleType !== 'self') {
|
||||
ElMessage.warning('仅可编辑自有车辆')
|
||||
visible.value = false
|
||||
return
|
||||
}
|
||||
|
||||
Object.assign(formData, {
|
||||
id: detail.id,
|
||||
licensePlate: detail.licensePlate,
|
||||
trailerNo: detail.trailerNo || '',
|
||||
vehicleType: props.vehicleType,
|
||||
carrierId: detail.carrierId || '',
|
||||
carrierName: detail.carrierName || '',
|
||||
loadCapacity: detail.loadCapacity ?? null,
|
||||
vehicleStatus: detail.vehicleStatus,
|
||||
remark: detail.remark,
|
||||
photoUrls: detail.photoUrls || ''
|
||||
})
|
||||
|
||||
if (formData.carrierId) {
|
||||
await ensureSelectedCarrierOption(formData.carrierId)
|
||||
} else {
|
||||
await loadCarrierOptions()
|
||||
}
|
||||
|
||||
personnelList.value = resolvePersonnelRows(
|
||||
(detail.personnelList || []).map((p) => ({
|
||||
...p,
|
||||
age: p.age ?? null
|
||||
}))
|
||||
)
|
||||
|
||||
// 等 trailerNo 相关 watch 先消化,再回填证书,避免被 hasTrailer 逻辑误清空
|
||||
await nextTick()
|
||||
const sourceCerts = detail.certList || []
|
||||
if (props.vehicleType === 'self') {
|
||||
certList.value = ensureSelfVehicleCertSlots(
|
||||
mapCertRowsForForm(sourceCerts),
|
||||
formData.trailerNo
|
||||
)
|
||||
} else {
|
||||
certList.value = ensureVehicleCertSlots(
|
||||
mapCertRowsForForm(sourceCerts),
|
||||
formData.trailerNo,
|
||||
'carrier'
|
||||
)
|
||||
}
|
||||
await nextTick()
|
||||
certFormListRef.value?.refreshRequiredSlots?.()
|
||||
} catch {
|
||||
visible.value = false
|
||||
ElMessage.error('加载车辆详情失败')
|
||||
} finally {
|
||||
dialogLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openDialog() {
|
||||
if (isEdit.value) {
|
||||
await openEdit()
|
||||
} else {
|
||||
await openCreate()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
if (!valid || !validateSubTables()) return
|
||||
if (certFormListRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('证书附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
if (photoUploadRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.carrierId) {
|
||||
const carrier = carrierList.value.find((c) => c.id === formData.carrierId)
|
||||
formData.carrierName = carrier?.carrierName || ''
|
||||
} else {
|
||||
formData.carrierName = ''
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
licensePlate: formData.licensePlate,
|
||||
trailerNo: formData.trailerNo || undefined,
|
||||
vehicleType: props.vehicleType,
|
||||
carrierId: formData.carrierId || undefined,
|
||||
carrierName: formData.carrierName || undefined,
|
||||
loadCapacity: formData.loadCapacity,
|
||||
vehicleStatus: formData.vehicleStatus,
|
||||
remark: formData.remark,
|
||||
photoUrls: formData.photoUrls ?? '',
|
||||
personnelList: personnelList.value.map((p) => ({
|
||||
personnelId: p.personnelId,
|
||||
personnelName: p.personnelName.trim(),
|
||||
phoneNo: p.phoneNo || undefined,
|
||||
age: p.age ?? undefined,
|
||||
role: p.role,
|
||||
isDefault: p.role === 'driver' ? (p.isDefault ?? 0) : 0,
|
||||
remark: p.remark || undefined
|
||||
})),
|
||||
certList: certList.value.map((c) => ({
|
||||
certId: c.certId,
|
||||
vehiclePart: c.vehiclePart,
|
||||
certName: c.certName.trim(),
|
||||
certNo: c.certNo || undefined,
|
||||
expireDate: c.expireDate || undefined,
|
||||
remindDays: c.remindDays ?? 30,
|
||||
attachmentUrls: c.attachmentUrls || undefined
|
||||
}))
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await vehicleApi.update(formData.id, payload)
|
||||
const res = await vehicleApi.get(formData.id)
|
||||
const detail = pickBxData<VehicleDetail>(res)
|
||||
if (!detail?.id) {
|
||||
ElMessage.error('编辑成功但未返回车辆信息')
|
||||
return
|
||||
}
|
||||
ElMessage.success('编辑成功')
|
||||
emit('saved', { mode: 'edit', vehicle: detail })
|
||||
} else {
|
||||
const res = await vehicleApi.create(payload)
|
||||
const detail = pickBxData<VehicleDetail>(res)
|
||||
if (!detail?.id) {
|
||||
ElMessage.error('新增成功但未返回车辆信息')
|
||||
return
|
||||
}
|
||||
ElMessage.success('新增成功')
|
||||
emit('saved', { mode: 'create', vehicle: detail })
|
||||
}
|
||||
visible.value = false
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDialogClose() {
|
||||
formRef.value?.resetFields()
|
||||
resetSubTables()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (open) => {
|
||||
if (open) {
|
||||
await openDialog()
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.sub-table-section {
|
||||
margin-top: 16px;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.sub-table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sub-table-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text, #1e293b);
|
||||
}
|
||||
|
||||
.sub-table {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,294 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="innerVisible"
|
||||
title="从车辆库选择"
|
||||
width="1360px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
class="dialog-form-layout pl-vehicle-library-select-dialog"
|
||||
modal-class="order-module-dialog-mask"
|
||||
:close-on-click-modal="false"
|
||||
style="backdrop-filter: none"
|
||||
@open="handleOpen"
|
||||
>
|
||||
<div class="panel dialog-list-panel">
|
||||
<el-tabs v-model="activeTab" class="pl-vehicle-library-select-tabs" @tab-change="handleTabChange">
|
||||
<el-tab-pane label="三方车辆" name="carrier" />
|
||||
<el-tab-pane label="自有车辆" name="self" />
|
||||
</el-tabs>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row pl-vehicle-library-select-toolbar">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
class="pl-vehicle-library-select-search"
|
||||
placeholder="搜索车牌号"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button class="btn-main" type="primary" @click="handleOpenCreate">
|
||||
<el-icon><Plus /></el-icon>新增车辆
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap dialog-list-body-only">
|
||||
<ListTable
|
||||
table-key="business-pl-vehicle-library-select-dialog"
|
||||
class="list-table pl-vehicle-library-select-table"
|
||||
:data="rows"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
highlight-current-row
|
||||
:max-height="560"
|
||||
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="licensePlate" label="车牌号" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="handleConfirm(row)">{{ row.licensePlate }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="trailerNo"
|
||||
label="挂车号"
|
||||
min-width="110"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
label="车辆状态"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlVehicleStatusTagType(row.vehicleStatus)" size="small">
|
||||
{{ getPlVehicleStatusLabel(row.vehicleStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="loadCapacity"
|
||||
label="载重(吨)"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="activeTab === 'carrier'"
|
||||
prop="carrierName"
|
||||
label="承运商"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="personnelSummary"
|
||||
label="关联人员摘要"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ normalizePlPersonnelSummaryDisplay(row.personnelSummary) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
fixed="right"
|
||||
class-name="col-actions"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleOpenEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="primary" @click="handleConfirm(row)">
|
||||
<el-icon><Select /></el-icon>选择
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
|
||||
<ListPagination
|
||||
v-show="pagination.total > 0"
|
||||
:total="pagination.total"
|
||||
v-model:page="pagination.page"
|
||||
v-model:limit="pagination.pageSize"
|
||||
@pagination="fetchList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PlVehicleFormDialog
|
||||
v-model="formVisible"
|
||||
:vehicle-type="activeTab"
|
||||
:vehicle-id="editingId"
|
||||
@saved="handleFormSaved"
|
||||
/>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Edit, Plus, Search, Select } from '@element-plus/icons-vue'
|
||||
import { vehicleApi, type Vehicle, type VehicleDetail } from '@/api/pl/index'
|
||||
import PlVehicleFormDialog from '@/components/business/pl-vehicle-form-dialog/index.vue'
|
||||
import ListPagination from '@/components/ListPagination/index.vue'
|
||||
import { pickBxPageResult } from '@/utils/http/bx'
|
||||
import {
|
||||
getPlVehicleStatusLabel,
|
||||
getPlVehicleStatusTagType,
|
||||
normalizePlPersonnelSummaryDisplay
|
||||
} from '@/utils/pl-display'
|
||||
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
||||
|
||||
type VehicleTab = 'carrier' | 'self'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
select: [row: Vehicle]
|
||||
}>()
|
||||
|
||||
const innerVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: boolean) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const activeTab = ref<VehicleTab>('carrier')
|
||||
const rows = ref<Vehicle[]>([])
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: DEFAULT_LIST_PAGE_SIZE,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const formVisible = ref(false)
|
||||
const editingId = ref<string | null>(null)
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleApi.list({
|
||||
vehicleType: activeTab.value,
|
||||
q: keyword.value.trim() || undefined,
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const { list, total } = pickBxPageResult<Vehicle>(res)
|
||||
rows.value = list
|
||||
pagination.total = total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleTabChange() {
|
||||
pagination.page = 1
|
||||
formVisible.value = false
|
||||
editingId.value = null
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleOpen() {
|
||||
keyword.value = ''
|
||||
activeTab.value = 'carrier'
|
||||
pagination.page = 1
|
||||
pagination.pageSize = DEFAULT_LIST_PAGE_SIZE
|
||||
pagination.total = 0
|
||||
rows.value = []
|
||||
formVisible.value = false
|
||||
editingId.value = null
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleOpenCreate() {
|
||||
editingId.value = null
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
function handleOpenEdit(row: Vehicle) {
|
||||
editingId.value = row.id
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
function handleFormSaved(payload: { mode: 'create' | 'edit'; vehicle: VehicleDetail }) {
|
||||
formVisible.value = false
|
||||
editingId.value = null
|
||||
if (payload.mode === 'create') {
|
||||
handleConfirm(payload.vehicle)
|
||||
return
|
||||
}
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleConfirm(row: Vehicle) {
|
||||
emit('select', row)
|
||||
innerVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pl-vehicle-library-select-dialog {
|
||||
:deep(.el-dialog__body) {
|
||||
min-height: 560px;
|
||||
}
|
||||
|
||||
.dialog-list-panel {
|
||||
.list-toolbar {
|
||||
margin-bottom: 0;
|
||||
padding: 10px 0 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pl-vehicle-library-select-tabs {
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-vehicle-library-select-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pl-vehicle-library-select-search {
|
||||
width: 280px;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,7 @@
|
||||
clearable
|
||||
style="width: 100%"
|
||||
@focus="handleFocus"
|
||||
@change="emit('change', $event)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in displayOptions"
|
||||
@@ -34,9 +35,15 @@
|
||||
|
||||
const model = defineModel<string>({ default: '' })
|
||||
|
||||
withDefaults(
|
||||
const emit = defineEmits<{
|
||||
change: [value: string]
|
||||
}>()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
placeholder?: string
|
||||
/** 透传车辆列表筛选,如 available */
|
||||
vehicleStatus?: string
|
||||
}>(),
|
||||
{
|
||||
placeholder: '请选择车牌号(三方/自有车辆)'
|
||||
@@ -58,6 +65,7 @@
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleApi.list({
|
||||
...(props.vehicleStatus ? { vehicleStatus: props.vehicleStatus } : {}),
|
||||
...(keyword.trim() ? { q: keyword.trim() } : {}),
|
||||
page: 1,
|
||||
pageSize: 100
|
||||
|
||||
Reference in new issue
Block a user