This commit is contained in:
2026-10-20 23:34:11 +08:00
parent f155976f52
commit 13e4aa56ec
321 changed files with 6756 additions and 2110 deletions
@@ -64,6 +64,33 @@ describe('CarrierScheduleQueryPanel loading states', () => {
expect(getPortSchedule).not.toHaveBeenCalled()
})
it('keeps complete ordered facts in the dense table and preserves the non-dense timeline', async () => {
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 })
const summary = '长说明:核验装柜结果及相关业务记录,保留全部内容。'.repeat(4)
const wrapper = mount(CarrierScheduleQueryPanel, {
props: { dense: true, oceanBookingId: '2', warehouseTimeline: [
{ eventType: 'PDA_SUBMIT_PICK', detail: summary, operatorName: '仓库作业员', occurredAt: '2026-08-20T08:00:00' },
{ eventType: 'PDA_COMPLETE_LOADING', detail: '完成装柜', operatorName: '复核员', occurredAt: '2026-08-20T09:00:00' },
] },
global: { plugins: [ElementPlus, pinia] },
})
await flushPromises()
const table = wrapper.get('table[aria-label="货柜履约轨迹"]')
expect(table.findAll('thead th').map(cell => cell.text())).toEqual(['时间', '类型', '事件', '说明', '操作人 / 来源'])
const rows = table.findAll('tbody tr')
expect(rows).toHaveLength(2)
expect(rows[0]!.text()).toContain('2026-08-20 09:00')
expect(rows[1]!.findAll('td')[3]!.text()).toBe(summary)
expect(rows[1]!.findAll('td')[4]!.text()).toBe('仓库作业员')
expect(wrapper.find('.fulfillment-timeline').exists()).toBe(false)
await wrapper.setProps({ dense: false })
expect(wrapper.find('table[aria-label="货柜履约轨迹"]').exists()).toBe(false)
expect(wrapper.findAll('.fulfillment-timeline li')).toHaveLength(2)
wrapper.unmount()
})
it('shows manual entry without creating a tracking task', async () => {
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 })
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', containerBookingId: '3' }, global: { plugins: [ElementPlus, pinia] } })
@@ -1,5 +1,6 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import '@/styles/adminWorkbench.css'
import { computed, reactive, ref, watch } from 'vue'
import { Plus } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
@@ -258,7 +259,7 @@ watch(manualDialogOpen, (open) => { if (!open && !manualSaving.value) { manualId
</script>
<template>
<section class="carrier-query" :class="{ 'admin-density': dense }" aria-label="货柜履约轨迹">
<section class="carrier-query admin-workbench" :class="{ 'admin-density': dense }" aria-label="货柜履约轨迹">
<header><div><h2>货柜履约轨迹</h2></div><div class="header-actions"><span class="history-count">仓内 {{ filteredWarehouseTimeline.length }} 条 · 共 {{ fulfillmentEvents.length }} 条</span><el-button v-if="props.containerBookingId && access.can('platform:firstleg:port-schedule-query:manual')" :class="{ 'density-action': dense }" data-tone="primary" size="small" type="primary" plain @click="openManualDialog"><Plus v-if="!dense" :size="15" />人工补录节点</el-button><el-tag v-if="query" :type="statusType" effect="light">{{ statusText }}</el-tag></div></header>
<LoadErrorState v-if="error" compact title="船节点查询状态加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<div v-else-if="loading" class="loading-state"><el-skeleton :rows="1" animated /></div>
@@ -268,24 +269,31 @@ watch(manualDialogOpen, (open) => { if (!open && !manualSaving.value) { manualId
<el-alert v-if="queryDetailError" type="warning" :closable="false" show-icon title="标准节点与变更历史暂不可用"><template #default><ApiErrorMeta :details="queryDetailError" /></template></el-alert>
<el-alert v-if="scheduleError" type="warning" :closable="false" show-icon title="关联船期结果暂不可用"><template #default><ApiErrorMeta :details="scheduleError" /></template></el-alert>
<div v-if="latestSchedule" class="latest-result"><strong>{{ latestSchedule.schedule.vesselName }} / {{ latestSchedule.schedule.voyageNo }}</strong><span>{{ latestSchedule.schedule.departurePortName || latestSchedule.schedule.departurePortCode }} → {{ latestSchedule.schedule.destinationPortName || latestSchedule.schedule.destinationPortCode }}</span><span>预计开船 {{ formatTime(latestSchedule.schedule.etdAt) }} · 预计到港 {{ formatTime(latestSchedule.schedule.etaAt) }}</span></div>
<ol v-if="fulfillmentEvents.length" class="fulfillment-timeline"><li v-for="item in fulfillmentEvents" :key="item.key"><time>{{ formatTime(item.time) }}</time><el-tag size="small" :type="eventTagType(item.type)">{{ item.type }}</el-tag><strong>{{ item.title }}</strong><span>{{ item.summary }}</span><small>{{ item.source }}</small></li></ol>
<div v-if="dense && fulfillmentEvents.length" class="fulfillment-table-wrap workbench-evidence">
<table class="fulfillment-table density-table" aria-label="货柜履约轨迹">
<colgroup><col class="fulfillment-table__time" /><col class="fulfillment-table__type" /><col class="fulfillment-table__event" /><col /><col class="fulfillment-table__source" /></colgroup>
<thead><tr><th scope="col">时间</th><th scope="col">类型</th><th scope="col">事件</th><th scope="col">说明</th><th scope="col">操作人 / 来源</th></tr></thead>
<tbody><tr v-for="item in fulfillmentEvents" :key="item.key"><td><time>{{ formatTime(item.time) }}</time></td><td><el-tag size="small" :type="eventTagType(item.type)">{{ item.type }}</el-tag></td><td><strong>{{ item.title }}</strong></td><td>{{ item.summary }}</td><td>{{ item.source }}</td></tr></tbody>
</table>
</div>
<ol v-else-if="fulfillmentEvents.length" class="fulfillment-timeline"><li v-for="item in fulfillmentEvents" :key="item.key"><time>{{ formatTime(item.time) }}</time><el-tag size="small" :type="eventTagType(item.type)">{{ item.type }}</el-tag><strong>{{ item.title }}</strong><span>{{ item.summary }}</span><small>{{ item.source }}</small></li></ol>
<p v-else class="empty-line">暂无货柜履约记录</p>
<p v-if="!query" class="tracking-prompt">当前未开启跟踪任务。</p>
<details v-if="supersededActualNodes.length" class="technical-details duplicate-node-history"><summary>历史重复实际节点({{ supersededActualNodes.length }})</summary><ol class="technical-history"><li v-for="item in supersededActualNodes" :key="item.id"><span>{{ nodeTimeText(item) }}</span><strong>{{ nodeText(item.standardNode) }}</strong><small>{{ nodeLocation(item) }} · {{ item.providerCode }}<template v-if="item.sourceReference"> · {{ item.sourceReference }}</template></small></li></ol></details>
<details v-if="query" class="technical-details"><summary>查询任务摘要、异常与变更历史</summary><dl class="query-facts" :class="{ 'density-fields': dense }"><div><dt>查询任务</dt><dd>{{ query.queryNo || query.id }}</dd></div><div><dt>跟踪标识</dt><dd>{{ trackingTypeText(query.trackingType) }} · {{ query.trackingNo || '—' }}</dd></div><div><dt>任务状态</dt><dd>{{ statusText }}</dd></div><div><dt>建立来源</dt><dd>{{ triggerSourceText(query.triggerSource) }}</dd></div><div><dt>最新实际节点</dt><dd>{{ nodeText(query.latestActualNode) }}</dd></div><div><dt>实际发生时间</dt><dd>{{ formatTime(query.latestActualAt) }}</dd></div><div><dt>最新来源</dt><dd>{{ latestSource }}</dd></div><div><dt>来源参考</dt><dd class="break-value">{{ sourceReference }}</dd></div><div><dt>观测时间</dt><dd>{{ formatTime(latestObservation) }}</dd></div><div><dt>最近成功</dt><dd>{{ formatTime(query.lastSuccessAt) }}</dd></div><div><dt>结果有效性</dt><dd>{{ queryView?.expired ? '结果已过期' : '结果有效' }}</dd></div><div v-if="query.lastErrorCode"><dt>最近错误码</dt><dd>{{ query.lastErrorCode }}</dd></div></dl><h3>任务变更历史</h3><p v-if="!histories.length" class="empty-line">暂无变更历史</p><ol v-else class="technical-history"><li v-for="item in histories" :key="item.id"><span>{{ formatTime(item.createdAt) }}</span><strong>{{ historyText(item.action) }}</strong><small>{{ item.operatorName || '系统' }}<template v-if="item.reason"> · {{ item.reason }}</template></small></li></ol></details>
<details v-if="query" class="technical-details"><summary>查询任务摘要、异常与变更历史</summary><dl class="query-facts" :class="{ 'density-fields': dense }"><div><dt>查询任务</dt><dd>{{ query.queryNo || '—' }}</dd></div><div><dt>跟踪标识</dt><dd>{{ trackingTypeText(query.trackingType) }} · {{ query.trackingNo || '—' }}</dd></div><div><dt>任务状态</dt><dd>{{ statusText }}</dd></div><div><dt>建立来源</dt><dd>{{ triggerSourceText(query.triggerSource) }}</dd></div><div><dt>最新实际节点</dt><dd>{{ nodeText(query.latestActualNode) }}</dd></div><div><dt>实际发生时间</dt><dd>{{ formatTime(query.latestActualAt) }}</dd></div><div><dt>最新来源</dt><dd>{{ latestSource }}</dd></div><div><dt>来源参考</dt><dd class="break-value">{{ sourceReference }}</dd></div><div><dt>观测时间</dt><dd>{{ formatTime(latestObservation) }}</dd></div><div><dt>最近成功</dt><dd>{{ formatTime(query.lastSuccessAt) }}</dd></div><div><dt>结果有效性</dt><dd>{{ queryView?.expired ? '结果已过期' : '结果有效' }}</dd></div><div v-if="query.lastErrorCode"><dt>最近错误码</dt><dd>{{ query.lastErrorCode }}</dd></div></dl><h3>任务变更历史</h3><p v-if="!histories.length" class="empty-line">暂无变更历史</p><ol v-else class="technical-history"><li v-for="item in histories" :key="item.id"><span>{{ formatTime(item.createdAt) }}</span><strong>{{ historyText(item.action) }}</strong><small>{{ item.operatorName || '系统' }}<template v-if="item.reason"> · {{ item.reason }}</template></small></li></ol></details>
</template>
<AppDialog :class="{ 'admin-density-dialog': dense }" v-model="manualDialogOpen" :title="manualDialogTitle" :width="dense ? 'min(780px, calc(100vw - 28px))' : 'min(620px, calc(100vw - 28px))'" :fullscreenable="dense" confirm-text="确认补录" :confirm-loading="manualSaving" @confirm="submitManualNode">
<AppDialog class="admin-workbench is-dialog" :class="{ 'admin-density-dialog': dense }" v-model="manualDialogOpen" :title="manualDialogTitle" :width="dense ? 'min(780px, calc(100vw - 28px))' : 'min(620px, calc(100vw - 28px))'" :fullscreenable="dense" confirm-text="确认补录" :confirm-loading="manualSaving" @confirm="submitManualNode">
<el-alert v-if="manualError" type="error" :closable="false" show-icon title="补录未成功,已保留当前填写内容"><template #default><ApiErrorMeta :details="manualError" /></template></el-alert>
<el-alert v-if="manualValidationError" type="warning" :closable="false" show-icon :title="manualValidationError" />
<el-alert type="info" :closable="false" title="观测时间按提交时刻记录;预计事实不推进实际进度,既有事实不可编辑或删除。" />
<section v-if="dense" class="density-dialog-section" data-tone="work">
<el-form class="density-form">
<table class="density-table density-table--editable manual-editor-table" aria-label="人工补录船节点"><tbody>
<div class="workbench-evidence"><table class="density-table density-table--editable manual-editor-table" aria-label="人工补录船节点"><tbody>
<tr><td colspan="2"><el-form-item label-position="left" label-width="90px" label="标准节点" required><el-select v-model="manualForm.standardNode" placeholder="请选择标准节点"><el-option v-for="node in standardNodes" :key="node" :label="nodeText(node)" :value="node" /></el-select></el-form-item></td></tr>
<tr><td><el-form-item label-position="left" label-width="90px" label="预计时间"><el-date-picker v-model="manualForm.estimatedAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="可选" /></el-form-item></td><td><el-form-item label-position="left" label-width="90px" label="实际时间"><el-date-picker v-model="manualForm.actualAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="可选,填写后推进实际节点" /></el-form-item></td></tr>
<tr><td><el-form-item label-position="left" label-width="90px" label="地点编码"><el-input v-model="manualForm.locationCode" maxlength="32" placeholder="可选,如 USLAX" /></el-form-item></td><td><el-form-item label-position="left" label-width="90px" label="地点名称"><el-input v-model="manualForm.locationName" maxlength="128" placeholder="可选" /></el-form-item></td></tr>
<tr><td colspan="2"><el-form-item label-position="left" label-width="90px" label="补录原因" required><el-input v-model="manualForm.reason" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="请输入事实来源与补录原因" /></el-form-item></td></tr>
</tbody></table>
</tbody></table></div>
</el-form>
</section>
<el-form v-else label-position="top" class="manual-form">
@@ -299,6 +307,13 @@ watch(manualDialogOpen, (open) => { if (!open && !manualSaving.value) { manualId
</template>
<style scoped>
.history-count{color:var(--cc-muted);font-size:var(--admin-font-caption, 12px);white-space:nowrap}
.carrier-query{display:grid;gap:9px;padding-top:2px}.carrier-query>header{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--admin-space-5, 12px)}.header-actions{display:flex;align-items:center;gap:var(--admin-space-3, 8px)}.carrier-query h2{margin:0;font-size:var(--admin-font-section, 15px)}.loading-state{padding:var(--admin-space-2, 6px) 0}.latest-result{display:flex;flex-wrap:wrap;align-items:center;gap:var(--admin-space-2, 6px) var(--admin-space-6, 16px);padding:7px var(--admin-space-4, 10px);border-left:3px solid var(--el-color-primary);background:var(--el-color-primary-light-9);font-size:var(--admin-font-caption, 12px)}.fulfillment-timeline{display:grid;margin:0;padding:0;list-style:none}.fulfillment-timeline li{display:grid;grid-template-columns:130px 86px 150px minmax(180px,1fr) minmax(120px,.8fr);align-items:center;gap:7px;padding:var(--admin-space-2, 6px) 2px;border-top:1px solid var(--cc-border);font-size:var(--admin-font-caption, 12px)}.fulfillment-timeline time,.fulfillment-timeline span,.fulfillment-timeline small{overflow:hidden;color:var(--cc-muted);text-overflow:ellipsis;white-space:nowrap}.tracking-prompt{margin:0;padding:7px var(--admin-space-4, 10px);border-left:3px solid var(--el-color-info);background:var(--cc-bg-subtle,#f7f8fa);color:var(--cc-muted);font-size:var(--admin-font-caption, 12px)}.empty-line{margin:0;color:var(--cc-muted);font-size:var(--admin-font-caption, 12px)}.technical-details{border-top:1px solid var(--cc-border);font-size:var(--admin-font-caption, 12px)}.technical-details summary{padding:7px 2px;color:var(--cc-primary);cursor:pointer}.query-facts:not(.density-fields){display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--admin-space-3, 8px) var(--admin-space-6, 16px);margin:4px 0 var(--admin-space-4, 10px)}.query-facts:not(.density-fields)>div{min-width:0}.query-facts:not(.density-fields) dt{color:var(--cc-muted);font-size:var(--admin-font-caption, 11px)}.query-facts:not(.density-fields) dd{margin:2px 0 0;font-size:var(--admin-font-caption, 12px)}.break-value{overflow-wrap:anywhere}.technical-details h3{margin:var(--admin-space-3, 8px) 0 5px;font-size:var(--admin-font-caption, 12px)}.technical-history{margin:0;padding:0;list-style:none}.technical-history li{display:grid;grid-template-columns:130px 150px minmax(0,1fr);gap:var(--admin-space-3, 8px);padding:5px 0;border-top:1px solid var(--cc-border)}.technical-history small{color:var(--cc-muted)}.manual-editor-table{table-layout:fixed}.manual-form{display:grid;gap:2px;margin-top:var(--admin-space-5, 12px)}.manual-form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--admin-space-5, 12px)}@media(max-width:1200px){.query-facts{grid-template-columns:repeat(2,minmax(0,1fr))}.fulfillment-timeline li{grid-template-columns:120px 82px 140px minmax(0,1fr)}.fulfillment-timeline small{grid-column:4}}@media(max-width:720px){.carrier-query>header{align-items:flex-start;flex-direction:column}.query-facts,.manual-form-grid{grid-template-columns:1fr}.fulfillment-timeline li{grid-template-columns:90px minmax(0,1fr)}.fulfillment-timeline strong,.fulfillment-timeline span,.fulfillment-timeline small{grid-column:2}.technical-history li{grid-template-columns:1fr}}.carrier-query.admin-density .fulfillment-timeline :is(time,span,small){overflow:visible;white-space:normal;overflow-wrap:anywhere}.carrier-query.admin-density .technical-history small{overflow-wrap:anywhere}
.fulfillment-table-wrap { min-width:0; overflow-x:auto; }
.fulfillment-table { min-width:800px; table-layout:fixed; }
.fulfillment-table__time { width:160px; }
.fulfillment-table__type { width:110px; }
.fulfillment-table__event { width:160px; }
.fulfillment-table__source { width:20%; }
.fulfillment-table time { white-space:nowrap; font-variant-numeric:tabular-nums; }
.history-count{color:var(--workbench-muted);font-size:var(--admin-font-caption, 12px);white-space:nowrap}
.carrier-query{display:grid;gap:9px;padding-top:2px}.carrier-query>header{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--admin-space-5, 12px)}.header-actions{display:flex;align-items:center;gap:var(--admin-space-3, 8px)}.carrier-query h2{margin:0;font-size:var(--admin-font-section, 15px)}.loading-state{padding:var(--admin-space-2, 6px) 0}.latest-result{display:flex;flex-wrap:wrap;align-items:center;gap:var(--admin-space-2, 6px) var(--admin-space-6, 16px);padding:7px var(--admin-space-4, 10px);border-left:3px solid var(--el-color-primary);background:var(--el-color-primary-light-9);font-size:var(--admin-font-caption, 12px)}.fulfillment-timeline{display:grid;margin:0;padding:0;list-style:none}.fulfillment-timeline li{display:grid;grid-template-columns:130px 86px 150px minmax(180px,1fr) minmax(120px,.8fr);align-items:center;gap:7px;padding:var(--admin-space-2, 6px) 2px;border-top:1px solid var(--workbench-border);font-size:var(--admin-font-caption, 12px)}.fulfillment-timeline time,.fulfillment-timeline span,.fulfillment-timeline small{overflow:hidden;color:var(--workbench-muted);text-overflow:ellipsis;white-space:nowrap}.tracking-prompt{margin:0;padding:7px var(--admin-space-4, 10px);border-left:3px solid var(--el-color-info);background:var(--cc-bg-subtle,#f7f8fa);color:var(--workbench-muted);font-size:var(--admin-font-caption, 12px)}.empty-line{margin:0;color:var(--workbench-muted);font-size:var(--admin-font-caption, 12px)}.technical-details{border-top:1px solid var(--workbench-border);font-size:var(--admin-font-caption, 12px)}.technical-details summary{padding:7px 2px;color:var(--workbench-primary);cursor:pointer}.query-facts:not(.density-fields){display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--admin-space-3, 8px) var(--admin-space-6, 16px);margin:4px 0 var(--admin-space-4, 10px)}.query-facts:not(.density-fields)>div{min-width:0}.query-facts:not(.density-fields) dt{color:var(--workbench-muted);font-size:var(--admin-font-caption, 11px)}.query-facts:not(.density-fields) dd{margin:2px 0 0;font-size:var(--admin-font-caption, 12px)}.break-value{overflow-wrap:anywhere}.technical-details h3{margin:var(--admin-space-3, 8px) 0 5px;font-size:var(--admin-font-caption, 12px)}.technical-history{margin:0;padding:0;list-style:none}.technical-history li{display:grid;grid-template-columns:130px 150px minmax(0,1fr);gap:var(--admin-space-3, 8px);padding:5px 0;border-top:1px solid var(--workbench-border)}.technical-history small{color:var(--workbench-muted)}.manual-editor-table{table-layout:fixed}.manual-form{display:grid;gap:2px;margin-top:var(--admin-space-5, 12px)}.manual-form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--admin-space-5, 12px)}@media(max-width:1200px){.query-facts{grid-template-columns:repeat(2,minmax(0,1fr))}.fulfillment-timeline li{grid-template-columns:120px 82px 140px minmax(0,1fr)}.fulfillment-timeline small{grid-column:4}}@media(max-width:720px){.carrier-query>header{align-items:flex-start;flex-direction:column}.query-facts,.manual-form-grid{grid-template-columns:1fr}.fulfillment-timeline li{grid-template-columns:90px minmax(0,1fr)}.fulfillment-timeline strong,.fulfillment-timeline span,.fulfillment-timeline small{grid-column:2}.technical-history li{grid-template-columns:1fr}}.carrier-query.admin-density .fulfillment-timeline :is(time,span,small){overflow:visible;white-space:normal;overflow-wrap:anywhere}.carrier-query.admin-density .technical-history small{overflow-wrap:anywhere}
</style>
@@ -1,5 +1,6 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import '@/styles/adminWorkbench.css'
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import AppDialog from '@/shared/components/AppDialog.vue'
@@ -102,24 +103,24 @@ watch(scope, () => { open.value = false; generation++; schedule.value = null; pe
onBeforeUnmount(() => { generation++ })
</script>
<template>
<div class="schedule-actions">
<div class="schedule-actions admin-workbench">
<el-button v-if="canRead" :disabled="!!prerequisite" @click="begin">船期维护与历史</el-button>
<span v-if="prerequisite">{{ prerequisite }}</span>
<span v-else-if="!canRead">当前账号无船期读取权限。</span>
</div>
<AppDialog v-model="open" class="admin-density-dialog" :title="dialogTitle" fullscreenable :width="container.currentPortScheduleId ? '1000px' : '1120px'" :show-confirm="canWrite" :confirm-disabled="loading || !!prerequisite || (!!container.currentPortScheduleId && !schedule)" :confirm-loading="busy" :confirm-text="pending ? '重试原请求' : '保存船期'" @confirm="save">
<AppDialog v-model="open" class="admin-density-dialog admin-workbench is-dialog" :title="dialogTitle" fullscreenable :width="container.currentPortScheduleId ? '1000px' : '1120px'" :show-confirm="canWrite" :confirm-disabled="loading || !!prerequisite || (!!container.currentPortScheduleId && !schedule)" :confirm-loading="busy" :confirm-text="pending ? '重试原请求' : '保存船期'" @confirm="save">
<div v-loading="loading" class="density-dialog-sections">
<el-alert v-if="error" :title="error" type="error" :closable="false" />
<section class="density-dialog-section" data-tone="source">
<table class="density-table schedule-context-table" aria-label="原柜船期归属"><tbody>
<div class="workbench-evidence"><table class="density-table schedule-context-table" aria-label="原柜船期归属"><tbody>
<tr><th>柜次号</th><td>{{ container.cabinetNo || '—' }}</td><th>集装箱号</th><td>{{ container.containerNo || '—' }}</td></tr>
<tr><th>提单号</th><td>{{ container.billOfLadingNo || '—' }}</td><th>客户</th><td>{{ container.customerName || '—' }}</td></tr>
</tbody></table>
</tbody></table></div>
</section>
<section class="density-dialog-section" data-tone="work">
<el-form v-if="canWrite" class="density-form" :disabled="busy || !!pending || loading">
<table class="density-table density-table--editable schedule-editor-table" aria-label="原柜船期填写"><tbody>
<div class="workbench-evidence"><table class="density-table density-table--editable schedule-editor-table" aria-label="原柜船期填写"><tbody>
<tr><td><el-form-item label="船名" label-position="left" label-width="120px" required><el-input v-model="form.vesselName" maxlength="128" placeholder="请输入船名" /></el-form-item></td><td><el-form-item label="航次" label-position="left" label-width="120px" required><el-input v-model="form.voyageNo" maxlength="64" placeholder="请输入航次" /></el-form-item></td></tr>
<tr><td v-for="key in dates" :key="key"><el-form-item :label="dateLabels[key]" label-position="left" label-width="120px" required><el-date-picker v-model="form[key]" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" :placeholder="`请选择${dateLabels[key]}`" /></el-form-item></td></tr>
<template v-if="!container.currentPortScheduleId">
@@ -127,12 +128,12 @@ onBeforeUnmount(() => { generation++ })
<tr v-for="(keys, index) in [extraDates.slice(0, 2), extraDates.slice(2, 4), extraDates.slice(4)]" :key="index"><td v-for="key in keys" :key="key" :colspan="keys.length === 1 ? 2 : 1"><el-form-item :label="dateLabels[key]" label-position="left" label-width="120px" :required="!['bookingCutoffAt', 'customsCutoffAt'].includes(key)"><el-date-picker v-model="form[key]" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" :placeholder="`请选择${dateLabels[key]}`" /></el-form-item></td></tr>
</template>
<tr><td colspan="2"><el-form-item label="修改原因" label-position="left" label-width="120px" required><el-input v-model="form.reason" type="textarea" maxlength="1000" placeholder="请说明本次修改原因" /></el-form-item></td></tr>
</tbody></table>
</tbody></table></div>
</el-form>
<table v-else class="density-table schedule-context-table" aria-label="当前船期只读资料"><tbody>
<div class="workbench-evidence" v-else><table class="density-table schedule-context-table" aria-label="当前船期只读资料"><tbody>
<tr><th>船名</th><td>{{ form.vesselName || '—' }}</td><th>航次</th><td>{{ form.voyageNo || '—' }}</td></tr>
<tr><template v-for="key in dates" :key="key"><th>{{ dateLabels[key] }}</th><td>{{ time(form[key]) }}</td></template></tr>
</tbody></table>
</tbody></table></div>
</section>
<section class="density-dialog-section" data-tone="source">
<h3 class="density-dialog-section__title">修改历史</h3>