2026-10-15 17:11:03 +08:00
< script setup lang = "ts" >
import { computed , onBeforeUnmount , onMounted , ref } from 'vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import { getWorkDetail , type WorkAction , type WorkDetail , type WorkCommand , type WorkCommandResult } from './workApi'
2026-10-16 15:41:56 +08:00
import { actionAllowed , actionRoute , referenceRoute , commandName , flowLabels , typeLabels , workDate , workStatus , workReason , sameAction } from './workPresentation'
2026-10-15 17:11:03 +08:00
import CargoVerificationForm from './CargoVerificationForm.vue'
import WorkCommandForm from './WorkCommandForm.vue'
import WorkAppointmentForm from './WorkAppointmentForm.vue'
import WorkCreateAppointmentForm from './WorkCreateAppointmentForm.vue'
import WorkDispatchForm from './WorkDispatchForm.vue'
import WorkExistingDispatchForm from './WorkExistingDispatchForm.vue'
import WorkReceiptPlans from './WorkReceiptPlans.vue'
import AppointmentBatchForm from './AppointmentBatchForm.vue'
2026-10-16 15:41:56 +08:00
import WorkReceivingPanel from './WorkReceivingPanel.vue'
import WorkBatchContinuation from './WorkBatchContinuation.vue'
import type { FbaAppointmentDto } from '@/modules/overseas-warehouse'
2026-10-15 17:11:03 +08:00
import type { LeaveGuard } from './useWorkDraft'
const props = defineProps < { itemKey : string ; initialAction ?: WorkAction | null ; canLeave : LeaveGuard } > ()
const emit = defineEmits < { open : [ key : string , action ?: WorkAction ]; external : [ path : string ]; saved : [ result ?: WorkCommandResult , command ?: WorkCommand [ 'command' ]]; updated : []; container : [ id : string , label : string ] } > ()
const detail = ref < WorkDetail | null >( null ), loading = ref ( false ), error = ref < ApiErrorDetails | null >( null ), action = ref < WorkAction | null >( null )
2026-10-16 15:41:56 +08:00
const receivingOwners = computed (() => [ detail . value ? . item . owner , ...( detail . value ? . relatedItems . map ( item => item . owner ) || [])]. filter (( person ) : person is { id : string ; name : string } => !! person ? . id ))
2026-10-15 17:11:03 +08:00
const actionNotice = ref ( '' )
2026-10-16 15:41:56 +08:00
const saveCompleted = ref ( false )
2026-10-15 17:11:03 +08:00
const creatingFromCargo = ref ( false )
const batchKey = ref < string | null >( null ), openedAppointmentNo = ref < string | null >( null )
2026-10-16 15:41:56 +08:00
const receivingKey = ref < string | null >( null )
const continuation = ref < { batchKey : string ; customerId ?: string ; labels ?: Array < { customerId : string ; label : string } > } | null > ( null )
2026-10-15 17:11:03 +08:00
let controller : AbortController | null = null , request = 0
async function load () {
const generation = ++ request ; controller ? . abort (); controller = new AbortController (); loading . value = true ; error . value = null
try { const result = await getWorkDetail ( props . itemKey , controller . signal ); if ( generation === request ) detail . value = result }
catch ( cause ) { if ( generation === request ) error . value = getApiErrorDetails ( cause , '工作事项读取失败' ) }
finally { if ( generation === request ) loading . value = false }
}
const fbaNumber = computed (() => {
const target = action . value ? . target
if ( target ? . kind !== 'FBA_APPOINTMENT' || ! target . appointmentId ) return null
return detail . value ? . references . find ( ref => ref . kind === 'FBA_APPOINTMENT' && ref . key === target . batchKey ) ? . businessNo || null
})
const cargoSource = computed (() => {
const target = action . value ? . target
return target ? . kind === 'CARGO_VERIFICATION' ? { ... target . source , destinationKey : target . source . destinationKey || undefined } : null
})
const createSources = computed (() => creatingFromCargo . value && action . value ? . target . kind === 'CARGO_VERIFICATION' ? [ action . value . target . source ] : action . value ? . target . kind === 'FBA_APPOINTMENT' && ! action . value . target . appointmentId && [ 'SAVE_PLAN' , 'REQUEST_BOOKING' , 'RECORD_CONFIRMATION' ]. includes ( action . value . code ) ? action . value . target . sources : null )
const dispatchBatchKeys = computed (() => {
const target = action . value ? . target
if ( target ? . kind !== 'FLEET_TRIP' || target . tripId || action . value ? . code !== 'ARRANGE_TRANSPORT' ) return null
return target . batchKeys . length && target . batchKeys . every ( key => /^ FBA_APPOINTMENT : \ d + $ / . test ( key )) ? target . batchKeys : null
})
const existingDispatches = computed (() => {
const target = action . value ? . target
if ( target ? . kind !== 'FLEET_TRIP' || target . tripId || action . value ? . code !== 'ARRANGE_TRANSPORT' || target . batchKeys . length !== 1 ) return []
const batchKey = target . batchKeys [ 0 ] ! , item = detail . value ? . item
return /^DISPATCH:\d+$/ . test ( batchKey ) && item ? . batchKey === batchKey && item . batchNo ? [{ batchKey , dispatchNo : item . batchNo }] : []
})
const canAct = computed (() => !! detail . value && !! action . value && actionAllowed ( detail . value . item , action . value ) && ! error . value && ! loading . value )
2026-10-16 15:41:56 +08:00
const secondaryActions = computed (() => detail . value ? . item . allowedActions . filter ( candidate => candidate . code !== action . value ? . code && ( action . value || ! detail . value ? . item . nextAction || ! sameAction ( candidate , detail . value . item . nextAction ))) || [])
2026-10-15 17:11:03 +08:00
async function choose ( next : WorkAction ) {
if ( ! detail . value || loading . value || error . value || ! await props . canLeave ()) return
actionNotice . value = ''
if ( ! actionAllowed ( detail . value . item , next )) { actionNotice . value = '操作资格已变化,请刷新事项后查看当前下一步。' ; return }
2026-10-16 15:41:56 +08:00
saveCompleted . value = false
2026-10-15 17:11:03 +08:00
if ( next . targetWorkItemKey && next . targetWorkItemKey !== props . itemKey ) { emit ( 'open' , next . targetWorkItemKey , next ); return }
const path = actionRoute ( next , detail . value . references , detail . value . item )
if ( path ) { emit ( 'external' , path ); return }
2026-10-16 15:41:56 +08:00
action . value = next ; batchKey . value = null ; openedAppointmentNo . value = null ; receivingKey . value = null ; continuation . value = null ; creatingFromCargo . value = false
2026-10-15 17:11:03 +08:00
if ( next . code === 'EDIT_BATCH' && next . target . kind === 'FBA_APPOINTMENT' && next . target . batchKey ) { batchKey . value = next . target . batchKey ; return }
if ( next . target . kind === 'WORK_COMMAND' && ! commandName ( next )) actionNotice . value = '当前操作尚未提供可用入口,请联系负责人核对。'
if ( next . target . kind === 'FBA_APPOINTMENT' && ! fbaNumber . value && ! createSources . value ) actionNotice . value = '尚未关联可操作的预约记录,请先核实批次与预约关系。'
if ( next . target . kind === 'FLEET_TRIP' && ! dispatchBatchKeys . value ? . length && ! existingDispatches . value . length ) actionNotice . value = '尚未关联可进入的车次或可排批次,请核对运输来源。'
}
2026-10-16 15:41:56 +08:00
async function saved ( result ?: WorkCommandResult , command ?: WorkCommand [ 'command' ]) {
action . value = null ; batchKey . value = null ; openedAppointmentNo . value = null ; receivingKey . value = null ; creatingFromCargo . value = false ; actionNotice . value = '' ; saveCompleted . value = true
emit ( 'saved' , result , command )
await load ()
}
async function savedAppointment ( appointment : FbaAppointmentDto ) {
if ( ! appointment . batchKey ) { await saved (); return }
action . value = null ; batchKey . value = null ; openedAppointmentNo . value = null ; receivingKey . value = null ; saveCompleted . value = true
continuation . value = { batchKey : appointment . batchKey , labels : [... new Set ( appointment . items . map ( item => item . customerId ))]. map ( customerId => ({ customerId , label : appointment . items . filter ( item => item . customerId === customerId ). map ( item => item . inboundCode || '—' ). join ( '、' ) })) }
emit ( 'updated' )
}
async function receivingUpdated () { emit ( 'updated' ); await load () }
2026-10-15 17:11:03 +08:00
async function cargoSaved () { emit ( 'updated' ); await load () }
async function createFromCargo () { if ( canAct . value && await props . canLeave ()) creatingFromCargo . value = true }
2026-10-16 15:41:56 +08:00
async function openBatch ( key : string ) { if ( await props . canLeave ()) { batchKey . value = key ; openedAppointmentNo . value = null ; receivingKey . value = null } }
async function openAppointment ( number : string ) { if ( await props . canLeave ()) { openedAppointmentNo . value = number ; batchKey . value = null ; receivingKey . value = null } }
async function openReceiving ( key : string ) { if ( await props . canLeave ()) { receivingKey . value = key ; batchKey . value = null ; openedAppointmentNo . value = null } }
2026-10-15 17:11:03 +08:00
onMounted ( async () => {
await load ()
// A predecessor's action is only a hint: use the newly fetched item's own qualification.
if ( props . initialAction && detail . value ) {
const own = detail . value . item . allowedActions . find ( candidate => candidate . code === props . initialAction ? . code && candidate . target . kind === props . initialAction ? . target . kind )
if ( own ) await choose ( own )
else if ( detail . value . item . nextAction ) await choose ( detail . value . item . nextAction )
}
})
onBeforeUnmount (() => { ++ request ; controller ? . abort () })
</ script >
< template >
< div :aria-busy = "loading" >
< LoadErrorState v-if = "error" title="工作事项读取失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></ template ></ LoadErrorState >
< p v-if = "loading" > 正在读取当前事项与操作资格 … </ p >
< template v-if = "detail" >
< h3 >{{ detail . item . title }}</ h3 >< p >{{ typeLabels [ detail . item . type ] }} · {{ flowLabels [ detail . item . flow ] }} · {{ workStatus ( detail . item ) }}</ p >
< p >{{ workDate ( detail . item ) }} · {{ detail . item . timezone || '地点时区待核实' }}</ p >< p >{{ detail . item . destinationName || detail . item . originName || '地点待核实' }} · {{ detail . item . owner ? . name || '待分派' }}</ p >
2026-10-16 15:41:56 +08:00
< p class = "followup-summary" > 下次跟进 : {{ detail . item . followupDate || '尚未安排' }}< span v-if = "detail.item.dateBasis === 'NONE'" > · 作业日期未定 , 跟进日不作为作业日期 </ span ></ p >
< p v-if = "detail.item.executionStatus !== 'COMPLETED' && (!detail.item.owner || !detail.item.followupDate)" class="work-warning" >{{ ! detail.item.owner ? ' 负责人待分派 。' : '' }}{{ ! detail.item.followupDate ? ' 请补充下一次跟进安排 。' : '' }} 请记录跟进 ; 若无操作权限 , 请联系负责人 。 </ p >
2026-10-15 17:11:03 +08:00
< div class = "container-links" >< button v-for = "container in detail.item.containers" :key="container.containerBookingId" type="button" @click="emit('container', container.containerBookingId, container.containerNo || container.bookingNo || '本柜')" >{{ container.containerNo || container.bookingNo || '—' }} · 本柜待办 </ button ></ div >
< p v-for = "reason in [...detail.item.blockReasons, ...detail.item.missingFields, ...detail.item.overdueReasons]" :key="reason" class="work-warning" >{{ workReason ( reason ) }}</ p >
< p v-if = "detail.returnHandoff" > 还空责任 : {{ detail.returnHandoff.from.name }} → {{ detail.returnHandoff.to.name }} · {{ detail.returnHandoff.status === ' PENDING_ACCEPTANCE ' ? ' 接手方待确认 , 持续跟进 ' : ' 已确认接手 ' }}</ p >
2026-10-16 15:41:56 +08:00
< template v-if = "!action && !continuation" >
< p v-if = "saveCompleted" role="status" class="save-result" > 本次保存已完成 。 {{ loading ? ' 正在读取当前下一步 …' : error ? ' 下一步读取失败 , 请重试读取事项 , 无需重复提交已保存内容 。' : detail.item.executionStatus === ' COMPLETED ' ? ' 当前事项已完成 , 可返回查看其他待办 。' : ' 请按当前下一步继续处理 。' }}</ p >
< el-button v-if = "detail.item.nextAction" class="primary-next-action" type="primary" :disabled="loading || !!error" @click="choose(detail.item.nextAction)" >{{ detail.item.nextAction.label }} </ el -button >
2026-10-15 17:11:03 +08:00
< p v-else-if = "detail.item.executionStatus !== 'COMPLETED'" > 当前没有可执行的下一步 , 请根据缺项或前置说明联系负责人 。 </ p >
</ template >
< p v-if = "actionNotice" role="status" class="work-warning" >{{ actionNotice }}</ p >
2026-10-16 15:41:56 +08:00
< WorkBatchContinuation v-if = "continuation" :key="JSON.stringify(continuation)" v-bind="continuation" @open="key => emit('open', key)" />
< WorkReceivingPanel v-else-if = "receivingKey" :key="receivingKey" :batch-key="receivingKey" :owners="receivingOwners" :can-write="!loading && !error" @updated="receivingUpdated" @appointment="openAppointment" @batch="openBatch" @external="path => emit('external', path)" />
< AppointmentBatchForm v-else-if = "batchKey" :key="batchKey" :batch-key="batchKey" @updated="emit('updated')" @appointment="openAppointment" />
< WorkAppointmentForm v-else-if = "openedAppointmentNo" :key="openedAppointmentNo" :appointment-no="openedAppointmentNo" :cancel="false" :can-write="true" @saved="savedAppointment" @batch="openBatch" @receiving="openReceiving" />
2026-10-15 17:11:03 +08:00
< WorkCommandForm v-else-if = "action && commandName(action)" :key="`${itemKey}:${action.code}`" :detail="detail" :action="action" :refreshing="loading || !!error" @refresh="load" @saved="saved" />
< template v-else-if = "cargoSource && !creatingFromCargo"><CargoVerificationForm :key="`${cargoSource.customerId}:${cargoSource.sourceKey}:${cargoSource.destinationKey}`" :source="cargoSource" @saved="cargoSaved" /><el-button :disabled="!canAct" @click="createFromCargo" > 核对货物并新建原预约 </ el -button ></ template >
2026-10-16 15:41:56 +08:00
< WorkAppointmentForm v-else-if = "fbaNumber && action" :key="`${fbaNumber}:${action.code}`" :appointment-no="fbaNumber" :cancel="action.code === 'CANCEL_BOOKING'" :can-write="canAct" @saved="savedAppointment" @batch="openBatch" @receiving="openReceiving" />
2026-10-15 17:11:03 +08:00
< WorkCreateAppointmentForm v-else-if = "createSources" :key="JSON.stringify(createSources)" :sources="createSources" :can-write="canAct" @saved="saved()" />
2026-10-16 15:41:56 +08:00
< WorkDispatchForm v-else-if = "dispatchBatchKeys?.length" :key="JSON.stringify(dispatchBatchKeys)" :batch-keys="dispatchBatchKeys" :can-write="canAct" @updated="emit('updated')" @external="path => emit('external', path)" @batch="openBatch" />
2026-10-15 17:11:03 +08:00
< WorkExistingDispatchForm v-else-if = "existingDispatches.length" :key="JSON.stringify(existingDispatches)" :dispatches="existingDispatches" :can-write="canAct" @updated="emit('updated')" @external="path => emit('external', path)" />
< WorkReceiptPlans v-else-if = "action?.target.kind === 'WAREHOUSE_RECEIPT'" :key="action.target.containerBookingId" :container-booking-id="action.target.containerBookingId" :can-write="canAct" @updated="emit('updated')" @external="path => emit('external', path)" />
< section v-else-if = "action?.target.kind === 'READ_ONLY'" aria-label="原业务进度"><h4>原业务进度</h4><p v-for="reference in action.target.references" :key="`${reference.kind}:${reference.key}`"><el-button v-if="referenceRoute(reference)" :disabled="!canAct" @click="emit('external', referenceRoute(reference)!)" >{{ reference.label }} · {{ reference.businessNo }} </ el -button >< span v-else >{{ reference.label }} · {{ reference.businessNo || '—' }}</ span ></ p ></ section >
2026-10-16 15:41:56 +08:00
< details v-if = "secondaryActions.length"><summary>更多操作</summary><div class="more-actions"><el-button v-for="candidate in secondaryActions" :key="`${candidate.code}:${candidate.targetWorkItemKey}`" :disabled="loading || !!error" @click="choose(candidate)" >{{ candidate.label }} </ el -button ></ div ></ details >
2026-10-15 17:11:03 +08:00
< details >< summary > 关联工作 · {{ detail . relatedItems . length }} 项 </ summary >< div v-for = "related in detail.relatedItems" :key="related.workItemKey" class="related-row"><div>{{ related.title }}<p>{{ workStatus(related) }} · {{ workDate(related) }}</p></div><el-button @click="emit('open', related.workItemKey)" > 查看 </ el -button ></ div ></ details >
< details >< summary > 原业务记录 · {{ detail . references . length }} 项 </ summary >< p v-for = "reference in detail.references" :key="`${reference.kind}:${reference.key}`" >{{ reference.label }} · {{ reference.businessNo || '—' }}</ p ></ details >
< details : open = "action?.code === 'VIEW_HISTORY'" >< summary > 操作记录 </ summary >< div v-for = "(entry, index) in detail.history" :key="index" class="history-entry"><b>{{ entry.action }}</b><p>{{ entry.summary }}</p><small>{{ entry.operatorName || '—' }} · {{ entry.occurredAt }}</small></div><p v-if="!detail.history.length" > 暂无记录 </ p ></ details >
</ template >
</ div >
</ template >
< style scoped >
. container - links , . more - actions { display : flex ; flex - wrap : wrap ; gap : var ( -- admin - space - 3 ); }. container - links button { background : transparent ; border : 0 ; color : var ( -- el - color - primary ); cursor : pointer ; font : inherit ; }
. related - row { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 3 ); }. related - row , . history - entry { border - bottom : 1 px solid var ( -- cc - border ); padding - block : var ( -- admin - space - 3 ); } details { margin - top : var ( -- admin - space - 5 ); } summary { cursor : pointer ; padding - block : var ( -- admin - space - 3 ); }. work - warning { color : var ( -- el - color - warning - dark - 2 ); }
</ style >