feat: 新增锌线生产监控模块及相关API和组件
refactor(auth): 增加锌线系统token管理功能 feat(api): 添加锌线停机记录、生产报表和设备快照API feat(views): 实现锌线实时监控、生产统计和停机统计页面 feat(components): 开发锌线生产报表、停机统计和班组绩效组件 feat(utils): 新增锌线专用请求工具zinc1Request chore(vue.config): 配置锌线API代理
This commit is contained in:
18
klp-ui/src/api/lines/zinc/dashboard.js
Normal file
18
klp-ui/src/api/lines/zinc/dashboard.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import zinc1Request from '@/utils/zinc1Request'
|
||||||
|
|
||||||
|
// 获取当前生产中的计划信息
|
||||||
|
export function getCurrentPlan() {
|
||||||
|
return zinc1Request({
|
||||||
|
url: '/business/dashboard/currentPlan',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前生产卷的关键工艺参数
|
||||||
|
export function getCurrentProcess() {
|
||||||
|
return zinc1Request({
|
||||||
|
url: '/business/dashboard/currentProcess',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
20
klp-ui/src/api/lines/zinc/report.js
Normal file
20
klp-ui/src/api/lines/zinc/report.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import zinc1Request from '@/utils/zinc1Request'
|
||||||
|
|
||||||
|
// 生产实绩汇总
|
||||||
|
export function getReportSummary(params) {
|
||||||
|
return zinc1Request({
|
||||||
|
url: '/report/summary',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生产实绩明细
|
||||||
|
export function getReportDetails(params) {
|
||||||
|
return zinc1Request({
|
||||||
|
url: '/report/details',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
11
klp-ui/src/api/lines/zinc/stoppage.js
Normal file
11
klp-ui/src/api/lines/zinc/stoppage.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import zinc1Request from '@/utils/zinc1Request'
|
||||||
|
|
||||||
|
// 停机记录列表
|
||||||
|
export function listStoppage(data) {
|
||||||
|
return zinc1Request({
|
||||||
|
url: '/stoppage/list',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
9
klp-ui/src/api/pocket/deviceEnum.js
Normal file
9
klp-ui/src/api/pocket/deviceEnum.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import zinc1Request from '@/utils/zinc1Request'
|
||||||
|
|
||||||
|
export function listDeviceEnumAll() {
|
||||||
|
return zinc1Request({
|
||||||
|
url: '/api/deviceEnum/all',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
9
klp-ui/src/api/pocket/deviceFieldMeta.js
Normal file
9
klp-ui/src/api/pocket/deviceFieldMeta.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import zinc1Request from '@/utils/zinc1Request'
|
||||||
|
|
||||||
|
export function getDeviceFieldMetaAll() {
|
||||||
|
return zinc1Request({
|
||||||
|
url: '/api/deviceFieldMeta/all',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
20
klp-ui/src/api/pocket/deviceSnapshot.js
Normal file
20
klp-ui/src/api/pocket/deviceSnapshot.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import zinc1Request from '@/utils/zinc1Request'
|
||||||
|
|
||||||
|
// 获取最新N条设备快照
|
||||||
|
export function listDeviceSnapshotLatest(params) {
|
||||||
|
return zinc1Request({
|
||||||
|
url: '/api/deviceSnapshot/latest',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按时间范围查询设备快照
|
||||||
|
export function listDeviceSnapshotRange(params) {
|
||||||
|
return zinc1Request({
|
||||||
|
url: '/api/deviceSnapshot/range',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
162
klp-ui/src/api/pocket/plantState.js
Normal file
162
klp-ui/src/api/pocket/plantState.js
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// =========================== 状态定义相关 ===========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有状态定义及其当前值(用于初始化缓存)
|
||||||
|
* 返回数据结构:
|
||||||
|
* {
|
||||||
|
* id: 定义ID (对应VALUE字段编号,如id=14对应VALUE14),
|
||||||
|
* name: 指标名称,
|
||||||
|
* units: 单位,
|
||||||
|
* comments: 说明,
|
||||||
|
* currentValue: 当前值,
|
||||||
|
* currentInsdate: 当前值时间
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export function getAllPlantStateDefines() {
|
||||||
|
return request({
|
||||||
|
url: '/pocket/proPlantStateDefine/allWithValues',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================== 当前数据相关 ===========================
|
||||||
|
|
||||||
|
// 查询设备状态当前数据列表
|
||||||
|
export function listPlantStateCurrent(query) {
|
||||||
|
return request({
|
||||||
|
url: '/pocket/proPlantStateCurrent/selectAll',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询单条设备状态当前数据
|
||||||
|
export function getPlantStateCurrent(type, insdate) {
|
||||||
|
return request({
|
||||||
|
url: '/pocket/proPlantStateCurrent/one',
|
||||||
|
method: 'get',
|
||||||
|
params: { type, insdate }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询设备状态历史数据列表
|
||||||
|
export function listPlantStateHistory(query) {
|
||||||
|
return request({
|
||||||
|
url: '/pocket/proPlantStateHistory/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询设备状态历史数据详情
|
||||||
|
export function getPlantStateHistory(insdate) {
|
||||||
|
return request({
|
||||||
|
url: '/pocket/proPlantStateHistory/' + insdate,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询停机列表
|
||||||
|
export function listStoppage(query) {
|
||||||
|
return request({
|
||||||
|
url: '/pocket/proStoppage/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询停机详情
|
||||||
|
export function getStoppage(stopId) {
|
||||||
|
return request({
|
||||||
|
url: '/pocket/proStoppage/' + stopId,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================== 班组信息相关 ===========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前班组信息
|
||||||
|
* 返回数据结构:
|
||||||
|
* {
|
||||||
|
* shift: 班次 (如:A/B/C 或 早/中/晚),
|
||||||
|
* crew: 班组编号,
|
||||||
|
* seqNum: 序列号,
|
||||||
|
* sysTime: 系统时间
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export function getCurrentShift() {
|
||||||
|
return request({
|
||||||
|
url: '/pocket/shiftCurrent/current',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================== 生产统计相关 ===========================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取生产统计汇总数据
|
||||||
|
* @param {string} startDate - 开始日期 (yyyy-MM-dd)
|
||||||
|
* @param {string} endDate - 结束日期 (yyyy-MM-dd)
|
||||||
|
*/
|
||||||
|
export function getProductionSummary(startDate, endDate) {
|
||||||
|
return request({
|
||||||
|
url: '/pocket/productionStatistics/summary',
|
||||||
|
method: 'get',
|
||||||
|
params: { startDate, endDate }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取班组产量统计
|
||||||
|
* @param {string} startDate - 开始日期
|
||||||
|
* @param {string} endDate - 结束日期
|
||||||
|
*/
|
||||||
|
export function getCrewProduction(startDate, endDate) {
|
||||||
|
return request({
|
||||||
|
url: '/pocket/productionStatistics/crewProduction',
|
||||||
|
method: 'get',
|
||||||
|
params: { startDate, endDate }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取厚度分布统计
|
||||||
|
* @param {string} startDate - 开始日期
|
||||||
|
* @param {string} endDate - 结束日期
|
||||||
|
*/
|
||||||
|
export function getThicknessDistribution(startDate, endDate) {
|
||||||
|
return request({
|
||||||
|
url: '/pocket/productionStatistics/thicknessDistribution',
|
||||||
|
method: 'get',
|
||||||
|
params: { startDate, endDate }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取宽度分布统计
|
||||||
|
* @param {string} startDate - 开始日期
|
||||||
|
* @param {string} endDate - 结束日期
|
||||||
|
*/
|
||||||
|
export function getWidthDistribution(startDate, endDate) {
|
||||||
|
return request({
|
||||||
|
url: '/pocket/productionStatistics/widthDistribution',
|
||||||
|
method: 'get',
|
||||||
|
params: { startDate, endDate }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取班组绩效统计
|
||||||
|
* @param {string} startDate - 开始日期
|
||||||
|
* @param {string} endDate - 结束日期
|
||||||
|
*/
|
||||||
|
export function getTeamPerformance(startDate, endDate) {
|
||||||
|
return request({
|
||||||
|
url: '/pocket/productionStatistics/teamPerformance',
|
||||||
|
method: 'get',
|
||||||
|
params: { startDate, endDate }
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import Cookies from 'js-cookie'
|
import Cookies from 'js-cookie'
|
||||||
|
|
||||||
const TokenKey = 'Admin-Token'
|
const TokenKey = 'Admin-Token'
|
||||||
|
const ZincTokenKey = 'Zinc-Token'
|
||||||
|
|
||||||
|
// L3 Token管理
|
||||||
export function getToken() {
|
export function getToken() {
|
||||||
return Cookies.get(TokenKey)
|
return Cookies.get(TokenKey)
|
||||||
}
|
}
|
||||||
@@ -13,3 +15,16 @@ export function setToken(token) {
|
|||||||
export function removeToken() {
|
export function removeToken() {
|
||||||
return Cookies.remove(TokenKey)
|
return Cookies.remove(TokenKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Zinc Token管理
|
||||||
|
export function getZincToken() {
|
||||||
|
return Cookies.get(ZincTokenKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setZincToken(token) {
|
||||||
|
return Cookies.set(ZincTokenKey, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeZincToken() {
|
||||||
|
return Cookies.remove(ZincTokenKey)
|
||||||
|
}
|
||||||
|
|||||||
142
klp-ui/src/utils/zinc1Request.js
Normal file
142
klp-ui/src/utils/zinc1Request.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { Notification, MessageBox, Message } from 'element-ui'
|
||||||
|
import store from '@/store'
|
||||||
|
import { getZincToken } from '@/utils/auth'
|
||||||
|
import errorCode from '@/utils/errorCode'
|
||||||
|
import { tansParams } from "@/utils/klp";
|
||||||
|
import cache from '@/plugins/cache'
|
||||||
|
// 是否显示重新登录
|
||||||
|
export let isRelogin = { show: false };
|
||||||
|
|
||||||
|
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
|
||||||
|
// 对应国际化资源文件后缀
|
||||||
|
axios.defaults.headers['Content-Language'] = 'zh_CN'
|
||||||
|
// 创建axios实例
|
||||||
|
const service = axios.create({
|
||||||
|
// axios中请求配置有baseURL选项,表示请求URL公共部分
|
||||||
|
baseURL: '/zinc-api',
|
||||||
|
// 超时
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
|
||||||
|
// request拦截器
|
||||||
|
service.interceptors.request.use(config => {
|
||||||
|
// 是否需要设置 token
|
||||||
|
const isToken = (config.headers || {}).isToken === false
|
||||||
|
// 是否需要防止数据重复提交
|
||||||
|
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
|
||||||
|
if (getZincToken() && !isToken) {
|
||||||
|
config.headers['Authorization'] = 'Bearer ' + getZincToken() // 让每个请求携带自定义token 请根据实际情况自行修改
|
||||||
|
}
|
||||||
|
// get请求映射params参数
|
||||||
|
if (config.method === 'get' && config.params) {
|
||||||
|
let url = config.url + '?' + tansParams(config.params);
|
||||||
|
url = url.slice(0, -1);
|
||||||
|
config.params = {};
|
||||||
|
config.url = url;
|
||||||
|
}
|
||||||
|
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
|
||||||
|
// 对于盖章请求,添加特殊处理确保 0 值不被过滤
|
||||||
|
if (config.url && config.url.includes('/stamp/java')) {
|
||||||
|
console.log('Stamp request - original data:', JSON.stringify(config.data, null, 2))
|
||||||
|
if (config.data && typeof config.data === 'object') {
|
||||||
|
// 确保 yPx 和 xPx 即使是 0 也被正确包含
|
||||||
|
// 创建一个新对象,确保所有值都被正确设置
|
||||||
|
const cleanData = {
|
||||||
|
targetFileUrl: String(config.data.targetFileUrl || ''),
|
||||||
|
stampImageUrl: String(config.data.stampImageUrl || ''),
|
||||||
|
pageNo: Number(config.data.pageNo) || 1,
|
||||||
|
xPx: Number(config.data.xPx) || 0,
|
||||||
|
yPx: Number(config.data.yPx) || 0
|
||||||
|
}
|
||||||
|
if (config.data.widthPx !== undefined && config.data.widthPx !== null) {
|
||||||
|
cleanData.widthPx = Number(config.data.widthPx)
|
||||||
|
}
|
||||||
|
if (config.data.heightPx !== undefined && config.data.heightPx !== null) {
|
||||||
|
cleanData.heightPx = Number(config.data.heightPx)
|
||||||
|
}
|
||||||
|
console.log('Stamp request - cleaned data:', JSON.stringify(cleanData, null, 2))
|
||||||
|
console.log('yPx in cleaned data:', cleanData.yPx, typeof cleanData.yPx)
|
||||||
|
config.data = cleanData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const requestObj = {
|
||||||
|
url: config.url,
|
||||||
|
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
|
||||||
|
time: new Date().getTime()
|
||||||
|
}
|
||||||
|
const sessionObj = cache.session.getJSON('sessionObj')
|
||||||
|
if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
|
||||||
|
cache.session.setJSON('sessionObj', requestObj)
|
||||||
|
} else {
|
||||||
|
const s_url = sessionObj.url; // 请求地址
|
||||||
|
const s_data = sessionObj.data; // 请求数据
|
||||||
|
const s_time = sessionObj.time; // 请求时间
|
||||||
|
const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交
|
||||||
|
if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
|
||||||
|
const message = '数据正在处理,请勿重复提交';
|
||||||
|
console.warn(`[${s_url}]: ` + message)
|
||||||
|
return Promise.reject(new Error(message))
|
||||||
|
} else {
|
||||||
|
cache.session.setJSON('sessionObj', requestObj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}, error => {
|
||||||
|
console.log(error)
|
||||||
|
Promise.reject(error)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
service.interceptors.response.use(res => {
|
||||||
|
// 未设置状态码则默认成功状态
|
||||||
|
const code = res.data.code || 200;
|
||||||
|
// 获取错误信息
|
||||||
|
const msg = errorCode[code] || res.data.msg || errorCode['default']
|
||||||
|
// 二进制数据则直接返回
|
||||||
|
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
if (code === 401) {
|
||||||
|
if (!isRelogin.show) {
|
||||||
|
isRelogin.show = true;
|
||||||
|
MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
|
||||||
|
isRelogin.show = false;
|
||||||
|
store.dispatch('LogOut').then(() => {
|
||||||
|
location.href = process.env.VUE_APP_CONTEXT_PATH + "index";
|
||||||
|
})
|
||||||
|
}).catch(() => {
|
||||||
|
isRelogin.show = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
|
||||||
|
} else if (code === 500) {
|
||||||
|
Message({ message: msg, type: 'error' })
|
||||||
|
return Promise.reject(new Error(msg))
|
||||||
|
} else if (code === 601) {
|
||||||
|
Message({ message: msg, type: 'warning' })
|
||||||
|
return Promise.reject('error')
|
||||||
|
} else if (code !== 200) {
|
||||||
|
Notification.error({ title: msg })
|
||||||
|
return Promise.reject('error')
|
||||||
|
} else {
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
console.log('err' + error)
|
||||||
|
let { message } = error;
|
||||||
|
if (message == "Network Error") {
|
||||||
|
message = "后端接口连接异常";
|
||||||
|
} else if (message.includes("timeout")) {
|
||||||
|
message = "系统接口请求超时";
|
||||||
|
} else if (message.includes("Request failed with status code")) {
|
||||||
|
message = "系统接口" + message.substr(message.length - 3) + "异常";
|
||||||
|
}
|
||||||
|
Message({ message: message, type: 'error', duration: 5 * 1000 })
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default service
|
||||||
640
klp-ui/src/views/lines/acid/components/product-statistic.vue
Normal file
640
klp-ui/src/views/lines/acid/components/product-statistic.vue
Normal file
@@ -0,0 +1,640 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<!-- 时间维度切换:Element-UI 选项卡替代uniapp自定义tab -->
|
||||||
|
<!-- 时间维度切换 -->
|
||||||
|
<div class="time-tab-bar">
|
||||||
|
<div v-for="item in timeTabs" :key="item.value" class="time-tab-item"
|
||||||
|
:class="{ 'time-tab-active': activeTab === item.value }" @click="handleTabChange(item.value)">
|
||||||
|
{{ item.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 日期选择区:Element-UI 日期选择器替代uniapp picker -->
|
||||||
|
<div class="date-selector">
|
||||||
|
<!-- 日模式-单日期选择 -->
|
||||||
|
<el-date-picker v-if="activeTab === 'day'" v-model="startDate" type="date" value-format="yyyy-MM-dd"
|
||||||
|
placeholder="选择日期" @change="handleDateChange" class="single-date-picker" />
|
||||||
|
|
||||||
|
<!-- 月模式-月份范围选择 -->
|
||||||
|
<div v-else-if="activeTab === 'month'" class="date-range-group">
|
||||||
|
<el-date-picker v-model="startDate" type="month" value-format="yyyy-MM" placeholder="选择开始月份"
|
||||||
|
@change="handleStartMonthChange" class="range-date-picker" />
|
||||||
|
<span class="date-separator">至</span>
|
||||||
|
<el-date-picker v-model="endDate" type="month" value-format="yyyy-MM" placeholder="选择结束月份"
|
||||||
|
:picker-options="monthPickerOptions" @change="handleEndMonthChange" class="range-date-picker" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 年模式-年份范围选择 -->
|
||||||
|
<div v-else class="date-range-group">
|
||||||
|
<el-date-picker v-model="startDate" type="year" value-format="yyyy" placeholder="选择开始年份"
|
||||||
|
@change="handleStartYearChange" class="range-date-picker" />
|
||||||
|
<span class="date-separator">至</span>
|
||||||
|
<el-date-picker v-model="endDate" type="year" value-format="yyyy" placeholder="选择结束年份"
|
||||||
|
@change="handleEndYearChange" class="range-date-picker" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 生产汇总数据 -->
|
||||||
|
<div class="summary-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">生产汇总</span>
|
||||||
|
<span class="section-date">{{ displayDateRange }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-grid">
|
||||||
|
<div class="summary-card" v-for="(item, index) in summaryData" :key="index">
|
||||||
|
<span class="summary-label">{{ item.label }}</span>
|
||||||
|
<div class="summary-value-box">
|
||||||
|
<span class="summary-value">{{ item.value }}</span>
|
||||||
|
<span v-if="item.unit" class="summary-unit">{{ item.unit }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<!-- 产量趋势图:Echarts容器 -->
|
||||||
|
<div class="chart-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">产量趋势</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper" ref="productionChart" id="productionChart"></div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<!-- 班组产量对比:Echarts容器 -->
|
||||||
|
<div class="chart-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">班组产量对比</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper" ref="crewChart" id="crewChart"></div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 规格分布:Echarts饼图容器 -->
|
||||||
|
<div class="chart-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">规格分布</span>
|
||||||
|
</div>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="pie-charts-row">
|
||||||
|
<div class="pie-chart-item">
|
||||||
|
<span class="pie-title">厚度分布</span>
|
||||||
|
<div class="chart-wrapper" ref="thicknessPie" id="thicknessPie"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="pie-charts-row">
|
||||||
|
<div class="pie-chart-item">
|
||||||
|
<span class="pie-title">宽度分布</span>
|
||||||
|
<div class="chart-wrapper" ref="widthPie" id="widthPie"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ✅ 【完全未改动】原API导入保持不变
|
||||||
|
import {
|
||||||
|
getProductionSummary,
|
||||||
|
getCrewProduction,
|
||||||
|
getThicknessDistribution,
|
||||||
|
getWidthDistribution
|
||||||
|
} from '@/api/pocket/plantState'
|
||||||
|
// 引入Echarts
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
|
// ✅ 【完全未改动】原工具函数
|
||||||
|
function getDefaultDate(type = "day") {
|
||||||
|
const date = new Date();
|
||||||
|
return formatDate(date, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(date, type) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||||
|
const day = date.getDate().toString().padStart(2, "0");
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "day": return `${year}-${month}-${day}`;
|
||||||
|
case "month": return `${year}-${month}`;
|
||||||
|
case "year": return `${year}`;
|
||||||
|
default: return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PlantProduction',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeTab: "day",
|
||||||
|
startDate: getDefaultDate(),
|
||||||
|
endDate: getDefaultDate(),
|
||||||
|
// Echarts实例存储
|
||||||
|
productionChart: null,
|
||||||
|
crewChart: null,
|
||||||
|
thicknessPie: null,
|
||||||
|
widthPie: null,
|
||||||
|
// 月份选择器限制条件
|
||||||
|
monthPickerOptions: {},
|
||||||
|
timeTabs: [
|
||||||
|
{ label: "日", value: "day" },
|
||||||
|
{ label: "月", value: "month" },
|
||||||
|
{ label: "年", value: "year" }
|
||||||
|
],
|
||||||
|
// ✅ 【完全未改动】业务数据
|
||||||
|
summaryData: [
|
||||||
|
{ label: "生产钢卷数", value: 0, unit: "卷" },
|
||||||
|
{ label: "平均宽度", value: 0, unit: "mm" },
|
||||||
|
{ label: "平均厚度", value: 0, unit: "mm" },
|
||||||
|
{ label: "原料总量", value: 0, unit: "t" },
|
||||||
|
{ label: "成品总量", value: 0, unit: "t" },
|
||||||
|
{ label: "成材率", value: 0, unit: "%" }
|
||||||
|
],
|
||||||
|
// Echarts配色沿用原配色
|
||||||
|
mainColor: ["#0066cc", "#00b96b"],
|
||||||
|
columnColor: ["#0066cc", "#409eff", "#66b1ff"],
|
||||||
|
pieColor: ["#0066cc", "#409eff", "#66b1ff", "#a0cfff", "#d9ecff"]
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// ✅ 【完全未改动】计算属性
|
||||||
|
computed: {
|
||||||
|
maxMonthEnd() {
|
||||||
|
if (!this.startDate) return "";
|
||||||
|
const date = new Date(this.startDate);
|
||||||
|
date.setFullYear(date.getFullYear() + 1);
|
||||||
|
return formatDate(date, "month");
|
||||||
|
},
|
||||||
|
displayDateRange() {
|
||||||
|
switch (this.activeTab) {
|
||||||
|
case "day":
|
||||||
|
return this.startDate;
|
||||||
|
case "month":
|
||||||
|
return `${this.startDate} 至 ${this.endDate}`;
|
||||||
|
case "year":
|
||||||
|
return `${this.startDate} 至 ${this.endDate}`;
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// 监听月份开始值,更新结束月份的可选范围
|
||||||
|
startDate(val) {
|
||||||
|
if (this.activeTab === 'month' && val) {
|
||||||
|
this.monthPickerOptions = {
|
||||||
|
disabledDate: (time) => {
|
||||||
|
const maxDate = new Date(this.maxMonthEnd + '-01')
|
||||||
|
return time.getTime() > maxDate.getTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// 初始化Echarts图表
|
||||||
|
this.initCharts()
|
||||||
|
// 加载业务数据
|
||||||
|
this.loadProductionData()
|
||||||
|
// 窗口自适应
|
||||||
|
window.addEventListener('resize', this.resizeCharts)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
// 销毁Echarts实例,防止内存泄漏
|
||||||
|
window.removeEventListener('resize', this.resizeCharts)
|
||||||
|
this.productionChart && this.productionChart.dispose()
|
||||||
|
this.crewChart && this.crewChart.dispose()
|
||||||
|
this.thicknessPie && this.thicknessPie.dispose()
|
||||||
|
this.widthPie && this.widthPie.dispose()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// ✅ 【逻辑未改动】仅适配Element的tab参数
|
||||||
|
handleTabChange(tab) {
|
||||||
|
this.activeTab = tab;
|
||||||
|
const defaultDate = getDefaultDate();
|
||||||
|
this.startDate = defaultDate;
|
||||||
|
this.endDate = tab === "day" ? defaultDate : getDefaultDate(tab);
|
||||||
|
this.loadProductionData();
|
||||||
|
},
|
||||||
|
// ✅ 【逻辑未改动】日期变更事件
|
||||||
|
handleDateChange(val) {
|
||||||
|
this.startDate = val;
|
||||||
|
this.endDate = val;
|
||||||
|
this.loadProductionData();
|
||||||
|
},
|
||||||
|
handleStartMonthChange(val) {
|
||||||
|
this.startDate = val;
|
||||||
|
const maxEndDate = new Date(this.startDate);
|
||||||
|
maxEndDate.setFullYear(maxEndDate.getFullYear() + 1);
|
||||||
|
const maxEndStr = formatDate(maxEndDate, "month");
|
||||||
|
if (new Date(this.endDate) > maxEndDate) {
|
||||||
|
this.endDate = maxEndStr;
|
||||||
|
}
|
||||||
|
this.loadProductionData();
|
||||||
|
},
|
||||||
|
handleEndMonthChange(val) {
|
||||||
|
this.endDate = val;
|
||||||
|
this.loadProductionData();
|
||||||
|
},
|
||||||
|
handleStartYearChange(val) {
|
||||||
|
this.startDate = val;
|
||||||
|
this.loadProductionData();
|
||||||
|
},
|
||||||
|
handleEndYearChange(val) {
|
||||||
|
this.endDate = val;
|
||||||
|
this.loadProductionData();
|
||||||
|
},
|
||||||
|
|
||||||
|
// ✅ 【核心逻辑未改动】仅替换uni加载提示为Element
|
||||||
|
loadProductionData() {
|
||||||
|
const loading = this.$loading({
|
||||||
|
lock: true,
|
||||||
|
text: '加载中',
|
||||||
|
spinner: 'el-icon-loading',
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)'
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
this.loadSummaryData(),
|
||||||
|
this.loadCrewProductionData(),
|
||||||
|
this.loadSpecDistribution()
|
||||||
|
]).finally(() => {
|
||||||
|
loading.close()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// ✅ ✅ ✅ 【完全未改动】汇总数据加载
|
||||||
|
loadSummaryData() {
|
||||||
|
const start = this.formatFullDate(this.startDate, true)
|
||||||
|
const end = this.formatFullDate(this.endDate, false)
|
||||||
|
|
||||||
|
return getProductionSummary(start, end).then(response => {
|
||||||
|
if (response.code === 200 && response.data) {
|
||||||
|
const data = response.data
|
||||||
|
this.summaryData = [
|
||||||
|
{ label: "生产钢卷数", value: data.coilCount || 0, unit: "卷" },
|
||||||
|
{ label: "平均宽度", value: data.avgWidth || 0, unit: "mm" },
|
||||||
|
{ label: "平均厚度", value: data.avgThick || 0, unit: "mm" },
|
||||||
|
{ label: "原料总量", value: data.totalEntryWeight || 0, unit: "t" },
|
||||||
|
{ label: "成品总量", value: data.totalExitWeight || 0, unit: "t" },
|
||||||
|
{ label: "成材率", value: data.yieldRate || 0, unit: "%" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('加载汇总数据失败:', error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// ✅ ✅ ✅ 【完全未改动】班组产量加载+图表数据组装
|
||||||
|
loadCrewProductionData() {
|
||||||
|
const start = this.formatFullDate(this.startDate, true)
|
||||||
|
const end = this.formatFullDate(this.endDate, false)
|
||||||
|
|
||||||
|
return getCrewProduction(start, end).then(response => {
|
||||||
|
if (response.code === 200 && response.data && response.data.length > 0) {
|
||||||
|
const crewData = response.data
|
||||||
|
|
||||||
|
const categories = []
|
||||||
|
const crewMap = {}
|
||||||
|
|
||||||
|
crewData.forEach(item => {
|
||||||
|
const label = `${item.crew}-${item.shift}`
|
||||||
|
categories.push(label)
|
||||||
|
|
||||||
|
if (!crewMap[item.shift]) {
|
||||||
|
crewMap[item.shift] = []
|
||||||
|
}
|
||||||
|
crewMap[item.shift].push(Number(item.totalWeight) || 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const series = Object.keys(crewMap).map(shift => ({
|
||||||
|
name: shift,
|
||||||
|
data: crewMap[shift],
|
||||||
|
type: 'bar'
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 渲染图表
|
||||||
|
this.setCrewChart(series.map(item => item.name), series)
|
||||||
|
this.setProductionChart(
|
||||||
|
categories,
|
||||||
|
crewData.map(item => Number(item.totalWeight) || 0),
|
||||||
|
crewData.map(item => Number(item.avgThick) || 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('加载班组产量失败:', error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// ✅ ✅ ✅ 【完全未改动】规格分布加载
|
||||||
|
loadSpecDistribution() {
|
||||||
|
const start = this.formatFullDate(this.startDate, true)
|
||||||
|
const end = this.formatFullDate(this.endDate, false)
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
getThicknessDistribution(start, end),
|
||||||
|
getWidthDistribution(start, end)
|
||||||
|
]).then(([thicknessRes, widthRes]) => {
|
||||||
|
if (thicknessRes.code === 200 && thicknessRes.data) {
|
||||||
|
const pieData = thicknessRes.data.map(item => ({
|
||||||
|
name: item.category,
|
||||||
|
value: item.count
|
||||||
|
}))
|
||||||
|
this.setThicknessPie(pieData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widthRes.code === 200 && widthRes.data) {
|
||||||
|
const pieData = widthRes.data.map(item => ({
|
||||||
|
name: item.category,
|
||||||
|
value: item.count
|
||||||
|
}))
|
||||||
|
this.setWidthPie(pieData)
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('加载规格分布失败:', error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// ✅ 【完全未改动】日期格式化工具
|
||||||
|
formatFullDate(dateStr, isStart) {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
if (dateStr.length === 10) {
|
||||||
|
return dateStr
|
||||||
|
}
|
||||||
|
if (dateStr.length === 7) {
|
||||||
|
if (isStart) {
|
||||||
|
return `${dateStr}-01`
|
||||||
|
} else {
|
||||||
|
const [year, month] = dateStr.split('-')
|
||||||
|
const lastDay = new Date(year, month, 0).getDate()
|
||||||
|
return `${dateStr}-${String(lastDay).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dateStr.length === 4) {
|
||||||
|
if (isStart) {
|
||||||
|
return `${dateStr}-01-01`
|
||||||
|
} else {
|
||||||
|
return `${dateStr}-12-31`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dateStr
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== Echarts 图表初始化/渲染/自适应 新增方法 =====
|
||||||
|
initCharts() {
|
||||||
|
// 产量趋势图-混合图(柱形+折线)
|
||||||
|
this.productionChart = echarts.init(document.getElementById('productionChart'))
|
||||||
|
// 班组产量-柱状图
|
||||||
|
this.crewChart = echarts.init(document.getElementById('crewChart'))
|
||||||
|
// 厚度分布-饼图
|
||||||
|
this.thicknessPie = echarts.init(document.getElementById('thicknessPie'))
|
||||||
|
// 宽度分布-饼图
|
||||||
|
this.widthPie = echarts.init(document.getElementById('widthPie'))
|
||||||
|
},
|
||||||
|
// 产量趋势图渲染
|
||||||
|
setProductionChart(xData, prodData, thickData) {
|
||||||
|
const option = {
|
||||||
|
color: this.mainColor,
|
||||||
|
grid: { left: 30, right: 30, top: 40, bottom: 30 },
|
||||||
|
legend: { top: 0, left: 'center' },
|
||||||
|
xAxis: { type: 'category', data: xData, axisLine: { show: false } },
|
||||||
|
yAxis: [
|
||||||
|
{ type: 'value', name: '产量(t)', position: 'left', splitLine: { type: 'dashed' } },
|
||||||
|
{ type: 'value', name: '平均厚度(mm)', position: 'right', splitLine: { show: false } }
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{ name: '产量', type: 'bar', data: prodData, barWidth: 20 },
|
||||||
|
{ name: '平均厚度', type: 'line', yAxisIndex: 1, data: thickData, smooth: true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
this.productionChart.setOption(option)
|
||||||
|
},
|
||||||
|
// 班组产量柱状图渲染
|
||||||
|
setCrewChart(xData, seriesData) {
|
||||||
|
console.log(seriesData, xData)
|
||||||
|
const option = {
|
||||||
|
color: this.columnColor,
|
||||||
|
grid: { left: 30, right: 30, top: 40, bottom: 30 },
|
||||||
|
legend: { top: 0, left: 'center' },
|
||||||
|
xAxis: { type: 'category', data: xData, axisLine: { show: false } },
|
||||||
|
yAxis: { type: 'value', name: '产量(t)', splitLine: { type: 'dashed' } },
|
||||||
|
series: seriesData
|
||||||
|
}
|
||||||
|
this.crewChart.setOption(option)
|
||||||
|
},
|
||||||
|
// 厚度饼图渲染
|
||||||
|
setThicknessPie(pieData) {
|
||||||
|
this.setPieOption(this.thicknessPie, pieData)
|
||||||
|
},
|
||||||
|
// 宽度饼图渲染
|
||||||
|
setWidthPie(pieData) {
|
||||||
|
this.setPieOption(this.widthPie, pieData)
|
||||||
|
},
|
||||||
|
// 饼图公共配置
|
||||||
|
setPieOption(chart, data) {
|
||||||
|
const option = {
|
||||||
|
color: this.pieColor,
|
||||||
|
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
|
||||||
|
legend: { bottom: 0, left: 'center', textStyle: { fontSize: 12 } },
|
||||||
|
series: [{ name: '分布占比', type: 'pie', radius: ['40%', '70%'], data }]
|
||||||
|
}
|
||||||
|
chart.setOption(option)
|
||||||
|
},
|
||||||
|
// 图表自适应
|
||||||
|
resizeCharts() {
|
||||||
|
this.productionChart && this.productionChart.resize()
|
||||||
|
this.crewChart && this.crewChart.resize()
|
||||||
|
this.thicknessPie && this.thicknessPie.resize()
|
||||||
|
this.widthPie && this.widthPie.resize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
// PC端样式适配:rpx转px(1rpx=0.5px),保留原有布局逻辑
|
||||||
|
.page-container {
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 12px;
|
||||||
|
min-height: calc(100vh - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-tab-bar {
|
||||||
|
display: flex;
|
||||||
|
width: 240px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-tab-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-tab-active {
|
||||||
|
background: #0066cc;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日期选择区
|
||||||
|
.date-selector {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
margin-left: 20px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-date-picker {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-date-picker {
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 3px 0 0 3px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-radius: 0 3px 3px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-separator {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 区块通用样式
|
||||||
|
.summary-section,
|
||||||
|
.chart-section {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-left: 8px;
|
||||||
|
border-left: 2px solid #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-date {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 汇总卡片网格
|
||||||
|
.summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 14px 10px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0066cc;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-unit {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #909399;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图表容器
|
||||||
|
.chart-wrapper {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px 8px;
|
||||||
|
min-height: 225px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 饼图布局
|
||||||
|
.pie-charts-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-chart-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pie-title {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
837
klp-ui/src/views/lines/acid/components/real-time-monitoring.vue
Normal file
837
klp-ui/src/views/lines/acid/components/real-time-monitoring.vue
Normal file
@@ -0,0 +1,837 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<!-- 刷新按钮(固定在右下角,保留) -->
|
||||||
|
<div class="refresh-btn-fixed" @click="refreshData">
|
||||||
|
<span class="refresh-icon" :class="{ 'rotating': isRefreshing }">⟳</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 快速导航菜单(固定在左下角,保留全部导航项) -->
|
||||||
|
<div class="nav-menu-fixed" :class="{ 'nav-expanded': navMenuExpanded }">
|
||||||
|
<div class="nav-toggle" @click="navMenuExpanded = !navMenuExpanded">
|
||||||
|
<span class="nav-toggle-icon">{{ navMenuExpanded ? '✕' : '☰' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-items" v-if="navMenuExpanded">
|
||||||
|
<div class="nav-item" @click="scrollToSection('speed-monitor')">
|
||||||
|
<span class="nav-label">速度监控</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="scrollToSection('exit-speed-chart')">
|
||||||
|
<span class="nav-label">出口速度趋势</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="scrollToSection('temp-chart')">
|
||||||
|
<span class="nav-label">酸槽温度趋势</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="scrollToSection('looper-status')">
|
||||||
|
<span class="nav-label">活套运行状态</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="scrollToSection('tank-concentration')">
|
||||||
|
<span class="nav-label">酸槽浓度监控</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="scrollToSection('force-chart')">
|
||||||
|
<span class="nav-label">轧制力趋势</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="scrollToSection('process-params')">
|
||||||
|
<span class="nav-label">工艺参数</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="scrollToSection('roll-speed')">
|
||||||
|
<span class="nav-label">轧辊速度监控</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="scrollToSection('reduc-rate')">
|
||||||
|
<span class="nav-label">机架压下率</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="scrollToSection('tension-monitor')">
|
||||||
|
<span class="nav-label">带钢张力监控</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="scrollToSection('power-ratio')">
|
||||||
|
<span class="nav-label">机架功率百分比</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="scrollToSection('paint-speed')">
|
||||||
|
<span class="nav-label">涂装速度监控</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="scrollToSection('tlv-params')">
|
||||||
|
<span class="nav-label">拉矫参数</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item" @click="scrollToSection('paint-temp-chart')">
|
||||||
|
<span class="nav-label">烘干温度趋势</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 核心:纯实时监控滚动容器 【移除tab判断,直接展示】 -->
|
||||||
|
<div class="scroll-container">
|
||||||
|
<!-- 顶部状态栏 -->
|
||||||
|
<div class="status-bar">
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">网络状态</span>
|
||||||
|
<span class="status-value" :class="'status-' + webStatus[0].value">{{ webStatus[0].value }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-divider"></div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">当前班组</span>
|
||||||
|
<span class="status-value">{{ webStatus[1].value }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-divider"></div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">更新时间</span>
|
||||||
|
<span class="status-value status-time">{{ lastUpdateTime }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 速度监控 -->
|
||||||
|
<div class="section" id="speed-monitor">
|
||||||
|
<div class="section-title">速度监控</div>
|
||||||
|
<div class="metrics-grid-3">
|
||||||
|
<div class="metric-box" v-for="(item, index) in speedMetrics" :key="index">
|
||||||
|
<span class="metric-name">{{ item.label }}</span>
|
||||||
|
<span class="metric-value">{{ item.value }}</span>
|
||||||
|
<span class="metric-unit">{{ item.unit }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 出口速度趋势 -->
|
||||||
|
<div class="section" id="exit-speed-chart">
|
||||||
|
<div class="section-title">出口速度趋势</div>
|
||||||
|
<div class="chart-box" ref="exitSpeedChart" id="exitSpeedChart"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 酸槽温度趋势 -->
|
||||||
|
<div class="section" id="temp-chart">
|
||||||
|
<div class="section-title">酸槽温度趋势</div>
|
||||||
|
<div class="chart-box" ref="tempChart" id="tempChart"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 活套运行状态 -->
|
||||||
|
<div class="section" id="looper-status">
|
||||||
|
<div class="section-title">活套运行状态</div>
|
||||||
|
<div class="metrics-grid-3">
|
||||||
|
<div class="metric-box" v-for="(item, index) in looperMetrics" :key="index">
|
||||||
|
<span class="metric-name">{{ item.label }}</span>
|
||||||
|
<span class="metric-value">{{ item.value }}</span>
|
||||||
|
<span class="metric-unit">{{ item.unit }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 酸槽浓度监控 -->
|
||||||
|
<div class="section" id="tank-concentration">
|
||||||
|
<div class="section-title">酸槽浓度监控</div>
|
||||||
|
<div class="tank-grid">
|
||||||
|
<div class="tank-card" v-for="(tank, index) in tankConcentration" :key="index">
|
||||||
|
<div class="tank-header">{{ tank.name }}</div>
|
||||||
|
<div class="tank-data">
|
||||||
|
<div class="tank-row">
|
||||||
|
<span class="data-label">酸浓度</span>
|
||||||
|
<span class="data-value">{{ tank.hclCont }} <span class="data-unit">g/L</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="tank-divider"></div>
|
||||||
|
<div class="tank-row">
|
||||||
|
<span class="data-label">铁盐浓度</span>
|
||||||
|
<span class="data-value">{{ tank.feCont }} <span class="data-unit">g/L</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 轧制力趋势 -->
|
||||||
|
<div class="section" id="force-chart">
|
||||||
|
<div class="section-title">轧制力趋势</div>
|
||||||
|
<div class="chart-box" ref="forceChart" id="forceChart"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 工艺参数 -->
|
||||||
|
<div class="section" id="process-params">
|
||||||
|
<div class="section-title">工艺参数</div>
|
||||||
|
<div class="metrics-grid-2">
|
||||||
|
<div class="metric-box" v-for="(item, index) in processMetrics" :key="index">
|
||||||
|
<span class="metric-name">{{ item.label }}</span>
|
||||||
|
<span class="metric-value">{{ item.value }}</span>
|
||||||
|
<span class="metric-unit">{{ item.unit }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ 镀锌线数据 ============ -->
|
||||||
|
<div class="section-divider">
|
||||||
|
<span class="divider-text">镀锌线监控数据</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 轧辊速度监控 -->
|
||||||
|
<div class="section" id="roll-speed">
|
||||||
|
<div class="section-title">轧辊速度监控</div>
|
||||||
|
<div class="metrics-grid-3">
|
||||||
|
<div class="metric-box" v-for="(item, index) in rollSpeedMetrics" :key="index">
|
||||||
|
<span class="metric-name">{{ item.label }}</span>
|
||||||
|
<span class="metric-value">{{ item.value }}</span>
|
||||||
|
<span class="metric-unit">{{ item.unit }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 机架压下率 -->
|
||||||
|
<div class="section" id="reduc-rate">
|
||||||
|
<div class="section-title">机架压下率</div>
|
||||||
|
<div class="metrics-grid-3">
|
||||||
|
<div class="metric-box" v-for="(item, index) in reducMetrics" :key="index">
|
||||||
|
<span class="metric-name">{{ item.label }}</span>
|
||||||
|
<span class="metric-value">{{ item.value }}</span>
|
||||||
|
<span class="metric-unit">{{ item.unit }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 带钢张力监控 -->
|
||||||
|
<div class="section" id="tension-monitor">
|
||||||
|
<div class="section-title">带钢张力监控</div>
|
||||||
|
<div class="metrics-grid-3">
|
||||||
|
<div class="metric-box" v-for="(item, index) in tensionMetrics" :key="index">
|
||||||
|
<span class="metric-name">{{ item.label }}</span>
|
||||||
|
<span class="metric-value">{{ item.value }}</span>
|
||||||
|
<span class="metric-unit">{{ item.unit }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 机架功率百分比 -->
|
||||||
|
<div class="section" id="power-ratio">
|
||||||
|
<div class="section-title">机架功率百分比</div>
|
||||||
|
<div class="metrics-grid-3">
|
||||||
|
<div class="metric-box" v-for="(item, index) in powerMetrics" :key="index">
|
||||||
|
<span class="metric-name">{{ item.label }}</span>
|
||||||
|
<span class="metric-value">{{ item.value }}</span>
|
||||||
|
<span class="metric-unit">{{ item.unit }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ 涂装线数据 ============ -->
|
||||||
|
<div class="section-divider">
|
||||||
|
<span class="divider-text">涂装线监控数据</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 涂装速度监控 -->
|
||||||
|
<div class="section" id="paint-speed">
|
||||||
|
<div class="section-title">涂装速度监控</div>
|
||||||
|
<div class="metrics-grid-3">
|
||||||
|
<div class="metric-box" v-for="(item, index) in paintSpeedMetrics" :key="index">
|
||||||
|
<span class="metric-name">{{ item.label }}</span>
|
||||||
|
<span class="metric-value">{{ item.value }}</span>
|
||||||
|
<span class="metric-unit">{{ item.unit }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 拉矫参数 -->
|
||||||
|
<div class="section" id="tlv-params">
|
||||||
|
<div class="section-title">拉矫参数</div>
|
||||||
|
<div class="metrics-grid-2">
|
||||||
|
<div class="metric-box" v-for="(item, index) in tlvMetrics" :key="index">
|
||||||
|
<span class="metric-name">{{ item.label }}</span>
|
||||||
|
<span class="metric-value">{{ item.value }}</span>
|
||||||
|
<span class="metric-unit">{{ item.unit }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 烘干温度趋势 -->
|
||||||
|
<div class="section" id="paint-temp-chart">
|
||||||
|
<div class="section-title">烘干温度趋势</div>
|
||||||
|
<div class="chart-box" ref="paintTempChart" id="paintTempChart"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ✅ 原接口导入 完全未改动
|
||||||
|
import { getAllPlantStateDefines, listPlantStateHistory, getCurrentShift } from '@/api/pocket/plantState'
|
||||||
|
// ✅ 引入Echarts5.x 兼容Vue2
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
// ✅ 引入axios替换uni.request (PC端网络请求)
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RealTimeMonitor',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
webStatus: [
|
||||||
|
{ label: '网络状态', value: '检测中...' },
|
||||||
|
{ label: '当前班组', value: '—' }
|
||||||
|
],
|
||||||
|
lastUpdateTime: '—', // 最后更新时间
|
||||||
|
isRefreshing: false, // 是否正在刷新
|
||||||
|
refreshTimer: null, // 定时器
|
||||||
|
navMenuExpanded: false, // 导航菜单是否展开
|
||||||
|
// ✅ 移除了 tabData/currentTab 无用属性
|
||||||
|
// 速度监控指标(ID=1,2,3)
|
||||||
|
speedMetrics: [
|
||||||
|
{ label: '出口带钢速度', value: '—', unit: 'm/min' },
|
||||||
|
{ label: '酸洗带钢速度', value: '—', unit: 'm/min' },
|
||||||
|
{ label: '圆盘剪速度', value: '—', unit: 'm/min' }
|
||||||
|
],
|
||||||
|
// 活套状态(ID=8,9,10)
|
||||||
|
looperMetrics: [
|
||||||
|
{ label: '入口活套', value: '—', unit: '%' },
|
||||||
|
{ label: '出口活套', value: '—', unit: '%' },
|
||||||
|
{ label: '联机活套', value: '—', unit: '%' }
|
||||||
|
],
|
||||||
|
// 酸槽浓度(ID=11-16)
|
||||||
|
tankConcentration: [
|
||||||
|
{ name: '1#酸槽', hclCont: '—', feCont: '—' },
|
||||||
|
{ name: '2#酸槽', hclCont: '—', feCont: '—' },
|
||||||
|
{ name: '3#酸槽', hclCont: '—', feCont: '—' }
|
||||||
|
],
|
||||||
|
// 其他工艺参数(ID=7,17,18,19,20)
|
||||||
|
processMetrics: [
|
||||||
|
{ label: '漂洗温度', value: '—', unit: '°C' },
|
||||||
|
{ label: '烘干温度', value: '—', unit: '°C' },
|
||||||
|
{ label: '漂洗电导率', value: '—', unit: 'g/L' },
|
||||||
|
{ label: '联机活套张力', value: '—', unit: 'kN' },
|
||||||
|
{ label: '拉矫机延伸率', value: '—', unit: '%' }
|
||||||
|
],
|
||||||
|
// 镀锌线数据
|
||||||
|
rollSpeedMetrics: [
|
||||||
|
{ label: '1#机架', value: '—', unit: 'm/min' },
|
||||||
|
{ label: '2#机架', value: '—', unit: 'm/min' },
|
||||||
|
{ label: '3#机架', value: '—', unit: 'm/min' },
|
||||||
|
{ label: '4#机架', value: '—', unit: 'm/min' },
|
||||||
|
{ label: '5#机架', value: '—', unit: 'm/min' },
|
||||||
|
{ label: '6#机架', value: '—', unit: 'm/min' }
|
||||||
|
],
|
||||||
|
reducMetrics: [
|
||||||
|
{ label: '1#机架', value: '—', unit: '%' },
|
||||||
|
{ label: '2#机架', value: '—', unit: '%' },
|
||||||
|
{ label: '3#机架', value: '—', unit: '%' },
|
||||||
|
{ label: '4#机架', value: '—', unit: '%' },
|
||||||
|
{ label: '5#机架', value: '—', unit: '%' },
|
||||||
|
{ label: '6#机架', value: '—', unit: '%' }
|
||||||
|
],
|
||||||
|
tensionMetrics: [
|
||||||
|
{ label: '0#张力', value: '—', unit: 'kN' },
|
||||||
|
{ label: '1#张力', value: '—', unit: 'kN' },
|
||||||
|
{ label: '2#张力', value: '—', unit: 'kN' },
|
||||||
|
{ label: '3#张力', value: '—', unit: 'kN' },
|
||||||
|
{ label: '4#张力', value: '—', unit: 'kN' },
|
||||||
|
{ label: '5#张力', value: '—', unit: 'kN' },
|
||||||
|
{ label: '6#张力', value: '—', unit: 'kN' }
|
||||||
|
],
|
||||||
|
powerMetrics: [
|
||||||
|
{ label: '1#机架', value: '—', unit: '%' },
|
||||||
|
{ label: '2#机架', value: '—', unit: '%' },
|
||||||
|
{ label: '3#机架', value: '—', unit: '%' },
|
||||||
|
{ label: '4#机架', value: '—', unit: '%' },
|
||||||
|
{ label: '5#机架', value: '—', unit: '%' },
|
||||||
|
{ label: '6#机架', value: '—', unit: '%' }
|
||||||
|
],
|
||||||
|
// 涂装线数据
|
||||||
|
paintSpeedMetrics: [
|
||||||
|
{ label: '出口带钢速度', value: '—', unit: 'm/min' },
|
||||||
|
{ label: '涂装带钢速度', value: '—', unit: 'm/min' },
|
||||||
|
{ label: '圆盘剪速度', value: '—', unit: 'm/min' }
|
||||||
|
],
|
||||||
|
tlvMetrics: [
|
||||||
|
{ label: '拉矫延伸率', value: '—', unit: '%' },
|
||||||
|
{ label: '破磷机插入量1', value: '—', unit: 'mm' },
|
||||||
|
{ label: '破磷机插入量2', value: '—', unit: 'mm' },
|
||||||
|
{ label: '破磷机插入量3', value: '—', unit: 'mm' }
|
||||||
|
],
|
||||||
|
// Echarts图表实例
|
||||||
|
exitSpeedChart: null,
|
||||||
|
tempChart: null,
|
||||||
|
forceChart: null,
|
||||||
|
paintTempChart: null,
|
||||||
|
// 图表配色与原qiun一致
|
||||||
|
lineColor: ["#0066cc", "#409eff", "#66b1ff"],
|
||||||
|
forceColor: ["#0066cc", "#409eff", "#66b1ff", "#a0cfff", "#d9ecff", "#ecf5ff"],
|
||||||
|
paintColor: ["#0066cc", "#409eff", "#66b1ff", "#a0cfff", "#d9ecff"],
|
||||||
|
plantStateDefines: [] // 缓存所有的状态定义
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadAllData() // 加载所有数据
|
||||||
|
this.startAutoRefresh() // 启动自动刷新
|
||||||
|
this.initEcharts() // 初始化图表
|
||||||
|
window.addEventListener('resize', this.resizeEcharts) // 窗口自适应
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.stopAutoRefresh() // 页面销毁时清除定时器
|
||||||
|
window.removeEventListener('resize', this.resizeEcharts)
|
||||||
|
this.disposeEcharts() // 销毁图表实例 防内存泄漏
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// ✅ 初始化Echarts图表
|
||||||
|
initEcharts() {
|
||||||
|
this.exitSpeedChart = echarts.init(document.getElementById('exitSpeedChart'))
|
||||||
|
this.tempChart = echarts.init(document.getElementById('tempChart'))
|
||||||
|
this.forceChart = echarts.init(document.getElementById('forceChart'))
|
||||||
|
this.paintTempChart = echarts.init(document.getElementById('paintTempChart'))
|
||||||
|
},
|
||||||
|
// ✅ 图表自适应
|
||||||
|
resizeEcharts() {
|
||||||
|
this.exitSpeedChart && this.exitSpeedChart.resize()
|
||||||
|
this.tempChart && this.tempChart.resize()
|
||||||
|
this.forceChart && this.forceChart.resize()
|
||||||
|
this.paintTempChart && this.paintTempChart.resize()
|
||||||
|
},
|
||||||
|
// ✅ 销毁图表实例
|
||||||
|
disposeEcharts() {
|
||||||
|
this.exitSpeedChart && this.exitSpeedChart.dispose()
|
||||||
|
this.tempChart && this.tempChart.dispose()
|
||||||
|
this.forceChart && this.forceChart.dispose()
|
||||||
|
this.paintTempChart && this.paintTempChart.dispose()
|
||||||
|
},
|
||||||
|
// ✅ 渲染折线图通用方法
|
||||||
|
renderLineChart(chart, xData, seriesData, color, yTitle) {
|
||||||
|
if(!chart) return
|
||||||
|
const option = {
|
||||||
|
color: color,
|
||||||
|
grid: { left: 30, right: 30, top: 40, bottom: 30 },
|
||||||
|
tooltip: { trigger: 'axis' },
|
||||||
|
legend: { top: 0, left: 'center', textStyle: { fontSize: 12 } },
|
||||||
|
xAxis: { type: 'category', data: xData, axisLine: { show: false } },
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: yTitle,
|
||||||
|
splitLine: { type: 'dashed', color: '#e4e7ed' }
|
||||||
|
},
|
||||||
|
series: seriesData.map(item => ({
|
||||||
|
name: item.name,
|
||||||
|
type: 'line',
|
||||||
|
data: item.data,
|
||||||
|
smooth: true,
|
||||||
|
lineStyle: { width: 2 },
|
||||||
|
symbol: 'none'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
chart.setOption(option, true)
|
||||||
|
},
|
||||||
|
// 启动自动刷新(每30秒) 逻辑完全未改
|
||||||
|
startAutoRefresh() {
|
||||||
|
this.refreshTimer = setInterval(() => {
|
||||||
|
console.log('自动刷新数据...')
|
||||||
|
this.refreshData(true) // 静默刷新
|
||||||
|
}, 30000) // 30秒刷新一次
|
||||||
|
},
|
||||||
|
// 停止自动刷新 逻辑完全未改
|
||||||
|
stopAutoRefresh() {
|
||||||
|
if (this.refreshTimer) {
|
||||||
|
clearInterval(this.refreshTimer)
|
||||||
|
this.refreshTimer = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 加载所有数据(初始化) 逻辑完全未改
|
||||||
|
loadAllData() {
|
||||||
|
this.checkNetworkStatus() // 检测网络状态
|
||||||
|
this.loadCurrentShift() // 加载当前班组
|
||||||
|
this.initPlantStateDefines() // 加载所有定义
|
||||||
|
this.updateLastTime() // 更新时间
|
||||||
|
},
|
||||||
|
// 刷新数据(手动或自动) ✅ 替换uni弹窗为Element弹窗
|
||||||
|
refreshData(isSilent = false) {
|
||||||
|
if (this.isRefreshing) return // 防止重复刷新
|
||||||
|
this.isRefreshing = true
|
||||||
|
|
||||||
|
let loading = null
|
||||||
|
if (!isSilent) {
|
||||||
|
loading = this.$loading({ lock: true, text: '刷新中', spinner: 'el-icon-loading' })
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
this.checkNetworkStatus(),
|
||||||
|
this.loadCurrentShift(),
|
||||||
|
this.initPlantStateDefines(isSilent)
|
||||||
|
]).finally(() => {
|
||||||
|
this.isRefreshing = false
|
||||||
|
loading && loading.close()
|
||||||
|
if (!isSilent) {
|
||||||
|
this.$message.success('刷新成功')
|
||||||
|
}
|
||||||
|
this.updateLastTime()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 更新最后刷新时间 逻辑完全未改
|
||||||
|
updateLastTime() {
|
||||||
|
const now = new Date()
|
||||||
|
const hour = String(now.getHours()).padStart(2, '0')
|
||||||
|
const minute = String(now.getMinutes()).padStart(2, '0')
|
||||||
|
const second = String(now.getSeconds()).padStart(2, '0')
|
||||||
|
this.lastUpdateTime = `${hour}:${minute}:${second}`
|
||||||
|
},
|
||||||
|
// 检测网络状态 ✅ 替换uni.request为axios
|
||||||
|
checkNetworkStatus() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const startTime = Date.now()
|
||||||
|
axios.get(`http://140.143.206.120:8080/pocket/proPlantStateDefine/allWithValues`, { timeout: 5000 })
|
||||||
|
.then(() => {
|
||||||
|
const responseTime = Date.now() - startTime
|
||||||
|
if (responseTime < 500) this.webStatus[0].value = '通畅'
|
||||||
|
else if (responseTime < 2000) this.webStatus[0].value = '卡顿'
|
||||||
|
else this.webStatus[0].value = '异常'
|
||||||
|
resolve()
|
||||||
|
}).catch(() => {
|
||||||
|
this.webStatus[0].value = '异常'
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 加载当前班组信息 逻辑完全未改
|
||||||
|
loadCurrentShift() {
|
||||||
|
return getCurrentShift().then(response => {
|
||||||
|
if (response.code === 200 && response.data) {
|
||||||
|
const shiftData = response.data
|
||||||
|
const shiftName = this.getShiftName(shiftData.shift)
|
||||||
|
const crewName = this.getCrewName(shiftData.crew)
|
||||||
|
this.webStatus[1].value = `${crewName} / ${shiftName}`
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('加载班组信息失败:', error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 获取班次名称 逻辑完全未改
|
||||||
|
getShiftName(shift) {
|
||||||
|
const shiftMap = { 'A': '早班', 'B': '中班', 'C': '晚班' }
|
||||||
|
return shiftMap[shift] || shift || '—'
|
||||||
|
},
|
||||||
|
// 获取班组名称 逻辑完全未改
|
||||||
|
getCrewName(crew) {
|
||||||
|
const crewMap = { 1: '甲', 2: '乙', 3: '丙', 4: '丁' }
|
||||||
|
return crewMap[crew] || crew || '—'
|
||||||
|
},
|
||||||
|
// 初始化:加载所有状态定义及其当前值 ✅ 替换uni弹窗为Element弹窗
|
||||||
|
initPlantStateDefines(isSilent = false) {
|
||||||
|
let loading = null
|
||||||
|
if (!isSilent) {
|
||||||
|
loading = this.$loading({ lock: true, text: '加载中', spinner: 'el-icon-loading' })
|
||||||
|
}
|
||||||
|
return getAllPlantStateDefines().then(response => {
|
||||||
|
if (response.code === 200 && response.data) {
|
||||||
|
this.plantStateDefines = response.data
|
||||||
|
this.updateCurrentMetrics()
|
||||||
|
return this.loadTempTrend(isSilent)
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
loading && loading.close()
|
||||||
|
}).catch(error => {
|
||||||
|
loading && loading.close()
|
||||||
|
console.error('加载状态定义失败:', error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 更新所有实时指标 逻辑完全未改
|
||||||
|
updateCurrentMetrics() {
|
||||||
|
// 速度监控
|
||||||
|
const exitSpeed = this.getDefineById(1)
|
||||||
|
const plSpeed = this.getDefineById(2)
|
||||||
|
const trimSpeed = this.getDefineById(3)
|
||||||
|
this.speedMetrics = [
|
||||||
|
{ label: exitSpeed?.comments || '出口带钢速度', value: this.formatValue(exitSpeed?.currentValue), unit: exitSpeed?.units || 'm/min' },
|
||||||
|
{ label: plSpeed?.comments || '酸洗带钢速度', value: this.formatValue(plSpeed?.currentValue), unit: plSpeed?.units || 'm/min' },
|
||||||
|
{ label: trimSpeed?.comments || '圆盘剪速度', value: this.formatValue(trimSpeed?.currentValue), unit: trimSpeed?.units || 'm/min' }
|
||||||
|
]
|
||||||
|
// 活套状态
|
||||||
|
const celLooper = this.getDefineById(8)
|
||||||
|
const cxlLooper = this.getDefineById(9)
|
||||||
|
const telLooper = this.getDefineById(10)
|
||||||
|
this.looperMetrics = [
|
||||||
|
{ label: celLooper?.comments || '入口重套', value: this.formatValue(celLooper?.currentValue), unit: celLooper?.units || '%' },
|
||||||
|
{ label: cxlLooper?.comments || '出口活套', value: this.formatValue(cxlLooper?.currentValue), unit: cxlLooper?.units || '%' },
|
||||||
|
{ label: telLooper?.comments || '联机活套', value: this.formatValue(telLooper?.currentValue), unit: telLooper?.units || '%' }
|
||||||
|
]
|
||||||
|
// 酸槽浓度
|
||||||
|
this.tankConcentration = [
|
||||||
|
{ name: '1#酸槽', hclCont: this.formatValue(this.getDefineById(11)?.currentValue), feCont: this.formatValue(this.getDefineById(12)?.currentValue) },
|
||||||
|
{ name: '2#酸槽', hclCont: this.formatValue(this.getDefineById(13)?.currentValue), feCont: this.formatValue(this.getDefineById(14)?.currentValue) },
|
||||||
|
{ name: '3#酸槽', hclCont: this.formatValue(this.getDefineById(15)?.currentValue), feCont: this.formatValue(this.getDefineById(16)?.currentValue) }
|
||||||
|
]
|
||||||
|
// 工艺参数
|
||||||
|
const rinseTemp = this.getDefineById(7)
|
||||||
|
const windTemp = this.getDefineById(17)
|
||||||
|
const rinseFlow = this.getDefineById(18)
|
||||||
|
const telTension = this.getDefineById(19)
|
||||||
|
const tlvElong = this.getDefineById(20)
|
||||||
|
this.processMetrics = [
|
||||||
|
{ label: rinseTemp?.comments || '漂洗温度', value: this.formatValue(rinseTemp?.currentValue), unit: rinseTemp?.units || '°C' },
|
||||||
|
{ label: windTemp?.comments || '烘干温度', value: this.formatValue(windTemp?.currentValue), unit: windTemp?.units || '°C' },
|
||||||
|
{ label: rinseFlow?.comments || '漂洗电导率', value: this.formatValue(rinseFlow?.currentValue), unit: rinseFlow?.units || 'g/L' },
|
||||||
|
{ label: telTension?.comments || '联机活套张力', value: this.formatValue(telTension?.currentValue), unit: telTension?.units || 'kN' },
|
||||||
|
{ label: tlvElong?.comments || '拉矫机延伸率', value: this.formatValue(tlvElong?.currentValue), unit: tlvElong?.units || '%' }
|
||||||
|
]
|
||||||
|
// 镀锌线-轧辊速度
|
||||||
|
this.rollSpeedMetrics = [
|
||||||
|
{ label: '1#机架', value: this.formatValue(this.getDefineById(36)?.currentValue), unit: 'm/min' },
|
||||||
|
{ label: '2#机架', value: this.formatValue(this.getDefineById(37)?.currentValue), unit: 'm/min' },
|
||||||
|
{ label: '3#机架', value: this.formatValue(this.getDefineById(38)?.currentValue), unit: 'm/min' },
|
||||||
|
{ label: '4#机架', value: this.formatValue(this.getDefineById(39)?.currentValue), unit: 'm/min' },
|
||||||
|
{ label: '5#机架', value: this.formatValue(this.getDefineById(40)?.currentValue), unit: 'm/min' },
|
||||||
|
{ label: '6#机架', value: this.formatValue(this.getDefineById(41)?.currentValue), unit: 'm/min' }
|
||||||
|
]
|
||||||
|
// 镀锌线-压下率
|
||||||
|
this.reducMetrics = [
|
||||||
|
{ label: '1#机架', value: this.formatValue(this.getDefineById(24)?.currentValue), unit: '%' },
|
||||||
|
{ label: '2#机架', value: this.formatValue(this.getDefineById(25)?.currentValue), unit: '%' },
|
||||||
|
{ label: '3#机架', value: this.formatValue(this.getDefineById(26)?.currentValue), unit: '%' },
|
||||||
|
{ label: '4#机架', value: this.formatValue(this.getDefineById(27)?.currentValue), unit: '%' },
|
||||||
|
{ label: '5#机架', value: this.formatValue(this.getDefineById(28)?.currentValue), unit: '%' },
|
||||||
|
{ label: '6#机架', value: this.formatValue(this.getDefineById(29)?.currentValue), unit: '%' }
|
||||||
|
]
|
||||||
|
// 镀锌线-张力
|
||||||
|
this.tensionMetrics = [
|
||||||
|
{ label: '0#张力', value: this.formatValue(this.getDefineById(42)?.currentValue), unit: 'kN' },
|
||||||
|
{ label: '1#张力', value: this.formatValue(this.getDefineById(43)?.currentValue), unit: 'kN' },
|
||||||
|
{ label: '2#张力', value: this.formatValue(this.getDefineById(44)?.currentValue), unit: 'kN' },
|
||||||
|
{ label: '3#张力', value: this.formatValue(this.getDefineById(45)?.currentValue), unit: 'kN' },
|
||||||
|
{ label: '4#张力', value: this.formatValue(this.getDefineById(46)?.currentValue), unit: 'kN' },
|
||||||
|
{ label: '5#张力', value: this.formatValue(this.getDefineById(47)?.currentValue), unit: 'kN' },
|
||||||
|
{ label: '6#张力', value: this.formatValue(this.getDefineById(48)?.currentValue), unit: 'kN' }
|
||||||
|
]
|
||||||
|
// 镀锌线-功率
|
||||||
|
this.powerMetrics = [
|
||||||
|
{ label: '1#机架', value: this.formatValue(this.getDefineById(49)?.currentValue), unit: '%' },
|
||||||
|
{ label: '2#机架', value: this.formatValue(this.getDefineById(50)?.currentValue), unit: '%' },
|
||||||
|
{ label: '3#机架', value: this.formatValue(this.getDefineById(51)?.currentValue), unit: '%' },
|
||||||
|
{ label: '4#机架', value: this.formatValue(this.getDefineById(52)?.currentValue), unit: '%' },
|
||||||
|
{ label: '5#机架', value: this.formatValue(this.getDefineById(53)?.currentValue), unit: '%' },
|
||||||
|
{ label: '6#机架', value: this.formatValue(this.getDefineById(54)?.currentValue), unit: '%' }
|
||||||
|
]
|
||||||
|
// 涂装线-速度
|
||||||
|
this.paintSpeedMetrics = [
|
||||||
|
{ label: '出口带钢速度', value: this.formatValue(this.getDefineById(1)?.currentValue), unit: 'm/min' },
|
||||||
|
{ label: '涂装带钢速度', value: this.formatValue(this.getDefineById(2)?.currentValue), unit: 'm/min' },
|
||||||
|
{ label: '圆盘剪速度', value: this.formatValue(this.getDefineById(3)?.currentValue), unit: 'm/min' }
|
||||||
|
]
|
||||||
|
// 涂装线-拉矫参数
|
||||||
|
this.tlvMetrics = [
|
||||||
|
{ label: '拉矫延伸率', value: this.formatValue(this.getDefineById(20)?.currentValue), unit: '%' },
|
||||||
|
{ label: '破磷机插入量1', value: this.formatValue(this.getDefineById(21)?.currentValue), unit: 'mm' },
|
||||||
|
{ label: '破磷机插入量2', value: this.formatValue(this.getDefineById(22)?.currentValue), unit: 'mm' },
|
||||||
|
{ label: '破磷机插入量3', value: this.formatValue(this.getDefineById(23)?.currentValue), unit: 'mm' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// 加载历史趋势图数据 ✅ 渲染Echarts图表
|
||||||
|
loadTempTrend(isSilent = false) {
|
||||||
|
return listPlantStateHistory({ pageNum: 1, pageSize: 30 }).then(response => {
|
||||||
|
if (response.code === 200 && response.rows && response.rows.length > 0) {
|
||||||
|
const categories = []
|
||||||
|
const tank1Data = [];const tank2Data = [];const tank3Data = [];
|
||||||
|
const exitSpeedData = [];
|
||||||
|
const force1Data = [];const force2Data = [];const force3Data = [];const force4Data = [];const force5Data = [];const force6Data = [];
|
||||||
|
const paintTemp1 = [];const paintTemp2 = [];const paintTemp3 = [];const paintTemp4 = [];const paintTemp5 = [];
|
||||||
|
|
||||||
|
response.rows.forEach(item => {
|
||||||
|
const dateStr = this.formatDate(item.insdate)
|
||||||
|
categories.push(dateStr)
|
||||||
|
tank1Data.push(Number(item.value4) || 0)
|
||||||
|
tank2Data.push(Number(item.value5) || 0)
|
||||||
|
tank3Data.push(Number(item.value6) || 0)
|
||||||
|
exitSpeedData.push(Number(item.value1) || 0)
|
||||||
|
force1Data.push(Number(item.value30) || 0)
|
||||||
|
force2Data.push(Number(item.value31) || 0)
|
||||||
|
force3Data.push(Number(item.value32) || 0)
|
||||||
|
force4Data.push(Number(item.value33) || 0)
|
||||||
|
force5Data.push(Number(item.value34) || 0)
|
||||||
|
force6Data.push(Number(item.value35) || 0)
|
||||||
|
paintTemp1.push(Number(item.value4) || 0)
|
||||||
|
paintTemp2.push(Number(item.value5) || 0)
|
||||||
|
paintTemp3.push(Number(item.value6) || 0)
|
||||||
|
paintTemp4.push(Number(item.value7) || 0)
|
||||||
|
paintTemp5.push(Number(item.value17) || 0)
|
||||||
|
})
|
||||||
|
const reverseCate = categories.reverse()
|
||||||
|
// 渲染出口速度趋势
|
||||||
|
this.renderLineChart(this.exitSpeedChart, reverseCate, [{name:'出口带钢速度',data:exitSpeedData.reverse()}], this.lineColor, 'm/min')
|
||||||
|
// 渲染酸槽温度趋势
|
||||||
|
this.renderLineChart(this.tempChart, reverseCate, [
|
||||||
|
{name:'1#酸槽温度',data:tank1Data.reverse()},
|
||||||
|
{name:'2#酸槽温度',data:tank2Data.reverse()},
|
||||||
|
{name:'3#酸槽温度',data:tank3Data.reverse()}
|
||||||
|
], this.lineColor, '°C')
|
||||||
|
// 渲染轧制力趋势
|
||||||
|
this.renderLineChart(this.forceChart, reverseCate, [
|
||||||
|
{name:'1#轧制力',data:force1Data.reverse()},
|
||||||
|
{name:'2#轧制力',data:force2Data.reverse()},
|
||||||
|
{name:'3#轧制力',data:force3Data.reverse()},
|
||||||
|
{name:'4#轧制力',data:force4Data.reverse()},
|
||||||
|
{name:'5#轧制力',data:force5Data.reverse()},
|
||||||
|
{name:'6#轧制力',data:force6Data.reverse()}
|
||||||
|
], this.forceColor, 'kN')
|
||||||
|
// 渲染烘干温度趋势
|
||||||
|
this.renderLineChart(this.paintTempChart, reverseCate, [
|
||||||
|
{name:'1#酸槽温度',data:paintTemp1.reverse()},
|
||||||
|
{name:'2#酸槽温度',data:paintTemp2.reverse()},
|
||||||
|
{name:'3#酸槽温度',data:paintTemp3.reverse()},
|
||||||
|
{name:'漂洗温度',data:paintTemp4.reverse()},
|
||||||
|
{name:'烘干温度',data:paintTemp5.reverse()}
|
||||||
|
], this.paintColor, '°C')
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('加载历史趋势失败:', error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 根据ID获取Define对象 逻辑完全未改
|
||||||
|
getDefineById(id) {
|
||||||
|
return this.plantStateDefines.find(item => item.id == id)
|
||||||
|
},
|
||||||
|
// 格式化日期 逻辑完全未改
|
||||||
|
formatDate(dateStr) {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const hour = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
return `${hour}:${minute}`
|
||||||
|
},
|
||||||
|
// 格式化数值 逻辑完全未改
|
||||||
|
formatValue(value) {
|
||||||
|
if (value === null || value === undefined || value === '') return '—'
|
||||||
|
const num = Number(value)
|
||||||
|
if (isNaN(num)) return '—'
|
||||||
|
return num.toFixed(2)
|
||||||
|
},
|
||||||
|
// 滚动到指定部分 ✅ PC端原生滚动适配
|
||||||
|
scrollToSection(sectionId) {
|
||||||
|
document.getElementById(sectionId).scrollIntoView({ behavior: 'smooth' })
|
||||||
|
this.navMenuExpanded = false // 点击后关闭菜单
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
// ✅ 所有rpx转px(1rpx=0.5px) PC端完美适配 | 移除tab相关样式
|
||||||
|
.page-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f7fa;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 刷新按钮(固定在右下角) */
|
||||||
|
.refresh-btn-fixed {
|
||||||
|
position: fixed;
|
||||||
|
right: 16px;
|
||||||
|
bottom: 60px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background: #0066cc;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 102, 204, 0.4);
|
||||||
|
z-index: 999;
|
||||||
|
cursor: pointer;
|
||||||
|
&:active { opacity: 0.8; transform: scale(0.95); }
|
||||||
|
}
|
||||||
|
.refresh-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #fff;
|
||||||
|
display: block;
|
||||||
|
line-height: 1;
|
||||||
|
&.rotating { animation: rotate 1s linear infinite; }
|
||||||
|
}
|
||||||
|
@keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
/* 快速导航菜单(固定在左下角) */
|
||||||
|
.nav-menu-fixed {
|
||||||
|
position: fixed;
|
||||||
|
left: 16px;
|
||||||
|
bottom: 60px;
|
||||||
|
z-index: 998;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.nav-toggle {
|
||||||
|
width: 48px;height: 48px;
|
||||||
|
background: #409eff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;align-items: center;justify-content: center;
|
||||||
|
box-shadow: 0 4px 10px rgba(64, 158, 255, 0.4);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
&:active { opacity: 0.8; transform: scale(0.95); }
|
||||||
|
}
|
||||||
|
.nav-toggle-icon { font-size: 24px; color: #fff; display: block; line-height: 1; }
|
||||||
|
.nav-items {
|
||||||
|
display: flex;flex-direction: column;gap:4px;
|
||||||
|
animation: slideUp 0.3s ease;
|
||||||
|
}
|
||||||
|
@keyframes slideUp { from { opacity:0; transform: translateY(10px); } to { opacity:1; transform: translateY(0); } }
|
||||||
|
.nav-item {
|
||||||
|
background: #fff;border:1px solid #409eff;border-radius:4px;
|
||||||
|
padding:8px 12px;cursor: pointer;
|
||||||
|
box-shadow: 0 2px 6px rgba(64,158,255,0.2);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
&:active { background: #f0f9ff; transform: scale(0.95); }
|
||||||
|
}
|
||||||
|
.nav-label { font-size:13px; color:#409eff; font-weight:500; white-space: nowrap; }
|
||||||
|
|
||||||
|
/* 滚动容器 - PC端原生滚动 核心修改 */
|
||||||
|
.scroll-container {
|
||||||
|
height: calc(100vh - 20px);
|
||||||
|
padding: 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部状态栏 */
|
||||||
|
.status-bar {
|
||||||
|
display: flex;align-items: center;
|
||||||
|
background: #fff;padding:12px 16px;margin-bottom:12px;
|
||||||
|
border-radius:4px;border:1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
.status-item { flex:1; display:flex;align-items:center;justify-content:center;gap:8px; }
|
||||||
|
.status-label { font-size:13px;color:#909399; }
|
||||||
|
.status-value {
|
||||||
|
font-size:14px;font-weight:500;color:#303133;
|
||||||
|
&.status-通畅 { color: #67c23a; }
|
||||||
|
&.status-卡顿 { color: #e6a23c; }
|
||||||
|
&.status-异常 { color: #f56c6c; }
|
||||||
|
&.status-time { color:#909399;font-size:12px; }
|
||||||
|
}
|
||||||
|
.status-divider { width:1px;height:20px;background:#e4e7ed; }
|
||||||
|
|
||||||
|
/* 区块样式 */
|
||||||
|
.section { margin-bottom:12px; }
|
||||||
|
.section-title {
|
||||||
|
font-size:15px;font-weight:500;color:#303133;
|
||||||
|
margin-bottom:10px;padding-left:8px;border-left:2px solid #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分隔符 */
|
||||||
|
.section-divider {
|
||||||
|
display:flex;align-items:center;margin:20px 0 12px 0;padding:0 8px;
|
||||||
|
}
|
||||||
|
.divider-text {
|
||||||
|
font-size:14px;font-weight:600;color:#0066cc;
|
||||||
|
padding:0 6px;background:#f5f7fa;
|
||||||
|
border-left:2px solid #0066cc;padding-left:8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 指标卡片 - 3列布局 */
|
||||||
|
.metrics-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap:10px; }
|
||||||
|
/* 指标卡片 - 2列布局 */
|
||||||
|
.metrics-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap:10px; }
|
||||||
|
.metric-box {
|
||||||
|
background:#fff;border:1px solid #e4e7ed;border-radius:4px;
|
||||||
|
padding:14px 10px;text-align:center;
|
||||||
|
}
|
||||||
|
.metric-name { display:block;font-size:12px;color:#909399;margin-bottom:8px; }
|
||||||
|
.metric-value { display:block;font-size:24px;font-weight:600;color:#0066cc;margin-bottom:4px;line-height:1; }
|
||||||
|
.metric-unit { display:block;font-size:11px;color:#909399; }
|
||||||
|
|
||||||
|
/* 图表容器 */
|
||||||
|
.chart-box {
|
||||||
|
background:#fff;border:1px solid #e4e7ed;border-radius:4px;
|
||||||
|
padding:12px 8px;height:225px;width:100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 酸槽监控网格 */
|
||||||
|
.tank-grid { display:grid;grid-template-columns:repeat(3,1fr);gap:10px; }
|
||||||
|
.tank-card { background:#fff;border:2px solid #0066cc;border-radius:4px;overflow:hidden; }
|
||||||
|
.tank-header { background:#0066cc;color:#fff;font-size:14px;font-weight:500;padding:10px;text-align:center; }
|
||||||
|
.tank-data { padding:12px 8px; }
|
||||||
|
.tank-row { display:flex;flex-direction:column;align-items:center;padding:6px 0; }
|
||||||
|
.data-label { font-size:11px;color:#909399;margin-bottom:4px; }
|
||||||
|
.data-value { font-size:18px;font-weight:600;color:#303133; }
|
||||||
|
.data-unit { font-size:10px;font-weight:400;color:#909399;margin-left:2px; }
|
||||||
|
.tank-divider { height:1px;background:#e4e7ed;margin:6px 0; }
|
||||||
|
</style>
|
||||||
908
klp-ui/src/views/lines/acid/components/shutdown-statistic.vue
Normal file
908
klp-ui/src/views/lines/acid/components/shutdown-statistic.vue
Normal file
@@ -0,0 +1,908 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<!-- 时间维度切换 -->
|
||||||
|
<div class="time-tab-bar">
|
||||||
|
<div v-for="item in timeTabs" :key="item.value" class="time-tab-item"
|
||||||
|
:class="{ 'time-tab-active': activeTab === item.value }" @click="handleTabChange(item.value)">
|
||||||
|
{{ item.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日期选择区 -->
|
||||||
|
<div class="date-selector">
|
||||||
|
<!-- 日模式 -->
|
||||||
|
<el-date-picker v-if="activeTab === 'day'" v-model="startDate" type="date" value-format="yyyy-MM-dd"
|
||||||
|
placeholder="选择日期" @change="handleDateChange" class="single-date-picker" />
|
||||||
|
|
||||||
|
<!-- 月模式 -->
|
||||||
|
<div v-else-if="activeTab === 'month'" class="date-range-group">
|
||||||
|
<el-date-picker v-model="startDate" type="month" value-format="yyyy-MM" placeholder="选择开始月份"
|
||||||
|
@change="handleStartMonthChange" class="range-date-picker" />
|
||||||
|
<span class="date-separator">至</span>
|
||||||
|
<el-date-picker v-model="endDate" type="month" value-format="yyyy-MM" placeholder="选择结束月份"
|
||||||
|
:picker-options="monthPickerOptions" @change="handleEndMonthChange" class="range-date-picker" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 年模式 -->
|
||||||
|
<div v-else class="date-range-group">
|
||||||
|
<el-date-picker v-model="startDate" type="year" value-format="yyyy" placeholder="选择开始年份"
|
||||||
|
@change="handleStartYearChange" class="range-date-picker" />
|
||||||
|
<span class="date-separator">至</span>
|
||||||
|
<el-date-picker v-model="endDate" type="year" value-format="yyyy" placeholder="选择结束年份"
|
||||||
|
@change="handleEndYearChange" class="range-date-picker" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 停机汇总 -->
|
||||||
|
<div class="summary-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">停机汇总</span>
|
||||||
|
<span class="section-date">{{ displayDateRange }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-grid">
|
||||||
|
<div class="summary-card" v-for="(item, index) in summaryData" :key="index">
|
||||||
|
<span class="summary-label">{{ item.label }}</span>
|
||||||
|
<div class="summary-value-box">
|
||||||
|
<span class="summary-value">{{ item.value }}</span>
|
||||||
|
<span v-if="item.unit" class="summary-unit">{{ item.unit }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-row :gutter="10">
|
||||||
|
<el-col :span="12">
|
||||||
|
<!-- 停机分布 - 班组 -->
|
||||||
|
<div class="chart-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">班组停机分布</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="crewPieData.length > 0" class="pie-chart-single" ref="crewPie" id="crewPie"></div>
|
||||||
|
<div class="empty-chart" v-else>
|
||||||
|
<span class="empty-icon">📊</span>
|
||||||
|
<span class="empty-text">此时间段未发生停机</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :span="12">
|
||||||
|
<!-- 停机分布 - 类型 -->
|
||||||
|
<div class="chart-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">停机类型分布</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="typePieData.length > 0" class="pie-chart-single" ref="typePie" id="typePie"></div>
|
||||||
|
<div class="empty-chart" v-else>
|
||||||
|
<span class="empty-icon">📊</span>
|
||||||
|
<span class="empty-text">此时间段未发生停机</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 停机详细列表(日视图) -->
|
||||||
|
<div class="detail-section" v-if="activeTab === 'day'">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">停机详情</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-list">
|
||||||
|
<div v-if="tableData.length === 0" class="empty-state">
|
||||||
|
<span class="empty-text">暂无停机记录</span>
|
||||||
|
</div>
|
||||||
|
<el-table v-else :data="tableData">
|
||||||
|
<el-table-column label="时间范围" prop="time"></el-table-column>
|
||||||
|
<el-table-column label="持续时间" prop="duration"></el-table-column>
|
||||||
|
<el-table-column label="机组" prop="machine"></el-table-column>
|
||||||
|
<el-table-column label="备注" prop="remark" show-overflow-tooltip></el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 停机趋势图(月/年视图) -->
|
||||||
|
<div class="chart-section" v-else>
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">停机趋势</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper trend-chart" ref="trendChart" id="trendChart"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ✅【完全未改动】原API导入
|
||||||
|
import { listStoppage } from '@/api/pocket/plantState'
|
||||||
|
// 引入Echarts5.x 兼容Vue2
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
|
// ✅【完全未改动】原独立工具函数
|
||||||
|
function getDefaultDate(type = "day") {
|
||||||
|
const date = new Date();
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||||
|
const day = date.getDate().toString().padStart(2, "0");
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "day":
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
case "month":
|
||||||
|
return `${year}-${month}`;
|
||||||
|
case "year":
|
||||||
|
return `${year}`;
|
||||||
|
default:
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLastMonth() {
|
||||||
|
const date = new Date();
|
||||||
|
date.setMonth(date.getMonth() - 1);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||||
|
return `${year}-${month}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(date, type) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||||
|
const day = date.getDate().toString().padStart(2, "0");
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "day":
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
case "month":
|
||||||
|
return `${year}-${month}`;
|
||||||
|
case "year":
|
||||||
|
return `${year}`;
|
||||||
|
default:
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PlantStoppage',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// ✅【完全未改动】业务数据
|
||||||
|
activeTab: "day",
|
||||||
|
startDate: getDefaultDate(),
|
||||||
|
endDate: getDefaultDate(),
|
||||||
|
timeTabs: [
|
||||||
|
{ label: "日", value: "day" },
|
||||||
|
{ label: "月", value: "month" },
|
||||||
|
{ label: "年", value: "year" }
|
||||||
|
],
|
||||||
|
summaryData: [
|
||||||
|
{ label: "停机时间", value: 0, unit: "min" },
|
||||||
|
{ label: "停机次数", value: 0, unit: "次" },
|
||||||
|
{ label: "作业率", value: 0, unit: "%" }
|
||||||
|
],
|
||||||
|
// Echarts图表实例
|
||||||
|
trendChart: null,
|
||||||
|
crewPie: null,
|
||||||
|
typePie: null,
|
||||||
|
// 月份选择器限制条件
|
||||||
|
monthPickerOptions: {},
|
||||||
|
// 趋势图临时存储X轴数据
|
||||||
|
trendXData: [],
|
||||||
|
// ✅【完全未改动】原图表配色
|
||||||
|
mainColor: ["#0066cc", "#f56c6c"],
|
||||||
|
pieColor: ["#0066cc", "#409eff", "#66b1ff", "#a0cfff", "#d9ecff"],
|
||||||
|
crewPieData: [],
|
||||||
|
typePieData: [],
|
||||||
|
tableData: [],
|
||||||
|
// ✅【新增】resize防抖定时器,解决窗口缩放卡顿
|
||||||
|
resizeTimer: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// ✅【完全未改动】计算属性
|
||||||
|
computed: {
|
||||||
|
maxMonthEnd() {
|
||||||
|
if (!this.startDate) return "";
|
||||||
|
const date = new Date(this.startDate);
|
||||||
|
date.setFullYear(date.getFullYear() + 1);
|
||||||
|
return formatDate(date, "month");
|
||||||
|
},
|
||||||
|
displayDateRange() {
|
||||||
|
switch (this.activeTab) {
|
||||||
|
case "day":
|
||||||
|
return this.startDate;
|
||||||
|
case "month":
|
||||||
|
return `${this.startDate} 至 ${this.endDate}`;
|
||||||
|
case "year":
|
||||||
|
return `${this.startDate} 至 ${this.endDate}`;
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// 监听开始月份,更新结束月份可选范围
|
||||||
|
startDate(val) {
|
||||||
|
if (this.activeTab === 'month' && val) {
|
||||||
|
this.monthPickerOptions = {
|
||||||
|
disabledDate: (time) => {
|
||||||
|
const maxDate = new Date(this.maxMonthEnd + '-01')
|
||||||
|
return time.getTime() > maxDate.getTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// ✅【新增】监听tab切换,自动触发图表自适应
|
||||||
|
activeTab() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.resizeEcharts()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// 初始化Echarts图表
|
||||||
|
this.initEcharts()
|
||||||
|
// 加载业务数据
|
||||||
|
this.loadStoppageData()
|
||||||
|
// 窗口自适应监听
|
||||||
|
window.addEventListener('resize', this.resizeEcharts)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
// ✅【修复】安全销毁图表实例+清除监听+清除定时器,防止内存泄漏
|
||||||
|
window.removeEventListener('resize', this.resizeEcharts)
|
||||||
|
clearTimeout(this.resizeTimer)
|
||||||
|
if (this.trendChart && !this.trendChart.isDisposed()) {
|
||||||
|
this.trendChart.dispose()
|
||||||
|
this.trendChart = null
|
||||||
|
}
|
||||||
|
if (this.crewPie && !this.crewPie.isDisposed()) {
|
||||||
|
this.crewPie.dispose()
|
||||||
|
this.crewPie = null
|
||||||
|
}
|
||||||
|
if (this.typePie && !this.typePie.isDisposed()) {
|
||||||
|
this.typePie.dispose()
|
||||||
|
this.typePie = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// ✅【逻辑未改动】仅适配Element传参
|
||||||
|
handleTabChange(tab) {
|
||||||
|
this.activeTab = tab;
|
||||||
|
if (tab === "day") {
|
||||||
|
const today = getDefaultDate();
|
||||||
|
this.startDate = today;
|
||||||
|
this.endDate = today;
|
||||||
|
} else if (tab === "month") {
|
||||||
|
this.startDate = getLastMonth();
|
||||||
|
this.endDate = getDefaultDate("month");
|
||||||
|
} else {
|
||||||
|
const currentYear = getDefaultDate("year");
|
||||||
|
this.startDate = currentYear;
|
||||||
|
this.endDate = currentYear;
|
||||||
|
}
|
||||||
|
this.loadStoppageData();
|
||||||
|
},
|
||||||
|
handleDateChange(val) {
|
||||||
|
this.startDate = val;
|
||||||
|
this.endDate = val;
|
||||||
|
this.loadStoppageData();
|
||||||
|
},
|
||||||
|
handleStartMonthChange(val) {
|
||||||
|
this.startDate = val;
|
||||||
|
const maxEndDate = new Date(this.startDate);
|
||||||
|
maxEndDate.setFullYear(maxEndDate.getFullYear() + 1);
|
||||||
|
const maxEndStr = formatDate(maxEndDate, "month");
|
||||||
|
if (new Date(this.endDate) > maxEndDate) {
|
||||||
|
this.endDate = maxEndStr;
|
||||||
|
}
|
||||||
|
this.loadStoppageData();
|
||||||
|
},
|
||||||
|
handleEndMonthChange(val) {
|
||||||
|
this.endDate = val;
|
||||||
|
this.loadStoppageData();
|
||||||
|
},
|
||||||
|
handleStartYearChange(val) {
|
||||||
|
this.startDate = val;
|
||||||
|
this.loadStoppageData();
|
||||||
|
},
|
||||||
|
handleEndYearChange(val) {
|
||||||
|
this.endDate = val;
|
||||||
|
this.loadStoppageData();
|
||||||
|
},
|
||||||
|
|
||||||
|
// ✅【核心逻辑完全未改动】仅替换uni加载提示为Element + 新增清空图表逻辑
|
||||||
|
loadStoppageData() {
|
||||||
|
const loading = this.$loading({
|
||||||
|
lock: true,
|
||||||
|
text: '加载中',
|
||||||
|
spinner: 'el-icon-loading',
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)'
|
||||||
|
});
|
||||||
|
|
||||||
|
const start = this.formatFullDate(this.startDate, true)
|
||||||
|
let end = this.formatFullDate(this.endDate, false)
|
||||||
|
|
||||||
|
if (this.activeTab === 'month' && this.endDate && this.endDate.length === 7) {
|
||||||
|
const today = new Date()
|
||||||
|
const todayYear = today.getFullYear()
|
||||||
|
const todayMonth = today.getMonth() + 1
|
||||||
|
const [endYear, endMonth] = this.endDate.split('-').map(Number)
|
||||||
|
if (endYear === todayYear && endMonth === todayMonth) {
|
||||||
|
const todayDay = today.getDate()
|
||||||
|
end = `${this.endDate}-${String(todayDay).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParams = {
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 9999,
|
||||||
|
startDate: start,
|
||||||
|
endDate: end
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('停机查询参数:', queryParams)
|
||||||
|
|
||||||
|
listStoppage(queryParams).then(response => {
|
||||||
|
loading.close()
|
||||||
|
console.log('停机统计响应:', response)
|
||||||
|
|
||||||
|
if (response.code === 200 && response.rows && response.rows.length > 0) {
|
||||||
|
this.tableData = response.rows.map(item => ({
|
||||||
|
time: this.formatDateTime(item.startDate) + ' - ' + this.formatDateTime(item.endDate),
|
||||||
|
duration: this.secondsToMinutes(item.duration) + 'min',
|
||||||
|
remark: item.remark || '-',
|
||||||
|
machine: item.unit || '-'
|
||||||
|
}))
|
||||||
|
|
||||||
|
const totalDurationSeconds = response.rows.reduce((sum, item) => sum + (Number(item.duration) || 0), 0)
|
||||||
|
const totalDurationMinutes = this.secondsToMinutes(totalDurationSeconds)
|
||||||
|
const totalCount = response.rows.length
|
||||||
|
const totalAvailableMinutes = this.getTotalAvailableMinutes()
|
||||||
|
const workRate = this.calculateWorkRate(totalDurationMinutes, totalAvailableMinutes)
|
||||||
|
|
||||||
|
this.summaryData = [
|
||||||
|
{ label: '停机时间', value: totalDurationMinutes, unit: 'min' },
|
||||||
|
{ label: '停机次数', value: totalCount, unit: '次' },
|
||||||
|
{ label: '作业率', value: workRate, unit: '%' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const crewMap = {}
|
||||||
|
const typeMap = {}
|
||||||
|
response.rows.forEach(item => {
|
||||||
|
const crew = item.crew || '未知班组'
|
||||||
|
const type = item.stopType || '未知类型'
|
||||||
|
const durationMinutes = this.secondsToMinutes(item.duration)
|
||||||
|
crewMap[crew] = (crewMap[crew] || 0) + durationMinutes
|
||||||
|
typeMap[type] = (typeMap[type] || 0) + durationMinutes
|
||||||
|
})
|
||||||
|
|
||||||
|
this.crewPieData = Object.keys(crewMap).map(crew => ({ name: crew, value: crewMap[crew] }))
|
||||||
|
this.typePieData = Object.keys(typeMap).map(type => ({ name: type, value: typeMap[type] }))
|
||||||
|
|
||||||
|
// 渲染饼图
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.renderPieChart('crew', this.crewPieData)
|
||||||
|
this.renderPieChart('type', this.typePieData)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.activeTab !== 'day') {
|
||||||
|
if (response.rows.length > 0) {
|
||||||
|
this.buildTrendChart(response.rows)
|
||||||
|
} else {
|
||||||
|
this.trendXData = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('暂无停机数据')
|
||||||
|
this.tableData = []
|
||||||
|
this.summaryData = [
|
||||||
|
{ label: '停机时间', value: 0, unit: 'min' },
|
||||||
|
{ label: '停机次数', value: 0, unit: '次' },
|
||||||
|
{ label: '作业率', value: 100, unit: '%' }
|
||||||
|
]
|
||||||
|
this.crewPieData = []
|
||||||
|
this.typePieData = []
|
||||||
|
this.trendXData = []
|
||||||
|
// ✅【修复】清空所有图表数据
|
||||||
|
this.clearChart(this.trendChart)
|
||||||
|
this.renderPieChart('crew', [])
|
||||||
|
this.renderPieChart('type', [])
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
loading.close()
|
||||||
|
console.error('加载停机数据失败:', error)
|
||||||
|
this.$message.error('加载失败,请稍后重试')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// ✅【完全未改动】原所有工具方法
|
||||||
|
formatDateTime(dateStr) {
|
||||||
|
if (!dateStr) return '-'
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||||
|
},
|
||||||
|
getTotalAvailableMinutes() {
|
||||||
|
const start = new Date(this.formatFullDate(this.startDate, true))
|
||||||
|
const end = new Date(this.formatFullDate(this.endDate, false))
|
||||||
|
const diffTime = end - start
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1
|
||||||
|
return diffDays * 1440
|
||||||
|
},
|
||||||
|
calculateWorkRate(stopDuration, totalAvailableMinutes) {
|
||||||
|
if (!totalAvailableMinutes || totalAvailableMinutes === 0) {
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
const workRate = ((totalAvailableMinutes - stopDuration) / totalAvailableMinutes) * 100
|
||||||
|
return Math.max(0, Math.min(100, workRate)).toFixed(2)
|
||||||
|
},
|
||||||
|
formatDate(dateStr) {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const month = date.getMonth() + 1
|
||||||
|
const day = date.getDate()
|
||||||
|
return `${month}/${day}`
|
||||||
|
},
|
||||||
|
formatDateByMonth(dateStr) {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const month = date.getMonth() + 1
|
||||||
|
return `${month}月`
|
||||||
|
},
|
||||||
|
secondsToMinutes(seconds) {
|
||||||
|
if (!seconds || seconds === 0) return 0
|
||||||
|
return Math.round(Number(seconds) / 60)
|
||||||
|
},
|
||||||
|
formatFullDate(dateStr, isStart) {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
if (dateStr.length === 10) {
|
||||||
|
return dateStr
|
||||||
|
}
|
||||||
|
if (dateStr.length === 7) {
|
||||||
|
if (isStart) {
|
||||||
|
return `${dateStr}-01`
|
||||||
|
} else {
|
||||||
|
const [year, month] = dateStr.split('-')
|
||||||
|
const lastDay = new Date(year, month, 0).getDate()
|
||||||
|
return `${dateStr}-${String(lastDay).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dateStr.length === 4) {
|
||||||
|
if (isStart) {
|
||||||
|
return `${dateStr}-01-01`
|
||||||
|
} else {
|
||||||
|
return `${dateStr}-12-31`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dateStr
|
||||||
|
},
|
||||||
|
|
||||||
|
// ✅【逻辑未改动】仅修改图表渲染方式
|
||||||
|
buildTrendChart(stoppageData) {
|
||||||
|
if (!stoppageData || stoppageData.length === 0) {
|
||||||
|
console.log('无法构建趋势图:数据为空')
|
||||||
|
this.trendXData = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const dateMap = {}
|
||||||
|
const isYearView = this.activeTab === 'year'
|
||||||
|
stoppageData.forEach(item => {
|
||||||
|
if (!item.startDate) return
|
||||||
|
let key
|
||||||
|
if (isYearView) {
|
||||||
|
key = this.formatDateByMonth(item.startDate)
|
||||||
|
} else {
|
||||||
|
key = this.formatDate(item.startDate)
|
||||||
|
}
|
||||||
|
if (!dateMap[key]) {
|
||||||
|
dateMap[key] = { duration: 0, count: 0 }
|
||||||
|
}
|
||||||
|
const durationMinutes = this.secondsToMinutes(item.duration)
|
||||||
|
dateMap[key].duration += durationMinutes
|
||||||
|
dateMap[key].count += 1
|
||||||
|
})
|
||||||
|
|
||||||
|
let categories = []
|
||||||
|
if (isYearView) {
|
||||||
|
categories = Array.from({ length: 12 }, (_, i) => `${i + 1}月`)
|
||||||
|
} else {
|
||||||
|
const startStr = this.formatFullDate(this.startDate, true)
|
||||||
|
let endStr = this.formatFullDate(this.endDate, false)
|
||||||
|
const today = new Date()
|
||||||
|
const todayYear = today.getFullYear()
|
||||||
|
const todayMonth = today.getMonth() + 1
|
||||||
|
if (this.endDate && this.endDate.length === 7) {
|
||||||
|
const [endYear, endMonth] = this.endDate.split('-').map(Number)
|
||||||
|
if (endYear === todayYear && endMonth === todayMonth) {
|
||||||
|
const todayDay = today.getDate()
|
||||||
|
endStr = `${this.endDate}-${String(todayDay).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const start = new Date(startStr)
|
||||||
|
const end = new Date(endStr)
|
||||||
|
const dateList = []
|
||||||
|
const currentDate = new Date(start)
|
||||||
|
while (currentDate <= end) {
|
||||||
|
const month = currentDate.getMonth() + 1
|
||||||
|
const day = currentDate.getDate()
|
||||||
|
const dateKey = `${month}/${day}`
|
||||||
|
dateList.push(dateKey)
|
||||||
|
currentDate.setDate(currentDate.getDate() + 1)
|
||||||
|
}
|
||||||
|
categories = dateList
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categories.length === 0) {
|
||||||
|
console.log('无法构建趋势图:无有效日期')
|
||||||
|
this.trendXData = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.trendXData = categories
|
||||||
|
const durationData = []
|
||||||
|
const rateData = []
|
||||||
|
categories.forEach(key => {
|
||||||
|
const data = dateMap[key] || { duration: 0, count: 0 }
|
||||||
|
durationData.push(data.duration)
|
||||||
|
let totalMinutes
|
||||||
|
if (isYearView) {
|
||||||
|
const year = parseInt(this.startDate) || new Date().getFullYear()
|
||||||
|
const monthIndex = parseInt(key.replace('月', '')) - 1
|
||||||
|
const daysInMonth = new Date(year, monthIndex + 1, 0).getDate()
|
||||||
|
totalMinutes = daysInMonth * 1440
|
||||||
|
} else {
|
||||||
|
totalMinutes = 1440
|
||||||
|
}
|
||||||
|
const rate = this.calculateWorkRate(data.duration, totalMinutes)
|
||||||
|
rateData.push(Number(rate))
|
||||||
|
})
|
||||||
|
// 渲染趋势混合图
|
||||||
|
this.renderTrendChart(categories, durationData, rateData)
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== ✅【全部修复】Echarts 图表初始化/渲染/自适应 核心方法 =====
|
||||||
|
// 安全初始化单个图表实例【新增核心方法】
|
||||||
|
initSingleChart(chartId) {
|
||||||
|
const dom = document.getElementById(chartId)
|
||||||
|
console.log('获取dom', dom)
|
||||||
|
if (!dom) return null
|
||||||
|
const chartInstance = echarts.init(dom)
|
||||||
|
return chartInstance
|
||||||
|
},
|
||||||
|
// 安全清空图表内容【新增核心方法】
|
||||||
|
clearChart(chartInstance) {
|
||||||
|
if (chartInstance && !chartInstance.isDisposed()) {
|
||||||
|
chartInstance.clear()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 初始化图表(懒初始化占位)
|
||||||
|
initEcharts() {
|
||||||
|
this.trendChart = this.initSingleChart('trendChart')
|
||||||
|
this.crewPie = this.initSingleChart('crewPie')
|
||||||
|
this.typePie = this.initSingleChart('typePie')
|
||||||
|
},
|
||||||
|
// 渲染停机趋势图 (柱状+折线 双Y轴)【修复完整版】
|
||||||
|
renderTrendChart(xData, durData, rateData) {
|
||||||
|
this.trendChart = this.initSingleChart('trendChart')
|
||||||
|
if (!this.trendChart) return
|
||||||
|
if (xData.length === 0) {
|
||||||
|
this.clearChart(this.trendChart)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const option = {
|
||||||
|
color: this.mainColor,
|
||||||
|
grid: { left: 30, right: 30, top: 40, bottom: 60 },
|
||||||
|
legend: { top: 0, left: 'center' },
|
||||||
|
tooltip: { trigger: 'axis' },
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: xData,
|
||||||
|
axisLine: { show: false },
|
||||||
|
axisLabel: { rotate: 60, fontSize: 12 }
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{ type: 'value', name: '停机时间(min)', position: 'left', splitLine: { type: 'dashed', color: '#e4e7ed' } },
|
||||||
|
{ type: 'value', name: '作业率(%)', position: 'right', splitLine: { show: false } }
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{ name: '停机时间', type: 'bar', data: durData, barWidth: 40 },
|
||||||
|
{ name: '作业率', type: 'line', yAxisIndex: 1, data: rateData, smooth: true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
this.trendChart.setOption(option, true)
|
||||||
|
},
|
||||||
|
// 渲染饼图 (班组/类型 通用)【修复完整版】
|
||||||
|
renderPieChart(type, data) {
|
||||||
|
const chartId = type === 'crew' ? 'crewPie' : 'typePie'
|
||||||
|
if (type === 'crew') {
|
||||||
|
this.crewPie = this.initSingleChart(chartId)
|
||||||
|
} else {
|
||||||
|
this.typePie = this.initSingleChart(chartId)
|
||||||
|
}
|
||||||
|
console.log('渲染饼图数据:', type, data, chartId)
|
||||||
|
const chart = type === 'crew' ? this.crewPie : this.typePie
|
||||||
|
console.log('获取到的图表实例:', chart)
|
||||||
|
if (!chart) return
|
||||||
|
if (data.length === 0) {
|
||||||
|
this.clearChart(chart)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const option = {
|
||||||
|
color: this.pieColor,
|
||||||
|
tooltip: { trigger: 'item', formatter: '{b}: {c}min ({d}%)' },
|
||||||
|
legend: { bottom: 0, left: 'center', textStyle: { fontSize: 12, color: '#666' } },
|
||||||
|
series: [{
|
||||||
|
name: type === 'crew' ? '班组停机' : '类型停机',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['40%', '70%'],
|
||||||
|
data: data,
|
||||||
|
label: { show: true, fontSize: 12 }
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
chart.setOption(option, true)
|
||||||
|
},
|
||||||
|
// 图表自适应 - 防抖+安全校验【修复完整版】
|
||||||
|
resizeEcharts: function () {
|
||||||
|
clearTimeout(this.resizeTimer)
|
||||||
|
this.resizeTimer = setTimeout(() => {
|
||||||
|
if (this.trendChart && !this.trendChart.isDisposed()) this.trendChart.resize()
|
||||||
|
if (this.crewPie && !this.crewPie.isDisposed()) this.crewPie.resize()
|
||||||
|
if (this.typePie && !this.typePie.isDisposed()) this.typePie.resize()
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
// ✅ PC端适配:rpx转px(1rpx=0.5px) 保留原布局+样式+间距 + 修复图表高度问题
|
||||||
|
.page-container {
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 12px;
|
||||||
|
min-height: calc(100vh - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-tab-bar {
|
||||||
|
display: flex;
|
||||||
|
width: 240px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-tab-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-tab-active {
|
||||||
|
background: #0066cc;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-selector {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
margin-left: 20px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-date-picker {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-range-group {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-date-picker {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-separator {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-section,
|
||||||
|
.chart-section,
|
||||||
|
.detail-section {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-left: 8px;
|
||||||
|
border-left: 2px solid #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-date {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 14px 10px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0066cc;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-unit {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #909399;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrapper {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px 8px;
|
||||||
|
min-height: 225px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅【修复】趋势图高度强制生效
|
||||||
|
.trend-chart {
|
||||||
|
height: 250px !important;
|
||||||
|
min-height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅【修复】饼图容器增加固定高度,解决高度塌陷
|
||||||
|
.pie-chart-single {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 16px 0;
|
||||||
|
height: 260px !important;
|
||||||
|
min-height: 240px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅【修复】空图表容器高度和饼图一致
|
||||||
|
.empty-chart {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 50px 0;
|
||||||
|
height: 260px !important;
|
||||||
|
min-height: 240px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 40px;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-list {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #f5f7fa;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-time {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-duration {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #0066cc;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #303133;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 50px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
526
klp-ui/src/views/lines/acid/components/team-performance.vue
Normal file
526
klp-ui/src/views/lines/acid/components/team-performance.vue
Normal file
@@ -0,0 +1,526 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<!-- 月份选择器 -->
|
||||||
|
<div class="month-selector">
|
||||||
|
<div class="month-arrow" @click="changeMonth(-1)" :class="{ disabled: isMinMonth }">
|
||||||
|
<span>◀</span>
|
||||||
|
</div>
|
||||||
|
<el-date-picker v-model="selectedMonth" type="month" value-format="yyyy-MM" placeholder="选择月份"
|
||||||
|
@change="handleMonthChange" class="month-display" />
|
||||||
|
<div class="month-arrow" @click="changeMonth(1)" :class="{ disabled: isMaxMonth }">
|
||||||
|
<span>▶</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<!-- 班组产量对比 -->
|
||||||
|
<div class="chart-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">产量对比</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper" ref="outputChart" id="outputChart"></div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<!-- 质量指标对比 -->
|
||||||
|
<div class="chart-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">质量指标</span>
|
||||||
|
</div>
|
||||||
|
<div class="chart-wrapper" ref="radarChart" id="radarChart"></div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 班组详细数据 -->
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<!-- 班组绩效排名 -->
|
||||||
|
<div class="ranking-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">绩效排名</span>
|
||||||
|
</div>
|
||||||
|
<div class="ranking-list">
|
||||||
|
<div class="ranking-item" v-for="(item, index) in teamRankingData" :key="index"
|
||||||
|
:class="'rank-' + (index + 1)">
|
||||||
|
<div class="rank-badge">{{ index + 1 }}</div>
|
||||||
|
<div class="rank-info">
|
||||||
|
<span class="team-name">{{ item.team }}</span>
|
||||||
|
<span class="team-shift">{{ item.shift }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="rank-score">
|
||||||
|
<span class="score-value">{{ item.score }}</span>
|
||||||
|
<span class="score-label">分</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">详细数据</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-table">
|
||||||
|
<el-table :data="tableData">
|
||||||
|
<el-table-column prop="team" label="班组" />
|
||||||
|
<el-table-column prop="output" label="产量(t)" />
|
||||||
|
<el-table-column prop="yieldRate" label="成材率" />
|
||||||
|
<el-table-column prop="passRate" label="合格率" />
|
||||||
|
<el-table-column prop="score" label="综合评分" />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ✅【完全未改动】原API导入
|
||||||
|
import { getTeamPerformance } from '@/api/pocket/plantState'
|
||||||
|
// 引入Echarts5.x 兼容Vue2
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
|
// ✅【完全未改动】原工具函数
|
||||||
|
function getCurrentMonth() {
|
||||||
|
const date = new Date();
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||||
|
return `${year}-${month}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TeamPerformance',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedMonth: getCurrentMonth(),
|
||||||
|
startDate: "2020-01",
|
||||||
|
endDate: getCurrentMonth(),
|
||||||
|
// ✅【完全未改动】表格列配置
|
||||||
|
columns: [
|
||||||
|
{ title: '班组', key: 'team' },
|
||||||
|
{ title: '产量(t)', key: 'output' },
|
||||||
|
{ title: '成材率', key: 'yieldRate' },
|
||||||
|
{ title: '合格率', key: 'passRate' },
|
||||||
|
{ title: '综合评分', key: 'score' }
|
||||||
|
],
|
||||||
|
tableData: [],
|
||||||
|
teamRankingData: [
|
||||||
|
{ team: '甲班', shift: '早班', score: 96.8 },
|
||||||
|
{ team: '乙班', shift: '中班', score: 95.5 },
|
||||||
|
{ team: '丙班', shift: '晚班', score: 94.2 },
|
||||||
|
{ team: '丁班', shift: '早班', score: 93.6 }
|
||||||
|
],
|
||||||
|
// Echarts图表实例
|
||||||
|
outputChart: null,
|
||||||
|
radarChart: null,
|
||||||
|
// ✅【保留原配色】
|
||||||
|
columnColor: ["#0066cc"],
|
||||||
|
radarColor: ["#0066cc", "#409eff", "#66b1ff", "#a0cfff"]
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// ✅【完全未改动】计算属性
|
||||||
|
computed: {
|
||||||
|
isMinMonth() {
|
||||||
|
return this.selectedMonth === this.startDate;
|
||||||
|
},
|
||||||
|
isMaxMonth() {
|
||||||
|
return this.selectedMonth === this.endDate;
|
||||||
|
},
|
||||||
|
formattedMonth() {
|
||||||
|
const [year, month] = this.selectedMonth.split("-");
|
||||||
|
return `${year}年${month}月`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// 初始化Echarts图表
|
||||||
|
this.initCharts()
|
||||||
|
// 加载业务数据
|
||||||
|
this.loadTeamPerformance();
|
||||||
|
// 窗口自适应监听
|
||||||
|
window.addEventListener('resize', this.resizeCharts)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
// 销毁图表实例,防止内存泄漏
|
||||||
|
window.removeEventListener('resize', this.resizeCharts)
|
||||||
|
this.outputChart && this.outputChart.dispose()
|
||||||
|
this.radarChart && this.radarChart.dispose()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// ✅【逻辑未改动】仅适配Element的参数格式
|
||||||
|
handleMonthChange(val) {
|
||||||
|
this.selectedMonth = val;
|
||||||
|
this.loadTeamPerformance();
|
||||||
|
},
|
||||||
|
|
||||||
|
// ✅【完全未改动】月份左右切换逻辑
|
||||||
|
changeMonth(step) {
|
||||||
|
const [year, month] = this.selectedMonth.split("-").map(Number);
|
||||||
|
const targetDate = new Date(year, month - 1 + step);
|
||||||
|
const targetMonth = `${targetDate.getFullYear()}-${(targetDate.getMonth() + 1).toString().padStart(2, "0")}`;
|
||||||
|
|
||||||
|
if (this.compareMonths(targetMonth, this.startDate) < 0) return;
|
||||||
|
if (this.compareMonths(targetMonth, this.endDate) > 0) return;
|
||||||
|
|
||||||
|
this.selectedMonth = targetMonth;
|
||||||
|
this.loadTeamPerformance();
|
||||||
|
},
|
||||||
|
|
||||||
|
// ✅【完全未改动】月份比较工具
|
||||||
|
compareMonths(a, b) {
|
||||||
|
const [aYear, aMonth] = a.split("-").map(Number);
|
||||||
|
const [bYear, bMonth] = b.split("-").map(Number);
|
||||||
|
if (aYear !== bYear) return aYear - bYear;
|
||||||
|
return aMonth - bMonth;
|
||||||
|
},
|
||||||
|
|
||||||
|
// ✅【核心逻辑完全未改动】加载数据 + 修复1个BUG + 替换加载提示
|
||||||
|
loadTeamPerformance() {
|
||||||
|
const loading = this.$loading({
|
||||||
|
lock: true,
|
||||||
|
text: '加载中',
|
||||||
|
spinner: 'el-icon-loading',
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)'
|
||||||
|
});
|
||||||
|
|
||||||
|
const startDate = `${this.selectedMonth}-01`
|
||||||
|
const [year, month] = this.selectedMonth.split('-')
|
||||||
|
const lastDay = new Date(year, month, 0).getDate()
|
||||||
|
const endDate = `${this.selectedMonth}-${String(lastDay).padStart(2, '0')}`
|
||||||
|
|
||||||
|
console.log('查询班组绩效:', startDate, endDate)
|
||||||
|
|
||||||
|
getTeamPerformance(startDate, endDate).then(response => {
|
||||||
|
loading.close()
|
||||||
|
|
||||||
|
if (response.code === 200 && response.data && response.data.length > 0) {
|
||||||
|
const teamData = response.data
|
||||||
|
console.log('班组绩效数据:', teamData)
|
||||||
|
|
||||||
|
// 排名数据(取前4名)
|
||||||
|
this.teamRankingData = teamData.slice(0, 4).map(item => ({
|
||||||
|
team: item.crew,
|
||||||
|
shift: item.shift,
|
||||||
|
score: Number(item.score) || 0
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ✅【修复原代码BUG】item.teamName 改为 item.crew (接口实际返回的字段)
|
||||||
|
this.tableData = teamData.map(item => ({
|
||||||
|
team: item.crew,
|
||||||
|
output: Number(item.output) || 0,
|
||||||
|
yieldRate: `${Number(item.yieldRate) || 0}%`,
|
||||||
|
passRate: `${Number(item.passRate) || 0}%`,
|
||||||
|
score: Number(item.score) || 0
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 提取图表所需数据
|
||||||
|
const categories = teamData.map(item => item.crew)
|
||||||
|
const outputData = teamData.map(item => Number(item.output) || 0)
|
||||||
|
const radarSeries = teamData.map(item => ({
|
||||||
|
name: item.crew,
|
||||||
|
value: [
|
||||||
|
Number(this.normalizeValue(item.output, 1500)),
|
||||||
|
Number(item.yieldRate) || 0,
|
||||||
|
Number(item.passRate) || 0,
|
||||||
|
Number(item.avgThickQuality) || 0,
|
||||||
|
Number(item.avgShapeQuality) || 0
|
||||||
|
],
|
||||||
|
areaStyle: {
|
||||||
|
color: '#409eff70'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 渲染图表
|
||||||
|
this.renderOutputChart(categories, outputData)
|
||||||
|
this.renderRadarChart(radarSeries)
|
||||||
|
} else {
|
||||||
|
console.log('暂无班组绩效数据')
|
||||||
|
this.tableData = []
|
||||||
|
this.teamRankingData = []
|
||||||
|
// 清空图表
|
||||||
|
this.renderOutputChart([], [])
|
||||||
|
this.renderRadarChart([])
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
loading.close()
|
||||||
|
console.error('加载班组绩效失败:', error)
|
||||||
|
this.$message.error('加载失败,请稍后重试')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// ✅【完全未改动】数值归一化工具
|
||||||
|
normalizeValue(value, max) {
|
||||||
|
return Math.min(100, (Number(value) / max) * 100).toFixed(2)
|
||||||
|
},
|
||||||
|
|
||||||
|
// ===== Echarts 图表初始化/渲染/自适应 新增方法 =====
|
||||||
|
initCharts() {
|
||||||
|
this.outputChart = echarts.init(document.getElementById('outputChart'))
|
||||||
|
this.radarChart = echarts.init(document.getElementById('radarChart'))
|
||||||
|
},
|
||||||
|
// 渲染产量对比柱状图
|
||||||
|
renderOutputChart(xData, yData) {
|
||||||
|
if (!this.outputChart) return
|
||||||
|
const emptyOption = {
|
||||||
|
grid: { left: 30, right: 30, top: 40, bottom: 30 },
|
||||||
|
title: { text: '暂无产量数据', left: 'center', textStyle: { color: '#909399' } }
|
||||||
|
}
|
||||||
|
if (!xData.length) {
|
||||||
|
this.outputChart.setOption(emptyOption)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const option = {
|
||||||
|
color: this.columnColor,
|
||||||
|
grid: { left: 30, right: 30, top: 20, bottom: 30 },
|
||||||
|
tooltip: { trigger: 'axis' },
|
||||||
|
xAxis: { type: 'category', data: xData, axisLine: { show: false } },
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: '产量(t)',
|
||||||
|
splitLine: { type: 'dashed', color: '#e4e7ed' }
|
||||||
|
},
|
||||||
|
series: [{ name: '产量', type: 'bar', data: yData, barWidth: 30 }]
|
||||||
|
}
|
||||||
|
this.outputChart.setOption(option, true)
|
||||||
|
},
|
||||||
|
// 渲染质量指标雷达图
|
||||||
|
renderRadarChart(seriesData) {
|
||||||
|
if (!this.radarChart) return
|
||||||
|
const emptyOption = {
|
||||||
|
grid: { left: 30, right: 30, top: 40, bottom: 30 },
|
||||||
|
title: { text: '暂无质量指标数据', left: 'center', textStyle: { color: '#909399' } }
|
||||||
|
}
|
||||||
|
if (!seriesData.length) {
|
||||||
|
this.radarChart.setOption(emptyOption)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const option = {
|
||||||
|
color: this.radarColor,
|
||||||
|
legend: { bottom: 0, left: 'center', textStyle: { fontSize: 12, color: '#666' } },
|
||||||
|
tooltip: { trigger: 'item' },
|
||||||
|
radar: {
|
||||||
|
indicator: [
|
||||||
|
{ name: '产量指标', max: 100 },
|
||||||
|
{ name: '成材率', max: 100 },
|
||||||
|
{ name: '合格率', max: 100 },
|
||||||
|
{ name: '厚度质量', max: 100 },
|
||||||
|
{ name: '板形质量', max: 100 }
|
||||||
|
],
|
||||||
|
splitLine: { lineStyle: { type: 'dashed', color: '#e4e7ed' } },
|
||||||
|
splitArea: { areaStyle: { opacity: 0.2 } }
|
||||||
|
},
|
||||||
|
series: [{ type: 'radar', data: seriesData }]
|
||||||
|
}
|
||||||
|
console.log('雷达图数据:', option)
|
||||||
|
this.radarChart.setOption(option, true)
|
||||||
|
},
|
||||||
|
// 图表自适应窗口大小
|
||||||
|
resizeCharts() {
|
||||||
|
this.outputChart && this.outputChart.resize()
|
||||||
|
this.radarChart && this.radarChart.resize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
// ✅ PC端完美适配:rpx转px(1rpx=0.5px) 保留原布局+样式+间距+配色 无改动
|
||||||
|
.page-container {
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 12px;
|
||||||
|
min-height: calc(100vh - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 月份选择器 */
|
||||||
|
.month-selector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-arrow {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #0066cc;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
color: #dcdfe6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-display {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 区块样式 */
|
||||||
|
.ranking-section,
|
||||||
|
.chart-section,
|
||||||
|
.detail-section {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-left: 8px;
|
||||||
|
border-left: 2px solid #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 排名列表 */
|
||||||
|
.ranking-list {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px;
|
||||||
|
border-bottom: 1px solid #f5f7fa;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rank-1 .rank-badge {
|
||||||
|
background: #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rank-2 .rank-badge {
|
||||||
|
background: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.rank-3 .rank-badge {
|
||||||
|
background: #66b1ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-badge {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #a0cfff;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-name {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-shift {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-score {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-value {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #0066cc;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图表容器 */
|
||||||
|
.chart-wrapper {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px 8px;
|
||||||
|
min-height: 300px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 详细数据表格 */
|
||||||
|
.detail-table {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
background: #0066cc;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-cell {
|
||||||
|
color: #fff !important;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid #f5f7fa;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-cell {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 6px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.empty-state {
|
||||||
|
padding: 50px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
56
klp-ui/src/views/lines/acid/index.vue
Normal file
56
klp-ui/src/views/lines/acid/index.vue
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-tabs v-model="activeTab" type="card">
|
||||||
|
<el-tab-pane label="实时监控" name="1">
|
||||||
|
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="生产统计" name="2">
|
||||||
|
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="停机统计" name="3">
|
||||||
|
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="班组绩效" name="4">
|
||||||
|
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div v-if="activeTab === '1'">
|
||||||
|
<realTimeMonitor></realTimeMonitor>
|
||||||
|
</div>
|
||||||
|
<div v-if="activeTab === '2'">
|
||||||
|
<productStatistic></productStatistic>
|
||||||
|
</div>
|
||||||
|
<div v-if="activeTab === '3'">
|
||||||
|
<shutdownStatistic></shutdownStatistic>
|
||||||
|
</div>
|
||||||
|
<div v-if="activeTab === '4'">
|
||||||
|
<TeamPerformance></TeamPerformance>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import TeamPerformance from './components/team-performance.vue'
|
||||||
|
import ShutdownStatistic from './components/shutdown-statistic.vue';
|
||||||
|
import ProductStatistic from './components/product-statistic.vue';
|
||||||
|
import RealTimeMonitor from './components/real-time-monitoring.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeTab: '1'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
TeamPerformance,
|
||||||
|
ShutdownStatistic,
|
||||||
|
ProductStatistic,
|
||||||
|
RealTimeMonitor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
155
klp-ui/src/views/lines/zinc/components/product-statistic.vue
Normal file
155
klp-ui/src/views/lines/zinc/components/product-statistic.vue
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<div class="report-page" style="padding: 20px;">
|
||||||
|
<!-- 汇总统计信息 el-descriptions 展示 -->
|
||||||
|
<el-card shadow="hover" class="summary-card" style="margin-bottom: 20px;">
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span style="font-size: 16px; font-weight: bold;">生产报表-数据汇总</span>
|
||||||
|
</div>
|
||||||
|
<el-descriptions
|
||||||
|
:data="productStatistic"
|
||||||
|
title=""
|
||||||
|
border
|
||||||
|
column="4"
|
||||||
|
size="middle"
|
||||||
|
label-width="140px"
|
||||||
|
>
|
||||||
|
<el-descriptions-item label="总出口宽度(mm)">{{ productStatistic.totalExitWidth || 0 }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="总出口长度(m)">{{ formatNum(productStatistic.totalExitLength) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="总理论重量(kg)">{{ formatNum(productStatistic.totalTheoryWeight) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="总实际重量(kg)">{{ formatNum(productStatistic.totalActualWeight) }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label="总出口厚度(mm)">{{ formatNum(productStatistic.totalExitThickness) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="平均出口宽度(mm)">{{ formatNum(productStatistic.avgExitWidth) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="平均出口长度(m)">{{ formatNum(productStatistic.avgExitLength) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="平均理论重量(kg)">{{ formatNum(productStatistic.avgTheoryWeight) }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label="平均实际重量(kg)">{{ formatNum(productStatistic.avgActualWeight) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="平均出口厚度(mm)">{{ formatNum(productStatistic.avgExitThickness) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="总来料重量(kg)">{{ formatNum(productStatistic.totalEntryWeight) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="总卷数">{{ productStatistic.coilCount || 0 }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label="成材率" span="4" label-width="140px">
|
||||||
|
<span style="color: #1890ff;">{{ formatRate(productStatistic.yieldRate) }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 明细数据 el-table 展示 -->
|
||||||
|
<el-card shadow="hover" class="detail-card">
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span style="font-size: 16px; font-weight: bold;">生产报表-明细数据</span>
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
:data="reportDetails"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
size="small"
|
||||||
|
v-loading="tableLoading"
|
||||||
|
element-loading-text="加载中..."
|
||||||
|
style="width: 100%;"
|
||||||
|
highlight-current-row
|
||||||
|
>
|
||||||
|
<el-table-column prop="exitMatId" label="出口物料编码" align="center" />
|
||||||
|
<el-table-column prop="entryMatId" label="入口物料编码" align="center" />
|
||||||
|
<el-table-column prop="groupNo" label="班组号" align="center" width="60" />
|
||||||
|
<el-table-column prop="shiftNo" label="班次号" align="center" width="60" />
|
||||||
|
<el-table-column prop="steelGrade" label="钢种" align="center" width="80" />
|
||||||
|
<el-table-column prop="exitWidth" label="出口宽度(mm)" align="center">
|
||||||
|
<template #default="scope">{{ formatNum(scope.row.exitWidth) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="exitLength" label="出口长度(m)" align="center">
|
||||||
|
<template #default="scope">{{ formatNum(scope.row.exitLength) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="exitThickness" label="出口厚度(mm)" align="center">
|
||||||
|
<template #default="scope">{{ formatNum(scope.row.exitThickness) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="theoryWeight" label="理论重量(kg)" align="center">
|
||||||
|
<template #default="scope">{{ formatNum(scope.row.theoryWeight) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="actualWeight" label="实际重量(kg)" align="center">
|
||||||
|
<template #default="scope">{{ formatNum(scope.row.actualWeight) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="onlineTime" label="上线时间" align="center" width="190">
|
||||||
|
<template #default="scope">{{ formatTime(scope.row.onlineTime) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="endTime" label="结束时间" align="center" width="190">
|
||||||
|
<template #default="scope">{{ formatTime(scope.row.endTime) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getReportSummary, getReportDetails } from '@/api/lines/zinc/report'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
productStatistic: {}, // 汇总数据
|
||||||
|
reportDetails: [], // 明细数据
|
||||||
|
tableLoading: false, // 表格加载状态
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 数字格式化:保留2位小数,空值显示0
|
||||||
|
formatNum(num) {
|
||||||
|
return num ? Number(num).toFixed(2) : '0.00'
|
||||||
|
},
|
||||||
|
// 成材率格式化:保留2位小数+百分比
|
||||||
|
formatRate(rate) {
|
||||||
|
return rate ? (Number(rate) * 100).toFixed(2) + '%' : '0.00%'
|
||||||
|
},
|
||||||
|
// 时间格式化:处理ISO时间/空值/null,统一格式
|
||||||
|
formatTime(time) {
|
||||||
|
if (!time) return '-'
|
||||||
|
const date = new Date(time)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||||
|
const day = date.getDate().toString().padStart(2, '0')
|
||||||
|
const hh = date.getHours().toString().padStart(2, '0')
|
||||||
|
const mm = date.getMinutes().toString().padStart(2, '0')
|
||||||
|
const ss = date.getSeconds().toString().padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day} ${hh}:${mm}:${ss}`
|
||||||
|
},
|
||||||
|
// 获取汇总数据
|
||||||
|
async getReportSummary() {
|
||||||
|
try {
|
||||||
|
const res = await getReportSummary()
|
||||||
|
this.productStatistic = res || {}
|
||||||
|
} catch (err) {
|
||||||
|
this.$message.error('获取汇总数据失败!')
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 获取明细数据
|
||||||
|
async getReportDetails() {
|
||||||
|
this.tableLoading = true
|
||||||
|
try {
|
||||||
|
const res = await getReportDetails()
|
||||||
|
this.reportDetails = res || []
|
||||||
|
} catch (err) {
|
||||||
|
this.$message.error('获取明细数据失败!')
|
||||||
|
console.error(err)
|
||||||
|
} finally {
|
||||||
|
this.tableLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// 页面加载时调用两个接口
|
||||||
|
this.getReportSummary()
|
||||||
|
this.getReportDetails()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.report-page {
|
||||||
|
background: #f5f7fa;
|
||||||
|
min-height: calc(100vh - 120px);
|
||||||
|
}
|
||||||
|
.summary-card, .detail-card {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
实时监控
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
165
klp-ui/src/views/lines/zinc/components/shutdown-statistic.vue
Normal file
165
klp-ui/src/views/lines/zinc/components/shutdown-statistic.vue
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<div class="stoppage-page" style="padding: 20px;">
|
||||||
|
<!-- 月份筛选查询区域 -->
|
||||||
|
<div class="search-box" style="margin: 16px 0; display: flex; align-items: center;">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="month"
|
||||||
|
type="month"
|
||||||
|
placeholder="请选择月份"
|
||||||
|
value-format="yyyy-MM"
|
||||||
|
style="width: 200px;"
|
||||||
|
@change="handleMonthChange"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" icon="el-icon-search" style="margin-left: 10px;" @click="handleQuery">查询</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 停机统计表格 ✅【全部修改】适配真实JSON数据字段 -->
|
||||||
|
<el-table
|
||||||
|
:data="stoppageList"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
size="small"
|
||||||
|
v-loading="loading"
|
||||||
|
element-loading-text="数据加载中..."
|
||||||
|
style="width: 100%;"
|
||||||
|
highlight-current-row
|
||||||
|
>
|
||||||
|
<el-table-column prop="stopType" label="停机类型" align="center" width="140" />
|
||||||
|
<el-table-column prop="startDate" label="停机开始时间" align="center" width="200" />
|
||||||
|
<el-table-column prop="endDate" label="停机结束时间" align="center" width="200" />
|
||||||
|
<el-table-column prop="duration" label="停机时长" align="center" width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDuration(scope.row.duration) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="insdate" label="数据录入时间" align="center" width="200" />
|
||||||
|
<!-- 生产相关附属字段 -->
|
||||||
|
<el-table-column prop="coilid" label="钢卷号" align="center" width="120" />
|
||||||
|
<el-table-column prop="shift" label="班次" align="center" width="100" />
|
||||||
|
<!-- <el-table-column prop="crew" label="班组人员" align="center" width="120" />
|
||||||
|
<el-table-column prop="area" label="区域" align="center" width="100" />
|
||||||
|
<el-table-column prop="unit" label="机组" align="center" width="100" />
|
||||||
|
<el-table-column prop="seton" label="开机人" align="center" width="100" /> -->
|
||||||
|
<!-- 备注字段 -->
|
||||||
|
<el-table-column prop="remark" label="备注" align="center" min-width="220" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listStoppage } from '@/api/lines/zinc/stoppage'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
stoppageList: [], // 停机统计列表
|
||||||
|
total: 0, // 数据总条数
|
||||||
|
loading: false, // 加载状态
|
||||||
|
month: '', // 选中的月份 yyyy-MM格式
|
||||||
|
queryParams: {
|
||||||
|
startDate: '', // 开始时间 yyyy-MM-dd✅ 与后端字段名一致
|
||||||
|
endDate: '' // 结束时间 yyyy-MM-dd✅ 与后端字段名一致
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// 初始化:默认选中当前月份,并生成当月的起止时间
|
||||||
|
this.initDefaultMonth()
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// 页面加载时,默认查询当月停机数据
|
||||||
|
this.listStoppage()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 格式化持续时间:将分钟数转换为"X天X小时X分钟"格式
|
||||||
|
formatDuration(minutes) {
|
||||||
|
if (minutes === null || minutes === undefined || minutes === '') return '—'
|
||||||
|
const totalMinutes = Math.floor(Number(minutes))
|
||||||
|
if (isNaN(totalMinutes) || totalMinutes < 0) return '—'
|
||||||
|
|
||||||
|
const days = Math.floor(totalMinutes / (24 * 60))
|
||||||
|
const hours = Math.floor((totalMinutes % (24 * 60)) / 60)
|
||||||
|
const mins = Math.floor(totalMinutes % 60)
|
||||||
|
|
||||||
|
let result = ''
|
||||||
|
if (days > 0) {
|
||||||
|
result += `${days}天`
|
||||||
|
}
|
||||||
|
if (hours > 0 || days > 0) {
|
||||||
|
result += `${hours}小时`
|
||||||
|
}
|
||||||
|
if (mins > 0 || result === '') {
|
||||||
|
result += `${mins}分钟`
|
||||||
|
}
|
||||||
|
|
||||||
|
return result || '0分钟'
|
||||||
|
},
|
||||||
|
/** 初始化默认月份和起止时间 */
|
||||||
|
initDefaultMonth() {
|
||||||
|
const now = new Date()
|
||||||
|
const year = now.getFullYear()
|
||||||
|
const month = (now.getMonth() + 1).toString().padStart(2, '0')
|
||||||
|
this.month = `${year}-${month}`
|
||||||
|
// 根据当前月份生成时间范围
|
||||||
|
this.setMonthTimeRange(this.month)
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 选中月份 → 生成 当月1号00:00:00 至 下个月1号00:00:00 的时间格式 */
|
||||||
|
setMonthTimeRange(month) {
|
||||||
|
if (!month) return
|
||||||
|
// 开始时间:选中月份的 1号 00:00:00
|
||||||
|
this.queryParams.startDate = `${month}-01`
|
||||||
|
// 解析选中的月份为日期对象
|
||||||
|
const selectMonth = new Date(month + '-01')
|
||||||
|
// 下个月1号:月份+1,日期为1号
|
||||||
|
const nextMonth = new Date(selectMonth.setMonth(selectMonth.getMonth() + 1))
|
||||||
|
const nextYear = nextMonth.getFullYear()
|
||||||
|
const nextMonthNum = (nextMonth.getMonth() + 1).toString().padStart(2, '0')
|
||||||
|
// 结束时间:下个月 1号 00:00:00
|
||||||
|
this.queryParams.endDate = `${nextYear}-${nextMonthNum}-01`
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 月份选择器切换事件 */
|
||||||
|
handleMonthChange(val) {
|
||||||
|
this.setMonthTimeRange(val)
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 查询按钮点击事件 */
|
||||||
|
handleQuery() {
|
||||||
|
this.listStoppage()
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 核心:查询停机统计列表数据,带时间筛选参数 */
|
||||||
|
async listStoppage() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const params = { ...this.queryParams }
|
||||||
|
const res = await listStoppage(params)
|
||||||
|
// 适配后端返回格式:数组/分页对象都兼容
|
||||||
|
this.stoppageList = res.data || res || []
|
||||||
|
this.total = res.total || this.stoppageList.length
|
||||||
|
} catch (err) {
|
||||||
|
this.$message.error('查询停机统计数据失败!')
|
||||||
|
console.error('停机统计查询异常:', err)
|
||||||
|
this.stoppageList = []
|
||||||
|
this.total = 0
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.stoppage-page {
|
||||||
|
background: #f5f7fa;
|
||||||
|
min-height: calc(100vh - 40px);
|
||||||
|
}
|
||||||
|
.search-box {
|
||||||
|
background: #fff;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
56
klp-ui/src/views/lines/zinc/index.vue
Normal file
56
klp-ui/src/views/lines/zinc/index.vue
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-tabs v-model="activeTab" type="card">
|
||||||
|
<el-tab-pane label="实时监控" name="1">
|
||||||
|
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="生产统计" name="2">
|
||||||
|
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="停机统计" name="3">
|
||||||
|
|
||||||
|
</el-tab-pane>
|
||||||
|
<!-- <el-tab-pane label="班组绩效" name="4">
|
||||||
|
|
||||||
|
</el-tab-pane> -->
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div v-if="activeTab === '1'">
|
||||||
|
<realTimeMonitor></realTimeMonitor>
|
||||||
|
</div>
|
||||||
|
<div v-if="activeTab === '2'">
|
||||||
|
<productStatistic></productStatistic>
|
||||||
|
</div>
|
||||||
|
<div v-if="activeTab === '3'">
|
||||||
|
<shutdownStatistic></shutdownStatistic>
|
||||||
|
</div>
|
||||||
|
<!-- <div v-if="activeTab === '4'">
|
||||||
|
<TeamPerformance></TeamPerformance>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// import TeamPerformance from './components/team-performance.vue'
|
||||||
|
import ShutdownStatistic from './components/shutdown-statistic.vue';
|
||||||
|
import ProductStatistic from './components/product-statistic.vue';
|
||||||
|
import RealTimeMonitor from './components/real-time-monitoring.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeTab: '1'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
// TeamPerformance,
|
||||||
|
ShutdownStatistic,
|
||||||
|
ProductStatistic,
|
||||||
|
RealTimeMonitor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
@@ -45,6 +45,14 @@ module.exports = {
|
|||||||
['^' + process.env.VUE_APP_BASE_API]: ''
|
['^' + process.env.VUE_APP_BASE_API]: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// 直接代理Zinc1相关路径
|
||||||
|
'/zinc-api': {
|
||||||
|
target: `http://140.143.206.120:18082/prod-api`,
|
||||||
|
changeOrigin: true,
|
||||||
|
pathRewrite: {
|
||||||
|
['^' + process.env.VUE_APP_BASE_API]: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
// 直接代理WebSocket相关路径
|
// 直接代理WebSocket相关路径
|
||||||
'/websocket': {
|
'/websocket': {
|
||||||
target: `http://localhost:8080`,
|
target: `http://localhost:8080`,
|
||||||
|
|||||||
Reference in New Issue
Block a user