fix: harden domestic container forms
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/domestic-containers/pages/DomesticContainerInventoryDetailView.vue'), 'utf8')
|
||||
|
||||
describe('DomesticContainerInventoryDetailView', () => {
|
||||
it('does not invent an action column for immutable transaction history', () => {
|
||||
expect(source).not.toContain("label: '操作'")
|
||||
expect(source).toContain('进销存交易记录')
|
||||
})
|
||||
it('surfaces loading and API diagnostic errors and supports retry', () => {
|
||||
expect(source).toContain('整柜库存详情加载失败')
|
||||
expect(source).toContain('@retry="load"')
|
||||
expect(source).toContain('<ApiErrorMeta :details="error" />')
|
||||
expect(source).toContain('v-loading="loading"')
|
||||
})
|
||||
it('uses a four-column desktop field grid and preserves long business text', () => {
|
||||
expect(source).toContain('grid-template-columns: repeat(4, minmax(0, 1fr))')
|
||||
expect(source).toContain('overflow-wrap: anywhere')
|
||||
})
|
||||
})
|
||||
@@ -12,4 +12,7 @@ describe('DomesticContainersView', () => {
|
||||
it('keeps desktop filters and actions on one row with explicit field widths', () => { expect(source).toContain("key: 'warehouseId', type: 'select', width: 210"); expect(source).toContain("key: 'orderNo', type: 'input', width: 220"); expect(source).toContain("key: 'containerNo', type: 'input', width: 220"); expect(source).toContain("key: 'cargoName', type: 'input', width: 220"); expect(source).toContain("key: 'status', type: 'select', width: 180") })
|
||||
it('uses the shared query toolbar flow without a page-level layout override', () => { expect(source).not.toContain('.query-toolbar__filters') })
|
||||
it('matches the prototype four-column inventory summary', () => { expect(source).toContain("import './domesticContainersSummary.css'"); expect(summaryStyles).toContain('grid-template-columns: repeat(4, minmax(0, 1fr))'); expect(summaryStyles).toContain('border: 1px solid #e7edf5'); expect(summaryStyles).toContain('font-size: 20px'); expect(summaryStyles).toContain('.inventory-summary + .data-table > .data-table__toolbar') })
|
||||
it('does not offer an inventory detail action before an inbound has formed inventory', () => { expect(source).toContain("canViewInventory(row as ContainerOrder | InventoryRecord)"); expect(source).toContain("row.status === 'COMPLETED'") })
|
||||
it('matches backend numeric and container-number input boundaries before submit', () => { expect(source).toContain("pattern: /^[A-Za-z0-9-]{4,32}$/"); expect(source).toContain(':max="999999999"'); expect(source).toContain(':max="999999999999.9999"') })
|
||||
it('requires a refreshed inventory version for completed outbound submission', () => { expect(source).toContain("outboundForm.inventoryVersion == null"); expect(source).toContain('库存版本未就绪,请重新选择集装箱') })
|
||||
})
|
||||
|
||||
@@ -45,6 +45,7 @@ const columns = computed<DataTableColumn[]>(() => activeTab.value === 'inventory
|
||||
])
|
||||
function displayTime(value: string) { return value ? value.replace('T', ' ').slice(0, 16) : '—' }
|
||||
function statusLabel(row: ContainerOrder | InventoryRecord) { if ('inventoryStatus' in row) return ({ IN_STOCK: '在库', PARTIALLY_OUTBOUND: '部分出库', OUTBOUND: '已出库' } as const)[row.inventoryStatus]; return row.status === 'PENDING' ? (activeTab.value === 'inbound' ? '待入库' : '待出库') : (activeTab.value === 'inbound' ? '已入库' : '已出库') }
|
||||
function canViewInventory(row: ContainerOrder | InventoryRecord) { return 'inventoryStatus' in row || activeTab.value === 'outbound' || row.status === 'COMPLETED' }
|
||||
function queryParams() { const model = appliedFilters.value; return { page: page.value, size: pageSize.value, warehouseId: String(model.warehouseId || '') || undefined, orderNo: activeTab.value === 'inventory' ? undefined : String(model.orderNo || '') || undefined, containerNo: String(model.containerNo || '') || undefined, cargoName: String(model.cargoName || '') || undefined, status: String(model.status || '') || undefined } }
|
||||
async function load() { loading.value = true; loadError.value = null; try { const result = activeTab.value === 'inbound' ? await listDomesticContainerInbounds(queryParams()) : activeTab.value === 'outbound' ? await listDomesticContainerOutbounds(queryParams()) : await listDomesticContainerInventories(queryParams()); rows.value = result.records; total.value = result.total; if ('summary' in result) summary.value = result.summary } catch (cause) { loadError.value = getApiErrorDetails(cause, '整柜业务加载失败') } finally { loading.value = false } }
|
||||
async function loadWarehouses() { warehouseLoading.value = true; warehouseError.value = null; try { warehouses.value = (await listAllPlatformWarehouses({ warehouseType: 'DOMESTIC', status: 'ACTIVE' })).filter((item) => item.warehouseType === 'DOMESTIC' && item.status === 'ACTIVE') } catch (cause) { warehouseError.value = getApiErrorDetails(cause, '国内仓候选加载失败') } finally { warehouseLoading.value = false } }
|
||||
@@ -59,20 +60,20 @@ const inboundForm = reactive({ warehouseId: '', cargoName: '', containerNo: '',
|
||||
const outboundForm = reactive({ warehouseId: '', inventoryId: '', containerNo: '', cargoName: '', availableBoxes: 0, inventoryVersion: undefined as number | undefined, boxCount: 1, operationTime: '', status: 'COMPLETED' as ContainerOrderStatus, remark: '' })
|
||||
const inventoryCandidates = ref<InventoryRecord[]>([]); const candidateLoading = ref(false); const candidateError = ref<ApiErrorDetails | null>(null)
|
||||
const currentForm = computed(() => dialogType.value === 'inbound' ? inboundForm : outboundForm)
|
||||
const rules: FormRules = { warehouseId: [{ required: true, message: '请选择国内仓', trigger: 'change' }], cargoName: [{ required: true, message: '请输入货物名称', trigger: 'blur' }], containerNo: [{ required: true, message: '请选择或输入集装箱号', trigger: 'change' }], boxCount: [{ required: true, type: 'integer', min: 1, message: '箱数必须为正整数', trigger: 'change' }], volumeCbm: [{ required: true, type: 'number', min: 0.0001, message: '体积必须大于等于 0.0001', trigger: 'change' }], operationTime: [{ required: true, message: '请选择操作时间', trigger: 'change' }], status: [{ required: true, message: '请选择状态', trigger: 'change' }] }
|
||||
const rules: FormRules = { warehouseId: [{ required: true, message: '请选择国内仓', trigger: 'change' }], cargoName: [{ required: true, message: '请输入货物名称', trigger: 'blur' }], containerNo: [{ required: true, message: '请选择或输入集装箱号', trigger: 'change' }, { pattern: /^[A-Za-z0-9-]{4,32}$/, message: '集装箱号须为 4–32 位字母、数字或连字符', trigger: 'blur' }], boxCount: [{ required: true, type: 'integer', min: 1, max: 999999999, message: '箱数须为 1–999999999 的整数', trigger: 'change' }], volumeCbm: [{ required: true, type: 'number', min: 0.0001, max: 999999999999.9999, message: '体积须在 0.0001–999999999999.9999 m³ 之间', trigger: 'change' }], operationTime: [{ required: true, message: '请选择操作时间', trigger: 'change' }], status: [{ required: true, message: '请选择状态', trigger: 'change' }] }
|
||||
function localDateTime() { const offset = new Date().getTimezoneOffset() * 60_000; return `${new Date(Date.now() - offset).toISOString().slice(0, 16)}:00` }
|
||||
function newIdempotencyKey(type: string) { return `${type}-${globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`}` }
|
||||
function openInbound() { dialogType.value = 'inbound'; Object.assign(inboundForm, { warehouseId: '', cargoName: '', containerNo: '', boxCount: 1, volumeCbm: 0.01, operationTime: localDateTime(), status: 'COMPLETED', remark: '' }); idempotencyKey.value = newIdempotencyKey('inbound'); submitError.value = null; dialogOpen.value = true }
|
||||
function openOutbound() { dialogType.value = 'outbound'; Object.assign(outboundForm, { warehouseId: '', inventoryId: '', containerNo: '', cargoName: '', availableBoxes: 0, inventoryVersion: undefined, boxCount: 1, operationTime: localDateTime(), status: 'COMPLETED', remark: '' }); inventoryCandidates.value = []; candidateError.value = null; idempotencyKey.value = newIdempotencyKey('outbound'); submitError.value = null; dialogOpen.value = true }
|
||||
async function loadCandidates() { inventoryCandidates.value = []; Object.assign(outboundForm, { inventoryId: '', containerNo: '', cargoName: '', availableBoxes: 0, inventoryVersion: undefined }); if (!outboundForm.warehouseId) return; candidateLoading.value = true; candidateError.value = null; try { const result = await listDomesticContainerInventories({ page: 1, size: 200, warehouseId: outboundForm.warehouseId }); inventoryCandidates.value = result.records.filter((item) => item.availableBoxes > 0) } catch (cause) { candidateError.value = getApiErrorDetails(cause, '可出库整柜库存加载失败') } finally { candidateLoading.value = false } }
|
||||
async function selectInventory(id: string) { const selected = inventoryCandidates.value.find((item) => String(item.id) === id); if (!selected) return; Object.assign(outboundForm, { inventoryId: id, containerNo: selected.containerNo, cargoName: selected.cargoName, availableBoxes: selected.availableBoxes, inventoryVersion: selected.version }); try { const detail = await getDomesticContainerInventory(id); Object.assign(outboundForm, { availableBoxes: detail.inventory.availableBoxes, inventoryVersion: detail.inventory.version }) } catch (cause) { candidateError.value = getApiErrorDetails(cause, '库存最新版本加载失败') } }
|
||||
async function submit() { if (!await formRef.value?.validate().catch(() => false)) return; if (dialogType.value === 'outbound' && outboundForm.status === 'COMPLETED' && outboundForm.boxCount > outboundForm.availableBoxes) { submitError.value = { message: `库存不足,当前可用 ${outboundForm.availableBoxes} 箱`, code: 'A0409' }; return } saving.value = true; submitError.value = null; try { const result = dialogType.value === 'inbound' ? await createDomesticContainerInbound({ warehouseId: inboundForm.warehouseId, cargoName: inboundForm.cargoName.trim(), containerNo: inboundForm.containerNo.trim().toUpperCase(), boxCount: inboundForm.boxCount, volumeCbm: inboundForm.volumeCbm, inboundTime: inboundForm.operationTime, status: inboundForm.status, remark: inboundForm.remark.trim() || undefined, idempotencyKey: idempotencyKey.value }) : await createDomesticContainerOutbound({ warehouseId: outboundForm.warehouseId, containerNo: outboundForm.containerNo, boxCount: outboundForm.boxCount, outboundTime: outboundForm.operationTime, status: outboundForm.status, remark: outboundForm.remark.trim() || undefined, idempotencyKey: idempotencyKey.value, inventoryVersion: outboundForm.status === 'COMPLETED' ? outboundForm.inventoryVersion : undefined }); ElMessage.success(result.replayed ? `请求已处理,单号 ${result.orderNo}` : `创建成功,单号 ${result.orderNo}`); dialogOpen.value = false; await load() } catch (cause) { submitError.value = getApiErrorDetails(cause, '整柜单据创建失败'); if (dialogType.value === 'outbound' && submitError.value.code === 'A0409') await loadCandidates() } finally { saving.value = false } }
|
||||
async function submit() { if (!await formRef.value?.validate().catch(() => false)) return; if (dialogType.value === 'outbound' && outboundForm.status === 'COMPLETED' && outboundForm.inventoryVersion == null) { submitError.value = { message: '库存版本未就绪,请重新选择集装箱', code: 'A0422' }; return } if (dialogType.value === 'outbound' && outboundForm.status === 'COMPLETED' && outboundForm.boxCount > outboundForm.availableBoxes) { submitError.value = { message: `库存不足,当前可用 ${outboundForm.availableBoxes} 箱`, code: 'A0409' }; return } saving.value = true; submitError.value = null; try { const result = dialogType.value === 'inbound' ? await createDomesticContainerInbound({ warehouseId: inboundForm.warehouseId, cargoName: inboundForm.cargoName.trim(), containerNo: inboundForm.containerNo.trim().toUpperCase(), boxCount: inboundForm.boxCount, volumeCbm: inboundForm.volumeCbm, inboundTime: inboundForm.operationTime, status: inboundForm.status, remark: inboundForm.remark.trim() || undefined, idempotencyKey: idempotencyKey.value }) : await createDomesticContainerOutbound({ warehouseId: outboundForm.warehouseId, containerNo: outboundForm.containerNo, boxCount: outboundForm.boxCount, outboundTime: outboundForm.operationTime, status: outboundForm.status, remark: outboundForm.remark.trim() || undefined, idempotencyKey: idempotencyKey.value, inventoryVersion: outboundForm.status === 'COMPLETED' ? outboundForm.inventoryVersion : undefined }); ElMessage.success(result.replayed ? `请求已处理,单号 ${result.orderNo}` : `创建成功,单号 ${result.orderNo}`); dialogOpen.value = false; await load() } catch (cause) { submitError.value = getApiErrorDetails(cause, '整柜单据创建失败'); if (dialogType.value === 'outbound' && submitError.value.code === 'A0409') await loadCandidates() } finally { saving.value = false } }
|
||||
watch([page, pageSize], load); onMounted(async () => { await Promise.all([loadWarehouses(), load()]) })
|
||||
</script>
|
||||
|
||||
<template><AdminPage as="div" class="domestic-containers-page">
|
||||
<LoadErrorState v-if="warehouseError" compact title="国内仓候选加载失败" :message="warehouseError.message" :loading="warehouseLoading" @retry="loadWarehouses"><template #details><ApiErrorMeta :details="warehouseError" /></template></LoadErrorState>
|
||||
<ListPageLayout :aria-label="`${activeTab === 'inbound' ? '整柜入库单' : activeTab === 'outbound' ? '整柜出库单' : '整柜库存'}列表`"><template #toolbar><div class="business-query-panel"><div class="status-tabs" role="tablist" aria-label="整柜业务类型"><button v-for="(tab, index) in businessTabs" :key="tab.value" type="button" role="tab" :aria-selected="activeTab === tab.value" aria-controls="domestic-container-results" :tabindex="activeTab === tab.value ? 0 : -1" :class="['status-tab', { 'is-active': activeTab === tab.value }]" @click="switchTab(tab.value)" @keydown="handleTabKeydown($event, index)">{{ tab.label }}</button></div><QueryToolbar v-model="filters" class="business-query-toolbar" ariaLabel="整柜业务筛选" :fields="queryFields" :preference-key="`domestic-containers-${activeTab}-query`" @search="search" /></div></template><template v-if="loadError" #state><LoadErrorState title="整柜业务加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template><section v-if="!loadError" id="domestic-container-results" class="surface-panel table-panel"><div v-if="activeTab === 'inventory'" class="inventory-summary"><div><span>入库箱数</span><strong>{{ summary.inboundBoxes }}</strong></div><div><span>已出库箱数</span><strong>{{ summary.outboundBoxes }}</strong></div><div><span>当前库存</span><strong>{{ summary.availableBoxes }}</strong></div><div><span>累计入库体积</span><strong>{{ summary.inboundVolumeCbm.toFixed(2) }} m³</strong></div></div><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" stripe row-key="id" :preference-key="`domestic-containers-${activeTab}-table`" :empty-text="activeTab === 'inbound' ? '暂无整柜入库单' : activeTab === 'outbound' ? '暂无整柜出库单' : '暂无整柜库存'"><template #toolbar-actions><el-button v-if="activeTab === 'inbound' && access.can('platform:domestic-container:inbound:create')" type="primary" @click="openInbound"><Plus :size="16" />新建入库单</el-button><el-button v-if="activeTab === 'outbound' && access.can('platform:domestic-container:outbound:create')" type="primary" @click="openOutbound"><Plus :size="16" />新建出库单</el-button></template><template #actions="{ row }"><el-button size="small" type="primary" plain @click="viewInventory(row as ContainerOrder | InventoryRecord)"><Eye :size="15" />查看</el-button></template><template #status="{ row }"><StatusBadge :status="statusLabel(row as ContainerOrder | InventoryRecord)" /></template></DataTable></section></ListPageLayout>
|
||||
<AppDialog v-model="dialogOpen" class="domestic-container-create-dialog" :title="dialogType === 'inbound' ? '新建整柜入库单' : '新建整柜出库单'" width="min(720px, calc(100vw - 28px))" :confirm-loading="saving" :close-on-click-modal="false" :confirm-text="dialogType === 'inbound' ? '创建入库单' : '创建出库单'" @confirm="submit"><LoadErrorState v-if="submitError" compact title="创建失败" :message="submitError.message" :show-retry="false"><template #details><ApiErrorMeta :details="submitError" /></template></LoadErrorState><el-form ref="formRef" :model="currentForm" :rules="rules" label-position="top"><el-row :gutter="16"><template v-if="dialogType === 'inbound'"><el-col :xs="24" :sm="12"><el-form-item label="国内仓" prop="warehouseId"><el-select v-model="inboundForm.warehouseId" :loading="warehouseLoading" :disabled="Boolean(warehouseError)" placeholder="请选择启用的国内仓"><el-option v-for="item in warehouseOptions" :key="String(item.value)" :label="item.label" :value="String(item.value)" /></el-select></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="货物名称" prop="cargoName"><el-input v-model="inboundForm.cargoName" maxlength="128" placeholder="请输入货物名称" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="集装箱号" prop="containerNo"><el-input v-model="inboundForm.containerNo" maxlength="32" placeholder="如 MSCU1234567" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="箱子数量" prop="boxCount"><el-input-number v-model="inboundForm.boxCount" :min="1" :precision="0" controls-position="right" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="体积(m³)" prop="volumeCbm"><el-input-number v-model="inboundForm.volumeCbm" :min="0.0001" :precision="4" controls-position="right" /></el-form-item></el-col></template><template v-else><el-col :xs="24" :sm="12"><el-form-item label="国内仓" prop="warehouseId"><el-select v-model="outboundForm.warehouseId" :loading="warehouseLoading" :disabled="Boolean(warehouseError)" placeholder="请选择启用的国内仓" @change="loadCandidates"><el-option v-for="item in warehouseOptions" :key="String(item.value)" :label="item.label" :value="String(item.value)" /></el-select></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="绑定集装箱号" prop="containerNo"><el-select v-model="outboundForm.inventoryId" :loading="candidateLoading" :disabled="!outboundForm.warehouseId || Boolean(candidateError)" placeholder="请选择已有集装箱号" @change="selectInventory"><el-option v-for="item in inventoryCandidates" :key="String(item.id)" :label="`${item.containerNo} · 可用 ${item.availableBoxes} 箱`" :value="String(item.id)" /></el-select><input v-model="outboundForm.containerNo" class="sr-only" tabindex="-1"></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="货物名称"><el-input v-model="outboundForm.cargoName" readonly /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="出库箱数" prop="boxCount"><el-input-number v-model="outboundForm.boxCount" :min="1" :precision="0" controls-position="right" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="可用库存"><el-input :model-value="`${outboundForm.availableBoxes} 箱`" readonly /></el-form-item></el-col><el-col v-if="candidateError" :xs="24"><LoadErrorState compact title="可出库库存加载失败" :message="candidateError.message" @retry="loadCandidates"><template #details><ApiErrorMeta :details="candidateError" /></template></LoadErrorState></el-col></template><el-col :xs="24" :sm="12"><el-form-item :label="dialogType === 'inbound' ? '入库时间' : '出库时间'" prop="operationTime"><el-date-picker v-model="currentForm.operationTime" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" format="YYYY-MM-DD HH:mm" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="状态" prop="status"><el-select v-model="currentForm.status"><el-option :label="dialogType === 'inbound' ? '已入库' : '已出库'" value="COMPLETED" /><el-option :label="dialogType === 'inbound' ? '待入库' : '待出库'" value="PENDING" /></el-select></el-form-item></el-col><el-col :xs="24"><el-form-item label="备注"><el-input v-model="currentForm.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入备注" /></el-form-item></el-col></el-row></el-form></AppDialog></AdminPage></template>
|
||||
<ListPageLayout :aria-label="`${activeTab === 'inbound' ? '整柜入库单' : activeTab === 'outbound' ? '整柜出库单' : '整柜库存'}列表`"><template #toolbar><div class="business-query-panel"><div class="status-tabs" role="tablist" aria-label="整柜业务类型"><button v-for="(tab, index) in businessTabs" :key="tab.value" type="button" role="tab" :aria-selected="activeTab === tab.value" aria-controls="domestic-container-results" :tabindex="activeTab === tab.value ? 0 : -1" :class="['status-tab', { 'is-active': activeTab === tab.value }]" @click="switchTab(tab.value)" @keydown="handleTabKeydown($event, index)">{{ tab.label }}</button></div><QueryToolbar v-model="filters" class="business-query-toolbar" ariaLabel="整柜业务筛选" :fields="queryFields" :preference-key="`domestic-containers-${activeTab}-query`" @search="search" /></div></template><template v-if="loadError" #state><LoadErrorState title="整柜业务加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template><section v-if="!loadError" id="domestic-container-results" class="surface-panel table-panel"><div v-if="activeTab === 'inventory'" class="inventory-summary"><div><span>入库箱数</span><strong>{{ summary.inboundBoxes }}</strong></div><div><span>已出库箱数</span><strong>{{ summary.outboundBoxes }}</strong></div><div><span>当前库存</span><strong>{{ summary.availableBoxes }}</strong></div><div><span>累计入库体积</span><strong>{{ summary.inboundVolumeCbm.toFixed(2) }} m³</strong></div></div><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" stripe row-key="id" :preference-key="`domestic-containers-${activeTab}-table`" :empty-text="activeTab === 'inbound' ? '暂无整柜入库单' : activeTab === 'outbound' ? '暂无整柜出库单' : '暂无整柜库存'"><template #toolbar-actions><el-button v-if="activeTab === 'inbound' && access.can('platform:domestic-container:inbound:create')" type="primary" @click="openInbound"><Plus :size="16" />新建入库单</el-button><el-button v-if="activeTab === 'outbound' && access.can('platform:domestic-container:outbound:create')" type="primary" @click="openOutbound"><Plus :size="16" />新建出库单</el-button></template><template #actions="{ row }"><el-button v-if="canViewInventory(row as ContainerOrder | InventoryRecord)" size="small" type="primary" plain @click="viewInventory(row as ContainerOrder | InventoryRecord)"><Eye :size="15" />查看</el-button><span v-else>—</span></template><template #status="{ row }"><StatusBadge :status="statusLabel(row as ContainerOrder | InventoryRecord)" /></template></DataTable></section></ListPageLayout>
|
||||
<AppDialog v-model="dialogOpen" class="domestic-container-create-dialog" :title="dialogType === 'inbound' ? '新建整柜入库单' : '新建整柜出库单'" width="min(720px, calc(100vw - 28px))" :confirm-loading="saving" :close-on-click-modal="false" :confirm-text="dialogType === 'inbound' ? '创建入库单' : '创建出库单'" @confirm="submit"><LoadErrorState v-if="submitError" compact title="创建失败" :message="submitError.message" :show-retry="false"><template #details><ApiErrorMeta :details="submitError" /></template></LoadErrorState><el-form ref="formRef" :model="currentForm" :rules="rules" label-position="top"><el-row :gutter="16"><template v-if="dialogType === 'inbound'"><el-col :xs="24" :sm="12"><el-form-item label="国内仓" prop="warehouseId"><el-select v-model="inboundForm.warehouseId" :loading="warehouseLoading" :disabled="Boolean(warehouseError)" placeholder="请选择启用的国内仓"><el-option v-for="item in warehouseOptions" :key="String(item.value)" :label="item.label" :value="String(item.value)" /></el-select></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="货物名称" prop="cargoName"><el-input v-model="inboundForm.cargoName" maxlength="128" placeholder="请输入货物名称" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="集装箱号" prop="containerNo"><el-input v-model="inboundForm.containerNo" maxlength="32" placeholder="如 MSCU1234567" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="箱子数量" prop="boxCount"><el-input-number v-model="inboundForm.boxCount" :min="1" :max="999999999" :precision="0" controls-position="right" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="体积(m³)" prop="volumeCbm"><el-input-number v-model="inboundForm.volumeCbm" :min="0.0001" :max="999999999999.9999" :precision="4" controls-position="right" /></el-form-item></el-col></template><template v-else><el-col :xs="24" :sm="12"><el-form-item label="国内仓" prop="warehouseId"><el-select v-model="outboundForm.warehouseId" :loading="warehouseLoading" :disabled="Boolean(warehouseError)" placeholder="请选择启用的国内仓" @change="loadCandidates"><el-option v-for="item in warehouseOptions" :key="String(item.value)" :label="item.label" :value="String(item.value)" /></el-select></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="绑定集装箱号" prop="containerNo"><el-select v-model="outboundForm.inventoryId" :loading="candidateLoading" :disabled="!outboundForm.warehouseId || Boolean(candidateError)" placeholder="请选择已有集装箱号" @change="selectInventory"><el-option v-for="item in inventoryCandidates" :key="String(item.id)" :label="`${item.containerNo} · 可用 ${item.availableBoxes} 箱`" :value="String(item.id)" /></el-select><input v-model="outboundForm.containerNo" class="sr-only" tabindex="-1"></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="货物名称"><el-input v-model="outboundForm.cargoName" readonly /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="出库箱数" prop="boxCount"><el-input-number v-model="outboundForm.boxCount" :min="1" :max="999999999" :precision="0" controls-position="right" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="可用库存"><el-input :model-value="`${outboundForm.availableBoxes} 箱`" readonly /></el-form-item></el-col><el-col v-if="candidateError" :xs="24"><LoadErrorState compact title="可出库库存加载失败" :message="candidateError.message" @retry="loadCandidates"><template #details><ApiErrorMeta :details="candidateError" /></template></LoadErrorState></el-col></template><el-col :xs="24" :sm="12"><el-form-item :label="dialogType === 'inbound' ? '入库时间' : '出库时间'" prop="operationTime"><el-date-picker v-model="currentForm.operationTime" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" format="YYYY-MM-DD HH:mm" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="状态" prop="status"><el-select v-model="currentForm.status"><el-option :label="dialogType === 'inbound' ? '已入库' : '已出库'" value="COMPLETED" /><el-option :label="dialogType === 'inbound' ? '待入库' : '待出库'" value="PENDING" /></el-select></el-form-item></el-col><el-col :xs="24"><el-form-item label="备注"><el-input v-model="currentForm.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入备注" /></el-form-item></el-col></el-row></el-form></AppDialog></AdminPage></template>
|
||||
|
||||
<style scoped>.domestic-containers-page{min-width:0}.business-query-panel{overflow:hidden;border:1px solid var(--cc-border);border-radius:8px;background:#fff}.status-tabs{display:flex;max-width:100%;overflow-x:auto;margin-bottom:0;border-bottom:1px solid #e5e7eb;background:#f9fafb;scrollbar-width:thin}.status-tab{position:relative;flex:0 0 auto;min-height:44px;padding:0 18px;border:0;background:transparent;color:var(--cc-muted);font:inherit;font-size:14px;cursor:pointer}.status-tab::after{position:absolute;right:12px;bottom:0;left:12px;height:3px;background:transparent;content:''}.status-tab:hover{color:var(--cc-primary);background:#f6f9fd}.status-tab:focus-visible{outline:2px solid var(--cc-primary);outline-offset:-2px}.status-tab.is-active{color:var(--cc-primary);font-weight:600}.status-tab.is-active::after{background:var(--cc-primary)}.business-query-toolbar{border:0}.inventory-summary{display:flex;align-items:center;gap:0;margin:0 16px 12px;border:1px solid var(--cc-border);background:#f8fafc}.inventory-summary>div{display:flex;align-items:baseline;gap:8px;min-width:0;padding:9px 16px;border-right:1px solid var(--cc-border)}.inventory-summary>div:last-child{border-right:0}.inventory-summary span{color:var(--cc-muted);font-size:12px}.inventory-summary strong{font-size:15px}.domestic-container-create-dialog :deep(.el-select),.domestic-container-create-dialog :deep(.el-date-editor),.domestic-container-create-dialog :deep(.el-input-number){width:100%}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}@media(max-width:720px){.status-tab{min-height:46px;padding:0 14px}.inventory-summary{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.inventory-summary>div:nth-child(2){border-right:0}.inventory-summary>div:nth-child(-n+2){border-bottom:1px solid var(--cc-border)}}@media(max-width:360px){.status-tab{padding:0 10px;font-size:13px}.inventory-summary{grid-template-columns:1fr}.inventory-summary>div{border-right:0;border-bottom:1px solid var(--cc-border)}.inventory-summary>div:last-child{border-bottom:0}}</style>
|
||||
|
||||
Reference in New Issue
Block a user