Files
klp-oa/klp-ui/src/views/erp/purchaseDelivery/index.vue
砂糖 f343552729 refactor: 优化多个页面UI与交互细节
1. 修改钢卷追踪页面标题,调整crm钢卷表格列宽与按钮文本
2. 优化采购审核、到货、计划页面表格列配置,添加材质列
3. 为多个页面添加详情加载状态,统一按钮间距样式
4. 新增采购到货页面模板下载功能,修复产品内容组件单元格内边距
2026-07-06 10:36:56 +08:00

351 lines
16 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 v-loading="detailLoading">
<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-button size="small" icon="el-icon-download" @click="downloadTemplate">下载模板</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="160" show-overflow-tooltip />
<el-table-column label="材质" prop="material" min-width="100" show-overflow-tooltip />
<el-table-column label="总重量(T)" prop="weight" min-width="100" align="right" />
<el-table-column label="厂商" prop="manufacturer" min-width="150" 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'
import * as XLSX from 'xlsx'
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,
detailLoading: 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
this.detailLoading = true
getPurchasePlan(planId).then(res => { this.current = { ...this.current, ...(res.data || {}) } }).finally(() => { this.detailLoading = false })
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] || '—'
},
downloadTemplate() {
const headers = ['日期', '牌号', '规格', '卷号', '单卷重量', '车号', '数量', '件数', '销售', '钢厂到站']
const sampleRow = ['2026-01-01', 'Q235B', '1.0*1250*C', 'COIL0001', '20.50', '辽A12345', '60.8', '3', 'XS001', '沈阳站']
const wb = XLSX.utils.book_new()
const ws = XLSX.utils.aoa_to_sheet([headers, sampleRow])
ws['!cols'] = headers.map(h => ({ wch: h.length > 4 ? 14 : 10 }))
XLSX.utils.book_append_sheet(wb, ws, '到货模板')
XLSX.writeFile(wb, '到货导入模板.xlsx')
}
}
}
</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>