工序计划。作业人员推送

This commit is contained in:
tzy 2026-04-14 23:02:16 +08:00
parent 81cc61a6c6
commit dcd221f21b
10 changed files with 927 additions and 32 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
package-lock.json package-lock.json
yarn.lock yarn.lock
node_modules

View File

@ -0,0 +1,46 @@
import request from '@/utils/request'
// 概览卡片
export function getOverview(query) {
return request({
url: '/system/workbench/overview',
method: 'get',
params: query
})
}
// 工单数量柱状图
export function getWorkOrderBar(query) {
return request({
url: '/system/workbench/workOrderBar',
method: 'get',
params: query
})
}
// 工序不良率排名
export function getProcessDefectRank(query) {
return request({
url: '/system/workbench/processDefectRank',
method: 'get',
params: query
})
}
// 工单实时进度
export function getWorkOrderProgress(query) {
return request({
url: '/system/workbench/workOrderProgress',
method: 'get',
params: query
})
}
// 出货订单数据
export function getShipOrderData(query) {
return request({
url: '/system/workbench/shipOrderData',
method: 'get',
params: query
})
}

View File

@ -62,6 +62,20 @@ export const constantRoutes = [
} }
] ]
}, },
{
path: '/workbench',
component: Layout,
hidden: false,
redirect: '/workbench/index',
children: [
{
path: 'index',
component: () => import('@/views/workbench/index'),
name: 'Workbench',
meta: { title: '工作台', icon: 'dashboard', affix: true }
}
]
},
{ {
path: '/redirect', path: '/redirect',
component: Layout, component: Layout,

View File

@ -1,23 +1,7 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px"> <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="76px">
<el-form-item label="单据编号" prop="billNo">
<el-input
v-model="queryParams.billNo"
placeholder="请输入单据编号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="生产订单" prop="moNumber">
<el-input
v-model="queryParams.moNumber"
placeholder="请输入生产订单编号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="生产令号" prop="hbytSclh"> <el-form-item label="生产令号" prop="hbytSclh">
<el-input <el-input
v-model="queryParams.hbytSclh" v-model="queryParams.hbytSclh"
@ -26,6 +10,22 @@
@keyup.enter.native="handleQuery" @keyup.enter.native="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="工作中心" prop="workCenterName">
<el-select
v-model="queryParams.workCenterName"
placeholder="请选择工作中心"
clearable
style="width: 215px"
@change="handleQuery"
>
<el-option label="机一工段" value="机一工段" />
<el-option label="机二工段" value="机二工段" />
<el-option label="铆焊工段" value="铆焊工段" />
<el-option label="装二工段" value="装二工段" />
<el-option label="装一工段" value="装一工段" />
<el-option label="委外中心" value="委外中心" />
</el-select>
</el-form-item>
<el-form-item label="产品编码" prop="productCode"> <el-form-item label="产品编码" prop="productCode">
<el-input <el-input
v-model="queryParams.productCode" v-model="queryParams.productCode"
@ -42,14 +42,14 @@
@keyup.enter.native="handleQuery" @keyup.enter.native="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-row :gutter="10" class="mb8"> <!-- <el-row :gutter="10" class="mb8">
<el-col :span="1.5"> <el-col :span="1.5">
<el-button <el-button
type="primary" type="primary"
@ -93,7 +93,7 @@
>导出</el-button> >导出</el-button>
</el-col> </el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row> </el-row>-->
<div v-if="selectedRows.length" class="selection-bar"> <div v-if="selectedRows.length" class="selection-bar">
<span class="selection-bar__text">已选{{ selectedRows.length }}</span> <span class="selection-bar__text">已选{{ selectedRows.length }}</span>
@ -286,13 +286,22 @@
<el-table-column label="作业人员" min-width="220"> <el-table-column label="作业人员" min-width="220">
<template slot-scope="scope"> <template slot-scope="scope">
<div class="operator-cell"> <div class="operator-cell">
<el-input <el-select
v-model="scope.row.__operatorDraft" v-model="scope.row.__operatorDraft"
size="mini" size="mini"
clearable clearable
placeholder="填写作业人员" filterable
@input="markOperatorDirty(scope.row)" placeholder="搜索选择人员"
/> @change="markOperatorDirty(scope.row)"
style="width: 100%"
>
<el-option
v-for="item in getOperatorOptions(scope.row.workCenter)"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<el-button <el-button
v-if="scope.row.__operatorDirty" v-if="scope.row.__operatorDirty"
type="text" type="text"
@ -306,13 +315,22 @@
<el-table-column label="设备" min-width="220"> <el-table-column label="设备" min-width="220">
<template slot-scope="scope"> <template slot-scope="scope">
<div class="operator-cell"> <div class="operator-cell">
<el-input <el-select
v-model="scope.row.__equipmentDraft" v-model="scope.row.__equipmentDraft"
size="mini" size="mini"
clearable clearable
placeholder="填写设备" filterable
@input="markEquipmentDirty(scope.row)" placeholder="搜索选择设备"
/> @change="(val) => handleEquipmentChange(val, scope.row)"
style="width: 100%"
>
<el-option
v-for="item in getEquipmentOptions(scope.row.workCenter)"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<el-button <el-button
v-if="scope.row.__equipmentDirty" v-if="scope.row.__equipmentDirty"
type="text" type="text"
@ -392,7 +410,7 @@
</template> </template>
<script> <script>
import { listOperationPlanningMain, cascadeListOperationPlanningMain, getOperationPlanningMain, delOperationPlanningMain, addOperationPlanningMain, updateOperationPlanningMain, updateOperationPlanningMainOperators } from "@/api/system/operationPlanningMain"; import { listOperationPlanningMain, cascadeListOperationPlanningMain, getOperationPlanningMain, delOperationPlanningMain, addOperationPlanningMain, updateOperationPlanningMain, updateOperationPlanningMainOperators, getEquipmentOptions } from "@/api/system/operationPlanningMain";
export default { export default {
name: "OperationPlanningMain", name: "OperationPlanningMain",
@ -429,18 +447,53 @@ export default {
queryParams: { queryParams: {
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
fid: undefined,
billNo: undefined,
moNumber: undefined, moNumber: undefined,
hbytSclh: undefined, hbytSclh: undefined,
workCenterName: undefined,
productCode: undefined, productCode: undefined,
productName: undefined, productName: undefined,
proSpecification: undefined,
planStartTime: undefined, planStartTime: undefined,
planFinishTime: undefined, planFinishTime: undefined,
}, },
// //
form: {}, form: {},
staticEquipmentData: {
'机一工段': [
'车铣复合', '双主轴走心机', '数车', '数控外圆磨', '线切割', '电火花',
'立铣', '龙门铣', '摇臂钻', '普车', '锯床', '转序员'
],
'机二工段': [
'立加', '立加 1370', '立加 1177', '数插 5035', '平磨', '立磨', '钳修',
'攻丝', '数控龙门铣', '卧加3519', '龙门2013', '龙门3018', '龙门'
],
'铆焊工段': [
'数控激光下料', '数控火焰下料', '数控切管下料', '铆工', '焊工',
'激光焊工', '机器人焊接', '打磨', '抛丸', '校形'
]
},
staticOperatorData: {
'机一工段': [
'刘彩茹', '王玉', '王瑞康', '王立兴', '张泽旭', '何亚平', '董柯朝', '张晓峰',
'李风存', '贾中恒', '丁世强', '吴崎', '孙小健', '池彦朋', '焦伟涛', '柴鑫豪',
'孙朋园', '仇建龙', '张召飞', '楚世光', '许会强', '王永旺', '王颜强', '秦孟青',
'王海芳', '宋娇娇'
],
'机二工段': [
'柴旭璞', '谷丽博', '裴旭卓', '刘少杰', '常书芳', '田由甲', '曹兴丽', '李子轩',
'张旭泽', '王子过', '李蒲', '杜晓龙', '刘命鑫', '张梧祎', '赵伟', '安松浩',
'刘思雨', '肖泽润', '魏双全', '李兴酉', '王锦翔', '张利明', '李少聪', '李强',
'崔金铎', '于亚乐', '鲁彦芹', '路静茹', '刘翠青', '杨冲冲', '李晓玉', '宋佳佳',
'秦孝宇', '赵铭豪', '李凯峰', '高子娟', '常志涛', '宁若奇', '周建坤', '张泽远',
'王晨光', '张佳祺', '王浩义', '王龙', '陈阳', '崔炫宇', '尚宇清', '金国林',
'李子洋', '康玉朝'
],
'铆焊工段': [
'马明广', '周佳乐', '李晓亮', '董献召', '王春炜', '付佳兴', '柳晓龙', '张雨轩',
'石泽明', '王晓凡', '郝亮', '耿利刚', '魏辰强', '李永文', '吴海庆', '范会雨',
'贾会涛', '吴凯亮', '赵兰争', '董亚军', '王晓军', '代程翼', '魏佳蕊', '齐乐芳',
'康坤兰', '赵锦泽', '游梦皓', '牛立革', '宋金娜', '赵科飞'
]
},
// //
rules: { rules: {
id: [ id: [
@ -497,6 +550,16 @@ export default {
} }
}, },
methods: { methods: {
getEquipmentOptions(workCenter) {
if (!workCenter) return [];
const opts = this.staticEquipmentData[workCenter] || [];
return opts;
},
getOperatorOptions(workCenter) {
if (!workCenter) return [];
const opts = this.staticOperatorData[workCenter] || [];
return opts;
},
initFromRoute() { initFromRoute() {
const q = (this.$route && this.$route.query) || {}; const q = (this.$route && this.$route.query) || {};
const batchNo = q.batchNo || q.hbytSclh || q.productionOrderNo; const batchNo = q.batchNo || q.hbytSclh || q.productionOrderNo;
@ -826,6 +889,8 @@ export default {
__operatorDirty: false, __operatorDirty: false,
__equipmentDraft: equipment, __equipmentDraft: equipment,
__equipmentDirty: false, __equipmentDirty: false,
__equipmentOptions: [],
__equipmentLoading: false,
__operatorSaving: false __operatorSaving: false
}; };
}); });
@ -897,6 +962,10 @@ export default {
row.__equipmentDraft = row.equipment != null ? String(row.equipment) : ''; row.__equipmentDraft = row.equipment != null ? String(row.equipment) : '';
row.__equipmentDirty = false; row.__equipmentDirty = false;
}, },
handleEquipmentChange(val, row) {
if (!row) return;
this.markEquipmentDirty(row);
},
buildOperatorUpdatePayload(rows) { buildOperatorUpdatePayload(rows) {
const order = this.processDialogOrder || {}; const order = this.processDialogOrder || {};
const list = Array.isArray(rows) ? rows : []; const list = Array.isArray(rows) ? rows : [];

View File

@ -0,0 +1,134 @@
<template>
<el-row :gutter="20" class="panel-group">
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col" v-for="(item, index) in list" :key="index">
<div class="card-panel">
<div class="card-panel-content">
<div class="card-panel-text">{{ item.title || '-' }}</div>
<div class="card-panel-num">{{ item.value || 0 }}</div>
<div class="card-panel-sub">
<span class="sub-item" v-if="item.mom != null">
环比: <span :class="item.mom >= 0 ? 'text-green' : 'text-red'">
<i :class="item.mom >= 0 ? 'el-icon-top' : 'el-icon-bottom'" />
{{ Math.abs(item.mom * 100).toFixed(0) }}%
</span>
</span>
<span class="sub-item" v-if="item.yoy != null">
同比: <span :class="item.yoy >= 0 ? 'text-green' : 'text-red'">
<i :class="item.yoy >= 0 ? 'el-icon-top' : 'el-icon-bottom'" />
{{ Math.abs(item.yoy * 100).toFixed(0) }}%
</span>
</span>
</div>
</div>
<div class="card-panel-icon-wrapper" :class="`icon-${item.key}`">
<!-- We can use a generic icon or specific svg here based on key -->
<i v-if="item.key === 'shipProductCount'" class="el-icon-s-data icon-green" />
<i v-else-if="item.key === 'workOrderCount'" class="el-icon-s-marketing icon-blue" />
<i v-else-if="item.key === 'wipCount'" class="el-icon-pie-chart icon-cyan" />
<i v-else-if="item.key === 'ngCount'" class="el-icon-warning-outline icon-red" />
<i v-else class="el-icon-s-data icon-green" />
</div>
<div class="card-panel-arrow">
<i class="el-icon-arrow-right" />
</div>
</div>
</el-col>
</el-row>
</template>
<script>
export default {
name: 'OverviewCards',
props: {
list: {
type: Array,
default: () => []
}
}
}
</script>
<style lang="scss" scoped>
.panel-group {
margin-top: 18px;
.card-panel-col {
margin-bottom: 20px;
}
.card-panel {
height: 120px;
cursor: pointer;
font-size: 12px;
position: relative;
overflow: hidden;
color: #666;
background: #fff;
box-shadow: 0 1px 4px rgba(0,21,41,.08);
border-radius: 6px;
border: 1px solid #ebeef5;
display: flex;
justify-content: space-between;
padding: 20px;
&:hover {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
}
.card-panel-content {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.card-panel-text {
color: #909399;
font-size: 14px;
font-weight: bold;
}
.card-panel-num {
font-size: 26px;
color: #303133;
font-weight: bold;
margin: 8px 0;
}
.card-panel-sub {
color: #909399;
font-size: 12px;
display: flex;
gap: 12px;
.text-green {
color: #16c784;
font-weight: bold;
}
.text-red {
color: #f56c6c;
font-weight: bold;
}
}
.card-panel-icon-wrapper {
padding: 10px;
transition: all 0.38s ease-out;
border-radius: 6px;
font-size: 40px;
.icon-green { color: #16c784; }
.icon-blue { color: #409EFF; }
.icon-cyan { color: #00b4ad; }
.icon-red { color: #f56c6c; }
}
.card-panel-arrow {
position: absolute;
right: 15px;
bottom: 15px;
font-size: 14px;
color: #c0c4cc;
}
}
}
</style>

View File

@ -0,0 +1,115 @@
<template>
<div class="defect-rank">
<div v-for="(item, index) in list" :key="index" class="rank-item">
<div class="rank-badge" :class="`rank-badge-${index + 1}`">{{ index + 1 }}</div>
<div class="rank-label" :title="item.processName">{{ item.processName }}</div>
<div class="rank-bar-container">
<div class="rank-bar" :style="getBarStyle(item.defectRate, index)" />
</div>
<div class="rank-value">{{ (item.defectRate * 100).toFixed(0) }}%</div>
</div>
<div v-if="!list || list.length === 0" class="empty-text">暂无数据</div>
</div>
</template>
<script>
export default {
name: 'ProcessDefectRank',
props: {
list: {
type: Array,
default: () => []
}
},
methods: {
getBarStyle(rate, index) {
const colors = [
'#f472b6', '#fb923c', '#2dd4bf', '#3b82f6', '#3b82f6',
'#3b82f6', '#3b82f6', '#3b82f6', '#3b82f6', '#3b82f6'
]
const color = colors[index] || '#3b82f6'
// Scale bar relative to max or absolute. We assume absolute rate is 0~1.
// But if max is 40%, we might want to scale it relative to the max so it fills the space better.
// Let's use relative scaling.
const maxRate = this.list.length ? Math.max(...this.list.map(i => i.defectRate)) : 1;
const width = maxRate > 0 ? (rate / maxRate) * 100 : 0;
return {
width: `${width}%`,
backgroundColor: color,
borderRadius: '4px'
}
}
}
}
</script>
<style lang="scss" scoped>
.defect-rank {
padding: 10px 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.rank-item {
display: flex;
align-items: center;
font-size: 14px;
}
.rank-badge {
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 12px;
font-weight: bold;
margin-right: 12px;
background-color: #e5e7eb; // default
color: #6b7280;
&-1 { background-color: #fce7f3; color: #db2777; }
&-2 { background-color: #ffedd5; color: #ea580c; }
&-3 { background-color: #ccfbf1; color: #0d9488; }
}
.rank-label {
width: 80px;
color: #4b5563;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 12px;
}
.rank-bar-container {
flex: 1;
height: 8px;
background-color: #f3f4f6;
border-radius: 4px;
overflow: hidden;
margin-right: 12px;
}
.rank-bar {
height: 100%;
transition: width 0.5s ease-out;
}
.rank-value {
width: 40px;
text-align: right;
color: #6b7280;
font-weight: 500;
}
.empty-text {
text-align: center;
color: #909399;
font-size: 14px;
padding: 20px 0;
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<el-table :data="list" :header-cell-style="{ background: '#f8f9fa', color: '#333', fontWeight: 'bold', borderBottom: 'none' }" style="width: 100%" class="custom-table">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="customerName" label="名称" min-width="180" align="center" />
<el-table-column label="同比" width="120" align="center">
<template slot-scope="{ row }">
<span class="trend-cell" :class="row.yoy >= 0 ? 'text-green' : 'text-red'">
<i :class="row.yoy >= 0 ? 'el-icon-top' : 'el-icon-bottom'" />
{{ Math.abs(row.yoy * 100).toFixed(0) }}%
</span>
</template>
</el-table-column>
<el-table-column prop="orderQty" label="订单量" width="100" align="center" />
</el-table>
</template>
<script>
export default {
name: 'ShipOrderData',
props: {
list: {
type: Array,
default: () => []
}
}
}
</script>
<style lang="scss" scoped>
.custom-table {
&::before {
display: none;
}
::v-deep .el-table__row > td {
border-bottom: 1px solid #f0f0f0;
padding: 12px 0;
}
}
.trend-cell {
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 4px;
&.text-green {
color: #52c41a;
}
&.text-red {
color: #f5222d;
}
}
</style>

View File

@ -0,0 +1,108 @@
<template>
<div :class="className" :style="{height:height,width:width}" />
</template>
<script>
import * as echarts from 'echarts'
import resize from '@/views/dashboard/mixins/resize'
export default {
mixins: [resize],
props: {
className: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '300px'
},
chartData: {
type: Object,
default: () => ({ xAxis: [], series: [] })
}
},
data() {
return {
chart: null
}
},
watch: {
chartData: {
deep: true,
handler(val) {
this.setOptions(val)
}
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
})
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.setOptions(this.chartData)
},
setOptions({ xAxis, xaxis, series } = {}) {
if (!this.chart) return
// Handle both xAxis (camelCase) and xaxis (lowercase) from backend
const finalXAxis = xAxis || xaxis || []
const chartSeries = (series || []).map(s => ({
...s,
type: 'bar',
barWidth: '40%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#60a5fa' },
{ offset: 1, color: '#2563eb' }
]),
borderRadius: [4, 4, 0, 0]
}
}))
this.chart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
},
grid: {
top: 30,
left: '2%',
right: '2%',
bottom: '3%',
containLabel: true
},
xAxis: [{
type: 'category',
data: finalXAxis,
axisTick: { alignWithLabel: true },
axisLabel: { color: '#909399' },
axisLine: { lineStyle: { color: '#dcdfe6' } }
}],
yAxis: [{
type: 'value',
axisTick: { show: false },
axisLabel: { color: '#909399' },
splitLine: { lineStyle: { type: 'dashed', color: '#ebeef5' } }
}],
series: chartSeries
}, true) // true means not merge, replace entirely
}
}
}
</script>

View File

@ -0,0 +1,101 @@
<template>
<el-table :data="list" :header-cell-style="{ background: '#f8f9fa', color: '#333', fontWeight: 'bold', borderBottom: 'none' }" style="width: 100%" class="custom-table">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="workOrderNo" label="工单号" min-width="140" align="center" />
<el-table-column label="状态" width="100" align="center">
<template slot-scope="{ row }">
<span class="status-tag" :class="row.status === '已完工' ? 'status-done' : 'status-doing'">
{{ row.status }}
</span>
</template>
</el-table-column>
<el-table-column label="进度" min-width="180" align="center">
<template slot-scope="{ row }">
<div class="progress-cell">
<div class="progress-bar-bg">
<div class="progress-bar-fill" :style="{ width: row.progress + '%', backgroundColor: row.progress >= 100 ? '#52c41a' : '#00bcd4' }"></div>
</div>
<span class="progress-text" v-if="row.progress < 100">{{ row.progress }}%</span>
<i class="el-icon-success progress-icon" v-else></i>
</div>
</template>
</el-table-column>
<el-table-column prop="totalQty" label="总数量" width="100" align="center" />
</el-table>
</template>
<script>
export default {
name: 'WorkOrderProgress',
props: {
list: {
type: Array,
default: () => []
}
}
}
</script>
<style lang="scss" scoped>
.custom-table {
&::before {
display: none;
}
::v-deep .el-table__row > td {
border-bottom: 1px solid #f0f0f0;
padding: 12px 0;
}
}
.status-tag {
display: inline-block;
padding: 2px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
&.status-done {
background-color: #f6ffed;
color: #52c41a;
}
&.status-doing {
background-color: #e6fffb;
color: #13c2c2;
}
}
.progress-cell {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
.progress-bar-bg {
width: 100px;
height: 6px;
background-color: #f0f0f0;
border-radius: 3px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
}
.progress-text {
font-size: 12px;
color: #8c8c8c;
width: 30px;
text-align: left;
}
.progress-icon {
color: #52c41a;
font-size: 14px;
width: 30px;
text-align: left;
}
}
</style>

View File

@ -0,0 +1,253 @@
<template>
<div class="app-container workbench-container">
<!-- 顶部概览卡片 -->
<overview-cards :list="overviewList" />
<el-row :gutter="20" class="workbench-row">
<!-- 左侧工单数量柱状图 -->
<el-col :xs="24" :sm="24" :lg="16">
<div class="workbench-panel">
<div class="workbench-panel-header">
<span class="workbench-panel-title">工单数量</span>
</div>
<div class="workbench-panel-body">
<work-order-bar :chart-data="workOrderData" height="400px" />
</div>
</div>
</el-col>
<!-- 右侧工序不良率排名 -->
<el-col :xs="24" :sm="24" :lg="8">
<div class="workbench-panel">
<div class="workbench-panel-header">
<span class="workbench-panel-title">工序不良率排名</span>
</div>
<div class="workbench-panel-body">
<process-defect-rank :list="defectRankList" />
</div>
</div>
</el-col>
</el-row>
<el-row :gutter="20" class="workbench-row">
<!-- 左侧工单实时进度 -->
<el-col :xs="24" :sm="24" :lg="14">
<div class="workbench-panel">
<div class="workbench-panel-header">
<span class="workbench-panel-title">工单实时进度</span>
</div>
<div class="workbench-panel-body">
<work-order-progress :list="workOrderProgressList" />
</div>
</div>
</el-col>
<!-- 右侧出货订单数据 -->
<el-col :xs="24" :sm="24" :lg="10">
<div class="workbench-panel">
<div class="workbench-panel-header" style="display: flex; justify-content: space-between; align-items: center;">
<span class="workbench-panel-title">出货订单数据</span>
<el-radio-group v-model="shipOrderTimeRange" size="mini" @change="fetchShipOrderData">
<el-radio-button label="lastMonth">上月</el-radio-button>
<el-radio-button label="thisMonth">本月</el-radio-button>
<el-radio-button label="halfYear">近6个月</el-radio-button>
</el-radio-group>
</div>
<div class="workbench-panel-body">
<ship-order-data :list="shipOrderList" />
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import OverviewCards from './components/OverviewCards'
import WorkOrderBar from './components/WorkOrderBar'
import ProcessDefectRank from './components/ProcessDefectRank'
import WorkOrderProgress from './components/WorkOrderProgress'
import ShipOrderData from './components/ShipOrderData'
import {
getOverview,
getWorkOrderBar,
getProcessDefectRank,
getWorkOrderProgress,
getShipOrderData
} from '@/api/system/workbench'
export default {
name: 'Workbench',
components: {
OverviewCards,
WorkOrderBar,
ProcessDefectRank,
WorkOrderProgress,
ShipOrderData
},
data() {
return {
overviewList: [
{ key: 'shipProductCount', title: '出货产品数', value: 0, mom: 0, yoy: 0 },
{ key: 'workOrderCount', title: '生产工单数', value: 0, mom: 0, yoy: 0 },
{ key: 'wipCount', title: '在制品', value: 0, mom: 0, yoy: 0 },
{ key: 'ngCount', title: '不良品数', value: 0, mom: 0, yoy: 0 }
],
workOrderData: { xAxis: [], series: [] },
defectRankList: [],
workOrderProgressList: [],
shipOrderTimeRange: 'lastMonth',
shipOrderList: []
}
},
created() {
this.fetchData()
},
methods: {
fetchData() {
//
getOverview().then(res => {
if (res && res.data && res.data.cards) {
this.overviewList = res.data.cards
} else {
this.mockOverview()
}
}).catch(() => {
this.mockOverview()
})
//
getWorkOrderBar().then(res => {
if (res && res.data && (res.data.xAxis || res.data.xaxis)) {
this.workOrderData = res.data
} else {
this.mockWorkOrder()
}
}).catch(() => {
this.mockWorkOrder()
})
//
getProcessDefectRank().then(res => {
if (res && res.data && res.data.items) {
this.defectRankList = res.data.items
} else {
this.mockDefectRank()
}
}).catch(() => {
this.mockDefectRank()
})
//
getWorkOrderProgress().then(res => {
if (res && res.data && res.data.items) {
this.workOrderProgressList = res.data.items
} else {
this.mockWorkOrderProgress()
}
}).catch(() => {
this.mockWorkOrderProgress()
})
//
this.fetchShipOrderData()
},
fetchShipOrderData() {
getShipOrderData({ range: this.shipOrderTimeRange }).then(res => {
if (res && res.data && res.data.items) {
this.shipOrderList = res.data.items
} else {
this.mockShipOrderData()
}
}).catch(() => {
this.mockShipOrderData()
})
},
mockOverview() {
this.overviewList = [
{ key: 'shipProductCount', title: '出货产品数', value: 7314, mom: 0.68, yoy: -0.05 },
{ key: 'workOrderCount', title: '生产工单数', value: 7326, mom: 0.58, yoy: -0.01 },
{ key: 'wipCount', title: '在制品', value: 5399, mom: 0.56, yoy: 0.03 },
{ key: 'ngCount', title: '不良品数', value: 85, mom: -0.45, yoy: -0.05 }
]
},
mockWorkOrder() {
this.workOrderData = {
xAxis: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
series: [{
name: "工单数量",
data: [9000, 8000, 12000, 9500, 10000, 9000, 14000, 6000, 9000, 13000, 10000, 9000]
}]
}
},
mockDefectRank() {
this.defectRankList = [
{ rank: 1, processName: "切割", defectRate: 0.40 },
{ rank: 2, processName: "焊接", defectRate: 0.38 },
{ rank: 3, processName: "委外烤漆", defectRate: 0.37 },
{ rank: 4, processName: "烧结炉", defectRate: 0.36 },
{ rank: 5, processName: "注水", defectRate: 0.32 },
{ rank: 6, processName: "冷却", defectRate: 0.28 },
{ rank: 7, processName: "散热", defectRate: 0.25 },
{ rank: 8, processName: "扣排线", defectRate: 0.22 },
{ rank: 9, processName: "上料", defectRate: 0.20 },
{ rank: 10, processName: "效能测试", defectRate: 0.18 }
]
},
mockWorkOrderProgress() {
this.workOrderProgressList = [
{ workOrderNo: 'MO202112160001', status: '已完工', progress: 100, totalQty: 679 },
{ workOrderNo: 'MO202112160002', status: '已开工', progress: 80, totalQty: 503 },
{ workOrderNo: 'MO202112160003', status: '已完工', progress: 100, totalQty: 7 },
{ workOrderNo: 'MO202112160004', status: '已开工', progress: 67, totalQty: 233 },
{ workOrderNo: 'MO202112160005', status: '已开工', progress: 55, totalQty: 983 }
]
},
mockShipOrderData() {
this.shipOrderList = [
{ customerName: '松下电器(中国)有限公司', yoy: 0.61, orderQty: 3261 },
{ customerName: '苹果科技有限公司', yoy: -0.38, orderQty: 3664 },
{ customerName: '华为技术有限公司', yoy: 0.47, orderQty: 2735 },
{ customerName: '小米科技有限责任公司', yoy: 0.05, orderQty: 3718 },
{ customerName: '联想集团', yoy: -0.62, orderQty: 3299 },
{ customerName: '广东移动通信有限公司', yoy: -0.18, orderQty: 7058 }
]
}
}
}
</script>
<style lang="scss" scoped>
.workbench-container {
background-color: #f0f2f5;
min-height: calc(100vh - 84px);
padding: 20px;
}
.workbench-row {
margin-top: 20px;
}
.workbench-panel {
background: #fff;
border-radius: 6px;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
margin-bottom: 20px;
.workbench-panel-header {
padding: 16px 20px;
border-bottom: 1px solid #ebeef5;
.workbench-panel-title {
font-size: 16px;
font-weight: bold;
color: #303133;
}
}
.workbench-panel-body {
padding: 20px;
}
}
</style>