feat(admin): add formal customs result actions
This commit is contained in:
@@ -9,10 +9,15 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
||||
import {
|
||||
completeAllBuyContainerCustoms,
|
||||
createDeclarationBatch,
|
||||
downloadContainerCustomsReceipt,
|
||||
downloadContainerCustomsFinal,
|
||||
downloadContainerCustomsDocument,
|
||||
getContainerCustoms,
|
||||
getDeclarationBatches,
|
||||
recordClearanceResult,
|
||||
resolveClearanceResult,
|
||||
submitDeclarationBatch,
|
||||
startContainerCustoms,
|
||||
submitContainerCustomsToBroker,
|
||||
uploadContainerCustomsReceipt,
|
||||
@@ -20,6 +25,7 @@ import {
|
||||
type ContainerCustomsDocument,
|
||||
type ContainerCustomsStatus,
|
||||
type ContainerCustomsView,
|
||||
type DeclarationBatch,
|
||||
} from '../api/platformContainerCustoms'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -31,6 +37,7 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{ refreshed: [] }>()
|
||||
|
||||
const customs = ref<ContainerCustomsView | null>(null)
|
||||
const declarationBatches = ref<DeclarationBatch[]>([])
|
||||
const loading = ref(false)
|
||||
const actionLoading = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
@@ -43,6 +50,8 @@ const missingInbounds = computed(() => customs.value?.inbounds.filter((item) =>
|
||||
const missingCoverageCount = computed(() => Math.max(0, (customs.value?.taxRefundCount || 0) - (customs.value?.coveredTaxRefundCount || 0)))
|
||||
const allBuy = computed(() => customs.value?.hasActualCargo === true && customs.value.taxRefundCount === 0)
|
||||
const brokerSubmitted = computed(() => Boolean(customs.value?.brokerSubmittedAt))
|
||||
const latestDeclarationBatch = computed(() => declarationBatches.value[0] || null)
|
||||
const openDeclarationBatch = computed(() => declarationBatches.value.find((item) => ['READY', 'SUBMITTED', 'ACKNOWLEDGED', 'RESULT_PENDING'].includes(item.status)) || null)
|
||||
const statusText: Record<ContainerCustomsStatus, string> = {
|
||||
WAITING_CUSTOMER_DOCUMENTS: '待客户补齐退税资料', READY_TO_START: '可开始报关', DECLARING: '报关中', COMPLETED: '报关已完成',
|
||||
}
|
||||
@@ -65,11 +74,65 @@ async function load() {
|
||||
if (!props.canList || !props.loadingPlanId) return
|
||||
loading.value = true
|
||||
loadError.value = null
|
||||
try { customs.value = await getContainerCustoms(props.loadingPlanId) }
|
||||
try {
|
||||
customs.value = await getContainerCustoms(props.loadingPlanId)
|
||||
if (customs.value?.status === 'COMPLETED') {
|
||||
try { declarationBatches.value = await getDeclarationBatches(props.loadingPlanId) } catch { declarationBatches.value = [] }
|
||||
} else declarationBatches.value = []
|
||||
}
|
||||
catch (error) { customs.value = null; loadError.value = getApiErrorDetails(error, '整柜报关状态加载失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
function declarationStatusLabel(value: DeclarationBatch['status']) {
|
||||
return ({ READY: '待提交', SUBMITTED: '已提交', ACKNOWLEDGED: '已受理', RESULT_PENDING: '待处理结果', CLOSED: '已关闭', CANCELLED: '已取消' } as Record<string, string>)[value] || value
|
||||
}
|
||||
|
||||
async function createFormalDeclaration() {
|
||||
if (!customs.value || customs.value.status !== 'COMPLETED' || openDeclarationBatch.value) return
|
||||
try { await ElMessageBox.confirm('当前柜的资料、回执和真实报关完税文件已完成,确认创建正式申报批次?', '创建正式申报批次', { customClass: 'admin-density-confirm', type: 'warning', confirmButtonText: '确认创建' }) } catch { return }
|
||||
actionLoading.value = true; actionError.value = null
|
||||
try { await createDeclarationBatch(props.loadingPlanId, customs.value.version); await load(); ElMessage.success('正式申报批次已创建') }
|
||||
catch (error) { actionError.value = getApiErrorDetails(error, '正式申报批次创建失败') }
|
||||
finally { actionLoading.value = false }
|
||||
}
|
||||
|
||||
async function submitFormalDeclaration(batch: DeclarationBatch) {
|
||||
try {
|
||||
const answer = await ElMessageBox.prompt('请输入报关单号', '提交正式申报', { inputValidator: (value) => value.trim() ? true : '请输入报关单号', confirmButtonText: '确认提交' })
|
||||
actionLoading.value = true; actionError.value = null
|
||||
await submitDeclarationBatch(batch, answer.value)
|
||||
await load(); ElMessage.success('正式申报已提交')
|
||||
} catch (error) {
|
||||
if (error === 'cancel' || error === 'close') return
|
||||
actionError.value = getApiErrorDetails(error, '正式申报提交失败')
|
||||
} finally { actionLoading.value = false }
|
||||
}
|
||||
|
||||
async function recordFormalResult(batch: DeclarationBatch) {
|
||||
try {
|
||||
const answer = await ElMessageBox.prompt('可填写查验处理说明,没有说明可直接确认', '录入海关查验结果', { inputPlaceholder: '处理说明(可选)', confirmButtonText: '确认录入' })
|
||||
actionLoading.value = true; actionError.value = null
|
||||
await recordClearanceResult(batch, 'INSPECTION', answer.value)
|
||||
await load(); ElMessage.success('海关结果已录入')
|
||||
} catch (error) {
|
||||
if (error === 'cancel' || error === 'close') return
|
||||
actionError.value = getApiErrorDetails(error, '海关结果录入失败')
|
||||
} finally { actionLoading.value = false }
|
||||
}
|
||||
|
||||
async function resolveFormalResult(batch: DeclarationBatch, result: DeclarationBatch['results'][number]) {
|
||||
try {
|
||||
const answer = await ElMessageBox.prompt('请输入处理说明', '处理海关结果', { inputValue: result.resolution || '', inputValidator: (value) => value.trim() ? true : '请输入处理说明', confirmButtonText: '确认处理' })
|
||||
actionLoading.value = true; actionError.value = null
|
||||
await resolveClearanceResult(result, batch.version, answer.value)
|
||||
await load(); ElMessage.success('海关结果已处理')
|
||||
} catch (error) {
|
||||
if (error === 'cancel' || error === 'close') return
|
||||
actionError.value = getApiErrorDetails(error, '海关结果处理失败')
|
||||
} finally { actionLoading.value = false }
|
||||
}
|
||||
|
||||
async function start() {
|
||||
if (!customs.value || customs.value.status !== 'READY_TO_START') return
|
||||
try {
|
||||
@@ -211,6 +274,18 @@ onMounted(load)
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</div>
|
||||
<div v-if="customs.status === 'COMPLETED'" class="compact-table-wrap declaration-batches-wrap">
|
||||
<div class="declaration-batches-heading"><div><h3>正式申报与海关结果</h3><p>正式申报批次建立在退税资料、回执确认和真实完税文件完成之后,与船期和 PDA 节点独立。</p></div><el-button v-if="canManage && !openDeclarationBatch" size="small" type="primary" :loading="actionLoading" @click="createFormalDeclaration">创建正式申报批次</el-button></div>
|
||||
<el-empty v-if="!declarationBatches.length" description="尚未创建正式申报批次" :image-size="48" />
|
||||
<div v-else class="declaration-batch-list">
|
||||
<article v-for="batch in declarationBatches" :key="batch.id" class="declaration-batch-card">
|
||||
<div class="declaration-batch-main"><strong>{{ batch.batchNo }}</strong><span>报关单号:{{ batch.customsEntryNo || '—' }} · 版本 {{ batch.version }}</span></div>
|
||||
<el-tag size="small" :type="batch.status === 'CLOSED' ? 'success' : batch.status === 'CANCELLED' ? 'info' : 'warning'">{{ declarationStatusLabel(batch.status) }}</el-tag>
|
||||
<div class="declaration-batch-actions" v-if="canManage"><el-button v-if="batch.status === 'READY'" size="small" type="primary" :loading="actionLoading" @click="submitFormalDeclaration(batch)">提交正式申报</el-button><el-button v-if="['SUBMITTED','ACKNOWLEDGED','RESULT_PENDING'].includes(batch.status)" size="small" plain :loading="actionLoading" @click="recordFormalResult(batch)">录入查验结果</el-button></div>
|
||||
<div v-if="batch.results.length" class="declaration-result-list"><div v-for="result in batch.results" :key="result.id" class="declaration-result-row"><span>{{ result.resultType === 'INSPECTION' ? '查验' : result.resultType === 'SUPPLEMENT_REQUIRED' ? '补充资料' : result.resultType === 'RELEASED' ? '放行' : result.resultType }}</span><el-tag size="small" :type="result.resultStatus === 'RESOLVED' ? 'success' : 'warning'">{{ result.resultStatus === 'RESOLVED' ? '已处理' : result.resultStatus === 'OPEN' ? '待处理' : '已取消' }}</el-tag><small>{{ result.resolution || '—' }}</small><el-button v-if="canManage && result.resultStatus === 'OPEN'" link type="primary" size="small" :loading="actionLoading" @click="resolveFormalResult(batch, result)">处理</el-button></div></div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="customs.status === 'COMPLETED'" class="departure-state"><strong>国内离仓状态:</strong><el-tag :type="departureState === 'DEPARTED' ? 'success' : 'warning'">{{ departureState === 'DEPARTED' ? '已自动离仓' : departureState === 'UNKNOWN' ? '离仓状态尚未读取' : '报关已完成,待其他离仓条件' }}</el-tag></div>
|
||||
</template>
|
||||
@@ -219,5 +294,5 @@ onMounted(load)
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container-customs{padding:0;border:0}.customs-header{display:flex;align-items:center;justify-content:space-between;gap:var(--admin-space-5)}.customs-header h2,.compact-table-wrap h3{margin:0;font-size:var(--admin-font-section)}.customs-header p{margin:3px 0 0;color:var(--cc-muted);font-size:var(--admin-font-caption)}.customs-actions{display:flex;align-items:center;gap:var(--admin-space-3)}.action-error{margin-top:var(--admin-space-3);padding:var(--admin-space-3) var(--admin-space-4);border:1px solid var(--el-color-danger-light-5);border-radius:var(--admin-radius);background:var(--el-color-danger-light-9);color:var(--el-color-danger);font-size:var(--admin-font-caption)}.action-error :deep(.api-error-meta){margin-top:5px}.customs-metrics{display:flex;gap:var(--admin-space-6);margin-top:var(--admin-space-4);padding:var(--admin-space-3) var(--admin-space-4);background:var(--cc-surface-muted);font-size:var(--admin-font-caption)}.customs-metrics strong{margin-left:var(--admin-space-1);font-size:var(--admin-font-body)}.customs-metrics .danger strong{color:var(--el-color-danger)}.compact-table-wrap{margin-top:var(--admin-space-4)}.compact-table-wrap h3{margin-bottom:var(--admin-space-2)}.compact-table{width:100%}.document-table th:first-child{width:270px}.document-actions,.file-cell{display:flex;align-items:center;gap:var(--admin-space-2)}.file-cell span{display:block;max-width:210px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.readonly-label{color:var(--cc-muted)}.departure-state{display:flex;align-items:center;gap:var(--admin-space-3);margin-top:var(--admin-space-4);padding-top:var(--admin-space-4);border-top:1px solid var(--cc-border);font-size:var(--admin-font-body)}
|
||||
.container-customs{padding:0;border:0}.customs-header{display:flex;align-items:center;justify-content:space-between;gap:var(--admin-space-5)}.customs-header h2,.compact-table-wrap h3{margin:0;font-size:var(--admin-font-section)}.customs-header p{margin:3px 0 0;color:var(--cc-muted);font-size:var(--admin-font-caption)}.customs-actions{display:flex;align-items:center;gap:var(--admin-space-3)}.action-error{margin-top:var(--admin-space-3);padding:var(--admin-space-3) var(--admin-space-4);border:1px solid var(--el-color-danger-light-5);border-radius:var(--admin-radius);background:var(--el-color-danger-light-9);color:var(--el-color-danger);font-size:var(--admin-font-caption)}.action-error :deep(.api-error-meta){margin-top:5px}.customs-metrics{display:flex;gap:var(--admin-space-6);margin-top:var(--admin-space-4);padding:var(--admin-space-3) var(--admin-space-4);background:var(--cc-surface-muted);font-size:var(--admin-font-caption)}.customs-metrics strong{margin-left:var(--admin-space-1);font-size:var(--admin-font-body)}.customs-metrics .danger strong{color:var(--el-color-danger)}.compact-table-wrap{margin-top:var(--admin-space-4)}.compact-table-wrap h3{margin-bottom:var(--admin-space-2)}.compact-table{width:100%}.document-table th:first-child{width:270px}.document-actions,.file-cell{display:flex;align-items:center;gap:var(--admin-space-2)}.file-cell span{display:block;max-width:210px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.readonly-label{color:var(--cc-muted)}.departure-state{display:flex;align-items:center;gap:var(--admin-space-3);margin-top:var(--admin-space-4);padding-top:var(--admin-space-4);border-top:1px solid var(--cc-border);font-size:var(--admin-font-body)}.declaration-batches-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--admin-space-4)}.declaration-batches-heading p{margin:3px 0 0;color:var(--cc-muted);font-size:var(--admin-font-caption)}.declaration-batch-list{display:grid;gap:var(--admin-space-3)}.declaration-batch-card{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:var(--admin-space-2) var(--admin-space-4);padding:var(--admin-space-3) var(--admin-space-4);border:1px solid var(--cc-border);background:var(--cc-surface-muted)}.declaration-batch-main strong,.declaration-batch-main span{display:block}.declaration-batch-main span{margin-top:4px;color:var(--cc-muted);font-size:var(--admin-font-caption)}.declaration-batch-actions,.declaration-result-list{grid-column:1 / -1}.declaration-result-list{display:grid;gap:5px;padding-top:var(--admin-space-2);border-top:1px solid var(--cc-border)}.declaration-result-row{display:flex;align-items:center;gap:var(--admin-space-2);font-size:var(--admin-font-caption)}.declaration-result-row small{flex:1;color:var(--cc-muted)}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user