Commit 7fb1f750 by guibin

Merge branch 'tht-project' of https://git.rokedata.com/dws/dwsproject into tht-project

parents 3e5c45a5 5a8d630e
# -*- coding: utf-8 -*-
# from odoo import http
# class ThtProject(http.Controller):
# @http.route('/tht_project/tht_project/', auth='public')
# def index(self, **kw):
# return "Hello, world"
# @http.route('/tht_project/tht_project/objects/', auth='public')
# def list(self, **kw):
# return http.request.render('tht_project.listing', {
# 'root': '/tht_project/tht_project',
# 'objects': http.request.env['tht_project.tht_project'].search([]),
# })
# @http.route('/tht_project/tht_project/objects/<model("tht_project.tht_project"):obj>/', auth='public')
# def object(self, obj, **kw):
# return http.request.render('tht_project.object', {
# 'object': obj
# })
from odoo import http
from odoo.http import request
from odoo.addons.roke_mes_three_colour_light.controller.main import RokeMesThreeColourLight
import os
from jinja2 import Environment, FileSystemLoader
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
templateloader = FileSystemLoader(searchpath=BASE_DIR + "/static/src/view")
env = Environment(loader=templateloader)
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('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
}
<!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="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 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: "f1991dbb-52d6-11ef-b13f-fa163e5bb392", // 公司CODE
allEquipmentData: [], // 所有已绑定设备数据
selectedWorkshop: "", // 新增字段:当前选中的车间
};
},
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() {
if (!this.isFullscreen) {
this.enterFullScreen()
} else {
this.exitFullScreen()
}
},
// 全屏方法
enterFullScreen() {
// 获取需要全屏的元素
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() {
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() {
this.initData(); // 触发重新加载数据
},
// 初始化当前时间
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, 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 + "/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 || {};
// 调用设备状态接口
await this.getDeviceStateList(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(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(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;
}
},
// 处理设备状态数据
processDeviceStateData(deviceStateData) {
if (!deviceStateData || !Array.isArray(deviceStateData)) {
return;
}
// 获取当前车间下的设备 code 列表
const validCodes = this.allEquipmentData
.filter(e => !this.selectedWorkshop || e.plant_name === this.selectedWorkshop)
.map(e => e.code);
// 过滤掉不属于当前车间的设备
const filteredDevices = deviceStateData.filter(d =>
!this.selectedWorkshop || validCodes.includes(d.code)
);
// 将API返回的数据转换为页面所需的格式
this.deviceList = filteredDevices.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返回的名称或编码
// 在所有设备列表中查找匹配的设备
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
}
}
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
}
});
},
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 {
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;
}
/* 设备卡片容器 */
.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
<odoo>
<data>
<!-- explicit list view definition -->
<!--
<record model="ir.ui.view" id="tht_project.list">
<field name="name">tht_project list</field>
<field name="model">tht_project.tht_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="tht_project.action_window">
<field name="name">tht_project window</field>
<field name="res_model">tht_project.tht_project</field>
<field name="view_mode">tree,form</field>
</record>
-->
<!-- server action to the one above -->
<!--
<record model="ir.actions.server" id="tht_project.action_server">
<field name="name">tht_project server</field>
<field name="model_id" ref="model_tht_project_tht_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="tht_project" id="tht_project.menu_root"/>
-->
<!-- menu categories -->
<!--
<menuitem name="Menu 1" id="tht_project.menu_1" parent="tht_project.menu_root"/>
<menuitem name="Menu 2" id="tht_project.menu_2" parent="tht_project.menu_root"/>
-->
<!-- actions -->
<!--
<menuitem name="List" id="tht_project.menu_1_list" parent="tht_project.menu_1"
action="tht_project.action_window"/>
<menuitem name="Server to list" id="tht_project" parent="tht_project.menu_2"
action="tht_project.action_server"/>
-->
<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>
</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