Files
klp-oa/klp-ui/src/views/erp/purchasePlan/index.vue
wangyu ce3998db74 feat(erp): 采购计划/采购审核/到货跟踪 + 供应商管理
- 采购计划:选合同自动带出明细、合同/供应商表格选择器、批量填充(可生成N行)、卷号/数量列、送审/重新送审流程
- 采购审核:通过/驳回 + 申请意见,每次审核留痕(erp_purchase_plan_audit_log),计划详情展示审核历史/驳回理由
- 到货跟踪:上传到货Excel按牌号+规格回填明细到货量与状态,列校验/kg→t纠正,满额自动归档
- 供应商管理页(复用既有 erp_supplier 后端)
- 综合搜索(计划号/供货商/合同号)、左右分栏工作台、全局表单按钮对齐修复
- 清理无用旧 erp 页面(看板/需求/订单/收货/退货/汇总)
- DDL 与菜单脚本:docs/purchase-plan-ddl.sql(按 path 解析父目录、可重复执行)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 16:53:21 +08:00

895 lines
41 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="pp-wb">
<div class="pp-main">
<!-- 计划列表 -->
<aside class="pp-col pp-plans">
<div class="pp-col-tool">
<el-input
v-model="queryParams.keyword"
size="small"
clearable
placeholder="搜索计划号 / 供货商 / 合同号"
prefix-icon="el-icon-search"
@keyup.enter.native="handleQuery"
@clear="handleQuery"
/>
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd" v-hasPermi="['erp:purchasePlan:add']">新增</el-button>
</div>
<div class="pp-col-filter">
<el-radio-group v-model="queryParams.auditStatus" size="mini" @change="handleQuery">
<el-radio-button label="">全部</el-radio-button>
<el-radio-button label="3">待送审</el-radio-button>
<el-radio-button label="0">待审</el-radio-button>
<el-radio-button label="1">通过</el-radio-button>
<el-radio-button label="2">驳回</el-radio-button>
</el-radio-group>
</div>
<ul class="pp-list" v-loading="loading">
<li
v-for="p in planList"
:key="p.planId"
class="pp-li"
:class="{ active: mode !== 'edit' && current.planId === p.planId }"
@click="selectPlan(p)"
>
<div class="pp-li-r1">
<span class="pp-no">{{ p.planNo }}</span>
<span class="pp-badge" :class="'a' + p.auditStatus">{{ auditText(p.auditStatus) }}</span>
</div>
<div class="pp-li-r2">
<span>{{ p.supplier || '—' }}</span>
<span class="pp-w">{{ p.planWeight }} T</span>
</div>
<el-progress :percentage="Number(p.progress) || 0" :stroke-width="4" :show-text="false" :color="progressColor" />
<div class="pp-li-r3">
<span>{{ p.purchaseDate || '未排期' }}</span>
<span>{{ (Number(p.progress) || 0).toFixed(0) }}%<i v-if="p.planStatus === '1'" class="pp-arch">已归档</i></span>
</div>
</li>
<li v-if="!loading && !planList.length" class="pp-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="pp-col pp-detail">
<div v-if="mode === 'empty'" class="pp-placeholder">
<i class="el-icon-tickets"></i>
<p>从左侧选择一条采购计划或点击新增按销售合同创建</p>
</div>
<!-- 编辑 / 新增 -->
<div v-else-if="mode === 'edit'" class="pp-edit">
<div class="pp-d-head">
<span class="pp-d-title">{{ form.planId ? '编辑采购计划' : '新增采购计划' }}</span>
<div>
<el-button size="small" @click="cancelEdit">取消</el-button>
<el-button size="small" type="primary" :loading="buttonLoading" @click="submitForm">保存</el-button>
</div>
</div>
<el-form :model="form" :rules="rules" ref="form" label-width="80px" class="pp-form" size="small">
<div class="pp-section">基本信息</div>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="计划号" prop="planNo">
<el-input v-model="form.planNo" placeholder="留空自动生成" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="采购日期" prop="purchaseDate">
<el-date-picker v-model="form.purchaseDate" type="date" value-format="yyyy-MM-dd" style="width:100%" placeholder="选择日期" />
</el-form-item>
</el-col>
</el-row>
<div class="pp-section">
销售合同
<el-button type="primary" plain size="mini" icon="el-icon-plus" class="pp-section-act" @click="openContractPicker">选择合同</el-button>
<span class="pp-section-hint" v-if="form.orderIds.length">已选 {{ form.orderIds.length }} 个合同</span>
</div>
<div class="pp-picked">
<template v-if="selectedContracts.length">
<span class="pp-chip" v-for="c in selectedContracts" :key="c.orderId">
<b>{{ c.orderCode }}</b><span class="pp-chip-sub">{{ c.contractName }} · {{ c.customer }}</span>
<i class="el-icon-close" @click="removeContract(c.orderId)" />
</span>
</template>
<template v-else-if="form.contractCodes && form.contractCodes.length">
<span class="pp-chip readonly" v-for="code in form.contractCodes" :key="code">{{ code }}</span>
<span class="pp-chip-tip">选择合同可修改</span>
</template>
<span v-else class="pp-picked-empty">未选择销售合同</span>
</div>
<div class="pp-section">
采购明细
<el-button type="text" size="mini" icon="el-icon-plus" class="pp-section-act" @click="addItem">加行</el-button>
<el-button type="text" size="mini" icon="el-icon-magic-stick" class="pp-section-act" @click="openBatchFill">批量填充</el-button>
<span class="pp-section-hint" v-if="form.items.length"> {{ form.items.length }} · {{ itemsWeight }} T</span>
</div>
<el-table :data="form.items" border size="mini" max-height="300">
<el-table-column label="#" type="index" width="40" align="center" />
<el-table-column label="产品" min-width="70">
<template slot-scope="s"><el-input v-model="s.row.productType" size="mini" placeholder="热轧卷板" /></template>
</el-table-column>
<el-table-column label="材质" min-width="80">
<template slot-scope="s"><el-input v-model="s.row.material" size="mini" /></template>
</el-table-column>
<el-table-column label="牌号" min-width="75">
<template slot-scope="s"><el-input v-model="s.row.grade" size="mini" /></template>
</el-table-column>
<el-table-column label="卷号" min-width="95">
<template slot-scope="s"><el-input v-model="s.row.coilNo" size="mini" /></template>
</el-table-column>
<el-table-column label="宽度" min-width="75">
<template slot-scope="s"><el-input v-model="s.row.width" size="mini" /></template>
</el-table-column>
<el-table-column label="厚度" min-width="75">
<template slot-scope="s"><el-input v-model="s.row.thickness" size="mini" /></template>
</el-table-column>
<el-table-column label="宽公差" min-width="75">
<template slot-scope="s"><el-input v-model="s.row.widthTolerance" size="mini" /></template>
</el-table-column>
<el-table-column label="厚公差" min-width="75">
<template slot-scope="s"><el-input v-model="s.row.thicknessTolerance" size="mini" /></template>
</el-table-column>
<el-table-column label="重量(T)" min-width="80">
<template slot-scope="s"><el-input v-model="s.row.weight" size="mini" /></template>
</el-table-column>
<el-table-column label="数量" min-width="65">
<template slot-scope="s"><el-input v-model="s.row.quantity" size="mini" /></template>
</el-table-column>
<el-table-column label="供货商" min-width="90">
<template slot-scope="s"><el-input v-model="s.row.supplier" size="mini" /></template>
</el-table-column>
<el-table-column label="操作" width="64" align="center" fixed="right">
<template slot-scope="s">
<i class="el-icon-document-copy pp-copy" title="复制此行" @click="copyItem(s.$index)" />
<i class="el-icon-delete pp-del" title="删除" @click="removeItem(s.$index)" />
</template>
</el-table-column>
<template slot="empty"><span>加行添加或在上方选择销售合同载入明细</span></template>
</el-table>
<el-form-item label="备注" prop="remark" style="margin-top:14px">
<el-input type="textarea" v-model="form.remark" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-form>
</div>
<!-- 查看 -->
<div v-else class="pp-view">
<div class="pp-d-head">
<div>
<span class="pp-d-title">{{ current.planNo }}</span>
<span class="pp-badge" :class="'a' + current.auditStatus">{{ auditText(current.auditStatus) }}</span>
<span class="pp-badge plan" :class="current.planStatus === '1' ? 'p1' : 'p0'">{{ current.planStatus === '1' ? '已归档' : '进行中' }}</span>
</div>
<div>
<el-button
v-if="current.auditStatus === '3' || current.auditStatus === '2'"
size="small" type="primary" icon="el-icon-s-promotion"
:loading="submitLoading" @click="submitForAudit"
v-hasPermi="['erp:purchasePlan:edit']"
>{{ current.auditStatus === '2' ? '重新送审' : '送审' }}</el-button>
<el-button size="small" icon="el-icon-edit" @click="handleEdit" v-hasPermi="['erp:purchasePlan:edit']">编辑</el-button>
<el-button size="small" type="danger" plain icon="el-icon-delete" @click="handleDelete(current)" v-hasPermi="['erp:purchasePlan:remove']">删除</el-button>
</div>
</div>
<div class="pp-meta">
<div class="pp-meta-i"><label>供货商</label><span>{{ current.supplier || '—' }}</span></div>
<div class="pp-meta-i"><label>采购日期</label><span>{{ current.purchaseDate || '—' }}</span></div>
<div class="pp-meta-i"><label>关联合同</label><span>{{ (current.contractCodes || []).join('、') || '—' }}</span></div>
<div class="pp-meta-i"><label>计划重量</label><span>{{ current.planWeight }} T</span></div>
<div class="pp-meta-i"><label>已到货</label><span>{{ current.arrivedWeight }} T</span></div>
<div class="pp-meta-i wide">
<label>到货进度</label>
<el-progress
:percentage="Number(current.progress) || 0"
:stroke-width="10" :text-inside="true" :color="progressColor"
:format="p => p.toFixed(1) + '%'"
/>
</div>
</div>
<div class="pp-audit" v-if="(current.auditLogs && current.auditLogs.length) || current.auditOpinion">
<div class="pp-audit-head">审核记录</div>
<ul class="pp-audit-list" v-if="current.auditLogs && current.auditLogs.length">
<li v-for="(log, i) in current.auditLogs" :key="i">
<span class="pp-badge" :class="'a' + log.auditStatus">{{ auditText(log.auditStatus) }}</span>
<span class="pp-audit-by">{{ log.auditor || '—' }}</span>
<span class="pp-audit-time">{{ log.auditTime }}</span>
<span class="pp-audit-op">{{ log.auditOpinion || '(无意见)' }}</span>
</li>
</ul>
<div v-else class="pp-audit-op single">
<span class="pp-badge" :class="'a' + current.auditStatus">{{ auditText(current.auditStatus) }}</span>
{{ current.auditOpinion || '—' }}
</div>
</div>
<el-tabs v-model="activeTab" class="pp-tabs">
<el-tab-pane label="采购明细" name="items">
<el-table :data="current.items" border size="mini" max-height="340">
<el-table-column label="#" type="index" width="40" align="center" />
<el-table-column label="产品" prop="productType" min-width="70" show-overflow-tooltip />
<el-table-column label="材质" prop="material" min-width="80" show-overflow-tooltip />
<el-table-column label="牌号" prop="grade" min-width="75" />
<el-table-column label="卷号" prop="coilNo" min-width="95" show-overflow-tooltip />
<el-table-column label="宽度" prop="width" min-width="70" />
<el-table-column label="厚度" prop="thickness" min-width="70" />
<el-table-column label="重量(T)" prop="weight" min-width="78" align="right" />
<el-table-column label="数量" prop="quantity" min-width="60" align="right" />
<el-table-column label="已到货(T)" prop="arrivedWeight" min-width="82" align="right" />
<el-table-column label="到货状态" min-width="84" align="center">
<template slot-scope="s">
<span class="pp-istat" :class="'s' + (s.row.itemStatus || '0')">{{ itemStatusText(s.row.itemStatus) }}</span>
</template>
</el-table-column>
<el-table-column label="供货商" prop="supplier" min-width="90" show-overflow-tooltip />
<template slot="empty"><span>无明细</span></template>
</el-table>
</el-tab-pane>
<el-tab-pane label="到货进度" name="delivery">
<div class="pp-deliv-bar">
<span class="pp-deliv-tip" v-if="current.auditStatus !== '1'">审核通过后才能上传到货 Excel</span>
<el-upload
v-else
: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">上传到货 Excel</el-button>
</el-upload>
<span class="pp-deliv-hint">上传后按牌号+规格回填明细到货量与状态</span>
</div>
<el-table :data="deliveryList" border stripe size="mini" max-height="340" v-loading="deliveryLoading">
<el-table-column label="日期" prop="arrivalDate" width="100" align="center" />
<el-table-column label="牌号" prop="grade" width="80" align="center" />
<el-table-column label="规格" prop="spec" width="110" align="center" />
<el-table-column label="卷号" prop="coilNo" width="110" align="center" />
<el-table-column label="单卷(T)" prop="coilWeight" width="80" align="right" />
<el-table-column label="车号" prop="truckNo" width="100" align="center" />
<el-table-column label="整车(T)" prop="truckWeight" width="80" align="right" />
<el-table-column label="件数" prop="pieceCount" width="56" align="center" />
<el-table-column label="销售" prop="salesCode" width="85" align="center" />
<el-table-column label="到站" prop="arrivalStation" width="90" align="center" />
<el-table-column label="" width="36" align="center">
<template slot-scope="s"><i class="el-icon-delete pp-del" @click="removeDelivery(s.row)" /></template>
</el-table-column>
<template slot="empty"><span>暂无到货记录</span></template>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</section>
</div>
<!-- 选择销售合同完整信息表格 + 多选 -->
<el-dialog title="选择销售合同" :visible.sync="pickerOpen" width="900px" append-to-body>
<div class="pp-picker-tool">
<el-input
v-model="pickerQuery.keyword"
size="small"
clearable
placeholder="搜索合同号 / 合同名称 / 客户"
prefix-icon="el-icon-search"
style="width:280px"
@keyup.enter.native="reloadPicker"
@clear="reloadPicker"
/>
<el-button type="primary" size="small" icon="el-icon-search" @click="reloadPicker">查询</el-button>
</div>
<el-table
ref="pickerTable"
:data="pickerList"
v-loading="pickerLoading"
border size="small"
max-height="420"
@selection-change="onPickerSelection"
row-key="orderId"
>
<el-table-column type="selection" width="46" reserve-selection />
<el-table-column label="订单编号" prop="orderCode" width="150" show-overflow-tooltip />
<el-table-column label="合同号" prop="contractCode" width="130" show-overflow-tooltip />
<el-table-column label="合同名称" prop="contractName" min-width="140" show-overflow-tooltip />
<el-table-column label="客户(需方)" prop="customer" min-width="160" show-overflow-tooltip />
<el-table-column label="销售员" prop="salesman" width="90" />
<el-table-column label="金额" prop="orderAmount" width="100" align="right" />
<el-table-column label="已有计划" prop="planCount" width="80" align="center">
<template slot-scope="s"><span :class="{ 'pp-cnt-zero': !s.row.planCount }">{{ s.row.planCount || 0 }}</span></template>
</el-table-column>
</el-table>
<pagination
v-show="pickerTotal > 0"
:total="pickerTotal"
:page.sync="pickerQuery.pageNum"
:limit.sync="pickerQuery.pageSize"
@pagination="loadPicker"
/>
<div slot="footer">
<span class="pp-picker-count">已选 {{ pickerSelection.length }} 个合同</span>
<el-button @click="pickerOpen = false">取消</el-button>
<el-button type="primary" @click="confirmPicker">确定</el-button>
</div>
</el-dialog>
<!-- 选择供应商表格 + 单选 -->
<el-dialog title="选择供应商" :visible.sync="supplierPickerOpen" width="760px" append-to-body>
<div class="pp-picker-tool">
<el-input
v-model="supplierQuery.name"
size="small"
clearable
placeholder="搜索供应商名称"
prefix-icon="el-icon-search"
style="width:260px"
@keyup.enter.native="reloadSuppliers"
@clear="reloadSuppliers"
/>
<el-button type="primary" size="small" icon="el-icon-search" @click="reloadSuppliers">查询</el-button>
</div>
<el-table :data="supplierList" v-loading="supplierLoading" border size="small" max-height="420" @row-click="chooseSupplier">
<el-table-column label="编码" prop="supplierCode" width="130" show-overflow-tooltip />
<el-table-column label="供应商名称" prop="name" min-width="180" show-overflow-tooltip />
<el-table-column label="类型" prop="type" width="90" align="center" />
<el-table-column label="信用" prop="creditRating" width="70" align="center" />
<el-table-column label="联系人" prop="contactPerson" width="100" />
<el-table-column label="电话" prop="contactPhone" width="130" />
<el-table-column label="操作" width="70" align="center">
<template slot-scope="s"><el-button type="text" size="mini" @click.stop="chooseSupplier(s.row)">选择</el-button></template>
</el-table-column>
</el-table>
<pagination
v-show="supplierTotal > 0"
:total="supplierTotal"
:page.sync="supplierQuery.pageNum"
:limit.sync="supplierQuery.pageSize"
@pagination="loadSuppliers"
/>
</el-dialog>
<!-- 批量填充明细设一次一键填满所有行留空不覆盖 -->
<el-dialog title="批量填充明细" :visible.sync="batchFillOpen" width="560px" append-to-body>
<p class="pp-batch-tip">下面填了值的字段会写入<b>全部明细行</b>留空的不覆盖常用于整批同产品/材质/规格</p>
<el-form :model="batchFill" label-width="72px" size="small">
<el-row :gutter="14">
<el-col :span="12"><el-form-item label="产品"><el-input v-model="batchFill.productType" placeholder="如 热轧卷板" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="材质"><el-input v-model="batchFill.material" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="牌号"><el-input v-model="batchFill.grade" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="卷号"><el-input v-model="batchFill.coilNo" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="宽度"><el-input v-model="batchFill.width" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="厚度"><el-input v-model="batchFill.thickness" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="宽公差"><el-input v-model="batchFill.widthTolerance" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="厚公差"><el-input v-model="batchFill.thicknessTolerance" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="重量(T)"><el-input v-model="batchFill.weight" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="数量"><el-input v-model="batchFill.quantity" /></el-form-item></el-col>
<el-col :span="12">
<el-form-item label="供货商">
<div class="pp-inline-pick">
<el-input v-model="batchFill.supplier" placeholder="选择或输入" />
<el-button icon="el-icon-search" @click="openSupplierPicker">选择</el-button>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="pp-batch-footer">
<el-checkbox v-model="batchFillNewRow">设为加行默认值</el-checkbox>
<span class="pp-batch-gen">生成 <el-input v-model="batchRows" size="mini" style="width:64px" /> </span>
<el-button @click="batchFillOpen = false">取消</el-button>
<el-button @click="applyBatchFill">应用到现有行</el-button>
<el-button type="primary" @click="generateRows">生成行并填充</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
listPurchasePlan,
purchasePlanStatistics,
getPurchasePlan,
getItemsByOrders,
addPurchasePlan,
updatePurchasePlan,
delPurchasePlan,
submitPurchasePlan,
listDelivery,
delDelivery,
listContracts
} from '@/api/erp/purchasePlan'
import { listSupplier } from '@/api/erp/purchase'
import { getToken } from '@/utils/auth'
export default {
name: 'ErpPurchasePlan',
data() {
return {
loading: true,
total: 0,
planList: [],
stats: { total: 0, completed: 0 },
queryParams: { pageNum: 1, pageSize: 20, keyword: undefined, auditStatus: '' },
mode: 'empty', // empty | view | edit
current: {},
activeTab: 'items',
form: {},
rules: {},
buttonLoading: false,
submitLoading: false,
selectedContracts: [],
// 批量填充
batchFillOpen: false,
batchFillNewRow: true,
batchRows: 0,
batchFill: { productType: '热轧卷板', material: '', grade: '', coilNo: '', width: '', thickness: '', widthTolerance: '', thicknessTolerance: '', weight: '', quantity: '', supplier: '' },
newRowDefaults: null,
// 合同选择器
pickerOpen: false,
pickerLoading: false,
pickerList: [],
pickerTotal: 0,
pickerQuery: { pageNum: 1, pageSize: 10, keyword: undefined },
pickerSelection: [],
// 供应商选择器
supplierPickerOpen: false,
supplierLoading: false,
supplierList: [],
supplierTotal: 0,
supplierQuery: { pageNum: 1, pageSize: 10, name: undefined },
// 到货
deliveryList: [],
deliveryLoading: false,
upload: { headers: { Authorization: 'Bearer ' + getToken() } },
progressColor: '#5b8db8'
}
},
computed: {
uploadUrl() {
return process.env.VUE_APP_BASE_API + '/erp/purchasePlan/' + (this.current.planId || '') + '/importDelivery'
},
itemsWeight() {
return (this.form.items || []).reduce((s, i) => s + (Number(i.weight) || 0), 0).toFixed(3)
}
},
created() {
this.getList()
},
methods: {
getList(keepCurrent) {
this.loading = true
listPurchasePlan(this.queryParams).then(res => {
this.planList = res.rows || []
this.total = res.total || 0
this.loading = false
if (!keepCurrent && this.mode !== 'edit') {
if (this.planList.length) this.selectPlan(this.planList[0])
else { this.mode = 'empty'; this.current = {} }
}
})
purchasePlanStatistics(this.queryParams).then(res => { this.stats = res.data || { total: 0, completed: 0 } })
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
selectPlan(p) {
this.activeTab = 'items'
this.mode = 'view'
this.current = { ...p }
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 })
},
// ---- 新增 / 编辑 ----
resetForm() {
this.form = { planId: null, planNo: '', supplier: '', purchaseDate: '', remark: '', items: [], orderIds: [], contractCodes: [] }
this.selectedContracts = []
this.newRowDefaults = null
this.batchFill = { productType: '热轧卷板', material: '', grade: '', coilNo: '', width: '', thickness: '', widthTolerance: '', thicknessTolerance: '', weight: '', quantity: '', supplier: '' }
this.batchRows = 0
},
handleAdd() {
this.resetForm()
this.mode = 'edit'
},
handleEdit() {
getPurchasePlan(this.current.planId).then(res => {
const d = res.data || {}
this.form = {
planId: d.planId, planNo: d.planNo, supplier: d.supplier,
purchaseDate: d.purchaseDate, remark: d.remark,
items: d.items || [], orderIds: d.orderIds || [], contractCodes: d.contractCodes || []
}
this.selectedContracts = []
this.mode = 'edit'
})
},
cancelEdit() {
this.mode = this.planList.length ? 'view' : 'empty'
if (this.planList.length) {
const back = this.current.planId ? this.planList.find(p => p.planId === this.current.planId) : this.planList[0]
this.selectPlan(back || this.planList[0])
}
},
// ---- 合同选择器 ----
openContractPicker() {
this.pickerQuery.pageNum = 1
this.loadPicker()
this.pickerOpen = true
},
reloadPicker() {
this.pickerQuery.pageNum = 1
this.loadPicker()
},
loadPicker() {
this.pickerLoading = true
listContracts(this.pickerQuery).then(res => {
this.pickerList = res.rows || []
this.pickerTotal = res.total || 0
this.pickerLoading = false
// 回显已选
this.$nextTick(() => {
if (!this.$refs.pickerTable) return
this.pickerList.forEach(row => {
if (this.form.orderIds.includes(row.orderId)) {
this.$refs.pickerTable.toggleRowSelection(row, true)
}
})
})
})
},
onPickerSelection(sel) {
this.pickerSelection = sel
},
confirmPicker() {
this.selectedContracts = this.pickerSelection.map(c => ({
orderId: c.orderId, orderCode: c.orderCode, contractName: c.contractName, customer: c.customer
}))
this.form.orderIds = this.pickerSelection.map(c => c.orderId)
this.form.contractCodes = []
this.pickerOpen = false
this.deriveItems(this.form.orderIds)
},
removeContract(orderId) {
this.selectedContracts = this.selectedContracts.filter(c => c.orderId !== orderId)
this.form.orderIds = this.form.orderIds.filter(id => id !== orderId)
this.deriveItems(this.form.orderIds)
},
// ---- 供应商选择器 ----
openSupplierPicker() {
this.supplierQuery.pageNum = 1
this.loadSuppliers()
this.supplierPickerOpen = true
},
reloadSuppliers() {
this.supplierQuery.pageNum = 1
this.loadSuppliers()
},
loadSuppliers() {
this.supplierLoading = true
listSupplier(this.supplierQuery).then(res => {
this.supplierList = res.rows || []
this.supplierTotal = res.total || 0
this.supplierLoading = false
})
},
chooseSupplier(row) {
// 供应商选择器服务于「批量填充」对话框
this.batchFill.supplier = row.name
this.supplierPickerOpen = false
},
deriveItems(orderIds) {
if (!orderIds || !orderIds.length) { this.form.items = []; return }
getItemsByOrders(orderIds).then(res => {
this.form.items = (res.data || []).map(it => ({
productType: it.productType || '热轧卷板',
material: it.material || '',
grade: it.grade || '',
coilNo: '',
width: it.width || '',
thickness: it.thickness || '',
widthTolerance: it.widthTolerance || '0',
thicknessTolerance: it.thicknessTolerance || '0',
weight: it.weight,
quantity: it.quantity,
supplier: this.form.supplier || ''
}))
this.$modal.msgSuccess(`已载入 ${this.form.items.length} 条明细,可继续编辑`)
})
},
blankItem() {
return { productType: '热轧卷板', material: '', grade: '', coilNo: '', width: '', thickness: '', widthTolerance: '0', thicknessTolerance: '0', weight: '', quantity: '', supplier: '' }
},
addItem() {
// 优先用批量默认值,其次继承上一行,最后空行;重量每行不同故清空
const last = this.form.items[this.form.items.length - 1]
let row
if (this.newRowDefaults) row = { ...this.newRowDefaults }
else if (last) row = { ...last }
else row = this.blankItem()
row.weight = ''
this.form.items.push(row)
},
copyItem(index) {
this.form.items.splice(index + 1, 0, { ...this.form.items[index] })
},
removeItem(index) {
this.form.items.splice(index, 1)
},
openBatchFill() {
this.batchFillOpen = true
},
batchFillKeys() {
const f = this.batchFill
const keys = ['productType', 'material', 'grade', 'coilNo', 'width', 'thickness', 'widthTolerance', 'thicknessTolerance', 'weight', 'quantity', 'supplier']
return keys.filter(k => f[k] !== '' && f[k] != null)
},
applyBatchFill() {
const f = this.batchFill
const filled = this.batchFillKeys()
if (!filled.length) { this.$modal.msgWarning('请至少填写一个要填充的字段'); return }
if (!this.form.items.length) { this.$modal.msgWarning('当前没有明细行,请用右侧「生成行并填充」'); return }
this.form.items.forEach(it => { filled.forEach(k => { it[k] = f[k] }) })
if (f.supplier) this.form.supplier = f.supplier
this.saveNewRowDefaults(filled)
this.batchFillOpen = false
this.$modal.msgSuccess(`已填充 ${this.form.items.length}`)
},
generateRows() {
const n = parseInt(this.batchRows, 10) || 0
if (n <= 0) { this.$modal.msgWarning('请填写要生成的行数'); return }
const f = this.batchFill
const filled = this.batchFillKeys()
for (let i = 0; i < n; i++) {
const row = this.blankItem()
filled.forEach(k => { row[k] = f[k] })
this.form.items.push(row)
}
if (f.supplier) this.form.supplier = f.supplier
this.saveNewRowDefaults(filled)
this.batchFillOpen = false
this.$modal.msgSuccess(`已生成 ${n}`)
},
saveNewRowDefaults(filled) {
if (!this.batchFillNewRow) return
const def = this.blankItem()
filled.forEach(k => { def[k] = this.batchFill[k] })
this.newRowDefaults = def
},
submitForm() {
this.$refs['form'].validate(valid => {
if (!valid) return
if (!this.form.orderIds.length) { this.$modal.msgWarning('请先选择销售合同'); return }
this.buttonLoading = true
const api = this.form.planId ? updatePurchasePlan : addPurchasePlan
const editedId = this.form.planId
api(this.form).then(() => {
this.$modal.msgSuccess('保存成功')
this.mode = 'view'
this.loading = true
listPurchasePlan(this.queryParams).then(res => {
this.planList = res.rows || []
this.total = res.total || 0
this.loading = false
const target = editedId ? this.planList.find(p => p.planId === editedId) : this.planList[0]
if (target) this.selectPlan(target)
})
purchasePlanStatistics(this.queryParams).then(res => { this.stats = res.data || { total: 0, completed: 0 } })
}).finally(() => { this.buttonLoading = false })
})
},
submitForAudit() {
const tip = this.current.auditStatus === '2' ? '确认重新送审该计划?将再次进入审核' : '确认送审该计划?送审后将出现在采购审核页'
this.$modal.confirm(tip).then(() => {
this.submitLoading = true
return submitPurchasePlan(this.current.planId)
}).then(() => {
this.$modal.msgSuccess('已送审')
this.getList(true)
this.refreshDetail()
}).catch(() => {}).finally(() => { this.submitLoading = false })
},
handleDelete(row) {
this.$modal.confirm('确认删除采购计划「' + row.planNo + '」?').then(() => {
return delPurchasePlan(row.planId)
}).then(() => {
this.$modal.msgSuccess('删除成功')
this.mode = 'empty'
this.current = {}
this.getList()
}).catch(() => {})
},
// ---- 到货 ----
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.refreshDetail()
this.getList(true)
} else {
this.$alert(res.msg || '导入失败', '到货文件校验未通过', { dangerouslyUseHTMLString: true, type: 'error' })
}
},
handleUploadError() {
this.$modal.msgError('上传失败,请检查文件后重试')
},
removeDelivery(row) {
this.$modal.confirm('确定删除该到货记录吗?').then(() => {
return delDelivery(row.deliveryId)
}).then(() => {
this.$modal.msgSuccess('删除成功')
this.refreshDetail()
this.getList(true)
}).catch(() => {})
},
auditText(s) {
return { '0': '待审核', '1': '已通过', '2': '已驳回', '3': '待送审' }[s] || '—'
},
itemStatusText(s) {
return { '0': '未到货', '1': '部分到货', '2': '已到货' }[s] || '未到货'
}
}
}
</script>
<style lang="scss" scoped>
$accent: #5b8db8;
$line: #e4e7ed;
$ink: #303133;
$sub: #909399;
.pp-wb {
height: calc(100vh - 84px);
display: flex; flex-direction: column;
background: #f5f6f8; padding: 12px; box-sizing: border-box;
}
.pp-main { flex: 1; display: flex; gap: 12px; min-height: 0; }
.pp-col { background: #fff; border: 1px solid $line; display: flex; flex-direction: column; min-height: 0; }
.pp-plans { width: 300px; flex-shrink: 0; }
.pp-detail { flex: 1; min-width: 0; overflow-y: auto; }
.pp-col-tool {
padding: 10px; border-bottom: 1px solid $line;
display: flex; gap: 8px; align-items: center;
// 仅对齐,不改高度(沿用全局 24px
::v-deep .el-input { flex: 1; }
::v-deep .el-button { margin: 0; flex-shrink: 0; }
}
// 供货商:输入框 + 选择按钮 并排,等高对齐
.pp-inline-pick {
display: flex; gap: 8px; align-items: center;
::v-deep .el-input { flex: 1; }
::v-deep .el-button { flex-shrink: 0; margin: 0; }
}
.pp-col-filter { padding: 8px 10px; border-bottom: 1px solid $line; }
.pp-list { flex: 1; overflow-y: auto; margin: 0; padding: 0; list-style: none; }
.pp-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; }
}
.pp-li-r1 { display: flex; justify-content: space-between; align-items: center; }
.pp-no { font-size: 13px; font-weight: 600; color: $ink; }
.pp-li-r2 { display: flex; justify-content: space-between; font-size: 12px; color: $sub; margin: 4px 0 6px; }
.pp-w { color: #606266; }
.pp-li-r3 { display: flex; justify-content: space-between; font-size: 11px; color: $sub; margin-top: 4px; }
.pp-arch { font-style: normal; color: $accent; margin-left: 6px; }
.pp-empty { text-align: center; color: $sub; padding: 36px 12px; font-size: 13px; }
.pp-badge {
font-size: 11px; line-height: 16px; padding: 0 6px; border-radius: 2px;
border: 1px solid #dcdfe6; color: $sub; background: #fafafa;
&.a1 { color: $accent; border-color: #b9d2e6; background: #eef3f8; }
&.a2 { color: #c45656; border-color: #e6c4c4; background: #fbf0f0; }
&.a3 { color: #d6a256; border-color: #ecd4a6; background: #fdf6ec; }
&.plan { margin-left: 6px; }
&.p1 { color: $accent; border-color: #b9d2e6; }
}
.pp-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; }
}
.pp-d-head {
display: flex; justify-content: space-between; align-items: center;
padding: 14px 18px; border-bottom: 1px solid $line;
}
.pp-d-title { font-size: 15px; font-weight: 600; color: $ink; margin-right: 8px; }
.pp-meta { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px 24px; padding: 16px 18px; }
.pp-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 3; }
}
.pp-tabs { padding: 0 18px 18px; }
.pp-edit { padding: 0 0 18px; }
.pp-form { padding: 0 18px; }
.pp-section {
font-size: 13px; font-weight: 600; color: $ink;
border-left: 3px solid $accent; padding-left: 8px; margin: 16px 0 12px;
.pp-section-act { margin-left: 10px; }
.pp-section-hint { float: right; font-weight: 400; color: $sub; font-size: 12px; }
}
/* 已选合同 chips */
.pp-picked { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 4px; min-height: 24px; }
.pp-chip {
display: inline-flex; align-items: center; gap: 6px;
border: 1px solid #b9d2e6; background: #eef3f8; color: $accent;
border-radius: 3px; padding: 3px 8px; font-size: 12px;
b { font-weight: 600; }
.pp-chip-sub { color: $sub; font-weight: 400; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.el-icon-close { cursor: pointer; color: $sub; &:hover { color: #c45656; } }
&.readonly { color: #606266; border-color: #dcdfe6; background: #fafafa; }
}
.pp-chip-tip { color: $sub; font-size: 12px; align-self: center; }
.pp-picked-empty { color: $sub; font-size: 13px; }
.pp-deliv-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; min-height: 32px; }
.pp-deliv-tip { color: $sub; font-size: 13px; }
.pp-deliv-hint { color: $sub; font-size: 12px; }
.pp-del { color: #c45656; cursor: pointer; }
.pp-copy { color: #5b8db8; cursor: pointer; margin-right: 10px; }
.pp-batch-tip { font-size: 12px; color: #909399; margin: 0 0 14px; line-height: 1.6; b { color: #5b8db8; } }
.pp-batch-footer { display: flex; align-items: center; gap: 10px;
.el-checkbox { margin-right: auto; }
.pp-batch-gen { font-size: 13px; color: #606266; }
}
/* 审核记录 */
.pp-audit { margin: 0 18px 14px; border: 1px solid $line; border-radius: 3px; }
.pp-audit-head { font-size: 13px; font-weight: 600; color: $ink; padding: 8px 12px; border-bottom: 1px solid $line; background: #fafbfc; }
.pp-audit-list { margin: 0; padding: 0; list-style: none;
li { display: flex; align-items: center; gap: 10px; padding: 8px 12px; font-size: 12px; color: #606266; border-bottom: 1px solid #f0f2f5;
&:last-child { border-bottom: none; }
}
}
.pp-audit-by { color: $ink; }
.pp-audit-time { color: $sub; }
.pp-audit-op { flex: 1; color: #606266;
&.single { padding: 10px 12px; }
}
.pp-istat {
font-size: 11px; line-height: 16px; padding: 0 6px; border-radius: 2px; border: 1px solid #dcdfe6; color: $sub;
&.s1 { color: #d6a256; border-color: #ecd4a6; background: #fdf6ec; }
&.s2 { color: $accent; border-color: #b9d2e6; background: #eef3f8; }
}
/* 合同选择器 */
.pp-picker-tool { display: flex; gap: 8px; margin-bottom: 10px; }
.pp-picker-count { float: left; color: $sub; font-size: 13px; line-height: 32px; }
.pp-cnt-zero { color: #c0c4cc; }
</style>