工序计划。作业人员推送
This commit is contained in:
parent
81cc61a6c6
commit
dcd221f21b
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
node_modules
|
||||
|
||||
46
src/api/system/workbench.js
Normal file
46
src/api/system/workbench.js
Normal 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
|
||||
})
|
||||
}
|
||||
@ -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',
|
||||
component: Layout,
|
||||
|
||||
@ -1,23 +1,7 @@
|
||||
<template>
|
||||
<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-input
|
||||
v-model="queryParams.hbytSclh"
|
||||
@ -26,6 +10,22 @@
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</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-input
|
||||
v-model="queryParams.productCode"
|
||||
@ -49,7 +49,7 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<!-- <el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="primary"
|
||||
@ -93,7 +93,7 @@
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</el-row>-->
|
||||
|
||||
<div v-if="selectedRows.length" class="selection-bar">
|
||||
<span class="selection-bar__text">已选{{ selectedRows.length }}项</span>
|
||||
@ -286,13 +286,22 @@
|
||||
<el-table-column label="作业人员" min-width="220">
|
||||
<template slot-scope="scope">
|
||||
<div class="operator-cell">
|
||||
<el-input
|
||||
<el-select
|
||||
v-model="scope.row.__operatorDraft"
|
||||
size="mini"
|
||||
clearable
|
||||
placeholder="填写作业人员"
|
||||
@input="markOperatorDirty(scope.row)"
|
||||
filterable
|
||||
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
|
||||
v-if="scope.row.__operatorDirty"
|
||||
type="text"
|
||||
@ -306,13 +315,22 @@
|
||||
<el-table-column label="设备" min-width="220">
|
||||
<template slot-scope="scope">
|
||||
<div class="operator-cell">
|
||||
<el-input
|
||||
<el-select
|
||||
v-model="scope.row.__equipmentDraft"
|
||||
size="mini"
|
||||
clearable
|
||||
placeholder="填写设备"
|
||||
@input="markEquipmentDirty(scope.row)"
|
||||
filterable
|
||||
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
|
||||
v-if="scope.row.__equipmentDirty"
|
||||
type="text"
|
||||
@ -392,7 +410,7 @@
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
name: "OperationPlanningMain",
|
||||
@ -429,18 +447,53 @@ export default {
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
fid: undefined,
|
||||
billNo: undefined,
|
||||
moNumber: undefined,
|
||||
hbytSclh: undefined,
|
||||
workCenterName: undefined,
|
||||
productCode: undefined,
|
||||
productName: undefined,
|
||||
proSpecification: undefined,
|
||||
planStartTime: undefined,
|
||||
planFinishTime: undefined,
|
||||
},
|
||||
// 表单参数
|
||||
form: {},
|
||||
staticEquipmentData: {
|
||||
'机一工段': [
|
||||
'车铣复合', '双主轴走心机', '数车', '数控外圆磨', '线切割', '电火花',
|
||||
'立铣', '龙门铣', '摇臂钻', '普车', '锯床', '转序员'
|
||||
],
|
||||
'机二工段': [
|
||||
'立加', '立加 1370', '立加 1177', '数插 5035', '平磨', '立磨', '钳修',
|
||||
'攻丝', '数控龙门铣', '卧加3519', '龙门2013', '龙门3018', '龙门'
|
||||
],
|
||||
'铆焊工段': [
|
||||
'数控激光下料', '数控火焰下料', '数控切管下料', '铆工', '焊工',
|
||||
'激光焊工', '机器人焊接', '打磨', '抛丸', '校形'
|
||||
]
|
||||
},
|
||||
staticOperatorData: {
|
||||
'机一工段': [
|
||||
'刘彩茹', '王玉', '王瑞康', '王立兴', '张泽旭', '何亚平', '董柯朝', '张晓峰',
|
||||
'李风存', '贾中恒', '丁世强', '吴崎', '孙小健', '池彦朋', '焦伟涛', '柴鑫豪',
|
||||
'孙朋园', '仇建龙', '张召飞', '楚世光', '许会强', '王永旺', '王颜强', '秦孟青',
|
||||
'王海芳', '宋娇娇'
|
||||
],
|
||||
'机二工段': [
|
||||
'柴旭璞', '谷丽博', '裴旭卓', '刘少杰', '常书芳', '田由甲', '曹兴丽', '李子轩',
|
||||
'张旭泽', '王子过', '李蒲', '杜晓龙', '刘命鑫', '张梧祎', '赵伟', '安松浩',
|
||||
'刘思雨', '肖泽润', '魏双全', '李兴酉', '王锦翔', '张利明', '李少聪', '李强',
|
||||
'崔金铎', '于亚乐', '鲁彦芹', '路静茹', '刘翠青', '杨冲冲', '李晓玉', '宋佳佳',
|
||||
'秦孝宇', '赵铭豪', '李凯峰', '高子娟', '常志涛', '宁若奇', '周建坤', '张泽远',
|
||||
'王晨光', '张佳祺', '王浩义', '王龙', '陈阳', '崔炫宇', '尚宇清', '金国林',
|
||||
'李子洋', '康玉朝'
|
||||
],
|
||||
'铆焊工段': [
|
||||
'马明广', '周佳乐', '李晓亮', '董献召', '王春炜', '付佳兴', '柳晓龙', '张雨轩',
|
||||
'石泽明', '王晓凡', '郝亮', '耿利刚', '魏辰强', '李永文', '吴海庆', '范会雨',
|
||||
'贾会涛', '吴凯亮', '赵兰争', '董亚军', '王晓军', '代程翼', '魏佳蕊', '齐乐芳',
|
||||
'康坤兰', '赵锦泽', '游梦皓', '牛立革', '宋金娜', '赵科飞'
|
||||
]
|
||||
},
|
||||
// 表单校验
|
||||
rules: {
|
||||
id: [
|
||||
@ -497,6 +550,16 @@ export default {
|
||||
}
|
||||
},
|
||||
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() {
|
||||
const q = (this.$route && this.$route.query) || {};
|
||||
const batchNo = q.batchNo || q.hbytSclh || q.productionOrderNo;
|
||||
@ -826,6 +889,8 @@ export default {
|
||||
__operatorDirty: false,
|
||||
__equipmentDraft: equipment,
|
||||
__equipmentDirty: false,
|
||||
__equipmentOptions: [],
|
||||
__equipmentLoading: false,
|
||||
__operatorSaving: false
|
||||
};
|
||||
});
|
||||
@ -897,6 +962,10 @@ export default {
|
||||
row.__equipmentDraft = row.equipment != null ? String(row.equipment) : '';
|
||||
row.__equipmentDirty = false;
|
||||
},
|
||||
handleEquipmentChange(val, row) {
|
||||
if (!row) return;
|
||||
this.markEquipmentDirty(row);
|
||||
},
|
||||
buildOperatorUpdatePayload(rows) {
|
||||
const order = this.processDialogOrder || {};
|
||||
const list = Array.isArray(rows) ? rows : [];
|
||||
|
||||
134
src/views/workbench/components/OverviewCards.vue
Normal file
134
src/views/workbench/components/OverviewCards.vue
Normal 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>
|
||||
115
src/views/workbench/components/ProcessDefectRank.vue
Normal file
115
src/views/workbench/components/ProcessDefectRank.vue
Normal 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>
|
||||
54
src/views/workbench/components/ShipOrderData.vue
Normal file
54
src/views/workbench/components/ShipOrderData.vue
Normal 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>
|
||||
108
src/views/workbench/components/WorkOrderBar.vue
Normal file
108
src/views/workbench/components/WorkOrderBar.vue
Normal 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>
|
||||
101
src/views/workbench/components/WorkOrderProgress.vue
Normal file
101
src/views/workbench/components/WorkOrderProgress.vue
Normal 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>
|
||||
253
src/views/workbench/index.vue
Normal file
253
src/views/workbench/index.vue
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user