Merge origin/release/test and reconcile overseas business tests
This commit is contained in:
@@ -4,6 +4,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import { listDrivers, listVehicles, overseasMasterDataPermissions as resourcePermission, type DriverDetail, type VehicleDetail } from '@/modules/overseas-master-data'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import type { Driver, Vehicle } from '../dispatchContracts'
|
||||
import DispatchTimeZonePreview from './DispatchTimeZonePreview.vue'
|
||||
const vehicle = defineModel<Vehicle>('vehicle', { required: true })
|
||||
const driver = defineModel<Driver>('driver', { required: true })
|
||||
const props = defineProps<{ disabled: boolean; continuous?: boolean; dispatchNo?: string }>()
|
||||
@@ -66,7 +67,7 @@ function selectDriver(id: string) {
|
||||
<tr><td><el-form-item label="司机来源"><el-select v-model="driver.mode" :disabled="disabled" @change="changeDriver"><el-option label="司机档案" value="MASTER" /><el-option label="临时外配司机" value="EXTERNAL" /></el-select></el-form-item></td><td><el-form-item v-if="driver.mode === 'MASTER'" label="司机"><el-select v-model="driver.driverId" filterable remote :remote-method="searchDrivers" :disabled="disabled" placeholder="按姓名搜索" @focus="searchDrivers()" @change="selectDriver"><el-option v-if="driver.driverId && !drivers.some(row => row.id === driver.driverId)" :value="driver.driverId" :label="driver.name || '已关联司机(待核实)'" /><el-option v-for="item in drivers" :key="item.id" :value="item.id" :label="item.details.name" /></el-select></el-form-item><el-form-item v-else label="司机姓名"><el-input v-model="driver.name" :disabled="disabled" placeholder="请输入本趟司机姓名" /></el-form-item></td></tr>
|
||||
<tr v-if="driver.mode === 'MASTER'"><td colspan="2"><el-form-item label="联系电话"><el-input :model-value="masterDriverPhone" readonly /></el-form-item></td></tr>
|
||||
<tr v-if="driver.mode === 'EXTERNAL'"><td><el-form-item label="联系电话"><el-input v-model="driver.phone" :disabled="disabled" placeholder="请输入联系电话" /></el-form-item></td><td><el-form-item v-if="driver.evidence" label="驾驶证号"><el-input v-model="driver.evidence.licenseNo" :disabled="disabled" placeholder="审核前补齐证照资料" /></el-form-item></td></tr>
|
||||
<tr v-if="driver.evidence"><td v-if="driver.mode === 'EXTERNAL'"><el-form-item label="驾驶证到期日"><el-date-picker v-model="driver.evidence.licenseExpiresOn" :disabled="disabled" type="date" value-format="YYYY-MM-DD" placeholder="请选择证照到期日" /></el-form-item></td><td :colspan="driver.mode === 'MASTER' ? 2 : 1"><el-form-item label="预计任务结束"><el-date-picker v-model="driver.evidence.expectedTaskEndAt" :disabled="disabled" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" placeholder="包括返程或转场" /></el-form-item></td></tr>
|
||||
<tr v-if="driver.evidence"><td v-if="driver.mode === 'EXTERNAL'"><el-form-item label="驾驶证到期日"><el-date-picker v-model="driver.evidence.licenseExpiresOn" :disabled="disabled" type="date" value-format="YYYY-MM-DD" placeholder="请选择证照到期日" /></el-form-item></td><td :colspan="driver.mode === 'MASTER' ? 2 : 1"><el-form-item label="预计任务结束"><el-date-picker v-model="driver.evidence.expectedTaskEndAt" :disabled="disabled" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" placeholder="包括返程或转场" /><DispatchTimeZonePreview :value="driver.evidence.expectedTaskEndAt" label="预计任务结束" /></el-form-item></td></tr>
|
||||
</tbody></table>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { expect, it } from 'vitest'
|
||||
import DispatchTimeZonePreview from './DispatchTimeZonePreview.vue'
|
||||
|
||||
it.each([
|
||||
['2026-07-01T10:00:00Z', '2026-07-01 06:00:00', '2026-07-01 03:00:00'],
|
||||
['2026-12-01T10:00:00Z', '2026-12-01 05:00:00', '2026-12-01 02:00:00'],
|
||||
['2026-07-01T08:00:00+08:00', '2026-06-30 20:00:00', '2026-06-30 17:00:00'],
|
||||
['2026-11-01T05:30:00Z', '2026-11-01 01:30:00', '2026-10-31 22:30:00'],
|
||||
['2026-11-01T06:30:00Z', '2026-11-01 01:30:00', '2026-10-31 23:30:00'],
|
||||
])('converts the same instant with regional daylight saving rules: %s', (value, eastern, pacific) => {
|
||||
const wrapper = mount(DispatchTimeZonePreview, { props: { value, label: '计划发车时间' } })
|
||||
expect(wrapper.attributes('aria-label')).toBe('计划发车时间时区换算')
|
||||
expect(wrapper.findAll('dt').map(item => item.text())).toEqual(['美东', '美西'])
|
||||
expect(wrapper.findAll('dd').map(item => item.text())).toEqual([eastern, pacific])
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it.each([null, '', 'invalid', '2026-07-01T10:00:00', '2026-07-01'])('does not invent an instant for an empty or unzoned value: %s', value => {
|
||||
const wrapper = mount(DispatchTimeZonePreview, { props: { value, label: '发车时间' } })
|
||||
expect(wrapper.findAll('dd').map(item => item.text())).toEqual(['—', '—'])
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('updates and clears both previews without emitting a changed input', async () => {
|
||||
const wrapper = mount(DispatchTimeZonePreview, { props: { value: '2026-07-01T10:00:00Z', label: '预计到达时间' } })
|
||||
await wrapper.setProps({ value: '2026-12-01T10:00:00Z' })
|
||||
expect(wrapper.findAll('dd').map(item => item.text())).toEqual(['2026-12-01 05:00:00', '2026-12-01 02:00:00'])
|
||||
await wrapper.setProps({ value: '' })
|
||||
expect(wrapper.findAll('dd').map(item => item.text())).toEqual(['—', '—'])
|
||||
expect(wrapper.emitted()).toEqual({})
|
||||
wrapper.unmount()
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{ value?: string | null; label: string }>()
|
||||
const zones = [
|
||||
{ label: '美东', timeZone: 'America/New_York' },
|
||||
{ label: '美西', timeZone: 'America/Los_Angeles' },
|
||||
]
|
||||
|
||||
// 只换算带时差的同一时间点,地区时区负责夏令时;不回写原始录入值。
|
||||
const times = computed(() => {
|
||||
const instant = props.value && /(?:Z|[+-]\d{2}:\d{2})$/i.test(props.value) ? Date.parse(props.value) : NaN
|
||||
return zones.map(zone => {
|
||||
if (!Number.isFinite(instant)) return { label: zone.label, value: '—' }
|
||||
const parts = Object.fromEntries(new Intl.DateTimeFormat('en-GB', {
|
||||
timeZone: zone.timeZone, year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23',
|
||||
}).formatToParts(instant).map(part => [part.type, part.value]))
|
||||
return { label: zone.label, value: `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second}` }
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<dl class="dispatch-time-zone-preview" :aria-label="`${label}时区换算`">
|
||||
<div v-for="time in times" :key="time.label"><dt>{{ time.label }}</dt><dd>{{ time.value }}</dd></div>
|
||||
</dl>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dispatch-time-zone-preview { display: flex; flex: 0 0 100%; flex-wrap: wrap; gap: 2px 12px; width: 100%; margin: 0; color: var(--cc-muted); font-size: var(--admin-font-body, 13px); line-height: 1.6; }
|
||||
.dispatch-time-zone-preview > div { display: flex; flex-wrap: wrap; gap: 0 6px; }
|
||||
.dispatch-time-zone-preview dt, .dispatch-time-zone-preview dd { margin: 0; white-space: nowrap; }
|
||||
</style>
|
||||
@@ -4,6 +4,7 @@ import type { DispatchDetail, Driver, Vehicle } from '../dispatchContracts'
|
||||
import type { DispatchDestinationAppointment, DispatchVehicleMaintenance } from '../dispatchVehicleMaintenance'
|
||||
import { formatOverseasDateTime } from '../lastMilePresentation'
|
||||
import DispatchResourceFields from './DispatchResourceFields.vue'
|
||||
import DispatchTimeZonePreview from './DispatchTimeZonePreview.vue'
|
||||
|
||||
const props = defineProps<{ detail: DispatchDetail; disabled: boolean }>()
|
||||
const vehicle = defineModel<Vehicle>('vehicle', { required: true })
|
||||
@@ -18,9 +19,9 @@ const loadTypes = ['托盘', '地板', '混装']
|
||||
const vehicleTypes = ['厢式货车', '平板车', '集装箱卡车']
|
||||
const appointmentFacts = computed(() => new Map(props.detail.dispatchInfo.fbaAppointments.map(row => [row.destinationKey, row])))
|
||||
/** 修改编号后不继续显示原编号的确认时间,保存后由服务端重新匹配正式确认。 */
|
||||
function confirmedTime(appointment: DispatchDestinationAppointment) {
|
||||
function confirmedAt(appointment: DispatchDestinationAppointment) {
|
||||
const fact = appointmentFacts.value.get(appointment.destinationKey)
|
||||
return formatOverseasDateTime(fact?.appointmentNo === appointment.appointmentNo?.trim() ? fact?.confirmedAt : null)
|
||||
return fact?.appointmentNo === appointment.appointmentNo?.trim() ? fact?.confirmedAt : null
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -30,12 +31,12 @@ function confirmedTime(appointment: DispatchDestinationAppointment) {
|
||||
<div class="density-dialog-sections">
|
||||
<section class="density-dialog-section" aria-label="本车资源登记">
|
||||
<h3 class="density-dialog-section__title">车辆与司机</h3>
|
||||
<table class="density-table density-table--editable maintenance-table"><tbody><tr><td><el-form-item label="排车单号"><el-input :model-value="detail.dispatchNo" readonly /></el-form-item></td><td><el-form-item label="发车时间"><el-input :model-value="formatOverseasDateTime(detail.dispatchInfo.departedAt)" readonly /></el-form-item></td></tr></tbody></table>
|
||||
<table class="density-table density-table--editable maintenance-table"><tbody><tr><td><el-form-item label="排车单号"><el-input :model-value="detail.dispatchNo" readonly /></el-form-item></td><td><el-form-item label="发车时间"><el-input :model-value="formatOverseasDateTime(detail.dispatchInfo.departedAt)" readonly /><DispatchTimeZonePreview :value="detail.dispatchInfo.departedAt" label="发车时间" /></el-form-item></td></tr></tbody></table>
|
||||
<DispatchResourceFields :dispatch-no="detail.dispatchNo" continuous v-model:vehicle="vehicle" v-model:driver="driver" :disabled="disabled" />
|
||||
</section>
|
||||
<section class="density-dialog-section" aria-label="本车时间登记">
|
||||
<h3 class="density-dialog-section__title">排车时间</h3>
|
||||
<table class="density-table density-table--editable maintenance-table"><tbody><tr><td><el-form-item label="计划发车时间"><el-date-picker v-model="plannedDepartureAt" :disabled="disabled" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" placeholder="请选择计划发车时间" /></el-form-item></td><td><el-form-item label="预计到达时间"><el-date-picker v-model="estimatedArrivalAt" :disabled="disabled" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" placeholder="请选择预计到达时间" /></el-form-item></td></tr></tbody></table>
|
||||
<table class="density-table density-table--editable maintenance-table"><tbody><tr><td><el-form-item label="计划发车时间"><el-date-picker v-model="plannedDepartureAt" :disabled="disabled" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" placeholder="请选择计划发车时间" /><DispatchTimeZonePreview :value="plannedDepartureAt" label="计划发车时间" /></el-form-item></td><td><el-form-item label="预计到达时间"><el-date-picker v-model="estimatedArrivalAt" :disabled="disabled" type="datetime" value-format="YYYY-MM-DDTHH:mm:ssZ" placeholder="请选择预计到达时间" /><DispatchTimeZonePreview :value="estimatedArrivalAt" label="预计到达时间" /></el-form-item></td></tr></tbody></table>
|
||||
</section>
|
||||
<section class="density-dialog-section" aria-label="派送补充资料">
|
||||
<h3 class="density-dialog-section__title">派送信息</h3>
|
||||
@@ -49,7 +50,7 @@ function confirmedTime(appointment: DispatchDestinationAppointment) {
|
||||
<el-form-item label="预约账号"><el-input v-model="maintenance.appointmentAccount" :disabled="disabled" maxlength="128" placeholder="请输入预约账号" /></el-form-item>
|
||||
</div>
|
||||
<table v-if="maintenance.destinationAppointments.length" class="density-table density-table--editable" aria-label="目的仓预约"><thead><tr><th>目的仓</th><th>预约号</th><th>确认送达时间</th><th>货件号</th></tr></thead><tbody>
|
||||
<tr v-for="appointment in maintenance.destinationAppointments" :key="appointment.destinationKey"><td>{{ appointmentFacts.get(appointment.destinationKey)?.destinationWarehouseCode || '—' }}</td><td><el-form-item :label="`${appointmentFacts.get(appointment.destinationKey)?.destinationWarehouseCode || '目的仓'} 预约号`"><el-input v-model="appointment.appointmentNo" :disabled="disabled" maxlength="128" placeholder="填写已约好的编号,提交审核前必填" /></el-form-item></td><td>{{ confirmedTime(appointment) }}</td><td>{{ appointmentFacts.get(appointment.destinationKey)?.shipmentNumbers?.join('、') || '—' }}</td></tr>
|
||||
<tr v-for="appointment in maintenance.destinationAppointments" :key="appointment.destinationKey"><td>{{ appointmentFacts.get(appointment.destinationKey)?.destinationWarehouseCode || '—' }}</td><td><el-form-item :label="`${appointmentFacts.get(appointment.destinationKey)?.destinationWarehouseCode || '目的仓'} 预约号`"><el-input v-model="appointment.appointmentNo" :disabled="disabled" maxlength="128" placeholder="填写已约好的编号,提交审核前必填" /></el-form-item></td><td>{{ formatOverseasDateTime(confirmedAt(appointment)) }}<DispatchTimeZonePreview v-if="confirmedAt(appointment)" :value="confirmedAt(appointment)" label="确认送达时间" /></td><td>{{ appointmentFacts.get(appointment.destinationKey)?.shipmentNumbers?.join('、') || '—' }}</td></tr>
|
||||
</tbody></table>
|
||||
</section>
|
||||
<section class="density-dialog-section" aria-label="本车费用信息"><h3 class="density-dialog-section__title">费用信息</h3><slot name="fees"><p class="density-dialog-note">{{ detail.documents.deliveries.length ? '费用以关联派送单的正式费用记录为准。' : '尚未生成派送单费用。' }}</p></slot></section>
|
||||
|
||||
@@ -88,6 +88,8 @@ describe('dispatch workflow native layout', () => {
|
||||
expect(table.findAll('input')).toHaveLength(1)
|
||||
expect((table.get('input').element as HTMLInputElement).value).toBe('ISA-ONE')
|
||||
expect(table.findAll('tbody td')[2]!.text()).not.toBe('—')
|
||||
expect(table.get('[aria-label="确认送达时间时区换算"]').text()).toContain('美东2026-10-25 13:00:00')
|
||||
expect(table.get('[aria-label="确认送达时间时区换算"]').text()).toContain('美西2026-10-25 10:00:00')
|
||||
for (const shipment of shipmentNumbers) expect(table.text()).toContain(shipment)
|
||||
await table.get('input').setValue('ISA-CHANGED')
|
||||
expect(table.findAll('tbody td')[2]!.text()).toBe('—')
|
||||
@@ -144,6 +146,26 @@ describe('dispatch workflow native layout', () => {
|
||||
await dialog.findAll('button').find(button => button.text() === '驳回')!.trigger('click'); await flushPromises()
|
||||
expect(action === 'DISPATCH_REVIEW' ? api.reviewDispatch : api.reviewDispatchLoading).toHaveBeenCalledWith('PC/1', expect.objectContaining({ decision: 'REJECTED', reason: '请客服补齐预约时间' }))
|
||||
})
|
||||
it('shows eastern and pacific previews for maintenance times and saves the original offset values', async () => {
|
||||
const source = detail()
|
||||
const { dialog } = await render('EDIT', undefined, false, { dispatchInfo: { ...source.dispatchInfo,
|
||||
departedAt: '2026-07-01T15:00:00Z', plannedDepartureAt: '2026-07-01T16:00:00Z', estimatedArrivalAt: '2026-07-01T17:00:00Z',
|
||||
} })
|
||||
expect(field(dialog, '发车时间').get('[aria-label="发车时间时区换算"]').text()).toBe('美东2026-07-01 11:00:00美西2026-07-01 08:00:00')
|
||||
expect(field(dialog, '计划发车时间').get('[aria-label="计划发车时间时区换算"]').text()).toBe('美东2026-07-01 12:00:00美西2026-07-01 09:00:00')
|
||||
expect(field(dialog, '预计到达时间').get('[aria-label="预计到达时间时区换算"]').text()).toBe('美东2026-07-01 13:00:00美西2026-07-01 10:00:00')
|
||||
expect(field(dialog, '预计任务结束').get('[aria-label="预计任务结束时区换算"]').text()).toBe('美东2026-11-01 05:00:00美西2026-11-01 02:00:00')
|
||||
field(dialog, '计划发车时间').getComponent({ name: 'ElDatePicker' }).vm.$emit('update:modelValue', '2026-07-02T08:00:00+08:00')
|
||||
await flushPromises()
|
||||
expect(field(dialog, '计划发车时间').get('[aria-label="计划发车时间时区换算"]').text()).toBe('美东2026-07-01 20:00:00美西2026-07-01 17:00:00')
|
||||
dialog.vm.$emit('confirm'); await flushPromises()
|
||||
expect(api.updateDispatch).toHaveBeenCalledWith('PC/1', expect.objectContaining({
|
||||
plannedDepartureAt: '2026-07-02T08:00:00+08:00', estimatedArrivalAt: '2026-07-01T17:00:00Z',
|
||||
driver: expect.objectContaining({ evidence: expect.objectContaining({ expectedTaskEndAt: '2026-11-01T10:00:00Z' }) }),
|
||||
}))
|
||||
expect(vi.mocked(api.updateDispatch).mock.calls[0]![1]).not.toHaveProperty('departedAt')
|
||||
})
|
||||
|
||||
it('submits all edited resources, dates and zero version through the continuous fullscreen form', async () => {
|
||||
const { dialog } = await render('EDIT')
|
||||
expect(dialog.props('title')).toBe('维护车辆 · PC/1')
|
||||
|
||||
Reference in New Issue
Block a user