108 lines
9.2 KiB
Vue
108 lines
9.2 KiB
Vue
<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>
|