You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1074 lines
32 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="sales-order-scheduling">
<el-card class="mb-4">
<template #header>
<div class="flex justify-between items-center">
<span>销售订单排产</span>
</div>
</template>
<!-- 搜索区域 -->
<el-form :inline="true" :model="searchForm" class="mb-4">
<el-form-item label="订单编号">
<el-input v-model="searchForm.orderNo" placeholder="请输入订单编号" />
</el-form-item>
<el-form-item label="交货日期">
<el-date-picker
v-model="searchForm.deliveryDate"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item label="优先级">
<el-select v-model="searchForm.priority" placeholder="请选择优先级">
<el-option label="紧急" value="urgent" />
<el-option label="普通" value="normal" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态">
<el-option label="待排产" value="pending" />
<el-option label="排产中" value="processing" />
<el-option label="已完成" value="completed" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<!-- 订单列表 -->
<el-table
:data="sortedOrderList"
border
style="width: 100%"
@sort-change="handleSortChange"
>
<el-table-column prop="saleorder_code" label="订单编号" width="180" sortable="custom" />
<el-table-column prop="material_name" label="物料名称" width="180" />
<el-table-column prop="material_code" label="物料编码" width="120" />
<el-table-column prop="material_model" label="规格型号" width="120" />
<el-table-column prop="order_amount" label="订单数量" width="120" sortable="custom" />
<el-table-column prop="complete_amount" label="完成数量" width="120" />
<el-table-column prop="release_qty" label="下达数量" width="120" />
<el-table-column prop="plan_delivery_date" label="计划交货日期" width="180" sortable="custom" />
<el-table-column prop="priority" label="优先级" width="120">
<template #default="{ row }">
<el-tag :type="getPriorityType(row.priority)">
{{ getPriorityText(row.priority) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="document_status" label="状态" width="120">
<template #default="{ row }">
<el-tag :type="getStatusType(row.document_status)">
{{ getStatusText(row.document_status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="180" sortable="custom" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleViewBom(row)">查看BOM</el-button>
<el-button
v-if="row.document_status === 'pending'"
type="primary"
link
@click="handleSchedule(row)"
>排产</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="flex justify-end mt-4">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- BOM分解弹窗 -->
<el-dialog
v-model="bomDialogVisible"
title="BOM分解结果"
width="90%"
>
<div class="bom-container">
<!-- 订单信息 -->
<div class="bom-header mb-4">
<el-descriptions :column="4" border>
<el-descriptions-item label="订单编号">{{ currentOrder?.saleorder_code }}</el-descriptions-item>
<el-descriptions-item label="物料名称">{{ currentOrder?.material_name }}</el-descriptions-item>
<el-descriptions-item label="订单数量">{{ currentOrder?.order_amount }}</el-descriptions-item>
<el-descriptions-item label="计划交货日期">{{ currentOrder?.plan_delivery_date }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- BOM树 -->
<el-table
:data="flattenedBomData"
border
style="width: 100%"
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column prop="materialName" label="物料名称" min-width="200">
<template #default="{ row }">
<span :style="{ paddingLeft: row.level * 20 + 'px' }">
{{ row.level > 0 ? '└─ ' : '' }}{{ row.materialName }}
</span>
</template>
</el-table-column>
<el-table-column prop="materialCode" label="物料编码" width="120" />
<el-table-column prop="unit" label="单位" width="80" />
<el-table-column prop="bomRatio" label="BOM用量系数" width="120">
<template #default="{ row }">
{{ row.bomRatio }}
</template>
</el-table-column>
<el-table-column prop="requiredAmount" label="需求数量" width="120">
<template #default="{ row }">
{{ row.requiredAmount }}
</template>
</el-table-column>
<el-table-column prop="inventoryAmount" label="当前库存" width="120">
<template #default="{ row }">
<el-tag :type="row.inventoryAmount >= row.requiredAmount ? 'success' : 'warning'">
{{ row.inventoryAmount }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="inTransitAmount" label="在途库存" width="120">
<template #default="{ row }">
{{ row.inTransitAmount }}
</template>
</el-table-column>
<el-table-column prop="netRequiredAmount" label="净需求" width="120">
<template #default="{ row }">
<el-tag :type="row.netRequiredAmount > 0 ? 'danger' : 'success'">
{{ row.netRequiredAmount }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="120">
<template #default="{ row }">
<el-tag :type="getBomStatusType(row)">
{{ getBomStatusText(row) }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
<!-- 排产弹窗 -->
<el-dialog
v-model="scheduleDialogVisible"
title="生产计划排产"
width="90%"
>
<div class="schedule-container">
<!-- 订单信息 -->
<div class="schedule-header mb-4">
<el-descriptions :column="4" border>
<el-descriptions-item label="订单编号">{{ currentOrder?.saleorder_code }}</el-descriptions-item>
<el-descriptions-item label="物料名称">{{ currentOrder?.material_name }}</el-descriptions-item>
<el-descriptions-item label="订单数量">{{ currentOrder?.order_amount }}</el-descriptions-item>
<el-descriptions-item label="计划交货日期">{{ currentOrder?.plan_delivery_date }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- 排产表单 -->
<el-form :model="scheduleForm" label-width="120px" class="mb-4">
<el-form-item label="排产类型">
<el-radio-group v-model="scheduleForm.scheduleType" @change="handleScheduleTypeChange">
<el-radio label="auto">自动排产</el-radio>
<el-radio label="manual">手动排产</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<!-- BOM物料排产列表 -->
<div class="schedule-bom-list">
<h3 class="mb-2">BOM物料排产</h3>
<el-table :data="scheduleBomList" border style="width: 100%">
<el-table-column prop="materialName" label="物料名称" min-width="200">
<template #default="{ row }">
<span :style="{ paddingLeft: row.level * 20 + 'px' }">
{{ row.level > 0 ? '└─ ' : '' }}{{ row.materialName }}
</span>
</template>
</el-table-column>
<el-table-column prop="materialCode" label="物料编码" width="120" />
<el-table-column prop="unit" label="单位" width="80" />
<el-table-column prop="bomRatio" label="BOM用量系数" width="120" />
<el-table-column prop="netRequiredAmount" label="净需求" width="120">
<template #default="{ row }">
<el-tag :type="row.netRequiredAmount > 0 ? 'danger' : 'success'">
{{ row.netRequiredAmount }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="计划生产数量" width="150">
<template #default="{ row }">
<el-input-number
v-model="row.planAmount"
:min="0"
:max="row.netRequiredAmount"
:step="1"
@change="handlePlanAmountChange(row)"
/>
</template>
</el-table-column>
<el-table-column label="计划开始时间" width="180">
<template #default="{ row }">
<el-date-picker
v-model="row.planStartDate"
type="datetime"
placeholder="选择开始时间"
@change="handleStartDateChange(row)"
/>
</template>
</el-table-column>
<el-table-column label="计划结束时间" width="180">
<template #default="{ row }">
<el-date-picker
v-model="row.planEndDate"
type="datetime"
placeholder="选择结束时间"
:disabled-date="(time) => time < row.planStartDate"
/>
</template>
</el-table-column>
<el-table-column label="生产车间" width="150">
<template #default="{ row }">
<el-tag :type="getWorkshopTagType(row.workshop)">
{{ getWorkshopName(row.workshop) }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="scheduleDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleConfirmSchedule">确认排产</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup name="salesOrderScheduling" lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
//
const searchForm = ref({
orderNo: '',
deliveryDate: [],
priority: '',
status: ''
})
//
const sortConfig = ref({
prop: 'planDeliveryDate',
order: 'ascending'
})
// BOM
interface BomItem {
id: string
materialName: string
materialCode: string
unit: string
bomRatio: number
requiredAmount: number
inventoryAmount: number
inTransitAmount: number
netRequiredAmount: number
level: number
children?: BomItem[]
planAmount?: number
workshop?: string
planStartDate?: string | Date
planEndDate?: string | Date
}
//
interface SaleOrder {
sale_order_id: number
saleorder_code: string
material_id: number
material_code: string
material_name: string
material_model: string
order_amount: number
complete_amount: number
release_qty: number
plan_delivery_date: string
priority: number
document_status: string
create_time: string
remark: string
}
//
interface ProductionPlan {
orderId: number
orderNo: string
materialId: number
materialName: string
orderAmount: number
planDeliveryDate: string
priority: number
bomItems: BomItem[]
}
//
const orderList = ref<SaleOrder[]>([
{
sale_order_id: 1,
saleorder_code: 'SO20240301001',
material_id: 1001,
material_code: 'TYRE-A',
material_name: '轮胎成品A',
material_model: '205/55R16',
order_amount: 100,
complete_amount: 0,
release_qty: 0,
plan_delivery_date: '2024-03-15',
priority: 1,
document_status: 'pending',
create_time: '2024-03-01 10:00:00',
remark: '加急订单'
},
{
sale_order_id: 2,
saleorder_code: 'SO20240301002',
material_id: 1002,
material_code: 'TYRE-B',
material_name: '轮胎成品B',
material_model: '215/55R17',
order_amount: 200,
complete_amount: 50,
release_qty: 100,
plan_delivery_date: '2024-03-20',
priority: 2,
document_status: 'processing',
create_time: '2024-03-01 11:00:00',
remark: '常规订单'
},
{
sale_order_id: 3,
saleorder_code: 'SO20240301003',
material_id: 1003,
material_code: 'TYRE-C',
material_name: '轮胎成品C',
material_model: '225/55R18',
order_amount: 150,
complete_amount: 150,
release_qty: 150,
plan_delivery_date: '2024-03-25',
priority: 2,
document_status: 'completed',
create_time: '2024-03-01 12:00:00',
remark: '常规订单'
}
])
// 计算排序后的订单列表
const sortedOrderList = computed(() => {
let result = [...orderList.value]
// 优先级排序
result.sort((a, b) => {
return a.priority - b.priority
})
// 交货日期排序
if (sortConfig.value.prop === 'plan_delivery_date') {
result.sort((a, b) => {
const dateA = new Date(a.plan_delivery_date).getTime()
const dateB = new Date(b.plan_delivery_date).getTime()
return sortConfig.value.order === 'ascending'
? dateA - dateB
: dateB - dateA
})
}
// 订单数量排序
if (sortConfig.value.prop === 'order_amount') {
result.sort((a, b) => {
return sortConfig.value.order === 'ascending'
? a.order_amount - b.order_amount
: b.order_amount - a.order_amount
})
}
// 创建时间排序
if (sortConfig.value.prop === 'create_time') {
result.sort((a, b) => {
const dateA = new Date(a.create_time).getTime()
const dateB = new Date(b.create_time).getTime()
return sortConfig.value.order === 'ascending'
? dateA - dateB
: dateB - dateA
})
}
return result
})
// 当前选中的订单
const currentOrder = ref<SaleOrder | null>(null)
// BOM弹窗数据
const bomDialogVisible = ref(false)
const bomTreeData = ref<BomItem[]>([
{
id: '1001',
materialName: '轮胎成品A',
materialCode: 'TYRE-A',
unit: '个',
bomRatio: 1,
requiredAmount: 100,
inventoryAmount: 20,
inTransitAmount: 10,
netRequiredAmount: 70,
level: 0,
children: [
{
id: '1001-1',
materialName: '胎面组件',
materialCode: 'TREAD-ASSY',
unit: '套',
bomRatio: 1,
requiredAmount: 100,
inventoryAmount: 30,
inTransitAmount: 20,
netRequiredAmount: 50,
level: 1,
children: [
{
id: '1001-1-1',
materialName: '胎面胶',
materialCode: 'TREAD-1',
unit: 'kg',
bomRatio: 2.5,
requiredAmount: 250,
inventoryAmount: 100,
inTransitAmount: 50,
netRequiredAmount: 100,
level: 2
},
{
id: '1001-1-2',
materialName: '钢丝帘布',
materialCode: 'STEEL-CORD',
unit: '米',
bomRatio: 3,
requiredAmount: 300,
inventoryAmount: 150,
inTransitAmount: 50,
netRequiredAmount: 100,
level: 2
}
]
},
{
id: '1001-2',
materialName: '胎侧组件',
materialCode: 'SIDEWALL-ASSY',
unit: '套',
bomRatio: 2,
requiredAmount: 200,
inventoryAmount: 80,
inTransitAmount: 40,
netRequiredAmount: 80,
level: 1,
children: [
{
id: '1001-2-1',
materialName: '胎侧胶',
materialCode: 'SIDEWALL-1',
unit: 'kg',
bomRatio: 1.5,
requiredAmount: 300,
inventoryAmount: 100,
inTransitAmount: 50,
netRequiredAmount: 150,
level: 2
},
{
id: '1001-2-2',
materialName: '尼龙帘布',
materialCode: 'NYLON-CORD',
unit: '米',
bomRatio: 2,
requiredAmount: 400,
inventoryAmount: 200,
inTransitAmount: 100,
netRequiredAmount: 100,
level: 2
}
]
},
{
id: '1001-3',
materialName: '胎圈组件',
materialCode: 'BEAD-ASSY',
unit: '套',
bomRatio: 2,
requiredAmount: 200,
inventoryAmount: 60,
inTransitAmount: 40,
netRequiredAmount: 100,
level: 1,
children: [
{
id: '1001-3-1',
materialName: '胎圈钢丝',
materialCode: 'BEAD-WIRE',
unit: 'kg',
bomRatio: 0.8,
requiredAmount: 160,
inventoryAmount: 50,
inTransitAmount: 30,
netRequiredAmount: 80,
level: 2
},
{
id: '1001-3-2',
materialName: '胎圈包布',
materialCode: 'BEAD-WRAP',
unit: '米',
bomRatio: 1.2,
requiredAmount: 240,
inventoryAmount: 100,
inTransitAmount: 50,
netRequiredAmount: 90,
level: 2
}
]
}
]
}
])
// 扁平化的BOM数据
const flattenedBomData = computed(() => {
const flatten = (items: BomItem[], level = 0): BomItem[] => {
return items.reduce((acc: BomItem[], item) => {
const flatItem = { ...item, level }
acc.push(flatItem)
if (item.children) {
acc.push(...flatten(item.children, level + 1))
}
return acc
}, [])
}
return flatten(bomTreeData.value)
})
// 获取BOM状态类型
const getBomStatusType = (row: BomItem) => {
if (row.netRequiredAmount <= 0) return 'success'
if (row.inventoryAmount + row.inTransitAmount > 0) return 'warning'
return 'danger'
}
// 获取BOM状态文本
const getBomStatusText = (row: BomItem) => {
if (row.netRequiredAmount <= 0) return '库存充足'
if (row.inventoryAmount + row.inTransitAmount > 0) return '部分库存'
return '需要生产'
}
// 排产弹窗
const scheduleDialogVisible = ref(false)
const scheduleForm = ref({
scheduleType: 'auto'
})
// 排产BOM列表
const scheduleBomList = ref<BomItem[]>([])
// 获取优先级类型
const getPriorityType = (priority) => {
const priorityMap = {
1: 'danger',
2: 'info'
}
return priorityMap[priority] || 'info'
}
// 获取优先级文本
const getPriorityText = (priority) => {
const priorityMap = {
1: '紧急',
2: '普通'
}
return priorityMap[priority] || '普通'
}
// 处理排序变化
const handleSortChange = ({ prop, order }) => {
sortConfig.value = { prop, order }
}
// 获取订单列表
const getOrderList = async () => {
try {
// TODO: 调用后端API获取订单列表
// const res = await getSaleOrders({
// page: currentPage.value,
// pageSize: pageSize.value,
// ...searchForm.value
// })
// orderList.value = res.data.list
// total.value = res.data.total
} catch (error) {
ElMessage.error('获取订单列表失败')
}
}
// 查看BOM
const handleViewBom = async (row) => {
try {
currentOrder.value = row
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 500))
// 根据订单ID筛选对应的BOM数据
const bomData = bomTreeData.value.find(item => item.id === row.material_id.toString())
if (bomData) {
bomTreeData.value = [bomData]
bomDialogVisible.value = true
} else {
ElMessage.warning('未找到对应的BOM数据')
}
} catch (error) {
ElMessage.error('获取BOM数据失败')
}
}
// 排产
const handleSchedule = (row) => {
currentOrder.value = row
scheduleForm.value.scheduleType = 'auto' // 默认选择自动排产
// 根据BOM数据生成排产列表自动关联车间和计算时间
scheduleBomList.value = flattenedBomData.value.map(item => {
const workshop = determineWorkshop(item.materialCode)
const planAmount = item.netRequiredAmount > 0 ? item.netRequiredAmount : 0
const planStartDate = calculatePlanStartDate(item.materialCode, row.plan_delivery_date)
const planEndDate = calculatePlanEndDate(item.materialCode, planAmount, planStartDate)
return {
...item,
planAmount,
workshop,
planStartDate,
planEndDate
}
})
scheduleDialogVisible.value = true
}
// 处理计划生产数量变化
const handlePlanAmountChange = (row: BomItem) => {
// 更新子件的计划生产数量
if (row.children) {
row.children.forEach(child => {
child.planAmount = row.planAmount * child.bomRatio
})
}
}
// 处理计划开始时间变化
const handleStartDateChange = (row: BomItem) => {
// 如果结束时间早于新的开始时间,清空结束时间
if (row.planEndDate && new Date(row.planEndDate) < new Date(row.planStartDate)) {
row.planEndDate = null
}
}
// 确认排产
const handleConfirmSchedule = async () => {
try {
// 验证BOM排产数据
const invalidItems = scheduleBomList.value.filter(item =>
item.netRequiredAmount > 0 && (
item.planAmount <= 0 ||
!item.planStartDate ||
!item.planEndDate ||
new Date(item.planStartDate) > new Date(item.planEndDate)
)
)
if (invalidItems.length > 0) {
ElMessage.warning('请完善BOM物料的排产信息')
return
}
// TODO: 调用后端API保存排产计划
// await saveSchedulePlan({
// orderId: currentOrder.value.id,
// scheduleType: scheduleForm.value.scheduleType,
// bomScheduleList: scheduleBomList.value
// })
ElMessage.success('排产计划保存成功')
scheduleDialogVisible.value = false
} catch (error) {
ElMessage.error('排产计划保存失败')
}
}
// 物料类型枚举
const MaterialType = {
FINISHED: 'TYRE', // 成品轮胎
SEMIFINISHED: { // 半成品
TREAD: 'TREAD', // 胎面
SIDEWALL: 'SIDEWALL',// 胎侧
BEAD: 'BEAD', // 胎圈
INNER: 'INNER' // 内胎
},
RAW: { // 原材料
RUBBER: 'RUBBER', // 橡胶
CORD: 'CORD', // 帘布
WIRE: 'WIRE', // 钢丝
OTHER: 'OTHER' // 其他
}
}
// 车间类型枚举
const WorkshopType = {
VULCANIZATION: 'vulcanization', // 硫化车间
FORMING: 'forming', // 成型车间
SEMIFINISHED: 'semiFinished' // 半成品车间
}
// 根据物料编码确定生产车间
const determineWorkshop = (materialCode: string): string => {
// 成品轮胎在硫化车间生产
if (materialCode.startsWith(MaterialType.FINISHED)) {
return WorkshopType.VULCANIZATION
}
// 胎面组件、胎侧组件、胎圈组件在成型车间组装
if (materialCode.includes('-ASSY')) {
return WorkshopType.FORMING
}
// 其他半成品在半成品车间生产
return WorkshopType.SEMIFINISHED
}
// 计算BOM生产计划
const calculateBomPlan = (bomItem: BomItem, orderAmount: number) => {
const plans: any[] = []
const workshop = determineWorkshop(bomItem.materialCode)
// 计算当前层级的计划
const requiredAmount = orderAmount * bomItem.bomRatio
const netRequiredAmount = requiredAmount - bomItem.inventoryAmount - bomItem.inTransitAmount
if (netRequiredAmount > 0) {
// 只为需要生产的物料创建计划
plans.push({
materialId: bomItem.id,
materialName: bomItem.materialName,
materialCode: bomItem.materialCode,
unit: bomItem.unit,
bomRatio: bomItem.bomRatio,
requiredAmount,
inventoryAmount: bomItem.inventoryAmount,
inTransitAmount: bomItem.inTransitAmount,
netRequiredAmount,
workshop,
planStartDate: calculatePlanStartDate(bomItem.materialCode, bomItem.plan_delivery_date),
planEndDate: calculatePlanEndDate(bomItem.materialCode, netRequiredAmount, calculatePlanStartDate(bomItem.materialCode, bomItem.plan_delivery_date))
})
}
// 递归计算子件的计划
if (bomItem.children) {
bomItem.children.forEach(child => {
plans.push(...calculateBomPlan(child, orderAmount))
})
}
return plans
}
// 生成生产计划
const handleGeneratePlan = async () => {
try {
// 获取所有待排产的订单
const pendingOrders = orderList.value.filter(order => order.document_status === 'pending')
if (pendingOrders.length === 0) {
ElMessage.warning('没有待排产的订单')
return
}
// 按优先级和交货日期排序
const sortedOrders = [...pendingOrders].sort((a, b) => {
// 优先级排序
const priorityDiff = a.priority - b.priority
if (priorityDiff !== 0) return priorityDiff
// 交货日期排序
const dateA = new Date(a.plan_delivery_date).getTime()
const dateB = new Date(b.plan_delivery_date).getTime()
return dateA - dateB
})
// 生成生产计划
const productionPlans: ProductionPlan[] = []
for (const order of sortedOrders) {
// 获取订单的BOM数据
const bomData = bomTreeData.value.find(item => item.id === order.material_id.toString())
if (!bomData) {
ElMessage.warning(`订单 ${order.saleorder_code} 未找到BOM数据`)
continue
}
// 计算所有需要生产的物料计划
const allBomPlans = calculateBomPlan(bomData, order.order_amount)
// 按车间分组整理生产计划
const vulcanizationPlans = allBomPlans.filter(plan => plan.workshop === WorkshopType.VULCANIZATION)
const formingPlans = allBomPlans.filter(plan => plan.workshop === WorkshopType.FORMING)
const semiFinishedPlans = allBomPlans.filter(plan => plan.workshop === WorkshopType.SEMIFINISHED)
// 确保每个订单只生成一个硫化计划和一个成型计划
if (vulcanizationPlans.length > 1 || formingPlans.length > 1) {
ElMessage.warning(`订单 ${order.saleorder_code} 的成型或硫化计划数量异常`)
continue
}
// 生成最终的生产计划
const plan: ProductionPlan = {
orderId: order.sale_order_id,
orderNo: order.saleorder_code,
materialId: order.material_id,
materialName: order.material_name,
orderAmount: order.order_amount,
planDeliveryDate: order.plan_delivery_date,
priority: order.priority,
vulcanizationPlan: vulcanizationPlans[0],
formingPlan: formingPlans[0],
semiFinishedPlans: semiFinishedPlans
}
productionPlans.push(plan)
}
// TODO: 调用后端API保存生产计划
// await saveProductionPlans(productionPlans)
ElMessage.success('生产计划生成成功')
// 刷新订单列表
getOrderList()
} catch (error) {
ElMessage.error('生产计划生成失败')
}
}
// 计算计划开始时间
const calculatePlanStartDate = (materialCode: string, deliveryDate: string): Date => {
const delivery = new Date(deliveryDate)
const now = new Date()
// 根据物料类型和车间计算提前天数
let daysBeforeDelivery = 0
if (materialCode.startsWith(MaterialType.FINISHED)) {
// 成品轮胎在硫化车间提前3天
daysBeforeDelivery = 3
} else if (materialCode.includes('-ASSY')) {
// 组件在成型车间提前5天
daysBeforeDelivery = 5
} else {
// 半成品提前7天
daysBeforeDelivery = 7
}
// 计算计划开始时间
const planStartDate = new Date(delivery)
planStartDate.setDate(planStartDate.getDate() - daysBeforeDelivery)
// 如果计算出的开始时间早于当前时间,则使用当前时间
return planStartDate < now ? now : planStartDate
}
// 计算计划结束时间
const calculatePlanEndDate = (materialCode: string, amount: number, startDate: Date): Date => {
// 根据物料类型和数量计算生产时间
let hoursPerUnit = 0
if (materialCode.startsWith(MaterialType.FINISHED)) {
// 成品轮胎每个单位需要2小时
hoursPerUnit = 2
} else if (materialCode.includes('-ASSY')) {
// 组件每个单位需要1.5小时
hoursPerUnit = 1.5
} else {
// 半成品每个单位需要1小时
hoursPerUnit = 1
}
// 计算总生产时间(小时)
const totalHours = amount * hoursPerUnit
// 计算结束时间
return new Date(startDate.getTime() + totalHours * 60 * 60 * 1000)
}
// 获取状态类型
const getStatusType = (status) => {
const statusMap = {
pending: 'info',
processing: 'warning',
completed: 'success'
}
return statusMap[status] || 'info'
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
pending: '待排产',
processing: '排产中',
completed: '已完成'
}
return statusMap[status] || status
}
// 分页相关方法
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
const handleSizeChange = (val) => {
pageSize.value = val
getOrderList()
}
const handleCurrentChange = (val) => {
currentPage.value = val
getOrderList()
}
// 搜索和重置
const handleSearch = () => {
currentPage.value = 1
getOrderList()
}
const resetSearch = () => {
searchForm.value = {
orderNo: '',
deliveryDate: [],
priority: '',
status: ''
}
handleSearch()
}
// 获取车间标签类型
const getWorkshopTagType = (workshop: string) => {
const typeMap = {
[WorkshopType.VULCANIZATION]: 'danger',
[WorkshopType.FORMING]: 'warning',
[WorkshopType.SEMIFINISHED]: 'info'
}
return typeMap[workshop] || 'info'
}
// 获取车间名称
const getWorkshopName = (workshop: string) => {
const nameMap = {
[WorkshopType.VULCANIZATION]: '硫化车间',
[WorkshopType.FORMING]: '成型车间',
[WorkshopType.SEMIFINISHED]: '半成品车间'
}
return nameMap[workshop] || workshop
}
// 处理排产类型变化
const handleScheduleTypeChange = (type: string) => {
if (type === 'auto' && currentOrder.value) {
// 自动排产时,重新计算所有物料的时间
scheduleBomList.value = scheduleBomList.value.map(item => {
const planStartDate = calculatePlanStartDate(item.materialCode, currentOrder.value.plan_delivery_date)
const planEndDate = calculatePlanEndDate(item.materialCode, item.planAmount, planStartDate)
return {
...item,
planStartDate,
planEndDate
}
})
} else {
// 手动排产时,清空时间
scheduleBomList.value = scheduleBomList.value.map(item => ({
...item,
planStartDate: null,
planEndDate: null
}))
}
}
onMounted(() => {
getOrderList()
})
</script>
<style scoped>
.sales-order-scheduling {
padding: 20px;
}
.bom-container {
padding: 10px;
}
.bom-header {
background-color: #f5f7fa;
padding: 15px;
border-radius: 4px;
}
:deep(.el-table .el-table__row) {
height: 50px;
}
:deep(.el-table .el-table__row:hover) {
background-color: #f5f7fa;
}
.schedule-container {
padding: 10px;
}
.schedule-header {
background-color: #f5f7fa;
padding: 15px;
border-radius: 4px;
}
.schedule-bom-list {
margin-top: 20px;
}
</style>