@@ -3,7 +3,7 @@ import '@/styles/adminDensity.css'
import { computed , onMounted , reactive , ref , watch } from 'vue'
import type { FormInstance , FormRules } from 'element-plus'
import { Eye , Save } from '@lucide/vue'
import { TRUCK _APPOINTMENT _CURRENCIES , normalizeTruckAppointmentCurrency , type SaveTruckAppointmentRequest , type TruckAppointmentCandidate , type TruckAppointmentResource } from '../types'
import { normalizeTruckAppointmentCurrency , type SaveTruckAppointmentRequest , type TruckAppointmentCandidate , type TruckAppointmentResource } from '../types'
import { listTruckAppointmentCandidates } from '../api/platformTrucking'
import { listSuppliers , type Supplier } from '@/modules/suppliers'
import { getOceanBooking , type OceanBookingDocument } from '@/modules/ocean-bookings'
@@ -14,18 +14,19 @@ const props = defineProps<{ modelValue: SaveTruckAppointmentRequest; resource?:
const emit = defineEmits < { 'update:modelValue' : [ SaveTruckAppointmentRequest ] ; submit : [ ] ; attachments : [ ] } > ( )
const formRef = ref < FormInstance > ( )
const relationWarning = ref ( '' )
const dispatchDetailsOpen = ref ( false )
const bookingContainersLoading = ref ( false )
const bookingContainersError = ref < ApiErrorDetails | null > ( null )
const bookingDocuments = ref < OceanBookingDocument [ ] > ( [ ] )
const relationLocked = computed ( ( ) => Boolean ( props . resource ) )
const defaults : Partial < SaveTruckAppointmentRequest > = { expectedVersion : 0 , currency : 'RMB' , payableAmount : '0.00' , packageCount : 0 , palletCount : 0 , transitMinutes : 0 }
const defaults : Partial < SaveTruckAppointmentRequest > = { expectedVersion : 0 , currency : 'RMB' , payableAmount : '0.00' }
function normalizeDraft ( value : SaveTruckAppointmentRequest ) : SaveTruckAppointmentRequest { return { ... defaults , ... value , currency : normalizeTruckAppointmentCurrency ( value . currency ) } }
const draft = reactive < SaveTruckAppointmentRequest > ( normalizeDraft ( props . modelValue ) )
watch ( ( ) => props . modelValue , ( value ) => Object . assign ( draft , normalizeDraft ( value ) ) , { deep : true } )
watch ( draft , ( value ) => emit ( 'update:modelValue' , { ... value } ) , { deep : true } )
const nonNegative = ( label : string , integer = false ) => ( { validator : ( _rule : unknown , value : unknown , callback : ( error ? : Error ) => void ) => {
const nonNegative = ( label : string , integer = false , precision = 3 ) => ( { validator : ( _rule : unknown , value : unknown , callback : ( error ? : Error ) => void ) => {
if ( value === null || value === undefined || value === '' ) return callback ( )
const text = String ( value ) . trim ( ) ; const valid = integer ? /^\d+$/ . test ( text ) : /^(?:\ d+) (?:\.\d{1,3})?$/ . test ( text )
const text = String ( value ) . trim ( ) ; const valid = integer ? /^\d+$/ . test ( text ) : new RegExp ( ` ^ \\ d+(?: \\ . \\ d{1, ${ precision } })? $ ` ) . test ( text )
callback ( valid ? undefined : new Error ( ` ${ label } 请输入 ${ integer ? '非负整数' : '非负数字' } ` ) )
} , trigger : [ 'blur' , 'change' ] } )
const phoneRule = ( label : string ) => ( { validator : ( _rule : unknown , value : unknown , callback : ( error ? : Error ) => void ) => {
@@ -39,11 +40,11 @@ const rules: FormRules = {
if ( ! draft . billOfLadingNo ? . trim ( ) ) return callback ( new Error ( '所选订舱缺少有效提单号' ) )
return callback ( bookingDocumentReady . value ? undefined : new Error ( '所选订舱缺少有效的订舱确认文件或提单文件' ) )
} , trigger : 'change' } ] ,
carrierId : [ { required : true , message : '请选择承运 商' , trigger : 'change' } , { validator : ( _rule , value , callback ) => callback ( selectableCarriers . value . some ( ( item ) => String ( item . id ) === String ( value ) ) ? undefined : new Error ( '请选择有效的合作中拖车供应商' ) ) , trigger : 'change' } ] ,
plannedLoadingAt : [ { required : true , message : '请选择预计装箱 时间' , trigger : 'change' } ] ,
carrierId : [ { required : true , message : '请选择拖车供应 商' , trigger : 'change' } , { validator : ( _rule , value , callback ) => callback ( selectableCarriers . value . some ( ( item ) => String ( item . id ) === String ( value ) ) ? undefined : new Error ( '请选择有效的合作中拖车供应商' ) ) , trigger : 'change' } ] ,
plannedLoadingAt : [ { required : true , message : '请选择预约装柜 时间' , trigger : 'change' } ] ,
fleetPhone : [ phoneRule ( '车队电话' ) ] , driverPhone : [ phoneRule ( '司机电话' ) ] , storePhone : [ phoneRule ( '门店电话' ) ] ,
packageCount : [ nonNegative ( '件数' , true ) ] , palletCount : [ nonNegative ( '托盘数量' , true ) ] , transitMinutes : [ nonNegative ( '在途分钟' , true ) ] ,
grossWeightKg : [ nonNegative ( '毛重' ) ] , volumeCbm : [ nonNegative ( '体积' ) ] , payableAmount : [ nonNegative ( '应付拖车费' ) ] ,
grossWeightKg : [ nonNegative ( '毛重' ) ] , volumeCbm : [ nonNegative ( '体积' ) ] , payableAmount : [ nonNegative ( '应付拖车费' , false , 2 ) ] ,
}
const carriers = ref < Supplier [ ] > ( [ ] )
const candidates = ref < TruckAppointmentCandidate [ ] > ( [ ] )
@@ -76,7 +77,17 @@ const selectedBookingFallback = computed(() => {
if ( ! id || ! relationLocked . value || String ( snapshot ? . oceanBookingId ) !== String ( id ) ) return null
return { id : String ( id ) , label : ` ${ snapshot ? . oceanBookingNo || '订舱号缺失' } (当前绑定,不可重选) ` }
} )
function selectCarrier ( carrierId : string ) { const carrier = selectableCarriers . value . find ( ( item ) => String ( item . id ) === carrierId ) ; draft . carrierId = carrier ? . id || null ; draft . carrierName = carrier ? . name || null }
function selectCarrier ( carrierId : string ) {
if ( String ( draft . carrierId || '' ) === carrierId ) return
const carrier = selectableCarriers . value . find ( ( item ) => String ( item . id ) === carrierId )
draft . carrierId = carrier ? . id || null
draft . carrierName = carrier ? . name || null
draft . fleetContact = carrier ? . contactName || null
draft . fleetPhone = carrier ? . phone || null
}
function toggleDispatchDetails ( event : Event ) {
dispatchDetailsOpen . value = ( event . target as HTMLDetailsElement ) . open
}
async function loadCurrentBookingContext ( bookingId : string ) {
const sequence = ++ bookingLoadSequence
bookingContainersLoading . value = true
@@ -125,7 +136,7 @@ async function loadOptions() {
if ( sequence !== optionsLoadSequence ) return
if ( results [ 0 ] . status === 'fulfilled' ) carriers . value = results [ 0 ] . value . records . filter ( ( item ) => item . category === 'TRUCKING' && item . status === 'ACTIVE' )
if ( results [ 1 ] . status === 'fulfilled' ) candidates . value = results [ 1 ] . value . records
const failed = [ '承运 商' , '可用订舱' ] . filter ( ( _ , index ) => results [ index ] ? . status === 'rejected' )
const failed = [ '拖车供应 商' , '可用订舱' ] . filter ( ( _ , index ) => results [ index ] ? . status === 'rejected' )
if ( failed . length ) optionsError . value = ` ${ failed . join ( '、' ) } 选项加载失败 `
optionsLoading . value = false
if ( relationLocked . value && draft . oceanBookingId ) await loadCurrentBookingContext ( String ( draft . oceanBookingId ) )
@@ -139,8 +150,14 @@ async function loadOptions() {
}
}
}
async function validate ( ) { return formRef . value ? . validate ( ) }
async function submitForm ( ) { if ( props . submitting ) return ; if ( await formRef . value ? . validate ( ) ) emit ( 'submit' ) }
async function validate ( ) {
try { return await formRef . value ? . validate ( ) }
catch ( fields ) {
if ( fields && typeof fields === 'object' && ( 'fleetPhone' in fields || 'driverPhone' in fields ) ) dispatchDetailsOpen . value = true
return false
}
}
async function submitForm ( ) { if ( props . submitting ) return ; if ( await validate ( ) ) emit ( 'submit' ) }
defineExpose ( { validate , loadOptions } )
onMounted ( ( ) => { void loadOptions ( ) } )
watch ( ( ) => props . readonly , ( readonly ) => { if ( ! readonly && ! carriers . value . length ) void loadOptions ( ) } )
@@ -148,38 +165,63 @@ watch(() => props.resource?.id, (id, previous) => { if (id && id !== previous) v
< / script >
< template >
< el-form ref = "formRef" :model = "draft" :rules = "rules" label -position = " top " : disabled = "readonly || submitting" class = "truck-form admin-density density-form" >
< div class = "relation-note" > < span > 一张已放舱订舱对应一张有效拖车单 ; 仓库和提单号由订舱冻结资料带入 , 货柜资料在货柜与预装流程维护 , 最终资格仍由服务端校验 。 < / span > < div v-if = "$slots['relation-note-actions']" class="relation-note__actions"><slot name="relation-note-actions" / > < / div > < / div >
< el-form ref = "formRef" :model = "draft" :rules = "rules" label -position = " top " : disabled = "readonly || submitting" class = "truck-form admin-density density-form" : class = "{ 'truck-form--create': !resource }" >
< div class = "relation-note" >
< span > 选择订舱和拖车供应商 , 约定装柜时间即可保存预约 。 < / span >
< div v-if = "$slots['relation-note-actions']" class="relation-note__actions"><slot name="relation-note-actions" / > < / div >
< / div >
< el-alert v-if = "relationWarning" type="warning" :closable="false" show-icon :title="relationWarning" class="relation-alert" / >
< el-alert v-if = "optionsError" type="warning" :closable="false" show-icon :title="optionsError" class="relation-alert"><el-button link type="warning" :loading="optionsLoading" @click="loadOptions" > 重新加载选项 < / el -button > < / el-alert >
< el-row :gutter = "16 ">
< el-col :xs = "24" > < el-divider content -position = " left " > 订舱 ( 必填 ) < / el-divider > < / el-col >
< el-col class = "relation-column" :xs = "24" :sm = "12" > < el-form-item label = "订舱" prop = "oceanBookingId" > < el-select :model-value = "draft.oceanBookingId" filterable :disabled = "relationLocked" placeholder = "请选择可创建拖车单的 已放舱订舱" :loading = "optionsLoading" no -data -text = " 暂无可创建拖车单的已放舱订舱 " style = "width:100%" @ update : model -value = " selectBooking ( String ( $ event | | ' ' ) ) " > < el-option v-if = "selectedBookingFallback" :key="selectedBookingFallback.id" :label="selectedBookingFallback.label" :value="selectedBookingFallback.id" disabled /><el-option v-for="item in bookings" :key="item.id" :label="`${item.bookingNo || '订舱号缺失'} / 提单号:${item.billOfLadingNo || '缺失'}`" :value="item.id" /></el-select><div v-if="bookingContainersError" class="field-error"><span>{{ bookingContainersError.message }}</span><ApiErrorMeta :details="bookingContainersError" /><el-button link type="primary" :loading="bookingContainersLoading" @click="draft.oceanBookingId && loadCurrentBookingContext(String(draft.oceanBookingId))" > 重新加载 < / el -button > < / div > < / el-form-item > < / el-col >
< el-col :xs = "24" > < el-divider content -position = " left " > 基本信息 < / el-divider > < / el-col >
< el-col v-if = "draft.oceanBookingId && !bookingContainersLoading && !bookingContainersError && !draft.billOfLadingNo?.trim()" :xs="24"><el-alert title="所选订舱缺少有效提单号,暂不能创建或保存拖车预约" type="warning" :closable="false" show -icon / > < / el-col >
< el-col v-if = "draft.oceanBookingId && !bookingContainersLoading && !bookingContainersError && !bookingDocumentReady" :xs="24"><el-alert title="所选订舱缺少有效的订舱确认文件或提单文件,暂不能创建或保存拖车预约" type="warning" :closable="false" show -icon / > < / el-col >
< el-col :xs = "24" :sm = "12" > < el-form-item label = "承运商" prop = "carrierId" > < el-select :model-value = "draft.carrierId" filterable clearable placeholder = "请选择承运商" :loading = "optionsLoading" no -data -text = " 暂无可用拖车供应商 " style = "width:100%" @ update : model -value = " selectCarrier ( String ( $ event | | ' ' ) ) " > < el-option v-if = "draft.carrierId && !selectableCarriers.some((item) => String(item.id) === String(draft.carrierId))" :label="draft.carrierName || '当前承运商(需重新选择)'" :value="String(draft.carrierId)" disabled /><el-option v-for="item in selectableCarriers" :key="String(item.id)" :label="item.name" :value="String(item.id)" / > < / el-select > < / el-form-item > < / el-col >
< el-col :xs = "24" :sm = "12" > < el-form-item label = "预计装箱时间" prop = "plannedLoadingAt" > < el-date-picker v-model = "draft.plannedLoadingAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计装箱时间" style="width:100%" / > < / el-form-item > < / el-col >
< el-col :xs = "24" :sm = "12" > < el-form-item label = "提单号" > < el-input : model -value = " draft.billOfLadingNo | | ' ' " disabled placeholder = "选择订舱后自动带入" / > < div v-if = "selectedReleasedBooking" class="field-hint" > 已从放舱订舱冻结资料带入 < / div > < / el -form -item > < / el-col >
< el-col :xs = "24" :sm = "12" > < el-form-item label = "仓库" > < el-input : model -value = " selectedReleasedBooking ? .warehouseName | | resource ? .warehouseName | | ' ' " disabled placeholder = "选择订舱后自动带入" / > < / el-form-item > < / el-col >
< el-col v-if = "resource" :xs="24" :sm="12"><el-form-item label="派车单号"><el-input :model-value="resource.appointmentNo || ''" disabled / > < / el-form-item > < / el-col >
< el-col :xs = "24" > < el-divider content -position = " left " > 运输与时间计划 < / el-divider > < / el-col >
< el-col :xs = "24" :sm = "12" > < el-form-item label = "车队联系人" > < el-input v-model = "draft.fleetContact" placeholder="请输入车队联系人" / > < / el-form-item > < / el-col >
< el-col :xs = "24" :sm = "12" > < el-form-item label = "车队电话" prop = "fleetPhone" > < el-input v-model = "draft.fleetPhone" placeholder="请输入车队联系电话" maxlength="32" show -word -limit / > < / el-form-item > < / el-col >
< el-col :xs = "24" :sm = "12" > < el-form-item label = "司机" > < el-input v-model = "draft.d riv erName" placeholder="请输入司机姓名 " / > < / el-form-item > < / el-col >
< el-col :xs = "24" :sm = "12" > < el-form-item label = "司机电话" prop = "d riv erPhone" > < el-input v-model = "draft.driverPhone " p laceholder="请输入司机联系电话" maxlength="32" show -word -limit / > < / el-form-item > < / el-col >
< div class = "appointment-grid ">
< el-form-item label = "订舱" prop = "oceanBookingId" >
< el-select :model-value = "draft.oceanBookingId" filterable :disabled = "relationLocked" placeholder = "请选择已放舱订舱" :loading = "optionsLoading" no -data -text = " 暂无可创建拖车单的已放舱订舱 " @ update : model -value = " selectBooking ( String ( $ event | | ' ' ) ) " >
< el-option v-if = "selectedBookingFallback" :key="selectedBookingFallback.id" :label="selectedBookingFallback.label" :value="selectedBookingFallback.id" disabled / >
< el-option v-for = "item in bookings" :key="item.id" :label="`${item.bookingNo || '订舱号缺失'} / 提单号:${item.billOfLadingNo || '缺失'}`" :value="item.id" / >
< / el-select >
< dl v-if = "draft.oceanBookingId" class="selection-summary" aria-label="订舱带入信息" >
< div > < dt > 仓库 < / dt > < dd > { { selectedReleasedBooking ? . warehouseName || resource ? . warehouseName || '—' } } < / dd > < / div >
< div > < dt > 提单号 < / dt > < dd > { { draft . billOfLadingNo || '—' } } < / dd > < / div >
< / dl >
< div v-if = "bookingContainersError" class="field-error"><span>{{ bookingContainersError.message }}</span><ApiErrorMeta :details="bookingContainersError" /><el-button link type="primary" :loading="bookingContainersLoading" @click="draft.oceanBookingId && loadCurrentBookingContext(String(draft.oceanBookingId))" > 重新加载 < / el -button > < / div >
< / el-form-item >
< el-form-item label = "拖车供应商" prop = "carrierId" >
< el-select :model-value = "draft.carrierId" filterable clearable placeholder = "请选择拖车供应商" :loading = "optionsLoading" no -data -text = " 暂无可用拖车供应商 " @ update : model -value = " selectCarrier ( String ( $ event | | ' ' ) ) " >
< el-option v-if = "draft.carrierId && !selectableCarriers.some((item) => String(item.id) === String(draft.carrierId))" :label= "draft.car rierName || '当前供应商(需重新选择)'" :value="String(draft.carrierId) " disabled / >
< el-option v-for = "item in selectableCar riers" :key="String(item.id) " : label="item.name" :value="String(item.id)" / >
< / el-select >
< dl v-if = "draft.carrierId" class="selection-summary" aria-label="车队联系方式" >
< div > < dt > 联系人 < / dt > < dd > { { draft . fleetContact || '—' } } < / dd > < / div >
< div > < dt > 电话 < / dt > < dd > { { draft . fleetPhone || '—' } } < / dd > < / div >
< / dl >
< / el-form-item >
< div v-if = "draft.oceanBookingId && !bookingContainersLoading && !bookingContainersError" class="appointment-grid__wide readiness-alerts" >
< el -alert v-if = "!draft.billOfLadingNo?.trim()" title="所选订舱缺少有效提单号,暂不能创建或保存拖车预约" type="warning" :closable="false" show -icon / >
< el-alert v-if = "!bookingDocumentReady" title="所选订舱缺少有效的订舱确认文件或提单文件,暂不能创建或保存拖车预约" type="warning" :closable="false" show -icon / >
< / div >
< el-form-item label = "预约装柜时间" prop = "plannedLoadingAt" > < el-date-picker v-model = "draft.plannedLoadingAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预约装柜时间" / > < / el-form-item >
< el-form-item label = "拖车费" prop = "payableAmount" > < el-input v-model = "draft.payableAmount" inputmode="decimal" placeholder="请输入应付拖车费"><template #append>元</template></el-input><div class="field-hint" > 人民币 ; 离仓完成后计入应付 。 < / div > < / el -form -item >
< el-form-item class = "appointment-grid__wide" label = "备注(选填)" > < el-input v-model = "draft.remark" type="textarea" :rows="2" maxlength="1000" placeholder="例如到仓时间要求、装柜注意事项" / > < / el-form-item >
< / div >
< details class = "dispatch-details" :open = "dispatchDetailsOpen" @toggle ="toggleDispatchDetails" >
< summary > 派车信息 < span > 选填 , 司机和车辆确定后可补充 < / span > < / summary >
< div class = "appointment-grid dispatch-grid" >
< el-form-item label = "车队联系人" > < el-input v-model = "draft.fleetContact" maxlength="64" placeholder="选择供应商后带入,可修改" / > < / el-form-item >
< el-form-item label = "车队电话" prop = "fleetPhone" > < el-input v-model = "draft.fleetPhone" maxlength="32" placeholder="选择供应商后带入,可修改" / > < / el-form-item >
< el-form-item label = "司机" > < el-input v-model = "draft.driverName" maxlength="64" placeholder="司机姓名" / > < / el-form-item >
< el-form-item label = "司机电话" prop = "driverPhone" > < el-input v-model = "draft.driverPhone" maxlength="32" placeholder="司机联系电话" / > < / el-form-item >
< el-form-item label = "车牌号" > < el-input v-model = "draft.vehicleNo" maxlength="32" placeholder="车牌或车头号" / > < / el-form-item >
< / div >
< / details >
< details v-if = "resource" class="dispatch-details" >
< summary > 其他已存资料 < / summary >
< el-row :gutter = "16" >
< el-col :xs = "24" :sm = "12" > < el-form-item label = "司机身份证号" > < el-input v-model = "draft.driverIdCard" placeholder="请输入司机身份证号" / > < / el-form-item > < / el-col >
< el-col :xs = "24" :sm = "12" > < el-form-item label = "车头号" > < el-input v-model = "draft.vehicleNo" placeholder="请输入车牌或车头号" / > < / el-form-item > < / el-col >
< el-col v-if = "resource" :xs="24" :sm="12"><el-form-item label="实际装箱时间"><el-input :model-value="resource.actualLoadingAt || ''" disabled / > < / el-form-item > < / el-col >
< el-col :xs = "24" :sm = "8" > < el-form-item label = "件数" prop = "packageCount" > < el-input v-model = "draft.packageCount" inputmode="numeric" placeholder="请输入件数" / > < / el-form-item > < / el-col >
< el-col :xs = "24" :sm = "8" > < el-form-item label = "毛重(kg) " prop = "grossWeightKg" > < el-input v-model = "draft.grossWeightKg" inputmode="decimal" placeholder="请输入毛重" / > < / el-form-item > < / el-col >
< el-col :xs = "24" :sm = "8" > < el-form-item label = "体积(m³)" prop = "volumeCbm" > < el-input v-model = "draft.volumeCbm" inputmode="decimal" placeholder="请输入体积" / > < / el-form-item > < / el-col >
< el-col :xs = "24" :sm = "8" > < el-form-item label = "托盘数量" prop = "palletCount" > < el-input v-model = "draft.palletCount" inputmode="numeric" placeholder="请输入托盘数量" / > < / el-form-item > < / el-col >
< el-col :xs = "24" :sm = "8" > < el-form-item label = "在途分钟" prop = "transitMinutes" > < el-input v-model = "draft.transitMinutes" inputmode="numeric" placeholder="请输入预计在途分钟" / > < / el-form-item > < / el-col >
< el-col :xs = "24" > < el-divider content -position = " left " > 费用信息 < / el-divider > < / el-col >
< el-col :xs = "24" :sm = "12" > < el-form-item label = "应付拖车费" prop = "payableAmount" > < el-input v-model = "draft.payableAmount" placeholder="请输入应付金额" > < template # append > { { draft . currency } } < / template > < / el-input > < / el-form-item > < / el-col >
< el-col :xs = "24" :sm = "12" > < el-form-item label = "币种" prop = "currency" > < el-select v-model = "draft.currency" aria-label="币种" placeholder="请选择币种" style="width:100%"><el-option v-for="currency in TRUCK_APPOINTMENT_CURRENCIES" :key="currency" :label="currency" :value="currency" / > < / el-select > < / el-form-item > < / el-col >
< el-col :xs = "24" > < el-divider content -position = " left " > 门店信息与备注 < / el-divider > < / el-col >
< el-col :xs = "24" :sm = "12" > < el-form-item label = "门店名称" > < el-input v-model = "draft.storeName" placeholder="请输入目的门店名称" / > < / el-form-item > < / el-col >
< el-col class = "wide-column" :xs = "24" :sm = "12" > < el-form-item label = "门店地址" > < el-input v-model = "draft.storeAddress" placeholder="请输入目的门店地址" / > < / el-form-item > < / el-col >
< el-col :xs = "24" :sm = "12" > < el-form-item label = "门店联系人" > < el-input v-model = "draft.storeContact" placeholder="请输入门店联系人" / > < / el-form-item > < / el-col >
@@ -187,34 +229,43 @@ watch(() => props.resource?.id, (id, previous) => { if (id && id !== previous) v
< el-col :xs = "24" :sm = "12" > < el-form-item label = "城市" > < el-input v-model = "draft.storeCity" placeholder="请输入城市" / > < / el-form-item > < / el-col >
< el-col :xs = "24" :sm = "12" > < el-form-item label = "邮编" > < el-input v-model = "draft.storeZipcode" placeholder="请输入邮政编码" / > < / el-form-item > < / el-col >
< el-col class = "wide-column" :xs = "24" :sm = "12" > < el-form-item label = "PO" > < el-input v-model = "draft.poNo" placeholder="请输入采购订单号(可选)" / > < / el-form-item > < / el-col >
< el-col :xs = "24" > < el-form-item label = "备注" > < el-input v-model = "draft.remark" type="textarea" :rows="3" placeholder="请输入拖车预约备注(可选)" / > < / el-form-item > < / el-col >
< / el-row >
< div class = "form-actions" > < el-button :disabled = "submitting" @click ="emit('attachments')" > < Eye :size = "16" / > 查看附件引用 < / el-button > < el-button v-if = "!readonly" type="primary" :loading="submitting" :disabled="submitBlocked" @click="submitForm"><Save :size="16" / > 保存 < / el-button > < / div >
< / el-row >
< / details >
< div class = "form-actions" > < el-button v-if = "resource" :disabled= "submitting" @click="emit('attachments')"><Eye :size="16" />查看附件引用</el-button><el-button v-if= "!readonly" type="primary" :loading="submitting" :disabled="submitBlocked" @click="submitForm"><Save :size="16" / > { { resource ? '保存' : '保存预约' } } < / el-button > < / div >
< / el-form >
< / template >
< style scoped >
. truck - form { padding - bottom : var ( -- admin - space - 1 ) ; }
. relation - note { display : flex ; min - height : 40 px ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 5 ) ; margin : 0 0 var ( -- admin - space - 3 ) ; padding : var ( -- admin - space - 2 ) var ( -- admin - space - 3 ) var ( -- admin - space - 2 ) var ( -- admin - space - 4 ) ; border - left : 3 px solid var ( -- cc - primary ) ; background : var ( -- el - fill - color - extra - light ) ; color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption ) ; line - height : 1.5 ; }
. relation - note > span { min - width : 0 ; }
. relation - note _ _actions { display : flex ; flex : 0 0 auto ; align - items : center ; }
. relation - note _ _actions : deep ( . el - button ) { margin - left : 0 ; }
. relation - alert { margin - bottom : var ( -- admin - space - 5 ) ; }
. form - actions { display : flex ; justify - content : flex - end ; gap : var ( -- admin - space - 3 ) ; flex - wrap : wrap ; }
: deep ( . el - divider ) { margin : var ( -- admin - space - 5 ) 0 var ( -- admin - space - 4 ) ; }
: deep ( . el - divider _ _text ) { color : var ( -- el - text - color - primary ) ; font - size : var ( -- admin - font - section ) ; font - weight : 600 ; }
: deep ( . el - input ) , : deep ( . el - textarea ) , : deep ( . el - date - editor ) { max - width : 100 % ; }
: deep ( . el - select _ _selected - item ) , : deep ( . el - select - dropdown _ _item ) { min - width : 0 ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
. field - error , . field - hint { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 3 ) ; margin - top : var ( -- admin - space - 1 ) ; color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption ) ; line - height : 20 px } . field - error { color : var ( -- el - color - danger ) }
@ media ( min - width : 1200 px ) {
. truck - form : deep ( . el - col - sm - 12 ) , . truck - form : deep ( . el - col - sm - 8 ) { max - width : 25 % ; flex : 0 0 25 % ; }
. truck - form : deep ( . relation - column ) , . truck - form : deep ( . wide - column ) { max - width : 50 % ; flex - basis : 50 % ; }
. truck - form { min - width : 0 ; padding - bottom : var ( -- admin - space - 1 ) ; }
. truck - form -- create { max - width : 1040 px ; margin - inline : auto ; }
. relation - note { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 4 ) ; margin - bottom : var ( -- admin - space - 5 ) ; color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption ) ; line - height : 1.5 ; }
. relation - note _ _actions { display : flex ; flex : 0 0 auto ; }
. relation - alert { margin - bottom : var ( -- admin - space - 5 ) ; }
. appointment - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; column - gap : var ( -- admin - space - 6 ) ; }
. appointment - grid _ _wide { grid - column : 1 / - 1 ; }
: deep ( . el - form - item ) , : deep ( . el - form - item _ _content ) { min - width : 0 ; }
: deep ( . el - select ) , : deep ( . el - date - editor ) , : deep ( . el - input ) , : deep ( . el - textarea ) { width : 100 % ; max - width : 100 % ; }
: deep ( . el - select _ _selected - item ) { min - width : 0 ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
. selection - summary { display : flex ; flex - wrap : wrap ; gap : var ( -- admin - space - 1 ) var ( -- admin - space - 5 ) ; width : 100 % ; margin : var ( -- admin - space - 2 ) 0 0 ; color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption ) ; line - height : 1.5 ; }
. selection - summary div { display : flex ; min - width : 0 ; gap : var ( -- admin - space - 2 ) ; }
. selection - summary dt { flex - shrink : 0 ; }
. selection - summary dd { margin : 0 ; color : var ( -- el - text - color - regular ) ; overflow - wrap : anywhere ; }
. field - error , . field - hint { width : 100 % ; display : flex ; flex - wrap : wrap ; gap : var ( -- admin - space - 2 ) ; margin - top : var ( -- admin - space - 1 ) ; color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption ) ; line - height : 1.5 ; }
. field - error { color : var ( -- el - color - danger ) ; }
. readiness - alerts : empty { display : none ; }
. readiness - alerts : deep ( . el - alert ) { margin - bottom : var ( -- admin - space - 4 ) ; }
. dispatch - details { border - top : 1 px solid var ( -- el - border - color - lighter ) ; }
. dispatch - details summary { padding : var ( -- admin - space - 4 ) 0 ; cursor : pointer ; font - size : var ( -- admin - font - body ) ; font - weight : 600 ; color : var ( -- el - text - color - primary ) ; }
. dispatch - details summary span { margin - left : var ( -- admin - space - 3 ) ; color : var ( -- cc - muted ) ; font - size : var ( -- admin - font - caption ) ; font - weight : 400 ; }
. dispatch - details summary : focus - visible { outline : 2 px solid var ( -- cc - primary ) ; outline - offset : 2 px ; }
. dispatch - grid { grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ; }
. form - actions { display : flex ; justify - content : flex - end ; gap : var ( -- admin - space - 3 ) ; padding - top : var ( -- admin - space - 4 ) ; border - top : 1 px solid var ( -- el - border - color - lighter ) ; }
@ media ( max - width : 720 px ) {
. appointment - grid , . dispatch - grid { grid - template - columns : minmax ( 0 , 1 fr ) ; }
. dispatch - details summary span { display : block ; margin - left : var ( -- admin - space - 4 ) ; }
}
@ media ( max - width : 480 px ) {
. truck - form { padding - inline : 2 px ; }
. relation - note { align - items : flex - start ; flex - direction : column ; }
. relation - note _ _actions { width : 100 % ; }
. form - actions { position : sticky ; bottom : 0 ; z - index : 2 ; margin : 8 px - 2 px - 4 px ; padding : 10 px 2 px ; background : var ( -- el - bg - color ) ; border - top : 1 px solid var ( -- el - border - color - lighter ) ; }
. form - actions : deep ( . el - button ) { flex : 1 ; min - height : 40 px ; }
@ media ( max - width : 480 px ) {
. form - actions { position : sticky ; bottom : 0 ; background : var ( -- el - bg - color ) ; padding - bottom : var ( -- admin - space - 3 ) ; z - index : 2 ; }
. form - actions : deep ( . el - button ) { flex : 1 ; min - height : 40 px ; }
}
< / style >