front/src/views/order/customs9610Order/index.vue
zzh b178a2a35d
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m56s
feat: 支付选券、优惠入应收展示、申报产品标识编码、创建9610订单等
- 支付弹窗(PC/H5)支持选券与批量支付体验优化,运单管理复用我的订单支付弹窗;立即支付仅允许待支付订单
- 订单详情费用明细展示优惠券抵扣/会员折扣负数行与「含优惠」;价格配置与手工加费过滤抵扣费用项
- 申报明细新增产品标识编码/非标产品标识编码/标准产品标识编码(PC/H5 下单、改单、详情、开放接口文档、公式目录、Excel 模板)
- 9610 页「批量申报」改为「创建9610订单」:选运单并补充出口资质备案、电商平台
- 店铺订单批量按钮按动作独立 loading,获取面单限并发并显示进度
- 另含:国家管理、登录门户切换、监控与工作台、公告弹窗、nginx/vite 配置等改动
2026-10-19 16:34:06 +08:00

1267 lines
41 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 {
fetchCustoms9610DeclarationFormBatch,
fetchCustoms9610ManifestBatch,
getCustoms9610Detail,
getCustoms9610List,
getCustoms9610StatusCounts,
updateCustoms9610,
} from '@/api/order/customs9610'
import { $baseMessage } from '@/utils'
import { hasPermission } from '@/utils/rbac-permission'
import { formatDateTime, normalizeListRow, pickField } from '@/utils/warehouseListMapper'
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
import SlidingTabs from '@/components/SlidingTabs/index.vue'
import CreateCustoms9610Modal from './components/CreateCustoms9610Modal.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 createModalRef = ref<InstanceType<typeof CreateCustoms9610Modal> | null>(null)
const fetchingManifest = ref(false)
const fetchingDeclarationForm = ref(false)
const downstreamForwardModalVisible = ref(false)
const downstreamForwardResultText = ref('')
const saving = ref(false)
const detailLoading = ref(false)
/** edit:可保存;detail:只读查看(与编辑同表单,不打开运单详情) */
const modalMode = ref<'edit' | 'detail'>('edit')
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: 'SZ',
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 isFormEditable = computed(() => modalMode.value === 'edit' && isPending.value)
const modalTitle = computed(() =>
modalMode.value === 'detail'
? t('order.customs9610.modal.detail')
: t('order.customs9610.modal.edit'),
)
const modalTip = computed(() =>
modalMode.value === 'detail'
? t('order.customs9610.modal.tipDetail')
: t('order.customs9610.modal.tipEdit'),
)
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: t('order.customs9610.field.businessTypeA'), disabled: !isFormEditable.value },
{ key: 'B', label: t('order.customs9610.field.businessTypeB'), disabled: !isFormEditable.value },
])
const preRecordFlagTabs = computed(() => [
{ key: '0', label: t('order.customs9610.field.preRecordFlag0'), disabled: !isFormEditable.value },
{ key: '1', label: t('order.customs9610.field.preRecordFlag1'), disabled: !isFormEditable.value },
])
const regionOptions = computed(() => [
{ value: 'SZ', label: t('order.customs9610.field.regionSZ') },
{ value: 'other', label: t('order.customs9610.field.regionOther') },
])
const agentDeclareChecked = computed({
get: () => editForm.agentDeclare === '是',
set: (checked: boolean) => {
editForm.agentDeclare = checked ? '是' : '否'
},
})
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: 'clientNo', label: t('order.customs9610.field.clientNo'), width: 140 },
{ key: 'serviceNo', label: t('order.customs9610.field.serviceNo'), width: 150 },
{ key: 'downstreamRefNo', label: t('order.customs9610.field.downstreamRefNo'), width: 150 },
// 基础信息
{ key: 'platformCode', label: t('order.customs9610.field.platformCode'), width: 110 },
{ key: 'businessTypeLabel', label: t('order.customs9610.field.businessType'), width: 120 },
{ key: 'preRecordFlagLabel', label: t('order.customs9610.field.preRecordFlag'), width: 110 },
{ key: 'destCountry', label: t('order.customs9610.field.destCountry'), width: 80 },
{ key: 'currency', label: t('order.customs9610.field.currency'), width: 70 },
{ key: 'grossWeight', label: t('order.customs9610.field.grossWeight'), width: 100 },
{ key: 'freight', label: t('order.customs9610.field.freight'), width: 90 },
// 企业信息
{ key: 'ecomCorpCode', label: t('order.customs9610.field.ecomCorpCode'), width: 130 },
{ key: 'ecomCorpName', label: t('order.customs9610.field.ecomCorpName'), width: 140 },
{ key: 'producerCode', label: t('order.customs9610.field.producerCode'), width: 140 },
{ key: 'producerName', label: t('order.customs9610.field.producerName'), width: 140 },
{ key: 'regionLabel', label: t('order.customs9610.field.region'), width: 110 },
{ key: 'agentDeclare', label: t('order.customs9610.field.agentDeclare'), width: 110 },
{ key: 'actualSellerCode', label: t('order.customs9610.field.actualSellerCode'), width: 140 },
// 列表附加
{ key: 'customerName', label: t('warehouse.common.customerName'), width: 120 },
{ key: 'statusLabel', label: t('order.customs9610.table.status'), width: 90 },
{ 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 displayText(value: unknown) {
const text = String(value ?? '').trim()
return text || '-'
}
function businessTypeLabel(code: unknown) {
const value = String(code ?? '').trim().toUpperCase()
if (value === 'A') return t('order.customs9610.field.businessTypeA')
if (value === 'B') return t('order.customs9610.field.businessTypeB')
return displayText(code)
}
function preRecordFlagLabel(code: unknown) {
const value = String(code ?? '').trim()
if (value === '0') return t('order.customs9610.field.preRecordFlag0')
if (value === '1') return t('order.customs9610.field.preRecordFlag1')
return displayText(code)
}
function regionLabel(code: unknown) {
const text = String(code ?? '').trim()
if (!text) return '-'
const value = normalizeRegion(code)
if (value === 'SZ') return t('order.customs9610.field.regionSZ')
if (value === 'other') return t('order.customs9610.field.regionOther')
return displayText(code)
}
function agentDeclareLabel(raw: unknown) {
const text = String(raw ?? '').trim()
if (!text) return '-'
return normalizeAgentDeclare(raw)
}
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') ?? '')
const businessType = String(pickField(raw, 'businessType', 'FormType') ?? '')
const preRecordFlag = String(pickField(raw, 'preRecordFlag', 'SoLabelFlag') ?? '')
const region = pickField(raw, 'region', 'Region')
const freight = pickField(raw, 'freight', 'Freight')
return {
...row,
id: Number(pickField(raw, 'id', 'Id') ?? 0),
orderId: Number(pickField(raw, 'orderId', 'OrderId') ?? 0),
status,
platformCode: displayText(pickField(raw, 'platformCode', 'Ebp')),
businessTypeLabel: businessTypeLabel(businessType),
preRecordFlagLabel: preRecordFlagLabel(preRecordFlag),
clientNo: displayText(pickField(raw, 'clientNo', 'ClientNo')),
serviceNo: displayText(pickField(raw, 'serviceNo', 'ServiceNo')),
downstreamRefNo: displayText(pickField(raw, 'downstreamRefNo', 'DownstreamRefNo')),
destCountry: displayText(pickField(raw, 'destCountry', 'Country')),
currency: displayText(pickField(raw, 'currency', 'Currency')),
grossWeight: pickField(raw, 'grossWeight', 'GrossWeight') ?? '-',
freight: freight == null || freight === '' ? '-' : freight,
ecomCorpCode: displayText(pickField(raw, 'ecomCorpCode', 'EbcCode')),
ecomCorpName: displayText(pickField(raw, 'ecomCorpName', 'EbcName')),
producerCode: displayText(pickField(raw, 'producerCode', 'OwnerCode')),
producerName: displayText(pickField(raw, 'producerName', 'OwnerName')),
regionLabel: regionLabel(region),
agentDeclare: agentDeclareLabel(pickField(raw, 'agentDeclare', 'IsAgent')),
actualSellerCode: displayText(pickField(raw, 'actualSellerCode', 'ActBuyer')),
customerName: displayText(pickField(raw, 'customerName', 'CustomerName')),
statusLabel: statusLabel(status),
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 normalizeRegion(raw: unknown) {
const value = String(raw ?? '').trim()
if (value.toLowerCase() === 'other') return 'other'
if (value.toUpperCase() === 'SZ') return 'SZ'
return value || 'SZ'
}
function normalizeAgentDeclare(raw: unknown) {
const value = String(raw ?? '').trim()
if (!value) return '是'
if (['否', 'N', 'n', '0', 'false', 'False'].includes(value)) return '否'
return '是'
}
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: normalizeRegion(pickField(data, 'region', 'Region')),
agentDeclare: normalizeAgentDeclare(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 openCustomsModal(id: number, mode: 'edit' | 'detail') {
modalMode.value = mode
detailLoading.value = true
openEditModal(true)
try {
const data = await getCustoms9610Detail(id)
fillEditForm(data)
} 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') {
void openCustomsModal(record.id, 'detail')
} else if (action === 'edit') {
void openCustomsModal(record.id, 'edit')
}
}
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 (!isFormEditable.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
}
}
function handleOpenCreate() {
void createModalRef.value?.open()
}
async function handleCreateSuccess() {
await Promise.all([loadList(), loadCounts()])
}
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') || '-'}`,
]
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
}
const missingPlatform = tableRows.value.filter((row) => {
if (!ids.includes(Number(row.id))) return false
const raw = String(pickField(row._raw, 'platformCode', 'Ebp', 'PlatformCode') ?? '').trim()
return !raw
})
if (missingPlatform.length) {
$baseMessage(t('order.customs9610.message.platformCodeMissingForForward'), '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(() => {
void Promise.all([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"
@click="handleOpenCreate"
>
{{ t('order.customs9610.actions.createFromOrders') }}
</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>
<CreateCustoms9610Modal
ref="createModalRef"
:is-admin="isAdmin"
:customer-options="customerOptions"
:default-customer-id="adminCustomerId"
@success="handleCreateSuccess"
/>
<ModalPro
@register="registerEditModal"
:title="modalTitle"
width="920px"
:mask-closable="false"
:confirm-loading="saving"
:ok-text="t('warehouse.common.save')"
:footer="modalMode === 'edit' ? undefined : false"
@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="!isFormEditable" :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>
<template #label>
<span class="label-with-tip">
{{ t('order.customs9610.field.preRecordFlag') }}
<a-tooltip :content="t('order.customs9610.tip.preRecordFlag')">
<icon-question-circle class="tip-icon" />
</a-tooltip>
</span>
</template>
<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="!isFormEditable" :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="!isFormEditable" :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="!isFormEditable" :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="!isFormEditable" :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="!isFormEditable"
: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="!isFormEditable"
: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="!isFormEditable" :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="!isFormEditable" :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="!isFormEditable" :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="!isFormEditable" :max-length="100" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.region')">
<a-select
v-model="editForm.region"
:disabled="!isFormEditable"
:options="regionOptions"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item :label="t('order.customs9610.field.agentDeclare')">
<a-switch v-model="agentDeclareChecked" :disabled="!isFormEditable" />
</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="!isFormEditable" :max-length="18" />
</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="isFormEditable" size="mini" class="app-btn-primary-soft" @click="addItemRow">
{{ t('order.customs9610.items.add') }}
</a-button>
</div>
<div class="items-table-wrap">
<HandsontablePro
:key="`customs-items-${editForm.id}-${isFormEditable ? 'edit' : 'view'}`"
:columns="itemHotColumns"
:data="editForm.items"
:height="260"
:read-only="!isFormEditable"
:stretch-columns="true"
:enable-column-filter="false"
:show-action-column="isFormEditable"
: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%;
}
.label-with-tip {
display: inline-flex;
align-items: center;
gap: 4px;
}
.tip-icon {
color: var(--color-text-3);
cursor: help;
font-size: 14px;
}
.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>