Files
client-web/src/modules/cross-border/components/ProcurementDetailPanel.vue
T
2026-10-30 11:20:03 +08:00

108 lines
9.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onBeforeUnmount, ref } from 'vue'
import { getProcurementFile, type ProcurementDetail, type ProcurementFee, type ProcurementWorkflow } from '../api/procurement'
import { directionLabel, dateTime, feeStatus, feeTone, feeTotals, money, productHref, statusLabel, text } from '../procurementPresentation'
const props = defineProps<{ detail: ProcurementDetail; workflow: ProcurementWorkflow }>()
const previewUrl = ref(''), previewName = ref(''), previewImage = ref(false), fileLoading = ref(''), fileError = ref('')
let active = true
function clearPreview() { if (previewUrl.value) URL.revokeObjectURL(previewUrl.value); previewUrl.value = '' }
function fileName(fileId: string, index: string | number) { return props.workflow.files.find(file => file.fileId === fileId)?.fileName || '附件 ' + (Number(index) + 1) }
async function openFile(fileId: string) {
if (fileLoading.value) return
fileLoading.value = fileId; fileError.value = ''
try {
const blob = await getProcurementFile(props.workflow.orderNo, fileId)
if (!active) return
clearPreview(); previewUrl.value = URL.createObjectURL(blob)
previewName.value = props.workflow.files.find(file => file.fileId === fileId)?.fileName || '附件'
previewImage.value = /^image\/(png|jpeg|gif|webp|bmp)$/i.test(blob.type)
} catch (cause) { if (active) fileError.value = cause instanceof Error ? cause.message : '附件加载失败' }
finally { fileLoading.value = '' }
}
onBeforeUnmount(() => { active = false; clearPreview() })
const totals = computed(() => feeTotals(props.detail.fees))
</script>
<template>
<el-alert v-if="fileError" :title="fileError" type="error" :closable="false" />
<section class="procurement-section" aria-label="采购数量统计">
<dl class="procurement-grid money-grid">
<div><dt>累计收货</dt><dd>{{ workflow.receivedQuantity }} 件</dd></div>
<div><dt>采购库存</dt><dd>{{ workflow.stockQuantity }} 件</dd></div>
<div><dt>出库处理中</dt><dd>{{ workflow.reservedQuantity }} 件</dd></div>
<div><dt>可出库</dt><dd>{{ workflow.availableQuantity }} 件</dd></div>
</dl>
</section>
<section class="procurement-section" aria-label="分批收货">
<h3>收货记录</h3>
<el-table :data="workflow.receipts" border stripe empty-text="暂无收货记录" row-key="id">
<el-table-column label="批次" width="80" align="center"><template #default="{ $index }">{{ $index + 1 }}</template></el-table-column>
<el-table-column prop="quantity" label="收货数量(件)" width="145" align="right" />
<el-table-column label="收货时间" width="185"><template #default="{ row }">{{ dateTime(row.receivedAt) }}</template></el-table-column>
<el-table-column label="收货照片" min-width="160"><template #default="{ row }"><span v-if="!row.fileIds.length">—</span><el-button v-for="(fileId, index) in row.fileIds" :key="fileId" link type="primary" :loading="fileLoading === fileId" :disabled="!!fileLoading" @click="openFile(fileId)">{{ fileName(fileId, index) }}</el-button></template></el-table-column>
</el-table>
</section>
<section class="procurement-section" aria-label="分批出库">
<h3>出库记录</h3>
<el-table :data="workflow.outbounds.filter(row => row.status !== 'IN_TRANSIT' && row.status !== 'CANCELLED')" border stripe empty-text="暂无出库记录" row-key="id">
<el-table-column prop="outboundNo" label="出库单号" min-width="180" />
<el-table-column prop="quantity" label="出库数量(件)" width="145" align="right" />
<el-table-column prop="trackingNo" label="物流跟踪号" min-width="160" />
<el-table-column label="状态" width="100" align="center"><template #default="{ row }">{{ statusLabel(row.status) }}</template></el-table-column>
<el-table-column label="创建时间" width="185"><template #default="{ row }">{{ dateTime(row.createdAt) }}</template></el-table-column>
<el-table-column label="签收时间" width="185"><template #default="{ row }">{{ dateTime(row.completedAt) }}</template></el-table-column>
<el-table-column label="签收凭证" min-width="220"><template #default="{ row }"><span v-if="!row.fileIds.length">—</span><el-button v-for="(fileId, index) in row.fileIds" :key="fileId" link type="primary" :loading="fileLoading === fileId" :disabled="!!fileLoading" @click="openFile(fileId)">{{ fileName(fileId, index) }}</el-button></template></el-table-column>
</el-table>
</section>
<section class="procurement-section" aria-label="费用明细">
<h3>费用明细 <span class="fee-total">已扣款 {{ money(totals?.net) }}</span></h3>
<el-table :data="detail.fees" border stripe empty-text="暂无费用" row-key="feeNo">
<el-table-column prop="name" label="费用名称" min-width="180" />
<el-table-column label="原币金额" min-width="145" align="right"><template #default="{ row }">{{ money(row.originalAmount, row.originalCurrency) }}</template></el-table-column>
<el-table-column label="人民币金额" min-width="145" align="right"><template #default="{ row }">{{ money(row.amount) }}</template></el-table-column>
<el-table-column label="扣款结果" min-width="160" align="center"><template #default="{ row }"><el-tag :type="feeTone(row as ProcurementFee)" size="small">{{ feeStatus(row as ProcurementFee) }}</el-tag></template></el-table-column>
</el-table>
</section>
<section class="procurement-section" aria-label="商品与收件信息">
<h3>商品与收件信息</h3>
<dl class="procurement-grid">
<div><dt>采购方向</dt><dd>{{ directionLabel(detail.order.procurementDirection) }}</dd></div>
<div><dt>商品名称</dt><dd>{{ detail.order.productName }}</dd></div>
<div><dt>商品规格</dt><dd>{{ text(detail.order.productSpecification) }}</dd></div>
<div><dt>采购品类</dt><dd>{{ text(detail.order.purchaseCategory) }}</dd></div>
<div><dt>采购数量</dt><dd>{{ detail.order.purchaseQuantity }} 件</dd></div>
<div><dt>预算单价</dt><dd>{{ money(detail.order.budgetUnitPrice, detail.order.currency) }}</dd></div>
<div><dt>预算合计</dt><dd>{{ money(detail.budget.totalAmount, detail.budget.currency) }}</dd></div>
<div><dt>采购仓</dt><dd>{{ detail.order.actualWarehouseName || detail.order.plannedWarehouseName || '待审核指定' }}<span v-if="detail.order.actualWarehouseCode">({{ detail.order.actualWarehouseCode }})</span></dd></div>
<div><dt>收件人</dt><dd>{{ detail.order.contactName }}</dd></div>
<div><dt>联系电话</dt><dd>{{ detail.order.contactPhone }}</dd></div>
<div class="wide"><dt>收货地址</dt><dd>{{ detail.order.receivingAddress }}</dd></div>
<div v-if="detail.order.remark" class="full"><dt>采购备注</dt><dd>{{ detail.order.remark }}</dd></div>
<div v-if="detail.order.productUrl" class="full"><dt>商品链接</dt><dd><a v-if="productHref(detail.order.productUrl)" :href="productHref(detail.order.productUrl)" target="_blank" rel="noopener noreferrer">查看商品</a><span v-else>{{ text(detail.order.productUrl) }}</span></dd></div>
</dl>
</section>
<el-dialog :model-value="!!previewUrl" :title="previewName" width="800px" append-to-body @close="clearPreview">
<img v-if="previewImage" :src="previewUrl" :alt="previewName" style="display: block; max-width: 100%; max-height: 70vh; margin: auto" />
<a v-else :href="previewUrl" :download="previewName">下载 {{ previewName }}</a>
</el-dialog>
</template>
<style scoped>
.procurement-section { margin-top: 18px; min-width: 0; }
.fee-total { margin-left: 14px; font-weight: 400; font-size: 12px; color: var(--muted); }
h3 { margin: 0 0 10px; padding-bottom: 7px; border-bottom: 1px solid var(--border); color: var(--navy); font-size: 14px; }
.procurement-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0; border-left: 1px solid var(--border); border-top: 1px solid var(--border); }
.procurement-grid > div { display: grid; grid-template-columns: 110px minmax(0, 1fr); align-items: center; gap: 8px; padding: 9px 12px; border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); min-width: 0; }
.procurement-grid dt { font-size: 12px; color: var(--muted); }.procurement-grid dd { margin: 0; font-size: 13px; line-height: 1.6; color: var(--navy); overflow-wrap: anywhere; }
.procurement-grid .wide { grid-column: span 2; }.money-grid { gap: 16px; border: 0; }.money-grid > div { grid-template-columns: minmax(0, 1fr); padding: 18px; border: 1px solid var(--border); border-radius: 8px; background: #f8fbff; }.money-grid dd { font-size: 16px; font-weight: 600; }
.procurement-grid .full { grid-column: 1 / -1; }
.procurement-section :deep(.el-table .cell) { overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
.procurement-section :deep(.el-table .el-button.is-link) { display: flex; justify-content: flex-start; max-width: 100%; height: auto; margin: 0; padding: 3px 0; white-space: normal; text-align: left; }
.procurement-section :deep(.el-table .el-button.is-link > span) { min-width: 0; overflow-wrap: anywhere; }
a { color: var(--client-action); }
@media (max-width: 767px) {
.procurement-grid { grid-template-columns: 1fr; }.procurement-grid .wide { grid-column: auto; }
}
</style>