Commit 964bf964 authored by liubinyu's avatar liubinyu

12月7日,湖南第一次完整版

parents
/node_modules
/dist
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
{
"name": "hnsc",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
"dependencies": {
"axios": "^0.19.2",
"core-js": "^3.6.4",
"echarts": "^4.9.0",
"element-ui": "^2.13.1",
"vue": "^2.6.11",
"vue-router": "^3.1.6",
"vuex": "^3.1.3"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.3.0",
"@vue/cli-plugin-router": "~4.3.0",
"@vue/cli-plugin-vuex": "~4.3.0",
"@vue/cli-service": "~4.3.0",
"compression-webpack-plugin": "^5.0.2",
"node-sass": "^4.12.0",
"sass-loader": "^8.0.2",
"sass-resources-loader": "^2.0.3",
"vue-template-compiler": "^2.6.11"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
</body>
</html>
<!-- 入口 -->
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {};
</script>
// 电厂信息接口
import request from '@/common/request.js'
export default {
/**
* @todo: 获取电厂列表
* @param: 无
*/
getPlantList: () => {
return request({
url: 'plant/info/list',
method: 'get',
})
},
/**
* @todo: 获取机组上所有测点
* @param: id 机组id
*/
getCharacterList: (id) => {
return request({
url: `plant/device/downBox/${id}`,
method: 'get',
})
},
/**
* @todo: 获取测点趋势数据
* @param: keys 测点kkscode列表,status 数据类型0原始1周均线,timeModelQuery 时间范围
*/
getTendency: (data) => {
return request({
url: 'plant/measure/tendency',
method: 'post',
data
})
}
}
// 用户和角色相关接口
import request from '@/common/request.js'
import {Message} from 'element-ui'
export default {
/**
* @todo: 登录
* @param: username
* @param: password
*/
login: (username, password) => {
return request({
url: 'user/login',
method: 'post',
data: {username,password},
noToken: true,
// 自定义拦截器,获取token
responseInterceptor: response => {
localStorage.setItem('electric_user_token', response.headers.authorization || '');
const res = response.data
// 后台返回的数据如果不是200,也抛出reject
if (res.code !== 200) {
Message({message: res.msg || 'Error', type: 'error',})
return Promise.reject(new Error(res.msg || 'Error'))
} else {
return res.data
}
}
})
},
/**
* @todo: 获取电厂的用户列表
* @param {Number} pageNo 页码
* @param {Number} pageSize 每页记录数
*/
getUserList: (params) => {
return request({
url: 'userInfo/page',
method: 'get',
params
})
},
/**
* @todo: 新增用户
* @param: {String} username 用户名
* @param: {String} password 密码
*/
addUser: (data) => {
return request({
url: 'userInfo/new',
method: 'post',
data
})
},
/**
* @todo: 修改用户
* @param: {Number} id 用户id
* @param: {String} username 用户名
* @param: {String} password 密码
*/
editUser: (data) => {
return request({
url: 'userInfo/edit',
method: 'post',
data
})
},
/**
* @todo: 删除用户
* @param: {Number} id 用户id
*/
delUser: (id) => {
return request({
url: `userInfo/del?id=${id}`,
method: 'post',
})
},
/**
* @todo: 获取电厂的角色列表
*/
getRoleList: () => {
return request({
url: `role/list`,
method: 'post',
})
},
/**
* @todo: 新增/修改角色
* @param: {Object} param 角色信息参数,其中id为角色id(有就是更新,没有则为新增)
*/
newOrUpdateRole: (param) => {
return request({
url: `role/newOrUpdate`,
method: 'post',
data: param
})
},
}
\ No newline at end of file
// 报警记录
import request from '@/common/request.js'
export default {
/**
* @todo: 获取报警列表
* @param: plantId 电厂id
* @param: deviceId 机组id
* @param: eventInfo 0未处理1已处理
* @param: pageNo 页码
* @param: pageSize 页尺寸
*/
getWarnList: (params) => {
return request({
url: 'alarm/record/page/list',
method: 'get',
params
})
},
/**
* @todo: 改变事件状态
* @param: id 事件id
* @param: eventInfo 0未处理1已处理
*/
changeState: (data) => {
return request({
url: '/alarm/record/eventInfo/status',
method: 'put',
data
})
},
}
This source diff could not be displayed because it is too large. You can view the blob instead.
// 常量
// const server_ip = '8.130.25.40'; // 测试服务器
// const server_ip = '127.0.0.1';
const server_ip = '192.168.192.128'; // 线上内网ip
// const server_ip = '192.168.43.122:8258';
export const CONFIG = {
SERVER_IP: server_ip,
SOCKET: server_ip + '/socket', // 线上
// SOCKET: server_ip, // 本地
API_URL: '/plant/',
TITLE: '湖南三厂机组实时监测系统', // 项目名称
CHARACTER_TAB: '#', // 测点名字里的标识,比如1F定子CA线电压,中的"F",因为会根据这个标识来分割测点名字,可以为空,也可以为多个字符
}
// 路由映射,key为路由名name(存库),value为显示名
export const RouteMap = {
FactoryMonitoring: '全厂监测',
CrewMonitoring: '机组监测',
TrendAnalysis: '趋势分析',
AlarmRecord: '报警记录',
UserManage: '用户管理',
RoleManage: '角色管理'
}
// 按钮映射
export const ButtonMap = {
UserDelete: '用户删除',
}
// 首页图表样式
export const ChartStyle = {
loadingColor: 'rgba(0, 0, 0, 0.5)', // 加载动画颜色
color: ['#509FFF', '#E5394C', '#0011A5', '#FF57BF', '#43C6AC', '#AA076B', '#0ED2F7', '#9733EE'], // 数据颜色
colorGradients: ['#6AF6FE', '#F0724D', '#00BAE6', '#FF8B7C', '#F8FFAE', '#E991C3', '#92FE9D', '#C3A0FF'], // 数据渐变颜色
// 标题样式
title: {
color: '#93FCFF',
fontSize: 14,
},
// 图例样式
legend: {
color: '#ffffff',
fontSize: 12,
},
// 轴样式
axis: {
color: '#5caac3',
fontSize: 10,
lineHeight: 14,
},
// 数据文字样式
label: {
color: '#ffffff',
fontSize: 12,
},
// 辅助线样式
split: {
color: '#93FCFF',
opacity: 0.2,
}
}
// 机组状态
export const CrewState = {
// 报警
warn: {
key: 'warn',
name: '报警',
color: '#F3B242'
},
// 危险
danger: {
key: 'danger',
name: '危险',
color: '#E53B4C'
},
// 停机
shutdown: {
key: 'shutdown',
name: '停机',
color: '#55B8F0'
},
// 正常
normal: {
key: 'normal',
name: '正常',
color: '#59E594'
}
}
// 报警数据,key为字段前缀,不要更改
export const WarningConfig = {
// 高高报警(一级)
protect: {
name: '一级报警',
color: '#FF636B',
color2: '#823237',
state: 5,
warnLevel: 1, // 报警等级,会依次按照这个字段统计报警数量,用于阈值更新里的测试
},
// 高报警(二级)
alert: {
name: '二级报警',
color: '#FFBA49',
color2: '#825e24',
state: 4,
warnLevel: 2,
},
// 预报警(三级)
earlywarning: {
name: '三级报警',
color: '#ffff00',
color2: '#828200',
state: 3,
warnLevel: 3,
},
// // 上趋势报警
// trendalert: {
// name: '趋势报警',
// color: '#0000ff',
// color2: '#000082',
// state: 6,
// warnLevel: 4,
// },
// // 下趋势报警
// trendalert2: {
// name: '趋势报警',
// color: '#00aaff',
// color2: '#005782',
// state: 7,
// warnLevel: 5,
// },
// 正常
normal: {
name: '正常',
color: '#00de6b',
color2: '#00823c',
state: 1
},
// 无效
invalid: {
name: '无效',
color: '#a2a2a2',
color2: '#484848',
state: 2
},
}
// 时间线的按钮组
export const AllDateLineBtn = [{
name: '原始分析',
value: 0
}, {
name: '本周',
value: 1
// }, {
// name: '本月趋势',
// value: 2
// }, {
// name: '本年趋势',
// value: 3
}];
// 获取日期时间选择快捷时间,用于方便管理
export function GetPickerOptions() {
return {
shortcuts: [{
text: '最近一小时',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000);
picker.$emit('pick', [start, end]);
}
},
{
text: '最近三小时',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 3);
picker.$emit('pick', [start, end]);
}
},
{
text: '最近一天',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24);
picker.$emit('pick', [start, end]);
}
},
{
text: '最近三天',
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 3);
picker.$emit('pick', [start, end]);
}
},
]
}
}
// 日期选择的默认时间
export const DefaultDateSelect = () => [Date.now() - 3600 * 1000, Date.now()]; // 最近1小时
// 全厂监测测点配置
export const FactoryMConfig = {
power: ['有功功率'], // 功率
// 框框
list: [
["上导X向摆度"],
["上导Y向摆度"],
["下导X向摆度"],
["下导Y向摆度"],
["法兰X向摆度"],
["法兰Y向摆度"],
["水导X向摆度"],
["水导Y向摆度"],
["上机架X向振动"],
["上机架Y向振动"],
["上机架Z向振动"],
["下机架X向振动"],
["下机架Y向振动"],
["下机架Z向振动"],
["顶盖X向振动"],
["顶盖Y向振动"],
["顶盖Z向振动X"],
["顶盖Z向振动Y"],
["顶盖Z向振动"]
]
}
// 机组监测测点配置
export const CrewMConfig = {
// 顶部
top: [
['转速'],
['有功功率'],
],
// 框框左上
listLeftTop: [
["上导X向摆度"],
["上导Y向摆度"],
["下导X向摆度"],
["下导Y向摆度"],
["法兰X向摆度"],
["法兰Y向摆度"],
["水导X向摆度"],
["水导Y向摆度"],
["上机架X向振动"],
["上机架Y向振动"],
["上机架Z向振动"],
],
// 框框右上
listRightTop: [
["大轴轴向位移A"],
["大轴轴向位移B"],
["大轴轴向位移C"],
["下机架X向振动"],
["下机架Y向振动"],
["下机架Z向振动"],
["瓦架X向振动"],
["瓦架Z向振动"],
],
// 框框左下
listLeftBottom: [
["定子机座X向振动"],
["定子机座Y向振动"],
["定子基座X向振动"],
["定子基座Y向振动"],
["定子铁芯X向振动"],
["定子铁芯Y向振动"],
["蜗壳进口压力脉动"],
["蜗壳差压"],
["导叶后压力脉动"]
],
// 框框右下
listRightBottom: [
["顶盖碰磨Z向振动"],
["顶盖碰磨X向振动"],
["顶盖X向振动"],
["顶盖Y向振动"],
["顶盖Z向振动X"],
["顶盖Z向振动Y"],
["顶盖Z向振动"],
["顶盖下压力脉动"],
["尾水管X向振动"],
["尾水管进口压力脉动"],
["尾水管出口压力脉动"],
["尾水管出口压力"],
],
// 图表左下
leftBottom: [
["有功功率"],
["无功功率"],
["接力器行程"],
["励磁电压"],
["励磁电流"],
["转速"],
["发电机出口开关"],
["励磁开关"],
["上游水位"],
["下游水位"],
["水头"],
["导叶开度"]
],
// 图表右上
rightTop: [
["上机架X向振动"],
["上机架Y向振动"],
["上机架Z向振动"],
["定子机座X向振动"],
["定子机座Y向振动"],
["定子基座X向振动"],
["定子基座Y向振动"],
["定子铁芯X向振动"],
["定子铁芯Y向振动"]
],
// 图表右中
rightCenter: [
["顶盖碰磨Z向振动"],
["顶盖碰磨X向振动"],
["顶盖X向振动"],
["顶盖Y向振动"],
["顶盖Z向振动X"],
["顶盖Z向振动Y"],
["顶盖Z向振动"],
["尾水管X向振动"],
["顶盖下压力脉动"],
["尾水管进口压力脉动"],
["尾水管出口压力脉动"],
["尾水管出口压力"],
["蜗壳进口压力脉动"],
["蜗壳差压"],
["导叶后压力脉动"]
],
// 图表右下
rightBottom: [
["上导X向摆度"],
["上导Y向摆度"],
["上导瓦温"],
["下导X向摆度"],
["下导Y向摆度"],
["推力瓦温"],
["水导X向摆度"],
["水导Y向摆度"],
["水导瓦温"]
],
}
import axios from 'axios'
import {
CONFIG
} from './const.js'
import {
userLogout
} from './util.js'
import {
Message
} from 'element-ui'
import router from '../router'
// 封装接口请求,如果不是正常返回,均会用reject处理,并且会默认抛出message提示,如果需要自定义,可以在不同接口配置响应拦截器
const service = axios.create({
baseURL: CONFIG.API_URL,
})
service.interceptors.request.use(
config => {
// 添加token
const token = localStorage.getItem('electric_user_token');
if (!config.noToken && token) config.headers['Authorization'] = token;
return config
},
error => {
console.log('request error:', error)
return Promise.reject(error)
}
)
service.interceptors.response.use(
response => {
// 如果设置了拦截器,执行,否则执行默认拦截器
if (response.config.responseInterceptor) {
return response.config.responseInterceptor(response);
} else {
const res = response.data
// 后台返回的数据如果不是200,也抛出reject
if (res.code !== 200) {
Message({
message: res.msg || 'Error',
type: 'error',
})
return Promise.reject(new Error(res.msg || 'Error'))
} else {
return res.data
}
}
},
error => {
console.log('response error:' + error)
if (error && error.response) {
// 处理响应状态
switch (error.response.status) {
case 401:
userLogout();
break
default:
Message({
message: error.message,
type: 'error',
})
break
}
}
return Promise.reject(error)
}
)
export default service
// 工具函数
import {
CONFIG,
WarningConfig
} from '@/common/const.js';
import store from '../store/index.js';
import {
Message
} from 'element-ui'
/**
* @auto 刘彬瑜
* @todo 封装日期格式化,这里代替moment,因为moment太大了
* @param {Date} date date对象
* @param {String} fmt 输出的格式(参考moment)
* @return
*/
export function dateFormat(date, fmt) {
var o = {
"M+": date.getMonth() + 1, //月份
"D+": date.getDate(), //日
"H+": date.getHours(), //小时
"m+": date.getMinutes(), //分
"s+": date.getSeconds(), //秒
"z": ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'][date.getDay()], // 中文星期
"e": ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()], // 英文星期
"q+": Math.floor((date.getMonth() + 3) / 3), //季度
"x": date.getMilliseconds() //毫秒
};
if (/(Y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
for (var k in o) {
if (new RegExp("(" + k + ")").test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
}
}
return fmt;
}
/**
* @todo: 防抖函数
* @param: {Function} func 目标方法
* @param: {Number} wait 延时时间
* @param: {Boolean} immediate 是否立即执行
*/
export function debounce(func, wait, immediate) {
let timeout, args, context, timestamp, result
const later = function() {
// 据上一次触发时间间隔
const last = +new Date() - timestamp
// 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last)
} else {
timeout = null
// 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
if (!immediate) {
result = func.apply(context, args)
if (!timeout) context = args = null
}
}
}
return function(...args) {
context = this
timestamp = +new Date()
const callNow = immediate && !timeout
// 如果延时不存在,重新设定延时
if (!timeout) timeout = setTimeout(later, wait)
if (callNow) {
result = func.apply(context, args)
context = args = null
}
return result
}
}
/**
* @todo 根据信号状态或名字,返回对应的报警对象
* @param {Number} state 信号状态
* @param {String} name 报警名称
* @return {Object} warn 报警对象
*/
export function getWarnItem(state, name) {
if (state) {
for (let key in WarningConfig) {
if (WarningConfig[key].state == parseInt(state)) {
return WarningConfig[key];
}
}
}
if (name) {
for (let key in WarningConfig) {
if (WarningConfig[key].name == name) {
return WarningConfig[key];
}
}
}
return null;
}
/**
* @todo 根据信号状态,返回对应的报警颜色
* @param {Number} state 信号状态
* @param {Boolean} gradients 是否渐变,1是0否
* @return {String} color 颜色,如果选择了渐变,则返回数组[color, color2]
*/
export function getWarnColor(state, gradients) {
for (let key in WarningConfig) {
if (WarningConfig[key].state == parseInt(state)) {
return gradients ? [WarningConfig[key].color, WarningConfig[key].color2] : WarningConfig[key].color;
}
}
return gradients ? ['#fff', '#fff'] : '#fff'; //如果找不到,返回白色,正常应该是能找到的
}
/**
* @todo 根据报警名称,返回对应的报警颜色
* @param {String} name 报警等级名称
* @param {Boolean} gradients 是否渐变,1是0否
* @return {String} color 颜色,如果选择了渐变,则返回数组[color, color2]
*/
export function getWarnColorByName(name, gradients) {
for (let key in WarningConfig) {
if (WarningConfig[key].name == name) {
return gradients ? [WarningConfig[key].color, WarningConfig[key].color2] : WarningConfig[key].color;
}
}
return gradients ? ['#fff', '#fff'] : '#fff'; //如果找不到,返回白色,正常应该是能找到的
}
/**
* @todo: 根据给定值的数组和阈值,返回对应的报警比例
* @param {Array} arr 一组值
* @param {Object} threshold 阈值,如果不传入阈值,则按数据里的信号状态统计,传了按阈值重新统计
* @return {Object} distribution 分布,例如{'02': 600, '03': 200},代表信号状态为2的报警有600次,信号状态为3的报警有200次
*/
export function getWarnDistribution(arr, threshold) {
if (!arr || !arr.length) return {};
let count = {};
// 按信号状态统计
if (!threshold) {
for (let i = (arr.length - 1); i >= 0; i--) {
const state = arr[i]?.toString().substr(-2) || 0; // 取出信号状态
if (count[state]) {
count[state]++;
} else {
count[state] = 1;
}
}
// 按阈值统计
} else {
// 整理各个等级的阈值
let warnLevel = [];
for (let key in WarningConfig) {
if (WarningConfig[key].warnLevel && (key + 'RuleExclude' in threshold)) {
warnLevel.push({
state: WarningConfig[key].state,
max: threshold[key + 'RuleMaxValue'],
min: threshold[key + 'RuleMinValue'],
exclude: threshold[key + 'RuleExclude'],
level: WarningConfig[key].warnLevel
})
}
}
warnLevel.sort((a, b) => b.level - a.level); // 按报警等级,从低到高排序
for (let i = (arr.length - 1); i >= 0; i--) {
let state = 0;
let val = parseFloat(arr[i]);
// 遍历每个报警等级,判断是否报警,这里是先判断低的,比如先判断趋势报警是否触发,如果没触发,则state没有赋值,则跳过(未报警),如果触发了,再找上一个报警等级(预警),如果触发了,更新state
for (let level of warnLevel) {
if ((level.exclude && ((level.min !== '' && val <= level.min) || (level.max !== '' && val >= level.max))) || (!
level.exclude && ((level.min !== '' && level.max !== '' && val >= level.min && val <= level.max) || (
level.min !== '' && level.max === '' && val >= level.min) || (
level.min === '' && level.max !== '' && val <= level.max)))) {
state = level.state;
}
}
if (!state) continue;
if (count[state]) {
count[state]++;
} else {
count[state] = 1;
}
}
}
return count;
}
// 正态分布数据
export function initDataNormal() {
// let data = {
// xAxis: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
// dataList: [{name: 'a', value: [0, 0.02, 0.12, 0.42, 0.64, 0.7, 0.64, 0.42, 0.12, 0.02, 0]}],
// }
let data = [
[0, 0],
[1, 0.1],
[2, 0.2],
[3, 0.4],
[4, 0.7],
[5, 1.2],
[6, 1.95],
[7, 2.7],
[8, 3.4],
[9, 4.05],
[10, 4.6],
[11, 5.15],
[12, 5.6],
[13, 5.95],
[14, 6.2],
[15, 6.35],
[16, 6.44],
[17, 6.48],
[18, 6.44],
[19, 6.35],
[20, 6.2],
[21, 5.95],
[22, 5.6],
[23, 5.15],
[24, 4.6],
[25, 4.05],
[26, 3.4],
[27, 2.7],
[28, 1.95],
[29, 1.2],
[30, 0.7],
[31, 0.4],
[32, 0.2],
[33, 0.1],
[34, 0]
]
return data;
}
/**
* @todo 导出数据到文件
* @param {Json} data json格式的数据
* @param {String} name 文件名
* @param {String} type 文件后缀
* @return 无
*/
export function exportDataToFile(data, name, type) {
var urlObject = window.URL || window.webkitURL || window;
var export_blob = type == '.csv' ? new Blob(['\uFEFF' + data], { type: 'text/csv;charset=utf-8;' }) : new Blob([data]);
var save_link = document.createElement('a');
save_link.href = urlObject.createObjectURL(export_blob);
save_link.download = name;
save_link.click();
}
/**
* @todo 将字符串按一定长度换行,目前用于处理图表数据名称过长
* @param {String} str 待处理的字符串
* @param {Number} len 切割的每一块的长度
* @return 处理后的字符串
*/
export function sliceString(str, len) {
let newStr = '';
for (let i = 0; i < str.length / len; i++) {
newStr += str.slice(i * len, i * len + len);
newStr += (i < str.length / len - 1) ? '\n' : '';
}
return newStr;
}
/**
* @todo 数字显示n位小数,如果没这么多小数,则不处理
* @param {Number} value 待处理的数值
* @param {Number} n 四舍五入保留位数
* @return 处理后的数字
*/
export function toFixedNum(value, n) {
if (!value) return value;
value = parseFloat(value);
let arr = value.toString().split('.');
if (arr.length == 1 || arr[1].length <= n) return value;
return value.toFixed(n);
}
/**
* @todo 将实时数据转化为历史数据,即将每一次的实时数据记录下来,保留n位,用于折线图展示,注意每次只能新增一个点值
* @param {Object} oldData 旧数据
* @param {Object} newData 新实时数据
* @param {Number} n 保留的位数
* @return 无返回,会直接在旧数据上新增新数据
*/
export function chartDataRecord(oldData, newData, n) {
if (oldData.xAxis[oldData.xAxis.length - 1] == newData.xAxis[0]) return; // 如果时间戳相同,则判断为重复数据,不添加
// 最多保留n位,将x轴和每个数据的y轴删掉,只剩n-1位
if (oldData.xAxis.length >= n) {
oldData.xAxis = oldData.xAxis.slice(-1 * (n - 1));
oldData.dataList.forEach(i => {
i.value = i.value.slice(-1 * (n - 1));
if (i.value2) i.value2 = i.value2.slice(-1 * (n - 1));
});
}
oldData.xAxis.push(newData.xAxis[0]); // 添加x轴
// 遍历datalist,找到相同的数据对象的值添加进去,没找到则添加0
oldData.dataList.forEach(i => {
i.value.push(newData.dataList.find(j => j.kksCode == i.kksCode)?.value[0] || 0);
if (i.value2) i.value2.push(newData.dataList.find(j => j.kksCode == i.kksCode)?.value2[0] || 0);
});
}
/**
* @todo 将数据转化为图表数据,将相同监测点,不同机组的数据归类,用于柱状图展示
* @param {Object} data 接口数据,格式为{xAxis: [1548937293(时间戳)], dataList: [{name: '1F某某监测点', kksCode: 'C0A00101MKD11CT601A', value: [11]}, {name: '2F某某监测点', value: [22]}, {name: '1F某某监测点2', value: [33]}, {name: '2F某某监测点2', value: [44]}]}
* @return 处理后的新数据,格式为{xAxis: ['某某监测点', '某某监测点2'], dataList: [{name: 'C0A00101', value: [11, 33]}, {name: 'C0A00102', value: [22, 44]}]}
*/
export function chartDataTransform(data) {
let newData = {
xAxis: [],
dataList: []
}
// 测点按机组排序
data.dataList.sort((a, b) => parseInt(characterDelCrewCode(a.kksCode)[0].substr(6)) - parseInt(characterDelCrewCode(
a.kksCode)[0].substr(6)));
// 遍历监测点
for (let i = 0; i < data.dataList.length; i++) {
let item = data.dataList[i]; // 单个监测点
let arr = characterDelCrewCode(item.kksCode); // 前半截为机组code
let nameArr = characterDelCrewNo(item.name); // 分离名字
if (!newData.dataList.find(i => i.name == arr[0])) newData.dataList.push({
name: arr[0],
value: []
}); // 判断是否是新的机组,是则重新建一个对应的对象
// 添加监测点值
let index = newData.xAxis.findIndex(i => i == nameArr[1]); // 数据x轴的索引,用于添加到对应的y轴里
// 如果是新的监测点,则新增
if (index == -1) {
newData.xAxis.push(nameArr[1]);
index = newData.xAxis.length - 1; // 更新索引为最后添加的这个
}
newData.dataList.find(i => i.name == arr[0]).value[index] = item.value[0]; // 这里重新找了一次,正常应该是一定可以找到的,因为如果没有,在前面已经添加了
}
// 转换机组标题
newData.dataList.forEach(i => i.name = getDeviceNameById(i.name, 1));
// 测点名后加单位
newData.xAxis = newData.xAxis.map(i => {
return i + '(' + (data.dataList.find(j => j.name.indexOf(i) != -1).cpunit || '') + ')';
})
return Object.assign(data, newData);
}
/**
* @todo 将数据转化为图表数据,将相同监测点,不同测点的数据归类,用于柱状图展示,区别于上一个,x轴不一样了
* @param {Object} data 接口数据,格式为{xAxis: [1548937293(时间戳)], dataList: [{name: '1F某某监测点', kksCode: 'C0A00101MKD11CT601A', value: [11]}, {name: '2F某某监测点', value: [22]}, {name: '1F某某监测点2', value: [33]}, {name: '2F某某监测点2', value: [44]}]}
* @return 处理后的新数据,格式为{xAxis: ['C0A00101', 'C0A00102'], dataList: [{name: '某某监测点', value: [11, 22]}, {name: '某某监测点2', value: [33, 44]}]}
*/
export function chartDataTransform2(data) {
let newData = {
xAxis: [],
dataList: []
}
// 测点按机组排序
data.dataList.sort((a, b) => parseInt(characterDelCrewCode(a.kksCode)[0].substr(6)) - parseInt(characterDelCrewCode(
a.kksCode)[0].substr(6)));
// 遍历监测点
for (let i = 0; i < data.dataList.length; i++) {
let item = data.dataList[i]; // 单个监测点
let arr = characterDelCrewCode(item.kksCode); // 前半截为机组code
let nameArr = characterDelCrewNo(item.name); // 分离名字
if (!newData.dataList.find(i => i.name == nameArr[1])) newData.dataList.push({
name: nameArr[1],
value: []
}); // 判断是否是新的测点,是则重新建一个对应的对象
// 添加监测点值
let index = newData.xAxis.findIndex(i => i == arr[0]); // 数据x轴的索引,用于添加到对应的y轴里
// 如果是新的监测点,则新增
if (index == -1) {
newData.xAxis.push(arr[0]);
index = newData.xAxis.length - 1; // 更新索引为最后添加的这个
}
newData.dataList.find(i => i.name == nameArr[1]).value[index] = item.value[0]; // 这里重新找了一次,正常应该是一定可以找到的,因为如果没有,在前面已经添加了
}
// 转换机组标题
newData.xAxis = newData.xAxis.map(i => getDeviceNameById(i, 1));
// 测点名后加单位
newData.dataList.forEach(i => {
i.name += '(' + (data.dataList.find(j => j.name.indexOf(i.name) != -1).cpunit || '') + ')';
})
return Object.assign(data, newData);
}
/**
* @todo 将数据转化为图表数据,将相同监测点,不同机组的数据归类,用于柱状图展示
* @param {Object} data 接口数据,格式为{xAxis: [1548937293(时间戳)], dataList: [{name: '1F某某监测点', kksCode: 'C0A00101MKD11CT601A', value: [11]}, {name: '2F某某监测点', value: [22]}, {name: '1F某某监测点2', value: [33]}, {name: '2F某某监测点2', value: [44]}]}
* @return 处理后的新数据,格式为{xAxis: ['1F某某监测点', '2F某某监测点', '1F某某监测点2', '2F某某监测点2'], dataList: [{value: [11, 22, 33, 44]}]}
*/
export function chartDataTransform3(data) {
let newData = {
xAxis: [],
dataList: [{
value: []
}]
}
// 遍历监测点
for (let i = 0; i < data.dataList.length; i++) {
let item = data.dataList[i]; // 单个监测点
if (!item.name) continue
newData.xAxis.push(item.name + '(' + (item.cpunit || '') + ')'); // 名字后面加上单位
newData.dataList[0].value.push(item.value[0]);
}
return Object.assign(data, newData);
}
/**
* @todo 返回范围内的随机数据,默认整数
* @param {Number} min 最小值
* @param {Number} max 最大值
* @param {Number} n 保留的位数
* @return 随机数据
*/
export function randomData(min, max, n = 0) {
return (Math.random() * (parseFloat(max) - parseFloat(min)) + parseFloat(min)).toFixed(n);
}
/**
* @todo 从测点kkscode中分离出机组code(也就是最开始的8位)
* @param {String} kksCode 测点码
* @return {Array} [0] - 机组code, [1] - 去掉机组编号后的测点code
*/
export function characterDelCrewCode(kksCode) {
const len = 8;
return [kksCode.substr(0, len), kksCode.substr(len)];
}
/**
* @todo 从测点名字中分离出机组序号(也就是最开始的数字),以及序号后的名字
* @param {String} name 测点名
* @return {Array} [0] - 机组序号, [1] - 去掉机组编号后的测点名
*/
export function characterDelCrewNo(name) {
let reg = new RegExp('^\\d+' + CONFIG.CHARACTER_TAB, 'i'); // 匹配数字+标识开头
let no = '';
let res = name.replace(reg, (a, b, c, d, e, f) => {
no = a.substr(0, a.length - CONFIG.CHARACTER_TAB.length);
return '';
});
return [no, res];
}
/**
* @todo 下载文件
* @param {Blob} blob 文件流
* @return {String} fileName 文件名
*/
export function downloadFile(blob, fileName) {
// IE10+下载
if (window.navigator.msSaveOrOpenBlob) {
navigator.msSaveBlob(blob, fileName);
} else {
const link = document.createElement('a');
link.download = fileName;
link.style.display = 'none';
link.href = window.URL.createObjectURL(blob);
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(link.href);
document.body.removeChild(link);
}
}
/**
* @todo 执行用户注销,用于token过期时的用户强制退出
* @param 无
* @return 无
*/
export function userLogout() {
Message({
message: '用户过期,请重新登录',
type: 'error'
});
localStorage.removeItem('electric_user');
localStorage.removeItem('electric_user_token');
localStorage.removeItem('electric_factory');
localStorage.removeItem('electric_factoryList');
location.reload()
}
/**
* @auto 刘彬瑜
* @todo 封装浮点数计算
* @param arg1,arg2待计算参数
* @return 计算后的结果
*/
export const M = {
// 除法函数
Div: function(arg1, arg2) {
arg1 = parseFloat(arg1);
arg2 = parseFloat(arg2);
var t1 = 0,
t2 = 0,
r1, r2;
try {
t1 = arg1.toString().split(".")[1].length;
} catch (e) {}
try {
t2 = arg2.toString().split(".")[1].length;
} catch (e) {}
r1 = Number(arg1.toString().replace(".", ""));
r2 = Number(arg2.toString().replace(".", ""));
return (r1 / r2) * Math.pow(10, t2 - t1);
},
// 加法函数
Add: function(arg1, arg2) {
arg1 = parseFloat(arg1);
arg2 = parseFloat(arg2);
var r1 = 0,
r2 = 0,
m;
try {
r1 = arg1.toString().split(".")[1].length
} catch (e) {}
try {
r2 = arg2.toString().split(".")[1].length
} catch (e) {}
m = Math.pow(10, Math.max(r1, r2));
return (this.Mul(arg1, m) + this.Mul(arg2, m)) / m;
},
// 减法函数
Sub: function(arg1, arg2) {
arg1 = parseFloat(arg1);
arg2 = parseFloat(arg2);
var r1 = 0,
r2 = 0,
m, n;
try {
r1 = arg1.toString().split(".")[1].length
} catch (e) {}
try {
r2 = arg2.toString().split(".")[1].length
} catch (e) {}
m = Math.pow(10, Math.max(r1, r2));
//动态控制精度长度
n = (r1 >= r2) ? r1 : r2;
return ((this.Mul(arg1, m) - this.Mul(arg2, m)) / m).toFixed(n);
},
// 乘法函数
Mul: function(arg1, arg2) {
arg1 = parseFloat(arg1);
arg2 = parseFloat(arg2);
var m = 0,
s1 = arg1.toString(),
s2 = arg2.toString();
try {
m += s1.split(".")[1].length
} catch (e) {}
try {
m += s2.split(".")[1].length
} catch (e) {}
return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, m);
},
}
/**
* @auto 刘彬瑜
* @todo 动态加载js文件
* @param {String} src js文件地址
* @return {Promise} 加载结果
*/
export function loadJs(src) {
return new Promise((resolve, reject) => {
let script = document.createElement('script')
script.type = "text/javascript"
script.src = src
script.onload = () => resolve()
script.onerror = () => reject()
document.getElementsByTagName('body')[0].appendChild(script)
})
}
/**
* @auto 刘彬瑜
* @todo 根据数据计算特征分布,统计数据在每个数据段出现的次数
* @param {Array} value 数据数组
* @return {Array} xAxis 数据段
* @return {Array} yAxis 当前数据段的数据数量
* @return {Array} arr x和y结合的二维数组,例如[[1, 2], [2.1,5], [3.02, 1]]表示:数据中1(包含)~2.1(不包含)有2个,2.1~3.02有5个,3.02有一个
*/
export function gaosi(value) {
const size = 50; // 分成20份
let max = value[0],
min = value[0];
// 记录最大值最小值
for (let i = (value.length - 1); i >= 0; i--) {
if (parseFloat(value[i]) < min) min = value[i];
if (parseFloat(value[i]) > max) max = value[i];
}
const step = M.Div(Math.ceil((M.Sub(max, min) / size) * 100), 100); // x轴的间段长度,这里做了向上取整,以保证一定能落到x轴里,同时保留两位
const xAxis = [];
// 处理x轴(最小值为起点,最大值为终点,step为间距
for (let i = 0; i <= size; i++) {
xAxis.push(M.Add(min, M.Mul(step, i)));
}
const yAxis = new Array(xAxis.length).fill(0);
// 遍历值,公式:取整((当前值 - 最小值) / 间距)
for (let i = (value.length - 1); i >= 0; i--) {
yAxis[parseInt(M.Div(M.Sub(value[i], min), step))]++;
}
// 合并二维
const arr = xAxis.map((x, i) => [x, yAxis[i]]);
return {
xAxis,
yAxis,
arr
}
}
/**
* @auto 刘彬瑜
* @todo 根据机组id获取机组名
* @param {String} id 机组id(code)
* @param {Boolean} short 是否使用简称:1#,2#,默认不使用
* @return {String} name 机组名
*/
export function getDeviceNameById(id, short = false) {
let name = '';
if (store.state.$x_factory) {
name = store.state.$x_factory.plantDevices.find(i => i.deviceId == id)?.deviceName || '';
} else if (store.state.$x_factoryList) {
store.state.$x_factoryList.forEach(f => {
f.plantDevices.forEach(i => {
if(i.deviceId == id) name = i.deviceName;
return;
})
if(name) return;
})
}
// if (name && short) {
// return changeDeviceNameToShort(name);
// }
return name;
}
/**
* @auto 刘彬瑜
* @todo 根据机组名获取机组id
* @param {String} name 机组名
* @return {String} id 机组id(code)
*/
export function getDeviceIdByName(name) {
// let id = '';
// if (store.state.current.deviceList) {
// id = store.state.current.deviceList.find(i => i.deviceName == name)?.deviceId || '';
// }
// return id;
}
/**
* @auto 刘彬瑜
* @todo 将机组名转为简写
* @param {String} name 机组名
* @return {String} 机组名,简写
*/
export function changeDeviceNameToShort(name) {
return (name.match('^\\d+')?.[0] || '') + '#';
}
/**
* @auto 刘彬瑜
* @todo 将字符串用逗号分隔,做成map映射,便于查询是否存在
* @param {String} str 需要处理的字符串
* @param {Boolean} lowerCase 是否区分大小写比较
* @return {Function} 返回一个函数,用于查询,比如const checkFormat = makeMap('txt,xlsx,docx'); checkFormat('txt')返回true checkFormat('aaa')返回false
*/
export function makeMap(str, lowerCase) {
const map = Object.create(null);
if (str) {
const list = str.toString().split(',');
for (let i = 0; i < list.length; i++) {
const key = lowerCase ? list[i].toLowerCase() : list[i];
map[key] = true;
}
}
return lowerCase ? val => !!map[val.toLowerCase()] : val => !!map[val];
}
/**
* @auto 刘彬瑜
* @todo 在全部测点数据中,从机组里匹配出对应的测点名或kks的项
* @param {Object} crew 全部测点数据里的机组,之所以要传,是为了不用每次都找一次机组
* @param {String} name 测点名关键词
* @return {String} kks 测点kks
*/
export function getCharacterFromCrew(crew, name, kks) {
let res = crew.tSdb.find(i => {
if(name) return i.cpName.indexOf(name) != -1;
if(kks) return i.kksCode.indexOf(kks) != -1;
});
if(!res) return false; // 如果没有查出值,则跳过
// 补数据
if(!res.kksValueR) {
let str = res.kksValue?.value?.toString();
res.state = str ? parseInt(str.substr(-2)) : 0; // 信号状态
res.kksValueR = str ? parseFloat(str.substr(0, str.length - 2)) : 0; // 真实值
}
return res;
}
\ No newline at end of file
// 图表基础,负责建立,销毁图表,监听尺寸变化,监听图表事件
import echarts from 'echarts';
import {
debounce
} from '@/common/util.js'
export default {
props: {
// 图表dom的id,注意同一个页面的图表使用不同的id区分,最好全局都是唯一的
id: {
type: String,
required: true
},
// 数据体,只关心数据
chartData: {
type: Object | Array,
required: true
},
},
data() {
return {
chart: null, // 图表对象
resizeHandler: null, // 监听窗口变化的监听器
initMaxCount: 10, // 图表在初始化时会获取dom元素,如果没有获取到(可能是延迟加载),则会用settimeout延时初始化,这个数字为最多尝试初始化次数
};
},
watch: {
// 数据改变时改变图表
chartData: {
deep: true,
handler(val) {
this.setOptions(val);
}
}
},
created() {
this.beforeInit?.(echarts);
},
mounted() {
this.$nextTick(() => {
this.initChart();
this.initListener()
});
},
activated() {
// 激活时,如果没有图表,则重新生成,因为有可能在mounted阶段还未生成图表时,就切换了页面,导致未捕获到元素,无法生成图表
if (!this.chart) {
this.$nextTick(() => {
this.initChart();
});
}
this.initListener()
},
deactivated() {
this.destroyListener()
},
beforeDestroy() {
this.destroyListener();
this.destroyChart();
},
methods: {
// 建立图表对象
initChart() {
let dom = document.getElementById(this.id);
// if (dom) console.log(this.id, 'echart-width', dom.offsetWidth, 'echart-height', dom.offsetHeight);
// else console.log(this.id, 'dom不存在');
// 如果没有dom,则延时建立
if (!dom || !dom.offsetWidth) {
this.initMaxCount--;
if (this.initMaxCount > 0) setTimeout(() => this.initChart(), 100);
return;
}
this.chart = echarts.init(dom); // 初始化图表
// 绑定监听事件,如果有的话
if (this.eventListener) {
for (let item of this.eventListener) {
this.chart.on(item.event, item.handler);
}
}
this.setOptions(this.chartData);
},
// 释放图表对象
destroyChart() {
if (!this.chart) return;
this.chart.dispose();
this.chart = null;
},
// 监听窗口尺寸变化
initListener() {
if (this.resizeHandler) return;
// 防抖处理
this.resizeHandler = debounce(() => {
this.resize()
}, 100);
window.addEventListener('resize', this.resizeHandler);
},
// 移除监听器
destroyListener() {
if (!this.resizeHandler) return;
window.removeEventListener('resize', this.resizeHandler);
this.resizeHandler = null;
},
// echart的resize
resize() {
this.chart && this.chart._dom.offsetWidth && this.chart.resize();
},
// 清屏
clear() {
this.chart && this.chart.clear();
}
}
}
<!-- 柱状 - 圆弧 -->
<template>
<div :id="id" class="chart" />
</template>
<script>
import ChartBase from '@/components/Base/ChartsBase.js';
import {
ChartStyle
} from '@/common/const.js';
import echarts from 'echarts';
export default {
mixins: [ChartBase],
methods: {
setOptions(chartData) {
if (!this.chart || !chartData) return;
let option = {
tooltip: {
formatter: params => {
if (!params.value) return '';
return params.value;
}
},
title: [],
xAxis: [],
yAxis: [],
series: [],
grid: [],
};
// 找到最大值,作为单轴时y轴的最大值
let yMax;
for (let i = 0; i < chartData.length; i++) {
for (let j = 0; j < chartData[i].dataList.length; j++) {
for (let k = 0; k < chartData[i].dataList[j].value.length; k++) {
if(!yMax || yMax < chartData[i].dataList[j].value[k]) yMax = chartData[i].dataList[j].value[k];
}
}
}
for (let i = 0, len = chartData.length; i < len; i++) {
option.title.push({
text: '{a| } ' + chartData[i].title,
textStyle: {
color: ChartStyle.title.color,
fontSize: ChartStyle.title.fontSize,
// width: '100%',
fontWeight: 200,
rich: {
a: {
width: 5,
height: 5,
backgroundColor: ChartStyle.title.color,
borderRadius: 50,
shadowColor: 'rgba(147,252,255,0.30)',
shadowBlur: 5,
}
}
},
left: 100 / len / 2 + (100 / len) * i - 1 + '%',
top: '3',
textAlign: 'center'
});
option.xAxis.push({
data: chartData[i].xAxis,
axisLine: {
lineStyle: {
color: ChartStyle.axis.color,
}
},
axisTick: {
show: false
},
axisLabel: {
color: ChartStyle.axis.color,
fontSize: ChartStyle.axis.fontSize
},
gridIndex: i
});
option.yAxis.push({
max: yMax * 1.2, // 单轴
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
show: false
},
splitLine: {
show: false
},
gridIndex: i
});
option.grid.push({
top: '20%',
left: (100 / len) * i + 15 + '%',
right: (100 / len) * (len - i - 1) + 15 + '%',
bottom: '5%',
containLabel: true
});
for (let j = 0; j < chartData[i].dataList.length; j++) {
option.series.push({
type: 'bar',
name: chartData[i].dataList[j].name,
data: chartData[i].dataList[j].value,
// label: {
// show: true,
// position: 'top',
// color: ChartStyle.label.color,
// fontSize: ChartStyle.label.fontSize ,
// },
itemStyle: {
barBorderRadius: [50, 50, 50, 50], //(顺时针左上,右上,右下,左下)
// 4个参数用于配置渐变色的起止位置, 这4个参数依次对应右/下/左/上四个方位. 而0 0 0 1则代表渐变色从正上方开始
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: ChartStyle.colorGradients[i] },
{ offset: 1, color: ChartStyle.color[i] },
])
},
barMaxWidth: 12,
xAxisIndex: i,
yAxisIndex: i
});
}
}
this.chart.setOption(option);
},
}
};
</script>
<style lang="scss" scoped>
.chart {
width: 100%;
height: 100%;
}
</style>
<!-- 柱状 - 圆柱 -->
<template>
<div :id="id" class="chart" />
</template>
<script>
import ChartBase from '@/components/Base/ChartsBase.js';
import {
ChartStyle
} from '@/common/const.js';
import echarts from 'echarts';
export default {
mixins: [ChartBase],
methods: {
setOptions(chartData) {
if (!this.chart || !chartData) return;
let option = {
tooltip: {
formatter: params => {
if (!params.value) return '';
return params.value;
}
},
title: [],
xAxis: [],
yAxis: [],
series: [],
grid: [],
};
// 找到最大值,作为单轴时y轴的最大值
let yMax;
for (let i = 0; i < chartData.length; i++) {
for (let j = 0; j < chartData[i].dataList.length; j++) {
for (let k = 0; k < chartData[i].dataList[j].value.length; k++) {
if(!yMax || yMax < chartData[i].dataList[j].value[k]) yMax = chartData[i].dataList[j].value[k];
}
}
}
for (let i = 0, len = chartData.length; i < len; i++) {
option.title.push({
text: '{a| } ' + chartData[i].title,
textStyle: {
color: ChartStyle.title.color,
fontSize: ChartStyle.title.fontSize,
// width: '100%',
fontWeight: 200,
rich: {
a: {
width: 5,
height: 5,
backgroundColor: ChartStyle.title.color,
borderRadius: 50,
shadowColor: 'rgba(147,252,255,0.30)',
shadowBlur: 5,
}
}
},
left: 100 / len / 2 + (100 / len) * i - 1 + '%',
top: '3',
textAlign: 'center'
});
option.xAxis.push({
data: chartData[i].xAxis,
axisLine: {
lineStyle: {
color: ChartStyle.axis.color,
}
},
axisTick: {
show: false
},
axisLabel: {
color: ChartStyle.axis.color,
fontSize: ChartStyle.axis.fontSize
},
gridIndex: i
});
option.yAxis.push({
max: yMax * 1.2, // 单轴
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
show: false
},
splitLine: {
show: false
},
gridIndex: i
});
option.grid.push({
top: '20%',
left: (100 / len) * i + 15 + '%',
right: (100 / len) * (len - i - 1) + 15 + '%',
bottom: '5%',
containLabel: true
});
for (let j = 0; j < chartData[i].dataList.length; j++) {
option.series.push({
type: 'bar',
data: chartData[i].dataList[j].value,
showBackground: true,
backgroundStyle: {
color: 'rgba(0, 0, 0, 0.1)'
},
itemStyle: {
color: 'transparent'
},
barMaxWidth: 25,
xAxisIndex: i,
yAxisIndex: i
});
option.series.push({
type: 'pictorialBar',
data: chartData[i].dataList[j].value,
symbolSize: [25, 10],
symbolOffset: [0, -10],
symbolPosition: 'start',
itemStyle: {
normal: {
color: ChartStyle.color[i],
}
},
xAxisIndex: i,
yAxisIndex: i
});
option.series.push({
type: 'pictorialBar',
name: chartData[i].dataList[j].name,
data: chartData[i].dataList[j].value,
symbol: 'rect',
symbolClip: true,
symbolSize: [25, '100%'],
symbolOffset: [0, -15],
symbolPosition: 'start',
itemStyle: {
// 4个参数用于配置渐变色的起止位置, 这4个参数依次对应右/下/左/上四个方位. 而0 0 0 1则代表渐变色从正上方开始
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: ChartStyle.colorGradients[i] },
{ offset: 1, color: ChartStyle.color[i] },
])
},
xAxisIndex: i,
yAxisIndex: i
});
option.series.push({
type: 'pictorialBar',
data: chartData[i].dataList[j].value,
symbolSize: [25, 10],
symbolOffset: [0, -5],
symbolPosition: 'end',
itemStyle: {
normal: {
color: ChartStyle.colorGradients[i],
shadowColor: ChartStyle.color[i],
shadowBlur: 1
}
},
// label: {
// show: true,
// position: 'top',
// color: ChartStyle.label.color,
// fontSize: ChartStyle.label.fontSize ,
// },
xAxisIndex: i,
yAxisIndex: i
})
}
}
this.chart.setOption(option);
},
}
};
</script>
<style lang="scss" scoped>
.chart {
width: 100%;
height: 100%;
}
</style>
<!-- 柱状 - 方格 -->
<template>
<div :id="id" class="chart" />
</template>
<script>
import ChartBase from '@/components/Base/ChartsBase.js';
import {
ChartStyle
} from '@/common/const.js';
import {
getWarnColor,
sliceString
} from '@/common/util.js';
export default {
mixins: [ChartBase],
methods: {
setOptions(chartData) {
if (!this.chart || !chartData) return;
let option = {
tooltip: {
// formatter: params => {
// if (!params.value) return '';
// let str = params.value.toString();
// return parseFloat(str.substr(0, str.length - 2));
// }
},
xAxis: {
type: 'category',
data: chartData.xAxis || chartData.xaxis,
axisLine: {
lineStyle: {
color: ChartStyle.axis.color,
}
},
axisTick: {
show: false
},
axisLabel: {
fontSize: ChartStyle.axis.fontSize,
lineHeight: ChartStyle.axis.lineHeight,
formatter(value, index) {
return sliceString(value, 6);
}
}
},
yAxis: {
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: ChartStyle.axis.color,
fontSize: ChartStyle.axis.fontSize
},
splitLine: {
lineStyle: {
color: ChartStyle.split.color,
type: 'dashed',
opacity: ChartStyle.split.opacity,
}
}
},
grid: {
top: '18%',
left: '2%',
right: '2%',
bottom: '1%',
containLabel: true
},
series: [],
animationDurationUpdate: 500,
};
for (let i = 0, len = chartData.dataList.length; i < len; i++) {
const item = chartData.dataList[i];
option.series.push({
type: 'bar',
data: item.value,
showBackground: true,
backgroundStyle: {
color: 'rgba(0, 0, 0, 0.1)'
},
itemStyle: {
color: 'transparent'
},
barMaxWidth: 25,
});
option.series.push({
type: 'pictorialBar',
symbol: 'rect',
symbolRepeat: true,
symbolSize: ['90%', '18%'],
symbolMargin: 1.5,
symbolOffset: [0, -2],
symbolRepeatDirection: 'end',
name: item.name,
data: item.value,
label: {
show: true,
position: 'top',
color: ChartStyle.label.color,
fontSize: ChartStyle.label.fontSize,
// formatter: params => {
// if (!params.value) return '';
// let str = params.value.toString();
// return parseFloat(str.substr(0, str.length - 2));
// }
},
itemStyle: {
normal: {
// 最后两位为信号状态
color: params => {
// return getWarnColor(params.value?.toString().substr(-2));
return getWarnColor(item.state[params.dataIndex]);
}
}
},
barMaxWidth: 25,
});
option.series.push({
type: 'line',
name: item.name,
data: item.value,
});
}
this.chart.setOption(option);
},
// setOptions(chartData) {
// if (!this.chart || !chartData) return;
// let option = {
// tooltip: {
// formatter: params => {
// if (!params.value) return '';
// let str = params.value.toString();
// return parseFloat(str.substr(0, str.length - 2));
// }
// },
// title: [],
// xAxis: [],
// yAxis: [],
// series: [],
// grid: [],
// animationDurationUpdate: 500,
// };
// // 找到最大值,作为单轴时y轴的最大值
// let yMax;
// for (let i = 0; i < chartData.length; i++) {
// for (let j = 0; j < chartData[i].dataList.length; j++) {
// for (let k = 0; k < chartData[i].dataList[j].value.length; k++) {
// if(!yMax || yMax < chartData[i].dataList[j].value[k]) yMax = chartData[i].dataList[j].value[k];
// }
// }
// }
// for (let i = 0, len = chartData.length; i < len; i++) {
// option.title.push({
// text: '{a| } ' + chartData[i].title,
// textStyle: {
// color: ChartStyle.title.color,
// fontSize: ChartStyle.title.fontSize,
// // width: '100%',
// fontWeight: 200,
// rich: {
// a: {
// width: 5,
// height: 5,
// backgroundColor: ChartStyle.title.color,
// borderRadius: 50,
// shadowColor: 'rgba(147,252,255,0.30)',
// shadowBlur: 5,
// }
// }
// },
// left: 100 / len / 2 + (100 / len) * i - 1 + '%',
// top: '3',
// textAlign: 'center'
// });
// option.xAxis.push({
// data: chartData[i].xAxis,
// axisLine: {
// lineStyle: {
// color: ChartStyle.axis.color,
// }
// },
// axisTick: {
// show: false
// },
// axisLabel: {
// color: ChartStyle.axis.color,
// fontSize: ChartStyle.axis.fontSize
// },
// gridIndex: i
// });
// option.yAxis.push({
// max: yMax * 1.2, // 单轴
// axisLine: {
// show: false
// },
// axisTick: {
// show: false
// },
// axisLabel: {
// show: false
// },
// splitLine: {
// show: false
// },
// gridIndex: i
// });
// option.grid.push({
// top: '20%',
// left: (100 / len) * i + 5 + '%',
// right: (100 / len) * (len - i - 1) + 5 + '%',
// bottom: '5%',
// containLabel: true
// });
// for (let j = 0; j < chartData[i].dataList.length; j++) {
// option.series.push({
// type: 'bar',
// data: chartData[i].dataList[j].value,
// showBackground: true,
// backgroundStyle: {
// color: 'rgba(0, 0, 0, 0.1)'
// },
// itemStyle: {
// color: 'transparent'
// },
// barMaxWidth: 25,
// xAxisIndex: i,
// yAxisIndex: i
// });
// option.series.push({
// type: 'pictorialBar',
// symbol: 'rect',
// symbolRepeat: true,
// symbolSize: ['90%', '18%'],
// symbolMargin: 1.5,
// symbolOffset: [0, -2],
// symbolRepeatDirection: 'end',
// name: chartData[i].dataList[j].name,
// data: chartData[i].dataList[j].value,
// // label: {
// // show: true,
// // position: 'top',
// // color: ChartStyle.label.color,
// // fontSize: ChartStyle.label.fontSize
// // formatter: params => {
// // if (!params.value) return '';
// // let str = params.value.toString();
// // return str.substr(0, str.length - 2);
// // }
// // },
// itemStyle: {
// normal: {
// // 最后两位为信号状态
// color: params => {
// return getWarnColor(params.value?.toString().substr(-2));
// }
// }
// },
// barMaxWidth: 25,
// xAxisIndex: i,
// yAxisIndex: i
// });
// }
// }
// this.chart.setOption(option);
// },
}
};
</script>
<style lang="scss" scoped>
.chart {
width: 100%;
height: 100%;
}
</style>
<!-- 报警统计 -->
<template>
<div :id="id" class="chart" />
</template>
<script>
import ChartBase from '@/components/Base/ChartsBase.js';
import {
ChartStyle,
} from '@/common/const.js';
import {
getWarnColorByName,
getWarnColor,
sliceString
} from '@/common/util.js';
import echarts from 'echarts';
export default {
mixins: [ChartBase],
methods: {
// 自定义柱状
beforeInit(echarts) {
// 绘制正面
const CubeLeft = echarts.graphic.extendShape({
shape: {
x: 0,
y: 0
},
buildPath: function(ctx, shape) {
const xAxisPoint = shape.xAxisPoint;
const c0 = [shape.x + 15, shape.y]; // 右上角
const c1 = [shape.x - 15, shape.y]; // 左上角
const c2 = [xAxisPoint[0] - 15, xAxisPoint[1]]; // 左下角
const c3 = [xAxisPoint[0] + 15, xAxisPoint[1]]; // 右下角
ctx.moveTo(c0[0], c0[1]).lineTo(c1[0], c1[1]).lineTo(c2[0], c2[1]).lineTo(c3[0], c3[
1]).closePath();
}
})
// 绘制右侧面
const CubeRight = echarts.graphic.extendShape({
shape: {
x: 0,
y: 0
},
buildPath: function(ctx, shape) {
const xAxisPoint = shape.xAxisPoint;
const c1 = [shape.x + 15, shape.y]; // 左上角
const c2 = [xAxisPoint[0] + 15, xAxisPoint[1]]; // 左下角
const c3 = [xAxisPoint[0] + 25, xAxisPoint[1] - 10]; // 右下角
const c4 = [shape.x + 25, shape.y - 10]; // 右上角
ctx.moveTo(c1[0], c1[1]).lineTo(c2[0], c2[1]).lineTo(c3[0], c3[1]).lineTo(c4[0], c4[
1]).closePath();
}
})
// 绘制顶面
const CubeTop = echarts.graphic.extendShape({
shape: {
x: 0,
y: 0
},
buildPath: function(ctx, shape) {
const c1 = [shape.x + 15, shape.y]; // 右下角
const c2 = [shape.x + 25, shape.y - 10]; // 右上角
const c3 = [shape.x - 5, shape.y - 10]; // 左上角
const c4 = [shape.x - 15, shape.y]; // 左下角
ctx.moveTo(c1[0], c1[1]).lineTo(c2[0], c2[1]).lineTo(c3[0], c3[1]).lineTo(c4[0], c4[
1]).closePath();
}
})
// 注册三个面图形
echarts.graphic.registerShape('CubeLeft', CubeLeft)
echarts.graphic.registerShape('CubeRight', CubeRight)
echarts.graphic.registerShape('CubeTop', CubeTop)
},
setOptions(chartData) {
if (!this.chart || !chartData) return;
let option = {
tooltip: {
// formatter: params => {
// if (!params.value) return '';
// let str = params.value.toString();
// return parseFloat(str.substr(0, str.length - 2));
// }
},
xAxis: {
type: 'category',
data: chartData.xAxis || chartData.xaxis,
axisLine: {
lineStyle: {
color: ChartStyle.axis.color,
}
},
axisTick: {
show: false
},
axisLabel: {
fontSize: ChartStyle.axis.fontSize,
formatter(value, index) {
return sliceString(value, 6);
}
}
},
yAxis: {
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: ChartStyle.axis.color,
fontSize: ChartStyle.axis.fontSize
},
splitLine: {
lineStyle: {
color: ChartStyle.split.color,
type: 'dashed',
opacity: ChartStyle.split.opacity,
}
}
},
grid: {
top: '15%',
left: '2%',
right: '2%',
bottom: '1%',
containLabel: true
},
series: []
};
// 渐变颜色
for (let i = 0, len = chartData.dataList.length; i < len; i++) {
const item = chartData.dataList[i];
option.series.push({
type: 'custom',
name: item.name,
data: item.value,
renderItem: (params, api) => {
const location = api.coord([api.value(0), api.value(1)]);
// const colors = getWarnColor(api.value(1).toString().substr(-2), 1);
const colors = getWarnColor(item.state[params.dataIndex], 1);
const shape = {
api,
xValue: api.value(0),
yValue: api.value(1),
x: location[0],
y: location[1],
xAxisPoint: api.coord([api.value(0), 0])
}
const toTop = new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: colors[1]
}, {
offset: 1,
color: colors[0]
}]);
const toBottom = new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: colors[0]
}, {
offset: 1,
color: colors[1]
}]);
return {
type: 'group',
position: [-5, 0],
children: [{
type: 'CubeLeft',
shape,
style: {
fill: toTop
}
}, {
type: 'CubeRight',
shape,
style: {
fill: toBottom
}
}, {
type: 'CubeTop',
shape,
style: {
fill: toTop
}
}]
}
},
})
option.series.push({
type: 'bar',
name: item.name,
data: item.value,
label: {
show: true,
position: 'top',
color: ChartStyle.label.color,
fontSize: ChartStyle.label.fontSize,
// formatter: params => {
// if (!params.value) return '';
// let str = params.value.toString();
// return parseFloat(str.substr(0, str.length - 2));
// }
},
itemStyle: {
color: 'transparent'
},
barMaxWidth: 40
});
}
this.chart.setOption(option);
},
// setOptions(chartData) {
// if (!this.chart || !chartData) return;
// let option = {
// tooltip: {},
// legend: {
// bottom: 3,
// itemWidth: 10,
// itemHeight: 10,
// textStyle: {
// color: ChartStyle.legend.color,
// fontSize: ChartStyle.legend.fontSize,
// padding: [2, 0, 0, 0]
// },
// icon: 'rect',
// selectedMode: false
// },
// color: chartData.dataList.map(i => getWarnColorByName(i.name)),
// xAxis: {
// type: 'category',
// data: chartData.xAxis || chartData.xaxis,
// axisLine: {
// lineStyle: {
// color: ChartStyle.axis.color,
// }
// },
// axisTick: {
// show: false
// },
// axisLabel: {
// fontSize: ChartStyle.axis.fontSize
// }
// },
// yAxis: {
// axisLine: {
// show: false
// },
// axisTick: {
// show: false
// },
// axisLabel: {
// color: ChartStyle.axis.color,
// fontSize: ChartStyle.axis.fontSize
// },
// splitLine: {
// lineStyle: {
// color: ChartStyle.split.color,
// type: 'dashed',
// opacity: ChartStyle.split.opacity,
// }
// }
// },
// grid: {
// top: '10%',
// left: '1%',
// right: '1%',
// bottom: '12%',
// containLabel: true
// },
// series: []
// };
// // 定义x轴偏移量
// let xPos = [];
// if (chartData.dataList.length == 3) {
// xPos = [-60, -8, 44];
// }
// for (let i = 0, len = chartData.dataList.length; i < len; i++) {
// const item = chartData.dataList[i];
// const colors = getWarnColorByName(item.name, 1); // 渐变颜色
// option.series.push({
// type: 'custom',
// name: item.name,
// data: item.value,
// renderItem: (params, api) => {
// const location = api.coord([api.value(0), api.value(1)]);
// const shape = {
// api,
// xValue: api.value(0),
// yValue: api.value(1),
// x: location[0],
// y: location[1],
// xAxisPoint: api.coord([api.value(0), 0])
// }
// const toTop = new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
// offset: 0,
// color: colors[1]
// }, {
// offset: 1,
// color: colors[0]
// }]);
// const toBottom = new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
// offset: 0,
// color: colors[0]
// }, {
// offset: 1,
// color: colors[1]
// }]);
// return {
// type: 'group',
// position: [xPos[i], 0],
// children: [{
// type: 'CubeLeft',
// shape,
// style: {
// fill: toTop
// }
// }, {
// type: 'CubeRight',
// shape,
// style: {
// fill: toBottom
// }
// }, {
// type: 'CubeTop',
// shape,
// style: {
// fill: toTop
// }
// }]
// }
// },
// })
// option.series.push({
// type: 'bar',
// name: item.name,
// data: item.value,
// label: {
// show: true,
// position: 'top',
// color: ChartStyle.label.color,
// fontSize: ChartStyle.label.fontSize
// },
// itemStyle: {
// color: 'transparent'
// },
// barMaxWidth: 40
// });
// }
// this.chart.setOption(option);
// },
},
};
</script>
<style lang="scss" scoped>
.chart {
width: 100%;
height: 100%;
}
</style>
<!-- 图表的按钮 -->
<template>
<div class="btns global">
<!-- 图表类型按钮,注意按钮的值需要和modelinfo组件里的对应 -->
<div class="btn-line">
<el-button class="btn-style" :class="param.chartBtn == 1 ? 'btn-active' : 'btn'" type="primary" @click="changeChart(1)">
趋势分析
</el-button>
<el-button class="btn-style" :class="param.sameAxis ? 'btn-active' : 'btn'" type="primary" @click="changeAxis">
共用坐标
</el-button>
</div>
<!-- 时间线按钮 -->
<div class="btn-line" v-if="param.chartBtn == 1">
<el-button v-for="btn of AllDateLineBtn" :key="btn.value" class="btn-style" :class="param.dateLineBtn == btn.value ? 'btn-active' : 'btn'"
type="primary" @click="changeDateLine(btn.value)">
{{ btn.name }}
</el-button>
<el-date-picker class="date-picker" v-model="param.dateSelect" type="datetimerange" unlink-panels
prefix-icon="none" :picker-options="pickerOptions" :clearable="false" :editable="false"
value-format="timestamp" start-placeholder="开始时间" end-placeholder="结束时间" @change="changeDate" />
</div>
</div>
</template>
<script>
import {
GetPickerOptions,
DefaultDateSelect,
AllDateLineBtn
} from '@/common/const.js';
export default {
props: {
// 选择的列表
checkList: {
type: Array,
default () {
return [];
}
},
},
data() {
return {
// 时间线的按钮组
AllDateLineBtn: AllDateLineBtn,
// 选择的按钮,即图表参数
param: {
chartBtn: 1, // 选择的图表按钮
dateLineBtn: 0, // 选择的时间趋势按钮
dateSelect: DefaultDateSelect(), // 选择的日期
sameAxis: true, // 是否共坐标
},
// 快捷日期选择
pickerOptions: GetPickerOptions(),
};
},
watch: {
checkList: {
immediate: true,
handler(val) {
if (!val || !val.length) return;
this.changeChart(1); // 模拟按钮点击
}
}
},
methods: {
// 切换图表类型
changeChart(val) {
this.param.chartBtn = val;
this.$emit('changeBtn', this.param);
},
// 切换时间线
changeDateLine(val) {
this.param.dateLineBtn = val;
let today = new Date();
let minusDay = today.getDay() != 0 ? today.getDay() - 1 : 6;
this.param.dateSelect = val ? [today.setHours(0, 0, 0, 0) - (minusDay * 1000 * 3600 * 24), Date.now()] : DefaultDateSelect();
this.$emit('changeBtn', this.param);
},
// 切换日期
changeDate(val) {
if (!val) this.param.dateSelect = DefaultDateSelect(); // 如果没有选择日期,则采用默认日期
this.$emit('changeBtn', this.param);
},
// 切换共用坐标
changeAxis() {
this.param.sameAxis = !this.param.sameAxis;
this.$emit('changeAxis', this.param);
},
// 重置按钮状态
reset() {
this.param = {
chartBtn: 1,
dateLineBtn: 0,
dateSelect: DefaultDateSelect(),
sameAxis: true,
};
},
}
};
</script>
<style lang="scss" scoped>
.btns {
display: flex;
flex-direction: column;
.btn-line {
display: flex;
.btn-style {
min-width: 6rem;
height: 2.5rem;
margin: 0 1rem 1rem 0;
font-size: 0.875rem;
}
.btn {
@extend %btn-default;
border-radius: 50px;
}
.btn-active {
@extend %btn-active;
border-radius: 50px;
}
}
.date-picker {
width: 30rem;
height: 2.5rem !important;
margin: 0 1rem 1rem 0;
padding: 0;
border-radius: 50px;
display: flex;
justify-content: center;
/deep/ .el-range-input {
font-size: 0.875rem;
background-color: transparent;
color: #fff;
}
/deep/ .el-input__icon {
width: 0;
}
/deep/ .el-range-separator{
@extend %flex-center;
color: #fff;
}
}
}
</style>
<!-- 普通折线图 -->
<template>
<div :id="id" class="charts-line" />
</template>
<script>
import ChartBase from '@/components/Base/ChartsBase.js';
import {
ChartStyle
} from '@/common/const.js';
import {
toFixedNum
} from '@/common/util.js';
import echarts from 'echarts';
export default {
mixins: [ChartBase],
methods: {
setOptions(chartData) {
if (!this.chart || !chartData) return;
let option;
// 共轴
if (chartData.sameAxis) {
option = {
tooltip: {
trigger: 'axis',
formatter: params => {
let newParams = [...params];
let tooltip = (chartData.xName || '') + params[0].axisValue + '<br/>';
newParams.sort((a, b) => a.seriesIndex - b.seriesIndex);
newParams.forEach(p => {
let item = chartData.dataList.find(i => i.name == p.seriesName);
const cont = p.seriesName + ': ' + toFixedNum(p.value,
4) + (item.unit || '') + '<br/>';
tooltip += cont;
});
return tooltip;
},
textStyle: {
align: 'left'
}
},
axisPointer: {
lineStyle: {
type: 'dashed'
}
},
dataZoom: {
type: 'inside'
},
legend: {
bottom: '5%',
textStyle: {
color: ChartStyle.legend.color,
fontSize: ChartStyle.legend.fontSize,
},
itemGap: 40,
icon: 'pin',
},
color: ChartStyle.color,
xAxis: {
data: chartData.xAxis ?? chartData.xaxis,
axisLine: {
lineStyle: {
color: ChartStyle.title.color,
}
},
axisTick: {
show: false
},
axisLabel: {
fontSize: ChartStyle.axis.fontSize,
formatter: val => {
let arr = val.split(' ');
return arr[0] + (arr[1] ? '\n' + arr[1] : '');
}
},
},
yAxis: {
nameTextStyle: {
color: ChartStyle.title.color
},
scale: true,
splitNumber: 3,
axisLine: {
show: false,
},
axisTick: {
show: false
},
axisLabel: {
color: ChartStyle.title.color,
fontSize: ChartStyle.axis.fontSize
},
splitLine: {
lineStyle: {
type: 'dashed',
color: ChartStyle.split.color,
opacity: ChartStyle.split.opacity,
}
}
},
series: [],
grid: {
top: '5%',
left: '2%',
right: '2%',
bottom: '15%',
containLabel: true
}
};
for (let i = 0, len = chartData.dataList.length; i < len; i++) {
let item = chartData.dataList[i];
option.series.push({
type: 'line',
name: item.name,
data: item.value,
showSymbol: false,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 1,
color: ChartStyle.color[i]
},
{
offset: 0,
color: ChartStyle.colorGradients[i]
},
])
}
});
}
// 不共轴
} else {
option = {
tooltip: {
trigger: 'axis',
formatter: params => {
let newParams = [...params];
let tooltip = (chartData.xName || '') + params[0].axisValue + '<br/>';
newParams.sort((a, b) => a.seriesIndex - b.seriesIndex);
newParams.forEach(p => {
let item = chartData.dataList.find(i => i.name == p.seriesName);
const cont = p.seriesName + ': ' + toFixedNum(p.value,
4) + (item.unit || '') + '<br/>';
tooltip += cont;
});
return tooltip;
},
textStyle: {
align: 'left'
}
},
axisPointer: {
lineStyle: {
type: 'dashed'
},
link: {
xAxisIndex: []
}
},
dataZoom: {
type: 'inside',
xAxisIndex: []
},
legend: [],
color: ChartStyle.color,
xAxis: [],
yAxis: [],
series: [],
grid: [],
};
for (let i = 0, len = chartData.dataList.length; i < len; i++) {
let item = chartData.dataList[i];
option.axisPointer.link.xAxisIndex.push(i);
option.dataZoom.xAxisIndex.push(i);
option.legend.push({
data: [item.name],
top: (100 / len) * i + 10 + '%',
right: '10',
textStyle: {
color: ChartStyle.legend.color,
fontSize: ChartStyle.legend.fontSize,
},
itemGap: 40,
icon: 'pin',
formatter: function (name) {
let len = name.length
if(len < 15){
for (let i = 0; i < (15 - len); i++) {
name += ' ';
}
}
return name;
}
});
option.xAxis.push({
data: chartData.xAxis ?? chartData.xaxis,
axisLine: {
lineStyle: {
color: ChartStyle.title.color,
}
},
axisTick: {
show: false
},
axisLabel: {
fontSize: ChartStyle.axis.fontSize,
formatter: val => {
let arr = val.split(' ');
return arr[0] + (arr[1] ? '\n' + arr[1] : '');
}
},
gridIndex: i
});
option.yAxis.push({
name: item.unit || '',
nameTextStyle: {
color: ChartStyle.title.color
},
scale: true,
splitNumber: 1,
axisLine: {
show: false,
},
axisTick: {
show: false
},
axisLabel: {
color: ChartStyle.title.color,
fontSize: ChartStyle.axis.fontSize
},
splitLine: {
lineStyle: {
type: 'dashed',
color: ChartStyle.split.color,
opacity: ChartStyle.split.opacity,
}
},
gridIndex: i
});
option.series.push({
type: 'line',
name: item.name,
data: item.value,
showSymbol: false,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 1,
color: ChartStyle.color[i]
},
{
offset: 0,
color: ChartStyle.colorGradients[i]
},
])
},
xAxisIndex: i,
yAxisIndex: i,
});
option.grid.push({
top: (100 / len) * i + 7 + '%',
left: '2%',
right: '230',
bottom: (100 / len) * (len - i - 1) + 2 + '%',
containLabel: true
});
}
}
this.chart.setOption(option);
}
}
};
</script>
<style lang="scss" scoped>
.charts-line {
width: 100%;
height: 100%;
}
</style>
<!-- 折线图 -->
<template>
<div :id="id" class="chart" />
</template>
<script>
import ChartBase from '@/components/Base/ChartsBase.js';
import {
ChartStyle
} from '@/common/const.js';
import {
toFixedNum
} from '@/common/util.js';
export default {
mixins: [ChartBase],
methods: {
setOptions(chartData) {
if (!this.chart || !chartData) return;
let option = {
tooltip: {
trigger: 'axis',
formatter: params => {
let newParams = [...params];
let tooltip = params[0].axisValue + '<br/>';
newParams.forEach(p => {
const cont = p.marker + ' ' + p.seriesName + ': ' + toFixedNum(p.value,
2) + '<br/>';
tooltip += cont;
});
return tooltip;
},
textStyle: {
align: 'left'
}
},
dataZoom: {
type: 'inside'
},
color: ChartStyle.color,
xAxis: {
data: chartData.xAxis,
axisLine: {
lineStyle: {
color: ChartStyle.axis.color,
}
},
axisTick: {
show: false
},
axisLabel: {
fontSize: ChartStyle.axis.fontSize,
interval: parseInt(chartData.xAxis.length / 5),
showMinLabel: false,
}
},
yAxis: {
name: chartData.unit ?? '',
nameTextStyle: {
color: ChartStyle.axis.color
},
scale: true,
splitNumber: 3,
axisLine: {
show: false
},
axisTick: {
show: false
},
axisLabel: {
color: ChartStyle.axis.color,
fontSize: ChartStyle.axis.fontSize
},
splitLine: {
lineStyle: {
type: 'dashed',
color: ChartStyle.split.color,
opacity: ChartStyle.split.opacity,
}
}
},
series: [],
grid: {
top: '15%',
left: '2%',
right: '2%',
bottom: '12%',
containLabel: true
}
};
// if(chartData.dataList.length > 1){
option.legend = {
bottom: 0,
textStyle: {
color: ChartStyle.legend.color,
fontSize: ChartStyle.legend.fontSize
},
icon: 'diamond',
itemHeight: 10,
itemGap: 40,
}
// }
for (let i of chartData.dataList) {
option.series.push({
type: 'line',
step: 'end',
name: i.name,
data: i.value
});
}
this.chart.setOption(option);
}
}
};
</script>
<style lang="scss" scoped>
.chart {
width: 100%;
height: 100%;
}
</style>
<!-- 地图 -->
<template>
<div :id="id" class="chart" />
</template>
<script>
import ChartBase from '@/components/Base/ChartsBase.js';
export default {
mixins: [ChartBase],
computed: {
// 事件监听列表
eventListener() {
return [{
event: 'click',
handler: this.onclick
}, {
event: 'mouseover',
handler: this.onmouseover
}, {
event: 'mouseout',
handler: this.onmouseout
}];
}
},
methods: {
// 注册地图数据
beforeInit(echarts) {
echarts.registerMap('hunan', require('@/assets/json/hunan.json'));
},
async setOptions(chartData) {
if (!this.chart || !chartData) return;
let option;
option = {
geo: [{
map: 'hunan',
silent: true,
aspectScale: 0.9,
itemStyle: {
normal: {
areaColor: '#1b3370',
borderColor: '#93FCFF',
}
},
}],
series: [{
type: 'effectScatter',
coordinateSystem: 'geo',
data: chartData,
rippleEffect: {
scale: 4,
brushType: 'stroke'
},
label: {
show: true,
position: [0, -45],
color: '#fff',
backgroundColor: 'rgba(0,0,0,0.5)',
borderColor: '#93FCFF',
borderWidth: 1,
padding: 10,
formatter: (val) => {
return val.name;
},
},
hoverAnimation: true,
itemStyle: {
color: (params) => {
return params.data.active ? '#ff0000' : '#0055ff'
},
shadowBlur: 15,
shadowColor: '#333'
},
}]
};
this.chart.setOption(option);
},
onclick(e) {
this.$emit('clickFactory', e.dataIndex);
},
onmouseover(e) {
this.$emit('selectFactory', e.dataIndex);
},
onmouseout(e) {
this.$emit('selectFactory');
}
}
};
</script>
<style lang="scss" scoped>
.chart {
width: 100%;
height: 100%;
}
</style>
<!-- 图表左侧的机组状态统计 -->
<template>
<div class="state-count">
<div class="item" :style="{'background-image': 'linear-gradient(to right, rgba(0,0,0,0), ' + item.color + ', rgba(0,0,0,0))'}" v-for="(item, key) in crewState">
{{item.name}}: {{copyCount[key]}}
</div>
</div>
</template>
<script>
import {CrewState} from '@/common/const.js';
export default {
props: {
count: {
type: Object,
default(){
return {
warn: 0,
danger: 0,
shutdown: 0,
normal: 0
}
}
}
},
watch: {
count(val){
if(!val) return;
else this.copyCount = val;
}
},
data() {
return {
crewState: CrewState, // 机组状态
// 复制count,防止count为空
copyCount: {
warn: 0,
danger: 0,
shutdown: 0,
normal: 0
},
}
}
}
</script>
<style lang="scss" scoped>
.state-count{
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
.item{
width: 100%;
height: 17%;
@extend %flex-center;
font-size: 0.875rem;
}
}
</style>
<!-- 数字翻牌器 -->
<template>
<div class="number-flipper">
<div class="up" v-if="title">
<div class="i" />{{title}}
</div>
<div class="down">
<num :number="relNumber" />
<span class="unit">{{unit}}</span>
</div>
</div>
</template>
<script>
import Num from './num.vue'
export default {
components: {
Num,
},
props: {
title: {
type: String,
default: '',
},
unit: {
type: String,
default: '',
},
number: {
type: Number | String,
default: 0,
},
// 小数位数
point: {
type:Number,
default: 0,
}
},
data() {
return {
relNumber: 0,
}
},
watch: {
number: {
immediate: true,
handler(val) {
this.relNumber = parseFloat(val).toFixed(this.point < 0 ? 0 : this.point);
}
}
},
}
</script>
<style lang="scss" scoped>
.number-flipper {
.up{
color: $color-cyan;
@extend %flex-center;
margin-bottom: 0.5rem;
.i {
width: 0.2rem;
height: 0.2rem;
border-radius: 50%;
background: $color-cyan;
box-shadow: 0 0 0.12rem 0.15rem rgba($color: $color-cyan, $alpha: 0.3);
margin-right: 0.5rem;
}
}
.down{
display: flex;
justify-content: center;
align-items: flex-end;
.unit{
font-size: 0.875rem;
color: $color-cyan;
}
}
}
</style>
<!-- 数字翻牌器单个数字 -->
<template>
<div id="dataNums" class="number">
<div :class="item.num != '.' ? 'n-bg' : 'p-bg'" v-for="(item,index) in list" :key="index">
<div v-if="item.num != '.'" class="nums" :style="{transition:'all 1s ease-in-out 0s',top:'-'+item.top+'px'}">
<span v-for="(item2,index2) in numList" :key="index2">{{item2}}</span>
</div>
<div v-else class="point"></div>
</div>
</div>
</template>
<script>
export default {
props: {
number: {
type: Number | String,
default: 0
},
},
data() {
return {
height: 0,
list: [],
numList: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
}
},
watch: {
number: {
immediate: true,
handler(val) {
this.$nextTick(()=>{
this.scroll();
})
}
}
},
created() {
this.init();
},
methods: {
init() {
this.list = this.number.toString().split('').map(i => {
return {
num: i == '.' ? i : 0,
top: 0,
}
});
},
getHeight(){
this.height = parseFloat(getComputedStyle(document.getElementById("dataNums")).height);
},
scroll() {
if(!this.height) this.getHeight();
this.list = this.number.toString().split('').map(i => {
return {
num: i,
top: parseFloat(i * this.height),
}
});
}
}
}
</script>
<style lang="scss" scoped>
$bg-height: 2.8125rem;
.number {
text-align: center;
height: $bg-height;
display: flex;
.n-bg{
position: relative;
width: 2.15625rem;
height: $bg-height;
overflow: hidden;
background-image: url(../../assets/img/num-bg.png);
@extend %background-center;
margin-right: 0.3rem;
}
.p-bg{
position: relative;
width: 0.2rem;
height: $bg-height;
margin-right: 0.5rem;
}
.nums {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
span {
width: 100%;
height: 100%;
line-height: $bg-height;
float: left;
font-size: 2.25rem;
font-weight: bold;
color: $color-cyan;
}
}
.point{
position: absolute;
bottom: 0.3rem;
width: 0.3rem;
height: 0.3rem;
background: $color-cyan;
}
}
</style>
<!-- 滚动窗口 -->
<template>
<div class="scroll-window">
<img v-if="direction" class="top top-right" src="../../assets/img/win-right-top.png" />
<img v-else class="top top-left" src="../../assets/img/win-left-top.png" />
<div class="main">
<slot />
</div>
<img v-if="direction" class="bottom bottom-left" src="../../assets/img/win-right-bottom.png" />
<img v-else class="bottom bottom-right" src="../../assets/img/win-left-bottom.png" />
</div>
</template>
<script>
export default {
// 方向,0为左边,1为右边
props: ['direction'],
}
</script>
<style lang="scss" scoped>
.scroll-window{
width: 100%;
height: 100%;
position: relative;
.top{
width: 8.5rem;
height: auto;
position: absolute;
top: -0.25rem;
}
.top-left {
left: 0;
}
.top-right {
right: 0;
}
.bottom{
width: 5.75rem;
height: auto;
position: absolute;
bottom: -0.75rem;
}
.bottom-left {
left: -0.625rem;
}
.bottom-right {
right: -0.625rem;
}
.main{
height: 100%;
background-color: rgba(10,23,55,0.80);
border: 1px solid rgba(9,77,148,0.50);
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.10);
padding: 0.5rem 0;
box-sizing: border-box;
}
}
</style>
<!-- 机组诊断的侧边导航 -->
<template>
<div class="sidebar" v-loading="loading" :element-loading-background="loadingColor">
<el-scrollbar class="scrollbar">
<div class="menu-block">
<!-- 搜索 -->
<div class="menu-search global">
<el-input class="input-style" v-model="searchValue" placeholder="快速搜索" @keyup.enter.native="search"></el-input>
<el-button class="btn-style" @click="search">搜索</el-button>
<el-button class="btn-style" @click="cleanUp">清空</el-button>
</div>
<!-- 菜单 -->
<el-menu ref="menu" text-color="#fff" unique-opened1 @open="openMenu" @close="closeMenu">
<el-submenu v-for="(factory, index) of menu" :key="index" :index="index+''">
<template slot="title">
<span>{{ factory.plantName }}</span>
</template>
<div class="sub-menu" v-for="(crew, index2) of factory.plantDevices" :key="index2">
<div v-if="index2 == factory.plantDevices.length - 1" class="line-y-item"></div>
<div class="line-x-item"></div>
<el-submenu :index="index + '_' + index2">
<template slot="title">
<span>{{ crew.deviceName }}</span>
</template>
<div class="sub-menu" v-for="(character, index3) of crew.characters" :key="index3">
<div v-if="index3 == crew.characters.length - 1" class="line-y-item"></div>
<div class="line-x-item"></div>
<el-checkbox-group class="check-list" v-model="checkList" :max="pointListMax">
<el-checkbox class="check-style" :label="index + '_' + index2 + '_' + index3" @change="changeMenu">
{{ character.cpName }} </el-checkbox>
</el-checkbox-group>
</div>
</el-submenu>
</div>
</el-submenu>
</el-menu>
</div>
</el-scrollbar>
</div>
</template>
<script>
import API from '@/api/factory.js';
import {
ChartStyle
} from '@/common/const.js';
export default {
data() {
return {
loading: false,
loadingColor: ChartStyle.loadingColor,
searchValue: '', // 搜索值
menuAll: null, // 所有菜单列表
menu: null, // 当前菜单列表
pointListMax: 4, // 报警最多选择数量
checkList: [], // 选择列表
lastMenuKey: '', // 存储菜单最后一次的key,用于打开其他菜单时,主动调用关闭前一个菜单(清空测点,优化性能)
};
},
computed: {
// 根据当前菜单和选择列表,返回选择列表中每个项的测点信息
checkNodeList() {
if (!this.checkList.length) return [];
let list = this.checkList.map(i => {
let arr = i.split('_');
let obj = {
factory: this.menu[arr[0]],
crew: this.menu[arr[0]].plantDevices[arr[1]],
character: this.menu[arr[0]].plantDevices[arr[1]].characters[arr[2]],
index: i
};
return obj;
});
return list;
},
},
created() {
this.initMenu();
},
methods: {
// 初始化菜单
initMenu() {
this.menu = this.menuAll = this.$x_factoryList;
this.initDefaultActive();
},
// 展开菜单,如果没有选择机组,则展开第一个机组的第一个部件的第一个测点,否则展开选择机组的第一个部件的第一个测点,如果有默认按钮,则找到默认按钮对应的测点展开
async initDefaultActive() {
this.$nextTick(async () => {
let pointList = this.$route.query.pointList;
try{
pointList = JSON.parse(pointList);
}catch(e){
pointList = null;
}
if (pointList) {
// 选择了测点
let indexFactory = this.menu.findIndex(i => i.plantId == this.$x_factory.plantId); // 电厂索引,当前电厂
let itemFactory = this.menu[indexFactory];
let checkList = [];
// 构造机组测点树结构
let tmpTree = {};
pointList.forEach(i => {
let indexCrew = itemFactory.plantDevices.findIndex(j => j.deviceId == i.deviceId); // 机组索引
if (!tmpTree[indexCrew]) tmpTree[indexCrew] = [i.kksCode];
else tmpTree[indexCrew].push(i.kksCode);
})
// 遍历获取测点
let tmpCount = 0; // 记录遍历次数
let tmpLen = Object.keys(tmpTree).length;
for (let indexCrew in tmpTree) {
try{
await this.openMenu(indexFactory + '_' + indexCrew); // 获取测点
let itemCrew = itemFactory.plantDevices[indexCrew];
tmpTree[indexCrew].forEach(i => {
let indexCharacter = itemCrew.characters.findIndex(j => j.kksCode == i); // 测点索引
checkList.push(indexFactory + '_' + indexCrew + '_' + indexCharacter); // 放入选择列表
})
// 如果都放进去了,模拟点击菜单
if(++tmpCount == tmpLen) {
this.checkList = checkList;
this.changeMenu();
this.$refs.menu.open(indexFactory + '_' + indexCrew); // 展开机组菜单
}
}catch(e){
console.log(e);
}
}
} else {
// 没有选择默认测点
let indexFactory = 0; // 电厂索引,默认第一个
let indexCrew = 0; // 机组索引,默认第一个
let indexCharacter = 0; // 测点索引,默认第一个
try{
await this.openMenu(indexFactory + '_' + indexCrew); // 获取测点
this.checkList = [indexFactory + '_' + indexCrew + '_' + indexCharacter]; // 模拟点击测点
this.changeMenu();
this.$refs.menu.open(indexFactory + '_' + indexCrew); // 展开机组菜单
}catch(e){
console.log(e);
this.$refs.menu.open(indexFactory); // 展开电厂菜单
}
}
});
},
// 展开菜单,获取机组测点
async openMenu(key) {
let arr = key.split('_');
if (arr.length != 2) return; // 只有等于2(即机组菜单)时才需要后续的获取测点
// 由于设置了只同一时间只存在一个菜单的展开,所以清空上一次菜单的测点
this.closeMenu(this.lastMenuKey);
this.lastMenuKey = key;
// 查询测点
let crew = this.menu[arr[0]].plantDevices[arr[1]];
if (!crew.characters?.length) {
this.loading = true;
try {
let res = await API.getCharacterList(crew.deviceId); // 获取测点信息
crew.characters = res;
} catch (e) {
console.log(e);
}
this.loading = false;
}
return crew.characters;
},
// 关闭展开的菜单时,如果选择列表里有当前菜单的测点,则不清空测点,没有则清空
closeMenu(key) {
// let arr = key.split('_');
// if (arr.length != 2) return;
// if (this.checkList.find(i => i.indexOf(arr[0] + '_' + arr[1]) == 0)) return; // 这里只判断机组部件的索引是否包含,不去判断二级菜单
// this.menu[arr[0]].plantDevices[arr[1]].characters = []; // 清空测点
},
// 切换菜单
changeMenu() {
this.$emit('changeMenu', this.checkNodeList);
},
// 搜索测点名
search() {
this.checkList = [];
this.lastMenuKey = '';
let copyMenu = JSON.parse(JSON.stringify(this.menuAll));
this.menu = copyMenu.filter(i => {
let newDev = i.plantDevices.filter(j => {
let newCha = j.characters ? j.characters.filter(k => k.cpName.indexOf(this.searchValue) != -1) : [];
j.characters = newCha;
return newCha.length;
})
i.plantDevices = newDev;
return newDev.length;
});
},
// 清空搜索
cleanUp() {
this.checkList = [];
this.lastMenuKey = '';
this.searchValue = '';
this.menu = this.menuAll;
}
}
};
</script>
<style lang="scss" scoped>
.sidebar {
// el滚动条
.scrollbar {
height: 100%;
}
/deep/.el-scrollbar__wrap {
overflow-x: hidden;
}
.menu-block {
margin: 1.25rem;
}
// 搜索样式
.menu-search {
display: flex;
margin-bottom: 0.625rem;
height: 2.5rem;
.input-style {
margin-right: 0.625rem;
}
/deep/.el-input__inner {
border-radius: 50px;
height: 100%;
}
.btn-style {
@extend %btn-default;
font-size: 0.875rem;
flex-shrink: 0;
}
}
$item-height: 2.5rem;
$line-color: rgba($color: #fff, $alpha: 0.3);
// 菜单样式
/deep/ .el-menu {
border: none;
position: relative;
background-color: transparent;
// 横向线条
.line-x-item {
width: calc(6% + 1px);
height: 2px;
background-color: $line-color;
position: absolute;
top: 2.125rem;
left: 0;
}
// 纵向线条
.line-y-item {
width: 2px;
height: 2.1875rem;
background-color: $line-color;
position: absolute;
top: 0%;
left: 0%;
}
.sub-menu {
position: relative;
width: 92%;
padding-left: 6%;
box-sizing: border-box;
border-left: 2px solid $line-color;
}
.sub-menu:last-child {
width: 92%;
padding-left: 6%;
box-sizing: border-box;
border-left: none;
}
.check-list {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-end;
.check-style {
width: 100%;
height: $item-height;
margin-top: 1rem;
padding: 0 1.25rem;
box-sizing: border-box;
background-color: rgba($color: $color-cyan, $alpha: 0.1);
border-radius: 50px;
display: flex;
align-items: center;
/deep/ .el-checkbox__label {
font-size: 1rem !important;
color: #fff;
}
}
}
// 组菜单标题和单项标题
/deep/.el-submenu__title,
/deep/.el-menu-item {
height: 2.5rem;
background-color: rgba($color: $color-cyan, $alpha: 0.1);
display: flex;
align-items: center;
border-radius: 50px;
font-size: 1rem;
padding: 0 1.25rem !important;
overflow: hidden;
}
// 组菜单标题和单项标题滑过
/deep/.el-submenu__title:hover,
/deep/.el-menu-item:hover {
background-color: $color-deep-blue;
}
// 组菜单标题
/deep/.el-submenu__title {
margin-top: 1rem;
}
// 去掉图标
/deep/.el-icon-arrow-down:before {
content: '';
}
$disable-color: #888888;
/deep/.el-checkbox__input.is-disabled+span.el-checkbox__label {
color: $disable-color;
}
/deep/.el-checkbox__input.is-disabled .el-checkbox__inner {
background-color: $disable-color;
border: none;
}
/deep/ul {
display: flex;
flex-direction: column;
align-items: flex-end;
}
}
}
</style>
<!-- 日期和用户 -->
<template>
<div class="user-info">
<div class="block">
<i class="el-icon-date"></i>
<div>{{ time }}</div>
</div>
<div class="block">
<i class="el-icon-user"></i>
<el-dropdown trigger="click" @command="handler">
<span class="dropdown-txt"> {{ $x_user ? $x_user.username : '未登录' }} </span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="quit">注销</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
<script>
import {
dateFormat
} from '@/common/util.js'
export default {
data() {
return {
userTimer: null, // 存放定时器
time: ''
};
},
mounted() {
this.$nextTick(() => {
this.initTimer();
});
},
beforeDestroy() {
window.clearTimeout(this.userTimer);
this.userTimer = null;
},
methods: {
// 初始化定时器
initTimer() {
this.getTime();
this.userTimer = setInterval(this.getTime, 1000);
},
// 获取当前时间
getTime() {
this.time = dateFormat(new Date(), 'YYYY/MM/DD HH:mm:ss z');
},
// 点击用户名的下拉菜单命令
handler(command) {
switch (command) {
case 'quit':
this.logout();
break;
}
},
// 注销,这里没清除用户,是考虑还可以返回,不重新登录
logout() {
this.$router.push('/login');
}
}
};
</script>
<style lang="scss" scoped>
.user-info {
font-size: 1rem;
color: $color-cyan;
width: 100%;
height: 1.875rem;
display: flex;
justify-content: flex-start;
align-items: center;
.block {
height: 100%;
display: flex;
align-items: center;
margin-right: 1.875rem;
i {
margin-right: 0.625rem;
}
.dropdown-txt {
color: $color-cyan;
}
}
}
</style>
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import { mapState } from 'vuex'
import './style/index.scss'
// import './style/icons.css'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css';
Vue.config.productionTip = false
Vue.use(ElementUI)
// 将state混入computed
Vue.mixin({
computed: {
...mapState(Object.keys(store.state))
}
})
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
import Vue from 'vue'
import VueRouter from 'vue-router'
import store from '@/store/index.js'
import Index from '@/views/Index'
import {
makeMap
} from '@/common/util.js';
Vue.use(VueRouter)
let IndexRoutePath = '/FactoryMonitoring'; // 首页即('/')跳转的页面路径,默认为机组监测,如果用户的角色没有这个页面的权限,会改为用户权限里的第一个页面
// 普通路由,这里的路由为初始路由,不能有角色控制,后面的为角色路由,角色字段为role,表示该数组内的角色都可以访问此路由,如果没有角色,则都可以访问
const normalRoutes = [{
// 默认页
path: '/',
}, {
// 登录页
path: '/login',
name: 'Login',
component: () => import('@/views/Login')
}, {
// 电厂选择页
path: '/factory',
name: 'Factory',
component: () => import('@/views/Factory')
}, {
// 404
path: '/404',
name: '404',
component: () => import('@/views/404')
}]
// 角色路由
const roleRoutes = [{
// 全厂监测
path: '/FactoryMonitoring',
name: 'FactoryMonitoring',
component: Index,
redirect: '/FactoryMonitoring/index',
children: [{
path: 'index',
component: () => import('@/views/FactoryMonitoring'),
}]
}, {
// 机组监测
path: '/CrewMonitoring',
name: 'CrewMonitoring',
component: Index,
redirect: '/CrewMonitoring/index',
children: [{
path: 'index',
component: () => import('@/views/CrewMonitoring'),
}]
}, {
// 趋势分析
path: '/TrendAnalysis',
name: 'TrendAnalysis',
component: Index,
redirect: '/TrendAnalysis/index',
children: [{
path: 'index',
component: () => import('@/views/TrendAnalysis'),
}]
}, {
// 报警记录
path: '/AlarmRecord',
name: 'AlarmRecord',
component: Index,
redirect: '/AlarmRecord/index',
children: [{
path: 'index',
component: () => import('@/views/AlarmRecord'),
}]
}, {
// 用户管理
path: '/UserManage',
name: 'UserManage',
component: Index,
redirect: '/UserManage/index',
children: [{
path: 'index',
component: () => import('@/views/UserManage'),
}]
}, {
// 角色管理
path: '/RoleManage',
name: 'RoleManage',
component: Index,
redirect: '/RoleManage/index',
children: [{
path: 'index',
component: () => import('@/views/RoleManage'),
}]
}]
// 匹配其他所有路由,必须放在最后添加到路由里,所以单独建了一个
const anyRoutes = [{
path: '*',
redirect: '/404'
}]
// 过滤掉角色相关路由
function filterRoute(routes, checkPermission) {
let newRoute = [];
if (checkPermission) {
routes.forEach(i => {
let tmp = { ...i
};
if (!tmp.name || checkPermission(tmp.name)) newRoute.push(tmp);
if (tmp.children) tmp.children = filterRoute(tmp.children, checkPermission); // 过滤子路由
})
}
return newRoute
}
// 初始化路由方法,返回新路由对象,只包含基础路由,不包含角色,角色路由应该在登录以后调用initRoleRoute方法动态添加
const createRouter = () => new VueRouter({
mode: 'hash',
base: process.env.BASE_URL,
routes: normalRoutes,
})
const router = createRouter();
// 重置角色路由
function initRoleRoute() {
let newRoleRoutes = roleRoutes;
// 过滤角色路由
if (store.state.$x_user?.checkPermission) {
newRoleRoutes = filterRoute(roleRoutes, store.state.$x_user.checkPermission);
// 如果该角色没有机组监测页(CrewMonitoring),则把该角色的第一个权限页改为默认页
if (newRoleRoutes.length && !newRoleRoutes.find(i => i.path == IndexRoutePath)) IndexRoutePath = newRoleRoutes[0].path;
}
normalRoutes[0].redirect = IndexRoutePath;
const newRouter = createRouter(); // 新建路由
router.matcher = newRouter.matcher; // 置换router
router.addRoutes([...newRoleRoutes, ...anyRoutes]);
console.log('路由初始化完成', newRoleRoutes);
}
// 初始化用户、电厂、机组列表,从本地读取,如果都存在并未过期,返回undefined,否则返回1或2
function initLocal() {
// 获取用户信息
let user = JSON.parse(localStorage.getItem('electric_user'));
let token = localStorage.getItem('electric_user_token');
if (!user || !token || user.endTime < Date.now()) return 1; // 如果没有用户和token,或者过期,说明未登录,则返回1,意味着需要跳转到登录页
user.data.token = token; // 将token放入user里,方便全局取
user.data.checkPermission = makeMap(user.data.permission); // 添加一个查询权限的参数
store.commit('user', user.data);
// 获取电厂信息
let factoryList = JSON.parse(localStorage.getItem('electric_factoryList'));
let factory = JSON.parse(localStorage.getItem('electric_factory'));
if (!factory || !factoryList || factory.endTime < Date.now()) return 2; // 如果没有电厂和设备列表,或者过期,说明未选择电厂,则返回2,意味着需要跳转到电厂选择页
store.commit('factory', factory.data);
store.commit('factoryList', factoryList.data);
initRoleRoute();
}
// 如果没有用户则跳转到登录
router.beforeEach((to, from, next) => {
// 如果是已登录,放行
if (to.path === '/login' || (store.state.$x_user && store.state.$x_factory)) {
next();
} else if (to.path === '/factory' && store.state.$x_user) {
next();
// 如果未登录,检查本地用户,并且添加角色路由后放行
} else {
const res = initLocal();
if (res == 1) {
next('/login');
} else if (res == 2) {
next('/factory');
} else {
next({ ...to
})
}
}
})
export default router
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
$x_user: null, // 用户信息
$x_factoryList: null, // 电厂列表
$x_factory: null, // 当前选择的电厂信息
$x_crew: null, // 当前选择的机组信息
$x_allData: [], // 所有测点数据
},
mutations: {
user(state, data) {
state.$x_user = data;
},
factoryList(state, data) {
state.$x_factoryList = data;
},
factory(state, data) {
state.$x_factory = data;
},
crew(state, data) {
state.$x_crew = data;
},
allData(state, data) {
state.$x_allData = data;
},
},
actions: {},
modules: {}
})
// 全局样式,最底部修改了el的默认样式,但必须在父元素加上global才行
@media (max-width: 1900px) {
html {
font-size: 12px;
}
}
@media (max-width: 1400px) {
html {
font-size: 10px;
}
}
#app {
width: 100%;
height: 100%;
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
}
html,
body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
color: #FFF;
letter-spacing: 0.125rem;
background-color: #061540;
}
$color-cyan: #93FCFF;
$color-blue: #0984FF;
$color-deep-blue: #1C529D;
$txt-color: #eeeeee;
// 背景图片居中
%background-center {
background-size: cover;
background-repeat: no-repeat;
background-position: center;
}
// flex居中
%flex-center {
display: flex;
justify-content: center;
align-items: center;
}
// 文字渐变
%txt-gradients {
text-shadow: 0 0.125rem 0.5rem rgba(78, 97, 214, 0.50);
// background: linear-gradient(to bottom, #C0D0FF, #ffffff, #A8D4FE);
// -webkit-background-clip: text;
// background-clip: text;
// color: transparent;
color: #C0D0FF;
}
// position居中
%pos-center{
position: absolute;
left: 0;
right: 0;
margin: auto;
}
// 背景框的位置
%bg-position {
@extend %pos-center;
top: 3.125rem;
width: 98.2%;
height: 93.5%;
background-image: url(../assets/img/border-inside.png);
background-size: 100% 100%;
}
// 按钮样式
%btn-default {
color: #fff;
background-color: transparent;
border-color: $color-cyan;
font-size: 1rem;
padding: 0.75rem 1rem;
letter-spacing: 0.125rem;
@extend %flex-center;
}
%btn-hover {
background-color: $color-blue;
}
%btn-active {
@extend %btn-default;
background-color: $color-deep-blue;
}
// 默认输入框样式
.global ::v-deep.el-input__inner {
border: 1px solid $color-cyan;
background-color: transparent;
color: #fff;
}
// 默认图表样式
.global ::v-deep.el-table {
color: $txt-color;
background-color: transparent;
}
.global ::v-deep.el-table thead {
color: $txt-color;
}
.global ::v-deep.el-table th {
font-weight: 200;
background-color: $color-deep-blue;
border-bottom: none;
border-right: 1px solid #000;
font-size: 1rem;
padding: 0;
height: 3.5rem;
}
.global ::v-deep.el-table th:nth-last-child(2),
.global ::v-deep.el-table th:nth-last-child(1) {
border-right: none;
}
.global ::v-deep.el-table tr {
background-color: transparent;
}
.global ::v-deep.el-table td {
font-size: 0.875rem;
padding: 0;
height: 3.5rem;
border-bottom-color: rgba($color: #fff, $alpha: 0.2);
}
.global ::v-deep.el-table--border::after, .el-table--group::after, .el-table::before{
background-color: transparent;
}
.global ::v-deep.el-table tbody tr:hover > td {
background-color: $color-deep-blue;
}
// 默认分页样式
.global ::v-deep.btn-prev,
.global ::v-deep.btn-next,
.global ::v-deep.el-pager li,
.global ::v-deep.el-pager li.btn-quicknext,
.global ::v-deep.el-pager li.btn-quickprev {
color: #fff;
background-color: transparent;
}
.global ::v-deep.el-pager li.active{
color: $color-blue;
}
.global ::v-deep.el-pager li:hover{
color: $color-blue;
}
// 默认弹框样式
.global ::v-deep.el-dialog {
background-color: rgba(10, 23, 55, 1);
border: 1px solid rgba(9, 77, 148, 0.5);
}
.global ::v-deep.el-dialog__title {
color: #fff;
}
\ No newline at end of file
<template>
<div class="wscn-http404-container">
<div class="wscn-http404">
<div class="pic-404">
<img class="pic-404__parent" src="@/assets/img/404.png" alt="404" />
<img class="pic-404__child left" src="@/assets/img/404_cloud.png" alt="404" />
<img class="pic-404__child mid" src="@/assets/img/404_cloud.png" alt="404" />
<img class="pic-404__child right" src="@/assets/img/404_cloud.png" alt="404" />
</div>
<div class="bullshit">
<div class="bullshit__oops">OOPS!</div>
<div class="bullshit__headline">Administrators said that you can not enter this page...</div>
<div class="bullshit__info">Please check that the URL you entered is correct, or click the button below to return to the homepage.</div>
<a href="" class="bullshit__return-home">Back to home</a>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.wscn-http404-container {
transform: translate(-50%, -50%);
position: absolute;
top: 40%;
left: 50%;
}
.wscn-http404 {
position: relative;
width: 1200px;
padding: 0 50px;
overflow: hidden;
.pic-404 {
position: relative;
float: left;
width: 600px;
overflow: hidden;
&__parent {
width: 100%;
}
&__child {
position: absolute;
&.left {
width: 80px;
top: 17px;
left: 220px;
opacity: 0;
animation-name: cloudLeft;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
&.mid {
width: 46px;
top: 10px;
left: 420px;
opacity: 0;
animation-name: cloudMid;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1.2s;
}
&.right {
width: 62px;
top: 100px;
left: 500px;
opacity: 0;
animation-name: cloudRight;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
@keyframes cloudLeft {
0% {
top: 17px;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
@keyframes cloudMid {
0% {
top: 10px;
left: 420px;
opacity: 0;
}
20% {
top: 40px;
left: 360px;
opacity: 1;
}
70% {
top: 130px;
left: 180px;
opacity: 1;
}
100% {
top: 160px;
left: 120px;
opacity: 0;
}
}
@keyframes cloudRight {
0% {
top: 100px;
left: 500px;
opacity: 0;
}
20% {
top: 120px;
left: 460px;
opacity: 1;
}
80% {
top: 180px;
left: 340px;
opacity: 1;
}
100% {
top: 200px;
left: 300px;
opacity: 0;
}
}
}
}
.bullshit {
position: relative;
float: left;
width: 300px;
padding: 30px 0;
overflow: hidden;
&__oops {
font-size: 32px;
font-weight: bold;
line-height: 40px;
color: #1482f0;
opacity: 0;
margin-bottom: 20px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&__headline {
font-size: 20px;
line-height: 24px;
color: #222;
font-weight: bold;
opacity: 0;
margin-bottom: 10px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&__info {
font-size: 13px;
line-height: 21px;
color: grey;
opacity: 0;
margin-bottom: 30px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&__return-home {
display: block;
float: left;
width: 110px;
height: 36px;
background: #1482f0;
border-radius: 100px;
text-align: center;
color: #ffffff;
opacity: 0;
font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
@keyframes slideUp {
0% {
transform: translateY(60px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
}
}
</style>
<!-- 报警记录 -->
<template>
<div class="alarm-record">
<div class="header">
<div class="left">
<el-button class="btn" @click="goPage">趋势分析</el-button>
</div>
<div class="center">
<!-- {{$x_factory.plantName}}{{$x_crew ? ' - ' + $x_crew.deviceName : ''}} - 报警记录 -->
{{$x_factory.plantName}} - 报警记录
</div>
<div class="right">
<span class="txt">仅显示报警中</span>
<el-switch v-model="tableQuery.onlyWarn" active-color="#0984FF" @change="changeState" />
</div>
</div>
<div class="main global">
<div class="table">
<el-table ref="table" :data="tableData" height="100%" @selection-change="selectTable" @row-click="switchSelect"
v-loading="loadingtable" :element-loading-background="loadingColor">
<el-table-column type="selection" min-width="5%" align="center" :selectable="tableCanSelect"></el-table-column>
<el-table-column type="index" label="序号" min-width="5%" align="center"></el-table-column>
<el-table-column label="机组名称" min-width="10%" align="center">
<template v-slot="scope">
{{scope.row.deviceId | getDeviceNameById}}
</template>
</el-table-column>
<el-table-column prop="kkscode.cpName" label="测点名称" min-width="20%" align="center"></el-table-column>
<el-table-column prop="kkscode.kksCode" label="测点编码" min-width="20%" align="center"></el-table-column>
<el-table-column prop="kkscode.cpUnit" label="单位" min-width="5%" align="center"></el-table-column>
<el-table-column prop="value" label="报警值" min-width="10%" align="center"> </el-table-column>
<el-table-column label="报警时刻" min-width="15%" align="center">
<template v-slot="scope">
{{scope.row.valueTime | dateFormat('YYYY/MM/DD HH:mm:ss')}}
</template>
</el-table-column>
<el-table-column label="操作" min-width="10%" align="center">
<template v-slot="scope">
<el-button v-if="!scope.row.eventInfo" class="table-btn" type="text" size="small" @click.stop="delForm(scope.row)">解除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-pagination class="page" layout="prev, pager, next" :total="tableTotal" @current-change="pageChange" />
</div>
</div>
</template>
<script>
import {
ChartStyle
} from '@/common/const.js';
import {
dateFormat,
getDeviceNameById
} from '@/common/util.js';
import Warn from '@/api/warn.js';
export default {
components: {},
data() {
return {
loadingtable: false,
loadingColor: ChartStyle.loadingColor,
// 查询参数
tableQuery: {
onlyWarn: false, // 是否仅显示正在报警中的记录
pageNo: 0, // 0为第一页
pageSize: 10
},
tableData: null,
tableTotal: 100,
pointList: [], // 表格里选择的报警点
pointListMax: 4, // 报警最多选择数量
// 报警表格是否可选,小于5个点可选,大于5个点,则已选的点可选
tableCanSelect: row => this.pointList.length < this.pointListMax || this.pointList.find(i => i.id ==row.id),
};
},
created() {
this.getList();
},
methods: {
// 跳转到趋势分析
goPage() {
if(!this.pointList.length) {
this.$message.warning('请选择一个测点');
return;
}
this.$router.push('/TrendAnalysis?pointList=' + JSON.stringify(this.pointList));
},
// 返回上一页
back() {
this.$router.go(-1);
},
// 获取报警记录
async getList() {
// this.tableData = this.initWarnRecordData();
this.loadingtable = true;
try {
// if(this.$x_crew) this.tableQuery.deviceId = this.$x_crew.deviceId;
// else this.tableQuery.plantId = this.$x_factory.plantId;
this.tableQuery.plantId = this.$x_factory.plantId;
if(this.tableQuery.onlyWarn) this.tableQuery.eventInfo = 0;
else delete this.tableQuery.eventInfo;
let res = await Warn.getWarnList(this.tableQuery);
this.tableData = res.content;
this.tableTotal = res.totalElements;
} catch (e) {
console.log(e);
}
this.loadingtable = false;
},
// 翻页
pageChange(val) {
this.tableQuery.pageNo = val - 1;
this.getList();
},
// 改变状态筛选
changeState(val) {
this.tableQuery.pageNo = 0;
this.getList();
},
// 表格里选择报警点
selectTable(row) {
// 如果是增加操作,则去重,并只保留5个
if (row.length > 1 && row.length > this.pointList.length) {
let map = {},
hasSame = false,
count = this.pointListMax;
for (let i = 0; i < row.length; i++) {
let item = row[i];
if (!map[item.kkscode.kksCode] && count > 0) {
map[item.kkscode.kksCode] = 1;
count--;
} else {
if (map[item.kkscode.kksCode]) hasSame = true;
this.$refs.table.toggleRowSelection(item);
row.splice(i, 1);
i--;
}
}
// if (hasSame) this.$message.warning('已过滤相同测点');
}
this.pointList = row.map(i => {
return {
id: i.id,
kksCode: i.kkscode.kksCode,
deviceId: i.deviceId
}
});
},
// 点击表格行时,切换选中
switchSelect(row) {
if (this.tableCanSelect(row)) this.$refs.table.toggleRowSelection(row);
else this.$message.warning('最多选择' + this.pointListMax + '个');
},
// 解除报警
async delForm(row) {
if (!row.id) return;
try {
await this.$confirm('确认解除报警?');
await Warn.changeState({id: row.id, eventInfo: 1});
this.$message.success('解除成功!');
if (this.tableData.length == 1 && this.tableQuery.pageNo != 0) this.tableQuery.pageNo--; // 如果删除时是当前页的最后一条,则页数往前翻一页
this.getList();
} catch (e) {
console.log(e);
}
},
// 模拟报警记录
initWarnRecordData() {
let param = {
size: 20, // 数据数量
item: [{
kkscode: {
cpName: '本体温度',
kksCode: 'C0A00101MKA20CT001',
cpUnit: '℃',
},
value: 54,
alertLimit: 87,
valueTime: '2020/03/27 02:43'
},
{
kkscode: {
cpName: '绝缘过热',
kksCode: 'C0A00101MKA20CT002',
cpUnit: '℃',
},
value: 48,
alertLimit: 73,
valueTime: '2020/02/12 08:12'
},
{
kkscode: {
cpName: '氢参数',
kksCode: 'C0A00101MKA20CT003',
cpUnit: 'kv',
},
value: 239,
alertLimit: 252,
valueTime: '2020/03/25 13:32'
},
{
kkscode: {
cpName: '密封参数',
kksCode: 'C0A00101MKA20CT004',
cpUnit: 'mm',
},
value: 132,
alertLimit: 164,
valueTime: '2020/03/26 00:23'
},
{
kkscode: {
cpName: '定转子冷却参数',
kksCode: 'C0A00101MKA20CT005',
cpUnit: 'cm',
},
value: 302,
alertLimit: 328,
valueTime: '2020/03/22 16:04'
},
{
kkscode: {
cpName: '润滑参数',
kksCode: 'C0A00101MKA20CT006',
cpUnit: 'p',
},
value: 210,
alertLimit: 234,
valueTime: '2020/03/23 18:19'
},
{
kkscode: {
cpName: '外部冷却水',
kksCode: 'C0A00101MKA20CT007',
cpUnit: '℃',
},
value: 109,
alertLimit: 124,
valueTime: '2020/03/14 18:59'
},
{
kkscode: {
cpName: '化学参数',
kksCode: 'C0A00101MKA20CT008',
cpUnit: 'ml',
},
value: 214,
alertLimit: 238,
valueTime: '2020/03/10 23:19'
},
{
kkscode: {
cpName: '轴振监测',
kksCode: 'C0A00101MKA20CT001',
cpUnit: 'cm',
},
value: 195,
alertLimit: 228,
valueTime: '2020/03/11 05:28'
},
{
kkscode: {
cpName: '转速监测',
kksCode: 'C0A00101MKA20CT001',
cpUnit: 'm',
},
value: 323,
alertLimit: 298,
valueTime: '2020/03/13 12:41'
}
],
};
let arr = [];
for (let i = 0; i < param.size; i++) {
let r = Math.floor(Math.random() * param.item.length);
let item = Object.assign({}, param.item[r]);
item.id = i + 1;
arr.push(item);
}
return arr;
}
},
filters: {
getDeviceNameById(id) {
return getDeviceNameById(id);
},
dateFormat(date, format){
return dateFormat(new Date(date), format);
},
}
};
</script>
<style lang="scss" scoped>
.alarm-record {
height: 100%;
padding: 1.875rem 3.125rem;
box-sizing: border-box;
.header {
height: 4.25rem;
background-image: linear-gradient(270deg, rgba(197, 254, 255, 0.10) 0%, rgba(147, 252, 255, 0.50) 52%, rgba(147, 252, 255, 0.10) 100%);
padding: 0 1.875rem;
box-sizing: border-box;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
.left {
display: flex;
z-index: 1;
height: 2.5rem;
.btn {
@extend %btn-default;
margin: 0 1rem 0 0;
}
.btn:hover {
@extend %btn-hover;
}
}
.center {
@extend %pos-center;
font-size: 1.625rem;
letter-spacing: 0.1625rem;
text-shadow: 0 2px 8px rgba(78, 97, 214, 0.50);
}
.right {
z-index: 1;
.txt{
margin-right: 0.5rem;
}
}
}
.main {
height: calc(100% - 4.25rem);
background: rgba(147, 252, 255, 0.08);
padding: 1.875rem 1.875rem 0;
box-sizing: border-box;
.table {
height: calc(100% - 40px);
.table-btn {
color: $color-cyan;
}
}
.page {
margin: 0.3125rem 0;
}
}
}
</style>
<!-- 机组监测 -->
<template>
<div class="crew-monitoring">
<div class="title">
{{$x_factory.plantName}} - {{$x_crew ? $x_crew.deviceName : ''}}
</div>
<!-- <div class="number">
<number-flipper class="num" title="机组转速" unit="r/min" :number="dataTop ? dataTop[0].kksValueR : 0" :point="1" />
<number-flipper class="num" title="有功功率" unit="kW" :number="dataTop ? dataTop[1].kksValueR : 0" :point="1" />
</div> -->
<div class="model">
<img src="../assets/img/crew.png" />
<scroll-window class="roll-left-top">
<el-scrollbar class="win-list" v-if="listLeftTop">
<div class="win-item" :style="{color: getWarnColor(v.state)}" v-for="v of listLeftTop">{{v.cpName + ' ' + v.kksValueR + ' ' + v.cpUnit}}</div>
</el-scrollbar>
</scroll-window>
<scroll-window class="roll-right-top" direction="1">
<el-scrollbar class="win-list" v-if="listRightTop">
<div class="win-item" :style="{color: getWarnColor(v.state)}" v-for="v of listRightTop">{{v.cpName + ' ' + v.kksValueR + ' ' + v.cpUnit}}</div>
</el-scrollbar>
</scroll-window>
<scroll-window class="roll-left-bottom">
<el-scrollbar class="win-list" v-if="listLeftBottom">
<div class="win-item" :style="{color: getWarnColor(v.state)}" v-for="v of listLeftBottom">{{v.cpName + ' ' + v.kksValueR + ' ' + v.cpUnit}}</div>
</el-scrollbar>
</scroll-window>
<scroll-window class="roll-right-bottom" direction="1">
<el-scrollbar class="win-list" v-if="listRightBottom">
<div class="win-item" :style="{color: getWarnColor(v.state)}" v-for="v of listRightBottom">{{v.cpName + ' ' + v.kksValueR + ' ' + v.cpUnit}}</div>
</el-scrollbar>
</scroll-window>
</div>
<div class="block left">
<div class="border-bg">
<div class="chart-title">电机功率曲线</div>
<chart-line-broken id="ChartLeftTop" ref="ChartLeftTop" class="chart-main" :chartData="dataLeftTop" />
</div>
<div class="border-bg">
<div class="chart-title">机组参数对比</div>
<div class="chart-main">
<div class="chart-nums">
<number-flipper v-for="v of dataLeftBottom" class="chart-num" :title="v.cpName" :unit="v.cpUnit" :number="v.kksValueR" :point="1" />
</div>
<!-- <chart-bar-three id="ChartLeftBottom" ref="ChartLeftBottom" class="chart-chart" :chartData="dataLeftBottom" /> -->
</div>
</div>
</div>
<div class="block right">
<div class="border-sm">
<div class="chart-title">发电机监测</div>
<div class="chart-main">
<!-- <crew-state-count class="chart-count" :count="countRightTop" /> -->
<chart-bar-pic id="ChartRightTop" ref="ChartRightTop" class="chart-chart" :chartData="dataRightTop" />
</div>
</div>
<div class="border-sm">
<div class="chart-title">水轮机监测</div>
<div class="chart-main">
<!-- <crew-state-count class="chart-count" :count="countRightCenter" /> -->
<chart-bar-pic id="ChartRightCenter" ref="ChartRightCenter" class="chart-chart" :chartData="dataRightCenter" />
</div>
</div>
<div class="border-sm">
<div class="chart-title">轴瓦状态监测</div>
<div class="chart-main">
<!-- <crew-state-count class="chart-count" :count="countRightBottom" /> -->
<chart-bar-pic id="ChartRightBottom" ref="ChartRightBottom" class="chart-chart" :chartData="dataRightBottom" />
</div>
</div>
</div>
</div>
</template>
<script>
import {
CONFIG,
CrewState,
CrewMConfig
} from '@/common/const.js';
import {
randomData,
dateFormat,
getCharacterFromCrew,
getWarnColor,
toFixedNum
} from '@/common/util.js';
import NumberFlipper from '@/components/NumberFlipper'
import ChartLineBroken from '@/components/Charts/ChartLineBroken'
import ChartBarThree from '@/components/Charts/ChartBarThree';
import ChartBarArc from '@/components/Charts/ChartBarArc'
import ChartBarPic from '@/components/Charts/ChartBarPic'
import ChartBarCylinder from '@/components/Charts/ChartBarCylinder'
import CrewStateCount from '@/components/CrewStateCount'
import ScrollWindow from '@/components/ScrollWindow'
import User from '@/api/user.js';
export default {
components: {
NumberFlipper,
ChartLineBroken,
ChartBarThree,
ChartBarArc,
ChartBarPic,
ChartBarCylinder,
CrewStateCount,
ScrollWindow,
},
data() {
return {
dataTop: null,
dataLeftTop: null,
dataLeftBottom: null,
dataRightTop: null,
dataRightCenter: null,
dataRightBottom: null,
countLeftBottom: null,
countRightTop: null,
countRightCenter: null,
countRightBottom: null,
listLeftTop: null,
listLeftBottom: null,
listRightTop: null,
listRightBottom: null,
timer: null,
socket: null, // socket
getWarnColor: getWarnColor,
}
},
watch: {
$x_crew: {
immediate: true,
handler(val) {
if (!val) this.$router.push('/FactoryMonitoring');
else {
// this.initRandomData(); // 模拟数据
this.initData(); // 图表数据
this.closeSocket();
this.initSocket(); // 左上角数据
}
}
},
$x_allData: {
handler(val) {
this.initData(); // 图表数据
}
}
},
beforeDestroy() {
this.closeSocket();
this.clearTimer();
},
methods: {
initData() {
if(!this.$x_crew || !this.$x_allData.length) return;
// this.dataTop = this.initDataList('top');
this.dataLeftBottom = this.initDataList('leftBottom', 6);
this.dataRightTop = this.initDataChart('rightTop', 4);
this.dataRightCenter = this.initDataChart('rightCenter', 4);
this.dataRightBottom = this.initDataChart('rightBottom', 4);
this.listLeftTop = this.initDataList('listLeftTop');
this.listLeftBottom = this.initDataList('listLeftBottom');
this.listRightTop = this.initDataList('listRightTop');
this.listRightBottom = this.initDataList('listRightBottom');
},
// 处理框框数据,传入位置, len为截取长度,不传则不截取
initDataList(pos, len) {
const crew = this.$x_allData.find(i => i.deviceId == this.$x_crew.deviceId);
// 提取相关测点
let list = CrewMConfig[pos].map(arr => {
for (let key of arr) {
let res = getCharacterFromCrew(crew, key);
if(res) return res;
}
return false;
}).filter(i => !!i);
if(len) list.splice(parseInt(len), list.length);
return list;
},
// 处理图表数据,传入位置, len为截取长度,不传则不截取
initDataChart(pos, len) {
let list = this.initDataList(pos, len);
let data = {
xAxis: list.map(i => i.cpName),
dataList: [{
name: this.$x_crew.deviceName,
// value: list.map(i => i.kksValue?.value), // 传原值,图表里处理state
value: list.map(i => i.kksValueR), // 传真实值
// value: list.map(i => toFixedNum(i.kksValueR, 2)), // 保留小数,看用户是否需要
state: list.map(i => i.state),
}],
}
return data;
},
// 初始化socket
initSocket() {
try{
this.socket = new WebSocket('ws://' + CONFIG.SOCKET + '/powerCurve/' + this.$x_crew.deviceId, [this.$x_user.token.split(' ')[1]]);
this.socket.onopen = () => console.log('socket_crew success');
this.socket.onerror = e => console.log('socket_crew error', e);
this.socket.onmessage = msg => {
try{
const data = JSON.parse(msg.data);
console.log('socket_crew data', data);
if (!data) return;
data.xAxis = data.xaxis.map(i => dateFormat(new Date(i * 1000), 'YY/MM/DD\nHH:mm:ss'));
this.dataLeftTop = data;
}catch(e){
console.log(e);
}
}
}catch(e){
console.log(e);
User.getRoleList(); // 捕获错误,然后发送一个请求,因为可能是token导致的socket连接错误,发送请求后如果是token过期,会自动注销用户
}
},
// 关闭socket
closeSocket() {
if (this.socket) {
this.socket.close();
this.socket = null;
}
},
// 清除定时器
clearTimer() {
if (this.timer) {
window.clearTimeout(this.timer);
this.timer = null;
}
},
// 初始化虚拟数据
initRandomData() {
this.clearTimer();
const fun = () => {
this.dataLeftTop = this.initDataLeftTop();
this.dataLeftBottom = this.initDataLeftBottom();
this.dataRightTop = this.initDataRightTop();
this.dataRightCenter = this.initDataRightCenter();
this.dataRightBottom = this.initDataRightBottom();
// this.countLeftBottom = this.initCountLeftBottom();
// this.countRightTop = this.initCountRightTop();
// this.countRightCenter = this.initCountRightCenter();
// this.countRightBottom = this.initCountRightBottom();
};
fun();
this.timer = setInterval(fun, 3000);
},
initDataLeftTop() {
let data = {
unit: '(MW)',
xAxis: ['08-25 08', '08-25 09', '08-25 10', '08-25 11', '08-25 12'],
dataList: [{
name: '机组功率',
value: []
}, {
name: '水位头',
value: []
}],
}
for (let i = 0; i < data.xAxis.length; i++) {
for (let j = 0; j < data.dataList.length; j++) {
data.dataList[j].value.push(Math.ceil(Math.random() * 20 + 20));
}
}
return data;
},
initDataLeftBottom() {
let data = {
xAxis: ['一级报警', '二级报警', '三级报警'],
dataList: [{
name: '1号机组',
value: []
}],
}
for (let i = 0; i < data.xAxis.length; i++) {
for (let j = 0; j < data.dataList.length; j++) {
data.dataList[j].value.push(Math.ceil(Math.random() * 20 + 20));
}
}
return data;
},
initDataRightTop() {
// let data = {
// xAxis: ['一级报警', '二级报警', '三级报警'],
// dataList: [{
// name: '1号机组',
// value: []
// }],
// }
// for (let i = 0; i < data.xAxis.length; i++) {
// for (let j = 0; j < data.dataList.length; j++) {
// data.dataList[j].value.push(Math.ceil(Math.random() * 20 + 20) + '.005');
// }
// }
// return data;
let data = [{
title: '上导轴承振动',
xAxis: ['X', 'Y'],
dataList: [{
name: '1号机组',
value: []
}],
}, {
title: '上机架振动',
xAxis: ['X', 'Y'],
dataList: [{
name: '1号机组',
value: []
}],
}]
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].xAxis.length; j++) {
for (let k = 0; k < data[i].dataList.length; k++) {
data[i].dataList[k].value.push(Math.ceil(Math.random() * 20 + 20) + '.005');
}
}
}
return data;
},
initDataRightCenter() {
// let data = {
// xAxis: ['一级报警', '二级报警', '三级报警'],
// dataList: [{
// name: '1号机组',
// value: []
// }],
// }
// for (let i = 0; i < data.xAxis.length; i++) {
// for (let j = 0; j < data.dataList.length; j++) {
// data.dataList[j].value.push(Math.ceil(Math.random() * 20 + 20) + '.005');
// }
// }
// return data;
let data = [{
title: '上导轴承振动',
xAxis: ['X', 'Y'],
dataList: [{
name: '1号机组',
value: []
}],
}, {
title: '法兰振动',
xAxis: ['X', 'Y'],
dataList: [{
name: '1号机组',
value: []
}],
}, {
title: '水导轴承振动',
xAxis: ['X', 'Y'],
dataList: [{
name: '1号机组',
value: []
}],
}]
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].xAxis.length; j++) {
for (let k = 0; k < data[i].dataList.length; k++) {
data[i].dataList[k].value.push(Math.ceil(Math.random() * 20 + 20) + '.005');
}
}
}
return data;
},
initDataRightBottom() {
// let data = {
// xAxis: ['一级报警', '二级报警', '三级报警'],
// dataList: [{
// name: '1号机组',
// value: []
// }],
// }
// for (let i = 0; i < data.xAxis.length; i++) {
// for (let j = 0; j < data.dataList.length; j++) {
// data.dataList[j].value.push(Math.ceil(Math.random() * 20 + 20) + '.005');
// }
// }
// return data;
let data = [{
title: '上导轴承振动',
xAxis: ['X', 'Y'],
dataList: [{
name: '1号机组',
value: []
}],
}, {
title: '顶盖振动',
xAxis: ['X', 'Y'],
dataList: [{
name: '1号机组',
value: []
}],
}]
for (let i = 0; i < data.length; i++) {
for (let j = 0; j < data[i].xAxis.length; j++) {
for (let k = 0; k < data[i].dataList.length; k++) {
data[i].dataList[k].value.push(Math.ceil(Math.random() * 20 + 20) + '.005');
}
}
}
return data;
},
initCountLeftBottom() {
let count = {};
for (let key in CrewState) {
count[key] = Math.ceil(Math.random() * 20 + 20);
}
return count;
},
initCountRightTop() {
let count = {};
for (let key in CrewState) {
count[key] = Math.ceil(Math.random() * 20 + 20);
}
return count;
},
initCountRightCenter() {
let count = {};
for (let key in CrewState) {
count[key] = Math.ceil(Math.random() * 20 + 20);
}
return count;
},
initCountRightBottom() {
let count = {};
for (let key in CrewState) {
count[key] = Math.ceil(Math.random() * 20 + 20);
}
return count;
},
}
}
</script>
<style lang="scss" scoped>
.crew-monitoring {
position: relative;
width: 100%;
height: 100%;
.title {
font-size: 1.625rem;
color: $color-cyan;
@extend %pos-center;
top: 0;
}
.number {
width: 30%;
@extend %pos-center;
top: 3.75rem;
display: flex;
justify-content: space-between;
.num {
// transform: scale(0.9);
}
}
.model {
width: 40%;
height: calc(100% - 5rem);
@extend %pos-center;
bottom: 1.25rem;
img {
width: auto;
height: 50%;
@extend %pos-center;
top: 0;
bottom: 0;
transform: translateX(-1.5rem);
}
$win-width: 40%;
$win-pos: 0;
.roll-left-top {
width: $win-width;
height: 30vh;
position: absolute;
top: 0;
left: $win-pos;
}
.roll-right-top {
width: $win-width;
height: 28vh;
position: absolute;
top: 5%;
right: $win-pos;
}
.roll-left-bottom {
width: $win-width;
height: 30vh;
position: absolute;
left: $win-pos;
bottom: 14%;
}
.roll-right-bottom {
width: $win-width;
height: 40vh;
position: absolute;
right: $win-pos;
bottom: 3%;
}
.win-list{
height: 100%;
line-height: 1.5rem;
}
/deep/.el-scrollbar__wrap {
overflow-x: hidden;
}
}
.block {
width: 30%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 1% 1.5%;
box-sizing: border-box;
}
.left {
position: absolute;
left: 0;
}
.right {
position: absolute;
right: 0;
}
.border-bg {
height: 49%;
background-image: url(../assets/img/border-big.png);
background-size: 100% 100%;
}
.border-sm {
height: 31.5%;
background-image: url(../assets/img/border-small.png);
background-size: 100% 100%;
}
.chart-main {
height: calc(100% - 2.5rem);
box-sizing: border-box;
padding: 1.25rem;
display: flex;
align-items: center;
.chart-count {
width: 20%;
height: 11.25rem;
box-sizing: border-box;
padding: 0 2%;
}
.chart-chart {
// width: 80%;
width: 100%;
}
.chart-nums{
width: 100%;
height: 100%;
display: flex;
flex-wrap: wrap;
.chart-num {
width: 50%;
height: 33%;
// transform: scale(0.9);
@extend %flex-center;
flex-direction: column;
}
}
}
.chart-title {
font-size: 1.125rem;
color: $color-cyan;
line-height: 2.5rem;
}
}
</style>
<!-- 电厂选择页 -->
<template>
<div class="factory">
<img class="left-border" src="../assets/img/border-left.png" />
<img class="right-border" src="../assets/img/border-right.png" />
<div class="title">
<span class="txt">{{title}}</span>
</div>
<user-info class="user-info" />
<div class="main" v-loading="loading" :element-loading-background="loadingColor">
<chart-map ref="ChartMap" id="ChartMap" class="chart-map" :chartData="factoryList" @selectFactory="selectFactory" @clickFactory="clickFactory" />
<!-- 关闭 -->
<transition name="list">
<div v-if="!factoryItem" class="factory-block factory-list">
<div class="factory-item" v-for="fac of factoryList">
<div class="factory-name" @mouseover="showFactory(fac)">{{fac.plantName}}</div>
</div>
</div>
</transition>
<!-- 展开 -->
<transition name="item">
<div v-if="factoryShow" class="factory-block factory-sign" @mouseleave="closeFactory" @click="goPage(factoryItem)">
<div class="factory-name">{{factoryItem.plantName}}</div>
<div class="y-line" :style="{height: (factoryItem.plantDevices.length > 11 ? 10 * 4.75 + 'rem' : (factoryItem.plantDevices.length - 1) * 4.75 + 'rem')}"></div>
<div class="crew-list">
<div class="crew-item" v-for="(dev,i) of factoryItem.plantDevices.slice(0,11)" :class="dev.runstate ? 'crew-item-run' : 'crew-item-stop'"
:style="{'align-self': i % 2 !== 1 ? 'flex-start' : 'flex-end'}">
<span>{{dev.deviceName}}</span>
<span>{{dev.runstate ? '运行' : '停机'}}</span>
<div class="x-line" :class="i % 2 !== 1 ? 'x-line-s' : 'x-line-l'">
<img class="x-line-arrow" src="../assets/img/arrow-double-s.png" />
</div>
</div>
</div>
<div class="crew-list crew-list2" v-if="factoryItem.plantDevices.length > 11">
<div class="crew-item" v-for="(dev,i) in factoryItem.plantDevices.slice(11)" :class="dev.runstate ? 'crew-item-run' : 'crew-item-stop'"
:style="{'align-self': i % 2 !== 1 ? 'flex-start' : 'flex-end'}">
<span>{{dev.deviceName}}</span>
<span>{{dev.runstate ? '运行' : '停机'}}</span>
<div class="x-line x-line-m"></div>
</div>
</div>
</div>
</transition>
</div>
</div>
</template>
<script>
import {
CONFIG,
ChartStyle
} from '@/common/const.js'
import UserInfo from '@/components/UserInfo'
import ChartMap from '@/components/Charts/ChartMap'
import Factory from '@/api/factory.js';
import User from '@/api/user.js';
export default {
components: {
UserInfo,
ChartMap
},
data() {
return {
loading: false,
loadingColor: ChartStyle.loadingColor,
title: CONFIG.TITLE,
// 电厂列表
factoryList: [],
factoryShow: false, // 选择展开的电厂
factoryItem: null, // 选择展开的电厂
socket: null, // socket
socketData: null,
};
},
created() {
this.getFactoryList();
this.initSocket();
},
beforeDestroy() {
this.closeSocket();
},
methods: {
initData() {
if(!this.factoryItem) return;
this.factoryItem.plantDevices.forEach(i => i.runstate = this.socketData[i.deviceId]);
},
// 初始化socket
initSocket() {
try{
this.socket = new WebSocket('ws://' + CONFIG.SOCKET + '/runningState/deviceInfo', [this.$x_user.token.split(' ')[1]]);
this.socket.onopen = () => console.log('socket_factory success');
this.socket.onerror = e => console.log('socket_factory error', e);
this.socket.onmessage = msg => {
try{
const data = JSON.parse(msg.data);
console.log('socket_factory data', data);
if (!data) return;
this.socketData = data;
this.initData();
}catch(e){
console.log(e);
}
}
}catch(e){
console.log(e);
User.getRoleList(); // 捕获错误,然后发送一个请求,因为可能是token导致的socket连接错误,发送请求后如果是token过期,会自动注销用户
}
},
// 关闭socket
closeSocket() {
if (this.socket) {
this.socket.close();
this.socket = null;
}
},
// 获取电厂列表
async getFactoryList() {
this.loading = true;
try {
let res = await Factory.getPlantList();
res.forEach(i => {
i.name = i.plantName;
i.value = [i.latitude, i.longitude];
})
this.factoryList = res;
} catch (e) {
console.log(e);
}
this.loading = false;
},
// 选择电厂
clickFactory(i) {
this.goPage(this.factoryList[i]);
},
selectFactory(i) {
if(i !== undefined) this.showFactory(this.factoryList[i]);
else this.closeFactory();
},
// 页面跳转
goPage(item) {
this.$store.commit('factoryList', null);
this.$store.commit('factory', null);
this.$store.commit('crew', null);
this.$store.commit('allData', null);
let endTime = Date.now() + 3600 * 1000 * 24; // 24h后过期
localStorage.setItem('electric_factoryList', JSON.stringify({
data: this.factoryList,
endTime
}));
localStorage.setItem('electric_factory', JSON.stringify({
data: item,
endTime
}));
this.$router.push('/', () => {}, () => {}); // 加后面两个空回调方法,是为了解决一个router的报错
},
// 展开电厂
showFactory(item) {
if(!this.socketData) return;
this.factoryItem = item;
this.initData();
// 加上延迟,因为有动画
setTimeout(()=>{
this.factoryShow = true;
}, 100)
},
// 收起电厂
closeFactory() {
this.factoryShow = false;
setTimeout(()=>{
this.factoryItem = null;
}, 100)
}
}
};
</script>
<style lang="scss" scoped>
$line-color: #00CCF7;
.list-enter-active, .list-leave-active {
transition: all .1s;
}
.list-enter, .list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.item-enter-active, .item-leave-active {
transition: all .1s;
}
.item-enter, .item-leave-to {
opacity: 0;
transform: translateX(30px);
}
.factory {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
background-image: url(../assets/img/bg.png);
@extend %background-center;
.left-border {
position: absolute;
left: 0;
height: 100%;
}
.right-border {
position: absolute;
right: 0;
height: 100%;
}
.title {
@extend %pos-center;
width: 58.75rem;
height: 4.875rem;
background-image: url(../assets/img/title-bg.png);
@extend %background-center;
.txt {
font-size: 2.5rem;
font-weight: bold;
letter-spacing: 0.25rem;
@extend %txt-gradients;
}
}
.user-info {
width: calc((100% - 58.75rem) / 2);
position: absolute;
top: 0.8rem;
right: 0;
}
.main {
@extend %bg-position;
display: flex;
padding: 1.5% 15% 0;
box-sizing: border-box;
.chart-map {
width: 60%;
height: 100%;
}
.factory-block {
width: 40%;
height: 100%;
padding: 3% 0;
box-sizing: border-box;
.factory-name {
width: 9rem;
height: 8.875rem;
background-image: url(../assets/img/circle.png);
@extend %background-center;
@extend %flex-center;
font-size: 1.25rem;
padding: 1.5rem;
box-sizing: border-box;
margin-right: 3rem;
z-index: 1;
}
.crew-list {
width: 16.875rem;
display: flex;
flex-direction: column;
.crew-item {
width: 11.625rem;
height: 4.75rem;
@extend %background-center;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 1.5rem;
box-sizing: border-box;
position: relative;
}
.crew-item-run {
background-image: url(../assets/img/crew-run.png);
}
.crew-item-stop {
background-image: url(../assets/img/crew-stop.png);
}
}
.crew-list2 {
position: relative;
left: -3.125rem;
height: 52.25rem;
}
.x-line {
// height: 0.0625rem;
height: 1px;
background-color: $line-color;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
.x-line-arrow {
width: 0.7rem;
height: auto;
}
}
.x-line-m {
width: 4.4rem;
left: -3.3rem;
}
.x-line-s {
width: 8.6rem;
left: -7.5rem;
}
.x-line-c {
width: 9.4rem;
left: -8.3rem;
}
.x-line-l {
width: 13.9rem;
left: -12.8rem;
}
}
.factory-list {
display: flex;
flex-direction: column;
justify-content: space-between;
padding-left: 10%;
.factory-item {
height: 30%;
display: flex;
align-items: center;
// justify-content: center;
position: relative;
}
.arrow {
width: 3rem;
height: auto;
position: absolute;
right: 8%;
cursor: pointer;
}
.y-line {
// width: 0.0625rem;
width: 1px;
background-color: $line-color;
position: absolute;
top: 0;
bottom: 0;
margin: auto;
left: 4.5rem;
}
}
.factory-sign {
display: flex;
align-items: center;
position: relative;
cursor: pointer;
.arrow {
width: 3rem;
height: auto;
cursor: pointer;
margin-right: 1rem;
}
.y-line {
// width: 0.0625rem;
width: 1px;
background-color: $line-color;
position: absolute;
left: 4.5rem;
}
}
}
}
</style>
<!-- 全厂监测 -->
<template>
<div class="factory-monitoring">
<div class="title">
{{$x_factory.plantName}} - 全部机组
</div>
<transition name="fade">
<el-carousel v-show="crewListShow" class="swiper" trigger="click" height="95%" :autoplay="false" indicator-position="outside" arrow="always">
<el-carousel-item class="swiper-item" v-for="page in Math.ceil(crewList.length / 4)" :key="page">
<div class="crew-item" v-for="item of crewList.slice(page * 4 - 4, page * 4)" :key="item.deviceId">
<img src="../assets/img/crew.png" @click="goPage(item)" />
<number-flipper class="number" unit="kW" :number="item.power" :point="1" />
<div class="name" @click="goPage(item)">{{item.deviceName}}</div>
<scroll-window class="roll-top">
<el-scrollbar class="win-list">
<div class="win-item" :style="{color: getWarnColor(v.state)}" v-for="v of item.vListW">{{v.cpName + ' ' + v.kksValueR + ' ' + v.cpUnit}}</div>
</el-scrollbar>
</scroll-window>
<scroll-window class="roll-bottom">
<el-scrollbar class="win-list">
<div class="win-item" :style="{color: getWarnColor(v.state)}" v-for="v of item.vList">{{v.cpName + ' ' + v.kksValueR + ' ' + v.cpUnit}}</div>
</el-scrollbar>
</scroll-window>
</div>
</el-carousel-item>
</el-carousel>
</transition>
</div>
</template>
<script>
import {
CONFIG,
FactoryMConfig
} from '@/common/const.js';
import {
getWarnColor,
getCharacterFromCrew
} from '@/common/util.js';
import NumberFlipper from '@/components/NumberFlipper'
import ScrollWindow from '@/components/ScrollWindow'
export default {
components: {
NumberFlipper,
ScrollWindow,
},
data() {
return {
crewList: null,
crewListShow: false,
getWarnColor: getWarnColor,
}
},
watch: {
$x_factory: {
immediate: true,
handler(val) {
this.crewListShow = false;
this.initData(); // 图表数据
// 延迟执行,因为动画
setTimeout(()=> {
this.crewListShow = true;
}, 200)
}
},
$x_allData: {
handler(val) {
this.initData();
}
}
},
methods: {
// 处理页面数据
initData() {
let crewList = [...this.$x_factory.plantDevices];
if(this.$x_allData.length) {
crewList.forEach(i => {
const crew = this.$x_allData.find(j => j.deviceId == i.deviceId);
// 框框
i.vList = FactoryMConfig.list.map(arr => {
for (let key of arr) {
let res = getCharacterFromCrew(crew, key);
if(res) return res;
}
return false;
}).filter(i => !!i);
// 报警
i.vListW = i.vList.filter(i => i.state == 3 || i.state == 4 || i.state == 5);
// 功率
i.power = 0;
FactoryMConfig.power.forEach(key => {
let res = getCharacterFromCrew(crew, key);
if(res) i.power = res.kksValueR;
})
})
}
this.crewList = crewList;
},
// 跳转到机组
goPage(item) {
this.$store.commit('crew', item);
this.$router.push('/CrewMonitoring');
},
},
}
</script>
<style lang="scss" scoped>
.fade-enter-active, .fade-leave-active {
transition: all .2s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
transform: translateX(-30px);
}
.factory-monitoring {
position: relative;
width: 100%;
height: 100%;
.title {
font-size: 1.625rem;
color: $color-cyan;
@extend %pos-center;
top: 0;
height: 4%;
}
.swiper {
@extend %pos-center;
bottom: 0;
width: 100%;
height: 96%;
/deep/.el-carousel__arrow {
width: 2.9375rem;
height: 4.5rem;
background-color: transparent;
color: transparent;
top: 40%;
}
/deep/.el-carousel__arrow--left {
background-image: url(../assets/img/arrow-l.png);
@extend %background-center;
left: 2rem;
}
/deep/.el-carousel__arrow--right {
background-image: url(../assets/img/arrow-r.png);
@extend %background-center;
right: 2rem;
}
/deep/.el-carousel__button {
width: 1.5rem;
height: 0.5rem;
border-radius: 1.125rem;
}
}
.swiper-item {
display: flex;
// justify-content: space-between;
align-items: center;
padding: 0 5rem;
box-sizing: border-box;
}
.crew-item {
width: 25%;
height: 100%;
position: relative;
img {
width: 50%;
height: auto;
@extend %pos-center;
top: 0;
bottom: 0;
cursor: pointer;
}
.name {
width: 8rem;
height: 2.25rem;
padding: 0 0.5rem;
border-radius: 4px;
background: rgba(0, 0, 0, 0.20);
border: 1px dashed $color-cyan;
@extend %flex-center;
@extend %pos-center;
top: 0;
bottom: 0;
transform: translateY(6rem);
cursor: pointer;
}
.number {
@extend %pos-center;
top: 0.5rem;
transform: scale(0.8);
}
.roll-top{
@extend %pos-center;
width: 80%;
height: 21vh;
top: 4rem;
}
.roll-bottom{
@extend %pos-center;
width: 80%;
height: 27vh;
bottom: 0;
}
.win-list{
height: 100%;
line-height: 1.5rem;
}
/deep/.el-scrollbar__wrap {
overflow-x: hidden;
}
}
}
</style>
<!-- 全厂监测 -->
<template>
<div class="factory-monitoring">
<div class="title">
{{$x_factory.plantName}} - 全部机组
</div>
<transition name="fade">
<div class="crew-list" v-show="crewListShow">
<div class="crew-item" v-for="item of crewList" :key="item.deviceId">
<img src="../assets/img/crew.png" />
<div class="name" @click="goPage(item)">{{item.deviceName}}</div>
<number-flipper class="number" unit="kW" :number="item.power" :point="1" />
<scroll-window class="roll-left">
<el-scrollbar class="win-list">
<div class="win-item" :style="{color: getWarnColor(v.state)}" v-for="v of item.vList">{{v.cpName + ' ' + v.kksValueR + ' ' + v.cpUnit}}</div>
</el-scrollbar>
</scroll-window>
</div>
</div>
</transition>
<!--
<div class="power">
<number-flipper title="全厂发电功率" unit="MW" :number="8021" :point="1" />
</div>
<el-carousel class="swiper" trigger="click" height="90%" :autoplay="false" indicator-position="outside" arrow="always">
<el-carousel-item class="swiper-item" v-for="page in Math.ceil($x_factory.plantDevices.length / 4)" :key="page">
<div class="crew-item" v-for="item of $x_factory.plantDevices.slice(page * 4 - 4, page * 4)" :key="item.deviceId">
<img src="../assets/img/crew.png" @click.stop="showWin(item, $event)" />
<div class="name" @click="goPage(item)">{{item.deviceName}}</div>
<number-flipper class="number" title="有功功率" unit="kW" :number="102.1" :point="1" />
</div>
</el-carousel-item>
</el-carousel>
<div class="mask" v-if="rollWin.show" @click="closeWin">
<scroll-window class="roll-win" :style="{top: 'calc(' + rollWin.top + 'px - 15rem)', left: rollWin.left + 'px'}" direction="1" @click.native.stop="" />
</div> -->
</div>
</template>
<script>
import {
CONFIG,
FactoryMConfig
} from '@/common/const.js';
import {
getWarnColor,
getCharacterFromCrew
} from '@/common/util.js';
import NumberFlipper from '@/components/NumberFlipper'
import ScrollWindow from '@/components/ScrollWindow'
export default {
components: {
NumberFlipper,
ScrollWindow,
},
data() {
return {
crewList: null,
crewListShow: false,
// 小窗口
rollWin: {
show: false,
item: null,
top: null,
left: null,
},
getWarnColor: getWarnColor,
}
},
watch: {
$x_factory: {
immediate: true,
handler(val) {
this.crewListShow = false;
this.initData(); // 图表数据
// 延迟执行,因为动画
setTimeout(()=> {
this.crewListShow = true;
}, 200)
}
},
$x_allData: {
handler(val) {
this.initData();
}
}
},
methods: {
// 处理页面数据
initData() {
let crewList = [...this.$x_factory.plantDevices];
if(this.$x_allData.length) {
crewList.forEach(i => {
const crew = this.$x_allData.find(j => j.deviceId == i.deviceId);
// 框框
i.vList = FactoryMConfig.list.map(arr => {
for (let key of arr) {
let res = getCharacterFromCrew(crew, key);
if(res) return res;
}
return false;
}).filter(i => !!i);
// 功率
i.power = 0;
FactoryMConfig.power.forEach(key => {
let res = getCharacterFromCrew(crew, key);
if(res) i.power = res.kksValueR;
})
})
}
this.crewList = crewList;
},
// 跳转到机组
goPage(item) {
this.$store.commit('crew', item);
this.$router.push('/CrewMonitoring');
},
// 显示小窗口
showWin(item, e) {
this.rollWin.item = item;
this.rollWin.show = true;
this.rollWin.left = e.clientX + 10;
this.rollWin.top = e.clientY - 10;
},
// 关闭小窗口
closeWin() {
this.rollWin.show = false;
},
},
}
</script>
<style lang="scss" scoped>
.fade-enter-active, .fade-leave-active {
transition: all .2s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
transform: translateX(-30px);
}
.factory-monitoring {
position: relative;
width: 100%;
height: 100%;
.title {
font-size: 1.625rem;
color: $color-cyan;
@extend %pos-center;
top: 0;
height: 4%;
}
.crew-list {
@extend %pos-center;
bottom: 0;
height: 96%;
padding: 0 2% 1% 2%;
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
.crew-item {
position: relative;
width: 20%;
height: 50%;
img {
width: auto;
height: 70%;
@extend %pos-center;
bottom: 2%;
}
.name {
width: 6rem;
height: 2.25rem;
border-radius: 4px;
background: rgba(0, 0, 0, 0.20);
border: 1px dashed $color-cyan;
@extend %flex-center;
position: absolute;
top: 7%;
left: 5%;
cursor: pointer;
}
.number{
position: absolute;
top: 5%;
right: 0;
transform: scale(0.7);
}
.roll-left {
width: 90%;
height: 80%;
@extend %pos-center;
bottom: 0;
.win-list{
height: 100%;
line-height: 2rem;
}
/deep/.el-scrollbar__wrap {
overflow-x: hidden;
}
}
}
}
// .power {
// @extend %pos-center;
// top: 3.125rem;
// }
// .swiper {
// @extend %pos-center;
// bottom: 0.625rem;
// width: 100%;
// height: 80%;
// /deep/.el-carousel__arrow {
// width: 2.9375rem;
// height: 4.5rem;
// background-color: transparent;
// color: transparent;
// top: 40%;
// }
// /deep/.el-carousel__arrow--left {
// background-image: url(../assets/img/arrow-l.png);
// @extend %background-center;
// left: 3.75rem;
// }
// /deep/.el-carousel__arrow--right {
// background-image: url(../assets/img/arrow-r.png);
// @extend %background-center;
// right: 3.75rem;
// }
// /deep/.el-carousel__button {
// width: 1.5rem;
// height: 0.5rem;
// border-radius: 1.125rem;
// }
// }
// .swiper-item {
// display: flex;
// // justify-content: space-between;
// align-items: center;
// padding: 0 9.375rem;
// box-sizing: border-box;
// }
// .crew-item {
// width: 25%;
// padding: 1%;
// box-sizing: border-box;
// position: relative;
// .warn {
// position: relative;
// left: -1.875rem;
// height: 7.875rem;
// background-size: 100% 100%;
// display: flex;
// align-items: center;
// padding-left: 12.5rem;
// .txt {
// margin-right: 0.9375rem;
// }
// }
// .warn-bg1 {
// background-image: url(../assets/img/warn-1.png);
// }
// .warn-bg2 {
// background-image: url(../assets/img/warn-2.png);
// }
// .warn-bg3 {
// background-image: url(../assets/img/warn-3.png);
// }
// img {
// width: 80%;
// height: auto;
// margin-bottom: 0.625rem;
// }
// .name {
// width: 6rem;
// height: 2.25rem;
// border-radius: 4px;
// background: rgba(0, 0, 0, 0.20);
// border: 1px dashed $color-cyan;
// @extend %flex-center;
// @extend %pos-center;
// bottom: 10.625rem;
// cursor: pointer;
// }
// .number {
// transform: scale(0.9);
// }
// }
// .mask{
// width: 100vw;
// height: 100vh;
// z-index: 2;
// position: fixed;
// top: 0;
// left: 0;
// }
// .roll-win{
// width: 13rem;
// height: 15rem;
// position: fixed;
// }
}
</style>
<!-- 布局页 -->
<template>
<div class="index">
<img class="left-border" src="../assets/img/border-left.png" />
<img class="right-border" src="../assets/img/border-right.png" />
<div class="title">
<span class="txt">{{title}}</span>
</div>
<user-info class="user-info" />
<div class="main">
<div class="nav">
<div class="block">
<div class="btn left-bg" :class="$route.path == '/FactoryMonitoring/index' ? 'left-bg2' : ''">
<el-select v-model="factoryValue" :popper-append-to-body="false" @change="changeFactory">
<el-option label="全部电厂" :value="0" />
<el-option v-for="item of $x_factoryList" :key="item.plantId" :label="item.plantName" :value="item.plantId" @click.native="clickFactory(item.plantId)" />
</el-select>
</div>
<div class="btn left-bg left-pos2" :class="$route.path == '/CrewMonitoring/index' ? 'left-bg2' : ''">
<el-select v-model="crewValue" :popper-append-to-body="false" @change="changeCrew">
<el-option label="全部机组" :value="0" />
<el-option v-for="item of $x_factory.plantDevices" :key="item.deviceId" :label="item.deviceName" :value="item.deviceId" @click.native="clickCrew(item.deviceId)" />
</el-select>
</div>
<div class="btn left-bg left-pos3" :class="$route.path == '/TrendAnalysis/index' ? 'left-bg2' : ''" @click="goPage('/TrendAnalysis')">趋势分析</div>
</div>
<div class="block">
<div class="btn right-bg right-pos2" :class="$route.path == '/AlarmRecord/index' ? 'right-bg2' : ''" @click="goPage('/AlarmRecord')">报警记录</div>
<div class="btn right-bg" :class="$route.path == '/UserManage/index' ? 'right-bg2' : ''" @click="goPage('/UserManage')">用户管理</div>
</div>
</div>
<div class="router-view">
<transition name="fade-transform" mode="out-in">
<router-view />
</transition>
</div>
</div>
<div v-show="!dialogShow" class="global-circle" v-drag @click="showDialog">
报警监控
</div>
<!-- <el-dialog class="dialog-index global" title="报警监控" top="10vh" width="80%" :visible.sync="dialogShow" destroy-on-close center> -->
<div v-show="dialogShow" class="dialog-index global" v-drag>
<div class="dialog-title">
<div class="left"></div>
<div class="center"> 报警监控 </div>
<div class="right">
<i class="el-icon-close" @click="dialogShow = false" />
</div>
</div>
<div class="dialog-main">
<div class="tabs">
<div class="tab" :class="tabIndex == 0 ? 'tab-active' : ''" @click="changeTab(0)"><i class="el-icon-warning" style="color: #E53B4C;" />报警参数</div>
<div class="tab" :class="tabIndex == 1 ? 'tab-active' : ''" @click="changeTab(1)"><i class="el-icon-remove" style="color: #ccc;" />无效参数</div>
<!-- <div class="tab" :class="tabIndex == 2 ? 'tab-active' : ''" @click="changeTab(2)"><i class="el-icon-s-help" style="color: #00aaff;" />全部参数</div> -->
</div>
<div class="table">
<el-table :data="tableData" height="100%" v-loading="tableLoading" :element-loading-background="loadingColor">
<el-table-column type="index" label="序号" min-width="4%" align="center"></el-table-column>
<el-table-column prop="deviceName" label="机组" min-width="20%" align="center"></el-table-column>
<el-table-column prop="cpName" label="监测参数" min-width="32%" align="center"></el-table-column>
<el-table-column prop="kksValueR" label="测值" min-width="18%" align="center"></el-table-column>
<el-table-column prop="cpUnit" label="单位" min-width="14%" align="center"></el-table-column>
<el-table-column prop="state" label="状态" min-width="12%" align="center">
<template v-slot="scope">
<div class="table-tip">
<i class="table-tip-circle" :style="{'background-image': 'linear-gradient(to bottom, ' + getColors(scope.row.state)}" />
</div>
</template>
</el-table-column>
</el-table>
</div>
<div class="tips">
<div class="tip-item" v-for="item in WarningConfig">
<i class="tip-circle" :style="{'background-image': 'linear-gradient(to bottom, ' + item.color + ',' + item.color2}" />
<span class="tip-txt">{{item.name}}</span>
</div>
</div>
</div>
</div>
<!-- </el-dialog> -->
</div>
</template>
<script>
import {
CONFIG,
ChartStyle,
WarningConfig
} from '@/common/const.js'
import {
getWarnItem,
getDeviceNameById,
randomData
} from '@/common/util.js'
import UserInfo from '@/components/UserInfo'
import User from '@/api/user.js';
export default {
components: {
UserInfo
},
data() {
return {
loadingColor: ChartStyle.loadingColor,
title: CONFIG.TITLE,
factoryValue: 0,
crewValue: 0,
socket: null, // socket
dialogShow: false,
tabIndex: 0,
tableDataAll: [[], [], []],
tableData: [],
tableLoading: false,
WarningConfig: WarningConfig,
};
},
watch: {
$x_factory: {
immediate: true,
handler(val) {
if(!val) return;
this.factoryValue = val.plantId;
this.$store.commit('allData', []);
this.closeSocket();
this.initSocket();
}
},
$x_crew: {
immediate: true,
handler(val) {
this.crewValue = val?.deviceId || 0
}
}
},
beforeDestroy() {
this.closeSocket();
},
methods: {
// 初始化socket
initSocket() {
try{
this.socket = new WebSocket('ws://' + CONFIG.SOCKET + '/all/measure/point/' + this.$x_factory.plantId, [this.$x_user.token.split(' ')[1]]);
this.socket.onopen = () => console.log('socket_all success');
this.socket.onerror = e => console.log('socket_all error', e);
this.socket.onmessage = msg => {
try{
const data = JSON.parse(msg.data);
console.log('socket_all data', data);
if (!data) return;
this.$store.commit('allData', data);
if(this.dialogShow) this.initTableData();
// this.initTableData();
}catch(e){
console.log(e);
}
}
}catch(e){
console.log(e);
User.getRoleList(); // 捕获错误,然后发送一个请求,因为可能是token导致的socket连接错误,发送请求后如果是token过期,会自动注销用户
}
},
// 关闭socket
closeSocket() {
if (this.socket) {
this.socket.close();
this.socket = null;
}
},
// 页面跳转
goPage(path) {
if (this.$route.path.indexOf(path) != -1) return;
this.$router.push(path);
},
// 改变电厂
changeFactory(val) {
if (!val) this.$router.push('/factory');
else {
let item = this.$x_factoryList.find(i => i.plantId === val)
let endTime = Date.now() + 3600 * 1000 * 24; // 24h后过期
localStorage.setItem('electric_factory', JSON.stringify({
data: item,
endTime
}));
this.$store.commit('factory', item);
this.$store.commit('crew', null);
this.crewValue = 0;
if (this.$route.path !== '/FactoryMonitoring/index') this.$router.push('/FactoryMonitoring');
}
},
// 改变机组
changeCrew(val) {
if (!val) {
this.$store.commit('crew', null);
if (this.$route.path !== '/FactoryMonitoring/index') this.$router.push('/FactoryMonitoring');
} else {
let item = this.$x_factory.plantDevices.find(i => i.deviceId === val)
this.$store.commit('crew', item);
if (this.$route.path !== '/CrewMonitoring/index') this.$router.push('/CrewMonitoring');
}
},
// 点击电厂,如果当前页面不是全厂页,并且点击了同样的电厂,则跳转到电厂页(因为不会触发changeFactory
clickFactory(id) {
if(id == this.factoryValue && this.$route.path !== '/FactoryMonitoring/index') this.$router.push('/FactoryMonitoring');
},
// 点击机组,同上
clickCrew(id) {
if(id == this.crewValue && this.$route.path !== '/CrewMonitoring/index') this.$router.push('/CrewMonitoring');
},
// 打开弹窗
showDialog() {
this.dialogShow = true;
this.initTableData();
},
// 切换标签
changeTab(i) {
this.tabIndex = i;
this.tableData = this.tableDataAll[i];
},
// 处理表格数据
initTableData() {
let tableData1 = [], tableData2 = [], tableData3 = [], warnStateMap = {3:true,4:true,5:true};
for (let i = 0; i < this.$x_allData.length; i++) {
for (let j = 0; j < this.$x_allData[i].tSdb.length; j++) {
const item = this.$x_allData[i].tSdb[j]; // 当前测点
if(!item.kksValue) continue; // 如果没有查出值,则跳过
if(!item.kksValueR) {
let str = item.kksValue.value.toString();
item.state = parseInt(str.substr(-2));
item.kksValueR = parseFloat(str.substr(0, str.length - 2));
}
if(!item.deviceName) item.deviceName = getDeviceNameById(item.deviceId);
// tableData3.push(item); // 全部
if(warnStateMap[item.state]) tableData1.push(item); // 报警
if(item.state == 2) tableData2.push(item); // 无效
}
}
// 排序
tableData1.sort((a, b) => a.kksCode.localeCompare(b.kksCode));
tableData2.sort((a, b) => a.kksCode.localeCompare(b.kksCode));
// tableData3.sort((a, b) => a.kksCode.localeCompare(b.kksCode));
this.tableDataAll = [tableData1, tableData2, tableData3];
this.tableData = this.tableDataAll[this.tabIndex];
// if(this.tableData1.length) this.dialogShow = true; // 如果有报警,则弹出
},
// 处理渐变色
getColors(state) {
const item = getWarnItem(state);
if(!item) return '#fff, #fff';
return item.color + ',' + item.color2;
},
},
directives: {
// 元素拖拽指令
drag(el, binding) {
el.onmousedown = e => {
el.style.cursor = 'move';
let domW = el.offsetWidth;
let domH = el.offsetHeight;
let disX = e.clientX - el.offsetLeft;
let disY = e.clientY - el.offsetTop;
document.onmousemove = e => {
let left = e.clientX - disX;
let top = e.clientY - disY;
if (top > document.body.clientHeight - domH) top = document.body.clientHeight - domH;
if (left > document.body.clientWidth - domW) left = document.body.clientWidth - domW;
if (top < 0) top = 0;
if (left < 0) left = 0;
el.style.left = left + 'px';
el.style.top = top + 'px';
}
document.onmouseup = e => {
el.style.cursor = 'pointer';
document.onmousemove = document.onmouseup = null;
}
return false; // 阻止冒泡
}
}
},
};
</script>
<style lang="scss" scoped>
.index {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
background-image: url(../assets/img/bg.png);
@extend %background-center;
.left-border {
position: absolute;
left: 0;
height: 100%;
}
.right-border {
position: absolute;
right: 0;
height: 100%;
}
.title {
@extend %pos-center;
width: 58.75rem;
height: 4.875rem;
background-image: url(../assets/img/title-bg.png);
@extend %background-center;
.txt {
font-size: 2.5rem;
font-weight: bold;
letter-spacing: 0.25rem;
@extend %txt-gradients;
}
}
.user-info {
width: calc((100% - 58.75rem) / 2);
position: absolute;
top: 0.8rem;
right: 0;
}
.main {
@extend %bg-position;
overflow: hidden;
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all .1s;
}
.fade-transform-enter {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
.nav {
display: flex;
justify-content: space-between;
height: 3.125rem;
margin: 0 4.0625rem;
.block {
display: flex;
.btn {
width: 11.0625rem;
height: 2.9375rem;
background-size: 100% 100%;
@extend %flex-center;
cursor: pointer;
font-size: 1rem;
text-shadow: 0 2px 8px rgba(78, 97, 214, 0.50);
/deep/ .el-select {
width: 7.5rem;
}
/deep/ .el-input__inner {
color: #fff;
font-size: 1rem;
text-shadow: 0 2px 8px rgba(78, 97, 214, 0.50);
border: none;
background-color: transparent;
}
/deep/ .el-select .el-input .el-select__caret {
font-size: 1.2rem;
color: #fff;
}
/deep/ .el-icon-arrow-up:before {
content: "\e78f";
}
/deep/ .el-select-dropdown {
// background-color: rgba(147,252,255,0.10);
background-color: rgba(20, 43, 83, 1.0);
border: 1px solid #1B529C;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.50);
}
/deep/ .el-select-dropdown__item {
color: #eee;
font-size: 0.875rem;
text-shadow: none;
font-weight: 200;
}
/deep/ .el-select-dropdown__item.selected {
color: $color-cyan;
}
/deep/ .el-select-dropdown__item.hover,
/deep/ .el-select-dropdown__item:hover {
background-color: rgba(147, 252, 255, 0.20);
}
/deep/ .popper__arrow {
border-bottom-color: #1B529C;
}
/deep/ .popper__arrow:after {
border-bottom-color: transparent;
}
}
.left-bg {
background-image: url(../assets/img/btn-l0.png);
}
.left-bg2 {
background-image: url(../assets/img/btn-l1.png);
}
.right-bg {
background-image: url(../assets/img/btn-r0.png);
}
.right-bg2 {
background-image: url(../assets/img/btn-r1.png);
}
.left-pos2 {
position: relative;
left: -1.875rem;
}
.left-pos3 {
position: relative;
left: -3.75rem;
}
.right-pos2 {
position: relative;
right: -1.875rem;
}
}
}
.router-view {
height: calc(100% - 3.125rem);
}
}
}
.global-circle {
width: 5rem;
height: 5rem;
z-index: 10;
position: fixed;
right: 2rem;
bottom: 2rem;
background-image: linear-gradient(to bottom, rgba(0, 209, 255, 0.5), rgba(0, 85, 255, 0.5), rgba(0, 0, 255, 0.5));
cursor: pointer;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
font-size: 1.2rem;
font-weight: 700;
padding: 0.8rem;
box-sizing: border-box;
}
.dialog-index {
/deep/.el-dialog__body{
padding-top: 0;
}
width: 35rem;
height: 40rem;
z-index: 10;
position: fixed;
right: 2rem;
bottom: 2rem;
background-color: rgba(10, 23, 55, 1);
border: 1px solid rgba(9, 77, 148, 0.5);
padding: 1.5rem;
padding-top: 0;
box-sizing: border-box;
cursor: pointer;
.dialog-title {
height: 3rem;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
.center {
@extend %pos-center;
}
.right {
z-index: 1;
}
}
.dialog-main {
height: calc(100% - 3rem);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
background-color: rgba($color: $color-blue, $alpha: 0.1);
color: #eee;
.tabs{
height: 3rem;
display: flex;
align-items: center;
border-bottom: 1px solid #000;
box-sizing: border-box;
.tab {
font-size: 0.875rem;
cursor: pointer;
height: 100%;
@extend %flex-center;
padding: 0 1rem;
}
.tab-active {
background-color: $color-deep-blue;
}
i {
font-size: 1.2rem;
margin-right: 0.5rem;
}
}
.table {
height: calc(100% - 6rem);
background-color: rgba($color: $color-deep-blue, $alpha: 0.3);
::v-deep .el-table th {
font-size: 0.875rem;
}
.table-tip{
@extend %flex-center;
.table-tip-circle {
width: 1rem;
height: 1rem;
border-radius: 50%;
}
}
}
.tips{
height: 3rem;
display: flex;
align-items: center;
padding: 0 0.5rem;
box-sizing: border-box;
.tip-item {
height: 100%;
margin: 0 0.5rem;
display: flex;
align-items: center;
.tip-circle {
width: 1rem;
height: 1rem;
border-radius: 50%;
margin-right: 0.5rem;
}
.tip-txt {
font-size: 0.75rem;
}
}
}
}
}
</style>
<!-- 登录页 -->
<template>
<div class="login">
<div class="title">{{title}}</div>
<div class="main">
<div class="line">
<div class="circle-o">
<div class="circle-i"></div>
</div>
<span class="line-title">用户登录</span>
</div>
<div class="line input">
<img src="../assets/img/icon-user.png" />
<el-input v-model="user.username" placeholder="请输入账号信息" @keyup.enter.native="login" />
</div>
<div class="line input">
<img src="../assets/img/icon-pass.png" />
<el-input v-model="user.password" placeholder="请输入账号密码" show-password @keyup.enter.native="login" />
<div class="reset" @click="resetForm">
<img src="../assets/img/icon-reset.png" />
<span>重置</span>
</div>
</div>
<div class="line" v-loading="loading" :element-loading-background="loadingColor">
<el-button class="line-btn" type="primary" @click="login">登录</el-button>
</div>
</div>
</div>
</template>
<script>
import {
CONFIG,
ChartStyle
} from '@/common/const.js'
import User from '@/api/user.js';
export default {
data() {
return {
loading: false,
loadingColor: ChartStyle.loadingColor,
title: CONFIG.TITLE,
user: {
username: '',
password: ''
},
};
},
methods: {
// 重置表单
resetForm() {
this.user = {
username: '',
password: ''
}
},
// 登录
async login() {
if (!this.user.username || !this.user.password) {
this.$message.warning('请输入用户信息!');
return;
}
this.loading = true;
try {
let res = await User.login(this.user.username, this.user.password);
res = JSON.parse(res);
this.$message.success('登录成功!');
this.user.permission = res?.role?.permission || '';
this.$store.commit('user', null); // 清除用户,这里只需要清除,在路由跳转时会将用户从本地添加到store里
// 保存信息,用于下一次刷新页面时直接进入,不再登录
let endTime = Date.now() + 3600 * 1000 * 24; // 24h后过期
localStorage.setItem('electric_user', JSON.stringify({
data: this.user,
endTime
}));
this.$router.push('/factory', () => {}, () => {}); // 加后面两个空回调方法,是为了解决一个router的报错
} catch (e) {
console.log(e);
}
this.loading = false;
}
}
};
</script>
<style lang="scss" scoped>
.login {
width: 100%;
height: 100%;
overflow: hidden;
background-image: url(../assets/img/bg.png);
@extend %background-center;
@extend %flex-center;
flex-direction: column;
.title {
font-size: 3rem;
font-weight: bold;
letter-spacing: 0.3rem;
@extend %txt-gradients;
}
.main {
width: 58.6875rem;
height: 39.625rem;
background-image: url(../assets/img/login-border.png);
@extend %background-center;
box-sizing: border-box;
padding: 9.375rem 9.375rem 0 9.375rem;
.line {
height: 4rem;
margin-bottom: 2rem;
display: flex;
align-items: center;
.line-title {
font-size: 1.75rem;
font-weight: bold;
letter-spacing: 0.175rem;
@extend %txt-gradients;
}
.line-btn {
width: 100%;
height: 100%;
border-radius: 2px;
font-size: 1.25rem;
letter-spacing: 0.125rem;
background-color: $color-blue;
border: none;
}
}
.line:last-of-type {
margin-top: 4.375rem;
}
.circle-o {
width: 1.25rem;
height: 1.25rem;
border-radius: 50px;
background-color: rgba($color: $color-cyan, $alpha: 0.2);
@extend %flex-center;
margin-right: 1rem;
.circle-i {
width: 0.625rem;
height: 0.625rem;
border-radius: 50px;
background-color: $color-cyan;
opacity: 0.8;
}
}
.input {
position: relative;
border: 1px solid rgba($color: $color-cyan, $alpha: 0.5);
border-radius: 2px;
padding: 0 1.5rem;
box-sizing: border-box;
background-color: rgba($color: $color-cyan, $alpha: 0.1);
img {
width: 1.5rem;
height: 1.5rem;
}
::v-deep .el-input__inner {
background-color: transparent;
border: none;
color: #fff;
}
::v-deep .el-input__inner::placeholder {
color: rgba($color: #fff, $alpha: 0.6);
letter-spacing: 0.1rem;
}
.reset {
position: absolute;
top: 5rem;
right: 0;
font-size: 0.875rem;
letter-spacing: 0.1rem;
display: flex;
align-items: center;
cursor: pointer;
img {
width: 0.875rem;
height: 0.875rem;
margin-right: 0.625rem;
}
}
}
}
}
</style>
<!-- 角色管理 -->
<template>
<div class="role">
<div class="header">
<div class="left">
<el-button class="btn" @click="showForm(0)">+ 新增</el-button>
</div>
<div class="center">
{{$x_factory.plantName}} - 角色管理
</div>
<div class="right">
<i class="el-icon-close" @click="back" />
</div>
</div>
<div class="main global">
<div class="table">
<el-table ref="table" :data="tableData" height="100%" v-loading="tableLoading" :element-loading-background="loadingColor">
<el-table-column prop="name" label="角色名" align="center"></el-table-column>
<el-table-column prop="permissionT" label="权限" align="center">
</el-table-column>
<el-table-column label="操作" align="center">
<template v-slot="scope">
<span class="table-btn" @click="showForm(scope.row)">编辑</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
<el-dialog class="dialog global" width="40%" :title="formTitle" :visible.sync="formShow" destroy-on-close
v-loading="formLoading" :element-loading-background="loadingColor">
<el-form ref="form" :model="formData" :rules="formRules" label-width="100px" size="small">
<el-form-item label="角色名" prop="name">
<el-input v-model.trim="formData.name" placeholder="请填写角色名" />
</el-form-item>
<el-form-item label="角色权限" prop="permissionPage">
<el-checkbox class="checkbox" :indeterminate="formData.isIndeterminatePage" v-model="formData.checkAllPage" @change="allCheck($event, 1)">全选所有页面(至少选择一个)</el-checkbox>
<el-checkbox-group class="checkgroup" v-model="formData.permissionPage" @change="checkHandler($event, 1)">
<el-checkbox v-for="(route, key) in RouteMap" :label="key" :key="key">{{route}}</el-checkbox>
</el-checkbox-group>
<el-checkbox class="checkbox" :indeterminate="formData.isIndeterminateBtn" v-model="formData.checkAllBtn" @change="allCheck($event, 2)">全选所有功能</el-checkbox>
<el-checkbox-group class="checkgroup" v-model="formData.permissionBtn" @change="checkHandler($event, 2)">
<el-checkbox v-for="(btn, key) in ButtonMap" :label="key" :key="key">{{btn}}</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-form>
<div class="btn-list">
<el-button class="btn-active" type="primary" @click="onSubmit">保存</el-button>
<el-button class="btn-default" @click="formShow = false">取消</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
ChartStyle,
RouteMap,
ButtonMap
} from '@/common/const.js';
import User from '@/api/user.js';
export default {
components: {},
data() {
return {
tableLoading: false,
formLoading: false,
loadingColor: ChartStyle.loadingColor,
RouteMap: RouteMap,
ButtonMap: ButtonMap,
RouteMapValue: Object.keys(RouteMap),
ButtonMapValue: Object.keys(ButtonMap),
tableData: null,
formShow: false,
formTitle: '',
formData: null,
formRules: {
name: [{
required: true,
message: '请输入角色名称',
trigger: 'blur'
}],
permissionPage: [{
required: true,
message: '至少选择一个页面权限',
trigger: 'blur'
}],
},
};
},
created() {
this.formReset();
this.getList();
},
methods: {
// 返回用户管理
back() {
this.$router.push('/UserManage');
},
// 重置表单
formReset() {
this.formData = {
name: '',
permission: '',
permissionPage: [],
permissionBtn: [],
checkAllPage: false,
checkAllBtn: false,
isIndeterminatePage: false,
isIndeterminateBtn: false,
};
},
// 获取角色列表
async getList() {
this.tableLoading = true;
try {
let res = await User.getRoleList();
if(!res) res = [];
res.forEach(i => i.permissionT = i.permission?.toString().split(',').map(j => this.RouteMap[j] || this.ButtonMap[j]).join(',') || '' ); // 中文版
this.tableData = res;
} catch (e) {
console.log(e);
}
this.tableLoading = false;
},
// 新增编辑
showForm(row) {
// 编辑
if (row) {
this.formTitle = '编辑角色';
// 回显赋值
let permissionPage = [],
permissionBtn = [];
if (row.permission) {
for (let item of row.permission.split(',')) {
if (item in RouteMap) permissionPage.push(item);
if (item in ButtonMap) permissionBtn.push(item);
}
}
this.formData = {
roleId: row.roleId,
name: row.name,
permission: row.permission,
permissionPage,
permissionBtn,
checkAllPage: permissionPage.length === this.RouteMapValue.length,
checkAllBtn: permissionBtn.length === this.ButtonMapValue.length,
isIndeterminatePage: permissionPage.length > 0 && permissionPage.length < this.RouteMapValue.length,
isIndeterminateBtn: permissionBtn.length > 0 && permissionBtn.length < this.ButtonMapValue.length,
};
// 新增
} else {
this.formTitle = '新增角色';
this.formReset(); // 清空表单
}
this.formShow = true;
},
// 全选,type - 1为页面,2为按钮
allCheck(val, type) {
if (type == 1) {
this.formData.permissionPage = val ? this.RouteMapValue : [];
this.formData.isIndeterminatePage = false;
}
if (type == 2) {
this.formData.permissionBtn = val ? this.ButtonMapValue : [];
this.formData.isIndeterminateBtn = false;
}
},
// 单选,type - 1为页面,2为按钮
checkHandler(val, type) {
if (type == 1) {
let isAllChecked = val.length === this.RouteMapValue.length;
this.formData.checkAllPage = isAllChecked;
this.formData.isIndeterminatePage = val.length > 0 && !isAllChecked;
}
if (type == 2) {
let isAllChecked = val.length === this.ButtonMapValue.length;
this.formData.checkAllBtn = isAllChecked;
this.formData.isIndeterminateBtn = val.length > 0 && !isAllChecked;
}
},
// 提交表单
onSubmit() {
this.$refs.form.validate(async valid => {
if (!valid) return;
if (this.formData.roleId == 1) {
this.$message.error('超级管理员无法编辑!')
return;
}
this.formData.permission = this.formData.permissionPage.concat(this.formData.permissionBtn).join(',');
this.formLoading = true;
try {
await User.newOrUpdateRole(this.formData);
this.$message.success('操作成功');
this.formShow = false;
this.getList();
} catch (e) {
console.log(e);
}
this.formLoading = false;
});
}
}
};
</script>
<style lang="scss" scoped>
.role {
height: 100%;
padding: 1.875rem 3.125rem;
box-sizing: border-box;
.header {
height: 4.25rem;
background-image: linear-gradient(270deg, rgba(197, 254, 255, 0.10) 0%, rgba(147, 252, 255, 0.50) 52%, rgba(147, 252, 255, 0.10) 100%);
padding: 0 1.875rem;
box-sizing: border-box;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
.left {
display: flex;
z-index: 1;
.btn {
@extend %btn-default;
}
.btn:hover {
@extend %btn-hover;
}
}
.center {
@extend %pos-center;
font-size: 1.625rem;
letter-spacing: 0.1625rem;
text-shadow: 0 2px 8px rgba(78, 97, 214, 0.50);
}
.right {
z-index: 1;
i {
font-size: 1.6rem;
}
}
}
.main {
height: calc(100% - 4.25rem);
background: rgba(147, 252, 255, 0.08);
padding: 1.875rem 1.875rem 0;
box-sizing: border-box;
.table {
height: calc(100% - 40px);
.table-btn {
color: $color-cyan;
cursor: pointer;
}
}
.page {
margin: 0.3125rem 0;
}
}
.dialog {
/deep/ .el-dialog__body{
text-align: left;
}
/deep/ .el-form-item__label {
color: #fff;
}
.checkbox{
width: 100%;
}
.checkgroup{
flex-flow: wrap;
}
::v-deep .el-checkbox{
color: #bebebe;
}
::v-deep .el-checkbox__label{
line-height: 2rem;
}
::v-deep .el-checkbox__input.is-checked+.el-checkbox__label{
color: #fff;
}
.btn-list {
@extend %flex-center;
margin-top: 2rem;
}
.btn-active {
@extend %btn-active;
}
.btn-default {
@extend %btn-default;
}
}
}
</style>
<!-- 趋势分析 -->
<template>
<div class="trend-analysis">
<div class="header">
<div class="left">
<el-button class="btn" @click="goPage">机组监测</el-button>
<!-- <el-button class="btn">趋势分析</el-button> -->
<el-button class="btn" @click="exportData">导出数据</el-button>
</div>
<div class="center">
{{$x_factory.plantName}} - 趋势分析
</div>
<div class="right">
<!-- <i class="el-icon-close" @click="back" /> -->
</div>
</div>
<div class="main">
<div class="sidebar">
<sidebar @changeMenu="changeMenu" />
</div>
<div class="charts">
<chart-btn-list class="btns" :checkList="param.checkList" @changeBtn="changeChart" @changeAxis="changeAxis" />
<div class="chart" v-loading="loadingChart" :element-loading-background="loadingColor">
<chart-line v-if="dataTrend" ref="ChartTrend" id="ChartTrend" :chartData="dataTrend" />
</div>
</div>
</div>
</div>
</template>
<script>
import {
ChartStyle
} from '@/common/const.js';
import {
dateFormat,
exportDataToFile
} from '@/common/util.js';
import Sidebar from '@/components/Sidebar';
import ChartBtnList from '@/components/Charts/ChartBtnList';
import ChartLine from '@/components/Charts/ChartLine';
import API from '@/api/factory.js';
export default {
components: {
Sidebar,
ChartBtnList,
ChartLine
},
data() {
return {
loadingChart: false,
loadingColor: ChartStyle.loadingColor,
dataTrend: null,
// 按钮参数
param: {
pointValue: [],
checkList: [], // 菜单里选择的列表,区别于menuCheckList,这个包含的信息更多
chartBtn: null
},
requestIdChart: 0, // main内的图表请求记录,每次请求会+1,返回时,只处理最后一个请求,用于同时间发送多个请求时,抛弃调之前请求的结果
};
},
methods: {
// 跳转到机组监测
goPage() {
if(this.$x_crew) this.$router.push('/CrewMonitoring');
else this.$router.push('/FactoryMonitoring')
},
// 切换菜单
changeMenu(arr) {
this.param.pointValue = arr.map(i => i.character.kksCode);
this.param.checkList = arr;
},
// 切换图表
changeChart(param) {
this.param = Object.assign(this.param, param);
if (!this.param.chartBtn || !this.param.pointValue.length) return;
console.log('当前图表参数:', this.param);
this.clearChart();
// 查询趋势数据
if (this.param.chartBtn == 1) this.getDataTendency();
},
// 切换共用坐标
changeAxis(param) {
if (this.dataTrend) {
if (this.$refs.ChartTrend) this.$refs.ChartTrend.clear();
this.dataTrend.sameAxis = param.sameAxis;
}
},
// 获取趋势数据
async getDataTendency() {
// this.dataTrend = this.initDataTrend(); // 模拟数据
let post = {
keys: [...this.param.pointValue],
status: 0,
timeModelQuery: {
start: this.param.dateSelect[0],
end: this.param.dateSelect[1],
}
};
this.loadingChart = true;
let requestId = ++this.requestIdChart; // 当前请求标记id
API.getTendency(post)
.then(data => {
if (requestId != this.requestIdChart) return; // 如果不是最后一个请求,则抛弃
if (!data || !data.xaxis || !data.xaxis.length) {
this.$message.warning('暂无数据,请更换测点或日期');
return;
}
// 赋值数据
let newData = {
xAxis: [],
dataList: [],
sameAxis: this.param.sameAxis,
};
// 处理时间轴
for (let i = 0; i < data.xaxis.length; i++) {
// 这里不进行换行,因为要导出数据,换行在组件里处理
newData.xAxis.push(dateFormat(new Date(parseInt(data.xaxis[i]) * 1000), 'YYYY/MM/DD HH:mm:ss'));
}
// 处理数据,这里是根据查询的测点,按顺序寻找结果中对应的测点数据,因为接口返回的顺序不能保证是查询顺序,并且还包含了功率和电流测点
newData.dataList = this.param.pointValue.map(i => {
let item = this.param.checkList.find(j => j.character.kksCode == i); // 从选择的测点里取出测点信息
let itemRes = data.dataList.find(j => j.kksCode == item.character.kksCode); // 从返回的结果里取出测点
return {
name: item.character.cpName,
value: itemRes?.value || [],
kksCode: i,
unit: item.character.cpUnit || '',
// warning: itemRes?.color || ''
};
});
this.dataTrend = newData;
})
.catch(e => console.log(e))
.finally(() => {
this.loadingChart = false;
});
},
// 清空图表
clearChart() {
this.dataTrend = null;
if (this.$refs.ChartTrend) this.$refs.ChartTrend.clear();
},
// 导出数据
exportData() {
if (!this.dataTrend) {
this.$message.warning('无可导出数据');
return;
}
let data, // 数据体
txt = '', // 文件内容
tmpArr = [], // 文件内容数组形式,每一列为一个数组元素
name = '', // 文件名
rowSplit = '\n', // 行分隔符
colSplit = ',', // 列分隔符
fileType = '.csv'; // 文件类型,注意不同类型的分隔符
if (this.dataTrend) data = JSON.parse(JSON.stringify(this.dataTrend));
data.xAxis.unshift('时间');
tmpArr = [data.xAxis]; // 放入时间轴
data.dataList.forEach(i => {
name += i.name + ','; // 拼文件名
i.value.unshift(i.name);
tmpArr.push(i.value); // 放入数据y轴
});
// 删除最后一个逗号
if (name.length) name = name.substr(0, name.length - 1) + fileType;
else name = '图表数据' + fileType; // 如果没有名字,则数据有问题,需要检查
// 拼数据格式
for (let i = 0; i < tmpArr[0].length; i++) {
tmpArr.forEach((item, index) => (index == tmpArr.length - 1 ? (txt += item[i]) : (txt += item[i] + colSplit)));
txt += rowSplit;
}
exportDataToFile(txt, name, fileType);
},
// 模拟趋势数据
initDataTrend() {
let data = {
xAxis: ['2020/11/23 08:00', '2020/11/23 09:00', '2020/11/23 10:00', '2020/11/23 11:00',
'2020/11/23 12:00'
],
dataList: [],
sameAxis: this.param.sameAxis,
};
for (let i = 0; i < this.param.checkList.length; i++) {
data.dataList.push({
name: this.param.checkList[i].character.cpName,
value: [Math.ceil(Math.random() * 20 + 20), Math.ceil(Math.random() * 20 + 20), Math.ceil(
Math.random() * 20 + 20), Math.ceil(Math.random() * 20 + 20), Math.ceil(
Math.random() * 20 + 20)],
unit: 'km' + i
});
}
return data;
}
}
};
</script>
<style lang="scss" scoped>
.trend-analysis {
height: 100%;
padding: 1.875rem 3.125rem;
box-sizing: border-box;
.header {
height: 4.25rem;
background-image: linear-gradient(270deg, rgba(197, 254, 255, 0.10) 0%, rgba(147, 252, 255, 0.50) 52%, rgba(147, 252, 255, 0.10) 100%);
padding: 0 1.875rem;
box-sizing: border-box;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
.left {
display: flex;
z-index: 1;
.btn {
@extend %btn-default;
margin: 0 1rem 0 0;
}
.btn:hover {
@extend %btn-hover;
}
}
.center {
@extend %pos-center;
font-size: 1.625rem;
letter-spacing: 0.1625rem;
text-shadow: 0 2px 8px rgba(78, 97, 214, 0.50);
}
.right {
z-index: 1;
i {
font-size: 1.6rem;
}
}
}
.main {
height: calc(100% - 4.25rem);
background: rgba(147, 252, 255, 0.08);
padding: 0 1.875rem 1.875rem;
box-sizing: border-box;
display: flex;
.sidebar {
width: 30rem;
height: 100%;
}
.charts {
width: calc(100% - 30rem);
overflow: hidden;
.btns {
padding: 1rem;
height: 6rem;
}
.chart {
height: calc(100% - 6rem);
}
}
}
}
</style>
<!-- 用户管理 -->
<template>
<div class="user">
<div class="header">
<div class="left">
<el-button class="btn" @click="goPage">角色管理</el-button>
<el-button class="btn" @click="showForm(0)">+ 新增</el-button>
</div>
<div class="center">
{{$x_factory.plantName}} - 用户管理
</div>
<div class="right">
<!-- <i class="el-icon-close" @click="back" /> -->
</div>
</div>
<div class="main global">
<div class="table">
<el-table ref="table" :data="tableData" height="100%" v-loading="tableLoading" :element-loading-background="loadingColor">
<el-table-column prop="username" label="用户名" align="center"></el-table-column>
<el-table-column prop="role.name" label="角色" align="center"></el-table-column>
<el-table-column prop="name" label="姓名" align="center"></el-table-column>
<el-table-column prop="phone" label="手机" align="center"></el-table-column>
<el-table-column prop="sex" label="性别" align="center"></el-table-column>
<el-table-column label="操作" align="center">
<template v-slot="scope">
<span class="table-btn" @click="showForm(scope.row)">编辑</span>
<el-divider v-if="scope.row.id != 1 && $x_user.checkPermission('UserDelete')" direction="vertical"></el-divider>
<span v-if="scope.row.id != 1 && $x_user.checkPermission('UserDelete')" class="table-btn" @click="delForm(scope.row)">删除</span>
</template>
</el-table-column>
</el-table>
</div>
<el-pagination class="page" layout="prev, pager, next" :total="tableTotal" @current-change="pageChange" />
</div>
<el-dialog class="dialog global" width="30%" :title="formTitle" :visible.sync="formShow" destroy-on-close v-loading="formLoading"
:element-loading-background="loadingColor">
<el-form ref="form" :model="formData" :rules="formRules" label-width="100px" size="small">
<el-form-item label="用户名" prop="username">
<el-input v-model.trim="formData.username" placeholder="请填写用户名(以此登录)" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="formData.password" type="password" :placeholder="placeholderPass" />
</el-form-item>
<el-form-item label="角色" prop="roleId">
<el-select style="width: 100%;" v-model="formData.roleId" :popper-append-to-body="false" popper-class="select-popper"
placeholder="请选择角色">
<el-option v-for="(item, index) in roleList" :key="index" :label="item.name" :value="item.roleId" />
</el-select>
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-input v-model.trim="formData.name" placeholder="请填写姓名" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model.trim="formData.phone" placeholder="请填写手机号" />
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select style="width: 100%;" v-model="formData.sex" :popper-append-to-body="false" popper-class="select-popper"
placeholder="请选择性别">
<el-option label="男" value="男" />
<el-option label="女" value="女" />
</el-select>
</el-form-item>
</el-form>
<div class="btn-list">
<el-button class="btn-active" type="primary" @click="onSubmit">保存</el-button>
<el-button class="btn-default" @click="formShow = false">取消</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {
ChartStyle
} from '@/common/const.js';
import User from '@/api/user.js';
export default {
components: {},
data() {
// 用户名验证规则,不能包含特殊字符
const validateUsername = (rule, value, callback) => {
if (value && /[|]+/.test(value)) {
callback(new Error('不能包含特殊字符'));
} else {
callback();
}
};
// 手机验证规则,可以为空
const validatePhone = (rule, value, callback) => {
if (value && !/^((\+?86)|(\(\+86\)))?(1\d{10})$/.test(value)) {
callback(new Error('手机格式错误'));
} else {
callback();
}
};
return {
tableLoading: false,
formLoading: false,
loadingColor: ChartStyle.loadingColor,
// 查询参数
tableQuery: {
pageNo: 0, // 0为第一页
pageSize: 10
},
tableData: null,
tableTotal: 100,
formShow: false,
formTitle: '',
formData: null,
formRules: {
username: [{
required: true,
message: '请输入用户名称',
trigger: 'blur'
}, {
validator: validateUsername,
trigger: 'blur'
}],
password: [],
// name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
phone: [{
validator: validatePhone,
trigger: 'blur'
}],
roleId: [{
required: true,
message: '请选择角色',
trigger: 'blur'
}],
},
placeholderPass: '',
roleList: [],
};
},
created() {
this.formReset();
this.getList();
},
methods: {
// 跳转角色管理
goPage() {
this.$router.push('/RoleManage');
},
// 返回上一页
back() {
this.$router.go(-1);
},
// 重置表单
formReset() {
this.formData = {
username: '',
password: '',
name: '',
phone: '',
sex: '男',
roleId: '',
};
},
// 获取用户列表
async getList() {
this.tableLoading = true;
try {
let res = await User.getUserList(this.tableQuery);
this.tableData = res.content;
this.tableTotal = res.totalElements;
} catch (e) {
console.log(e);
}
this.tableLoading = false;
},
// 翻页
pageChange(val) {
this.tableQuery.pageNo = val - 1;
this.getList();
},
// 新增编辑
async showForm(row) {
this.getRoleList();
// 编辑
if (row) {
this.formTitle = '编辑用户';
// 回显赋值,之所以要一个个的赋值,是因为后端的编辑接口不能把多余的数据传过去
this.formData = {
id: row.id,
username: row.username || '',
name: row.name || '',
phone: row.phone || '',
sex: row.sex || '男',
roleId: row.role?.roleId || '',
};
// 密码验证规则
const validatePass = (rule, value, callback) => {
if (value && value.length < 6) {
callback(new Error('密码至少6个字符'));
} else {
callback();
}
};
this.placeholderPass = '不填写则不修改';
// 增加密码验证规则
this.formRules.password = [{
validator: validatePass,
trigger: 'blur'
}];
// 新增
} else {
this.formTitle = '新增用户';
this.formReset(); // 清空表单
this.placeholderPass = '请填写密码,至少6位字符';
this.formRules.password = [{
required: true,
message: '请输入用户密码',
trigger: 'blur'
}]; // 增加密码验证规则
}
this.formShow = true;
},
// 获取角色列表
async getRoleList() {
try {
let res = await User.getRoleList();
this.roleList = res;
} catch (e) {
console.log(e);
}
},
// 删除数据
async delForm(row) {
if (!row.id || row.id == 1) return;
this.tableLoading = true;
try {
await this.$confirm('确认删除该用户?');
await User.delUser(row.id);
this.$message.success('删除成功');
if (this.tableData.length == 1 && this.tableQuery.pageNo != 0) this.tableQuery.pageNo--; // 如果删除时是当前页的最后一条,则页数往前翻一页
this.getList();
} catch (e) {
console.log(e);
this.tableLoading = false;
}
},
// 提交表单
onSubmit() {
this.$refs.form.validate(async valid => {
if (!valid) return;
this.formData.role = {
roleId: this.formData.roleId
};
let apiName = this.formData.id ? 'editUser' : 'addUser'; // 根据是否有id选择调用对应的接口
this.formLoading = true;
try {
await User[apiName](this.formData);
this.$message.success('操作成功');
this.formShow = false;
this.getList();
} catch (e) {
console.log(e);
}
this.formLoading = false;
});
}
}
};
</script>
<style lang="scss" scoped>
.user {
height: 100%;
padding: 1.875rem 3.125rem;
box-sizing: border-box;
.header {
height: 4.25rem;
background-image: linear-gradient(270deg, rgba(197, 254, 255, 0.10) 0%, rgba(147, 252, 255, 0.50) 52%, rgba(147, 252, 255, 0.10) 100%);
padding: 0 1.875rem;
box-sizing: border-box;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
.left {
display: flex;
z-index: 1;
.btn {
@extend %btn-default;
}
.btn:hover {
@extend %btn-hover;
}
}
.center {
@extend %pos-center;
font-size: 1.625rem;
letter-spacing: 0.1625rem;
text-shadow: 0 2px 8px rgba(78, 97, 214, 0.50);
}
.right {
z-index: 1;
i {
font-size: 1.6rem;
}
}
}
.main {
height: calc(100% - 4.25rem);
background: rgba(147, 252, 255, 0.08);
padding: 1.875rem 1.875rem 0;
box-sizing: border-box;
.table {
height: calc(100% - 40px);
.table-btn {
color: $color-cyan;
cursor: pointer;
}
}
.page {
margin: 0.3125rem 0;
}
}
.dialog {
/deep/ .el-form-item__label {
color: #fff;
}
.btn-list{
@extend %flex-center;
margin-top: 2rem;
}
.btn-active{
@extend %btn-active;
}
.btn-default {
@extend %btn-default;
}
}
}
</style>
module.exports = {
publicPath: './',
assetsDir: 'static',
// productionSourceMap: false,
// 代理
devServer: {
proxy: {
'^/plant/': {
// target: 'http://8.130.25.40',
target: 'http://192.168.43.122:8258',
ws: true,
changeOrigin: true,
pathRewrite: {
'/plant/' : '/'
}
}
}
},
// gzip压缩
configureWebpack: config => {
const CompressionWebpackPlugin = require('compression-webpack-plugin');
config.plugins.push(
new CompressionWebpackPlugin()
)
},
chainWebpack: config => {
const oneOfsMap = config.module.rule('scss').oneOfs.store
oneOfsMap.forEach(item => {
item
.use('sass-resources-loader')
.loader('sass-resources-loader')
.options({
resources: './src/style/index.scss',
})
.end()
})
config
.plugin('html')
.tap(args => {
args[0].title = 'hn-monitor'
return args
})
}
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment