Merge pull request 'Release/test' (#6) from release/test into main
Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
Vendored
+2
@@ -22,6 +22,8 @@ declare module 'vue' {
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
|
||||
@@ -42,14 +42,24 @@ const emit = defineEmits<{
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.first-leg-editor-page { min-width: 0; border: 1px solid #edf0f5; background: #fff; box-shadow: 0 5px 18px rgba(15, 31, 58, .04); }
|
||||
.first-leg-editor-page { min-width: 0; padding-bottom: 72px; border: 1px solid #edf0f5; background: #fff; box-shadow: 0 5px 18px rgba(15, 31, 58, .04); }
|
||||
.editor-page-nav { display: flex; min-height: 52px; align-items: center; justify-content: space-between; gap: 16px; padding: 8px 24px; border-bottom: 1px solid #edf0f5; }
|
||||
.editor-page-nav span { color: var(--muted); font-size: 12px; }
|
||||
.editor-page-body { padding: 26px 32px 34px; }
|
||||
.editor-page-footer { position: sticky; z-index: 3; bottom: 0; display: flex; justify-content: center; padding: 12px 24px; border-top: 1px solid #edf0f5; background: rgba(255, 255, 255, 0.96); box-shadow: 0 -6px 18px rgba(15, 31, 58, .06); backdrop-filter: blur(10px); }
|
||||
.editor-page-footer { position: fixed; z-index: 18; right: 0; bottom: 0; left: 224px; display: flex; min-height: 68px; align-items: center; justify-content: center; padding: 10px 24px; border-top: 1px solid #dfe6ee; background: rgba(255, 255, 255, 0.97); box-shadow: 0 -6px 18px rgba(15, 31, 58, .08); backdrop-filter: blur(10px); }
|
||||
.editor-page-footer :deep(.editor-actions) { width: min(100%, 900px); justify-content: center; }
|
||||
.editor-page-footer :deep(.editor-actions > .el-button) { min-width: 132px; min-height: 42px; }
|
||||
.editor-page-footer :deep(.editor-actions > .el-button:last-child) { min-width: 172px; }
|
||||
.editor-page-footer :deep(.submit-order-button) { position: relative; isolation: isolate; overflow: hidden; border-color: rgba(255, 255, 255, .24); background: linear-gradient(105deg, #08a9bd 0%, #167da2 28%, #2563eb 62%, #4338ca 100%); background-size: 220% 100%; box-shadow: 0 10px 24px rgba(23, 107, 135, .3), 0 0 0 1px rgba(34, 211, 238, .13), 0 0 26px rgba(37, 99, 235, .12); animation: submit-gradient 5s ease-in-out infinite, submit-pulse 2.8s ease-in-out infinite; }
|
||||
.editor-page-footer :deep(.submit-order-button::before) { position: absolute; z-index: -1; top: -80%; bottom: -80%; left: -48%; width: 34%; transform: rotate(18deg); background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .68), transparent); content: ''; animation: submit-shine 3.2s ease-in-out infinite; }
|
||||
.editor-page-footer :deep(.submit-order-button::after) { position: absolute; z-index: -1; inset: 1px; border-radius: 5px; background: linear-gradient(180deg, rgba(255, 255, 255, .13), transparent 48%); content: ''; pointer-events: none; }
|
||||
.editor-page-footer :deep(.submit-order-button:hover) { transform: translateY(-2px) scale(1.025); border-color: rgba(255, 255, 255, .3); background: linear-gradient(105deg, #08b8cb 0%, #167da2 28%, #2563eb 62%, #4338ca 100%); background-size: 220% 100%; box-shadow: 0 15px 30px rgba(23, 107, 135, .35), 0 0 0 1px rgba(34, 211, 238, .25), 0 0 34px rgba(37, 99, 235, .2); }
|
||||
.editor-page-footer :deep(.submit-order-button svg) { position: relative; z-index: 1; filter: drop-shadow(0 2px 4px rgba(0, 0, 0, .18)); animation: submit-plane 2.6s ease-in-out infinite; }
|
||||
@keyframes submit-gradient { 0%, 100% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } }
|
||||
@keyframes submit-pulse { 0%, 100% { filter: saturate(1); } 50% { filter: saturate(1.12) brightness(1.04); } }
|
||||
@keyframes submit-shine { 0%, 52% { left: -48%; opacity: 0; } 62% { opacity: 1; } 86%, 100% { left: 122%; opacity: 0; } }
|
||||
@keyframes submit-plane { 0%, 100% { transform: translate3d(0, 1px, 0) rotate(-3deg); } 50% { transform: translate3d(3px, -2px, 0) rotate(2deg); } }
|
||||
@media (prefers-reduced-motion: reduce) { .editor-page-footer :deep(.submit-order-button), .editor-page-footer :deep(.submit-order-button::before), .editor-page-footer :deep(.submit-order-button svg) { animation: none; } }
|
||||
:global(.first-leg-editor-dialog.el-dialog) { display: flex; max-width: 1500px; height: 94vh; margin-bottom: 0; flex-direction: column; overflow: hidden; border-radius: 6px; }
|
||||
:global(.first-leg-editor-dialog .el-dialog__header) { flex: 0 0 auto; margin-right: 0; padding: 17px 22px; border-bottom: 1px solid var(--border); }
|
||||
:global(.first-leg-editor-dialog .el-dialog__title) { color: var(--navy); font-size: 16px; font-weight: 700; }
|
||||
@@ -58,10 +68,11 @@ const emit = defineEmits<{
|
||||
:global(.first-leg-editor-dialog .el-dialog__footer) { flex: 0 0 auto; padding: 12px 22px; border-top: 1px solid var(--border); background: #fff; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.first-leg-editor-page { padding-bottom: 0; }
|
||||
.editor-page-nav { align-items: flex-start; flex-direction: column; gap: 2px; padding: 10px 16px; }
|
||||
.editor-page-nav span { padding-left: 1px; line-height: 1.5; }
|
||||
.editor-page-body { padding: 18px 16px 24px; }
|
||||
.editor-page-footer { padding: 10px 16px; }
|
||||
.editor-page-footer { position: sticky; bottom: 0; left: auto; padding: 10px 16px; }
|
||||
:global(.first-leg-editor-dialog.el-dialog) { width: calc(100vw - 24px) !important; height: 96vh; margin-top: 2vh; }
|
||||
}
|
||||
|
||||
|
||||
@@ -37,5 +37,5 @@ function selectMode(mode: 'standard' | 'quick') {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.first-leg-create-hub{display:grid;min-width:0;gap:12px}.create-hub-heading{display:grid;gap:5px}.create-hub-heading h1{margin:0;color:var(--navy);font-size:24px;line-height:1.25}.create-hub-heading p{margin:0;color:var(--muted);font-size:13px}.order-mode-switch{display:inline-flex;width:max-content;padding:5px;border:1px solid #dbe4f1;border-radius:12px;background:#fff;box-shadow:0 8px 20px rgba(26,43,75,.06)}.order-mode-switch button{display:inline-flex;height:38px;align-items:center;gap:8px;padding:0 20px;border:0;border-radius:8px;background:transparent;color:#607087;font:inherit;font-size:13px;font-weight:650;cursor:pointer}.order-mode-switch button.active{background:linear-gradient(135deg,#4f73ee,#8a56dc);color:#fff;box-shadow:0 6px 14px rgba(79,115,238,.22)}.order-mode-switch button:focus-visible{outline:2px solid var(--primary);outline-offset:2px}
|
||||
.first-leg-create-hub{display:grid;min-width:0;gap:14px}.create-hub-heading{display:grid;gap:5px}.create-hub-heading h1{margin:0;color:var(--navy);font-size:24px;line-height:1.25}.create-hub-heading p{margin:0;color:var(--muted);font-size:13px}.order-mode-switch{display:inline-flex;width:max-content;padding:4px;border:1px solid #d9e2ec;border-radius:8px;background:#f4f7fa}.order-mode-switch button{display:inline-flex;height:38px;align-items:center;gap:8px;padding:0 20px;border:0;border-radius:6px;background:transparent;color:#607087;font:inherit;font-size:13px;font-weight:650;cursor:pointer}.order-mode-switch button.active{background:var(--primary);color:#fff;box-shadow:0 2px 6px rgba(23,107,135,.2)}.order-mode-switch button:focus-visible{outline:2px solid var(--primary);outline-offset:2px}
|
||||
</style>
|
||||
|
||||
@@ -48,6 +48,13 @@ test('exposes real list, draft, submit, and detail workflow entries without canc
|
||||
assert.doesNotMatch(source, /cancelOrder|取消订单/)
|
||||
})
|
||||
|
||||
test('returns edited submissions to the refreshed order list instead of opening detail', () => {
|
||||
const submitSource = source.slice(source.indexOf('async function submitOrder()'), source.indexOf('async function searchOrders'))
|
||||
assert.match(submitSource, /if \(isCreatePage\.value\)[\s\S]*router\.push\(\{ name: 'first-leg-orders' \}\)[\s\S]*return/)
|
||||
assert.match(submitSource, /await loadPageData\(\)/)
|
||||
assert.doesNotMatch(submitSource, /first-leg-order-detail/)
|
||||
})
|
||||
|
||||
test('copies an order into the dedicated create page without reusing order identity', () => {
|
||||
assert.match(source, /key: 'copy', label: '复制下单'/)
|
||||
assert.match(source, /name: 'first-leg-order-create'[\s\S]*copyFrom: order\.id/)
|
||||
@@ -351,7 +358,8 @@ test('shows a loading state before the list can create an order', () => {
|
||||
test('auto-fills trusted warehouse and company data instead of asking customers to repeat it', () => {
|
||||
assert.match(source, /async function selectDestinationWarehouse\(id: string\)/)
|
||||
assert.match(source, /await store\.loadWarehouseDetail\(warehouse\.id\)/)
|
||||
assert.match(source, /availableWarehouses\.value\.length === 1/)
|
||||
assert.match(source, /warehouse\.code\.toUpperCase\(\)\.includes\('YIWU'\)/)
|
||||
assert.match(source, /warehouse\.name\.includes\('义乌'\)/)
|
||||
assert.match(source, /availableDestinationWarehouses\.value\.length === 1/)
|
||||
assert.match(source, /function createPickupPartySnapshot\(\)/)
|
||||
assert.match(source, /contactName: tenantStore\.company\.contactName/)
|
||||
@@ -415,6 +423,24 @@ test('distills order creation into the confirmed prototype sections with server
|
||||
assert.doesNotMatch(source, /v-model="form\.(?:businessType|origin|destination|priority|tradeTerm)"/)
|
||||
})
|
||||
|
||||
test('matches the prototype two-column create layout and reset action', () => {
|
||||
assert.match(source, /class="order-form-section prototype-section order-basic-section"/)
|
||||
assert.match(source, /<h3>客户下单<\/h3><p>创建新订单<\/p>/)
|
||||
assert.match(source, /\.is-create-route \.prototype-order-form \{[^}]*grid-template-columns: repeat\(2, minmax\(0, 1fr\)\)/)
|
||||
assert.match(source, /\.is-create-route \.cargo-section, \.is-create-route \.cost-estimate-section \{ grid-column: 1 \/ -1; \}/)
|
||||
assert.match(source, /\.is-create-route \.prototype-form-row :deep\(\.el-form-item__label\) \{[^}]*height: 40px[^}]*align-items: center[^}]*justify-content: flex-end/)
|
||||
assert.match(source, /\.is-create-route \.compact-transport-grid > :deep\(\.el-form-item__label\) \{[^}]*align-items: center[^}]*text-align: right/)
|
||||
assert.match(source, /async function resetCreateForm\(\)/)
|
||||
assert.match(source, /确认清空当前填写的订单内容并恢复默认值/)
|
||||
assert.match(source, /v-if="isCreatePage"[^>]*@click="resetCreateForm"[^>]*><RotateCcw[^>]*\/>重置/)
|
||||
assert.match(source, /class="submit-order-button"/)
|
||||
assert.match(editorShellSource, /\.first-leg-editor-page \{[^}]*padding-bottom: 72px/)
|
||||
assert.match(editorShellSource, /\.editor-page-footer \{[^}]*position: fixed[^}]*bottom: 0[^}]*left: 224px/)
|
||||
assert.match(editorShellSource, /submit-gradient 5s ease-in-out infinite/)
|
||||
assert.match(editorShellSource, /submit-shine 3\.2s ease-in-out infinite/)
|
||||
assert.match(editorShellSource, /@media \(prefers-reduced-motion: reduce\)/)
|
||||
})
|
||||
|
||||
test('shows an optional LX inbound order number for one-click dropship orders', () => {
|
||||
assert.match(source, /v-if="form\.fulfillmentType === 'ONE_CLICK_DROPSHIP'" label="LX入库单号" prop="lxCode"/)
|
||||
assert.match(source, /v-model="form\.lxCode"[^>]*placeholder="请输入LX入库单号"/)
|
||||
@@ -478,6 +504,9 @@ test('shows field-level validation and concise guidance for required order input
|
||||
assert.match(source, /prop="shipperPartySnapshot\.contactName" required/)
|
||||
assert.match(source, /prop="consigneePartySnapshot\.address" required/)
|
||||
assert.match(source, /:prop="`items\.\$\{index\}\.cargoNameCn`" required/)
|
||||
assert.match(source, /form\.destinationType === 'FBA' \|\| form\.fulfillmentType === 'ONE_CLICK_DROPSHIP' \? '必填' : '选填'/)
|
||||
assert.match(source, /SKU NO\. 和 UPC NO\. 均为选填项/)
|
||||
assert.match(source, /一件代发必须填写 SKU/)
|
||||
assert.match(source, /:prop="`items\.\$\{index\}\.grossWeightKg`" required/)
|
||||
assert.match(source, /<span class="required-field-mark" aria-hidden="true">\*<\/span>总毛重/)
|
||||
assert.match(source, /<span class="required-field-mark" aria-hidden="true">\*<\/span>总体积/)
|
||||
|
||||
@@ -1262,8 +1262,12 @@ async function loadSelectedWarehouse(id: string | number | boolean | undefined)
|
||||
|
||||
async function autofillWarehouseSelections() {
|
||||
if (!isCreatePage.value || editingId.value) return
|
||||
if (!form.receivingWarehouseId && availableWarehouses.value.length === 1) {
|
||||
form.receivingWarehouseId = availableWarehouses.value[0].id
|
||||
if (!form.receivingWarehouseId) {
|
||||
const yiwuWarehouse = availableWarehouses.value.find((warehouse) => warehouse.code.toUpperCase().includes('YIWU'))
|
||||
?? availableWarehouses.value.find((warehouse) => warehouse.name.includes('义乌'))
|
||||
if (yiwuWarehouse) form.receivingWarehouseId = yiwuWarehouse.id
|
||||
}
|
||||
if (form.receivingWarehouseId) {
|
||||
await loadSelectedWarehouse(form.receivingWarehouseId)
|
||||
}
|
||||
if (form.destinationType !== 'PRIVATE' && !form.destinationWarehouseId && availableDestinationWarehouses.value.length === 1) {
|
||||
@@ -1485,6 +1489,27 @@ function closeEditor() {
|
||||
})
|
||||
}
|
||||
|
||||
async function resetCreateForm() {
|
||||
if (saving.value) return
|
||||
if (editorDirty.value) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认清空当前填写的订单内容并恢复默认值?', '重置订单', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认重置',
|
||||
cancelButtonText: '继续填写',
|
||||
})
|
||||
} catch (error) {
|
||||
if (error === 'cancel' || error === 'close') return
|
||||
throw error
|
||||
}
|
||||
}
|
||||
editingId.value = undefined
|
||||
replaceForm(emptyDraft())
|
||||
await autofillWarehouseSelections()
|
||||
resetEditorValidation()
|
||||
await router.replace({ name: 'first-leg-order-create' })
|
||||
}
|
||||
|
||||
function addCargoItem() {
|
||||
form.items.push(createEmptyCargoItem(`new-cargo-${++cargoSequence}`))
|
||||
resetEditorValidation()
|
||||
@@ -1954,7 +1979,7 @@ async function submitOrder() {
|
||||
const knownCompanyAuditStatus = companyAuditLoaded.value ? tenantStore.company.auditStatus : undefined
|
||||
const savedOrder = await store.saveDraft(cloneDraft(form), tenantStore.company.companyName, editingId.value)
|
||||
editingId.value = savedOrder.id
|
||||
const order = await store.submitSaved(savedOrder.id, knownCompanyAuditStatus)
|
||||
await store.submitSaved(savedOrder.id, knownCompanyAuditStatus)
|
||||
markEditorClean()
|
||||
editorOpen.value = false
|
||||
if (isCreatePage.value) {
|
||||
@@ -1963,7 +1988,7 @@ async function submitOrder() {
|
||||
ElMessage.success('订单已提交,等待平台审核')
|
||||
return
|
||||
}
|
||||
await router.push({ name: 'first-leg-order-detail', params: { orderId: order.id } })
|
||||
await loadPageData()
|
||||
ElMessage.success('订单已提交,等待平台审核')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '订单提交失败')
|
||||
@@ -2134,8 +2159,8 @@ async function searchOrders(model: QueryModel) {
|
||||
|
||||
<FirstLegOrderEditorShell v-model="editorOpen" :page="isCreatePage" :title="editingId ? '编辑订单草稿' : (routeCopySourceId ? '复制订单' : '创建订单')" :before-close="requestCloseEditor" @back="closeEditor">
|
||||
<el-form ref="orderFormRef" v-loading="isCreatePage && supportingDataLoading" :model="form" :rules="orderFormRules" label-position="top" class="order-form prototype-order-form" :class="{ 'is-dialog-order-form': !isCreatePage }" scroll-to-error>
|
||||
<section class="order-form-section prototype-section">
|
||||
<div class="order-section-heading"><div><h3>客户下单</h3><p>先选择货物如何送到国内收货仓,并填写本次运输安排。</p></div></div>
|
||||
<section class="order-form-section prototype-section order-basic-section">
|
||||
<div class="order-section-heading"><div><h3>客户下单</h3><p>创建新订单</p></div></div>
|
||||
<div class="prototype-form-flow shipping-form-flow">
|
||||
<el-form-item label="履约类型" prop="fulfillmentType" required class="prototype-form-row prototype-form-row--tabs"><el-radio-group v-model="form.fulfillmentType" class="segmented-options" @change="changeFulfillmentType"><el-radio-button value="STANDARD">普通头程</el-radio-button><el-radio-button value="ONE_CLICK_DROPSHIP">一件代发</el-radio-button></el-radio-group><small v-if="form.fulfillmentType === 'ONE_CLICK_DROPSHIP'" class="field-guidance">测量完成后不办理国内库存入库,将直接推送 PDA 合托。</small></el-form-item>
|
||||
<el-form-item v-if="form.fulfillmentType === 'ONE_CLICK_DROPSHIP'" label="LX入库单号" prop="lxCode" class="prototype-form-row prototype-form-row--wide"><el-input v-model="form.lxCode" maxlength="128" placeholder="请输入LX入库单号" /><small class="field-guidance">用于绑定领星入库单,提交后可在尾程订单、入库单和出库单中查询。</small></el-form-item>
|
||||
@@ -2213,7 +2238,7 @@ async function searchOrders(model: QueryModel) {
|
||||
<table class="cargo-edit-table">
|
||||
<thead><tr>
|
||||
<th class="cargo-check-cell"><el-checkbox :model-value="allCargoSelected" :indeterminate="someCargoSelected" aria-label="全选货物" @change="toggleAllCargoItems" /></th><th class="cargo-index-cell">#</th>
|
||||
<th>SKU NO.<small>SKU必填</small></th><th>中文品名</th><th class="cargo-image-column">图片</th><th>英文品名</th><th>报关 HS CODE</th><th>清关 HS CODE</th><th>包装类型</th><th>材质</th><th class="cargo-switch-column">危险品</th><th class="cargo-switch-column">木制品<small>勾选后须商检</small></th>
|
||||
<th>SKU NO.<small>{{ form.destinationType === 'FBA' || form.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? '必填' : '选填' }}</small></th><th>中文品名</th><th class="cargo-image-column">图片</th><th>英文品名</th><th>报关 HS CODE</th><th>清关 HS CODE</th><th>包装类型</th><th>材质</th><th class="cargo-switch-column">危险品</th><th class="cargo-switch-column">木制品<small>勾选后须商检</small></th>
|
||||
<th class="cargo-number-column">箱规长 cm</th><th class="cargo-number-column">箱规宽 cm</th><th class="cargo-number-column">箱规高 cm</th><th class="cargo-number-column">每箱数量</th><th class="cargo-number-column">箱数</th><th class="cargo-number-column"><span class="required-field-mark" aria-hidden="true">*</span>总毛重</th><th class="cargo-number-column"><span class="required-field-mark" aria-hidden="true">*</span>总体积</th><th class="cargo-number-column">总数量</th><th class="cargo-number-column">总净重</th>
|
||||
<th>清关申报单价 USD</th><th>清关申报金额 USD</th><th>用途</th><th>型号</th><th>品牌</th><th class="cargo-dimensions-column">产品规格 长×宽×高</th><th>货件编号<small>货件编号必填</small></th><th>跟踪编号<small>跟踪编号必填</small></th><th>UPC NO.</th><th>备注</th><th class="cargo-operation-column">操作</th>
|
||||
</tr></thead>
|
||||
@@ -2233,7 +2258,7 @@ async function searchOrders(model: QueryModel) {
|
||||
</tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="cargo-table-hint"><span v-if="form.fulfillmentType === 'ONE_CLICK_DROPSHIP'">每个箱组默认装一个 SKU;同一箱内有多个 SKU 时点击“添加 SKU”,箱数、箱规、重量和体积只填写一次。</span><span v-else>SKU NO.、货件编号、跟踪编号为 FBA 货物必填项,UPC NO. 选填</span></div>
|
||||
<div class="cargo-table-hint"><span v-if="form.fulfillmentType === 'ONE_CLICK_DROPSHIP'">一件代发必须填写 SKU;每个箱组默认装一个 SKU,同一箱内有多个 SKU 时点击“添加 SKU”,箱数、箱规、重量和体积只填写一次。</span><span v-else-if="form.destinationType === 'FBA'">SKU NO.、货件编号、跟踪编号为 FBA 货物必填项,UPC NO. 选填</span><span v-else>SKU NO. 和 UPC NO. 均为选填项</span></div>
|
||||
</section>
|
||||
|
||||
<section class="order-form-section prototype-section cost-estimate-section" aria-label="费用预估">
|
||||
@@ -2271,7 +2296,7 @@ async function searchOrders(model: QueryModel) {
|
||||
</div>
|
||||
</section>
|
||||
</el-form>
|
||||
<template #footer><div class="editor-actions"><el-button :disabled="saving" @click="closeEditor">返回</el-button><el-button v-if="editingId ? canEditOrder : canCreateOrder" :loading="saving" @click="saveDraft">保存草稿</el-button><el-button v-if="auth.hasPermission('order:first-leg:submit')" type="primary" :loading="saving" @click="submitOrder"><Send :size="16" />{{ companySubmissionBlocked ? '提交前需完成资料审核' : '提交订单' }}</el-button></div></template>
|
||||
<template #footer><div class="editor-actions"><el-button v-if="isCreatePage" :disabled="saving" @click="resetCreateForm"><RotateCcw :size="16" />重置</el-button><el-button v-else :disabled="saving" @click="closeEditor">返回</el-button><el-button v-if="editingId ? canEditOrder : canCreateOrder" :loading="saving" @click="saveDraft">保存草稿</el-button><el-button v-if="auth.hasPermission('order:first-leg:submit')" class="submit-order-button" type="primary" :loading="saving" @click="submitOrder"><Send :size="17" />{{ companySubmissionBlocked ? '提交前需完成资料审核' : '提交订单' }}</el-button></div></template>
|
||||
</FirstLegOrderEditorShell>
|
||||
|
||||
<el-dialog v-model="batchImportOpen" title="Excel 批量创建订单草稿" width="680px" destroy-on-close append-to-body>
|
||||
@@ -2528,8 +2553,23 @@ async function searchOrders(model: QueryModel) {
|
||||
.is-create-route { gap: 0; }
|
||||
.is-create-route .first-leg-editor-page { width: 100%; }
|
||||
.is-create-route .supporting-data-error, .is-create-route > .el-alert { margin-bottom: 14px; }
|
||||
.is-create-route .prototype-order-form { width: 100%; margin: 0; padding: 0 0 16px; }
|
||||
.is-create-route .prototype-section:not(.cargo-section):not(.cost-estimate-section):not(.destination-section) { width: min(100%, 900px); }
|
||||
.is-create-route .prototype-order-form { display: grid; width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: stretch; gap: 16px; margin: 0; padding: 0 0 16px; }
|
||||
.is-create-route .prototype-section { min-width: 0; margin: 0; padding: 0 24px 24px; border: 1px solid #dfe6ee; border-radius: 8px; background: #fff; }
|
||||
.is-create-route .prototype-section + .prototype-section { margin-top: 0; }
|
||||
.is-create-route .order-basic-section, .is-create-route .destination-section { height: 100%; }
|
||||
.is-create-route .cargo-section, .is-create-route .cost-estimate-section { grid-column: 1 / -1; }
|
||||
.is-create-route .order-section-heading { min-height: 66px; align-items: center; margin: 0 -24px 22px; padding: 15px 24px; border-bottom: 1px solid #e7ecf2; }
|
||||
.is-create-route .order-section-heading h3, .is-create-route .prototype-section:first-child > .order-section-heading h3 { margin: 0; color: var(--navy); font-size: 17px; font-weight: 700; }
|
||||
.is-create-route .order-section-heading p { margin-top: 4px; color: #8a98a8; }
|
||||
.is-create-route .prototype-form-flow, .is-create-route .destination-form-flow { width: 100%; max-width: none; }
|
||||
.is-create-route .prototype-form-row, .is-create-route .prototype-form-row--wide, .is-create-route .prototype-form-row--compact, .is-create-route .prototype-form-row--short, .is-create-route .prototype-form-row--tabs { grid-template-columns: 120px minmax(0, 1fr); }
|
||||
.is-create-route .prototype-form-row :deep(.el-form-item__label) { display: flex; height: 40px; min-height: 40px; align-items: center; justify-content: flex-end; padding: 0 18px 0 0; font-size: 13px; line-height: 1.35; text-align: right; }
|
||||
.is-create-route .segmented-options, .is-create-route .destination-type-options { width: 100%; max-width: 560px; background: #f3f6f9; }
|
||||
.is-create-route .compact-transport-grid { width: 100%; margin-top: 4px; }
|
||||
.is-create-route .compact-transport-grid > :deep(.el-form-item), .is-create-route .compact-transport-grid > :deep(.el-form-item:nth-child(1)), .is-create-route .compact-transport-grid > :deep(.el-form-item:nth-child(6)), .is-create-route .compact-transport-grid > :deep(.el-form-item:nth-child(7)) { grid-template-columns: 120px minmax(0, 1fr); }
|
||||
.is-create-route .compact-transport-grid > :deep(.el-form-item__label) { display: flex; height: 40px; min-height: 40px; align-items: center; justify-content: flex-end; padding: 0 18px 0 0; line-height: 1.35; text-align: right; }
|
||||
.is-create-route .selected-destination { width: auto; margin-left: 138px; }
|
||||
.is-create-route .cargo-table-heading, .is-create-route .cost-estimate-section .order-section-heading { margin-bottom: 16px; }
|
||||
.order-form { width: min(100%, 1000px); margin: 0; padding: 0 0 12px; }
|
||||
.order-form.is-dialog-order-form { width: 100%; max-width: none; }
|
||||
.is-dialog-order-form > .cargo-section, .is-dialog-order-form > .cost-estimate-section { width: 100%; max-width: none; }
|
||||
@@ -2604,6 +2644,7 @@ async function searchOrders(model: QueryModel) {
|
||||
.compact-transport-grid :deep(.el-form-item__label) { color: #4d5a6c; font-size: 13px; }
|
||||
.shipping-form-flow { max-width: 900px; }
|
||||
.prototype-order-form > .prototype-section:first-child { width: 100%; }
|
||||
.is-create-route .prototype-order-form > .prototype-section:first-child > .order-section-heading { margin-top: 0; }
|
||||
.shipping-form-flow .prototype-form-row { margin-bottom: 10px; }
|
||||
.shipping-form-flow .prototype-form-row--tabs { margin-bottom: 12px; }
|
||||
.compact-transport-grid { display: grid; width: 100%; grid-template-columns: 1fr; gap: 0; margin: 6px 0 0; }
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { overseasApiRequest } from '@/api/overseasHttp'
|
||||
import { overseasApiRequest, overseasBlobRequest } from '@/api/overseasHttp'
|
||||
import type { PageResult } from './warehouseServices'
|
||||
|
||||
export interface ReturnOrderSummary {
|
||||
returnNo: string; overseasWarehouseId: string; warehouseContactName?: string; warehouseContactPhone?: string; customerReferenceNo?: string; status: string
|
||||
skuTypes: number; expectedQuantity: number; receivedQuantity: number; needRelabel: boolean; createdAt: string
|
||||
skuTypes: number; expectedQuantity: number; receivedQuantity: number; needRelabel: boolean; relabelNo?: string; relabelStatus?: string; createdAt: string
|
||||
}
|
||||
export interface ReturnOrderItem {
|
||||
skuCode: string; productNameZh?: string; expectedQuantity: number; receivedQuantity: number
|
||||
}
|
||||
export interface ReturnReceipt {
|
||||
receiptNo: string; receiptBatchNo: string; cartonMark?: string; trackingNo?: string; startedBy: 'RETURN_NO' | 'SKU'
|
||||
receiptNo: string; receiptBatchNo: string; palletCode: string; trackingNo?: string; startedBy: 'RETURN_NO' | 'SKU'
|
||||
receivedQuantity: number; operatorName?: string; signedAt: string
|
||||
}
|
||||
export interface ReturnOrderDetail { order: ReturnOrderSummary; remark?: string; items: ReturnOrderItem[]; receipts: ReturnReceipt[] }
|
||||
export interface ReturnPallet { palletCode: string; skuTypes: number; totalQuantity: number; items: Array<{ skuCode: string; productNameZh?: string; quantity: number }> }
|
||||
export interface ReturnPhoto { fileId: number; fileName: string; contentType: string; sizeBytes: number }
|
||||
export interface ReturnOrderDetail { order: ReturnOrderSummary; remark?: string; items: ReturnOrderItem[]; receipts: ReturnReceipt[]; pallets: ReturnPallet[]; photos: ReturnPhoto[] }
|
||||
export interface ReturnOrderCreate {
|
||||
overseasWarehouseId: string; warehouseContactName: string; warehouseContactPhone: string; customerReferenceNo?: string; remark?: string; needRelabel: boolean; idempotencyKey: string
|
||||
items: Array<{ skuCode: string; productNameZh?: string; expectedQuantity: number }>
|
||||
@@ -27,3 +29,21 @@ export function getReturnOrder(returnNo: string) {
|
||||
export function createReturnOrder(payload: ReturnOrderCreate) {
|
||||
return overseasApiRequest<ReturnOrderDetail>({ method: 'POST', url: '/merchant/v1/last-leg/return-orders', data: payload })
|
||||
}
|
||||
export function updateReturnOrder(returnNo: string, payload: ReturnOrderCreate) {
|
||||
return overseasApiRequest<ReturnOrderDetail>({ method: 'PUT', url: `/merchant/v1/last-leg/return-orders/${encodeURIComponent(returnNo)}`, data: payload })
|
||||
}
|
||||
export interface ReturnRelabelCreate {
|
||||
fileScope: 'CARTON_TOTAL' | 'SKU_TOTAL'; destinationFbaWarehouseId: string; destinationFbaWarehouseCode: string; cartonRemark?: string; idempotencyKey: string
|
||||
items: Array<{ skuCode: string; quantity: number }>
|
||||
}
|
||||
export interface ReturnRelabelCreated { relabelNo: string; relabelApplicationId: string; returnNo: string; skuCodes: string[]; status: string }
|
||||
export function createReturnRelabel(returnNo: string, payload: ReturnRelabelCreate) {
|
||||
return overseasApiRequest<ReturnRelabelCreated>({ method: 'POST', url: `/merchant/v1/last-leg/return-orders/${encodeURIComponent(returnNo)}/relabel`, data: payload })
|
||||
}
|
||||
export function uploadReturnRelabelFile(relabelNo: string, scope: 'CARTON_TOTAL' | 'SKU_TOTAL', file: File) {
|
||||
const data = new FormData(); data.append('file', file)
|
||||
return overseasApiRequest<unknown>({ method: 'POST', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/files`, params: { scope }, data, headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
}
|
||||
export function downloadReturnPhoto(returnNo: string, fileId: string | number) {
|
||||
return overseasBlobRequest({ method: 'GET', url: `/merchant/v1/last-leg/return-orders/${encodeURIComponent(returnNo)}/photos/files/${encodeURIComponent(String(fileId))}/download` })
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface RelabelInventoryCandidate {
|
||||
inventoryBalanceId: string; inboundOrderId: string; inboundOrderNo?: string; inboundCode?: string
|
||||
overseasWarehouseId: string; overseasWarehouseCode?: string; overseasWarehouseName?: string
|
||||
destinationFbaWarehouseCode?: string; destinationFbaWarehouseName?: string; skuCode?: string; productNameZh?: string
|
||||
availablePackages: number; version: number
|
||||
availablePackages: number | string; version: number
|
||||
positions: Array<{ positionId: string; palletNo?: string; batchNo?: string; warehouseLocationCode?: string; availablePackages: number }>
|
||||
sourceType?: 'INBOUND' | 'RETURN_CARTON'; returnCartonId?: string; cartonMark?: string
|
||||
skuItems?: Array<{ skuCode: string; newSkuCode?: string; quantity: number; remark?: string }>
|
||||
|
||||
@@ -3,6 +3,15 @@ import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const view = readFileSync(new URL('./ReturnOrdersView.vue', import.meta.url), 'utf8')
|
||||
const api = readFileSync(new URL('../api/returnOrders.ts', import.meta.url), 'utf8')
|
||||
|
||||
test('退货签收照片通过客户鉴权下载接口加载并释放对象 URL', () => {
|
||||
assert.match(api, /\/return-orders\/\$\{encodeURIComponent\(returnNo\)\}\/photos\/files\/\$\{encodeURIComponent\(String\(fileId\)\)\}\/download/)
|
||||
assert.match(api, /overseasBlobRequest/)
|
||||
assert.match(view, /URL\.createObjectURL\(await downloadReturnPhoto\(detailReturnNo\.value, photo\.fileId\)\)/)
|
||||
assert.match(view, /URL\.revokeObjectURL\(url\)/)
|
||||
assert.match(view, /onBeforeUnmount\(clearPhotoUrls\)/)
|
||||
})
|
||||
|
||||
test('新建退货预报选择海外仓后带出联系人和电话', () => {
|
||||
assert.match(view, /@change="selectOperationWarehouse"/)
|
||||
@@ -18,7 +27,7 @@ test('新建退货预报选择海外仓后带出联系人和电话', () => {
|
||||
})
|
||||
|
||||
test('新建退货预报使用加宽的双列表单弹窗', () => {
|
||||
assert.match(view, /title="新建退货预报" width="1120px"/)
|
||||
assert.match(view, /:title="editingReturnNo \? '编辑退货预报' : '新建退货预报'" width="1120px"/)
|
||||
assert.match(view, /<el-form label-width="190px" class="return-create-form">/)
|
||||
assert.match(view, /return-create-form \{ display:grid; grid-template-columns:repeat\(2,minmax\(0,1fr\)\)/)
|
||||
})
|
||||
@@ -28,10 +37,10 @@ test('退货列表和详情使用客户可读仓库、状态与时间', () => {
|
||||
assert.match(view, /return warehouse \? warehouseLabel\(warehouse\) : '未关联'/)
|
||||
assert.match(view, /PENDING_RECEIPT: '待签收'/)
|
||||
assert.match(view, /PARTIALLY_RECEIVED: '部分签收'/)
|
||||
assert.match(view, /COMPLETED: '已完成'/)
|
||||
assert.match(view, /return needRelabel \? '已完成入库\/待换标' : '已完成入库'/)
|
||||
assert.match(view, /function formatDateTime\(value\?: string \| null\)/)
|
||||
assert.match(view, /\{\{ operationWarehouseLabel\(row\.overseasWarehouseId\) \}\}/)
|
||||
assert.match(view, /\{\{ statusLabel\(row\.status\) \}\}/)
|
||||
assert.match(view, /\{\{ statusLabel\(row\.status, row\.needRelabel\) \}\}/)
|
||||
assert.match(view, /\{\{ formatDateTime\(row\.createdAt\) \}\}/)
|
||||
assert.doesNotMatch(view, /<el-table-column prop="overseasWarehouseId"/)
|
||||
assert.doesNotMatch(view, /\{\{ selected\.order\.overseasWarehouseId \}\}/)
|
||||
@@ -39,7 +48,7 @@ test('退货列表和详情使用客户可读仓库、状态与时间', () => {
|
||||
|
||||
test('可操作退货表格把操作列固定在最左侧', () => {
|
||||
const createTable = view.slice(view.indexOf('<el-table :data="form.items"'), view.indexOf('</el-table>', view.indexOf('<el-table :data="form.items"')))
|
||||
assert.match(view, /label: '操作', slotName: 'actions', width: 90, fixed: 'left'/)
|
||||
assert.match(view, /label: '操作', slotName: 'actions', width: 220, fixed: 'left'/)
|
||||
assert.match(view, /<DataTable[^>]*:row-actions="rowActions"/)
|
||||
assert.match(createTable, /^<el-table[\s\S]*?<el-table-column label="操作" width="80" fixed="left">/)
|
||||
})
|
||||
@@ -58,3 +67,13 @@ test('退货签收详情使用独立路由和紧凑连续页面', () => {
|
||||
assert.doesNotMatch(view, /showDetail/)
|
||||
assert.doesNotMatch(view, /class="return-detail-dialog"/)
|
||||
})
|
||||
|
||||
test('待审核退货可编辑,完成入库后从独立新增页发起贴标', () => {
|
||||
assert.match(view, /row\.status === 'PENDING_REVIEW'.*key: 'edit'/)
|
||||
assert.match(view, /await updateReturnOrder\(editingReturnNo\.value, payload\)/)
|
||||
assert.match(view, /order\.status === 'COMPLETED' && order\.needRelabel === true && !order\.relabelNo/)
|
||||
assert.match(view, /name: 'overseas-labeling-create'/)
|
||||
assert.match(view, /query: \{ returnNo: order\.returnNo, warehouseId: String\(order\.overseasWarehouseId\) \}/)
|
||||
assert.match(view, /prop: 'relabelNo', label: '换标单号'/)
|
||||
assert.match(view, /label: '换标单状态'/)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ArrowLeft, Plus } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
@@ -8,7 +8,7 @@ import DataTable, { type DataTableColumn } from '@/components/common/DataTable.v
|
||||
import type { TableAction } from '@/components/common/tableActions'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-leg/public'
|
||||
import { createReturnOrder, getReturnOrder, listReturnOrders, type ReturnOrderDetail, type ReturnOrderSummary, type ReturnReceipt } from '../api/returnOrders'
|
||||
import { createReturnOrder, downloadReturnPhoto, getReturnOrder, listReturnOrders, updateReturnOrder, type ReturnOrderDetail, type ReturnOrderSummary, type ReturnReceipt } from '../api/returnOrders'
|
||||
|
||||
const props = withDefaults(defineProps<{ detailPage?: boolean; returnNo?: string }>(), { detailPage: false, returnNo: '' })
|
||||
const router = useRouter()
|
||||
@@ -17,9 +17,12 @@ const detailReturnNo = computed(() => props.returnNo || String(route.params.retu
|
||||
const loading = ref(false)
|
||||
const auth = useAuthStore()
|
||||
const canCreate = computed(() => auth.hasPermission('merchant:overseas-warehouse:return-order:create'))
|
||||
const canCreateRelabel = computed(() => auth.hasPermission('merchant:overseas-warehouse:relabel:create'))
|
||||
const creating = ref(false)
|
||||
const showCreate = ref(false)
|
||||
const editingReturnNo = ref('')
|
||||
const selected = ref<ReturnOrderDetail>()
|
||||
const photoUrls = reactive<Record<string, string>>({})
|
||||
const rows = ref<ReturnOrderSummary[]>([])
|
||||
const total = ref(0)
|
||||
const operationWarehouses = ref<MerchantWarehouse[]>([])
|
||||
@@ -31,14 +34,22 @@ const queryFields: QueryField[] = [
|
||||
{ key: 'returnNo', label: '退货单号', type: 'input', placeholder: '请输入退货单号' },
|
||||
{ key: 'trackingNo', label: '快递单号', type: 'input', placeholder: '请输入快递单号' },
|
||||
{ key: 'skuCode', label: 'SKU', type: 'input', placeholder: '请输入 SKU' },
|
||||
{ key: 'status', label: '签收状态', type: 'select', options: [{ label: '待签收', value: 'PENDING_RECEIPT' }, { label: '部分签收', value: 'PARTIALLY_RECEIVED' }, { label: '已完成', value: 'COMPLETED' }] },
|
||||
{ key: 'status', label: '签收状态', type: 'select', options: [
|
||||
{ label: '待审核', value: 'PENDING_REVIEW' },
|
||||
{ label: '待签收', value: 'PENDING_RECEIPT' },
|
||||
{ label: '部分签收', value: 'PARTIALLY_RECEIVED' },
|
||||
{ label: '已完成入库', value: 'COMPLETED' },
|
||||
{ label: '已驳回', value: 'REJECTED' },
|
||||
{ label: '已取消', value: 'CANCELLED' },
|
||||
] },
|
||||
]
|
||||
const tableColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', slotName: 'actions', width: 90, fixed: 'left' },
|
||||
{ key: 'actions', label: '操作', slotName: 'actions', width: 220, fixed: 'left' },
|
||||
{ prop: 'returnNo', label: '退货单号', minWidth: 190 }, { prop: 'customerReferenceNo', label: '客户参考号', minWidth: 160 },
|
||||
{ key: 'warehouse', label: '操作仓', slotName: 'warehouse', minWidth: 200 }, { key: 'relabel', label: '换标', slotName: 'relabel', width: 100 },
|
||||
{ prop: 'skuTypes', label: 'SKU 种类', width: 105 }, { prop: 'expectedQuantity', label: '预报数量', width: 110 },
|
||||
{ prop: 'receivedQuantity', label: '实收数量', width: 110 }, { key: 'status', label: '状态', slotName: 'status', width: 120 },
|
||||
{ prop: 'receivedQuantity', label: '实收数量', width: 110 }, { key: 'status', label: '状态', slotName: 'status', width: 150 },
|
||||
{ prop: 'relabelNo', label: '换标单号', minWidth: 190 }, { key: 'relabelStatus', label: '换标单状态', slotName: 'relabelStatus', width: 130 },
|
||||
{ key: 'createdAt', label: '创建时间', slotName: 'createdAt', width: 180 },
|
||||
]
|
||||
const itemColumns: DataTableColumn[] = [
|
||||
@@ -50,7 +61,7 @@ const itemColumns: DataTableColumn[] = [
|
||||
const receiptColumns: DataTableColumn[] = [
|
||||
{ prop: 'receiptBatchNo', label: '批次号', minWidth: 170 },
|
||||
{ prop: 'receiptNo', label: '签收单号', minWidth: 170 },
|
||||
{ prop: 'cartonMark', label: '箱号', minWidth: 170 },
|
||||
{ prop: 'palletCode', label: '托盘码', minWidth: 190 },
|
||||
{ key: 'trackingNo', label: '快递单号', slotName: 'trackingNo', minWidth: 160 },
|
||||
{ key: 'startedBy', label: '开始方式', slotName: 'startedBy', width: 110 },
|
||||
{ prop: 'receivedQuantity', label: '实收', width: 90 },
|
||||
@@ -99,10 +110,25 @@ async function loadOperationWarehouses() {
|
||||
} finally { operationWarehousesLoading.value = false }
|
||||
}
|
||||
function openCreate() {
|
||||
editingReturnNo.value = ''
|
||||
resetForm()
|
||||
showCreate.value = true
|
||||
if (!operationWarehouses.value.length && !operationWarehousesLoading.value) void loadOperationWarehouses()
|
||||
}
|
||||
async function openEdit(returnNo: string) {
|
||||
const detail = await getReturnOrder(returnNo)
|
||||
editingReturnNo.value = returnNo
|
||||
Object.assign(form, {
|
||||
overseasWarehouseId: String(detail.order.overseasWarehouseId),
|
||||
warehouseContactName: detail.order.warehouseContactName || '',
|
||||
warehouseContactPhone: detail.order.warehouseContactPhone || '',
|
||||
customerReferenceNo: detail.order.customerReferenceNo || '',
|
||||
remark: detail.remark || '',
|
||||
needRelabel: Boolean(detail.order.needRelabel),
|
||||
items: detail.items.map(item => ({ id: ++skuSequence, skuCode: item.skuCode, productNameZh: item.productNameZh || '', expectedQuantity: item.expectedQuantity })),
|
||||
})
|
||||
showCreate.value = true
|
||||
}
|
||||
function addItem() { form.items.push(emptySku()) }
|
||||
function removeItem(index: number) { if (form.items.length > 1) form.items.splice(index, 1) }
|
||||
async function load() {
|
||||
@@ -118,17 +144,42 @@ function changePage(page: number) {
|
||||
function search() { if (query.page === 1) void load(); else query.page = 1 }
|
||||
function rowActions(candidate: object): TableAction[] {
|
||||
const row = candidate as ReturnOrderSummary
|
||||
return [{ key: 'detail', label: '详情', run: () => openDetail(row.returnNo) }]
|
||||
const actions: TableAction[] = [{ key: 'detail', label: '详情', run: () => openDetail(row.returnNo) }]
|
||||
if (canCreate.value && row.status === 'PENDING_REVIEW') actions.push({ key: 'edit', label: '编辑', run: () => openEdit(row.returnNo) })
|
||||
if (canCreateRelabel.value && canApplyRelabel(row)) actions.push({ key: 'relabel', label: '贴标申请', run: () => openRelabel(row) })
|
||||
return actions
|
||||
}
|
||||
async function loadDetail() {
|
||||
if (!detailReturnNo.value) return
|
||||
loading.value = true
|
||||
try { selected.value = await getReturnOrder(detailReturnNo.value) }
|
||||
clearPhotoUrls()
|
||||
try {
|
||||
selected.value = await getReturnOrder(detailReturnNo.value)
|
||||
await Promise.all((selected.value.photos || []).map(async (photo) => {
|
||||
try {
|
||||
photoUrls[String(photo.fileId)] = URL.createObjectURL(await downloadReturnPhoto(detailReturnNo.value, photo.fileId))
|
||||
} catch {
|
||||
// 单张照片加载失败不阻断详情展示。
|
||||
}
|
||||
}))
|
||||
}
|
||||
catch (error) { ElMessage.error(error instanceof Error ? error.message : '退货签收详情加载失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function clearPhotoUrls() {
|
||||
Object.values(photoUrls).forEach(url => URL.revokeObjectURL(url))
|
||||
Object.keys(photoUrls).forEach(key => delete photoUrls[key])
|
||||
}
|
||||
function previewPhoto(fileId: string | number) {
|
||||
const url = photoUrls[String(fileId)]
|
||||
if (url) window.open(url, '_blank', 'noopener')
|
||||
}
|
||||
function openDetail(returnNo: string) { void router.push({ name: 'overseas-return-relabel-detail', params: { returnNo } }) }
|
||||
function returnToList() { void router.push({ name: 'overseas-return-relabel' }) }
|
||||
function canApplyRelabel(order: { status?: unknown; needRelabel?: unknown; relabelNo?: unknown }) { return order.status === 'COMPLETED' && order.needRelabel === true && !order.relabelNo }
|
||||
function openRelabel(order: Pick<ReturnOrderSummary, 'returnNo' | 'overseasWarehouseId'>) {
|
||||
void router.push({ name: 'overseas-labeling-create', query: { returnNo: order.returnNo, warehouseId: String(order.overseasWarehouseId) } })
|
||||
}
|
||||
async function submit() {
|
||||
const items = form.items.map(({ skuCode, productNameZh, expectedQuantity }) => ({ skuCode: skuCode.trim(), productNameZh: productNameZh.trim(), expectedQuantity }))
|
||||
if (!form.overseasWarehouseId || items.some(item => !item.skuCode || item.expectedQuantity <= 0)) { ElMessage.warning('请填写操作仓、SKU 和预报数量'); return }
|
||||
@@ -136,13 +187,28 @@ async function submit() {
|
||||
if (duplicateSkuIds.value.size) { ElMessage.warning('同一退货单不能重复填写相同 SKU'); return }
|
||||
creating.value = true
|
||||
try {
|
||||
const detail = await createReturnOrder({ overseasWarehouseId: form.overseasWarehouseId, warehouseContactName: form.warehouseContactName.trim(), warehouseContactPhone: form.warehouseContactPhone.trim(), customerReferenceNo: form.customerReferenceNo || undefined, remark: form.remark || undefined, needRelabel: form.needRelabel, idempotencyKey: crypto.randomUUID(), items: items.map(item => ({ ...item, productNameZh: item.productNameZh || undefined })) })
|
||||
ElMessage.success('退货预报已创建'); showCreate.value = false; resetForm(); await router.push({ name: 'overseas-return-relabel-detail', params: { returnNo: detail.order.returnNo } })
|
||||
const payload = { overseasWarehouseId: form.overseasWarehouseId, warehouseContactName: form.warehouseContactName.trim(), warehouseContactPhone: form.warehouseContactPhone.trim(), customerReferenceNo: form.customerReferenceNo || undefined, remark: form.remark || undefined, needRelabel: form.needRelabel, idempotencyKey: crypto.randomUUID(), items: items.map(item => ({ ...item, productNameZh: item.productNameZh || undefined })) }
|
||||
const detail = editingReturnNo.value ? await updateReturnOrder(editingReturnNo.value, payload) : await createReturnOrder(payload)
|
||||
ElMessage.success(editingReturnNo.value ? '退货预报已更新' : '退货预报已创建'); showCreate.value = false; editingReturnNo.value = ''; resetForm(); await router.push({ name: 'overseas-return-relabel-detail', params: { returnNo: detail.order.returnNo } })
|
||||
} finally { creating.value = false }
|
||||
}
|
||||
function statusType(status: string) { return status === 'COMPLETED' ? 'success' : status === 'PARTIALLY_RECEIVED' ? 'warning' : 'info' }
|
||||
function statusLabel(status: string) {
|
||||
return ({ PENDING_RECEIPT: '待签收', PARTIALLY_RECEIVED: '部分签收', COMPLETED: '已完成' } as Record<string, string>)[status] ?? '未知状态'
|
||||
function statusType(status: string, needRelabel = false) {
|
||||
if (status === 'COMPLETED') return needRelabel ? 'warning' : 'success'
|
||||
return ({ PENDING_REVIEW: 'warning', PENDING_RECEIPT: 'primary', PARTIALLY_RECEIVED: 'warning', REJECTED: 'danger', CANCELLED: 'info' } as Record<string, 'primary' | 'success' | 'warning' | 'danger' | 'info'>)[status] || 'info'
|
||||
}
|
||||
function statusClass(status: string, needRelabel = false) {
|
||||
if (status === 'COMPLETED') return needRelabel ? 'return-status--relabel' : 'return-status--completed'
|
||||
return `return-status--${status.toLowerCase().replaceAll('_', '-')}`
|
||||
}
|
||||
function statusLabel(status: string, needRelabel = false) {
|
||||
if (status === 'COMPLETED') return needRelabel ? '已完成入库/待换标' : '已完成入库'
|
||||
return ({ PENDING_REVIEW: '待审核', PENDING_RECEIPT: '待签收', PARTIALLY_RECEIVED: '部分签收', REJECTED: '已驳回', CANCELLED: '已取消' } as Record<string, string>)[status] ?? (status || '未知状态')
|
||||
}
|
||||
function relabelStatusType(status?: string) {
|
||||
return ({ PENDING_REVIEW: 'warning', PENDING_PROCESSING: 'primary', PROCESSING: 'primary', CUSTOMER_CONFIRM_PENDING: 'warning', COMPLETED: 'success', REJECTED: 'danger', CANCELLED: 'info' } as Record<string, 'primary' | 'success' | 'warning' | 'danger' | 'info'>)[status || ''] || 'info'
|
||||
}
|
||||
function relabelStatusLabel(status?: string) {
|
||||
return ({ PENDING_REVIEW: '待审核', PENDING_PROCESSING: '待处理', PROCESSING: '处理中', CUSTOMER_CONFIRM_PENDING: '待客户确认', COMPLETED: '已完成', REJECTED: '已驳回', CANCELLED: '已取消' } as Record<string, string>)[status || ''] ?? (status || '—')
|
||||
}
|
||||
function startedByLabel(startedBy: ReturnReceipt['startedBy']) { return startedBy === 'SKU' ? 'SKU 查询' : '退货单号' }
|
||||
function formatDateTime(value?: string | null) {
|
||||
@@ -159,6 +225,7 @@ watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousP
|
||||
if (nextSize !== previousSize && nextPage !== 1) { query.page = 1; return }
|
||||
if (!props.detailPage) void load()
|
||||
})
|
||||
onBeforeUnmount(clearPhotoUrls)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -170,12 +237,13 @@ watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousP
|
||||
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新建退货预报</el-button></template>
|
||||
<template #warehouse="{ row }">{{ operationWarehouseLabel(row.overseasWarehouseId) }}</template>
|
||||
<template #relabel="{ row }"><el-tag v-if="row.needRelabel" type="warning" effect="light">需要换标</el-tag><span v-else>否</span></template>
|
||||
<template #status="{ row }"><el-tag :type="statusType(row.status)" effect="light">{{ statusLabel(row.status) }}</el-tag></template>
|
||||
<template #status="{ row }"><el-tag :type="statusType(row.status, row.needRelabel)" :class="statusClass(row.status, row.needRelabel)" effect="light">{{ statusLabel(row.status, row.needRelabel) }}</el-tag></template>
|
||||
<template #relabelStatus="{ row }"><el-tag v-if="row.relabelStatus" :type="relabelStatusType(row.relabelStatus)" effect="light">{{ relabelStatusLabel(row.relabelStatus) }}</el-tag><span v-else>—</span></template>
|
||||
<template #createdAt="{ row }">{{ formatDateTime(row.createdAt) }}</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="showCreate" title="新建退货预报" width="1120px" destroy-on-close>
|
||||
<el-dialog v-model="showCreate" :title="editingReturnNo ? '编辑退货预报' : '新建退货预报'" width="1120px" destroy-on-close>
|
||||
<el-form label-width="190px" class="return-create-form">
|
||||
<el-form-item label="海外仓" required class="return-form-wide"><el-select v-model="form.overseasWarehouseId" filterable clearable :loading="operationWarehousesLoading" :disabled="Boolean(operationWarehousesError)" placeholder="请选择海外仓" @change="selectOperationWarehouse"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><div v-if="operationWarehousesError" class="warehouse-error">{{ operationWarehousesError }}</div></el-form-item>
|
||||
<el-form-item label="海外仓联系人" required><el-input v-model="form.warehouseContactName" :readonly="Boolean(selectedOperationWarehouse?.contactName)" :placeholder="selectedOperationWarehouse?.contactName ? '已从海外仓资料带出' : '该海外仓未维护,请手动填写'" /></el-form-item>
|
||||
@@ -192,7 +260,7 @@ watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousP
|
||||
<el-table-column label="中文品名" min-width="260"><template #default="{ row }"><el-input v-model="row.productNameZh" placeholder="填写中文品名(选填)" /></template></el-table-column>
|
||||
<el-table-column label="预报数量" width="160"><template #default="{ row }"><el-input-number v-model="row.expectedQuantity" :min="1" controls-position="right" placeholder="数量" /></template></el-table-column>
|
||||
</el-table>
|
||||
<template #footer><el-button @click="showCreate=false">取消</el-button><el-button v-if="canCreate" type="primary" :loading="creating" @click="submit">提交预报</el-button></template>
|
||||
<template #footer><el-button @click="showCreate=false; editingReturnNo=''">取消</el-button><el-button v-if="canCreate" type="primary" :loading="creating" @click="submit">{{ editingReturnNo ? '保存修改' : '提交预报' }}</el-button></template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
@@ -201,7 +269,7 @@ watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousP
|
||||
<header class="return-detail-header">
|
||||
<button type="button" class="return-back" @click="returnToList"><ArrowLeft :size="16" />返回列表</button>
|
||||
<div class="return-detail-title"><strong>{{ selected.order.returnNo }}</strong><span>{{ operationWarehouseLabel(selected.order.overseasWarehouseId) }} · 退货签收</span></div>
|
||||
<el-tag :type="statusType(selected.order.status)" effect="light">{{ statusLabel(selected.order.status) }}</el-tag>
|
||||
<div class="return-detail-actions"><el-button v-if="canCreateRelabel && canApplyRelabel(selected.order)" type="primary" plain @click="openRelabel(selected.order)">贴标申请</el-button><el-tag :type="statusType(selected.order.status, selected.order.needRelabel)" :class="statusClass(selected.order.status, selected.order.needRelabel)" effect="light">{{ statusLabel(selected.order.status, selected.order.needRelabel) }}</el-tag></div>
|
||||
</header>
|
||||
<section class="return-detail-section">
|
||||
<h3>签收概览</h3>
|
||||
@@ -209,6 +277,8 @@ watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousP
|
||||
<div><span>实收 / 预报</span><strong>{{ selected.order.receivedQuantity }} / {{ selected.order.expectedQuantity }}</strong></div>
|
||||
<div><span>SKU 种类</span><strong>{{ selected.order.skuTypes }}</strong></div>
|
||||
<div><span>是否换标</span><strong>{{ selected.order.needRelabel ? '需要换标' : '否' }}</strong></div>
|
||||
<div><span>换标单号</span><strong>{{ selected.order.relabelNo || '—' }}</strong></div>
|
||||
<div><span>换标单状态</span><strong>{{ relabelStatusLabel(selected.order.relabelStatus) }}</strong></div>
|
||||
<div><span>客户参考号</span><strong>{{ selected.order.customerReferenceNo || '—' }}</strong></div>
|
||||
<div><span>海外仓联系人</span><strong>{{ selected.order.warehouseContactName || '—' }}</strong></div>
|
||||
<div><span>联系电话</span><strong>{{ selected.order.warehouseContactPhone || '—' }}</strong></div>
|
||||
@@ -220,6 +290,31 @@ watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousP
|
||||
<h3>预报与实收</h3>
|
||||
<DataTable :data="selected.items" :columns="itemColumns" row-key="skuCode" empty-text="暂无 SKU 明细" />
|
||||
</section>
|
||||
<section class="return-detail-section">
|
||||
<h3>打托明细({{ selected.pallets?.length || 0 }} 托)</h3>
|
||||
<el-collapse v-if="selected.pallets?.length" class="return-pallet-list">
|
||||
<el-collapse-item v-for="pallet in selected.pallets" :key="pallet.palletCode" :name="pallet.palletCode">
|
||||
<template #title><strong>{{ pallet.palletCode }}</strong><span class="return-pallet-summary">{{ pallet.skuTypes }} 种 SKU · {{ pallet.totalQuantity }} 件</span></template>
|
||||
<el-table :data="pallet.items" size="small" border>
|
||||
<el-table-column prop="skuCode" label="SKU" min-width="180" />
|
||||
<el-table-column prop="productNameZh" label="中文品名" min-width="220"><template #default="{ row }">{{ row.productNameZh || '—' }}</template></el-table-column>
|
||||
<el-table-column prop="quantity" label="数量" width="100" />
|
||||
</el-table>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<el-empty v-else description="暂无打托记录" :image-size="64" />
|
||||
</section>
|
||||
<section class="return-detail-section">
|
||||
<h3>退货打托照片({{ selected.photos?.length || 0 }} 张)</h3>
|
||||
<div v-if="selected.photos?.length" class="return-photo-grid">
|
||||
<button v-for="photo in selected.photos" :key="photo.fileId" class="return-photo" type="button" @click="previewPhoto(photo.fileId)">
|
||||
<img v-if="photoUrls[String(photo.fileId)]" :src="photoUrls[String(photo.fileId)]" :alt="photo.fileName">
|
||||
<span v-else>照片加载失败</span>
|
||||
<small>{{ photo.fileName }}</small>
|
||||
</button>
|
||||
</div>
|
||||
<el-empty v-else description="暂无退货打托照片" :image-size="64" />
|
||||
</section>
|
||||
<section class="return-detail-section">
|
||||
<h3>签收批次</h3>
|
||||
<DataTable :data="selected.receipts" :columns="receiptColumns" row-key="receiptNo" empty-text="暂无签收批次">
|
||||
@@ -237,6 +332,13 @@ watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousP
|
||||
.return-orders-page { display:grid; gap:12px; } .return-create-form { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); column-gap:20px; } .return-form-wide { grid-column:1 / -1; } .items-title { display:flex; justify-content:space-between; align-items:center; margin:16px 0 4px; font-weight:600; } .items-hint { margin-bottom:10px; color:var(--el-text-color-secondary); font-size:12px; } h3 { margin:24px 0 10px; font-size:16px; } h4 { margin:16px 0 8px; font-size:14px; } .remark { white-space:pre-wrap; } .warehouse-error, .duplicate-sku-error { margin-top:6px; color:var(--el-color-danger); font-size:12px; } :deep(.duplicate-sku-input .el-input__wrapper) { box-shadow:0 0 0 1px var(--el-color-danger) inset; } :deep(.el-dialog .el-select) { width:100%; }
|
||||
.return-table-card { overflow:hidden; border:1px solid var(--client-border); border-radius:var(--client-radius); background:#fff; box-shadow:none; }
|
||||
.return-table-card :deep(.el-table) { width:100%; }
|
||||
.return-status--pending-review { --el-tag-bg-color:#fff7e6; --el-tag-border-color:#f4c26b; --el-tag-text-color:#a86100; }
|
||||
.return-status--pending-receipt { --el-tag-bg-color:#edf5ff; --el-tag-border-color:#8bbcff; --el-tag-text-color:#1764c0; }
|
||||
.return-status--partially-received { --el-tag-bg-color:#f5efff; --el-tag-border-color:#b99aee; --el-tag-text-color:#6f42b5; }
|
||||
.return-status--relabel { --el-tag-bg-color:#fff1e8; --el-tag-border-color:#f1a36f; --el-tag-text-color:#bd4d08; }
|
||||
.return-status--completed { --el-tag-bg-color:#edf9f0; --el-tag-border-color:#8bd29b; --el-tag-text-color:#23823b; }
|
||||
.return-status--rejected { --el-tag-bg-color:#fff0f0; --el-tag-border-color:#ee9696; --el-tag-text-color:#c73535; }
|
||||
.return-status--cancelled { --el-tag-bg-color:#f2f4f7; --el-tag-border-color:#c7cdd6; --el-tag-text-color:#667085; }
|
||||
.return-pagination { display:flex; justify-content:flex-end; padding:14px 18px; border-top:1px solid #eef1f5; }
|
||||
.return-pagination :deep(.el-pagination) { margin:0; }
|
||||
.return-pagination-total { color:#526176; font-size:14px; }
|
||||
@@ -248,6 +350,7 @@ watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousP
|
||||
.return-detail-title { display:flex; min-width:0; flex-direction:column; gap:3px; }
|
||||
.return-detail-title strong { overflow:hidden; color:#1f2d45; font-size:18px; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.return-detail-title span { color:#7c899c; font-size:12px; }
|
||||
.return-detail-actions { display:flex; align-items:center; gap:8px; }
|
||||
.return-detail-section + .return-detail-section { border-top:1px solid var(--client-border); }
|
||||
.return-detail-section > h3 { min-height:40px; margin:0; padding:11px 16px 9px; color:#26364f; font-size:14px; }
|
||||
.return-detail-grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); border-top:1px solid #edf1f5; }
|
||||
@@ -256,7 +359,14 @@ watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousP
|
||||
.return-detail-grid strong,.return-detail-remark strong { min-width:0; overflow-wrap:anywhere; color:#2c3c55; font-size:13px; }
|
||||
.return-detail-remark { display:flex; align-items:flex-start; gap:10px; padding:10px 14px; background:#f8fafc; }
|
||||
.return-detail-section :deep(.data-table) { border:0; border-radius:0; }
|
||||
.return-pallet-list { padding:0 16px 12px; }
|
||||
.return-pallet-summary { margin-left:12px; color:#7c899c; font-size:12px; font-weight:400; }
|
||||
.return-photo-grid { display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:12px; padding:0 16px 16px; }
|
||||
.return-photo { min-width:0; overflow:hidden; padding:0; border:1px solid #dfe6ef; border-radius:6px; color:#526176; background:#fff; cursor:pointer; }
|
||||
.return-photo img,.return-photo>span { display:flex; width:100%; height:120px; align-items:center; justify-content:center; object-fit:cover; background:#f4f7fb; }
|
||||
.return-photo small { display:block; overflow:hidden; padding:7px; text-overflow:ellipsis; white-space:nowrap; }
|
||||
@media (max-width: 760px) { .return-create-form { grid-template-columns:1fr; } }
|
||||
@media (max-width: 1100px) { .return-detail-grid { grid-template-columns:repeat(2,minmax(0,1fr)); } }
|
||||
@media (max-width: 720px) { .return-detail-header { grid-template-columns:1fr auto; } .return-back { grid-column:1/-1; } .return-pagination { padding:12px; } }
|
||||
@media (max-width: 900px) { .return-photo-grid { grid-template-columns:repeat(3,minmax(0,1fr)); } }
|
||||
@media (max-width: 720px) { .return-detail-header { grid-template-columns:1fr auto; } .return-back { grid-column:1/-1; } .return-pagination { padding:12px; } .return-photo-grid { grid-template-columns:repeat(2,minmax(0,1fr)); } }
|
||||
</style>
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
type MerchantWarehouseDetail,
|
||||
} from '@/modules/first-leg/api/firstLeg'
|
||||
import { loadAllWarehouseRecords } from '@/modules/first-leg/utils/warehousePagination'
|
||||
import { createReturnRelabel, uploadReturnRelabelFile } from '../api/returnOrders'
|
||||
|
||||
type ServiceType = 'labeling' | 'transfer' | 'disposal' | 'relabel'
|
||||
type PageMode = 'list' | 'create' | 'edit' | 'detail'
|
||||
@@ -33,6 +34,7 @@ type RelabelTemplateType = 'carton' | 'sku' | 'mixed'
|
||||
interface CargoRow { id: number; included?: boolean; sku: string; name: string; quantity: number; availableQuantity?: number; sourceOrderId?: string; sourceInventoryBalanceId?: string; sourceInventoryPositionId?: string; sourceInboundOrderId?: string; returnCartonId?: string; sourceInboundOrderNo?: string; sourceInboundCode?: string; sourceWarehouseName?: string; palletNo?: string; warehouseLocationCode?: string; fnsku: string; shipmentNo: string; newSkuCode: string; shippingMark: string; customerCartonMarks: string; cartonMark: string; fbaCartonMark: string; destinationWarehouseId: string; remark: string }
|
||||
interface SkuRelabelRow { id: number; sourceRowId: number | null; cartonMark: string; originalSku: string; newSku: string; quantity: number; fbaCartonMark: string; destinationWarehouseId: string; remark: string }
|
||||
interface ImportedRelabelBinding extends SkuRelabelRow { templateType: RelabelTemplateType; selected: boolean; shipmentNo: string }
|
||||
interface ReturnRelabelSkuRow { skuCode: string; productNameZh: string; availableQuantity: number; selected: boolean; quantity: number }
|
||||
interface ServiceConfig { title: string; createTitle: string; numberLabel: string; emptyText: string; columns: string[] }
|
||||
interface TransferCarton { cartonMark: string; customerCartonMark?: string; cartonIndex: number; totalCartons: number; status: string }
|
||||
|
||||
@@ -100,11 +102,6 @@ const shippingLabelFile = ref<File | null>(null)
|
||||
const skuRelabelRows = ref<SkuRelabelRow[]>([])
|
||||
const importedRelabelBindings = ref<ImportedRelabelBinding[]>([])
|
||||
const cartonDetailRows = computed(() => cargoRows.value.filter((row) => row.included))
|
||||
const labelingModeHint = computed(() => labelingMode.value === 'carton'
|
||||
? '只替换外箱箱唛,不修改箱内商品 SKU 标签;整张申请统一使用订单信息中的 FBA 目的仓。'
|
||||
: labelingMode.value === 'sku'
|
||||
? '只替换箱内商品 SKU 标签,保留原箱唛;同一箱唛可以添加多条 SKU 明细。'
|
||||
: '同一箱同时替换商品 SKU 标签和外箱箱唛;整张申请统一使用订单信息中的 FBA 目的仓。')
|
||||
const submittedRows = ref<Record<string, string>[]>([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
@@ -168,17 +165,27 @@ const discardCandidatesLoading = ref(false)
|
||||
const relabelCandidates = ref<RelabelInventoryCandidate[]>([])
|
||||
const selectedRelabelInboundOrderId = ref('')
|
||||
const relabelCandidatesLoading = ref(false)
|
||||
const returnRelabelSkuRows = ref<ReturnRelabelSkuRow[]>([])
|
||||
const isReturnRelabelSource = computed(() => props.service === 'labeling' && selectedRelabelInboundOrderId.value.startsWith('RETURN_ORDER:'))
|
||||
const selectedReturnOrderNo = computed(() => isReturnRelabelSource.value ? selectedRelabelInboundOrderId.value.slice('RETURN_ORDER:'.length) : '')
|
||||
const requiresBothTotalFiles = computed(() => (props.service === 'labeling' || (props.service === 'transfer' && form.relabel)) && !isReturnRelabelSource.value && labelingMode.value === 'carton_and_sku')
|
||||
function inventoryQuantity(value: number | string | null | undefined) {
|
||||
const quantity = Number(value)
|
||||
return Number.isFinite(quantity) && quantity > 0 ? quantity : 0
|
||||
}
|
||||
const relabelInboundOptions = computed(() => {
|
||||
const grouped = new Map<string, { value: string; inboundOrderNo: string; inboundCode: string; availablePackages: number }>()
|
||||
const grouped = new Map<string, { value: string; inboundOrderNo: string; inboundCode: string; availablePackages: number; quantityUnit: '箱' | '件' }>()
|
||||
for (const candidate of relabelCandidates.value) {
|
||||
const value = candidate.sourceType === 'RETURN_CARTON' ? `RETURN_CARTON:${candidate.returnCartonId}` : String(candidate.inboundOrderId)
|
||||
const returnOrderNo = candidate.inboundOrderNo || String(candidate.returnCartonId || '')
|
||||
const value = candidate.sourceType === 'RETURN_CARTON' ? `RETURN_ORDER:${returnOrderNo}` : String(candidate.inboundOrderId)
|
||||
const current = grouped.get(value)
|
||||
if (current) current.availablePackages += candidate.availablePackages
|
||||
if (current) current.availablePackages += inventoryQuantity(candidate.availablePackages)
|
||||
else grouped.set(value, {
|
||||
value,
|
||||
inboundOrderNo: candidate.sourceType === 'RETURN_CARTON' ? `退货合箱 ${candidate.cartonMark || '—'}` : (candidate.inboundOrderNo || '—'),
|
||||
inboundCode: candidate.sourceType === 'RETURN_CARTON' ? '仓库唛头库存' : (candidate.inboundCode || '—'),
|
||||
availablePackages: candidate.availablePackages,
|
||||
inboundOrderNo: candidate.sourceType === 'RETURN_CARTON' ? `退货单 ${candidate.inboundOrderNo || '—'}` : (candidate.inboundOrderNo || '—'),
|
||||
inboundCode: candidate.sourceType === 'RETURN_CARTON' ? '已签收退货库存' : (candidate.inboundCode || '—'),
|
||||
availablePackages: inventoryQuantity(candidate.availablePackages),
|
||||
quantityUnit: candidate.sourceType === 'RETURN_CARTON' ? '件' : '箱',
|
||||
})
|
||||
}
|
||||
return [...grouped.values()]
|
||||
@@ -256,14 +263,32 @@ function resetForm() {
|
||||
selectedDiscardOrderId.value = ''
|
||||
relabelCandidates.value = []
|
||||
selectedRelabelInboundOrderId.value = ''
|
||||
cartonTotalFile.value = null
|
||||
skuTotalFile.value = null
|
||||
shippingLabelFile.value = null
|
||||
if (cartonTotalCreateInput.value) cartonTotalCreateInput.value.value = ''
|
||||
if (skuTotalCreateInput.value) skuTotalCreateInput.value.value = ''
|
||||
transferDestinationType.value = 'NONE'
|
||||
selectedOperationWarehouse.value = null
|
||||
editingTransferNo.value = ''
|
||||
}
|
||||
async function openCreate() {
|
||||
resetForm(); mode.value = 'create'
|
||||
if (props.service === 'labeling' || props.service === 'relabel' || props.service === 'transfer') void loadWarehouses()
|
||||
if (props.service === 'labeling') return
|
||||
if (props.service === 'labeling' || props.service === 'relabel' || props.service === 'transfer') await loadWarehouses()
|
||||
if (props.service === 'labeling') {
|
||||
const requestedReturnNo = String(route.query.returnNo || '').trim()
|
||||
const requestedWarehouseId = String(route.query.warehouseId || '').trim()
|
||||
if (requestedReturnNo && requestedWarehouseId && operationWarehouses.value.some(warehouse => String(warehouse.id) === requestedWarehouseId)) {
|
||||
form.warehouse = requestedWarehouseId
|
||||
await selectOperationWarehouse()
|
||||
const sourceValue = `RETURN_ORDER:${requestedReturnNo}`
|
||||
if (relabelInboundOptions.value.some(option => option.value === sourceValue)) {
|
||||
selectedRelabelInboundOrderId.value = sourceValue
|
||||
selectRelabelInventory(sourceValue)
|
||||
} else ElMessage.warning(`退货单 ${requestedReturnNo} 暂无可申请贴标的库存`)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (props.service !== 'disposal') { addCargoRow(); return }
|
||||
}
|
||||
|
||||
@@ -336,6 +361,36 @@ async function selectDiscardWarehouse() {
|
||||
}
|
||||
function createCargoRow(): CargoRow { return { id: ++cargoSequence, sku: '', name: '', quantity: 1, fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', customerCartonMarks: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' } }
|
||||
function addCargoRow() { cargoRows.value.push(createCargoRow()) }
|
||||
function escapePrintHtml(value: string) {
|
||||
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''')
|
||||
}
|
||||
function printTransferDraftCartonMarks() {
|
||||
if (props.service !== 'transfer') return
|
||||
const validRows = cargoRows.value.filter((row) => row.sku.trim() && Number(row.quantity) > 0)
|
||||
if (!validRows.length || validRows.some((row) => !Number.isInteger(Number(row.quantity)))) {
|
||||
ElMessage.warning('请先填写商品 SKU 和对应的整数箱数'); return
|
||||
}
|
||||
const totalCartons = validRows.reduce((sum, row) => sum + Number(row.quantity), 0)
|
||||
if (totalCartons > 500) { ElMessage.warning('单次最多打印 500 张箱唛'); return }
|
||||
const batch = new Date().toISOString().replace(/\D/g, '').slice(2, 14)
|
||||
const labels: Array<{ mark: string; sku: string; index: number }> = []
|
||||
let cartonIndex = 0
|
||||
for (const [rowIndex, row] of validRows.entries()) {
|
||||
let marks = transferCustomerMarks(row)
|
||||
if (marks.length !== Number(row.quantity)) {
|
||||
marks = Array.from({ length: Number(row.quantity) }, (_, index) => `CT-${batch}-${String(rowIndex + 1).padStart(2, '0')}-${String(index + 1).padStart(3, '0')}`)
|
||||
row.customerCartonMarks = marks.join('\n')
|
||||
}
|
||||
for (const mark of marks) labels.push({ mark, sku: row.sku.trim(), index: ++cartonIndex })
|
||||
}
|
||||
const printWindow = window.open('', '_blank')
|
||||
if (!printWindow) { ElMessage.warning('浏览器阻止了打印窗口,请允许弹窗后重试'); return }
|
||||
printWindow.opener = null
|
||||
const sections = labels.map((label) => `<section class="label"><div class="title">仓储转运客户箱唛</div><div class="mark">${escapePrintHtml(label.mark)}</div><div class="sku">SKU:${escapePrintHtml(label.sku)}</div><div class="meta">${label.index} / ${labels.length}</div></section>`).join('')
|
||||
printWindow.document.open()
|
||||
printWindow.document.write(`<!doctype html><html><head><meta charset="utf-8"><title>仓储转运客户箱唛</title><style>@page{size:100mm 100mm;margin:8mm}body{margin:0;font-family:Arial,'Microsoft YaHei',sans-serif;color:#111}.label{height:84mm;display:flex;flex-direction:column;align-items:center;justify-content:center;page-break-after:always;border:1px solid #222;box-sizing:border-box;padding:8mm}.title{font-size:18px;font-weight:700}.mark{margin:11mm 0 6mm;font-size:26px;font-weight:700;letter-spacing:1px;word-break:break-all;text-align:center}.sku{font-size:17px;font-weight:650}.meta{font-size:12px;margin-top:4mm}</style></head><body>${sections}</body></html>`)
|
||||
printWindow.document.close(); printWindow.focus(); printWindow.print()
|
||||
}
|
||||
function addTransferRelabelRow() { transferRelabelRows.value.push(createCargoRow()) }
|
||||
function clearTransferRelabelRows() {
|
||||
transferRelabelRows.value = []
|
||||
@@ -364,8 +419,14 @@ function applyExistingCartonBinding(row: SkuRelabelRow) {
|
||||
if (!row.destinationWarehouseId) row.destinationWarehouseId = mapping.destinationWarehouseId
|
||||
}
|
||||
function selectShippingLabel(event: Event) { shippingLabelFile.value = (event.target as HTMLInputElement).files?.[0] || null }
|
||||
function selectCartonTotalFile(event: Event) { cartonTotalFile.value = (event.target as HTMLInputElement).files?.[0] || null }
|
||||
function selectSkuTotalFile(event: Event) { skuTotalFile.value = (event.target as HTMLInputElement).files?.[0] || null }
|
||||
function selectCartonTotalFile(event: Event) {
|
||||
cartonTotalFile.value = (event.target as HTMLInputElement).files?.[0] || null
|
||||
if (cartonTotalFile.value && !requiresBothTotalFiles.value) clearCreateTotalFile('SKU_TOTAL')
|
||||
}
|
||||
function selectSkuTotalFile(event: Event) {
|
||||
skuTotalFile.value = (event.target as HTMLInputElement).files?.[0] || null
|
||||
if (skuTotalFile.value && !requiresBothTotalFiles.value) clearCreateTotalFile('CARTON_TOTAL')
|
||||
}
|
||||
function clearCreateTotalFile(scope: 'CARTON_TOTAL' | 'SKU_TOTAL') {
|
||||
if (saving.value) return
|
||||
const file = scope === 'CARTON_TOTAL' ? cartonTotalFile : skuTotalFile
|
||||
@@ -662,7 +723,8 @@ function warehouseDisplay(value?: string) {
|
||||
function statusLabel(value: string) {
|
||||
if (props.service === 'labeling') {
|
||||
if (value === 'PROCESSING') return '处理中'
|
||||
return ['CUSTOMER_CONFIRM_PENDING', 'READY_FOR_OUTBOUND', 'COMPLETED'].includes(value) ? '换标完成' : '待处理'
|
||||
if (value === 'CUSTOMER_CONFIRM_PENDING') return '待客户确认'
|
||||
return ['READY_FOR_OUTBOUND', 'COMPLETED'].includes(value) ? '换标完成' : '待处理'
|
||||
}
|
||||
if (props.service === 'transfer') {
|
||||
const transferLabels: Record<string, string> = {
|
||||
@@ -736,11 +798,41 @@ function listRowActions(candidate: object): TableAction[] {
|
||||
if (canConfirmRelabel.value && props.service === 'labeling' && item.__status === 'CUSTOMER_CONFIRM_PENDING' && !item.__confirmedAt) actions.push({ key: 'confirm-relabel', label: '确认换标', run: () => confirmRelabelFromList(item['0']) })
|
||||
return actions
|
||||
}
|
||||
async function submitReturnRelabel() {
|
||||
const items = returnRelabelSkuRows.value.filter(item => item.selected)
|
||||
.map(item => ({ skuCode: item.skuCode, quantity: Number(item.quantity) }))
|
||||
if (!selectedReturnOrderNo.value || !items.length) { ElMessage.warning('请至少选择一个需要贴标的 SKU'); return }
|
||||
if (items.some(item => item.quantity < 1 || item.quantity > (returnRelabelSkuRows.value.find(row => row.skuCode === item.skuCode)?.availableQuantity || 0))) {
|
||||
ElMessage.warning('本次贴标数量必须大于 0,且不能超过该 SKU 的库存总数'); return
|
||||
}
|
||||
const destination = fbaWarehouses.value.find(warehouse => warehouse.warehouseCode === form.destination)
|
||||
if (!destination) { ElMessage.warning('请选择 FBA 目的仓'); return }
|
||||
const selectedFile = cartonTotalFile.value || skuTotalFile.value
|
||||
const fileScope = cartonTotalFile.value ? 'CARTON_TOTAL' as const : 'SKU_TOTAL' as const
|
||||
if (!selectedFile) { ElMessage.warning('请上传箱唛总文件或 SKU 总文件(二选一)'); return }
|
||||
try { await ElMessageBox.confirm(`确认提交退货单 ${selectedReturnOrderNo.value} 的贴标申请吗?`, '提交确认', { confirmButtonText: '提交', cancelButtonText: '取消' }) } catch { return }
|
||||
saving.value = true
|
||||
try {
|
||||
const created = await createReturnRelabel(selectedReturnOrderNo.value, {
|
||||
items,
|
||||
fileScope,
|
||||
destinationFbaWarehouseId: String(destination.id),
|
||||
destinationFbaWarehouseCode: destination.warehouseCode,
|
||||
cartonRemark: form.remark.trim() || undefined,
|
||||
idempotencyKey: crypto.randomUUID(),
|
||||
})
|
||||
await uploadReturnRelabelFile(created.relabelNo, fileScope, selectedFile)
|
||||
ElMessage.success(`贴标申请 ${created.relabelNo} 已提交,等待审核`)
|
||||
await router.push({ name: 'overseas-labeling' })
|
||||
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '退货贴标申请提交失败,请重试') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
watch([page, pageSize], ([nextPage, nextSize], [previousPage, previousSize]) => {
|
||||
if (nextSize !== previousSize && nextPage !== 1) { page.value = 1; return }
|
||||
void load()
|
||||
})
|
||||
async function submit(action: 'draft' | 'submit' = 'submit') {
|
||||
if (isReturnRelabelSource.value) { await submitReturnRelabel(); return }
|
||||
const primaryWarehouse = props.service === 'relabel' ? form.sourceWarehouse : form.warehouse
|
||||
const relabelRows = labelingMode.value === 'carton' ? cartonDetailRows.value : skuRelabelRows.value
|
||||
const invalidCargo = props.service === 'disposal'
|
||||
@@ -757,6 +849,12 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
|
||||
if (props.service === 'labeling' && (!form.labelDeadline || !form.contact || !form.phone)) {
|
||||
ElMessage.warning('请填写贴标截止时间和海外仓联系人信息'); return
|
||||
}
|
||||
if (props.service === 'labeling' && requiresBothTotalFiles.value && (!cartonTotalFile.value || !skuTotalFile.value)) {
|
||||
ElMessage.warning('SKU 和箱唛都换时,必须同时上传箱唛总文件和 SKU 总文件'); return
|
||||
}
|
||||
if (props.service === 'labeling' && !requiresBothTotalFiles.value && !cartonTotalFile.value && !skuTotalFile.value) {
|
||||
ElMessage.warning('请上传箱唛总文件或 SKU 总文件(二选一)'); return
|
||||
}
|
||||
if (props.service === 'labeling' && labelingMode.value !== 'sku' && !form.deliveryMethod) {
|
||||
ElMessage.warning('请选择换标完成后的派送方式'); return
|
||||
}
|
||||
@@ -792,6 +890,8 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
|
||||
if (props.service === 'transfer' && form.relabel) {
|
||||
if (!form.labelDeadline || !form.relabelContact || !form.relabelPhone || (labelingMode.value !== 'sku' && !form.relabelDeliveryMethod)) { ElMessage.warning('请填写换标截止时间、换标联系人和换标后派送方式'); return }
|
||||
if (!transferRelabelDefaultDestination.value) { ElMessage.warning('请选择整票换标货物的 FBA 目的仓'); return }
|
||||
if (action === 'submit' && requiresBothTotalFiles.value && (!cartonTotalFile.value || !skuTotalFile.value)) { ElMessage.warning('SKU 和箱唛都换时,必须同时上传箱唛总文件和 SKU 总文件'); return }
|
||||
if (action === 'submit' && !requiresBothTotalFiles.value && !cartonTotalFile.value && !skuTotalFile.value) { ElMessage.warning('请上传箱唛总文件或 SKU 总文件(二选一)'); return }
|
||||
const relabelRows = transferRelabelRows.value
|
||||
const allMarks = relabelRows.flatMap(transferCustomerMarks)
|
||||
if (!relabelRows.length || relabelRows.some((row) => transferCustomerMarks(row).length !== row.quantity)) { ElMessage.warning('每个换标明细的客户箱唛数量必须与换标数量一致'); return }
|
||||
@@ -848,7 +948,19 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
|
||||
const transferNo = String(created.transferNo || '')
|
||||
if (!transferNo) throw new Error('转运草稿已保存,但未返回转运单号')
|
||||
if (action === 'submit') {
|
||||
await submitTransfer(transferNo)
|
||||
const submitted = await submitTransfer(transferNo)
|
||||
if (form.relabel) {
|
||||
const relabelNo = String(submitted.relabelNo || created.relabelNo || '')
|
||||
try {
|
||||
if (!relabelNo) throw new Error('转运单已提交,但未返回关联换标单号')
|
||||
if (cartonTotalFile.value) await uploadRelabelFile(relabelNo, 'CARTON_TOTAL', cartonTotalFile.value)
|
||||
if (skuTotalFile.value) await uploadRelabelFile(relabelNo, 'SKU_TOTAL', skuTotalFile.value)
|
||||
} catch (error) {
|
||||
await ElMessageBox.alert(`转运单 ${transferNo} 已提交,但换标总文件上传失败:${error instanceof Error ? error.message : '上传失败'}。请进入关联换标单详情补传,不要重复提交转运单。`, '转运单已提交,附件待补传', { confirmButtonText: '知道了' })
|
||||
await openDetail({ '0': transferNo })
|
||||
return
|
||||
}
|
||||
}
|
||||
ElMessage.success('转运单已提交审核')
|
||||
await openDetail({ '0': transferNo })
|
||||
return
|
||||
@@ -1044,15 +1156,31 @@ async function selectOperationWarehouse() {
|
||||
}
|
||||
function selectRelabelInventory(inboundOrderId: string) {
|
||||
const rows: CargoRow[] = []
|
||||
for (const candidate of relabelCandidates.value.filter((item) => (item.sourceType === 'RETURN_CARTON' ? `RETURN_CARTON:${item.returnCartonId}` : String(item.inboundOrderId)) === inboundOrderId)) {
|
||||
if (candidate.sourceType === 'RETURN_CARTON') rows.push({
|
||||
id: ++cargoSequence, sku: candidate.skuCode || '', name: candidate.productNameZh || '退货合箱库存',
|
||||
included: true, quantity: 1, availableQuantity: 1, returnCartonId: String(candidate.returnCartonId),
|
||||
sourceInboundOrderNo: candidate.inboundOrderNo, sourceWarehouseName: candidate.overseasWarehouseName,
|
||||
fnsku: '', shipmentNo: candidate.inboundOrderNo || candidate.cartonMark || '', newSkuCode: '', shippingMark: '', customerCartonMarks: '',
|
||||
cartonMark: candidate.cartonMark || '', fbaCartonMark: '', destinationWarehouseId: candidate.destinationFbaWarehouseCode || '', remark: '',
|
||||
})
|
||||
else for (const position of candidate.positions) rows.push(...Array.from({ length: Math.min(position.availablePackages, 500) }, () => ({
|
||||
const selectedCandidates = relabelCandidates.value.filter((item) => {
|
||||
const returnOrderNo = item.inboundOrderNo || String(item.returnCartonId || '')
|
||||
return (item.sourceType === 'RETURN_CARTON' ? `RETURN_ORDER:${returnOrderNo}` : String(item.inboundOrderId)) === inboundOrderId
|
||||
})
|
||||
returnRelabelSkuRows.value = []
|
||||
if (inboundOrderId.startsWith('RETURN_ORDER:')) {
|
||||
const grouped = new Map<string, ReturnRelabelSkuRow>()
|
||||
for (const candidate of selectedCandidates) {
|
||||
const skuItems = candidate.skuItems?.length
|
||||
? candidate.skuItems
|
||||
: candidate.skuCode ? [{ skuCode: candidate.skuCode, quantity: inventoryQuantity(candidate.availablePackages) }] : []
|
||||
for (const item of skuItems) {
|
||||
const skuCode = item.skuCode.trim()
|
||||
if (!skuCode) continue
|
||||
const key = skuCode.toUpperCase()
|
||||
const current = grouped.get(key)
|
||||
const quantity = inventoryQuantity(item.quantity)
|
||||
if (current) current.availableQuantity += quantity
|
||||
else grouped.set(key, { skuCode, productNameZh: candidate.skuCode?.trim().toUpperCase() === key ? (candidate.productNameZh || '') : '', availableQuantity: quantity, selected: false, quantity })
|
||||
}
|
||||
}
|
||||
returnRelabelSkuRows.value = [...grouped.values()]
|
||||
labelingMode.value = 'sku'
|
||||
} else for (const candidate of selectedCandidates) {
|
||||
for (const position of candidate.positions) rows.push(...Array.from({ length: Math.min(inventoryQuantity(position.availablePackages), 500) }, () => ({
|
||||
id: ++cargoSequence, sku: candidate.skuCode || '', name: candidate.productNameZh || '库存货物',
|
||||
included: false, quantity: 1, availableQuantity: 1,
|
||||
sourceInventoryBalanceId: String(candidate.inventoryBalanceId), sourceInventoryPositionId: String(position.positionId),
|
||||
@@ -1070,12 +1198,12 @@ function selectRelabelInventory(inboundOrderId: string) {
|
||||
shippingLabelFile.value = null
|
||||
cartonTotalFile.value = null
|
||||
skuTotalFile.value = null
|
||||
if (labelingMode.value !== 'carton') addSkuRelabelRow()
|
||||
if (!isReturnRelabelSource.value && labelingMode.value !== 'carton') addSkuRelabelRow()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="warehouse-service-page" :class="{ 'warehouse-service-page--detail': mode === 'detail' }">
|
||||
<section class="warehouse-service-page" :class="{ 'warehouse-service-page--detail': mode === 'detail', 'warehouse-service-page--return-relabel': isReturnRelabelSource }">
|
||||
<template v-if="mode === 'list'">
|
||||
<QueryToolbar v-model="listQueryModel" layout="grid" :ariaLabel="`${config.title}筛选`" :fields="listQueryFields" :search-on-reset="false" @search="query" @reset="resetListQuery" />
|
||||
<div class="warehouse-card warehouse-table-card">
|
||||
@@ -1089,17 +1217,17 @@ function selectRelabelInventory(inboundOrderId: string) {
|
||||
<template v-else-if="mode === 'create' || mode === 'edit'">
|
||||
<div class="warehouse-page-heading"><h2>{{ mode === 'edit' ? `编辑转运草稿 ${editingTransferNo}` : config.createTitle }}</h2><el-button @click="returnToList"><ArrowLeft :size="16" />返回列表</el-button></div>
|
||||
<form :class="{ 'warehouse-transfer-form': props.service === 'transfer', 'warehouse-transfer-form--relabel': props.service === 'transfer' && form.relabel }" @submit.prevent="submit('submit')">
|
||||
<div class="warehouse-card warehouse-form-section warehouse-transfer-main-card"><h3>{{ props.service === 'transfer' ? '入库与转运信息' : props.service === 'relabel' ? '退货信息' : props.service === 'labeling' ? '订单信息' : '基础信息' }}</h3>
|
||||
<div class="warehouse-card warehouse-form-section warehouse-transfer-main-card"><h3>{{ props.service === 'transfer' ? '转运信息' : props.service === 'relabel' ? '退货信息' : props.service === 'labeling' ? '订单信息' : '基础信息' }}</h3>
|
||||
<div class="warehouse-form-grid">
|
||||
<label v-if="props.service === 'disposal'" class="warehouse-span-2"><span><i>*</i>操作仓</span><el-select v-model="form.warehouse" filterable clearable :loading="fbaWarehousesLoading" :disabled="Boolean(operationWarehousesError)" placeholder="请选择海外仓" @change="selectDiscardWarehouse"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint">{{ operationWarehousesError }}</small><small v-else class="warehouse-hint">请先选择本次废弃货物所在的海外仓。</small></label>
|
||||
<label v-if="props.service === 'disposal'" class="warehouse-span-2"><span><i>*</i>选择入库单</span><el-select v-model="selectedDiscardOrderId" filterable clearable :loading="discardCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前海外仓暂无可废弃库存" placeholder="请先选择操作仓,再选择有可用库存的入库单" @change="selectDiscardInbound"><el-option v-for="candidate in discardCandidates" :key="candidate.orderId" :label="`${candidate.inboundOrderNo || candidate.inboundCode || '—'}|${candidate.inboundCode || '—'}|可用 ${candidate.availablePackages} 箱`" :value="candidate.orderId" /></el-select><small class="warehouse-hint">仅显示当前操作仓下一件代发中仍有可用箱数的入库单。</small></label>
|
||||
<label v-if="props.service === 'relabel'"><span><i>*</i>进仓编号</span><el-input v-model="form.inboundNo" placeholder="请输入已有入仓编号" /></label>
|
||||
<label v-if="props.service === 'relabel'"><span><i>*</i>来源 FBA 仓库</span><el-select v-model="form.sourceWarehouse" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" placeholder="请选择平台维护的 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
|
||||
<label v-else-if="props.service === 'disposal'"><span>进仓编号</span><el-input v-model="form.inboundNo" readonly /></label>
|
||||
<label v-else><span><i>*</i>{{ props.service === 'labeling' ? '海外操作仓(中转仓)' : props.service === 'transfer' ? '入库海外仓' : '操作仓' }}</span><el-select v-model="form.warehouse" :loading="fbaWarehousesLoading || operationWarehouseDetailLoading" :disabled="Boolean(operationWarehousesError)" clearable :placeholder="props.service === 'transfer' ? '请选择海外仓' : '请选择国外中转仓'" @change="props.service === 'labeling' ? selectOperationWarehouse() : props.service === 'transfer' && selectTransferWarehouse()"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint">{{ operationWarehousesError }}</small><small v-else-if="props.service === 'labeling'" class="warehouse-hint">选择后仅查询当前客户在该中转仓的可用库存。</small><small v-else-if="props.service === 'transfer'" class="warehouse-hint">本批货物将先进入该海外仓完成签收和入库。</small></label>
|
||||
<label v-if="props.service === 'labeling'" class="warehouse-span-2"><span><i>*</i>选择有库存的历史入仓单</span><el-select v-model="selectedRelabelInboundOrderId" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前中转仓暂无已入库可用库存" placeholder="请选择曾经下单且当前仍有库存的入仓单" @change="selectRelabelInventory"><el-option v-for="option in relabelInboundOptions" :key="option.value" :label="`${option.inboundOrderNo}|入仓码 ${option.inboundCode}|可用 ${option.availablePackages} 箱`" :value="option.value" /></el-select><small class="warehouse-hint">仅显示当前客户在所选中转仓已经完成入库且仍有可用库存的历史入仓单。</small></label>
|
||||
<label v-if="props.service === 'labeling' && labelingMode !== 'sku'"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择本张申请统一的 FBA 目的仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select><small class="warehouse-hint">整张贴标申请只能选择一个目的仓,全部明细统一使用该目的仓。</small></label>
|
||||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>货物如何送到海外仓</span><el-radio-group v-model="form.deliveryMethod" @change="selectTransferInboundMethod"><el-radio-button value="WAREHOUSE_PICKUP">仓库上门提货</el-radio-button><el-radio-button value="CUSTOMER_DELIVERY">客户送货到仓</el-radio-button></el-radio-group><small>仓库上门提货:填写货物当前地址;客户送货到仓:下方自动展示海外仓收货信息。</small></div>
|
||||
<label v-else><span><i>*</i>{{ props.service === 'labeling' ? '海外操作仓(中转仓)' : props.service === 'transfer' ? '入库海外仓' : '操作仓' }}</span><el-select v-model="form.warehouse" :loading="fbaWarehousesLoading || operationWarehouseDetailLoading" :disabled="Boolean(operationWarehousesError)" clearable :placeholder="props.service === 'transfer' ? '请选择海外仓' : '请选择国外中转仓'" @change="props.service === 'labeling' ? selectOperationWarehouse() : props.service === 'transfer' && selectTransferWarehouse()"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint">{{ operationWarehousesError }}</small></label>
|
||||
<label v-if="props.service === 'labeling'" class="warehouse-span-2"><span><i>*</i>选择有库存的入仓单或退货单</span><el-select v-model="selectedRelabelInboundOrderId" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前中转仓暂无可用库存" placeholder="请选择当前仍有可用库存的入仓单或退货单" @change="selectRelabelInventory"><el-option v-for="option in relabelInboundOptions" :key="option.value" :label="`${option.inboundOrderNo}|${option.inboundCode}|可用 ${option.availablePackages} ${option.quantityUnit}`" :value="option.value" /></el-select></label>
|
||||
<label v-if="props.service === 'labeling' && (isReturnRelabelSource || labelingMode !== 'sku')"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择本张申请统一的 FBA 目的仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
|
||||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>入仓方式</span><el-radio-group v-model="form.deliveryMethod" @change="selectTransferInboundMethod"><el-radio-button value="WAREHOUSE_PICKUP">仓库上门提货</el-radio-button><el-radio-button value="CUSTOMER_DELIVERY">客户送货到仓</el-radio-button></el-radio-group></div>
|
||||
<label v-if="props.service === 'transfer' && form.deliveryMethod === 'WAREHOUSE_PICKUP'" class="pickup-address-field"><span><i>*</i>提货地址</span><el-input v-model="form.address" placeholder="请输入仓库上门提货的详细地址" /></label>
|
||||
<label v-if="props.service === 'transfer' && form.deliveryMethod === 'WAREHOUSE_PICKUP'"><span><i>*</i>提货联系人</span><el-input v-model="form.pickupContact" placeholder="请输入提货联系人" maxlength="128" /></label>
|
||||
<label v-if="props.service === 'transfer' && form.deliveryMethod === 'WAREHOUSE_PICKUP'"><span><i>*</i>提货联系人电话</span><el-input v-model="form.pickupPhone" placeholder="请输入提货联系人电话" maxlength="64" /></label>
|
||||
@@ -1115,15 +1243,15 @@ function selectRelabelInventory(inboundOrderId: string) {
|
||||
<label v-if="props.service === 'labeling'"><span><i>*</i>海外仓签收联系人<em v-if="form.warehouse && !form.contact" class="warehouse-unmaintained">未维护</em></span><el-input v-model="form.contact" placeholder="选择海外操作仓后自动带出" /></label>
|
||||
<label v-if="props.service === 'labeling'"><span><i>*</i>海外仓联系电话<em v-if="form.warehouse && !form.phone" class="warehouse-unmaintained">未维护</em></span><el-input v-model="form.phone" placeholder="选择海外操作仓后自动带出" /></label>
|
||||
<label v-if="props.service === 'labeling'"><span><i>*</i>贴标截止时间</span><el-date-picker v-model="form.labelDeadline" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择截止时间" /></label>
|
||||
<label v-if="props.service === 'labeling' && labelingMode !== 'sku'"><span><i>*</i>换标后派送方式</span><el-select v-model="form.deliveryMethod" placeholder="请选择派送方式"><el-option label="卡车派送" value="TRUCK" /><el-option label="快递派送" value="EXPRESS" /><el-option label="卡车自提" value="TRUCK_PICKUP" /><el-option label="快递自提" value="EXPRESS_PICKUP" /></el-select><small class="warehouse-hint">卡车派送进入预排车;其余方式确认换标完成后进入换标出库。</small></label>
|
||||
<label v-if="props.service === 'labeling' && labelingMode !== 'sku'"><span><i>*</i>换标后派送方式</span><el-select v-model="form.deliveryMethod" placeholder="请选择派送方式"><el-option label="卡车派送" value="TRUCK" /><el-option label="快递派送" value="EXPRESS" /><el-option label="卡车自提" value="TRUCK_PICKUP" /><el-option label="快递自提" value="EXPRESS_PICKUP" /></el-select></label>
|
||||
<div v-if="props.service === 'labeling' && labelingMode !== 'sku' && (form.deliveryMethod === 'EXPRESS' || form.deliveryMethod === 'EXPRESS_PICKUP')" class="warehouse-checkbox-field"><span>是否打印面单</span><el-checkbox v-model="form.relabel">需要仓库打印面单</el-checkbox></div>
|
||||
<label v-if="canManageRelabelFiles && props.service === 'labeling' && labelingMode !== 'sku' && (form.deliveryMethod === 'EXPRESS' || form.deliveryMethod === 'EXPRESS_PICKUP') && form.relabel"><span><i>*</i>上传面单文件</span><input class="warehouse-native-file" type="file" accept="application/pdf,image/*,.pdf" @change="selectShippingLabel"><small class="warehouse-hint">支持 PDF 或图片,最大 20 MB。</small></label>
|
||||
<label v-if="canManageRelabelFiles && props.service === 'labeling' && labelingMode !== 'sku' && (form.deliveryMethod === 'EXPRESS' || form.deliveryMethod === 'EXPRESS_PICKUP') && form.relabel"><span><i>*</i>上传面单文件</span><input class="warehouse-native-file" type="file" accept="application/pdf,image/*,.pdf" @change="selectShippingLabel"></label>
|
||||
<label v-if="props.service === 'relabel'" class="warehouse-span-2"><span><i>*</i>详细派送地址</span><el-input v-model="form.address" placeholder="请输入派送地址" /></label>
|
||||
<label v-if="props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>预估总体积(CBM)</span><el-input-number v-model="form.volumeCbm" :min="0" :precision="4" :step="0.0001" controls-position="right" placeholder="请输入预估总体积" /></label>
|
||||
<label v-if="props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>{{ props.service === 'transfer' ? '预估体积(CBM)' : '预估总体积(CBM)' }}</span><el-input-number v-model="form.volumeCbm" :min="0" :precision="4" :step="0.0001" controls-position="right" placeholder="请输入预估总体积" /></label>
|
||||
<label v-if="props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>{{ props.service === 'transfer' ? '预计入仓时间' : '预计到达时间' }}</span><el-date-picker v-model="form.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" :placeholder="props.service === 'transfer' ? '请选择预计入仓时间' : '请选择预计到达时间'" /></label>
|
||||
<label v-if="props.service === 'transfer'"><span>预计发货时间</span><el-date-picker v-model="form.estimatedShipAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="选填,可线下确认后补充" /><small class="warehouse-hint">暂未确定时可不填写,不影响货物先入海外仓。</small></label>
|
||||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>是否有目的去向</span><el-radio-group v-model="transferDestinationType" @change="selectTransferDestinationType"><el-radio-button value="NONE">暂未确定</el-radio-button><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="PRIVATE">私人仓</el-radio-button></el-radio-group><small>暂未确定也可以提交;完成入库后可在新建出库单中批量绑定目的地。</small></div>
|
||||
<label v-if="props.service === 'transfer' && transferDestinationType === 'FBA'"><span><i>*</i>未换标货物目的 FBA 仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择平台维护的 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select><small class="warehouse-hint">此处只表示不换标箱子的去向;需要换箱唛的箱子在换标明细中单独选择目的仓。</small></label>
|
||||
<label v-if="props.service === 'transfer'"><span>预计发货时间</span><el-date-picker v-model="form.estimatedShipAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="选填" /></label>
|
||||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>目的去向</span><el-radio-group v-model="transferDestinationType" @change="selectTransferDestinationType"><el-radio-button value="NONE">暂未确定</el-radio-button><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="PRIVATE">私人仓</el-radio-button></el-radio-group></div>
|
||||
<label v-if="props.service === 'transfer' && transferDestinationType === 'FBA'"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select></label>
|
||||
<label v-if="props.service === 'transfer' && transferDestinationType === 'FBA'"><span>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="选填,可后续补充" /></label>
|
||||
<label v-if="props.service === 'transfer' && transferDestinationType === 'PRIVATE'" class="warehouse-span-2"><span><i>*</i>私人仓地址</span><el-input v-model="form.privateDestinationAddress" maxlength="1000" placeholder="请输入私人仓完整地址" /></label>
|
||||
<label v-if="props.service === 'transfer' && transferDestinationType === 'PRIVATE'"><span><i>*</i>邮编</span><el-input v-model="form.privateDestinationPostalCode" maxlength="32" placeholder="请输入邮编" /></label>
|
||||
@@ -1132,66 +1260,86 @@ function selectRelabelInventory(inboundOrderId: string) {
|
||||
<label v-if="props.service === 'relabel'"><span><i>*</i>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="请输入 FBA 货件编号" /></label>
|
||||
<label v-if="props.service === 'labeling'"><span>贴标要求</span><el-input v-model="form.requirement" placeholder="请输入贴标要求" /></label>
|
||||
<label v-else-if="props.service === 'relabel' || (props.service === 'transfer' && form.deliveryMethod === 'CUSTOMER_DELIVERY')"><span>物流跟踪号</span><el-input v-model="form.trackingNo" placeholder="请输入物流跟踪号(选填)" /></label>
|
||||
<label class="warehouse-span-2"><span>订单备注</span><el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注信息" /></label>
|
||||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>是否需要换标</span><el-radio-group v-model="form.relabel"><el-radio-button :value="false">不需要换标</el-radio-button><el-radio-button :value="true">需要换标</el-radio-button></el-radio-group><small>{{ form.relabel ? '本次提交将同时登记换标申请,入库完成后自动进入换标作业。' : '入库完成且已有目的仓后,可直接进入排车流程。' }}</small></div>
|
||||
<label class="warehouse-span-2"><span>备注</span><el-input v-model="form.remark" type="textarea" :rows="2" placeholder="选填" /></label>
|
||||
<div v-if="props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>换标服务</span><el-radio-group v-model="form.relabel"><el-radio-button :value="false">不需要</el-radio-button><el-radio-button :value="true">需要</el-radio-button></el-radio-group></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="warehouse-card warehouse-form-section warehouse-transfer-cargo-card">
|
||||
<template v-if="props.service === 'labeling'">
|
||||
<div class="warehouse-section-heading warehouse-labeling-heading">
|
||||
<h3>货物明细</h3>
|
||||
<div class="warehouse-template-actions">
|
||||
<el-button v-if="canManageRelabelFiles" size="small" @click="chooseCreateTotalFile('CARTON_TOTAL')"><Upload :size="15" />上传箱唛总文件</el-button>
|
||||
<el-button v-if="canManageRelabelFiles" size="small" @click="chooseCreateTotalFile('SKU_TOTAL')"><Upload :size="15" />上传 SKU 总文件</el-button>
|
||||
<el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('sku')"><Download :size="15" />下载 SKU 关联模板</el-button>
|
||||
<el-button size="small" :disabled="!cargoRows.length" @click="skuTemplateInput?.click()"><Upload :size="15" />上传 SKU 关联</el-button>
|
||||
<el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('carton')"><Download :size="15" />下载箱号关联模板</el-button>
|
||||
<el-button size="small" :disabled="!cargoRows.length" @click="cartonTemplateInput?.click()"><Upload :size="15" />上传箱号关联</el-button>
|
||||
<el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('mixed')"><Download :size="15" />下载 SKU 混箱模板</el-button>
|
||||
<el-button size="small" :disabled="!cargoRows.length" @click="mixedTemplateInput?.click()"><Upload :size="15" />上传 SKU 混箱</el-button>
|
||||
<input ref="skuTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'sku')">
|
||||
<input ref="cartonTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'carton')">
|
||||
<input ref="mixedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'mixed')">
|
||||
<input ref="cartonTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectCartonTotalFile">
|
||||
<input ref="skuTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectSkuTotalFile">
|
||||
</div>
|
||||
<div><h3>货物明细</h3></div>
|
||||
</div>
|
||||
<div v-if="canManageRelabelFiles" class="warehouse-upload-summary">
|
||||
<span>箱唛总文件:{{ cartonTotalFile?.name || '未选择' }}<button v-if="cartonTotalFile" type="button" class="warehouse-file-clear" title="清除箱唛总文件" aria-label="清除箱唛总文件" :disabled="saving" @click="clearCreateTotalFile('CARTON_TOTAL')"><X :size="14" /></button></span>
|
||||
<span>SKU 总文件:{{ skuTotalFile?.name || '未选择' }}<button v-if="skuTotalFile" type="button" class="warehouse-file-clear" title="清除 SKU 总文件" aria-label="清除 SKU 总文件" :disabled="saving" @click="clearCreateTotalFile('SKU_TOTAL')"><X :size="14" /></button></span>
|
||||
<div class="warehouse-file-groups" :class="{ 'warehouse-file-groups--single': isReturnRelabelSource }">
|
||||
<section v-if="canManageRelabelFiles" class="warehouse-file-group warehouse-file-group--primary">
|
||||
<div class="warehouse-file-group__heading"><div><strong><span>1</span>贴标总文件</strong></div><em>{{ requiresBothTotalFiles ? '必传 · 两份文件' : '必传 · 二选一' }}</em></div>
|
||||
<div class="warehouse-file-options">
|
||||
<div class="warehouse-file-option" :class="{ 'is-selected': Boolean(cartonTotalFile) }"><div class="warehouse-file-option__name"><strong>箱唛总文件</strong></div><el-button :type="cartonTotalFile ? 'primary' : 'default'" plain @click="chooseCreateTotalFile('CARTON_TOTAL')"><Upload :size="15" />{{ cartonTotalFile ? '重新上传' : '选择文件' }}</el-button><div class="warehouse-file-option__status" :class="{ 'is-empty': !cartonTotalFile }"><span>{{ cartonTotalFile?.name || '尚未选择文件' }}</span><button v-if="cartonTotalFile" type="button" class="warehouse-file-clear" title="清除箱唛总文件" aria-label="清除箱唛总文件" :disabled="saving" @click="clearCreateTotalFile('CARTON_TOTAL')"><X :size="14" /></button></div></div>
|
||||
<div class="warehouse-file-choice-or" :class="{ 'is-both': requiresBothTotalFiles }">{{ requiresBothTotalFiles ? '和' : '或' }}</div>
|
||||
<div class="warehouse-file-option" :class="{ 'is-selected': Boolean(skuTotalFile) }"><div class="warehouse-file-option__name"><strong>SKU 总文件</strong></div><el-button :type="skuTotalFile ? 'primary' : 'default'" plain @click="chooseCreateTotalFile('SKU_TOTAL')"><Upload :size="15" />{{ skuTotalFile ? '重新上传' : '选择文件' }}</el-button><div class="warehouse-file-option__status" :class="{ 'is-empty': !skuTotalFile }"><span>{{ skuTotalFile?.name || '尚未选择文件' }}</span><button v-if="skuTotalFile" type="button" class="warehouse-file-clear" title="清除 SKU 总文件" aria-label="清除 SKU 总文件" :disabled="saving" @click="clearCreateTotalFile('SKU_TOTAL')"><X :size="14" /></button></div></div>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="!isReturnRelabelSource" class="warehouse-file-group">
|
||||
<div class="warehouse-file-group__heading"><div><strong><span>2</span>明细关联模板</strong></div><em>按需使用</em></div>
|
||||
<div class="warehouse-template-list">
|
||||
<div><span><strong>SKU 关联</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('sku')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="skuTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div>
|
||||
<div><span><strong>箱号关联</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('carton')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="cartonTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div>
|
||||
<div><span><strong>SKU 混箱</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('mixed')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="mixedTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="warehouse-labeling-mode">
|
||||
<input ref="skuTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'sku')"><input ref="cartonTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'carton')"><input ref="mixedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'mixed')"><input ref="cartonTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectCartonTotalFile"><input ref="skuTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectSkuTotalFile">
|
||||
<div v-if="isReturnRelabelSource" class="warehouse-table-scroll">
|
||||
<table class="warehouse-edit-table warehouse-return-relabel-table">
|
||||
<thead><tr><th>选择</th><th>SKU</th><th>中文品名</th><th>库存总数</th><th>本次贴标数量</th></tr></thead>
|
||||
<tbody><tr v-for="row in returnRelabelSkuRows" :key="row.skuCode"><td><el-checkbox v-model="row.selected" /></td><td><strong>{{ row.skuCode }}</strong></td><td>{{ row.productNameZh || '—' }}</td><td><strong>{{ row.availableQuantity }}</strong></td><td><el-input-number v-model="row.quantity" :disabled="!row.selected" :min="1" :max="row.availableQuantity" :precision="0" controls-position="right" placeholder="请输入贴标数量" /></td></tr><tr v-if="returnRelabelSkuRows.length === 0"><td colspan="5"><div class="warehouse-empty">该退货单暂无可申请贴标的 SKU 库存</div></td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-if="!isReturnRelabelSource" class="warehouse-labeling-mode">
|
||||
<span>换标类型</span>
|
||||
<el-radio-group v-model="labelingMode" aria-label="换标类型">
|
||||
<el-radio-button value="carton">只换箱唛</el-radio-button>
|
||||
<el-radio-button value="sku">只换 SKU 标签</el-radio-button>
|
||||
<el-radio-button value="carton_and_sku">SKU 和箱唛都换</el-radio-button>
|
||||
</el-radio-group>
|
||||
<p>{{ labelingModeHint }}</p>
|
||||
</div>
|
||||
<div v-if="importedRelabelBindings.length" class="warehouse-import-preview">
|
||||
<div v-if="!isReturnRelabelSource && importedRelabelBindings.length" class="warehouse-import-preview">
|
||||
<div class="warehouse-section-heading"><div><strong>待导入绑定关系</strong><p>可只勾选本次需要换标的部分,也可以全选后一次导入。</p></div><div><el-checkbox :model-value="importedRelabelBindings.every((item) => item.selected)" @change="selectAllImportedBindings(Boolean($event))">全选</el-checkbox><el-button type="primary" size="small" @click="importSelectedBindingsToDetails">导入到明细</el-button></div></div>
|
||||
<div class="warehouse-table-scroll"><table class="warehouse-binding-preview-table"><thead><tr><th>选择</th><th>关系类型</th><th>仓库唛头</th><th>旧 SKU</th><th>新 SKU</th><th>数量</th><th>新 FBA 箱唛</th></tr></thead><tbody><tr v-for="item in importedRelabelBindings" :key="item.id"><td><el-checkbox v-model="item.selected" /></td><td>{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}</td><td>{{ item.cartonMark }}</td><td>{{ item.originalSku || '—' }}</td><td>{{ item.newSku || '—' }}</td><td>{{ item.quantity }}</td><td>{{ item.fbaCartonMark || '—' }}</td></tr></tbody></table></div>
|
||||
</div>
|
||||
<div class="warehouse-section-heading warehouse-tab-actions">
|
||||
<div><strong>绑定明细</strong><p>上传后在下方回显,也可以手动添加;SKU 与箱号关联通过仓库唛头合并。系统只提供大概库存位置,不代表箱唛已绑定具体托盘。</p></div>
|
||||
<div v-if="!isReturnRelabelSource" class="warehouse-section-heading warehouse-tab-actions">
|
||||
<div><strong>绑定明细</strong></div>
|
||||
<div v-if="labelingMode === 'carton'"><el-button type="primary" size="small" @click="addRelabelCargoRow"><Plus :size="15" />手动添加行</el-button><el-button size="small" @click="clearRelabelRows">清空</el-button></div>
|
||||
<div v-else><el-button type="primary" size="small" @click="addSkuRelabelRow"><Plus :size="15" />添加 SKU 明细</el-button><el-button size="small" @click="skuRelabelRows = []">清空</el-button></div>
|
||||
</div>
|
||||
<div v-if="labelingMode === 'carton'" class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-label-table"><thead><tr><th>商品 SKU</th><th>中文品名 *</th><th>FBA 货件编号 *</th><th>箱数</th><th>仓库唛头 *</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in cartonDetailRows" :key="row.id"><td><el-input v-model="row.sku" placeholder="选填" /></td><td><el-input v-model="row.name" placeholder="请输入中文品名" /></td><td><el-input v-model="row.shipmentNo" placeholder="请输入FBA货件编号" /></td><td><strong>1 箱</strong></td><td><el-input v-model="row.cartonMark" placeholder="请输入仓库唛头" /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" maxlength="1000" show-word-limit placeholder="请输入明细备注" /></td><td><button type="button" class="warehouse-delete" title="移出本次明细" @click="removeCargoRow(row.id)"><Trash2 :size="16" />移出</button></td></tr><tr v-if="cartonDetailRows.length === 0"><td colspan="8"><div class="warehouse-empty">暂无本次换标明细,请上传关系后导入或手动添加</div></td></tr></tbody></table></div>
|
||||
<div v-if="!isReturnRelabelSource && labelingMode === 'carton'" class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-label-table"><thead><tr><th>商品 SKU</th><th>中文品名 *</th><th>FBA 货件编号 *</th><th>箱数</th><th>仓库唛头 *</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in cartonDetailRows" :key="row.id"><td><el-input v-model="row.sku" placeholder="选填" /></td><td><el-input v-model="row.name" placeholder="请输入中文品名" /></td><td><el-input v-model="row.shipmentNo" placeholder="请输入FBA货件编号" /></td><td><strong>1 箱</strong></td><td><el-input v-model="row.cartonMark" placeholder="请输入仓库唛头" /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" maxlength="1000" show-word-limit placeholder="请输入明细备注" /></td><td><button type="button" class="warehouse-delete" title="移出本次明细" @click="removeCargoRow(row.id)"><Trash2 :size="16" />移出</button></td></tr><tr v-if="cartonDetailRows.length === 0"><td colspan="8"><div class="warehouse-empty">暂无本次换标明细,请上传关系后导入或手动添加</div></td></tr></tbody></table></div>
|
||||
<div v-else class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-sku-label-table"><thead><tr><th>仓库唛头 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th v-if="labelingMode === 'carton_and_sku'">新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in skuRelabelRows" :key="row.id"><td><el-input v-model="row.cartonMark" placeholder="请输入仓库唛头" @change="applyExistingCartonBinding(row)" /></td><td><el-input v-model="row.originalSku" placeholder="请输入旧 SKU" @change="applyExistingSkuBinding(row)" /></td><td><el-input v-model="row.newSku" placeholder="请输入新 SKU" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="请输入件数" /></td><td v-if="labelingMode === 'carton_and_sku'"><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除" @click="removeSkuRelabelRow(row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="skuRelabelRows.length === 0"><td :colspan="labelingMode === 'carton_and_sku' ? 7 : 6"><div class="warehouse-empty">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="warehouse-section-heading"><h3>货物明细</h3><div v-if="props.service !== 'disposal'"><el-button type="primary" size="small" @click="addCargoRow"><Plus :size="15" />添加行</el-button><el-button size="small" @click="cargoRows = []">清空</el-button></div></div>
|
||||
<div class="warehouse-section-heading" :class="{ 'warehouse-step-heading': props.service === 'transfer' }"><div><h3><span v-if="props.service === 'transfer'" class="warehouse-step-number">1</span>货物明细</h3></div><div v-if="props.service !== 'disposal'" class="warehouse-heading-actions"><el-button v-if="props.service === 'transfer'" size="small" :disabled="!cargoRows.some((row) => row.sku.trim() && row.quantity > 0)" @click="printTransferDraftCartonMarks"><Printer :size="15" />打印唛头</el-button><el-button type="primary" size="small" @click="addCargoRow"><Plus :size="15" />添加行</el-button><el-button size="small" @click="cargoRows = []">清空</el-button></div></div>
|
||||
<div class="warehouse-table-scroll"><table class="warehouse-edit-table"><thead><tr><th>商品 SKU{{ props.service === 'transfer' ? '' : ' *' }}</th><th>中文品名{{ props.service === 'transfer' ? '' : ' *' }}</th><th v-if="props.service === 'disposal'">可用箱数</th><th v-if="props.service === 'relabel'">FNSKU / 箱唛</th><th>{{ props.service === 'disposal' ? '本次废弃箱数' : '预计箱数' }} *</th><th v-if="props.service !== 'disposal'">操作</th></tr></thead><tbody><tr v-for="row in cargoRows" :key="row.id"><td><el-input v-model="row.sku" :readonly="props.service === 'disposal'" :placeholder="props.service === 'transfer' ? '选填' : '请输入商品 SKU'" /></td><td><el-input v-model="row.name" :readonly="props.service === 'disposal'" :placeholder="props.service === 'transfer' ? '选填' : '请输入中文品名'" /></td><td v-if="props.service === 'disposal'"><strong>{{ row.availableQuantity }} 箱</strong></td><td v-if="props.service === 'relabel'"><el-input v-model="row.shippingMark" placeholder="请输入FNSKU或箱唛" /></td><td><el-input-number v-model="row.quantity" :min="props.service === 'disposal' ? 0 : 1" :max="row.availableQuantity" controls-position="right" placeholder="请输入箱数" /></td><td v-if="props.service !== 'disposal'"><button type="button" class="warehouse-delete" title="删除" @click="removeCargoRow(row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="cargoRows.length === 0"><td :colspan="props.service === 'disposal' ? 4 : props.service === 'relabel' ? 5 : 4"><div class="warehouse-empty">{{ props.service === 'disposal' ? '请先选择入库单,系统将自动带出 SKU 和可用箱数' : '暂无货物明细,请点击“添加行”' }}</div></td></tr></tbody></table></div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="props.service === 'transfer' && form.relabel" class="warehouse-card warehouse-form-section warehouse-transfer-relabel">
|
||||
<div class="warehouse-section-heading warehouse-labeling-heading"><div><h3>货物明细</h3><p>换标明细与贴标申请保持一致;客户只填写自己的箱唛,我方箱唛由系统生成。</p></div><div class="warehouse-template-actions"><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('sku')"><Download :size="15" />下载 SKU 关联模板</el-button><el-button size="small" :disabled="!cargoRows.length" @click="skuTemplateInput?.click()"><Upload :size="15" />上传 SKU 关联</el-button><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('carton')"><Download :size="15" />下载箱号关联模板</el-button><el-button size="small" :disabled="!cargoRows.length" @click="cartonTemplateInput?.click()"><Upload :size="15" />上传箱号关联</el-button><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('mixed')"><Download :size="15" />下载 SKU 混箱模板</el-button><el-button size="small" :disabled="!cargoRows.length" @click="mixedTemplateInput?.click()"><Upload :size="15" />上传 SKU 混箱</el-button><input ref="skuTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'sku')"><input ref="cartonTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'carton')"><input ref="mixedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'mixed')"></div></div>
|
||||
<div class="warehouse-section-heading warehouse-labeling-heading warehouse-step-heading"><div><h3><span class="warehouse-step-number">2</span>换标申请</h3></div></div>
|
||||
<div class="warehouse-form-grid warehouse-transfer-relabel-fields"><label><span><i>*</i>换标截止时间</span><el-date-picker v-model="form.labelDeadline" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择截止时间" /></label><label><span><i>*</i>换标联系人</span><el-input v-model="form.relabelContact" placeholder="选择海外仓后自动带出,也可手动填写" maxlength="128" /></label><label><span><i>*</i>换标联系人电话</span><el-input v-model="form.relabelPhone" placeholder="选择海外仓后自动带出,也可手动填写" maxlength="64" /></label><label v-if="labelingMode !== 'sku'"><span><i>*</i>换标后派送方式</span><el-select v-model="form.relabelDeliveryMethod" placeholder="请选择派送方式"><el-option label="卡车派送" value="TRUCK" /><el-option label="快递派送" value="EXPRESS" /><el-option label="卡车自提" value="TRUCK_PICKUP" /><el-option label="快递自提" value="EXPRESS_PICKUP" /></el-select></label><label class="warehouse-span-2"><span><i>*</i>整票换标货物目的 FBA 仓</span><el-select v-model="transferRelabelDefaultDestination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择整票货物的目的仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" /></el-select><small class="warehouse-hint">本票所有换标货物统一前往该目的仓,暂不支持一票多个目的仓。</small></label><label class="warehouse-span-2"><span>贴标要求</span><el-input v-model="form.requirement" type="textarea" :rows="2" maxlength="1000" show-word-limit placeholder="请输入贴标要求(选填)" /></label></div>
|
||||
<div class="warehouse-labeling-mode"><span>换标类型</span><el-radio-group v-model="labelingMode" aria-label="换标类型"><el-radio-button value="carton">只换箱唛</el-radio-button><el-radio-button value="sku">只换 SKU 标签</el-radio-button><el-radio-button value="carton_and_sku">SKU 和箱唛都换</el-radio-button></el-radio-group><p>{{ labelingModeHint }}</p></div>
|
||||
<div class="warehouse-labeling-mode"><span>换标类型</span><el-radio-group v-model="labelingMode" aria-label="换标类型"><el-radio-button value="carton">只换箱唛</el-radio-button><el-radio-button value="sku">只换 SKU 标签</el-radio-button><el-radio-button value="carton_and_sku">SKU 和箱唛都换</el-radio-button></el-radio-group></div>
|
||||
<div class="warehouse-transfer-file-groups">
|
||||
<section v-if="canManageRelabelFiles" class="warehouse-file-group warehouse-transfer-total-files">
|
||||
<div class="warehouse-file-group__heading"><div><strong>贴标总文件</strong></div><em>{{ requiresBothTotalFiles ? '必传 · 两份文件' : '必传 · 二选一' }}</em></div>
|
||||
<div class="warehouse-file-options">
|
||||
<div class="warehouse-file-option" :class="{ 'is-selected': Boolean(cartonTotalFile) }"><div class="warehouse-file-option__name"><strong>箱唛总文件</strong></div><el-button :type="cartonTotalFile ? 'primary' : 'default'" plain @click="chooseCreateTotalFile('CARTON_TOTAL')"><Upload :size="15" />{{ cartonTotalFile ? '重新上传' : '选择文件' }}</el-button><div class="warehouse-file-option__status" :class="{ 'is-empty': !cartonTotalFile }"><span>{{ cartonTotalFile?.name || '尚未选择文件' }}</span><button v-if="cartonTotalFile" type="button" class="warehouse-file-clear" title="清除箱唛总文件" aria-label="清除箱唛总文件" :disabled="saving" @click="clearCreateTotalFile('CARTON_TOTAL')"><X :size="14" /></button></div></div>
|
||||
<div class="warehouse-file-choice-or" :class="{ 'is-both': requiresBothTotalFiles }">{{ requiresBothTotalFiles ? '和' : '或' }}</div>
|
||||
<div class="warehouse-file-option" :class="{ 'is-selected': Boolean(skuTotalFile) }"><div class="warehouse-file-option__name"><strong>SKU 总文件</strong></div><el-button :type="skuTotalFile ? 'primary' : 'default'" plain @click="chooseCreateTotalFile('SKU_TOTAL')"><Upload :size="15" />{{ skuTotalFile ? '重新上传' : '选择文件' }}</el-button><div class="warehouse-file-option__status" :class="{ 'is-empty': !skuTotalFile }"><span>{{ skuTotalFile?.name || '尚未选择文件' }}</span><button v-if="skuTotalFile" type="button" class="warehouse-file-clear" title="清除 SKU 总文件" aria-label="清除 SKU 总文件" :disabled="saving" @click="clearCreateTotalFile('SKU_TOTAL')"><X :size="14" /></button></div></div>
|
||||
</div>
|
||||
<input ref="cartonTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectCartonTotalFile"><input ref="skuTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectSkuTotalFile">
|
||||
</section>
|
||||
<section class="warehouse-file-group warehouse-transfer-template-group">
|
||||
<div class="warehouse-file-group__heading"><div><strong><span>3</span>明细关联模板</strong></div><em>按需使用</em></div><div class="warehouse-template-list"><div><span><strong>SKU 关联</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('sku')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="skuTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div><div><span><strong>箱号关联</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('carton')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="cartonTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div><div><span><strong>SKU 混箱</strong></span><el-button size="small" :disabled="!cargoRows.length" @click="downloadRelabelTemplate('mixed')"><Download :size="15" />下载模板</el-button><el-button type="primary" plain size="small" :disabled="!cargoRows.length" @click="mixedTemplateInput?.click()"><Upload :size="15" />上传文件</el-button></div></div>
|
||||
<input ref="skuTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'sku')"><input ref="cartonTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'carton')"><input ref="mixedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'mixed')">
|
||||
</section>
|
||||
</div>
|
||||
<div v-if="importedRelabelBindings.length" class="warehouse-import-preview"><div class="warehouse-section-heading"><div><strong>待导入绑定关系</strong><p>可只勾选本次需要换标的部分,也可以全选后一次导入。</p></div><div><el-checkbox :model-value="importedRelabelBindings.every((item) => item.selected)" @change="selectAllImportedBindings(Boolean($event))">全选</el-checkbox><el-button type="primary" size="small" @click="importSelectedBindingsToDetails">导入到明细</el-button></div></div><div class="warehouse-table-scroll"><table class="warehouse-binding-preview-table"><thead><tr><th>选择</th><th>关系类型</th><th>客户箱唛号</th><th>旧 SKU</th><th>新 SKU</th><th>数量</th><th>新 FBA 箱唛</th></tr></thead><tbody><tr v-for="item in importedRelabelBindings" :key="item.id"><td><el-checkbox v-model="item.selected" /></td><td>{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}</td><td>{{ item.cartonMark }}</td><td>{{ item.originalSku || '—' }}</td><td>{{ item.newSku || '—' }}</td><td>{{ item.quantity }}</td><td>{{ item.fbaCartonMark || '—' }}</td></tr></tbody></table></div></div>
|
||||
<div class="warehouse-section-heading warehouse-tab-actions"><div><strong>绑定明细</strong><p>上传后在下方回显,也可以手动添加;客户箱唛作为仓库唛头来源,系统我方箱唛不需要手填。</p></div><div><el-button type="primary" size="small" @click="addTransferRelabelRow"><Plus :size="15" />添加 SKU 明细</el-button><el-button size="small" @click="clearTransferRelabelRows">清空</el-button></div></div>
|
||||
<div class="warehouse-section-heading warehouse-tab-actions warehouse-binding-heading"><div><strong><span class="warehouse-step-number">4</span>绑定明细</strong></div><div><el-button type="primary" size="small" @click="addTransferRelabelRow"><Plus :size="15" />添加 SKU 明细</el-button><el-button size="small" @click="clearTransferRelabelRows">清空</el-button></div></div>
|
||||
<div v-if="labelingMode === 'carton'" class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-transfer-mark-table"><thead><tr><th>客户箱唛号 *</th><th>旧 SKU</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in transferRelabelRows" :key="row.id"><td><el-input v-model="row.customerCartonMarks" type="textarea" :rows="2" placeholder="请输入客户箱唛号" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除该明细" @click="transferRelabelRows = transferRelabelRows.filter((item) => item.id !== row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="transferRelabelRows.length === 0"><td colspan="5"><div class="warehouse-empty">暂无绑定明细,请点击“添加换标明细”</div></td></tr></tbody></table></div>
|
||||
<div v-else-if="labelingMode === 'sku'" class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-transfer-mark-table"><thead><tr><th>客户箱唛号 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in transferRelabelRows" :key="row.id"><td><el-input v-model="row.customerCartonMarks" type="textarea" :rows="2" placeholder="请输入客户箱唛号" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.newSkuCode" placeholder="请输入新 SKU" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="请输入箱数" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除该明细" @click="transferRelabelRows = transferRelabelRows.filter((item) => item.id !== row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="transferRelabelRows.length === 0"><td colspan="6"><div class="warehouse-empty">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
|
||||
<div v-else class="warehouse-table-scroll"><table class="warehouse-edit-table warehouse-transfer-mark-table"><thead><tr><th>客户箱唛号 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for="row in transferRelabelRows" :key="row.id"><td><el-input v-model="row.customerCartonMarks" type="textarea" :rows="2" placeholder="请输入客户箱唛号" /></td><td><el-input :model-value="row.sku" readonly /></td><td><el-input v-model="row.newSkuCode" placeholder="请输入新 SKU" /></td><td><el-input-number v-model="row.quantity" :min="1" :precision="0" controls-position="right" placeholder="请输入数量" /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入新的 FBA 箱唛" /></td><td><el-input v-model="row.remark" type="textarea" :rows="2" placeholder="请输入备注" /></td><td><button type="button" class="warehouse-delete" title="删除该明细" @click="transferRelabelRows = transferRelabelRows.filter((item) => item.id !== row.id)"><Trash2 :size="16" />删除</button></td></tr><tr v-if="transferRelabelRows.length === 0"><td colspan="7"><div class="warehouse-empty">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
|
||||
@@ -1261,18 +1409,21 @@ function selectRelabelInventory(inboundOrderId: string) {
|
||||
.warehouse-hidden-input{display:none}.warehouse-label-table{min-width:1120px}.warehouse-attachment-tip{margin:10px 0 12px;color:#7d8797;font-size:12px}.warehouse-mark-mapping{display:flex;flex-wrap:wrap;gap:8px;margin:4px 0 8px;padding:12px;border:1px solid #dce7f5;border-radius:5px;background:#f7faff}.warehouse-mark-mapping strong{width:100%;color:#34445c}.warehouse-mark-mapping span{padding:5px 9px;border-radius:3px;background:#eaf2fc;color:#3e6593;font-size:12px}
|
||||
.warehouse-upload-summary{display:flex;flex-wrap:wrap;gap:8px 24px;margin:-4px 0 14px;padding:9px 12px;border:1px solid #dce7f5;border-radius:4px;background:#f8fbff;color:#596579;font-size:12px}
|
||||
.warehouse-upload-summary>span{display:inline-flex;align-items:center;gap:6px;overflow-wrap:anywhere}
|
||||
.warehouse-file-groups{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px;margin:0 0 18px}.warehouse-file-groups--single{grid-template-columns:minmax(0,1fr)}.warehouse-file-group{min-width:0;overflow:hidden;border:1px solid #dde5f0;border-radius:8px;background:#fbfcfe;box-shadow:0 1px 2px rgba(31,54,86,.04)}.warehouse-file-group--primary{border-color:#cbdcf2;background:linear-gradient(180deg,#f7faff 0,#fbfdff 100%)}.warehouse-file-group__heading{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:14px 16px;border-bottom:1px solid #e7edf5;background:rgba(255,255,255,.68)}.warehouse-file-group__heading strong{display:flex;align-items:center;gap:8px;color:#2d405c;font-size:14px}.warehouse-file-group__heading strong>span{display:inline-grid;width:22px;height:22px;border-radius:50%;background:#3478c8;color:#fff;font-size:12px;place-items:center}.warehouse-file-group__heading em{flex-shrink:0;padding:4px 8px;border-radius:10px;background:#edf4fd;color:#4774aa;font-size:11px;font-style:normal}.warehouse-file-options{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:stretch;padding:14px}.warehouse-file-option{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px 12px;align-items:center;min-width:0;padding:12px;border:1px dashed #ced8e6;border-radius:6px;background:#fff}.warehouse-file-option.is-selected{border-style:solid;border-color:#79a8df;background:#f5f9ff}.warehouse-file-option__name strong{color:#34445a;font-size:13px}.warehouse-file-option__status{display:flex;grid-column:1/-1;align-items:center;justify-content:space-between;gap:8px;min-width:0;padding:7px 8px;border-radius:4px;background:#eaf3ff;color:#376da9;font-size:11px}.warehouse-file-option__status>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.warehouse-file-option__status.is-empty{background:#f4f6f8;color:#9aa4b2}.warehouse-file-choice-or{display:grid;color:#9ba6b5;font-size:11px;place-items:center}.warehouse-file-choice-or.is-both{color:#3478c8;font-weight:700}.warehouse-template-list{display:flex;flex-direction:column;padding:7px 14px}.warehouse-template-list>div{display:grid;grid-template-columns:minmax(150px,1fr) auto auto;align-items:center;gap:8px;padding:9px 0;border-bottom:1px solid #edf1f6}.warehouse-template-list>div:last-child{border-bottom:0}.warehouse-template-list strong{color:#3f4e63;font-size:12px}.warehouse-transfer-template-group{margin:0 0 14px}@media(max-width:1180px){.warehouse-file-groups{grid-template-columns:minmax(0,1fr)}}@media(max-width:720px){.warehouse-file-options{grid-template-columns:minmax(0,1fr)}.warehouse-file-choice-or{height:24px}.warehouse-template-list>div{grid-template-columns:minmax(0,1fr) auto}.warehouse-template-list>div>span{grid-column:1/-1}}
|
||||
.warehouse-file-clear{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:24px;height:24px;border:0;border-radius:4px;background:transparent;color:var(--muted);cursor:pointer}
|
||||
.warehouse-file-clear:hover:not(:disabled){color:#dc2626;background:#fee2e2}
|
||||
.warehouse-file-clear:disabled{opacity:.5;cursor:not-allowed}
|
||||
.warehouse-unmaintained{margin-left:8px;color:#e6a23c;font-size:12px;font-style:normal}
|
||||
.warehouse-labeling-tabs{margin-top:-8px}.warehouse-labeling-heading{align-items:flex-start}.warehouse-labeling-heading h3{margin-bottom:16px}.warehouse-template-actions{display:flex;gap:8px;padding-bottom:12px}.warehouse-tab-actions{align-items:center;margin:2px 0 14px}.warehouse-tab-actions p{margin:0;color:#7d8797;font-size:12px}.warehouse-tab-actions>div{display:flex;gap:8px}.warehouse-sku-notice{margin:0 0 14px;padding:10px 12px;border:1px solid #f2d39b;border-radius:4px;background:#fff8e8;color:#8a6116;font-size:12px}.warehouse-sku-label-table{min-width:980px}.warehouse-sku-label-table :deep(.el-input-number){width:160px}:deep(.warehouse-labeling-tabs>.el-tabs__header){margin-bottom:18px}:deep(.warehouse-labeling-tabs .el-tabs__item){height:42px;padding:0 26px;font-weight:600}.warehouse-labeling-mode{display:flex;align-items:center;gap:14px;margin:0 0 16px;padding:14px 16px;border:1px solid #dce5f5;border-radius:5px;background:#f7f9ff}.warehouse-labeling-mode>span{color:#34445c;font-size:13px;font-weight:650;white-space:nowrap}.warehouse-labeling-mode p{margin:0;color:#71809a;font-size:12px}.warehouse-labeling-mode :deep(.el-radio-group){display:flex;flex-shrink:0}.warehouse-labeling-mode :deep(.el-radio-button__inner){padding:8px 14px}
|
||||
.warehouse-relabel-detail-table{min-width:1420px}.warehouse-relabel-detail-table td strong{color:#253f67}
|
||||
.warehouse-service-page--return-relabel .warehouse-table-scroll:has(> .warehouse-return-relabel-table)~.warehouse-table-scroll{display:none}
|
||||
.warehouse-native-file{min-height:32px;padding:5px 8px;border:1px solid #dcdfe6;border-radius:4px;background:#fff;color:#606266}.warehouse-fulfillment-grid{margin-bottom:18px}.warehouse-evidence{display:flex;align-items:flex-start;gap:16px;padding:14px;border:1px solid #e7ebf1;border-radius:4px;background:#f8fafc}.warehouse-evidence>span{padding-top:6px;color:#71809a;font-size:12px;white-space:nowrap}.warehouse-evidence>strong{padding-top:6px;color:#8a95a5;font-size:12px;font-weight:400}.warehouse-photo-grid{display:grid;grid-template-columns:repeat(4,140px);gap:12px}.warehouse-photo-item{width:140px;height:105px;overflow:hidden;border:1px solid #dce3ed;border-radius:5px;background:#fff}.warehouse-photo-item :deep(.el-image){display:block;width:100%;height:100%;cursor:zoom-in}.warehouse-photo-placeholder{display:flex;width:100%;height:100%;align-items:center;justify-content:center;padding:10px;border:0;background:#f4f7fb;color:#8792a2;font-size:12px;text-align:center}.warehouse-photo-retry{cursor:pointer}.warehouse-completion-remark{margin:10px 0 0;color:#596579;font-size:12px}
|
||||
.warehouse-measurement-evidence{margin-top:16px}
|
||||
.warehouse-template-actions{flex-wrap:wrap;justify-content:flex-end}.warehouse-import-preview{margin:0 0 16px;padding:14px;border:1px solid #cfe0f5;border-radius:5px;background:#f8fbff}.warehouse-import-preview .warehouse-section-heading{margin-bottom:10px}.warehouse-import-preview .warehouse-section-heading>div{display:flex;align-items:center;gap:10px}.warehouse-import-preview p{margin:0;color:#7d8797;font-size:12px}.warehouse-binding-preview-table{min-width:980px;background:#fff}
|
||||
.warehouse-transfer-relabel .warehouse-labeling-heading>div:first-child{min-width:0}.warehouse-transfer-relabel .warehouse-labeling-heading h3{margin-bottom:6px}.warehouse-transfer-relabel .warehouse-labeling-heading p{margin:0 0 16px;color:#7d8797;font-size:12px}.warehouse-transfer-relabel-fields{margin-bottom:18px}.warehouse-transfer-mark-table{min-width:1420px}.warehouse-transfer-mark-table td{vertical-align:top}.warehouse-transfer-mark-table :deep(.el-textarea){min-width:220px}.warehouse-transfer-mark-table :deep(.el-input-number){width:150px}.warehouse-count-error{color:#d64545}
|
||||
.warehouse-transfer-form{display:grid;grid-template-columns:minmax(0,1fr);gap:12px}.warehouse-transfer-form--relabel{grid-template-columns:minmax(0,1fr) minmax(0,1.35fr);align-items:start}.warehouse-transfer-form--relabel .warehouse-transfer-main-card{grid-column:1;grid-row:1}.warehouse-transfer-form--relabel .warehouse-transfer-cargo-card{grid-column:1;grid-row:2}.warehouse-transfer-form--relabel .warehouse-transfer-relabel{grid-column:2;grid-row:1 / span 2;margin-bottom:0}.warehouse-transfer-form--relabel .warehouse-transfer-submit-bar{grid-column:1 / -1}.warehouse-transfer-form :deep(.el-input__wrapper),.warehouse-transfer-form :deep(.el-textarea__inner),.warehouse-transfer-form :deep(.el-select__wrapper),.warehouse-transfer-form :deep(.el-date-editor.el-input__wrapper){min-height:32px}.warehouse-transfer-form .warehouse-form-section{padding:16px}.warehouse-transfer-form .warehouse-form-section h3{margin-bottom:12px;padding-bottom:9px;font-size:14px}.warehouse-transfer-form .warehouse-form-grid{gap:12px 16px}.warehouse-transfer-form .warehouse-choice-field{min-height:34px;padding:8px 10px;grid-template-columns:118px auto 1fr;gap:10px}.warehouse-transfer-form .warehouse-choice-field>span{font-size:12px}.warehouse-transfer-form .warehouse-choice-field small{font-size:11px}.warehouse-transfer-form .warehouse-receiving-card{gap:12px;padding:10px 12px}.warehouse-transfer-form .warehouse-section-heading{margin-bottom:8px}.warehouse-transfer-form .warehouse-edit-table{font-size:11px}.warehouse-transfer-form .warehouse-edit-table th{height:36px;padding:0 9px}.warehouse-transfer-form .warehouse-edit-table td{height:42px;padding:6px 7px}.warehouse-transfer-form .warehouse-transfer-mark-table :deep(.el-textarea){min-width:150px}.warehouse-transfer-form .warehouse-transfer-mark-table :deep(.el-input-number){width:112px}.warehouse-transfer-form .warehouse-hint{margin-top:5px;font-size:11px}.warehouse-transfer-form .warehouse-labeling-mode{gap:9px;margin-bottom:10px;padding:9px 10px}.warehouse-transfer-form .warehouse-labeling-mode>span{font-size:12px}.warehouse-transfer-form .warehouse-labeling-mode p{font-size:11px}.warehouse-transfer-form .warehouse-labeling-mode :deep(.el-radio-button__inner){padding:6px 9px}.warehouse-transfer-form .warehouse-template-actions{gap:5px;padding-bottom:8px}.warehouse-transfer-form .warehouse-template-actions :deep(.el-button){padding:6px 8px;font-size:11px}.warehouse-transfer-form .warehouse-submit-bar{padding:4px 0 18px}.warehouse-transfer-form .warehouse-submit-bar :deep(.el-button){min-height:32px;padding:7px 14px}.warehouse-transfer-form .warehouse-transfer-relabel{overflow:hidden}.warehouse-transfer-form .warehouse-transfer-relabel-fields{margin-bottom:10px}
|
||||
.warehouse-transfer-relabel .warehouse-labeling-heading>div:first-child{min-width:0}.warehouse-transfer-relabel .warehouse-labeling-heading h3{margin-bottom:6px}.warehouse-transfer-relabel .warehouse-labeling-heading p{margin:0 0 16px;color:#7d8797;font-size:12px}.warehouse-transfer-relabel-fields{margin-bottom:18px}.warehouse-transfer-file-groups{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(360px,.85fr);align-items:stretch;gap:14px;margin-bottom:14px}.warehouse-transfer-total-files,.warehouse-transfer-template-group{height:100%;margin:0}.warehouse-transfer-mark-table{min-width:1420px}.warehouse-transfer-mark-table td{vertical-align:top}.warehouse-transfer-mark-table :deep(.el-textarea){min-width:220px}.warehouse-transfer-mark-table :deep(.el-input-number){width:150px}.warehouse-count-error{color:#d64545}
|
||||
.warehouse-transfer-form{display:grid;grid-template-columns:minmax(0,1fr);gap:12px}.warehouse-transfer-form--relabel{grid-template-columns:minmax(0,1fr) minmax(0,1.35fr);align-items:start}.warehouse-transfer-form--relabel .warehouse-transfer-main-card{grid-column:1;grid-row:1}.warehouse-transfer-form--relabel .warehouse-transfer-cargo-card{grid-column:1;grid-row:2}.warehouse-transfer-form--relabel .warehouse-transfer-relabel{grid-column:2;grid-row:1 / span 2;margin-bottom:0}.warehouse-transfer-form--relabel .warehouse-transfer-submit-bar{grid-column:1 / -1}.warehouse-transfer-form :deep(.el-input__wrapper),.warehouse-transfer-form :deep(.el-textarea__inner),.warehouse-transfer-form :deep(.el-select__wrapper),.warehouse-transfer-form :deep(.el-date-editor.el-input__wrapper){min-height:32px}.warehouse-transfer-form .warehouse-form-section{padding:16px}.warehouse-transfer-form .warehouse-form-section h3{margin-bottom:12px;padding-bottom:9px;font-size:14px}.warehouse-transfer-form .warehouse-form-grid{gap:12px 16px}.warehouse-transfer-form .warehouse-choice-field{min-height:34px;padding:8px 10px;grid-template-columns:118px minmax(0,1fr);gap:10px}.warehouse-transfer-form .warehouse-choice-field>span{font-size:12px}.warehouse-transfer-form .warehouse-receiving-card{gap:12px;padding:10px 12px}.warehouse-transfer-form .warehouse-section-heading{margin-bottom:8px}.warehouse-transfer-form .warehouse-edit-table{font-size:11px}.warehouse-transfer-form .warehouse-edit-table th{height:36px;padding:0 9px}.warehouse-transfer-form .warehouse-edit-table td{height:42px;padding:6px 7px}.warehouse-transfer-form .warehouse-transfer-mark-table :deep(.el-textarea){min-width:150px}.warehouse-transfer-form .warehouse-transfer-mark-table :deep(.el-input-number){width:112px}.warehouse-transfer-form .warehouse-hint{margin-top:5px;font-size:11px}.warehouse-transfer-form .warehouse-labeling-mode{gap:9px;margin-bottom:10px;padding:9px 10px}.warehouse-transfer-form .warehouse-labeling-mode>span{font-size:12px}.warehouse-transfer-form .warehouse-labeling-mode :deep(.el-radio-button__inner){padding:6px 9px}.warehouse-transfer-form .warehouse-template-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:5px;padding-bottom:8px}.warehouse-transfer-form .warehouse-template-actions :deep(.el-button){padding:6px 8px;font-size:11px}.warehouse-transfer-form .warehouse-submit-bar{padding:4px 0 18px}.warehouse-transfer-form .warehouse-submit-bar :deep(.el-button){min-height:32px;padding:7px 14px}.warehouse-transfer-form .warehouse-transfer-relabel{overflow:hidden}.warehouse-transfer-form .warehouse-transfer-relabel-fields{margin-bottom:10px}.warehouse-step-heading{align-items:flex-start}.warehouse-step-heading h3{display:flex;align-items:center;gap:9px}.warehouse-step-number{display:inline-grid;flex:0 0 auto;width:24px;height:24px;border-radius:50%;background:#3478c8;color:#fff;font-size:12px;place-items:center}.warehouse-heading-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px}.warehouse-transfer-template-group{margin:0 0 14px;padding:12px 14px;border:1px solid #dde5f0;border-radius:7px;background:#fbfcfe}.warehouse-binding-heading{margin-top:14px;padding:12px 14px;border:1px solid #dde5f0;border-radius:7px;background:#f8faff}.warehouse-binding-heading strong{display:flex;align-items:center;gap:8px;color:#34445c;font-size:13px}
|
||||
@media(max-width:1250px){.warehouse-transfer-form--relabel{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.warehouse-transfer-form .warehouse-template-actions{justify-content:flex-start}.warehouse-transfer-form .warehouse-labeling-heading{align-items:flex-start;flex-direction:column}}
|
||||
@media(max-width:1180px){.warehouse-transfer-file-groups{grid-template-columns:minmax(0,1fr)}}
|
||||
.warehouse-transfer-form .pickup-address-field{grid-column:span 1}
|
||||
/* Keep the count and controls visible together on narrow list pages. */
|
||||
.warehouse-pagination { justify-content: flex-end; }
|
||||
|
||||
@@ -17,11 +17,32 @@ test('总文件可以独立清除并重选同名文件,提交时禁止清除',
|
||||
assert.match(view, /aria-label="清除 SKU 总文件" :disabled="saving"/)
|
||||
})
|
||||
|
||||
test('贴标申请从当前客户仍有库存的历史入仓单选择来源', () => {
|
||||
test('贴标文件按总文件和关联模板分组展示', () => {
|
||||
assert.match(view, /warehouse-file-groups/)
|
||||
assert.match(view, /<span>1<\/span>贴标总文件/)
|
||||
assert.match(view, /必传 · 二选一/)
|
||||
assert.match(view, /必传 · 两份文件/)
|
||||
assert.match(view, /<span>2<\/span>明细关联模板/)
|
||||
assert.match(view, /SKU 关联/)
|
||||
assert.match(view, /箱号关联/)
|
||||
assert.match(view, /SKU 混箱/)
|
||||
assert.match(view, /warehouse-file-groups--single/)
|
||||
})
|
||||
|
||||
test('贴标申请移除操作项下方的解释文字,保留必要状态', () => {
|
||||
assert.doesNotMatch(view, /选择后仅查询当前客户在该中转仓的可用库存/)
|
||||
assert.doesNotMatch(view, /托盘仅作为库内承载位置/)
|
||||
assert.doesNotMatch(view, /整张贴标申请只能选择一个目的仓/)
|
||||
assert.doesNotMatch(view, /SKU 与箱号关联通过仓库唛头合并/)
|
||||
assert.match(view, /必传 · 二选一/)
|
||||
assert.match(view, /必传 · 两份文件/)
|
||||
})
|
||||
|
||||
test('贴标申请可从当前客户仍有库存的入仓单或退货单选择来源', () => {
|
||||
assert.match(view, /海外操作仓(中转仓)/)
|
||||
assert.match(view, /选择有库存的历史入仓单/)
|
||||
assert.match(view, /曾经下单且当前仍有库存的入仓单/)
|
||||
assert.match(view, /当前中转仓暂无已入库可用库存/)
|
||||
assert.match(view, /选择有库存的入仓单或退货单/)
|
||||
assert.match(view, /quantityUnit: candidate\.sourceType === 'RETURN_CARTON' \? '件' : '箱'/)
|
||||
assert.match(view, /当前中转仓暂无可用库存/)
|
||||
assert.match(view, /relabelInboundOptions/)
|
||||
assert.match(view, /listRelabelInventoryCandidates\(form\.warehouse\)/)
|
||||
assert.match(api, /relabel-applications\/inventory-candidates/)
|
||||
@@ -38,12 +59,11 @@ test('客户贴标表单不展示客户单号、托盘和内部库存来源列',
|
||||
|
||||
test('贴标申请保留关联模板下载和上传入口', () => {
|
||||
assert.match(view, /downloadRelabelTemplate/)
|
||||
assert.match(view, /下载 SKU 关联模板/)
|
||||
assert.match(view, /上传 SKU 关联/)
|
||||
assert.match(view, /下载箱号关联模板/)
|
||||
assert.match(view, /上传箱号关联/)
|
||||
assert.match(view, /下载 SKU 混箱模板/)
|
||||
assert.match(view, /上传 SKU 混箱/)
|
||||
assert.match(view, /<strong>SKU 关联<\/strong>/)
|
||||
assert.match(view, /<strong>箱号关联<\/strong>/)
|
||||
assert.match(view, /<strong>SKU 混箱<\/strong>/)
|
||||
assert.match(view, />下载模板<\/el-button>/)
|
||||
assert.match(view, />上传文件<\/el-button>/)
|
||||
assert.match(view, /skuTemplateInput\?\.click\(\)/)
|
||||
assert.match(view, /cartonTemplateInput\?\.click\(\)/)
|
||||
assert.match(view, /importRelabelTemplate/)
|
||||
@@ -51,7 +71,7 @@ test('贴标申请保留关联模板下载和上传入口', () => {
|
||||
assert.match(view, /'明细行标识', '旧箱唛', '旧SKU标签', '新SKU标签'/)
|
||||
assert.match(view, /const includeDestination = false/)
|
||||
assert.doesNotMatch(view, /const includeDestination = props\.service !== 'transfer'/)
|
||||
assert.match(view, /SKU 与箱号关联通过仓库唛头合并/)
|
||||
assert.match(view, /<strong>绑定明细<\/strong>/)
|
||||
assert.match(view, /<th>仓库唛头 \*<\/th>/)
|
||||
assert.doesNotMatch(view, /库存位置ID/)
|
||||
})
|
||||
@@ -76,7 +96,7 @@ test('上传关系支持部分或全量选择后导入本次换标明细', () =>
|
||||
})
|
||||
|
||||
test('箱唛换标一行固定代表一箱', () => {
|
||||
const cartonTable = view.match(/<div v-if="labelingMode === 'carton'" class="warehouse-table-scroll">.+?(?=<div v-else class="warehouse-table-scroll">)/s)?.[0]
|
||||
const cartonTable = view.match(/<div v-if="!isReturnRelabelSource && labelingMode === 'carton'" class="warehouse-table-scroll">.+?(?=<div v-else class="warehouse-table-scroll">)/s)?.[0]
|
||||
assert.ok(cartonTable)
|
||||
assert.match(cartonTable, /<th>箱数<\/th>/)
|
||||
assert.match(cartonTable, /<td><strong>1 箱<\/strong><\/td>/)
|
||||
@@ -115,22 +135,47 @@ test('箱唛相关换标必须选择四选一派送方式', () => {
|
||||
assert.match(view, /labelingMode\.value !== 'sku' && !form\.deliveryMethod/)
|
||||
})
|
||||
|
||||
test('一次申请单选库存来源且保留普通入仓或退货合箱归属', () => {
|
||||
test('一次申请单选库存来源且退货库存按退货单汇总', () => {
|
||||
assert.doesNotMatch(view, /multiple filterable collapse-tags/)
|
||||
assert.match(view, /RETURN_CARTON:\$\{item\.returnCartonId\}/)
|
||||
assert.match(view, /RETURN_ORDER:\$\{returnOrderNo\}/)
|
||||
assert.match(view, /current\.availablePackages \+= inventoryQuantity\(candidate\.availablePackages\)/)
|
||||
assert.match(view, /availablePackages: inventoryQuantity\(candidate\.availablePackages\)/)
|
||||
assert.match(view, /const quantity = Number\(value\)/)
|
||||
assert.match(view, /interface ReturnRelabelSkuRow/)
|
||||
assert.match(view, /current\.availableQuantity \+= quantity/)
|
||||
assert.match(view, /库存总数/)
|
||||
assert.match(view, /本次贴标数量/)
|
||||
assert.match(view, /returnRelabelSkuRows\.value = \[\.\.\.grouped\.values\(\)\]/)
|
||||
assert.match(view, /createReturnRelabel\(selectedReturnOrderNo\.value/)
|
||||
assert.match(view, /uploadReturnRelabelFile\(created\.relabelNo, fileScope, selectedFile\)/)
|
||||
assert.doesNotMatch(view, /candidate\.sourceType === 'RETURN_CARTON'\) rows\.push\(\.\.\.Array\.from/)
|
||||
assert.match(view, /sourceInventoryBalanceId: String\(candidate\.inventoryBalanceId\)/)
|
||||
assert.match(view, /sourceInventoryPositionId: String\(position\.positionId\)/)
|
||||
assert.match(view, /sourceInboundOrderId: String\(candidate\.inboundOrderId\)/)
|
||||
assert.match(view, /returnCartonId: String\(candidate\.returnCartonId\)/)
|
||||
assert.match(api, /sourceInventoryBalanceId\?: string; sourceInventoryPositionId\?: string; sourceInboundOrderId\?: string; returnCartonId\?: string/)
|
||||
})
|
||||
|
||||
test('退货贴标总文件二选一且提交使用退货专用链路', () => {
|
||||
assert.match(view, /cartonTotalFile\.value && !requiresBothTotalFiles\.value\) clearCreateTotalFile\('SKU_TOTAL'\)/)
|
||||
assert.match(view, /skuTotalFile\.value && !requiresBothTotalFiles\.value\) clearCreateTotalFile\('CARTON_TOTAL'\)/)
|
||||
assert.match(view, /fileScope = cartonTotalFile\.value \? 'CARTON_TOTAL' as const : 'SKU_TOTAL' as const/)
|
||||
assert.match(view, /destinationFbaWarehouseId: String\(destination\.id\)/)
|
||||
})
|
||||
|
||||
test('SKU 和箱唛都换时两份总文件必须同时上传', () => {
|
||||
assert.match(view, /labelingMode\.value === 'carton_and_sku'/)
|
||||
assert.match(view, /requiresBothTotalFiles\.value && \(!cartonTotalFile\.value \|\| !skuTotalFile\.value\)/)
|
||||
assert.match(view, /必须同时上传箱唛总文件和 SKU 总文件/)
|
||||
assert.match(view, /requiresBothTotalFiles \? '和' : '或'/)
|
||||
assert.match(view, /必传 · 两份文件/)
|
||||
})
|
||||
|
||||
test('海外操作仓和FBA目的仓使用独立字段', () => {
|
||||
assert.match(api, /operationWarehouseId: string/)
|
||||
assert.match(api, /destinationFbaWarehouseCode\?: string/)
|
||||
assert.match(view, /operationWarehouseId: form\.warehouse/)
|
||||
assert.match(view, /destinationFbaWarehouseCode: form\.destination/)
|
||||
assert.match(view, /整张贴标申请只能选择一个目的仓/)
|
||||
assert.match(view, /<el-select v-model="form\.destination"[^\n]+FBA 目的仓/)
|
||||
assert.doesNotMatch(view, /v-model="row\.destinationWarehouseId"[^\n]+请选择 FBA 目的仓/)
|
||||
})
|
||||
|
||||
@@ -179,5 +224,6 @@ test('客户详情进入页面后直接回显完工照片并支持大图预览',
|
||||
})
|
||||
|
||||
test('进入复核后客户订单回显换标完成状态', () => {
|
||||
assert.match(view, /\['CUSTOMER_CONFIRM_PENDING', 'READY_FOR_OUTBOUND', 'COMPLETED'\]\.includes\(value\) \? '换标完成'/)
|
||||
assert.match(view, /value === 'CUSTOMER_CONFIRM_PENDING'\) return '待客户确认'/)
|
||||
assert.match(view, /\['READY_FOR_OUTBOUND', 'COMPLETED'\]\.includes\(value\) \? '换标完成'/)
|
||||
})
|
||||
|
||||
@@ -5,6 +5,12 @@ import test from 'node:test'
|
||||
const view = readFileSync(new URL('./WarehouseServiceView.vue', import.meta.url), 'utf8')
|
||||
const api = readFileSync(new URL('../api/warehouseServices.ts', import.meta.url), 'utf8')
|
||||
|
||||
test('贴标总文件与关联模板保持左右分组,退货来源使用专用明细', () => {
|
||||
assert.match(view, /class="warehouse-file-groups" :class="\{ 'warehouse-file-groups--single': isReturnRelabelSource \}"/)
|
||||
assert.match(view, /warehouse-service-page--return-relabel/)
|
||||
assert.match(view, /const sourceValue = `RETURN_ORDER:\$\{requestedReturnNo\}`/)
|
||||
})
|
||||
|
||||
test('仓储转运先选择入仓方式并按方式切换地址信息', () => {
|
||||
assert.match(view, /value="WAREHOUSE_PICKUP">仓库上门提货/)
|
||||
assert.match(view, /value="CUSTOMER_DELIVERY">客户送货到仓/)
|
||||
@@ -14,14 +20,14 @@ test('仓储转运先选择入仓方式并按方式切换地址信息', () => {
|
||||
})
|
||||
|
||||
test('仓储转运支持无目的地、FBA 仓和私人仓并显式选择是否换标', () => {
|
||||
assert.match(view, /是否有目的去向/)
|
||||
assert.match(view, />目的去向</)
|
||||
assert.match(view, /value="NONE">暂未确定/)
|
||||
assert.match(view, /value="FBA">FBA 仓/)
|
||||
assert.match(view, /value="PRIVATE">私人仓/)
|
||||
assert.match(view, /transferDestinationType === 'PRIVATE'.*私人仓地址.*邮编.*私人仓联系人.*联系电话/s)
|
||||
assert.match(view, /是否需要换标/)
|
||||
assert.match(view, /:value="false">不需要换标/)
|
||||
assert.match(view, /:value="true">需要换标/)
|
||||
assert.match(view, />换标服务</)
|
||||
assert.match(view, /:value="false">不需要/)
|
||||
assert.match(view, /:value="true">需要/)
|
||||
assert.match(api, /fbaWarehouseCode\?: string/)
|
||||
assert.match(api, /privateDestinationAddress\?: string; privateDestinationPostalCode\?: string/)
|
||||
assert.match(api, /fbaShipmentNo\?: string/)
|
||||
@@ -30,13 +36,11 @@ test('仓储转运支持无目的地、FBA 仓和私人仓并显式选择是否
|
||||
})
|
||||
|
||||
test('仓储转运换标整票只允许一个目的仓', () => {
|
||||
assert.match(view, /props\.service === 'transfer' && transferDestinationType === 'FBA'[^\n]+未换标货物目的 FBA 仓/)
|
||||
assert.match(view, /此处只表示不换标箱子的去向/)
|
||||
assert.match(view, /props\.service === 'transfer' && transferDestinationType === 'FBA'[^\n]+FBA 目的仓/)
|
||||
assert.match(view, /FBA 货件编号<\/span><el-input v-model="form\.fbaShipmentNo" placeholder="选填,可后续补充"/)
|
||||
assert.match(view, /transferDestinationType\.value === 'FBA' && !form\.destination/)
|
||||
assert.match(view, /transferDestinationType\.value === 'PRIVATE'.*!form\.privateDestinationPostalCode/)
|
||||
assert.match(view, /整票换标货物目的 FBA 仓/)
|
||||
assert.match(view, /暂不支持一票多个目的仓/)
|
||||
assert.match(view, /if \(!transferRelabelDefaultDestination\.value\) \{ ElMessage\.warning\('请选择整票换标货物的 FBA 目的仓'\)/)
|
||||
assert.match(view, /destinationWarehouseCode: transferRelabelDefaultDestination\.value \|\| undefined/)
|
||||
const transferRelabelSection = view.split('warehouse-transfer-relabel">')[1]?.split('warehouse-transfer-submit-bar')[0] || ''
|
||||
@@ -56,15 +60,14 @@ test('仓储转运不要求客户填写内部入仓号和我方箱唛,但换
|
||||
assert.doesNotMatch(transferConfig, /进仓编号/)
|
||||
assert.doesNotMatch(transferSubmit, /inboundCode|sourceLastlegOrderIds/)
|
||||
assert.match(view, /货物明细/)
|
||||
assert.match(view, /客户只填写自己的箱唛,我方箱唛由系统生成/)
|
||||
assert.match(view, /客户箱唛号 \*/)
|
||||
assert.match(view, /downloadTransferCartonMarkTemplate/)
|
||||
assert.match(view, /importTransferCartonMarks/)
|
||||
assert.match(view, /下载 SKU 关联模板/)
|
||||
assert.match(view, /上传 SKU 关联/)
|
||||
assert.match(view, /下载箱号关联模板/)
|
||||
assert.match(view, /上传箱号关联/)
|
||||
assert.match(view, /下载 SKU 混箱模板/)
|
||||
assert.match(view, /上传 SKU 混箱/)
|
||||
assert.match(view, /<strong>SKU 关联<\/strong>/)
|
||||
assert.match(view, /<strong>箱号关联<\/strong>/)
|
||||
assert.match(view, /<strong>SKU 混箱<\/strong>/)
|
||||
assert.match(view, />下载模板<\/el-button>/)
|
||||
assert.match(view, />上传文件<\/el-button>/)
|
||||
assert.match(view, /换标类型/)
|
||||
assert.match(view, /v-if="labelingMode === 'carton'"/)
|
||||
assert.match(view, /v-else-if="labelingMode === 'sku'"/)
|
||||
@@ -80,9 +83,50 @@ test('仓储转运不要求客户填写内部入仓号和我方箱唛,但换
|
||||
assert.doesNotMatch(view, /<th v-if="props\.service === 'transfer'">FNSKU \/ 箱唛<\/th>/)
|
||||
})
|
||||
|
||||
test('仓储转运货物明细可按 SKU 和箱数打印客户预贴箱唛', () => {
|
||||
assert.match(view, /printTransferDraftCartonMarks/)
|
||||
assert.match(view, /请先填写商品 SKU 和对应的整数箱数/)
|
||||
assert.match(view, /Array\.from\(\{ length: Number\(row\.quantity\) \}/)
|
||||
assert.match(view, /row\.customerCartonMarks = marks\.join\('\\n'\)/)
|
||||
assert.match(view, /仓储转运客户箱唛/)
|
||||
assert.match(view, /@click="printTransferDraftCartonMarks"/)
|
||||
assert.match(view, /打印唛头/)
|
||||
assert.match(view, /单次最多打印 500 张箱唛/)
|
||||
})
|
||||
|
||||
test('仓储转运换标资料按步骤分组展示', () => {
|
||||
assert.match(view, /<span v-if="props\.service === 'transfer'" class="warehouse-step-number">1<\/span>货物明细/)
|
||||
assert.match(view, /<span class="warehouse-step-number">2<\/span>换标申请/)
|
||||
assert.match(view, /<strong><span>3<\/span>明细关联模板<\/strong>/)
|
||||
assert.match(view, /<span class="warehouse-step-number">4<\/span>绑定明细/)
|
||||
assert.match(view, /warehouse-transfer-template-group/)
|
||||
assert.match(view, /warehouse-transfer-relabel/)
|
||||
})
|
||||
|
||||
test('仓储转运需要换标时上传总文件并挂到关联换标单', () => {
|
||||
const transferRelabelSection = view.split('warehouse-transfer-relabel">')[1]?.split('warehouse-transfer-submit-bar')[0] || ''
|
||||
assert.match(transferRelabelSection, /warehouse-transfer-total-files/)
|
||||
assert.match(transferRelabelSection, />贴标总文件</)
|
||||
assert.match(transferRelabelSection, />箱唛总文件</)
|
||||
assert.match(transferRelabelSection, />SKU 总文件</)
|
||||
assert.match(view, /action === 'submit' && requiresBothTotalFiles\.value/)
|
||||
assert.match(view, /const submitted = await submitTransfer\(transferNo\)/)
|
||||
assert.match(view, /submitted\.relabelNo \|\| created\.relabelNo/)
|
||||
assert.match(view, /uploadRelabelFile\(relabelNo, 'CARTON_TOTAL', cartonTotalFile\.value\)/)
|
||||
assert.match(view, /uploadRelabelFile\(relabelNo, 'SKU_TOTAL', skuTotalFile\.value\)/)
|
||||
assert.match(view, /转运单已提交,附件待补传/)
|
||||
})
|
||||
|
||||
test('仓储转运换标总文件与明细模板在桌面端左右排列', () => {
|
||||
assert.match(view, /class="warehouse-transfer-file-groups"/)
|
||||
assert.match(view, /\.warehouse-transfer-file-groups\{display:grid;grid-template-columns:minmax\(0,1\.15fr\) minmax\(360px,\.85fr\)/)
|
||||
assert.match(view, /\.warehouse-transfer-total-files,\.warehouse-transfer-template-group\{height:100%;margin:0\}/)
|
||||
assert.match(view, /@media\(max-width:1180px\)\{\.warehouse-transfer-file-groups\{grid-template-columns:minmax\(0,1fr\)\}\}/)
|
||||
})
|
||||
|
||||
test('上门提货不提交物流号,需要换标时随转运单登记申请', () => {
|
||||
assert.match(view, /trackingNo:\s*form\.deliveryMethod === 'CUSTOMER_DELIVERY'\s*\?[^:]+:\s*undefined/)
|
||||
assert.match(view, /本次提交将同时登记换标申请/)
|
||||
assert.match(view, /relabelItems: form\.relabel/)
|
||||
assert.match(view, /form\.deliveryMethod === 'CUSTOMER_DELIVERY'.*物流跟踪号/s)
|
||||
assert.match(view, /提货联系人/)
|
||||
assert.match(view, /联系人电话/)
|
||||
@@ -109,7 +153,7 @@ test('仓储转运列表操作仓使用海外仓接口数据', () => {
|
||||
test('仓储转运预计发货时间可选并只读展示实际发货时间', () => {
|
||||
assert.match(view, /预计发货时间/)
|
||||
assert.match(view, /v-model="form\.estimatedShipAt"/)
|
||||
assert.match(view, /选填,可线下确认后补充/)
|
||||
assert.match(view, /v-model="form\.estimatedShipAt"[^>]+placeholder="选填"/)
|
||||
assert.doesNotMatch(view, /!form\.estimatedShipAt && '预计发货时间'/)
|
||||
assert.match(view, /estimatedShipAt: form\.estimatedShipAt/)
|
||||
assert.match(view, /实际发货时间由仓库出库或 PDA 完成后回填/)
|
||||
|
||||
@@ -56,7 +56,7 @@ test('organises customer-relevant business pages under five task-oriented sideba
|
||||
},
|
||||
{
|
||||
title: '海外仓',
|
||||
children: ['贴标申请', '仓储转运', '货物废弃', '退货预报与签收'],
|
||||
children: ['贴标申请', '仓储转运', '货物废弃', '退货换标'],
|
||||
},
|
||||
{
|
||||
title: '费用与账单',
|
||||
|
||||
@@ -65,7 +65,7 @@ export const clientNavigation: ClientNavigationNode[] = [
|
||||
requireLeaf('overseas-labeling'),
|
||||
requireLeaf('overseas-storage-transfer'),
|
||||
requireLeaf('overseas-cargo-disposal'),
|
||||
requireLeaf('overseas-return-relabel', '退货预报与签收'),
|
||||
requireLeaf('overseas-return-relabel', '退货换标'),
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user