Commit 1681e680 by malihua

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

parents 28dbdfb0 2d5c9ae8
# -*- 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() {
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() {
// 截取url后面的参数
this.factoryCode = this.getUrlSearch("factory_code") || ""
// 初始化当前时间
this.initCurrentTimeFn()
},
async mounted() {
this.$nextTick(() => {
document.getElementById("bodyId").style.display = "block"
})
// 初始化数据 - 实际使用时取消这行的注释
await this.initData()
// 设置定时刷新(每分钟刷新一次,静默模式)
this.refreshInterval = setInterval(() => {
this.initData(true) // 传入true表示静默刷新,不显示加载提示
}, 60000)
},
beforeDestroy() {
// 清除定时器
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() {
this.timer = setInterval(() => {
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}`
}, 1000)
},
// 通过网址跳转过来的页面,截取后面的参数
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]
},
// 初始化数据
async initData(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
}
},
// 获取设备计划运行时间
async getDevicePlanTime() {
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;
}
},
// 获取所有已绑定设备数据
async getAllEquipmentData() {
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
}
},
// 获取设备状态列表
async getDeviceStateList(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
async getFactoryCode(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
}
},
// 获取设备维护信息
async getEquipmentMaintenanceInfoApi() {
try {
const response = await axios({
method: "post",
url: this.dwsURL + "/roke/get/equipment/maintenance_info",
data: {},
headers: { "Content-Type": "application/json" },
})
if (response?.data?.result?.state === "success") {
this.equipmentMaintenanceInfos = response.data.result.equipment_maintenance_list || []
} else {
const errorMsg = response?.data?.result?.msgs || "获取设备维护信息失败!"
throw new Error(errorMsg)
}
} catch (error) {
throw error
}
},
// 处理设备状态数据
processDeviceStateData: function (deviceStateData) {
var self = this
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
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 > 0) {
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((device) => device)
},
// 处理小数方法
formatDecimal(value, digit) {
if (value === Infinity) return 0
// 没设置位数默认返回原来的值
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.length <= digit) return parsedNum
// 保留要求位数(处理浮点误差)
return parseFloat((Math.round((parsedNum + Number.EPSILON) * 100) / 100).toFixed(digit))
},
// 处理时间方法
formatTime(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(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(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(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(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(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(status) {
switch (status) {
case "running":
return "运行中";
case "waiting":
return "等待中";
case "error":
return "故障中";
case "off":
default:
return "停机中";
}
},
// 获取设备维护信息状态颜色
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') {
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(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(value, num = 4) {
if (value) {
let strValue = String(value);
if (strValue.split(".").length > 1 || strValue.split(".")[1]?.length > num) {
strValue = Number(strValue).toFixed(num);
}
return Number(strValue);
} else {
return 0;
}
},
// 计算高度百分比
calculateHeight(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(event, device) {
const fullTitle = device.name + " | " + device.code;
event.target.setAttribute("title", fullTitle);
},
// 获取ERR文字的CSS类名
getErrClass(status) {
if (status === "error") {
return "err-error";
}
return "";
},
// 获取OFF文字的CSS类名
getOffClass(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%;
gap: 5px;
}
/* 新增:图标和文字的水平容器 */
.maintenance-header {
display: flex;
align-items: center;
gap: 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;
}
.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%;
gap: 10px;
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) {
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;
gap: 8px;
flex: 0 0 auto;
/* 改为固定宽度 */
width: 80px;
/* 设置固定宽度 */
}
/* 右侧状态 */
.right-status {
display: flex;
flex-direction: column;
gap: 8px;
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;
gap: 6px;
}
/* 右侧状态项布局调整 */
.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;
}
/* 移动端虚线样式调整 */
.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;
/* 减小间距 */
gap: 4px;
}
.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
# -*- 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
import os
import datetime
import logging
import requests
from odoo.addons.roke_mes_client.controller import login as mes_login
from odoo import http, tools, SUPERUSER_ID
from jinja2 import FileSystemLoader, Environment
import pytz
from dateutil.relativedelta import relativedelta
_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 RokeMesThreeColourLight(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_qdry.html')
return template.render(data)
@http.route("/roke/three_color_light/device_analysis", type="http", auth='none', cors='*', csrf=False)
def device_analysis(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_analysis.html')
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="*")
def get_roke_workstation_plant(self):
_self = http.request
no_icon = _self.jsonrequest.get("no_icon", False)
data = []
workshop_ids = http.request.env(user=SUPERUSER_ID)['roke.workshop'].search([
("plant_id", "=", False)
], order="name asc")
if len(workshop_ids) > 0:
data.append({
"id": 0,
"name": "未分配至车间",
"type": "plant",
"file_list": [],
"workshops": [
{
"id": workshop_id.id,
"name": workshop_id.name or "",
"type": "workshop",
"workshop_icon": workshop_id.workshop_icon or "",
"classes_id": workshop_id.classes_id.id,
"classes_name": workshop_id.classes_id.name or "",
"center_count": len(workshop_id.center_ids),
"file_list": [{
"name": attachment.name,
"file_type": attachment.mimetype,
"data": f"data:{attachment.mimetype};base64,{attachment.datas.decode('utf-8')}" if
not no_icon else "",
"url": self._get_attachment_file_url(attachment)
} for attachment in workshop_id.attachment_ids]
} for workshop_id in workshop_ids
]
})
plant_ids = http.request.env(user=SUPERUSER_ID)['roke.plant'].search([
], order="name asc")
for plant_id in plant_ids:
# print("plant_id", plant_id.company_ids)
data.append({
"id": plant_id.id,
"name": plant_id.name or "未分配至车间",
"type": "plant",
"file_list": [{
"name": attachment.name,
"file_type": attachment.mimetype,
"data": f"data:{attachment.mimetype};base64,{attachment.datas.decode('utf-8')}" if not no_icon
else "",
"url": self._get_attachment_file_url(attachment)
} for attachment in plant_id.attachment_ids],
"workshops": [
{
"id": workshop_id.id,
"name": workshop_id.name or "",
"type": "workshop",
"workshop_icon": workshop_id.workshop_icon or "",
"classes_id": workshop_id.classes_id.id,
"classes_name": workshop_id.classes_id.name or "",
"center_count": len(workshop_id.center_ids),
"file_list": [{
"name": attachment.name,
"file_type": attachment.mimetype,
"data": f"data:{attachment.mimetype};base64,{attachment.datas.decode('utf-8')}" if
not no_icon else "",
"url": self._get_attachment_file_url(attachment)
} for attachment in workshop_id.attachment_ids]
} for workshop_id in plant_id.workshop_ids
]
})
return {"code": 0, "message": "获取车间列表成功", "data": data}
\ No newline at end of file
<odoo>
<data>
<!--
<record id="object0" model="qdry_project.qdry_project">
<field name="name">Object 0</field>
<field name="value">0</field>
</record>
<record id="object1" model="qdry_project.qdry_project">
<field name="name">Object 1</field>
<field name="value">10</field>
</record>
<record id="object2" model="qdry_project.qdry_project">
<field name="name">Object 2</field>
<field name="value">20</field>
</record>
<record id="object3" model="qdry_project.qdry_project">
<field name="name">Object 3</field>
<field name="value">30</field>
</record>
<record id="object4" model="qdry_project.qdry_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 qdry_project(models.Model):
# _name = 'qdry_project.qdry_project'
# _description = 'qdry_project.qdry_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_qdry_project_qdry_project,qdry_project.qdry_project,model_qdry_project_qdry_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 v-for="category in deviceList" :key="category.category_name">
<div class="device-category-name-container">[[category.category_name]]</div>
<div class="device-cards-container">
<div v-for="(device, index) in category.data" :key="index" class="device-card new-design"
:class="getCardClass(device.status)">
<!-- 设备名称 - 左上角 -->
<el-tooltip :disabled="device.name.length<16" effect="dark" :content="device.name" placement="top">
<div class="device-name">
[[truncateText(device.name, 16)]]
</div>
</el-tooltip>
<!-- 设备状态 - 右上角 -->
<div class="device-status-tag" :class="getStatusClass(device.status)">
[[getStatusText(device.status)]]
</div>
<!-- 设备状态水波纹 - 中间 -->
<div class="device-wave-container">
<!-- v-if="device.status === 'running' || device.status === 'waiting'" -->
<div class="oee-text">
OEE
</div>
<!-- <div class="err-text" v-if="device.status === 'error'" :class="getErrClass(device.status)">
ERR
</div>
<div class="off-text" v-if="device.status === 'off'" :class="getOffClass(device.status)">
OFF
</div> -->
<!-- v-if="device.status === 'running' || device.status === 'waiting'" -->
<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) + '%',
'bottom': '0',
'left': '0',
'position': 'absolute',
'width': '100%',
'overflow': 'hidden'
}">
<div class="water-wave-content">
<div class="water-wave-ripple"></div>
</div>
</div>
</div>
</div>
<!-- 设备状态信息 - 底部 -->
<div class="device-status-info">
<span>已持续 [[device.duration]]</span>
</div>
<div style="width: 100%; display: flex;margin-left: 13px;">
<div class="flxe_sty">
<div class="flxe_label_sty" style=" background-color: #00aa00;">开机</div>
[[ device.run_seconds ]]
</div>
<div class="flxe_sty">
<div class="flxe_label_sty" style=" background-color: #ffaa00;">加工</div>
[[ device.green_seconds ]]
</div>
</div>
<div style="width: 100%;display: flex;margin-left: 13px;">
<div class="flxe_sty">
<div class="flxe_label_sty" style=" background-color: #797979;">空闲</div>
[[ device.yellow_seconds ]]
</div>
<div class="flxe_sty">
<div class="flxe_label_sty" style=" background-color: #f87171;">故障</div>
[[ device.red_seconds ]]
</div>
</div>
</div>
</div>
</div>
<!-- 底部状态图 -->
<div class="status-chart glass-effect" v-if="false">
<!-- 这部分保留但不显示,如果将来需要可以启用 -->
<div class="section-title">
<span class="title-text">设备运行状态统计</span>
<div class="title-decoration"></div>
</div>
<div class="chart-header">
<el-select v-model="selectedDevice" placeholder="请选择设备" size="medium" @change="selDeviceChange">
<el-option v-for="item in deviceList" :label="item.name" :value="item.id" :key="item.id">
</el-option>
</el-select>
<h4>近10天设备运行状态统计</h4>
</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">h</span>
</span>
</div>
<!-- 图表主体区域 -->
<div class="chart-content">
<!-- 网格区域 -->
<div class="grid-area">
<!-- 背景网格 -->
<div class="">
<div class="horizontal-lines">
<div class="horizontal-line" v-for="(value, index) in yAxisValues" :key="index"></div>
</div>
</div>
<!-- 柱状图组 -->
<div class="columns-container">
<div class="column-group" v-for="(item, dayIndex) in pickingOrderList" :key="dayIndex">
<div class="column-stack">
<div v-for="(segment, stackIndex) in item.data" :key="stackIndex"
class="column-segment" :style="{
'height': calculateHeight(segment.value) + '%',
'background-color': segment.color,
'margin-top': '0px'
}"></div>
</div>
</div>
</div>
</div>
</div>
<!-- X轴 -->
<div class="x-axis">
<span v-for="(item, index) in pickingOrderList" :key="index" class="x-axis-label">
[[item.name_format]]
</span>
</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 {
isFullscreen: false, // 全屏状态
currentTime: null, // 当前时间
timer: null, // 定时器
windowHeight: window.innerHeight, // 窗口高度
// dwsURL: "https://workstation.rokeris.com", // 基地址
dwsURL: "", // 基地址
baseURL: "https://dws-platform.xbg.rokeris.com/dev-api", // 基地址
loading: false, // 全局加载效果
deviceList: [], // 设备列表
selectedDevice: null, // 选中的设备
yAxisValues: ["24", "20", "16", "12", "8", "4", "0"], // Y轴刻度值
pickingOrderList: [], // 拣选单列表
dateList: [], // 日期列表
start_time: "", // 开始时间
end_time: "", // 结束时间
refreshInterval: null, // 定时刷新计时器
factoryCode: "", // 公司CODE
allEquipmentData: [], // 所有已绑定设备数据
};
},
created() {
if (this.getUrlSearch("factory_code")) {
this.factoryCode = this.getUrlSearch("factory_code"); //截取url后面的参数
}
this.initCurrentTimeFn()
},
computed: {
// 选中设备的信息
selDeviceInfo() {
return this.deviceList.find((item) => item.id == this.selectedDevice);
},
},
async mounted() {
window.addEventListener("resize", this.handleResize);
this.$nextTick(() => {
document.getElementById("bodyId").style.display = "block";
});
// // 先加载测试数据以便查看效果
// this.initMockData();
// 初始化数据 - 实际使用时取消这行的注释
await this.initData();
// 获取最后指定天数的日期
this.dateList = this.getLastAssignDays();
// 设置定时刷新(每分钟刷新一次,静默模式)
this.refreshInterval = setInterval(() => {
this.initData(true); // 传入true表示静默刷新,不显示加载提示
}, 60000);
// 设置设备信息的title属性
this.$nextTick(() => {
document.querySelectorAll(".device-info").forEach((el) => {
el.title = el.dataset.fullTitle;
});
});
},
updated() {
// 在数据更新后也设置title属性
this.$nextTick(() => {
document.querySelectorAll(".device-info").forEach((el) => {
el.title = el.dataset.fullTitle;
});
});
},
beforeDestroy() {
// 清除定时器
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
if (this.timer) {
clearInterval(this.timer)
}
// 移除事件监听
window.removeEventListener("resize", this.handleResize);
},
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() {
this.timer = setInterval(() => {
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}`
}, 1000)
},
// 通过网址跳转过来的页面,截取后面的参数
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];
},
// 初始化数据
async initData(silent = false) {
try {
// 只有在非静默模式下才显示加载提示
if (!silent) {
this.loading = true;
}
// 并行请求设备计划运行时间和所有已绑定设备数据
const [planTimeResult, allEquipmentResult] = await Promise.all([
this.getDevicePlanTime(),
this.getAllEquipmentData(),
]);
} catch (error) {
// console.error("初始化数据出错:", error);
// 只有在非静默模式下才显示错误提示
if (!silent) {
this.$message.error("初始化数据出错: " + (error.message || "未知错误"));
}
// 如果接口请求失败,使用模拟数据
this.initMockData();
} finally {
// 关闭加载提示
if (!silent) {
this.loading = false;
}
}
},
// 获取设备计划运行时间
async getDevicePlanTime() {
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;
}
},
// 获取所有已绑定设备数据
async getAllEquipmentData() {
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) {
// console.error("获取所有已绑定设备数据出错:", error);
throw error;
}
},
// 获取设备状态列表
async getDeviceStateList(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
async getFactoryCode(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) {
// console.error("获取账套db_uuid失败:", error);
throw error;
}
},
// 处理设备状态数据
processDeviceStateData(deviceStateData) {
if (!deviceStateData || !Array.isArray(deviceStateData)) {
return;
}
// 将API返回的数据转换为页面所需的格式
let list = deviceStateData.map((device) => {
// 根据API返回的状态确定前端显示的状态
let 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";
}
// 计算持续时间的显示格式
let durationText = "0"
if (device.duration_hours !== undefined) { durationText = this.formatTime(Number(device.duration_hours * 3600)) }
let run_seconds = "0"
if (device.run_seconds !== undefined) run_seconds = this.formatTime(Number(device.run_seconds))
let green_seconds = "0"
if (device.green_seconds !== undefined) green_seconds = this.formatTime(Number(device.green_seconds))
let yellow_seconds = "0"
if (device.yellow_seconds !== undefined) yellow_seconds = this.formatTime(Number(device.yellow_seconds))
let red_seconds = "0"
if (device.red_seconds !== undefined) yellow_seconds = this.formatTime(Number(device.yellow_seconds))
// 计算利用率百分比,确保有效值
const percentage = device.utilization_rate !== undefined ? Math.round(device
.utilization_rate) : 0
// 从所有设备列表中获取准确的设备名称
let deviceName = device.name || device.code // 默认使用API返回的名称或编码
let categoryName = null
let categorySequence = null
// 在所有设备列表中查找匹配的设备
if (this.allEquipmentData && this.allEquipmentData.length > 0) {
const matchedDevice = this.allEquipmentData.find(
(equip) => equip.code === device.code
)
// 如果找到匹配的设备,使用其名称
if (matchedDevice && matchedDevice.name) {
deviceName = device.name ? matchedDevice.name : device.code
categoryName = matchedDevice.category_name
categorySequence = matchedDevice.category_sequence
}
}
return {
id: device.code,
code: device.code,
name: deviceName,
status: status,
percentage: percentage,
duration: durationText,
run_seconds: run_seconds,
green_seconds: green_seconds,
yellow_seconds: yellow_seconds,
red_seconds: red_seconds,
category_name: categoryName,
category_sequence: categorySequence
}
});
// 按category_name分组并组装成指定结构
const grouped = {};
list.forEach(item => {
const cname = item.category_name || '未分类';
if (!grouped[cname]) grouped[cname] = [];
grouped[cname].push(item);
});
// 组装成 [{category_name:'', data:[]}, ...] 并排序
const result = Object.keys(grouped).map(category_name => {
// 每组内部按percentage降序排序
const data = grouped[category_name].sort((a, b) => b.percentage - a.percentage);
// 取第一个的category_sequence作为分组的category_sequence
const category_sequence = data[0]?.category_sequence ?? 9999;
return { category_name, category_sequence, data };
});
// 最终按category_sequence从小到大排序
result.sort((a, b) => (a.category_sequence ?? 9999) - (b.category_sequence ?? 9999));
this.deviceList = result
},
formatTime(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`; // 只返回天
}
},
// 初始化模拟数据 (保留原有的模拟数据方法,作为备用)
initMockData() {
// 模拟设备数据 - 添加不同状态和明显不同百分比的测试数据
this.deviceList = [
// {
// id: "device001",
// code: "device001",
// name: "设备名称1",
// status: "running", // 运行中
// percentage: 90, // 非常高的百分比
// duration: "2h15m",
// },
// {
// id: "device002",
// code: "device002",
// name: "设备名称2",
// status: "error", // 故障中
// percentage: 50, // 中等百分比
// duration: "4h30m",
// },
// {
// id: "device003",
// code: "device003",
// name: "设备名称3",
// status: "waiting", // 等待中
// percentage: 20, // 非常低的百分比
// duration: "1h45m",
// },
// {
// id: "device004",
// code: "device004",
// name: "设备名称4",
// status: "off", // 停机中
// percentage: 50, // 中等百分比
// duration: "8h20m",
// },
];
// 添加测试日志
console.log("测试数据初始化完成,设备列表:", this.deviceList);
},
// 获取卡片的CSS类名
getCardClass(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(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(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(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(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(status) {
switch (status) {
case "running":
return "运行中";
case "waiting":
return "等待中";
case "error":
return "故障中";
case "off":
default:
return "停机中";
}
},
// 处理小数位数方法
toFixedHandle(value, num = 4) {
if (value) {
let strValue = String(value);
if (strValue.split(".").length > 1 || strValue.split(".")[1]?.length > num) {
strValue = Number(strValue).toFixed(num);
}
return Number(strValue);
} else {
return 0;
}
},
// 计算高度百分比
calculateHeight(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);
},
// 获取最后指定天数的日期
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;
},
// 处理窗口大小变化
handleResize() {
// 窗口大小变化时的处理逻辑
},
// 文本截断方法
truncateText(text, maxLength) {
if (!text) return "";
if (text.length <= maxLength) return text;
return text.substring(0, maxLength);
},
// 显示完整标题
showFullTitle(event, device) {
const fullTitle = device.name + " | " + device.code;
event.target.setAttribute("title", fullTitle);
},
// 获取ERR文字的CSS类名
getErrClass(status) {
if (status === "error") {
return "err-error";
}
return "";
},
// 获取OFF文字的CSS类名
getOffClass(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-category-name-container {
width: 100%;
padding: 0px 15px;
font-weight: bold;
font-size: 20px;
}
/* 设备卡片容器 */
.device-cards-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 20px;
padding: 15px;
}
/* 新设计设备卡片样式 */
.device-card.new-design {
position: relative;
padding: 5px 10px 10px;
display: flex;
flex-direction: column;
align-items: center;
min-height: 180px;
border-radius: 12px;
transition: all 0.3s ease;
background-color: rgba(20, 22, 28, 0.95);
/* 更深的背景色 */
}
/* 卡片颜色和阴影 - 调整为适度柔和的阴影效果 */
.card-running {
border: none;
box-shadow: 0 0 0 1px rgba(78, 198, 138, 0.5), 5px 5px 2px 0 rgba(78, 198, 138, 0.7),
5px 5px 15px 2px rgba(78, 198, 138, 0.5);
}
.card-waiting {
border: none;
box-shadow: 0 0 0 1px rgba(235, 186, 22, 0.5), 5px 5px 2px 0 rgba(235, 186, 22, 0.7),
5px 5px 15px 2px rgba(235, 186, 22, 0.5);
}
.card-error {
border: none;
box-shadow: 0 0 0 1px rgba(235, 86, 86, 0.5), 5px 5px 2px 0 rgba(235, 86, 86, 0.7),
5px 5px 15px 2px rgba(235, 86, 86, 0.5);
}
.card-off {
border: none;
box-shadow: 0 0 0 1px rgba(144, 147, 153, 0.5), 5px 5px 2px 0 rgba(144, 147, 153, 0.7),
5px 5px 15px 2px rgba(144, 147, 153, 0.5);
}
.device-card.new-design:hover {
transform: translateY(-3px);
box-shadow: 0 0 0 1px rgba(78, 198, 138, 0.5), 7px 7px 4px 0 rgba(78, 198, 138, 0.7),
7px 7px 20px 2px rgba(78, 198, 138, 0.6);
}
/* 设备名称 - 左上角 */
.device-name {
align-self: flex-start;
font-size: 14px;
font-weight: bold;
color: #fff;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
/* text-overflow: ellipsis; */
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
margin-bottom: 5px;
}
/* 设备状态标签 - 右上角 */
.device-status-tag {
position: absolute;
top: 25px;
right: 5px;
padding: 2px 5px 5px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
color: #fff;
min-width: 50px;
text-align: center;
}
/* 移除状态标签的阴影 */
.status-running {
background-color: rgba(78, 198, 138, 0.9);
box-shadow: none;
}
.status-waiting {
background-color: rgba(235, 186, 22, 0.9);
box-shadow: none;
}
.status-error {
background-color: rgba(235, 86, 86, 0.9);
box-shadow: none;
}
.status-off {
background-color: rgba(144, 147, 153, 0.9);
box-shadow: none;
}
/* 设备波纹容器 */
.device-wave-container {
width: 100%;
height: 120px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
}
/* OEE文字 */
.oee-text {
position: absolute;
top: 30px;
font-size: 16px;
font-weight: bold;
color: #fff;
z-index: 3;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
/* ERR文字 */
.err-text {
position: absolute;
top: 30px;
font-size: 16px;
font-weight: bold;
z-index: 3;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
left: 50%;
transform: translateX(-50%);
}
/* ERR颜色 */
.err-error {
color: rgba(235, 86, 86, 0.9);
}
/* OFF文字 */
.off-text {
position: absolute;
top: 30px;
font-size: 16px;
font-weight: bold;
z-index: 3;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
left: 50%;
transform: translateX(-50%);
}
/* OFF颜色 */
.off-status {
color: rgba(144, 147, 153, 0.9);
}
/* 百分比文字 */
.percentage-text {
position: absolute;
top: 50px;
font-size: 25px;
font-weight: bold;
color: #fff;
z-index: 3;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
/* 圆形容器 */
.circle-container {
width: 100px;
height: 100px;
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);
box-shadow: none;
}
.border-waiting {
border: 2px solid rgba(235, 186, 22, 0.9);
box-shadow: none;
}
.border-error {
border: 2px solid rgba(235, 86, 86, 0.9);
box-shadow: none;
}
.border-off {
border: 2px solid rgba(144, 147, 153, 0.9);
box-shadow: none;
}
/* 水波纹 */
.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;
}
/* 设备状态信息 */
.device-status-info {
font-size: 12px;
color: rgba(255, 255, 255, 0.85);
text-align: center;
line-height: 1.5;
}
/* 响应式调整 */
@media (max-width: 768px) {
.device-cards-container {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.device-card.new-design {
min-height: 160px;
padding: 10px;
}
.circle-container {
width: 80px;
height: 80px;
}
.oee-text,
.err-text,
.off-text {
top: 20px;
font-size: 14px;
}
.percentage-text {
top: 40px;
font-size: 20px;
}
}
/* 为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
This source diff could not be displayed because it is too large. You can view the blob instead.
<!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="multi-utilization-chart glass-effect">
<div class="section-title">
<div class="title-bar"></div>
<span class="title-text">多设备空闲率对比</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">
<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="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': calculateHeightByMinutes(convertSecondsToMinutes(segment.duration)) + '%',
'background-color': segment.state,
'margin-top': '0px'
}"
>
<el-tooltip
effect="dark"
:content="convertSecondsToMinutes(segment.duration) + '分钟'"
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: "", // 基地址 https://workstation.rokeris.com
localURL: "https://dws-platform.xbg.rokeris.com/dev-api", // 基地址 https://workstation.rokeris.com
idle_rate_loading: false, // 加载效果
chart_loading: false,
deviceList: [], // 设备列表
selectedDevice: "", // 选中的设备,默认为空字符串而不是null
yAxisValues: ["1440", "1152", "864", "576", "288", "0"], // Y轴刻度值(分钟)
pickingOrderList: [], // 拣选单列表
dateList: [], // 日期列表
start_time: "", // 开始时间
end_time: "", // 结束时间
utilizationChart: null, // 设备空闲率图表实例变量
multiUtilizationChart: null, // 新增:多设备空闲率图表实例变量
plan_time_list: null,
latestDateList: [],
factory_code: "8d8dec6e-0d44-11f0-9692-00163e04c506",
};
},
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);
}
});
});
// 设备列表加载完成后,默认选择第一个设备
if (this.deviceList.length > 0) {
this.selectedDevice = this.deviceList[0].code;
}
this.get_plan_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() {
if (!this.selectedDevice) {
this.pickingOrderList = [];
return;
}
this.idle_rate_loading = true;
axios({
method: "post",
url: this.localURL + "/public/device_efficiency/daily_running_time",
data: {
device_code: this.selectedDevice,
},
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();
// 同时获取日均运行时间数据
this.get_daily_running_time();
}
// 初始化多设备图表
if (this.multiUtilizationChart) {
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();
} else {
// 当设备被清空时,清空折线图数据
this.initUtilizationChart([]);
}
// 同时更新柱状图数据
this.get_daily_running_time();
},
// 处理窗口大小变化修改图表大小
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: data.length > 0,
position: "top",
color: "#FFFFFF",
fontSize: 12,
formatter: "{c}%",
},
lineStyle: {
color: "#36cfc9",
width: 3,
},
itemStyle: {
color: "#36cfc9",
},
data: data.length > 0 ? data.map((item) => (item > 100 ? 100 : item)) : [],
},
],
};
this.utilizationChart.setOption(option);
// 手动触发resize以确保正确渲染
this.utilizationChart.resize();
}, 300); // 延迟300ms初始化
},
// 将秒转换为分钟
convertSecondsToMinutes(seconds) {
return Math.round(seconds / 60);
},
// 基于分钟数计算高度百分比
calculateHeightByMinutes(minutes) {
// 将分钟数转换为相对于1440分钟(一天)的百分比
const percentage = (minutes / 1440) * 100;
return Math.min(Math.max(percentage, 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);
}, 300);
},
// 新增:获取多设备空闲率数据(Top 5)
get_series_utilization_rate_top_5() {
// 构造设备列表参数
const allowedEqpts = ["激光切割机EE0032261", "激光切割机EQ0032221", "自动带锯EQ0032082", "数控中走丝线切割机床EQ0032256", "数控相贯线切割设备EQ0032227"];
const device_code_list = this.deviceList.filter(device => allowedEqpts.includes(device.name)).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 20px 15px 20px;
height: auto;
min-height: 460px;
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 5px 40px;
height: 370px;
}
.columns-container {
position: absolute;
top: 0;
left: 60px;
width: calc(100% - 70px);
height: 330px;
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: 12px;
text-align: center;
width: 100%;
color: #fff;
}
/* 移除原有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>
<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="qdry_project.list">
<field name="name">qdry_project list</field>
<field name="model">qdry_project.qdry_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="qdry_project.action_window">
<field name="name">qdry_project window</field>
<field name="res_model">qdry_project.qdry_project</field>
<field name="view_mode">tree,form</field>
</record>
-->
<!-- server action to the one above -->
<!--
<record model="ir.actions.server" id="qdry_project.action_server">
<field name="name">qdry_project server</field>
<field name="model_id" ref="model_qdry_project_qdry_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="qdry_project" id="qdry_project.menu_root"/>
-->
<!-- menu categories -->
<!--
<menuitem name="Menu 1" id="qdry_project.menu_1" parent="qdry_project.menu_root"/>
<menuitem name="Menu 2" id="qdry_project.menu_2" parent="qdry_project.menu_root"/>
-->
<!-- actions -->
<!--
<menuitem name="List" id="qdry_project.menu_1_list" parent="qdry_project.menu_1"
action="qdry_project.action_window"/>
<menuitem name="Server to list" id="qdry_project" parent="qdry_project.menu_2"
action="qdry_project.action_server"/>
-->
</data>
</odoo>
\ No newline at end of file
# -*- 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': ['roke_mes_three_colour_light'],
# always loaded
'data': [
# 'security/ir.model.access.csv',
'views/views.xml',
'views/templates.xml',
'views/plant_working_time_config.xml',
'views/big_screen.xml',
'views/assets.xml',
'views/menus.xml',
],
# only loaded in demonstration mode
'demo': [
'demo/demo.xml',
],
'qweb': [
'static/src/xml/*.xml',
]
}
# -*- coding: utf-8 -*-
from . import controllers
from . import big_screen
\ No newline at end of file
# -*- coding: utf-8 -*-
from odoo import http, fields,SUPERUSER_ID
from odoo.http import request
from odoo.addons.roke_mes_three_colour_light.controller.main import RokeMesThreeColourLight
import os
import math
from datetime import datetime, time, timedelta
from jinja2 import Environment, FileSystemLoader
import logging
import requests
import json
_logger = logging.getLogger(__name__)
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
templateloader = FileSystemLoader(searchpath=BASE_DIR + "/static")
env = Environment(loader=templateloader)
dws_platform_url ='https://dws-platform.xbg.rokeris.com/dev-api/public/device'
# dws_platform_url = 'http://localhost/dev-api/public/device'
headers = {
'Content-Type': 'application/json',
}
class ResMesBigScreen(http.Controller):
@http.route('/roke/tht/expected_process', type='http', auth='public', csrf=False, cors="*")
def roke_expected_process_module(self, **kwargs):
template = env.get_template('html/big_screen/view/expected_process.html')
html = template.render({})
return html
@http.route('/roke/tht/cook_process', type='http', auth='public', csrf=False, cors="*")
def roke_cook_process_module(self, **kwargs):
template = env.get_template('html/big_screen/view/cook_process.html')
html = template.render({})
return html
def search_equipments(self, data_acquisition_code='', plant_name='', category_name=''):
"""查询设备"""
domain = []
if data_acquisition_code:
domain.append(('data_acquisition_code', 'in', data_acquisition_code))
# 构建查询条件
if plant_name:
domain.append(('plant_id.name', '=', plant_name))
if category_name:
domain.append(('category_id.name', '=', category_name))
# 查询设备
equipments = http.request.env['roke.mes.equipment'].sudo().search(domain)
# 构造响应数据
equipment_list = [{
'id': eq.id,
'device_name': eq.name,
'device_code': eq.code,
'data_acquisition_code': eq.data_acquisition_code,
'category': eq.category_id.name if eq.category_id else '',
'plant_name': eq.plant_id.name if eq.plant_id else '',
} for eq in equipments]
return equipment_list
def common_dws_interface(self, body=None, url='', cate=''):
"""获取大屏数据"""
plant_name = body.get("plant_name", '')
today = body.get("today", '')
device_code_list = body.get("device_code_list", [])
# if not today:
# today = fields.Date.today().strftime('%Y-%m-%d')
category_name = body.get("category_name",'')
# 构建查询条件
if device_code_list:
equipment_list = device_code_list
else:
equipment_list = self.search_equipments(plant_name=plant_name, category_name=category_name)
try:
api_path = dws_platform_url + url
payload = {
"plant_name": plant_name,
"today": today,
"category_name": category_name,
"device_code_list": equipment_list
}
if cate:
payload.update({"cate": cate})
res = requests.post(api_path, data=json.dumps(payload), headers=headers, )
res_json = res.json()
return res_json
except Exception as e:
_logger.error(e)
return {
"code": 100,
"msg": str(e),
"data": {
},
"success": False,
"time": fields.Datetime.now()
}
@http.route('/big_screen_count', type='json', methods=['POST', 'OPTIONS'], auth="none", csrf=False,
cors='*')
def big_screen_count(self):
"""获取大屏数据"""
body = http.request.jsonrequest
return self.common_dws_interface(body, '/big_screen_count')
@http.route('/big_screen_today', type='json', methods=['POST', 'OPTIONS'], auth="none", csrf=False,
cors='*')
def big_screen_today(self):
body = http.request.jsonrequest
return self.common_dws_interface(body, '/big_screen_today')
@http.route('/get_jiedongji_temperature', type='json', methods=['POST', 'OPTIONS'], auth="none", csrf=False,
cors="*")
def get_jiedongji_temperature(self):
body = http.request.jsonrequest
return self.common_dws_interface(body, '/get_real_time_device_data', cate="解冻机")
@http.route('/get_qiekuaiji_data', type='json', methods=['POST', 'OPTIONS'], auth="none", csrf=False, cors="*")
def get_qiekuaiji_data(self):
body = http.request.jsonrequest
device_code_list = body.get('device_code_list')
try:
api_path = dws_platform_url + '/get_qiekuaiji_data'
payload = {
"device_code_list": device_code_list
}
res = requests.post(api_path, data=json.dumps(payload), headers=headers, )
res_json = res.json()
return res_json
except Exception as e:
_logger.error(e)
return {
"code": 100,
"msg": str(e),
"data": {
},
"success": False,
"time": fields.Datetime.now()
}
@http.route('/get_yanxunlu_data', type='json', methods=['POST', 'OPTIONS'], auth="none", csrf=False, cors="*")
def get_yanxunlu_data(self):
body = http.request.jsonrequest
return self.common_dws_interface(body, '/get_real_time_device_data', cate="烟熏炉")
@http.route('/get_lashengmo_data', type='json', methods=['POST', 'OPTIONS'], auth="none", csrf=False, cors="*")
def get_lashengmo_data(self):
body = http.request.jsonrequest
return self.common_dws_interface(body, '/get_real_time_device_data', cate="拉伸膜包装机")
@http.route('/get_lishibaozhuang_data', type='json', methods=['POST', 'OPTIONS'], auth="none", csrf=False, cors="*")
def get_lishibaozhuang_data(self):
body = http.request.jsonrequest
return self.common_dws_interface(body, '/get_real_time_device_data', cate="制袋包装机")
@http.route('/roke/tht/get_abnormal_alarm_list', type='json', auth="none", methods=['POST', 'OPTIONS'], csrf=False, cors='*')
def get_abnormal_alarm_list(self):
"""
获取异常表单列表
:return:
"""
jsonrequest = http.request.jsonrequest
domain = []
abnormal_alarm_id = jsonrequest.get('abnormal_alarm_id', False)
#表单id
if abnormal_alarm_id:
domain.append(('id', '=', abnormal_alarm_id))
abnormal_id = jsonrequest.get('abnormal_id', False)
#异常类型id
if abnormal_id:
domain.append(('abnormal_id', '=', abnormal_id))
page_size = int(http.request.jsonrequest.get('page_size', 20))
page_no = int(http.request.jsonrequest.get('page_no', 1))
abnormal_alarm_ids = http.request.env(user=SUPERUSER_ID)['roke.abnormal.alarm'].search(domain, limit=page_size, offset=(page_no - 1) * page_size, order="originating_time desc")
abnormal_alarm_list = []
for item in abnormal_alarm_ids:
note = ''
if item.abnormal_id.name == '设备异常':
note = f"{item.equipment_id.name or ''}发生了故障"
elif item.abnormal_id.name == '缺料断料':
note = f"{item.sponsor.name or item.create_uid.name or ''} 发起了缺料申请"
abnormal_alarm_list.append({
"id": item.id, # 异常表单id
"abnormal_name": item.abnormal_id.name, # 异常类型
"originating_time": (item.originating_time + timedelta(hours=8)).strftime("%Y-%m-%d %H:%M:%S") if item.originating_time else '', #发起时间
"note": note, #报警表述
})
total = http.request.env(user=SUPERUSER_ID)['roke.abnormal.alarm'].search_count(domain)
return {
"state": "success",
"msgs": "获取成功",
"total": total,
"abnormal_alarm_list": abnormal_alarm_list
}
@http.route('/roke/tht/utilization_rate_top_5', type='json', auth='none', methods=['POST','OPTIONS',], csrf=False,cors="*")
def get_oee_top5(self):
try:
url = "https://dws-platform.xbg.rokeris.com/dev-api/public/device_efficiency/series_utilization_rate_top_5"
date = fields.Date.today().strftime("%Y-%m-%d")
eq_ids = http.request.env["roke.mes.equipment"].sudo().search([('code',"!=",'')])
query_data = [{"device_name": eq_id.name,"device_code": eq_id.code} for eq_id in eq_ids]
res = requests.post(url, data=json.dumps({
"device_code_list": query_data,
"start_date": date,
"plan_time_list":{}
}), headers=headers)
if res.status_code != 200:
return {"code": 1, "message": "获取数据失败", "data": None}
res = res.json()
data = res.get("data", {})
device_data = data.get("device_data", [])
return_data = [{"device_name": item.get("device_name"),
"today_utilization_rate": str(item.get("today_utilization_rate",0)) + "%",}
for item in device_data]
return {"code": "success", "message": "", "data": return_data,"date": date}
except Exception as e:
return {"code": "error", "message": f"获取数据失败:{str(e)}", "data": None,"date": date}
# -*- coding: utf-8 -*-
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")
env = Environment(loader=templateloader)
class ThtProject(http.Controller):
@http.route('/tht/get/equipment/working_time', type='json', auth="none", csrf=False, cors='*')
def get_equipment_working_time(self, **kwargs):
"""获取车间工作时间"""
jsonrequest = http.request.jsonrequest
plant_name = jsonrequest.get("plant_name", False)
domain = [("code", "!=", False)]
if plant_name:
domain.append(("plant_id.name", "=", plant_name))
equipment_ids = http.request.env['roke.mes.equipment'].sudo().search(domain)
today = datetime.now()
start_working_time = {}
end_working_time = {}
wait_time = {}
plant_ids = equipment_ids.mapped("plant_id")
for plant_id in plant_ids:
config_id = http.request.env['plant.working.time.config'].sudo().search([("plant_id", "=", plant_id.id)], limit=1)
for item in equipment_ids.filtered(lambda x: x.plant_id == plant_id):
start_hour = math.floor(config_id.start_time)
start_working_time[item.code] = datetime.combine(today, time(start_hour, int((config_id.start_time - start_hour) * 60))).strftime("%Y-%m-%d %H:%M:%S")
end_hour = math.floor(config_id.end_time)
end_working_time[item.code] = datetime.combine(today, time(end_hour, int((config_id.end_time - end_hour) * 60))).strftime("%Y-%m-%d %H:%M:%S")
wait_time[item.code] = config_id.wait_time
return {"state": "success", "msgs": "获取数据成功!",
"start_working_time": start_working_time,
"end_working_time": end_working_time,
"wait_time": wait_time}
class RokeMesThreeColourLightExt(RokeMesThreeColourLight):
#重写
@http.route("/roke/three_color_light/device_state_list", type="http", auth='none', cors='*', csrf=False)
def device_state_list(self, **kwargs):
# 自定义逻辑
_self = request
factory_code = "custom_factory_code_123" # 自定义 factory_code
data = {
"code": 1,
"message": "请求通过",
"data": {
"factory_code": factory_code,
"override": True # 添加额外字段
}
}
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,
cors='*')
def search_equipment(self):
"""
根据 plant_id category_id 查询设备(JSON POST)
请求示例:
{
"plant_name": ,
"category_name":
}
"""
# 获取请求数据
data = http.request.jsonrequest
plant_name = data.get('plant_name')
category_name = data.get('category_name')
data_acquisition_code = data.get('data_acquisition_code')
domain = []
if data_acquisition_code:
domain.append(('data_acquisition_code', 'in', data_acquisition_code))
# 构建查询条件
if plant_name:
domain.append(('plant_id.name', '=', plant_name))
if category_name:
domain.append(('category_id.name', '=', category_name))
if not domain:
return {
"state": "error",
"msgs": "参数不全;车间和 设备类别不能同时为空",
"data": []
}
# 查询设备
equipments = http.request.env['roke.mes.equipment'].sudo().search(domain)
# 构造响应数据
equipment_list = [{
'id': eq.id,
'device_name': eq.name,
'device_code': eq.code,
'data_acquisition_code': eq.data_acquisition_code,
'category': eq.category_id.name if eq.category_id else '',
'plant_name': eq.plant_id.name if eq.plant_id else '',
} for eq in equipments]
return {
'status': 'success',
'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
<odoo>
<data>
<!--
<record id="object0" model="tht_project.tht_project">
<field name="name">Object 0</field>
<field name="value">0</field>
</record>
<record id="object1" model="tht_project.tht_project">
<field name="name">Object 1</field>
<field name="value">10</field>
</record>
<record id="object2" model="tht_project.tht_project">
<field name="name">Object 2</field>
<field name="value">20</field>
</record>
<record id="object3" model="tht_project.tht_project">
<field name="name">Object 3</field>
<field name="value">30</field>
</record>
<record id="object4" model="tht_project.tht_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
from . import plant_working_time_config
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
# -*- coding: utf-8 -*-
# from odoo import models, fields, api
# class tht_project(models.Model):
# _name = 'tht_project.tht_project'
# _description = 'tht_project.tht_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
from odoo import api, fields, models
class PlantWorkingTimeConfig(models.Model):
_name = "plant.working.time.config"
_description = "车间工作时间配置"
plant_id = fields.Many2one("roke.plant", string="车间")
start_time = fields.Float(string="开始时间", default="0")
end_time = fields.Float(string="结束时间", default="0")
color = fields.Selection([
('red', '红'),
('yellow', '黄'),
('green', '绿'),
('blue', '蓝'),
('gray', '灰')
], string="颜色")
wait_time = fields.Float(string="等待时间")
\ No newline at end of file
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_plant_working_time_config_group_user,plant_working_time_config group_user,model_plant_working_time_config,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/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
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
!(function (e, t) {
"object" == typeof exports && "undefined" != typeof module
? (module.exports = t())
: "function" == typeof define && define.amd
? define(t)
: ((e = "undefined" != typeof globalThis ? globalThis : e || self).axios =
t());
})(this, function () {
"use strict";
function e(t) {
return (
(e =
"function" == typeof Symbol && "symbol" == typeof Symbol.iterator
? function (e) {
return typeof e;
}
: function (e) {
return e &&
"function" == typeof Symbol &&
e.constructor === Symbol &&
e !== Symbol.prototype
? "symbol"
: typeof e;
}),
e(t)
);
}
function t(e, t) {
if (!(e instanceof t))
throw new TypeError("Cannot call a class as a function");
}
function n(e, t) {
for (var n = 0; n < t.length; n++) {
var r = t[n];
(r.enumerable = r.enumerable || !1),
(r.configurable = !0),
"value" in r && (r.writable = !0),
Object.defineProperty(e, r.key, r);
}
}
function r(e, t, r) {
return (
t && n(e.prototype, t),
r && n(e, r),
Object.defineProperty(e, "prototype", { writable: !1 }),
e
);
}
function o(e, t) {
return (
(function (e) {
if (Array.isArray(e)) return e;
})(e) ||
(function (e, t) {
var n =
null == e
? null
: ("undefined" != typeof Symbol && e[Symbol.iterator]) ||
e["@@iterator"];
if (null == n) return;
var r,
o,
i = [],
a = !0,
s = !1;
try {
for (
n = n.call(e);
!(a = (r = n.next()).done) &&
(i.push(r.value), !t || i.length !== t);
a = !0
);
} catch (e) {
(s = !0), (o = e);
} finally {
try {
a || null == n.return || n.return();
} finally {
if (s) throw o;
}
}
return i;
})(e, t) ||
(function (e, t) {
if (!e) return;
if ("string" == typeof e) return i(e, t);
var n = Object.prototype.toString.call(e).slice(8, -1);
"Object" === n && e.constructor && (n = e.constructor.name);
if ("Map" === n || "Set" === n) return Array.from(e);
if (
"Arguments" === n ||
/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)
)
return i(e, t);
})(e, t) ||
(function () {
throw new TypeError(
"Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."
);
})()
);
}
function i(e, t) {
(null == t || t > e.length) && (t = e.length);
for (var n = 0, r = new Array(t); n < t; n++) r[n] = e[n];
return r;
}
function a(e, t) {
return function () {
return e.apply(t, arguments);
};
}
var s,
u = Object.prototype.toString,
c = Object.getPrototypeOf,
f =
((s = Object.create(null)),
function (e) {
var t = u.call(e);
return s[t] || (s[t] = t.slice(8, -1).toLowerCase());
}),
l = function (e) {
return (
(e = e.toLowerCase()),
function (t) {
return f(t) === e;
}
);
},
d = function (t) {
return function (n) {
return e(n) === t;
};
},
p = Array.isArray,
h = d("undefined");
var m = l("ArrayBuffer");
var y = d("string"),
v = d("function"),
b = d("number"),
g = function (t) {
return null !== t && "object" === e(t);
},
w = function (e) {
if ("object" !== f(e)) return !1;
var t = c(e);
return !(
(null !== t &&
t !== Object.prototype &&
null !== Object.getPrototypeOf(t)) ||
Symbol.toStringTag in e ||
Symbol.iterator in e
);
},
E = l("Date"),
O = l("File"),
S = l("Blob"),
R = l("FileList"),
A = l("URLSearchParams");
function T(t, n) {
var r,
o,
i = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {},
a = i.allOwnKeys,
s = void 0 !== a && a;
if (null != t)
if (("object" !== e(t) && (t = [t]), p(t)))
for (r = 0, o = t.length; r < o; r++) n.call(null, t[r], r, t);
else {
var u,
c = s ? Object.getOwnPropertyNames(t) : Object.keys(t),
f = c.length;
for (r = 0; r < f; r++) (u = c[r]), n.call(null, t[u], u, t);
}
}
function j(e, t) {
t = t.toLowerCase();
for (var n, r = Object.keys(e), o = r.length; o-- > 0; )
if (t === (n = r[o]).toLowerCase()) return n;
return null;
}
var N =
"undefined" != typeof globalThis
? globalThis
: "undefined" != typeof self
? self
: "undefined" != typeof window
? window
: global,
C = function (e) {
return !h(e) && e !== N;
};
var x,
P =
((x = "undefined" != typeof Uint8Array && c(Uint8Array)),
function (e) {
return x && e instanceof x;
}),
k = l("HTMLFormElement"),
U = (function (e) {
var t = Object.prototype.hasOwnProperty;
return function (e, n) {
return t.call(e, n);
};
})(),
_ = l("RegExp"),
F = function (e, t) {
var n = Object.getOwnPropertyDescriptors(e),
r = {};
T(n, function (n, o) {
!1 !== t(n, o, e) && (r[o] = n);
}),
Object.defineProperties(e, r);
},
B = "abcdefghijklmnopqrstuvwxyz",
L = "0123456789",
D = { DIGIT: L, ALPHA: B, ALPHA_DIGIT: B + B.toUpperCase() + L };
var I = l("AsyncFunction"),
q = {
isArray: p,
isArrayBuffer: m,
isBuffer: function (e) {
return (
null !== e &&
!h(e) &&
null !== e.constructor &&
!h(e.constructor) &&
v(e.constructor.isBuffer) &&
e.constructor.isBuffer(e)
);
},
isFormData: function (e) {
var t;
return (
e &&
(("function" == typeof FormData && e instanceof FormData) ||
(v(e.append) &&
("formdata" === (t = f(e)) ||
("object" === t &&
v(e.toString) &&
"[object FormData]" === e.toString()))))
);
},
isArrayBufferView: function (e) {
return "undefined" != typeof ArrayBuffer && ArrayBuffer.isView
? ArrayBuffer.isView(e)
: e && e.buffer && m(e.buffer);
},
isString: y,
isNumber: b,
isBoolean: function (e) {
return !0 === e || !1 === e;
},
isObject: g,
isPlainObject: w,
isUndefined: h,
isDate: E,
isFile: O,
isBlob: S,
isRegExp: _,
isFunction: v,
isStream: function (e) {
return g(e) && v(e.pipe);
},
isURLSearchParams: A,
isTypedArray: P,
isFileList: R,
forEach: T,
merge: function e() {
for (
var t = (C(this) && this) || {},
n = t.caseless,
r = {},
o = function (t, o) {
var i = (n && j(r, o)) || o;
w(r[i]) && w(t)
? (r[i] = e(r[i], t))
: w(t)
? (r[i] = e({}, t))
: p(t)
? (r[i] = t.slice())
: (r[i] = t);
},
i = 0,
a = arguments.length;
i < a;
i++
)
arguments[i] && T(arguments[i], o);
return r;
},
extend: function (e, t, n) {
var r =
arguments.length > 3 && void 0 !== arguments[3] ? arguments[3] : {},
o = r.allOwnKeys;
return (
T(
t,
function (t, r) {
n && v(t) ? (e[r] = a(t, n)) : (e[r] = t);
},
{ allOwnKeys: o }
),
e
);
},
trim: function (e) {
return e.trim
? e.trim()
: e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "");
},
stripBOM: function (e) {
return 65279 === e.charCodeAt(0) && (e = e.slice(1)), e;
},
inherits: function (e, t, n, r) {
(e.prototype = Object.create(t.prototype, r)),
(e.prototype.constructor = e),
Object.defineProperty(e, "super", { value: t.prototype }),
n && Object.assign(e.prototype, n);
},
toFlatObject: function (e, t, n, r) {
var o,
i,
a,
s = {};
if (((t = t || {}), null == e)) return t;
do {
for (i = (o = Object.getOwnPropertyNames(e)).length; i-- > 0; )
(a = o[i]),
(r && !r(a, e, t)) || s[a] || ((t[a] = e[a]), (s[a] = !0));
e = !1 !== n && c(e);
} while (e && (!n || n(e, t)) && e !== Object.prototype);
return t;
},
kindOf: f,
kindOfTest: l,
endsWith: function (e, t, n) {
(e = String(e)),
(void 0 === n || n > e.length) && (n = e.length),
(n -= t.length);
var r = e.indexOf(t, n);
return -1 !== r && r === n;
},
toArray: function (e) {
if (!e) return null;
if (p(e)) return e;
var t = e.length;
if (!b(t)) return null;
for (var n = new Array(t); t-- > 0; ) n[t] = e[t];
return n;
},
forEachEntry: function (e, t) {
for (
var n, r = (e && e[Symbol.iterator]).call(e);
(n = r.next()) && !n.done;
) {
var o = n.value;
t.call(e, o[0], o[1]);
}
},
matchAll: function (e, t) {
for (var n, r = []; null !== (n = e.exec(t)); ) r.push(n);
return r;
},
isHTMLForm: k,
hasOwnProperty: U,
hasOwnProp: U,
reduceDescriptors: F,
freezeMethods: function (e) {
F(e, function (t, n) {
if (v(e) && -1 !== ["arguments", "caller", "callee"].indexOf(n))
return !1;
var r = e[n];
v(r) &&
((t.enumerable = !1),
"writable" in t
? (t.writable = !1)
: t.set ||
(t.set = function () {
throw Error("Can not rewrite read-only method '" + n + "'");
}));
});
},
toObjectSet: function (e, t) {
var n = {},
r = function (e) {
e.forEach(function (e) {
n[e] = !0;
});
};
return p(e) ? r(e) : r(String(e).split(t)), n;
},
toCamelCase: function (e) {
return e
.toLowerCase()
.replace(/[-_\s]([a-z\d])(\w*)/g, function (e, t, n) {
return t.toUpperCase() + n;
});
},
noop: function () {},
toFiniteNumber: function (e, t) {
return (e = +e), Number.isFinite(e) ? e : t;
},
findKey: j,
global: N,
isContextDefined: C,
ALPHABET: D,
generateString: function () {
for (
var e =
arguments.length > 0 && void 0 !== arguments[0]
? arguments[0]
: 16,
t =
arguments.length > 1 && void 0 !== arguments[1]
? arguments[1]
: D.ALPHA_DIGIT,
n = "",
r = t.length;
e--;
)
n += t[(Math.random() * r) | 0];
return n;
},
isSpecCompliantForm: function (e) {
return !!(
e &&
v(e.append) &&
"FormData" === e[Symbol.toStringTag] &&
e[Symbol.iterator]
);
},
toJSONObject: function (e) {
var t = new Array(10);
return (function e(n, r) {
if (g(n)) {
if (t.indexOf(n) >= 0) return;
if (!("toJSON" in n)) {
t[r] = n;
var o = p(n) ? [] : {};
return (
T(n, function (t, n) {
var i = e(t, r + 1);
!h(i) && (o[n] = i);
}),
(t[r] = void 0),
o
);
}
}
return n;
})(e, 0);
},
isAsyncFn: I,
isThenable: function (e) {
return e && (g(e) || v(e)) && v(e.then) && v(e.catch);
},
};
function M(e, t, n, r, o) {
Error.call(this),
Error.captureStackTrace
? Error.captureStackTrace(this, this.constructor)
: (this.stack = new Error().stack),
(this.message = e),
(this.name = "AxiosError"),
t && (this.code = t),
n && (this.config = n),
r && (this.request = r),
o && (this.response = o);
}
q.inherits(M, Error, {
toJSON: function () {
return {
message: this.message,
name: this.name,
description: this.description,
number: this.number,
fileName: this.fileName,
lineNumber: this.lineNumber,
columnNumber: this.columnNumber,
stack: this.stack,
config: q.toJSONObject(this.config),
code: this.code,
status:
this.response && this.response.status ? this.response.status : null,
};
},
});
var z = M.prototype,
H = {};
[
"ERR_BAD_OPTION_VALUE",
"ERR_BAD_OPTION",
"ECONNABORTED",
"ETIMEDOUT",
"ERR_NETWORK",
"ERR_FR_TOO_MANY_REDIRECTS",
"ERR_DEPRECATED",
"ERR_BAD_RESPONSE",
"ERR_BAD_REQUEST",
"ERR_CANCELED",
"ERR_NOT_SUPPORT",
"ERR_INVALID_URL",
].forEach(function (e) {
H[e] = { value: e };
}),
Object.defineProperties(M, H),
Object.defineProperty(z, "isAxiosError", { value: !0 }),
(M.from = function (e, t, n, r, o, i) {
var a = Object.create(z);
return (
q.toFlatObject(
e,
a,
function (e) {
return e !== Error.prototype;
},
function (e) {
return "isAxiosError" !== e;
}
),
M.call(a, e.message, t, n, r, o),
(a.cause = e),
(a.name = e.name),
i && Object.assign(a, i),
a
);
});
function J(e) {
return q.isPlainObject(e) || q.isArray(e);
}
function W(e) {
return q.endsWith(e, "[]") ? e.slice(0, -2) : e;
}
function K(e, t, n) {
return e
? e
.concat(t)
.map(function (e, t) {
return (e = W(e)), !n && t ? "[" + e + "]" : e;
})
.join(n ? "." : "")
: t;
}
var V = q.toFlatObject(q, {}, null, function (e) {
return /^is[A-Z]/.test(e);
});
function G(t, n, r) {
if (!q.isObject(t)) throw new TypeError("target must be an object");
n = n || new FormData();
var o = (r = q.toFlatObject(
r,
{ metaTokens: !0, dots: !1, indexes: !1 },
!1,
function (e, t) {
return !q.isUndefined(t[e]);
}
)).metaTokens,
i = r.visitor || f,
a = r.dots,
s = r.indexes,
u =
(r.Blob || ("undefined" != typeof Blob && Blob)) &&
q.isSpecCompliantForm(n);
if (!q.isFunction(i)) throw new TypeError("visitor must be a function");
function c(e) {
if (null === e) return "";
if (q.isDate(e)) return e.toISOString();
if (!u && q.isBlob(e))
throw new M("Blob is not supported. Use a Buffer instead.");
return q.isArrayBuffer(e) || q.isTypedArray(e)
? u && "function" == typeof Blob
? new Blob([e])
: Buffer.from(e)
: e;
}
function f(t, r, i) {
var u = t;
if (t && !i && "object" === e(t))
if (q.endsWith(r, "{}"))
(r = o ? r : r.slice(0, -2)), (t = JSON.stringify(t));
else if (
(q.isArray(t) &&
(function (e) {
return q.isArray(e) && !e.some(J);
})(t)) ||
((q.isFileList(t) || q.endsWith(r, "[]")) && (u = q.toArray(t)))
)
return (
(r = W(r)),
u.forEach(function (e, t) {
!q.isUndefined(e) &&
null !== e &&
n.append(
!0 === s ? K([r], t, a) : null === s ? r : r + "[]",
c(e)
);
}),
!1
);
return !!J(t) || (n.append(K(i, r, a), c(t)), !1);
}
var l = [],
d = Object.assign(V, {
defaultVisitor: f,
convertValue: c,
isVisitable: J,
});
if (!q.isObject(t)) throw new TypeError("data must be an object");
return (
(function e(t, r) {
if (!q.isUndefined(t)) {
if (-1 !== l.indexOf(t))
throw Error("Circular reference detected in " + r.join("."));
l.push(t),
q.forEach(t, function (t, o) {
!0 ===
(!(q.isUndefined(t) || null === t) &&
i.call(n, t, q.isString(o) ? o.trim() : o, r, d)) &&
e(t, r ? r.concat(o) : [o]);
}),
l.pop();
}
})(t),
n
);
}
function $(e) {
var t = {
"!": "%21",
"'": "%27",
"(": "%28",
")": "%29",
"~": "%7E",
"%20": "+",
"%00": "\0",
};
return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g, function (e) {
return t[e];
});
}
function X(e, t) {
(this._pairs = []), e && G(e, this, t);
}
var Q = X.prototype;
function Z(e) {
return encodeURIComponent(e)
.replace(/%3A/gi, ":")
.replace(/%24/g, "$")
.replace(/%2C/gi, ",")
.replace(/%20/g, "+")
.replace(/%5B/gi, "[")
.replace(/%5D/gi, "]");
}
function Y(e, t, n) {
if (!t) return e;
var r,
o = (n && n.encode) || Z,
i = n && n.serialize;
if (
(r = i
? i(t, n)
: q.isURLSearchParams(t)
? t.toString()
: new X(t, n).toString(o))
) {
var a = e.indexOf("#");
-1 !== a && (e = e.slice(0, a)),
(e += (-1 === e.indexOf("?") ? "?" : "&") + r);
}
return e;
}
(Q.append = function (e, t) {
this._pairs.push([e, t]);
}),
(Q.toString = function (e) {
var t = e
? function (t) {
return e.call(this, t, $);
}
: $;
return this._pairs
.map(function (e) {
return t(e[0]) + "=" + t(e[1]);
}, "")
.join("&");
});
var ee,
te = (function () {
function e() {
t(this, e), (this.handlers = []);
}
return (
r(e, [
{
key: "use",
value: function (e, t, n) {
return (
this.handlers.push({
fulfilled: e,
rejected: t,
synchronous: !!n && n.synchronous,
runWhen: n ? n.runWhen : null,
}),
this.handlers.length - 1
);
},
},
{
key: "eject",
value: function (e) {
this.handlers[e] && (this.handlers[e] = null);
},
},
{
key: "clear",
value: function () {
this.handlers && (this.handlers = []);
},
},
{
key: "forEach",
value: function (e) {
q.forEach(this.handlers, function (t) {
null !== t && e(t);
});
},
},
]),
e
);
})(),
ne = {
silentJSONParsing: !0,
forcedJSONParsing: !0,
clarifyTimeoutError: !1,
},
re = {
isBrowser: !0,
classes: {
URLSearchParams:
"undefined" != typeof URLSearchParams ? URLSearchParams : X,
FormData: "undefined" != typeof FormData ? FormData : null,
Blob: "undefined" != typeof Blob ? Blob : null,
},
isStandardBrowserEnv:
("undefined" == typeof navigator ||
("ReactNative" !== (ee = navigator.product) &&
"NativeScript" !== ee &&
"NS" !== ee)) &&
"undefined" != typeof window &&
"undefined" != typeof document,
isStandardBrowserWebWorkerEnv:
"undefined" != typeof WorkerGlobalScope &&
self instanceof WorkerGlobalScope &&
"function" == typeof self.importScripts,
protocols: ["http", "https", "file", "blob", "url", "data"],
};
function oe(e) {
function t(e, n, r, o) {
var i = e[o++],
a = Number.isFinite(+i),
s = o >= e.length;
return (
(i = !i && q.isArray(r) ? r.length : i),
s
? (q.hasOwnProp(r, i) ? (r[i] = [r[i], n]) : (r[i] = n), !a)
: ((r[i] && q.isObject(r[i])) || (r[i] = []),
t(e, n, r[i], o) &&
q.isArray(r[i]) &&
(r[i] = (function (e) {
var t,
n,
r = {},
o = Object.keys(e),
i = o.length;
for (t = 0; t < i; t++) r[(n = o[t])] = e[n];
return r;
})(r[i])),
!a)
);
}
if (q.isFormData(e) && q.isFunction(e.entries)) {
var n = {};
return (
q.forEachEntry(e, function (e, r) {
t(
(function (e) {
return q.matchAll(/\w+|\[(\w*)]/g, e).map(function (e) {
return "[]" === e[0] ? "" : e[1] || e[0];
});
})(e),
r,
n,
0
);
}),
n
);
}
return null;
}
var ie = { "Content-Type": void 0 };
var ae = {
transitional: ne,
adapter: ["xhr", "http"],
transformRequest: [
function (e, t) {
var n,
r = t.getContentType() || "",
o = r.indexOf("application/json") > -1,
i = q.isObject(e);
if ((i && q.isHTMLForm(e) && (e = new FormData(e)), q.isFormData(e)))
return o && o ? JSON.stringify(oe(e)) : e;
if (
q.isArrayBuffer(e) ||
q.isBuffer(e) ||
q.isStream(e) ||
q.isFile(e) ||
q.isBlob(e)
)
return e;
if (q.isArrayBufferView(e)) return e.buffer;
if (q.isURLSearchParams(e))
return (
t.setContentType(
"application/x-www-form-urlencoded;charset=utf-8",
!1
),
e.toString()
);
if (i) {
if (r.indexOf("application/x-www-form-urlencoded") > -1)
return (function (e, t) {
return G(
e,
new re.classes.URLSearchParams(),
Object.assign(
{
visitor: function (e, t, n, r) {
return re.isNode && q.isBuffer(e)
? (this.append(t, e.toString("base64")), !1)
: r.defaultVisitor.apply(this, arguments);
},
},
t
)
);
})(e, this.formSerializer).toString();
if ((n = q.isFileList(e)) || r.indexOf("multipart/form-data") > -1) {
var a = this.env && this.env.FormData;
return G(
n ? { "files[]": e } : e,
a && new a(),
this.formSerializer
);
}
}
return i || o
? (t.setContentType("application/json", !1),
(function (e, t, n) {
if (q.isString(e))
try {
return (t || JSON.parse)(e), q.trim(e);
} catch (e) {
if ("SyntaxError" !== e.name) throw e;
}
return (n || JSON.stringify)(e);
})(e))
: e;
},
],
transformResponse: [
function (e) {
var t = this.transitional || ae.transitional,
n = t && t.forcedJSONParsing,
r = "json" === this.responseType;
if (e && q.isString(e) && ((n && !this.responseType) || r)) {
var o = !(t && t.silentJSONParsing) && r;
try {
return JSON.parse(e);
} catch (e) {
if (o) {
if ("SyntaxError" === e.name)
throw M.from(e, M.ERR_BAD_RESPONSE, this, null, this.response);
throw e;
}
}
}
return e;
},
],
timeout: 0,
xsrfCookieName: "XSRF-TOKEN",
xsrfHeaderName: "X-XSRF-TOKEN",
maxContentLength: -1,
maxBodyLength: -1,
env: { FormData: re.classes.FormData, Blob: re.classes.Blob },
validateStatus: function (e) {
return e >= 200 && e < 300;
},
headers: { common: { Accept: "application/json, text/plain, */*" } },
};
q.forEach(["delete", "get", "head"], function (e) {
ae.headers[e] = {};
}),
q.forEach(["post", "put", "patch"], function (e) {
ae.headers[e] = q.merge(ie);
});
var se = ae,
ue = q.toObjectSet([
"age",
"authorization",
"content-length",
"content-type",
"etag",
"expires",
"from",
"host",
"if-modified-since",
"if-unmodified-since",
"last-modified",
"location",
"max-forwards",
"proxy-authorization",
"referer",
"retry-after",
"user-agent",
]),
ce = Symbol("internals");
function fe(e) {
return e && String(e).trim().toLowerCase();
}
function le(e) {
return !1 === e || null == e ? e : q.isArray(e) ? e.map(le) : String(e);
}
function de(e, t, n, r, o) {
return q.isFunction(r)
? r.call(this, t, n)
: (o && (t = n),
q.isString(t)
? q.isString(r)
? -1 !== t.indexOf(r)
: q.isRegExp(r)
? r.test(t)
: void 0
: void 0);
}
var pe = (function (e, n) {
function i(e) {
t(this, i), e && this.set(e);
}
return (
r(
i,
[
{
key: "set",
value: function (e, t, n) {
var r = this;
function o(e, t, n) {
var o = fe(t);
if (!o)
throw new Error("header name must be a non-empty string");
var i = q.findKey(r, o);
(!i ||
void 0 === r[i] ||
!0 === n ||
(void 0 === n && !1 !== r[i])) &&
(r[i || t] = le(e));
}
var i,
a,
s,
u,
c,
f = function (e, t) {
return q.forEach(e, function (e, n) {
return o(e, n, t);
});
};
return (
q.isPlainObject(e) || e instanceof this.constructor
? f(e, t)
: q.isString(e) &&
(e = e.trim()) &&
!/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim())
? f(
((c = {}),
(i = e) &&
i.split("\n").forEach(function (e) {
(u = e.indexOf(":")),
(a = e.substring(0, u).trim().toLowerCase()),
(s = e.substring(u + 1).trim()),
!a ||
(c[a] && ue[a]) ||
("set-cookie" === a
? c[a]
? c[a].push(s)
: (c[a] = [s])
: (c[a] = c[a] ? c[a] + ", " + s : s));
}),
c),
t
)
: null != e && o(t, e, n),
this
);
},
},
{
key: "get",
value: function (e, t) {
if ((e = fe(e))) {
var n = q.findKey(this, e);
if (n) {
var r = this[n];
if (!t) return r;
if (!0 === t)
return (function (e) {
for (
var t,
n = Object.create(null),
r = /([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;
(t = r.exec(e));
)
n[t[1]] = t[2];
return n;
})(r);
if (q.isFunction(t)) return t.call(this, r, n);
if (q.isRegExp(t)) return t.exec(r);
throw new TypeError("parser must be boolean|regexp|function");
}
}
},
},
{
key: "has",
value: function (e, t) {
if ((e = fe(e))) {
var n = q.findKey(this, e);
return !(
!n ||
void 0 === this[n] ||
(t && !de(0, this[n], n, t))
);
}
return !1;
},
},
{
key: "delete",
value: function (e, t) {
var n = this,
r = !1;
function o(e) {
if ((e = fe(e))) {
var o = q.findKey(n, e);
!o || (t && !de(0, n[o], o, t)) || (delete n[o], (r = !0));
}
}
return q.isArray(e) ? e.forEach(o) : o(e), r;
},
},
{
key: "clear",
value: function (e) {
for (var t = Object.keys(this), n = t.length, r = !1; n--; ) {
var o = t[n];
(e && !de(0, this[o], o, e, !0)) || (delete this[o], (r = !0));
}
return r;
},
},
{
key: "normalize",
value: function (e) {
var t = this,
n = {};
return (
q.forEach(this, function (r, o) {
var i = q.findKey(n, o);
if (i) return (t[i] = le(r)), void delete t[o];
var a = e
? (function (e) {
return e
.trim()
.toLowerCase()
.replace(/([a-z\d])(\w*)/g, function (e, t, n) {
return t.toUpperCase() + n;
});
})(o)
: String(o).trim();
a !== o && delete t[o], (t[a] = le(r)), (n[a] = !0);
}),
this
);
},
},
{
key: "concat",
value: function () {
for (
var e, t = arguments.length, n = new Array(t), r = 0;
r < t;
r++
)
n[r] = arguments[r];
return (e = this.constructor).concat.apply(e, [this].concat(n));
},
},
{
key: "toJSON",
value: function (e) {
var t = Object.create(null);
return (
q.forEach(this, function (n, r) {
null != n &&
!1 !== n &&
(t[r] = e && q.isArray(n) ? n.join(", ") : n);
}),
t
);
},
},
{
key: Symbol.iterator,
value: function () {
return Object.entries(this.toJSON())[Symbol.iterator]();
},
},
{
key: "toString",
value: function () {
return Object.entries(this.toJSON())
.map(function (e) {
var t = o(e, 2);
return t[0] + ": " + t[1];
})
.join("\n");
},
},
{
key: Symbol.toStringTag,
get: function () {
return "AxiosHeaders";
},
},
],
[
{
key: "from",
value: function (e) {
return e instanceof this ? e : new this(e);
},
},
{
key: "concat",
value: function (e) {
for (
var t = new this(e),
n = arguments.length,
r = new Array(n > 1 ? n - 1 : 0),
o = 1;
o < n;
o++
)
r[o - 1] = arguments[o];
return (
r.forEach(function (e) {
return t.set(e);
}),
t
);
},
},
{
key: "accessor",
value: function (e) {
var t = (this[ce] = this[ce] = { accessors: {} }).accessors,
n = this.prototype;
function r(e) {
var r = fe(e);
t[r] ||
(!(function (e, t) {
var n = q.toCamelCase(" " + t);
["get", "set", "has"].forEach(function (r) {
Object.defineProperty(e, r + n, {
value: function (e, n, o) {
return this[r].call(this, t, e, n, o);
},
configurable: !0,
});
});
})(n, e),
(t[r] = !0));
}
return q.isArray(e) ? e.forEach(r) : r(e), this;
},
},
]
),
i
);
})();
pe.accessor([
"Content-Type",
"Content-Length",
"Accept",
"Accept-Encoding",
"User-Agent",
"Authorization",
]),
q.freezeMethods(pe.prototype),
q.freezeMethods(pe);
var he = pe;
function me(e, t) {
var n = this || se,
r = t || n,
o = he.from(r.headers),
i = r.data;
return (
q.forEach(e, function (e) {
i = e.call(n, i, o.normalize(), t ? t.status : void 0);
}),
o.normalize(),
i
);
}
function ye(e) {
return !(!e || !e.__CANCEL__);
}
function ve(e, t, n) {
M.call(this, null == e ? "canceled" : e, M.ERR_CANCELED, t, n),
(this.name = "CanceledError");
}
q.inherits(ve, M, { __CANCEL__: !0 });
var be = re.isStandardBrowserEnv
? {
write: function (e, t, n, r, o, i) {
var a = [];
a.push(e + "=" + encodeURIComponent(t)),
q.isNumber(n) && a.push("expires=" + new Date(n).toGMTString()),
q.isString(r) && a.push("path=" + r),
q.isString(o) && a.push("domain=" + o),
!0 === i && a.push("secure"),
(document.cookie = a.join("; "));
},
read: function (e) {
var t = document.cookie.match(
new RegExp("(^|;\\s*)(" + e + ")=([^;]*)")
);
return t ? decodeURIComponent(t[3]) : null;
},
remove: function (e) {
this.write(e, "", Date.now() - 864e5);
},
}
: {
write: function () {},
read: function () {
return null;
},
remove: function () {},
};
function ge(e, t) {
return e && !/^([a-z][a-z\d+\-.]*:)?\/\//i.test(t)
? (function (e, t) {
return t ? e.replace(/\/+$/, "") + "/" + t.replace(/^\/+/, "") : e;
})(e, t)
: t;
}
var we = re.isStandardBrowserEnv
? (function () {
var e,
t = /(msie|trident)/i.test(navigator.userAgent),
n = document.createElement("a");
function r(e) {
var r = e;
return (
t && (n.setAttribute("href", r), (r = n.href)),
n.setAttribute("href", r),
{
href: n.href,
protocol: n.protocol ? n.protocol.replace(/:$/, "") : "",
host: n.host,
search: n.search ? n.search.replace(/^\?/, "") : "",
hash: n.hash ? n.hash.replace(/^#/, "") : "",
hostname: n.hostname,
port: n.port,
pathname:
"/" === n.pathname.charAt(0) ? n.pathname : "/" + n.pathname,
}
);
}
return (
(e = r(window.location.href)),
function (t) {
var n = q.isString(t) ? r(t) : t;
return n.protocol === e.protocol && n.host === e.host;
}
);
})()
: function () {
return !0;
};
function Ee(e, t) {
var n = 0,
r = (function (e, t) {
e = e || 10;
var n,
r = new Array(e),
o = new Array(e),
i = 0,
a = 0;
return (
(t = void 0 !== t ? t : 1e3),
function (s) {
var u = Date.now(),
c = o[a];
n || (n = u), (r[i] = s), (o[i] = u);
for (var f = a, l = 0; f !== i; ) (l += r[f++]), (f %= e);
if (((i = (i + 1) % e) === a && (a = (a + 1) % e), !(u - n < t))) {
var d = c && u - c;
return d ? Math.round((1e3 * l) / d) : void 0;
}
}
);
})(50, 250);
return function (o) {
var i = o.loaded,
a = o.lengthComputable ? o.total : void 0,
s = i - n,
u = r(s);
n = i;
var c = {
loaded: i,
total: a,
progress: a ? i / a : void 0,
bytes: s,
rate: u || void 0,
estimated: u && a && i <= a ? (a - i) / u : void 0,
event: o,
};
(c[t ? "download" : "upload"] = !0), e(c);
};
}
var Oe = {
http: null,
xhr:
"undefined" != typeof XMLHttpRequest &&
function (e) {
return new Promise(function (t, n) {
var r,
o = e.data,
i = he.from(e.headers).normalize(),
a = e.responseType;
function s() {
e.cancelToken && e.cancelToken.unsubscribe(r),
e.signal && e.signal.removeEventListener("abort", r);
}
q.isFormData(o) &&
(re.isStandardBrowserEnv || re.isStandardBrowserWebWorkerEnv
? i.setContentType(!1)
: i.setContentType("multipart/form-data;", !1));
var u = new XMLHttpRequest();
if (e.auth) {
var c = e.auth.username || "",
f = e.auth.password
? unescape(encodeURIComponent(e.auth.password))
: "";
i.set("Authorization", "Basic " + btoa(c + ":" + f));
}
var l = ge(e.baseURL, e.url);
function d() {
if (u) {
var r = he.from(
"getAllResponseHeaders" in u && u.getAllResponseHeaders()
);
!(function (e, t, n) {
var r = n.config.validateStatus;
n.status && r && !r(n.status)
? t(
new M(
"Request failed with status code " + n.status,
[M.ERR_BAD_REQUEST, M.ERR_BAD_RESPONSE][
Math.floor(n.status / 100) - 4
],
n.config,
n.request,
n
)
)
: e(n);
})(
function (e) {
t(e), s();
},
function (e) {
n(e), s();
},
{
data:
a && "text" !== a && "json" !== a
? u.response
: u.responseText,
status: u.status,
statusText: u.statusText,
headers: r,
config: e,
request: u,
}
),
(u = null);
}
}
if (
(u.open(
e.method.toUpperCase(),
Y(l, e.params, e.paramsSerializer),
!0
),
(u.timeout = e.timeout),
"onloadend" in u
? (u.onloadend = d)
: (u.onreadystatechange = function () {
u &&
4 === u.readyState &&
(0 !== u.status ||
(u.responseURL &&
0 === u.responseURL.indexOf("file:"))) &&
setTimeout(d);
}),
(u.onabort = function () {
u &&
(n(new M("Request aborted", M.ECONNABORTED, e, u)), (u = null));
}),
(u.onerror = function () {
n(new M("Network Error", M.ERR_NETWORK, e, u)), (u = null);
}),
(u.ontimeout = function () {
var t = e.timeout
? "timeout of " + e.timeout + "ms exceeded"
: "timeout exceeded",
r = e.transitional || ne;
e.timeoutErrorMessage && (t = e.timeoutErrorMessage),
n(
new M(
t,
r.clarifyTimeoutError ? M.ETIMEDOUT : M.ECONNABORTED,
e,
u
)
),
(u = null);
}),
re.isStandardBrowserEnv)
) {
var p =
(e.withCredentials || we(l)) &&
e.xsrfCookieName &&
be.read(e.xsrfCookieName);
p && i.set(e.xsrfHeaderName, p);
}
void 0 === o && i.setContentType(null),
"setRequestHeader" in u &&
q.forEach(i.toJSON(), function (e, t) {
u.setRequestHeader(t, e);
}),
q.isUndefined(e.withCredentials) ||
(u.withCredentials = !!e.withCredentials),
a && "json" !== a && (u.responseType = e.responseType),
"function" == typeof e.onDownloadProgress &&
u.addEventListener("progress", Ee(e.onDownloadProgress, !0)),
"function" == typeof e.onUploadProgress &&
u.upload &&
u.upload.addEventListener("progress", Ee(e.onUploadProgress)),
(e.cancelToken || e.signal) &&
((r = function (t) {
u &&
(n(!t || t.type ? new ve(null, e, u) : t),
u.abort(),
(u = null));
}),
e.cancelToken && e.cancelToken.subscribe(r),
e.signal &&
(e.signal.aborted
? r()
: e.signal.addEventListener("abort", r)));
var h,
m = ((h = /^([-+\w]{1,25})(:?\/\/|:)/.exec(l)) && h[1]) || "";
m && -1 === re.protocols.indexOf(m)
? n(new M("Unsupported protocol " + m + ":", M.ERR_BAD_REQUEST, e))
: u.send(o || null);
});
},
};
q.forEach(Oe, function (e, t) {
if (e) {
try {
Object.defineProperty(e, "name", { value: t });
} catch (e) {}
Object.defineProperty(e, "adapterName", { value: t });
}
});
var Se = function (e) {
for (
var t, n, r = (e = q.isArray(e) ? e : [e]).length, o = 0;
o < r && ((t = e[o]), !(n = q.isString(t) ? Oe[t.toLowerCase()] : t));
o++
);
if (!n) {
if (!1 === n)
throw new M(
"Adapter ".concat(t, " is not supported by the environment"),
"ERR_NOT_SUPPORT"
);
throw new Error(
q.hasOwnProp(Oe, t)
? "Adapter '".concat(t, "' is not available in the build")
: "Unknown adapter '".concat(t, "'")
);
}
if (!q.isFunction(n)) throw new TypeError("adapter is not a function");
return n;
};
function Re(e) {
if (
(e.cancelToken && e.cancelToken.throwIfRequested(),
e.signal && e.signal.aborted)
)
throw new ve(null, e);
}
function Ae(e) {
return (
Re(e),
(e.headers = he.from(e.headers)),
(e.data = me.call(e, e.transformRequest)),
-1 !== ["post", "put", "patch"].indexOf(e.method) &&
e.headers.setContentType("application/x-www-form-urlencoded", !1),
Se(e.adapter || se.adapter)(e).then(
function (t) {
return (
Re(e),
(t.data = me.call(e, e.transformResponse, t)),
(t.headers = he.from(t.headers)),
t
);
},
function (t) {
return (
ye(t) ||
(Re(e),
t &&
t.response &&
((t.response.data = me.call(
e,
e.transformResponse,
t.response
)),
(t.response.headers = he.from(t.response.headers)))),
Promise.reject(t)
);
}
)
);
}
var Te = function (e) {
return e instanceof he ? e.toJSON() : e;
};
function je(e, t) {
t = t || {};
var n = {};
function r(e, t, n) {
return q.isPlainObject(e) && q.isPlainObject(t)
? q.merge.call({ caseless: n }, e, t)
: q.isPlainObject(t)
? q.merge({}, t)
: q.isArray(t)
? t.slice()
: t;
}
function o(e, t, n) {
return q.isUndefined(t)
? q.isUndefined(e)
? void 0
: r(void 0, e, n)
: r(e, t, n);
}
function i(e, t) {
if (!q.isUndefined(t)) return r(void 0, t);
}
function a(e, t) {
return q.isUndefined(t)
? q.isUndefined(e)
? void 0
: r(void 0, e)
: r(void 0, t);
}
function s(n, o, i) {
return i in t ? r(n, o) : i in e ? r(void 0, n) : void 0;
}
var u = {
url: i,
method: i,
data: i,
baseURL: a,
transformRequest: a,
transformResponse: a,
paramsSerializer: a,
timeout: a,
timeoutMessage: a,
withCredentials: a,
adapter: a,
responseType: a,
xsrfCookieName: a,
xsrfHeaderName: a,
onUploadProgress: a,
onDownloadProgress: a,
decompress: a,
maxContentLength: a,
maxBodyLength: a,
beforeRedirect: a,
transport: a,
httpAgent: a,
httpsAgent: a,
cancelToken: a,
socketPath: a,
responseEncoding: a,
validateStatus: s,
headers: function (e, t) {
return o(Te(e), Te(t), !0);
},
};
return (
q.forEach(Object.keys(Object.assign({}, e, t)), function (r) {
var i = u[r] || o,
a = i(e[r], t[r], r);
(q.isUndefined(a) && i !== s) || (n[r] = a);
}),
n
);
}
var Ne = "1.4.0",
Ce = {};
["object", "boolean", "number", "function", "string", "symbol"].forEach(
function (t, n) {
Ce[t] = function (r) {
return e(r) === t || "a" + (n < 1 ? "n " : " ") + t;
};
}
);
var xe = {};
Ce.transitional = function (e, t, n) {
function r(e, t) {
return (
"[Axios v1.4.0] Transitional option '" +
e +
"'" +
t +
(n ? ". " + n : "")
);
}
return function (n, o, i) {
if (!1 === e)
throw new M(
r(o, " has been removed" + (t ? " in " + t : "")),
M.ERR_DEPRECATED
);
return (
t &&
!xe[o] &&
((xe[o] = !0),
console.warn(
r(
o,
" has been deprecated since v" +
t +
" and will be removed in the near future"
)
)),
!e || e(n, o, i)
);
};
};
var Pe = {
assertOptions: function (t, n, r) {
if ("object" !== e(t))
throw new M("options must be an object", M.ERR_BAD_OPTION_VALUE);
for (var o = Object.keys(t), i = o.length; i-- > 0; ) {
var a = o[i],
s = n[a];
if (s) {
var u = t[a],
c = void 0 === u || s(u, a, t);
if (!0 !== c)
throw new M(
"option " + a + " must be " + c,
M.ERR_BAD_OPTION_VALUE
);
} else if (!0 !== r)
throw new M("Unknown option " + a, M.ERR_BAD_OPTION);
}
},
validators: Ce,
},
ke = Pe.validators,
Ue = (function () {
function e(n) {
t(this, e),
(this.defaults = n),
(this.interceptors = { request: new te(), response: new te() });
}
return (
r(e, [
{
key: "request",
value: function (e, t) {
"string" == typeof e ? ((t = t || {}).url = e) : (t = e || {});
var n,
r = (t = je(this.defaults, t)),
o = r.transitional,
i = r.paramsSerializer,
a = r.headers;
void 0 !== o &&
Pe.assertOptions(
o,
{
silentJSONParsing: ke.transitional(ke.boolean),
forcedJSONParsing: ke.transitional(ke.boolean),
clarifyTimeoutError: ke.transitional(ke.boolean),
},
!1
),
null != i &&
(q.isFunction(i)
? (t.paramsSerializer = { serialize: i })
: Pe.assertOptions(
i,
{ encode: ke.function, serialize: ke.function },
!0
)),
(t.method = (
t.method ||
this.defaults.method ||
"get"
).toLowerCase()),
(n = a && q.merge(a.common, a[t.method])) &&
q.forEach(
["delete", "get", "head", "post", "put", "patch", "common"],
function (e) {
delete a[e];
}
),
(t.headers = he.concat(n, a));
var s = [],
u = !0;
this.interceptors.request.forEach(function (e) {
("function" == typeof e.runWhen && !1 === e.runWhen(t)) ||
((u = u && e.synchronous),
s.unshift(e.fulfilled, e.rejected));
});
var c,
f = [];
this.interceptors.response.forEach(function (e) {
f.push(e.fulfilled, e.rejected);
});
var l,
d = 0;
if (!u) {
var p = [Ae.bind(this), void 0];
for (
p.unshift.apply(p, s),
p.push.apply(p, f),
l = p.length,
c = Promise.resolve(t);
d < l;
)
c = c.then(p[d++], p[d++]);
return c;
}
l = s.length;
var h = t;
for (d = 0; d < l; ) {
var m = s[d++],
y = s[d++];
try {
h = m(h);
} catch (e) {
y.call(this, e);
break;
}
}
try {
c = Ae.call(this, h);
} catch (e) {
return Promise.reject(e);
}
for (d = 0, l = f.length; d < l; ) c = c.then(f[d++], f[d++]);
return c;
},
},
{
key: "getUri",
value: function (e) {
return Y(
ge((e = je(this.defaults, e)).baseURL, e.url),
e.params,
e.paramsSerializer
);
},
},
]),
e
);
})();
q.forEach(["delete", "get", "head", "options"], function (e) {
Ue.prototype[e] = function (t, n) {
return this.request(
je(n || {}, { method: e, url: t, data: (n || {}).data })
);
};
}),
q.forEach(["post", "put", "patch"], function (e) {
function t(t) {
return function (n, r, o) {
return this.request(
je(o || {}, {
method: e,
headers: t ? { "Content-Type": "multipart/form-data" } : {},
url: n,
data: r,
})
);
};
}
(Ue.prototype[e] = t()), (Ue.prototype[e + "Form"] = t(!0));
});
var _e = Ue,
Fe = (function () {
function e(n) {
if ((t(this, e), "function" != typeof n))
throw new TypeError("executor must be a function.");
var r;
this.promise = new Promise(function (e) {
r = e;
});
var o = this;
this.promise.then(function (e) {
if (o._listeners) {
for (var t = o._listeners.length; t-- > 0; ) o._listeners[t](e);
o._listeners = null;
}
}),
(this.promise.then = function (e) {
var t,
n = new Promise(function (e) {
o.subscribe(e), (t = e);
}).then(e);
return (
(n.cancel = function () {
o.unsubscribe(t);
}),
n
);
}),
n(function (e, t, n) {
o.reason || ((o.reason = new ve(e, t, n)), r(o.reason));
});
}
return (
r(
e,
[
{
key: "throwIfRequested",
value: function () {
if (this.reason) throw this.reason;
},
},
{
key: "subscribe",
value: function (e) {
this.reason
? e(this.reason)
: this._listeners
? this._listeners.push(e)
: (this._listeners = [e]);
},
},
{
key: "unsubscribe",
value: function (e) {
if (this._listeners) {
var t = this._listeners.indexOf(e);
-1 !== t && this._listeners.splice(t, 1);
}
},
},
],
[
{
key: "source",
value: function () {
var t;
return {
token: new e(function (e) {
t = e;
}),
cancel: t,
};
},
},
]
),
e
);
})();
var Be = {
Continue: 100,
SwitchingProtocols: 101,
Processing: 102,
EarlyHints: 103,
Ok: 200,
Created: 201,
Accepted: 202,
NonAuthoritativeInformation: 203,
NoContent: 204,
ResetContent: 205,
PartialContent: 206,
MultiStatus: 207,
AlreadyReported: 208,
ImUsed: 226,
MultipleChoices: 300,
MovedPermanently: 301,
Found: 302,
SeeOther: 303,
NotModified: 304,
UseProxy: 305,
Unused: 306,
TemporaryRedirect: 307,
PermanentRedirect: 308,
BadRequest: 400,
Unauthorized: 401,
PaymentRequired: 402,
Forbidden: 403,
NotFound: 404,
MethodNotAllowed: 405,
NotAcceptable: 406,
ProxyAuthenticationRequired: 407,
RequestTimeout: 408,
Conflict: 409,
Gone: 410,
LengthRequired: 411,
PreconditionFailed: 412,
PayloadTooLarge: 413,
UriTooLong: 414,
UnsupportedMediaType: 415,
RangeNotSatisfiable: 416,
ExpectationFailed: 417,
ImATeapot: 418,
MisdirectedRequest: 421,
UnprocessableEntity: 422,
Locked: 423,
FailedDependency: 424,
TooEarly: 425,
UpgradeRequired: 426,
PreconditionRequired: 428,
TooManyRequests: 429,
RequestHeaderFieldsTooLarge: 431,
UnavailableForLegalReasons: 451,
InternalServerError: 500,
NotImplemented: 501,
BadGateway: 502,
ServiceUnavailable: 503,
GatewayTimeout: 504,
HttpVersionNotSupported: 505,
VariantAlsoNegotiates: 506,
InsufficientStorage: 507,
LoopDetected: 508,
NotExtended: 510,
NetworkAuthenticationRequired: 511,
};
Object.entries(Be).forEach(function (e) {
var t = o(e, 2),
n = t[0],
r = t[1];
Be[r] = n;
});
var Le = Be;
var De = (function e(t) {
var n = new _e(t),
r = a(_e.prototype.request, n);
return (
q.extend(r, _e.prototype, n, { allOwnKeys: !0 }),
q.extend(r, n, null, { allOwnKeys: !0 }),
(r.create = function (n) {
return e(je(t, n));
}),
r
);
})(se);
return (
(De.Axios = _e),
(De.CanceledError = ve),
(De.CancelToken = Fe),
(De.isCancel = ye),
(De.VERSION = Ne),
(De.toFormData = G),
(De.AxiosError = M),
(De.Cancel = De.CanceledError),
(De.all = function (e) {
return Promise.all(e);
}),
(De.spread = function (e) {
return function (t) {
return e.apply(null, t);
};
}),
(De.isAxiosError = function (e) {
return q.isObject(e) && !0 === e.isAxiosError;
}),
(De.mergeConfig = je),
(De.AxiosHeaders = he),
(De.formToJSON = function (e) {
return oe(q.isHTMLForm(e) ? new FormData(e) : e);
}),
(De.HttpStatusCode = Le),
(De.default = De),
De
);
});
This source diff could not be displayed because it is too large. You can view the blob instead.
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.moment=t()}(this,function(){"use strict";var H;function f(){return H.apply(null,arguments)}function a(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function F(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function c(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function L(e){if(Object.getOwnPropertyNames)return 0===Object.getOwnPropertyNames(e).length;for(var t in e)if(c(e,t))return;return 1}function o(e){return void 0===e}function u(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function V(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function G(e,t){for(var n=[],s=e.length,i=0;i<s;++i)n.push(t(e[i],i));return n}function E(e,t){for(var n in t)c(t,n)&&(e[n]=t[n]);return c(t,"toString")&&(e.toString=t.toString),c(t,"valueOf")&&(e.valueOf=t.valueOf),e}function l(e,t,n,s){return Wt(e,t,n,s,!0).utc()}function m(e){return null==e._pf&&(e._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidEra:null,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],era:null,meridiem:null,rfc2822:!1,weekdayMismatch:!1}),e._pf}function A(e){var t,n,s=e._d&&!isNaN(e._d.getTime());return s&&(t=m(e),n=j.call(t.parsedDateParts,function(e){return null!=e}),s=t.overflow<0&&!t.empty&&!t.invalidEra&&!t.invalidMonth&&!t.invalidWeekday&&!t.weekdayMismatch&&!t.nullInput&&!t.invalidFormat&&!t.userInvalidated&&(!t.meridiem||t.meridiem&&n),e._strict&&(s=s&&0===t.charsLeftOver&&0===t.unusedTokens.length&&void 0===t.bigHour)),null!=Object.isFrozen&&Object.isFrozen(e)?s:(e._isValid=s,e._isValid)}function I(e){var t=l(NaN);return null!=e?E(m(t),e):m(t).userInvalidated=!0,t}var j=Array.prototype.some||function(e){for(var t=Object(this),n=t.length>>>0,s=0;s<n;s++)if(s in t&&e.call(this,t[s],s,t))return!0;return!1},Z=f.momentProperties=[],z=!1;function q(e,t){var n,s,i,r=Z.length;if(o(t._isAMomentObject)||(e._isAMomentObject=t._isAMomentObject),o(t._i)||(e._i=t._i),o(t._f)||(e._f=t._f),o(t._l)||(e._l=t._l),o(t._strict)||(e._strict=t._strict),o(t._tzm)||(e._tzm=t._tzm),o(t._isUTC)||(e._isUTC=t._isUTC),o(t._offset)||(e._offset=t._offset),o(t._pf)||(e._pf=m(t)),o(t._locale)||(e._locale=t._locale),0<r)for(n=0;n<r;n++)o(i=t[s=Z[n]])||(e[s]=i);return e}function $(e){q(this,e),this._d=new Date(null!=e._d?e._d.getTime():NaN),this.isValid()||(this._d=new Date(NaN)),!1===z&&(z=!0,f.updateOffset(this),z=!1)}function d(e){return e instanceof $||null!=e&&null!=e._isAMomentObject}function B(e){!1===f.suppressDeprecationWarnings&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+e)}function e(r,a){var o=!0;return E(function(){if(null!=f.deprecationHandler&&f.deprecationHandler(null,r),o){for(var e,t,n=[],s=arguments.length,i=0;i<s;i++){if(e="","object"==typeof arguments[i]){for(t in e+="\n["+i+"] ",arguments[0])c(arguments[0],t)&&(e+=t+": "+arguments[0][t]+", ");e=e.slice(0,-2)}else e=arguments[i];n.push(e)}B(r+"\nArguments: "+Array.prototype.slice.call(n).join("")+"\n"+(new Error).stack),o=!1}return a.apply(this,arguments)},a)}var J={};function Q(e,t){null!=f.deprecationHandler&&f.deprecationHandler(e,t),J[e]||(B(t),J[e]=!0)}function h(e){return"undefined"!=typeof Function&&e instanceof Function||"[object Function]"===Object.prototype.toString.call(e)}function X(e,t){var n,s=E({},e);for(n in t)c(t,n)&&(F(e[n])&&F(t[n])?(s[n]={},E(s[n],e[n]),E(s[n],t[n])):null!=t[n]?s[n]=t[n]:delete s[n]);for(n in e)c(e,n)&&!c(t,n)&&F(e[n])&&(s[n]=E({},s[n]));return s}function K(e){null!=e&&this.set(e)}f.suppressDeprecationWarnings=!1,f.deprecationHandler=null;var ee=Object.keys||function(e){var t,n=[];for(t in e)c(e,t)&&n.push(t);return n};function r(e,t,n){var s=""+Math.abs(e);return(0<=e?n?"+":"":"-")+Math.pow(10,Math.max(0,t-s.length)).toString().substr(1)+s}var te=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|N{1,5}|YYYYYY|YYYYY|YYYY|YY|y{2,4}|yo?|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,ne=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,se={},ie={};function s(e,t,n,s){var i="string"==typeof s?function(){return this[s]()}:s;e&&(ie[e]=i),t&&(ie[t[0]]=function(){return r(i.apply(this,arguments),t[1],t[2])}),n&&(ie[n]=function(){return this.localeData().ordinal(i.apply(this,arguments),e)})}function re(e,t){return e.isValid()?(t=ae(t,e.localeData()),se[t]=se[t]||function(s){for(var e,i=s.match(te),t=0,r=i.length;t<r;t++)ie[i[t]]?i[t]=ie[i[t]]:i[t]=(e=i[t]).match(/\[[\s\S]/)?e.replace(/^\[|\]$/g,""):e.replace(/\\/g,"");return function(e){for(var t="",n=0;n<r;n++)t+=h(i[n])?i[n].call(e,s):i[n];return t}}(t),se[t](e)):e.localeData().invalidDate()}function ae(e,t){var n=5;function s(e){return t.longDateFormat(e)||e}for(ne.lastIndex=0;0<=n&&ne.test(e);)e=e.replace(ne,s),ne.lastIndex=0,--n;return e}var oe={D:"date",dates:"date",date:"date",d:"day",days:"day",day:"day",e:"weekday",weekdays:"weekday",weekday:"weekday",E:"isoWeekday",isoweekdays:"isoWeekday",isoweekday:"isoWeekday",DDD:"dayOfYear",dayofyears:"dayOfYear",dayofyear:"dayOfYear",h:"hour",hours:"hour",hour:"hour",ms:"millisecond",milliseconds:"millisecond",millisecond:"millisecond",m:"minute",minutes:"minute",minute:"minute",M:"month",months:"month",month:"month",Q:"quarter",quarters:"quarter",quarter:"quarter",s:"second",seconds:"second",second:"second",gg:"weekYear",weekyears:"weekYear",weekyear:"weekYear",GG:"isoWeekYear",isoweekyears:"isoWeekYear",isoweekyear:"isoWeekYear",w:"week",weeks:"week",week:"week",W:"isoWeek",isoweeks:"isoWeek",isoweek:"isoWeek",y:"year",years:"year",year:"year"};function _(e){return"string"==typeof e?oe[e]||oe[e.toLowerCase()]:void 0}function ue(e){var t,n,s={};for(n in e)c(e,n)&&(t=_(n))&&(s[t]=e[n]);return s}var le={date:9,day:11,weekday:11,isoWeekday:11,dayOfYear:4,hour:13,millisecond:16,minute:14,month:8,quarter:7,second:15,weekYear:1,isoWeekYear:1,week:5,isoWeek:5,year:1};var de=/\d/,t=/\d\d/,he=/\d{3}/,ce=/\d{4}/,fe=/[+-]?\d{6}/,n=/\d\d?/,me=/\d\d\d\d?/,_e=/\d\d\d\d\d\d?/,ye=/\d{1,3}/,ge=/\d{1,4}/,we=/[+-]?\d{1,6}/,pe=/\d+/,ke=/[+-]?\d+/,Me=/Z|[+-]\d\d:?\d\d/gi,ve=/Z|[+-]\d\d(?::?\d\d)?/gi,i=/[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i,y=/^[1-9]\d?/,g=/^([1-9]\d|\d)/;function w(e,n,s){Ye[e]=h(n)?n:function(e,t){return e&&s?s:n}}function De(e,t){return c(Ye,e)?Ye[e](t._strict,t._locale):new RegExp(p(e.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(e,t,n,s,i){return t||n||s||i})))}function p(e){return e.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function k(e){return e<0?Math.ceil(e)||0:Math.floor(e)}function M(e){var e=+e,t=0;return t=0!=e&&isFinite(e)?k(e):t}var Ye={},Se={};function v(e,n){var t,s,i=n;for("string"==typeof e&&(e=[e]),u(n)&&(i=function(e,t){t[n]=M(e)}),s=e.length,t=0;t<s;t++)Se[e[t]]=i}function Oe(e,i){v(e,function(e,t,n,s){n._w=n._w||{},i(e,n._w,n,s)})}function be(e){return e%4==0&&e%100!=0||e%400==0}var D=0,Y=1,S=2,O=3,b=4,T=5,Te=6,xe=7,Ne=8;function We(e){return be(e)?366:365}s("Y",0,0,function(){var e=this.year();return e<=9999?r(e,4):"+"+e}),s(0,["YY",2],0,function(){return this.year()%100}),s(0,["YYYY",4],0,"year"),s(0,["YYYYY",5],0,"year"),s(0,["YYYYYY",6,!0],0,"year"),w("Y",ke),w("YY",n,t),w("YYYY",ge,ce),w("YYYYY",we,fe),w("YYYYYY",we,fe),v(["YYYYY","YYYYYY"],D),v("YYYY",function(e,t){t[D]=2===e.length?f.parseTwoDigitYear(e):M(e)}),v("YY",function(e,t){t[D]=f.parseTwoDigitYear(e)}),v("Y",function(e,t){t[D]=parseInt(e,10)}),f.parseTwoDigitYear=function(e){return M(e)+(68<M(e)?1900:2e3)};var x,Pe=Re("FullYear",!0);function Re(t,n){return function(e){return null!=e?(Ue(this,t,e),f.updateOffset(this,n),this):Ce(this,t)}}function Ce(e,t){if(!e.isValid())return NaN;var n=e._d,s=e._isUTC;switch(t){case"Milliseconds":return s?n.getUTCMilliseconds():n.getMilliseconds();case"Seconds":return s?n.getUTCSeconds():n.getSeconds();case"Minutes":return s?n.getUTCMinutes():n.getMinutes();case"Hours":return s?n.getUTCHours():n.getHours();case"Date":return s?n.getUTCDate():n.getDate();case"Day":return s?n.getUTCDay():n.getDay();case"Month":return s?n.getUTCMonth():n.getMonth();case"FullYear":return s?n.getUTCFullYear():n.getFullYear();default:return NaN}}function Ue(e,t,n){var s,i,r;if(e.isValid()&&!isNaN(n)){switch(s=e._d,i=e._isUTC,t){case"Milliseconds":return i?s.setUTCMilliseconds(n):s.setMilliseconds(n);case"Seconds":return i?s.setUTCSeconds(n):s.setSeconds(n);case"Minutes":return i?s.setUTCMinutes(n):s.setMinutes(n);case"Hours":return i?s.setUTCHours(n):s.setHours(n);case"Date":return i?s.setUTCDate(n):s.setDate(n);case"FullYear":break;default:return}t=n,r=e.month(),e=29!==(e=e.date())||1!==r||be(t)?e:28,i?s.setUTCFullYear(t,r,e):s.setFullYear(t,r,e)}}function He(e,t){if(isNaN(e)||isNaN(t))return NaN;var n=(t%(n=12)+n)%n;return e+=(t-n)/12,1==n?be(e)?29:28:31-n%7%2}x=Array.prototype.indexOf||function(e){for(var t=0;t<this.length;++t)if(this[t]===e)return t;return-1},s("M",["MM",2],"Mo",function(){return this.month()+1}),s("MMM",0,0,function(e){return this.localeData().monthsShort(this,e)}),s("MMMM",0,0,function(e){return this.localeData().months(this,e)}),w("M",n,y),w("MM",n,t),w("MMM",function(e,t){return t.monthsShortRegex(e)}),w("MMMM",function(e,t){return t.monthsRegex(e)}),v(["M","MM"],function(e,t){t[Y]=M(e)-1}),v(["MMM","MMMM"],function(e,t,n,s){s=n._locale.monthsParse(e,s,n._strict);null!=s?t[Y]=s:m(n).invalidMonth=e});var Fe="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),Le="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),Ve=/D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/,Ge=i,Ee=i;function Ae(e,t){if(e.isValid()){if("string"==typeof t)if(/^\d+$/.test(t))t=M(t);else if(!u(t=e.localeData().monthsParse(t)))return;var n=(n=e.date())<29?n:Math.min(n,He(e.year(),t));e._isUTC?e._d.setUTCMonth(t,n):e._d.setMonth(t,n)}}function Ie(e){return null!=e?(Ae(this,e),f.updateOffset(this,!0),this):Ce(this,"Month")}function je(){function e(e,t){return t.length-e.length}for(var t,n,s=[],i=[],r=[],a=0;a<12;a++)n=l([2e3,a]),t=p(this.monthsShort(n,"")),n=p(this.months(n,"")),s.push(t),i.push(n),r.push(n),r.push(t);s.sort(e),i.sort(e),r.sort(e),this._monthsRegex=new RegExp("^("+r.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+i.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+s.join("|")+")","i")}function Ze(e,t,n,s,i,r,a){var o;return e<100&&0<=e?(o=new Date(e+400,t,n,s,i,r,a),isFinite(o.getFullYear())&&o.setFullYear(e)):o=new Date(e,t,n,s,i,r,a),o}function ze(e){var t;return e<100&&0<=e?((t=Array.prototype.slice.call(arguments))[0]=e+400,t=new Date(Date.UTC.apply(null,t)),isFinite(t.getUTCFullYear())&&t.setUTCFullYear(e)):t=new Date(Date.UTC.apply(null,arguments)),t}function qe(e,t,n){n=7+t-n;return n-(7+ze(e,0,n).getUTCDay()-t)%7-1}function $e(e,t,n,s,i){var r,t=1+7*(t-1)+(7+n-s)%7+qe(e,s,i),n=t<=0?We(r=e-1)+t:t>We(e)?(r=e+1,t-We(e)):(r=e,t);return{year:r,dayOfYear:n}}function Be(e,t,n){var s,i,r=qe(e.year(),t,n),r=Math.floor((e.dayOfYear()-r-1)/7)+1;return r<1?s=r+N(i=e.year()-1,t,n):r>N(e.year(),t,n)?(s=r-N(e.year(),t,n),i=e.year()+1):(i=e.year(),s=r),{week:s,year:i}}function N(e,t,n){var s=qe(e,t,n),t=qe(e+1,t,n);return(We(e)-s+t)/7}s("w",["ww",2],"wo","week"),s("W",["WW",2],"Wo","isoWeek"),w("w",n,y),w("ww",n,t),w("W",n,y),w("WW",n,t),Oe(["w","ww","W","WW"],function(e,t,n,s){t[s.substr(0,1)]=M(e)});function Je(e,t){return e.slice(t,7).concat(e.slice(0,t))}s("d",0,"do","day"),s("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),s("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),s("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),s("e",0,0,"weekday"),s("E",0,0,"isoWeekday"),w("d",n),w("e",n),w("E",n),w("dd",function(e,t){return t.weekdaysMinRegex(e)}),w("ddd",function(e,t){return t.weekdaysShortRegex(e)}),w("dddd",function(e,t){return t.weekdaysRegex(e)}),Oe(["dd","ddd","dddd"],function(e,t,n,s){s=n._locale.weekdaysParse(e,s,n._strict);null!=s?t.d=s:m(n).invalidWeekday=e}),Oe(["d","e","E"],function(e,t,n,s){t[s]=M(e)});var Qe="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),Xe="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Ke="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),et=i,tt=i,nt=i;function st(){function e(e,t){return t.length-e.length}for(var t,n,s,i=[],r=[],a=[],o=[],u=0;u<7;u++)s=l([2e3,1]).day(u),t=p(this.weekdaysMin(s,"")),n=p(this.weekdaysShort(s,"")),s=p(this.weekdays(s,"")),i.push(t),r.push(n),a.push(s),o.push(t),o.push(n),o.push(s);i.sort(e),r.sort(e),a.sort(e),o.sort(e),this._weekdaysRegex=new RegExp("^("+o.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+a.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+r.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+i.join("|")+")","i")}function it(){return this.hours()%12||12}function rt(e,t){s(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function at(e,t){return t._meridiemParse}s("H",["HH",2],0,"hour"),s("h",["hh",2],0,it),s("k",["kk",2],0,function(){return this.hours()||24}),s("hmm",0,0,function(){return""+it.apply(this)+r(this.minutes(),2)}),s("hmmss",0,0,function(){return""+it.apply(this)+r(this.minutes(),2)+r(this.seconds(),2)}),s("Hmm",0,0,function(){return""+this.hours()+r(this.minutes(),2)}),s("Hmmss",0,0,function(){return""+this.hours()+r(this.minutes(),2)+r(this.seconds(),2)}),rt("a",!0),rt("A",!1),w("a",at),w("A",at),w("H",n,g),w("h",n,y),w("k",n,y),w("HH",n,t),w("hh",n,t),w("kk",n,t),w("hmm",me),w("hmmss",_e),w("Hmm",me),w("Hmmss",_e),v(["H","HH"],O),v(["k","kk"],function(e,t,n){e=M(e);t[O]=24===e?0:e}),v(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),v(["h","hh"],function(e,t,n){t[O]=M(e),m(n).bigHour=!0}),v("hmm",function(e,t,n){var s=e.length-2;t[O]=M(e.substr(0,s)),t[b]=M(e.substr(s)),m(n).bigHour=!0}),v("hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[O]=M(e.substr(0,s)),t[b]=M(e.substr(s,2)),t[T]=M(e.substr(i)),m(n).bigHour=!0}),v("Hmm",function(e,t,n){var s=e.length-2;t[O]=M(e.substr(0,s)),t[b]=M(e.substr(s))}),v("Hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[O]=M(e.substr(0,s)),t[b]=M(e.substr(s,2)),t[T]=M(e.substr(i))});i=Re("Hours",!0);var ot,ut={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",w:"a week",ww:"%d weeks",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Fe,monthsShort:Le,week:{dow:0,doy:6},weekdays:Qe,weekdaysMin:Ke,weekdaysShort:Xe,meridiemParse:/[ap]\.?m?\.?/i},W={},lt={};function dt(e){return e&&e.toLowerCase().replace("_","-")}function ht(e){for(var t,n,s,i,r=0;r<e.length;){for(t=(i=dt(e[r]).split("-")).length,n=(n=dt(e[r+1]))?n.split("-"):null;0<t;){if(s=ct(i.slice(0,t).join("-")))return s;if(n&&n.length>=t&&function(e,t){for(var n=Math.min(e.length,t.length),s=0;s<n;s+=1)if(e[s]!==t[s])return s;return n}(i,n)>=t-1)break;t--}r++}return ot}function ct(t){var e,n;if(void 0===W[t]&&"undefined"!=typeof module&&module&&module.exports&&((n=t)&&n.match("^[^/\\\\]*$")))try{e=ot._abbr,require("./locale/"+t),ft(e)}catch(e){W[t]=null}return W[t]}function ft(e,t){return e&&((t=o(t)?P(e):mt(e,t))?ot=t:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),ot._abbr}function mt(e,t){if(null===t)return delete W[e],null;var n,s=ut;if(t.abbr=e,null!=W[e])Q("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),s=W[e]._config;else if(null!=t.parentLocale)if(null!=W[t.parentLocale])s=W[t.parentLocale]._config;else{if(null==(n=ct(t.parentLocale)))return lt[t.parentLocale]||(lt[t.parentLocale]=[]),lt[t.parentLocale].push({name:e,config:t}),null;s=n._config}return W[e]=new K(X(s,t)),lt[e]&&lt[e].forEach(function(e){mt(e.name,e.config)}),ft(e),W[e]}function P(e){var t;if(!(e=e&&e._locale&&e._locale._abbr?e._locale._abbr:e))return ot;if(!a(e)){if(t=ct(e))return t;e=[e]}return ht(e)}function _t(e){var t=e._a;return t&&-2===m(e).overflow&&(t=t[Y]<0||11<t[Y]?Y:t[S]<1||t[S]>He(t[D],t[Y])?S:t[O]<0||24<t[O]||24===t[O]&&(0!==t[b]||0!==t[T]||0!==t[Te])?O:t[b]<0||59<t[b]?b:t[T]<0||59<t[T]?T:t[Te]<0||999<t[Te]?Te:-1,m(e)._overflowDayOfYear&&(t<D||S<t)&&(t=S),m(e)._overflowWeeks&&-1===t&&(t=xe),m(e)._overflowWeekday&&-1===t&&(t=Ne),m(e).overflow=t),e}var yt=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/,gt=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d|))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/,wt=/Z|[+-]\d\d(?::?\d\d)?/,pt=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/],["YYYYMM",/\d{6}/,!1],["YYYY",/\d{4}/,!1]],kt=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],Mt=/^\/?Date\((-?\d+)/i,vt=/^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/,Dt={UT:0,GMT:0,EDT:-240,EST:-300,CDT:-300,CST:-360,MDT:-360,MST:-420,PDT:-420,PST:-480};function Yt(e){var t,n,s,i,r,a,o=e._i,u=yt.exec(o)||gt.exec(o),o=pt.length,l=kt.length;if(u){for(m(e).iso=!0,t=0,n=o;t<n;t++)if(pt[t][1].exec(u[1])){i=pt[t][0],s=!1!==pt[t][2];break}if(null==i)e._isValid=!1;else{if(u[3]){for(t=0,n=l;t<n;t++)if(kt[t][1].exec(u[3])){r=(u[2]||" ")+kt[t][0];break}if(null==r)return void(e._isValid=!1)}if(s||null==r){if(u[4]){if(!wt.exec(u[4]))return void(e._isValid=!1);a="Z"}e._f=i+(r||"")+(a||""),xt(e)}else e._isValid=!1}}else e._isValid=!1}function St(e,t,n,s,i,r){e=[function(e){e=parseInt(e,10);{if(e<=49)return 2e3+e;if(e<=999)return 1900+e}return e}(e),Le.indexOf(t),parseInt(n,10),parseInt(s,10),parseInt(i,10)];return r&&e.push(parseInt(r,10)),e}function Ot(e){var t,n,s,i,r=vt.exec(e._i.replace(/\([^()]*\)|[\n\t]/g," ").replace(/(\s\s+)/g," ").replace(/^\s\s*/,"").replace(/\s\s*$/,""));r?(t=St(r[4],r[3],r[2],r[5],r[6],r[7]),n=r[1],s=t,i=e,n&&Xe.indexOf(n)!==new Date(s[0],s[1],s[2]).getDay()?(m(i).weekdayMismatch=!0,i._isValid=!1):(e._a=t,e._tzm=(n=r[8],s=r[9],i=r[10],n?Dt[n]:s?0:60*(((n=parseInt(i,10))-(s=n%100))/100)+s),e._d=ze.apply(null,e._a),e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),m(e).rfc2822=!0)):e._isValid=!1}function bt(e,t,n){return null!=e?e:null!=t?t:n}function Tt(e){var t,n,s,i,r,a,o,u,l,d,h,c=[];if(!e._d){for(s=e,i=new Date(f.now()),n=s._useUTC?[i.getUTCFullYear(),i.getUTCMonth(),i.getUTCDate()]:[i.getFullYear(),i.getMonth(),i.getDate()],e._w&&null==e._a[S]&&null==e._a[Y]&&(null!=(i=(s=e)._w).GG||null!=i.W||null!=i.E?(u=1,l=4,r=bt(i.GG,s._a[D],Be(R(),1,4).year),a=bt(i.W,1),((o=bt(i.E,1))<1||7<o)&&(d=!0)):(u=s._locale._week.dow,l=s._locale._week.doy,h=Be(R(),u,l),r=bt(i.gg,s._a[D],h.year),a=bt(i.w,h.week),null!=i.d?((o=i.d)<0||6<o)&&(d=!0):null!=i.e?(o=i.e+u,(i.e<0||6<i.e)&&(d=!0)):o=u),a<1||a>N(r,u,l)?m(s)._overflowWeeks=!0:null!=d?m(s)._overflowWeekday=!0:(h=$e(r,a,o,u,l),s._a[D]=h.year,s._dayOfYear=h.dayOfYear)),null!=e._dayOfYear&&(i=bt(e._a[D],n[D]),(e._dayOfYear>We(i)||0===e._dayOfYear)&&(m(e)._overflowDayOfYear=!0),d=ze(i,0,e._dayOfYear),e._a[Y]=d.getUTCMonth(),e._a[S]=d.getUTCDate()),t=0;t<3&&null==e._a[t];++t)e._a[t]=c[t]=n[t];for(;t<7;t++)e._a[t]=c[t]=null==e._a[t]?2===t?1:0:e._a[t];24===e._a[O]&&0===e._a[b]&&0===e._a[T]&&0===e._a[Te]&&(e._nextDay=!0,e._a[O]=0),e._d=(e._useUTC?ze:Ze).apply(null,c),r=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[O]=24),e._w&&void 0!==e._w.d&&e._w.d!==r&&(m(e).weekdayMismatch=!0)}}function xt(e){if(e._f===f.ISO_8601)Yt(e);else if(e._f===f.RFC_2822)Ot(e);else{e._a=[],m(e).empty=!0;for(var t,n,s,i,r,a=""+e._i,o=a.length,u=0,l=ae(e._f,e._locale).match(te)||[],d=l.length,h=0;h<d;h++)n=l[h],(t=(a.match(De(n,e))||[])[0])&&(0<(s=a.substr(0,a.indexOf(t))).length&&m(e).unusedInput.push(s),a=a.slice(a.indexOf(t)+t.length),u+=t.length),ie[n]?(t?m(e).empty=!1:m(e).unusedTokens.push(n),s=n,r=e,null!=(i=t)&&c(Se,s)&&Se[s](i,r._a,r,s)):e._strict&&!t&&m(e).unusedTokens.push(n);m(e).charsLeftOver=o-u,0<a.length&&m(e).unusedInput.push(a),e._a[O]<=12&&!0===m(e).bigHour&&0<e._a[O]&&(m(e).bigHour=void 0),m(e).parsedDateParts=e._a.slice(0),m(e).meridiem=e._meridiem,e._a[O]=function(e,t,n){if(null==n)return t;return null!=e.meridiemHour?e.meridiemHour(t,n):null!=e.isPM?((e=e.isPM(n))&&t<12&&(t+=12),t=e||12!==t?t:0):t}(e._locale,e._a[O],e._meridiem),null!==(o=m(e).era)&&(e._a[D]=e._locale.erasConvertYear(o,e._a[D])),Tt(e),_t(e)}}function Nt(e){var t,n,s,i=e._i,r=e._f;if(e._locale=e._locale||P(e._l),null===i||void 0===r&&""===i)return I({nullInput:!0});if("string"==typeof i&&(e._i=i=e._locale.preparse(i)),d(i))return new $(_t(i));if(V(i))e._d=i;else if(a(r))!function(e){var t,n,s,i,r,a,o=!1,u=e._f.length;if(0===u)return m(e).invalidFormat=!0,e._d=new Date(NaN);for(i=0;i<u;i++)r=0,a=!1,t=q({},e),null!=e._useUTC&&(t._useUTC=e._useUTC),t._f=e._f[i],xt(t),A(t)&&(a=!0),r=(r+=m(t).charsLeftOver)+10*m(t).unusedTokens.length,m(t).score=r,o?r<s&&(s=r,n=t):(null==s||r<s||a)&&(s=r,n=t,a&&(o=!0));E(e,n||t)}(e);else if(r)xt(e);else if(o(r=(i=e)._i))i._d=new Date(f.now());else V(r)?i._d=new Date(r.valueOf()):"string"==typeof r?(n=i,null!==(t=Mt.exec(n._i))?n._d=new Date(+t[1]):(Yt(n),!1===n._isValid&&(delete n._isValid,Ot(n),!1===n._isValid&&(delete n._isValid,n._strict?n._isValid=!1:f.createFromInputFallback(n))))):a(r)?(i._a=G(r.slice(0),function(e){return parseInt(e,10)}),Tt(i)):F(r)?(t=i)._d||(s=void 0===(n=ue(t._i)).day?n.date:n.day,t._a=G([n.year,n.month,s,n.hour,n.minute,n.second,n.millisecond],function(e){return e&&parseInt(e,10)}),Tt(t)):u(r)?i._d=new Date(r):f.createFromInputFallback(i);return A(e)||(e._d=null),e}function Wt(e,t,n,s,i){var r={};return!0!==t&&!1!==t||(s=t,t=void 0),!0!==n&&!1!==n||(s=n,n=void 0),(F(e)&&L(e)||a(e)&&0===e.length)&&(e=void 0),r._isAMomentObject=!0,r._useUTC=r._isUTC=i,r._l=n,r._i=e,r._f=t,r._strict=s,(i=new $(_t(Nt(i=r))))._nextDay&&(i.add(1,"d"),i._nextDay=void 0),i}function R(e,t,n,s){return Wt(e,t,n,s,!1)}f.createFromInputFallback=e("value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are discouraged. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",function(e){e._d=new Date(e._i+(e._useUTC?" UTC":""))}),f.ISO_8601=function(){},f.RFC_2822=function(){};me=e("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var e=R.apply(null,arguments);return this.isValid()&&e.isValid()?e<this?this:e:I()}),_e=e("moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var e=R.apply(null,arguments);return this.isValid()&&e.isValid()?this<e?this:e:I()});function Pt(e,t){var n,s;if(!(t=1===t.length&&a(t[0])?t[0]:t).length)return R();for(n=t[0],s=1;s<t.length;++s)t[s].isValid()&&!t[s][e](n)||(n=t[s]);return n}var Rt=["year","quarter","month","week","day","hour","minute","second","millisecond"];function Ct(e){var e=ue(e),t=e.year||0,n=e.quarter||0,s=e.month||0,i=e.week||e.isoWeek||0,r=e.day||0,a=e.hour||0,o=e.minute||0,u=e.second||0,l=e.millisecond||0;this._isValid=function(e){var t,n,s=!1,i=Rt.length;for(t in e)if(c(e,t)&&(-1===x.call(Rt,t)||null!=e[t]&&isNaN(e[t])))return!1;for(n=0;n<i;++n)if(e[Rt[n]]){if(s)return!1;parseFloat(e[Rt[n]])!==M(e[Rt[n]])&&(s=!0)}return!0}(e),this._milliseconds=+l+1e3*u+6e4*o+1e3*a*60*60,this._days=+r+7*i,this._months=+s+3*n+12*t,this._data={},this._locale=P(),this._bubble()}function Ut(e){return e instanceof Ct}function Ht(e){return e<0?-1*Math.round(-1*e):Math.round(e)}function Ft(e,n){s(e,0,0,function(){var e=this.utcOffset(),t="+";return e<0&&(e=-e,t="-"),t+r(~~(e/60),2)+n+r(~~e%60,2)})}Ft("Z",":"),Ft("ZZ",""),w("Z",ve),w("ZZ",ve),v(["Z","ZZ"],function(e,t,n){n._useUTC=!0,n._tzm=Vt(ve,e)});var Lt=/([\+\-]|\d\d)/gi;function Vt(e,t){var t=(t||"").match(e);return null===t?null:0===(t=60*(e=((t[t.length-1]||[])+"").match(Lt)||["-",0,0])[1]+M(e[2]))?0:"+"===e[0]?t:-t}function Gt(e,t){var n;return t._isUTC?(t=t.clone(),n=(d(e)||V(e)?e:R(e)).valueOf()-t.valueOf(),t._d.setTime(t._d.valueOf()+n),f.updateOffset(t,!1),t):R(e).local()}function Et(e){return-Math.round(e._d.getTimezoneOffset())}function At(){return!!this.isValid()&&(this._isUTC&&0===this._offset)}f.updateOffset=function(){};var It=/^(-|\+)?(?:(\d*)[. ])?(\d+):(\d+)(?::(\d+)(\.\d*)?)?$/,jt=/^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/;function C(e,t){var n,s=e,i=null;return Ut(e)?s={ms:e._milliseconds,d:e._days,M:e._months}:u(e)||!isNaN(+e)?(s={},t?s[t]=+e:s.milliseconds=+e):(i=It.exec(e))?(n="-"===i[1]?-1:1,s={y:0,d:M(i[S])*n,h:M(i[O])*n,m:M(i[b])*n,s:M(i[T])*n,ms:M(Ht(1e3*i[Te]))*n}):(i=jt.exec(e))?(n="-"===i[1]?-1:1,s={y:Zt(i[2],n),M:Zt(i[3],n),w:Zt(i[4],n),d:Zt(i[5],n),h:Zt(i[6],n),m:Zt(i[7],n),s:Zt(i[8],n)}):null==s?s={}:"object"==typeof s&&("from"in s||"to"in s)&&(t=function(e,t){var n;if(!e.isValid()||!t.isValid())return{milliseconds:0,months:0};t=Gt(t,e),e.isBefore(t)?n=zt(e,t):((n=zt(t,e)).milliseconds=-n.milliseconds,n.months=-n.months);return n}(R(s.from),R(s.to)),(s={}).ms=t.milliseconds,s.M=t.months),i=new Ct(s),Ut(e)&&c(e,"_locale")&&(i._locale=e._locale),Ut(e)&&c(e,"_isValid")&&(i._isValid=e._isValid),i}function Zt(e,t){e=e&&parseFloat(e.replace(",","."));return(isNaN(e)?0:e)*t}function zt(e,t){var n={};return n.months=t.month()-e.month()+12*(t.year()-e.year()),e.clone().add(n.months,"M").isAfter(t)&&--n.months,n.milliseconds=+t-+e.clone().add(n.months,"M"),n}function qt(s,i){return function(e,t){var n;return null===t||isNaN(+t)||(Q(i,"moment()."+i+"(period, number) is deprecated. Please use moment()."+i+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),n=e,e=t,t=n),$t(this,C(e,t),s),this}}function $t(e,t,n,s){var i=t._milliseconds,r=Ht(t._days),t=Ht(t._months);e.isValid()&&(s=null==s||s,t&&Ae(e,Ce(e,"Month")+t*n),r&&Ue(e,"Date",Ce(e,"Date")+r*n),i&&e._d.setTime(e._d.valueOf()+i*n),s&&f.updateOffset(e,r||t))}C.fn=Ct.prototype,C.invalid=function(){return C(NaN)};Fe=qt(1,"add"),Qe=qt(-1,"subtract");function Bt(e){return"string"==typeof e||e instanceof String}function Jt(e){return d(e)||V(e)||Bt(e)||u(e)||function(t){var e=a(t),n=!1;e&&(n=0===t.filter(function(e){return!u(e)&&Bt(t)}).length);return e&&n}(e)||function(e){var t,n,s=F(e)&&!L(e),i=!1,r=["years","year","y","months","month","M","days","day","d","dates","date","D","hours","hour","h","minutes","minute","m","seconds","second","s","milliseconds","millisecond","ms"],a=r.length;for(t=0;t<a;t+=1)n=r[t],i=i||c(e,n);return s&&i}(e)||null==e}function Qt(e,t){if(e.date()<t.date())return-Qt(t,e);var n=12*(t.year()-e.year())+(t.month()-e.month()),s=e.clone().add(n,"months"),t=t-s<0?(t-s)/(s-e.clone().add(n-1,"months")):(t-s)/(e.clone().add(1+n,"months")-s);return-(n+t)||0}function Xt(e){return void 0===e?this._locale._abbr:(null!=(e=P(e))&&(this._locale=e),this)}f.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",f.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";Ke=e("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(e){return void 0===e?this.localeData():this.locale(e)});function Kt(){return this._locale}var en=126227808e5;function tn(e,t){return(e%t+t)%t}function nn(e,t,n){return e<100&&0<=e?new Date(e+400,t,n)-en:new Date(e,t,n).valueOf()}function sn(e,t,n){return e<100&&0<=e?Date.UTC(e+400,t,n)-en:Date.UTC(e,t,n)}function rn(e,t){return t.erasAbbrRegex(e)}function an(){for(var e,t,n,s=[],i=[],r=[],a=[],o=this.eras(),u=0,l=o.length;u<l;++u)e=p(o[u].name),t=p(o[u].abbr),n=p(o[u].narrow),i.push(e),s.push(t),r.push(n),a.push(e),a.push(t),a.push(n);this._erasRegex=new RegExp("^("+a.join("|")+")","i"),this._erasNameRegex=new RegExp("^("+i.join("|")+")","i"),this._erasAbbrRegex=new RegExp("^("+s.join("|")+")","i"),this._erasNarrowRegex=new RegExp("^("+r.join("|")+")","i")}function on(e,t){s(0,[e,e.length],0,t)}function un(e,t,n,s,i){var r;return null==e?Be(this,s,i).year:(r=N(e,s,i),function(e,t,n,s,i){e=$e(e,t,n,s,i),t=ze(e.year,0,e.dayOfYear);return this.year(t.getUTCFullYear()),this.month(t.getUTCMonth()),this.date(t.getUTCDate()),this}.call(this,e,t=r<t?r:t,n,s,i))}s("N",0,0,"eraAbbr"),s("NN",0,0,"eraAbbr"),s("NNN",0,0,"eraAbbr"),s("NNNN",0,0,"eraName"),s("NNNNN",0,0,"eraNarrow"),s("y",["y",1],"yo","eraYear"),s("y",["yy",2],0,"eraYear"),s("y",["yyy",3],0,"eraYear"),s("y",["yyyy",4],0,"eraYear"),w("N",rn),w("NN",rn),w("NNN",rn),w("NNNN",function(e,t){return t.erasNameRegex(e)}),w("NNNNN",function(e,t){return t.erasNarrowRegex(e)}),v(["N","NN","NNN","NNNN","NNNNN"],function(e,t,n,s){s=n._locale.erasParse(e,s,n._strict);s?m(n).era=s:m(n).invalidEra=e}),w("y",pe),w("yy",pe),w("yyy",pe),w("yyyy",pe),w("yo",function(e,t){return t._eraYearOrdinalRegex||pe}),v(["y","yy","yyy","yyyy"],D),v(["yo"],function(e,t,n,s){var i;n._locale._eraYearOrdinalRegex&&(i=e.match(n._locale._eraYearOrdinalRegex)),n._locale.eraYearOrdinalParse?t[D]=n._locale.eraYearOrdinalParse(e,i):t[D]=parseInt(e,10)}),s(0,["gg",2],0,function(){return this.weekYear()%100}),s(0,["GG",2],0,function(){return this.isoWeekYear()%100}),on("gggg","weekYear"),on("ggggg","weekYear"),on("GGGG","isoWeekYear"),on("GGGGG","isoWeekYear"),w("G",ke),w("g",ke),w("GG",n,t),w("gg",n,t),w("GGGG",ge,ce),w("gggg",ge,ce),w("GGGGG",we,fe),w("ggggg",we,fe),Oe(["gggg","ggggg","GGGG","GGGGG"],function(e,t,n,s){t[s.substr(0,2)]=M(e)}),Oe(["gg","GG"],function(e,t,n,s){t[s]=f.parseTwoDigitYear(e)}),s("Q",0,"Qo","quarter"),w("Q",de),v("Q",function(e,t){t[Y]=3*(M(e)-1)}),s("D",["DD",2],"Do","date"),w("D",n,y),w("DD",n,t),w("Do",function(e,t){return e?t._dayOfMonthOrdinalParse||t._ordinalParse:t._dayOfMonthOrdinalParseLenient}),v(["D","DD"],S),v("Do",function(e,t){t[S]=M(e.match(n)[0])});ge=Re("Date",!0);s("DDD",["DDDD",3],"DDDo","dayOfYear"),w("DDD",ye),w("DDDD",he),v(["DDD","DDDD"],function(e,t,n){n._dayOfYear=M(e)}),s("m",["mm",2],0,"minute"),w("m",n,g),w("mm",n,t),v(["m","mm"],b);var ln,ce=Re("Minutes",!1),we=(s("s",["ss",2],0,"second"),w("s",n,g),w("ss",n,t),v(["s","ss"],T),Re("Seconds",!1));for(s("S",0,0,function(){return~~(this.millisecond()/100)}),s(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),s(0,["SSS",3],0,"millisecond"),s(0,["SSSS",4],0,function(){return 10*this.millisecond()}),s(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),s(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),s(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),s(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),s(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),w("S",ye,de),w("SS",ye,t),w("SSS",ye,he),ln="SSSS";ln.length<=9;ln+="S")w(ln,pe);function dn(e,t){t[Te]=M(1e3*("0."+e))}for(ln="S";ln.length<=9;ln+="S")v(ln,dn);fe=Re("Milliseconds",!1),s("z",0,0,"zoneAbbr"),s("zz",0,0,"zoneName");y=$.prototype;function hn(e){return e}y.add=Fe,y.calendar=function(e,t){1===arguments.length&&(arguments[0]?Jt(arguments[0])?(e=arguments[0],t=void 0):function(e){for(var t=F(e)&&!L(e),n=!1,s=["sameDay","nextDay","lastDay","nextWeek","lastWeek","sameElse"],i=0;i<s.length;i+=1)n=n||c(e,s[i]);return t&&n}(arguments[0])&&(t=arguments[0],e=void 0):t=e=void 0);var e=e||R(),n=Gt(e,this).startOf("day"),n=f.calendarFormat(this,n)||"sameElse",t=t&&(h(t[n])?t[n].call(this,e):t[n]);return this.format(t||this.localeData().calendar(n,this,R(e)))},y.clone=function(){return new $(this)},y.diff=function(e,t,n){var s,i,r;if(!this.isValid())return NaN;if(!(s=Gt(e,this)).isValid())return NaN;switch(i=6e4*(s.utcOffset()-this.utcOffset()),t=_(t)){case"year":r=Qt(this,s)/12;break;case"month":r=Qt(this,s);break;case"quarter":r=Qt(this,s)/3;break;case"second":r=(this-s)/1e3;break;case"minute":r=(this-s)/6e4;break;case"hour":r=(this-s)/36e5;break;case"day":r=(this-s-i)/864e5;break;case"week":r=(this-s-i)/6048e5;break;default:r=this-s}return n?r:k(r)},y.endOf=function(e){var t,n;if(void 0===(e=_(e))||"millisecond"===e||!this.isValid())return this;switch(n=this._isUTC?sn:nn,e){case"year":t=n(this.year()+1,0,1)-1;break;case"quarter":t=n(this.year(),this.month()-this.month()%3+3,1)-1;break;case"month":t=n(this.year(),this.month()+1,1)-1;break;case"week":t=n(this.year(),this.month(),this.date()-this.weekday()+7)-1;break;case"isoWeek":t=n(this.year(),this.month(),this.date()-(this.isoWeekday()-1)+7)-1;break;case"day":case"date":t=n(this.year(),this.month(),this.date()+1)-1;break;case"hour":t=this._d.valueOf(),t+=36e5-tn(t+(this._isUTC?0:6e4*this.utcOffset()),36e5)-1;break;case"minute":t=this._d.valueOf(),t+=6e4-tn(t,6e4)-1;break;case"second":t=this._d.valueOf(),t+=1e3-tn(t,1e3)-1}return this._d.setTime(t),f.updateOffset(this,!0),this},y.format=function(e){return e=e||(this.isUtc()?f.defaultFormatUtc:f.defaultFormat),e=re(this,e),this.localeData().postformat(e)},y.from=function(e,t){return this.isValid()&&(d(e)&&e.isValid()||R(e).isValid())?C({to:this,from:e}).locale(this.locale()).humanize(!t):this.localeData().invalidDate()},y.fromNow=function(e){return this.from(R(),e)},y.to=function(e,t){return this.isValid()&&(d(e)&&e.isValid()||R(e).isValid())?C({from:this,to:e}).locale(this.locale()).humanize(!t):this.localeData().invalidDate()},y.toNow=function(e){return this.to(R(),e)},y.get=function(e){return h(this[e=_(e)])?this[e]():this},y.invalidAt=function(){return m(this).overflow},y.isAfter=function(e,t){return e=d(e)?e:R(e),!(!this.isValid()||!e.isValid())&&("millisecond"===(t=_(t)||"millisecond")?this.valueOf()>e.valueOf():e.valueOf()<this.clone().startOf(t).valueOf())},y.isBefore=function(e,t){return e=d(e)?e:R(e),!(!this.isValid()||!e.isValid())&&("millisecond"===(t=_(t)||"millisecond")?this.valueOf()<e.valueOf():this.clone().endOf(t).valueOf()<e.valueOf())},y.isBetween=function(e,t,n,s){return e=d(e)?e:R(e),t=d(t)?t:R(t),!!(this.isValid()&&e.isValid()&&t.isValid())&&(("("===(s=s||"()")[0]?this.isAfter(e,n):!this.isBefore(e,n))&&(")"===s[1]?this.isBefore(t,n):!this.isAfter(t,n)))},y.isSame=function(e,t){var e=d(e)?e:R(e);return!(!this.isValid()||!e.isValid())&&("millisecond"===(t=_(t)||"millisecond")?this.valueOf()===e.valueOf():(e=e.valueOf(),this.clone().startOf(t).valueOf()<=e&&e<=this.clone().endOf(t).valueOf()))},y.isSameOrAfter=function(e,t){return this.isSame(e,t)||this.isAfter(e,t)},y.isSameOrBefore=function(e,t){return this.isSame(e,t)||this.isBefore(e,t)},y.isValid=function(){return A(this)},y.lang=Ke,y.locale=Xt,y.localeData=Kt,y.max=_e,y.min=me,y.parsingFlags=function(){return E({},m(this))},y.set=function(e,t){if("object"==typeof e)for(var n=function(e){var t,n=[];for(t in e)c(e,t)&&n.push({unit:t,priority:le[t]});return n.sort(function(e,t){return e.priority-t.priority}),n}(e=ue(e)),s=n.length,i=0;i<s;i++)this[n[i].unit](e[n[i].unit]);else if(h(this[e=_(e)]))return this[e](t);return this},y.startOf=function(e){var t,n;if(void 0===(e=_(e))||"millisecond"===e||!this.isValid())return this;switch(n=this._isUTC?sn:nn,e){case"year":t=n(this.year(),0,1);break;case"quarter":t=n(this.year(),this.month()-this.month()%3,1);break;case"month":t=n(this.year(),this.month(),1);break;case"week":t=n(this.year(),this.month(),this.date()-this.weekday());break;case"isoWeek":t=n(this.year(),this.month(),this.date()-(this.isoWeekday()-1));break;case"day":case"date":t=n(this.year(),this.month(),this.date());break;case"hour":t=this._d.valueOf(),t-=tn(t+(this._isUTC?0:6e4*this.utcOffset()),36e5);break;case"minute":t=this._d.valueOf(),t-=tn(t,6e4);break;case"second":t=this._d.valueOf(),t-=tn(t,1e3)}return this._d.setTime(t),f.updateOffset(this,!0),this},y.subtract=Qe,y.toArray=function(){var e=this;return[e.year(),e.month(),e.date(),e.hour(),e.minute(),e.second(),e.millisecond()]},y.toObject=function(){var e=this;return{years:e.year(),months:e.month(),date:e.date(),hours:e.hours(),minutes:e.minutes(),seconds:e.seconds(),milliseconds:e.milliseconds()}},y.toDate=function(){return new Date(this.valueOf())},y.toISOString=function(e){if(!this.isValid())return null;var t=(e=!0!==e)?this.clone().utc():this;return t.year()<0||9999<t.year()?re(t,e?"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYYYY-MM-DD[T]HH:mm:ss.SSSZ"):h(Date.prototype.toISOString)?e?this.toDate().toISOString():new Date(this.valueOf()+60*this.utcOffset()*1e3).toISOString().replace("Z",re(t,"Z")):re(t,e?"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYY-MM-DD[T]HH:mm:ss.SSSZ")},y.inspect=function(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var e,t="moment",n="";return this.isLocal()||(t=0===this.utcOffset()?"moment.utc":"moment.parseZone",n="Z"),t="["+t+'("]',e=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",this.format(t+e+"-MM-DD[T]HH:mm:ss.SSS"+(n+'[")]'))},"undefined"!=typeof Symbol&&null!=Symbol.for&&(y[Symbol.for("nodejs.util.inspect.custom")]=function(){return"Moment<"+this.format()+">"}),y.toJSON=function(){return this.isValid()?this.toISOString():null},y.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},y.unix=function(){return Math.floor(this.valueOf()/1e3)},y.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},y.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},y.eraName=function(){for(var e,t=this.localeData().eras(),n=0,s=t.length;n<s;++n){if(e=this.clone().startOf("day").valueOf(),t[n].since<=e&&e<=t[n].until)return t[n].name;if(t[n].until<=e&&e<=t[n].since)return t[n].name}return""},y.eraNarrow=function(){for(var e,t=this.localeData().eras(),n=0,s=t.length;n<s;++n){if(e=this.clone().startOf("day").valueOf(),t[n].since<=e&&e<=t[n].until)return t[n].narrow;if(t[n].until<=e&&e<=t[n].since)return t[n].narrow}return""},y.eraAbbr=function(){for(var e,t=this.localeData().eras(),n=0,s=t.length;n<s;++n){if(e=this.clone().startOf("day").valueOf(),t[n].since<=e&&e<=t[n].until)return t[n].abbr;if(t[n].until<=e&&e<=t[n].since)return t[n].abbr}return""},y.eraYear=function(){for(var e,t,n=this.localeData().eras(),s=0,i=n.length;s<i;++s)if(e=n[s].since<=n[s].until?1:-1,t=this.clone().startOf("day").valueOf(),n[s].since<=t&&t<=n[s].until||n[s].until<=t&&t<=n[s].since)return(this.year()-f(n[s].since).year())*e+n[s].offset;return this.year()},y.year=Pe,y.isLeapYear=function(){return be(this.year())},y.weekYear=function(e){return un.call(this,e,this.week(),this.weekday()+this.localeData()._week.dow,this.localeData()._week.dow,this.localeData()._week.doy)},y.isoWeekYear=function(e){return un.call(this,e,this.isoWeek(),this.isoWeekday(),1,4)},y.quarter=y.quarters=function(e){return null==e?Math.ceil((this.month()+1)/3):this.month(3*(e-1)+this.month()%3)},y.month=Ie,y.daysInMonth=function(){return He(this.year(),this.month())},y.week=y.weeks=function(e){var t=this.localeData().week(this);return null==e?t:this.add(7*(e-t),"d")},y.isoWeek=y.isoWeeks=function(e){var t=Be(this,1,4).week;return null==e?t:this.add(7*(e-t),"d")},y.weeksInYear=function(){var e=this.localeData()._week;return N(this.year(),e.dow,e.doy)},y.weeksInWeekYear=function(){var e=this.localeData()._week;return N(this.weekYear(),e.dow,e.doy)},y.isoWeeksInYear=function(){return N(this.year(),1,4)},y.isoWeeksInISOWeekYear=function(){return N(this.isoWeekYear(),1,4)},y.date=ge,y.day=y.days=function(e){if(!this.isValid())return null!=e?this:NaN;var t,n,s=Ce(this,"Day");return null!=e?(t=e,n=this.localeData(),e="string"!=typeof t?t:isNaN(t)?"number"==typeof(t=n.weekdaysParse(t))?t:null:parseInt(t,10),this.add(e-s,"d")):s},y.weekday=function(e){if(!this.isValid())return null!=e?this:NaN;var t=(this.day()+7-this.localeData()._week.dow)%7;return null==e?t:this.add(e-t,"d")},y.isoWeekday=function(e){return this.isValid()?null!=e?(t=e,n=this.localeData(),n="string"==typeof t?n.weekdaysParse(t)%7||7:isNaN(t)?null:t,this.day(this.day()%7?n:n-7)):this.day()||7:null!=e?this:NaN;var t,n},y.dayOfYear=function(e){var t=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==e?t:this.add(e-t,"d")},y.hour=y.hours=i,y.minute=y.minutes=ce,y.second=y.seconds=we,y.millisecond=y.milliseconds=fe,y.utcOffset=function(e,t,n){var s,i=this._offset||0;if(!this.isValid())return null!=e?this:NaN;if(null==e)return this._isUTC?i:Et(this);if("string"==typeof e){if(null===(e=Vt(ve,e)))return this}else Math.abs(e)<16&&!n&&(e*=60);return!this._isUTC&&t&&(s=Et(this)),this._offset=e,this._isUTC=!0,null!=s&&this.add(s,"m"),i!==e&&(!t||this._changeInProgress?$t(this,C(e-i,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,f.updateOffset(this,!0),this._changeInProgress=null)),this},y.utc=function(e){return this.utcOffset(0,e)},y.local=function(e){return this._isUTC&&(this.utcOffset(0,e),this._isUTC=!1,e&&this.subtract(Et(this),"m")),this},y.parseZone=function(){var e;return null!=this._tzm?this.utcOffset(this._tzm,!1,!0):"string"==typeof this._i&&(null!=(e=Vt(Me,this._i))?this.utcOffset(e):this.utcOffset(0,!0)),this},y.hasAlignedHourOffset=function(e){return!!this.isValid()&&(e=e?R(e).utcOffset():0,(this.utcOffset()-e)%60==0)},y.isDST=function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},y.isLocal=function(){return!!this.isValid()&&!this._isUTC},y.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},y.isUtc=At,y.isUTC=At,y.zoneAbbr=function(){return this._isUTC?"UTC":""},y.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},y.dates=e("dates accessor is deprecated. Use date instead.",ge),y.months=e("months accessor is deprecated. Use month instead",Ie),y.years=e("years accessor is deprecated. Use year instead",Pe),y.zone=e("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,t){return null!=e?(this.utcOffset(e="string"!=typeof e?-e:e,t),this):-this.utcOffset()}),y.isDSTShifted=e("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!o(this._isDSTShifted))return this._isDSTShifted;var e,t={};return q(t,this),(t=Nt(t))._a?(e=(t._isUTC?l:R)(t._a),this._isDSTShifted=this.isValid()&&0<function(e,t,n){for(var s=Math.min(e.length,t.length),i=Math.abs(e.length-t.length),r=0,a=0;a<s;a++)(n&&e[a]!==t[a]||!n&&M(e[a])!==M(t[a]))&&r++;return r+i}(t._a,e.toArray())):this._isDSTShifted=!1,this._isDSTShifted});g=K.prototype;function cn(e,t,n,s){var i=P(),s=l().set(s,t);return i[n](s,e)}function fn(e,t,n){if(u(e)&&(t=e,e=void 0),e=e||"",null!=t)return cn(e,t,n,"month");for(var s=[],i=0;i<12;i++)s[i]=cn(e,i,n,"month");return s}function mn(e,t,n,s){t=("boolean"==typeof e?u(t)&&(n=t,t=void 0):(t=e,e=!1,u(n=t)&&(n=t,t=void 0)),t||"");var i,r=P(),a=e?r._week.dow:0,o=[];if(null!=n)return cn(t,(n+a)%7,s,"day");for(i=0;i<7;i++)o[i]=cn(t,(i+a)%7,s,"day");return o}g.calendar=function(e,t,n){return h(e=this._calendar[e]||this._calendar.sameElse)?e.call(t,n):e},g.longDateFormat=function(e){var t=this._longDateFormat[e],n=this._longDateFormat[e.toUpperCase()];return t||!n?t:(this._longDateFormat[e]=n.match(te).map(function(e){return"MMMM"===e||"MM"===e||"DD"===e||"dddd"===e?e.slice(1):e}).join(""),this._longDateFormat[e])},g.invalidDate=function(){return this._invalidDate},g.ordinal=function(e){return this._ordinal.replace("%d",e)},g.preparse=hn,g.postformat=hn,g.relativeTime=function(e,t,n,s){var i=this._relativeTime[n];return h(i)?i(e,t,n,s):i.replace(/%d/i,e)},g.pastFuture=function(e,t){return h(e=this._relativeTime[0<e?"future":"past"])?e(t):e.replace(/%s/i,t)},g.set=function(e){var t,n;for(n in e)c(e,n)&&(h(t=e[n])?this[n]=t:this["_"+n]=t);this._config=e,this._dayOfMonthOrdinalParseLenient=new RegExp((this._dayOfMonthOrdinalParse.source||this._ordinalParse.source)+"|"+/\d{1,2}/.source)},g.eras=function(e,t){for(var n,s=this._eras||P("en")._eras,i=0,r=s.length;i<r;++i)switch("string"==typeof s[i].since&&(n=f(s[i].since).startOf("day"),s[i].since=n.valueOf()),typeof s[i].until){case"undefined":s[i].until=1/0;break;case"string":n=f(s[i].until).startOf("day").valueOf(),s[i].until=n.valueOf()}return s},g.erasParse=function(e,t,n){var s,i,r,a,o,u=this.eras();for(e=e.toUpperCase(),s=0,i=u.length;s<i;++s)if(r=u[s].name.toUpperCase(),a=u[s].abbr.toUpperCase(),o=u[s].narrow.toUpperCase(),n)switch(t){case"N":case"NN":case"NNN":if(a===e)return u[s];break;case"NNNN":if(r===e)return u[s];break;case"NNNNN":if(o===e)return u[s]}else if(0<=[r,a,o].indexOf(e))return u[s]},g.erasConvertYear=function(e,t){var n=e.since<=e.until?1:-1;return void 0===t?f(e.since).year():f(e.since).year()+(t-e.offset)*n},g.erasAbbrRegex=function(e){return c(this,"_erasAbbrRegex")||an.call(this),e?this._erasAbbrRegex:this._erasRegex},g.erasNameRegex=function(e){return c(this,"_erasNameRegex")||an.call(this),e?this._erasNameRegex:this._erasRegex},g.erasNarrowRegex=function(e){return c(this,"_erasNarrowRegex")||an.call(this),e?this._erasNarrowRegex:this._erasRegex},g.months=function(e,t){return e?(a(this._months)?this._months:this._months[(this._months.isFormat||Ve).test(t)?"format":"standalone"])[e.month()]:a(this._months)?this._months:this._months.standalone},g.monthsShort=function(e,t){return e?(a(this._monthsShort)?this._monthsShort:this._monthsShort[Ve.test(t)?"format":"standalone"])[e.month()]:a(this._monthsShort)?this._monthsShort:this._monthsShort.standalone},g.monthsParse=function(e,t,n){var s,i;if(this._monthsParseExact)return function(e,t,n){var s,i,r,e=e.toLocaleLowerCase();if(!this._monthsParse)for(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],s=0;s<12;++s)r=l([2e3,s]),this._shortMonthsParse[s]=this.monthsShort(r,"").toLocaleLowerCase(),this._longMonthsParse[s]=this.months(r,"").toLocaleLowerCase();return n?"MMM"===t?-1!==(i=x.call(this._shortMonthsParse,e))?i:null:-1!==(i=x.call(this._longMonthsParse,e))?i:null:"MMM"===t?-1!==(i=x.call(this._shortMonthsParse,e))||-1!==(i=x.call(this._longMonthsParse,e))?i:null:-1!==(i=x.call(this._longMonthsParse,e))||-1!==(i=x.call(this._shortMonthsParse,e))?i:null}.call(this,e,t,n);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),s=0;s<12;s++){if(i=l([2e3,s]),n&&!this._longMonthsParse[s]&&(this._longMonthsParse[s]=new RegExp("^"+this.months(i,"").replace(".","")+"$","i"),this._shortMonthsParse[s]=new RegExp("^"+this.monthsShort(i,"").replace(".","")+"$","i")),n||this._monthsParse[s]||(i="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[s]=new RegExp(i.replace(".",""),"i")),n&&"MMMM"===t&&this._longMonthsParse[s].test(e))return s;if(n&&"MMM"===t&&this._shortMonthsParse[s].test(e))return s;if(!n&&this._monthsParse[s].test(e))return s}},g.monthsRegex=function(e){return this._monthsParseExact?(c(this,"_monthsRegex")||je.call(this),e?this._monthsStrictRegex:this._monthsRegex):(c(this,"_monthsRegex")||(this._monthsRegex=Ee),this._monthsStrictRegex&&e?this._monthsStrictRegex:this._monthsRegex)},g.monthsShortRegex=function(e){return this._monthsParseExact?(c(this,"_monthsRegex")||je.call(this),e?this._monthsShortStrictRegex:this._monthsShortRegex):(c(this,"_monthsShortRegex")||(this._monthsShortRegex=Ge),this._monthsShortStrictRegex&&e?this._monthsShortStrictRegex:this._monthsShortRegex)},g.week=function(e){return Be(e,this._week.dow,this._week.doy).week},g.firstDayOfYear=function(){return this._week.doy},g.firstDayOfWeek=function(){return this._week.dow},g.weekdays=function(e,t){return t=a(this._weekdays)?this._weekdays:this._weekdays[e&&!0!==e&&this._weekdays.isFormat.test(t)?"format":"standalone"],!0===e?Je(t,this._week.dow):e?t[e.day()]:t},g.weekdaysMin=function(e){return!0===e?Je(this._weekdaysMin,this._week.dow):e?this._weekdaysMin[e.day()]:this._weekdaysMin},g.weekdaysShort=function(e){return!0===e?Je(this._weekdaysShort,this._week.dow):e?this._weekdaysShort[e.day()]:this._weekdaysShort},g.weekdaysParse=function(e,t,n){var s,i;if(this._weekdaysParseExact)return function(e,t,n){var s,i,r,e=e.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],s=0;s<7;++s)r=l([2e3,1]).day(s),this._minWeekdaysParse[s]=this.weekdaysMin(r,"").toLocaleLowerCase(),this._shortWeekdaysParse[s]=this.weekdaysShort(r,"").toLocaleLowerCase(),this._weekdaysParse[s]=this.weekdays(r,"").toLocaleLowerCase();return n?"dddd"===t?-1!==(i=x.call(this._weekdaysParse,e))?i:null:"ddd"===t?-1!==(i=x.call(this._shortWeekdaysParse,e))?i:null:-1!==(i=x.call(this._minWeekdaysParse,e))?i:null:"dddd"===t?-1!==(i=x.call(this._weekdaysParse,e))||-1!==(i=x.call(this._shortWeekdaysParse,e))||-1!==(i=x.call(this._minWeekdaysParse,e))?i:null:"ddd"===t?-1!==(i=x.call(this._shortWeekdaysParse,e))||-1!==(i=x.call(this._weekdaysParse,e))||-1!==(i=x.call(this._minWeekdaysParse,e))?i:null:-1!==(i=x.call(this._minWeekdaysParse,e))||-1!==(i=x.call(this._weekdaysParse,e))||-1!==(i=x.call(this._shortWeekdaysParse,e))?i:null}.call(this,e,t,n);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),s=0;s<7;s++){if(i=l([2e3,1]).day(s),n&&!this._fullWeekdaysParse[s]&&(this._fullWeekdaysParse[s]=new RegExp("^"+this.weekdays(i,"").replace(".","\\.?")+"$","i"),this._shortWeekdaysParse[s]=new RegExp("^"+this.weekdaysShort(i,"").replace(".","\\.?")+"$","i"),this._minWeekdaysParse[s]=new RegExp("^"+this.weekdaysMin(i,"").replace(".","\\.?")+"$","i")),this._weekdaysParse[s]||(i="^"+this.weekdays(i,"")+"|^"+this.weekdaysShort(i,"")+"|^"+this.weekdaysMin(i,""),this._weekdaysParse[s]=new RegExp(i.replace(".",""),"i")),n&&"dddd"===t&&this._fullWeekdaysParse[s].test(e))return s;if(n&&"ddd"===t&&this._shortWeekdaysParse[s].test(e))return s;if(n&&"dd"===t&&this._minWeekdaysParse[s].test(e))return s;if(!n&&this._weekdaysParse[s].test(e))return s}},g.weekdaysRegex=function(e){return this._weekdaysParseExact?(c(this,"_weekdaysRegex")||st.call(this),e?this._weekdaysStrictRegex:this._weekdaysRegex):(c(this,"_weekdaysRegex")||(this._weekdaysRegex=et),this._weekdaysStrictRegex&&e?this._weekdaysStrictRegex:this._weekdaysRegex)},g.weekdaysShortRegex=function(e){return this._weekdaysParseExact?(c(this,"_weekdaysRegex")||st.call(this),e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(c(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=tt),this._weekdaysShortStrictRegex&&e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)},g.weekdaysMinRegex=function(e){return this._weekdaysParseExact?(c(this,"_weekdaysRegex")||st.call(this),e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(c(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=nt),this._weekdaysMinStrictRegex&&e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)},g.isPM=function(e){return"p"===(e+"").toLowerCase().charAt(0)},g.meridiem=function(e,t,n){return 11<e?n?"pm":"PM":n?"am":"AM"},ft("en",{eras:[{since:"0001-01-01",until:1/0,offset:1,name:"Anno Domini",narrow:"AD",abbr:"AD"},{since:"0000-12-31",until:-1/0,offset:1,name:"Before Christ",narrow:"BC",abbr:"BC"}],dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(e){var t=e%10;return e+(1===M(e%100/10)?"th":1==t?"st":2==t?"nd":3==t?"rd":"th")}}),f.lang=e("moment.lang is deprecated. Use moment.locale instead.",ft),f.langData=e("moment.langData is deprecated. Use moment.localeData instead.",P);var _n=Math.abs;function yn(e,t,n,s){t=C(t,n);return e._milliseconds+=s*t._milliseconds,e._days+=s*t._days,e._months+=s*t._months,e._bubble()}function gn(e){return e<0?Math.floor(e):Math.ceil(e)}function wn(e){return 4800*e/146097}function pn(e){return 146097*e/4800}function kn(e){return function(){return this.as(e)}}de=kn("ms"),t=kn("s"),ye=kn("m"),he=kn("h"),Fe=kn("d"),_e=kn("w"),me=kn("M"),Qe=kn("Q"),i=kn("y"),ce=de;function Mn(e){return function(){return this.isValid()?this._data[e]:NaN}}var we=Mn("milliseconds"),fe=Mn("seconds"),ge=Mn("minutes"),Pe=Mn("hours"),g=Mn("days"),vn=Mn("months"),Dn=Mn("years");var Yn=Math.round,Sn={ss:44,s:45,m:45,h:22,d:26,w:null,M:11};function On(e,t,n,s){var i=C(e).abs(),r=Yn(i.as("s")),a=Yn(i.as("m")),o=Yn(i.as("h")),u=Yn(i.as("d")),l=Yn(i.as("M")),d=Yn(i.as("w")),i=Yn(i.as("y")),r=(r<=n.ss?["s",r]:r<n.s&&["ss",r])||a<=1&&["m"]||a<n.m&&["mm",a]||o<=1&&["h"]||o<n.h&&["hh",o]||u<=1&&["d"]||u<n.d&&["dd",u];return(r=(r=null!=n.w?r||d<=1&&["w"]||d<n.w&&["ww",d]:r)||l<=1&&["M"]||l<n.M&&["MM",l]||i<=1&&["y"]||["yy",i])[2]=t,r[3]=0<+e,r[4]=s,function(e,t,n,s,i){return i.relativeTime(t||1,!!n,e,s)}.apply(null,r)}var bn=Math.abs;function Tn(e){return(0<e)-(e<0)||+e}function xn(){if(!this.isValid())return this.localeData().invalidDate();var e,t,n,s,i,r,a,o=bn(this._milliseconds)/1e3,u=bn(this._days),l=bn(this._months),d=this.asSeconds();return d?(e=k(o/60),t=k(e/60),o%=60,e%=60,n=k(l/12),l%=12,s=o?o.toFixed(3).replace(/\.?0+$/,""):"",i=Tn(this._months)!==Tn(d)?"-":"",r=Tn(this._days)!==Tn(d)?"-":"",a=Tn(this._milliseconds)!==Tn(d)?"-":"",(d<0?"-":"")+"P"+(n?i+n+"Y":"")+(l?i+l+"M":"")+(u?r+u+"D":"")+(t||e||o?"T":"")+(t?a+t+"H":"")+(e?a+e+"M":"")+(o?a+s+"S":"")):"P0D"}var U=Ct.prototype;return U.isValid=function(){return this._isValid},U.abs=function(){var e=this._data;return this._milliseconds=_n(this._milliseconds),this._days=_n(this._days),this._months=_n(this._months),e.milliseconds=_n(e.milliseconds),e.seconds=_n(e.seconds),e.minutes=_n(e.minutes),e.hours=_n(e.hours),e.months=_n(e.months),e.years=_n(e.years),this},U.add=function(e,t){return yn(this,e,t,1)},U.subtract=function(e,t){return yn(this,e,t,-1)},U.as=function(e){if(!this.isValid())return NaN;var t,n,s=this._milliseconds;if("month"===(e=_(e))||"quarter"===e||"year"===e)switch(t=this._days+s/864e5,n=this._months+wn(t),e){case"month":return n;case"quarter":return n/3;case"year":return n/12}else switch(t=this._days+Math.round(pn(this._months)),e){case"week":return t/7+s/6048e5;case"day":return t+s/864e5;case"hour":return 24*t+s/36e5;case"minute":return 1440*t+s/6e4;case"second":return 86400*t+s/1e3;case"millisecond":return Math.floor(864e5*t)+s;default:throw new Error("Unknown unit "+e)}},U.asMilliseconds=de,U.asSeconds=t,U.asMinutes=ye,U.asHours=he,U.asDays=Fe,U.asWeeks=_e,U.asMonths=me,U.asQuarters=Qe,U.asYears=i,U.valueOf=ce,U._bubble=function(){var e=this._milliseconds,t=this._days,n=this._months,s=this._data;return 0<=e&&0<=t&&0<=n||e<=0&&t<=0&&n<=0||(e+=864e5*gn(pn(n)+t),n=t=0),s.milliseconds=e%1e3,e=k(e/1e3),s.seconds=e%60,e=k(e/60),s.minutes=e%60,e=k(e/60),s.hours=e%24,t+=k(e/24),n+=e=k(wn(t)),t-=gn(pn(e)),e=k(n/12),n%=12,s.days=t,s.months=n,s.years=e,this},U.clone=function(){return C(this)},U.get=function(e){return e=_(e),this.isValid()?this[e+"s"]():NaN},U.milliseconds=we,U.seconds=fe,U.minutes=ge,U.hours=Pe,U.days=g,U.weeks=function(){return k(this.days()/7)},U.months=vn,U.years=Dn,U.humanize=function(e,t){if(!this.isValid())return this.localeData().invalidDate();var n=!1,s=Sn;return"object"==typeof e&&(t=e,e=!1),"boolean"==typeof e&&(n=e),"object"==typeof t&&(s=Object.assign({},Sn,t),null!=t.s&&null==t.ss&&(s.ss=t.s-1)),e=this.localeData(),t=On(this,!n,s,e),n&&(t=e.pastFuture(+this,t)),e.postformat(t)},U.toISOString=xn,U.toString=xn,U.toJSON=xn,U.locale=Xt,U.localeData=Kt,U.toIsoString=e("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",xn),U.lang=Ke,s("X",0,0,"unix"),s("x",0,0,"valueOf"),w("x",ke),w("X",/[+-]?\d+(\.\d{1,3})?/),v("X",function(e,t,n){n._d=new Date(1e3*parseFloat(e))}),v("x",function(e,t,n){n._d=new Date(M(e))}),f.version="2.30.1",H=R,f.fn=y,f.min=function(){return Pt("isBefore",[].slice.call(arguments,0))},f.max=function(){return Pt("isAfter",[].slice.call(arguments,0))},f.now=function(){return Date.now?Date.now():+new Date},f.utc=l,f.unix=function(e){return R(1e3*e)},f.months=function(e,t){return fn(e,t,"months")},f.isDate=V,f.locale=ft,f.invalid=I,f.duration=C,f.isMoment=d,f.weekdays=function(e,t,n){return mn(e,t,n,"weekdays")},f.parseZone=function(){return R.apply(null,arguments).parseZone()},f.localeData=P,f.isDuration=Ut,f.monthsShort=function(e,t){return fn(e,t,"monthsShort")},f.weekdaysMin=function(e,t,n){return mn(e,t,n,"weekdaysMin")},f.defineLocale=mt,f.updateLocale=function(e,t){var n,s;return null!=t?(s=ut,null!=W[e]&&null!=W[e].parentLocale?W[e].set(X(W[e]._config,t)):(t=X(s=null!=(n=ct(e))?n._config:s,t),null==n&&(t.abbr=e),(s=new K(t)).parentLocale=W[e],W[e]=s),ft(e)):null!=W[e]&&(null!=W[e].parentLocale?(W[e]=W[e].parentLocale,e===ft()&&ft(e)):null!=W[e]&&delete W[e]),W[e]},f.locales=function(){return ee(W)},f.weekdaysShort=function(e,t,n){return mn(e,t,n,"weekdaysShort")},f.normalizeUnits=_,f.relativeTimeRounding=function(e){return void 0===e?Yn:"function"==typeof e&&(Yn=e,!0)},f.relativeTimeThreshold=function(e,t){return void 0!==Sn[e]&&(void 0===t?Sn[e]:(Sn[e]=t,"s"===e&&(Sn.ss=t-1),!0))},f.calendarFormat=function(e,t){return(e=e.diff(t,"days",!0))<-6?"sameElse":e<-1?"lastWeek":e<0?"lastDay":e<1?"sameDay":e<2?"nextDay":e<7?"nextWeek":"sameElse"},f.prototype=y,f.HTML5_FMT={DATETIME_LOCAL:"YYYY-MM-DDTHH:mm",DATETIME_LOCAL_SECONDS:"YYYY-MM-DDTHH:mm:ss",DATETIME_LOCAL_MS:"YYYY-MM-DDTHH:mm:ss.SSS",DATE:"YYYY-MM-DD",TIME:"HH:mm",TIME_SECONDS:"HH:mm:ss",TIME_MS:"HH:mm:ss.SSS",WEEK:"GGGG-[W]WW",MONTH:"YYYY-MM"},f});
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
<!DOCTYPE html>
<html lang="zh-CN">
<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" />
<link rel="stylesheet" href="/tht_project/static/html/big_screen/element-ui/index.css" />
<script src="/tht_project/static/html/big_screen/js/echarts.min.js"></script>
<script src="/tht_project/static/html/big_screen/js/moment.min.js"></script>
<script src="/tht_project/static/html/big_screen/js/vue.js"></script>
<script src="/tht_project/static/html/big_screen/js/axios.min.js"></script>
<script src="/tht_project/static/html/big_screen/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="pageHeaderBox">
<div class="weatherInfo">
<span class="location">成型工序</span>
</div>
<img class="logo" src="/tht_project/static/html/big_screen/image/logo.png">
<div class="title">天合堂数据指挥中心可视化大屏</div>
<div class="dateTimeBox">
<i style="font-size: 30px;margin-right: 10px;" @click="toggleFullscreen"
:class="isFullscreen ? 'el-icon-close' : 'el-icon-full-screen'"></i>
<div class="time">[[currentTime]]</div>
<div class="dateBox">
<span>[[ currentDate ]]</span>
<span>[[ currentWeek ]]</span>
</div>
</div>
</div>
<div class="pageContentBox">
<div class="pageContentBackgroundBox">
<img class="badgeIcon" src="/tht_project/static/html/big_screen/image/777.png" alt="" v-for="item in 4"
:key="item">
</div>
<div class="pageContentArea">
<div class="contentColumnBox one">
<div class="itemCardBox">
<div class="cardHeader">
<span>水煮线温度</span>
</div>
<div class="cardContentBox">
<div id="waterBoilTempChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>静置间温度</span>
</div>
<div class="cardContentBox">
<div id="stationaryTempChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>真空包装机罐装速度(次/分钟)</span>
</div>
<div class="cardContentBox">
<div id="vacuumCanningSpeedChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>烟熏炉箱温(℃)</span>
</div>
<div class="cardContentBox">
<div id="smudgingFurnaceTemperatureChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
</div>
<div class="contentColumnBox two">
<div class="itemCardBox">
<div class="cardHeader">
<span>水煮线液位控制</span>
</div>
<div class="cardContentBox">
<div id="waterLevelChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>静置间设备状态</span>
</div>
<div class="cardContentBox">
<div id="stationaryStatusChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>真空包装机设备加工时长(小时)</span>
</div>
<div class="cardContentBox">
<div id="vacuumProcessTimeChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>烟熏炉芯温(℃)</span>
</div>
<div class="cardContentBox">
<div id="smokeOvenCoreTemptChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
</div>
<div class="contentColumnBox three">
<div class="itemCardBox">
<div class="cardHeader">
<span>水煮线设备状态</span>
</div>
<div class="cardContentBox">
<div id="waterBoilStatusChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>真空包装机真空度</span>
</div>
<div class="cardContentBox">
<div id="vacuumDegreesChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>烟熏炉工艺时间(分钟)</span>
</div>
<div class="cardContentBox">
<div id="smokeOvenProcessTimeChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>烟熏炉设备资源利用率</span>
</div>
<div class="cardContentBox">
<div id="smokeOvenStatusChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
</div>
<div class="contentColumnBox four">
<div class="itemCardBox">
<div class="cardHeader">
<span>报警中心</span>
</div>
<div class="cardContentBox tableBox">
<el-table class="tableClass" :data="alarmList" height="1" tooltip-effect="light"
:row-style="{height: '30px', backgroundColor: 'transparent'}"
:cell-style="{padding: '5px 0', borderBottom:'none'}"
:header-row-style="{ backgroundColor: 'transparent'}" :header-cell-style="{
backgroundColor: 'transparent',
color: '#01c4f9',
fontWeight: 'bold',
fontSzie:'14px',
height:'30px',
lineHeight:'30px',
padding: '5px 0',
borderBottom:'none'
}">
<el-table-column prop="device" label="报警类型">
<template slot-scope="scope">
<span style="color: #fff; font-size: 12px;">[[ scope.row.device ]]</span>
</template>
</el-table-column>
<el-table-column prop="time" label="报警时间" width="80">
<template slot-scope="scope">
<span style="color: #fff; font-size: 12px;">[[ scope.row.time ]]</span>
</template>
</el-table-column>
<el-table-column prop="detail" label="报警描述" show-overflow-tooltip>
<template slot-scope="scope">
<span style="color: #fff; font-size: 12px;">[[ scope.row.detail ]]</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>今日计划执行情况</span>
</div>
<div class="cardContentBox">
<div id="meatGrainAnalysisChart" style="width: 100%; height: 100%;"></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, // 窗口高度
isFullscreen: false, // 全屏状态
baseURL: "https://tht-test.newerp.rokedata.com", // 基地址
dwsBaseUrl: "https://dws-platform.xbg.rokeris.com/dev-api", // dws系统基地址
loading: false, // 全局加载效果
currentTime: "", // 当前时间
currentDate: "", // 当前日期
currentWeek: "", // 当前周
timeInterval: null, // 时间定时器
dateInterval: null, // 日期定时器
charts: {}, // 图表
alarmList: [
{ device: "设备故障", time: "2025-4-23", detail: "切丁机电源了故障" },
{ device: "原料临期", time: "2025-4-22", detail: "酱油将于2025-5-1过期" },
{ device: "原料临期", time: "2025-4-21", detail: "酱油将于2025-5-1过期" },
{ device: "质检不合格", time: "2025-4-21", detail: "有猪肉1斤质检不合格" },
{ device: "质检不合格", time: "2025-4-21", detail: "有猪肉1斤质检不合格" },
{ device: "缺料", time: "2025-4-21", detail: "胡椒粉即将缺料" },
{ device: "缺料", time: "2025-4-21", detail: "牛肉即将缺料" },
{ device: "配比异常", time: "2025-4-23", detail: "淀粉配比不达标" },
{ device: "配比异常", time: "2025-4-21", detail: "淀粉配比不达标" },
{ device: "设备故障", time: "2025-4-21", detail: "切丁机电源了故障" },
{ device: "设备故障", time: "2025-4-21", detail: "切丁机电源了故障" },
{ device: "设备故障", time: "2025-4-21", detail: "切丁机电源了故障" },
{ device: "设备故障", time: "2025-4-21", detail: "切丁机电源了故障" },
{ device: "设备故障", time: "2025-4-21", detail: "切丁机电源了故障" },
{ device: "设备故障", time: "2025-4-21", detail: "切丁机电源了故障" },
],
}
},
computed: {
// 当前位置
currentLocation() {
if (this.currentProvince && this.currentCity) {
return `${this.currentProvince}-${this.currentCity}`
} else {
return "未知城市"
}
}
},
created() {
// 添加resize事件监听
window.addEventListener("resize", this.handleResize)
// 更新日期、时间、周
this.updateDateTime()
// 每秒更新时间
this.timeInterval = setInterval(this.updateTime, 1000)
// 每分钟检查一次日期是否需要更新(每天只需更新一次)
this.dateInterval = setInterval(() => {
const now = new Date()
if (now.getHours() === 0 && now.getMinutes() === 0) {
this.updateDate()
this.updateWeek()
}
}, 60000)
},
mounted() {
this.$nextTick(() => {
document.getElementById("bodyId").style.display = "block"
this.initAllCharts()
})
},
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
},
// 处理窗口大小变化修改图表大小
handleResize() {
for (let key in this.charts) {
this.charts[key] && this.charts[key].resize()
}
},
// 更新日期、时间、周
updateDateTime() {
this.updateTime()
this.updateDate()
this.updateWeek()
},
// 更新时间
updateTime() {
this.currentTime = moment().format('HH:mm:ss')
},
// 更新日期
updateDate() {
this.currentDate = moment().format('YYYY-MM-DD')
},
// 更新周
updateWeek() {
this.currentWeek = '周' + ['日', '一', '二', '三', '四', '五', '六'][moment().day()]
},
// 接口请求方法封装
requestApi(
url,
config = {},
errorMessage = "操作失败,请稍后重试",
contentType = "application/json"
) {
return new Promise((resolve, reject) => {
if (!url) reject(null)
axios({
method: "POST",
url: this.baseURL + url,
data: config,
headers: { "Content-Type": contentType },
}).then((result) => {
if (
result?.data?.result?.code == 0 ||
result?.data?.result?.state == "success" ||
result?.data?.code == 0
) {
resolve(result.data)
} else if (result?.data?.result?.code == 1) {
reject(result.data.result.message)
} else if (result?.data?.result?.state == "error") {
reject(result.data.result.megs)
} else if (result?.data?.code == 0) {
reject(result.data.message)
} else if (result?.data?.error) {
reject(result.data.error.message)
}
}).catch((error) => {
reject(errorMessage)
})
})
},
requestSelfApi(
url,
config = {},
errorMessage = "操作失败,请稍后重试",
contentType = "application/json",
baseurl=""
) {
return new Promise((resolve, reject) => {
if (!url) reject(null)
axios({
method: "POST",
url: baseurl + url,
data: config,
headers: { "Content-Type": contentType },
}).then((result) => {
if (
result?.data?.result?.success ||
result?.data?.result?.code == 0 ||
result?.data?.result?.state == "success" ||
result?.data?.code == 0
) {
resolve(result.data)
} else if (result?.data?.result?.code == 1) {
reject(result.data.result.message)
} else if (result?.data?.result?.state == "error") {
reject(result.data.result.megs)
} else if (result?.data?.code == 1) {
reject(result.data.message)
} else if (!result?.data?.result?.success) {
reject(result.data.result.msg)
} else if (result?.data?.error) {
reject(result.data.error.message)
}
}).catch((error) => {
reject(errorMessage)
})
})
},
// 接口请求Dws系统方法封装
requestDwsApi(
url,
config = {},
errorMessage = "操作失败,请稍后重试",
contentType = "application/json"
) {
return new Promise((resolve, reject) => {
if (!url) reject(null)
axios({
method: "POST",
url: this.dwsBaseUrl + url,
data: config,
headers: { "Content-Type": contentType },
}).then((result) => {
if (result?.data?.success) {
resolve(result.data)
} else if (!result.data.success) {
reject(result.data.msg)
} else {
reject(errorMessage)
}
}).catch((error) => {
reject(errorMessage)
})
})
},
// 初始化所有图表
async initAllCharts() {
this.loading = true
// 烟熏机
let smudgingChance = {
xAxisData: [], // 设备名
processTime: [], // 工艺时间
temperatureX: [], // 箱温
xinwen: [], // 芯温
useRatio: []// 设备资源利用率
}
// 查询烟熏机数据
await this.requestSelfApi('/get_yanxunlu_data', {
"plant_name": "蒸煮车间",
"category_name": "烟熏炉",
'today':'',
}).then(result => {
result.result.data.forEach(item => {
smudgingChance.xAxisData.push(item.device_name)
smudgingChance.processTime.push(item.process)
smudgingChance.temperatureX.push(item.temperatureX)
smudgingChance.xinwen.push(item.xinwen)
})
}).catch(error => { console.error(error) })
// 查询烟熏机设备资源利用率
await this.requestSelfApi('/big_screen_today', {
"plant_name": "蒸煮车间",
"category_name": "烟熏炉",
'today':'',
}).then(result => {
}).catch(error => { console.error(error) })
setTimeout(() => {
this.initWaterBoilTempChart()
this.initStationaryTempChart()
this.initWaterLevelChart()
this.initVacuumCanningSpeedChart()
this.initSmudgingFurnaceTemperatureChart(smudgingChance.xAxisData, smudgingChance.temperatureX)
this.initStationaryStatusChart()
this.initVacuumProcessTimeChart()
this.initSmokeOvenCoreTemptChart(smudgingChance.xAxisData, smudgingChance.xinwen)
this.initWaterBoilStatusChart()
this.initVacuumDegreesChart()
this.initSmokeOvenProcessTimeChart(smudgingChance.xAxisData, smudgingChance.processTime)
this.initSmokeOvenStatusChart(smudgingChance.xAxisData, smudgingChance.useRatio)
this.initMeatGrainAnalysisChart()
}, 1)
this.loading = false
},
// 水煮线温度
initWaterBoilTempChart() {
const chart = echarts.init(document.getElementById('waterBoilTempChart'))
this.charts.waterBoilTemp = chart
// 单位
const unit = ''
// x轴数据
const xAxisData = ['水煮线1', '水煮线2', '水煮线3', '水煮线4']
// 数量
const seriesData = [100, 140, 230, 50]
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = [...seriesData].length ? Math.ceil(Math.max(...[...seriesData]) * 1.5) : 100
chart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
valueFormatter: (value) => value + unit
},
dataZoom: [{
id: 'waterBoilTempChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#0a73ff' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3 } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '{value}' + unit }
},
series: [{
type: 'bar',
barWidth: '40%',
barGap: '0%',
data: seriesData,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#01c4f9' },
{ offset: 1, color: '#0a73ff' }
])
},
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + unit
}
}]
})
},
// 静置间温度
initStationaryTempChart() {
const chart = echarts.init(document.getElementById('stationaryTempChart'))
this.charts.stationaryTemp = chart
// 数值
const valueData = {
standing_room_1: 60, // 静置间1
standing_room_2: 80, // 静置间2
}
// series配置
const seriesData = []
for (let i = 0; i < 2; i++) {
let data = {
name: '',
type: 'gauge',
center: [],
radius: '60%',
startAngle: 200,
endAngle: -20,
splitNumber: 5,
itemStyle: { color: '#3a8fff' },
progress: { show: true, width: 5 },
pointer: { show: false },
axisLine: { lineStyle: { width: 5 } },
axisTick: {
distance: -15,
length: 5,
splitNumber: 5,
lineStyle: { width: 1, color: '#999' }
},
splitLine: {
distance: -15,
length: 8,
lineStyle: { width: 1, color: '#999' }
},
axisLabel: { distance: -15, color: '#999', fontSize: 10, },
detail: {
valueAnimation: true,
offsetCenter: [0, 0],
fontSize: 10,
formatter: '{value}℃',
color: 'inherit'
},
data: [0]
}
if (i == 0) {
data.name = '静置间1'
data.center = ['25%', '70%']
data.data = [valueData.standing_room_1 || 0]
} else if (i == 1) {
data.name = '静置间2'
data.center = ['75%', '70%']
data.data = [valueData.standing_room_2 || 0]
}
seriesData.push(data)
}
chart.setOption({
grid: { left: '5%', right: '5%', top: '8%', bottom: '5%' },
graphic: [{
type: 'text',
left: '19%',
top: '80%',
style: { text: '静置间1', fill: '#fff', fontSize: 10 }
}, {
type: 'text',
left: '69%',
top: '80%',
style: { text: '静置间2', fill: '#fff', fontSize: 10 }
}],
series: seriesData
})
},
// 真空包装机罐装速度
initVacuumCanningSpeedChart() {
const chart = echarts.init(document.getElementById('vacuumCanningSpeedChart'))
this.charts.vacuumCanningSpeed = chart
// 单位
const unit = '次/分钟'
// x轴数据
const xAxisData = ['罐装机1', '罐装机2', '罐装机3', '罐装机4']
// 数量
const seriesData = [100, 140, 230, 50]
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = [...seriesData].length ? Math.ceil(Math.max(...[...seriesData])) * 1.5 : 100
chart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
valueFormatter: (value) => value + unit
},
dataZoom: [{
id: 'waterBoilTempChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#0a73ff' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3 } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '{value}' + unit }
},
series: [{
type: 'bar',
barWidth: '40%',
barGap: '0%',
data: seriesData,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#01c4f9' },
{ offset: 1, color: '#0a73ff' }
])
},
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + unit
}
}]
})
},
// 烟熏炉箱温
initSmudgingFurnaceTemperatureChart(xAxisData = [], temperatureX = []) {
const chart = echarts.init(document.getElementById('smudgingFurnaceTemperatureChart'))
this.charts.smudgingFurnaceTemperature = chart
// 单位
const unit = '℃'
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = [...temperatureX].length ? Math.ceil(Math.max(...[...temperatureX]) * 1.5) : 100
chart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
valueFormatter: (value) => value + unit
},
dataZoom: [{
id: 'waterLevelChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxisData,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
axisTick: { show: false },
axisLabel: { color: '#fff', fontSize: 10, interval: 0 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3, type: 'dashed' } },
axisLabel: { color: '#ccc', fontSize: 10, formatter: '{value}' + unit }
},
series: [{
type: 'line',
data: temperatureX,
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + unit
}
}]
})
},
// 水煮线液位控制
initWaterLevelChart() {
const chart = echarts.init(document.getElementById('waterLevelChart'))
this.charts.waterLevel = chart
// x轴数据
const xAxisData = ['水煮线1', '水煮线2', '水煮线3', '水煮线4']
// 数量
const seriesData = [20, 50, 30, 100]
// 单位
const unit = '%'
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = [...seriesData].length ? Math.ceil(Math.max(...[...seriesData]) * 1.5) : 100
chart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
valueFormatter: (value) => value + unit
},
dataZoom: [{
id: 'waterLevelChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxisData,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
axisTick: { show: false },
axisLabel: { color: '#fff', fontSize: 10, interval: 0 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3, type: 'dashed' } },
axisLabel: { color: '#ccc', fontSize: 10, formatter: '{value}' + unit }
},
series: [{
type: 'line',
data: seriesData,
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + unit
}
}]
})
},
// 静置间设备状态
initStationaryStatusChart() {
const chart = echarts.init(document.getElementById('stationaryStatusChart'))
this.charts.stationaryStatus = chart
const unit = ''
// x轴数据
const xAxisData = ['静置间1', '静置间2', '静置间3', '静置间4']
// 数量
const seriesData = [100, 140, 230, 50]
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = [...seriesData].length ? Math.ceil(Math.max(...[...seriesData]) * 1.5) : 100
chart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
valueFormatter: (value) => value + unit
},
dataZoom: [{
id: 'stationaryStatusChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#0a73ff' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3 } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '{value}' + unit }
},
series: [{
type: 'bar',
barWidth: '40%',
barGap: '0%',
data: seriesData,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#01c4f9' },
{ offset: 1, color: '#0a73ff' }
])
},
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + unit
}
}]
})
},
// 真空包装机设备加工时长
initVacuumProcessTimeChart() {
const chart = echarts.init(document.getElementById('vacuumProcessTimeChart'))
this.charts.vacuumProcessTime = chart
// 单位
const unit = 'h'
// x轴数据
const xAxisData = ['罐装机1', '罐装机2', '罐装机3', '罐装机4']
// 数量
const seriesData = [100, 140, 230, 50]
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = [...seriesData].length ? Math.ceil(Math.max(...[...seriesData]) * 1.5) : 100
chart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
valueFormatter: (value) => value + unit
},
dataZoom: [{
id: 'vacuumProcessTimeChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#0a73ff' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3 } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '{value}' + unit }
},
series: [{
type: 'bar',
barWidth: '40%',
barGap: '0%',
data: seriesData,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#01c4f9' },
{ offset: 1, color: '#0a73ff' }
])
},
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + unit
}
}]
})
},
// 烟熏炉芯温
initSmokeOvenCoreTemptChart(xAxisData = [], xinwen = []) {
const chart = echarts.init(document.getElementById('smokeOvenCoreTemptChart'))
this.charts.smokeOvenCoreTempt = chart
// 单位
const unit = '℃'
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = [...xinwen].length ? Math.ceil(Math.max(...[...xinwen]) * 1.5) : 100
chart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
valueFormatter: (value) => value + unit
},
dataZoom: [{
id: 'vacuumProcessTimeChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#0a73ff' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3 } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '{value}' + unit }
},
series: [{
type: 'bar',
barWidth: '40%',
barGap: '0%',
data: xinwen,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#01c4f9' },
{ offset: 1, color: '#0a73ff' }
])
},
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + unit
}
}]
})
},
// 水煮线设备状态
initWaterBoilStatusChart() {
const chart = echarts.init(document.getElementById('waterBoilStatusChart'))
this.charts.waterBoilStatus = chart
chart.setOption({
tooltip: {
trigger: 'item',
valueFormatter: (value) => value + '台',
},
legend: {
orient: 'vertical',
left: 'top',
itemWidth: 20,
itemHeight: 10,
itemGap: 5,
textStyle: {
color: "#fff",
fontSize: 10
}
},
series: [{
type: 'pie',
radius: '60%',
center: ['60%', '45%'],
itemStyle: {
borderRadius: 0,
borderColor: '#061058',
borderWidth: 2
},
label: {
show: true,
formatter: '{c}台',
color: '#fff',
fontSize: 12
},
data: [{
value: 50,
name: '运行',
itemStyle: { color: '#91cc75' },
}, {
value: 10,
name: '等待',
itemStyle: { color: '#ffd700' },
}, {
value: 5,
name: '故障',
itemStyle: { color: '#ee6666' },
}, {
value: 10,
name: '关机',
itemStyle: { color: '#787878' },
}]
}]
})
},
// 真空包装机真空度
initVacuumDegreesChart() {
const chart = echarts.init(document.getElementById('vacuumDegreesChart'))
this.charts.vacuumDegrees = chart
// x轴数据
const xAxisData = ['罐装机1', '罐装机2', '罐装机3', '罐装机4']
// 数量
const seriesData = [20, 50, 30, 100]
// 单位
const unit = 'kpa'
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = [...seriesData].length ? Math.ceil(Math.max(...[...seriesData]) * 1.5) : 100
chart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
valueFormatter: (value) => value + unit
},
dataZoom: [{
id: 'vacuumDegreesChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxisData,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
axisTick: { show: false },
axisLabel: { color: '#fff', fontSize: 10, interval: 0 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3, type: 'dashed' } },
axisLabel: { color: '#ccc', fontSize: 10, formatter: '{value}' + unit }
},
series: [{
type: 'line',
data: seriesData,
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + unit
}
}]
})
},
// 烟熏炉工艺时间
initSmokeOvenProcessTimeChart(xAxisData = [], processTime = []) {
const chart = echarts.init(document.getElementById('smokeOvenProcessTimeChart'))
this.charts.smokeOvenProcessTime = chart
// 单位
const unit = 'min'
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = [...processTime].length ? Math.ceil(Math.max(...[...processTime]) * 1.5) : 100
chart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
valueFormatter: (value) => value + unit
},
dataZoom: [{
id: 'vacuumProcessTimeChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#0a73ff' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3 } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '{value}' + unit }
},
series: [{
type: 'bar',
barWidth: '40%',
barGap: '0%',
data: processTime,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#01c4f9' },
{ offset: 1, color: '#0a73ff' }
])
},
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + unit
}
}]
})
},
// 烟熏炉设备资源利用率
initSmokeOvenStatusChart() {
const chart = echarts.init(document.getElementById('smokeOvenStatusChart'))
this.charts.smokeOvenStatus = chart
// 单位
const unit = '%'
// x轴数据
const xAxisData = ['烟熏炉1', '烟熏炉2', '烟熏炉3', '烟熏炉4']
// 数量
const seriesData = [100, 140, 230, 50]
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = [...seriesData].length ? Math.ceil(Math.max(...[...seriesData]) * 1.5) : 100
chart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
valueFormatter: (value) => value + unit
},
dataZoom: [{
id: 'smokeOvenStatusChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#0a73ff' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3 } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '{value}' + unit }
},
series: [{
type: 'bar',
barWidth: '40%',
barGap: '0%',
data: seriesData,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#01c4f9' },
{ offset: 1, color: '#0a73ff' }
])
},
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + unit
}
}]
})
},
// 今日计划执行情况
initMeatGrainAnalysisChart() {
const chart = echarts.init(document.getElementById('meatGrainAnalysisChart'))
this.charts.meatGrainAnalysis = chart
// x轴数据
const xAxisData = ['产品1', '产品2', '产品3', '产品4']
// 投产数量
const operationData = [{
value: 100,
uom_name: 'kg'
}, {
value: 140,
uom_name: '件'
}, {
value: 230,
uom_name: 'm'
}, {
value: 50,
uom_name: 'km'
}]
// 完成数量
const quantityData = [{
value: 20,
uom_name: 'kg'
}, {
value: 140,
uom_name: '件'
}, {
value: 90,
uom_name: 'm'
}, {
value: 290,
uom_name: 'km'
}]
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = this.calculateMaxValue([operationData, quantityData], 1.5)
chart.setOption({
grid: {
left: '5%', right: '5%', top: '15%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
legend: {
top: '-2%',
right: '5%',
textStyle: { color: '#fff', fontSize: 10 },
itemWidth: 20,
itemHeight: 10,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: (params) => {
let tooltipHtml = `<div style="font-weight:bold;margin-bottom:5px">${params[0].name}</div>`
params.forEach(param => {
const dataIndex = param.dataIndex
const unit = param.seriesIndex === 0 ? operationData[dataIndex].unit : quantityData[dataIndex].unit
// 使用系列颜色作为小圆点颜色
tooltipHtml += `
<div style="display:flex;align-items:center;margin:5px 0">
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${param.color};margin-right:10px"></span>
<span style="margin-right:10px">${param.seriesName}:</span>
<strong>${param.value}</strong>
<span>${unit}</span>
</div>
`
})
// 计算完成率
const total = params[0].value
const completed = params[1].value
const percentage = total > 0 ? ((completed / total) * 100).toFixed(1) : 0
tooltipHtml += `
<div style="margin-top:10px;border-top:1px solid #eee;padding-top:5px">
完成率: <strong>${percentage}%</strong>
</div>
`
return tooltipHtml
}
},
dataZoom: [{
id: 'meatGrainAnalysisChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#0a73ff' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitNumber: 5,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3 } },
axisLabel: { color: '#fff', fontSize: 10 }
},
series: [{
name: '投产数量',
type: 'bar',
barWidth: '40%',
barGap: '0%',
data: operationData,
itemStyle: { color: '#5087ec' },
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + params.data.unit
}
}, {
name: '完成数量',
type: 'bar',
barWidth: '40%',
barGap: '0%',
data: quantityData,
itemStyle: { color: '#68bbc4' },
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + params.data.unit
}
}]
})
},
/**
* 计算图表Y轴最大值
* @param {Array} dataArrays 需要合并计算的数据数组
* @param {number} multiplier 放大系数,默认1.2
* @param {number} defaultValue 默认最大值,当无数据时使用
* @returns {number} 计算后的最大值
*/
calculateMaxValue(dataArrays, multiplier = 1.2, defaultValue = 100) {
// 合并所有数组并提取value值
const allValues = dataArrays.flat().map(item => item.value);
// 如果没有数据,返回默认值
if (allValues.length === 0) return defaultValue;
// 计算最大值并应用放大系数,然后向上取整
const maxValue = Math.max(...allValues);
return Math.ceil(maxValue * multiplier);
}
},
beforeDestroy() {
// 清除时间定时器
clearInterval(this.timeInterval)
// 清除日期定时器
clearInterval(this.dateInterval)
// 清除resize事件监听
window.removeEventListener("resize", this.handleResize)
}
})
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
::-webkit-scrollbar {
width: 0px;
height: 0px;
}
::-webkit-scrollbar-thumb {
background: #c0c4cc;
border-radius: 3px;
}
::-webkit-scrollbar-track {
background: #f5f7fa;
}
#app {
width: 100vw;
height: 100vh;
overflow: hidden;
color: #fff;
font-family: "Microsoft YaHei", sans-serif;
background-color: #06114f;
display: flex;
flex-direction: column;
}
.pageHeaderBox {
height: 60px;
display: flex;
justify-content: center;
align-items: center;
padding: 0 20px;
border-bottom: 1px solid rgba(10, 115, 255, 0.3);
background-image: url(/tht_project/static/html/big_screen/image/header_background.png);
background-size: 100%;
position: relative;
font-size: 24px;
z-index: 2;
.weatherInfo {
position: absolute;
left: 20px;
bottom: 5px;
font-size: 23px;
display: flex;
align-items: baseline;
gap: 5px;
.location {
font-weight: bold;
}
.icon {
margin-top: 5px;
}
}
.logo {
width: 1.3em;
height: 1.3em;
}
.title {
font-weight: bold;
letter-spacing: 4px;
margin-left: 10px;
}
.dateTimeBox {
position: absolute;
right: 20px;
bottom: 5px;
display: flex;
align-items: center;
font-size: 24px;
.time {
margin-right: 5px;
}
.dateBox {
display: flex;
flex-direction: column;
font-size: 10px;
}
}
}
.pageContentBox {
width: 100%;
height: 1px;
flex: auto;
position: relative;
padding: 10px;
.pageContentBackgroundBox {
position: absolute;
width: calc(100% - 20px);
height: calc(100% - 20px);
padding: 10px;
border-left: 2px solid #cbdbf8;
border-right: 2px solid #cbdbf8;
background: transparent;
border-radius: 10px;
.badgeIcon {
position: absolute;
background-color: #06114f;
}
.badgeIcon:first-child {
top: 0;
left: 0;
transform: translate(-12%, -35%);
}
.badgeIcon:nth-child(2) {
top: 0;
right: 0;
transform: translate(12%, -35%) scaleX(-1);
}
.badgeIcon:nth-child(3) {
bottom: 0;
left: 0;
transform: translate(-12%, 35%) scaley(-1);
}
.badgeIcon:last-child {
bottom: 0;
right: 0;
transform: translate(12%, 35%) scale(-1);
}
}
/* 发光小点 */
.pageContentBackgroundBox::before,
.pageContentBackgroundBox::after {
content: '';
position: absolute;
top: 30px;
transform: translateX(-30%);
width: 0px;
height: 10px;
background: #fff;
border-radius: 50%;
box-shadow: 0 0 15px 3px rgba(255, 255, 255, 0.8),
0 0 15px 3px rgba(255, 255, 255, 0.8);
animation: dot-move 4s linear infinite;
z-index: 2;
}
.pageContentBackgroundBox::before {
/* 定位在左边框 */
left: -1px;
}
/* 发光小点 */
.pageContentBackgroundBox::after {
/* 定位在右边框 */
right: -1px;
}
.pageContentArea {
display: flex;
gap: 5px;
padding: 10px;
height: 100%;
.contentColumnBox {
height: 100%;
display: flex;
flex-direction: column;
gap: 5px;
z-index: 1;
.itemCardBox {
height: calc((100% - 15px) / 4);
background-image: url(/tht_project/static/html/big_screen/image/group_background.png);
background-size: 100% 100%;
padding: 5px 8px 0px;
display: flex;
flex-direction: column;
.cardHeader {
height: 30px;
span {
padding: 0 8px;
font-size: 14px;
font-weight: bold;
background: linear-gradient(180deg, #FFF 10%, #00bbf3 100%);
/* 将文字颜色设置成透明色 */
color: transparent;
-webkit-text-fill-color: transparent;
/* 将背景裁剪成文字前景色 */
background-clip: text;
-webkit-background-clip: text;
}
}
.cardContentBox {
flex: auto;
height: 1px;
}
.tableBox {
display: flex;
flex-direction: column;
padding-bottom: 15px;
.tableClass {
flex: auto;
width: 100%;
background-color: transparent;
.el-table__body tr:hover>td.el-table__cell {
background-color: rgba(10, 115, 255, 0.3);
}
}
.tableClass::before {
display: none;
}
}
}
}
.one {
width: calc((100% - 15px) * 0.2);
.itemCardBox {
background-image: none;
.cardHeader {
background-image: url(/tht_project/static/html/big_screen/image/card_header_left_background.png);
background-size: 100% 80%;
background-repeat: no-repeat;
background-position: center;
}
}
}
.two {
width: calc((100% - 15px) * 0.3);
}
.three {
width: calc((100% - 15px) * 0.3);
}
.four {
width: calc((100% - 15px) * 0.2);
.itemCardBox {
height: calc((100% - 15px) * 0.25);
.cardHeader {
background-image: url(/tht_project/static/html/big_screen/image/card_header_right_background.png);
background-size: 100% 80%;
background-repeat: no-repeat;
background-position: center;
text-align: right;
}
}
.itemCardBox:first-child {
flex: 1;
}
}
}
}
/* 多彩跑马灯效果 */
@keyframes rainbow-glow {
0% {
box-shadow: -10px 0 10px -10px #ff0000,
10px 0 10px -10px #ff0000,
inset -10px 0 10px -10px #ff0000,
inset 10px 0 10px -10px #ff0000;
border-left-color: #ff0000;
border-right-color: #ff0000;
}
16% {
box-shadow: -10px 0 10px -10px #ff9900,
10px 0 10px -10px #ff9900,
inset -10px 0 10px -10px #ff9900,
inset 10px 0 10px -10px #ff9900;
border-left-color: #ff9900;
border-right-color: #ff9900;
}
33% {
box-shadow: -10px 0 10px -10px #ffff00,
10px 0 10px -10px #ffff00,
inset -10px 0 10px -10px #ffff00,
inset 10px 0 10px -10px #ffff00;
border-left-color: #ffff00;
border-right-color: #ffff00;
}
50% {
box-shadow: -10px 0 10px -10px #33cc33,
10px 0 10px -10px #33cc33,
inset -10px 0 10px -10px #33cc33,
inset 10px 0 10px -10px #33cc33;
border-left-color: #33cc33;
border-right-color: #33cc33;
}
66% {
box-shadow: -10px 0 10px -10px #0099ff,
10px 0 10px -10px #0099ff,
inset -10px 0 10px -10px #0099ff,
inset 10px 0 10px -10px #0099ff;
border-left-color: #0099ff;
border-right-color: #0099ff;
}
83% {
box-shadow: -10px 0 10px -10px #6633ff,
10px 0 10px -10px #6633ff,
inset -10px 0 10px -10px #6633ff,
inset 10px 0 10px -10px #6633ff;
border-left-color: #6633ff;
border-right-color: #6633ff;
}
100% {
box-shadow: -10px 0 10px -10px #ff0066,
10px 0 10px -10px #ff0066,
inset -10px 0 10px -10px #ff0066,
inset 10px 0 10px -10px #ff0066;
border-left-color: #ff0066;
border-right-color: #ff0066;
}
}
/* 小点移动动画 */
@keyframes dot-move {
0% {
top: 30px;
opacity: 1;
}
10% {
opacity: 1;
}
40% {
opacity: 1;
}
50% {
opacity: 0;
top: calc(100% - 30px);
}
60% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
opacity: 0;
top: 30px;
}
}
</style>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html lang="zh-CN">
<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" />
<link rel="stylesheet" href="/tht_project/static/html/big_screen/element-ui/index.css" />
<script src="/tht_project/static/html/big_screen/js/echarts.min.js"></script>
<script src="/tht_project/static/html/big_screen/js/moment.min.js"></script>
<script src="/tht_project/static/html/big_screen/js/vue.js"></script>
<script src="/tht_project/static/html/big_screen/js/axios.min.js"></script>
<script src="/tht_project/static/html/big_screen/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="pageHeaderBox">
<div class="weatherInfo">
<span class="location">预料工序</span>
</div>
<img class="logo" src="/tht_project/static/html/big_screen/image/logo.png">
<div class="title">天合堂数据指挥中心可视化大屏</div>
<div class="dateTimeBox">
<i style="font-size: 30px;margin-right: 10px;" @click="toggleFullscreen"
:class="isFullscreen ? 'el-icon-close' : 'el-icon-full-screen'"></i>
<div class="time">[[currentTime]]</div>
<div class="dateBox">
<span>[[ currentDate ]]</span>
<span>[[ currentWeek ]]</span>
</div>
</div>
</div>
<div class="pageContentBox">
<div class="pageContentBackgroundBox">
<img class="badgeIcon" src="/tht_project/static/html/big_screen/image/777.png" alt="" v-for="item in 4"
:key="item">
</div>
<div class="pageContentArea">
<div class="contentColumnBox one">
<div class="itemCardBox">
<div class="cardHeader">
<span>今日产量</span>
</div>
<div class="cardContentBox">
<div id="proportionChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>今日投料情况</span>
</div>
<div class="cardContentBox">
<div id="mainMaterialChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>设备实时运转情况</span>
</div>
<div class="cardContentBox">
<div id="seasonMaterialChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>设备资源利用率</span>
</div>
<div class="cardContentBox">
<div id="rawMaterialChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
</div>
<div class="contentColumnBox two">
<div class="itemCardBox">
<div class="cardHeader">
<span>片冰机</span>
</div>
<div class="cardContentBox">
<div id="sliceIceMachineChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>解冻机组</span>
</div>
<div class="cardContentBox">
<div id="thawingUnitChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>切丁机组</span>
</div>
<div class="cardContentBox">
<div style="width: 100%; height: 100%; display: flex;">
<div id="dicingUnitLineChart" style="width: 65%; height: 100%;"></div>
<div id="dicingUnitGaugeChart" style="width: 35%; height: 100%;"></div>
</div>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>连续切块机</span>
</div>
<div class="cardContentBox">
<div style="width: 100%; height: 100%; display: flex;">
<div id="continuouSlabberLeftChart" style="width: 50%; height: 100%;"></div>
<div id="continuouSlabberRightChart" style="width: 50%; height: 100%;"></div>
</div>
</div>
</div>
<div class="itemCardBox" style="position: relative;">
<div class="cardHeader" style="position: absolute;top: 5px; width: 100%;">
<span>绞肉机</span>
</div>
<div class="cardContentBox" style="z-index: 1;">
<div id="meatGrinderChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
</div>
<div class="contentColumnBox three">
<div class="itemCardBox">
<div class="cardHeader">
<span>斩拌机</span>
</div>
<div class="cardContentBox">
<div id="ZBY550ChopperMixerChart" style="width: 100%; height: 100%;"></div>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>真空搅拌机</span>
</div>
<div class="cardContentBox">
<div style="width: 100%; height: 100%; display: flex;flex-wrap: wrap;">
<div id="vacuumMixerRotateSpeedChart" style="width: 50%; height: 50%;"></div>
<div id="vacuumMixerRunTimeChart" style="width: 50%; height: 50%;"></div>
<div id="vacuumMixerRemainTimeChart" style="width: 50%; height: 50%;"></div>
<div id="vacuumMixerCWVoltageChart" style="width: 50%; height: 50%;"></div>
</div>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>真空滚揉机</span>
</div>
<div class="cardContentBox">
<div style="width: 100%; height: 100%; display: flex;flex-wrap: wrap;">
<div id="vacuumTumblingRotateSpeedChart" style="width: 50%; height: 50%;"></div>
<div id="vacuumTumblingRunTimeChart" style="width: 50%; height: 50%;"></div>
<div id="vacuumTumblingRemainTimeChart" style="width: 50%; height: 50%;"></div>
</div>
</div>
</div>
</div>
<div class="contentColumnBox four">
<div class="itemCardBox">
<div class="cardHeader">
<span>报警中心</span>
</div>
<div class="cardContentBox tableBox">
<el-table class="tableClass" :data="alarmList" height="1" tooltip-effect="light"
:row-style="{height: '30px', backgroundColor: 'transparent'}"
:cell-style="{padding: '5px 0', borderBottom:'none'}"
:header-row-style="{ backgroundColor: 'transparent'}" :header-cell-style="{
backgroundColor: 'transparent',
color: '#01c4f9',
fontWeight: 'bold',
fontSzie:'14px',
height:'30px',
lineHeight:'30px',
padding: '5px 0',
borderBottom:'none'
}">
<el-table-column prop="device" label="报警类型">
<template slot-scope="scope">
<span style="color: #fff; font-size: 12px;">[[ scope.row.device ]]</span>
</template>
</el-table-column>
<el-table-column prop="time" label="报警时间" width="80">
<template slot-scope="scope">
<span style="color: #fff; font-size: 12px;">[[ scope.row.time ]]</span>
</template>
</el-table-column>
<el-table-column prop="detail" label="报警描述" show-overflow-tooltip>
<template slot-scope="scope">
<span style="color: #fff; font-size: 12px;">[[ scope.row.detail ]]</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div class="itemCardBox">
<div class="cardHeader">
<span>今日计划执行情况</span>
</div>
<div class="cardContentBox">
<div id="meatGrainAnalysisChart" style="width: 100%; height: 100%;"></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, // 窗口高度
isFullscreen: false, // 全屏状态
baseURL: "https://tht-test.newerp.rokedata.com", // 基地址,
dwsBaseUrl: "https://dws-platform.xbg.rokeris.com/dev-api", // dws系统基地址
loading: false, // 全局加载效果
currentTime: "", // 当前时间
currentDate: "", // 当前日期
currentWeek: "", // 当前周
timeInterval: null, // 时间定时器
dateInterval: null, // 日期定时器
charts: {}, // 图表
alarmList: [
{ device: "设备故障", time: "2025-4-23", detail: "切丁机电源了故障" },
{ device: "原料临期", time: "2025-4-22", detail: "酱油将于2025-5-1过期" },
{ device: "原料临期", time: "2025-4-21", detail: "酱油将于2025-5-1过期" },
{ device: "质检不合格", time: "2025-4-21", detail: "有猪肉1斤质检不合格" },
{ device: "质检不合格", time: "2025-4-21", detail: "有猪肉1斤质检不合格" },
{ device: "缺料", time: "2025-4-21", detail: "胡椒粉即将缺料" },
{ device: "缺料", time: "2025-4-21", detail: "牛肉即将缺料" },
{ device: "配比异常", time: "2025-4-23", detail: "淀粉配比不达标" },
{ device: "配比异常", time: "2025-4-21", detail: "淀粉配比不达标" },
{ device: "设备故障", time: "2025-4-21", detail: "切丁机电源了故障" },
{ device: "设备故障", time: "2025-4-21", detail: "切丁机电源了故障" },
{ device: "设备故障", time: "2025-4-21", detail: "切丁机电源了故障" },
{ device: "设备故障", time: "2025-4-21", detail: "切丁机电源了故障" },
{ device: "设备故障", time: "2025-4-21", detail: "切丁机电源了故障" },
{ device: "设备故障", time: "2025-4-21", detail: "切丁机电源了故障" },
],
}
},
created() {
// 添加resize事件监听
window.addEventListener("resize", this.handleResize)
// 更新日期、时间、周
this.updateDateTime()
// 每秒更新时间
this.timeInterval = setInterval(this.updateTime, 1000)
// 每分钟检查一次日期是否需要更新(每天只需更新一次)
this.dateInterval = setInterval(() => {
const now = new Date()
if (now.getHours() === 0 && now.getMinutes() === 0) {
this.updateDate()
this.updateWeek()
}
}, 60000)
},
mounted() {
this.$nextTick(() => {
document.getElementById("bodyId").style.display = "block"
this.initAllCharts()
})
},
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
},
// 处理窗口大小变化修改图表大小
handleResize() {
for (let key in this.charts) {
this.charts[key] && this.charts[key].resize()
}
},
// 更新日期、时间、周
updateDateTime() {
this.updateTime()
this.updateDate()
this.updateWeek()
},
// 更新时间
updateTime() {
this.currentTime = moment().format('HH:mm:ss')
},
// 更新日期
updateDate() {
this.currentDate = moment().format('YYYY-MM-DD')
},
// 更新周
updateWeek() {
this.currentWeek = '周' + ['日', '一', '二', '三', '四', '五', '六'][moment().day()]
},
// 接口请求方法封装
requestApi(
url,
config = {},
errorMessage = "操作失败,请稍后重试",
contentType = "application/json"
) {
return new Promise((resolve, reject) => {
if (!url) reject(null)
axios({
method: "POST",
url: this.baseURL + url,
data: config,
headers: { "Content-Type": contentType },
}).then((result) => {
if (
result?.data?.result?.code == 0 ||
result?.data?.result?.state == "success" ||
result?.data?.code == 0
) {
resolve(result.data)
} else if (result?.data?.result?.code == 1) {
reject(result.data.result.message)
} else if (result?.data?.result?.state == "error") {
reject(result.data.result.megs)
} else if (result?.data?.code == 0) {
reject(result.data.message)
} else if (result?.data?.error) {
reject(result.data.error.message)
}
}).catch((error) => {
reject(errorMessage)
})
})
},
requestSelfApi(
url,
config = {},
errorMessage = "操作失败,请稍后重试",
contentType = "application/json",
baseurl = ""
) {
return new Promise((resolve, reject) => {
if (!url) reject(null)
axios({
method: "POST",
url: baseurl + url,
data: config,
headers: { "Content-Type": contentType },
}).then((result) => {
if (
result?.data?.result?.success ||
result?.data?.result?.code == 0 ||
result?.data?.result?.state == "success" ||
result?.data?.code == 0
) {
resolve(result.data)
} else if (result?.data?.result?.code == 1) {
reject(result.data.result.message)
} else if (result?.data?.result?.state == "error") {
reject(result.data.result.megs)
} else if (result?.data?.code == 1) {
reject(result.data.message)
} else if (!result?.data?.result?.success) {
reject(result.data.result.msg)
} else if (result?.data?.error) {
reject(result.data.error.message)
}
}).catch((error) => {
reject(errorMessage)
})
})
},
// 接口请求Dws系统方法封装
requestDwsApi(
url,
config = {},
errorMessage = "操作失败,请稍后重试",
contentType = "application/json"
) {
return new Promise((resolve, reject) => {
if (!url) reject(null)
axios({
method: "POST",
url: this.dwsBaseUrl + url,
data: config,
headers: { "Content-Type": contentType },
}).then((result) => {
if (result?.data?.success) {
resolve(result.data)
} else if (!result.data.success) {
reject(result.data.msg)
} else {
reject(errorMessage)
}
}).catch((error) => {
reject(errorMessage)
})
})
},
// 初始化所有图表
async initAllCharts() {
this.loading = true
let proportionData = [] // 今日产量数据
let mainMaterialData = [] // 今日投料情况
let seasonMaterialData = {} // 设备实时运转情况
let rawMaterialData = [] // 设备资源利用率
let thawingUnitData = [] // 解冻机数据
let continuouSlabberData = [] // 连续切块机
let meatGrainAnalysisData = [] // 今日计划执行情况
// 查询今日产量数据
await this.requestApi('/roke/big_screen_kanban/today_production', {
"process_code": "001",
"today": "2025-03-28",
}).then(result => {
proportionData = result.result.data
}).catch(error => {
console.error(error)
})
// 查询今日投料情况
await this.requestApi('/roke/big_screen_kanban/today_material_input', {
"process_code": "001",
"today": "2025-03-28",
}).then(result => {
mainMaterialData = result.result.data
}).catch(error => {
console.error(error)
})
// 查询设备实时运转情况
await this.requestSelfApi('/big_screen_count', {
"plant_name": "预料车间",
"category_name": '',
"today": ''
}).then(result => {
seasonMaterialData = result.result.data
}).catch(error => {
console.error(error)
})
// 查询设备资源利用率
await this.requestSelfApi('/big_screen_today', {
"plant_name": "预料车间",
"category_name": '',
"today": ''
}).then(result => {
rawMaterialData = result.result.data
}).catch(error => {
console.error(error)
})
// 查询解冻机
await this.requestSelfApi('/get_jiedongji_temperature', {
"device_code_list": ['868892078264059'],
"category_name": '',
"today": '',
'plant_name': '',
}).then(result => {
thawingUnitData = result.result.data
}).catch(error => {
console.error(error)
})
// 查询连续切块机
await this.requestSelfApi('/get_qiekuaiji_data', {
"device_code_list": ['868892078346237'],
"category_name": '',
"today": '',
'plant_name': '',
}).then(result => {
continuouSlabberData = result.result.data
}).catch(error => {
console.error(error)
})
// 查询今日计划执行情况
await this.requestApi('/roke/big_screen_kanban/today_production_summary', {
"order_department_code": "001",
}).then(result => {
meatGrainAnalysisData = result.result.data
}).catch(error => {
console.error(error)
})
setTimeout(() => {
this.initProportionChart(proportionData)
this.initMainMaterialChart(mainMaterialData)
this.initSeasonMaterialChart(seasonMaterialData)
this.initRawMaterialChart(rawMaterialData)
this.initSliceIceMachineChart()
this.initThawingUnitChart(thawingUnitData)
this.initDicingUnitChart()
this.initContinuouSlabberChart(continuouSlabberData)
this.initMeatGrinderChart()
this.initZBY550ChopperMixerChart()
this.initVacuumMixerChart()
this.initVacuumTumblingChart()
this.initMeatGrainAnalysisChart(meatGrainAnalysisData)
}, 1)
this.loading = false
},
// 今日产量
initProportionChart(data) {
const chart = echarts.init(document.getElementById('proportionChart'))
this.charts.proportion = chart
// x轴数据
const xAxisData = []
// 数量
const seriesData = []
data.forEach(item => {
xAxisData.push(item.product_name)
seriesData.push({
value: item.finish_qty,
unit: item.uom_name
})
})
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = this.calculateMaxValue(seriesData, 1.5)
chart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: (params) => {
console.log(params)
let tooltipHtml = `<div style="font-weight:bold;margin-bottom:5px">${params[0].name}</div>`
params.forEach(param => {
const dataIndex = param.dataIndex
const unit = seriesData[dataIndex].unit
// 使用系列颜色作为小圆点颜色
tooltipHtml += `
<div style="display:flex;align-items:center;margin:5px 0">
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${param.color};margin-right:10px"></span>
<strong>${param.value}</strong>
<span>${unit}</span>
</div>
`
})
return tooltipHtml
}
},
dataZoom: [{
id: 'proportionChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (2 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#0a73ff' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3 } },
axisLabel: { color: '#fff', fontSize: 10 }
},
series: [{
type: 'bar',
barWidth: '40%',
barGap: '0%',
data: seriesData,
itemStyle: { color: '#5087ec' },
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + params.data.unit
}
}]
})
},
// 今日投料情况
initMainMaterialChart(data) {
const chart = echarts.init(document.getElementById('mainMaterialChart'))
this.charts.mainMaterial = chart
// x轴数据
const xAxisData = []
// 数据
const seriesData = []
// value数据
const seriesValues = []
data.forEach(item => {
xAxisData.push(item.product_name)
seriesData.push({
value: item.qty,
unit: item.unit
})
seriesValues.push(item.qty)
})
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = [...seriesValues].length ? Math.max(...[...seriesValues]) * 1.5 : 100
chart.setOption({
grid: {
left: '5%', right: '10%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
formatter: (params) => {
return params[0].marker + params[0].name + ': ' + params[0].data.value + params[0].data.unit
}
},
dataZoom: [{
id: 'mainMaterialChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (2 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxisData,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
axisTick: { show: false },
axisLabel: { color: '#fff', fontSize: 10, interval: 0 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3, type: 'dashed' } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '{value}' }
},
series: [{
type: 'line',
data: seriesData,
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.data.value + params.data.unit
}
}]
})
},
// 设备实时运转情况
initSeasonMaterialChart(data) {
const chart = echarts.init(document.getElementById('seasonMaterialChart'))
this.charts.seasonMaterial = chart
chart.setOption({
tooltip: {
trigger: 'item',
valueFormatter: (value) => value + '台',
},
legend: {
orient: 'vertical',
left: 'top',
itemWidth: 20,
itemHeight: 10,
itemGap: 5,
textStyle: {
color: "#fff",
fontSize: 10
}
},
series: [{
type: 'pie',
radius: '60%',
center: ['60%', '60%'],
itemStyle: {
borderRadius: 0,
borderColor: '#061058',
borderWidth: 2
},
label: {
show: true,
formatter: '{c}台',
color: '#fff',
fontSize: 12
},
data: [{
value: data?.green || 0,
name: '运行',
itemStyle: { color: '#91cc75' },
}, {
value: data?.yellow || 0,
name: '等待',
itemStyle: { color: '#ffd700' },
}, {
value: data?.red || 0,
name: '故障',
itemStyle: { color: '#ee6666' },
}, {
value: data?.gray || 0,
name: '关机',
itemStyle: { color: '#787878' },
}]
}]
})
},
// 设备资源利用率
initRawMaterialChart(data) {
const chart = echarts.init(document.getElementById('rawMaterialChart'))
this.charts.rawMaterial = chart
// x轴数据
const xAxisData = []
// 数量
const seriesData = []
data.forEach(item => {
if (item.category) {
xAxisData.push(item.category)
seriesData.push(item.avg_utilization)
}
})
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = [...seriesData].length ? Math.max(...[...seriesData]) * 1.5 : 100
chart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
valueFormatter: (value) => value + '%',
},
dataZoom: [{
id: 'rawMaterialChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (2 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#0a73ff' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3 } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '{value}%' }
},
series: [{
type: 'bar',
barWidth: '40%',
barGap: '0%',
data: seriesData,
itemStyle: { color: '#5087ec' },
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + '%'
}
}]
})
},
// 片冰机
initSliceIceMachineChart() {
const chart = echarts.init(document.getElementById('sliceIceMachineChart'))
this.charts.sliceIceMachine = chart
// x轴数据
const xAxisData = ['8点', '9点', '10点', '11点']
// 数量
const seriesData = [45, 60, 80, 10]
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = [...seriesData].length ? Math.max(...[...seriesData]) * 1.5 : 100
chart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
valueFormatter: (value) => value + '℃',
},
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#0a73ff' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3 } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '{value}℃' }
},
series: [{
type: 'bar',
barWidth: '40%',
barGap: '0%',
data: seriesData,
itemStyle: { color: '#5087ec' },
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + '℃'
}
}]
})
},
// 解冻机组
initThawingUnitChart(data) {
const chart = echarts.init(document.getElementById('thawingUnitChart'))
this.charts.thawingUnit = chart
// 数值
const valueData = {
current_cune_temp: data[0].current_temperature || 0, // 当前库内温度
center_temp: data[0].center_temperature || 0, // 中心温度
surface_temp: data[0].surface_temperature || 0, // 表面温度
cune_humidity: data[0].humidity || 0, // 库内湿度
}
// series配置
const seriesData = []
for (let i = 0; i < 3; i++) {
let data = {
name: '',
type: 'gauge',
center: [],
radius: '60%',
startAngle: 200,
endAngle: -20,
splitNumber: 5,
itemStyle: { color: '#3a8fff' },
progress: { show: true, width: 5 },
pointer: { show: false },
axisLine: { lineStyle: { width: 5 } },
axisTick: {
distance: -15,
length: 5,
splitNumber: 5,
lineStyle: { width: 1, color: '#999' }
},
splitLine: {
distance: -15,
length: 8,
lineStyle: { width: 1, color: '#999' }
},
axisLabel: { distance: -15, color: '#999', fontSize: 10, },
detail: {
valueAnimation: true,
offsetCenter: [0, 0],
fontSize: 10,
formatter: '{value}℃',
color: 'inherit'
},
data: [0]
}
if (i == 0) {
data.name = '当前库内温度'
data.center = ['20%', '60%']
data.data = [valueData.current_cune_temp || 0]
} else if (i == 1) {
data.name = '中心温度'
data.center = ['50%', '60%']
data.data = [valueData.center_temp || 0]
} else if (i == 2) {
data.name = '库内湿度'
data.center = ['80%', '60%']
data.data = [valueData.cune_humidity || 0]
data.detail.formatter = '{value}%'
}
seriesData.push(data)
}
chart.setOption({
grid: { left: '5%', right: '5%', top: '8%', bottom: '5%' },
graphic: [{
type: 'text',
left: '12%',
top: '80%',
style: { text: '当前库内温度', fill: '#fff', fontSize: 10 }
}, {
type: 'text',
left: '45.5%',
top: '80%',
style: { text: '中心温度', fill: '#fff', fontSize: 10 }
}, {
type: 'text',
left: '75.5%',
top: '80%',
style: { text: '库内湿度', fill: '#fff', fontSize: 10 }
}],
series: seriesData
})
},
// 切丁机组
initDicingUnitChart() {
const lineChart = echarts.init(document.getElementById('dicingUnitLineChart'))
const gaugeChart = echarts.init(document.getElementById('dicingUnitGaugeChart'))
this.charts.dicingUnitLine = lineChart
this.charts.dicingUnitGauge = gaugeChart
// 折线x轴数据
const xAxisData = ['8点', '9点', '10点', '11点', '12点', '13点']
// 折线数量
const seriesData = [100, 140, 230, 50, 150, 180]
// 折线单位
const unit = 'Hz'
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = [...seriesData].length ? Math.max(...[...seriesData]) * 1.5 : 100
lineChart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
valueFormatter: (value) => value + unit
},
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxisData,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
axisTick: { show: false },
axisLabel: { color: '#fff', fontSize: 10, interval: 0 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3, type: 'dashed' } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '{value}' + unit }
},
series: [{
type: 'line',
data: seriesData,
smooth: true,
lineStyle: { type: 'dashed' },
label: {
show: true,
position: 'top',
color: '#fff',
fontSize: 10,
formatter: (params) => params.value + unit
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(58, 143, 255, 0.8)' },
{ offset: 1, color: 'rgba(58, 143, 255, 0.1)' }
])
},
}]
})
gaugeChart.setOption({
series: [{
type: 'gauge',
radius: '100%',
center: ['50%', '50%'],
startAngle: 200,
endAngle: -20,
splitNumber: 5,
itemStyle: { color: '#3a8fff' },
progress: { show: false, width: 5 },
pointer: { itemStyle: { color: '#fe827a', }, length: "60%", width: 3 },
axisLine: {
roundCap: true,
lineStyle: {
width: 5,
color: [[1, {
type: 'linear', x: 0, y: 0, x2: 1, y2: 0,
colorStops: [{ offset: 0, color: '#91cc75' }, { offset: 1, color: '#ff4b4b' }]
}]]
}
},
axisTick: { show: false },
splitLine: { show: false },
axisLabel: { show: false },
detail: {
valueAnimation: true,
width: '40%',
formatter: '{value} s',
color: '#fff',
fontSize: 10,
fontWeight: 'bold',
offsetCenter: [0, '40%']
},
data: [{ value: 50, name: "加速时间" }],
title: { color: "#FFF", fontSize: 10, valueAnimation: false, offsetCenter: ['0', '70%'] }
}]
})
},
// 连续切块机
initContinuouSlabberChart(data) {
const leftChart = echarts.init(document.getElementById('continuouSlabberLeftChart'))
const rightChart = echarts.init(document.getElementById('continuouSlabberRightChart'))
this.charts.continuouSlabberLeft = leftChart
this.charts.continuouSlabberRight = rightChart
// x轴数据
const xAxisData = []
// 运行频率
const runFrequencyData = []
// 切割速度
const cuttingSpeedData = []
data.forEach(item => {
xAxisData.push(item.time)
runFrequencyData.push(item.yunxingpinlv)
cuttingSpeedData.push(item.susongsudu)
})
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const runFrequencyMax = runFrequencyData.length ? Math.max(...runFrequencyData) * 1.5 : 100
const cuttingSpeedMax = runFrequencyData.length ? Math.max(...runFrequencyData) * 1.5 : 100
leftChart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
valueFormatter: (value) => value + 'Hz',
},
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#0a73ff' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: runFrequencyMax,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3 } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '{value}Hz' }
},
series: [{
type: 'bar',
barWidth: '40%',
barGap: '0%',
data: runFrequencyData,
itemStyle: { color: '#5087ec' },
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + 'Hz'
}
}]
})
rightChart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
valueFormatter: (value) => value + 'ppm',
},
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#0a73ff' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
position: 'right',
splitNumber: 4,
min: 0,
max: cuttingSpeedMax,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3 } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '{value}ppm' }
},
series: [{
type: 'bar',
barWidth: '40%',
barGap: '0%',
data: cuttingSpeedData,
itemStyle: { color: '#5087ec' },
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + 'ppm'
}
}]
})
},
// 绞肉机
initMeatGrinderChart() {
const chart = echarts.init(document.getElementById('meatGrinderChart'))
this.charts.meatGrinder = chart
// 折线x轴数据
const xAxisData = ['8点', '9点', '10点', '11点']
// 折线数量
const deviceA = [230, 100, 230, 50]
const deviceB = [50, 230, 140, 170]
// 折线单位
const unit = 'ppm'
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = [...deviceA, ...deviceB].length ? Math.max(...[...deviceA, ...deviceB]) * 1.5 : 100
chart.setOption({
grid: {
left: '5%', right: '5%', top: '26%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
valueFormatter: (value) => value + unit
},
legend: {
data: ["设备A", "设备B"],
top: '0%',
itemWidth: 10,
itemHeight: 10,
textStyle: { color: "#fff", fontSize: 10 }
},
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxisData,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
axisTick: { show: false },
axisLabel: { color: '#fff', fontSize: 10, interval: 0 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3, type: 'dashed' } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '{value}' + unit }
},
series: [{
name: '设备A',
type: 'line',
data: deviceA,
smooth: true,
lineStyle: { type: 'dashed' },
label: {
show: true,
position: 'top',
color: '#fff',
fontSize: 10,
formatter: (params) => params.value + unit
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(58, 143, 255, 0.8)' },
{ offset: 1, color: 'rgba(58, 143, 255, 0.1)' }
])
},
}, {
name: '设备B',
type: 'line',
data: deviceB,
smooth: true,
lineStyle: { type: 'dashed' },
label: {
show: true,
position: 'top',
color: '#fff',
fontSize: 10,
formatter: (params) => params.value + unit
},
}]
})
},
// 斩拌机
initZBY550ChopperMixerChart() {
const chart = echarts.init(document.getElementById('ZBY550ChopperMixerChart'))
this.charts.ZBY550ChopperMixer = chart
// x轴数据
const xAxisData = ['设备1', '设备2', '设备3', '设备4']
// 数量
const seriesData = [100, 140, 230, 50]
// 单位
const unit = 'ppm'
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = [...seriesData].length ? Math.max(...[...seriesData]) * 1.5 : 100
chart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
valueFormatter: (value) => value + unit
},
dataZoom: [{
id: 'ZBY550ChopperMixerChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxisData,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
axisTick: { show: false },
axisLabel: { color: '#fff', fontSize: 10, interval: 0 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3, type: 'dashed' } },
axisLabel: { color: '#ccc', fontSize: 10, formatter: '{value}' + unit }
},
series: [{
type: 'line',
data: seriesData,
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + unit
}
}]
})
},
// 真空搅拌机
initVacuumMixerChart() {
const leftTopChart = echarts.init(document.getElementById('vacuumMixerRotateSpeedChart'))
const rightTopChart = echarts.init(document.getElementById('vacuumMixerRunTimeChart'))
const leftBottomChart = echarts.init(document.getElementById('vacuumMixerRemainTimeChart'))
const rightBottomChart = echarts.init(document.getElementById('vacuumMixerCWVoltageChart'))
this.charts.vacuumMixerRotateSpeed = leftTopChart
this.charts.vacuumMixerRunTime = rightTopChart
this.charts.vacuumMixerRemainTime = leftBottomChart
this.charts.vacuumMixerCWVoltage = rightBottomChart
// x轴数据
const xAxisData = ['搅拌机1', '搅拌机2', '搅拌机3', '搅拌机4']
// 数量
const seriesData = [45, 60, 80, 20]
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = [...seriesData].length ? Math.max(...[...seriesData]) * 1.5 : 100
leftTopChart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
valueFormatter: (value) => value + 'ppm',
},
dataZoom: [{
id: 'vacuumMixerRotateSpeedChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#0a73ff' } },
axisLabel: { color: '#fff', fontSize: 10, interval: 0 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3 } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '{value}ppm' }
},
series: [{
type: 'bar',
barWidth: '40%',
barGap: '0%',
data: seriesData,
itemStyle: { color: '#5087ec' },
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + 'ppm'
}
}]
})
rightTopChart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
valueFormatter: (value) => value + 'min'
},
dataZoom: [{
id: 'vacuumMixerRunTimeChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxisData,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
axisTick: { show: false },
axisLabel: { color: '#fff', fontSize: 10, interval: 0 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3, type: 'dashed' } },
axisLabel: { color: '#ccc', fontSize: 10, formatter: '{value}' + 'min' }
},
series: [{
type: 'line',
data: seriesData,
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + 'min'
}
}]
})
leftBottomChart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '10%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
valueFormatter: (value) => value + 'min'
},
dataZoom: [{
id: 'vacuumMixerRunTimeChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxisData,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
axisTick: { show: false },
axisLabel: { color: '#fff', fontSize: 10, interval: 0 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3, type: 'dashed' } },
axisLabel: { color: '#ccc', fontSize: 10, formatter: '{value}' + 'min' }
},
series: [{
type: 'line',
data: seriesData,
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + 'min'
}
}]
})
rightBottomChart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '10%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
valueFormatter: (value) => value + 'kpa'
},
dataZoom: [{
id: 'vacuumMixerRunTimeChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxisData,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
axisTick: { show: false },
axisLabel: { color: '#fff', fontSize: 10, interval: 0 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3, type: 'dashed' } },
axisLabel: { color: '#ccc', fontSize: 10, formatter: '{value}' + 'kpa' }
},
series: [{
type: 'line',
data: seriesData,
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + 'kpa'
}
}]
})
},
// 真空滚揉机
initVacuumTumblingChart() {
const leftTopChart = echarts.init(document.getElementById('vacuumTumblingRotateSpeedChart'))
const rightTopChart = echarts.init(document.getElementById('vacuumTumblingRunTimeChart'))
const leftBottomChart = echarts.init(document.getElementById('vacuumTumblingRemainTimeChart'))
this.charts.vacuumTumblingRotateSpeed = leftTopChart
this.charts.vacuumTumblingRunTime = rightTopChart
this.charts.vacuumTumblingRemainTime = leftBottomChart
// x轴数据
const xAxisData = ['滚揉机1', '滚揉机2', '滚揉机3', '滚揉机4']
// 数量
const seriesData = [45, 60, 80, 20]
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = [...seriesData].length ? Math.max(...[...seriesData]) * 1.5 : 100
leftTopChart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
valueFormatter: (value) => value + 'ppm',
},
dataZoom: [{
id: 'vacuumMixerRotateSpeedChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#0a73ff' } },
axisLabel: { color: '#fff', fontSize: 10, interval: 0 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3 } },
axisLabel: { color: '#fff', fontSize: 10, formatter: '{value}ppm' }
},
series: [{
type: 'bar',
barWidth: '40%',
barGap: '0%',
data: seriesData,
itemStyle: { color: '#5087ec' },
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + 'ppm'
}
}]
})
rightTopChart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
valueFormatter: (value) => value + 'min'
},
dataZoom: [{
id: 'vacuumMixerRunTimeChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxisData,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
axisTick: { show: false },
axisLabel: { color: '#fff', fontSize: 10, interval: 0 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3, type: 'dashed' } },
axisLabel: { color: '#ccc', fontSize: 10, formatter: '{value}' + 'min' }
},
series: [{
type: 'line',
data: seriesData,
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + 'min'
}
}]
})
leftBottomChart.setOption({
grid: {
left: '5%', right: '5%', top: '8%', bottom: '10%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
tooltip: {
trigger: 'axis',
valueFormatter: (value) => value + 'min'
},
dataZoom: [{
id: 'vacuumMixerRunTimeChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (3 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxisData,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
axisTick: { show: false },
axisLabel: { color: '#fff', fontSize: 10, interval: 0 }
},
yAxis: {
type: 'value',
splitNumber: 4,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3, type: 'dashed' } },
axisLabel: { color: '#ccc', fontSize: 10, formatter: '{value}' + 'min' }
},
series: [{
type: 'line',
data: seriesData,
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + 'min'
}
}]
})
},
// 今日计划执行情况
initMeatGrainAnalysisChart(data) {
const chart = echarts.init(document.getElementById('meatGrainAnalysisChart'))
this.charts.meatGrainAnalysis = chart
// x轴数据
const xAxisData = []
// 投产数量
const operationData = []
// 完成数量
const quantityData = []
data.forEach(item => {
xAxisData.push(item.product_name)
operationData.push({
value: item.produce_qty || 0,
unit: item.uom_name
})
quantityData.push({
value: item.finish_qty || 0,
unit: item.uom_name
})
})
// 设置最大值处理显示数值时和顶部重叠问题; 三方表达式:防止无数据时不显示y轴
const max = this.calculateMaxValue([operationData, quantityData], 1.5)
chart.setOption({
grid: {
left: '5%', right: '5%', top: '15%', bottom: '5%',
containLabel: true,
show: true,
borderWidth: 0,
backgroundColor: 'rgba(0, 0, 0, 0)',
shadowColor: 'rgba(0, 0, 0, 0.3)',
shadowBlur: 2
},
legend: {
top: '-2%',
right: '5%',
textStyle: { color: '#fff', fontSize: 10 },
itemWidth: 20,
itemHeight: 10,
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: (params) => {
let tooltipHtml = `<div style="font-weight:bold;margin-bottom:5px">${params[0].name}</div>`
params.forEach(param => {
const dataIndex = param.dataIndex
const unit = param.seriesIndex === 0 ? operationData[dataIndex].unit : quantityData[dataIndex].unit
// 使用系列颜色作为小圆点颜色
tooltipHtml += `
<div style="display:flex;align-items:center;margin:5px 0">
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${param.color};margin-right:10px"></span>
<span style="margin-right:10px">${param.seriesName}:</span>
<strong>${param.value}</strong>
<span>${unit}</span>
</div>
`
})
// 计算完成率
const total = params[0].value
const completed = params[1].value
const percentage = total > 0 ? ((completed / total) * 100).toFixed(1) : 0
tooltipHtml += `
<div style="margin-top:10px;border-top:1px solid #eee;padding-top:5px">
完成率: <strong>${percentage}%</strong>
</div>
`
return tooltipHtml
}
},
dataZoom: [{
id: 'meatGrainAnalysisChart',
type: 'inside', // 表明在图表内部进行拖动
start: 0,
end: (2 / xAxisData.length) * 100, // 默认显示3条
filterMode: 'none'
}],
xAxis: {
type: 'category',
data: xAxisData,
axisLine: { lineStyle: { color: '#0a73ff' } },
axisLabel: { color: '#fff', fontSize: 10 }
},
yAxis: {
type: 'value',
splitNumber: 5,
min: 0,
max: max,
axisLine: { show: true, lineStyle: { color: '#0a73ff' } },
splitLine: { lineStyle: { color: '#0a73ff', width: 0.5, opacity: 0.3 } },
axisLabel: { color: '#fff', fontSize: 10 }
},
series: [{
name: '投产数量',
type: 'bar',
barWidth: '40%',
barGap: '0%',
data: operationData,
itemStyle: { color: '#5087ec' },
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + params.data.unit
}
}, {
name: '完成数量',
type: 'bar',
barWidth: '40%',
barGap: '0%',
data: quantityData,
itemStyle: { color: '#68bbc4' },
label: {
show: true,
position: 'top',
color: '#FFF',
fontSize: 10,
formatter: (params) => params.value + params.data.unit
}
}]
})
},
/**
* 计算图表Y轴最大值
* @param {Array} dataArrays 需要合并计算的数据数组
* @param {number} multiplier 放大系数,默认1.2
* @param {number} defaultValue 默认最大值,当无数据时使用
* @returns {number} 计算后的最大值
*/
calculateMaxValue(dataArrays, multiplier = 1.2, defaultValue = 100) {
// 合并所有数组并提取value值
const allValues = dataArrays.flat().map(item => item.value);
// 如果没有数据,返回默认值
if (allValues.length === 0) return defaultValue;
// 计算最大值并应用放大系数,然后向上取整
const maxValue = Math.max(...allValues);
return Math.ceil(maxValue * multiplier);
}
},
beforeDestroy() {
// 清除时间定时器
clearInterval(this.timeInterval)
// 清除日期定时器
clearInterval(this.dateInterval)
// 清除resize事件监听
window.removeEventListener("resize", this.handleResize)
}
})
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
::-webkit-scrollbar {
width: 0px;
height: 0px;
}
::-webkit-scrollbar-thumb {
background: #c0c4cc;
border-radius: 3px;
}
::-webkit-scrollbar-track {
background: #f5f7fa;
}
#app {
width: 100vw;
height: 100vh;
overflow: hidden;
color: #fff;
font-family: "Microsoft YaHei", sans-serif;
background-color: #06114f;
display: flex;
flex-direction: column;
}
.pageHeaderBox {
height: 60px;
display: flex;
justify-content: center;
align-items: center;
padding: 0 20px;
border-bottom: 1px solid rgba(10, 115, 255, 0.3);
background-image: url(/tht_project/static/html/big_screen/image/header_background.png);
background-size: 100%;
position: relative;
font-size: 24px;
z-index: 2;
.weatherInfo {
position: absolute;
left: 20px;
bottom: 5px;
font-size: 23px;
display: flex;
align-items: baseline;
gap: 5px;
.location {
font-weight: bold;
}
.icon {
margin-top: 5px;
}
}
.logo {
width: 1.3em;
height: 1.3em;
}
.title {
font-weight: bold;
letter-spacing: 4px;
margin-left: 10px;
}
.dateTimeBox {
position: absolute;
right: 20px;
bottom: 5px;
display: flex;
align-items: center;
font-size: 24px;
.time {
margin-right: 5px;
}
.dateBox {
display: flex;
flex-direction: column;
font-size: 10px;
}
}
}
.pageContentBox {
width: 100%;
height: 1px;
flex: auto;
position: relative;
padding: 10px;
.pageContentBackgroundBox {
position: absolute;
width: calc(100% - 20px);
height: calc(100% - 20px);
padding: 10px;
border-left: 2px solid #cbdbf8;
border-right: 2px solid #cbdbf8;
background: transparent;
border-radius: 10px;
.badgeIcon {
position: absolute;
background-color: #06114f;
}
.badgeIcon:first-child {
top: 0;
left: 0;
transform: translate(-12%, -35%);
}
.badgeIcon:nth-child(2) {
top: 0;
right: 0;
transform: translate(12%, -35%) scaleX(-1);
}
.badgeIcon:nth-child(3) {
bottom: 0;
left: 0;
transform: translate(-12%, 35%) scaley(-1);
}
.badgeIcon:last-child {
bottom: 0;
right: 0;
transform: translate(12%, 35%) scale(-1);
}
}
/* 发光小点 */
.pageContentBackgroundBox::before,
.pageContentBackgroundBox::after {
content: '';
position: absolute;
top: 30px;
transform: translateX(-30%);
width: 0px;
height: 10px;
background: #fff;
border-radius: 50%;
box-shadow: 0 0 15px 3px rgba(255, 255, 255, 0.8),
0 0 15px 3px rgba(255, 255, 255, 0.8);
animation: dot-move 4s linear infinite;
z-index: 2;
}
.pageContentBackgroundBox::before {
/* 定位在左边框 */
left: -1px;
}
/* 发光小点 */
.pageContentBackgroundBox::after {
/* 定位在右边框 */
right: -1px;
}
.pageContentArea {
display: flex;
gap: 5px;
padding: 10px;
height: 100%;
.contentColumnBox {
height: 100%;
display: flex;
flex-direction: column;
gap: 5px;
z-index: 1;
.itemCardBox {
height: calc((100% - 15px) / 4);
background-image: url(/tht_project/static/html/big_screen/image/group_background.png);
background-size: 100% 100%;
padding: 5px 8px 0px;
display: flex;
flex-direction: column;
.cardHeader {
height: 30px;
span {
padding: 0 8px;
font-size: 14px;
font-weight: bold;
background: linear-gradient(180deg, #FFF 10%, #00bbf3 100%);
/* 将文字颜色设置成透明色 */
color: transparent;
-webkit-text-fill-color: transparent;
/* 将背景裁剪成文字前景色 */
background-clip: text;
-webkit-background-clip: text;
}
}
.cardContentBox {
flex: auto;
height: 1px;
}
.tableBox {
display: flex;
flex-direction: column;
padding-bottom: 15px;
.tableClass {
flex: auto;
width: 100%;
background-color: transparent;
.el-table__body tr:hover>td.el-table__cell {
background-color: rgba(10, 115, 255, 0.3);
}
}
.tableClass::before {
display: none;
}
}
}
}
.one {
width: calc((100% - 15px) * 0.2);
.itemCardBox {
background-image: none;
.cardHeader {
background-image: url(/tht_project/static/html/big_screen/image/card_header_left_background.png);
background-size: 100% 80%;
background-repeat: no-repeat;
background-position: center;
}
}
}
.two {
width: calc((100% - 15px) * 0.3);
.itemCardBox {
height: calc((100% - 20px) / 5);
}
}
.three {
width: calc((100% - 15px) * 0.3);
.itemCardBox {
height: calc((100% - 10px) * 0.4);
}
.itemCardBox:first-child {
height: calc((100% - 10px) * 0.2);
}
}
.four {
width: calc((100% - 15px) * 0.2);
.itemCardBox {
height: calc((100% - 15px) * 0.25);
.cardHeader {
background-image: url(/tht_project/static/html/big_screen/image/card_header_right_background.png);
background-size: 100% 80%;
background-repeat: no-repeat;
background-position: center;
text-align: right;
}
}
.itemCardBox:first-child {
flex: 1;
}
}
}
}
/* 多彩跑马灯效果 */
@keyframes rainbow-glow {
0% {
box-shadow: -10px 0 10px -10px #ff0000,
10px 0 10px -10px #ff0000,
inset -10px 0 10px -10px #ff0000,
inset 10px 0 10px -10px #ff0000;
border-left-color: #ff0000;
border-right-color: #ff0000;
}
16% {
box-shadow: -10px 0 10px -10px #ff9900,
10px 0 10px -10px #ff9900,
inset -10px 0 10px -10px #ff9900,
inset 10px 0 10px -10px #ff9900;
border-left-color: #ff9900;
border-right-color: #ff9900;
}
33% {
box-shadow: -10px 0 10px -10px #ffff00,
10px 0 10px -10px #ffff00,
inset -10px 0 10px -10px #ffff00,
inset 10px 0 10px -10px #ffff00;
border-left-color: #ffff00;
border-right-color: #ffff00;
}
50% {
box-shadow: -10px 0 10px -10px #33cc33,
10px 0 10px -10px #33cc33,
inset -10px 0 10px -10px #33cc33,
inset 10px 0 10px -10px #33cc33;
border-left-color: #33cc33;
border-right-color: #33cc33;
}
66% {
box-shadow: -10px 0 10px -10px #0099ff,
10px 0 10px -10px #0099ff,
inset -10px 0 10px -10px #0099ff,
inset 10px 0 10px -10px #0099ff;
border-left-color: #0099ff;
border-right-color: #0099ff;
}
83% {
box-shadow: -10px 0 10px -10px #6633ff,
10px 0 10px -10px #6633ff,
inset -10px 0 10px -10px #6633ff,
inset 10px 0 10px -10px #6633ff;
border-left-color: #6633ff;
border-right-color: #6633ff;
}
100% {
box-shadow: -10px 0 10px -10px #ff0066,
10px 0 10px -10px #ff0066,
inset -10px 0 10px -10px #ff0066,
inset 10px 0 10px -10px #ff0066;
border-left-color: #ff0066;
border-right-color: #ff0066;
}
}
/* 小点移动动画 */
@keyframes dot-move {
0% {
top: 30px;
opacity: 1;
}
10% {
opacity: 1;
}
40% {
opacity: 1;
}
50% {
opacity: 0;
top: calc(100% - 30px);
}
60% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
opacity: 0;
top: 30px;
}
}
</style>
</html>
\ No newline at end of file
odoo.define('tht_project.cook_process', function (require) {
"use strict";
const AbstractAction = require('web.AbstractAction');
const core = require('web.core');
const QWeb = core.qweb;
const session = require('web.session');
const Dialog = require("web.Dialog");
const CookProcessTemplate = AbstractAction.extend({
template: 'CookProcessTemplate',
start: async function () {
await this._super(...arguments);
let self = this;
window.addEventListener("message", function (event) {
});
},
});
core.action_registry.add('tht_project.cook_process', CookProcessTemplate);
return CookProcessTemplate;
});
odoo.define('tht_project.expected_process', function (require) {
"use strict";
const AbstractAction = require('web.AbstractAction');
const core = require('web.core');
const QWeb = core.qweb;
const session = require('web.session');
const Dialog = require("web.Dialog");
const ExpectedProcessTemplate = AbstractAction.extend({
template: 'ExpectedProcessTemplate',
start: async function () {
await this._super(...arguments);
let self = this;
window.addEventListener("message", function (event) {
});
},
});
core.action_registry.add('tht_project.expected_process', ExpectedProcessTemplate);
return ExpectedProcessTemplate;
});
<!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="workshop-filter" style="text-align: center; margin: 10px 0">
<el-radio-group v-model="selectedWorkshop" @change="onWorkshopChange" size="medium">
<el-radio label="">
<span style="font-size: 18px;">全部<span></el-radio>
<el-radio label="预料车间">
<span style="font-size: 18px;">预料车间<span>
</el-radio>
<el-radio label="蒸煮车间">
<span style="font-size: 18px">成型车间<span>
</el-radio>
<el-radio label="包装车间">
<span style="font-size: 18px">包装车间<span>
</el-radio>
</el-radio-group>
</div>
<!-- 设备状态卡片区域 - 新设计 -->
<div class="scrollBoxClass">
<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">[[truncateText(device.name, 12)]]</div>
<!-- 设备状态 - 右上角 -->
<div class="device-status-tag" :class="getStatusClass(device.status)">
[[getStatusText(device.status)]]
</div>
<!-- 设备状态水波纹 - 中间 -->
<div class="device-wave-container">
<!-- v-if="device.status === 'running' || device.status === 'waiting'" -->
<div class="oee-text">
OEE
</div>
<!-- <div class="err-text" v-if="device.status === 'error'" :class="getErrClass(device.status)">
ERR
</div>
<div class="off-text" v-if="device.status === 'off'" :class="getOffClass(device.status)">
OFF
</div> -->
<!-- v-if="device.status === 'running' || device.status === 'waiting'" -->
<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) + '%',
'bottom': '0',
'left': '0',
'position': 'absolute',
'width': '100%',
'overflow': 'hidden'
}">
<div class="water-wave-content">
<div class="water-wave-ripple"></div>
</div>
</div>
</div>
</div>
<!-- 设备状态信息 - 底部 -->
<div class="device-status-info">
<span>已持续 [[device.duration]]</span>
</div>
<div style="width: 100%; display: flex;margin-left: 13px;">
<div class="flxe_sty">
<div class="flxe_label_sty" style=" background-color: #00aa00;">开机</div>
[[ device.run_seconds ]]
</div>
<div class="flxe_sty">
<div class="flxe_label_sty" style=" background-color: #ffaa00;">加工</div>
[[ device.green_seconds ]]
</div>
</div>
<div style="width: 100%;display: flex;margin-left: 13px;">
<div class="flxe_sty">
<div class="flxe_label_sty" style=" background-color: #797979;">空闲</div>
[[ device.yellow_seconds ]]
</div>
<div class="flxe_sty">
<div class="flxe_label_sty" style=" background-color: #f87171;">故障</div>
[[ device.red_seconds ]]
</div>
</div>
</div>
</div>
</div>
<!-- 底部状态图 -->
<div class="status-chart glass-effect" v-if="false">
<!-- 这部分保留但不显示,如果将来需要可以启用 -->
<div class="section-title">
<span class="title-text">设备运行状态统计</span>
<div class="title-decoration"></div>
</div>
<div class="chart-header">
<el-select v-model="selectedDevice" placeholder="请选择设备" size="medium" @change="selDeviceChange">
<el-option v-for="item in deviceList" :label="item.name" :value="item.id" :key="item.id">
</el-option>
</el-select>
<h4>近10天设备运行状态统计</h4>
</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">h</span>
</span>
</div>
<!-- 图表主体区域 -->
<div class="chart-content">
<!-- 网格区域 -->
<div class="grid-area">
<!-- 背景网格 -->
<div class="">
<div class="horizontal-lines">
<div class="horizontal-line" v-for="(value, index) in yAxisValues" :key="index"></div>
</div>
</div>
<!-- 柱状图组 -->
<div class="columns-container">
<div class="column-group" v-for="(item, dayIndex) in pickingOrderList" :key="dayIndex">
<div class="column-stack">
<div v-for="(segment, stackIndex) in item.data" :key="stackIndex" class="column-segment" :style="{
'height': calculateHeight(segment.value) + '%',
'background-color': segment.color,
'margin-top': '0px'
}"></div>
</div>
</div>
</div>
</div>
</div>
<!-- X轴 -->
<div class="x-axis">
<span v-for="(item, index) in pickingOrderList" :key="index" class="x-axis-label">
[[item.name_format]]
</span>
</div>
</div>
</div>
</div>
</body>
<script>
// 发送消息给父页面(关闭odoo的菜单弹窗)
document.addEventListener("click", function () {
window.parent.postMessage("hidePopover", "*");
});
let vue = new Vue({
el: "#app",
delimiters: ["[[", "]]"], // 替换原本vue的[[ key ]]取值方式(与odoo使用的jinja2冲突问题)
data: function () {
return {
isFullscreen: false, // 全屏状态
currentTime: null, // 当前时间
timer: null, // 定时器
windowHeight: window.innerHeight, // 窗口高度
dwsURL: "https://tht.dws.rokecloud.com", // 基地址
// dwsURL: "", // 基地址
baseURL: "https://dws-platform.xbg.rokeris.com/dev-api", // 基地址
loading: false, // 全局加载效果
deviceList: [], // 设备列表
selectedDevice: null, // 选中的设备
yAxisValues: ["24", "20", "16", "12", "8", "4", "0"], // Y轴刻度值
pickingOrderList: [], // 拣选单列表
dateList: [], // 日期列表
start_time: "", // 开始时间
end_time: "", // 结束时间
refreshInterval: null, // 定时刷新计时器
factoryCode: "", // 公司CODE
allEquipmentData: [], // 所有已绑定设备数据
selectedWorkshop: "", // 新增字段:当前选中的车间
};
},
created: function () {
if (this.getUrlSearch("factory_code")) {
this.factoryCode = this.getUrlSearch("factory_code"); //截取url后面的参数
}
// this.factoryCode = "66c6bd8c-fd58-11ef-9692-00163e04c506"
this.initCurrentTimeFn()
},
computed: {
// 选中设备的信息
selDeviceInfo: function () {
return this.deviceList.find(function (item) { return item.id == this.selectedDevice; }.bind(this));
},
},
async mounted() {
window.addEventListener("resize", this.handleResize);
this.$nextTick(function () {
document.getElementById("bodyId").style.display = "block";
});
// // 先加载测试数据以便查看效果
// this.initMockData();
// 初始化数据 - 实际使用时取消这行的注释
await this.initData();
// 获取最后指定天数的日期
this.dateList = this.getLastAssignDays();
// 设置定时刷新(每分钟刷新一次,静默模式)
this.refreshInterval = setInterval(function () {
this.initData(true); // 传入true表示静默刷新,不显示加载提示
}.bind(this), 60000);
// 设置设备信息的title属性
this.$nextTick(function () {
document.querySelectorAll(".device-info").forEach(function (el) {
el.title = el.dataset.fullTitle;
});
});
},
updated: function () {
// 在数据更新后也设置title属性
this.$nextTick(function () {
document.querySelectorAll(".device-info").forEach(function (el) {
el.title = el.dataset.fullTitle;
});
});
},
beforeDestroy: function () {
// 清除定时器
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
if (this.timer) {
clearInterval(this.timer)
}
// 移除事件监听
window.removeEventListener("resize", this.handleResize);
},
methods: {
// 全屏icon点击事件
toggleFullscreen: function () {
if (!this.isFullscreen) {
this.enterFullScreen()
} else {
this.exitFullScreen()
}
},
// 全屏方法
enterFullScreen: function () {
// 获取需要全屏的元素
const elem = this.$refs.fullScreenElement
if (elem.requestFullscreen) {
elem.requestFullscreen()
} else if (elem.mozRequestFullScreen) {
// Firefox
elem.mozRequestFullScreen()
} else if (elem.webkitRequestFullscreen) {
// Chrome, Safari & Opera
elem.webkitRequestFullscreen()
} else if (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
},
onWorkshopChange: function () {
this.initData(); // 触发重新加载数据
},
// 初始化当前时间
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];
},
// 初始化数据
async initData(silent = false, workshopName = this.selectedWorkshop) {
try {
// 只有在非静默模式下才显示加载提示
if (!silent) {
this.loading = true;
}
// 并行请求设备计划运行时间和所有已绑定设备数据
const [planTimeResult, allEquipmentResult] = await Promise.all([
this.getDevicePlanTime(),
this.getAllEquipmentData(workshopName),
]);
} catch (error) {
// console.error("初始化数据出错:", error);
// 只有在非静默模式下才显示错误提示
if (!silent) {
this.$message.error("初始化数据出错: " + (error.message || "未知错误"));
}
// 如果接口请求失败,使用模拟数据
this.initMockData();
} finally {
// 关闭加载提示
if (!silent) {
this.loading = false;
}
}
},
// 获取设备计划运行时间
async getDevicePlanTime() {
try {
// 发送请求获取计划运行时间
const response = await axios({
method: "post",
url: this.dwsURL + "/tht/get/equipment/working_time",
data: {},
headers: { "Content-Type": "application/json" },
});
// 处理JSON-RPC格式的响应
if (response.data && response.data.result && response.data.result.state === "success") {
// 获取计划运行时间数据
const start_working_time = response.data.result.start_working_time || [];
const end_working_time = response.data.result.end_working_time || [];
const wait_time = response.data.result.wait_time || [];
if (this.factoryCode) {
// 调用设备状态接口
await this.getDeviceStateList(start_working_time, end_working_time, wait_time);
}
else {
await this.getFactoryCode(start_working_time, end_working_time, wait_time);
}
return start_working_time, end_working_time, wait_time;
} else {
const errorMsg = response.data.result
? response.data.result.message
: "获取计划运行时间失败";
throw new Error(errorMsg);
}
} catch (error) {
// console.error("获取计划运行时间出错:", error);
throw error;
}
},
// 获取所有已绑定设备数据
async getAllEquipmentData(workshopName = this.selectedWorkshop) {
try {
// 发送请求获取所有已绑定设备数据
const response = await axios({
method: "post",
url: this.dwsURL + "/roke/workstation/equipment/get_equipment_data",
data: {
plant_name: workshopName,
},
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) {
// console.error("获取所有已绑定设备数据出错:", error);
throw error;
}
},
// 获取设备状态列表
async getDeviceStateList(start_time, end_time, wait_time) {
try {
// 使用CORS代理
// 发送请求获取设备状态
const response = await axios({
method: "POST",
url: this.baseURL + "/public/device_efficiency/device_working_time_state_list",
data: {
factory_code: this.factoryCode,
start_time: start_time,
end_time: end_time,
wait_time: wait_time,
},
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
async getFactoryCode(start_working_time, end_working_time, wait_time) {
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(start_working_time, end_working_time, wait_time);
} else {
const errorMsg = response.data.result
? response.data.result.message
: "获取账套db_uuid失败";
throw new Error(errorMsg);
}
} catch (error) {
// console.error("获取账套db_uuid失败:", error);
throw error;
}
},
// 处理设备状态数据
processDeviceStateData: function (deviceStateData) {
if (!deviceStateData || !Array.isArray(deviceStateData)) {
return;
}
var codeToSeqMap = {};
this.allEquipmentData.forEach(function (equip) {
codeToSeqMap[equip.code] = equip.sequence;
});
// 获取当前车间下的设备 code 列表
var validCodes = this.allEquipmentData.filter(function (e) {
return !this.selectedWorkshop || e.plant_name === this.selectedWorkshop;
}.bind(this)).map(function (e) {
return e.code;
});
// 过滤掉不属于当前车间的设备
var filteredDevices = deviceStateData.filter(function (d) {
return !this.selectedWorkshop || validCodes.includes(d.code);
}.bind(this)).map(function (device) {
var newDevice = Object.assign({}, device);
newDevice.seq = codeToSeqMap[device.code] || Number.MAX_SAFE_INTEGER; // 如果找不到 seq,默认排最后
return newDevice;
});
// 将API返回的数据转换为页面所需的格式
var deviceList = filteredDevices.map(function (device, index) {
// 根据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 = this.formatTime(Number(device.duration_hours * 3600))
}
var run_seconds = "0"
if (device.run_seconds !== undefined) run_seconds = this.formatTime(Number(device.run_seconds))
var green_seconds = "0"
if (device.green_seconds !== undefined) green_seconds = this.formatTime(Number(device.green_seconds))
var yellow_seconds = "0"
if (device.yellow_seconds !== undefined) yellow_seconds = this.formatTime(Number(device.yellow_seconds))
var red_seconds = "0"
if (device.red_seconds !== undefined) red_seconds = this.formatTime(Number(device.red_seconds))
// 计算利用率百分比,确保有效值
var percentage = device.utilization_rate !== undefined ? Math.round(device.utilization_rate) : 0
// 从所有设备列表中获取准确的设备名称
var deviceName = device.name || device.code // 默认使用API返回的名称或编码
// 在所有设备列表中查找匹配的设备
if (this.allEquipmentData && this.allEquipmentData.length > 0) {
var matchedDevice = this.allEquipmentData.find(function (equip) { return equip.code === device.code; })
// 如果找到匹配的设备,使用其名称
if (matchedDevice && matchedDevice.name) {
deviceName = matchedDevice?.name || device.code
} else {
return false
}
}
return {
id: device.code,
code: device.code,
name: deviceName,
status: status,
percentage: percentage,
duration: durationText,
run_seconds: run_seconds,
green_seconds: green_seconds,
yellow_seconds: yellow_seconds,
red_seconds: red_seconds
}
}.bind(this)).sort(function (a, b) {
return (codeToSeqMap[a.code] || Number.MAX_SAFE_INTEGER) - (codeToSeqMap[b.code] || Number.MAX_SAFE_INTEGER);
})
// 过滤没有信息项的数据
this.deviceList = deviceList.filter(item => item.id)
},
formatTime: function (seconds) {
if (seconds < 60) {
return `0min`; // 不足 1 分钟显示 0min
} else if (seconds < 3600) {
var minutes = Math.floor(seconds / 60); // 转换为分钟
return `${minutes}min`; // 显示分钟
} else if (seconds < 86400) { // 小于 1 天
var hours = Math.floor(seconds / 3600); // 转换为小时
return `${hours}h`; // 只返回小时
} else {
var days = Math.floor(seconds / 86400); // 转换为天数
return `${days}d`; // 只返回天
}
},
// 初始化模拟数据 (保留原有的模拟数据方法,作为备用)
initMockData: function () {
// 模拟设备数据 - 添加不同状态和明显不同百分比的测试数据
this.deviceList = [
// {
// id: "device001",
// code: "device001",
// name: "设备名称1",
// status: "running", // 运行中
// percentage: 90, // 非常高的百分比
// duration: "2h15m",
// },
// {
// id: "device002",
// code: "device002",
// name: "设备名称2",
// status: "error", // 故障中
// percentage: 50, // 中等百分比
// duration: "4h30m",
// },
// {
// id: "device003",
// code: "device003",
// name: "设备名称3",
// status: "waiting", // 等待中
// percentage: 20, // 非常低的百分比
// duration: "1h45m",
// },
// {
// id: "device004",
// code: "device004",
// name: "设备名称4",
// status: "off", // 停机中
// percentage: 50, // 中等百分比
// duration: "8h20m",
// },
];
// 添加测试日志
console.log("测试数据初始化完成,设备列表:", this.deviceList);
},
// 获取卡片的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%之间,确保即使是低百分比也有一定的水位可见
var height = percentage;
// 确保最小高度为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 "停机中";
}
},
// 处理小数位数方法
toFixedHandle: function (value, num) {
if (num === undefined) num = 4;
if (value) {
var strValue = String(value);
if (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%
var heightPerHour = 100 / 24;
var percentage = hours * heightPerHour;
// 确保高度在0-100%之间
return Math.min(Math.max(percentage, 0), 100);
},
// 获取最后指定天数的日期
getLastAssignDays: function (num) {
if (num === undefined) num = 10;
var dates = [];
for (var i = num - 1; i >= 0; i--) {
var date = new Date();
date.setDate(date.getDate() - i);
var year = date.getFullYear();
var month = String(date.getMonth() + 1).padStart(2, "0");
var 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;
},
// 处理窗口大小变化
handleResize: function () {
// 窗口大小变化时的处理逻辑
},
// 文本截断方法
truncateText: function (text, maxLength) {
if (!text) return "";
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + "...";
},
// 显示完整标题
showFullTitle: function (event, device) {
var 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 {
background: url("/roke_workstation_api/static/html/routing/image/bg-ok.png") no-repeat center center fixed;
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(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: flex;
align-items: center;
gap: 10px;
}
.scrollBoxClass {
height: 1px;
flex: auto;
overflow-y: auto;
}
/* 设备卡片容器 */
.device-cards-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 20px;
padding: 15px;
}
/* 新设计设备卡片样式 */
.device-card.new-design {
position: relative;
padding: 15px;
display: flex;
flex-direction: column;
align-items: center;
min-height: 180px;
border-radius: 12px;
transition: all 0.3s ease;
background-color: rgba(20, 22, 28, 0.95);
/* 更深的背景色 */
}
/* 卡片颜色和阴影 - 调整为适度柔和的阴影效果 */
.card-running {
border: none;
box-shadow: 0 0 0 1px rgba(78, 198, 138, 0.5), 5px 5px 2px 0 rgba(78, 198, 138, 0.7),
5px 5px 15px 2px rgba(78, 198, 138, 0.5);
}
.card-waiting {
border: none;
box-shadow: 0 0 0 1px rgba(235, 186, 22, 0.5), 5px 5px 2px 0 rgba(235, 186, 22, 0.7),
5px 5px 15px 2px rgba(235, 186, 22, 0.5);
}
.card-error {
border: none;
box-shadow: 0 0 0 1px rgba(235, 86, 86, 0.5), 5px 5px 2px 0 rgba(235, 86, 86, 0.7),
5px 5px 15px 2px rgba(235, 86, 86, 0.5);
}
.card-off {
border: none;
box-shadow: 0 0 0 1px rgba(144, 147, 153, 0.5), 5px 5px 2px 0 rgba(144, 147, 153, 0.7),
5px 5px 15px 2px rgba(144, 147, 153, 0.5);
}
.device-card.new-design:hover {
transform: translateY(-3px);
box-shadow: 0 0 0 1px rgba(78, 198, 138, 0.5), 7px 7px 4px 0 rgba(78, 198, 138, 0.7),
7px 7px 20px 2px rgba(78, 198, 138, 0.6);
}
/* 设备名称 - 左上角 */
.device-name {
align-self: flex-start;
font-size: 14px;
font-weight: bold;
color: #fff;
max-width: 150px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
margin-bottom: 5px;
}
/* 设备状态标签 - 右上角 */
.device-status-tag {
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);
box-shadow: none;
}
.status-waiting {
background-color: rgba(235, 186, 22, 0.9);
box-shadow: none;
}
.status-error {
background-color: rgba(235, 86, 86, 0.9);
box-shadow: none;
}
.status-off {
background-color: rgba(144, 147, 153, 0.9);
box-shadow: none;
}
/* 设备波纹容器 */
.device-wave-container {
width: 100%;
height: 120px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
}
/* OEE文字 */
.oee-text {
position: absolute;
top: 30px;
font-size: 16px;
font-weight: bold;
color: #fff;
z-index: 3;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
/* ERR文字 */
.err-text {
position: absolute;
top: 30px;
font-size: 16px;
font-weight: bold;
z-index: 3;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
left: 50%;
transform: translateX(-50%);
}
/* ERR颜色 */
.err-error {
color: rgba(235, 86, 86, 0.9);
}
/* OFF文字 */
.off-text {
position: absolute;
top: 30px;
font-size: 16px;
font-weight: bold;
z-index: 3;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
left: 50%;
transform: translateX(-50%);
}
/* OFF颜色 */
.off-status {
color: rgba(144, 147, 153, 0.9);
}
/* 百分比文字 */
.percentage-text {
position: absolute;
top: 50px;
font-size: 25px;
font-weight: bold;
color: #fff;
z-index: 3;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
/* 圆形容器 */
.circle-container {
width: 100px;
height: 100px;
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);
box-shadow: none;
}
.border-waiting {
border: 2px solid rgba(235, 186, 22, 0.9);
box-shadow: none;
}
.border-error {
border: 2px solid rgba(235, 86, 86, 0.9);
box-shadow: none;
}
.border-off {
border: 2px solid rgba(144, 147, 153, 0.9);
box-shadow: none;
}
/* 水波纹 */
.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;
}
/* 设备状态信息 */
.device-status-info {
font-size: 12px;
color: rgba(255, 255, 255, 0.85);
text-align: center;
line-height: 1.5;
}
/* 响应式调整 */
@media (max-width: 768px) {
.device-cards-container {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.device-card.new-design {
min-height: 160px;
padding: 10px;
}
.circle-container {
width: 80px;
height: 80px;
}
.oee-text,
.err-text,
.off-text {
top: 20px;
font-size: 14px;
}
.percentage-text {
top: 40px;
font-size: 20px;
}
}
/* 为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
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="CookProcessTemplate">
<iframe id="cook_process_template_iframe" src="/roke/tht/cook_process" frameBorder="no" width="100%" height="100%"/>
</t>
</templates>
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="ExpectedProcessTemplate">
<iframe id="expected_process_template_iframe" src="/roke/tht/expected_process" frameBorder="no" width="100%" height="100%"/>
</t>
</templates>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="tht_project_assets_backend" name="tht_project_assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/tht_project/static/src/js/expected_process.js"/>
<script type="text/javascript" src="/tht_project/static/src/js/cook_process.js"/>
</xpath>
</template>
</odoo>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="action_expected_process" model="ir.actions.client">
<field name="name">预料工序看板</field>
<field name="tag">tht_project.expected_process</field>
<field name="target">current</field>
</record>
<record id="action_cook_process" model="ir.actions.client">
<field name="name">成型工序看板</field>
<field name="tag">tht_project.cook_process</field>
<field name="target">current</field>
</record>
<!-- <menuitem id="roke_mes_equipment_kanban" name="设备看板" sequence="31"-->
<!-- parent="roke_mes_equipment.roke_mes_equipment_main_menu"-->
<!-- active="1"/>-->
<menuitem id="roke_mes_expected_process" name="预料看板" sequence="10"
action="action_expected_process" active="1"/>
<menuitem id="roke_mes_cook_process" name="成型工序" sequence="20"
action="action_cook_process" active="1"/>
</data>
</odoo>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="menu_plant_working_time_config" name="车间工作时间配置"
action="tht_project.action_plant_working_time_config"
parent="roke_mes_three_colour_light.roke_three_color_light_iframe_device_monitor_menu"
sequence="40"
groups="base.group_system"
/>
</odoo>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_plant_working_time_config_tree" model="ir.ui.view">
<field name="name">view_plant_working_time_config_tree</field>
<field name="model">plant.working.time.config</field>
<field name="arch" type="xml">
<tree>
<field name="plant_id"/>
<field name="start_time" widget="float_time"/>
<field name="end_time" widget="float_time"/>
</tree>
</field>
</record>
<record id="view_plant_working_time_config_form" model="ir.ui.view">
<field name="name">view_plant_working_time_config_form</field>
<field name="model">plant.working.time.config</field>
<field name="arch" type="xml">
<form>
<group col="3">
<group>
<field name="plant_id"/>
</group>
<group>
<field name="start_time" widget="float_time"/>
</group>
<group>
<field name="end_time" widget="float_time"/>
</group>
</group>
<notebook>
<page string="停机规则设置">
<div class="mb16" style="display: flex; flex-direction: row; align-items: center;">
<div class="ml16" style="width: 150px">
<field name="color"/>
</div>
<span class="ml16">色下连续超过</span>
<div class="ml16" style="width: 150px">
<field name="wait_time"/>
</div>
<span class="ml16">分钟,页面展示停机状态。</span>
</div>
</page>
</notebook>
</form>
</field>
</record>
<record id="action_plant_working_time_config" model="ir.actions.act_window">
<field name="name">车间工作时间配置</field>
<field name="res_model">plant.working.time.config</field>
<field name="view_mode">tree,form</field>
<field name="type">ir.actions.act_window</field>
<field name="domain">[]</field>
<field name="context">{}</field>
</record>
</odoo>
\ 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>
<!-- <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="model">roke.mes.equipment</field>
<field name="inherit_id" ref="roke_workstation_api.view_dws_inherit_roke_mes_equipment_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='workshop_id']" position="after">
<field name="data_acquisition_code"/>
</xpath>
<xpath expr="//field[@name='work_center_id']" position="after">
<field name="sequence"/>
</xpath>
</field>
</record> -->
<record id="view_roke_mes_equipment_tree_tht" model="ir.ui.view">
<field name="name">view_roke_mes_equipment_tree_tht</field>
<field name="model">roke.mes.equipment</field>
<field name="inherit_id" ref="roke_mes_equipment.view_roke_mes_equipment_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<field name="data_acquisition_code"/>
</xpath>
<xpath expr="//field[@name='code']" position="attributes">
<attribute name="string">三色灯编号</attribute>
</xpath>
</field>
</record>
</data>
</odoo>
\ 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