Commit df835fc2 by guibin

添加金牛模块

parent d3e20d66
# -*- 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) {
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 (!digit) return value
// 处理 null/undefined/空字符串
if (value === null || value === undefined || value === '') return null
// 转为数字(避免字符串或科学计数法问题)
const parsedNum = parseFloat(value)
// 处理 NaN
if (isNaN(parsedNum)) return NaN
// 检查是否是整数,整数直接返回
if (Number.isInteger(parsedNum)) return parsedNum
// 截取小数位数
const decimalPart = parsedNum.toString().split('.')[1]
// 检查小数长度是否大于要求位数,小于或等于直接返回
if (decimalPart && decimalPart?.length <= digit) return parsedNum
// 保留要求位数(处理浮点误差)
return parseFloat((Math.round((parsedNum + Number.EPSILON) * 100) / 100).toFixed(digit))
},
// 处理时间方法
formatTime(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
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