<template>
|
<div class="app-container">
|
<!-- 搜索区域 -->
|
<el-card class="search-card" shadow="never">
|
<el-form :model="searchForm" :inline="true" class="search-form">
|
<el-form-item label="计划名称">
|
<el-input v-model="searchForm.planName" placeholder="请输入计划名称" clearable />
|
</el-form-item>
|
<el-form-item label="状态">
|
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable style="width: 150px">
|
<el-option label="启用" value="active" />
|
<el-option label="禁用" value="disabled" />
|
</el-select>
|
</el-form-item>
|
<el-form-item>
|
<el-button type="primary" @click="handleSearch">
|
<el-icon><Search /></el-icon>
|
搜索
|
</el-button>
|
<el-button @click="handleReset">
|
<el-icon><Refresh /></el-icon>
|
重置
|
</el-button>
|
</el-form-item>
|
</el-form>
|
</el-card>
|
|
<!-- 操作按钮 -->
|
<el-card class="table-card" shadow="never">
|
<div class="table-header">
|
<div class="table-title">采购计划列表</div>
|
<div class="table-actions">
|
<el-button type="primary" @click="handleAdd">
|
<el-icon><Plus /></el-icon>
|
新增计划
|
</el-button>
|
<el-button type="info" @click="handleExport">
|
<el-icon><Download /></el-icon>
|
导出
|
</el-button>
|
</div>
|
</div>
|
|
<!-- 数据表格 -->
|
<el-table
|
v-loading="loading"
|
:data="tableData"
|
stripe
|
border
|
style="width: 100%"
|
>
|
<el-table-column prop="planName" label="计划名称" min-width="150" />
|
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
|
<el-table-column prop="formula" label="计算公式" min-width="200" show-overflow-tooltip>
|
<template #default="{ row }">
|
<el-tag type="info" size="small">{{ row.formula }}</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="status" label="状态" width="80" align="center">
|
<template #default="{ row }">
|
<el-tag :type="row.status === 'active' ? 'success' : 'info'" size="small">
|
{{ row.status === 'active' ? '启用' : '禁用' }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="updateTime" label="最后计算时间" width="160" />
|
<el-table-column label="操作" width="200" fixed="right" align="center">
|
<template #default="{ row }">
|
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
<el-button type="success" link @click="handleCalculate(row)">计算</el-button>
|
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<!-- 分页 -->
|
<div class="pagination-container">
|
<el-pagination
|
v-model:current-page="pagination.current"
|
v-model:page-size="pagination.size"
|
:page-sizes="[10, 20, 50, 100]"
|
:total="total"
|
layout="total, sizes, prev, pager, next, jumper"
|
@size-change="handleSizeChange"
|
@current-change="handleCurrentChange"
|
/>
|
</div>
|
</el-card>
|
|
<!-- 新增/编辑对话框 -->
|
<el-dialog
|
v-model="dialogVisible"
|
:title="dialogType === 'add' ? '新增采购计划' : '编辑采购计划'"
|
width="1000px"
|
:close-on-click-modal="false"
|
>
|
<div class="form-container">
|
<!-- 基本信息 -->
|
<div class="form-section">
|
<div class="section-title">基本信息</div>
|
<el-form
|
ref="formRef"
|
:model="formData"
|
:rules="formRules"
|
label-width="120px"
|
>
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="编码" prop="code">
|
<el-input v-model="formData.code" placeholder="系统自动生成" disabled />
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="名称" prop="planName" required>
|
<el-input v-model="formData.planName" placeholder="请输入计划名称" />
|
</el-form-item>
|
</el-col>
|
</el-row>
|
|
<el-form-item label="描述" prop="description">
|
<el-input
|
v-model="formData.description"
|
type="textarea"
|
:rows="3"
|
placeholder="请输入计划描述"
|
/>
|
</el-form-item>
|
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="状态" prop="status">
|
<el-select v-model="formData.status" placeholder="请选择状态" style="width: 100%">
|
<el-option label="启用" value="active" />
|
<el-option label="禁用" value="disabled" />
|
</el-select>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="是否系统预置">
|
<el-checkbox v-model="formData.isSystemPreset">是</el-checkbox>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
</el-form>
|
</div>
|
|
<!-- 计算参数 -->
|
<div class="form-section">
|
<div class="section-title">计算参数</div>
|
<el-tabs v-model="activeTab" class="param-tabs">
|
<el-tab-pane label="需求参数" name="demand">
|
<div class="checkbox-group">
|
<el-checkbox v-model="formData.considerExistingStock">考虑现有库存</el-checkbox>
|
<el-checkbox v-model="formData.warehouseControl">仓库运行MRP的控制</el-checkbox>
|
<el-checkbox v-model="formData.calculateTotalDemand">计算总需求</el-checkbox>
|
<el-checkbox v-model="formData.considerSafetyStock">考虑安全库存</el-checkbox>
|
<el-checkbox v-model="formData.considerLockedStock">考虑锁库</el-checkbox>
|
<el-checkbox v-model="formData.notConsiderMaterialAux">不考虑物料辅助属性</el-checkbox>
|
<el-checkbox v-model="formData.negativeStockAsDemand">负库存作为需求</el-checkbox>
|
</div>
|
</el-tab-pane>
|
<el-tab-pane label="计算参数" name="calculation">
|
<div class="checkbox-group">
|
<el-checkbox v-model="formData.considerExistingStock">考虑现有库存</el-checkbox>
|
<el-checkbox v-model="formData.warehouseControl">仓库运行MRP的控制</el-checkbox>
|
<el-checkbox v-model="formData.calculateTotalDemand">计算总需求</el-checkbox>
|
<el-checkbox v-model="formData.considerSafetyStock">考虑安全库存</el-checkbox>
|
<el-checkbox v-model="formData.considerLockedStock">考虑锁库</el-checkbox>
|
<el-checkbox v-model="formData.notConsiderMaterialAux">不考虑物料辅助属性</el-checkbox>
|
<el-checkbox v-model="formData.negativeStockAsDemand">负库存作为需求</el-checkbox>
|
</div>
|
</el-tab-pane>
|
</el-tabs>
|
</div>
|
|
<!-- 汇总合并选项 -->
|
<div class="form-section">
|
<div class="section-title">汇总合并选项</div>
|
<div class="checkbox-group">
|
<el-checkbox v-model="formData.summaryMaterial">物料</el-checkbox>
|
<el-checkbox v-model="formData.summaryAuxAttributes">辅助属性</el-checkbox>
|
<el-checkbox v-model="formData.summaryDemandDate">需求日期</el-checkbox>
|
</div>
|
</div>
|
|
<!-- 计算公式 -->
|
<div class="form-section">
|
<div class="section-title">计算公式</div>
|
<div class="formula-input-section">
|
<el-form-item label="计算公式" prop="formula" required>
|
<el-input
|
v-model="formData.formula"
|
placeholder="例如: 预计出库数量 - 现有库存 + 安全库存 - 预计入库数量"
|
@input="validateFormula"
|
/>
|
</el-form-item>
|
<div class="formula-help">
|
<el-text type="info" size="small">
|
支持变量:预计出库数量、现有库存、安全库存、预计入库数量
|
</el-text>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<template #footer>
|
<div class="dialog-footer">
|
<el-button @click="dialogVisible = false">取消</el-button>
|
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
|
</div>
|
</template>
|
</el-dialog>
|
|
<!-- 产品选择对话框 -->
|
<el-dialog
|
v-model="productSelectDialogVisible"
|
title="选择产品"
|
width="800px"
|
:close-on-click-modal="false"
|
>
|
<div class="product-select">
|
<el-alert
|
title="请选择要计算的产品"
|
type="info"
|
:closable="false"
|
show-icon
|
>
|
<template #default>
|
<p>选择产品后,系统将根据当前计算公式和产品库存情况进行计算。</p>
|
</template>
|
</el-alert>
|
|
<el-table
|
v-loading="productLoading"
|
:data="productList"
|
@selection-change="handleProductSelectionChange"
|
stripe
|
border
|
style="width: 100%; margin-top: 20px;"
|
>
|
<el-table-column type="selection" width="55" />
|
<el-table-column prop="productCategory" label="产品大类" min-width="150" />
|
<el-table-column prop="specificationModel" label="产品规格" width="120" />
|
<el-table-column prop="inboundNum0" label="现有库存" width="100" align="right" />
|
<el-table-column prop="inboundNum" label="安全库存" width="100" align="right" />
|
<el-table-column prop="inboundNum" label="预计出库" width="100" align="right" />
|
<el-table-column prop="inboundNum0" label="预计入库" width="100" align="right" />
|
</el-table>
|
</div>
|
|
<template #footer>
|
<div class="dialog-footer">
|
<el-button @click="productSelectDialogVisible = false">取消</el-button>
|
<el-button type="primary" @click="handleConfirmProductSelection" :disabled="selectedProducts.length === 0">
|
确认计算
|
</el-button>
|
</div>
|
</template>
|
</el-dialog>
|
|
<!-- 计算结果对话框 -->
|
<el-dialog
|
v-model="calculateDialogVisible"
|
title="采购计算结果"
|
width="1000px"
|
:close-on-click-modal="false"
|
>
|
<div class="calculate-result">
|
<el-alert
|
title="计算结果"
|
type="success"
|
:closable="false"
|
show-icon
|
>
|
<template #default>
|
<p>基于当前配置的计算公式和库存情况,系统已计算出各产品的采购需求。</p>
|
</template>
|
</el-alert>
|
|
<el-table :data="calculateResult" stripe border style="width: 100%; margin-top: 20px;">
|
<el-table-column prop="productCategory" label="产品大类" min-width="150" />
|
<el-table-column prop="specificationModel" label="产品规格" width="120" />
|
<el-table-column prop="inboundNum0" label="现有库存" width="100" align="right" />
|
<el-table-column prop="inboundNum" label="安全库存" width="100" align="right" />
|
<el-table-column prop="inboundNum" label="预计出库数量" width="120" align="right" />
|
<el-table-column prop="inboundNum0" label="预计入库数量" width="120" align="right" />
|
<el-table-column prop="weeklyNetDemand" label="按周净需求" width="120" align="right">
|
<template #default="{ row }">
|
<el-tag :type="row.weeklyNetDemand > 0 ? 'warning' : 'success'" size="small">
|
{{ row.weeklyNetDemand }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="suggestedPurchase" label="建议采购" width="100" align="right">
|
<template #default="{ row }">
|
<el-tag :type="row.suggestedPurchase > 0 ? 'danger' : 'success'" size="small">
|
{{ row.suggestedPurchase }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
|
<template #footer>
|
<div class="dialog-footer">
|
<el-button @click="calculateDialogVisible = false">关闭</el-button>
|
<el-button type="primary" @click="handleCreatePurchaseOrder">确认</el-button>
|
</div>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import {ref, reactive, onMounted, getCurrentInstance} from 'vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { Search, Refresh, Plus, Download } from '@element-plus/icons-vue'
|
import {listPage,add,update,del,listPageCopy} from "@/api/procurementManagement/procurementPlan.js"
|
|
// 响应式数据
|
const loading = ref(false)
|
const submitLoading = ref(false)
|
const dialogVisible = ref(false)
|
const productSelectDialogVisible = ref(false)
|
const calculateDialogVisible = ref(false)
|
const dialogType = ref('add')
|
const productLoading = ref(false)
|
const selectedProducts = ref([])
|
const currentPlan = ref(null)
|
|
// 搜索表单
|
const searchForm = reactive({
|
planName: '',
|
status: ''
|
})
|
|
// 分页数据
|
const pagination = reactive({
|
current: 1,
|
size: 20
|
})
|
|
// 表单数据
|
const formData = reactive({
|
code: '',
|
planName: '',
|
description: '',
|
status: '',
|
isSystemPreset: false,
|
formula: '',
|
// 计算参数
|
considerExistingStock: false,
|
warehouseControl: false,
|
calculateTotalDemand: false,
|
considerSafetyStock: false,
|
considerLockedStock: false,
|
notConsiderMaterialAux: false,
|
negativeStockAsDemand: false,
|
// 汇总合并选项
|
summaryMaterial: false,
|
summaryAuxAttributes: false,
|
summaryDemandDate: false
|
})
|
|
// 当前激活的标签页
|
const activeTab = ref('demand')
|
|
// 表单验证规则
|
const formRules = {
|
planName: [
|
{ required: true, message: '请输入计划名称', trigger: 'blur' }
|
],
|
status: [
|
{ required: true, message: '请选择状态', trigger: 'change' }
|
],
|
formula: [
|
{ required: true, message: '请输入计算公式', trigger: 'blur' }
|
]
|
}
|
|
// 表格数据
|
const tableData = ref([])
|
|
// 产品列表数据
|
const productList = ref([
|
{
|
id: 4,
|
productName: '产品D',
|
productCode: 'PD004',
|
existingStock: 90,
|
safetyStock: 40,
|
expectedOutbound: 160,
|
expectedInbound: 35
|
}
|
])
|
|
// 计算结果数据
|
const calculateResult = ref([
|
{
|
productName: '产品A',
|
existingStock: 100,
|
safetyStock: 50,
|
expectedOutbound: 200,
|
expectedInbound: 30,
|
weeklyNetDemand: 120,
|
suggestedPurchase: 150
|
},
|
{
|
productName: '产品B',
|
existingStock: 80,
|
safetyStock: 30,
|
expectedOutbound: 150,
|
expectedInbound: 20,
|
weeklyNetDemand: 100,
|
suggestedPurchase: 120
|
}
|
])
|
const total = ref(0)
|
// 方法
|
const handleSearch = () => {
|
pagination.current = 1
|
loadData()
|
}
|
|
const handleReset = () => {
|
Object.assign(searchForm, {
|
planName: '',
|
status: ''
|
})
|
handleSearch()
|
}
|
|
const loadData = () => {
|
loading.value = true
|
listPage({...searchForm,...pagination}).then(res => {
|
if(res.code === 200){
|
tableData.value = res.data.records
|
total.value = res.data.total
|
loading.value = false
|
}
|
})
|
}
|
|
const handleAdd = () => {
|
dialogType.value = 'add'
|
resetForm()
|
// 自动生成编码
|
formData.code = 'CGJH' + String(Date.now()).slice(-4)
|
dialogVisible.value = true
|
}
|
|
const handleEdit = (row) => {
|
dialogType.value = 'edit'
|
Object.assign(formData, row)
|
dialogVisible.value = true
|
}
|
|
const handleDelete = async (row) => {
|
try {
|
await ElMessageBox.confirm('确定要删除这个采购计划吗?', '提示', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning'
|
})
|
let ids = [row.id]
|
del(ids).then(res =>{
|
if(res.code === 200){
|
ElMessage.success('删除成功')
|
loadData()
|
}
|
})
|
} catch {
|
// 用户取消删除
|
}
|
}
|
|
const handleSubmit = async () => {
|
try {
|
// 表单验证
|
if (!formData.planName || !formData.formula) {
|
ElMessage.error('请填写必填项')
|
return
|
}
|
|
submitLoading.value = true
|
|
if (dialogType.value === 'add') {
|
add(formData).then(res => {
|
if(res.code === 200){
|
ElMessage.success('新增成功')
|
dialogVisible.value = false
|
loadData()
|
}
|
})
|
} else {
|
// 编辑
|
update(formData).then(res => {
|
if(res.code === 200){
|
ElMessage.success('编辑成功')
|
dialogVisible.value = false
|
loadData()
|
}
|
})
|
}
|
|
|
} catch (error) {
|
ElMessage.error('操作失败')
|
} finally {
|
submitLoading.value = false
|
}
|
}
|
|
const resetForm = () => {
|
Object.assign(formData, {
|
code: '',
|
planName: '',
|
description: '',
|
status: '',
|
isSystemPreset: false,
|
formula: '预计出库数量 - 现有库存 + 安全库存 - 预计入库数量',
|
// 计算参数
|
considerExistingStock: false,
|
warehouseControl: false,
|
calculateTotalDemand: false,
|
considerSafetyStock: false,
|
considerLockedStock: false,
|
notConsiderMaterialAux: false,
|
negativeStockAsDemand: false,
|
// 汇总合并选项
|
summaryMaterial: false,
|
summaryAuxAttributes: false,
|
summaryDemandDate: false
|
})
|
activeTab.value = 'demand'
|
}
|
|
const validateFormula = () => {
|
// 简单的公式验证
|
const formula = formData.formula
|
if (formula && !/^[a-zA-Z\u4e00-\u9fa5\s\*\+\-\/\(\)\d\.]+$/.test(formula)) {
|
ElMessage.warning('公式格式可能不正确,请检查')
|
}
|
}
|
|
const handleCalculate = (row) => {
|
currentPlan.value = row
|
productSelectDialogVisible.value = true
|
loadProductList()
|
}
|
|
const loadProductList = () => {
|
productLoading.value = true
|
// 模拟加载产品数据
|
listPageCopy({size:-1}).then(res => {
|
if(res.code === 200){
|
productList.value = res.data.records
|
productLoading.value = false
|
}
|
})
|
}
|
|
const handleProductSelectionChange = (selection) => {
|
selectedProducts.value = selection
|
}
|
|
const handleConfirmProductSelection = () => {
|
if (selectedProducts.value.length === 0) {
|
ElMessage.warning('请选择要计算的产品')
|
return
|
}
|
|
ElMessage.success(`正在计算 ${currentPlan.value.planName} 的采购需求...`)
|
productSelectDialogVisible.value = false
|
|
// 根据选择的产品和计算公式进行计算
|
calculateWithSelectedProducts()
|
}
|
|
const calculateWithSelectedProducts = () => {
|
// 模拟计算过程
|
// 根据选择的产品更新计算结果
|
const result = selectedProducts.value.map(product => {
|
// 这里应该根据实际的计算公式进行计算
|
// 示例:预计出库数量 - 现有库存 + 安全库存 - 预计入库数量
|
const weeklyNetDemand = product.inboundNum - product.inboundNum0 + product.inboundNum - product.inboundNum0
|
const suggestedPurchase = Math.max(0, weeklyNetDemand)
|
|
return {
|
productCategory: product.productCategory,
|
specificationModel: product.specificationModel,
|
inboundNum0: product.inboundNum0,
|
inboundNum: product.inboundNum,
|
weeklyNetDemand: weeklyNetDemand,
|
suggestedPurchase: suggestedPurchase
|
}
|
})
|
|
calculateResult.value = result
|
calculateDialogVisible.value = true
|
}
|
|
|
const handleCreatePurchaseOrder = () => {
|
calculateDialogVisible.value = false
|
}
|
const { proxy } = getCurrentInstance();
|
const handleExport = () => {
|
ElMessageBox.confirm("内容将被导出,是否确认导出?", "导出", {
|
confirmButtonText: "确认",
|
cancelButtonText: "取消",
|
type: "warning",
|
})
|
.then(() => {
|
proxy.download("/procurementPlan/export", {}, "采购计划.xlsx");
|
})
|
.catch(() => {
|
proxy.$modal.msg("已取消");
|
});
|
}
|
|
|
const handleSizeChange = (size) => {
|
pagination.size = size
|
loadData()
|
}
|
|
const handleCurrentChange = (current) => {
|
pagination.current = current
|
loadData()
|
}
|
|
// 生命周期
|
onMounted(() => {
|
loadData()
|
})
|
</script>
|
|
<style scoped>
|
.app-container {
|
padding: 20px;
|
}
|
|
.page-header {
|
margin-bottom: 20px;
|
}
|
|
.page-header h2 {
|
margin: 0 0 8px 0;
|
color: #303133;
|
font-size: 24px;
|
font-weight: 600;
|
}
|
|
.page-header p {
|
margin: 0;
|
color: #909399;
|
font-size: 14px;
|
}
|
|
.search-card {
|
margin-bottom: 20px;
|
}
|
|
.search-form {
|
margin-bottom: 0;
|
}
|
|
.table-card {
|
margin-bottom: 20px;
|
}
|
|
.table-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 20px;
|
}
|
|
.table-title {
|
font-size: 16px;
|
font-weight: 600;
|
color: #303133;
|
}
|
|
.table-actions {
|
display: flex;
|
gap: 10px;
|
}
|
|
.pagination-container {
|
margin-top: 20px;
|
display: flex;
|
justify-content: end;
|
}
|
|
.form-container {
|
padding: 0 20px;
|
}
|
|
.formula-help {
|
margin-top: 5px;
|
}
|
|
.calculate-result {
|
padding: 20px 0;
|
}
|
|
.dialog-footer {
|
text-align: right;
|
}
|
|
:deep(.el-card__body) {
|
padding: 20px;
|
}
|
|
:deep(.el-table) {
|
font-size: 14px;
|
}
|
|
:deep(.el-form-item__label) {
|
font-weight: 500;
|
}
|
|
.form-container {
|
padding: 0;
|
}
|
|
.form-section {
|
margin-bottom: 24px;
|
border: 1px solid #e4e7ed;
|
border-radius: 6px;
|
overflow: hidden;
|
}
|
|
.section-title {
|
background-color: #f5f7fa;
|
padding: 12px 16px;
|
font-weight: 600;
|
color: #303133;
|
border-bottom: 1px solid #e4e7ed;
|
}
|
|
.form-section .el-form {
|
padding: 20px;
|
}
|
|
.param-tabs {
|
padding: 20px;
|
}
|
|
.param-tabs :deep(.el-tabs__header) {
|
margin-bottom: 20px;
|
}
|
|
.param-tabs :deep(.el-tabs__item.is-active) {
|
color: #f56c6c;
|
border-bottom-color: #f56c6c;
|
}
|
|
.checkbox-group {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 16px;
|
}
|
|
.checkbox-group .el-checkbox {
|
margin-right: 0;
|
margin-bottom: 8px;
|
}
|
|
.formula-input-section {
|
padding: 20px;
|
}
|
|
.formula-input-section .el-form-item {
|
margin-bottom: 12px;
|
}
|
|
.formula-help {
|
text-align: center;
|
margin-top: 8px;
|
}
|
</style>
|