Files
admin-web/src/modules/overseas-business/components/BusinessDocumentFees.vue
T
2026-10-26 13:43:08 +08:00

64 lines
4.8 KiB
Vue

<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import BusinessOrderFees from './BusinessOrderFees.vue'
import { listBusinessFeeOptions, listBusinessOverseasFees, type BusinessFeeOption, type OverseasFeeDirection } from '../api/overseasFees'
import { feePermissions } from '../overseasFeePresentation'
interface DocumentIdentity { businessType: string; businessNo: string; customerId?: string; businessId?: string }
class DocumentIdentityError extends Error {}
const props = withDefaults(defineProps<DocumentIdentity & { relatedDocuments?: DocumentIdentity[]; canCreate?: boolean; readonly?: boolean; direction?: OverseasFeeDirection; hideWhenEmpty?: boolean; heading?: string }>(), { canCreate: true, readonly: false, direction: 'RECEIVABLE', hideWhenEmpty: false })
const emit = defineEmits<{ changed: [] }>()
const access = useAccessPreviewStore()
const canRead = computed(() => access.can(feePermissions.list) || access.can('platform:finance:customer-arap:list'))
const business = ref<BusinessFeeOption | null>(null), related = ref<BusinessFeeOption[]>([])
const loading = ref(false), error = ref<ApiErrorDetails | null>(null)
const panel = ref<InstanceType<typeof BusinessOrderFees>>()
let generation = 0
async function load() {
const request = ++generation
business.value = null; related.value = []; error.value = null; loading.value = false
if (!canRead.value) return
loading.value = true
try {
const identities: DocumentIdentity[] = [props, ...(props.relatedDocuments || [])]
const resolved = await Promise.all(identities.map(async (identity, index) => {
if ((!identity.customerId && !identity.businessId) || !identity.businessNo) throw new DocumentIdentityError('单据信息不完整,无法读取费用')
const options = await listBusinessFeeOptions({ businessType: identity.businessType, customerId: identity.customerId, keyword: identity.businessNo })
// 候选搜索是模糊查询,只有当前客户和完整单号一致时才能承载收费。
const matches = options.filter(option => (!identity.customerId || option.customerId === identity.customerId) && option.businessType === identity.businessType
&& option.businessNo === identity.businessNo && (!identity.businessId || option.businessId === identity.businessId))
// 尾程来源号可能只是外部参考号;没有内部业务实体时不能据此造出费用归属。
if (index > 0 && matches.length === 0) return null
if (matches.length !== 1) throw new DocumentIdentityError(`未找到唯一关联单据:${identity.businessNo}`)
return matches[0]!
}))
if (request === generation) { business.value = resolved[0]!; related.value = resolved.slice(1).filter((value): value is BusinessFeeOption => value !== null) }
} catch (failure) { if (request === generation) error.value = failure instanceof DocumentIdentityError ? { message: failure.message } : getApiErrorDetails(failure, '单据费用加载失败') }
finally { if (request === generation) loading.value = false }
}
watch(() => [props.businessType, props.businessNo, props.customerId, props.businessId, props.relatedDocuments, canRead.value], load, { immediate: true, deep: true })
async function refresh() { if (business.value) await panel.value?.refresh(); else await load() }
/** 新增前读取完整单据范围,重复项目提示不受当前表格分页影响。 */
async function getFees() {
if (!business.value) await load()
if (!business.value || error.value) throw new Error(error.value?.message || '单据费用尚未加载完成')
return (await listBusinessOverseasFees([business.value, ...related.value], { page: 1, size: Number.MAX_SAFE_INTEGER }, props.direction)).records
}
defineExpose({ refresh, getFees })
</script>
<template>
<section aria-label="单据费用">
<el-alert v-if="!canRead" title="暂无费用查看权限" type="info" :closable="false" />
<LoadErrorState v-else-if="error" title="单据费用加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
<el-skeleton v-else-if="loading" :rows="3" animated />
<BusinessOrderFees v-else-if="business" ref="panel" :business="business" :related-businesses="related" :direction="direction" :can-create="canCreate" :readonly="readonly" :hide-when-empty="hideWhenEmpty" :heading="heading" @changed="emit('changed')">
<template v-if="$slots['toolbar-actions']" #toolbar-actions><slot name="toolbar-actions" /></template>
</BusinessOrderFees>
</section>
</template>