调试生产完成率

This commit is contained in:
17630416519 2026-01-23 16:54:51 +08:00
parent b46e8f430d
commit 51f57889c6
2 changed files with 236 additions and 466 deletions

View File

@ -0,0 +1,9 @@
import request from '@/utils/request'
//日生产完成率
export function ProPlanAchievementrate(data) {
return request({
url: '/mes/reportManagement/report/productionCompletionRate',
method: 'get',
params: data
})
}

View File

@ -1,104 +1,58 @@
<template>
<div class="production-completion-rate">
<!-- 顶部操作栏 -->
<!-- <div class="top-bar"> -->
<!-- <div class="group-config">
<span>当前组数配置:</span>
<el-button size="small" @click="decreaseGroup">-</el-button>
<el-input v-model="groupCount" size="small" style="width: 60px; text-align: center;" />
<el-button size="small" @click="increaseGroup">+</el-button>
</div> -->
<!-- <div class="order-status">
<span>工单状态操作:</span>
<el-select v-model="selectedOrder" placeholder="选择工单" size="small" style="width: 200px;">
<el-option v-for="order in workOrderOptions" :key="order.value" :label="order.label"
:value="order.value" />
</el-select>
<el-button size="small" type="warning" @click="pauseWorkOrder">暂停工单</el-button>
</div> -->
<!-- </div> -->
<!-- /月完成率切换标签 -->
<div class="tab-container">
<div :class="['tab', { active: activeTab === 'day' }]" @click="switchTab('day')">日生产完成率</div>
<div :class="['tab', { active: activeTab === 'month' }]" @click="switchTab('month')">月生产完成率</div>
</div>
<!-- 进度条组件 -->
<div class="progress-container">
<div class="progress-info">
<div class="info-item">
<span class="label">目标值:</span>
<span class="value">{{ targetValue }}</span>
</div>
<div class="info-item">
<span class="label">实际值:</span>
<span class="value actual">{{ actualValue }}</span>
</div>
<div class="info-item">
<span class="label">完成率:</span>
<span class="value">{{ completionRate }}%</span>
</div>
</div>
<div class="progress-bar-container">
<div class="progress-bar" :style="{ width: progressBarWidth + '%' }" :class="{
'progress-success': completionRate >= 90,
'progress-warning': completionRate >= 70 && completionRate < 90,
'progress-danger': completionRate < 70
}">
<span class="progress-text">{{ completionRate }}%</span>
</div>
</div>
</div>
<!-- 查询区域 -->
<!-- <div class="query-area">
<el-form :model="queryForm" inline @submit.prevent>
<el-form-item label="选择日期">
<el-date-picker v-model="queryForm.date" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div> -->
<!-- 整体完成率柱状图 -->
<!-- <div class="chart-container">
<h3>整体生产完成率目标vs实际</h3>
<div id="completionChart" style="width: 20%; height: 20vh;"></div>
</div> -->
<!-- 小组小时产量统计表格 -->
<div class="table-container">
<!-- 固定表头 -->
<div class="table-header">
<table class="production-table">
<div v-if="loading" style="text-align: center; padding: 20px;">
<el-icon size="20">
<Loading />
</el-icon>
<span style="margin-left: 8px;">加载中...</span>
</div>
<div v-else-if="hourlyProductionList.length === 0"
style="text-align: center; padding: 20px; color: #909399;">
暂无生产数据
</div>
<div v-else class="scroll-wrapper">
<table class="production-table-header">
<thead>
<tr>
<th>小组</th>
<th>工单编号</th>
<th>型号</th>
<th>排班时段</th>
<th>8:30目标</th>
<th>8:30实际</th>
<th>9:30目标</th>
<th>9:30实际</th>
<th>10:30目标</th>
<th>10:30实际</th>
<th>11:30目标</th>
<th>11:30实际</th>
<th>13:30目标</th>
<th>13:30实际</th>
<th>14:30目标</th>
<th>14:30实际</th>
<th>15:30目标</th>
<th>15:30实际</th>
<th>16:30目标</th>
<th>16:30实际</th>
<th>8:00目标</th>
<th>8:00实际</th>
<th>9:00目标</th>
<th>9:00实际</th>
<th>10:00目标</th>
<th>10:00实际</th>
<th>11:00目标</th>
<th>11:00实际</th>
<th>12:00目标</th>
<th>12:00实际</th>
<th>13:00目标</th>
<th>13:00实际</th>
<th>14:00目标</th>
<th>14:00实际</th>
<th>15:00目标</th>
<th>15:00实际</th>
<th>16:00目标</th>
<th>16:00实际</th>
<th>17:00目标</th>
<th>17:00实际</th>
<th>18:00目标</th>
<th>18:00实际</th>
<th>19:00目标</th>
<th>19:00实际</th>
<th>20:00目标</th>
<th>20:00实际</th>
<th>21:00目标</th>
<th>21:00实际</th>
<th>当日总目标</th>
<th>当日实际</th>
<th>完成率</th>
@ -106,35 +60,45 @@
</tr>
</thead>
</table>
</div>
<!-- 可滚动的数据行 -->
<div class="scroll-wrapper">
<vue3-seamless-scroll :list="hourlyProductionList" :wheel="true" :step="0.3" :hover="true"
direction="up" class="scroll-table">
<table class="production-table">
<!-- 表格内容无缝滚动 -->
<Vue3SeamlessScroll class="seamless-scroll" :list="hourlyProductionList" :step="0.3" :hover="true"
direction="up" :singleHeight="60" :isWatch="true">
<table class="production-table-body">
<tbody>
<tr v-for="(item, index) in hourlyProductionList" :key="index">
<td>{{ item.groupName }}</td>
<td>{{ item.orderNo }}</td>
<td>{{ item.model }}</td>
<td>{{ item.model || '-' }}</td>
<td>{{ item.shiftTime }}</td>
<td>{{ item['8:30Target'] }}</td>
<td>{{ item['8:30Actual'] }}</td>
<td>{{ item['9:30Target'] }}</td>
<td>{{ item['9:30Actual'] }}</td>
<td>{{ item['10:30Target'] }}</td>
<td>{{ item['10:30Actual'] }}</td>
<td>{{ item['11:30Target'] }}</td>
<td>{{ item['11:30Actual'] }}</td>
<td>{{ item['13:30Target'] }}</td>
<td>{{ item['13:30Actual'] }}</td>
<td>{{ item['14:30Target'] }}</td>
<td>{{ item['14:30Actual'] }}</td>
<td>{{ item['15:30Target'] }}</td>
<td>{{ item['15:30Actual'] }}</td>
<td>{{ item['16:30Target'] }}</td>
<td>{{ item['16:30Actual'] }}</td>
<td>{{ item['8:00Target'] || 0 }}</td>
<td>{{ item['8:00Actual'] || 0 }}</td>
<td>{{ item['9:00Target'] || 0 }}</td>
<td>{{ item['9:00Actual'] || 0 }}</td>
<td>{{ item['10:00Target'] || 0 }}</td>
<td>{{ item['10:00Actual'] || 0 }}</td>
<td>{{ item['11:00Target'] || 0 }}</td>
<td>{{ item['11:00Actual'] || 0 }}</td>
<td>{{ item['12:00Target'] || 0 }}</td>
<td>{{ item['12:00Actual'] || 0 }}</td>
<td>{{ item['13:00Target'] || 0 }}</td>
<td>{{ item['13:00Actual'] || 0 }}</td>
<td>{{ item['14:00Target'] || 0 }}</td>
<td>{{ item['14:00Actual'] || 0 }}</td>
<td>{{ item['15:00Target'] || 0 }}</td>
<td>{{ item['15:00Actual'] || 0 }}</td>
<td>{{ item['16:00Target'] || 0 }}</td>
<td>{{ item['16:00Actual'] || 0 }}</td>
<td>{{ item['17:00Target'] || 0 }}</td>
<td>{{ item['17:00Actual'] || 0 }}</td>
<td>{{ item['18:00Target'] || 0 }}</td>
<td>{{ item['18:00Actual'] || 0 }}</td>
<td>{{ item['19:00Target'] || 0 }}</td>
<td>{{ item['19:00Actual'] || 0 }}</td>
<td>{{ item['20:00Target'] || 0 }}</td>
<td>{{ item['20:00Actual'] || 0 }}</td>
<td>{{ item['21:00Target'] || 0 }}</td>
<td>{{ item['21:00Actual'] || 0 }}</td>
<td>{{ item.dailyTarget }}</td>
<td>{{ item.dailyActual }}</td>
<td>
@ -142,267 +106,132 @@
<span style="margin-left: 5px;">{{ item.completionRate }}%</span>
</td>
<td>
<el-tag :type="item.status === '正常生产' ? 'success' : 'warning'">
{{ item.status }}
<el-tag :type="getStatusTagType(item.status)">
{{ getStatusText(item.status) }}
</el-tag>
</td>
</tr>
</tbody>
</table>
</vue3-seamless-scroll>
</Vue3SeamlessScroll>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import * as echarts from 'echarts'
import { ref, reactive, onMounted, computed } from 'vue'
import { Loading } from '@element-plus/icons-vue'
import { Vue3SeamlessScroll } from 'vue3-seamless-scroll'
//
const groupCount = ref(18)
const selectedOrder = ref('WO001(Model-X1)')
const workOrderOptions = ref([
{ label: 'WO001(Model-X1)', value: 'WO001(Model-X1)' },
{ label: 'WO002(Model-Y2)', value: 'WO002(Model-Y2)' },
{ label: 'WO003(Model-Z3)', value: 'WO003(Model-Z3)' }
])
//
import { ProPlanAchievementrate } from "@/api/productManagement/analysisOfProductionCompletionRate"
const activeTab = ref('day')
const loading = ref(false)
const hourlyProductionList = ref([])
const getStatusText = (status) => {
const statusMap = {
'未开始': '未开始',
'开始': '正常生产',
'完成': '已完成',
'暂停': '暂停生产'
}
return statusMap[status] || status
}
const getStatusTagType = (status) => {
const typeMap = {
'未开始': 'info',
'开始': 'success',
'完成': 'primary',
'暂停': 'warning'
}
return typeMap[status] || 'info'
}
const transformApiData = (apiData) => {
if (!Array.isArray(apiData)) return []
return apiData.map(item => {
const tableItem = {
groupName: item.groupName || '',
orderNo: item.workOrder || '',
model: item.model,
shiftTime: item.workTimePeriod || '',
dailyTarget: item.deliveryNum || 0,
dailyActual: item.completionNum || 0,
completionRate: item.completionRate || 0,
status: item.status || ''
}
const hours = ['8:00', '9:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00', '17:00', '18:00', '19:00', '20:00', '21:00']
hours.forEach(hour => {
tableItem[`${hour}Target`] = 0
tableItem[`${hour}Actual`] = 0
})
if (Array.isArray(item.list)) {
item.list.forEach(hourItem => {
const cleanTime = hourItem.startTime.replace(//g, ':').replace(/\s/g, '')
if (hours.includes(cleanTime)) {
tableItem[`${cleanTime}Target`] = hourItem.planNum || 0
tableItem[`${cleanTime}Actual`] = hourItem.completionNum || 0
}
})
}
return tableItem
})
}
const getProductionData = () => {
loading.value = true
ProPlanAchievementrate().then(res => {
loading.value = false
if (res && res.code === 200 && Array.isArray(res.data)) {
hourlyProductionList.value = transformApiData(res.data)
console.log('接口数据加载成功:', res.data)
} else {
hourlyProductionList.value = []
console.error('接口返回格式异常:', res)
}
}).catch(err => {
loading.value = false
hourlyProductionList.value = []
console.error('接口请求失败:', err)
})
}
const switchTab = (tab) => {
activeTab.value = tab
//
initChart()
getProductionData()
}
//
const targetValue = ref(5000) //
const actualValue = ref(4200) //
const completionRate = computed(() => {
return Math.round((actualValue.value / targetValue.value) * 100)
})
const progressBarWidth = computed(() => {
return Math.min(100, Math.max(0, (actualValue.value / targetValue.value) * 100))
})
//
const queryForm = reactive({
date: '2026-01-14'
})
//
const hourlyProductionList = ref([
{
groupName: '第5组',
orderNo: 'WO001',
model: 'Model-X1',
shiftTime: '9:00-18:00',
'8:30Target': 14,
'8:30Actual': 13,
'9:30Target': 16,
'9:30Actual': 14,
'10:30Target': 14,
'10:30Actual': 11,
'11:30Target': 15,
'11:30Actual': 12,
'13:30Target': 15,
'13:30Actual': 11,
'14:30Target': 14,
'14:30Actual': 11,
'15:30Target': 15,
'15:30Actual': 14,
'16:30Target': 14,
'16:30Actual': 13,
dailyTarget: 123,
dailyActual: 99,
completionRate: 80.49,
status: '正常生产'
},
{
groupName: '第6组',
orderNo: 'WO002',
model: 'Model-Y2',
shiftTime: '8:30-17:30',
'8:30Target': 29,
'8:30Actual': 25,
'9:30Target': 28,
'9:30Actual': 21,
'10:30Target': 27,
'10:30Actual': 22,
'11:30Target': 28,
'11:30Actual': 24,
'13:30Target': 28,
'13:30Actual': 22,
'14:30Target': 28,
'14:30Actual': 25,
'15:30Target': 28,
'15:30Actual': 20,
'16:30Target': 27,
'16:30Actual': 24,
dailyTarget: 227,
dailyActual: 183,
completionRate: 80.62,
status: '正常生产'
},
{
groupName: '第7组',
orderNo: 'WO003',
model: 'Model-Z3',
shiftTime: '8:30-17:30',
'8:30Target': 62,
'8:30Actual': 50,
'9:30Target': 63,
'9:30Actual': 59,
'10:30Target': 56,
'10:30Actual': 50,
'11:30Target': 64,
'11:30Actual': 48,
'13:30Target': 63,
'13:30Actual': 58,
'14:30Target': 61,
'14:30Actual': 45,
'15:30Target': 57,
'15:30Actual': 48,
'16:30Target': 57,
'16:30Actual': 48,
dailyTarget: 472,
dailyActual: 406,
completionRate: 86.02,
status: '暂停生产'
},
{
groupName: '第8组',
orderNo: 'WO001',
model: 'Model-X1',
shiftTime: '8:30-17:30',
'8:30Target': 48,
'8:30Actual': 45,
'9:30Target': 41,
'9:30Actual': 37,
'10:30Target': 49,
'10:30Actual': 40,
'11:30Target': 46,
'11:30Actual': 43,
'13:30Target': 47,
'13:30Actual': 46,
'14:30Target': 43,
'14:30Actual': 34,
'15:30Target': 41,
'15:30Actual': 39,
'16:30Target': 48,
'16:30Actual': 46,
dailyTarget: 358,
dailyActual: 330,
completionRate: 92.18,
status: '正常生产'
}
])
//
const decreaseGroup = () => {
if (groupCount.value > 15) groupCount.value--
}
const increaseGroup = () => {
if (groupCount.value < 20) groupCount.value++
}
//
const pauseWorkOrder = () => {
//
console.log('暂停工单:', selectedOrder.value)
alert(`已暂停工单: ${selectedOrder.value}`)
}
//
const handleSearch = () => {
console.log('搜索条件:', queryForm)
//
}
const handleReset = () => {
queryForm.date = '2026-01-14'
}
//
let chartInstance = null
const initChart = () => {
if (chartInstance) chartInstance.dispose()
chartInstance = echarts.init(document.getElementById('completionChart'))
const option = {
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { data: ['目标产量', '实际产量'] },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: ['整体生产'] },
yAxis: { type: 'value', max: 5000 },
series: [
{
name: '目标产量',
type: 'bar',
data: [4800],
itemStyle: { color: '#409eff' }
},
{
name: '实际产量',
type: 'bar',
data: [4200],
itemStyle: { color: '#67c23a' }
}
]
}
chartInstance.setOption(option)
window.addEventListener('resize', () => chartInstance.resize())
}
//
onMounted(() => {
// initChart()
getProductionData()
})
</script>
<style scoped>
.production-completion-rate {
/* padding: 20px; */
/* background-color: #f5f7fa; */
/* min-height: 100vh; */
}
.top-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 10px 15px;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.group-config,
.order-status {
display: flex;
align-items: center;
gap: 8px;
/* padding: 10px; */
width: 100%;
box-sizing: border-box;
}
.tab-container {
display: flex;
background-color: #fff;
border-radius: 4px 4px 0 0;
overflow: hidden;
margin-bottom: 16px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.tab {
flex: 1;
padding: 12px;
text-align: center;
padding: 10px 20px;
cursor: pointer;
border: 1px solid #409eff;
color: #409eff;
font-weight: bold;
transition: background-color 0.3s;
width: 45vw;
text-align: center;
}
.tab.active {
@ -410,158 +239,90 @@ onMounted(() => {
color: #fff;
}
.progress-container {
background-color: #fff;
padding: 20px;
border-radius: 4px;
margin-bottom: 16px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.progress-info {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
}
.info-item {
display: flex;
flex-direction: column;
align-items: center;
}
.label {
font-size: 14px;
color: #606266;
margin-bottom: 5px;
}
.value {
font-size: 18px;
font-weight: bold;
color: #303133;
}
.value.actual {
color: #67c23a;
}
.progress-bar-container {
width: 100%;
height: 30px;
background-color: #ebeef5;
border-radius: 15px;
overflow: hidden;
position: relative;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
}
.progress-bar {
height: 100%;
width: 0;
background: linear-gradient(to right, #409eff, #67c23a);
border-radius: 15px;
transition: width 0.5s ease-in-out;
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.progress-success {
background: linear-gradient(to right, #67c23a, #52b12d);
}
/* .progress-warning {
background: linear-gradient(to right, #e6a23c, #d78c1b);
} */
.progress-danger {
background: linear-gradient(to right, #f56c6c, #f34b4b);
}
.progress-text {
color: white;
font-size: 12px;
font-weight: bold;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
z-index: 2;
}
.query-area {
background-color: #fff;
padding: 15px;
border-radius: 4px;
margin-bottom: 16px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.chart-container {
background-color: #fff;
padding: 15px;
border-radius: 4px;
margin-bottom: 16px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.table-container {
background-color: #fff;
padding: 15px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.table-header {
overflow-x: auto;
}
.table-header table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
box-sizing: border-box;
}
.scroll-wrapper {
position: relative;
height: 45vh;
overflow: hidden;
}
.scroll-table {
height: 100%;
background-color: red !important;
overflow: hidden;
}
.production-table {
height: 67vh;
overflow-x: auto;
overflow-y: hidden;
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.production-table-header {
width: 100%;
border-collapse: separate;
border-spacing: 0;
min-width: 120px;
position: relative;
z-index: 999;
}
.production-table th,
.production-table td {
.production-table-header th {
border: 1px solid #ebeef5;
padding: 8px 12px;
border-bottom: none;
padding: 8px 10px;
text-align: center;
font-size: 12px;
word-wrap: break-word;
}
.production-table th {
white-space: normal;
min-width: 100px;
background-color: #eef2f7;
font-weight: bold;
color: #606266;
position: sticky;
top: 0;
z-index: 100;
z-index: 999 !important;
}
.seamless-scroll {
overflow: hidden;
position: relative;
z-index: 1;
}
.production-table td {
background-color: #fff;
.production-table-body {
width: 100%;
border-collapse: separate;
border-spacing: 0;
min-width: 1200px;
}
.production-table-body tr:hover {
background-color: #f5f7fa;
}
.production-table-body td {
border: 1px solid #ebeef5;
padding: 8px 10px;
text-align: center;
font-size: 12px;
word-wrap: break-word;
white-space: normal;
min-width: 100px;
height: 40px;
}
.scroll-wrapper::-webkit-scrollbar {
width: 6px;
height: 8px;
}
.scroll-wrapper::-webkit-scrollbar-thumb {
background-color: #dcdfe6;
border-radius: 4px;
}
.scroll-wrapper::-webkit-scrollbar-track {
background-color: #f5f7fa;
}
</style>