122 lines
7.1 KiB
Vue
122 lines
7.1 KiB
Vue
<script setup lang="ts">
|
||
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'
|
||
import CustomsHelp from '../components/CustomsHelp.vue'
|
||
|
||
import { useCustomsPendingStore } from '../store/customsPending'
|
||
|
||
const customsPending = useCustomsPendingStore()
|
||
const loading = ref(false)
|
||
const errorMessage = ref('')
|
||
const queryModel = ref<QueryModel>({ orderNo: '', inboundCode: '', containerNo: '', status: '' })
|
||
const appliedQuery = ref<QueryModel>({ ...queryModel.value })
|
||
const page = ref(1)
|
||
const pageSize = ref(20)
|
||
const queryFields: QueryField[] = [
|
||
{ key: 'orderNo', label: '订单号', type: 'input', placeholder: '请输入订单号' },
|
||
{ key: 'inboundCode', label: '入仓码', type: 'input', placeholder: '请输入入仓码' },
|
||
{ key: 'containerNo', label: '柜号', type: 'input', placeholder: '请输入柜号' },
|
||
{ key: 'status', label: '状态', type: 'select', placeholder: '全部', options: Object.entries(MERCHANT_CUSTOMS_STATUS_LABELS).map(([value, label]) => ({ label, value })) },
|
||
]
|
||
const columns: DataTableColumn[] = [
|
||
{ label: '操作', slotName: 'actions', width: 120, fixed: 'left' },
|
||
{ label: '订单号', slotName: 'orderNo', minWidth: 230 },
|
||
{ label: '入仓码', slotName: 'inboundCode', minWidth: 190 },
|
||
{ label: '柜号', slotName: 'containerNo', minWidth: 170 },
|
||
{ label: '柜内退税箱数', prop: 'detail.actualBoxCount', width: 110, align: 'center' },
|
||
{ label: '柜内资料覆盖', slotName: 'coverage', width: 110, align: 'center' },
|
||
{ label: '状态', slotName: 'status', width: 130 },
|
||
]
|
||
const records = ref<CustomsDocumentRow[]>([])
|
||
const dialogOpen = ref(false)
|
||
const selectedOrderId = ref('')
|
||
const selectedDetail = ref<MerchantCustomsContainer | null>(null)
|
||
|
||
const filteredRecords = computed(() => {
|
||
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),
|
||
)
|
||
})
|
||
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() {
|
||
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">
|
||
<QueryToolbar v-model="queryModel" layout="grid" ariaLabel="报关资料查询条件" :fields="queryFields" @search="search" />
|
||
<section class="customs-card">
|
||
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="pagedRecords" :columns="columns" :loading="loading" :total="filteredRecords.length" :page-sizes="[10, 20, 50, 100]" :row-actions="rowActions" row-key="order.id" :empty-text="emptyText">
|
||
<template #toolbar><strong class="customs-list-heading">资料列表<CustomsHelp title="资料统计口径"><p>统计为本客户同柜退税货物汇总,资料覆盖按入仓码计。</p></CustomsHelp></strong></template>
|
||
<template #toolbar-actions><el-button :loading="loading" @click="load"><RefreshCcw :size="16" />刷新</el-button></template>
|
||
<template v-if="errorMessage" #error><div class="state-error" role="alert"><span>{{ errorMessage }}</span><el-button :loading="loading" @click="load">重新加载</el-button></div></template>
|
||
<template #orderNo="{ row }">{{ row.order.orderNo || '—' }}</template>
|
||
<template #inboundCode="{ row }">{{ row.order.businessSummary.inboundCode || '—' }}</template>
|
||
<template #containerNo="{ row }">{{ row.detail.containerNo || row.detail.cabinetNo || '—' }}</template>
|
||
<template #coverage="{ row }">{{ row.detail.coveredTaxRefundCount }} / {{ row.detail.taxRefundCount }}</template>
|
||
<template #status="{ row }"><el-tag :type="statusType(row.detail)" effect="light" size="small">{{ statusLabel(row.detail) }}</el-tag></template>
|
||
</DataTable>
|
||
</section>
|
||
<CustomsDocumentsDialog v-model="dialogOpen" :order-id="selectedOrderId" :can-edit="selectedDetail?.status !== 'COMPLETED'" :initial-detail="selectedDetail" @updated="updateDetail" />
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.customs-page { display: grid; gap: var(--client-section-gap); }
|
||
.customs-list-heading { display: flex; align-items: center; gap: 4px; }
|
||
.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>
|