Commit 5d8b76d7 by nningxx

Merge branch 'master' of https://git.rokedata.com/dws/dwsproject

parents 3d6611a7 15f1b20f
......@@ -160,7 +160,7 @@
let vue = new Vue({
el: "#app",
delimiters: ["[[", "]]"], // 替换原本vue的[[ key ]]取值方式(与odoo使用的jinja2冲突问题)
data: function () {
data() {
return {
dwsURL: "", // 基地址
baseURL: "https://dws-platform.xbg.rokeris.com/dev-api", // 基地址
......@@ -175,24 +175,24 @@
equipmentMaintenanceInfos: [], // 设备维护信息
}
},
created: function () {
created() {
// 截取url后面的参数
this.factoryCode = this.getUrlSearch("factory_code") || ""
// 初始化当前时间
this.initCurrentTimeFn()
},
mounted: async function () {
this.$nextTick(function () {
async mounted() {
this.$nextTick(() => {
document.getElementById("bodyId").style.display = "block"
})
// 初始化数据 - 实际使用时取消这行的注释
await this.initData()
// 设置定时刷新(每分钟刷新一次,静默模式)
this.refreshInterval = setInterval(function () {
this.refreshInterval = setInterval(() => {
this.initData(true) // 传入true表示静默刷新,不显示加载提示
}.bind(this), 60000)
}, 60000)
},
beforeDestroy: function () {
beforeDestroy() {
// 清除定时器
if (this.refreshInterval) clearInterval(this.refreshInterval)
if (this.timer) clearInterval(this.timer)
......@@ -241,8 +241,8 @@
this.isFullscreen = false
},
// 初始化当前时间
initCurrentTimeFn: function () {
this.timer = setInterval(function () {
initCurrentTimeFn() {
this.timer = setInterval(() => {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, "0")
......@@ -251,10 +251,10 @@
const minutes = String(now.getMinutes()).padStart(2, "0")
const seconds = String(now.getSeconds()).padStart(2, "0")
this.currentTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}.bind(this), 1000)
}, 1000)
},
// 通过网址跳转过来的页面,截取后面的参数
getUrlSearch: function (name) {
getUrlSearch(name) {
// 未传参,返回空
if (!name) return ""
// 查询参数:先通过search取值,如果取不到就通过hash来取
......@@ -272,8 +272,7 @@
return r[2]
},
// 初始化数据
initData: async function (silent) {
if (silent === undefined) silent = false
async initData(silent = false) {
try {
// 只有在非静默模式下才显示加载提示
if (!silent) this.loading = true
......@@ -292,7 +291,7 @@
}
},
// 获取设备计划运行时间
getDevicePlanTime: async function () {
async getDevicePlanTime() {
try {
// 发送请求获取计划运行时间
const response = await axios({
......@@ -322,7 +321,7 @@
}
},
// 获取所有已绑定设备数据
getAllEquipmentData: async function () {
async getAllEquipmentData() {
try {
// 发送请求获取所有已绑定设备数据
const response = await axios({
......@@ -347,7 +346,7 @@
}
},
// 获取设备状态列表
getDeviceStateList: async function (planTimeList) {
async getDeviceStateList(planTimeList) {
try {
// 使用CORS代理
// 发送请求获取设备状态
......@@ -375,7 +374,7 @@
}
},
// 获取factoryCode
getFactoryCode: async function (planTimeList) {
async getFactoryCode(planTimeList) {
try {
// 使用CORS代理
// 发送请求获取设备状态
......@@ -400,7 +399,7 @@
}
},
// 获取设备维护信息
getEquipmentMaintenanceInfoApi: async function () {
async getEquipmentMaintenanceInfoApi() {
try {
const response = await axios({
method: "post",
......@@ -408,10 +407,10 @@
data: {},
headers: { "Content-Type": "application/json" },
})
if (response && response.data && response.data.result && response.data.result.state === "success") {
if (response?.data?.result?.state === "success") {
this.equipmentMaintenanceInfos = response.data.result.equipment_maintenance_list || []
} else {
const errorMsg = response && response.data && response.data.result && response.data.result.msgs || "获取设备维护信息失败!"
const errorMsg = response?.data?.result?.msgs || "获取设备维护信息失败!"
throw new Error(errorMsg)
}
} catch (error) {
......@@ -421,8 +420,8 @@
// 处理设备状态数据
processDeviceStateData: function (deviceStateData) {
var self = this
self.allEquipmentData.forEach(function (item) {
self.equipmentMaintenanceInfos.forEach(function (it) {
self.allEquipmentData.forEach(item => {
self.equipmentMaintenanceInfos.forEach(it => {
if (it.equipment_code == item.code) {
item['check_info'] = it.check_info
item['change_info'] = it.change_info
......@@ -484,7 +483,7 @@
aging = self.formatDecimal(device.pulse_count / Math.floor(Number(device.run_seconds) / 3600), 2)
}
// 在所有设备列表中查找匹配的设备
if (self.allEquipmentData && self.allEquipmentData.length) {
if (self.allEquipmentData && self.allEquipmentData.length > 0) {
var matchedDevice = self.allEquipmentData.find(function (equip) {
return equip.code === device.code
})
......@@ -520,10 +519,11 @@
finish_qty: finishQty
}
})
this.deviceList = this.deviceList.filter(function (device) { return device })
this.deviceList = this.deviceList.filter((device) => device)
},
// 处理小数方法
formatDecimal: function (value, digit) {
formatDecimal(value, digit) {
if (value === Infinity) return 0
// 没设置位数默认返回原来的值
if (!digit) return value
// 处理 null/undefined/空字符串
......@@ -537,12 +537,12 @@
// 截取小数位数
const decimalPart = parsedNum.toString().split('.')[1]
// 检查小数长度是否大于要求位数,小于或等于直接返回
if (decimalPart && decimalPart.length <= digit) return parsedNum
if (decimalPart.length <= digit) return parsedNum
// 保留要求位数(处理浮点误差)
return parseFloat((Math.round((parsedNum + Number.EPSILON) * 100) / 100).toFixed(digit))
},
// 处理时间方法
formatTime: function (seconds) {
formatTime(seconds) {
if (seconds < 60) {
return `0min` // 不足 1 分钟显示 0min
} else if (seconds < 3600) {
......@@ -558,7 +558,7 @@
}
},
// 获取卡片的CSS类名
getCardClass: function (status) {
getCardClass(status) {
switch (status) {
case "running":
return "card-running"
......@@ -572,7 +572,7 @@
}
},
// 获取边框的CSS类名
getBorderClass: function (status) {
getBorderClass(status) {
switch (status) {
case "running":
return "border-running";
......@@ -586,7 +586,7 @@
}
},
// 获取设备状态对应的CSS类名
getStatusClass: function (status) {
getStatusClass(status) {
switch (status) {
case "running":
return "status-running";
......@@ -600,7 +600,7 @@
}
},
// 获取波浪效果的类名
getWaveClass: function (status) {
getWaveClass(status) {
switch (status) {
case "running":
return "wave-running";
......@@ -614,7 +614,7 @@
}
},
// 获取波浪高度
getWaveHeight: function (status, percentage) {
getWaveHeight(status, percentage) {
// 将百分比限制在10%-100%之间,确保即使是低百分比也有一定的水位可见
let height = percentage;
......@@ -630,7 +630,7 @@
},
// 获取设备状态对应的文本
getStatusText: function (status) {
getStatusText(status) {
switch (status) {
case "running":
return "运行中";
......@@ -644,7 +644,7 @@
}
},
// 获取设备维护信息状态颜色
getMaintenanceColor: function (status, type) {
getMaintenanceColor(status, type) {
if (type && type == '维修' && status == 'in_progress') {
return "rgba(230, 188, 92, 0.7)"
} else if (status == 'no_task' || status == 'in_progress' || status == 'finished') {
......@@ -656,7 +656,7 @@
}
},
// 获取设备维护信息状态颜色
getMaintenanceText: function (status, type) {
getMaintenanceText(status, type) {
if (type == '点检' || type == '保养') {
if (status == 'no_task') return "无任务"
if (status == 'not_started') return "未开始"
......@@ -674,11 +674,10 @@
return "未知"
},
// 处理小数位数方法
toFixedHandle: function (value, num) {
if (num === undefined) num = 4
toFixedHandle(value, num = 4) {
if (value) {
let strValue = String(value);
if (strValue.split(".") && strValue.split(".").length > 1 || strValue.split(".")[1] && strValue.split(".")[1].length > num) {
if (strValue.split(".").length > 1 || strValue.split(".")[1]?.length > num) {
strValue = Number(strValue).toFixed(num);
}
return Number(strValue);
......@@ -688,7 +687,7 @@
},
// 计算高度百分比
calculateHeight: function (hours) {
calculateHeight(hours) {
// 24小时对应整个高度(100%)
// 每4小时对应一个刻度区间,总共6个区间
// 计算每小时占的百分比:100% / 24 ≈ 4.167%
......@@ -700,13 +699,13 @@
},
// 显示完整标题
showFullTitle: function (event, device) {
showFullTitle(event, device) {
const fullTitle = device.name + " | " + device.code;
event.target.setAttribute("title", fullTitle);
},
// 获取ERR文字的CSS类名
getErrClass: function (status) {
getErrClass(status) {
if (status === "error") {
return "err-error";
}
......@@ -714,7 +713,7 @@
},
// 获取OFF文字的CSS类名
getOffClass: function (status) {
getOffClass(status) {
if (status === "off") {
return "off-status";
}
......@@ -955,13 +954,14 @@
/* 让每个区域平均分配宽度 */
position: relative;
height: 100%;
gap: 5px;
}
/* 新增:图标和文字的水平容器 */
.maintenance-header {
display: flex;
align-items: center;
margin-bottom: 5px;
gap: 5px;
}
/* 维护状态竖向虚线分割 - 改为竖向分割线,从上到下,实现密闭效果 */
......@@ -1030,7 +1030,6 @@
margin-bottom: 0;
/* 移除底部边距 */
text-align: center;
margin-left: 5px;
}
.maintenance-status-badge {
......@@ -1065,6 +1064,7 @@
display: flex;
justify-content: space-between;
width: 100%;
gap: 10px;
padding: 0 10px;
/* 增加间距 */
}
......@@ -1085,7 +1085,6 @@
}
.stat-item:nth-child(2) {
margin: 0 10px;
background: linear-gradient(135deg, rgba(46, 204, 113, 0.6), rgba(39, 174, 96, 0.6));
}
......@@ -1141,6 +1140,7 @@
.left-status {
display: flex;
flex-direction: column;
gap: 8px;
flex: 0 0 auto;
/* 改为固定宽度 */
width: 80px;
......@@ -1151,6 +1151,7 @@
.right-status {
display: flex;
flex-direction: column;
gap: 8px;
flex: 0 0 auto;
/* 改为固定宽度 */
width: 80px;
......@@ -1175,14 +1176,7 @@
display: flex;
align-items: center;
justify-content: flex-start;
}
.time-item-side:nth-child(1) {
margin-bottom: 5px;
}
.time-item-side:nth-child(2) {
margin-top: 5px;
gap: 6px;
}
/* 右侧状态项布局调整 */
......@@ -1205,12 +1199,6 @@
font-size: 10px;
color: rgba(255, 255, 255, 0.9);
font-weight: 500;
margin-left: 6px;
}
.right-status .time-value {
margin-left: 0;
margin-right: 6px;
}
/* 移动端虚线样式调整 */
......@@ -1466,6 +1454,7 @@
.maintenance-header {
margin-bottom: 6px;
/* 减小间距 */
gap: 4px;
}
.maintenance-icon {
......
......@@ -2,12 +2,13 @@
from odoo import http
from odoo.http import request
from odoo.addons.roke_mes_three_colour_light.controller.main import RokeMesThreeColourLight
from odoo.addons.roke_workstation_api.controllers.abnormal import RokeWorkstationAbnormal
import os
import math
from datetime import datetime, time
from jinja2 import Environment, FileSystemLoader
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
templateloader = FileSystemLoader(searchpath=BASE_DIR + "/static/src/view")
templateloader = FileSystemLoader(searchpath=BASE_DIR + "/static")
env = Environment(loader=templateloader)
class ThtProject(http.Controller):
......@@ -54,7 +55,7 @@ class RokeMesThreeColourLightExt(RokeMesThreeColourLight):
"override": True # 添加额外字段
}
}
template = env.get_template('equipment_status.html')
template = env.get_template('src/view/equipment_status.html')
return template.render(data)
@http.route('/roke/equipment/search', type='json', methods=['POST', 'OPTIONS'], auth="none", csrf=False,
......@@ -113,3 +114,11 @@ class RokeMesThreeColourLightExt(RokeMesThreeColourLight):
'code': 200,
'data': equipment_list
}
class RokeWorkstationAbnormalExt(RokeWorkstationAbnormal):
@http.route('/roke/abnormal_alarm/census', type='http', auth='public', csrf=False, cors="*")
def roke_index_demo_module(self, **kwargs):
template = env.get_template('html/abnormal_alarm/view/index.html')
html = template.render({})
return html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>安灯数据</title>
<meta content="width=device-width,initial-scale=1.0, maximum-scale=1.0,user-scalable=0" name="viewport" />
<!-- /roke_workstation_api/static/html/abnormal_alarm -->
<link rel="stylesheet" href="/roke_workstation_api/static/html/abnormal_alarm/element-ui/index.css" />
<link rel="stylesheet" href="/roke_workstation_api/static/html/abnormal_alarm/css/dark_element_ui.css" />
<script src="/roke_workstation_api/static/html/abnormal_alarm/js/vue.js"></script>
<script src="/roke_workstation_api/static/html/abnormal_alarm/js/axios.min.js"></script>
<script src="/roke_workstation_api/static/html/abnormal_alarm/element-ui/index.js"></script>
<script src="/roke_workstation_api/static/html/abnormal_alarm/js/echarts.min.js"></script>
</head>
<body id="bodyId" style="display: none">
<div id="app" v-loading.body.fullscreen.lock="loading">
<!-- 今日工位呼叫明细 -->
<div class="panelCardBox">
<div class="panelHeaderBox">今日工位呼叫明细</div>
<div ref="panelTableRef" class="panelTableBox" @mouseenter="mouseEnterHandle" @mouseleave="mouseLeaveHandle"
v-show="stationCallRecordList.length">
<div class="panelTableHeader">
<div class="roll_line">
<div>产线</div>
<div>工位</div>
<div>呼叫类型</div>
<div>呼叫时间</div>
<div>处理时间</div>
</div>
</div>
<div ref="vlist" class="vabsolute_content">
<div class="fragment_content"></div>
<div class="fragment_content"></div>
</div>
</div>
<div class="emptyBox" v-show="!stationCallRecordList.length">
<el-empty image="/roke_workstation_api/static/html/abnormal_alarm/images/empty.png" description="暂无数据"
:image-size="100"></el-empty>
</div>
</div>
<!-- 今日产品缺陷走势图 -->
<div class="panelCardBox">
<div class="panelHeaderBox">
<span>今日产品缺陷走势图</span>
<span class="unitBox">单位:个</span>
<el-select v-model="selClassesValue" size="mini" placeholder="选择班次"
@change="selChangeHandle($event,'今日产品缺陷走势图')">
<el-option v-for="item in classesList" :key="item.id" :label="item.name" :value="item.id">
</el-option>
</el-select>
</div>
<div ref="defectTrendRef" style="width: 100%; height: calc(100% - 28px)"></div>
</div>
<!-- 今日呼叫处理情况分析 -->
<div class="panelCardBox">
<div class="panelHeaderBox">今日呼叫处理情况分析</div>
<div class="handleStatuBox">
<div class="statuItemBox" v-for="(item, index) in statusData" :key="index">
<span class="number" :style="{background: item.color}">[[item.count]]</span>
<span class="label">[[item.label]]</span>
</div>
</div>
</div>
<!-- 近一周生产中断等待时长统计 -->
<div class="panelCardBox">
<div class="panelHeaderBox">
<span>近一周生产中断等待时长统计</span>
<span class="unitBox">单位:分钟</span>
<el-select v-model="selProductionLineValue" size="mini" placeholder="选择产线" filterable
@change="selChangeHandle($event,'近一周生产中断等待时长统计')">
<el-option v-for="item in productionLineList" :key="item.id" :label="item.name" :value="item.id">
</el-option>
</el-select>
</div>
<div ref="weeklyWaitTimeRef" style="width: 100%; height: calc(100% - 28px)"></div>
</div>
<!-- 近一周安灯呼叫类型统计 -->
<div class="panelCardBox">
<div class="panelHeaderBox">近一周安灯呼叫类型统计</div>
<div v-show="callTypePieChartData.length>0" ref="callTypePieRef"
style="width: 100%; height: calc(100% - 28px)"></div>
<div class="emptyBox" v-show="!callTypePieChartData.length">
<el-empty image="/roke_workstation_api/static/html/abnormal_alarm/images/empty.png" description="暂无数据"
:image-size="100"></el-empty>
</div>
</div>
<!-- 近一周设备故障频频率 -->
<div class="panelCardBox">
<div class="panelHeaderBox">
<span>近一周设备故障频率</span>
<span class="unitBox">单位:次</span>
<el-select v-model="selEquipmentValue" size="mini" placeholder="选择设备" filterable
@change="selChangeHandle($event,'近一周设备故障频频率')">
<el-option v-for="item in equipmentList" :key="item.id" :label="item.name" :value="item.id">
</el-option>
</el-select>
</div>
<div ref="deviceFailureRef" style="width: 100%; height: calc(100% - 28px)"></div>
</div>
</div>
</body>
<script>
// 发送消息给父页面(关闭odoo的菜单弹窗)
document.addEventListener("click", () => {
window.parent.postMessage("hidePopover", "*")
})
let vue = new Vue({
el: "#app",
delimiters: ["[[", "]]"],
data() {
return {
windowHeight: window.innerHeight, // 页面高度
baseURL: "", // 基地址 http://192.168.8.114:8069
loading: false, // 全局加载效果
stationCallRecordList: [], // 工位呼叫明细数据
containerHeight: 0, // 表体区域高度
perCount: 0, // 可以展示多少行
tableLineHeight: 40, // 表格行高
startIndex: 0, // 片段的起始索引
animationFrameId: null, // 动画帧id
frameCount: 0, // 当前帧数
chartsProject: {}, // 存储图表实例
classesList: [], // 班次列表
selClassesValue: null, // 选中班次的值
defectTrendChartData: { keys: [], values: [] }, // 今日产品缺陷走势图数据
// 今日呼叫处理情况分析数据
statusData: [
{ count: 0, label: '待处理', color: '#F7BA2A' },
{ count: 0, label: '处理中', color: '#67C23A' },
{ count: 0, label: '超时处理', color: '#F56C6C' },
{ count: 0, label: '停线呼叫', color: '#F56C6C' },
{ count: 0, label: '非停线呼叫', color: '#909399' },
{ count: 0, label: '质量异常', color: '#409EFF' },
{ count: 0, label: '工艺异常', color: '#B8E6FF' },
{ count: 0, label: '设备异常', color: '#E6A23C' }
],
productionLineList: [], // 产线列表
selProductionLineValue: null, // 选中产线的值
weeklyWaitTimeChartData: { keys: [], values: [] }, // 近一周生产中断等待时长统计数据
callTypePieChartData: [], // 近一周安灯呼叫类型统计数据
equipmentList: [], // 设备列表
selEquipmentValue: null, // 选中设备的值
deviceFailureChartData: { keys: [], values: [] }, // 近一周设备故障频率数据
}
},
async created() {
this.loading = true
await Promise.allSettled([
this.getStationCallRecordListApi(), // 获取今日工位呼叫明细数据
this.generalQueryApi('班次'), // 获取班次列表
this.generalQueryApi('产线'), // 获取产线列表
this.generalQueryApi('设备'), // 获取设备列表
this.getChartDataApi() // 获取图表数据
]).then(() => {
// 确保DOM完全加载后再初始化图表
this.initAllCharts()
})
this.loading = false
},
mounted() {
this.$nextTick(() => {
document.getElementById("bodyId").style.display = "block"
})
window.addEventListener("resize", this.resizeHandle)
},
methods: {
// 通用查询接口
generalQueryApi(type) {
return new Promise((resolve, reject) => {
let config = {
model: '',
fields: ['id', 'name'],
domain: []
}
if (type == '班次') {
config.model = 'roke.classes'
config.fields = ['id', 'name', 'start_time', 'end_time']
config.domain = [["main_id.is_default", "=", true]]
} else if (type == '产线') {
config.model = 'roke.workshop'
} else if (type == '设备') {
config.model = 'roke.mes.equipment'
}
if (!config.model) return
axios({
method: "POST",
url: this.baseURL + '/roke/workstation/search_read',
data: config,
headers: { "Content-Type": "application/json" }
}).then(async (result) => {
if (result?.data?.result?.code == 0) {
const data = result.data.result.data
if (type == '班次') {
data.forEach(item => {
item.start_time = this.convertNumberToTime(item.start_time)
item.end_time = this.convertNumberToTime(item.end_time)
item.name = `${item.name}(${item.start_time}~${item.end_time})`
})
this.classesList = data
this.selClassesValue = data.length > 0 ? data[0].id : null
// 获取今日产品缺陷走势图
if (this.selClassesValue) await this.getProductDefectChartListApi()
} else if (type == '产线') {
let list = [{ id: 0, name: '所有产线' }]
this.productionLineList = [...list, ...data]
this.selProductionLineValue = 0
} else if (type == '设备') {
let list = [{ id: 0, name: '所有设备' }]
this.equipmentList = [...list, ...data]
this.selEquipmentValue = 0
}
resolve()
} else if (result?.data?.result?.code == 1) {
this.$message.error(result.data.result.message)
reject()
} else if (result?.data?.error) {
this.$message.error(result.data.error.message)
reject()
}
}).catch((error) => {
console.error(error)
reject()
})
})
},
// 获取今日工位呼叫明细数据
getStationCallRecordListApi(isRefresh) {
return new Promise((resolve, reject) => {
if (isRefresh) this.resetAnimationFn()
axios({
method: "POST",
url: this.baseURL + '/get/workstation/everyday/abnormal',
data: {},
headers: { "Content-Type": "application/json" }
}).then((result) => {
if (result?.data?.result?.code == 0) {
const data = result.data.result.data
const general = result.data.result.general
this.statusData.forEach(item => {
if (item.label == '待处理') {
item.count = general.draft
} else if (item.label == '处理中') {
item.count = general.start
} else if (item.label == '超时处理') {
item.count = general.overtime
} else if (item.label == '停线呼叫') {
item.count = general.stop
} else if (item.label == '非停线呼叫') {
item.count = general.running
} else if (item.label == '质量异常') {
item.count = general.quality
} else if (item.label == '工艺异常') {
item.count = general.routing
} else if (item.label == '设备异常') {
item.count = general.equipment
}
})
data.forEach(item => {
item.originating_time = item?.originating_time ? item.originating_time.split(' ')[1] : '/'
item.arrive_time = item?.arrive_time ? item.arrive_time.split(' ')[1] : '/'
})
this.stationCallRecordList = data
// 重新初始化滚动表格
this.initScrollTable()
resolve()
} else if (result?.data?.result?.code == 1) {
this.$message.error(result.data.result.message)
reject()
} else if (result?.data?.error) {
this.$message.error(result.data.error.message)
reject()
}
}).catch((error) => {
this.$message.error(error)
reject()
})
})
},
// 获取今日产品缺陷走势图
getProductDefectChartListApi() {
return new Promise((resolve, reject) => {
axios({
method: "POST",
url: this.baseURL + '/get/workstation/everyday/hourly_unqualified_qty',
data: {
classes: this.selClassesValue // 班次id
},
headers: { "Content-Type": "application/json" }
}).then((result) => {
if (result?.data?.result?.code == 0) {
const data = result.data.result.data
this.defectTrendChartData.keys = Object.keys(data)
this.defectTrendChartData.values = Object.values(data)
resolve()
} else if (result?.data?.result?.code == 1) {
this.$message.error(result.data.result.message)
reject()
} else if (result?.data?.error) {
this.$message.error(result.data.error.message)
reject()
}
}).catch((error) => {
this.$message.error(error)
reject()
})
})
},
// 获取图表数据
getChartDataApi() {
return new Promise((resolve, reject) => {
axios({
method: "POST",
url: this.baseURL + '/get/workstation/week/abnormal',
data: {
workshop_id: this.selProductionLineValue, // 产线id
equipment_id: this.selEquipmentValue, // 设备id
},
headers: { "Content-Type": "application/json" }
}).then((result) => {
if (result?.data?.result?.code == 0) {
const data = result.data.result.data
let dayList = []
let countList = []
let equipmentCountList = []
data.week.forEach(item => {
dayList.push(item.day + '号')
countList.push(item.count)
equipmentCountList.push(item.equipment_count)
})
this.weeklyWaitTimeChartData.keys = dayList
this.deviceFailureChartData.keys = dayList
this.weeklyWaitTimeChartData.values = countList
this.deviceFailureChartData.values = equipmentCountList
let callTypePieChartData = []
if (data.routing) callTypePieChartData.push({ name: '工艺问题', value: data.routing })
if (data.quality) callTypePieChartData.push({ name: '质量问题', value: data.quality })
if (data.starving) callTypePieChartData.push({ name: '缺料问题', value: data.starving })
if (data.equipment) callTypePieChartData.push({ name: '设备维修', value: data.equipment })
if (data.shape) callTypePieChartData.push({ name: '外形问题', value: data.shape })
this.callTypePieChartData = callTypePieChartData
resolve()
} else if (result?.data?.result?.code == 1) {
this.$message.error(result.data.result.message)
reject()
} else if (result?.data?.error) {
this.$message.error(result.data.error.message)
reject()
}
}).catch((error) => {
this.$message.error(error)
reject()
})
})
},
// 页面大小改变事件
resizeHandle() {
this.windowHeight = window.innerHeight
this.chartsResizeFn()
},
// 重置动画方法
resetAnimationFn() {
// 清空动画帧
if (this.animationFrameId) {
window.cancelAnimationFrame(this.animationFrameId)
this.animationFrameId = null
}
// 重置起始索引
this.startIndex = 0
// 清空现有数据
this.stationCallRecordList = []
// 清空现有片段内容
const fragments = this.$refs.vlist.children
Array.from(fragments).forEach(fragment => {
fragment.innerHTML = ''
})
},
// 表格区域鼠标进去事件
mouseEnterHandle() {
// 鼠标放到表格暂停滚动
if (this.animationFrameId) {
window.cancelAnimationFrame(this.animationFrameId)
this.animationFrameId = null
}
},
// 表格区域鼠标离开事件
mouseLeaveHandle() {
// 数据量少于可显示行数时,不启动滚动
if (this.stationCallRecordList.length <= this.perCount) return
// 否则启动滚动
if (!this.animationFrameId) this.loopScrollAnimationFn()
},
// 初始化滚动表格
initScrollTable() {
this.$nextTick(() => {
setTimeout(() => {
// 获取表格高度
this.containerHeight = this.$refs.panelTableRef.offsetHeight - 40
// 计算可以展示多少行
this.perCount = Math.floor(this.containerHeight / this.tableLineHeight)
// 清空现有片段内容
const vlist = this.$refs.vlist
vlist.innerHTML = ''
// 创建第一个片段
const firstFragment = document.createElement('div')
firstFragment.className = 'fragment_content'
vlist.appendChild(firstFragment)
// 如果数据量少于可显示行数,直接显示所有数据不滚动
if (this.stationCallRecordList.length <= this.perCount) {
// 填充所有数据
this.fillScrollTableContentFn(firstFragment, 0, this.stationCallRecordList.length)
return
}
// 数据量足够时,创建第二个片段并启动滚动
const secondFragment = document.createElement('div')
secondFragment.className = 'fragment_content'
vlist.appendChild(secondFragment)
this.loopScrollAnimationFn(true)
}, 1)
})
},
// 循环滚动动画方法
loopScrollAnimationFn(isInit = false) {
// 是否有数据可以显示,如果没有数据,直接返回
if (!this.stationCallRecordList.length > 0) return
if (this.startIndex === 0 && isInit === true) {
// 初始化阶段
// 获取第一个片段DOM元素
const firstFragment = this.$refs.vlist.firstElementChild
// 填充第一个片段的数据
this.fillScrollTableContentFn(firstFragment, this.startIndex)
// 计算下一个片段的起始索引
this.startIndex = this.countStartIndexFn(this.startIndex)
// 获取第二个片段DOM元素
const secondFragment = this.$refs.vlist.lastElementChild
// 填充第二个片段的数据
this.fillScrollTableContentFn(secondFragment, this.startIndex)
// 再次计算下一个片段的起始索引
this.startIndex = this.countStartIndexFn(this.startIndex)
// 设置初始位置为0
this.$refs.vlist.style.transform = 'translateY(0)'
} else {
// 滚动阶段
// 获取滚动容器DOM
const vlist = this.$refs.vlist
// 获取当前 transform 值
let currentTransform = vlist.style.transform
// 解析当前Y轴位置,如果没有则默认为0
let currentY = currentTransform ? parseInt(currentTransform.match(/-?\d+/)?.[0] || 0) : 0
// 帧计数器递增
this.frameCount++
// 每2帧执行一次滚动
if (this.frameCount >= 2) {
// 重置帧计数器
this.frameCount = 0
// 判断是否滚动到了一个片段的高度
if (Math.abs(currentY) >= this.tableLineHeight * this.perCount) {
// 移除第一个片段
vlist.removeChild(vlist.firstElementChild)
// 重置位置到顶部
vlist.style.transform = 'translateY(0)'
// 创建新的片段元素
const newFragment = document.createElement('div')
// 设置片段的类名
newFragment.className = 'fragment_content'
// 填充新片段的数据
this.fillScrollTableContentFn(newFragment, this.startIndex)
// 计算下一个片段的起始索引
this.startIndex = this.countStartIndexFn(this.startIndex)
// 将新片段添加到容器末尾
vlist.appendChild(newFragment)
} else {
// 如果未滚动到片段高度,继续向上滚动1像素
vlist.style.transform = `translateY(${currentY - 1}px)`
}
}
}
// 请求下一帧动画
this.animationFrameId = window.requestAnimationFrame(() => this.loopScrollAnimationFn())
},
// 填充滚动表格内容
fillScrollTableContentFn(ele, startIndex, customCount) {
// 创建文档片段,用于提高性能,避免多次DOM操作
const fragment = document.createDocumentFragment()
// 记录当前数据索引
let currentIndex = startIndex
// 使用自定义数量或默认的perCount
const count = customCount || this.perCount
// 循环创建每一行数据
for (let i = 0; i < count; i++) {
// 获取当前行的数据
const curData = this.stationCallRecordList[currentIndex]
if (!curData) break // 如果没有数据了就停止
// 创建行容器
const newLine = document.createElement('div')
// 设置行的样式类
newLine.className = 'roll_line'
// 创建产线列
const workshop_name = document.createElement('div')
workshop_name.innerText = curData?.workshop_name ? curData.workshop_name : '/'
// 创建工位列
const work_center_name = document.createElement('div')
work_center_name.innerText = curData?.work_center_name ? curData.work_center_name : '/'
// 创建呼叫类型列
const abnormal_item = document.createElement('div')
abnormal_item.innerText = curData?.abnormal_item ? curData.abnormal_item : '/'
// 根据不同的呼叫类型设置不同的样式
if (curData?.abnormal_item === '设备故障') {
abnormal_item.className = 'danger'
} else if (curData?.abnormal_item === '质量问题') {
abnormal_item.className = 'warning'
} else if (curData?.abnormal_item === '其他') {
abnormal_item.className = 'info'
}
// 创建呼叫时间列
const originating_time = document.createElement('div')
originating_time.innerText = curData?.originating_time ? curData.originating_time : '/'
// 创建处理时间列
const arrive_time = document.createElement('div')
arrive_time.innerText = curData?.arrive_time ? curData.arrive_time : '/'
// 将所有列添加到行容器中
newLine.appendChild(workshop_name)
newLine.appendChild(work_center_name)
newLine.appendChild(abnormal_item)
newLine.appendChild(originating_time)
newLine.appendChild(arrive_time)
// 将行添加到文档片段中
fragment.appendChild(newLine)
// 更新索引,如果到达末尾则重新从0开始
currentIndex = currentIndex === this.stationCallRecordList.length - 1 ? 0 : currentIndex + 1
}
// 将整个文档片段一次性添加到DOM中
ele.appendChild(fragment)
},
// 计算下一个片段开始显示的数据索引
countStartIndexFn(startIndex) {
// 数据不够填满下一个片段
if (startIndex + this.perCount >= this.stationCallRecordList.length) {
return this.perCount - (this.stationCallRecordList.length - startIndex)
}
// 数据足够填满下一个片段,直接返回当前索引加上每页显示数量
return this.perCount + startIndex
},
// 数字转换成时间格式方法
convertNumberToTime(num) {
// 提取小时部分,使用 Math.floor 函数向下取整得到整数小时
let hours = Math.floor(num)
// 提取分钟部分,先将小数部分乘以 60 得到分钟数
let minutes = Math.round((num - hours) * 60)
// 将小时和分钟转换为字符串,并确保它们是两位数的格式
// 如果小时数小于 10,在前面补 0
hours = hours.toString().padStart(2, '0')
// 如果分钟数小于 10,在前面补 0
minutes = minutes.toString().padStart(2, '0')
// 返回格式化后的时间字符串
return `${hours}:${minutes}`
},
// 初始化所有图标
initAllCharts() {
this.$nextTick(() => {
setTimeout(() => {
if (this.$refs.defectTrendRef) this.initDefectTrendChart()
if (this.$refs.weeklyWaitTimeRef) this.initWeeklyWaitTimeChart()
if (this.$refs.callTypePieRef) this.initCallTypePieChart()
if (this.$refs.deviceFailureRef) this.initDeviceFailureChart()
}, 1)
})
},
// 初始化今日产品缺陷走势图图表
initDefectTrendChart() {
const chart = echarts.init(this.$refs.defectTrendRef)
this.chartsProject.defectTrendProject = chart
const option = {
tooltip: { trigger: 'item' },
grid: { top: '45px', left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: {
type: 'category',
axisLine: { lineStyle: { color: '#7c808d' } },
axisLabel: { color: '#fff', fontSize: 11 },
data: this.defectTrendChartData.keys
},
yAxis: {
type: 'value',
axisLine: { show: true, lineStyle: { color: '#7c808d' } },
splitLine: { lineStyle: { color: '#7c808d', width: 0.5, opacity: 0.5 } },
axisLabel: { color: '#fff', fontSize: 11, formatter: '{value}' }
},
series: [{
data: this.defectTrendChartData.values,
type: 'line',
smooth: true
}]
}
chart.setOption(option)
},
// 初始化近一周生产中断等待时长统计图表
initWeeklyWaitTimeChart() {
const chart = echarts.init(this.$refs.weeklyWaitTimeRef)
this.chartsProject.weeklyWaitTimeProject = chart
const option = {
tooltip: { trigger: 'item' },
grid: { top: '45px', left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: {
type: 'category',
axisLine: { lineStyle: { color: '#7c808d' } },
axisLabel: { color: '#fff', fontSize: 11 },
data: this.weeklyWaitTimeChartData.keys
},
yAxis: {
type: 'value',
axisLine: { show: true, lineStyle: { color: '#7c808d' } },
splitLine: { lineStyle: { color: '#7c808d', width: 0.5, opacity: 0.5 } },
axisLabel: { color: '#fff', fontSize: 11, formatter: '{value}' }
},
series: [{
data: this.weeklyWaitTimeChartData.values,
type: 'bar'
}]
}
chart.setOption(option)
},
// 初始化近一周安灯呼叫类型统计图表
initCallTypePieChart() {
const chart = echarts.init(this.$refs.callTypePieRef)
this.chartsProject.callTypePieProject = chart
const option = {
tooltip: { trigger: 'item' },
series: [{
type: 'pie',
radius: '70%',
data: this.callTypePieChartData
}]
}
chart.setOption(option)
},
// 初始化近一周设备故障频率图表
initDeviceFailureChart() {
const chart = echarts.init(this.$refs.deviceFailureRef)
this.chartsProject.deviceFailureProject = chart
const option = {
tooltip: { trigger: 'item' },
grid: { top: '45px', left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: {
type: 'category',
axisLine: { lineStyle: { color: '#7c808d' } },
axisLabel: { color: '#fff', fontSize: 11 },
data: this.deviceFailureChartData.keys
},
yAxis: {
type: 'value',
axisLine: { show: true, lineStyle: { color: '#7c808d' } },
splitLine: { lineStyle: { color: '#7c808d', width: 0.5, opacity: 0.5 } },
axisLabel: { color: '#fff', fontSize: 11, formatter: '{value}' }
},
series: [{
data: this.deviceFailureChartData.values,
type: 'bar'
}]
}
chart.setOption(option)
},
// 图标调整大小方法
chartsResizeFn() {
Object.values(this.chartsProject).forEach(chart => {
chart && chart.resize()
})
},
// 下拉框改变事件
async selChangeHandle(el, type) {
this.loading = true
if (type == '今日产品缺陷走势图') {
await this.getProductDefectChartListApi().then(result => {
if (this.$refs.defectTrendRef) this.initDefectTrendChart()
}).catch(error => {
this.$message.error('获取今日产品缺陷走势图数据失败')
})
} else if (type == '近一周生产中断等待时长统计' || type == '近一周设备故障频频率') {
await this.getChartDataApi().then(result => {
if (type == '近一周生产中断等待时长统计') {
if (this.$refs.weeklyWaitTimeRef) this.initWeeklyWaitTimeChart()
} else if (type == '近一周设备故障频频率') {
if (this.$refs.deviceFailureRef) this.initDeviceFailureChart()
}
}).catch(error => {
this.$message.error(`获取${type}数据失败`)
})
}
this.loading = false
},
beforeDestroy() {
window.removeEventListener("resize", this.resizeHandle)
// 销毁所有图表实例
Object.values(this.chartsProject).forEach(chart => {
chart && chart.dispose()
})
if (this.animationFrameId) {
window.cancelAnimationFrame(this.animationFrameId)
}
}
}
})
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-thumb {
background: #c0c4cc;
border-radius: 3px;
}
::-webkit-scrollbar-track {
background: #f5f7fa;
}
#app {
display: flex;
align-content: space-between;
flex-wrap: wrap;
width: 100vw;
height: 100vh;
padding: 8px;
background-color: #06114f;
gap: 8px;
.panelCardBox {
width: calc((100% - 16px) / 3);
height: calc(50% - 4px);
border-radius: 4px;
padding: 8px;
border: 1px solid #7c808d;
.panelHeaderBox {
font-size: 14px;
font-weight: bold;
color: #FFF;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 28px;
position: relative;
.text {
font-weight: 400;
border-bottom: 1px solid #303133;
}
.unitBox {
position: absolute;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
bottom: -28px;
font-size: 12px;
color: #ccc;
font-weight: 400;
}
}
.panelTableBox {
height: calc(100% - 28px);
width: 100%;
overflow-y: hidden;
position: relative;
border: 1px solid #7c808d;
font-size: 13px;
border-radius: 2px;
.panelTableHeader {
border-bottom: 1px solid #7c808d;
position: relative;
z-index: 2;
.roll_line {
font-weight: bold;
color: #FFF;
border-bottom: none;
}
.roll_line:hover {
background-color: transparent !important;
cursor: default !important;
}
}
.vabsolute_content {
width: 100%;
position: absolute;
top: 40px;
transform: translateY(0);
will-change: transform;
height: 100%;
}
.roll_line {
display: flex;
padding: 0 8px;
box-sizing: border-box;
width: 100%;
height: 40px;
color: #FFF;
align-items: center;
border-bottom: 1px solid #7c808d;
transition: background-color 0.25s ease;
gap: 8px;
}
.roll_line:hover {
background-color: rgba(255, 255, 255, 0.2);
cursor: pointer;
}
.roll_line>div {
flex: 1;
}
.roll_line>div:nth-child(4),
.roll_line>div:nth-child(5) {
min-width: 65px;
max-width: 65px;
}
}
.handleStatuBox {
display: flex;
align-content: flex-start;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
min-height: calc(100% - 28px - 8px);
overflow-y: auto;
.statuItemBox {
width: calc((100% - 24px) / 3);
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
.number {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
}
.label {
font-size: 12px;
color: #FFF;
}
}
}
.emptyBox {
width: 100%;
height: calc(100% - 28px);
display: flex;
align-items: center;
justify-content: center;
.el-empty {
padding: 0
}
}
}
}
</style>
</html>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment