Commit 64505bea by wangkangjie

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

parents 07afdc43 a7dce4c0
# -*- coding: utf-8 -*-
from . import controllers
from . import models
\ No newline at end of file
# -*- coding: utf-8 -*-
{
'name': "融科-金牛",
'summary': """
Short (1 phrase/line) summary of the module's purpose, used as
subtitle on modules listing or apps.openerp.com""",
'description': """
Long description of module's purpose
""",
'author': "My Company",
'website': "http://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/14.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Uncategorized',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base'],
# always loaded
'data': [
# 'security/ir.model.access.csv',
'views/views.xml',
'views/templates.xml',
],
# only loaded in demonstration mode
'demo': [
'demo/demo.xml',
],
}
# -*- coding: utf-8 -*-
from . import controllers
\ No newline at end of file
# -*- coding: utf-8 -*-
from odoo import http, fields
import os
import logging
from jinja2 import FileSystemLoader, Environment
from datetime import datetime, date, timedelta
from odoo import http, tools, SUPERUSER_ID
from odoo.addons.roke_mes_base.tools import http_tool
_logger = logging.getLogger(__name__)
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
templateloader = FileSystemLoader(searchpath=BASE_DIR + "/static/src/view")
env = Environment(loader=templateloader)
class JnzgProject(http.Controller):
@http.route("/roke/three_color_light/device_state_list", type="http", auth='none', cors='*', csrf=False)
def device_state_list(self, **kwargs):
_self = http.request
factory_code = http.request.env(user=SUPERUSER_ID)['ir.config_parameter'].get_param('database.uuid', default="")
data = {"code": 1, "message": "请求通过", "data": {"factory_code": factory_code}}
template = env.get_template('equipment_status_jnzg.html')
return template.render(data)
# @http.route('/roke/get/equipment/maintenance_info', type='json', methods=['POST', 'OPTIONS'], auth='none', cors='*', csrf=False)
# def get_equipment_maintenance_info(self):
# """
# 获取设备点检,保养,维修,更换件记录,设备当天生产合格数
# 入参: {"equipment_id": 设备ID}
# 返回: {
# "state": "success",
# "msgs": "获取成功",
# "equipment_maintenance_list": [...]
# }
# """
# req = http.request.jsonrequest
# equipment_id = req.get("equipment_id")
# domain = [("code", "!=", False)]
# if equipment_id:
# domain.append(("id", "=", equipment_id))
# today = fields.Date.today()
# tomorrow = today + timedelta(days=1)
# week_start = today - timedelta(days=today.weekday())
# week_end = week_start + timedelta(days=7)
# equipment_ids = http.request.env["roke.mes.equipment"].sudo().search(domain)
# equipment_maintenance_list = []
# for equipment_id in equipment_ids:
# check_record_id = http.request.env["roke.mes.eqpt.spot.check.record"].sudo().search([
# ("equipment_id", "=", equipment_id.id),
# ("create_date", ">=", datetime.combine(today, datetime.min.time()) - timedelta(hours=8)),
# ("create_date", "<", datetime.combine(tomorrow, datetime.min.time()) - timedelta(hours=8))
# ], limit=1)
# MaintenanceRecord = http.request.env["roke.mes.maintenance.order"].sudo()
# maintain_order_id = MaintenanceRecord.search([
# ("equipment_id", "=", equipment_id.id),
# ("create_date", ">=", datetime.combine(week_start, datetime.min.time()) - timedelta(hours=8)),
# ("create_date", "<", datetime.combine(week_end, datetime.min.time()) - timedelta(hours=8)),
# ("type", "=", "maintain")
# ], limit=1)
# repair_id = MaintenanceRecord.search([
# ("equipment_id", "=", equipment_id.id),
# ("report_time", ">=", datetime.combine(today, datetime.min.time()) - timedelta(hours=8)),
# ("report_time", "<", datetime.combine(tomorrow, datetime.min.time()) - timedelta(hours=8)),
# ("type", "=", "repair")
# ], limit=1)
# ChangeRecord = http.request.env["roke.spare.part.usage.record"].sudo()
# change_record_ids = ChangeRecord
# if repair_id:
# change_record_ids = ChangeRecord.search([
# ("maintenance_order_id", "=", repair_id.id),
# ])
# equipment_maintenance_list.append({
# "equipment_id": equipment_id.id,
# "equipment_code": equipment_id.code,
# "equipment_name": equipment_id.name,
# "check_info": self.get_today_spot_check(check_record_id),
# "maintain_info": self.get_this_week_maintenance(maintain_order_id),
# "repair_info": self.get_equipment_repair(repair_id, MaintenanceRecord),
# "change_info": self.get_equipment_change(change_record_ids, ChangeRecord),
# "finish_qty": self.get_equipment_today_finish_qty(equipment_id, today, tomorrow)
# })
# return {"state": "success", "msgs": "获取成功", "equipment_maintenance_list": equipment_maintenance_list}
# def get_equipment_today_finish_qty(self, equipment_id, today, tomorrow):
# """获取设备当天生产合格数"""
# finish_qty = sum(http.request.env["roke.work.record"].sudo().search([("work_center_id", "=", equipment_id.work_center_id.id),
# ("work_time", ">=", datetime.combine(today, datetime.min.time())),
# ("work_time", "<", datetime.combine(tomorrow, datetime.min.time()))]).mapped("finish_qty"))
# return finish_qty
# def get_equipment_change(self, change_record_ids, ChangeRecord):
# """
# 获取设备更换件记录
# """
# status = "no_task" # 无更换
# detail = {}
# if change_record_ids:
# # 有任务,判断状态
# status = "has_task" # 已更换
# detail = {
# "record_date": change_record_ids[0].replacement_time, # 最近更换时间
# "removed_part": ".".join(change_record_ids.mapped("removed_part_id.name")), # 拆下备件
# "installed_part": ".".join(change_record_ids.mapped("spare_part_id.name")), # 换上备件
# "change_time": ChangeRecord.search_count([("equipment_id", "=", change_record_ids[0].equipment_id.id)]) # 更换次数
# }
# else:
# # 无任务时的基本信息
# detail = {
# "record_date": "", # 最近更换时间
# "removed_part": "", # 拆下备件
# "installed_part": "", # 换上备件
# "change_time": 0 # 更换次数
# }
# return {
# "status": status,
# "detail": detail
# }
# def get_equipment_repair(self, repair_id, MaintenanceRecord):
# """
# 获取设备维修记录
# """
# status = "no_task" # 无维修
# detail = {}
# if repair_id:
# # 有任务,判断状态
# if repair_id.state == "finish":
# status = "finished" # 已完成
# else:
# status = "in_progress" # 维修中
# change_record_ids = http.request.env["roke.spare.part.usage.record"].sudo().search([("maintenance_order_id", "=", repair_id.id)])
# detail = {
# "equipment_name": repair_id.equipment_id.name, # 设备名称
# "last_maintenance_time": repair_id.report_time and str(repair_id.report_time) or "", # 最近保养时间
# "repair_time": MaintenanceRecord.search_count([("equipment_id", "=", repair_id.equipment_id.id)]), # 维修次数
# "repair_user": repair_id.repair_user_id.name if repair_id and repair_id.repair_user_id else "", # 维修人
# "removed_part": ".".join(change_record_ids.mapped("removed_part_id.name")), # 拆下备件
# "installed_part": ".".join(change_record_ids.mapped("spare_part_id.name")), # 换上备件
# }
# else:
# # 无任务时的基本信息
# detail = {
# "last_maintenance_time": "", # 最近保养时间
# "repair_time": "", # 维修次数
# "repair_user": "", # 维修人
# "removed_part": "", # 拆下备件
# "installed_part": "", # 换上备件
# }
# return {
# "status": status,
# "detail": detail
# }
# def get_this_week_maintenance(self, maintain_order_id):
# """
# 获取指定设备本周的保养记录及状态
# """
# status = "no_task" # 无任务
# detail = {}
# if maintain_order_id:
# # 有任务,判断状态
# now = fields.Datetime.now()
# if maintain_order_id.state == "finish":
# status = "finished" # 已完成
# elif maintain_order_id.estimated_completion_time and now > maintain_order_id.estimated_completion_time:
# status = "timeout" # 超时
# elif maintain_order_id.item_ids.filtered(lambda x: x.state != "wait"):
# status = "in_progress" # 进行中
# else:
# status = "not_started" # 未开始
# # 组装详细信息
# maintenance_items = []
# for line in maintain_order_id.item_ids:
# maintenance_items.append(line.item_id.name)
# detail = {
# "last_maintenance_time": maintain_order_id.report_time and str(maintain_order_id.report_time) or "", # 最近保养时间
# "maintenance_plan_name": maintain_order_id.maintenance_scheme_id.name if maintain_order_id.maintenance_scheme_id else "", # 保养方案名称
# "maintenance_items": ",".join(maintenance_items), # 保养项目
# "maintenance_result": "normal", # 保养结果
# }
# # 判断是否有异常/故障,需要显示维修相关字段
# if maintain_order_id.normal_state == "abnormal":
# change_record_ids = http.request.env["roke.spare.part.usage.record"].sudo().search([("maintenance_order_id", "=", maintain_order_id.id)])
# # 维修相关字段(只有保养结果异常时才显示)
# detail.update({
# "maintenance_result": "abnormal", # 保养结果
# "repair_status": http_tool.get_selection_field_values(maintain_order_id, "state") if maintain_order_id else "", # 维修状态
# "repair_user": maintain_order_id.repair_user_id.name if maintain_order_id and maintain_order_id.repair_user_id else "", # 维修人
# "repair_finish_time": maintain_order_id.finish_time and str(maintain_order_id.finish_time) or "" if maintain_order_id else "", # 维修完成时间
# "removed_part": ".".join(change_record_ids.mapped("removed_part_id.name")), # 拆下备件
# "installed_part": ".".join(change_record_ids.mapped("spare_part_id.name")), # 换上备件
# })
# else:
# # 无任务时的基本信息
# detail = {
# "last_maintenance_time": "", # 最近保养时间
# "maintenance_plan_name": "", # 保养方案名称
# "maintenance_items": "", # 保养项目
# "maintenance_result": "normal", # 保养结果
# }
# return {
# "status": status,
# "detail": detail
# }
# def get_today_spot_check(self, check_record_id):
# """
# 获取设备当天的点检记录及状态
# """
# status = "no_task" # 无任务
# detail = {}
# # 有任务,判断状态
# if check_record_id.state == "finish":
# status = "finished" # 已完成
# else:
# # 检查是否超时
# now = fields.Datetime.now()
# _logger.info(f"get_today_spot_check, now: {now}, estimated_completion_time: {check_record_id.estimated_completion_time}")
# if check_record_id.estimated_completion_time and now > check_record_id.estimated_completion_time:
# status = "timeout" # 超时
# elif check_record_id.item_record_ids.filtered(lambda x: x.result != False):
# status = "in_progress" # 进行中
# else:
# status = "not_started" # 未开始
# # 组装详细信息
# spot_items = []
# for line in check_record_id.item_record_ids:
# spot_items.append(line.check_item_id.name)
# detail = {
# "last_spot_check_time": check_record_id.start_date and str(check_record_id.start_date) or "", # 最近点检时间
# "spot_check_plan": check_record_id.check_plan_id.name if check_record_id.check_plan_id else "", # 点检方案
# "spot_check_items": ",".join(spot_items), # 点检项目
# "spot_check_result": "normal", # 点检结果
# }
# # 判断是否有异常/故障
# if check_record_id.normal_state == "abnormal":
# repair_order_id = http.request.env["roke.mes.maintenance.order"].sudo().search([("spot_check_record_id", "=", check_record_id.id)], limit=1)
# change_record_id = http.request.env["roke.spare.part.usage.record"].sudo().search([("maintenance_order_id", "=", repair_order_id.id)], limit=1)
# # 维修相关字段
# detail.update({
# "repair_status": http_tool.get_selection_field_values(repair_order_id, "state"), # 维修状态
# "repair_user": repair_order_id.repair_user_id.name if repair_order_id.repair_user_id else "", # 维修人
# "repair_finish_time": repair_order_id.finish_time and str(repair_order_id.finish_time) or "", # 维修完成时间
# "removed_part": change_record_id.removed_part_id.name or "", # 拆下备件
# "installed_part": change_record_id.spare_part_id.name or "", # 换上备件
# "spot_check_result": "abnormal" # 点检结果
# })
# return {
# "status": status,
# "detail": detail
# }
<odoo>
<data>
<!--
<record id="object0" model="jnzg_project.jnzg_project">
<field name="name">Object 0</field>
<field name="value">0</field>
</record>
<record id="object1" model="jnzg_project.jnzg_project">
<field name="name">Object 1</field>
<field name="value">10</field>
</record>
<record id="object2" model="jnzg_project.jnzg_project">
<field name="name">Object 2</field>
<field name="value">20</field>
</record>
<record id="object3" model="jnzg_project.jnzg_project">
<field name="name">Object 3</field>
<field name="value">30</field>
</record>
<record id="object4" model="jnzg_project.jnzg_project">
<field name="name">Object 4</field>
<field name="value">40</field>
</record>
-->
</data>
</odoo>
\ No newline at end of file
# -*- coding: utf-8 -*-
from . import models
\ No newline at end of file
# -*- coding: utf-8 -*-
# from odoo import models, fields, api
# class jnzg_project(models.Model):
# _name = 'jnzg_project.jnzg_project'
# _description = 'jnzg_project.jnzg_project'
# name = fields.Char()
# value = fields.Integer()
# value2 = fields.Float(compute="_value_pc", store=True)
# description = fields.Text()
#
# @api.depends('value')
# def _value_pc(self):
# for record in self:
# record.value2 = float(record.value) / 100
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_jnzg_project_jnzg_project,jnzg_project.jnzg_project,model_jnzg_project_jnzg_project,base.group_user,1,1,1,1
\ No newline at end of file
<!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/routing -->
<link rel="stylesheet" href="/roke_workstation_api/static/html/routing/element-ui/index.css" />
<script src="/roke_workstation_api/static/html/routing/js/echarts.min.js"></script>
<script src="/roke_workstation_api/static/html/routing/js/moment.min.js"></script>
<script src="/roke_workstation_api/static/html/routing/js/vue.js"></script>
<script src="/roke_workstation_api/static/html/routing/js/axios.min.js"></script>
<script src="/roke_workstation_api/static/html/routing/element-ui/index.js"></script>
</head>
<body id="bodyId" style="display: none">
<div id="app" v-loading.body.fullscreen.lock="loading" ref="fullScreenElement">
<!-- 页面标题 -->
<div class="dashboard-header">
<div class="header-content">
<img src="/roke_workstation_api/static/html/routing/image/header_bg.png" class="header-bg"
alt="header background" />
<span class="header-text">设备实时看板</span>
<span class="hintText">
<i style="font-size: 30px" @click="toggleFullscreen"
:class="isFullscreen ? 'el-icon-close' : 'el-icon-full-screen'"></i>
<span> [[currentTime ]]</span>
</span>
</div>
</div>
<!-- 设备状态卡片区域 - 新设计 -->
<div class="device-cards-container">
<div v-for="(device, index) in deviceList" :key="index" class="device-card new-design"
:class="getCardClass(device.status)">
<!-- 设备名称 - 左上角 -->
<div class="device-name">[[device.name]]</div>
<!-- 设备状态 - 右上角 -->
<div class="device-status-tag" :class="getStatusClass(device.status)"> [[getStatusText(device.status)]]</div>
<!-- 设备状态水波纹 - 中间 -->
<div class="device-wave-container">
<!-- 左侧状态 -->
<div class="left-status">
<div class="time-item-side">
<div class="time-label" style="background-color: #007bff">开机</div>
<span class="time-value">[[ device.run_seconds ]]</span>
</div>
<div class="time-item-side">
<div class="time-label" style="background-color: #ffc107">等待</div>
<span class="time-value">[[ device.yellow_seconds ]]</span>
</div>
</div>
<!-- 中间圆形OEE -->
<div class="center-oee">
<div class="oee-text">OEE</div>
<div class="percentage-text"> [[ device.percentage > 100 ? 100 : device.percentage ]]%</div>
<!-- 圆形容器 -->
<div class="circle-container" :class="getBorderClass(device.status)">
<!-- 水波纹效果 - 通过内联样式直接控制高度百分比 -->
<div class="water-wave" :class="getWaveClass(device.status)"
:style="{'height': getWaveHeight(device.status, device.percentage) + '%'}">
<div class="water-wave-content">
<div class="water-wave-ripple"></div>
</div>
</div>
</div>
</div>
<!-- 右侧状态 -->
<div class="right-status">
<div class="time-item-side">
<span class="time-value">[[ device.green_seconds ]]</span>
<div class="time-label" style="background-color: #28a745">加工</div>
</div>
<div class="time-item-side">
<span class="time-value">[[ device.red_seconds ]]</span>
<div class="time-label" style="background-color: #dc3545">故障</div>
</div>
</div>
</div>
<!-- 设备状态信息 - 底部 -->
<div class="device-status-info">
<span>[[getStatusText(device.status)]]·已持续[[device.duration]]</span>
</div>
<!-- 虚线分割 - 全宽 -->
<div class="full-width-dashed-divider"></div>
<!-- 维护状态区域 - 竖向排列 -->
<div class="maintenance-status-vertical">
<div class="maintenance-item-vertical">
<div class="maintenance-header">
<div class="maintenance-icon maintenance-icon-daily"></div>
<span class="maintenance-label">点检</span>
</div>
<div class="maintenance-status-badge"
:style="{'background-color': getMaintenanceColor(device.check_info.status)}">
<span>[[getMaintenanceText(device.check_info.status,'点检')]]</span>
</div>
</div>
<div class="maintenance-item-vertical">
<div class="maintenance-header">
<div class="maintenance-icon maintenance-icon-weekly"></div>
<span class="maintenance-label">保养</span>
</div>
<div class="maintenance-status-badge"
:style="{'background-color': getMaintenanceColor(device.maintain_info.status)}">
<span>[[getMaintenanceText(device.maintain_info.status,'保养')]]</span>
</div>
</div>
<div class="maintenance-item-vertical">
<div class="maintenance-header">
<div class="maintenance-icon maintenance-icon-repair"></div>
<span class="maintenance-label">维修</span>
</div>
<div class="maintenance-status-badge"
:style="{'background-color': getMaintenanceColor(device.repair_info.status,'维修')}">
<span>[[getMaintenanceText(device.repair_info.status,'维修')]]</span>
</div>
</div>
<div class="maintenance-item-vertical">
<div class="maintenance-header">
<div class="maintenance-icon maintenance-icon-replace"></div>
<span class="maintenance-label">备件</span>
</div>
<div class="maintenance-status-badge"
:style="{'background-color': getMaintenanceColor(device.change_info.status)}">
<span>[[getMaintenanceText(device.change_info.status,'备件')]]</span>
</div>
</div>
</div>
<!-- 虚线分割 - 全宽 -->
<div class="full-width-dashed-divider"></div>
<!-- 统计信息区域 -->
<div class="statistics-info">
<div class="stat-item">
<div class="stat-icon"></div>
<div class="stat-number">[[device.pulse_count]]</div>
<div class="stat-label">产量</div>
</div>
<div class="stat-item">
<div class="stat-icon"></div>
<div class="stat-number">[[device.finish_qty]]</div>
<div class="stat-label">日效</div>
</div>
<div class="stat-item">
<div class="stat-icon"></div>
<div class="stat-number">[[device.aging]]</div>
<div class="stat-label">时效</div>
</div>
</div>
</div>
</div>
</div>
</body>
<script>
// 发送消息给父页面(关闭odoo的菜单弹窗)
document.addEventListener("click", () => {
window.parent.postMessage("hidePopover", "*");
});
let vue = new Vue({
el: "#app",
delimiters: ["[[", "]]"], // 替换原本vue的[[ key ]]取值方式(与odoo使用的jinja2冲突问题)
data: function () {
return {
dwsURL: "", // 基地址
baseURL: "https://dws-platform.xbg.rokeris.com/dev-api", // 基地址
isFullscreen: false, // 全屏状态
currentTime: null, // 当前时间
timer: null, // 时间定时器
refreshInterval: null, // 数据刷新计时器
factoryCode: "", // 公司CODE
loading: false, // 全局加载效果
deviceList: [], // 设备列表
allEquipmentData: [], // 所有已绑定设备数据
equipmentMaintenanceInfos: [], // 设备维护信息
}
},
created: function () {
// 截取url后面的参数
this.factoryCode = this.getUrlSearch("factory_code") || ""
// 初始化当前时间
this.initCurrentTimeFn()
},
mounted: async function () {
this.$nextTick(function () {
document.getElementById("bodyId").style.display = "block"
})
// 初始化数据 - 实际使用时取消这行的注释
await this.initData()
// 设置定时刷新(每分钟刷新一次,静默模式)
this.refreshInterval = setInterval(function () {
this.initData(true) // 传入true表示静默刷新,不显示加载提示
}.bind(this), 60000)
},
beforeDestroy: function () {
// 清除定时器
if (this.refreshInterval) clearInterval(this.refreshInterval)
if (this.timer) clearInterval(this.timer)
},
methods: {
// 全屏icon点击事件
toggleFullscreen: function () {
if (!this.isFullscreen) {
this.enterFullScreen()
} else {
this.exitFullScreen()
}
},
// 全屏方法
enterFullScreen: function () {
// 获取需要全屏的元素
var elem = this.$refs.fullScreenElement
if (elem && elem.requestFullscreen) {
elem.requestFullscreen()
} else if (elem && elem.mozRequestFullScreen) {
// Firefox
elem.mozRequestFullScreen()
} else if (elem && elem.webkitRequestFullscreen) {
// Chrome, Safari & Opera
elem.webkitRequestFullscreen()
} else if (elem && elem.msRequestFullscreen) {
// IE/Edge
elem.msRequestFullscreen()
}
this.isFullscreen = true
},
// 退出全屏
exitFullScreen: function () {
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.mozCancelFullScreen) {
// Firefox
document.mozCancelFullScreen()
} else if (document.webkitExitFullscreen) {
// Chrome, Safari and Opera
document.webkitExitFullscreen()
} else if (document.msExitFullscreen) {
// IE/Edge
document.msExitFullscreen()
}
this.isFullscreen = false
},
// 初始化当前时间
initCurrentTimeFn: function () {
this.timer = setInterval(function () {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, "0")
const day = String(now.getDate()).padStart(2, "0")
const hours = String(now.getHours()).padStart(2, "0")
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)
},
// 通过网址跳转过来的页面,截取后面的参数
getUrlSearch: function (name) {
// 未传参,返回空
if (!name) return ""
// 查询参数:先通过search取值,如果取不到就通过hash来取
var after = window.location.search
after = after.substr(1) || window.location.hash.split("?")[1]
// 地址栏URL没有查询参数,返回空
if (!after) return null
// 如果查询参数中没有"name",返回空
if (after.indexOf(name) === -1) return null
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)")
// 当地址栏参数存在中文时,需要解码,不然会乱码
var r = decodeURI(after).match(reg)
// 如果url中"name"没有值,返回空
if (!r) return null
return r[2]
},
// 初始化数据
initData: async function (silent) {
if (silent === undefined) silent = false
try {
// 只有在非静默模式下才显示加载提示
if (!silent) this.loading = true
// 并行请求设备计划运行时间和所有已绑定设备数据
const [planTimeResult, allEquipmentResult] = await Promise.all([
this.getDevicePlanTime(), // 获取设备计划运行时间
this.getAllEquipmentData(), // 获取所有已绑定设备数据
this.getEquipmentMaintenanceInfoApi(), // 获取设备维护信息
])
} catch (error) {
// 只有在非静默模式下才显示错误提示
if (!silent) this.$message.error("初始化数据出错: " + (error.message || "未知错误"))
} finally {
// 关闭加载提示
if (!silent) this.loading = false
}
},
// 获取设备计划运行时间
getDevicePlanTime: async function () {
try {
// 发送请求获取计划运行时间
const response = await axios({
method: "post",
url: this.dwsURL + "/roke/workstation/equipment/get_plan_time",
data: {},
headers: { "Content-Type": "application/json" },
})
// 处理JSON-RPC格式的响应
if (response.data && response.data.result && response.data.result.code === 0) {
// 获取计划运行时间数据
const planTimeList = response.data.result.data || {}
if (this.factoryCode) {
// 调用设备状态接口
await this.getDeviceStateList(planTimeList)
} else {
await this.getFactoryCode(planTimeList)
}
return planTimeList
} else {
const errorMsg = response.data.result ? response.data.result.message : "获取计划运行时间失败";
throw new Error(errorMsg)
}
} catch (error) {
// console.error("获取计划运行时间出错:", error);
throw error;
}
},
// 获取所有已绑定设备数据
getAllEquipmentData: async function () {
try {
// 发送请求获取所有已绑定设备数据
const response = await axios({
method: "post",
url: this.dwsURL + "/roke/workstation/equipment/get_equipment_data",
data: {},
headers: {
"Content-Type": "application/json",
},
})
// 处理JSON-RPC格式的响应
if (response.data && response.data.result && response.data.result.code === 0) {
// 存储所有设备数据
this.allEquipmentData = response.data.result.data || []
return this.allEquipmentData
} else {
const errorMsg = response.data.result ? response.data.result.message : "获取所有已绑定设备数据失败"
throw new Error(errorMsg)
}
} catch (error) {
throw error
}
},
// 获取设备状态列表
getDeviceStateList: async function (planTimeList) {
try {
// 使用CORS代理
// 发送请求获取设备状态
const response = await axios({
method: "POST",
url: this.baseURL + "/public/device_efficiency/device_state_list",
data: {
factory_code: this.factoryCode,
plan_time_list: planTimeList,
},
headers: {
"Content-Type": "application/json",
},
})
// 处理响应
if (response.data && response.data.success) {
// 处理设备状态数据
this.processDeviceStateData(response.data.data)
} else {
throw new Error(response.data.msg || "获取设备状态失败")
}
} catch (error) {
// console.error("获取设备状态出错:", error);
throw error;
}
},
// 获取factoryCode
getFactoryCode: async function (planTimeList) {
try {
// 使用CORS代理
// 发送请求获取设备状态
const response = await axios({
method: "post",
url: this.dwsURL + "/roke/workstation/db_uuid/get",
data: {},
headers: { "Content-Type": "application/json" },
})
// 处理JSON-RPC格式的响应
if (response.data && response.data.result && response.data.result.code === 0) {
// 获取计划运行时间数据
this.factoryCode = response.data.result.data || ""
// 调用设备状态接口
await this.getDeviceStateList(planTimeList)
} else {
const errorMsg = response.data.result ? response.data.result.message : "获取账套db_uuid失败"
throw new Error(errorMsg)
}
} catch (error) {
throw error
}
},
// 获取设备维护信息
getEquipmentMaintenanceInfoApi: async function () {
try {
const response = await axios({
method: "post",
url: this.dwsURL + "/roke/get/equipment/maintenance_info",
data: {},
headers: { "Content-Type": "application/json" },
})
if (response && response.data && response.data.result && 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 || "获取设备维护信息失败!"
throw new Error(errorMsg)
}
} catch (error) {
throw error
}
},
// 处理设备状态数据
processDeviceStateData: function (deviceStateData) {
var self = this
self.allEquipmentData.forEach(function (item) {
self.equipmentMaintenanceInfos.forEach(function (it) {
if (it.equipment_code == item.code) {
item['check_info'] = it.check_info
item['change_info'] = it.change_info
item['finish_qty'] = it.finish_qty
item['maintain_info'] = it.maintain_info
item['repair_info'] = it.repair_info
}
})
})
if (!deviceStateData || !Array.isArray(deviceStateData)) return
// 将API返回的数据转换为页面所需的格式
this.deviceList = deviceStateData.map(function (device) {
// 根据API返回的状态确定前端显示的状态
var status = "off"
if (device.state === "green") {
status = "running"
} else if (device.state === "yellow") {
status = "waiting"
} else if (device.state === "red") {
status = "error"
} else if (device.state === "gray") {
status = "off"
}
// 计算持续时间的显示格式
var durationText = "0"
if (device.duration_hours !== undefined) {
durationText = self.formatTime(Number(device.duration_hours * 3600))
}
var run_seconds = "0"
if (device.run_seconds !== undefined)
run_seconds = self.formatTime(Number(device.run_seconds))
var green_seconds = "0"
if (device.green_seconds !== undefined)
green_seconds = self.formatTime(Number(device.green_seconds))
var yellow_seconds = "0"
if (device.yellow_seconds !== undefined)
yellow_seconds = self.formatTime(Number(device.yellow_seconds))
var red_seconds = "0"
if (device.red_seconds !== undefined)
red_seconds = self.formatTime(Number(device.red_seconds))
// 计算利用率百分比,确保有效值
var percentage =
device.utilization_rate !== undefined ? Math.round(device.utilization_rate) : 0
// 从所有设备列表中获取准确的设备名称
var deviceName = device.name || device.code // 默认使用API返回的名称或编码
var checkInfo = {} // 点检信息
var maintainInfo = {} // 保养信息
var repairInfo = {} // 维修信息
var changeInfo = {} // 更换件信息
var finishQty = 0 // 当天生产合格数
device.pulse_count = 20
var aging = 0 // 日效(产量 / 绿灯时长)
if (device.pulse_count && Math.floor(Number(device.run_seconds))) {
aging = self.formatDecimal(device.pulse_count / Math.floor(Number(device.run_seconds) / 3600), 2)
}
// 在所有设备列表中查找匹配的设备
if (self.allEquipmentData && self.allEquipmentData.length) {
var matchedDevice = self.allEquipmentData.find(function (equip) {
return equip.code === device.code
})
// 如果找到匹配的设备,使用其名称
if (matchedDevice && matchedDevice.name) {
deviceName = device.name ? matchedDevice.name : device.code
checkInfo = matchedDevice.check_info || {}
maintainInfo = matchedDevice.maintain_info || {}
repairInfo = matchedDevice.repair_info || {}
changeInfo = matchedDevice.change_info || {}
finishQty = matchedDevice.finish_qty || 0
} else {
return false
}
}
return {
id: device.code,
code: device.code,
name: deviceName,
status: status,
percentage: percentage,
duration: durationText,
run_seconds: run_seconds,
aging: aging,
pulse_count: device.pulse_count || 0,
green_seconds: green_seconds,
yellow_seconds: yellow_seconds,
red_seconds: red_seconds,
check_info: checkInfo,
maintain_info: maintainInfo,
repair_info: repairInfo,
change_info: changeInfo,
finish_qty: finishQty
}
})
this.deviceList = this.deviceList.filter(function (device) { return device })
},
// 处理小数方法
formatDecimal: function (value, digit) {
// 没设置位数默认返回原来的值
if (!digit) return value
// 处理 null/undefined/空字符串
if (value === null || value === undefined || value === '') return null
// 转为数字(避免字符串或科学计数法问题)
const parsedNum = parseFloat(value)
// 处理 NaN
if (isNaN(parsedNum)) return NaN
// 检查是否是整数,整数直接返回
if (Number.isInteger(parsedNum)) return parsedNum
// 截取小数位数
const decimalPart = parsedNum.toString().split('.')[1]
// 检查小数长度是否大于要求位数,小于或等于直接返回
if (decimalPart && decimalPart.length <= digit) return parsedNum
// 保留要求位数(处理浮点误差)
return parseFloat((Math.round((parsedNum + Number.EPSILON) * 100) / 100).toFixed(digit))
},
// 处理时间方法
formatTime: function (seconds) {
if (seconds < 60) {
return `0min` // 不足 1 分钟显示 0min
} else if (seconds < 3600) {
const minutes = Math.floor(seconds / 60) // 转换为分钟
return `${minutes}min` // 显示分钟
} else if (seconds < 86400) {
// 小于 1 天
const hours = Math.floor(seconds / 3600) // 转换为小时
return `${hours}h` // 只返回小时
} else {
const days = Math.floor(seconds / 86400) // 转换为天数
return `${days}d` // 只返回天
}
},
// 获取卡片的CSS类名
getCardClass: function (status) {
switch (status) {
case "running":
return "card-running"
case "waiting":
return "card-waiting"
case "error":
return "card-error"
case "off":
default:
return "card-off"
}
},
// 获取边框的CSS类名
getBorderClass: function (status) {
switch (status) {
case "running":
return "border-running";
case "waiting":
return "border-waiting";
case "error":
return "border-error";
case "off":
default:
return "border-off";
}
},
// 获取设备状态对应的CSS类名
getStatusClass: function (status) {
switch (status) {
case "running":
return "status-running";
case "waiting":
return "status-waiting";
case "error":
return "status-error";
case "off":
default:
return "status-off";
}
},
// 获取波浪效果的类名
getWaveClass: function (status) {
switch (status) {
case "running":
return "wave-running";
case "waiting":
return "wave-waiting";
case "error":
return "wave-error";
case "off":
default:
return "wave-off";
}
},
// 获取波浪高度
getWaveHeight: function (status, percentage) {
// 将百分比限制在10%-100%之间,确保即使是低百分比也有一定的水位可见
let height = percentage;
// 如果是故障或停机状态,固定显示50%
// if (status === "error" || status === "off") {
// height = 50;
// }
// 确保最小高度为10%,最大为100%
height = Math.min(Math.max(height, 10), 100);
return height;
},
// 获取设备状态对应的文本
getStatusText: function (status) {
switch (status) {
case "running":
return "运行中";
case "waiting":
return "等待中";
case "error":
return "故障中";
case "off":
default:
return "停机中";
}
},
// 获取设备维护信息状态颜色
getMaintenanceColor: function (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') {
return "rgba(80, 200, 120, 1)"
} else if (status == 'not_started' || status == 'timeout' || status == 'has_task') {
return "rgba(230, 188, 92, 0.7)"
} else {
return "rgba(255, 255, 255, 0.3)"
}
},
// 获取设备维护信息状态颜色
getMaintenanceText: function (status, type) {
if (type == '点检' || type == '保养') {
if (status == 'no_task') return "无任务"
if (status == 'not_started') return "未开始"
if (status == 'in_progress') return "进行中"
if (status == 'timeout') return "超时"
if (status == 'finished') return "已完成"
} else if (type == '维修') {
if (status == 'no_task') return "无维修"
if (status == 'in_progress') return "维修中"
if (status == 'finished') return "已完成"
} else if (type == '备件') {
if (status == 'no_task') return "无更换"
if (status == 'has_task') return "已更换"
}
return "未知"
},
// 处理小数位数方法
toFixedHandle: function (value, num) {
if (num === undefined) num = 4
if (value) {
let strValue = String(value);
if (strValue.split(".") && strValue.split(".").length > 1 || strValue.split(".")[1] && strValue.split(".")[1].length > num) {
strValue = Number(strValue).toFixed(num);
}
return Number(strValue);
} else {
return 0;
}
},
// 计算高度百分比
calculateHeight: function (hours) {
// 24小时对应整个高度(100%)
// 每4小时对应一个刻度区间,总共6个区间
// 计算每小时占的百分比:100% / 24 ≈ 4.167%
const heightPerHour = 100 / 24;
const percentage = hours * heightPerHour;
// 确保高度在0-100%之间
return Math.min(Math.max(percentage, 0), 100);
},
// 显示完整标题
showFullTitle: function (event, device) {
const fullTitle = device.name + " | " + device.code;
event.target.setAttribute("title", fullTitle);
},
// 获取ERR文字的CSS类名
getErrClass: function (status) {
if (status === "error") {
return "err-error";
}
return "";
},
// 获取OFF文字的CSS类名
getOffClass: function (status) {
if (status === "off") {
return "off-status";
}
return "";
},
},
});
</script>
<style>
/* 基础样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
color: #fff;
overflow: hidden;
}
#app {
width: 100vw;
height: 100vh;
padding: 20px;
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
background: url("/roke_workstation_api/static/html/routing/image/bg-ok.png") no-repeat center center fixed;
background-size: cover;
}
/* 玻璃态效果 */
.glass-effect {
background: rgba(22, 41, 60, 0.8);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 15px;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}
/* 标题样式 */
.dashboard-header {
width: 100vw;
margin: -20px -20px 10px -20px;
display: flex;
justify-content: center;
align-items: center;
}
.header-content {
width: 100%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.header-bg {
width: 100%;
height: auto;
}
.header-text {
position: absolute;
font-size: 28px;
font-weight: 450;
color: #fff;
text-shadow: 0 0 10px rgba(0, 195, 255, 0.5);
z-index: 1;
}
.hintText {
position: absolute;
font-size: 14px;
bottom: -5px;
right: 30px;
color: rgba(255, 255, 255, 0.85);
display: -webkit-box;
display: -webkit-flex;
display: -moz-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-webkit-align-items: center;
-moz-align-items: center;
-ms-flex-align: center;
align-items: center;
}
.hintText>* {
margin-left: 10px;
}
.hintText>*:first-child {
margin-left: 0;
}
/* 设备卡片容器 */
.device-cards-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
/* 增加最小宽度到300px */
gap: 10px;
}
/* 新设计设备卡片样式 */
.device-card.new-design {
position: relative;
padding: 10px 0;
display: flex;
flex-direction: column;
align-items: center;
min-height: 360px;
/* 稍微调整高度 */
border-radius: 12px;
transition: all 0.3s ease;
background-color: rgba(20, 22, 28, 0.5);
border: 1px solid rgba(255, 255, 255, 0.3);
/* 增强边框亮度 */
}
/* 卡片颜色 - 增强边框亮度 */
.card-running {
border: 1px solid rgba(78, 198, 138, 0.8);
/* 增强亮度 */
}
.card-waiting {
border: 1px solid rgba(235, 186, 22, 0.8);
/* 增强亮度 */
}
.card-error {
border: 1px solid rgba(235, 86, 86, 0.8);
/* 增强亮度 */
}
.card-off {
border: 1px solid rgba(144, 147, 153, 0.8);
/* 增强亮度 */
}
.device-card.new-design:hover {
transform: translateY(-2px);
border-color: rgba(78, 198, 138, 1);
/* 悬停时完全不透明 */
}
/* 设备名称 - 左上角 */
.device-name {
width: 100%;
font-size: 14px;
font-weight: bold;
color: #fff;
padding: 0px 10px;
align-self: flex-start;
/* 增加最大宽度 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
/* 设备状态标签 - 右上角 */
.device-status-tag {
display: none;
position: absolute;
top: 10px;
right: 10px;
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
color: #fff;
min-width: 60px;
text-align: center;
}
/* 移除状态标签的阴影 */
.status-running {
background-color: rgba(78, 198, 138, 0.9);
}
.status-waiting {
background-color: rgba(235, 186, 22, 0.9);
}
.status-error {
background-color: rgba(235, 86, 86, 0.9);
}
.status-off {
background-color: rgba(144, 147, 153, 0.9);
}
/* 设备状态信息 */
.device-status-info {
font-size: 12px;
color: rgba(255, 255, 255, 0.85);
text-align: center;
line-height: 1.5;
}
/* 虚线分割 - 全宽 */
.full-width-dashed-divider {
width: 100%;
height: 1px;
border-top: 1px dashed rgba(255, 255, 255, 0.6);
/* 增强虚线亮度 */
margin: 5px 0;
}
/* 维护状态区域 - 竖向排列 */
.maintenance-status-vertical {
width: 100%;
display: flex;
justify-content: space-between;
/* 改回space-between,让四个区域平均分配 */
align-items: stretch;
height: 110px;
/* 稍微减小高度,使更紧凑 */
position: relative;
/* 添加相对定位 */
padding: 0;
/* 移除左右内边距,让标签占满整个宽度 */
}
.maintenance-item-vertical {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
/* 进一步减小左右内边距 */
flex: 1;
/* 让每个区域平均分配宽度 */
position: relative;
height: 100%;
}
/* 新增:图标和文字的水平容器 */
.maintenance-header {
display: flex;
align-items: center;
margin-bottom: 5px;
}
/* 维护状态竖向虚线分割 - 改为竖向分割线,从上到下,实现密闭效果 */
/* 已移除 .vertical-dashed-divider 样式,改用伪元素实现 */
/* 为每个虚线分割器设置具体位置 - 增强亮度和确保连接 */
.maintenance-item-vertical:nth-child(1)::after,
.maintenance-item-vertical:nth-child(2)::after,
.maintenance-item-vertical:nth-child(3)::after {
content: "";
position: absolute;
right: 0;
/* 改为右边缘 */
top: -12px;
/* 向上延伸到横向虚线 */
width: 1px;
height: calc(100% + 39px);
/* 进一步增加高度:区域高度 + margin-bottom(15px) + 下方虚线margin(12px) + 补偿(12px) */
border-right: 1px dashed rgba(255, 255, 255, 0.6);
/* 改为右边框虚线 */
z-index: 2;
}
.maintenance-icon {
width: 26px;
/* 稍微增大 */
height: 26px;
/* 稍微增大 */
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.3);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
/* 增大字体 */
font-weight: bold;
color: #fff;
margin-bottom: 0;
/* 移除底部边距 */
}
/* 为不同图标添加背景色 */
.maintenance-icon-daily {
background-color: rgba(52, 152, 219, 0.8);
/* 蓝色 - 日检 */
}
.maintenance-icon-weekly {
background-color: rgba(46, 204, 113, 0.8);
/* 绿色 - 周保养 */
}
.maintenance-icon-repair {
background-color: rgba(231, 76, 60, 0.8);
/* 红色 - 维修 */
}
.maintenance-icon-replace {
background-color: rgba(230, 126, 34, 0.8);
/* 橙色 - 备件 */
}
.maintenance-label {
font-size: 11px;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 0;
/* 移除底部边距 */
text-align: center;
margin-left: 5px;
}
.maintenance-status-badge {
padding: 8px 8px;
/* 稍微减小内边距保持紧凑 */
border-radius: 6px;
font-size: 11px;
font-weight: bold;
color: #fff;
text-align: center;
min-width: 50px;
/* 稍微减小宽度 */
min-height: 55px;
/* 稍微减小高度保持紧凑 */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
line-height: 1.2;
letter-spacing: 0.1em;
flex: 1;
}
.maintenance-status-badge span {
display: block;
margin: 1px 0;
writing-mode: vertical-rl;
}
/* 统计信息区域 */
.statistics-info {
display: flex;
justify-content: space-between;
width: 100%;
padding: 0 10px;
/* 增加间距 */
}
.stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 8px;
/* 减小内边距,降低高度 */
border-radius: 8px;
background: linear-gradient(135deg, rgba(52, 152, 219, 0.6), rgba(41, 128, 185, 0.6));
min-height: 65px;
/* 减小最小高度 */
position: relative;
/* 添加相对定位以便放置叹号 */
}
.stat-item:nth-child(2) {
margin: 0 10px;
background: linear-gradient(135deg, rgba(46, 204, 113, 0.6), rgba(39, 174, 96, 0.6));
}
.stat-item:nth-child(3) {
background: linear-gradient(135deg, rgba(26, 188, 156, 0.6), rgba(22, 160, 133, 0.6));
}
.stat-icon {
position: absolute;
/* 改为绝对定位 */
top: 5px;
/* 距离顶部5px */
right: 5px;
/* 距离右边5px */
font-size: 14px;
/* 调整图标字体大小 */
color: rgba(255, 255, 255, 0.7);
/* 调整透明度 */
cursor: pointer;
}
.stat-number {
font-size: 20px;
/* 稍微减小数字字体 */
font-weight: bold;
color: #fff;
margin: 8px 0 4px 0;
/* 调整上下边距 */
}
.stat-label {
font-size: 12px;
/* 调整标签字体大小 */
color: rgba(255, 255, 255, 0.9);
margin-top: 0;
/* 移除顶部边距 */
}
/* 设备波纹容器 */
.device-wave-container {
width: 100%;
height: 100px;
/* 减小高度 */
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
padding: 0 15px;
/* 增加左右内边距,让标签不贴边 */
}
/* 左侧状态 */
.left-status {
display: flex;
flex-direction: column;
flex: 0 0 auto;
/* 改为固定宽度 */
width: 80px;
/* 设置固定宽度 */
}
/* 右侧状态 */
.right-status {
display: flex;
flex-direction: column;
flex: 0 0 auto;
/* 改为固定宽度 */
width: 80px;
/* 设置固定宽度 */
align-items: flex-end;
/* 右对齐 */
}
/* 中间OEE区域 */
.center-oee {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
flex: 1;
/* 占据剩余空间 */
}
/* 侧边状态项 */
.time-item-side {
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;
}
/* 右侧状态项布局调整 */
.right-status .time-item-side {
justify-content: flex-end;
/* 右对齐整个项目 */
}
.time-label {
padding: 3px 8px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
color: #fff;
min-width: 30px;
text-align: center;
}
.time-value {
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;
}
/* 移动端虚线样式调整 */
.maintenance-item-vertical:nth-child(1)::after,
.maintenance-item-vertical:nth-child(2)::after,
.maintenance-item-vertical:nth-child(3)::after {
height: calc(100% + 10px);
/* 移动端增加虚线高度,确保完全闭合 */
top: -5px;
border-right: 1px dashed rgba(255, 255, 255, 0.6);
/* 保持右边框虚线 */
}
.maintenance-status-vertical {
height: 95px;
/* 移动端调整高度 */
padding: 0;
/* 移动端也移除内边距 */
}
/* OEE文字 */
.oee-text {
position: absolute;
top: 20px;
/* 调整位置 */
font-size: 14px;
/* 缩小字体 */
font-weight: bold;
color: #fff;
z-index: 3;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
/* 百分比文字 */
.percentage-text {
position: absolute;
top: 35px;
/* 调整位置 */
font-size: 20px;
/* 缩小字体 */
font-weight: bold;
color: #fff;
z-index: 3;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
/* 圆形容器 */
.circle-container {
width: 80px;
/* 从100px减小到80px */
height: 80px;
/* 从100px减小到80px */
border-radius: 50%;
overflow: hidden;
position: relative;
background-color: rgba(10, 12, 15, 0.8);
}
/* 圆形容器边框 */
.border-running {
border: 2px solid rgba(78, 198, 138, 0.9);
}
.border-waiting {
border: 2px solid rgba(235, 186, 22, 0.9);
}
.border-error {
border: 2px solid rgba(235, 86, 86, 0.9);
}
.border-off {
border: 2px solid rgba(144, 147, 153, 0.9);
}
/* 水波纹 */
.water-wave {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
transition: height 0.7s ease;
overflow: hidden;
border-radius: 0 0 50px 50px;
}
/* 水波纹颜色 */
.wave-running {
background-color: rgba(78, 198, 138, 0.7);
}
.wave-waiting {
background-color: rgba(235, 186, 22, 0.7);
}
.wave-error {
background-color: rgba(235, 86, 86, 0.7);
}
.wave-off {
background-color: rgba(144, 147, 153, 0.7);
}
/* 水波纹内容 - 设置相对定位以包含波浪元素 */
.water-wave-content {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
/* 波浪容器 - 水平波浪效果 */
.water-wave-ripple {
width: 200%;
height: 100%;
position: absolute;
bottom: 0;
left: 0;
background: transparent;
z-index: 2;
}
/* 波浪图形 - 使用SVG背景实现波浪形状 */
.water-wave-ripple::before {
content: "";
position: absolute;
top: -12px;
left: 0;
width: 100%;
height: 25px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 120' preserveAspectRatio='none'%3E%3Cpath fill='white' opacity='0.5' d='M0,0V46.29c47.79,22.2,103.59,32.17,158,28,70.36-5.37,136.33-33.31,206.8-37.5C438.64,32.43,512.34,53.67,583,72.05c69.27,18,138.3,24.88,209.4,13.08,36.15-6,69.85-17.84,104.45-29.34C989.49,25,1113,-14.29,1200,52.47V0Z'%3E%3C/path%3E%3C/svg%3E");
background-size: 100% 100%;
-webkit-animation: wave-horizontal 6s linear infinite;
animation: wave-horizontal 6s linear infinite;
}
/* 第二层波浪 - 与第一层错开,增强效果 */
.water-wave-ripple::after {
content: "";
position: absolute;
top: -14px;
left: 0;
width: 100%;
height: 28px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 120' preserveAspectRatio='none'%3E%3Cpath fill='white' opacity='0.3' d='M0,0V46.29c47.79,22.2,103.59,32.17,158,28,70.36-5.37,136.33-33.31,206.8-37.5C438.64,32.43,512.34,53.67,583,72.05c69.27,18,138.3,24.88,209.4,13.08,36.15-6,69.85-17.84,104.45-29.34C989.49,25,1113,-14.29,1200,52.47V0Z'%3E%3C/path%3E%3C/svg%3E");
background-size: 100% 100%;
-webkit-animation: wave-horizontal 8s linear infinite;
animation: wave-horizontal 8s linear infinite;
animation-delay: -2s;
}
/* 为不同状态设置不同波浪颜色 */
.wave-running .water-wave-ripple::before,
.wave-running .water-wave-ripple::after {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 120' preserveAspectRatio='none'%3E%3Cpath fill='%234EC68A' opacity='0.5' d='M0,0V46.29c47.79,22.2,103.59,32.17,158,28,70.36-5.37,136.33-33.31,206.8-37.5C438.64,32.43,512.34,53.67,583,72.05c69.27,18,138.3,24.88,209.4,13.08,36.15-6,69.85-17.84,104.45-29.34C989.49,25,1113,-14.29,1200,52.47V0Z'%3E%3C/path%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 120' preserveAspectRatio='none'%3E%3Cpath fill='%234EC68A' opacity='0.3' d='M0,0V46.29c47.79,22.2,103.59,32.17,158,28,70.36-5.37,136.33-33.31,206.8-37.5C438.64,32.43,512.34,53.67,583,72.05c69.27,18,138.3,24.88,209.4,13.08,36.15-6,69.85-17.84,104.45-29.34C989.49,25,1113,-14.29,1200,52.47V0Z'%3E%3C/path%3E%3C/svg%3E");
}
.wave-waiting .water-wave-ripple::before,
.wave-waiting .water-wave-ripple::after {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 120' preserveAspectRatio='none'%3E%3Cpath fill='%23EBBA16' opacity='0.5' d='M0,0V46.29c47.79,22.2,103.59,32.17,158,28,70.36-5.37,136.33-33.31,206.8-37.5C438.64,32.43,512.34,53.67,583,72.05c69.27,18,138.3,24.88,209.4,13.08,36.15-6,69.85-17.84,104.45-29.34C989.49,25,1113,-14.29,1200,52.47V0Z'%3E%3C/path%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 120' preserveAspectRatio='none'%3E%3Cpath fill='%23EBBA16' opacity='0.3' d='M0,0V46.29c47.79,22.2,103.59,32.17,158,28,70.36-5.37,136.33-33.31,206.8-37.5C438.64,32.43,512.34,53.67,583,72.05c69.27,18,138.3,24.88,209.4,13.08,36.15-6,69.85-17.84,104.45-29.34C989.49,25,1113,-14.29,1200,52.47V0Z'%3E%3C/path%3E%3C/svg%3E");
}
.wave-error .water-wave-ripple::before,
.wave-error .water-wave-ripple::after {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 120' preserveAspectRatio='none'%3E%3Cpath fill='%23EB5656' opacity='0.5' d='M0,0V46.29c47.79,22.2,103.59,32.17,158,28,70.36-5.37,136.33-33.31,206.8-37.5C438.64,32.43,512.34,53.67,583,72.05c69.27,18,138.3,24.88,209.4,13.08,36.15-6,69.85-17.84,104.45-29.34C989.49,25,1113,-14.29,1200,52.47V0Z'%3E%3C/path%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 120' preserveAspectRatio='none'%3E%3Cpath fill='%23EB5656' opacity='0.3' d='M0,0V46.29c47.79,22.2,103.59,32.17,158,28,70.36-5.37,136.33-33.31,206.8-37.5C438.64,32.43,512.34,53.67,583,72.05c69.27,18,138.3,24.88,209.4,13.08,36.15-6,69.85-17.84,104.45-29.34C989.49,25,1113,-14.29,1200,52.47V0Z'%3E%3C/path%3E%3C/svg%3E");
}
.wave-off .water-wave-ripple::before,
.wave-off .water-wave-ripple::after {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 120' preserveAspectRatio='none'%3E%3Cpath fill='%23909399' opacity='0.5' d='M0,0V46.29c47.79,22.2,103.59,32.17,158,28,70.36-5.37,136.33-33.31,206.8-37.5C438.64,32.43,512.34,53.67,583,72.05c69.27,18,138.3,24.88,209.4,13.08,36.15-6,69.85-17.84,104.45-29.34C989.49,25,1113,-14.29,1200,52.47V0Z'%3E%3C/path%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 120' preserveAspectRatio='none'%3E%3Cpath fill='%23909399' opacity='0.3' d='M0,0V46.29c47.79,22.2,103.59,32.17,158,28,70.36-5.37,136.33-33.31,206.8-37.5C438.64,32.43,512.34,53.67,583,72.05c69.27,18,138.3,24.88,209.4,13.08,36.15-6,69.85-17.84,104.45-29.34C989.49,25,1113,-14.29,1200,52.47V0Z'%3E%3C/path%3E%3C/svg%3E");
}
/* 顶部发光效果 */
.water-wave::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 8px;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.7), transparent);
z-index: 10;
}
/* 响应式调整 */
@media (max-width: 768px) {
.device-cards-container {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
/* 移动端调整最小宽度 */
}
.device-card.new-design {
min-height: 320px;
/* 移动端调整高度 */
padding: 10px;
}
.device-wave-container {
flex-direction: column;
height: auto;
gap: 10px;
}
.left-status,
.right-status {
flex-direction: row;
justify-content: space-around;
width: 100%;
}
.center-oee {
order: -1;
margin-bottom: 10px;
}
.circle-container {
width: 70px;
height: 70px;
}
.oee-text {
top: 15px;
font-size: 12px;
}
.percentage-text {
top: 30px;
font-size: 16px;
}
.time-label {
font-size: 9px;
padding: 2px 6px;
}
.time-value {
font-size: 10px;
color: rgba(255, 255, 255, 0.9);
font-weight: 500;
}
.maintenance-item-vertical {
padding: 5px 3px;
/* 移动端进一步减小内边距 */
max-width: 70px;
/* 移动端减小最大宽度 */
}
.maintenance-header {
margin-bottom: 6px;
/* 减小间距 */
}
.maintenance-icon {
width: 20px;
height: 20px;
font-size: 10px;
}
.maintenance-label {
font-size: 10px;
}
.maintenance-status-badge {
font-size: 10px;
padding: 10px 6px;
/* 移动端调整内边距 */
min-width: 40px;
/* 减小宽度 */
min-height: 45px;
/* 减小高度 */
}
.maintenance-status-badge span {
margin: 0.5px 0;
}
/* 移动端设备波纹容器调整 */
.device-wave-container {
padding: 0 15px;
/* 移动端调整内边距 */
}
.left-status,
.right-status {
width: 70px;
/* 移动端减小宽度 */
}
/* 移动端统计标签样式 */
.statistics-info {
gap: 8px;
padding: 0 8px;
/* 移动端减小间距 */
}
.stat-item {
padding: 10px 6px;
/* 移动端进一步减小内边距 */
min-height: 55px;
/* 移动端减小最小高度 */
}
.stat-icon {
font-size: 12px;
/* 移动端调整图标大小 */
top: 3px;
/* 移动端调整位置 */
right: 3px;
}
.stat-number {
font-size: 16px;
/* 移动端调整数字大小 */
margin: 6px 0 3px 0;
/* 移动端调整边距 */
}
.stat-label {
font-size: 10px;
/* 移动端调整标签字体 */
}
/* 移动端全宽虚线调整 */
.full-width-dashed-divider {
border-top: 1px dashed rgba(255, 255, 255, 0.6);
/* 保持增强的亮度 */
width: calc(100% + 24px);
margin-left: -12px;
margin-right: -12px;
}
}
/* 为Safari和iOS设备的特别修复 */
@supports (-webkit-appearance: none) {
.water-wave-ripple::before,
.water-wave-ripple::after {
-webkit-animation-play-state: running;
animation-play-state: running;
}
}
/* 波浪水平移动动画 */
@-webkit-keyframes wave-horizontal {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
@keyframes wave-horizontal {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
.flxe_sty {
width: 50%;
display: flex;
align-items: center;
font-size: 10px;
color: #fff;
margin-top: 5px;
.flxe_label_sty {
margin-right: 2px;
font-weight: bold;
color: #000;
display: flex;
justify-content: center;
align-items: center;
font-size: 10px;
width: 30px;
height: auto;
border-radius: 2px;
margin-left: 10px;
}
}
</style>
</html>
\ No newline at end of file
<odoo>
<data>
<!--
<template id="listing">
<ul>
<li t-foreach="objects" t-as="object">
<a t-attf-href="#{ root }/objects/#{ object.id }">
<t t-esc="object.display_name"/>
</a>
</li>
</ul>
</template>
<template id="object">
<h1><t t-esc="object.display_name"/></h1>
<dl>
<t t-foreach="object._fields" t-as="field">
<dt><t t-esc="field"/></dt>
<dd><t t-esc="object[field]"/></dd>
</t>
</dl>
</template>
-->
</data>
</odoo>
\ No newline at end of file
<odoo>
<data>
<!-- explicit list view definition -->
<!--
<record model="ir.ui.view" id="jnzg_project.list">
<field name="name">jnzg_project list</field>
<field name="model">jnzg_project.jnzg_project</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="value"/>
<field name="value2"/>
</tree>
</field>
</record>
-->
<!-- actions opening views on models -->
<!--
<record model="ir.actions.act_window" id="jnzg_project.action_window">
<field name="name">jnzg_project window</field>
<field name="res_model">jnzg_project.jnzg_project</field>
<field name="view_mode">tree,form</field>
</record>
-->
<!-- server action to the one above -->
<!--
<record model="ir.actions.server" id="jnzg_project.action_server">
<field name="name">jnzg_project server</field>
<field name="model_id" ref="model_jnzg_project_jnzg_project"/>
<field name="state">code</field>
<field name="code">
action = {
"type": "ir.actions.act_window",
"view_mode": "tree,form",
"res_model": model._name,
}
</field>
</record>
-->
<!-- Top menu item -->
<!--
<menuitem name="jnzg_project" id="jnzg_project.menu_root"/>
-->
<!-- menu categories -->
<!--
<menuitem name="Menu 1" id="jnzg_project.menu_1" parent="jnzg_project.menu_root"/>
<menuitem name="Menu 2" id="jnzg_project.menu_2" parent="jnzg_project.menu_root"/>
-->
<!-- actions -->
<!--
<menuitem name="List" id="jnzg_project.menu_1_list" parent="jnzg_project.menu_1"
action="jnzg_project.action_window"/>
<menuitem name="Server to list" id="jnzg_project" parent="jnzg_project.menu_2"
action="jnzg_project.action_server"/>
-->
</data>
</odoo>
\ No newline at end of file
...@@ -30,6 +30,14 @@ class RokeMesThreeColourLight(http.Controller): ...@@ -30,6 +30,14 @@ class RokeMesThreeColourLight(http.Controller):
data = {"code": 1, "message": "请求通过", "data": {"factory_code": factory_code}} data = {"code": 1, "message": "请求通过", "data": {"factory_code": factory_code}}
template = env.get_template('oee_analysis.html') template = env.get_template('oee_analysis.html')
return template.render(data) return template.render(data)
@http.route("/roke/three_color_light/oee_time_sequence_table", type="http", auth='none', cors='*', csrf=False)
def oee_time_sequence_table(self, **kwargs):
_self = http.request
factory_code = http.request.env(user=SUPERUSER_ID)['ir.config_parameter'].get_param('database.uuid', default="")
data = {"code": 1, "message": "请求通过", "data": {"factory_code": factory_code}}
template = env.get_template('oee_time_sequence_table.html')
return template.render(data)
@http.route('/roke/workstation/plant/tree', type='json', auth='none', csrf=False, cors="*") @http.route('/roke/workstation/plant/tree', type='json', auth='none', csrf=False, cors="*")
def get_roke_workstation_plant(self): def get_roke_workstation_plant(self):
......
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>OEE时间利用率</title>
<meta
content="width=device-width,initial-scale=1.0, maximum-scale=1.0,user-scalable=0"
name="viewport"
/>
<!-- 本地资源引用 -->
<link rel="stylesheet" href="/roke_workstation_api/static/html/routing/element-ui/index.css" />
<script src="/roke_workstation_api/static/html/routing/js/echarts.min.js"></script>
<script src="/roke_workstation_api/static/html/routing/js/moment.min.js"></script>
<script src="/roke_workstation_api/static/html/routing/js/vue.js"></script>
<script src="/roke_workstation_api/static/html/routing/js/axios.min.js"></script>
<script src="/roke_workstation_api/static/html/routing/element-ui/index.js"></script>
</head>
<body id="bodyId" style="display: none">
<div id="app">
<!-- v-show="performanceShow" -->
<!-- v-loading.body.fullscreen.lock="idle_rate_loading" -->
<!-- 新增:多设备空闲率图表 -->
<!-- 多设备图表的设备选择器 -->
<div class="chart-select-wrapper">
<span class="select-label">设备</span>
<el-select
v-model="selectedMultiDevice"
placeholder="请选择设备"
size="small"
clearable
@change="multiDeviceChange"
class="chart-select"
>
<el-option
v-for="(item,index) in deviceList"
:label="item.name"
:value="item.code"
:key="index"
>
</el-option>
</el-select>
</div>
<div class="multi-utilization-chart glass-effect">
<div class="section-title">
<div class="title-bar"></div>
<span class="title-text"
>[[ selectedMultiDevice ? '设备空闲率概览' : '多设备空闲率对比' ]]</span
>
</div>
<div
id="multiUtilizationChart"
ref="multiUtilizationChart"
style="width: 100%; height: 280px"
></div>
</div>
<!-- 单设备图表的设备选择器 -->
<div class="chart-select-wrapper">
<span class="select-label">设备</span>
<el-select
v-model="selectedDevice"
placeholder="请选择设备"
size="small"
clearable
@change="selDeviceChange"
class="chart-select"
>
<el-option
v-for="(item,index) in deviceList"
:label="item.name"
:value="item.code"
:key="index"
>
</el-option>
</el-select>
</div>
<div class="utilization-chart glass-effect" v-if="selectedDevice">
<div class="section-title">
<div class="title-bar"></div>
<span class="title-text">设备空闲率</span>
</div>
<div id="utilizationChart" ref="utilizationChart" style="width: 100%; height: 280px"></div>
</div>
<!-- 底部状态图 -->
<!-- v-loading.body.fullscreen.lock="chart_loading" -->
<!-- 柱状图的设备选择器 -->
<div class="chart-select-wrapper">
<span class="select-label">设备</span>
<el-select
v-model="selectedBarChartDevice"
placeholder="请选择设备"
size="small"
@change="barChartDeviceChange"
class="chart-select"
>
<el-option
v-for="(item,index) in deviceList"
:label="item.name"
:value="item.code"
:key="index"
>
</el-option>
</el-select>
</div>
<div class="status-chart glass-effect">
<div class="section-title">
<div class="title-bar"></div>
<span class="title-text">日均运行时间统计</span>
</div>
<div class="chart-container">
<!-- 图例区域 -->
<div class="flex_legend">
<div class="single_sty">
<div style="background-color: red"></div>
<span>故障</span>
</div>
<div class="single_sty">
<div style="background-color: yellow"></div>
<span>等待</span>
</div>
<div class="single_sty">
<div style="background-color: green"></div>
<span>运行</span>
</div>
<div class="single_sty">
<div style="background-color: gray"></div>
<span>关机</span>
</div>
</div>
<!-- Y轴刻度 -->
<div class="y-axis">
<span v-for="(value, index) in yAxisValues" :key="index" class="y-axis-label">
[[value]]
<span class="unit">%</span>
</span>
</div>
<!-- 图表主体区域 -->
<div class="chart-content">
<div class="horizontal-lines">
<div v-for="(value, index) in yAxisValues" :key="index" class="horizontal-line"></div>
</div>
<!-- 改为单个容器包含柱状图和X轴标签 -->
<div class="columns-container">
<div
class="column-with-label"
v-for="(item, dayIndex) in pickingOrderList"
:key="dayIndex"
>
<!-- 柱状图部分 -->
<div class="column-group">
<div class="column-stack">
<div
class="column-segment"
v-for="(segment, stackIndex) in item"
:key="stackIndex"
:style="{
'height': calculateHeight(segment.duration_percentage) + '%',
'background-color': segment.state,
'margin-top': '0px'
}"
>
<el-tooltip
effect="dark"
:content="calculateHeight(segment.duration_percentage) + '%'"
placement="top"
>
<div style="height: 100%; width: 100%"></div>
</el-tooltip>
</div>
</div>
</div>
<!-- X轴标签部分 - 直接绑定在柱状图下方 -->
<div class="x-axis-label">[[ latestDateList[dayIndex] ]]</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
<script>
// 发送消息给父页面(关闭odoo的菜单弹窗)
document.addEventListener("click", () => {
window.parent.postMessage("hidePopover", "*");
});
let vue = new Vue({
el: "#app",
delimiters: ["[[", "]]"], // 替换原本vue的[[ key ]]取值方式(与odoo使用的jinja2冲突问题)
data() {
return {
windowHeight: window.innerHeight, // 窗口高度
// baseURL: "https://workstation.rokeris.com", // 基地址 https://workstation.rokeris.com
// baseURL: "http://qdry.dws.rokecloud.com",
baseURL: "", //
localURL: "https://dws-platform.xbg.rokeris.com/dev-api", // 基地址 https://workstation.rokeris.com
idle_rate_loading: false, // 加载效果
chart_loading: false,
deviceList: [], // 设备列表
selectedDevice: null, // 选中的设备
yAxisValues: ["100", "80", "60", "40", "20", "0"], // Y轴刻度值
pickingOrderList: [], // 拣选单列表
dateList: [], // 日期列表
start_time: "", // 开始时间
end_time: "", // 结束时间
utilizationChart: null, // 设备空闲率图表实例变量
multiUtilizationChart: null, // 新增:多设备空闲率图表实例变量
plan_time_list: null,
latestDateList: [],
factory_code: "8d8dec6e-0d44-11f0-9692-00163e04c506",
selectedMultiDevice: null, // 新增:多设备图表选中的设备
selectedBarChartDevice: null, // 新增:柱状图选中的设备
};
},
computed: {
// 选中设备的信息
selDeviceInfo() {
return this.deviceList.find((item) => item.id == this.selectedDevice);
},
},
created() {
if (this.getUrlSearch("factory_code")) {
this.factory_code = this.getUrlSearch("factory_code"); //截取url后面的参数
}
this.get_device_list();
// 先设置好日期范围,以便后续使用
this.latestDateList = this.getLastTenDays();
this.$nextTick(() => {
document.getElementById("bodyId").style.display = "block";
// 在DOM渲染后初始化图表
if (this.$refs.utilizationChart) {
this.initUtilizationChart();
}
// 初始化多设备空闲率图表
if (this.$refs.multiUtilizationChart) {
this.initMultiUtilizationChart();
}
});
window.addEventListener("resize", this.handleResize);
// 获取最后指定天数的日期
this.dateList = this.getLastAssignDays();
},
methods: {
// 获取所有已绑定设备数据
async getAllEquipmentData(data_list) {
try {
// 发送请求获取所有已绑定设备数据
const response = await axios({
method: "post",
url: this.baseURL + "/roke/workstation/equipment/get_equipment_data",
data: {},
headers: {
"Content-Type": "application/json",
},
});
// 处理JSON-RPC格式的响应
if (response.data && response.data.result && response.data.result.code === 0) {
// 存储所有设备数据
data_list.forEach((data_item) => {
response.data.result.data.forEach((item) => {
if (data_item.device_code == item.code) {
this.deviceList.push(item);
}
});
});
// 为柱状图设置默认设备
this.selectedBarChartDevice = this.deviceList[0].code;
this.get_plan_time();
this.get_daily_running_time();
console.log("获取到所有已绑定设备数据:", this.deviceList.length, "条");
} else {
const errorMsg = response.data.result
? response.data.result.message
: "获取所有已绑定设备数据失败";
throw new Error(errorMsg);
}
} catch (error) {
console.error("获取所有已绑定设备数据出错:", error);
throw error;
}
},
// 获取日均运行时间统计
get_daily_running_time() {
this.idle_rate_loading = true;
axios({
method: "post",
url: this.localURL + "/public/device_efficiency/daily_running_time",
data: {
device_code: this.selectedBarChartDevice,
},
headers: { "Content-Type": "application/json" },
})
.then((res) => {
if (res.data.code == 200) {
this.pickingOrderList = res.data.data.daily_running_list;
// 调用一致性检查
this.ensureAlignmentData();
} else {
this.$message.error("获取日均运行时间失败");
}
this.idle_rate_loading = false;
})
.catch((err) => {
this.idle_rate_loading = false;
this.$message.error("获取日均运行时间接口捕获到错误");
});
},
// 获取设备列表
get_device_list() {
axios({
method: "get",
url: this.localURL + "/public/device_efficiency/device_list/" + this.factory_code,
data: {},
headers: { "Content-Type": "application/json" },
})
.then((res) => {
if (res.data.code == 200) {
// this.deviceList = res.data.data
this.getAllEquipmentData(res.data.data);
} else {
this.$message.error("设备列表数据获取失败!");
}
})
.catch((err) => {});
},
// 获取设备编号 设备计划运行时间 plan_time_list 参数
get_plan_time() {
axios({
method: "post",
url: this.baseURL + "/roke/workstation/equipment/get_plan_time",
data: {},
headers: { "Content-Type": "application/json" },
})
.then((res) => {
if (res.data.result.code == 0) {
this.plan_time_list = res.data.result.data;
// 只有在选择了设备时才调用单设备空闲率接口
if (this.selectedDevice) {
this.get_series_utilization_rate();
}
// 如果多设备图表已初始化且没有选择设备,则调用多设备接口
if (this.multiUtilizationChart && !this.selectedMultiDevice) {
this.get_series_utilization_rate_top_5();
}
}
})
.catch((err) => {});
},
// 获取OEE报表:近十天设备空闲率
get_series_utilization_rate() {
this.chart_loading = true;
axios({
method: "post",
url: this.localURL + "/public/device_efficiency/series_utilization_rate",
data: {
device_code: this.selectedDevice,
plan_time_list: this.plan_time_list,
},
headers: { "Content-Type": "application/json" },
})
.then((res) => {
if (res.data.code == 200) {
this.initUtilizationChart(res.data.data);
} else {
this.$message.error("设备空闲率获取失败!");
}
this.chart_loading = false;
})
.catch((err) => {
this.chart_loading = false;
this.$message.error("设备空闲率捕获到错误!");
});
},
// 选择设备改变事件
selDeviceChange(el) {
// 只有在选择了设备时才调用接口
if (this.selectedDevice) {
this.get_series_utilization_rate();
}
},
// 处理窗口大小变化修改图表大小
handleResize() {
if (this.utilizationChart) this.utilizationChart.resize();
if (this.multiUtilizationChart) this.multiUtilizationChart.resize();
},
// 初始化设备空闲率图表
initUtilizationChart(data = []) {
// 延迟初始化,确保DOM已完全渲染
setTimeout(() => {
// 确保元素存在
const chartDom = document.getElementById("utilizationChart");
if (!chartDom) {
console.error("找不到utilizationChart元素");
return;
}
// 如果已有实例,先销毁
if (this.utilizationChart) {
this.utilizationChart.dispose();
}
// 初始化图表
this.utilizationChart = echarts.init(chartDom);
const option = {
grid: {
left: "3%",
right: "4%",
bottom: "3%",
top: "8%",
containLabel: true,
},
tooltip: {
trigger: "axis",
formatter: "{b} : {c}%",
},
xAxis: {
type: "category",
data: this.latestDateList,
axisLine: {
lineStyle: {
color: "#fff",
},
},
axisLabel: {
color: "#fff",
rotate: this.latestDateList[0].length > 5 ? 30 : 0, // 如果日期文本较长则旋转标签
},
},
yAxis: {
type: "value",
min: 0,
max: 100,
interval: 20,
axisLine: {
show: false,
},
axisTick: {
show: false,
},
splitLine: {
lineStyle: {
color: "rgba(255, 255, 255, 0.1)",
},
},
axisLabel: {
color: "#fff",
formatter: "{value}%",
},
},
series: [
{
name: "设备空闲率",
type: "line",
smooth: true,
symbol: "circle",
symbolSize: 8,
label: {
show: true,
position: "top",
color: "#FFFFFF",
fontSize: 12,
formatter: "{c}%",
},
lineStyle: {
color: "#36cfc9",
width: 3,
},
itemStyle: {
color: "#36cfc9",
},
data: data.map((item) => (item > 100 ? 100 : item)),
},
],
};
this.utilizationChart.setOption(option);
// 手动触发resize以确保正确渲染
this.utilizationChart.resize();
}, 300); // 延迟300ms初始化
},
// 计算高度百分比
calculateHeight(hours) {
// 直接使用百分比值
// yAxisValues = ["100", "80", "60", "40", "20", "0"],每个刻度区间是20%
// 确保高度在0-100%之间
return Math.min(Math.max(hours, 0), 100);
},
// 获取最后指定天数的日期
getLastAssignDays(num = 10) {
const dates = [];
for (let i = num - 1; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
dates.push(`${year}-${month}-${day}`);
}
this.start_time = dates[0]; // 第一天
this.end_time = dates[dates.length - 1]; // 最后一天
return dates;
},
// 改进日期格式化方法
getLastTenDays() {
const dates = [];
const formattedDates = [];
const endDate = new Date();
this.end_time = moment(endDate).format("YYYY-MM-DD");
const startDate = new Date();
startDate.setDate(startDate.getDate() - 9);
this.start_time = moment(startDate).format("YYYY-MM-DD");
for (let i = 0; i < 10; i++) {
const date = new Date(startDate);
date.setDate(startDate.getDate() + i);
dates.push(date);
// 修改为MM-DD格式
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
formattedDates.push(`${month}-${day}`);
}
this.dateList = dates;
return formattedDates;
},
// 在methods中添加
ensureAlignmentData() {
// 确保X轴标签数量与柱状图数量一致
this.$nextTick(() => {
if (this.pickingOrderList.length !== this.latestDateList.length) {
console.warn(
"柱状图数量与X轴标签数量不一致",
"柱状图:",
this.pickingOrderList.length,
"X轴标签:",
this.latestDateList.length
);
}
});
},
// 通过网址跳转过来的页面,截取后面的参数
getUrlSearch(name) {
// 未传参,返回空
if (!name) return "";
// 查询参数:先通过search取值,如果取不到就通过hash来取
var after = window.location.search;
after = after.substr(1) || window.location.hash.split("?")[1];
// 地址栏URL没有查询参数,返回空
if (!after) return null;
// 如果查询参数中没有"name",返回空
if (after.indexOf(name) === -1) return null;
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
// 当地址栏参数存在中文时,需要解码,不然会乱码
var r = decodeURI(after).match(reg);
// 如果url中"name"没有值,返回空
if (!r) return null;
return r[2];
},
// 新增:初始化多设备空闲率图表
initMultiUtilizationChart() {
setTimeout(() => {
const chartDom = document.getElementById("multiUtilizationChart");
if (!chartDom) {
console.error("找不到multiUtilizationChart元素");
return;
}
if (this.multiUtilizationChart) {
this.multiUtilizationChart.dispose();
}
this.multiUtilizationChart = echarts.init(chartDom);
// 根据selectedMultiDevice来决定显示内容
if (!this.selectedMultiDevice) {
this.showMultiDeviceChart();
} else {
this.showSingleDeviceChart();
}
}, 300);
},
// 新增:显示多设备图表
showMultiDeviceChart() {
// 调用真实的多设备接口
this.get_series_utilization_rate_top_5();
},
// 新增:显示单设备图表
showSingleDeviceChart() {
// 这里复用现有的空闲率接口逻辑
this.get_series_utilization_rate_for_multi();
},
// 新增:为多设备图表获取单设备空闲率数据
get_series_utilization_rate_for_multi() {
axios({
method: "post",
url: this.localURL + "/public/device_efficiency/series_utilization_rate",
data: {
device_code: this.selectedMultiDevice,
plan_time_list: this.plan_time_list,
},
headers: { "Content-Type": "application/json" },
})
.then((res) => {
if (res.data.code == 200) {
this.updateMultiChartWithSingleDevice(res.data.data);
} else {
this.$message.error("多设备图表-设备空闲率获取失败!");
}
})
.catch((err) => {
this.$message.error("多设备图表-设备空闲率捕获到错误!");
});
},
// 新增:用单设备数据更新多设备图表
updateMultiChartWithSingleDevice(data) {
const selectedDeviceInfo = this.deviceList.find(
(device) => device.code === this.selectedMultiDevice
);
const deviceName = selectedDeviceInfo ? selectedDeviceInfo.name : "当前设备";
const option = {
grid: {
left: "3%",
right: "4%",
bottom: "3%",
top: "8%",
containLabel: true,
},
tooltip: {
trigger: "axis",
formatter: "{b} : {c}%",
},
legend: {
show: false,
},
xAxis: {
type: "category",
data: this.latestDateList,
axisLine: {
lineStyle: {
color: "#fff",
},
},
axisLabel: {
color: "#fff",
rotate: this.latestDateList[0].length > 5 ? 30 : 0,
},
},
yAxis: {
type: "value",
min: 0,
max: 100,
interval: 20,
axisLine: {
show: false,
},
axisTick: {
show: false,
},
splitLine: {
lineStyle: {
color: "rgba(255, 255, 255, 0.1)",
},
},
axisLabel: {
color: "#fff",
formatter: "{value}%",
},
},
series: [
{
name: deviceName,
type: "line",
smooth: true,
symbol: "circle",
symbolSize: 8,
label: {
show: true,
position: "top",
color: "#FFFFFF",
fontSize: 12,
formatter: "{c}%",
},
lineStyle: {
color: "#36cfc9",
width: 3,
},
itemStyle: {
color: "#36cfc9",
},
data: data.map((item) => (item > 100 ? 100 : item)),
},
],
};
// 使用 setOption 的第二个参数 true 来完全替换配置,清除之前的多设备折线
this.multiUtilizationChart.setOption(option, true);
this.multiUtilizationChart.resize();
},
// 新增:更新多设备图表
updateMultiUtilizationChart() {
if (!this.multiUtilizationChart) return;
if (this.selectedMultiDevice) {
this.showSingleDeviceChart();
} else {
this.showMultiDeviceChart();
}
},
// 新增:多设备图表设备选择变化事件
multiDeviceChange(el) {
this.updateMultiUtilizationChart();
},
// 新增:柱状图设备选择变化事件
barChartDeviceChange(el) {
this.get_daily_running_time();
},
// 新增:获取多设备空闲率数据(Top 5)
get_series_utilization_rate_top_5() {
// 构造设备列表参数
const device_code_list = this.deviceList.map((device) => ({
device_name: device.name,
device_code: device.code,
}));
axios({
method: "post",
url: this.localURL + "/public/device_efficiency/series_utilization_rate_top_5",
data: {
device_code_list: device_code_list,
plan_time_list: this.plan_time_list || {},
},
headers: { "Content-Type": "application/json" },
})
.then((res) => {
if (res.data.code == 200) {
this.updateMultiChartWithTopDevices(res.data.data);
} else {
this.$message.error("多设备空闲率获取失败!");
}
})
.catch((err) => {
this.$message.error("多设备空闲率接口捕获到错误!");
console.error("多设备空闲率接口错误:", err);
});
},
// 新增:用多设备数据更新图表
updateMultiChartWithTopDevices(data) {
if (!data.device_data || !Array.isArray(data.device_data)) {
console.error("多设备数据格式错误");
return;
}
const series = data.device_data.map((device) => ({
name: device.device_name,
type: "line",
smooth: true,
symbol: "circle",
symbolSize: 6,
lineStyle: {
width: 2,
},
data: device.utilization_rates.map((item) => (item > 100 ? 100 : item)),
}));
const option = {
grid: {
left: "3%",
right: "4%",
bottom: "3%",
top: "15%",
containLabel: true,
},
tooltip: {
trigger: "axis",
formatter: function (params) {
let result = params[0].axisValue + "<br/>";
params.forEach((param) => {
result += param.marker + param.seriesName + ": " + param.value + "%<br/>";
});
return result;
},
},
legend: {
top: "5%",
textStyle: {
color: "#fff",
},
},
xAxis: {
type: "category",
data: this.latestDateList,
axisLine: {
lineStyle: {
color: "#fff",
},
},
axisLabel: {
color: "#fff",
rotate: this.latestDateList[0].length > 5 ? 30 : 0,
},
},
yAxis: {
type: "value",
min: 0,
max: 100,
interval: 20,
axisLine: {
show: false,
},
axisTick: {
show: false,
},
splitLine: {
lineStyle: {
color: "rgba(255, 255, 255, 0.1)",
},
},
axisLabel: {
color: "#fff",
formatter: "{value}%",
},
},
series: series,
color: ["#36cfc9", "#1890ff", "#722ed1", "#eb2f96", "#fa8c16", "#52c41a", "#faad14"],
};
// 使用 setOption 的第二个参数 true 来完全替换配置,确保正确切换
this.multiUtilizationChart.setOption(option, true);
this.multiUtilizationChart.resize();
},
},
beforeDestroy() {
// 页面销毁移除resize事件监听
window.removeEventListener("resize", this.handleResize);
// 清理图表实例
if (this.utilizationChart) {
this.utilizationChart.dispose();
}
if (this.multiUtilizationChart) {
this.multiUtilizationChart.dispose();
}
},
});
</script>
<style>
/* 基础样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #0c1217;
background-size: cover;
color: #fff;
overflow: hidden;
}
#app {
/* width: 100vw; */
height: 100vh;
padding: 20px;
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
}
/* 玻璃态效果 */
.glass-effect {
background: rgba(6, 82, 158, 0.65);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 15px;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
}
.section-title {
position: relative;
margin-bottom: 20px;
display: flex;
align-items: center;
justify-content: flex-start;
padding-left: 20px;
}
.title-text {
font-size: 16px;
font-weight: 500;
color: #fff;
}
.title-bar {
width: 4px;
height: 18px;
background-color: #1890ff;
margin-right: 8px;
border-radius: 2px;
}
.el-progress__text {
white-space: pre-wrap;
line-height: 1.5;
}
/* 底部状态图样式 */
.status-chart {
padding: 20px;
height: auto;
min-height: 500px;
position: relative;
margin-bottom: 10px;
}
.chart-header {
display: flex;
align-items: center;
gap: 20px;
margin: 10px 0;
}
.chart-container {
width: 100%;
margin: 0 auto;
position: relative;
padding: 10px 40px;
height: 360px;
}
.columns-container {
position: absolute;
top: 0;
left: 60px;
width: calc(100% - 70px);
height: 320px;
display: flex;
justify-content: space-between;
padding: 0 10px;
}
.column-group {
width: 30px;
height: 280px;
display: flex;
flex-direction: column;
align-items: center;
}
.column-stack {
width: 100%;
height: 100%;
display: flex;
flex-direction: column-reverse;
background-color: transparent;
}
/* Y轴样式 */
.y-axis {
position: absolute;
height: 280px;
width: 50px;
z-index: 2;
}
.y-axis-label {
position: absolute;
left: 10px;
font-size: 14px;
font-weight: normal;
text-align: right;
transform: translateY(-50%);
white-space: nowrap;
}
/* 水平网格线调整 */
.horizontal-lines {
position: absolute;
width: 100%;
height: 280px;
top: 0;
left: 0;
}
.horizontal-line {
position: absolute;
width: 100%;
height: 1px;
background-color: rgba(255, 255, 255, 0.1);
left: 0;
}
/* 精确调整Y轴刻度和水平线位置 */
.y-axis-label:nth-child(1),
.horizontal-line:nth-child(1) {
top: 0;
}
/* 100% */
.y-axis-label:nth-child(2),
.horizontal-line:nth-child(2) {
top: 56px;
}
/* 80% */
.y-axis-label:nth-child(3),
.horizontal-line:nth-child(3) {
top: 112px;
}
/* 60% */
.y-axis-label:nth-child(4),
.horizontal-line:nth-child(4) {
top: 168px;
}
/* 40% */
.y-axis-label:nth-child(5),
.horizontal-line:nth-child(5) {
top: 224px;
}
/* 20% */
.y-axis-label:nth-child(6),
.horizontal-line:nth-child(6) {
top: 280px;
}
/* 0% */
/* X轴样式调整 */
.x-axis {
display: none;
}
.grid-area {
width: 100%;
height: 280px;
position: relative;
overflow: visible;
}
/* 图例样式优化 */
.flex_legend {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
margin-bottom: 15px;
}
.single_sty {
display: flex;
align-items: center;
}
.single_sty div {
width: 20px;
height: 10px;
margin-right: 8px;
border-radius: 2px;
}
.single_sty span {
color: #fff;
font-size: 14px;
}
.chart-content {
position: relative;
width: calc(100% - 60px);
height: 280px;
margin-top: 10px;
margin-left: 60px;
}
.unit {
font-size: 12px;
margin-left: 4px;
color: rgba(255, 255, 255, 0.7);
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.status-chart {
animation: fadeIn 0.8s ease-out;
}
/* Element UI 样式覆盖 */
.el-select {
width: 180px;
}
.chart-header h4 {
font-size: 14px;
color: #fff;
margin: 0;
}
/* Element UI 深色主题样式覆盖 */
.el-select {
width: 180px;
}
/* 输入框样式 */
.el-select .el-input__inner {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
}
/* 下拉图标颜色 */
.el-select .el-input__icon {
color: #fff;
}
/* 下拉面板样式 */
.el-select-dropdown {
background: rgba(31, 45, 61, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
}
/* 下拉选项样式 */
.el-select-dropdown__item {
color: #fff;
}
/* 下拉选项悬停样式 */
.el-select-dropdown__item.hover,
.el-select-dropdown__item:hover {
background-color: rgba(64, 158, 255, 0.2);
}
/* 选中项样式 */
.el-select-dropdown__item.selected {
color: #409eff;
background-color: rgba(64, 158, 255, 0.1);
}
/* 禁用项样式 */
.el-select-dropdown__item.is-disabled {
color: rgba(255, 255, 255, 0.3);
}
/* 聚焦时的边框颜色 */
.el-select .el-input.is-focus .el-input__inner {
border-color: #409eff;
}
/* 选择框占位符颜色 */
.el-select .el-input__inner::placeholder {
color: rgba(255, 255, 255, 0.5);
}
/* 在样式部分添加以下代码 */
.utilization-chart {
width: 100% !important;
padding: 15px 20px;
margin-bottom: 15px;
}
.utilization-chart .section-title {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.utilization-chart .title-bar {
width: 4px;
height: 18px;
background-color: #1890ff;
margin-right: 8px;
border-radius: 2px;
}
.utilization-chart .title-text {
font-size: 16px;
font-weight: 500;
color: #fff;
}
/* 新增单元结构样式 */
.column-with-label {
display: flex;
flex-direction: column;
align-items: center;
width: 40px;
/* 稍微宽一点,给标签留足空间 */
}
/* 调整柱状图容器样式 */
.columns-container {
position: absolute;
top: 0;
left: 60px;
width: calc(100% - 70px);
height: 320px;
/* 增加高度,包含X轴标签 */
display: flex;
justify-content: space-between;
padding: 0 10px;
}
/* 柱状图组样式 */
.column-group {
width: 40px;
height: 280px;
/* 固定高度为图表区域高度 */
display: flex;
flex-direction: column;
align-items: center;
}
/* X轴标签样式 */
.x-axis-label {
margin-top: 10px;
font-size: 14px;
text-align: center;
width: 100%;
}
/* 移除原有X轴容器样式 */
.x-axis {
display: none;
}
/* 新增:多设备空闲率图表样式 */
.multi-utilization-chart {
width: 100% !important;
padding: 15px 20px;
margin-bottom: 15px;
}
.multi-utilization-chart .section-title {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.multi-utilization-chart .title-bar {
width: 4px;
height: 18px;
background-color: #1890ff;
margin-right: 8px;
border-radius: 2px;
}
.multi-utilization-chart .title-text {
font-size: 16px;
font-weight: 500;
color: #fff;
}
.utilization-chart .section-title {
display: flex;
align-items: center;
margin-bottom: 15px;
}
/* 新增:图表选择器容器样式 */
.chart-select-container {
margin-left: auto;
margin-right: 20px;
display: flex;
align-items: center;
gap: 8px;
}
/* 新增:卡片外部选择器包装器样式 */
.chart-select-wrapper {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
padding-left: 4px;
}
.select-label {
color: #fff;
font-size: 14px;
white-space: nowrap;
}
.chart-select {
width: 250px;
}
/* 针对图表内选择器的Element UI样式覆盖 */
.chart-select .el-input__inner {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
font-size: 12px;
height: 32px;
}
.chart-select .el-input__icon {
color: #fff;
}
.chart-select .el-input__inner::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.chart-select .el-input.is-focus .el-input__inner {
border-color: #409eff;
}
</style>
</html>
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from . import models from . import models
from . import plant_working_time_config from . import plant_working_time_config
\ No newline at end of file from . import inherit_roke_mes_equipment
\ No newline at end of file
from odoo import models, fields, api, _
class InheritRokeMesEquipment(models.Model):
_inherit = "roke.mes.equipment"
sequence = fields.Integer(string="序号")
\ No newline at end of file
<odoo> <odoo>
<data> <data>
<record id="view_dws_inherit_roke_mes_equipment_tht_form" model="ir.ui.view"> <!-- <record id="view_dws_inherit_roke_mes_equipment_tht_form" model="ir.ui.view">
<field name="name">view_dws_inherit_roke_mes_equipment_tht_form</field> <field name="name">view_dws_inherit_roke_mes_equipment_tht_form</field>
<field name="model">roke.mes.equipment</field> <field name="model">roke.mes.equipment</field>
<field name="inherit_id" ref="roke_workstation_api.view_dws_inherit_roke_mes_equipment_form"/> <field name="inherit_id" ref="roke_workstation_api.view_dws_inherit_roke_mes_equipment_form"/>
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
<field name="sequence"/> <field name="sequence"/>
</xpath> </xpath>
</field> </field>
</record> </record> -->
<record id="view_roke_mes_equipment_tree_tht" model="ir.ui.view"> <record id="view_roke_mes_equipment_tree_tht" model="ir.ui.view">
<field name="name">view_roke_mes_equipment_tree_tht</field> <field name="name">view_roke_mes_equipment_tree_tht</field>
<field name="model">roke.mes.equipment</field> <field name="model">roke.mes.equipment</field>
......
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