front/src/views/order/customs9610Order/index.vue
zzh 08ceb34b3b
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m44s
fix: 其他配置/9610 默认 API 按当前环境解析,避免测试站写死正式域名
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-24 11:29:41 +08:00

1180 lines
38 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { Modal } from '@arco-design/web-vue'
import HandsontablePro from '@/components/HandsontablePro/index.vue'
import PaginationPro from '@/components/PaginationPro/index.vue'
import ModalPro from '@/components/ModalPro/index.vue'
import useModal from '@/components/ModalPro/useModal'
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
import {
declareCustoms9610Batch,
fetchCustoms9610DeclarationFormBatch,
fetchCustoms9610ManifestBatch,
getCustoms9610Detail,
getCustoms9610List,
getCustoms9610StatusCounts,
updateCustoms9610,
} from '@/api/order/customs9610'
import { $baseMessage } from '@/utils'
import { hasPermission } from '@/utils/rbac-permission'
import { prodApiOrigin } from '@/utils/rewriteBrowserApiPath'
import { formatDateTime, normalizeListRow, pickField } from '@/utils/warehouseListMapper'
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
import SlidingTabs from '@/components/SlidingTabs/index.vue'
defineOptions({ name: 'OrderCustoms9610' })
const { t } = useI18n()
const { tableContainerRef, tableHeight } = useTableContainerHeight()
const {
isAdmin,
customerId: adminCustomerId,
customerOptions,
withCustomerId,
} = useWarehouseAdminCustomer()
const STATUS_ALL = 'all'
const statusTab = ref(STATUS_ALL)
const keyword = ref('')
const listLoading = ref(false)
const tableRows = ref<any[]>([])
const currentPage = ref(1)
const pageSize = ref(50)
const totalCount = ref(0)
const selectedIds = ref<number[]>([])
const statusCounts = ref({ total: 0, pending: 0, declared: 0 })
const declaring = ref(false)
const fetchingManifest = ref(false)
const fetchingDeclarationForm = ref(false)
const downstreamForwardModalVisible = ref(false)
const downstreamForwardResultText = ref('')
const saving = ref(false)
const detailLoading = ref(false)
const [registerEditModal, { openModal: openEditModal, closeModal: closeEditModal }] = useModal() as any
const editForm = reactive({
id: 0,
status: '',
platformCode: '',
ecomCorpCode: '',
ecomCorpName: '',
producerCode: '',
producerName: '',
clientNo: '',
serviceNo: '',
businessType: 'B',
region: '',
agentDeclare: '',
actualSellerCode: '',
preRecordFlag: '1',
isSummarySimple: false,
grossWeight: 0 as number,
currency: 'CNY',
destCountry: '',
freight: undefined as number | undefined,
shipmentNo: '',
customsOrderNo: '',
failReason: '',
items: [] as Array<{
id?: number
orderDeclarationId?: number
sku: string
cnName: string
exHsCode?: string
declareElement?: string
unitPrice: number
quantity: number
freight?: number
sortNo: number
}>,
})
const isPending = computed(() => editForm.status === 'pending')
const statusTabs = computed(() => [
{ key: STATUS_ALL, label: t('common.all'), count: statusCounts.value.total, showCount: true },
{
key: 'pending',
label: t('order.customs9610.status.pending'),
count: statusCounts.value.pending,
showCount: true,
},
{
key: 'declared',
label: t('order.customs9610.status.declared'),
count: statusCounts.value.declared,
showCount: true,
},
])
const slidingTabItems = computed(() =>
statusTabs.value.map((tab) => ({
key: tab.key,
label: tab.label,
badge: tab.showCount && tab.count != null ? tab.count : undefined,
})),
)
const businessTypeTabs = computed(() => [
{ key: 'A', label: 'A', disabled: !isPending.value },
{ key: 'B', label: 'B', disabled: !isPending.value },
])
const preRecordFlagTabs = computed(() => [
{ key: '0', label: '0', disabled: !isPending.value },
{ key: '1', label: '1', disabled: !isPending.value },
])
const modalTip = computed(() =>
isPending.value ? t('order.customs9610.modal.tipEdit') : t('order.customs9610.modal.tipDetail'),
)
const itemHotColumns = computed(() => [
{ key: 'sku', label: t('order.customs9610.items.sku'), width: 120 },
{ key: 'cnName', label: t('order.customs9610.items.cnName'), width: 160 },
{ key: 'exHsCode', label: t('order.customs9610.items.exHsCode'), width: 110 },
{ key: 'declareElement', label: t('order.customs9610.items.declareElement'), width: 140 },
{ key: 'unitPrice', label: t('order.customs9610.items.unitPrice'), width: 100, type: 'numeric' },
{ key: 'quantity', label: t('order.customs9610.items.quantity'), width: 90, type: 'numeric' },
])
const selectedRowCount = computed(() => selectedIds.value.length)
function handleTabChange(key: string) {
if (statusTab.value === key) return
statusTab.value = key
currentPage.value = 1
loadList()
}
function handleResetSearch() {
keyword.value = ''
if (isAdmin.value) adminCustomerId.value = undefined
currentPage.value = 1
loadList()
loadCounts()
}
const hotColumns = computed(() => [
{ key: 'serviceNo', label: t('order.customs9610.table.serviceNo'), width: 150 },
{ key: 'clientNo', label: t('order.customs9610.table.clientNo'), width: 140 },
{ key: 'customerName', label: t('warehouse.common.customerName'), width: 120 },
{ key: 'destCountry', label: t('order.customs9610.table.destCountry'), width: 80 },
{ key: 'grossWeight', label: t('order.customs9610.table.grossWeight'), width: 90 },
{ key: 'currency', label: t('order.customs9610.table.currency'), width: 70 },
{ key: 'businessType', label: t('order.customs9610.table.businessType'), width: 70 },
{ key: 'statusLabel', label: t('order.customs9610.table.status'), width: 90 },
{ key: 'customsOrderNo', label: t('order.customs9610.table.customsOrderNo'), width: 140 },
{ key: 'declareTime', label: t('order.customs9610.table.declareTime'), width: 160 },
{ key: 'createTime', label: t('order.customs9610.table.createTime'), width: 160 },
])
function statusLabel(code: string) {
if (code === 'declared') return t('order.customs9610.status.declared')
if (code === 'pending') return t('order.customs9610.status.pending')
return code || '-'
}
function actionRenderer(rowData: any, rowIndex?: number) {
const record = rowData || tableRows.value[rowIndex ?? -1]
if (!record) return ''
const status = String(record.status || '')
const buttons = [
renderWarehouseActionBtn('detail', t('warehouse.common.detail'), 'view'),
]
if (status === 'pending' && hasPermission('Order.Customs9610.Update')) {
buttons.push(renderWarehouseActionBtn('edit', t('warehouse.common.edit'), 'edit'))
}
return wrapWarehouseActions(buttons, rowIndex)
}
function handleSelectionChange(rows: any[]) {
selectedIds.value = (rows || [])
.map((x) => Number(x?.id ?? x?.Id ?? 0))
.filter((x) => Number.isFinite(x) && x > 0)
}
async function loadCounts() {
try {
const data = await getCustoms9610StatusCounts(withCustomerId({}))
statusCounts.value = {
total: Number(pickField(data, 'total', 'Total') ?? 0),
pending: Number(pickField(data, 'pending', 'Pending') ?? 0),
declared: Number(pickField(data, 'declared', 'Declared') ?? 0),
}
} catch {
// ignore
}
}
async function loadList() {
listLoading.value = true
try {
const body = withCustomerId({
page: currentPage.value,
pageSize: pageSize.value,
keyword: keyword.value.trim() || undefined,
status: statusTab.value === STATUS_ALL ? undefined : statusTab.value,
})
const { items, total } = await getCustoms9610List(body)
tableRows.value = (items || []).map((raw: any) => {
const row = normalizeListRow(raw)
const status = String(pickField(raw, 'status', 'Status') ?? '')
return {
...row,
id: Number(pickField(raw, 'id', 'Id') ?? 0),
status,
statusLabel: statusLabel(status),
serviceNo: pickField(raw, 'serviceNo', 'ServiceNo') || '-',
clientNo: pickField(raw, 'clientNo', 'ClientNo') || '-',
customerName: pickField(raw, 'customerName', 'CustomerName') || '-',
destCountry: pickField(raw, 'destCountry', 'Country') || '-',
grossWeight: pickField(raw, 'grossWeight', 'GrossWeight') ?? '-',
currency: pickField(raw, 'currency', 'Currency') || '-',
businessType: pickField(raw, 'businessType', 'FormType') || '-',
customsOrderNo: pickField(raw, 'customsOrderNo', 'OrderNo') || '-',
declareTime: formatDateTime(pickField(raw, 'declareTime', 'DeclareTime')),
createTime: formatDateTime(pickField(raw, 'createTime', 'CreateTime')),
_raw: raw,
}
})
totalCount.value = total || 0
selectedIds.value = []
} catch (e: any) {
$baseMessage(e?.message || t('order.customs9610.message.loadFailed'), 'error')
} finally {
listLoading.value = false
}
}
function handleSearch() {
currentPage.value = 1
loadList()
loadCounts()
}
function handlePageChange(page: number) {
currentPage.value = page
loadList()
}
function handlePageSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
loadList()
}
function fillEditForm(data: any) {
const items = (pickField(data, 'items', 'Items') as any[]) || []
Object.assign(editForm, {
id: Number(pickField(data, 'id', 'Id') ?? 0),
status: String(pickField(data, 'status', 'Status') ?? ''),
platformCode: String(pickField(data, 'platformCode', 'Ebp') ?? ''),
ecomCorpCode: String(pickField(data, 'ecomCorpCode', 'EbcCode') ?? ''),
ecomCorpName: String(pickField(data, 'ecomCorpName', 'EbcName') ?? ''),
producerCode: String(pickField(data, 'producerCode', 'OwnerCode') ?? ''),
producerName: String(pickField(data, 'producerName', 'OwnerName') ?? ''),
clientNo: String(pickField(data, 'clientNo', 'ClientNo') ?? ''),
serviceNo: String(pickField(data, 'serviceNo', 'ServiceNo') ?? ''),
businessType: String(pickField(data, 'businessType', 'FormType') ?? 'B'),
region: String(pickField(data, 'region', 'Region') ?? ''),
agentDeclare: String(pickField(data, 'agentDeclare', 'IsAgent') ?? ''),
actualSellerCode: String(pickField(data, 'actualSellerCode', 'ActBuyer') ?? ''),
preRecordFlag: String(pickField(data, 'preRecordFlag', 'SoLabelFlag') ?? '1'),
isSummarySimple: Boolean(pickField(data, 'isSummarySimple', 'IsSimple')),
grossWeight: Number(pickField(data, 'grossWeight', 'GrossWeight') ?? 0),
currency: String(pickField(data, 'currency', 'Currency') ?? 'CNY'),
destCountry: String(pickField(data, 'destCountry', 'Country') ?? ''),
freight: pickField(data, 'freight', 'Freight') as number | undefined,
shipmentNo: String(pickField(data, 'shipmentNo', 'BlNo') ?? ''),
customsOrderNo: String(pickField(data, 'customsOrderNo', 'OrderNo') ?? ''),
failReason: String(pickField(data, 'failReason', 'FailReason') ?? ''),
items: items.map((it: any, idx: number) => ({
id: Number(pickField(it, 'id', 'Id') ?? 0) || undefined,
orderDeclarationId: pickField(it, 'orderDeclarationId', 'OrderDeclarationId') as number | undefined,
sku: String(pickField(it, 'sku', 'Sku', 'itemNo', 'ItemNo') ?? ''),
cnName: String(pickField(it, 'cnName', 'CnName', 'itemName', 'ItemName') ?? ''),
exHsCode: String(pickField(it, 'exHsCode', 'ExHsCode', 'gcode', 'Gcode') ?? ''),
declareElement: String(pickField(it, 'declareElement', 'Gmodel') ?? ''),
unitPrice: Number(pickField(it, 'unitPrice', 'UnitPrice', 'price', 'Price') ?? 0),
quantity: Number(pickField(it, 'quantity', 'Quantity', 'qty', 'Qty') ?? 0),
freight: pickField(it, 'freight', 'Freight') as number | undefined,
sortNo: Number(pickField(it, 'sortNo', 'SortNo') ?? idx),
})),
})
}
async function openDetail(id: number, editable: boolean) {
detailLoading.value = true
openEditModal(true)
try {
const data = await getCustoms9610Detail(id)
fillEditForm(data)
if (!editable) {
editForm.status = String(pickField(data, 'status', 'Status') ?? editForm.status)
}
} catch (e: any) {
$baseMessage(e?.message || t('order.customs9610.message.loadFailed'), 'error')
closeEditModal()
} finally {
detailLoading.value = false
}
}
function handleRowAction(row: any, rowIndex: number, action?: string) {
const record = row || tableRows.value[rowIndex]
if (!record?.id) return
if (action === 'detail') {
openDetail(record.id, false)
} else if (action === 'edit') {
openDetail(record.id, true)
}
}
function addItemRow() {
editForm.items.push({
sku: '',
cnName: '',
exHsCode: '',
declareElement: '',
unitPrice: 0,
quantity: 1,
sortNo: editForm.items.length,
})
}
function removeItemRow(index: number) {
if (index < 0 || index >= editForm.items.length) return
editForm.items.splice(index, 1)
editForm.items.forEach((item, i) => {
item.sortNo = i
})
}
function itemActionRenderer(_rowData: any, rowIndex?: number) {
return wrapWarehouseActions(
[renderWarehouseActionBtn('delete', t('warehouse.common.delete'), 'delete')],
rowIndex,
)
}
function handleItemActionClick(_row: any, rowIndex: number, action?: string) {
if (action === 'delete') {
removeItemRow(rowIndex)
}
}
function validateEdit() {
if (!editForm.platformCode.trim()) {
$baseMessage(t('order.customs9610.message.platformCodeRequired'), 'warning')
return false
}
if (!editForm.clientNo.trim() || !editForm.serviceNo.trim()) {
$baseMessage(t('order.customs9610.message.clientServiceRequired'), 'warning')
return false
}
if (!(editForm.grossWeight > 0)) {
$baseMessage(t('order.customs9610.message.weightRequired'), 'warning')
return false
}
if (!editForm.currency.trim() || !editForm.destCountry.trim()) {
$baseMessage(t('order.customs9610.message.currencyDestCountryRequired'), 'warning')
return false
}
if (editForm.businessType === 'B' && !editForm.preRecordFlag) {
$baseMessage(t('order.customs9610.message.preRecordRequired'), 'warning')
return false
}
if (editForm.businessType === 'A' && (!editForm.region || !editForm.agentDeclare)) {
$baseMessage(t('order.customs9610.message.aFieldsRequired'), 'warning')
return false
}
if (!editForm.items.length || editForm.items.some((x) => !String(x.sku || '').trim() || !String(x.cnName || '').trim())) {
$baseMessage(t('order.customs9610.message.itemsRequired'), 'warning')
return false
}
return true
}
async function handleSave() {
if (!isPending.value) {
closeEditModal()
return
}
if (!validateEdit()) return
saving.value = true
try {
await updateCustoms9610({
id: editForm.id,
platformCode: editForm.platformCode,
ecomCorpCode: editForm.ecomCorpCode || undefined,
ecomCorpName: editForm.ecomCorpName || undefined,
producerCode: editForm.producerCode || undefined,
producerName: editForm.producerName || undefined,
clientNo: editForm.clientNo,
serviceNo: editForm.serviceNo,
businessType: editForm.businessType,
region: editForm.region || undefined,
agentDeclare: editForm.agentDeclare || undefined,
actualSellerCode: editForm.actualSellerCode || undefined,
preRecordFlag: editForm.preRecordFlag || undefined,
isSummarySimple: editForm.isSummarySimple,
grossWeight: editForm.grossWeight,
currency: editForm.currency,
destCountry: editForm.destCountry,
freight: editForm.freight,
shipmentNo: editForm.shipmentNo || undefined,
items: editForm.items.map((item, index) => ({
...item,
sortNo: index,
sku: String(item.sku || '').trim(),
cnName: String(item.cnName || '').trim(),
exHsCode: item.exHsCode ? String(item.exHsCode).trim() : undefined,
declareElement: item.declareElement ? String(item.declareElement).trim() : undefined,
unitPrice: Number(item.unitPrice) || 0,
quantity: Number(item.quantity) || 0,
})),
})
$baseMessage(t('order.customs9610.message.saveSuccess'), 'success')
closeEditModal()
await loadList()
await loadCounts()
} catch (e: any) {
$baseMessage(e?.message || t('order.customs9610.message.saveFailed'), 'error')
} finally {
saving.value = false
}
}
async function handleBatchDeclare() {
const ids = selectedIds.value
if (!ids.length) {
$baseMessage(t('order.customs9610.message.selectFirst'), 'warning')
return
}
Modal.confirm({
title: t('order.customs9610.confirm.batchDeclareTitle'),
content: t('order.customs9610.confirm.batchDeclareContent', { count: ids.length }),
onOk: async () => {
declaring.value = true
try {
const res = await declareCustoms9610Batch(ids)
const successCount = Number(pickField(res, 'successCount', 'SuccessCount') ?? 0)
const failCount = Number(pickField(res, 'failCount', 'FailCount') ?? 0)
$baseMessage(
t('order.customs9610.message.batchResult', { success: successCount, fail: failCount }),
failCount > 0 ? 'warning' : 'success',
)
await loadList()
await loadCounts()
} catch (e: any) {
$baseMessage(e?.message || t('order.customs9610.message.declareFailed'), 'error')
} finally {
declaring.value = false
}
},
})
}
async function runBatchFetch(
kind: 'manifest' | 'declarationForm',
api: (ids: Array<number | string>) => Promise<any>,
) {
const ids = selectedIds.value
if (!ids.length) {
$baseMessage(t('order.customs9610.message.selectFirst'), 'warning')
return
}
const loadingRef = kind === 'manifest' ? fetchingManifest : fetchingDeclarationForm
const title =
kind === 'manifest'
? t('order.customs9610.confirm.batchManifestTitle')
: t('order.customs9610.confirm.batchDeclarationFormTitle')
const content =
kind === 'manifest'
? t('order.customs9610.confirm.batchManifestContent', { count: ids.length })
: t('order.customs9610.confirm.batchDeclarationFormContent', { count: ids.length })
const failFallback =
kind === 'manifest'
? t('order.customs9610.message.fetchManifestFailed')
: t('order.customs9610.message.fetchDeclarationFormFailed')
Modal.confirm({
title,
content,
onOk: async () => {
loadingRef.value = true
try {
const res = await api(ids)
const successCount = Number(pickField(res, 'successCount', 'SuccessCount') ?? 0)
const failCount = Number(pickField(res, 'failCount', 'FailCount') ?? 0)
$baseMessage(
t('order.customs9610.message.batchResult', { success: successCount, fail: failCount }),
failCount > 0 ? 'warning' : 'success',
)
} catch (e: any) {
$baseMessage(e?.message || failFallback, 'error')
} finally {
loadingRef.value = false
}
},
})
}
function handleBatchFetchManifest() {
return runBatchFetch('manifest', fetchCustoms9610ManifestBatch)
}
function formatDownstreamForwardResult(item: Record<string, unknown>): string {
const id = pickField(item, 'id', 'Id')
const serviceNo = pickField(item, 'serviceNo', 'ServiceNo') || '-'
const clientNo = pickField(item, 'clientNo', 'ClientNo') || '-'
const success = Boolean(pickField(item, 'success', 'Success'))
const lines = [
`【${serviceNo} / ${clientNo} / #${id}】`,
success ? '成功' : '失败',
`standardCode:create9610Order`,
`supplierCode:BH`,
`消息:${pickField(item, 'message', 'Message') || '-'}`,
`请求:${pickField(item, 'url', 'Url') || `${prodApiOrigin()}/itfx/InterfaceForward/ForwardWithSubscribe?standardCode=create9610Order&supplierCode=BH`}`,
]
const requestJson = pickField(item, 'requestJson', 'RequestJson')
if (requestJson) {
lines.push(`请求体:\n${requestJson}`)
}
lines.push(`HTTP:${pickField(item, 'httpStatusCode', 'HttpStatusCode') ?? '-'}`)
const responseBody = pickField(item, 'responseBody', 'ResponseBody')
if (responseBody) {
lines.push(`响应:\n${responseBody}`)
}
return lines.join('\n')
}
function handleBatchFetchDeclarationForm() {
const ids = selectedIds.value
if (!ids.length) {
$baseMessage(t('order.customs9610.message.selectFirst'), 'warning')
return
}
Modal.confirm({
title: t('order.customs9610.confirm.batchDeclarationFormTitle'),
content: t('order.customs9610.confirm.batchDeclarationFormContent', { count: ids.length }),
onOk: async () => {
fetchingDeclarationForm.value = true
try {
const res = await fetchCustoms9610DeclarationFormBatch(ids)
const items = (pickField(res, 'items', 'Items') as any[]) || []
const successCount = Number(pickField(res, 'successCount', 'SuccessCount') ?? 0)
const failCount = Number(pickField(res, 'failCount', 'FailCount') ?? 0)
downstreamForwardResultText.value = items.length
? items.map((item) => formatDownstreamForwardResult(item as Record<string, unknown>)).join('\n\n')
: t('order.customs9610.message.batchResult', {
success: successCount,
fail: failCount,
})
downstreamForwardModalVisible.value = true
await loadList()
await loadCounts()
} catch (e: any) {
$baseMessage(e?.message || t('order.customs9610.message.fetchDeclarationFormFailed'), 'error')
} finally {
fetchingDeclarationForm.value = false
}
},
})
}
watch(adminCustomerId, () => {
handleSearch()
})
onMounted(() => {
loadList()
loadCounts()
})
</script>
<template>
<div class="customs9610-container">
<div class="table-section">
<div class="tabs-actions-section">
<div class="tabs-slot">
<SlidingTabs
:model-value="statusTab"
:items="slidingTabItems"
@change="handleTabChange"
/>
</div>
<div class="toolbar-left">
<div class="list-filters">
<a-select
v-if="isAdmin"
v-model="adminCustomerId"
allow-clear
allow-search
:placeholder="t('warehouse.common.selectCustomer')"
:options="customerOptions"
style="width: 200px"
@change="handleSearch"
/>
<a-input
v-model="keyword"
allow-clear
:placeholder="t('order.customs9610.search.keyword')"
style="width: 240px"
@press-enter="handleSearch"
/>
<a-button class="app-btn-search" @click="handleSearch">
{{ t('order.myOrders.search.actions.search') }}
</a-button>
<a-button class="app-btn-reset" @click="handleResetSearch">
{{ t('order.myOrders.search.actions.reset') }}
</a-button>
</div>
</div>
</div>
<div class="table-toolbar">
<div class="actions-right">
<a-button
v-if="hasPermission('Order.Customs9610.DeclareBatch')"
class="app-btn-toolbar"
:loading="declaring"
:disabled="!selectedIds.length || declaring"
@click="handleBatchDeclare"
>
{{ t('order.customs9610.actions.batchDeclare') }}
</a-button>
<a-button
v-if="hasPermission('Order.Customs9610.FetchManifestBatch')"
class="app-btn-toolbar"
:loading="fetchingManifest"
:disabled="!selectedIds.length || fetchingManifest"
@click="handleBatchFetchManifest"
>
{{ t('order.customs9610.actions.batchFetchManifest') }}
</a-button>
<a-button
v-if="hasPermission('Order.Customs9610.FetchDeclarationFormBatch')"
class="app-btn-toolbar"
:loading="fetchingDeclarationForm"
:disabled="!selectedIds.length || fetchingDeclarationForm"
@click="handleBatchFetchDeclarationForm"
>
{{ t('order.customs9610.actions.batchFetchDeclarationForm') }}
</a-button>
</div>
<div class="toolbar-right">
<span class="selected-count">
<i18n-t keypath="order.myOrders.selectedCount" tag="span" scope="global">
<template #count>
<span class="count-number">{{ selectedRowCount }}</span>
</template>
</i18n-t>
</span>
<div class="pagination-controls">
<PaginationPro
:total-count="totalCount"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</div>
</div>
</div>
<div ref="tableContainerRef" class="list-wrap">
<div v-if="!listLoading && totalCount === 0" class="list-empty-hint">
<span>{{ t('order.customs9610.empty.hint') }}</span>
</div>
<a-spin :loading="listLoading" class="table-spin">
<HandsontablePro
:data="tableRows"
:columns="hotColumns"
:height="tableHeight"
stretch-columns
show-action-column
:action-renderer="actionRenderer"
@action-click="handleRowAction"
@selection-change="handleSelectionChange"
/>
</a-spin>
</div>
</div>
<ModalPro
@register="registerEditModal"
:title="isPending ? t('order.customs9610.modal.edit') : t('order.customs9610.modal.detail')"
width="920px"
:mask-closable="false"
:confirm-loading="saving"
:ok-text="isPending ? t('warehouse.common.save') : t('warehouse.common.confirm')"
@ok="handleSave"
@cancel="closeEditModal"
>
<a-spin :loading="detailLoading" style="width: 100%">
<a-alert type="info" :show-icon="false" class="modal-form-tip">
{{ modalTip }}
</a-alert>
<a-alert
v-if="editForm.failReason"
type="error"
:show-icon="false"
class="modal-form-tip modal-form-tip--error"
>
{{ editForm.failReason }}
</a-alert>
<a-form :model="editForm" layout="vertical" class="customs-edit-form">
<div class="form-section-title">{{ t('order.customs9610.section.basic') }}</div>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.platformCode')" required>
<a-input v-model="editForm.platformCode" :disabled="!isPending" :max-length="16" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.businessType')" required>
<SlidingTabs
v-model="editForm.businessType"
size="mini"
class="field-tabs"
:items="businessTypeTabs"
:aria-label="t('order.customs9610.field.businessType')"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.preRecordFlag')">
<SlidingTabs
v-model="editForm.preRecordFlag"
size="mini"
class="field-tabs"
:items="preRecordFlagTabs"
:aria-label="t('order.customs9610.field.preRecordFlag')"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.clientNo')" required>
<a-input v-model="editForm.clientNo" :disabled="!isPending" :max-length="48" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.serviceNo')" required>
<a-input v-model="editForm.serviceNo" :disabled="!isPending" :max-length="48" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.destCountry')" required>
<a-input v-model="editForm.destCountry" :disabled="!isPending" :max-length="4" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.currency')" required>
<a-input v-model="editForm.currency" :disabled="!isPending" :max-length="4" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.grossWeight')" required>
<a-input-number
v-model="editForm.grossWeight"
:disabled="!isPending"
:min="0"
:precision="5"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.freight')">
<a-input-number
v-model="editForm.freight"
:disabled="!isPending"
:min="0"
:precision="2"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<div class="form-section-title">{{ t('order.customs9610.section.enterprise') }}</div>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.ecomCorpCode')">
<a-input v-model="editForm.ecomCorpCode" :disabled="!isPending" :max-length="18" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.ecomCorpName')">
<a-input v-model="editForm.ecomCorpName" :disabled="!isPending" :max-length="100" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.producerCode')">
<a-input v-model="editForm.producerCode" :disabled="!isPending" :max-length="18" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.producerName')">
<a-input v-model="editForm.producerName" :disabled="!isPending" :max-length="100" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.region')">
<a-input
v-model="editForm.region"
:disabled="!isPending"
:max-length="10"
:placeholder="t('order.customs9610.placeholder.region')"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.agentDeclare')">
<a-input v-model="editForm.agentDeclare" :disabled="!isPending" :max-length="2" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.actualSellerCode')">
<a-input v-model="editForm.actualSellerCode" :disabled="!isPending" :max-length="18" />
</a-form-item>
</a-col>
</a-row>
<div class="form-section-title">{{ t('order.customs9610.section.more') }}</div>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.shipmentNo')">
<a-input v-model="editForm.shipmentNo" :disabled="!isPending" :max-length="32" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.customsOrderNo')">
<a-input :model-value="editForm.customsOrderNo" disabled />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.isSummarySimple')">
<a-switch v-model="editForm.isSummarySimple" :disabled="!isPending" />
</a-form-item>
</a-col>
</a-row>
</a-form>
<div class="form-section-title items-section-title">
<span>{{ t('order.customs9610.items.title') }}</span>
<a-button v-if="isPending" size="mini" type="outline" @click="addItemRow">
{{ t('order.customs9610.items.add') }}
</a-button>
</div>
<div class="items-table-wrap">
<HandsontablePro
:key="`customs-items-${editForm.id}-${isPending ? 'edit' : 'view'}`"
:columns="itemHotColumns"
:data="editForm.items"
:height="260"
:read-only="!isPending"
:stretch-columns="true"
:enable-column-filter="false"
:show-action-column="isPending"
:action-column-header="t('warehouse.common.delete')"
:action-renderer="itemActionRenderer"
@action-click="handleItemActionClick"
/>
</div>
</a-spin>
</ModalPro>
<a-modal
v-model:visible="downstreamForwardModalVisible"
:title="t('order.customs9610.modal.downstreamForwardTitle')"
:width="800"
:footer="false"
>
<pre class="downstream-forward-result">{{ downstreamForwardResultText }}</pre>
</a-modal>
</div>
</template>
<style scoped lang="less">
.customs9610-container {
width: 100%;
box-sizing: border-box;
padding: 16px 16px 24px;
background-color: var(--app-page-bg);
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: hidden;
display: flex;
flex-direction: column;
}
.tabs-actions-section {
background: transparent;
border-radius: 0;
padding: 10px 16px;
margin-bottom: 0;
border-bottom: 1px solid var(--color-border-2);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: nowrap;
gap: 12px;
flex-shrink: 0;
min-height: 52px;
box-sizing: border-box;
.tabs-slot {
flex: 1 1 auto;
min-width: 0;
display: flex;
align-items: center;
overflow: hidden;
}
:deep(.t-tabs) {
flex: 0 1 auto;
min-width: 0;
max-width: 100%;
width: max-content;
}
}
.toolbar-left {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0;
max-width: 100%;
height: 32px;
.list-filters {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
height: 32px;
:deep(.arco-select-view),
:deep(.arco-input-wrapper),
:deep(.arco-btn) {
height: 32px;
}
:deep(.arco-select-view-single),
:deep(.arco-input-wrapper) {
display: inline-flex;
align-items: center;
}
:deep(.arco-input-wrapper) {
padding-top: 0;
padding-bottom: 0;
}
}
}
.table-section {
background: var(--app-card-bg);
border-radius: 8px;
border: 1px solid var(--app-card-border);
padding: 0;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.table-toolbar {
padding: 12px 16px 0;
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
.toolbar-right {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
.selected-count {
font-size: 13px;
color: var(--color-text-3);
white-space: nowrap;
line-height: 32px;
}
.pagination-controls {
display: flex;
align-items: center;
}
}
}
.actions-right {
display: flex;
flex-wrap: wrap;
gap: var(--app-button-gap, 8px);
align-items: center;
flex: 1;
min-width: 0;
:deep(.arco-btn) {
height: 32px;
padding: 0 12px;
border-radius: 4px;
font-size: 14px;
font-weight: 400;
line-height: 30px;
}
}
.list-empty-hint {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
pointer-events: none;
font-size: 14px;
color: var(--color-text-3);
text-align: center;
line-height: 1.6;
}
.table-spin {
width: 100%;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
:deep(.arco-spin-children) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
}
.list-wrap {
position: relative;
width: 100%;
overflow: hidden;
flex: 1;
min-height: 0;
padding: 0 16px 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overscroll-behavior: contain;
touch-action: pan-x pan-y;
:deep(.handsontable-pro-container) {
width: 100%;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
}
.items-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin: 8px 0;
font-weight: 600;
}
.modal-form-tip {
margin-bottom: 16px;
border-radius: 6px;
background: color-mix(in srgb, var(--app-primary-color) 8%, #fff);
border: 1px solid color-mix(in srgb, var(--app-primary-color) 18%, transparent);
color: var(--color-text-2);
font-size: 13px;
line-height: 1.5;
&--error {
background: color-mix(in srgb, rgb(var(--danger-6)) 8%, #fff);
border-color: color-mix(in srgb, rgb(var(--danger-6)) 22%, transparent);
color: rgb(var(--danger-6));
}
}
.form-section-title {
margin: 0 0 12px;
padding: 0 0 8px;
font-size: 13px;
font-weight: 600;
color: var(--color-text-2);
border-bottom: 1px solid var(--color-border-2);
~ .form-section-title {
margin-top: 8px;
}
}
.items-section-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.customs-edit-form {
:deep(.arco-form-item) {
margin-bottom: 16px;
}
}
.field-tabs {
width: fit-content;
max-width: 100%;
}
.items-table-wrap {
height: 260px;
min-height: 0;
overflow: hidden;
border: 1px solid var(--color-border-2);
border-radius: 6px;
:deep(.handsontable-pro-container) {
height: 100%;
min-height: 0;
}
}
.downstream-forward-result {
margin: 0;
max-height: 60vh;
overflow: auto;
padding: 12px;
background: var(--color-fill-2);
border-radius: 4px;
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
}
</style>