Release/test 合并上生产 #1

Merged
admin merged 236 commits from release/test into main 2026-09-17 11:51:50 +08:00
8 changed files with 201 additions and 23 deletions
Showing only changes of commit 61a562b828 - Show all commits
+33
View File
@@ -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\)/)
})
+8 -4
View File
@@ -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)
+10
View File
@@ -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
View File
@@ -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) {