This commit is contained in:
2026-09-29 17:56:03 +08:00
parent ffbe3480a8
commit a427aca5c3
19 changed files with 219 additions and 221 deletions
@@ -1,4 +1,4 @@
import { overseasApiRequest } from '@/api/overseasHttp'
import { apiRequest } from '@/api/http'
export interface PageResult<T> {
records: T[]
@@ -141,23 +141,23 @@ function normalizePage<T>(data: PageResult<T>): PageResult<T> {
}
export function listBusinessOrders<T>(kind: CrossBorderBusinessKind, params: BusinessOrderQuery) {
return overseasApiRequest<PageResult<T>>({ method: 'GET', url: endpointByKind[kind], params }).then(normalizePage)
return apiRequest<PageResult<T>>({ method: 'GET', url: endpointByKind[kind], params }).then(normalizePage)
}
export function getBusinessOrder<T>(kind: CrossBorderBusinessKind, orderNo: string) {
return overseasApiRequest<T>({ method: 'GET', url: `${endpointByKind[kind]}/${encodeURIComponent(orderNo)}` })
return apiRequest<T>({ method: 'GET', url: `${endpointByKind[kind]}/${encodeURIComponent(orderNo)}` })
}
export function createProcurement(payload: ProcurementCreate) {
return overseasApiRequest<ProcurementDetail>({ method: 'POST', url: endpointByKind.procurement, data: payload })
return apiRequest<ProcurementDetail>({ method: 'POST', url: endpointByKind.procurement, data: payload })
}
export function createTransport(kind: 'trucking' | 'looseboard', payload: TransportCreate) {
return overseasApiRequest<TransportDetail>({ method: 'POST', url: endpointByKind[kind], data: payload })
return apiRequest<TransportDetail>({ method: 'POST', url: endpointByKind[kind], data: payload })
}
export function updateTransport(kind: 'trucking' | 'looseboard', orderNo: string, payload: TransportUpdate) {
return overseasApiRequest<TransportDetail>({
return apiRequest<TransportDetail>({
method: kind === 'trucking' ? 'PATCH' : 'PUT',
url: `${endpointByKind[kind]}/${encodeURIComponent(orderNo)}`,
data: payload,
@@ -1,7 +1,9 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { FileText, RefreshCcw } from '@lucide/vue'
import { ElMessage } from 'element-plus'
import { computed, onMounted, ref, watch } from 'vue'
import { RefreshCcw } from '@lucide/vue'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
import type { TableAction } from '@/components/common/tableActions'
import { useFirstLegOrdersStore } from '../public'
import { getMerchantCustomsForOrder, type MerchantCustomsContainer } from '../api/customs'
import CustomsDocumentsDialog from '../components/CustomsDocumentsDialog.vue'
@@ -10,17 +12,59 @@ import type { FirstLegOrder } from '../types/firstLegOrder'
const store = useFirstLegOrdersStore()
const loading = ref(false)
const errorMessage = ref('')
const keyword = ref('')
const queryModel = ref<QueryModel>({ orderNo: '', inboundCode: '', containerNo: '', status: '' })
const appliedQuery = ref<QueryModel>({ ...queryModel.value })
const page = ref(1)
const pageSize = ref(20)
const queryFields: QueryField[] = [
{ key: 'orderNo', label: '订单号', type: 'input', placeholder: '请输入订单号' },
{ key: 'inboundCode', label: '入仓码', type: 'input', placeholder: '请输入入仓码' },
{ key: 'containerNo', label: '柜号', type: 'input', placeholder: '请输入柜号' },
{ key: 'status', label: '状态', type: 'select', placeholder: '全部', options: [
{ label: '待上传资料', value: 'WAITING_CUSTOMER_DOCUMENTS' },
{ label: '待报关', value: 'READY_TO_START' },
{ label: '报关中', value: 'DECLARING' },
{ label: '报关完成', value: 'COMPLETED' },
] },
]
const columns: DataTableColumn[] = [
{ label: '操作', slotName: 'actions', width: 120, fixed: 'left' },
{ label: '订单号', slotName: 'orderNo', minWidth: 230 },
{ label: '入仓码', slotName: 'inboundCode', minWidth: 190 },
{ label: '柜号', slotName: 'containerNo', minWidth: 170 },
{ label: '实际箱数', prop: 'detail.actualBoxCount', width: 110, align: 'center' },
{ label: '资料覆盖', slotName: 'coverage', width: 110, align: 'center' },
{ label: '状态', slotName: 'status', width: 130 },
]
const records = ref<Array<{ order: FirstLegOrder; detail: MerchantCustomsContainer }>>([])
const dialogOpen = ref(false)
const selectedOrderId = ref('')
const selectedDetail = ref<MerchantCustomsContainer | null>(null)
const filteredRecords = computed(() => {
const query = keyword.value.trim().toLowerCase()
if (!query) return records.value
return records.value.filter(({ order, detail }) => [order.orderNo, order.businessSummary.inboundCode, detail.containerNo, detail.cabinetNo].some((value) => String(value ?? '').toLowerCase().includes(query)))
const query = appliedQuery.value
const matches = (value: string | null | undefined, key: string) => String(value ?? '').toLowerCase().includes(String(query[key] || '').trim().toLowerCase())
return records.value.filter(({ order, detail }) =>
matches(order.orderNo, 'orderNo')
&& matches(order.businessSummary.inboundCode, 'inboundCode')
&& (matches(detail.containerNo, 'containerNo') || matches(detail.cabinetNo, 'containerNo'))
&& (!query.status || detail.status === query.status),
)
})
const pagedRecords = computed(() => filteredRecords.value.slice((page.value - 1) * pageSize.value, page.value * pageSize.value))
const emptyText = computed(() => Object.values(appliedQuery.value).some((value) => String(value || '').trim()) ? '暂无匹配的报关资料,请调整查询条件' : '暂无报关资料')
function search() {
appliedQuery.value = { ...queryModel.value }
page.value = 1
}
watch(pageSize, () => { page.value = 1 })
watch(() => filteredRecords.value.length, (total) => {
page.value = Math.min(page.value, Math.max(1, Math.ceil(total / pageSize.value)))
})
function rowActions(row: object): TableAction[] {
return [{ key: 'documents', label: '查看资料', disabled: loading.value, run: () => openDetail(row) }]
}
function statusLabel(detail: MerchantCustomsContainer) {
return ({ WAITING_CUSTOMER_DOCUMENTS: '待上传资料', READY_TO_START: '待报关', DECLARING: '报关中', COMPLETED: '报关完成' } as Record<string, string>)[detail.status] ?? detail.status
@@ -29,6 +73,7 @@ function statusType(detail: MerchantCustomsContainer) {
return detail.status === 'COMPLETED' ? 'success' : detail.status === 'DECLARING' ? 'primary' : 'warning'
}
async function load() {
if (loading.value) return
loading.value = true; errorMessage.value = ''
try {
await store.load()
@@ -53,26 +98,25 @@ onMounted(() => { void load() })
<template>
<div class="customs-page">
<section class="customs-hero"><div><span class="eyebrow">CUSTOMS DOCUMENTS</span><h1>报关资料</h1><p>查看装柜后退税订单的资料清单、文件版本与报关行回执。</p></div><el-button :loading="loading" @click="load"><RefreshCcw :size="15" />刷新</el-button></section>
<section class="customs-toolbar"><el-input v-model="keyword" clearable placeholder="搜索订单号、入仓码或柜号" /><span>共 {{ filteredRecords.length }} 个资料包</span></section>
<QueryToolbar v-model="queryModel" layout="grid" ariaLabel="报关资料查询条件" :fields="queryFields" @search="search" />
<section class="customs-card">
<el-skeleton v-if="loading" :rows="5" animated />
<div v-else-if="errorMessage" class="state-error" role="alert"><p>{{ errorMessage }}</p><el-button @click="load">重试</el-button></div>
<el-empty v-else-if="!filteredRecords.length" description="暂无待处理的报关资料" />
<el-table v-else :data="filteredRecords" row-key="order.id" stripe>
<el-table-column label="操作" width="120" fixed="left"><template #default="{ row }"><el-button link type="primary" @click="openDetail(row)"><FileText :size="15" />查看资料</el-button></template></el-table-column>
<el-table-column label="订单号" min-width="160"><template #default="{ row }">{{ row.order.orderNo || '—' }}</template></el-table-column>
<el-table-column label="入仓码" min-width="150"><template #default="{ row }">{{ row.order.businessSummary.inboundCode || '—' }}</template></el-table-column>
<el-table-column label="柜号" min-width="140"><template #default="{ row }">{{ row.detail.containerNo || row.detail.cabinetNo || '—' }}</template></el-table-column>
<el-table-column label="实际箱数" width="100" prop="detail.actualBoxCount" />
<el-table-column label="资料覆盖" width="110"><template #default="{ row }">{{ row.detail.coveredTaxRefundCount }} / {{ row.detail.taxRefundCount }}</template></el-table-column>
<el-table-column label="状态" width="120"><template #default="{ row }"><el-tag :type="statusType(row.detail)">{{ statusLabel(row.detail) }}</el-tag></template></el-table-column>
</el-table>
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="pagedRecords" :columns="columns" :loading="loading" :total="filteredRecords.length" :page-sizes="[10, 20, 50, 100]" :row-actions="rowActions" row-key="order.id" :empty-text="emptyText">
<template #toolbar><strong>资料列表</strong></template>
<template #toolbar-actions><el-button :loading="loading" @click="load"><RefreshCcw :size="16" />刷新</el-button></template>
<template v-if="errorMessage" #error><div class="state-error" role="alert"><span>{{ errorMessage }}</span><el-button :loading="loading" @click="load">重新加载</el-button></div></template>
<template #orderNo="{ row }">{{ row.order.orderNo || '—' }}</template>
<template #inboundCode="{ row }">{{ row.order.businessSummary.inboundCode || '—' }}</template>
<template #containerNo="{ row }">{{ row.detail.containerNo || row.detail.cabinetNo || '—' }}</template>
<template #coverage="{ row }">{{ row.detail.coveredTaxRefundCount }} / {{ row.detail.taxRefundCount }}</template>
<template #status="{ row }"><el-tag :type="statusType(row.detail)" effect="light" size="small">{{ statusLabel(row.detail) }}</el-tag></template>
</DataTable>
</section>
<CustomsDocumentsDialog v-model="dialogOpen" :order-id="selectedOrderId" :can-edit="selectedDetail?.status !== 'COMPLETED'" :initial-detail="selectedDetail" @updated="updateDetail" />
</div>
</template>
<style scoped>
.customs-page { display:flex; flex-direction:column; gap:16px; }.customs-hero { display:flex; justify-content:space-between; align-items:flex-start; padding:0 2px 4px; }.eyebrow { color:#5b82c9; font-size:11px; font-weight:750; letter-spacing:.14em; }.customs-hero h1 { margin:6px 0 5px; color:#1f2f46; font-size:26px; line-height:1.25; letter-spacing:-.025em; }.customs-hero p { margin:0; color:#7d8da2; font-size:13px; }.customs-hero .el-button { margin-top:3px; border-color:#d9e3ef; border-radius:9px; background:#fff; color:#61738b; box-shadow:0 3px 8px rgba(26,55,91,.04); }.customs-toolbar { display:flex; min-height:82px; align-items:center; gap:16px; padding:16px 18px; border:1px solid #dce4ef; border-radius:16px; background:#fff; box-shadow:0 5px 16px rgba(31,56,88,.045); }.customs-toolbar .el-input { width:min(520px, 100%); }.customs-toolbar :deep(.el-input__wrapper) { min-height:42px; border-radius:10px; box-shadow:0 0 0 1px #d9e3ef inset; }.customs-toolbar span { color:#7d8da2; font-size:12px; }.customs-card { min-height:320px; padding:4px; overflow:hidden; border:1px solid #dce4ef; border-radius:16px; background:#fff; box-shadow:0 5px 16px rgba(31,56,88,.045); }.customs-card :deep(.el-table) { --el-table-header-bg-color:#f4f7fb; --el-table-row-hover-bg-color:#eaf2ff; --el-table-border-color:#edf1f6; color:#53647a; }.customs-card :deep(.el-table th.el-table__cell) { padding:13px 16px; color:#607089; font-size:12px; font-weight:750; }.customs-card :deep(.el-table td.el-table__cell) { height:58px; padding:13px 16px; font-size:13px; }.customs-card :deep(.el-table__row:nth-child(even)) { background:#f8fbff; }.state-error { display:flex; min-height:260px; flex-direction:column; align-items:center; justify-content:center; gap:10px; color:#b42318; }.state-error p { margin:0; }
.customs-page { display: grid; gap: var(--client-section-gap); }
.customs-card { overflow: hidden; border: 1px solid var(--client-border); border-radius: var(--client-radius); background: #fff; }
.state-error { display: flex; min-height: 84px; align-items: center; justify-content: center; gap: 12px; padding: 14px; color: #b42318; }
</style>
@@ -1,4 +1,4 @@
import { overseasApiRequest } from '@/api/overseasHttp'
import { apiRequest } from '@/api/http'
export interface PageResult<T> {
records: T[]
@@ -133,7 +133,7 @@ export interface DeliveryTransferPayload {
}
export function listDeliveryOrders(params: DeliveryQuery) {
return overseasApiRequest<PageResult<DeliveryRow>>({
return apiRequest<PageResult<DeliveryRow>>({
method: 'GET',
url: '/merchant/v1/last-leg/delivery-orders',
params,
@@ -141,7 +141,7 @@ export function listDeliveryOrders(params: DeliveryQuery) {
}
export function getDeliveryOrder(deliveryNo: string) {
return overseasApiRequest<DeliveryOrderDetailResponse>({
return apiRequest<DeliveryOrderDetailResponse>({
method: 'GET',
url: `/merchant/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}`,
params: { page: 1, size: 200 },
@@ -154,7 +154,7 @@ export function getDeliveryOrder(deliveryNo: string) {
export function createDeliveryOrder(payload: DeliveryTransferPayload) {
const { dispatchNo, ...request } = payload
return overseasApiRequest<DeliveryTransferResult>({
return apiRequest<DeliveryTransferResult>({
method: 'POST',
url: '/merchant/v1/last-leg/delivery-orders',
params: { dispatchNo },
@@ -1,4 +1,4 @@
import { overseasApiRequest } from '@/api/overseasHttp'
import { apiRequest } from '@/api/http'
export interface PageResult<T> { records: T[]; page: number; size: number; total: number; pages: number }
export interface InboundPlanQuery {
@@ -66,10 +66,10 @@ export interface InboundPlanDetail {
}
export async function listInboundPlans(params: InboundPlanQuery) {
const result = await overseasApiRequest<PageResult<InboundPlanSummary>>({ method: 'GET', url: '/merchant/v1/last-leg/inbound-plans', params })
const result = await apiRequest<PageResult<InboundPlanSummary>>({ method: 'GET', url: '/merchant/v1/last-leg/inbound-plans', params })
return { ...result, page: Number(result.page), size: Number(result.size), total: Number(result.total), pages: Number(result.pages) }
}
export function getInboundPlan(planNo: string) {
return overseasApiRequest<InboundPlanDetail>({ method: 'GET', url: `/merchant/v1/last-leg/inbound-plans/${encodeURIComponent(planNo)}` })
return apiRequest<InboundPlanDetail>({ method: 'GET', url: `/merchant/v1/last-leg/inbound-plans/${encodeURIComponent(planNo)}` })
}
@@ -24,9 +24,9 @@ test('uses only authenticated merchant delivery endpoints without customer scope
assert.match(apiSource, /method: 'POST'[\s\S]*url: '\/merchant\/v1\/last-leg\/delivery-orders'/)
assert.match(apiSource, /const \{ dispatchNo, \.\.\.request \} = payload/)
assert.match(apiSource, /params: \{ dispatchNo \}/)
assert.match(apiSource, /overseasApiRequest<DeliveryOrderDetailResponse>/)
assert.match(apiSource, /apiRequest<DeliveryOrderDetailResponse>/)
assert.match(apiSource, /\.then\(\(\{ order, skus, fees \}\) => \(\{[\s\S]*\.\.\.order,[\s\S]*fees,[\s\S]*items: skus/)
assert.match(apiSource, /overseasApiRequest<DeliveryTransferResult>/)
assert.match(apiSource, /apiRequest<DeliveryTransferResult>/)
assert.doesNotMatch(apiSource, /customerId|tenantId|Record<string, unknown>|mock|fallback/i)
})
@@ -6,7 +6,7 @@ import { overseasWarehouseLabel } from './inboundPlanDisplay.ts'
const api = readFileSync(new URL('./api/inboundPlans.ts', import.meta.url), 'utf8')
const routes = readFileSync(new URL('./routes.ts', import.meta.url), 'utf8')
const view = readFileSync(new URL('./views/InboundOrdersView.vue', import.meta.url), 'utf8')
const overseasHttp = readFileSync(new URL('../../api/overseasHttp.ts', import.meta.url), 'utf8')
const overseasHttp = readFileSync(new URL('../../api/http.ts', import.meta.url), 'utf8')
test('activates the customer-scoped overseas inbound list and detail', () => {
assert.match(routes, /path: 'orders\/inbound'[\s\S]*?release: 'active'[\s\S]*?permission: 'merchant:warehouse:inbound:list'/)
@@ -48,9 +48,9 @@ test('renders a dash when either formal receiving warehouse field is absent', ()
})
test('preserves overseas API diagnostic metadata without rendering it as a business identifier', () => {
assert.match(overseasHttp, /response\.data\.traceId/)
assert.match(overseasHttp, /response\.data\.requestId/)
assert.match(overseasHttp, /body\.traceId, body\.requestId/)
assert.match(overseasHttp, /envelope\.traceId/)
assert.match(overseasHttp, /envelope\.requestId/)
assert.match(overseasHttp, /body\.traceId/)
assert.doesNotMatch(view, /requestId|traceId/)
})
@@ -1,4 +1,4 @@
import { overseasApiRequest } from '@/api/overseasHttp'
import { apiRequest } from '@/api/http'
import type { PageResult } from './warehouseServices'
export interface ReturnInventoryItem {
@@ -19,7 +19,7 @@ export interface ReturnInventoryCartonSummary {
}
export function listReturnInventoryCartons(params: Record<string, unknown>) {
return overseasApiRequest<PageResult<ReturnInventoryCartonSummary>>({ method: 'GET', url: '/merchant/v1/last-leg/return-inventory/cartons', params })
return apiRequest<PageResult<ReturnInventoryCartonSummary>>({ method: 'GET', url: '/merchant/v1/last-leg/return-inventory/cartons', params })
}
export interface ReturnInventoryCartonDetail {
@@ -33,19 +33,19 @@ export interface ReturnInventoryLabel {
}
export function listReturnInventory(params: Record<string, unknown>) {
return overseasApiRequest<PageResult<ReturnInventoryItem>>({ method: 'GET', url: '/merchant/v1/last-leg/return-inventory', params })
return apiRequest<PageResult<ReturnInventoryItem>>({ method: 'GET', url: '/merchant/v1/last-leg/return-inventory', params })
}
export function listReturnInventorySummary(params: Record<string, unknown>) {
return overseasApiRequest<PageResult<ReturnInventorySummaryItem>>({ method: 'GET', url: '/merchant/v1/last-leg/return-inventory/summary', params })
return apiRequest<PageResult<ReturnInventorySummaryItem>>({ method: 'GET', url: '/merchant/v1/last-leg/return-inventory/summary', params })
}
export function getReturnInventoryCarton(cartonMark: string) {
return overseasApiRequest<ReturnInventoryCartonDetail>({ method: 'GET', url: `/merchant/v1/last-leg/return-inventory/cartons/${encodeURIComponent(cartonMark)}` })
return apiRequest<ReturnInventoryCartonDetail>({ method: 'GET', url: `/merchant/v1/last-leg/return-inventory/cartons/${encodeURIComponent(cartonMark)}` })
}
export function getReturnInventoryLabel(cartonMark: string) {
return overseasApiRequest<ReturnInventoryLabel>({ method: 'GET', url: `/merchant/v1/last-leg/return-inventory/cartons/${encodeURIComponent(cartonMark)}/label` })
return apiRequest<ReturnInventoryLabel>({ method: 'GET', url: `/merchant/v1/last-leg/return-inventory/cartons/${encodeURIComponent(cartonMark)}/label` })
}
export interface ReturnPackingPreview {
@@ -55,14 +55,14 @@ export interface ReturnPackingPreview {
}
export function getReturnPackingPreview(returnNo?: string) {
return overseasApiRequest<ReturnPackingPreview>({ method: 'GET', url: '/pda/v1/last-leg/return-inventory/packing/orders', params: { returnNo: returnNo || undefined } })
return apiRequest<ReturnPackingPreview>({ method: 'GET', url: '/pda/v1/last-leg/return-inventory/packing/orders', params: { returnNo: returnNo || undefined } })
}
export function mergeReturnInventory(payload: {
returnNos: string[]; items: Array<{ skuCode: string; quantity: number }>
length: number; width: number; height: number; weight: number; idempotencyKey: string
}) {
return overseasApiRequest<{ cartonMark: string; skuCode: string; totalQuantity: number; label: ReturnInventoryLabel }>({
return apiRequest<{ cartonMark: string; skuCode: string; totalQuantity: number; label: ReturnInventoryLabel }>({
method: 'POST', url: '/pda/v1/last-leg/return-inventory/cartons/merge', data: payload,
})
}
@@ -1,4 +1,4 @@
import { overseasApiRequest, overseasBlobRequest } from '@/api/overseasHttp'
import { apiRequest, apiBlobRequest } from '@/api/http'
import type { PageResult } from './warehouseServices'
export interface ReturnOrderSummary {
@@ -22,16 +22,16 @@ export interface ReturnOrderCreate {
}
export function listReturnOrders(params: Record<string, unknown>) {
return overseasApiRequest<PageResult<ReturnOrderSummary>>({ method: 'GET', url: '/merchant/v1/last-leg/return-orders', params })
return apiRequest<PageResult<ReturnOrderSummary>>({ method: 'GET', url: '/merchant/v1/last-leg/return-orders', params })
}
export function getReturnOrder(returnNo: string) {
return overseasApiRequest<ReturnOrderDetail>({ method: 'GET', url: `/merchant/v1/last-leg/return-orders/${encodeURIComponent(returnNo)}` })
return apiRequest<ReturnOrderDetail>({ method: 'GET', url: `/merchant/v1/last-leg/return-orders/${encodeURIComponent(returnNo)}` })
}
export function createReturnOrder(payload: ReturnOrderCreate) {
return overseasApiRequest<ReturnOrderDetail>({ method: 'POST', url: '/merchant/v1/last-leg/return-orders', data: payload })
return apiRequest<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 })
return apiRequest<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
@@ -39,12 +39,12 @@ export interface ReturnRelabelCreate {
}
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 })
return apiRequest<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' } })
return apiRequest<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` })
return apiBlobRequest({ method: 'GET', url: `/merchant/v1/last-leg/return-orders/${encodeURIComponent(returnNo)}/photos/files/${encodeURIComponent(String(fileId))}/download` })
}
@@ -1,4 +1,4 @@
import { overseasApiRequest, overseasBlobRequest } from '@/api/overseasHttp'
import { apiRequest, apiBlobRequest } from '@/api/http'
import { createIdempotencyKey } from '@/utils/idempotencyKey'
export type RelabelDeliveryMethod = 'TRUCK' | 'EXPRESS' | 'TRUCK_PICKUP' | 'EXPRESS_PICKUP'
@@ -55,78 +55,78 @@ export interface RelabelCreate {
}
export function listRelabelHistoricalOrders(operationWarehouseId: string, keyword?: string) {
return overseasApiRequest<HistoricalOrderSummary[]>({ method: 'GET', url: '/merchant/v1/last-leg/relabel-applications/historical-orders', params: { operationWarehouseId, keyword: keyword || undefined } })
return apiRequest<HistoricalOrderSummary[]>({ method: 'GET', url: '/merchant/v1/last-leg/relabel-applications/historical-orders', params: { operationWarehouseId, keyword: keyword || undefined } })
}
export function listTransfers(params: Record<string, unknown>) {
return overseasApiRequest<PageResult<TransferSummary>>({ method: 'GET', url: '/merchant/v1/last-leg/warehouse-transfers', params })
return apiRequest<PageResult<TransferSummary>>({ method: 'GET', url: '/merchant/v1/last-leg/warehouse-transfers', params })
}
export function getTransfer(transferNo: string) {
return overseasApiRequest<Record<string, unknown>>({ method: 'GET', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}` })
return apiRequest<Record<string, unknown>>({ method: 'GET', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}` })
}
export function createTransfer(payload: TransferCreate) {
return overseasApiRequest<Record<string, unknown>>({ method: 'POST', url: '/merchant/v1/last-leg/warehouse-transfers', data: payload })
return apiRequest<Record<string, unknown>>({ method: 'POST', url: '/merchant/v1/last-leg/warehouse-transfers', data: payload })
}
export function updateTransfer(transferNo: string, payload: TransferCreate) {
return overseasApiRequest<Record<string, unknown>>({ method: 'POST', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/save-draft`, data: payload })
return apiRequest<Record<string, unknown>>({ method: 'POST', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/save-draft`, data: payload })
}
export function submitTransfer(transferNo: string) {
return overseasApiRequest<Record<string, unknown>>({ method: 'POST', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/submit` })
return apiRequest<Record<string, unknown>>({ method: 'POST', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/submit` })
}
export function listRelabels(params: Record<string, unknown>) {
return overseasApiRequest<PageResult<RelabelSummary>>({ method: 'GET', url: '/merchant/v1/last-leg/relabel-applications', params })
return apiRequest<PageResult<RelabelSummary>>({ method: 'GET', url: '/merchant/v1/last-leg/relabel-applications', params })
}
export function getRelabel(relabelNo: string) {
return overseasApiRequest<RelabelDetail>({ method: 'GET', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}` })
return apiRequest<RelabelDetail>({ method: 'GET', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}` })
}
export function confirmTransferMeasurement(transferNo: string, remark?: string) {
return overseasApiRequest<Record<string, unknown>>({ method: 'POST', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/confirm-measurement`, data: { idempotencyKey: createIdempotencyKey(), remark } })
return apiRequest<Record<string, unknown>>({ method: 'POST', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/confirm-measurement`, data: { idempotencyKey: createIdempotencyKey(), remark } })
}
export interface TransferCartonLabel { transferNo: string; cartons: Array<{ cartonMark: string; customerCartonMark?: string; cartonIndex: number; totalCartons: number; status: string }>; printableHtml: string }
export function getTransferCartonLabel(transferNo: string) {
return overseasApiRequest<TransferCartonLabel>({ method: 'GET', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/carton-label` })
return apiRequest<TransferCartonLabel>({ method: 'GET', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/carton-label` })
}
export function downloadTransferMeasurementPhoto(transferNo: string, fileId: string | number) {
return overseasBlobRequest({ method: 'GET', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/measurement-photos/files/${encodeURIComponent(fileId)}/download` })
return apiBlobRequest({ method: 'GET', url: `/merchant/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/measurement-photos/files/${encodeURIComponent(fileId)}/download` })
}
export function listRelabelInventoryCandidates(operationWarehouseId: string, inboundCode?: string) {
return overseasApiRequest<RelabelInventoryCandidate[]>({
return apiRequest<RelabelInventoryCandidate[]>({
method: 'GET', url: '/merchant/v1/last-leg/relabel-applications/inventory-candidates',
params: { operationWarehouseId, inboundCode: inboundCode || undefined },
})
}
export function createRelabel(payload: RelabelCreate) {
return overseasApiRequest<RelabelDetail>({ method: 'POST', url: '/merchant/v1/last-leg/relabel-applications', data: payload })
return apiRequest<RelabelDetail>({ method: 'POST', url: '/merchant/v1/last-leg/relabel-applications', data: payload })
}
export function resubmitRelabel(relabelNo: string, payload: { labelDeadline: string; contactName: string; contactPhone: string; remark?: string; idempotencyKey: string }) {
return overseasApiRequest<RelabelDetail>({ method: 'PUT', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/resubmit`, data: payload })
return apiRequest<RelabelDetail>({ method: 'PUT', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/resubmit`, data: payload })
}
export function uploadRelabelShippingLabel(relabelNo: string, file: File) {
const data = new FormData(); data.append('file', file)
return overseasApiRequest<RelabelDetail>({ method: 'POST', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/shipping-label`, data, headers: { 'Content-Type': 'multipart/form-data' } })
return apiRequest<RelabelDetail>({ method: 'POST', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/shipping-label`, data, headers: { 'Content-Type': 'multipart/form-data' } })
}
export function uploadRelabelFile(relabelNo: string, scope: 'CARTON_TOTAL' | 'SKU_TOTAL' | 'ITEM', file: File, itemId?: string) {
const data = new FormData(); data.append('file', file)
return overseasApiRequest<RelabelDetail>({ method: 'POST', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/files`, params: { scope, itemId }, data, headers: { 'Content-Type': 'multipart/form-data' } })
return apiRequest<RelabelDetail>({ method: 'POST', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/files`, params: { scope, itemId }, data, headers: { 'Content-Type': 'multipart/form-data' } })
}
export function confirmRelabelCompletion(relabelNo: string, remark?: string) {
return overseasApiRequest<RelabelDetail>({ method: 'POST', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/confirm-completion`, data: { idempotencyKey: createIdempotencyKey(), remark } })
return apiRequest<RelabelDetail>({ method: 'POST', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/confirm-completion`, data: { idempotencyKey: createIdempotencyKey(), remark } })
}
export function downloadRelabelCompletionPhoto(relabelNo: string, fileId: string) {
return overseasBlobRequest({ method: 'GET', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/completion-photos/${encodeURIComponent(fileId)}/download` })
return apiBlobRequest({ method: 'GET', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/completion-photos/${encodeURIComponent(fileId)}/download` })
}
export function listDiscards(params: Record<string, unknown>) {
return overseasApiRequest<PageResult<DiscardSummary>>({ method: 'GET', url: '/merchant/v1/last-leg/cargo-discards', params })
return apiRequest<PageResult<DiscardSummary>>({ method: 'GET', url: '/merchant/v1/last-leg/cargo-discards', params })
}
export function getDiscard(discardNo: string) {
return overseasApiRequest<Record<string, unknown>>({ method: 'GET', url: `/merchant/v1/last-leg/cargo-discards/${encodeURIComponent(discardNo)}` })
return apiRequest<Record<string, unknown>>({ method: 'GET', url: `/merchant/v1/last-leg/cargo-discards/${encodeURIComponent(discardNo)}` })
}
export function listDiscardCandidates(inboundCode?: string) {
return overseasApiRequest<DiscardCandidate[]>({ method: 'GET', url: '/merchant/v1/last-leg/cargo-discards/inventory-candidates', params: { inboundCode: inboundCode || undefined } })
return apiRequest<DiscardCandidate[]>({ method: 'GET', url: '/merchant/v1/last-leg/cargo-discards/inventory-candidates', params: { inboundCode: inboundCode || undefined } })
}
export function createDiscard(payload: { items: Array<{ orderId?: string; returnCartonId?: string; skuCode: string; discardPackages: number }>; idempotencyKey: string; remark?: string }) {
return overseasApiRequest<Record<string, unknown>>({ method: 'POST', url: '/merchant/v1/last-leg/cargo-discards', data: payload })
return apiRequest<Record<string, unknown>>({ method: 'POST', url: '/merchant/v1/last-leg/cargo-discards', data: payload })
}
export function cancelDiscard(discardNo: string) {
return overseasApiRequest<Record<string, unknown>>({ method: 'POST', url: `/merchant/v1/last-leg/cargo-discards/${encodeURIComponent(discardNo)}/cancel` })
return apiRequest<Record<string, unknown>>({ method: 'POST', url: `/merchant/v1/last-leg/cargo-discards/${encodeURIComponent(discardNo)}/cancel` })
}
@@ -7,7 +7,7 @@ const api = readFileSync(new URL('../api/returnOrders.ts', import.meta.url), 'ut
test('退货签收照片通过客户鉴权下载接口加载并释放对象 URL', () => {
assert.match(api, /\/return-orders\/\$\{encodeURIComponent\(returnNo\)\}\/photos\/files\/\$\{encodeURIComponent\(String\(fileId\)\)\}\/download/)
assert.match(api, /overseasBlobRequest/)
assert.match(api, /apiBlobRequest/)
assert.match(view, /URL\.createObjectURL\(await downloadReturnPhoto\(detailReturnNo\.value, photo\.fileId\)\)/)
assert.match(view, /URL\.revokeObjectURL\(url\)/)
assert.match(view, /onBeforeUnmount\(clearPhotoUrls\)/)
@@ -213,7 +213,7 @@ test('客户可在换标列表或详情确认完工且照片使用二进制下
assert.match(view, /confirmRelabelFromList\(item\['0'\]\)/)
assert.match(view, /selectedRelabelDetail\?\.application\.status === 'CUSTOMER_CONFIRM_PENDING'/)
assert.match(view, /@click="confirmSelectedRelabel">确认换标完成/)
assert.match(api, /overseasBlobRequest/)
assert.match(api, /apiBlobRequest/)
})
test('客户详情进入页面后直接回显完工照片并支持大图预览', () => {