97 lines
7.2 KiB
Vue
97 lines
7.2 KiB
Vue
<script setup lang="ts">
|
|
import '@/styles/adminWorkbench.css'
|
|
import '@/styles/adminDensity.css'
|
|
import { computed, onActivated, onBeforeUnmount, ref, watch } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { ArrowLeft } from '@lucide/vue'
|
|
import { useAuthStore } from '@/core/auth/auth'
|
|
import AdminPage from '@/shared/components/AdminPage.vue'
|
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
|
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
|
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
|
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
|
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
|
import { getOverseasPurchase } from '../api/platformOverseasBusiness'
|
|
import { downloadProcurementFile } from '../api/procurementCommands'
|
|
import { getProcurementWorkflow, type ProcurementWorkflow } from '../api/procurementWorkflow'
|
|
import ProcurementWorkflowPanel from '../components/ProcurementWorkflowPanel.vue'
|
|
import BusinessDocumentFees from '../components/BusinessDocumentFees.vue'
|
|
import { statusLabel, money, dateTime, text } from '../procurementPresentation'
|
|
import type { PlatformProcurement } from '../procurementTypes'
|
|
import { directionLabel } from '../procurementDirection'
|
|
import { hasProcurementFeeDetails } from '../procurementOutboundFees'
|
|
const route = useRoute(); const router = useRouter(); const auth = useAuthStore()
|
|
const purchaseId = computed(() => typeof route.params.purchaseId === 'string' ? route.params.purchaseId : '')
|
|
const customerId = computed(() => typeof route.query.customerId === 'string' ? route.query.customerId : '')
|
|
const detail = ref<PlatformProcurement | null>(null), workflow = ref<ProcurementWorkflow | null>(null), loading = ref(false)
|
|
const loadError = ref<ApiErrorDetails | null>(null), operationError = ref<ApiErrorDetails | null>(null), downloading = ref('')
|
|
const order = computed(() => detail.value?.customerView.order)
|
|
const feePanel = ref<InstanceType<typeof BusinessDocumentFees>>()
|
|
async function beforeOutbound(): Promise<boolean | null> {
|
|
if (!feePanel.value) return null
|
|
try { return !hasProcurementFeeDetails(await feePanel.value.getFees()) }
|
|
catch { return null }
|
|
}
|
|
const fields = computed(() => [
|
|
['客户', order.value?.customerName], ['采购方向', directionLabel(order.value?.procurementDirection)], ['收货仓', order.value?.actualWarehouseName || order.value?.plannedWarehouseName],
|
|
['采购品类', order.value?.purchaseCategory], ['商品名称', order.value?.productName], ['规格', order.value?.productSpecification],
|
|
['采购数量', order.value?.purchaseQuantity], ['预算单价', money(order.value?.budgetUnitPrice, order.value?.currency)], ['预计到仓', dateTime(order.value?.estimatedArrivalAt)],
|
|
['收件人', order.value?.contactName], ['联系电话', order.value?.contactPhone], ['收货地址', order.value?.receivingAddress],
|
|
...(order.value?.remark ? [['采购要求', order.value.remark]] : []),
|
|
...(detail.value?.internalRemark || order.value?.remark ? [['采购备注', detail.value?.internalRemark || order.value?.remark]] : []),
|
|
])
|
|
let generation = 0, inFlight: Promise<void> | null = null, inFlightScope = ''
|
|
const scope = computed(() => `${auth.user?.id || ''}:${customerId.value}:${purchaseId.value}`)
|
|
onBeforeUnmount(() => { generation++ })
|
|
function load() {
|
|
const currentScope = scope.value
|
|
if (inFlight && inFlightScope === currentScope) return inFlight
|
|
const current = ++generation; const number = purchaseId.value; const customer = customerId.value
|
|
detail.value = null; workflow.value = null; loadError.value = null; loading.value = false
|
|
if (!number || !customer) { loadError.value = { message: '缺少采购订单号或客户标识,请从采购订单列表重新进入' }; return Promise.resolve() }
|
|
loading.value = true; inFlightScope = currentScope
|
|
const promise = Promise.all([getOverseasPurchase(number, customer), getProcurementWorkflow(number, customer)]).then(([result, process]) => {
|
|
if (current === generation) { detail.value = result; workflow.value = process }
|
|
}).catch(error => { if (current === generation) loadError.value = getApiErrorDetails(error, '采购详情加载失败') }).finally(() => {
|
|
if (current === generation) loading.value = false
|
|
if (inFlight === promise) inFlight = null
|
|
})
|
|
inFlight = promise; return promise
|
|
}
|
|
async function download(fileId: string) {
|
|
if (!order.value || downloading.value) return
|
|
const file = detail.value?.files.find(item => item.fileId === fileId)
|
|
downloading.value = fileId; operationError.value = null; const current = scope.value
|
|
try {
|
|
const blob = await downloadProcurementFile(order.value.orderNo, customerId.value, fileId)
|
|
if (scope.value !== current) return
|
|
const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = file?.fileName || fileId; link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000)
|
|
} catch (error) { if (scope.value === current) operationError.value = getApiErrorDetails(error, '文件下载失败') }
|
|
finally { downloading.value = '' }
|
|
}
|
|
let activatedOnce = false
|
|
watch(scope, () => { void load() }, { immediate: true })
|
|
onActivated(() => { if (activatedOnce) void load(); else activatedOnce = true })
|
|
</script>
|
|
<template>
|
|
<AdminPage class="admin-density admin-workbench"><OperationErrorToast :details="operationError" />
|
|
<DetailPageLayout class="purchase-detail density-detail-layout workbench-panel is-flush is-page-surface" aria-label="采购订单详情">
|
|
<template #header><div class="density-detail-header"><div class="density-detail-heading"><el-button link class="density-back" @click="router.push({ name: 'overseas-business-purchases' })"><ArrowLeft :size="16" />返回列表</el-button><h1 class="density-detail-title">{{ order?.orderNo || '—' }}</h1><StatusBadge v-if="workflow" :status="statusLabel(workflow.status)" /></div><div class="density-actions"><el-button :loading="loading" @click="load">刷新</el-button></div></div></template>
|
|
<LoadErrorState v-if="loadError" title="采购详情暂时无法加载" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
|
|
<div v-else v-loading="loading" class="detail-grid">
|
|
<template v-if="detail && order && workflow">
|
|
<section><h3>采购信息</h3><dl class="density-fields"><div v-for="field in fields" :key="String(field[0])"><dt>{{ field[0] }}</dt><dd>{{ text(field[1]) }}</dd></div></dl></section>
|
|
<ProcurementWorkflowPanel :detail="detail" :workflow="workflow" :customer-id="customerId" :downloading="downloading" :before-outbound="beforeOutbound" @changed="load" @download="download" />
|
|
<BusinessDocumentFees ref="feePanel" business-type="OVERSEAS_PROCUREMENT" :business-no="order.orderNo" :customer-id="customerId" :business-id="order.id" />
|
|
</template>
|
|
</div>
|
|
</DetailPageLayout>
|
|
</AdminPage>
|
|
</template>
|
|
<style scoped>
|
|
.detail-grid { display: grid; min-width: 0; gap: var(--admin-space-5); }
|
|
.detail-grid section { min-width: 0; }
|
|
.detail-grid h3 { margin: 0 0 var(--admin-space-3); font-size: var(--admin-font-section); }
|
|
</style>
|