Release/test 合并上生产 #1
@@ -0,0 +1,33 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import test from 'node:test'
|
||||
import ts from 'typescript'
|
||||
|
||||
const source = await readFile(new URL('./types.ts', import.meta.url), 'utf8')
|
||||
const compiled = ts.transpileModule(source, {
|
||||
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
|
||||
}).outputText
|
||||
const { isApiFailure, isApiEnvelope } = await import(`data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`)
|
||||
|
||||
test('recognizes a security failure without data and preserves diagnostics', () => {
|
||||
const failure = { code: 'A0403', message: 'Access denied', requestId: 'request-1', traceId: 'trace-1' }
|
||||
assert.equal(isApiFailure(failure), true)
|
||||
assert.equal(isApiEnvelope(failure), false)
|
||||
assert.equal(failure.requestId, 'request-1')
|
||||
assert.equal(failure.traceId, 'trace-1')
|
||||
})
|
||||
|
||||
test('does not accept malformed or data-less successful responses', () => {
|
||||
for (const value of [null, {}, { code: 403, message: 'Denied' }, { code: 'A0403' },
|
||||
{ code: 'A0403', message: 'Denied', traceId: {} }, { code: '200', message: 'OK' }]) {
|
||||
assert.equal(isApiFailure(value), false)
|
||||
}
|
||||
assert.equal(isApiEnvelope({ code: '200', message: 'OK' }), false)
|
||||
assert.equal(isApiEnvelope({ code: '200', message: 'OK', data: [] }), true)
|
||||
})
|
||||
|
||||
test('overseas transport handles business-code and HTTP security failures', async () => {
|
||||
const transport = await readFile(new URL('./overseasHttp.ts', import.meta.url), 'utf8')
|
||||
assert.match(transport, /if \(isApiFailure\(response.data\)\)/)
|
||||
assert.match(transport, /isApiEnvelope\(body\) \|\| isApiFailure\(body\)/)
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios, { type AxiosRequestConfig, type InternalAxiosRequestConfig } from 'axios'
|
||||
import { parseApiJsonResponse } from '@chongchong/shared'
|
||||
import { ApiError, isApiEnvelope, type ApiEnvelope } from '@/api/types'
|
||||
import { ApiError, isApiEnvelope, isApiFailure } from '@/api/types'
|
||||
import { getOverseasAccessToken } from '@/api/session'
|
||||
import { refreshAuthenticatedSession } from '@/api/http'
|
||||
|
||||
@@ -37,7 +37,11 @@ transport.interceptors.response.use((response) => response, async (error) => {
|
||||
|
||||
export async function overseasApiRequest<T>(config: AxiosRequestConfig) {
|
||||
try {
|
||||
const response = await transport.request<ApiEnvelope<T>>(config)
|
||||
const response = await transport.request<unknown>(config)
|
||||
if (isApiFailure(response.data)) {
|
||||
throw new ApiError(response.data.message, response.data.code, response.status, undefined,
|
||||
response.data.traceId, response.data.requestId)
|
||||
}
|
||||
if (!isApiEnvelope(response.data) || response.data.code !== '200') {
|
||||
throw new ApiError(
|
||||
isApiEnvelope(response.data) ? response.data.message : '海外接口响应格式错误',
|
||||
@@ -48,12 +52,12 @@ export async function overseasApiRequest<T>(config: AxiosRequestConfig) {
|
||||
isApiEnvelope(response.data) ? response.data.requestId : null,
|
||||
)
|
||||
}
|
||||
return response.data.data
|
||||
return response.data.data as T
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) throw error
|
||||
if (axios.isAxiosError(error)) {
|
||||
const body = error.response?.data
|
||||
if (isApiEnvelope(body)) {
|
||||
if (isApiEnvelope(body) || isApiFailure(body)) {
|
||||
throw new ApiError(body.message, body.code, error.response?.status, undefined, body.traceId, body.requestId)
|
||||
}
|
||||
throw new ApiError(error.code === 'ERR_NETWORK' ? '无法连接海外服务' : error.message, error.code || 'OVERSEAS_HTTP_ERROR', error.response?.status)
|
||||
|
||||
@@ -30,3 +30,13 @@ export function isApiEnvelope(value: unknown): value is ApiEnvelope<unknown> {
|
||||
const record = value as Record<string, unknown>
|
||||
return typeof record.code === 'string' && typeof record.message === 'string' && 'data' in record
|
||||
}
|
||||
|
||||
// Security failures have no business data, unlike successful API responses.
|
||||
export function isApiFailure(value: unknown): value is Omit<ApiEnvelope<never>, 'data'> {
|
||||
if (!value || typeof value !== 'object') return false
|
||||
const record = value as Record<string, unknown>
|
||||
return typeof record.code === 'string' && record.code !== '200'
|
||||
&& typeof record.message === 'string'
|
||||
&& (record.requestId == null || typeof record.requestId === 'string')
|
||||
&& (record.traceId == null || typeof record.traceId === 'string')
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { useFirstLegOrdersStore } from './store/firstLegOrders'
|
||||
export { getMerchantWarehouses, type MerchantWarehouse } from './api/firstLeg'
|
||||
export { getFirstLegStatusLabel, getFirstLegStatusTone } from './utils/firstLegOrder'
|
||||
export { default as InboundCodeAssociationDialog } from './components/InboundCodeAssociationDialog.vue'
|
||||
|
||||
@@ -13,6 +13,15 @@ export interface ReturnInventorySummaryItem {
|
||||
|
||||
export interface ReturnInventoryCartonItem { skuCode: string; productNameZh?: string; quantity: number }
|
||||
|
||||
export interface ReturnInventoryCartonSummary {
|
||||
cartonMark: string; overseasWarehouseId: string; sourceBatchNo?: string; sourceReturnNo?: string; status: string
|
||||
skuTypes: number; totalQuantity: number; length?: number; width?: number; height?: number; weight?: number; createdAt: string
|
||||
}
|
||||
|
||||
export function listReturnInventoryCartons(params: Record<string, unknown>) {
|
||||
return overseasApiRequest<PageResult<ReturnInventoryCartonSummary>>({ method: 'GET', url: '/merchant/v1/last-leg/return-inventory/cartons', params })
|
||||
}
|
||||
|
||||
export interface ReturnInventoryCartonDetail {
|
||||
cartonMark: string; overseasWarehouseId: string; sourceBatchNo?: string; sourceReturnNo?: string; status: string
|
||||
skuTypes: number; totalQuantity: number; length?: number; width?: number; height?: number; weight?: number; receivedAt: string; items: ReturnInventoryCartonItem[]
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const inventory = readFileSync(new URL('./ReturnInventoryView.vue', import.meta.url), 'utf8')
|
||||
const orders = readFileSync(new URL('./ReturnOrdersView.vue', import.meta.url), 'utf8')
|
||||
|
||||
test('inventory failures are visible and are not presented as empty inventory', () => {
|
||||
assert.match(inventory, /catch \(error\)/)
|
||||
assert.match(inventory, /<el-alert v-if="loadError"/)
|
||||
assert.equal((inventory.match(/:empty-text="loadError \?/g) || []).length, 3)
|
||||
assert.match(inventory, /当前账号无权访问退货库存/)
|
||||
assert.match(inventory, /error\.requestId/)
|
||||
assert.match(inventory, /error\.traceId/)
|
||||
assert.match(inventory, /summaryRows\.value = \[\]/)
|
||||
assert.match(inventory, /cartonRows\.value = \[\]/)
|
||||
})
|
||||
|
||||
test('return pagination receives numeric totals from the wire format', () => {
|
||||
assert.equal((inventory.match(/total\.value = Number\(page.total\)/g) || []).length, 3)
|
||||
assert.match(orders, /total\.value = Number\(response.total\)/)
|
||||
})
|
||||
|
||||
test('inventory actions lead both tables and timestamps describe inventory changes', () => {
|
||||
const tables = inventory.match(/<el-table\b[^>]*>[\s\S]*?<\/el-table>/g).slice(0, 3)
|
||||
for (const table of tables) {
|
||||
assert.match(table, /<el-table\b[^>]*>\s*<el-table-column label="操作"[^>]*fixed="left"/)
|
||||
}
|
||||
assert.match(inventory, /prop="updatedAt" label="库存更新时间"/)
|
||||
assert.doesNotMatch(inventory, /实收并上架|最近入库时间/)
|
||||
})
|
||||
|
||||
test('carton management uses server carton pagination rather than deduplicating SKU pages', () => {
|
||||
assert.match(inventory, /if \(props.cartonOnly\) \{\s*const page = await listReturnInventoryCartons/)
|
||||
assert.match(inventory, /:data="cartonSummaryRows" row-key="cartonMark"/)
|
||||
assert.match(inventory, /cartonSummaryRows.value = \[\]/)
|
||||
})
|
||||
|
||||
test('printing opens its window before awaiting the label and closes on failure', () => {
|
||||
const print = inventory.slice(inventory.indexOf('async function printLabel'), inventory.indexOf('onMounted('))
|
||||
assert.ok(print.indexOf('window.open(') < print.indexOf('await getReturnInventoryLabel('))
|
||||
assert.match(print, /if \(printWindow.closed\) return/)
|
||||
assert.match(print, /catch \(error\) \{\s*printWindow.close\(\)/)
|
||||
})
|
||||
|
||||
test('warehouse selectors and displays use the catalog instead of internal IDs', () => {
|
||||
assert.match(inventory, /from '@\/modules\/first-leg\/public'/)
|
||||
assert.match(inventory, /warehouseType: 'OVERSEAS', page, size: 200/)
|
||||
assert.match(inventory, /warehouseLabel\(cartonDetail.overseasWarehouseId\)/)
|
||||
assert.match(inventory, /warehouse\.warehouseName/)
|
||||
assert.match(inventory, /warehouse\.warehouseCode/)
|
||||
assert.doesNotMatch(inventory, /海外仓 ID|\{\{ cartonDetail.overseasWarehouseId \}\}|prop="overseasWarehouseId"/)
|
||||
})
|
||||
@@ -2,11 +2,15 @@
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Printer, Search } from '@lucide/vue'
|
||||
import { ApiError } from '@/api/types'
|
||||
import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-leg/public'
|
||||
import {
|
||||
getReturnInventoryCarton,
|
||||
getReturnInventoryLabel,
|
||||
listReturnInventory,
|
||||
listReturnInventorySummary,
|
||||
listReturnInventoryCartons,
|
||||
type ReturnInventoryCartonSummary,
|
||||
type ReturnInventoryCartonDetail,
|
||||
type ReturnInventoryItem,
|
||||
type ReturnInventorySummaryItem,
|
||||
@@ -16,8 +20,33 @@ const activeTab = ref<'summary' | 'cartons'>('summary')
|
||||
const props = defineProps<{ cartonOnly?: boolean }>()
|
||||
if (props.cartonOnly) activeTab.value = 'cartons'
|
||||
const loading = ref(false)
|
||||
const loadError = ref('')
|
||||
const warehouses = ref<MerchantWarehouse[]>([])
|
||||
const warehouseError = ref('')
|
||||
const warehousesLoading = ref(false)
|
||||
function warehouseLabel(warehouseId: string) {
|
||||
const warehouse = warehouses.value.find(item => String(item.id) === String(warehouseId))
|
||||
return warehouse ? `${warehouse.warehouseName} · ${warehouse.warehouseCode}` : '—'
|
||||
}
|
||||
async function loadWarehouses() {
|
||||
warehousesLoading.value = true
|
||||
warehouseError.value = ''
|
||||
try {
|
||||
const result: MerchantWarehouse[] = []
|
||||
for (let page = 1; ; page++) {
|
||||
const response = await getMerchantWarehouses({ warehouseType: 'OVERSEAS', page, size: 200 })
|
||||
result.push(...response.records)
|
||||
if (!response.records.length || result.length >= Number(response.total)) break
|
||||
}
|
||||
warehouses.value = result
|
||||
} catch (error) {
|
||||
warehouses.value = []
|
||||
warehouseError.value = error instanceof Error ? error.message : '海外仓目录加载失败,请重试'
|
||||
} finally { warehousesLoading.value = false }
|
||||
}
|
||||
const summaryRows = ref<ReturnInventorySummaryItem[]>([])
|
||||
const cartonRows = ref<ReturnInventoryItem[]>([])
|
||||
const cartonSummaryRows = ref<ReturnInventoryCartonSummary[]>([])
|
||||
const total = ref(0)
|
||||
const showCartonDetail = ref(false)
|
||||
const cartonDetail = ref<ReturnInventoryCartonDetail>()
|
||||
@@ -37,16 +66,35 @@ function params(includeCartonMark: boolean) {
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
loadError.value = ''
|
||||
try {
|
||||
if (props.cartonOnly) {
|
||||
const page = await listReturnInventoryCartons(params(true))
|
||||
cartonSummaryRows.value = page.records
|
||||
total.value = Number(page.total)
|
||||
return
|
||||
}
|
||||
if (activeTab.value === 'summary') {
|
||||
const page = await listReturnInventorySummary(params(false))
|
||||
summaryRows.value = page.records
|
||||
total.value = page.total
|
||||
total.value = Number(page.total)
|
||||
return
|
||||
}
|
||||
const page = await listReturnInventory(params(true))
|
||||
cartonRows.value = page.records
|
||||
total.value = page.total
|
||||
total.value = Number(page.total)
|
||||
} catch (error) {
|
||||
summaryRows.value = []
|
||||
cartonRows.value = []
|
||||
cartonSummaryRows.value = []
|
||||
total.value = 0
|
||||
const message = error instanceof ApiError && (error.code === 'A0403' || error.status === 403)
|
||||
? '当前账号无权访问退货库存,请联系管理员核对海外库存权限。'
|
||||
: error instanceof Error ? error.message : '退货库存加载失败,请重试'
|
||||
const diagnostics = error instanceof ApiError
|
||||
? [error.code, error.requestId && `requestId: ${error.requestId}`, error.traceId && `traceId: ${error.traceId}`].filter(Boolean).join(' · ')
|
||||
: ''
|
||||
loadError.value = diagnostics ? `${message}(${diagnostics})` : message
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
@@ -67,51 +115,71 @@ async function openCarton(cartonMark: string) {
|
||||
}
|
||||
|
||||
async function printLabel(cartonMark: string) {
|
||||
const printWindow = window.open('', '_blank')
|
||||
if (!printWindow) { ElMessage.warning('浏览器阻止了打印窗口,请允许弹窗后重试'); return }
|
||||
printWindow.opener = null
|
||||
printWindow.document.title = '正在加载退货箱唛'
|
||||
printWindow.document.body.textContent = '正在加载退货箱唛,请稍候…'
|
||||
try {
|
||||
const label = await getReturnInventoryLabel(cartonMark)
|
||||
const printWindow = window.open('', '_blank')
|
||||
if (!printWindow) { ElMessage.warning('浏览器阻止了打印窗口,请允许弹窗后重试'); return }
|
||||
printWindow.opener = null
|
||||
if (printWindow.closed) return
|
||||
printWindow.document.open()
|
||||
printWindow.document.write(label.printableHtml)
|
||||
printWindow.document.close()
|
||||
printWindow.focus()
|
||||
printWindow.print()
|
||||
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '箱唛加载失败') }
|
||||
} catch (error) {
|
||||
printWindow.close()
|
||||
ElMessage.error(error instanceof Error ? error.message : '箱唛加载失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
onMounted(() => { void load(); void loadWarehouses() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="return-inventory-page">
|
||||
<div class="page-heading"><div><h2>{{ props.cartonOnly ? '退货箱号管理' : '退货库存管理' }}</h2><p>{{ props.cartonOnly ? '查询合箱完成后的退货箱号、箱规和箱内 SKU 明细。' : '库存数量以仓库 PDA 实收并上架的数量为准。' }}</p></div></div>
|
||||
<div class="page-heading"><div><h2>{{ props.cartonOnly ? '退货箱号管理' : '退货库存管理' }}</h2><p>{{ props.cartonOnly ? '查询签收及合箱生成的退货箱号、箱规和箱内 SKU 明细。' : '库存数量以仓库 PDA 确认实收后的入账数量为准。' }}</p></div></div>
|
||||
<el-tabs v-if="!props.cartonOnly" v-model="activeTab" @tab-change="changeTab"><el-tab-pane label="SKU 汇总库存" name="summary" /><el-tab-pane label="箱号明细" name="cartons" /></el-tabs>
|
||||
<div class="filter-bar">
|
||||
<el-input v-model="query.overseasWarehouseId" clearable placeholder="海外仓 ID" @keyup.enter="search" />
|
||||
<el-select v-model="query.overseasWarehouseId" clearable filterable placeholder="海外仓" :loading="warehousesLoading"><el-option v-for="warehouse in warehouses" :key="warehouse.id" :label="warehouseLabel(String(warehouse.id))" :value="String(warehouse.id)" /></el-select>
|
||||
<el-input v-model="query.skuCode" clearable placeholder="SKU" @keyup.enter="search" />
|
||||
<el-input v-if="activeTab === 'cartons'" v-model="query.cartonMark" clearable placeholder="箱号 / 箱唛" @keyup.enter="search" />
|
||||
<el-input v-if="activeTab === 'cartons'" v-model="query.returnNo" clearable placeholder="退货单号" @keyup.enter="search" />
|
||||
<el-select v-if="activeTab === 'cartons'" v-model="query.status" clearable placeholder="箱状态"><el-option label="可用" value="AVAILABLE" /><el-option label="已合箱" value="MERGED" /></el-select>
|
||||
<el-button type="primary" @click="search"><Search :size="16" />查询</el-button>
|
||||
</div>
|
||||
<el-table v-if="activeTab === 'summary'" v-loading="loading" :data="summaryRows">
|
||||
<el-table-column prop="overseasWarehouseId" label="海外仓" min-width="150" /><el-table-column prop="skuCode" label="SKU" min-width="180" />
|
||||
<el-alert v-if="loadError" :title="loadError" type="error" show-icon :closable="false" />
|
||||
<el-alert v-if="warehouseError" :title="warehouseError" type="warning" show-icon :closable="false"><el-button link type="primary" @click="loadWarehouses">重试加载海外仓</el-button></el-alert>
|
||||
<el-table v-if="activeTab === 'summary'" v-loading="loading" :data="summaryRows" :empty-text="loadError ? '库存加载失败,请核对权限或重试' : '暂无数据'">
|
||||
<el-table-column label="操作" width="110" fixed="left"><template #default="{ row }"><el-button link type="primary" @click="openSkuCartons(row.skuCode)">查看箱号</el-button></template></el-table-column>
|
||||
<el-table-column label="海外仓" min-width="150"><template #default="{ row }">{{ warehouseLabel(row.overseasWarehouseId) }}</template></el-table-column><el-table-column prop="skuCode" label="SKU" min-width="180" />
|
||||
<el-table-column prop="productNameZh" label="中文品名" min-width="190" show-overflow-tooltip /><el-table-column prop="availableQuantity" label="可用库存" min-width="120" align="right" />
|
||||
<el-table-column prop="cartonQuantity" label="所在箱数" min-width="120" align="right" /><el-table-column prop="updatedAt" label="最近入库时间" min-width="180" />
|
||||
<el-table-column label="操作" width="110" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openSkuCartons(row.skuCode)">查看箱号</el-button></template></el-table-column>
|
||||
<el-table-column prop="cartonQuantity" label="所在箱数" min-width="120" align="right" /><el-table-column prop="updatedAt" label="库存更新时间" min-width="180" />
|
||||
</el-table>
|
||||
<el-table v-else v-loading="loading" :data="cartonRows">
|
||||
<el-table v-else-if="props.cartonOnly" v-loading="loading" :data="cartonSummaryRows" row-key="cartonMark" :empty-text="loadError ? '库存加载失败,请核对权限或重试' : '暂无数据'">
|
||||
<el-table-column label="操作" width="150" fixed="left"><template #default="{ row }"><el-button link type="primary" @click="openCarton(row.cartonMark)">详情</el-button><el-button link type="primary" @click="printLabel(row.cartonMark)"><Printer :size="14" />箱唛</el-button></template></el-table-column>
|
||||
<el-table-column prop="cartonMark" label="箱号" min-width="235" />
|
||||
<el-table-column label="海外仓" min-width="150"><template #default="{ row }">{{ warehouseLabel(row.overseasWarehouseId) }}</template></el-table-column>
|
||||
<el-table-column prop="skuTypes" label="箱内 SKU 种类" min-width="130" align="right" />
|
||||
<el-table-column prop="totalQuantity" label="箱内总件数" min-width="115" align="right" />
|
||||
<el-table-column prop="sourceReturnNo" label="来源退货单" min-width="235" />
|
||||
<el-table-column prop="sourceBatchNo" label="签收批次" min-width="190" />
|
||||
<el-table-column label="箱规" min-width="160"><template #default="{ row }">{{ row.length && row.width && row.height ? `${row.length} × ${row.width} × ${row.height} cm` : '—' }}</template></el-table-column>
|
||||
<el-table-column label="重量" min-width="100"><template #default="{ row }">{{ row.weight ? `${row.weight} kg` : '—' }}</template></el-table-column>
|
||||
<el-table-column prop="createdAt" label="箱号生成时间" min-width="175" />
|
||||
</el-table>
|
||||
<el-table v-else v-loading="loading" :data="cartonRows" :empty-text="loadError ? '库存加载失败,请核对权限或重试' : '暂无数据'">
|
||||
<el-table-column label="操作" width="150" fixed="left"><template #default="{ row }"><el-button link type="primary" @click="openCarton(row.cartonMark)">详情</el-button><el-button link type="primary" @click="printLabel(row.cartonMark)"><Printer :size="14" />箱唛</el-button></template></el-table-column>
|
||||
<el-table-column prop="cartonMark" label="箱号" min-width="235"><template #default="{ row }"><el-button link type="primary" @click="openCarton(row.cartonMark)">{{ row.cartonMark }}</el-button></template></el-table-column>
|
||||
<el-table-column prop="overseasWarehouseId" label="海外仓" min-width="130" /><el-table-column prop="skuCode" label="SKU" min-width="150" />
|
||||
<el-table-column label="海外仓" min-width="150"><template #default="{ row }">{{ warehouseLabel(row.overseasWarehouseId) }}</template></el-table-column><el-table-column prop="skuCode" label="SKU" min-width="150" />
|
||||
<el-table-column prop="productNameZh" label="中文品名" min-width="170" show-overflow-tooltip /><el-table-column prop="quantity" label="本箱 SKU 数量" min-width="135" align="right" />
|
||||
<el-table-column prop="cartonSkuTypes" label="箱内 SKU 种类" min-width="130" align="right" /><el-table-column prop="cartonTotalQuantity" label="箱内总件数" min-width="115" align="right" />
|
||||
<el-table-column prop="status" label="箱状态" min-width="100" /><el-table-column prop="sourceBatchNo" label="签收批次" min-width="190" /><el-table-column prop="receivedAt" label="签收时间" min-width="175" />
|
||||
<el-table-column label="操作" width="150" fixed="right"><template #default="{ row }"><el-button link type="primary" @click="openCarton(row.cartonMark)">详情</el-button><el-button link type="primary" @click="printLabel(row.cartonMark)"><Printer :size="14" />箱唛</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="query.page" v-model:page-size="query.size" :total="total" layout="total, prev, pager, next" @current-change="load" />
|
||||
<el-drawer v-model="showCartonDetail" title="退货箱明细" size="680px"><template v-if="cartonDetail">
|
||||
<el-descriptions :column="2" border><el-descriptions-item label="箱号">{{ cartonDetail.cartonMark }}</el-descriptions-item><el-descriptions-item label="海外仓">{{ cartonDetail.overseasWarehouseId }}</el-descriptions-item><el-descriptions-item label="来源退货单">{{ cartonDetail.sourceReturnNo || '—' }}</el-descriptions-item><el-descriptions-item label="签收批次">{{ cartonDetail.sourceBatchNo || '—' }}</el-descriptions-item><el-descriptions-item label="状态">{{ cartonDetail.status }}</el-descriptions-item><el-descriptions-item label="SKU 种类">{{ cartonDetail.skuTypes }}</el-descriptions-item><el-descriptions-item label="箱内总件数">{{ cartonDetail.totalQuantity }}</el-descriptions-item><el-descriptions-item label="箱规">{{ cartonDetail.length && cartonDetail.width && cartonDetail.height ? `${cartonDetail.length} × ${cartonDetail.width} × ${cartonDetail.height} cm` : '—' }}</el-descriptions-item><el-descriptions-item label="重量">{{ cartonDetail.weight ? `${cartonDetail.weight} kg` : '—' }}</el-descriptions-item></el-descriptions>
|
||||
<el-descriptions :column="2" border><el-descriptions-item label="箱号">{{ cartonDetail.cartonMark }}</el-descriptions-item><el-descriptions-item label="海外仓">{{ warehouseLabel(cartonDetail.overseasWarehouseId) }}</el-descriptions-item><el-descriptions-item label="来源退货单">{{ cartonDetail.sourceReturnNo || '—' }}</el-descriptions-item><el-descriptions-item label="签收批次">{{ cartonDetail.sourceBatchNo || '—' }}</el-descriptions-item><el-descriptions-item label="状态">{{ cartonDetail.status }}</el-descriptions-item><el-descriptions-item label="SKU 种类">{{ cartonDetail.skuTypes }}</el-descriptions-item><el-descriptions-item label="箱内总件数">{{ cartonDetail.totalQuantity }}</el-descriptions-item><el-descriptions-item label="箱规">{{ cartonDetail.length && cartonDetail.width && cartonDetail.height ? `${cartonDetail.length} × ${cartonDetail.width} × ${cartonDetail.height} cm` : '—' }}</el-descriptions-item><el-descriptions-item label="重量">{{ cartonDetail.weight ? `${cartonDetail.weight} kg` : '—' }}</el-descriptions-item></el-descriptions>
|
||||
<div class="drawer-heading"><h3>箱内 SKU</h3><el-button type="primary" @click="printLabel(cartonDetail.cartonMark)"><Printer :size="16" />打印箱唛</el-button></div>
|
||||
<el-table :data="cartonDetail.items" size="small"><el-table-column prop="skuCode" label="SKU" min-width="170" /><el-table-column prop="productNameZh" label="中文品名" min-width="180" /><el-table-column prop="quantity" label="数量" min-width="100" align="right" /></el-table>
|
||||
</template></el-drawer>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Plus, Search } from '@lucide/vue'
|
||||
import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-leg/api/firstLeg'
|
||||
import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-leg/public'
|
||||
import { createReturnOrder, getReturnOrder, listReturnOrders, type ReturnOrderDetail, type ReturnOrderSummary, type ReturnReceipt } from '../api/returnOrders'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -63,7 +63,7 @@ function addItem() { form.items.push(emptySku()) }
|
||||
function removeItem(index: number) { if (form.items.length > 1) form.items.splice(index, 1) }
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try { const response = await listReturnOrders({ ...query, returnNo: query.returnNo || undefined, trackingNo: query.trackingNo || undefined, skuCode: query.skuCode || undefined, status: query.status || undefined }); rows.value = response.records; total.value = response.total }
|
||||
try { const response = await listReturnOrders({ ...query, returnNo: query.returnNo || undefined, trackingNo: query.trackingNo || undefined, skuCode: query.skuCode || undefined, status: query.status || undefined }); rows.value = response.records; total.value = Number(response.total) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function changePage(page: number) {
|
||||
|
||||
Reference in New Issue
Block a user