Files
client-web/src/modules/first-leg/views/CustomsDocumentsView.vue
T

122 lines
7.1 KiB
Vue
Raw Normal View History

<script setup lang="ts">
2026-09-29 17:56:03 +08:00
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 { MERCHANT_CUSTOMS_STATUS_LABELS, type MerchantCustomsContainer } from '../api/customs'
import { readCustomsDocuments, type CustomsDocumentRow } from '../utils/customsDocumentsReader'
import CustomsDocumentsDialog from '../components/CustomsDocumentsDialog.vue'
2026-10-20 14:24:55 +08:00
import CustomsHelp from '../components/CustomsHelp.vue'
import { useCustomsPendingStore } from '../store/customsPending'
const customsPending = useCustomsPendingStore()
const loading = ref(false)
const errorMessage = ref('')
2026-09-29 17:56:03 +08:00
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: Object.entries(MERCHANT_CUSTOMS_STATUS_LABELS).map(([value, label]) => ({ label, value })) },
2026-09-29 17:56:03 +08:00
]
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 },
2026-10-05 18:13:02 +08:00
{ label: '柜内退税箱数', prop: 'detail.actualBoxCount', width: 110, align: 'center' },
{ label: '柜内资料覆盖', slotName: 'coverage', width: 110, align: 'center' },
2026-09-29 17:56:03 +08:00
{ label: '状态', slotName: 'status', width: 130 },
]
const records = ref<CustomsDocumentRow[]>([])
const dialogOpen = ref(false)
const selectedOrderId = ref('')
const selectedDetail = ref<MerchantCustomsContainer | null>(null)
const filteredRecords = computed(() => {
2026-09-29 17:56:03 +08:00
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.merchantStatus === query.status),
2026-09-29 17:56:03 +08:00
)
})
2026-09-29 17:56:03 +08:00
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 detail.merchantStatus ? MERCHANT_CUSTOMS_STATUS_LABELS[detail.merchantStatus] ?? '—' : '—'
}
function statusType(detail: MerchantCustomsContainer) {
return detail.merchantStatus === 'CUSTOMS_COMPLETED' || detail.merchantStatus === 'DOCUMENT_UPLOADED'
? 'success' : detail.merchantStatus === 'DECLARING' ? 'primary' : detail.merchantStatus === 'WAITING_UPLOAD' ? 'warning' : 'info'
}
async function load() {
2026-09-29 17:56:03 +08:00
if (loading.value) return
void customsPending.refresh(true)
loading.value = true; errorMessage.value = ''
try {
const { records: loadedRecords, failedCount } = await readCustomsDocuments()
records.value = loadedRecords
if (failedCount) errorMessage.value = `${failedCount} 票订单的报关资料加载失败,请重新加载`
} catch (error) {
records.value = []
errorMessage.value = error instanceof Error ? error.message : '报关资料加载失败,请稍后重试'
} finally { loading.value = false }
}
function openDetail(item: unknown) {
const value = item as CustomsDocumentRow
selectedOrderId.value = value.order.id; selectedDetail.value = value.detail; dialogOpen.value = true
}
function updateDetail(value: MerchantCustomsContainer) {
const found = records.value.find((item) => item.order.id === selectedOrderId.value)
if (found) found.detail = value
}
onMounted(() => { void load() })
</script>
<template>
<div class="customs-page">
2026-09-29 17:56:03 +08:00
<QueryToolbar v-model="queryModel" layout="grid" ariaLabel="报关资料查询条件" :fields="queryFields" @search="search" />
<section class="customs-card">
2026-09-29 17:56:03 +08:00
<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">
2026-10-20 14:24:55 +08:00
<template #toolbar><strong class="customs-list-heading">资料列表<CustomsHelp title="资料统计口径"><p>统计为本客户同柜退税货物汇总,资料覆盖按入仓码计。</p></CustomsHelp></strong></template>
2026-09-29 17:56:03 +08:00
<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>
2026-09-29 17:56:03 +08:00
.customs-page { display: grid; gap: var(--client-section-gap); }
2026-10-20 14:24:55 +08:00
.customs-list-heading { display: flex; align-items: center; gap: 4px; }
2026-09-29 17:56:03 +08:00
.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>