This commit is contained in:
2026-08-18 16:09:43 +08:00
parent f9ae2d94cb
commit 21249d1dde
52 changed files with 980 additions and 698 deletions
@@ -27,6 +27,28 @@ describe('PreloadPlanDetailView prototype contract', () => {
expect(source).not.toContain('saveMfNote')
})
it('edits only the selected item warehouse remark with optimistic locking', () => {
expect(source).toContain('@edit-warehouse-remark="openWarehouseRemark"')
expect(source).toContain('updateLoadingItemWarehouseRemark(current.summary.planId, target.itemId')
expect(source).toContain('expectedPlanVersion: current.summary.version')
expect(source).toContain('expectedItemVersion: target.itemVersion')
expect(source).toContain('operationId: remarkOperationId.value')
expect(source).toContain('maxlength="1000"')
expect(source).toContain("warehouseRemark.value.trim() || null")
expect(source).toContain('summary: { ...current.summary, version: result.version }')
expect(source).toContain('updatedItem?.version')
expect(source).not.toContain('plan.remark')
})
it('preserves warehouse remark input and refreshes current data on conflicts', () => {
expect(source).toContain("details.code === 'A0409'")
expect(source).toContain('await loadDetail()')
expect(source).toContain("detail.value?.items.find((item) => item.itemId === target.itemId) || target")
expect(source).toContain('当前输入已保留')
expect(source).toContain('计划状态或明细版本已变化')
expect(source).toContain(':confirm-loading="remarkSaving"')
})
it('is the formal container-number entry and does not imply that preload directly opens PDA', () => {
expect(source).toContain("{ required: true, message: '请输入集装箱号'")
expect(source).toContain("detail.value?.summary.containerNo ? '修改集装箱号 / 航次' : '录入集装箱号'")
@@ -47,7 +69,7 @@ describe('PreloadPlanDetailView prototype contract', () => {
it('keeps backend confirmation errors visible and supports narrow dialogs', () => {
expect(source).toContain("confirmError.value = getApiErrorDetails(cause, '确认预装失败,请稍后重试')")
expect(source).toContain('<OperationErrorToast :details="editError || confirmError" />')
expect(source).toContain('<OperationErrorToast :details="remarkError || editError || confirmError" />')
expect(source).not.toContain(':message="confirmError.message"')
expect(source).toContain('width="min(520px, calc(100vw - 28px))"')
expect(source).toContain("label=\"柜次号\"")
@@ -11,7 +11,7 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import LoadingListDetailDrawer from '../components/LoadingListDetailDrawer.vue'
import { confirmPreloadPlan, getPreloadPlan, updatePreloadTransport, type LoadingDetail, type PreloadSummary } from '../api/platformLoadingLists'
import { confirmPreloadPlan, getPreloadPlan, updateLoadingItemWarehouseRemark, updatePreloadTransport, type LoadingDetail, type LoadingItem, type PreloadSummary } from '../api/platformLoadingLists'
const route = useRoute()
const router = useRouter()
@@ -28,6 +28,12 @@ const confirmError = ref<ApiErrorDetails | null>(null)
const confirmOperationId = ref('')
const editFormRef = ref<FormInstance>()
const editForm = reactive({ containerNo: '', voyageNo: '', operationReason: '' })
const remarkOpen = ref(false)
const remarkSaving = ref(false)
const remarkError = ref<ApiErrorDetails | null>(null)
const remarkTarget = ref<LoadingItem | null>(null)
const warehouseRemark = ref('')
const remarkOperationId = ref('')
const planId = computed(() => String(route.params.planId || ''))
const warehouseId = computed(() => typeof route.query.warehouseId === 'string' ? route.query.warehouseId : '')
const canEdit = computed(() => access.can('platform:firstleg:preload:edit'))
@@ -101,6 +107,56 @@ async function saveTransport() {
}
}
function openWarehouseRemark(item: LoadingItem) {
if (!canEdit.value || !detail.value || !['EDITING', 'LOADING'].includes(detail.value.summary.status)) return
remarkTarget.value = item
warehouseRemark.value = item.warehouseRemark || ''
remarkError.value = null
remarkOperationId.value = `admin-preload-warehouse-remark-${detail.value.summary.planId}-${item.itemId}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
remarkOpen.value = true
}
async function saveWarehouseRemark() {
const target = remarkTarget.value
const current = detail.value
if (!target || !current || !canEdit.value || remarkSaving.value) return
if (warehouseRemark.value.length > 1000) {
remarkError.value = { code: 'WAREHOUSE_REMARK_TOO_LONG', message: '仓库备注不能超过 1000 个字符' }
return
}
remarkSaving.value = true
remarkError.value = null
try {
const result = await updateLoadingItemWarehouseRemark(current.summary.planId, target.itemId, {
warehouseRemark: warehouseRemark.value.trim() || null,
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
? { ...item, itemVersion: updatedItem?.version ?? item.itemVersion, warehouseRemark: updatedItem?.warehouseRemark ?? null }
: item),
}
remarkOpen.value = false
remarkTarget.value = null
remarkOperationId.value = ''
ElMessage.success('仓库备注已更新')
} catch (cause) {
const details = getApiErrorDetails(cause, '仓库备注保存失败,请稍后重试')
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
}
}
function openConfirmPreload() {
const summary = detail.value?.summary
if (!canEdit.value || !summary?.readiness.canConfirm) return
@@ -133,7 +189,7 @@ onMounted(() => void loadDetail())
<template>
<AdminPage>
<div class="preload-detail-page">
<OperationErrorToast :details="editError || confirmError" />
<OperationErrorToast :details="remarkError || editError || confirmError" />
<el-button link type="primary" class="back-button" @click="goBack"><ArrowLeft :size="16" />返回列表</el-button>
<el-alert
v-if="detail"
@@ -146,7 +202,7 @@ onMounted(() => void loadDetail())
<LoadErrorState v-if="error && !detail" title="预装清单详情加载失败" :message="error.message" :loading="loading" @retry="loadDetail">
<template #details><ApiErrorMeta :details="error" /></template>
</LoadErrorState>
<LoadingListDetailDrawer v-else :mode="'preload'" :loading="loading" :detail="detail" :error="error" :can-edit="canEdit" :confirm-saving="confirmSaving" @retry="loadDetail" @edit="openEdit" @confirm-preload="openConfirmPreload" />
<LoadingListDetailDrawer v-else :mode="'preload'" :loading="loading" :detail="detail" :error="error" :can-edit="canEdit" :confirm-saving="confirmSaving" @retry="loadDetail" @edit="openEdit" @edit-warehouse-remark="openWarehouseRemark" @confirm-preload="openConfirmPreload" />
</div>
<AppDialog v-model="editOpen" class="loading-transport-dialog" :title="editDialogTitle" width="min(620px, calc(100vw - 28px))" confirm-text="保存" :confirm-loading="editSaving" :confirm-disabled="!canEdit" @confirm="saveTransport">
@@ -157,6 +213,16 @@ onMounted(() => void loadDetail())
</el-form>
</AppDialog>
<AppDialog v-model="remarkOpen" class="warehouse-remark-dialog" title="编辑仓库备注" width="min(560px, calc(100vw - 28px))" confirm-text="保存备注" cancel-text="取消" :confirm-loading="remarkSaving" :confirm-disabled="!canEdit" @confirm="saveWarehouseRemark">
<el-alert type="info" :closable="false" show-icon title="仓库备注将随装柜完成资料下发海外仓" />
<el-form label-position="top" class="warehouse-remark-form">
<el-form-item label="客户备注(只读)"><el-input :model-value="remarkTarget?.customerRemark || '—'" disabled /><small>来源:客户下单</small></el-form-item>
<el-form-item label="内部备注(只读)"><el-input :model-value="remarkTarget?.internalRemark || '—'" disabled /><small>来源:订单内部备注</small></el-form-item>
<el-form-item label="仓库备注"><el-input v-model="warehouseRemark" type="textarea" :rows="5" maxlength="1000" show-word-limit placeholder="可填写、修改或清空仓库备注" /></el-form-item>
</el-form>
<LoadErrorState v-if="remarkError" compact title="仓库备注保存失败" :message="remarkError.message" :show-retry="false"><template #details><ApiErrorMeta :details="remarkError" /></template></LoadErrorState>
</AppDialog>
<AppDialog v-model="confirmOpen" class="preload-confirm-dialog" title="确认整份预装计划" width="min(520px, calc(100vw - 28px))" confirm-text="确认预装" :confirm-loading="confirmSaving" :confirm-disabled="!detail?.summary.readiness.canConfirm" @confirm="submitConfirmPreload">
<div class="confirm-content">
<el-alert type="warning" :closable="false" show-icon title="本操作将确认整份预装计划" description="系统会原子确认本计划包含的全部入库预装票据;成功后由服务端按真实条件创建唯一 PDA 任务,当前页面不会伪造 PDA 成功。" />
@@ -176,6 +242,8 @@ onMounted(() => void loadDetail())
.transport-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; }
.transport-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }
.confirm-content { display: grid; gap: 16px; }
.warehouse-remark-form { margin-top: 14px; }
.warehouse-remark-form small { display: block; margin-top: 4px; color: var(--cc-muted); }
:global(.loading-transport-dialog.el-dialog) { max-height: 90vh; overflow: hidden; }
@media (min-width: 1920px) { .preload-detail-page { width: 100%; max-width: none; gap: 12px; } }
@media (max-width: 720px) { .transport-form { grid-template-columns: 1fr; } .transport-form :deep(.el-form-item:last-child) { grid-column: auto; } }