提交
This commit is contained in:
1 parent
2716c2b6f8
commit
f3693be913
140 files changed
+799199
-11262
No files matched your search
@@ -450,11 +450,13 @@ export interface Plan {
|
||||
pickupLng?: number | null
|
||||
pickupLat?: number | null
|
||||
pickupTime?: string
|
||||
pickupContact?: string
|
||||
pickupNotes?: string
|
||||
destinationAddress?: string
|
||||
destinationLng?: number | null
|
||||
destinationLat?: number | null
|
||||
deliveryTime?: string
|
||||
deliveryContact?: string
|
||||
destinationNotes?: string
|
||||
vehicleId?: string
|
||||
licensePlate?: string
|
||||
@@ -488,8 +490,13 @@ export interface PlanQuery {
|
||||
archiveStatus?: string
|
||||
customerName?: string
|
||||
productName?: string
|
||||
licensePlate?: string
|
||||
planDateFrom?: string
|
||||
planDateTo?: string
|
||||
/** 1=仅含易制毒/易制爆产品的计划 */
|
||||
hasHazardProduct?: number
|
||||
/** incomplete=至少一张必需危化证书未上传 */
|
||||
certUploadStatus?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
@@ -538,11 +545,13 @@ export interface PlanCreateFullPayload {
|
||||
pickupLng?: number | null
|
||||
pickupLat?: number | null
|
||||
pickupTime?: string
|
||||
pickupContact?: string
|
||||
pickupNotes?: string
|
||||
destinationAddress?: string
|
||||
destinationLng?: number | null
|
||||
destinationLat?: number | null
|
||||
deliveryTime?: string
|
||||
deliveryContact?: string
|
||||
destinationNotes?: string
|
||||
followUserId?: string
|
||||
followUserName?: string
|
||||
@@ -730,18 +739,20 @@ export interface Transport {
|
||||
|
||||
export interface TransportArrange {
|
||||
vehicleId: string
|
||||
driverId: string
|
||||
driverId?: string
|
||||
carrierId?: string
|
||||
transportMedium?: string
|
||||
pickupAddress?: string
|
||||
pickupLng?: number | null
|
||||
pickupLat?: number | null
|
||||
pickupTime?: string
|
||||
pickupContact?: string
|
||||
pickupNotes?: string
|
||||
destinationAddress?: string
|
||||
destinationLng?: number | null
|
||||
destinationLat?: number | null
|
||||
deliveryTime?: string
|
||||
deliveryContact?: string
|
||||
destinationNotes?: string
|
||||
distanceKm?: number
|
||||
unit?: string
|
||||
@@ -842,12 +853,14 @@ export interface PlIoOrder {
|
||||
pickupLng?: number | null
|
||||
pickupLat?: number | null
|
||||
pickupTime?: string
|
||||
pickupContact?: string
|
||||
pickupNotes?: string
|
||||
destinationName?: string
|
||||
destinationAddress?: string
|
||||
destinationLng?: number | null
|
||||
destinationLat?: number | null
|
||||
deliveryTime?: string
|
||||
deliveryContact?: string
|
||||
destinationNotes?: string
|
||||
siteAccessToken?: string
|
||||
siteUrl?: string
|
||||
|
||||
@@ -38,17 +38,20 @@ export interface PublicIoOrderPage {
|
||||
pickupLng?: number | null
|
||||
pickupLat?: number | null
|
||||
pickupTime?: string
|
||||
pickupContact?: string
|
||||
pickupNotes?: string
|
||||
destinationName?: string
|
||||
destinationAddress?: string
|
||||
destinationLng?: number | null
|
||||
destinationLat?: number | null
|
||||
deliveryTime?: string
|
||||
deliveryContact?: string
|
||||
destinationNotes?: string
|
||||
siteFeedbackRemark?: string
|
||||
siteFeedbackImages?: string[]
|
||||
siteFeedbackTime?: string
|
||||
voided: boolean
|
||||
archived?: boolean
|
||||
items: PublicIoOrderItem[]
|
||||
}
|
||||
|
||||
|
||||
@@ -12,18 +12,21 @@ export interface PublicTransportSitePage {
|
||||
pickupLng?: number | null
|
||||
pickupLat?: number | null
|
||||
pickupTime?: string
|
||||
pickupContact?: string
|
||||
pickupNotes?: string
|
||||
destinationName?: string
|
||||
destinationAddress?: string
|
||||
destinationLng?: number | null
|
||||
destinationLat?: number | null
|
||||
deliveryTime?: string
|
||||
deliveryContact?: string
|
||||
destinationNotes?: string
|
||||
products: PublicTransportProduct[]
|
||||
siteFeedbackRemark?: string
|
||||
siteFeedbackImages: string[]
|
||||
siteFeedbackTime?: string
|
||||
voided: boolean
|
||||
archived?: boolean
|
||||
}
|
||||
|
||||
export const publicPlSiteApi = {
|
||||
|
||||
@@ -37,12 +37,14 @@ export interface PublicTransportPage {
|
||||
pickupLng?: number | null
|
||||
pickupLat?: number | null
|
||||
pickupTime?: string
|
||||
pickupContact?: string
|
||||
pickupNotes?: string
|
||||
destinationName?: string
|
||||
destinationAddress?: string
|
||||
destinationLng?: number | null
|
||||
destinationLat?: number | null
|
||||
deliveryTime?: string
|
||||
deliveryContact?: string
|
||||
destinationNotes?: string
|
||||
licensePlate?: string
|
||||
driverName?: string
|
||||
@@ -54,6 +56,7 @@ export interface PublicTransportPage {
|
||||
feedbackQty?: number | null
|
||||
feedbackTime?: string
|
||||
planVoided?: boolean
|
||||
planArchived?: boolean
|
||||
}
|
||||
|
||||
export interface PublicUploadResult {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -115,7 +115,9 @@ export function usePlPlanArrangeActions() {
|
||||
destinationAddress?: string
|
||||
destinationLng?: number | null
|
||||
destinationLat?: number | null
|
||||
pickupContact?: string
|
||||
pickupNotes?: string
|
||||
deliveryContact?: string
|
||||
destinationNotes?: string
|
||||
licensePlate?: string
|
||||
driverUrl?: string
|
||||
@@ -160,7 +162,13 @@ export function usePlPlanArrangeActions() {
|
||||
)
|
||||
)
|
||||
|
||||
lines.push(
|
||||
`取货联系方式:${options?.pickupContact || plan.value?.pickupContact || '—'}`
|
||||
)
|
||||
lines.push(`取货注意事项:${options?.pickupNotes || plan.value?.pickupNotes || '—'}`)
|
||||
lines.push(
|
||||
`送货联系方式:${options?.deliveryContact || plan.value?.deliveryContact || '—'}`
|
||||
)
|
||||
lines.push(`送货注意事项:${options?.destinationNotes || plan.value?.destinationNotes || '—'}`)
|
||||
lines.push(`司机链接:${options?.driverUrl || driverUrl.value || '—'}`)
|
||||
return lines.join('\n')
|
||||
@@ -212,7 +220,9 @@ export function usePlPlanArrangeActions() {
|
||||
destinationAddress?: string
|
||||
destinationLng?: number | null
|
||||
destinationLat?: number | null
|
||||
pickupContact?: string
|
||||
pickupNotes?: string
|
||||
deliveryContact?: string
|
||||
destinationNotes?: string
|
||||
licensePlate?: string
|
||||
driverUrl?: string
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import {
|
||||
carrierApi,
|
||||
personnelApi,
|
||||
vehicleApi,
|
||||
type Carrier,
|
||||
type Personnel,
|
||||
type PersonnelQuery,
|
||||
type Vehicle,
|
||||
type VehicleDetail,
|
||||
type VehicleQuery
|
||||
} from '@/api/pl'
|
||||
import { pickBxData, pickBxList } from '@/utils/http/bx'
|
||||
|
||||
const REMOTE_PAGE_SIZE = 100
|
||||
|
||||
type IdItem = { id: string }
|
||||
|
||||
function keepSelected<T extends IdItem>(
|
||||
rows: T[],
|
||||
selectedId: string | undefined,
|
||||
currentList: T[]
|
||||
): T[] {
|
||||
if (!selectedId) return rows
|
||||
if (rows.some(item => item.id === selectedId)) return rows
|
||||
const kept = currentList.find(item => item.id === selectedId)
|
||||
return kept ? [kept, ...rows] : rows
|
||||
}
|
||||
|
||||
function keepSelectedByPlate(
|
||||
rows: Vehicle[],
|
||||
selectedPlate: string | undefined,
|
||||
currentList: Vehicle[]
|
||||
): Vehicle[] {
|
||||
const plate = selectedPlate?.trim()
|
||||
if (!plate) return rows
|
||||
if (rows.some(item => item.licensePlate === plate)) return rows
|
||||
const kept = currentList.find(item => item.licensePlate === plate)
|
||||
return kept ? [kept, ...rows] : rows
|
||||
}
|
||||
|
||||
/** 车辆下拉:服务端按车牌 q 检索 */
|
||||
export function usePlRemoteVehicleOptions(options?: {
|
||||
getSelectedId?: () => string | undefined
|
||||
getSelectedPlate?: () => string | undefined
|
||||
/** 默认按 id 保留已选项;选车牌字符串时传 licensePlate */
|
||||
valueMode?: 'id' | 'licensePlate'
|
||||
listParams?: Omit<VehicleQuery, 'q' | 'page' | 'pageSize'>
|
||||
}) {
|
||||
const list = ref<Vehicle[]>([])
|
||||
const loading = ref(false)
|
||||
const loaded = ref(false)
|
||||
const valueMode = options?.valueMode ?? 'id'
|
||||
|
||||
async function search(keyword: string) {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleApi.list({
|
||||
...(options?.listParams || {}),
|
||||
...(keyword.trim() ? { q: keyword.trim() } : {}),
|
||||
page: 1,
|
||||
pageSize: REMOTE_PAGE_SIZE
|
||||
})
|
||||
const rows = pickBxList<Vehicle>(res)
|
||||
list.value =
|
||||
valueMode === 'licensePlate'
|
||||
? keepSelectedByPlate(rows, options?.getSelectedPlate?.(), list.value)
|
||||
: keepSelected(rows, options?.getSelectedId?.(), list.value)
|
||||
loaded.value = true
|
||||
} catch (error) {
|
||||
console.error('加载车辆选项失败', error)
|
||||
list.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onFocus() {
|
||||
if (!loaded.value) void search('')
|
||||
}
|
||||
|
||||
function reset() {
|
||||
list.value = []
|
||||
loaded.value = false
|
||||
}
|
||||
|
||||
async function ensureById(vehicleId: string) {
|
||||
if (!vehicleId || list.value.some(item => item.id === vehicleId)) return
|
||||
try {
|
||||
const res = await vehicleApi.get(vehicleId)
|
||||
const detail = pickBxData<VehicleDetail>(res)
|
||||
if (detail?.id) list.value = [detail, ...list.value]
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return { list, loading, loaded, search, onFocus, reset, ensureById }
|
||||
}
|
||||
|
||||
/** 承运商下拉:服务端按 q 检索 */
|
||||
export function usePlRemoteCarrierOptions(options?: {
|
||||
getSelectedId?: () => string | undefined
|
||||
listParams?: { status?: string; auditStatus?: string }
|
||||
}) {
|
||||
const list = ref<Carrier[]>([])
|
||||
const loading = ref(false)
|
||||
const loaded = ref(false)
|
||||
|
||||
async function search(keyword: string) {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await carrierApi.list({
|
||||
...(options?.listParams || {}),
|
||||
...(keyword.trim() ? { q: keyword.trim() } : {}),
|
||||
page: 1,
|
||||
pageSize: REMOTE_PAGE_SIZE
|
||||
})
|
||||
list.value = keepSelected(
|
||||
pickBxList<Carrier>(res),
|
||||
options?.getSelectedId?.(),
|
||||
list.value
|
||||
)
|
||||
loaded.value = true
|
||||
} catch (error) {
|
||||
console.error('加载承运商选项失败', error)
|
||||
list.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onFocus() {
|
||||
if (!loaded.value) void search('')
|
||||
}
|
||||
|
||||
function reset() {
|
||||
list.value = []
|
||||
loaded.value = false
|
||||
}
|
||||
|
||||
async function ensureById(carrierId: string) {
|
||||
if (!carrierId || list.value.some(item => item.id === carrierId)) return
|
||||
try {
|
||||
const res = await carrierApi.get(carrierId)
|
||||
const detail = pickBxData<Carrier>(res)
|
||||
if (detail?.id) list.value = [detail, ...list.value]
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return { list, loading, loaded, search, onFocus, reset, ensureById }
|
||||
}
|
||||
|
||||
/** 人员下拉:服务端按姓名/手机号 q 检索 */
|
||||
export function usePlRemotePersonnelOptions(options?: {
|
||||
getSelectedId?: () => string | undefined
|
||||
listParams?: Omit<PersonnelQuery, 'q' | 'page' | 'pageSize'> | (() => Omit<PersonnelQuery, 'q' | 'page' | 'pageSize'>)
|
||||
}) {
|
||||
const list = ref<Personnel[]>([])
|
||||
const loading = ref(false)
|
||||
const loaded = ref(false)
|
||||
|
||||
async function search(keyword: string) {
|
||||
loading.value = true
|
||||
try {
|
||||
const extra =
|
||||
typeof options?.listParams === 'function' ? options.listParams() : options?.listParams || {}
|
||||
const res = await personnelApi.list({
|
||||
...extra,
|
||||
...(keyword.trim() ? { q: keyword.trim() } : {}),
|
||||
page: 1,
|
||||
pageSize: REMOTE_PAGE_SIZE
|
||||
})
|
||||
list.value = keepSelected(
|
||||
pickBxList<Personnel>(res),
|
||||
options?.getSelectedId?.(),
|
||||
list.value
|
||||
)
|
||||
loaded.value = true
|
||||
} catch (error) {
|
||||
console.error('加载人员选项失败', error)
|
||||
list.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onFocus() {
|
||||
if (!loaded.value) void search('')
|
||||
}
|
||||
|
||||
function reset() {
|
||||
list.value = []
|
||||
loaded.value = false
|
||||
}
|
||||
|
||||
/** 切换归属等过滤条件后需重新拉取 */
|
||||
function invalidate() {
|
||||
loaded.value = false
|
||||
}
|
||||
|
||||
return { list, loading, loaded, search, onFocus, reset, invalidate }
|
||||
}
|
||||
@@ -88,6 +88,15 @@ export function getPlanProductSpec(product: { model?: string; packageSpec?: stri
|
||||
return product.model || product.packageSpec || '—'
|
||||
}
|
||||
|
||||
/** 列表「型号/规格」列:仅型号,不用包装规格兜底 */
|
||||
export function getPlanProductModel(product: { model?: string | null }) {
|
||||
return product.model || '—'
|
||||
}
|
||||
|
||||
export function getPlanProductPackageSpec(product: { packageSpec?: string | null }) {
|
||||
return product.packageSpec || '—'
|
||||
}
|
||||
|
||||
export function getPlanProductQuantity(product: {
|
||||
quantity?: number | string | null
|
||||
actualQty?: number | string | null
|
||||
@@ -112,6 +121,15 @@ export function getPlanProductPlannedQuantity(product: {
|
||||
return String(qty)
|
||||
}
|
||||
|
||||
/** 列表「实际数量」列:仅实际数量,无回退 */
|
||||
export function getPlanProductActualQuantity(product: {
|
||||
actualQty?: number | string | null
|
||||
}) {
|
||||
const qty = product.actualQty
|
||||
if (qty === undefined || qty === null || qty === '') return '—'
|
||||
return String(qty)
|
||||
}
|
||||
|
||||
export function formatPlanProductListText(
|
||||
product: {
|
||||
productName?: string
|
||||
@@ -306,6 +324,9 @@ export function canPrintSlipCommand(planType: string | undefined, command: PlPri
|
||||
|
||||
export type PlPlanTypeTab = 'all' | 'inbound' | 'outbound' | 'direct'
|
||||
|
||||
/** 危化品证上传页 Tab(在单据类型后增加「未上传证书」) */
|
||||
export type PlPlanCertTab = PlPlanTypeTab | 'cert_incomplete'
|
||||
|
||||
export const PL_PLAN_TYPE_TABS: { label: string; name: PlPlanTypeTab }[] = [
|
||||
{ label: '全部', name: 'all' },
|
||||
{ label: '入库单', name: 'inbound' },
|
||||
@@ -313,10 +334,25 @@ export const PL_PLAN_TYPE_TABS: { label: string; name: PlPlanTypeTab }[] = [
|
||||
{ label: '直销单', name: 'direct' }
|
||||
]
|
||||
|
||||
export const PL_PLAN_CERT_TABS: { label: string; name: PlPlanCertTab }[] = [
|
||||
...PL_PLAN_TYPE_TABS,
|
||||
{ label: '未上传证书', name: 'cert_incomplete' }
|
||||
]
|
||||
|
||||
export function plPlanTypeTabToFilter(tab: PlPlanTypeTab): string {
|
||||
return tab === 'all' ? '' : tab
|
||||
}
|
||||
|
||||
export function plPlanCertTabToFilters(tab: PlPlanCertTab): {
|
||||
planType: string
|
||||
certUploadStatus?: string
|
||||
} {
|
||||
if (tab === 'cert_incomplete') {
|
||||
return { planType: '', certUploadStatus: 'incomplete' }
|
||||
}
|
||||
return { planType: plPlanTypeTabToFilter(tab) }
|
||||
}
|
||||
|
||||
export function plPlanTypeFilterToTab(planType?: string): PlPlanTypeTab {
|
||||
if (planType === 'inbound' || planType === 'outbound' || planType === 'direct') {
|
||||
return planType
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</div>
|
||||
<div class="detail-actions">
|
||||
<el-button
|
||||
v-if="detailContract?.status === 'draft'"
|
||||
v-if="isContractEditableStatus(detailContract?.status)"
|
||||
class="btn-ghost"
|
||||
@click="handleDetailEdit"
|
||||
>
|
||||
@@ -850,7 +850,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, defineAsyncComponent, nextTick } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { isFrameworkContractType } from '../contractType'
|
||||
import { isFrameworkContractType, isContractEditableStatus, getContractReeditConfirmMessage } from '../contractType'
|
||||
import {
|
||||
CircleCheck,
|
||||
CircleClose,
|
||||
@@ -1244,12 +1244,20 @@ const handleDetailDrawerClose = () => {
|
||||
};
|
||||
|
||||
/** 详情抽屉 - 编辑 */
|
||||
const handleDetailEdit = () => {
|
||||
const handleDetailEdit = async () => {
|
||||
if (!detailContract.value?.id) return
|
||||
if (detailContract.value.status !== 'draft') {
|
||||
$modal.msgWarning('只能修改草稿状态的合同')
|
||||
if (!isContractEditableStatus(detailContract.value.status)) {
|
||||
$modal.msgWarning('仅草稿、审批中或已审批(未执行)的合同才能修改')
|
||||
return
|
||||
}
|
||||
const confirmMsg = getContractReeditConfirmMessage(detailContract.value.status)
|
||||
if (confirmMsg) {
|
||||
try {
|
||||
await $modal.confirm(confirmMsg)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
visible.value = false
|
||||
formDialogEditId.value = detailContract.value.id
|
||||
formDialogVisible.value = true
|
||||
|
||||
@@ -10,3 +10,26 @@ export type FrameworkContractType = (typeof FRAMEWORK_CONTRACT_TYPES)[number]
|
||||
export function isFrameworkContractType(type?: string | null): boolean {
|
||||
return FRAMEWORK_CONTRACT_TYPES.includes((type || '').trim() as FrameworkContractType)
|
||||
}
|
||||
|
||||
/** 允许打开编辑的合同状态(保存后回草稿) */
|
||||
export const CONTRACT_EDITABLE_STATUSES = ['draft', 'approving', 'approved'] as const
|
||||
|
||||
export type ContractEditableStatus = (typeof CONTRACT_EDITABLE_STATUSES)[number]
|
||||
|
||||
export function isContractEditableStatus(status?: string | null): boolean {
|
||||
return CONTRACT_EDITABLE_STATUSES.includes(
|
||||
(status || '').trim() as ContractEditableStatus
|
||||
)
|
||||
}
|
||||
|
||||
/** 审批中 / 已审批打开编辑前的确认文案;草稿返回 null(无需确认) */
|
||||
export function getContractReeditConfirmMessage(status?: string | null): string | null {
|
||||
const s = (status || '').trim()
|
||||
if (s === 'approving') {
|
||||
return '编辑保存后将作废当前审批流,合同变回草稿,需重新提交审批'
|
||||
}
|
||||
if (s === 'approved') {
|
||||
return '编辑保存后合同将变回草稿,需重新提交审批'
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -24,7 +24,7 @@
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleEdit"
|
||||
v-if="contract?.status === 'draft'"
|
||||
v-if="isContractEditableStatus(contract?.status)"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>修改
|
||||
</el-button>
|
||||
@@ -1033,7 +1033,7 @@ import ImagePreview from '@/components/ImagePreview/index.vue';
|
||||
import TinyMceEditor from '@/components/TinyMceEditor/index.vue';
|
||||
import { getOpportunity } from '@/api/crm/opportunity';
|
||||
import { getQuote } from '@/api/crm/quote';
|
||||
import { isFrameworkContractType } from './contractType';
|
||||
import { isFrameworkContractType, isContractEditableStatus, getContractReeditConfirmMessage } from './contractType';
|
||||
import {
|
||||
parseCrmAttachmentField,
|
||||
resolveCrmAttachmentFiles,
|
||||
@@ -1296,12 +1296,24 @@ const goBack = () => {
|
||||
};
|
||||
|
||||
/** 编辑 */
|
||||
const handleEdit = () => {
|
||||
const handleEdit = async () => {
|
||||
if (!isContractEditableStatus(contract.value?.status)) {
|
||||
$modal.msgWarning('仅草稿、审批中或已审批(未执行)的合同才能修改')
|
||||
return
|
||||
}
|
||||
const confirmMsg = getContractReeditConfirmMessage(contract.value?.status)
|
||||
if (confirmMsg) {
|
||||
try {
|
||||
await $modal.confirm(confirmMsg)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
router.push({
|
||||
path: '/crm/contract/form',
|
||||
query: { id: contract.value?.id }
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除合同 */
|
||||
const handleDelete = async () => {
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="single || selectedRows[0]?.status !== 'draft'"
|
||||
:disabled="single || !isContractEditableStatus(selectedRows[0]?.status)"
|
||||
@click="handleUpdate()"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
@@ -516,6 +516,7 @@ import { $modal } from '@/utils/modal'
|
||||
import { parseTime } from '@/utils/format'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { isContractEditableStatus, getContractReeditConfirmMessage } from './contractType'
|
||||
import ApprovalDialog from './components/ApprovalDialog.vue'
|
||||
import ExecuteDialog from './components/ExecuteDialog.vue'
|
||||
import InvalidDialog from './components/InvalidDialog.vue'
|
||||
@@ -1003,24 +1004,32 @@ const handleAdd = () => {
|
||||
};
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = (row?: ContractVO) => {
|
||||
const handleUpdate = async (row?: ContractVO) => {
|
||||
if (!row && ids.value.length !== 1) {
|
||||
$modal.msgWarning('请选择一条数据进行修改');
|
||||
return;
|
||||
$modal.msgWarning('请选择一条数据进行修改')
|
||||
return
|
||||
}
|
||||
const targetRow = row || contractList.value[contractList.value.findIndex((item) => item.id === ids.value[0])];
|
||||
const targetRow =
|
||||
row || contractList.value[contractList.value.findIndex((item) => item.id === ids.value[0])]
|
||||
if (!targetRow) {
|
||||
$modal.msgWarning('未找到选中的数据');
|
||||
return;
|
||||
$modal.msgWarning('未找到选中的数据')
|
||||
return
|
||||
}
|
||||
// 检查状态,只能修改草稿状态的合同
|
||||
if (targetRow.status !== 'draft') {
|
||||
$modal.msgWarning('只能修改草稿状态的合同');
|
||||
return;
|
||||
if (!isContractEditableStatus(targetRow.status)) {
|
||||
$modal.msgWarning('仅草稿、审批中或已审批(未执行)的合同才能修改')
|
||||
return
|
||||
}
|
||||
const confirmMsg = getContractReeditConfirmMessage(targetRow.status)
|
||||
if (confirmMsg) {
|
||||
try {
|
||||
await $modal.confirm(confirmMsg)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
formDialogEditId.value = targetRow.id
|
||||
formDialogVisible.value = true
|
||||
};
|
||||
}
|
||||
|
||||
/** 表单对话框保存/提交成功回调 */
|
||||
const handleFormSuccess = () => {
|
||||
|
||||
@@ -546,11 +546,12 @@
|
||||
fullSpan: false
|
||||
},
|
||||
{
|
||||
label: '注意事项',
|
||||
value: editForm.value?.pickupNotes || '—',
|
||||
label: '取货联系方式',
|
||||
value: editForm.value?.pickupContact || '—',
|
||||
long: false,
|
||||
fullSpan: false
|
||||
}
|
||||
},
|
||||
{ label: '注意事项', value: editForm.value?.pickupNotes || '—', span: 'full' }
|
||||
])
|
||||
|
||||
const deliveryViewFields = computed<ViewField[]>(() => [
|
||||
@@ -560,17 +561,18 @@
|
||||
span: 'full'
|
||||
},
|
||||
{
|
||||
label: '送达时间',
|
||||
label: '送货时间',
|
||||
value: formatDisplayDate(editForm.value?.deliveryTime),
|
||||
long: false,
|
||||
fullSpan: false
|
||||
},
|
||||
{
|
||||
label: '注意事项',
|
||||
value: editForm.value?.destinationNotes || '—',
|
||||
label: '送货联系方式',
|
||||
value: editForm.value?.deliveryContact || '—',
|
||||
long: false,
|
||||
fullSpan: false
|
||||
}
|
||||
},
|
||||
{ label: '注意事项', value: editForm.value?.destinationNotes || '—', span: 'full' }
|
||||
])
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
const searchForm = reactive<PlIoOrderQuery>({
|
||||
|
||||
@@ -4,7 +4,16 @@
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="运输公司">
|
||||
<el-select v-model="searchForm.carrierId" placeholder="全部" clearable filterable>
|
||||
<el-select
|
||||
v-model="searchForm.carrierId"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchCarriers"
|
||||
:loading="carrierSelectLoading"
|
||||
@focus="onCarrierSelectFocus"
|
||||
>
|
||||
<el-option
|
||||
v-for="c in carrierList"
|
||||
:key="c.id"
|
||||
@@ -309,8 +318,8 @@ import {
|
||||
RefreshLeft,
|
||||
Search
|
||||
} from '@element-plus/icons-vue'
|
||||
import { carrierApi, freightApi, type Carrier, type FreightSettlement } from '@/api/pl'
|
||||
import { pickBxList } from '@/utils/http/bx'
|
||||
import { freightApi, type FreightSettlement } from '@/api/pl'
|
||||
import { usePlRemoteCarrierOptions } from '@/composables/usePlRemoteOptions'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import PlFreightApprovalDialog from '@/components/business/pl-freight-approval-dialog/index.vue'
|
||||
import PlFreightSettlementFormDialog from '@/components/business/pl-freight-settlement-form-dialog/index.vue'
|
||||
@@ -345,7 +354,15 @@ const {
|
||||
|
||||
const loading = ref(false)
|
||||
const cancelLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const carrierList = ref<Carrier[]>([])
|
||||
const {
|
||||
list: carrierList,
|
||||
loading: carrierSelectLoading,
|
||||
search: searchCarriers,
|
||||
onFocus: onCarrierSelectFocus
|
||||
} = usePlRemoteCarrierOptions({
|
||||
getSelectedId: () => searchForm.carrierId,
|
||||
listParams: { status: '1' }
|
||||
})
|
||||
const settlementList = ref<FreightSettlement[]>([])
|
||||
const selectedRows = ref<FreightSettlement[]>([])
|
||||
const viewingRow = ref<FreightSettlement | null>(null)
|
||||
@@ -421,12 +438,7 @@ function handleQuickSearchClear() {
|
||||
}
|
||||
|
||||
async function loadCarrierList() {
|
||||
try {
|
||||
const res = await carrierApi.list({ status: '1', page: 1, pageSize: 100 })
|
||||
carrierList.value = pickBxList(res)
|
||||
} catch {
|
||||
carrierList.value = []
|
||||
}
|
||||
await searchCarriers('')
|
||||
}
|
||||
|
||||
function handlePageChange() {
|
||||
|
||||
@@ -4,7 +4,16 @@
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="运输公司">
|
||||
<el-select v-model="searchForm.carrierId" placeholder="全部" clearable filterable>
|
||||
<el-select
|
||||
v-model="searchForm.carrierId"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchCarriers"
|
||||
:loading="carrierSelectLoading"
|
||||
@focus="onCarrierSelectFocus"
|
||||
>
|
||||
<el-option
|
||||
v-for="c in carrierList"
|
||||
:key="c.id"
|
||||
@@ -49,7 +58,7 @@
|
||||
<div class="list-toolbar-row">
|
||||
<ListQuickSearchInput
|
||||
v-model="quickSearchKeyword"
|
||||
placeholder="计划编号/客户/车牌"
|
||||
placeholder="计划编号/客户/车牌/承运商"
|
||||
@search="handleQuickSearch"
|
||||
@clear="handleQuickSearchClear"
|
||||
/>
|
||||
@@ -144,7 +153,7 @@
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('transportCompany')"
|
||||
prop="transportCompany"
|
||||
label="运输公司"
|
||||
label="承运商"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
@@ -197,9 +206,10 @@ import ListToolbarEndActions from '@/components/business/list-toolbar-end-action
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { DocumentAdd, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import { carrierApi, freightApi, vehicleApi, type Carrier, type UnsettledPlanItem, type Vehicle } from '@/api/pl'
|
||||
import { freightApi, vehicleApi, type UnsettledPlanItem, type Vehicle } from '@/api/pl'
|
||||
import { usePlRemoteCarrierOptions } from '@/composables/usePlRemoteOptions'
|
||||
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
|
||||
import { pickBxList, pickBxPageResult } from '@/utils/http/bx'
|
||||
import { pickBxPageResult } from '@/utils/http/bx'
|
||||
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
|
||||
import { getPlanTypeLabel, getPlanTypeTagType } from '@/views/pl/freight/freight-status'
|
||||
import { buildVehicleTrailerLookup, formatPlVehiclePlateDisplay } from '@/utils/pl-display'
|
||||
@@ -213,7 +223,7 @@ const listColumnDefs = [
|
||||
{ prop: 'planType', label: '单据类型' },
|
||||
{ prop: 'partnerName', label: '客户/供应商' },
|
||||
{ prop: 'licensePlate', label: '车牌号' },
|
||||
{ prop: 'transportCompany', label: '运输公司' },
|
||||
{ prop: 'transportCompany', label: '承运商' },
|
||||
{ prop: 'freightAmount', label: '运费(元)' },
|
||||
{ prop: 'archiveTime', label: '归档时间' }
|
||||
]
|
||||
@@ -228,7 +238,15 @@ const {
|
||||
|
||||
const loading = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const carrierList = ref<Carrier[]>([])
|
||||
const {
|
||||
list: carrierList,
|
||||
loading: carrierSelectLoading,
|
||||
search: searchCarriers,
|
||||
onFocus: onCarrierSelectFocus
|
||||
} = usePlRemoteCarrierOptions({
|
||||
getSelectedId: () => searchForm.carrierId,
|
||||
listParams: { status: '1' }
|
||||
})
|
||||
const vehicleOptions = ref<Vehicle[]>([])
|
||||
const vehicleTrailerLookup = computed(() => buildVehicleTrailerLookup(vehicleOptions.value))
|
||||
const planList = ref<UnsettledPlanItem[]>([])
|
||||
@@ -302,12 +320,7 @@ function handleQuickSearchClear() {
|
||||
}
|
||||
|
||||
async function loadCarrierList() {
|
||||
try {
|
||||
const res = await carrierApi.list({ status: '1', page: 1, pageSize: 100 })
|
||||
carrierList.value = pickBxList(res)
|
||||
} catch {
|
||||
carrierList.value = []
|
||||
}
|
||||
await searchCarriers('')
|
||||
}
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
<h2 class="title-bar-heading">危化品证上传</h2>
|
||||
</div>
|
||||
|
||||
<PlPlanTypeTabs v-model="activePlanTypeTab" @change="handlePlanTypeTabChange" />
|
||||
<PlPlanTypeTabs
|
||||
v-model="activePlanTypeTab"
|
||||
:tabs="PL_PLAN_CERT_TABS"
|
||||
@change="handlePlanTypeTabChange"
|
||||
/>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-block">
|
||||
@@ -179,6 +183,41 @@
|
||||
min-width="120"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('customerName')"
|
||||
prop="customerName"
|
||||
label="客户名称"
|
||||
min-width="240"
|
||||
align="left"
|
||||
header-align="left"
|
||||
class-name="col-name-wrap"
|
||||
label-class-name="col-name-wrap"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('planType')"
|
||||
prop="planType"
|
||||
label="单据类型"
|
||||
min-width="100"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlanTypeTagType(row.planType)" size="small">
|
||||
{{ getPlanTypeLabel(row.planType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('hazardCertReady')"
|
||||
label="证书齐备"
|
||||
min-width="100"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getHazardCertReadyTagType(row)" size="small">
|
||||
{{ getHazardCertReadyLabel(row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('productName')"
|
||||
label="产品名称"
|
||||
@@ -238,41 +277,6 @@
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('customerName')"
|
||||
prop="customerName"
|
||||
label="客户名称"
|
||||
min-width="240"
|
||||
align="left"
|
||||
header-align="left"
|
||||
class-name="col-name-wrap"
|
||||
label-class-name="col-name-wrap"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('planType')"
|
||||
prop="planType"
|
||||
label="单据类型"
|
||||
min-width="100"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlanTypeTagType(row.planType)" size="small">
|
||||
{{ getPlanTypeLabel(row.planType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('hazardCertReady')"
|
||||
label="证书齐备"
|
||||
min-width="100"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getHazardCertReadyTagType(row)" size="small">
|
||||
{{ getHazardCertReadyLabel(row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('followStatus')"
|
||||
prop="followStatus"
|
||||
@@ -409,9 +413,10 @@ import {
|
||||
getPlPlanFollowStatusLabel,
|
||||
getPlPlanFollowStatusTagType,
|
||||
normalizePlPersonnelSummaryDisplay,
|
||||
PL_PLAN_CERT_TABS,
|
||||
PL_PLAN_FOLLOW_STATUS_OPTIONS,
|
||||
plPlanTypeTabToFilter,
|
||||
type PlPlanTypeTab
|
||||
plPlanCertTabToFilters,
|
||||
type PlPlanCertTab
|
||||
} from '@/utils/pl-display'
|
||||
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
|
||||
import { refreshMenuBadges } from '@/utils/menu-badge-registry'
|
||||
@@ -430,14 +435,14 @@ const planList = ref<Plan[]>([])
|
||||
|
||||
const listColumnDefs = [
|
||||
{ prop: 'planDate', label: '计划日期' },
|
||||
{ prop: 'customerName', label: '客户名称' },
|
||||
{ prop: 'planType', label: '单据类型' },
|
||||
{ prop: 'hazardCertReady', label: '证书齐备' },
|
||||
{ prop: 'productName', label: '产品名称' },
|
||||
{ prop: 'productSpec', label: '型号/规格' },
|
||||
{ prop: 'productQty', label: '数量' },
|
||||
{ prop: 'productUnit', label: '单位' },
|
||||
{ prop: 'planAttachments', label: '计划附件' },
|
||||
{ prop: 'customerName', label: '客户名称' },
|
||||
{ prop: 'planType', label: '单据类型' },
|
||||
{ prop: 'hazardCertReady', label: '证书齐备' },
|
||||
{ prop: 'followStatus', label: '跟进状态' },
|
||||
{ prop: 'licensePlate', label: '车牌号' },
|
||||
{ prop: 'personnelSummary', label: '人员' },
|
||||
@@ -457,13 +462,14 @@ const viewingPlan = ref<Plan | null>(null)
|
||||
const selectedPlan = computed(() => (selectedRows.value.length === 1 ? selectedRows.value[0] : null))
|
||||
const dateRange = ref<string[]>([])
|
||||
const quickSearch = ref('')
|
||||
const activePlanTypeTab = ref<PlPlanTypeTab>('all')
|
||||
const activePlanTypeTab = ref<PlPlanCertTab>('all')
|
||||
|
||||
const searchForm = reactive({
|
||||
planNo: '',
|
||||
planType: '',
|
||||
followStatus: '',
|
||||
customerName: ''
|
||||
customerName: '',
|
||||
certUploadStatus: '' as string
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
@@ -494,6 +500,8 @@ async function fetchData() {
|
||||
customerName: keyword ? undefined : searchForm.customerName || undefined,
|
||||
planDateFrom: dateRange.value?.[0] || undefined,
|
||||
planDateTo: dateRange.value?.[1] || undefined,
|
||||
hasHazardProduct: 1,
|
||||
certUploadStatus: searchForm.certUploadStatus || undefined,
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
@@ -510,8 +518,10 @@ function handleSearch() {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handlePlanTypeTabChange(tab: PlPlanTypeTab) {
|
||||
searchForm.planType = plPlanTypeTabToFilter(tab)
|
||||
function handlePlanTypeTabChange(tab: PlPlanCertTab | string) {
|
||||
const filters = plPlanCertTabToFilters(tab as PlPlanCertTab)
|
||||
searchForm.planType = filters.planType
|
||||
searchForm.certUploadStatus = filters.certUploadStatus || ''
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
@@ -522,6 +532,7 @@ function handleReset() {
|
||||
searchForm.planType = ''
|
||||
searchForm.followStatus = ''
|
||||
searchForm.customerName = ''
|
||||
searchForm.certUploadStatus = ''
|
||||
quickSearch.value = ''
|
||||
resetDateShortcut()
|
||||
pagination.page = 1
|
||||
@@ -665,6 +676,7 @@ useDbInstantSearch(
|
||||
planType: searchForm.planType,
|
||||
followStatus: searchForm.followStatus,
|
||||
customerName: searchForm.customerName,
|
||||
certUploadStatus: searchForm.certUploadStatus,
|
||||
dateRange: dateRange.value
|
||||
}),
|
||||
() => {
|
||||
|
||||
@@ -35,6 +35,23 @@
|
||||
v-model="searchForm.customerName"
|
||||
placeholder="搜索客户名称"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品名称">
|
||||
<el-input
|
||||
v-model="searchForm.productName"
|
||||
placeholder="搜索产品名称"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="车牌号">
|
||||
<el-input
|
||||
v-model="searchForm.licensePlate"
|
||||
placeholder="搜索车牌号"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="计划日期">
|
||||
@@ -74,7 +91,7 @@
|
||||
/>
|
||||
<ListQuickSearchInput
|
||||
v-model="quickSearch"
|
||||
placeholder="计划编号/客户名称"
|
||||
placeholder="计划编号/客户名称/产品名称/车牌号"
|
||||
@search="handleQuickSearch"
|
||||
@clear="handleQuickSearchClear"
|
||||
/>
|
||||
@@ -241,18 +258,19 @@
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell :products="row.products" field="spec" />
|
||||
<PlPlanListProductCell :products="row.products" field="model" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('productQty')"
|
||||
label="数量"
|
||||
min-width="90"
|
||||
align="right"
|
||||
header-align="right"
|
||||
v-if="isColumnVisible('productPackageSpec')"
|
||||
label="包装规格"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell :products="row.products" field="qty" />
|
||||
<PlPlanListProductCell :products="row.products" field="packageSpec" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -266,6 +284,28 @@
|
||||
<PlPlanListProductCell :products="row.products" field="unit" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('productPlanQty')"
|
||||
label="计划数量"
|
||||
min-width="100"
|
||||
align="right"
|
||||
header-align="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell :products="row.products" field="planQty" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('productActualQty')"
|
||||
label="实际数量"
|
||||
min-width="100"
|
||||
align="right"
|
||||
header-align="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<PlPlanListProductCell :products="row.products" field="actualQty" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('planAttachments')"
|
||||
label="计划附件"
|
||||
@@ -502,8 +542,10 @@ const listColumnDefs = [
|
||||
{ prop: 'customerName', label: '客户名称' },
|
||||
{ prop: 'productName', label: '产品名称' },
|
||||
{ prop: 'productSpec', label: '型号/规格' },
|
||||
{ prop: 'productQty', label: '数量' },
|
||||
{ prop: 'productPackageSpec', label: '包装规格' },
|
||||
{ prop: 'productUnit', label: '单位' },
|
||||
{ prop: 'productPlanQty', label: '计划数量' },
|
||||
{ prop: 'productActualQty', label: '实际数量' },
|
||||
{ prop: 'planAttachments', label: '计划附件' },
|
||||
{ prop: 'licensePlate', label: '车牌号' },
|
||||
{ prop: 'personnelSummary', label: '人员' },
|
||||
@@ -516,7 +558,7 @@ const {
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-plan-list-columns', listColumnDefs)
|
||||
} = usePersistedListColumns('pl-plan-list-columns-v2', listColumnDefs)
|
||||
|
||||
const selectedRows = ref<Plan[]>([])
|
||||
const viewingPlan = ref<Plan | null>(null)
|
||||
@@ -597,7 +639,9 @@ const searchForm = reactive({
|
||||
planNo: '',
|
||||
planType: '',
|
||||
followStatus: '',
|
||||
customerName: ''
|
||||
customerName: '',
|
||||
productName: '',
|
||||
licensePlate: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
@@ -627,6 +671,8 @@ async function fetchData() {
|
||||
planType: searchForm.planType || undefined,
|
||||
followStatus: searchForm.followStatus || undefined,
|
||||
customerName: keyword ? undefined : searchForm.customerName || undefined,
|
||||
productName: keyword ? undefined : searchForm.productName || undefined,
|
||||
licensePlate: keyword ? undefined : searchForm.licensePlate || undefined,
|
||||
planDateFrom: dateRange.value?.[0] || undefined,
|
||||
planDateTo: dateRange.value?.[1] || undefined,
|
||||
page: pagination.page,
|
||||
@@ -657,6 +703,8 @@ function handleReset() {
|
||||
searchForm.planType = ''
|
||||
searchForm.followStatus = ''
|
||||
searchForm.customerName = ''
|
||||
searchForm.productName = ''
|
||||
searchForm.licensePlate = ''
|
||||
quickSearch.value = ''
|
||||
resetDateShortcut()
|
||||
pagination.page = 1
|
||||
@@ -666,6 +714,8 @@ function handleReset() {
|
||||
function handleQuickSearch() {
|
||||
searchForm.planNo = ''
|
||||
searchForm.customerName = ''
|
||||
searchForm.productName = ''
|
||||
searchForm.licensePlate = ''
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
@@ -908,6 +958,8 @@ useDbInstantSearch(
|
||||
planType: searchForm.planType,
|
||||
followStatus: searchForm.followStatus,
|
||||
customerName: searchForm.customerName,
|
||||
productName: searchForm.productName,
|
||||
licensePlate: searchForm.licensePlate,
|
||||
dateRange: dateRange.value
|
||||
}),
|
||||
() => {
|
||||
|
||||
@@ -25,7 +25,16 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="承运商">
|
||||
<el-select v-model="searchForm.carrierId" placeholder="全部" clearable filterable>
|
||||
<el-select
|
||||
v-model="searchForm.carrierId"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchCarriers"
|
||||
:loading="carrierSelectLoading"
|
||||
@focus="onCarrierSelectFocus"
|
||||
>
|
||||
<el-option
|
||||
v-for="c in carrierList"
|
||||
:key="c.id"
|
||||
@@ -258,7 +267,16 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="承运商" prop="carrierId">
|
||||
<el-select v-model="formData.carrierId" placeholder="请选择承运商" filterable>
|
||||
<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"
|
||||
@@ -309,7 +327,11 @@
|
||||
v-model="row.personnelId"
|
||||
placeholder="请选择人员"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
:remote-method="searchPersonnel"
|
||||
:loading="personnelSelectLoading"
|
||||
@focus="onPersonnelSelectFocus"
|
||||
@change="(id: string | undefined) => handlePersonnelSelect(row, id)"
|
||||
>
|
||||
<el-option
|
||||
@@ -441,8 +463,6 @@ import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search, Van } from '@element-plus/icons-vue'
|
||||
import {
|
||||
vehicleApi,
|
||||
personnelApi,
|
||||
carrierApi,
|
||||
type Vehicle,
|
||||
type VehicleDetail,
|
||||
type VehiclePersonnelRow,
|
||||
@@ -451,6 +471,10 @@ import {
|
||||
type Personnel,
|
||||
type Carrier
|
||||
} from '@/api/pl'
|
||||
import {
|
||||
usePlRemoteCarrierOptions,
|
||||
usePlRemotePersonnelOptions
|
||||
} from '@/composables/usePlRemoteOptions'
|
||||
import PlVehicleCertPartTabs from '@/components/business/pl-vehicle-cert-part-tabs/index.vue'
|
||||
import PlVehicleCertPersonnelTabs from '@/components/business/pl-vehicle-cert-personnel-tabs/index.vue'
|
||||
import PlVehicleCertBadges from '@/components/business/pl-vehicle-cert-badges/index.vue'
|
||||
@@ -462,7 +486,7 @@ import SystemViewDialog, {
|
||||
import { loadPersonnelWithCerts } from '@/composables/usePlVehicleViewDetail'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { useListMediaActive } from '@/composables/useListMediaActive'
|
||||
import { pickBxData, pickBxList, pickBxPageResult } from '@/utils/http/bx'
|
||||
import { pickBxData, pickBxPageResult } from '@/utils/http/bx'
|
||||
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import {
|
||||
@@ -500,7 +524,15 @@ const quickSearchKeyword = ref('')
|
||||
const submitLoading = ref(false)
|
||||
const dialogLoading = ref(false)
|
||||
const vehicleList = ref<Vehicle[]>([])
|
||||
const carrierList = ref<Carrier[]>([])
|
||||
const {
|
||||
list: carrierList,
|
||||
loading: carrierSelectLoading,
|
||||
search: searchCarriers,
|
||||
onFocus: onCarrierSelectFocus,
|
||||
ensureById: ensureSelectedCarrierOption
|
||||
} = usePlRemoteCarrierOptions({
|
||||
getSelectedId: () => formData.carrierId || searchForm.carrierId
|
||||
})
|
||||
|
||||
const listColumnDefs = [
|
||||
{ prop: 'photoUrls', label: '照片' },
|
||||
@@ -521,7 +553,18 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-self-vehicle-list-columns-v3', listColumnDefs)
|
||||
|
||||
const personnelOptions = ref<Personnel[]>([])
|
||||
const {
|
||||
list: personnelOptions,
|
||||
loading: personnelSelectLoading,
|
||||
search: searchPersonnel,
|
||||
onFocus: onPersonnelSelectFocus,
|
||||
invalidate: invalidatePersonnelOptions
|
||||
} = usePlRemotePersonnelOptions({
|
||||
listParams: () => ({
|
||||
status: '1',
|
||||
personnelAffiliation: 'self'
|
||||
})
|
||||
})
|
||||
const selectedRows = ref<Vehicle[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
@@ -728,13 +771,12 @@ function resolvePersonnelRows(rows: VehiclePersonnelRow[]) {
|
||||
}
|
||||
|
||||
async function loadPersonnelOptions() {
|
||||
const res = await personnelApi.list({ status: '1', pageSize: 100, personnelAffiliation: 'self' })
|
||||
personnelOptions.value = pickBxList<Personnel>(res)
|
||||
invalidatePersonnelOptions()
|
||||
await searchPersonnel('')
|
||||
}
|
||||
|
||||
async function loadCarrierOptions() {
|
||||
const res = await carrierApi.list({ page: 1, pageSize: 100 })
|
||||
carrierList.value = pickBxList<Carrier>(res)
|
||||
await searchCarriers('')
|
||||
}
|
||||
|
||||
function handleAddPersonnel() {
|
||||
@@ -971,6 +1013,9 @@ async function handleEdit(row: Vehicle) {
|
||||
remark: detail.remark,
|
||||
photoUrls: detail.photoUrls || ''
|
||||
})
|
||||
if (formData.carrierId) {
|
||||
await ensureSelectedCarrierOption(formData.carrierId)
|
||||
}
|
||||
personnelList.value = resolvePersonnelRows(
|
||||
(detail.personnelList || []).map(p => ({
|
||||
...p,
|
||||
|
||||
@@ -259,20 +259,11 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-select
|
||||
<PlVehicleSelect
|
||||
v-model="formData.licensePlate"
|
||||
filterable
|
||||
placeholder="请选择车牌"
|
||||
style="width: 100%"
|
||||
@change="handleLicensePlateChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in vehicleOptions"
|
||||
:key="item.id"
|
||||
:label="item.licensePlate"
|
||||
:value="item.licensePlate"
|
||||
/>
|
||||
</el-select>
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="维保类型" prop="maintenanceType">
|
||||
<el-select
|
||||
@@ -389,9 +380,11 @@ import {
|
||||
type Vehicle,
|
||||
type VehicleMaintenanceApply
|
||||
} from '@/api/pl'
|
||||
import { pickBxList } from '@/utils/http/bx'
|
||||
import { pickBxPageResult } from '@/utils/http/bx'
|
||||
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import PlVehicleSelect from '@/components/business/pl/PlVehicleSelect.vue'
|
||||
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
@@ -549,8 +542,14 @@ async function loadUserOptions() {
|
||||
}
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
const res = await vehicleApi.list({ page: 1, pageSize: 100 })
|
||||
vehicleOptions.value = pickBxList(res)
|
||||
try {
|
||||
vehicleOptions.value = await fetchAllPagedRecords<Vehicle>(async (page, pageSize) => {
|
||||
const res = await vehicleApi.list({ page, pageSize })
|
||||
return pickBxPageResult<Vehicle>(res)
|
||||
})
|
||||
} catch {
|
||||
vehicleOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function showMaintenanceReminders(licensePlate: string) {
|
||||
|
||||
@@ -322,20 +322,11 @@
|
||||
</el-form-item>
|
||||
<template v-if="dialogMode === 'supplement'">
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-select
|
||||
<PlVehicleSelect
|
||||
v-model="formData.licensePlate"
|
||||
filterable
|
||||
placeholder="请选择车牌"
|
||||
style="width: 100%"
|
||||
@change="handleLicensePlateChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in vehicleOptions"
|
||||
:key="item.id"
|
||||
:label="item.licensePlate"
|
||||
:value="item.licensePlate"
|
||||
/>
|
||||
</el-select>
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item label="处理日期" prop="processDate">
|
||||
@@ -427,8 +418,10 @@ import {
|
||||
type Vehicle,
|
||||
type VehicleMaintenanceProcessBoardItem
|
||||
} from '@/api/pl'
|
||||
import { pickBxList } from '@/utils/http/bx'
|
||||
import { pickBxPageResult } from '@/utils/http/bx'
|
||||
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import PlVehicleSelect from '@/components/business/pl/PlVehicleSelect.vue'
|
||||
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
@@ -620,8 +613,14 @@ async function loadUserOptions() {
|
||||
}
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
const res = await vehicleApi.list({ page: 1, pageSize: 100 })
|
||||
vehicleOptions.value = pickBxList(res)
|
||||
try {
|
||||
vehicleOptions.value = await fetchAllPagedRecords<Vehicle>(async (page, pageSize) => {
|
||||
const res = await vehicleApi.list({ page, pageSize })
|
||||
return pickBxPageResult<Vehicle>(res)
|
||||
})
|
||||
} catch {
|
||||
vehicleOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function showMaintenanceReminders(licensePlate: string) {
|
||||
|
||||
@@ -240,9 +240,10 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-select v-model="formData.licensePlate" filterable placeholder="请选择车牌" style="width: 100%">
|
||||
<el-option v-for="item in vehicleOptions" :key="item.id" :label="item.licensePlate" :value="item.licensePlate" />
|
||||
</el-select>
|
||||
<PlVehicleSelect
|
||||
v-model="formData.licensePlate"
|
||||
placeholder="请选择车牌"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="充值类型" prop="rechargeType">
|
||||
<el-select v-model="formData.rechargeType" placeholder="请选择" style="width: 100%">
|
||||
@@ -328,9 +329,11 @@ import {
|
||||
type Vehicle,
|
||||
type VehicleRechargeApply
|
||||
} from '@/api/pl'
|
||||
import { pickBxList } from '@/utils/http/bx'
|
||||
import { pickBxPageResult } from '@/utils/http/bx'
|
||||
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import PlVehicleSelect from '@/components/business/pl/PlVehicleSelect.vue'
|
||||
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
@@ -479,8 +482,14 @@ async function loadUserOptions() {
|
||||
}
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
const res = await vehicleApi.list({ page: 1, pageSize: 100 })
|
||||
vehicleOptions.value = pickBxList(res)
|
||||
try {
|
||||
vehicleOptions.value = await fetchAllPagedRecords<Vehicle>(async (page, pageSize) => {
|
||||
const res = await vehicleApi.list({ page, pageSize })
|
||||
return pickBxPageResult<Vehicle>(res)
|
||||
})
|
||||
} catch {
|
||||
vehicleOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
|
||||
@@ -324,19 +324,10 @@
|
||||
</el-form-item>
|
||||
<template v-if="dialogMode === 'supplement'">
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-select
|
||||
<PlVehicleSelect
|
||||
v-model="formData.licensePlate"
|
||||
filterable
|
||||
placeholder="请选择车牌"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in vehicleOptions"
|
||||
:key="item.id"
|
||||
:label="item.licensePlate"
|
||||
:value="item.licensePlate"
|
||||
/>
|
||||
</el-select>
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="充值类型" prop="rechargeType">
|
||||
<el-select v-model="formData.rechargeType" placeholder="请选择" style="width: 100%">
|
||||
@@ -439,8 +430,10 @@ import {
|
||||
type Vehicle,
|
||||
type VehicleRechargeDisburseBoardItem
|
||||
} from '@/api/pl'
|
||||
import { pickBxList } from '@/utils/http/bx'
|
||||
import { pickBxPageResult } from '@/utils/http/bx'
|
||||
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import PlVehicleSelect from '@/components/business/pl/PlVehicleSelect.vue'
|
||||
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
@@ -599,8 +592,14 @@ const viewSections = computed<ViewSection[]>(() => {
|
||||
})
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
const vehicles = await vehicleApi.list({ page: 1, pageSize: 100 })
|
||||
vehicleOptions.value = pickBxList(vehicles)
|
||||
try {
|
||||
vehicleOptions.value = await fetchAllPagedRecords<Vehicle>(async (page, pageSize) => {
|
||||
const vehicles = await vehicleApi.list({ page, pageSize })
|
||||
return pickBxPageResult<Vehicle>(vehicles)
|
||||
})
|
||||
} catch {
|
||||
vehicleOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
|
||||
@@ -244,9 +244,10 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-select v-model="formData.licensePlate" filterable placeholder="请选择车牌" style="width: 100%">
|
||||
<el-option v-for="item in vehicleOptions" :key="item.id" :label="item.licensePlate" :value="item.licensePlate" />
|
||||
</el-select>
|
||||
<PlVehicleSelect
|
||||
v-model="formData.licensePlate"
|
||||
placeholder="请选择车牌"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请时间" prop="applyDate">
|
||||
<el-date-picker v-model="formData.applyDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
@@ -344,9 +345,11 @@ import {
|
||||
type Vehicle,
|
||||
type VehicleUreaApply
|
||||
} from '@/api/pl'
|
||||
import { pickBxList } from '@/utils/http/bx'
|
||||
import { pickBxPageResult } from '@/utils/http/bx'
|
||||
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import PlVehicleSelect from '@/components/business/pl/PlVehicleSelect.vue'
|
||||
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
@@ -499,8 +502,14 @@ async function loadUserOptions() {
|
||||
}
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
const res = await vehicleApi.list({ page: 1, pageSize: 100 })
|
||||
vehicleOptions.value = pickBxList(res)
|
||||
try {
|
||||
vehicleOptions.value = await fetchAllPagedRecords<Vehicle>(async (page, pageSize) => {
|
||||
const res = await vehicleApi.list({ page, pageSize })
|
||||
return pickBxPageResult<Vehicle>(res)
|
||||
})
|
||||
} catch {
|
||||
vehicleOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
|
||||
@@ -300,19 +300,10 @@
|
||||
</el-form-item>
|
||||
<template v-if="dialogMode === 'supplement'">
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-select
|
||||
<PlVehicleSelect
|
||||
v-model="formData.licensePlate"
|
||||
filterable
|
||||
placeholder="请选择车牌"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in vehicleOptions"
|
||||
:key="item.id"
|
||||
:label="item.licensePlate"
|
||||
:value="item.licensePlate"
|
||||
/>
|
||||
</el-select>
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item label="发放时间" prop="disburseDate">
|
||||
@@ -396,9 +387,11 @@ import {
|
||||
type Vehicle,
|
||||
type VehicleUreaDisburseBoardItem
|
||||
} from '@/api/pl'
|
||||
import { pickBxList } from '@/utils/http/bx'
|
||||
import { pickBxPageResult } from '@/utils/http/bx'
|
||||
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import PlVehicleSelect from '@/components/business/pl/PlVehicleSelect.vue'
|
||||
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
@@ -583,8 +576,14 @@ async function loadUserOptions() {
|
||||
}
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
const vehicles = await vehicleApi.list({ page: 1, pageSize: 100 })
|
||||
vehicleOptions.value = pickBxList(vehicles)
|
||||
try {
|
||||
vehicleOptions.value = await fetchAllPagedRecords<Vehicle>(async (page, pageSize) => {
|
||||
const vehicles = await vehicleApi.list({ page, pageSize })
|
||||
return pickBxPageResult<Vehicle>(vehicles)
|
||||
})
|
||||
} catch {
|
||||
vehicleOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
|
||||
@@ -19,7 +19,16 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="承运商">
|
||||
<el-select v-model="searchForm.carrierId" placeholder="全部" clearable filterable>
|
||||
<el-select
|
||||
v-model="searchForm.carrierId"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
:remote-method="searchCarriers"
|
||||
:loading="carrierSelectLoading"
|
||||
@focus="onCarrierSelectFocus"
|
||||
>
|
||||
<el-option
|
||||
v-for="c in carrierList"
|
||||
:key="c.id"
|
||||
@@ -266,7 +275,16 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="承运商" prop="carrierId">
|
||||
<el-select v-model="formData.carrierId" placeholder="请选择承运商" filterable clearable>
|
||||
<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"
|
||||
@@ -318,7 +336,11 @@
|
||||
v-model="row.personnelId"
|
||||
placeholder="请选择人员"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
:remote-method="searchPersonnel"
|
||||
:loading="personnelSelectLoading"
|
||||
@focus="onPersonnelSelectFocus"
|
||||
@change="(id: string | undefined) => handlePersonnelSelect(row, id)"
|
||||
>
|
||||
<el-option
|
||||
@@ -459,7 +481,6 @@ import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { Check, Close, Delete, DocumentCopy, Edit, Plus, RefreshLeft, Search, Van } from '@element-plus/icons-vue'
|
||||
import {
|
||||
vehicleApi,
|
||||
carrierApi,
|
||||
personnelApi,
|
||||
type Vehicle,
|
||||
type VehicleDetail,
|
||||
@@ -469,6 +490,10 @@ import {
|
||||
type Carrier,
|
||||
type Personnel
|
||||
} from '@/api/pl'
|
||||
import {
|
||||
usePlRemoteCarrierOptions,
|
||||
usePlRemotePersonnelOptions
|
||||
} from '@/composables/usePlRemoteOptions'
|
||||
import PlVehicleCertPartTabs from '@/components/business/pl-vehicle-cert-part-tabs/index.vue'
|
||||
import PlVehicleCertPersonnelTabs from '@/components/business/pl-vehicle-cert-personnel-tabs/index.vue'
|
||||
import PlVehicleCertBadges from '@/components/business/pl-vehicle-cert-badges/index.vue'
|
||||
@@ -480,7 +505,7 @@ import SystemViewDialog, {
|
||||
} from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { useListMediaActive } from '@/composables/useListMediaActive'
|
||||
import { pickBxData, pickBxList, pickBxPageResult } from '@/utils/http/bx'
|
||||
import { pickBxData, pickBxPageResult } from '@/utils/http/bx'
|
||||
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import { formatPlVehiclePlateDisplay, normalizePlPersonnelSummaryDisplay } from '@/utils/pl-display'
|
||||
@@ -511,7 +536,15 @@ const dialogLoading = ref(false)
|
||||
/** 编辑回填中:跳过 vehicleType watch,避免证书被 ensure 覆盖清空 */
|
||||
const hydratingEdit = ref(false)
|
||||
const vehicleList = ref<Vehicle[]>([])
|
||||
const carrierList = ref<Carrier[]>([])
|
||||
const {
|
||||
list: carrierList,
|
||||
loading: carrierSelectLoading,
|
||||
search: searchCarriers,
|
||||
onFocus: onCarrierSelectFocus,
|
||||
ensureById: ensureSelectedCarrierOption
|
||||
} = usePlRemoteCarrierOptions({
|
||||
getSelectedId: () => formData.carrierId || searchForm.carrierId
|
||||
})
|
||||
|
||||
const listColumnDefs = [
|
||||
{ prop: 'photoUrls', label: '照片' },
|
||||
@@ -532,7 +565,23 @@ const {
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-vehicle-list-columns-v3', listColumnDefs)
|
||||
|
||||
const personnelOptions = ref<Personnel[]>([])
|
||||
const {
|
||||
list: personnelOptions,
|
||||
loading: personnelSelectLoading,
|
||||
search: searchPersonnel,
|
||||
onFocus: onPersonnelSelectFocus,
|
||||
invalidate: invalidatePersonnelOptions
|
||||
} = usePlRemotePersonnelOptions({
|
||||
listParams: () => ({
|
||||
status: '1',
|
||||
personnelAffiliation:
|
||||
formData.vehicleType === 'carrier'
|
||||
? 'carrier'
|
||||
: formData.vehicleType === 'self'
|
||||
? 'self'
|
||||
: undefined
|
||||
})
|
||||
})
|
||||
const selectedRows = ref<Vehicle[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const pasteVisible = ref(false)
|
||||
@@ -772,19 +821,12 @@ function resolvePersonnelRows(rows: VehiclePersonnelRow[]) {
|
||||
}
|
||||
|
||||
async function loadPersonnelOptions() {
|
||||
const affiliation =
|
||||
formData.vehicleType === 'carrier' ? 'carrier' : formData.vehicleType === 'self' ? 'self' : undefined
|
||||
const res = await personnelApi.list({
|
||||
status: '1',
|
||||
pageSize: 100,
|
||||
personnelAffiliation: affiliation
|
||||
})
|
||||
personnelOptions.value = pickBxList<Personnel>(res)
|
||||
invalidatePersonnelOptions()
|
||||
await searchPersonnel('')
|
||||
}
|
||||
|
||||
async function loadCarrierOptions() {
|
||||
const res = await carrierApi.list({ page: 1, pageSize: 100 })
|
||||
carrierList.value = pickBxList<Carrier>(res)
|
||||
await searchCarriers('')
|
||||
}
|
||||
|
||||
function handleAddPersonnel() {
|
||||
@@ -1019,6 +1061,9 @@ async function handleEdit(row: Vehicle) {
|
||||
remark: detail.remark,
|
||||
photoUrls: detail.photoUrls || ''
|
||||
})
|
||||
if (formData.carrierId) {
|
||||
await ensureSelectedCarrierOption(formData.carrierId)
|
||||
}
|
||||
personnelList.value = resolvePersonnelRows(
|
||||
(detail.personnelList || []).map(p => ({
|
||||
...p,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
<van-loading v-if="loading" vertical class="page-loading">加载中...</van-loading>
|
||||
<van-empty v-else-if="errorMessage" :description="errorMessage" />
|
||||
<van-empty v-else-if="page?.archived" description="运输计划已归档完成" />
|
||||
<template v-else-if="page">
|
||||
<van-notice-bar
|
||||
v-if="isReadOnly"
|
||||
@@ -48,6 +49,7 @@
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell title="取货时间" :value="formatDateTime(page.pickupTime)" />
|
||||
<van-cell title="取货联系方式" :value="page.pickupContact || '—'" class="multiline-cell" />
|
||||
<van-cell title="注意事项" :value="page.pickupNotes || '—'" class="multiline-cell" />
|
||||
</van-cell-group>
|
||||
|
||||
@@ -69,7 +71,8 @@
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell title="送达时间" :value="formatDateTime(page.deliveryTime)" />
|
||||
<van-cell title="送货时间" :value="formatDateTime(page.deliveryTime)" />
|
||||
<van-cell title="送货联系方式" :value="page.deliveryContact || '—'" class="multiline-cell" />
|
||||
<van-cell title="注意事项" :value="page.destinationNotes || '—'" class="multiline-cell" />
|
||||
</van-cell-group>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<van-loading v-if="loading" vertical class="page-loading">加载中...</van-loading>
|
||||
<van-empty v-else-if="errorMessage" :description="errorMessage" />
|
||||
<van-empty v-else-if="page?.voided" description="运输计划已作废" />
|
||||
<van-empty v-else-if="page?.archived" description="运输计划已归档完成" />
|
||||
<template v-else-if="page">
|
||||
<van-cell-group inset class="section-card">
|
||||
<van-cell title="业务类型" :value="page.businessType || '—'" />
|
||||
@@ -35,6 +36,11 @@
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell title="取货时间" :value="formatDateTime(page.pickupTime)" />
|
||||
<van-cell
|
||||
title="取货联系方式"
|
||||
:value="page.pickupContact || '—'"
|
||||
class="multiline-cell"
|
||||
/>
|
||||
<van-cell
|
||||
title="注意事项"
|
||||
:value="page.pickupNotes || '—'"
|
||||
@@ -60,7 +66,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell title="送达时间" :value="formatDateTime(page.deliveryTime)" />
|
||||
<van-cell title="送货时间" :value="formatDateTime(page.deliveryTime)" />
|
||||
<van-cell
|
||||
title="送货联系方式"
|
||||
:value="page.deliveryContact || '—'"
|
||||
class="multiline-cell"
|
||||
/>
|
||||
<van-cell
|
||||
title="注意事项"
|
||||
:value="page.destinationNotes || '—'"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<van-loading v-if="loading" vertical class="page-loading">加载中...</van-loading>
|
||||
<van-empty v-else-if="errorMessage" :description="errorMessage" />
|
||||
<van-empty v-else-if="page?.planVoided" description="运输计划已作废" />
|
||||
<van-empty v-else-if="page?.planArchived" description="运输计划已归档完成" />
|
||||
<template v-else-if="page">
|
||||
<van-cell-group inset class="section-card">
|
||||
<van-cell title="计划编号" :value="page.planNo" />
|
||||
@@ -47,6 +48,11 @@
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell title="取货时间" :value="formatDateTime(page.pickupTime)" />
|
||||
<van-cell
|
||||
title="取货联系方式"
|
||||
:value="page.pickupContact || '—'"
|
||||
class="multiline-cell"
|
||||
/>
|
||||
<van-cell
|
||||
title="注意事项"
|
||||
:value="page.pickupNotes || '—'"
|
||||
@@ -72,7 +78,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</van-cell>
|
||||
<van-cell title="送达时间" :value="formatDateTime(page.deliveryTime)" />
|
||||
<van-cell title="送货时间" :value="formatDateTime(page.deliveryTime)" />
|
||||
<van-cell
|
||||
title="送货联系方式"
|
||||
:value="page.deliveryContact || '—'"
|
||||
class="multiline-cell"
|
||||
/>
|
||||
<van-cell
|
||||
title="注意事项"
|
||||
:value="page.destinationNotes || '—'"
|
||||
|
||||
Reference in new issue
Block a user