front/src/views/order/customs9610Order/components/CreateCustoms9610Modal.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

520 lines
16 KiB
Vue

<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import type { TableColumnData, TableRowSelection } from '@arco-design/web-vue'
import ModalPro from '@/components/ModalPro/index.vue'
import useModal from '@/components/ModalPro/useModal'
import { postOrderList } from '@/api/order'
import { createCustoms9610FromOrders } from '@/api/order/customs9610'
import { getExportQualificationList } from '@/api/customer/exportQualification'
import { getDictListAll } from '@/api/system/dict'
import { getOrderStatusMeta, resolveOrderStatusCode } from '@/constants/orderStatus'
import { $baseMessage } from '@/utils'
import { formatDateTime, normalizeListRow, pickField } from '@/utils/warehouseListMapper'
interface OrderPickRow {
id: number
serviceNo: string
clientNo: string
customerName: string
country: string
statusName: string
createTime: string
/** 已是 9610 汇总申报的运单不可再选 */
is9610: boolean
/** a-table 行级禁用勾选 */
disabled: boolean
}
interface FailRow {
orderId: number
serviceNo: string
message: string
}
const ORDER_PAGE_SIZE = 30
const SUMMARY_MODE = '9610summary'
const props = defineProps<{
isAdmin: boolean
customerOptions: Array<{ label: string; value: number }>
/** 列表页已选客户,作为管理端默认客户 */
defaultCustomerId?: number
}>()
const emit = defineEmits<{
success: []
}>()
const { t } = useI18n()
const router = useRouter()
const [registerModal, { openModal, closeModal }] = useModal() as any
const submitting = ref(false)
const form = reactive({
customerId: undefined as number | undefined,
exportQualificationId: undefined as number | undefined,
platformCode: '',
})
const qualificationOptions = ref<Array<{ label: string; value: number }>>([])
const qualificationLoading = ref(false)
const platformOptions = ref<Array<{ label: string; value: string }>>([])
const keyword = ref('')
const orderLoading = ref(false)
const orderRows = ref<OrderPickRow[]>([])
const orderPage = ref(1)
const orderTotal = ref(0)
const selectedKeys = ref<number[]>([])
/** 跨页保留已选运单信息,用于结果展示 */
const selectedMap = ref<Map<number, OrderPickRow>>(new Map())
const failRows = ref<FailRow[]>([])
const needCustomer = computed(() => props.isAdmin && !form.customerId)
const orderColumns = computed<TableColumnData[]>(() => [
{ title: t('order.customs9610.create.col.serviceNo'), dataIndex: 'serviceNo', width: 180, ellipsis: true, tooltip: true },
{ title: t('order.customs9610.create.col.clientNo'), dataIndex: 'clientNo', width: 150, ellipsis: true, tooltip: true },
...(props.isAdmin
? [{ title: t('order.customs9610.create.col.customer'), dataIndex: 'customerName', width: 140, ellipsis: true, tooltip: true }]
: []),
{ title: t('order.customs9610.create.col.country'), dataIndex: 'country', width: 90 },
{ title: t('order.customs9610.create.col.status'), dataIndex: 'statusName', width: 100 },
{ title: t('order.customs9610.create.col.createTime'), dataIndex: 'createTime', width: 160 },
{ title: t('order.customs9610.create.col.remark'), slotName: 'remark', width: 120 },
])
const rowSelection = computed<TableRowSelection>(() => ({
type: 'checkbox',
showCheckedAll: true,
onlyCurrent: false,
}))
const orderPagination = computed(() => ({
current: orderPage.value,
pageSize: ORDER_PAGE_SIZE,
total: orderTotal.value,
size: 'small' as const,
showTotal: true,
}))
const canSubmit = computed(
() =>
!submitting.value
&& !needCustomer.value
&& selectedKeys.value.length > 0
&& !!form.platformCode,
)
function statusName(code: string) {
const meta = getOrderStatusMeta(code)
return meta ? t(meta.labelKey) : code
}
function mapOrderRow(raw: any): OrderPickRow | null {
const row = normalizeListRow(raw)
const id = Number(pickField(row, 'id', 'Id') || 0)
if (!id) return null
const status = resolveOrderStatusCode(row) || String(pickField(row, 'status', 'Status') || '')
if (status === 'deleted') return null
const mode = String(pickField(row, 'exportCustomsMode', 'ExportCustomsMode') || '').toLowerCase()
const is9610 = mode === SUMMARY_MODE
return {
id,
serviceNo: String(pickField(row, 'serviceNo', 'ServiceNo') || '-'),
clientNo: String(pickField(row, 'clientNo', 'ClientNo') || '-'),
customerName: String(pickField(row, 'customerName', 'CustomerName', 'customerCode', 'CustomerCode') || '-'),
country: String(pickField(row, 'consigneeCountry', 'ConsigneeCountry') || '-'),
statusName: statusName(status),
createTime: formatDateTime(pickField(row, 'createTime', 'CreateTime')) || '-',
is9610,
disabled: is9610,
}
}
async function loadOrders(page = 1) {
if (needCustomer.value) {
orderRows.value = []
orderTotal.value = 0
return
}
orderLoading.value = true
orderPage.value = page
try {
const body: Record<string, unknown> = {
page,
pageSize: ORDER_PAGE_SIZE,
status: 'all',
keyword: keyword.value.trim(),
}
if (props.isAdmin && form.customerId) body.customerId = form.customerId
const res: any = await postOrderList(body as any)
orderRows.value = (res.items || [])
.map((raw: any) => mapOrderRow(raw))
.filter((x: OrderPickRow | null): x is OrderPickRow => !!x)
orderTotal.value = Number(res.total ?? res.totalCount ?? orderRows.value.length) || 0
} catch (e: any) {
orderRows.value = []
orderTotal.value = 0
$baseMessage(e?.message || t('order.customs9610.create.message.loadOrdersFailed'), 'error')
} finally {
orderLoading.value = false
}
}
function handleSelectionChange(keys: Array<string | number>) {
const ids = (keys || []).map(Number).filter((id) => id > 0)
const next = new Map<number, OrderPickRow>()
for (const id of ids) {
const row = orderRows.value.find((r) => r.id === id) ?? selectedMap.value.get(id)
if (row && !row.is9610) next.set(id, row)
}
selectedMap.value = next
selectedKeys.value = [...next.keys()]
}
function clearSelection() {
selectedMap.value = new Map()
selectedKeys.value = []
}
async function loadQualifications() {
qualificationOptions.value = []
form.exportQualificationId = undefined
if (needCustomer.value) return
qualificationLoading.value = true
try {
const body: Record<string, unknown> = { page: 1, pageSize: 200 }
if (props.isAdmin && form.customerId) body.customerId = form.customerId
const res: any = await getExportQualificationList(body)
const items = Array.isArray(res?.items) ? res.items : []
let defaultId: number | undefined
qualificationOptions.value = items
.map((raw: any) => {
const id = Number(pickField(raw, 'id', 'Id') || 0)
if (!id) return null
const name = String(pickField(raw, 'companyName', 'CompanyName') || pickField(raw, 'socialCreditCode', 'SocialCreditCode') || `#${id}`)
const isDefault = Boolean(pickField(raw, 'isDefault', 'IsDefault'))
if (isDefault) defaultId = id
return {
label: isDefault ? `${name}${t('order.customs9610.create.defaultSuffix')}` : name,
value: id,
}
})
.filter((x: any): x is { label: string; value: number } => !!x)
form.exportQualificationId = defaultId ?? qualificationOptions.value[0]?.value
} catch {
qualificationOptions.value = []
} finally {
qualificationLoading.value = false
}
}
async function ensurePlatformOptions() {
if (platformOptions.value.length) return
try {
const rows = await getDictListAll({ dictType: 'e-commerce_platform' })
platformOptions.value = rows
.map((r: any) => ({
label: String(r.dictName ?? r.dictCode ?? '').trim(),
value: String(r.dictCode ?? '').trim(),
}))
.filter((x) => x.value)
} catch {
platformOptions.value = []
}
}
function goQualificationManage() {
router.push({ path: '/user/exportQualification' })
}
async function open() {
form.customerId = props.isAdmin ? props.defaultCustomerId : undefined
form.platformCode = ''
keyword.value = ''
failRows.value = []
clearSelection()
openModal()
await Promise.all([ensurePlatformOptions(), loadQualifications(), loadOrders(1)])
}
function close() {
if (submitting.value) return
closeModal()
}
async function handleSubmit() {
if (!canSubmit.value) return
submitting.value = true
try {
const res: any = await createCustoms9610FromOrders({
orderIds: [...selectedKeys.value],
exportQualificationId: form.exportQualificationId,
platformCode: form.platformCode,
})
const items: any[] = Array.isArray(res?.items ?? res?.Items) ? (res.items ?? res.Items) : []
const successCount = Number(pickField(res, 'successCount', 'SuccessCount') ?? 0)
const failed = items.filter((x) => !pickField(x, 'success', 'Success'))
if (successCount > 0) emit('success')
if (!failed.length) {
$baseMessage(t('order.customs9610.create.message.allSuccess', { count: successCount }), 'success')
closeModal()
return
}
failRows.value = failed.map((x) => {
const orderId = Number(pickField(x, 'orderId', 'OrderId') || 0)
return {
orderId,
serviceNo: String(pickField(x, 'serviceNo', 'ServiceNo') || selectedMap.value.get(orderId)?.serviceNo || orderId),
message: String(pickField(x, 'message', 'Message') || t('order.customs9610.create.message.failed')),
}
})
// 成功的运单从已选中移除,保留失败的便于调整后重试
const failedIds = new Set(failRows.value.map((x) => x.orderId))
const next = new Map([...selectedMap.value].filter(([id]) => failedIds.has(id)))
selectedMap.value = next
selectedKeys.value = [...next.keys()]
$baseMessage(
t('order.customs9610.create.message.partial', { success: successCount, fail: failed.length }),
'warning',
)
if (successCount > 0) void loadOrders(orderPage.value)
} catch (e: any) {
$baseMessage(e?.message || t('order.customs9610.create.message.failed'), 'error')
} finally {
submitting.value = false
}
}
watch(
() => form.customerId,
(val, old) => {
if (val === old) return
clearSelection()
failRows.value = []
void loadQualifications()
void loadOrders(1)
},
)
defineExpose({ open })
</script>
<template>
<ModalPro
@register="registerModal"
:title="t('order.customs9610.create.title')"
width="1000px"
:mask-closable="false"
:closable="!submitting"
@cancel="close"
>
<a-alert type="info" :show-icon="false" class="c9610-create__tip">
{{ t('order.customs9610.create.tip') }}
</a-alert>
<div class="c9610-create__section-title">{{ t('order.customs9610.create.sectionSupplement') }}</div>
<a-form :model="form" layout="vertical" class="c9610-create__form">
<a-row :gutter="20">
<a-col v-if="isAdmin" :span="8">
<a-form-item :label="t('order.customs9610.create.customer')" required>
<a-select
v-model="form.customerId"
allow-search
:options="customerOptions"
:placeholder="t('order.customs9610.create.customerPh')"
/>
</a-form-item>
</a-col>
<a-col :span="isAdmin ? 8 : 12">
<a-form-item :label="t('order.customs9610.create.exportQualification')">
<a-select
v-model="form.exportQualificationId"
allow-clear
:loading="qualificationLoading"
:disabled="needCustomer"
:options="qualificationOptions"
:placeholder="t('order.customs9610.create.exportQualificationPh')"
/>
<template #extra>
<a-link :hoverable="false" @click="goQualificationManage">
{{ t('order.customs9610.create.manageQualification') }}
</a-link>
</template>
</a-form-item>
</a-col>
<a-col :span="isAdmin ? 8 : 12">
<a-form-item :label="t('order.customs9610.create.platform')" required>
<a-select
v-model="form.platformCode"
allow-search
:options="platformOptions"
:placeholder="t('order.customs9610.create.platformPh')"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
<div class="c9610-create__section-title">
{{ t('order.customs9610.create.sectionOrders') }}
</div>
<div class="c9610-create__toolbar">
<a-input
v-model="keyword"
allow-clear
:disabled="needCustomer"
:placeholder="t('order.customs9610.create.keywordPh')"
style="width: 260px"
@press-enter="loadOrders(1)"
/>
<a-button class="app-btn-search" :loading="orderLoading" :disabled="needCustomer" @click="loadOrders(1)">
{{ t('order.myOrders.search.actions.search') }}
</a-button>
<span class="c9610-create__selected">
{{ t('order.customs9610.create.selected', { count: selectedKeys.length }) }}
<a-link v-if="selectedKeys.length" :hoverable="false" @click="clearSelection">
{{ t('order.customs9610.create.clearSelected') }}
</a-link>
</span>
</div>
<a-table
row-key="id"
size="small"
:loading="orderLoading"
:data="orderRows"
:columns="orderColumns"
:row-selection="rowSelection"
:selected-keys="selectedKeys"
:pagination="orderPagination"
:scroll="{ y: 300 }"
:row-class="(record: OrderPickRow) => (record.is9610 ? 'c9610-create__row--disabled' : '')"
@selection-change="handleSelectionChange"
@page-change="loadOrders"
>
<template #remark="{ record }">
<a-tag v-if="record.is9610" size="small" color="gray">
{{ t('order.customs9610.create.already9610') }}
</a-tag>
</template>
<template #empty>
<div class="c9610-create__empty">
{{ needCustomer ? t('order.customs9610.create.selectCustomerFirst') : t('order.customs9610.create.emptyOrders') }}
</div>
</template>
</a-table>
<div v-if="failRows.length" class="c9610-create__fail">
<div class="c9610-create__fail-title">
{{ t('order.customs9610.create.failTitle', { count: failRows.length }) }}
</div>
<ul>
<li v-for="row in failRows" :key="row.orderId">
<b>{{ row.serviceNo }}</b>
<span>{{ row.message }}</span>
</li>
</ul>
</div>
<template #footer>
<div class="c9610-create__footer">
<a-button :disabled="submitting" @click="close">{{ t('common.cancel') }}</a-button>
<a-button type="primary" :loading="submitting" :disabled="!canSubmit" @click="handleSubmit">
{{ t('order.customs9610.create.submit', { count: selectedKeys.length }) }}
</a-button>
</div>
</template>
</ModalPro>
</template>
<style scoped lang="less">
.c9610-create__tip {
margin-bottom: 16px;
}
.c9610-create__section-title {
margin-bottom: 12px;
padding-left: 8px;
border-left: 3px solid var(--app-primary-color);
font-size: 14px;
font-weight: 600;
color: var(--color-text-1);
line-height: 1.2;
}
.c9610-create__form {
margin-bottom: 4px;
}
.c9610-create__toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.c9610-create__selected {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--color-text-2);
}
.c9610-create__empty {
padding: 24px 12px;
text-align: center;
font-size: 13px;
color: var(--color-text-3);
}
:deep(.c9610-create__row--disabled) {
color: var(--color-text-4);
}
.c9610-create__fail {
margin-top: 12px;
padding: 10px 12px;
border-radius: 4px;
background: rgb(var(--danger-1));
ul {
margin: 6px 0 0;
padding: 0;
list-style: none;
max-height: 120px;
overflow-y: auto;
}
li {
display: flex;
gap: 12px;
padding: 2px 0;
font-size: 12px;
color: rgb(var(--danger-6));
}
}
.c9610-create__fail-title {
font-size: 13px;
font-weight: 600;
color: rgb(var(--danger-6));
}
.c9610-create__footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
:deep(.arco-table-th) {
background: var(--color-fill-1);
}
</style>