2026-10-10 15:33:10 +08:00
< script setup lang = "ts" >
2026-10-11 14:14:49 +08:00
import { inject , onBeforeUnmount , onMounted , reactive , ref } from 'vue'
import { onBeforeRouteLeave , onBeforeRouteUpdate , useRouter } from 'vue-router'
2026-10-10 15:33:10 +08:00
import { ElMessage } from 'element-plus'
import AppDialog from '@/shared/components/AppDialog.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
2026-10-10 18:38:08 +08:00
import { getImpact , handleImpact , impactStatus , impactReason , type ImpactDetail , type ImpactCommand } from './impactApi'
2026-10-10 15:33:10 +08:00
const props = defineProps < { containerId : string ; canWrite : boolean } > ()
const emit = defineEmits < { close : []; saved : [] } > ()
const router = useRouter (), detail = ref < ImpactDetail | null >( null ), loading = ref ( false ), saving = ref ( false ), historyPage = ref ( 1 )
const error = ref < ApiErrorDetails | null >( null ), pending = ref < ImpactCommand | null >( null )
const form = reactive ({ result : '' , nextAction : '' , followupDate : '' })
const actions : Record < string , string > = { OBSERVED : '船期核对' , CLAIM : '认领' , RECORD : '记录处理' , RESOLVE : '完成核对' }
2026-10-11 21:09:30 +08:00
let loadGeneration = 0
2026-10-10 15:33:10 +08:00
async function load () {
2026-10-11 21:09:30 +08:00
const generation = ++ loadGeneration
2026-10-10 15:33:10 +08:00
loading . value = true ; error . value = null
2026-10-11 21:09:30 +08:00
try { const result = await getImpact ( props . containerId , historyPage . value ); if ( generation === loadGeneration ) detail . value = result }
catch ( cause ) { if ( generation === loadGeneration ) error . value = getApiErrorDetails ( cause , '影响详情加载失败' ) }
finally { if ( generation === loadGeneration ) loading . value = false }
2026-10-10 15:33:10 +08:00
}
async function save ( action : ImpactCommand [ 'action' ]) {
2026-10-11 21:09:30 +08:00
if ( ! detail . value || loading . value || saving . value || ! props . canWrite ) return
2026-10-10 15:33:10 +08:00
if ( ! pending . value && action !== 'CLAIM' && ! form . result . trim ()) { ElMessage . warning ( '请填写实际处理结果' ); return }
pending . value ||= { expectedVersion : detail . value . impact . version , forecastToken : detail . value . impact . forecastToken , idempotencyKey : crypto . randomUUID (), action , result : form . result . trim () || null , nextAction : form . nextAction . trim () || null , followupDate : form . followupDate || null }
saving . value = true ; error . value = null
try { await handleImpact ( props . containerId , pending . value ); pending . value = null ; emit ( 'saved' ); historyPage . value = 1 ; await load () }
catch ( cause ) { error . value = getApiErrorDetails ( cause , '处理结果待核实,请重试原请求' ); if ([ '400' , '403' , '404' , '409' , 'A0400' , 'A0403' , 'A0404' , 'A0409' ]. includes ( error . value . code || '' )) pending . value = null }
finally { saving . value = false }
}
2026-10-11 14:14:49 +08:00
function canLeave () { if ( saving . value || pending . value ) { ElMessage . warning ( '请先核实本次处理结果,再关闭或切换' ); return false } return true }
function close () { if ( canLeave ()) emit ( 'close' ) }
function beforeClose ( done : () => void ) { if ( canLeave ()) done () }
function openTrip ( tripNo : string ) { if ( canLeave ()) void router . push ( `/overseas-master-data/trips/ ${ encodeURIComponent ( tripNo ) } ` ) }
const register = inject < (( guard : () => boolean ) => () => void ) | undefined > ( 'appointment-register-leave-guard' , undefined )
const unregister = register ? .( canLeave )
2026-10-11 21:09:30 +08:00
onBeforeUnmount (() => { ++ loadGeneration ; unregister ? .() })
2026-10-11 14:14:49 +08:00
onBeforeRouteLeave ( canLeave )
onBeforeRouteUpdate ( canLeave )
2026-10-10 15:33:10 +08:00
onMounted ( load )
</ script >
< template >
2026-10-11 14:14:49 +08:00
< AppDialog :model-value = "true" title = "船期变化影响核对" width = "min(1200px, calc(100vw - 48px))" :show-confirm = "false" class = "admin-density-dialog" @ update :model-value = "close" :before-close = "beforeClose" >
2026-10-10 15:33:10 +08:00
< LoadErrorState v-if = "error" title="核对未完成" :message="error.message" :show-retry="!pending" @retry="load" />
< template v-if = "detail" >
< p >{{ detail . impact . impactNo }} · {{ impactStatus [ detail . impact . status ] }} · 负责人 : {{ detail . impact . ownerName || '未认领' }}</ p >
2026-10-10 18:38:08 +08:00
< p > 核对原因 : {{ detail . impact . reason === 'FORECAST_MISSING' ? '来源预计到港缺失,待核实' : impactReason [ detail . impact . reason || '' ] || '待核实' }}</ p >
2026-10-10 15:33:10 +08:00
< p > 基准 ETA : {{ detail . impact . baseline ? . estimatedAt || '待核实' }} → 最新 ETA : {{ detail . impact . latest ? . estimatedAt || '待核实' }}</ p >
2026-10-10 18:38:08 +08:00
< p > 来源 : {{ detail . impact . latest ? . providerCode === 'MANUAL' ? '人工维护' : detail . impact . latest ? . providerCode || '待核实' }} · 观察时间 : {{ detail . impact . latest ? . observedAt || '—' }}</ p >
2026-10-10 15:33:10 +08:00
< p > 实际到港 : {{ detail . impact . latest ? . actualAt || '未记录' }} 。 以上为来源原始时间 , 时区未核实 ; 不能据此判定跨时区违约 。 </ p >
< details open >< summary > 直接关联货物 {{ detail . cargo . length }} 票 </ summary >< div class = "impact-list" >< p v-for = "(row, index) in detail.cargo" :key="index" >{{ row.inboundCode || ' 未关联入仓码 ' }} · {{ row.customerName || '—' }} · {{ row.destination || '—' }} · {{ row.packages ?? ' 待核实 ' }} 箱 </ p ></ div ></ details >
< details open >< summary > 需复核的收货预约 {{ detail . appointments . length }} 张 ( 确认时间不自动修改 ) </ summary >< div class = "impact-list" >< p v-for = "row in detail.appointments" :key="row.appointmentNo" >{{ row.appointmentNo }} · {{ row.destination }} · {{ row.confirmedAt || ' 尚未确认时段 ' }} {{ row.timezone || ' 时区待核实 ' }}</ p ></ div ></ details >
< p v-if = "!detail.transport" > 无车次查看权限 , 运输影响未加载 。 </ p >
< template v-else >
2026-10-11 14:14:49 +08:00
< details open >< summary > 关联车次 {{ detail . transport . trips . length }} 辆 </ summary >< div class = "impact-list" >< p v-for = "row in detail.transport.trips" :key="row.tripNo"><el-button link type="primary" @click="openTrip(row.tripNo)" >{{ row.tripNo }} </ el -button > · 计划出发 ( UTC ): {{ row . plannedDepartureAt || '待定' }}</ p ></ div ></ details >
2026-10-10 15:33:10 +08:00
< details >< summary > 同车外接承诺 {{ detail . transport . sameTripBookings . length }} 票 , 仅提示运输安排复核 </ summary >< div class = "impact-list" >< p v-for = "row in detail.transport.sameTripBookings" :key="row.bookingNo" >{{ row.bookingNo }} · {{ row.customerName || '—' }} · {{ row.tripNo }} · {{ row.destinationLabel || '—' }}</ p ></ div ></ details >
</ template >
< el-form v-if = "canWrite && detail.impact.status === 'OPEN'" label-position="top" :disabled="saving || !!pending" >
< el-form-item label = "实际处理结果" >< el-input v-model = "form.result" type="textarea" maxlength="1000" placeholder="记录已联系、核对或改约的实际结果;此处不会自动改约" /></ el-form-item >
2026-10-20 14:52:06 +08:00
< el-form-item label = "下一步" >< el-input v-model = "form.nextAction" maxlength="500" /></ el-form-item >
2026-10-10 15:33:10 +08:00
< el-form-item label = "下次跟进日期" >< el-date-picker v-model = "form.followupDate" value-format="YYYY-MM-DD" /></ el-form-item >
</ el-form >
< div v-if = "canWrite && detail.impact.status === 'OPEN'" >
< el-button v-if = "!pending" :disabled="saving || loading" @click="save('CLAIM')" > 认领跟进 </ el -button >
< el-button v-if = "!pending" :disabled="saving || loading" @click="save('RECORD')" > 记录处理 </ el -button >
< el-button v-if = "!pending" type="primary" :disabled="saving || loading" @click="save('RESOLVE')" > 完成本次核对 </ el -button >
< el-button v-else type = "primary" :loading = "saving" @click ="save(pending.action)" > 重试本次处理 </ el -button >
</ div >
2026-10-11 21:09:30 +08:00
< details >< summary > 观察及处理历史 ( {{ detail . history . total }} 条 ) </ summary >< div class = "impact-list" >< p v-for = "row in detail.history.records" :key="row.version">{{ actions[row.action] || '核对记录' }} · {{ row.operatorName || '—' }} · {{ row.createdAt }} UTC<br>{{ row.result || '记录来源观察' }} · {{ row.nextAction || '—' }} · {{ row.followupDate || '—' }}</p></div><el-pagination v-model:current-page="historyPage" :page-size="20" :total="detail.history.total" layout="prev, pager, next" :disabled="loading || saving || !!pending" @current-change="load" /></ details >
2026-10-10 15:33:10 +08:00
</ template >
</ AppDialog >
</ template >
< style scoped >. impact - list { max - height : 220 px ; overflow - y : auto ; overflow - wrap : anywhere ; }</ style >