Files
klp-oa/klp-ui/src/views/erp/purchaseDelivery/index.vue
Joshi ec40ab90ba fix(erp): 修复采购计划到货统计准确性问题
- 直接从到货明细重新计算到货/在途统计数据,避免数据库缓存导致的数据不准确
- 更新在途状态判断逻辑,在途等于所有已上传但尚未到货的卷,不论WMS中是否有记录
- 移除未到货状态,未到货的全部计入在途状态
- 立即加载详情而不等待WMS刷新,提升用户体验
- 在WMS钢卷导出功能中添加钢卷ID字段支持
2026-07-04 14:19:25 +08:00

337 lines
15 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.

<template>
<div class="pd-wb">
<div class="pd-main">
<!-- 审核通过的采购合同 -->
<aside class="pd-col pd-plans">
<div class="pd-col-tool">
<el-input
v-model="queryParams.keyword"
size="small"
clearable
placeholder="搜索合同号 / 出卖方"
prefix-icon="el-icon-search"
@keyup.enter.native="handleQuery"
@clear="handleQuery"
/>
</div>
<ul class="pd-list" v-loading="loading">
<li
v-for="p in planList"
:key="p.planId"
class="pd-li"
:class="{ active: current.planId === p.planId }"
@click="selectPlan(p)"
>
<div class="pd-li-r1">
<span class="pd-no">{{ p.planNo }}</span>
<span class="pd-pct" :class="{ done: Number(p.progress) >= 100 }">{{ (Number(p.progress) || 0).toFixed(0) }}%</span>
</div>
<div class="pd-li-r2">{{ p.supplier || '—' }}</div>
<el-progress :percentage="Number(p.progress) || 0" :stroke-width="4" :show-text="false" :color="progressColor" />
<div class="pd-li-r3">
<span>已到 {{ p.arrivedCount || 0 }} · 在途 {{ p.inTransitCount || 0 }}</span>
<span v-if="p.planStatus === '1'" class="pd-arch">已到齐</span>
</div>
</li>
<li v-if="!loading && !planList.length" class="pd-empty">暂无审核通过的合同</li>
</ul>
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
:pager-count="5"
layout="prev, pager, next"
@pagination="getList"
/>
</aside>
<!-- 到货跟踪 -->
<section class="pd-col pd-detail">
<div v-if="!current.planId" class="pd-placeholder">
<i class="el-icon-truck"></i>
<p>从左侧选择一条采购合同</p>
</div>
<div v-else>
<div class="pd-d-head">
<div>
<span class="pd-d-title">{{ current.planNo }}</span>
<span class="pd-badge" :class="current.planStatus === '1' ? 'p1' : 'p0'">{{ current.planStatus === '1' ? '已到齐' : '到货中' }}</span>
</div>
<div class="pd-head-act">
<el-button size="small" icon="el-icon-refresh" :loading="refreshing" @click="doRefreshArrival">刷新到货</el-button>
<el-upload
:headers="upload.headers"
:action="uploadUrl"
:show-file-list="false"
accept=".xlsx,.xls"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
>
<el-button type="primary" size="small" icon="el-icon-upload2">上传到货表格</el-button>
</el-upload>
</div>
</div>
<div class="pd-meta">
<div class="pd-meta-i"><label>出卖方</label><span>{{ current.supplier || '—' }}</span></div>
<div class="pd-meta-i"><label>关联销售合同</label><span>{{ (current.contractCodes || []).join('、') || '—' }}</span></div>
<div class="pd-meta-i"><label>要求总重量</label><span>{{ fmt(current.planWeight) }} T</span></div>
<div class="pd-meta-i"><label>已到 / 在途</label><span>{{ current.arrivedCount || 0 }} {{ fmt(current.arrivedWeight) }} T / {{ current.inTransitCount || 0 }} </span></div>
<div class="pd-meta-i wide">
<label>到货进度按重量</label>
<el-progress
:percentage="Number(current.progress) || 0"
:stroke-width="14" :text-inside="true" :color="progressColor"
:format="p => p.toFixed(1) + '%'"
/>
</div>
</div>
<el-tabs v-model="activeTab" class="pd-tabs">
<el-tab-pane label="采购与到货" name="all">
<div class="pd-sec-title">采购要求</div>
<el-table :data="current.items" border size="mini" max-height="300">
<el-table-column label="#" type="index" width="44" align="center" />
<el-table-column label="规格" prop="spec" min-width="200" show-overflow-tooltip />
<el-table-column label="总重量(T)" prop="weight" min-width="110" align="right" />
<el-table-column label="厂商" prop="manufacturer" min-width="180" show-overflow-tooltip />
<template slot="empty"><span>无采购要求</span></template>
</el-table>
<div class="pd-sec-title" style="margin-top:16px">到货明细{{ deliveryList.length }}</div>
<el-table
:data="deliveryList" border stripe size="mini" max-height="300"
v-loading="deliveryLoading" :row-class-name="rowClass"
>
<el-table-column label="日期" prop="arrivalDate" width="100" align="center" />
<el-table-column label="牌号" prop="grade" width="78" align="center" />
<el-table-column label="规格" prop="spec" width="105" align="center" />
<el-table-column label="卷号" prop="coilNo" width="120" align="center" />
<el-table-column label="单卷(T)" prop="coilWeight" width="78" align="right" />
<el-table-column label="车号" prop="truckNo" width="95" align="center" />
<el-table-column label="件数" prop="pieceCount" width="56" align="center" />
<el-table-column label="销售" prop="salesCode" width="80" align="center" />
<el-table-column label="状态" width="76" align="center">
<template slot-scope="s">
<span class="pd-mtag" :class="statusClass(s.row)">{{ statusText(s.row) }}</span>
</template>
</el-table-column>
<template slot="empty"><span>暂无到货记录点右上角上传到货表格</span></template>
</el-table>
</el-tab-pane>
<el-tab-pane :label="'关联合同(' + (current.contractInfos ? current.contractInfos.length : 0) + ''" name="contracts">
<el-table :data="current.contractInfos" border size="mini" max-height="420" style="width:100%">
<el-table-column label="订单编号" prop="orderCode" width="150" show-overflow-tooltip fixed />
<el-table-column label="合同号" prop="contractCode" width="140" show-overflow-tooltip />
<el-table-column label="合同名称" prop="contractName" min-width="150" show-overflow-tooltip />
<el-table-column label="客户(需方)" prop="customer" width="130" show-overflow-tooltip />
<el-table-column label="供方" prop="supplier" width="120" show-overflow-tooltip />
<el-table-column label="金额" prop="orderAmount" width="110" align="right" />
<el-table-column label="销售员" prop="salesman" width="80" />
<el-table-column label="签订时间" prop="signTime" width="105" align="center" />
<el-table-column label="签订地点" prop="signLocation" width="110" show-overflow-tooltip />
<el-table-column label="交货日期" prop="deliveryDate" width="105" align="center" />
<el-table-column label="合同状态" width="90" align="center">
<template slot-scope="s">{{ contractStatusText(s.row.status) }}</template>
</el-table-column>
<el-table-column label="应付定金" prop="depositPayable" width="100" align="right" />
<el-table-column label="已付定金" prop="depositPaid" width="100" align="right" />
<el-table-column label="未结款" prop="unpaidAmount" width="100" align="right" />
<el-table-column label="产品内容" prop="productContent" min-width="180" show-overflow-tooltip />
<el-table-column label="供方电话" prop="supplierPhone" width="120" show-overflow-tooltip />
<el-table-column label="需方电话" prop="customerPhone" width="120" show-overflow-tooltip />
<el-table-column label="备注" prop="remark" min-width="150" show-overflow-tooltip />
<template slot="empty"><span>无关联合同</span></template>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</section>
</div>
</div>
</template>
<script>
import {
getPurchasePlan,
listDeliveryPlans,
listDelivery,
refreshArrival
} from '@/api/erp/purchasePlan'
import { getToken } from '@/utils/auth'
export default {
name: 'ErpPurchaseDelivery',
data() {
return {
loading: true,
total: 0,
planList: [],
queryParams: { pageNum: 1, pageSize: 20, keyword: undefined },
current: {},
activeTab: 'all',
deliveryList: [],
deliveryLoading: false,
refreshing: false,
upload: { headers: { Authorization: 'Bearer ' + getToken() } },
progressColor: '#5b8db8'
}
},
computed: {
uploadUrl() {
return process.env.VUE_APP_BASE_API + '/erp/purchasePlan/' + (this.current.planId || '') + '/importDelivery'
}
},
created() {
this.getList()
},
methods: {
getList(keepCurrent) {
this.loading = true
listDeliveryPlans(this.queryParams).then(res => {
this.planList = res.rows || []
this.total = res.total || 0
this.loading = false
if (!keepCurrent) {
if (this.planList.length) this.selectPlan(this.planList[0])
else this.current = {}
}
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
selectPlan(p) {
this.activeTab = 'all'
this.current = { ...p }
this.deliveryList = []
// 立即加载详情,不等待 WMS 刷新
this.refreshDetail()
// 后台静默按钢卷表复核一次到货状态
const planId = p.planId
refreshArrival(planId).catch(() => {}).finally(() => {
if (this.current.planId === planId) this.refreshDetail()
})
},
refreshDetail() {
const planId = this.current.planId
if (!planId) return
getPurchasePlan(planId).then(res => { this.current = { ...this.current, ...(res.data || {}) } })
this.deliveryLoading = true
listDelivery(planId).then(res => { this.deliveryList = res.data || [] }).finally(() => { this.deliveryLoading = false })
},
doRefreshArrival() {
if (!this.current.planId) return
this.refreshing = true
refreshArrival(this.current.planId).then(() => {
this.$modal.msgSuccess('已按钢卷表刷新到货状态')
this.refreshDetail()
this.getList(true)
}).finally(() => { this.refreshing = false })
},
rowClass({ row }) {
if (row.arrived === 1) return 'pd-row-arrived'
if (row.inTransit === 1) return 'pd-row-transit'
return ''
},
statusText(row) {
if (row.arrived === 1) return '已到货'
return '在途'
},
statusClass(row) {
if (row.arrived === 1) return 'yes'
return 'transit'
},
handleUploadSuccess(res) {
if (res.code === 200) {
const data = res.data || {}
if (data.kgConverted) {
this.$alert(res.msg, '导入完成(含单位纠正)', { dangerouslyUseHTMLString: true, type: 'warning' })
} else {
this.$modal.msgSuccess(res.msg || '导入成功')
}
this.activeTab = 'all'
this.refreshDetail()
this.getList(true)
} else {
this.$alert(res.msg || '导入失败', '到货文件校验未通过', { dangerouslyUseHTMLString: true, type: 'error' })
}
},
handleUploadError() {
this.$modal.msgError('上传失败,请检查文件后重试')
},
fmt(v) {
return Number(v || 0).toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 3 })
},
contractStatusText(v) {
return { 0: '草稿', 1: '生效', 2: '作废', 3: '已完成' }[v] || '—'
}
}
}
</script>
<style lang="scss" scoped>
$accent: #5b8db8;
$line: #e4e7ed;
$ink: #303133;
$sub: #909399;
.pd-wb { height: calc(100vh - 84px); display: flex; flex-direction: column; background: #f5f6f8; padding: 12px; box-sizing: border-box; }
.pd-main { flex: 1; display: flex; gap: 12px; min-height: 0; }
.pd-col { background: #fff; border: 1px solid $line; display: flex; flex-direction: column; min-height: 0; }
.pd-plans { width: 300px; flex-shrink: 0; }
.pd-detail { flex: 1; min-width: 0; overflow-y: auto; }
.pd-col-tool { padding: 10px; border-bottom: 1px solid $line; ::v-deep .el-input { width: 100%; } }
.pd-list { flex: 1; overflow-y: auto; margin: 0; padding: 0; list-style: none; }
.pd-li {
padding: 10px 12px; border-bottom: 1px solid #f0f2f5; cursor: pointer; border-left: 3px solid transparent;
&:hover { background: #f7f9fb; }
&.active { background: #eef3f8; border-left-color: $accent; }
}
.pd-li-r1 { display: flex; justify-content: space-between; align-items: center; }
.pd-no { font-size: 13px; font-weight: 600; color: $ink; }
.pd-pct { font-size: 12px; font-weight: 600; color: $sub; &.done { color: #3a8a4d; } }
.pd-li-r2 { font-size: 12px; color: $sub; margin: 4px 0 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pd-li-r3 { display: flex; justify-content: space-between; font-size: 11px; color: $sub; margin-top: 4px; }
.pd-arch { font-style: normal; color: #3a8a4d; }
.pd-empty { text-align: center; color: $sub; padding: 36px 12px; font-size: 13px; }
.pd-placeholder {
height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; color: $sub;
i { font-size: 46px; margin-bottom: 12px; color: #d6dce1; }
p { font-size: 13px; }
}
.pd-d-head { display: flex; justify-content: space-between; align-items: center; padding: 14px 18px; border-bottom: 1px solid $line; }
.pd-head-act { display: flex; gap: 10px; align-items: center; ::v-deep .el-button { margin: 0; } }
.pd-d-title { font-size: 15px; font-weight: 600; color: $ink; margin-right: 8px; }
.pd-badge {
font-size: 11px; line-height: 16px; padding: 0 6px; border-radius: 2px; border: 1px solid #dcdfe6; color: $sub; background: #fafafa;
&.p1 { color: #3a8a4d; border-color: #b7d9bf; background: #f0f9f1; }
}
.pd-meta { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px 24px; padding: 16px 18px; }
.pd-meta-i {
display: flex; flex-direction: column; font-size: 13px;
label { color: $sub; font-size: 12px; margin-bottom: 3px; }
span { color: $ink; }
&.wide { grid-column: span 2; }
}
.pd-tabs { padding: 0 18px 18px; }
.pd-sec-title {
font-size: 13px; font-weight: 600; color: $ink;
border-left: 3px solid $accent; padding-left: 8px; margin-bottom: 10px;
}
.pd-mtag {
font-size: 11px; line-height: 16px; padding: 0 6px; border-radius: 2px; border: 1px solid #dcdfe6; color: $sub;
&.yes { color: #3a8a4d; border-color: #b7d9bf; background: #f0f9f1; }
&.transit { color: #d6a256; border-color: #ecd4a6; background: #fdf6ec; }
&.no { color: $sub; }
}
::v-deep .pd-row-arrived { background: #f3f9f4; }
::v-deep .pd-row-transit { background: #fdf9f0; }
</style>