2026-08-12 16:31:46 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-09-14 15:33:35 +08:00
|
|
|
|
import '@/styles/adminDensity.css'
|
2026-08-26 15:32:22 +08:00
|
|
|
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
2026-08-12 16:31:46 +08:00
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
|
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
|
|
|
|
|
import AdminPage from '@/shared/components/AdminPage.vue'
|
|
|
|
|
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
2026-08-18 09:09:34 +08:00
|
|
|
|
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
2026-08-12 16:31:46 +08:00
|
|
|
|
import AppDialog from '@/shared/components/AppDialog.vue'
|
|
|
|
|
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
2026-08-31 15:58:38 +08:00
|
|
|
|
import { getApiErrorDetails, http, unwrapApiResponse, type ApiEnvelope, type ApiErrorDetails } from '@/shared/api/http'
|
2026-08-12 16:31:46 +08:00
|
|
|
|
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
2026-08-31 17:05:13 +08:00
|
|
|
|
import { useWorkspaceTabsStore } from '@/core/workspace-tabs/workspaceTabs'
|
2026-08-12 16:31:46 +08:00
|
|
|
|
import LoadingListDetailDrawer from '../components/LoadingListDetailDrawer.vue'
|
2026-09-22 13:51:58 +08:00
|
|
|
|
import { confirmPreloadPlan, getConfirmedLoadingList, getPreloadPlan, listDomesticPdaOperators, updateLoadingItemRemarks, updatePreloadTransport, type DomesticPdaOperator, type LoadingDetail, type LoadingItem, type PreloadSummary } from '../api/platformLoadingLists'
|
2026-08-12 16:31:46 +08:00
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const access = useAccessPreviewStore()
|
2026-08-31 17:05:13 +08:00
|
|
|
|
const workspace = useWorkspaceTabsStore()
|
2026-08-12 16:31:46 +08:00
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const detail = ref<LoadingDetail<PreloadSummary> | null>(null)
|
|
|
|
|
|
const error = ref<ApiErrorDetails | null>(null)
|
|
|
|
|
|
const editOpen = ref(false)
|
|
|
|
|
|
const editSaving = ref(false)
|
|
|
|
|
|
const editError = ref<ApiErrorDetails | null>(null)
|
2026-08-15 17:04:00 +08:00
|
|
|
|
const confirmOpen = ref(false)
|
|
|
|
|
|
const confirmSaving = ref(false)
|
|
|
|
|
|
const confirmError = ref<ApiErrorDetails | null>(null)
|
|
|
|
|
|
const confirmOperationId = ref('')
|
2026-09-22 13:51:58 +08:00
|
|
|
|
const assigneeId = ref('')
|
|
|
|
|
|
const assigneeOptions = ref<DomesticPdaOperator[]>([])
|
|
|
|
|
|
const assigneeLoading = ref(false)
|
|
|
|
|
|
const selectedAssignee = computed(() => assigneeOptions.value.find((operator) => operator.userId === assigneeId.value))
|
2026-08-12 16:31:46 +08:00
|
|
|
|
const editFormRef = ref<FormInstance>()
|
|
|
|
|
|
const editForm = reactive({ containerNo: '', voyageNo: '', operationReason: '' })
|
2026-08-18 16:09:43 +08:00
|
|
|
|
const remarkOpen = ref(false)
|
|
|
|
|
|
const remarkSaving = ref(false)
|
|
|
|
|
|
const remarkError = ref<ApiErrorDetails | null>(null)
|
|
|
|
|
|
const remarkTarget = ref<LoadingItem | null>(null)
|
2026-09-14 16:56:11 +08:00
|
|
|
|
const remarkForm = reactive({ internalRemark: '', warehouseRemark: '', customerServiceRemark: '' })
|
2026-08-18 16:09:43 +08:00
|
|
|
|
const remarkOperationId = ref('')
|
2026-08-26 15:32:22 +08:00
|
|
|
|
let loadSequence = 0
|
2026-09-22 13:51:58 +08:00
|
|
|
|
let assigneeLoadSequence = 0
|
2026-08-12 16:31:46 +08:00
|
|
|
|
const planId = computed(() => String(route.params.planId || ''))
|
|
|
|
|
|
const warehouseId = computed(() => typeof route.query.warehouseId === 'string' ? route.query.warehouseId : '')
|
2026-08-31 15:58:38 +08:00
|
|
|
|
type LoadingPlanLifecycle = { planId: string | number; planStatus: string; detailResource: 'PRELOAD' | 'LOADING_LIST' }
|
|
|
|
|
|
|
|
|
|
|
|
async function resolveLoadingPlanLifecycle() {
|
|
|
|
|
|
const response = await http.get<ApiEnvelope<LoadingPlanLifecycle>, ApiEnvelope<LoadingPlanLifecycle>>(
|
|
|
|
|
|
`/platform/v1/first-leg/loading-plans/${encodeURIComponent(planId.value)}/lifecycle`,
|
|
|
|
|
|
{ params: { warehouseId: warehouseId.value } },
|
|
|
|
|
|
)
|
|
|
|
|
|
return unwrapApiResponse(response)
|
|
|
|
|
|
}
|
2026-08-12 16:31:46 +08:00
|
|
|
|
const canEdit = computed(() => access.can('platform:firstleg:preload:edit'))
|
2026-09-22 13:51:58 +08:00
|
|
|
|
const canPushPda = computed(() => canEdit.value && access.can('platform:firstleg:pda:assign'))
|
2026-08-31 12:12:51 +08:00
|
|
|
|
const canCorrectTransport = computed(() => {
|
|
|
|
|
|
const summary = detail.value?.summary
|
|
|
|
|
|
const pdaStatus = summary?.readiness.pdaTaskStatus
|
2026-09-16 17:49:23 +08:00
|
|
|
|
if (!canEdit.value || !summary || !['EDITING', 'LOADING'].includes(summary.status)) return false
|
|
|
|
|
|
if (!summary.containerNo) return true
|
|
|
|
|
|
return !pdaStatus || pdaStatus === 'PENDING'
|
2026-08-31 12:12:51 +08:00
|
|
|
|
})
|
2026-09-01 15:24:37 +08:00
|
|
|
|
const isContainerCorrection = computed(() => Boolean(detail.value?.summary.containerNo))
|
|
|
|
|
|
const editDialogTitle = computed(() => isContainerCorrection.value ? '纠正集装箱号 / 航次' : '补录集装箱号 / 航次')
|
2026-08-12 16:31:46 +08:00
|
|
|
|
const editRules: FormRules = {
|
2026-08-15 17:04:00 +08:00
|
|
|
|
containerNo: [
|
|
|
|
|
|
{ required: true, message: '请输入集装箱号', trigger: 'blur' },
|
|
|
|
|
|
{ max: 64, message: '集装箱号不能超过 64 个字符', trigger: 'blur' },
|
|
|
|
|
|
],
|
2026-08-12 16:31:46 +08:00
|
|
|
|
voyageNo: [{ max: 64, message: '航次不能超过 64 个字符', trigger: 'blur' }],
|
2026-08-31 12:12:51 +08:00
|
|
|
|
operationReason: [
|
2026-09-01 15:24:37 +08:00
|
|
|
|
{ validator: (_rule, value, callback) => callback(isContainerCorrection.value && !String(value || '').trim() ? new Error('纠错时请输入操作原因') : undefined), trigger: 'blur' },
|
2026-08-31 12:12:51 +08:00
|
|
|
|
{ max: 500, message: '操作原因不能超过 500 个字符', trigger: 'blur' },
|
|
|
|
|
|
],
|
2026-08-12 16:31:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-31 12:12:51 +08:00
|
|
|
|
function normalizedContainerNo(value: string | null | undefined) { return String(value || '').trim().toUpperCase() }
|
|
|
|
|
|
|
2026-08-12 16:31:46 +08:00
|
|
|
|
async function loadDetail() {
|
2026-08-26 15:32:22 +08:00
|
|
|
|
const sequence = ++loadSequence
|
|
|
|
|
|
detail.value = null
|
2026-08-12 16:31:46 +08:00
|
|
|
|
if (!planId.value || !warehouseId.value) {
|
2026-08-26 15:32:22 +08:00
|
|
|
|
loading.value = false
|
2026-08-12 16:31:46 +08:00
|
|
|
|
error.value = { code: 'MISSING_WAREHOUSE_CONTEXT', message: '该清单缺少所属仓库信息,无法安全查询详情,请返回列表重新进入。' }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
error.value = null
|
|
|
|
|
|
try {
|
2026-08-31 15:58:38 +08:00
|
|
|
|
const lifecycle = await resolveLoadingPlanLifecycle()
|
|
|
|
|
|
if (sequence !== loadSequence) return
|
|
|
|
|
|
if (lifecycle.detailResource === 'LOADING_LIST') {
|
|
|
|
|
|
if (lifecycle.planStatus !== 'COMPLETED') {
|
|
|
|
|
|
error.value = { code: 'MODE_STATUS_MISMATCH', message: '装柜计划阶段异常,暂时无法切换详情,请稍后重试。' }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-09-08 19:57:42 +08:00
|
|
|
|
const confirmed = await getConfirmedLoadingList(planId.value, warehouseId.value)
|
|
|
|
|
|
await router.replace({ name: 'domestic-container-detail', params: { containerId: confirmed.summary.containerBookingId }, query: { tab: 'loading-list' } })
|
2026-08-31 17:05:13 +08:00
|
|
|
|
workspace.removeByIds(['preload-plan-detail'])
|
2026-08-31 15:58:38 +08:00
|
|
|
|
if (sequence === loadSequence) ElMessage.success('装柜已完成,已切换至装柜清单详情')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (lifecycle.detailResource !== 'PRELOAD') {
|
|
|
|
|
|
error.value = { code: 'MODE_STATUS_MISMATCH', message: '无法识别当前装柜计划阶段,请稍后重试。' }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-12 16:31:46 +08:00
|
|
|
|
const result = await getPreloadPlan(planId.value, warehouseId.value)
|
2026-08-26 15:32:22 +08:00
|
|
|
|
if (sequence !== loadSequence) return
|
2026-08-12 16:31:46 +08:00
|
|
|
|
if (!['EDITING', 'LOADING'].includes(result.summary.status)) {
|
|
|
|
|
|
detail.value = null
|
|
|
|
|
|
error.value = { code: 'MODE_STATUS_MISMATCH', message: '该计划不属于预装清单状态,请从对应清单入口查看。' }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
detail.value = result
|
2026-09-22 13:51:58 +08:00
|
|
|
|
if (canPushPda.value && !result.summary.readiness.pdaTaskId) void loadAssigneeOptions()
|
2026-08-12 16:31:46 +08:00
|
|
|
|
} catch (cause) {
|
2026-08-26 15:32:22 +08:00
|
|
|
|
if (sequence !== loadSequence) return
|
2026-08-12 16:31:46 +08:00
|
|
|
|
error.value = getApiErrorDetails(cause, '预装清单详情加载失败,请稍后重试')
|
|
|
|
|
|
} finally {
|
2026-08-26 15:32:22 +08:00
|
|
|
|
if (sequence === loadSequence) loading.value = false
|
2026-08-12 16:31:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function goBack() {
|
|
|
|
|
|
if (window.history.length > 1) router.back()
|
|
|
|
|
|
else void router.push({ name: 'loading-plans' })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openEdit() {
|
2026-08-31 12:12:51 +08:00
|
|
|
|
if (!detail.value || !canCorrectTransport.value) return
|
2026-08-12 16:31:46 +08:00
|
|
|
|
editForm.containerNo = detail.value.summary.containerNo || ''
|
|
|
|
|
|
editForm.voyageNo = detail.value.summary.voyageNo || ''
|
|
|
|
|
|
editForm.operationReason = ''
|
|
|
|
|
|
editError.value = null
|
|
|
|
|
|
editOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function saveTransport() {
|
2026-08-31 12:12:51 +08:00
|
|
|
|
if (!detail.value || !canCorrectTransport.value || !(await editFormRef.value?.validate().catch(() => false))) return
|
2026-09-01 15:24:37 +08:00
|
|
|
|
const correcting = isContainerCorrection.value
|
2026-08-31 12:12:51 +08:00
|
|
|
|
editForm.containerNo = normalizedContainerNo(editForm.containerNo)
|
2026-08-12 16:31:46 +08:00
|
|
|
|
editSaving.value = true
|
|
|
|
|
|
editError.value = null
|
|
|
|
|
|
try {
|
|
|
|
|
|
detail.value = await updatePreloadTransport(planId.value, warehouseId.value, {
|
2026-08-31 12:12:51 +08:00
|
|
|
|
containerNo: editForm.containerNo,
|
2026-08-12 16:31:46 +08:00
|
|
|
|
voyageNo: editForm.voyageNo.trim() || null,
|
2026-09-01 15:24:37 +08:00
|
|
|
|
operationReason: editForm.operationReason.trim() || null,
|
2026-08-12 16:31:46 +08:00
|
|
|
|
expectedVersion: detail.value.summary.version,
|
|
|
|
|
|
})
|
|
|
|
|
|
editOpen.value = false
|
2026-09-01 15:24:37 +08:00
|
|
|
|
ElMessage.success(correcting ? '运输信息已纠正' : '真实集装箱号已补录')
|
2026-08-12 16:31:46 +08:00
|
|
|
|
} catch (cause) {
|
|
|
|
|
|
const details = getApiErrorDetails(cause, '运输信息保存失败,请刷新详情后重试')
|
|
|
|
|
|
if (details.code === 'A0409' || /版本|并发|已被其他人修改/.test(details.message)) {
|
|
|
|
|
|
await loadDetail()
|
|
|
|
|
|
editError.value = { ...details, message: '运输信息已被其他人修改,系统已刷新最新版本;当前输入已保留,请核对后重新保存。' }
|
|
|
|
|
|
} else editError.value = details
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
editSaving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 13:51:58 +08:00
|
|
|
|
async function loadAssigneeOptions() {
|
|
|
|
|
|
const sequence = ++assigneeLoadSequence
|
|
|
|
|
|
assigneeLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!warehouseId.value) return
|
|
|
|
|
|
const options = await listDomesticPdaOperators(warehouseId.value)
|
|
|
|
|
|
if (sequence !== assigneeLoadSequence) return
|
|
|
|
|
|
assigneeOptions.value = options
|
|
|
|
|
|
if (assigneeId.value && !assigneeOptions.value.some((operator) => operator.userId === assigneeId.value)) assigneeId.value = ''
|
|
|
|
|
|
confirmError.value = null
|
|
|
|
|
|
} catch (cause) {
|
|
|
|
|
|
if (sequence !== assigneeLoadSequence) return
|
|
|
|
|
|
confirmError.value = getApiErrorDetails(cause, '国内 PDA 作业员加载失败,请重试')
|
|
|
|
|
|
assigneeOptions.value = []
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (sequence === assigneeLoadSequence) assigneeLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-14 16:56:11 +08:00
|
|
|
|
function openRemarks(item: LoadingItem) {
|
2026-08-18 16:09:43 +08:00
|
|
|
|
if (!canEdit.value || !detail.value || !['EDITING', 'LOADING'].includes(detail.value.summary.status)) return
|
|
|
|
|
|
remarkTarget.value = item
|
2026-09-14 16:56:11 +08:00
|
|
|
|
remarkForm.internalRemark = item.internalRemark || ''
|
|
|
|
|
|
remarkForm.warehouseRemark = item.warehouseRemark || ''
|
|
|
|
|
|
remarkForm.customerServiceRemark = item.customerServiceRemark || ''
|
2026-08-18 16:09:43 +08:00
|
|
|
|
remarkError.value = null
|
2026-09-14 16:56:11 +08:00
|
|
|
|
remarkOperationId.value = `admin-preload-remarks-${detail.value.summary.planId}-${item.itemId}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
2026-08-18 16:09:43 +08:00
|
|
|
|
remarkOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-14 16:56:11 +08:00
|
|
|
|
async function saveRemarks() {
|
2026-08-18 16:09:43 +08:00
|
|
|
|
const target = remarkTarget.value
|
|
|
|
|
|
const current = detail.value
|
|
|
|
|
|
if (!target || !current || !canEdit.value || remarkSaving.value) return
|
2026-09-14 16:56:11 +08:00
|
|
|
|
if (Object.values(remarkForm).some((value) => value.length > 1000)) {
|
|
|
|
|
|
remarkError.value = { code: 'REMARK_TOO_LONG', message: '每项备注不能超过 1000 个字符' }
|
2026-08-18 16:09:43 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
remarkSaving.value = true
|
|
|
|
|
|
remarkError.value = null
|
|
|
|
|
|
try {
|
2026-09-14 16:56:11 +08:00
|
|
|
|
const result = await updateLoadingItemRemarks(current.summary.planId, target.itemId, {
|
|
|
|
|
|
internalRemark: remarkForm.internalRemark.trim() || null,
|
|
|
|
|
|
warehouseRemark: remarkForm.warehouseRemark.trim() || null,
|
|
|
|
|
|
customerServiceRemark: remarkForm.customerServiceRemark.trim() || null,
|
2026-08-18 16:09:43 +08:00
|
|
|
|
expectedPlanVersion: current.summary.version,
|
|
|
|
|
|
expectedItemVersion: target.itemVersion,
|
|
|
|
|
|
operationId: remarkOperationId.value,
|
|
|
|
|
|
})
|
|
|
|
|
|
const updatedItem = result.items.find((item) => item.id === target.itemId)
|
|
|
|
|
|
detail.value = {
|
|
|
|
|
|
...current,
|
|
|
|
|
|
summary: { ...current.summary, version: result.version },
|
|
|
|
|
|
items: current.items.map((item) => item.itemId === target.itemId
|
2026-09-14 16:56:11 +08:00
|
|
|
|
? { ...item, itemVersion: updatedItem?.version ?? item.itemVersion,
|
|
|
|
|
|
internalRemark: updatedItem?.internalRemark ?? null,
|
|
|
|
|
|
warehouseRemark: updatedItem?.warehouseRemark ?? null,
|
|
|
|
|
|
customerServiceRemark: updatedItem?.customerServiceRemark ?? null }
|
2026-08-18 16:09:43 +08:00
|
|
|
|
: item),
|
|
|
|
|
|
}
|
|
|
|
|
|
remarkOpen.value = false
|
|
|
|
|
|
remarkTarget.value = null
|
|
|
|
|
|
remarkOperationId.value = ''
|
2026-09-14 16:56:11 +08:00
|
|
|
|
ElMessage.success('备注已更新')
|
2026-08-18 16:09:43 +08:00
|
|
|
|
} catch (cause) {
|
2026-09-14 16:56:11 +08:00
|
|
|
|
const details = getApiErrorDetails(cause, '备注保存失败,请稍后重试')
|
2026-08-18 16:09:43 +08:00
|
|
|
|
if (details.code === 'A0409' || /版本|并发|状态.*变化/.test(details.message)) {
|
|
|
|
|
|
await loadDetail()
|
|
|
|
|
|
remarkTarget.value = detail.value?.items.find((item) => item.itemId === target.itemId) || target
|
|
|
|
|
|
remarkError.value = { ...details, message: '计划状态或明细版本已变化,系统已刷新最新数据;当前输入已保留,请核对后重新保存。' }
|
|
|
|
|
|
} else remarkError.value = details
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
remarkSaving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-15 17:04:00 +08:00
|
|
|
|
function openConfirmPreload() {
|
|
|
|
|
|
const summary = detail.value?.summary
|
2026-09-22 13:51:58 +08:00
|
|
|
|
if (!canPushPda.value || !summary?.readiness.canConfirm || !assigneeId.value) return
|
2026-08-15 17:04:00 +08:00
|
|
|
|
confirmError.value = null
|
|
|
|
|
|
confirmOperationId.value = `admin-preload-confirm-${summary.planId}-${summary.version}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
|
|
|
|
|
confirmOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitConfirmPreload() {
|
|
|
|
|
|
const summary = detail.value?.summary
|
2026-09-22 13:51:58 +08:00
|
|
|
|
if (!summary?.readiness.canConfirm || !canPushPda.value || !assigneeId.value || confirmSaving.value) return
|
2026-08-15 17:04:00 +08:00
|
|
|
|
confirmSaving.value = true
|
|
|
|
|
|
confirmError.value = null
|
|
|
|
|
|
try {
|
2026-09-22 13:51:58 +08:00
|
|
|
|
const result = await confirmPreloadPlan(summary.planId, summary.version, assigneeId.value, confirmOperationId.value)
|
2026-08-15 17:04:00 +08:00
|
|
|
|
await loadDetail()
|
|
|
|
|
|
confirmOpen.value = false
|
|
|
|
|
|
confirmOperationId.value = ''
|
2026-09-22 13:51:58 +08:00
|
|
|
|
ElMessage.success(result.readiness.readyForPda ? '预装已确认,PDA 任务已推送给指定作业员' : '预装已确认')
|
2026-08-15 17:04:00 +08:00
|
|
|
|
} catch (cause) {
|
|
|
|
|
|
confirmError.value = getApiErrorDetails(cause, '确认预装失败,请稍后重试')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
confirmSaving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-12 16:31:46 +08:00
|
|
|
|
onMounted(() => void loadDetail())
|
2026-09-22 13:51:58 +08:00
|
|
|
|
watch([planId, warehouseId], () => {
|
|
|
|
|
|
assigneeLoadSequence++
|
|
|
|
|
|
assigneeId.value = ''
|
|
|
|
|
|
assigneeOptions.value = []
|
|
|
|
|
|
assigneeLoading.value = false
|
|
|
|
|
|
void loadDetail()
|
|
|
|
|
|
})
|
2026-08-12 16:31:46 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<AdminPage>
|
2026-09-14 15:33:35 +08:00
|
|
|
|
<div class="preload-detail-page admin-density">
|
2026-08-18 16:09:43 +08:00
|
|
|
|
<OperationErrorToast :details="remarkError || editError || confirmError" />
|
2026-09-22 13:51:58 +08:00
|
|
|
|
<LoadingListDetailDrawer :mode="'preload'" :loading="loading" :detail="detail" :error="error" :can-edit="canEdit" :can-push-pda="canPushPda" :pda-assignee-selected="Boolean(assigneeId)" :can-edit-transport="canCorrectTransport" :confirm-saving="confirmSaving" @back="goBack" @retry="loadDetail" @edit="openEdit" @edit-remarks="openRemarks" @confirm-preload="openConfirmPreload">
|
|
|
|
|
|
<template #pda-operator>
|
|
|
|
|
|
<div v-if="canPushPda && !detail?.summary.readiness.pdaTaskId" class="pda-operator-picker">
|
|
|
|
|
|
<label for="preload-pda-operator">PDA 操作员 <span>(必选)</span></label>
|
|
|
|
|
|
<el-select id="preload-pda-operator" v-model="assigneeId" filterable placeholder="请选择操作员" :loading="assigneeLoading" :disabled="confirmSaving" aria-label="PDA 操作员">
|
|
|
|
|
|
<el-option v-for="operator in assigneeOptions" :key="operator.userId" :value="operator.userId" :label="operator.displayName" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</LoadingListDetailDrawer>
|
2026-08-12 16:31:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-09-14 15:33:35 +08:00
|
|
|
|
<AppDialog v-model="editOpen" class="loading-transport-dialog admin-density-dialog" :title="editDialogTitle" width="min(620px, calc(100vw - 28px))" confirm-text="保存" :confirm-loading="editSaving" :confirm-disabled="!canEdit" @confirm="saveTransport">
|
2026-08-12 16:31:46 +08:00
|
|
|
|
<el-form ref="editFormRef" class="transport-form" :model="editForm" :rules="editRules" label-position="top">
|
2026-09-01 15:24:37 +08:00
|
|
|
|
<el-form-item label="真实集装箱号" prop="containerNo"><el-input v-model="editForm.containerNo" maxlength="64" placeholder="请输入提柜后取得的真实集装箱号" @blur="editForm.containerNo = normalizedContainerNo(editForm.containerNo)" /></el-form-item>
|
2026-08-12 16:31:46 +08:00
|
|
|
|
<el-form-item label="航次" prop="voyageNo"><el-input v-model="editForm.voyageNo" maxlength="64" placeholder="请输入航次" /></el-form-item>
|
2026-09-01 15:24:37 +08:00
|
|
|
|
<el-form-item :label="isContainerCorrection ? '纠错原因' : '操作原因(可选)'" prop="operationReason"><el-input v-model="editForm.operationReason" type="textarea" :rows="3" maxlength="500" :placeholder="isContainerCorrection ? '必填,将记录在操作历史中' : '可填写补录说明'" /></el-form-item>
|
2026-08-12 16:31:46 +08:00
|
|
|
|
</el-form>
|
|
|
|
|
|
</AppDialog>
|
2026-08-15 17:04:00 +08:00
|
|
|
|
|
2026-09-14 16:56:11 +08:00
|
|
|
|
<AppDialog v-model="remarkOpen" class="remarks-dialog admin-density-dialog" title="编辑备注" width="min(680px, calc(100vw - 28px))" confirm-text="保存备注" cancel-text="取消" :confirm-loading="remarkSaving" :confirm-disabled="!canEdit" @confirm="saveRemarks">
|
|
|
|
|
|
<el-alert type="info" :closable="false" show-icon title="客户备注来自客户下单且不可修改;其余备注可维护" />
|
|
|
|
|
|
<el-form label-position="top" class="remarks-form">
|
2026-08-18 16:09:43 +08:00
|
|
|
|
<el-form-item label="客户备注(只读)"><el-input :model-value="remarkTarget?.customerRemark || '—'" disabled /><small>来源:客户下单</small></el-form-item>
|
2026-09-14 16:56:11 +08:00
|
|
|
|
<el-form-item label="内部备注"><el-input v-model="remarkForm.internalRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空内部备注" /></el-form-item>
|
|
|
|
|
|
<el-form-item label="仓库备注"><el-input v-model="remarkForm.warehouseRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空仓库备注" /></el-form-item>
|
|
|
|
|
|
<el-form-item label="客服备注"><el-input v-model="remarkForm.customerServiceRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空客服备注" /></el-form-item>
|
2026-08-18 16:09:43 +08:00
|
|
|
|
</el-form>
|
2026-09-14 16:56:11 +08:00
|
|
|
|
<LoadErrorState v-if="remarkError" compact title="备注保存失败" :message="remarkError.message" :show-retry="false"><template #details><ApiErrorMeta :details="remarkError" /></template></LoadErrorState>
|
2026-08-18 16:09:43 +08:00
|
|
|
|
</AppDialog>
|
|
|
|
|
|
|
2026-09-22 13:51:58 +08:00
|
|
|
|
<AppDialog v-model="confirmOpen" class="preload-confirm-dialog admin-density-dialog" title="确认预装并推送 PDA" width="min(520px, calc(100vw - 28px))" confirm-text="确认并推送" :confirm-loading="confirmSaving" :confirm-disabled="!detail?.summary.readiness.canConfirm || !assigneeId || assigneeLoading" @confirm="submitConfirmPreload">
|
2026-08-15 17:04:00 +08:00
|
|
|
|
<div class="confirm-content">
|
2026-09-15 10:38:03 +08:00
|
|
|
|
<el-alert type="warning" :closable="false" show-icon title="本操作将确认整份预装计划" />
|
2026-08-15 17:04:00 +08:00
|
|
|
|
<el-descriptions v-if="detail" :column="1" border>
|
|
|
|
|
|
<el-descriptions-item label="预装单号">{{ detail.summary.preloadNo }}</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="柜次号">{{ detail.summary.cabinetNo || '—' }}</el-descriptions-item>
|
2026-09-01 15:24:37 +08:00
|
|
|
|
<el-descriptions-item label="集装箱号">{{ detail.summary.containerNo || '待补录' }}</el-descriptions-item>
|
2026-08-15 17:04:00 +08:00
|
|
|
|
</el-descriptions>
|
2026-09-22 13:51:58 +08:00
|
|
|
|
<el-descriptions :column="1" border><el-descriptions-item label="PDA 操作员">{{ selectedAssignee?.displayName || '未选择' }}</el-descriptions-item></el-descriptions>
|
|
|
|
|
|
<LoadErrorState v-if="confirmError" compact title="推送失败" :message="confirmError.message" :show-retry="false" />
|
2026-08-15 17:04:00 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</AppDialog>
|
2026-08-12 16:31:46 +08:00
|
|
|
|
</AdminPage>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-09-14 17:54:21 +08:00
|
|
|
|
.preload-detail-page { display: grid; min-width: 0; width: 100%; gap: var(--admin-space-5, 12px); }
|
|
|
|
|
|
.transport-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--admin-space-6, 16px); }
|
2026-08-12 16:31:46 +08:00
|
|
|
|
.transport-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }
|
2026-09-14 17:54:21 +08:00
|
|
|
|
.confirm-content { display: grid; gap: var(--admin-space-5, 12px); }
|
2026-09-22 13:51:58 +08:00
|
|
|
|
.pda-operator-picker { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 8px; }
|
|
|
|
|
|
.pda-operator-picker label { flex: 0 0 auto; font-size: var(--admin-font-body, 13px); white-space: nowrap; }
|
|
|
|
|
|
.pda-operator-picker label span { color: var(--el-color-warning-dark-2); font-size: var(--admin-font-caption, 12px); }
|
|
|
|
|
|
.pda-operator-picker :deep(.el-select) { width: 180px; }
|
2026-09-14 16:56:11 +08:00
|
|
|
|
.remarks-form { margin-top: 14px; }
|
2026-09-14 17:54:21 +08:00
|
|
|
|
.remarks-form small { display: block; margin-top: var(--admin-space-1, 4px); color: var(--cc-muted); }
|
2026-08-12 16:31:46 +08:00
|
|
|
|
:global(.loading-transport-dialog.el-dialog) { max-height: 90vh; overflow: hidden; }
|
2026-09-14 17:54:21 +08:00
|
|
|
|
@media (min-width: 1920px) { .preload-detail-page { width: 100%; max-width: none; gap: var(--admin-space-5, 12px); } }
|
2026-08-12 16:31:46 +08:00
|
|
|
|
@media (max-width: 720px) { .transport-form { grid-template-columns: 1fr; } .transport-form :deep(.el-form-item:last-child) { grid-column: auto; } }
|
|
|
|
|
|
</style>
|