2 Commits

Author SHA1 Message Date
fe14089562 在路由权限校验中获取设备列表 2025-07-23 21:35:40 +08:00
b68f2608f3 本地 2025-07-21 23:00:41 +08:00
120 changed files with 4743 additions and 23135 deletions

View File

@ -1,15 +1,14 @@
# 页面标题 # 页面标题
VUE_APP_TITLE = 上动新能源-EMS管理系统 VUE_APP_TITLE = 上动新能源-EMS管理系统
# 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true
# 开发环境配置 # 开发环境配置
NODE_ENV = 'development' ENV = 'development'
# EMS管理系统/开发环境 # EMS管理系统/开发环境
VUE_APP_BASE_API = '/dev-api' VUE_APP_BASE_API = '/dev-api'
# 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true
# EMS管理系统/开发环境 图片拼接地址 # EMS管理系统/开发环境 图片拼接地址
VUE_APP_IMG_URL = '/dev-api' VUE_APP_IMG_URL = '/dev-api'

View File

@ -2,11 +2,10 @@
VUE_APP_TITLE = 上动新能源-EMS管理系统 VUE_APP_TITLE = 上动新能源-EMS管理系统
# 生产环境配置 # 生产环境配置
NODE_ENV = 'production' ENV = 'production'
# EMS管理系统/生产环境 # EMS管理系统/生产环境
VUE_APP_BASE_API= 'http://1.15.120.242:8089' VUE_APP_BASE_API = '/dev-api'
# EMS管理系统/生产环境 图片拼接地址
VUE_APP_IMG_URL = 'http://1.15.120.242:8089'
# EMS管理系统/生产环境 图片拼接地址 todo baseUrl有变更时 请更新
VUE_APP_IMG_URL = 'http://110.40.171.179:8089'

View File

@ -1,12 +1,12 @@
# 页面标题 # 页面标题
VUE_APP_TITLE = 上动新能源-EMS管理系统 VUE_APP_TITLE = 上动新能源-EMS管理系统
BABEL_ENV = production
NODE_ENV = production
# 测试环境配置 # 测试环境配置
NODE_ENV = 'staging' ENV = 'staging'
# EMS管理系统/测试环境 # EMS管理系统/测试环境
VUE_APP_BASE_API= 'http://110.40.171.179:8089' VUE_APP_BASE_API = '/stage-api'
# EMS管理系统/测试环境 图片拼接地址
VUE_APP_IMG_URL = 'http://110.40.171.179:8089'

View File

@ -5,7 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit"> <meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>logo-icon.png"> <link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= webpackConfig.name %></title> <title><%= webpackConfig.name %></title>
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]--> <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
<style> <style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

@ -10,7 +10,7 @@ import ThemePicker from "@/components/ThemePicker"
export default { export default {
name: "App", name: "App",
components: { ThemePicker } components: { ThemePicker },
} }
</script> </script>
<style scoped> <style scoped>

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +0,0 @@
import request from '@/utils/request'
// 新增
export function addPriceConfig(data) {
return request({
url: '/ems/energyPriceConfig',
method: 'post',
data
})
}
//修改
export function editPriceConfig(data) {
return request({
url: '/ems/energyPriceConfig',
method: 'put',
data
})
}
//删除
export function energyPriceConfig(id) {
return request({
url: `/ems/energyPriceConfig/${id}`,
method: 'DELETE',
})
}
//详情
export function detailPriceConfig(id) {
return request({
url: `/ems/energyPriceConfig/${id}`,
method: 'get',
})
}
//列表
export function listPriceConfig({startTime,endTime,pageSize,pageNum,siteId}) {
return request({
url: `/ems/energyPriceConfig/list?startTime=${startTime}&endTime=${endTime}&pageNum=${pageNum}&pageSize=${pageSize}&siteId=${siteId}`,
method: 'get',
})
}

View File

@ -1,40 +0,0 @@
import request from '@/utils/request'
// 获取设备列表
export function getAllDeviceCategory() {
return request({
url: '/ems/generalQuery/getAllDeviceCategory',
method: 'get'
})
}
// 点位列表
export function pointFuzzyQuery(data) {
return request({
url: '/ems/generalQuery/pointFuzzyQuery',
method: 'post',
data
})
}
// 图表
export function getPointValueList(data) {
return request({
url: '/ems/generalQuery/getPointValueList',
method: 'post',
data
})
}
// 图表
export function getAllBatteryIdsBySites(data) {
return request({
url: `/ems/generalQuery/getAllBatteryIdsBySites/${data}`,
method: 'get',
})
}
// 综合查询-按站点获取配置设备列表
export function getGeneralQueryDeviceList(siteId) {
return request({
url: `/ems/siteConfig/getDeviceList?siteId=${siteId}`,
method: 'get',
})
}

View File

@ -1,528 +1,64 @@
import request from '@/utils/request' import request from '@/utils/request'
// 站点列表 // 站点列表
export function getSiteInfoList({siteName, startTime, endTime, pageSize, pageNum}) { export function getSiteInfoList({siteName,startTime, endTime,pageSize,pageNum}) {
return request({ return request({
url: `/ems/siteConfig/getSiteInfoList?siteName=${siteName}&startTime=${startTime}&endTime=${endTime}&pageSize=${pageSize}&pageNum=${pageNum}`, url: `/ems/siteConfig/getSiteInfoList?siteName=${siteName}&startTime=${startTime}&endTime=${endTime}&pageSize=${pageSize}&pageNum=${pageNum}`,
method: 'get' method: 'get'
}) })
}
// 手动同步站点天气(收益报表)
export function syncSiteWeatherByDateRange({siteId, startTime, endTime}) {
return request({
url: `/ems/statsReport/syncWeatherByDateRange`,
method: 'post',
params: {siteId, startTime, endTime}
})
}
// 新增站点
export function addSite(data) {
return request({
url: `/ems/siteConfig/addSite`,
method: 'post',
data
})
}
// 编辑站点
export function updateSite(data) {
return request({
url: `/ems/siteConfig/updateSite`,
method: 'post',
data
})
} }
// 设备列表 // 设备列表
export function getDeviceInfoList(data) { export function getDeviceInfoList({siteId,pageSize,pageNum}) {
return request({ return request({
url: `/ems/siteConfig/getDeviceInfoList`, url: `/ems/siteConfig/getDeviceInfoList?siteId=${siteId}&pageSize=${pageSize}&pageNum=${pageNum}`,
method: 'get', method: 'get'
params: data })
})
} }
// 设备详情 // 设备详情
export function getDeviceDetailInfo(id) { export function getDeviceDetailInfo(id) {
return request({ return request({
url: `/ems/siteConfig/getDeviceDetailInfo?id=${id}`, url: `/ems/siteConfig/getDeviceDetailInfo?id=${id}`,
method: 'get' method: 'get'
}) })
} }
// 获取所有设备类别 // 获取所有设备类别
export function getDeviceCategory() { export function getDeviceCategory() {
return request({ return request({
url: `/ems/siteConfig/getDeviceCategory`, url: `/ems/siteConfig/getDeviceCategory`,
method: 'get' method: 'get'
}) })
} }
// 新增设备 // 新增设备
export function addDevice(data) { export function addDevice(data) {
return request({ return request({
url: `/ems/siteConfig/addDevice`, url: `/ems/siteConfig/addDevice`,
method: 'post', method: 'post',
data data
}) })
} }
// 编辑设备 // 编辑设备
export function updateDevice(data) { export function updateDevice(data) {
return request({ return request({
url: `/ems/siteConfig/updateDevice`, url: `/ems/siteConfig/updateDevice`,
method: 'post', method: 'post',
data data
}) })
} }
// 删除设备 // 删除设备
export function deleteService(id) { export function deleteService(id) {
return request({ return request({
url: `/ems/siteConfig/deleteService/` + id, url: `/ems/siteConfig/deleteService/`+id,
method: 'delete', method: 'delete',
}) })
}
//pcs开、关机
export function updateDeviceStatus(data) {
return request({
url: `/ems/siteConfig/updateDeviceStatus`,
method: 'post',
data
})
}
// 获取上级设备id列表
export function getParentDeviceId({siteId, deviceCategory}) {
return request({
url: `/ems/siteConfig/getParentDeviceId?siteId=${siteId}&deviceCategory=${deviceCategory}`,
method: 'get',
})
} }
//获取所有设备 //获取所有设备
export function getDeviceList(siteId) { export function getDeviceList(siteId) {
return request({ return request({
url: `/ems/siteConfig/getDeviceList?siteId=${siteId}`, url: `/ems/siteConfig/getDeviceList?siteId=${siteId}`,
method: 'get', method: 'get',
}) })
}
//获取设备点位table
export function getDevicePointList(data) {
return request({
url: `/ems/siteConfig/getDevicePointList`,
method: 'get',
params: data
})
}
//获取设备类型下面的所有设备列表
export function getDeviceListBySiteAndCategory({siteId, deviceCategory}) {
return request({
url: `/ems/siteConfig/getDeviceListBySiteAndCategory?siteId=${siteId}&deviceCategory=${deviceCategory}`,
method: 'get',
})
}
// 获取单站监控项目点位映射
export function getSingleMonitorProjectPointMapping(siteId) {
return request({
url: `/ems/siteConfig/getSingleMonitorProjectPointMapping?siteId=${siteId}`,
method: 'get',
})
}
// 保存单站监控项目点位映射
export function saveSingleMonitorProjectPointMapping(data) {
return request({
url: `/ems/siteConfig/saveSingleMonitorProjectPointMapping`,
method: 'post',
data,
headers: {
repeatSubmit: false
}
})
}
// 获取单站监控工作状态枚举映射PCS
export function getSingleMonitorWorkStatusEnumMappings(siteId) {
return request({
url: `/ems/siteConfig/getSingleMonitorWorkStatusEnumMappings?siteId=${siteId}`,
method: 'get',
})
}
// 保存单站监控工作状态枚举映射PCS
export function saveSingleMonitorWorkStatusEnumMappings(data) {
return request({
url: `/ems/siteConfig/saveSingleMonitorWorkStatusEnumMappings`,
method: 'post',
data,
headers: {
repeatSubmit: false
}
})
}
//新增设备保护
export function addProtectPlan(data) {
return request({
url: `/ems/protectPlan`,
method: 'post',
data
})
}
//修改设备保护
export function updateProtectPlan(data) {
return request({
url: `/ems/protectPlan`,
method: 'put',
data
})
}
//删除设备保护
export function deleteProtectPlan(id) {
return request({
url: `/ems/protectPlan/${id}`,
method: 'delete',
})
}
//设备保护详情
export function getProtectPlan(id) {
return request({
url: `/ems/protectPlan/${id}`,
method: 'get',
})
}
//设备保护详情列表
//http://localhost:8089/ems/protectPlan/list?pageSize=10&pageNum=1&faultName=总压&siteId=021_DDS_01
export function protectPlanList({siteId, faultName, pageSize, pageNum}) {
return request({
url: `/ems/protectPlan/list?siteId=${siteId}&faultName=${faultName}&pageSize=${pageSize}&pageNum=${pageNum}`,
method: 'get',
})
}
// 点位导出
export function exportPointList(data) {
return request({
url: `/ems/pointMatch/export`,
method: 'post',
data
})
}
// 点位导入
export function importPointList(data) {
return request({
url: `/ems/pointMatch/importData`,
method: 'post',
data
})
}
// 按站点导入模板点位配置
export function importPointTemplateBySite(data) {
return request({
url: `/ems/pointConfig/importTemplateBySite`,
method: 'post',
data
})
}
// CSV导入点位配置
export function importPointConfigCsv(data) {
return request({
url: `/ems/pointConfig/importCsv`,
method: 'post',
data,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 点位配置列表
export function getPointMatchList(params) {
return request({
url: `/ems/pointConfig/list`,
method: 'get',
params
})
}
// 下载单体电池导入模板
export function downloadSingleBatteryMonitorImportTemplate(siteId) {
return request({
url: `/ems/siteConfig/downloadSingleBatteryMonitorImportTemplate`,
method: 'get',
params: { siteId },
responseType: 'blob'
})
}
// 导入单体电池与监控点位映射
export function importSingleBatteryMonitorMappings(data) {
return request({
url: `/ems/siteConfig/importSingleBatteryMonitorMappings`,
method: 'post',
data,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 点位配置详情
export function getPointMatchDetail(id) {
return request({
url: `/ems/pointConfig/${id}`,
method: 'get',
})
}
// 新增点位配置
export function addPointMatch(data) {
return request({
url: `/ems/pointConfig`,
method: 'post',
data
})
}
// 编辑点位配置
export function updatePointMatch(data) {
return request({
url: `/ems/pointConfig`,
method: 'put',
data
})
}
// 删除点位配置
export function deletePointMatch(ids) {
return request({
url: `/ems/pointConfig/${ids}`,
method: 'delete',
})
}
// 点位配置-批量获取最新值(新接口)
export function getPointConfigLatestValues(data) {
return request({
url: `/ems/pointConfig/latestValues`,
method: 'post',
data,
headers: {
repeatSubmit: false
}
})
}
// 点位配置-曲线数据(新接口)
export function getPointConfigCurve(data) {
return request({
url: `/ems/pointConfig/curve`,
method: 'post',
data,
headers: {
repeatSubmit: false
}
})
}
// 点位配置-生成最近7天数据
export function generatePointConfigRecent7Days(data) {
return request({
url: `/ems/pointConfig/generateRecent7Days`,
method: 'post',
data,
headers: {
repeatSubmit: false
}
})
}
// 计算点配置列表
export function getPointCalcConfigList(params) {
return request({
url: `/ems/pointCalcConfig/list`,
method: 'get',
params
})
}
// 计算点配置详情
export function getPointCalcConfigDetail(id) {
return request({
url: `/ems/pointCalcConfig/${id}`,
method: 'get',
})
}
// 新增计算点配置
export function addPointCalcConfig(data) {
return request({
url: `/ems/pointCalcConfig`,
method: 'post',
data
})
}
// 编辑计算点配置
export function updatePointCalcConfig(data) {
return request({
url: `/ems/pointCalcConfig`,
method: 'put',
data
})
}
// 删除计算点配置
export function deletePointCalcConfig(ids) {
return request({
url: `/ems/pointCalcConfig/${ids}`,
method: 'delete',
})
}
// 数据修正列表ems_daily_energy_data
export function getDailyEnergyDataList(params) {
return request({
url: `/ems/dailyEnergyData/list`,
method: 'get',
params
})
}
// 数据修正详情
export function getDailyEnergyDataDetail(id) {
return request({
url: `/ems/dailyEnergyData/${id}`,
method: 'get',
})
}
// 新增数据修正
export function addDailyEnergyData(data) {
return request({
url: `/ems/dailyEnergyData`,
method: 'post',
data
})
}
// 编辑数据修正
export function updateDailyEnergyData(data) {
return request({
url: `/ems/dailyEnergyData`,
method: 'put',
data
})
}
// 删除数据修正
export function deleteDailyEnergyData(ids) {
return request({
url: `/ems/dailyEnergyData/${ids}`,
method: 'delete',
})
}
// 充放电修正列表ems_daily_charge_data
export function getDailyChargeDataList(params) {
return request({
url: `/ems/dailyChargeData/list`,
method: 'get',
params
})
}
// 充放电修正详情
export function getDailyChargeDataDetail(id) {
return request({
url: `/ems/dailyChargeData/${id}`,
method: 'get',
})
}
// 新增充放电修正
export function addDailyChargeData(data) {
return request({
url: `/ems/dailyChargeData`,
method: 'post',
data
})
}
// 编辑充放电修正
export function updateDailyChargeData(data) {
return request({
url: `/ems/dailyChargeData`,
method: 'put',
data
})
}
// 删除充放电修正
export function deleteDailyChargeData(ids) {
return request({
url: `/ems/dailyChargeData/${ids}`,
method: 'delete',
})
}
//mqtt
export function getMqttList({pageSize, pageNum, mqttTopic, topicName, siteId}) {
return request({
url: `/ems/mqttConfig/list?pageSize=${pageSize}&pageNum=${pageNum}&mqttTopic=${mqttTopic}&topicName=${topicName}&siteId=${siteId}`,
method: 'get',
})
}
export function getMqttDetail(id) {
return request({
url: `/ems/mqttConfig/${id}`,
method: 'get',
})
}
export function addMqtt(data) {
return request({
url: `/ems/mqttConfig`,
method: 'post',
data
})
}
export function editMqtt(data) {
return request({
url: `/ems/mqttConfig`,
method: 'put',
data
})
}
export function deleteMqtt(id) {
return request({
url: `/ems/mqttConfig/${id}`,
method: 'delete',
})
}
export function initializeSingleBatteryMonitorMappings(data) {
return request({
url: `/ems/siteConfig/initializeSingleBatteryMonitorMappings`,
method: 'post',
data
})
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1021 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

View File

@ -4,249 +4,101 @@
//右侧内容区域 //右侧内容区域
//父元素 //父元素
.ems-dashboard-editor-container { .ems-dashboard-editor-container{
background-color: #F1F5FC; background-color: #FFFFFF;//#F1F5FC
padding: 24px; padding: 24px;
font-size: 12px; font-size: 12px;
} }
//除去顶部信息(如搜索栏、站点基本信息等)外的 白色背景内容区域 //除去顶部信息(如搜索栏、站点基本信息等)外的 白色背景内容区域
.ems-content-container { .ems-content-container{
background-color: #ffffff; background-color: #ffffff;
margin-top: 24px; margin-top: 24px;
} }
//需要设置内padding的白色背景区域 //需要设置内padding的白色背景区域
.ems-content-container-padding { .ems-content-container-padding{
padding: 24px; padding: 24px;
} }
//card通用样式 标题、body //card通用样式 标题、body
.common-card-container { .common-card-container{
.el-card__header { .el-card__header{
padding: 14px; padding:14px;
border-bottom: none; border-bottom: none;
font-size: 12px;
background: #F1F5FB;
position: relative;
.card-title {
font-weight: 500;
color: #333333;
}
.el-button--text {
color: #666666;
}
}
}
.common-card-container-body-no-padding {
.el-card__body {
padding: 0;
}
}
.common-card-container-no-title-bg {
.el-card__header {
background-color: transparent;
}
}
//单站监控 设备监控card公共样式
.sbjk-card-container {
.el-card__header {
background-color: transparent;
padding: 5px 14px;
color: #ffffff;
position: relative;
border-radius: 5px 5px 0 0;
.large-title {
font-size: 18px;
font-weight: 500;
line-height: 40px;
padding: 0 50px 0 11px;
display: inline-block;
vertical-align: middle;
}
.info {
display: inline-block;
vertical-align: middle;
color: #ffffff;
font-size: 12px; font-size: 12px;
line-height: 20px; background: #F1F5FB ;
} .card-title{
font-weight: 500;
.el-button--text { color:#333333;
color: #666666;
}
.alarm {
position: absolute;
right: 25px;
top: 50%;
transform: translateY(-50%);
.alarm-icon {
font-size: 22px;
color: #fff;
display: block;
cursor: pointer;
} }
} .large-title{
} font-size: 20px;
font-weight: 500;
//红色背景颜色标题
&.warning-card-container {
.el-card__header {
background-color: #b64040; //#fc6b69;
}
.work-status {
color: #b64040 !important;;
}
}
//绿色背景颜色标题
&.running-card-container {
.el-card__header {
background-color: #40b6a5; //#05aea3;
}
.work-status {
color: #40b6a5 !important;
}
}
//灰色背景颜色标题
&.timing-card-container {
.el-card__header {
background-color: #666666;
}
.work-status {
color: #666666 !important;;
}
}
}
/* card标题里的时间选择器 */
.time-range-card {
&.common-card-container .el-card__header {
padding-top: 0;
padding-bottom: 0;
.time-range-header {
height: 40px;
display: flex;
justify-content: space-between;
align-items: center;
.card-title {
line-height: 40px; line-height: 40px;
} }
.el-button--text{
color: #666666;
}
}
}
.common-card-container-body-no-padding{
.el-card__body{
padding:0;
}
}
.common-card-container-no-title-bg {
.el-card__header{
background-color: transparent;
} }
}
} }
//描述样式 PCS、BMS总览、BMS电池簇页面公共样式 //描述样式 PCS、BMS总览、BMS电池簇页面公共样式
.descriptions-main { .descriptions-main{
padding: 24px; padding:24px;
position: relative; position: relative;
&.descriptions-main-bg-color{
&.descriptions-main-bg-color { background-color:#f1f5fc ;
background-color: #f1f5fc; .el-descriptions__body{
background-color:#f1f5fc ;
.el-descriptions__body {
background-color: #f1f5fc;
} }
} }
.el-descriptions-item__cell[colspan='1']{
.el-descriptions-item__cell[colspan='1'] { width:25%
width: 25%
} }
.el-descriptions__body .el-descriptions__table{
.el-descriptions__body .el-descriptions__table { .descriptions-direction{
.descriptions-direction {
line-height: 19px; line-height: 19px;
color: #666666; color: #666666;
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
} }
.descriptions-label{
.descriptions-label {
line-height: 14px; line-height: 14px;
color: #666666; color: #666666;
font-size: 12px; font-size: 12px;
} }
.danger{
.danger { color:#FC6B69;
color: #FC6B69;
} }
.save{
.save { color:#09ADA3;
color: #09ADA3;
} }
.keep{
.keep { color:#3C81FF;
color: #3C81FF;
} }
} }
} }
//电表、液冷公共样式
.device-info-row {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
border-left: 1px solid #eee;
border-top: 1px solid #eee;
.device-info-col {
padding: 10px 0;
text-align: center;
font-size: 12px;
color: #666666;
line-height: 14px;
border-bottom: 1px solid #eee;
border-right: 1px solid #eee;
.left {
}
.right {
display: block;
font-weight: 500;
font-size: 16px;
line-height: 18px;
margin-top: 10px;
}
}
}
//公共表格样式 //公共表格样式
.el-table { .common-table.el-table{
font-size: 13px; color:#333333;
}
.common-table.el-table {
color: #333333;
.el-table__header-wrapper th, .el-table__fixed-header-wrapper th { .el-table__header-wrapper th, .el-table__fixed-header-wrapper th {
background: #f1f5fc; background: #f1f5fc;
border-bottom: none; border-bottom: none;
.table-head { .table-head {
color: #515a6e; color: #515a6e;
} }
} }
.warning-status{
.warning-status { color:#FC6B69;
color: #FC6B69;
&.circle::before { &.circle::before {
content: ""; content: "";
display: inline-block; display: inline-block;
@ -260,61 +112,45 @@
} }
//二、三级菜单栏样式 //二、三级菜单栏样式
.ems-second-menu { .ems-second-menu{
width: fit-content; width:fit-content;
.el-menu-item{
.el-menu-item {
line-height: 40px; line-height: 40px;
height: 40px; height: 40px;
padding: 0; padding:0;
} }
&.el-menu--horizontal > .el-menu-item.is-active,&.el-menu--horizontal > .el-menu-item{
&.el-menu--horizontal > .el-menu-item.is-active, &.el-menu--horizontal > .el-menu-item { border-bottom:none!important;
border-bottom: none !important;
} }
.el-menu-item.is-active{
.el-menu-item.is-active { background: #0366c1!important;
background: #0366c1 !important;
} }
} }
.ems-third-menu{
.ems-third-menu-container { border-right: none;
position: relative; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
padding-left: 140px; height: fit-content;
background-color: #ffffff; .el-menu-item{
line-height: 45px;
.ems-third-menu { height: 45px;
border-right: none; padding: 0 !important;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); width: 125px;
height: fit-content; text-align: center;
position: absolute;
top: 0;
left: 0;
.el-menu-item {
line-height: 45px;
height: 45px;
padding: 0 !important;
width: 125px;
text-align: center;
}
.el-menu-item:hover {
background: #67b1ff !important;
color: #ffffff !important;
}
.el-menu-item.is-active {
background: #409eff !important;
}
} }
.el-menu-item:hover{
background: #67b1ff!important;
color:#ffffff!important;
}
.el-menu-item.is-active{
background: #409eff!important;
}
} }
//按钮栏 选中样式 //按钮栏 选中样式
.ems-btns-group { .ems-btns-group{
.activeBtn { .activeBtn{
background-color: #0366c1; background-color: #0366c1;
border-color: #0366c1; border-color: #0366c1;
color: #ffffff; color: #ffffff;
@ -323,12 +159,11 @@
} }
//搜索栏样式 //搜索栏样式
.select-container.el-form--inline .el-form-item { .select-container.el-form--inline .el-form-item{
margin-right: 15px; margin-right: 15px;
} }
//红色背景颜色按钮 //红色背景颜色按钮
.alarm-btn, .alarm-btn:hover, .alarm-btn:focus { .alarm-btn,.alarm-btn:hover, .alarm-btn:focus{
background-color: #FC6B69; background-color: #FC6B69;
border-color: #FC6B69; border-color: #FC6B69;
} }

View File

@ -1,137 +0,0 @@
<template>
<el-dialog
:fullscreen="true"
:append-to-body="true"
:visible.sync="show"
:show-close="false"
top="0"
custom-class="big-data-dialog"
>
<div class="swiper-container">
<div class="swiper-icon left-icon" v-show="imgIndex > 0">
<i class="el-icon-d-arrow-left icon" @click="toLeft"></i>
</div>
<div v-show="showRightIcon" class="swiper-icon right-icon">
<i class="el-icon-d-arrow-right icon" @click="toRight"></i>
</div>
<div
class="img-container"
:style="{ transform: 'translateX(' + imgIndex * -100 + 'vw)' }"
>
<img
v-for="index in maxImgNumber"
:key="'swiperImg' + index"
:src="require(`@/assets/images/ems/bigData-${index}.png`)"
alt=""
/>
</div>
</div>
<div class="close-btn" @click="show = false">
<i class="el-icon-close"></i>
</div>
</el-dialog>
</template>
<style lang="scss" scoped>
.close-btn {
position: absolute;
right: 10px;
top: 10px;
font-size: 23px;
line-height: 20px;
color: rgba(217, 242, 255, 1);
cursor: pointer;
}
.swiper-container {
height: 100%;
width: 100%;
overflow: hidden;
position: relative;
.swiper-icon {
color: rgba(217, 242, 255, 1);
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 20;
cursor: pointer;
font-size: 30px;
padding: 20px;
background: transparent;
&.left-icon {
left: 20px;
}
&.right-icon {
right: 20px;
}
&:hover {
.icon {
opacity: 1;
}
}
.icon {
transition: all 0.6s;
opacity: 0;
}
}
.img-container {
height: 100%;
transition: all 1s;
display: flex;
flex-direction: row;
z-index: 0;
img {
width: 100vw;
height: 100vh;
display: block;
margin: 0;
flex-shrink: 0;
}
}
}
</style>
<style lang="scss">
.big-data-dialog {
.el-dialog__header {
display: none;
}
.el-dialog__body {
padding: 0;
margin: 0;
position: relative;
}
}
</style>
<script>
export default {
data() {
return {
show: false,
imgIndex: 0,
maxImgNumber: 3,
};
},
computed: {
showRightIcon() {
return this.imgIndex < this.maxImgNumber - 1;
},
},
watch: {
show: {
handler(newValue) {
if (!newValue) this.imgIndex = 0;
},
immediate: true,
},
},
methods: {
toLeft() {
if (this.imgIndex === 0) return;
this.imgIndex -= 1;
},
toRight() {
if (this.imgIndex >= this.maxImgNumber - 1) return;
this.imgIndex += 1;
},
},
};
</script>

View File

@ -1,150 +0,0 @@
<template>
<div class="time-range">
<el-date-picker
v-model="dateRange"
:class="miniTimePicker ? 'mini-date-picker' : ''"
type="daterange"
range-separator=""
start-placeholder="开始时间"
value-format="yyyy-MM-dd"
:clearable="false"
:picker-options="pickerOptions"
end-placeholder="结束时间">
</el-date-picker>
<template v-if="!showIcon">
<el-button size="mini" style="margin-left: 10px;" :loading="loading" @click="reset">重置</el-button>
<el-button type="primary" size="mini" :loading="loading" @click="search">搜索</el-button>
<el-button type="primary" size="mini" :loading="loading" @click="timeLine('before')">上一时段</el-button>
<el-button type="primary" size="mini" :loading="loading" @click="timeLine('next')" :disabled="disabledNextBtn">
下一时段
</el-button>
</template>
<template v-else>
<el-button class="btn-icon" icon="el-icon-refresh-right" circle size="mini" style="margin-left: 8px;"
:loading="loading"
@click="reset"></el-button>
<el-button class="btn-icon" type="primary" size="mini" icon="el-icon-search" circle :loading="loading"
@click="search"></el-button>
<el-button class="btn-icon" type="primary" size="mini" icon="el-icon-d-arrow-left" circle :loading="loading"
@click="timeLine('before')"></el-button>
<el-button class="btn-icon" type="primary" size="mini" icon="el-icon-d-arrow-right" circle :loading="loading"
@click="timeLine('next')"
:disabled="disabledNextBtn"></el-button>
</template>
</div>
</template>
<script>
import {formatDate} from '@/filters/ems'
export default {
props: {
showIcon: {
type: Boolean,
required: false,
default: false
},
miniTimePicker: {
type: Boolean,
required: false,
default: false
}
},
computed: {
disabledNextBtn() {
return new Date(this.dateRange[1]) >= new Date(this.defaultDateRange[1])
}
},
data() {
return {
loading: false,
dateRange: [],
defaultDateRange: [],
pickerOptions: {
disabledDate(time) {
return time.getTime() > Date.now();
},
},
}
},
methods: {
init(today = false) {
const now = new Date(), formatNow = formatDate(now);
const weekAgo = formatDate(today ? new Date(now.getTime()) : new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000))
this.dateRange = [weekAgo, formatNow];
this.defaultDateRange = [weekAgo, formatNow];
this.$emit('updateDate', this.dateRange)
},
showBtnLoading(status) {
this.loading = status
},
resetDate() {
this.dateRange = this.defaultDateRange
},
//重置 设置时间范围为初始化时间段
reset() {
this.resetDate()
this.$emit('reset')
this.$emit('updateDate', this.dateRange)
},
// 搜索
search() {
this.$emit('updateDate', this.dateRange)
},
timeLine(type) {
if (!this.dateRange || !this.dateRange[0] || !this.dateRange[1]) return
const nowStartTimes = new Date(this.dateRange[0]).getTime(), nowEndTimes = new Date(this.dateRange[1]).getTime(),
maxTime = new Date(this.defaultDateRange[1]).getTime()
const nowDis = nowEndTimes - nowStartTimes//用户当前选择时间差 可能=0
//baseTime,maxTime 毫秒数
const baseDis = 24 * 60 * 60 * 1000
const calcDis = nowDis === 0 ? baseDis : nowDis
let start = type === 'before' ? nowStartTimes - calcDis : nowStartTimes + calcDis
if (start > maxTime) start = maxTime
let end = type === 'before' ? nowEndTimes - calcDis : nowEndTimes + calcDis
if (end > maxTime) end = maxTime
this.dateRange = [formatDate(start), formatDate(end)]
this.$emit('updateDate', this.dateRange)
},
}
}
</script>
<style lang="scss" scoped>
.time-range {
display: flex;
::v-deep {
.el-range-editor--medium .el-range__icon, .el-range-editor--medium .el-range__close-icon {
line-height: 22px;
}
.el-range-editor--medium.el-input__inner {
height: 30px;
}
.el-range-editor--medium .el-range-separator {
line-height: 22px;
}
.el-button--mini {
padding: 3px 10px;
}
// 展示icon的小组件
.btn-icon.el-button--mini {
padding: 3px 8px;
margin-left: 6px;
}
//小宽度时间选择框
.mini-date-picker {
width: 250px !important;
}
}
}
</style>

View File

@ -2,10 +2,7 @@
<template> <template>
<el-card shadow="always" class="single-square-box" :style="{background: 'linear-gradient(180deg, '+data.bgColor+' 0%,rgba(255,255,255,0) 100%)'}"> <el-card shadow="always" class="single-square-box" :style="{background: 'linear-gradient(180deg, '+data.bgColor+' 0%,rgba(255,255,255,0) 100%)'}">
<div class="single-square-box-title">{{ data.title }}</div> <div class="single-square-box-title">{{ data.title }}</div>
<div class="single-square-box-value"> <div class="single-square-box-value">{{ data.value | formatNumber }}</div>
<i v-if="data.loading" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ data.value | formatNumber }}</span>
</div>
</el-card> </el-card>
</template> </template>
@ -17,30 +14,19 @@
color:#666666; color:#666666;
text-align: left; text-align: left;
.single-square-box-title{ .single-square-box-title{
font-size: 10px; font-size: 12px;
line-height: 10px; line-height: 12px;
padding-bottom: 8px; padding-bottom: 12px;
} }
.single-square-box-value{ .single-square-box-value{
font-size: 18px; font-size: 26px;
line-height: 18px; line-height: 26px;
font-weight: 500; font-weight: 500;
} }
.point-loading-icon{
color: #409eff;
display: inline-block;
transform-origin: center;
animation: pointLoadingSpinPulse 1.1s linear infinite;
}
::v-deep .el-card__body{ ::v-deep .el-card__body{
padding: 8px 7px; padding: 12px 10px;
} }
} }
@keyframes pointLoadingSpinPulse {
0% { opacity: 0.45; transform: rotate(0deg) scale(0.9); }
50% { opacity: 1; transform: rotate(180deg) scale(1.08); }
100% { opacity: 0.45; transform: rotate(360deg) scale(0.9); }
}
</style> </style>
<script> <script>
export default { export default {

View File

@ -4,10 +4,7 @@
<el-row type="flex" > <el-row type="flex" >
<el-card shadow="hover" class="card common-card-container-body-no-padding" v-for="(item,index) in data" :key="index+'zdInfo'" :style="{borderBottomColor:item.color}"> <el-card shadow="hover" class="card common-card-container-body-no-padding" v-for="(item,index) in data" :key="index+'zdInfo'" :style="{borderBottomColor:item.color}">
<div class="info">{{ item.title }}</div> <div class="info">{{ item.title }}</div>
<div class="num"> <div class="num">{{item.num | formatNumber}}</div>
<i v-if="item.loading" class="el-icon-loading"></i>
<span v-else>{{item.num | formatNumber}}</span>
</div>
</el-card> </el-card>
</el-row> </el-row>
</template> </template>
@ -21,35 +18,30 @@ export default {
title:'站点总数(座)', title:'站点总数(座)',
num:'', num:'',
color:'#FFBD00', color:'#FFBD00',
attr:'siteNum', attr:'siteNum'
loading: true
},{ },{
title:'装机功率MW', title:'装机功率MW',
num:'', num:'',
color:'#3C81FF', color:'#3C81FF',
attr:'installPower', attr:'installPower'
loading: true
},{ },{
title:'装机容量MW', title:'装机容量MW',
num:'', num:'',
color:'#5AC7C0', color:'#5AC7C0',
attr:'installCapacity', attr:'installCapacity'
loading: true
},{ },{
title:'总充电量(KWh', title:'总充电量(MWh',
num:'', num:'',
color:'#A696FF', color:'#A696FF',
attr:'totalChargedCap', attr:'totalChargedCap'
loading: true
},{ },{
title:'总放电量(KWh', title:'总放电量(MWh',
num:'', num:'',
color:'#A696FF', color:'#A696FF',
attr:'totalDischargedCap', attr:'totalDischargedCap'
loading: true
}] }]
} }
@ -58,7 +50,6 @@ export default {
setData(res = {}){ setData(res = {}){
this.data.forEach((item)=>{ this.data.forEach((item)=>{
item.num =res[item.attr] item.num =res[item.attr]
item.loading = false
}) })
} }
}, },

View File

@ -2,42 +2,20 @@
<template> <template>
<div class="zd-select-container"> <div class="zd-select-container">
<el-form :inline="true"> <el-form :inline="true">
<el-form-item :label="showLabel ? '站点选择' : ''" :class="{'no-label': !showLabel}"> <el-form-item label="站点选择">
<el-select <el-select v-model="id" placeholder="请选择换电站名称" :loading="loading" loading-text="正在加载数据">
v-model="id"
:size="size"
:placeholder="placeholder"
:loading="loading"
loading-text="正在加载数据"
:style="{width: selectWidth}"
@change="onSubmit"
>
<el-option :label="item.siteName" :value="item.siteId" v-for="(item,index) in siteList" :key="index+'zdxeSelect'"></el-option> <el-option :label="item.siteName" :value="item.siteId" v-for="(item,index) in siteList" :key="index+'zdxeSelect'"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<!-- <el-form-item>--> <el-form-item>
<!-- <el-button type="primary" :loading="searchLoading" @click="onSubmit">搜索</el-button>--> <el-button type="primary" :loading="searchLoading" @click="onSubmit">搜索</el-button>
<!-- </el-form-item>--> </el-form-item>
</el-form> </el-form>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.zd-select-container {
.el-form {
display: inline-flex;
align-items: center;
}
.el-form-item {
margin-bottom: 0;
}
.no-label ::v-deep .el-form-item__label {
display: none;
}
.no-label ::v-deep .el-form-item__content {
margin-left: 0 !important;
}
}
</style> </style>
<script> <script>
import {getAllSites} from '@/api/ems/zddt' import {getAllSites} from '@/api/ems/zddt'
@ -53,26 +31,6 @@ import {mapGetters} from "vuex"
type:String, type:String,
default:'', default:'',
required:false required:false
},
showLabel: {
type: Boolean,
default: true,
required: false
},
size: {
type: String,
default: 'medium',
required: false
},
placeholder: {
type: String,
default: '请选择换电站名称',
required: false
},
selectWidth: {
type: String,
default: '220px',
required: false
} }
}, },
data() { data() {
@ -86,23 +44,10 @@ import {mapGetters} from "vuex"
computed:{ computed:{
...mapGetters(["zdList"]), ...mapGetters(["zdList"]),
}, },
watch: {
defaultSiteId(newVal) {
if (!newVal || !this.siteList || this.siteList.length === 0) {
return
}
if (this.siteList.find(item => item.siteId === newVal) && this.id !== newVal) {
this.id = newVal
}
}
},
methods:{ methods:{
onSubmit(){ onSubmit(){
this.$emit('submitSite',this.id) this.$emit('submitSite',this.id)
}, },
emitSitesLoaded() {
this.$emit('sitesLoaded', this.siteList || [])
},
setDefaultSite(){ setDefaultSite(){
const defaultSite = this.defaultSiteId const defaultSite = this.defaultSiteId
if(defaultSite && this.siteList.find(item=>item.siteId === defaultSite)){ if(defaultSite && this.siteList.find(item=>item.siteId === defaultSite)){
@ -115,7 +60,7 @@ import {mapGetters} from "vuex"
getList(){ getList(){
return getAllSites().then(response => { return getAllSites().then(response => {
this.siteList = response.data || [] this.siteList = response.data || []
this.emitSitesLoaded() console.log("获取站点列表返回数据",response,this.siteList)
this.setDefaultSite() this.setDefaultSite()
}).finally(() => {this.loading=false;this.searchLoading=false}) }).finally(() => {this.loading=false;this.searchLoading=false})
} }
@ -126,14 +71,15 @@ import {mapGetters} from "vuex"
this.$nextTick(()=>{ this.$nextTick(()=>{
if(this.getListByStore){ if(this.getListByStore){
if(this.zdList.length === 0){ if(this.zdList.length === 0){
this.getList().then(()=>{ this.getList().then(()=>{
this.$store.commit('SET_ZD_LIST', this.siteList) this.$store.commit('SET_ZD_LIST', this.siteList)
console.log("从store中获取站点列表数据,但是store中的zdList=[],所以从接口获取数据",this.zdList,this.siteList)
}) })
}else{ }else{
this.siteList = this.zdList this.siteList = this.zdList
this.emitSitesLoaded()
this.loading=false this.loading=false
this.searchLoading=false this.searchLoading=false
console.log("从store中获取站点列表数据",this.zdList,this.siteList)
this.setDefaultSite() this.setDefaultSite()
} }
}else{ }else{

View File

@ -17,7 +17,7 @@ export const formatDate = (val,toSeconds = false,onlyTime=false) => {
if(!toSeconds){ if(!toSeconds){
return front return front
} else{ } else{
return front +' '+back return front +''+back
} }

View File

@ -1,37 +1,22 @@
<template> <template>
<div class="navbar"> <div class="navbar">
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" <hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
@toggleClick="toggleSideBar"/>
<breadcrumb v-if="!topNav" id="breadcrumb-container" class="breadcrumb-container"/> <breadcrumb v-if="!topNav" id="breadcrumb-container" class="breadcrumb-container" />
<top-nav v-if="topNav" id="topmenu-container" class="topmenu-container"/> <top-nav v-if="topNav" id="topmenu-container" class="topmenu-container" />
<div class="right-menu"> <div class="right-menu">
<template v-if="device!=='mobile'"> <template v-if="device!=='mobile'">
<div class="big-data-container"> <search id="header-search" class="right-menu-item" />
<i class="el-icon-s-marketing big-data-icon" @click.stop="showBigDataImg"></i>
</div>
<search id="header-search" class="right-menu-item"/>
<screenfull id="screenfull" class="right-menu-item hover-effect"/> <screenfull id="screenfull" class="right-menu-item hover-effect" />
<el-tooltip content="布局大小" effect="dark" placement="bottom"> <el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect"/> <size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip> </el-tooltip>
</template> </template>
<div v-if="device!=='mobile'" class="site-select-wrap">
<zd-select
:get-list-by-store="true"
:show-label="false"
size="mini"
select-width="220px"
:default-site-id="$route.query.siteId"
@submitSite="onSiteChange"
/>
</div>
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="hover"> <el-dropdown class="avatar-container right-menu-item hover-effect" trigger="hover">
<div class="avatar-wrapper"> <div class="avatar-wrapper">
@ -47,24 +32,22 @@
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</el-dropdown> </el-dropdown>
<div class="right-menu-item hover-effect setting" @click="setLayout" v-if="setting"> <div class="right-menu-item hover-effect setting" @click="setLayout" v-if="setting">
<svg-icon icon-class="more-up"/> <svg-icon icon-class="more-up" />
</div> </div>
</div> </div>
<BigDataPopup ref="bigDataPopup"/>
</div> </div>
</template> </template>
<script> <script>
import {mapGetters} from 'vuex' import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb' import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav' import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger' import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull' import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect' import SizeSelect from '@/components/SizeSelect'
import Search from '@/components/HeaderSearch' import Search from '@/components/HeaderSearch'
import BigDataPopup from '@/components/BigDataPopup'
import ZdSelect from '@/components/Ems/ZdSelect/index.vue'
export default { export default {
emits: ['setLayout'], emits: ['setLayout'],
@ -74,9 +57,7 @@ export default {
Hamburger, Hamburger,
Screenfull, Screenfull,
SizeSelect, SizeSelect,
Search, Search
BigDataPopup,
ZdSelect
}, },
computed: { computed: {
...mapGetters([ ...mapGetters([
@ -97,28 +78,6 @@ export default {
} }
}, },
methods: { methods: {
onSiteChange(id) {
if (!id) {
return
}
const currentSite = (this.$store.getters.zdList || []).find(item => item.siteId === id)
const siteName = currentSite && currentSite.siteName ? currentSite.siteName : ''
localStorage.setItem('global_site_id', id)
if (id !== this.$route.query.siteId || siteName !== (this.$route.query.siteName || '')) {
this.$router.push({
path: this.$route.path,
query: {
...this.$route.query,
siteId: id,
siteName
}
})
}
this.$store.dispatch('getSiteAlarmNum', id)
},
showBigDataImg() {
this.$refs.bigDataPopup.show = true
},
toggleSideBar() { toggleSideBar() {
this.$store.dispatch('app/toggleSideBar') this.$store.dispatch('app/toggleSideBar')
}, },
@ -134,8 +93,7 @@ export default {
this.$store.dispatch('LogOut').then(() => { this.$store.dispatch('LogOut').then(() => {
location.href = '/index' location.href = '/index'
}) })
}).catch(() => { }).catch(() => {})
})
} }
} }
} }
@ -147,7 +105,7 @@ export default {
overflow: hidden; overflow: hidden;
position: relative; position: relative;
background: #fff; background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, .08); box-shadow: 0 1px 4px rgba(0,21,41,.08);
.hamburger-container { .hamburger-container {
line-height: 46px; line-height: 46px;
@ -155,7 +113,7 @@ export default {
float: left; float: left;
cursor: pointer; cursor: pointer;
transition: background .3s; transition: background .3s;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color:transparent;
&:hover { &:hover {
background: rgba(0, 0, 0, .025) background: rgba(0, 0, 0, .025)
@ -181,33 +139,6 @@ export default {
height: 100%; height: 100%;
line-height: 50px; line-height: 50px;
.site-select-wrap {
display: inline-flex;
align-items: center;
height: 100%;
padding: 0 10px 0 14px;
vertical-align: top;
::v-deep .el-form-item__content {
line-height: 1;
}
::v-deep .el-input__inner {
border-radius: 16px;
}
}
.big-data-container {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 24px;
color: #5a5e66;
vertical-align: text-bottom;
cursor: pointer;
}
&:focus { &:focus {
outline: none; outline: none;
} }
@ -237,7 +168,6 @@ export default {
.avatar-wrapper { .avatar-wrapper {
margin-top: 10px; margin-top: 10px;
position: relative; position: relative;
padding-right: 10px;
.user-avatar { .user-avatar {
cursor: pointer; cursor: pointer;
@ -246,7 +176,7 @@ export default {
border-radius: 50%; border-radius: 50%;
} }
.user-nickname { .user-nickname{
position: relative; position: relative;
bottom: 10px; bottom: 10px;
font-size: 14px; font-size: 14px;
@ -262,7 +192,6 @@ export default {
} }
} }
} }
} }
} }
</style> </style>

View File

@ -1,60 +1,48 @@
<template> <template>
<div <div class="sidebar-logo-container" :class="{'collapse':collapse}" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
class="sidebar-logo-container"
:class="{ collapse: collapse }"
:style="{
backgroundColor:
sideTheme === 'theme-dark'
? variables.menuBackground
: variables.menuLightBackground,
}"
>
<transition name="sidebarLogoFade"> <transition name="sidebarLogoFade">
<router-link <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
v-if="collapse" <img :src="logo" class="sidebar-logo" />
key="collapse" <!-- <h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>-->
class="sidebar-logo-link"
to="/"
>
<img :src="logoSmall" class="sidebar-logo" />
<!-- <h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>-->
</router-link> </router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/"> <router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img :src="logo" class="sidebar-logo" /> <img :src="logo" class="sidebar-logo" />
<!-- <h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>--> <!-- <h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>-->
</router-link> </router-link>
</transition> </transition>
</div> </div>
</template> </template>
<script> <script>
import variables from "@/assets/styles/variables.scss"; import logoImg from '@/assets/logo/logo.png'
import logo from "@/assets/images/ems/logo.png"; import variables from '@/assets/styles/variables.scss'
import logoSmall from "@/assets/images/ems/logo-small.png"; import logo from '@/assets/images/ems/logo.png'
import logoLarge from '@/assets/images/ems/logo-large.png'
export default { export default {
name: "SidebarLogo", name: 'SidebarLogo',
props: { props: {
collapse: { collapse: {
type: Boolean, type: Boolean,
required: true, required: true
}, }
}, },
computed: { computed: {
variables() { variables() {
return variables; return variables
}, },
sideTheme() { sideTheme() {
return this.$store.state.settings.sideTheme; return this.$store.state.settings.sideTheme
}, }
}, },
data() { data() {
return { return {
title: process.env.VUE_APP_TITLE, title: process.env.VUE_APP_TITLE,
logo: logo, // logo: logoImg
logoSmall: logoSmall, logo:logo,
}; logoLarge:logoLarge
}, }
}; }
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -55,12 +55,3 @@ export default {
} }
} }
</script> </script>
<style lang="scss" scoped>
::v-deep{
//,.el-submenu.is-active>.el-submenu__title 选中了二级菜单的以及菜单
.el-menu-item.is-active{
background-color: rgba(0,0,0,0.1) !important;
}
}
</style>

View File

@ -6,16 +6,16 @@ const getQuerySiteId= {
} }
}, },
watch: { watch: {
'$route.query':{ '$store.state.ems.zdList':{
handler (newQuery,oldQuery) { handler (newQuery,oldQuery) {
if(!newQuery || newQuery.length === 0){return}
// 参数变化处理 // 参数变化处理
this.$nextTick(() => { this.$nextTick(() => {
const {siteId} =newQuery const {siteId} =newQuery[0]
if(siteId){ this.siteId = siteId
this.siteId = siteId siteId && this.init(newQuery.siteId)
siteId && this.init(newQuery.siteId) console.log('watch站点列表返回数据newQuery=',newQuery)
console.log('mixin=>getQuerySiteId=>页面参数siteId发生了变化,this.siteId=',this.siteId) console.log('设置页面siteIdthis.siteId=',this.siteId)
}
}) })
}, },
immediate: true, immediate: true,

View File

@ -1,24 +0,0 @@
// 定时刷新
const intervalUpdate= {
data: function () {
return {
intervalUpdateTimer:null
}
},
beforeDestroy() {
console.log('销毁之前 清空定时器')
if( this.intervalUpdateTimer) {
window.clearInterval(this.intervalUpdateTimer)
this.intervalUpdateTimer = null
}
},
methods:{
updateInterval: function (cn,time=60000) {
window.clearInterval(this.intervalUpdateTimer)
this.intervalUpdateTimer = null
this.intervalUpdateTimer = window.setInterval(cn,time)
}
}
}
export default intervalUpdate

View File

@ -10,16 +10,11 @@ import { isRelogin } from '@/utils/request'
NProgress.configure({ showSpinner: false }) NProgress.configure({ showSpinner: false })
const whiteList = ['/login', '/register'] const whiteList = ['/login', '/register']
const GLOBAL_SITE_STORAGE_KEY = 'global_site_id'
const isWhiteList = (path) => { const isWhiteList = (path) => {
return whiteList.some(pattern => isPathMatch(pattern, path)) return whiteList.some(pattern => isPathMatch(pattern, path))
} }
const shouldAppendSiteId = (path) => {
return !['/login', '/register', '/404', '/401'].includes(path) && !path.startsWith('/redirect')
}
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
NProgress.start() NProgress.start()
if (getToken()) { if (getToken()) {
@ -31,29 +26,12 @@ router.beforeEach((to, from, next) => {
} else if (isWhiteList(to.path)) { } else if (isWhiteList(to.path)) {
next() next()
} else { } else {
const routeSiteId = to.query?.siteId
if (routeSiteId) {
localStorage.setItem(GLOBAL_SITE_STORAGE_KEY, routeSiteId)
} else {
const globalSiteId = localStorage.getItem(GLOBAL_SITE_STORAGE_KEY)
if (globalSiteId && shouldAppendSiteId(to.path)) {
next({
path: to.path,
query: {
...to.query,
siteId: globalSiteId
},
hash: to.hash,
replace: true
})
return
}
}
if (store.getters.roles.length === 0) { if (store.getters.roles.length === 0) {
isRelogin.show = true isRelogin.show = true
// 判断当前用户是否已拉取完user_info信息 // 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(() => { store.dispatch('GetInfo').then(() => {
isRelogin.show = false isRelogin.show = false
store.dispatch('getZdList')
store.dispatch('GenerateRoutes').then(accessRoutes => { store.dispatch('GenerateRoutes').then(accessRoutes => {
// 根据roles权限生成可访问的路由表 // 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表 router.addRoutes(accessRoutes) // 动态添加可访问路由表

View File

@ -1,324 +1,174 @@
import Layout from "@/layout/index.vue"; import Layout from "@/layout/index.vue";
//todo delete 删除动态路由页面的定义 接口会传递进来
// const ems = [
// {
// path: '',
// component: Layout,
// redirect: 'noRedirect',
// children: [
// {
// path: 'zddt',
// component: () => import('@/views/ems/zddt/index'),
// name: 'zddt',
// meta: { title: '站点地图', icon: 'guide' }
// }
// ]
// }
// ]
// export default ems
//单站监控 //单站监控
// todo 本地设置了 hidden:true,不会显示在侧边栏,需要在系统管理、菜单管理中手动添加菜单后才会展示在侧边栏 // todo 本地设置了 hidden:true,不会显示在侧边栏,需要在系统管理、菜单管理中手动添加菜单后才会展示在侧边栏
export const dzjk = [ export const dzjk=[
{ {
path: '/dzjk', path: '/dzjk',
component: Layout, component: Layout,
redirect: '/dzjk/home', redirect: '/dzjk/home',
meta: {title: '单站监控', icon: 'dashboard',}, meta: { title: '单站监控', icon: 'server',},
alwaysShow: false, alwaysShow: false,
name: 'DzjkLocal', name:'Dzjk',
hidden: true, children: [
children: [ {
{ path: '/dzjk/home',
path: '', component: () => import('@/views/ems/dzjk/home/index.vue'),
component: () => import('@/views/ems/dzjk/index'), name: 'DzjkHome',
name: 'DzjkRoot', meta: { title: '站点首页',breadcrumb: false,activeMenu: '/dzjk/home',activeSecondMenuName:'DzjkHome' }
redirect: '/dzjk/home', },
{
path: '/dzjk/zxlt',
component: () => import('@/views/ems/dzjk/zxlt/index.vue'),
name: 'DzjkZxlt',
meta: { title: '主线路图',breadcrumb: false,activeMenu: '/dzjk/zxlt',activeSecondMenuName:'DzjkZxlt' }
},
{
path: '/dzjk/sbjk',
component: () => import('@/views/ems/dzjk/sbjk/index.vue'),
name: 'DzjkSbjk',
alwaysShow: false,
meta: { title: '设备监控',breadcrumb: false,activeMenu: '/dzjk/sbjk'},
hidden: false,
redirect: '/dzjk/sbjk/ssyx',
children: [
{
path: 'ssyx',
component: () => import('@/views/ems/dzjk/sbjk/ssyx/index.vue'),
name: 'DzjkSbjkSsyx',
hidden: true, hidden: true,
children: [ meta: { title: '实时运行',breadcrumb: false,activeMenu: '/dzjk/sbjk',activeSecondMenuName:'DzjkSbjk'},
{ },
path: '/dzjk/home', {
component: () => import('@/views/ems/dzjk/home/index.vue'), path: 'pcs',
name: 'DzjkHome', component: () => import('@/views/ems/dzjk/sbjk/pcs/index.vue'),
meta: { name: 'DzjkSbjkPcs',
title: '站点首页', hidden: true,
breadcrumb: false, meta: { title: 'PCS',breadcrumb: false,activeMenu: '/dzjk/sbjk',activeSecondMenuName:'DzjkSbjk'},
activeMenu: '/dzjk', },
activeSecondMenuName: 'DzjkHome' {
} path: 'bmszl',
}, component: () => import('@/views/ems/dzjk/sbjk/bmszl/index.vue'),
{ name: 'DzjkSbjkBmszl',
path: '/dzjk/zxlt', hidden: true,
component: () => import('@/views/ems/dzjk/zxlt/index.vue'), meta: { title: 'BMS总览',breadcrumb: false,activeMenu: '/dzjk/sbjk',activeSecondMenuName:'DzjkSbjk'},
name: 'DzjkZxlt', },
meta: { {
title: '主线路图', path: 'bmsdcc',
breadcrumb: false, component: () => import('@/views/ems/dzjk/sbjk/bmsdcc/index.vue'),
activeMenu: '/dzjk', name: 'DzjkSbjkBmsdcc',
activeSecondMenuName: 'DzjkZxlt' hidden: true,
} meta: { title: 'BMS电池簇',breadcrumb: false,activeMenu: '/dzjk/sbjk',activeSecondMenuName:'DzjkSbjk'},
}, },
{ {
path: '/dzjk/sbjk', path: 'dtdc',
component: () => import('@/views/ems/dzjk/sbjk/index.vue'), component: () => import('@/views/ems/dzjk/sbjk/dtdc/index.vue'),
name: 'DzjkSbjk', name: 'DzjkSbjkDtdc',
meta: {title: '设备监控', breadcrumb: false, activeMenu: '/dzjk'}, hidden: true,
redirect: '/dzjk/sbjk/ssyx', meta: { title: '单体电池',breadcrumb: false,activeMenu: '/dzjk/sbjk',activeSecondMenuName:'DzjkSbjk'},
children: [ },
{ {
path: 'ssyx', path: 'db',
component: () => import('@/views/ems/dzjk/sbjk/ssyx/index.vue'), component: () => import('@/views/ems/dzjk/sbjk/db/index.vue'),
name: 'DzjkSbjkSsyx', name: 'DzjkSbjkDb',
meta: { hidden: true,
title: '实时运行', meta: { title: '电表',breadcrumb: false,activeMenu: '/dzjk/sbjk',activeSecondMenuName:'DzjkSbjk'},
breadcrumb: false, },
activeMenu: '/dzjk', {
activeSecondMenuName: 'DzjkSbjk', path: 'yl',
deviceCategory: 'SSYX' component: () => import('@/views/ems/dzjk/sbjk/yl/index.vue'),
}, name: 'DzjkSbjkYl',
}, hidden: true,
{ meta: { title: '液冷',breadcrumb: false,activeMenu: '/dzjk/sbjk',activeSecondMenuName:'DzjkSbjk'},
path: 'ems', }
component: () => import('@/views/ems/dzjk/sbjk/ems/index.vue'), ]
name: 'DzjkSbjkEms', },
meta: { {
title: 'EMS', path: '/dzjk/gzgj',
breadcrumb: false, component: () => import('@/views/ems/dzjk/gzgj/index.vue'),
activeMenu: '/dzjk', name: 'DzjkGzgj',
activeSecondMenuName: 'DzjkSbjk', meta: { title: '故障告警',breadcrumb: false,activeMenu: '/dzjk/gzgj',activeSecondMenuName:'DzjkGzgj' }
deviceCategory: 'EMS' },
}, {
}, path: '/dzjk/tjbb',
{ component: () => import('@/views/ems/dzjk/tjbb/index.vue'),
path: 'pcs', name: 'DzjkTjbb',
component: () => import('@/views/ems/dzjk/sbjk/pcs/index.vue'), alwaysShow: false,
name: 'DzjkSbjkPcs', meta: {title: '统计报表', breadcrumb: false, activeMenu: '/dzjk/tjbb'},
meta: { hidden: false,
title: 'PCS', redirect: '/dzjk/tjbb/gltj',
breadcrumb: false, children: [
activeMenu: '/dzjk', {
activeSecondMenuName: 'DzjkSbjk', path: 'gltj',
deviceCategory: 'PCS' component: () => import('@/views/ems/dzjk/tjbb/gltj/index.vue'),
}, name: 'DzjkTjbbGltj',
}, hidden: true,
{ meta: { title: '概率统计',breadcrumb: false,activeMenu: '/dzjk/tjbb',activeSecondMenuName:'DzjkTjbb'},
path: 'bmszl', },
component: () => import('@/views/ems/dzjk/sbjk/bmszl/index.vue'), {
name: 'DzjkSbjkBmszl', path: 'glqx',
meta: { component: () => import('@/views/ems/dzjk/tjbb/glqx/index.vue'),
title: 'BMS总览', name: 'DzjkTjbbGlqx',
breadcrumb: false, hidden: true,
activeMenu: '/dzjk', meta: { title: '功率曲线',breadcrumb: false,activeMenu: '/dzjk/tjbb',activeSecondMenuName:'DzjkTjbb'},
activeSecondMenuName: 'DzjkSbjk', },
deviceCategory: 'STACK' {
}, path: 'pcsqx',
}, component: () => import('@/views/ems/dzjk/tjbb/pcsqx/index.vue'),
{ name: 'DzjkTjbbPcsqx',
path: 'bmsdcc', hidden: true,
component: () => import('@/views/ems/dzjk/sbjk/bmsdcc/index.vue'), meta: { title: 'PCS曲线',breadcrumb: false,activeMenu: '/dzjk/tjbb',activeSecondMenuName:'DzjkTjbb'},
name: 'DzjkSbjkBmsdcc', },
meta: { {
title: 'BMS电池簇', path: 'dcdqx',
breadcrumb: false, component: () => import('@/views/ems/dzjk/tjbb/dcdqx/index.vue'),
activeMenu: '/dzjk', name: 'DzjkTjbbDcdqx',
activeSecondMenuName: 'DzjkSbjk', hidden: true,
deviceCategory: 'CLUSTER' meta: { title: '电池堆曲线',breadcrumb: false,activeMenu: '/dzjk/tjbb',activeSecondMenuName:'DzjkTjbb'},
}, },
}, {
{ path: 'dcwd',
path: 'dtdc', component: () => import('@/views/ems/dzjk/tjbb/dcwd/index.vue'),
component: () => import('@/views/ems/dzjk/sbjk/dtdc/index.vue'), name: 'DzjkTjbbDcwd',
name: 'DzjkSbjkDtdc', hidden: true,
meta: { meta: { title: '电池温度',breadcrumb: false,activeMenu: '/dzjk/tjbb',activeSecondMenuName:'DzjkTjbb'},
title: 'BMS单体电池', },
breadcrumb: false, {
activeMenu: '/dzjk', path: 'dbbb',
activeSecondMenuName: 'DzjkSbjk', component: () => import('@/views/ems/dzjk/tjbb/dbbb/index.vue'),
deviceCategory: 'BATTERY' name: 'DzjkTjbbDbbb',
}, hidden: true,
}, meta: { title: '电表报表',breadcrumb: false,activeMenu: '/dzjk/tjbb',activeSecondMenuName:'DzjkTjbb'},
{ }
path: 'db', ]
component: () => import('@/views/ems/dzjk/sbjk/db/index.vue'), },
name: 'DzjkSbjkDb', {
meta: { path: '/dzjk/clpz',
title: '电表', component: () => import('@/views/ems/dzjk/clpz/clyx/index.vue'),
breadcrumb: false, name: 'DzjkClpz',
activeMenu: '/dzjk', meta: {title: '策略配置', breadcrumb: false, activeMenu: '/dzjk/clpz'},
activeSecondMenuName: 'DzjkSbjk', }
deviceCategory: 'AMMETER'
},
},
{
path: 'yl',
component: () => import('@/views/ems/dzjk/sbjk/yl/index.vue'),
name: 'DzjkSbjkYl',
meta: {
title: '冷却',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'COOLING'
},
},
{
path: 'dh',
component: () => import('@/views/ems/dzjk/sbjk/dh/index.vue'),
name: 'DzjkSbjkDh',
meta: {
title: '动环',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'DH'
},
},
{
path: 'xf',
component: () => import('@/views/ems/dzjk/sbjk/xf/index.vue'),
name: 'DzjkSbjkXf',
meta: {
title: '消防',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'XF'
},
}
]
},
{
path: '/dzjk/gzgj',
component: () => import('@/views/ems/dzjk/gzgj/index.vue'),
name: 'DzjkGzgj',
meta: {
title: '故障告警',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkGzgj'
}
},
{
path: '/dzjk/tjbb',
component: () => import('@/views/ems/dzjk/tjbb/index.vue'),
name: 'DzjkTjbb',
meta: {title: '统计报表', breadcrumb: false, activeMenu: '/dzjk'},
redirect: '/dzjk/tjbb/gltj',
children: [
{
path: 'gltj',
component: () => import('@/views/ems/dzjk/tjbb/gltj/index.vue'),
name: 'DzjkTjbbGltj',
meta: {
title: '运行统计',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkTjbb'
},
},
{
path: 'glqx',
component: () => import('@/views/ems/dzjk/tjbb/glqx/index.vue'),
name: 'DzjkTjbbGlqx',
meta: {
title: '功率曲线',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkTjbb'
},
},
{
path: 'pcsqx',
component: () => import('@/views/ems/dzjk/tjbb/pcsqx/index.vue'),
name: 'DzjkTjbbPcsqx',
meta: {
title: 'PCS曲线',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkTjbb'
},
},
{
path: 'dcdqx',
component: () => import('@/views/ems/dzjk/tjbb/dcdqx/index.vue'),
name: 'DzjkTjbbDcdqx',
meta: {
title: '电池堆曲线',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkTjbb'
},
},
{
path: 'dcwd',
component: () => import('@/views/ems/dzjk/tjbb/dcwd/index.vue'),
name: 'DzjkTjbbDcwd',
meta: {
title: '电池温度',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkTjbb'
},
},
{
path: 'dbbb',
component: () => import('@/views/ems/dzjk/tjbb/dbbb/index.vue'),
name: 'DzjkTjbbDbbb',
meta: {
title: '电表报表',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkTjbb'
},
},
{
path: 'sybb',
component: () => import('@/views/ems/dzjk/tjbb/sybb/index.vue'),
name: 'DzjkTjbbSybb',
meta: {
title: '收益报表',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkTjbb'
},
}
]
},
{
path: '/dzjk/clpz',
component: () => import('@/views/ems/dzjk/clpz/index.vue'),
name: 'DzjkClpz',
meta: {title: '策略配置', breadcrumb: false, activeMenu: '/dzjk'},
redirect: '/dzjk/clpz/clyx',
children: [
{
path: 'clyx',
component: () => import('@/views/ems/dzjk/clpz/clyx/index.vue'),
name: 'DzjkClpzClyx',
meta: {
title: '策略运行',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkClpz'
},
},
{
path: 'runtimeParam',
component: () => import('@/views/ems/dzjk/clpz/runtimeParam/index.vue'),
name: 'DzjkClpzRuntimeParam',
meta: {
title: '运行参数',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkClpz'
},
},
{
path: 'sbbh',
component: () => import('@/views/ems/site/sbbh/index.vue'),
name: 'DzjkClpzSbbh',
meta: {
title: '设备保护',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkClpz'
},
},
// {
// path: 'xftg',
// component: () => import('@/views/ems/dzjk/clpz/xftg/index.vue'),
// hidden:true,
// breadcrumb: false,
// name: 'DzjkClpzXftg',
// meta: { title: '削峰填谷',breadcrumb: false,activeMenu: '/dzjk',activeSecondMenuName:'DzjkClpz'},
// }
]
}
]
}
] ]
} }
] ]

View File

@ -1,5 +1,7 @@
import Vue from 'vue' import Vue from 'vue'
import Router from 'vue-router' import Router from 'vue-router'
// todo delete
import ems from './ems'//EMS管理系统routers引用
import {dzjk} from '@/router/ems' import {dzjk} from '@/router/ems'
Vue.use(Router) Vue.use(Router)
@ -89,20 +91,8 @@ export const constantRoutes = [
} }
] ]
}, },
{
path: '/ems/site/zdlb',
component: Layout,
hidden: true,
children: [
{
path: 'monitor-point-mapping',
component: () => import('@/views/ems/site/zdlb/MonitorPointMapping.vue'),
name: 'MonitorPointMapping',
meta: { title: '单站监控项目点位配置', activeMenu: '/dzjk/clpz/sbbh' }
}
]
},
// EMS管理系统routers // EMS管理系统routers
// ...ems
...dzjk ...dzjk
] ]

View File

@ -7,12 +7,12 @@ module.exports = {
/** /**
* 侧边栏主题 深色主题theme-dark浅色主题theme-light * 侧边栏主题 深色主题theme-dark浅色主题theme-light
*/ */
sideTheme: 'theme-light', sideTheme: 'theme-dark',
/** /**
* 系统布局配置 * 系统布局配置
*/ */
showSettings: false, showSettings: true,
/** /**
* 是否显示顶部导航 * 是否显示顶部导航
@ -52,5 +52,5 @@ module.exports = {
/** /**
* 底部版权文本内容 * 底部版权文本内容
*/ */
footerContent: 'Copyright © 2025 上动新能源 版权所有' footerContent: 'Copyright © 2018-2025 xzzn. All Rights Reserved.'
} }

View File

@ -1,72 +1,37 @@
import {getAlarmDetailList, getSiteAllDeviceCategory} from '@/api/ems/dzjk' import {getAllSites} from '@/api/ems/zddt'
const ems = { const ems = {
state: { state: {
dzjkAlarmLighting: false,//单站监控 告警统计红点标志 zdList:[],
zdList: [], workStatusOptions:{'0':'正常','1':'异常','2':'停止'},//工作状态
zdDeviceCategoryOptions: {},//站点各个站点包含的设备种类 {021_DDS_01:["BATTERY","CLUSTER","STACK", "DH", "AMMETER", "PCS", "XF"],021_DDS_02:[]...} deviceStatusOptions:{'0':'在线','1':'离线','2':'维修中'},//设备状态
CLUSTERWorkStatusOptions: {'0': '静置', '1': '充电', '2': '放电', '3': '待机', '5': '运行', '9': "故障"},//电池簇工作状态 gridStatusOptions:{'0':'并网','1':'未并网'},//并网状态
PCSWorkStatusOptions: {'0': '运行', '1': '停机', '2': '故障', '3': '待机', '4': '充电', '5': '放电'},//PCS工作状态 controlModeOptions:{'0':'远程','1':'本地'},//控制模式
STACKWorkStatusOptions: { warnOptions:{0:'正常', 1:'中断', 2:'不在线',3:'异常'},//告警状态
"0": "静置", communicationStatusOptions:{'0':'正常','1':'通讯中断','2':'异常'},//通讯状态
"1": "充电", workModeOptions:{'0':'正常','1':'停止'},//工作模式
"2": "放电", alarmLevelOptions:{'A':'提示','B':'一般','C':'严重','D':'紧急'},//告警等级
"3": "浮充", alarmStatusOptions:{'0':'待处理','1':'已处理','2':'处理中'},//告警状态
'4': '待机', deviceTypeOptions:{'TCP':'TCP','RTU':'RTU'},//设备类型
'5': '运行', ticketStatusOptions:{0:'待处理', 1:'已处理', 2:'处理中'},//工单处理状态
'9': "故障" strategyStatusOptions:{'0':'未启用', '1':'已运行', '2':'已暂停', '3':'禁用', '4':'删除'},//策略状态
},//STACKBMS总览工作状态 chargeStatusOptions:{'1':'充电','2':'待机'},//冲放状态
deviceStatusOptions: {'0': '离线', '1': '在线'},//设备状态 },
gridStatusOptions: {'0': '并网', '1': '未并网'},//并网状态 mutations: {
controlModeOptions: {'0': '远程', '1': '本地'},//控制模式 SET_ZD_LIST(state, list) {
warnOptions: {0: '正常', 1: '中断', 2: '不在线', 3: '异常'},//告警状态 state.zdList = list || []
communicationStatusOptions: {'0': '正常', '1': '通讯中断', '2': '异常'},//通讯状态 }
workModeOptions: {'0': '正常', '1': '停止'},//工作模式 },
alarmLevelOptions: {'A': '提示', 'B': '一般', 'C': '严重', 'D': '紧急'},//告警等级 actions: {
alarmStatusOptions: {'0': '待处理', '1': '已处理', '2': '处理中'},//告警状态 getZdList({commit,state}){
deviceTypeOptions: {'TCP': 'TCP', 'RTU': 'RTU'},//设备类型 if(state.zdList.length === 0){
ticketStatusOptions: {1: '待处理', 2: '处理中', 3: '已处理'},//工单处理状态 getAllSites().then(response => {
strategyStatusOptions: {'0': '未启用', '1': '已运行', '2': '已暂停', '3': '禁用', '4': '删除'},//策略状态 commit('SET_ZD_LIST', response?.data || [])
chargeStatusOptions: {'1': '充电', '2': '待机', '3': '放电'},//冲放状态 console.log('store action getZdList 获取站点数据',state.zdList)
comparisonOperatorOptions: {'>': '>', '<': '<', '=': '=', '>=': '>=', '<=': '<='}, })
relationWithPoint: {'||': '||', '&&': '&&'} }
},
mutations: {
SET_ZD_LIST(state, list) {
state.zdList = list || []
},
SET_DZJK_ALARM_LIGHTING(state, status) {
state.dzjkAlarmLighting = status
},
SET_ZD_DEVICE_CATEGORY_OPTIONS(state, {siteId, data}) {
state.zdDeviceCategoryOptions = Object.assign({}, state.zdDeviceCategoryOptions, {[siteId]: data})
}
},
actions: {
//查询站点的所有待处理0的告警 存在展示红点标志
getSiteAlarmNum({state, commit}, siteId) {
getAlarmDetailList({
status: 0,
siteId,
pageSize: 10,
pageNum: 1,
deviceId: '',
alarmLevel: '',
alarmStartTime: '',
alarmEndTime: ''
}).then(response => {
commit('SET_DZJK_ALARM_LIGHTING', !!response?.total || false)
})
},
getSiteDeviceCategory({state, commit}, siteId) {
getSiteAllDeviceCategory(siteId).then(response => {
let data = response?.data || [];
data.unshift('SSYX');
commit('SET_ZD_DEVICE_CATEGORY_OPTIONS', {siteId, data})
})
}
} }
}
} }
export default ems export default ems

View File

@ -34,12 +34,6 @@ const permission = {
return new Promise(resolve => { return new Promise(resolve => {
// 向后端请求路由数据 // 向后端请求路由数据
getRouters().then(res => { getRouters().then(res => {
let hasDzjk = false
if(res?.data){
res.data.forEach(i=>{
i.children && i.children.find(j=>j.path.indexOf('dzjk')>-1) && (hasDzjk=true)
})
}
const sdata = JSON.parse(JSON.stringify(res.data)) const sdata = JSON.parse(JSON.stringify(res.data))
const rdata = JSON.parse(JSON.stringify(res.data)) const rdata = JSON.parse(JSON.stringify(res.data))
const sidebarRoutes = filterAsyncRouter(sdata) const sidebarRoutes = filterAsyncRouter(sdata)
@ -47,13 +41,6 @@ const permission = {
const asyncRoutes = filterDynamicRoutes(dynamicRoutes) const asyncRoutes = filterDynamicRoutes(dynamicRoutes)
rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true }) rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true })
router.addRoutes(asyncRoutes) router.addRoutes(asyncRoutes)
// 后端已下发 dzjk 菜单时,移除本地静态 dzjk 路由,避免重名/重复注册
if (hasDzjk) {
const index = constantRoutes.findIndex(i=>i.path.indexOf('dzjk')>-1)
if (index > -1) {
constantRoutes.splice(index, 1)
}
}
commit('SET_ROUTES', rewriteRoutes) commit('SET_ROUTES', rewriteRoutes)
commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes)) commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))
commit('SET_DEFAULT_ROUTES', sidebarRoutes) commit('SET_DEFAULT_ROUTES', sidebarRoutes)
@ -124,16 +111,11 @@ export function filterDynamicRoutes(routes) {
} }
export const loadView = (view) => { export const loadView = (view) => {
const normalizedView = String(view || '') if (process.env.NODE_ENV === 'development') {
.replace(/^\.\/+/, '') return (resolve) => require([`@/views/${view}`], resolve)
.replace(/^\/+/, '')
.replace(/^@\/views\//, '')
if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'staging') {
return (resolve) => require([`@/views/${normalizedView}`], resolve)
} else { } else {
// 使用 import 实现生产环境的路由懒加载 // 使用 import 实现生产环境的路由懒加载
return () => import(`@/views/${normalizedView}`) return () => import(`@/views/${view}`)
} }
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<div v-loading="loading"> <div v-loading="loading" class="ems-dashboard-editor-container ems-content-container-padding">
<template v-if="!showTemp"> <template v-if="!showTemp">
<el-button v-if="!showTemp" type="primary" plain @click="settingStrategy" style="margin-bottom: 20px;">新增策略</el-button> <el-button v-if="!showTemp" type="primary" plain @click="settingStrategy" style="margin-bottom: 20px;">新增策略</el-button>
<cl-container v-for="(item,index) in list" :key="index+'clContainer'" :info="item" :hide-setting-btn="showTemp" class="contain" @update="init" @showSetting="settingStrategy(item)"> <cl-container v-for="(item,index) in list" :key="index+'clContainer'" :info="item" :hide-setting-btn="showTemp" class="contain" @update="init" @showSetting="settingStrategy(item)">

View File

@ -1,52 +0,0 @@
<template>
<div class="ems-dashboard-editor-container ems-third-menu-container">
<el-menu
class="ems-third-menu"
:default-active="$route.name"
background-color="#ffffff"
text-color="#666666"
active-text-color="#ffffff"
>
<el-menu-item :index="item.name" v-for="(item,index) in childrenRoute" :key="index+'clpzChildrenRoute'">
<router-link style="height: 100%;width: 100%;display: block;" :to="{path:item.path,query:$route.query}">
{{item.meta.title}}
</router-link>
</el-menu-item>
</el-menu>
<div class="ems-content-container ems-content-container-padding clpz-ems-content-container">
<keep-alive>
<router-view></router-view>
</keep-alive>
</div>
</div>
</template>
<script>
import { dzjk } from '@/router/ems'
const childrenRoute = dzjk[0].children[0].children.find(item=> item.name==='DzjkClpz').children.filter(item=>!item?.hidden)//获取到统计报表下面的字路由
console.log('设备监控子路由',childrenRoute)
export default {
name:'DzjkClpz',
data(){
return {
childrenRoute,
activeMenu:''
}
},
mounted() {
console.log('当前统计报表页面路由',this.$route)
}
}
</script>
<style scoped lang="scss">
.clpz-ems-content-container{
margin-top:0;
padding-top:0;
padding-right: 0;
flex: 1;
}
</style>

View File

@ -1,248 +0,0 @@
<template>
<div class="ems-dashboard-editor-container" v-loading="loading">
<el-card shadow="never" class="common-card-container">
<div slot="header" class="clearfix">
<span class="card-title">运行参数配置</span>
<span class="site-tag">站点{{ siteId || '-' }}</span>
</div>
<el-form ref="form" :model="form" :rules="rules" label-width="150px">
<el-row :gutter="20" class="runtime-grid">
<el-col :xs="24" :sm="24" :md="8">
<div class="group-card">
<div class="group-title">SOC上下限</div>
<el-form-item label="SOC下限(%)" prop="socDown">
<el-input-number v-model="form.socDown" :min="0" :max="100" :step="1" :precision="2" :controls="false" style="width: 160px;" />
</el-form-item>
<el-form-item label="SOC上限(%)" prop="socUp">
<el-input-number v-model="form.socUp" :min="0" :max="100" :step="1" :precision="2" :controls="false" style="width: 160px;" />
</el-form-item>
</div>
</el-col>
<el-col :xs="24" :sm="24" :md="8">
<div class="group-card">
<div class="group-title">防逆流参数</div>
<el-form-item label="防逆流阈值(kW)" prop="antiReverseThreshold">
<el-input-number v-model="form.antiReverseThreshold" :min="0" :step="1" :precision="2" :controls="false" style="width: 160px;" />
</el-form-item>
<el-form-item label="阈值上浮比例(%)" prop="antiReverseRangePercent">
<el-input-number v-model="form.antiReverseRangePercent" :min="0" :step="1" :precision="2" :controls="false" style="width: 160px;" />
</el-form-item>
<el-form-item label="恢复上限(kW)" prop="antiReverseUp">
<el-input-number v-model="form.antiReverseUp" :min="0" :step="1" :precision="2" :controls="false" style="width: 160px;" />
</el-form-item>
<el-form-item label="降功率比例(%)" prop="antiReversePowerDownPercent">
<el-input-number v-model="form.antiReversePowerDownPercent" :min="0" :max="100" :step="1" :precision="2" :controls="false" style="width: 160px;" />
</el-form-item>
<el-form-item label="硬停阈值(kW)" prop="antiReverseHardStopThreshold">
<el-input-number v-model="form.antiReverseHardStopThreshold" :min="0" :step="1" :precision="2" :controls="false" style="width: 160px;" />
</el-form-item>
</div>
</el-col>
<el-col :xs="24" :sm="24" :md="8">
<div class="group-card">
<div class="group-title">功率与保护</div>
<el-form-item label="设定功率倍率" prop="powerSetMultiplier">
<el-input-number v-model="form.powerSetMultiplier" :min="0.0001" :step="0.1" :precision="4" :controls="false" style="width: 160px;" />
</el-form-item>
<el-form-item label="保护介入" prop="protectInterveneEnable">
<el-switch v-model="form.protectInterveneEnable" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="一级降额比例(%)" prop="protectL1DeratePercent">
<el-input-number v-model="form.protectL1DeratePercent" :min="0" :max="100" :step="1" :precision="2" :controls="false" style="width: 160px;" />
</el-form-item>
<el-form-item label="释放稳定时长(s)" prop="protectRecoveryStableSeconds">
<el-input-number v-model="form.protectRecoveryStableSeconds" :min="0" :max="3600" :step="1" :precision="0" :controls="false" style="width: 160px;" />
</el-form-item>
<el-form-item label="三级锁存" prop="protectL3LatchEnable">
<el-switch v-model="form.protectL3LatchEnable" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="冲突策略" prop="protectConflictPolicy">
<el-select v-model="form.protectConflictPolicy" style="width: 160px;">
<el-option label="最高等级优先" value="MAX_LEVEL_WIN" />
</el-select>
</el-form-item>
</div>
</el-col>
</el-row>
<div class="action-row">
<el-button type="primary" :loading="saveLoading" @click="handleSave">保存</el-button>
<el-button @click="init">重置</el-button>
</div>
</el-form>
</el-card>
</div>
</template>
<script>
import getQuerySiteId from '@/mixins/ems/getQuerySiteId'
import { getStrategyRuntimeConfig, saveStrategyRuntimeConfig } from '@/api/ems/dzjk'
const emptyForm = () => ({
siteId: '',
socDown: 0,
socUp: 100,
antiReverseThreshold: 30,
powerSetMultiplier: 10,
antiReverseRangePercent: 20,
antiReverseUp: 100,
antiReversePowerDownPercent: 10,
antiReverseHardStopThreshold: 20,
protectInterveneEnable: 1,
protectL1DeratePercent: 50,
protectRecoveryStableSeconds: 5,
protectL3LatchEnable: 1,
protectConflictPolicy: 'MAX_LEVEL_WIN'
})
export default {
name: 'DzjkClpzRuntimeParam',
mixins: [getQuerySiteId],
data() {
return {
loading: false,
saveLoading: false,
form: emptyForm(),
rules: {
socDown: [
{ required: true, message: '请输入SOC下限', trigger: 'change' }
],
socUp: [
{ required: true, message: '请输入SOC上限', trigger: 'change' }
],
antiReverseThreshold: [
{ required: true, message: '请输入防逆流阈值', trigger: 'change' }
],
powerSetMultiplier: [
{ required: true, message: '请输入设定功率倍率', trigger: 'change' }
],
antiReverseRangePercent: [
{ required: true, message: '请输入防逆流阈值上浮比例', trigger: 'change' }
],
antiReverseUp: [
{ required: true, message: '请输入防逆流恢复上限', trigger: 'change' }
],
antiReversePowerDownPercent: [
{ required: true, message: '请输入防逆流降功率比例', trigger: 'change' }
],
antiReverseHardStopThreshold: [
{ required: true, message: '请输入防逆流硬停阈值', trigger: 'change' }
],
protectInterveneEnable: [
{ required: true, message: '请选择是否启用保护介入', trigger: 'change' }
],
protectL1DeratePercent: [
{ required: true, message: '请输入一级降额比例', trigger: 'change' }
],
protectRecoveryStableSeconds: [
{ required: true, message: '请输入释放稳定时长', trigger: 'change' }
],
protectL3LatchEnable: [
{ required: true, message: '请选择三级锁存开关', trigger: 'change' }
],
protectConflictPolicy: [
{ required: true, message: '请选择冲突策略', trigger: 'change' }
]
}
}
},
methods: {
init() {
if (!this.siteId) {
this.form = emptyForm()
return
}
this.loading = true
getStrategyRuntimeConfig(this.siteId).then(response => {
const data = response?.data || {}
this.form = {
...emptyForm(),
...data,
siteId: this.siteId
}
}).finally(() => {
this.loading = false
})
},
handleSave() {
if (!this.siteId) {
this.$message.error('缺少站点ID')
return
}
this.$refs.form.validate(valid => {
if (!valid) return
if (Number(this.form.socDown) > Number(this.form.socUp)) {
this.$message.error('SOC下限不能大于SOC上限')
return
}
if (Number(this.form.powerSetMultiplier) <= 0) {
this.$message.error('设定功率倍率必须大于0')
return
}
if (Number(this.form.protectL1DeratePercent) < 0 || Number(this.form.protectL1DeratePercent) > 100) {
this.$message.error('一级降额比例必须在0~100之间')
return
}
if (Number(this.form.protectRecoveryStableSeconds) < 0) {
this.$message.error('释放稳定时长不能小于0')
return
}
this.saveLoading = true
saveStrategyRuntimeConfig({ ...this.form, siteId: this.siteId }).then(response => {
if (response?.code === 200) {
this.$message.success('保存成功')
this.init()
}
}).finally(() => {
this.saveLoading = false
})
})
}
}
}
</script>
<style scoped lang="scss">
.site-tag {
float: right;
color: #909399;
font-size: 13px;
}
.common-card-container {
border: none;
box-shadow: none !important;
}
.runtime-grid {
max-width: 1180px;
display: flex;
flex-wrap: wrap;
}
.runtime-grid > .el-col {
display: flex;
}
.group-card {
border: none;
border-radius: 4px;
padding: 14px 14px 2px;
min-height: 330px;
width: 100%;
height: 100%;
margin-bottom: 12px;
}
.group-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 14px;
}
.action-row {
display: flex;
justify-content: center;
margin-top: 24px;
}
</style>

View File

@ -1,221 +1,124 @@
<template> <template>
<el-dialog :visible.sync="dialogTableVisible" class="ems-dialog add-template-dialog" <el-dialog :visible.sync="dialogTableVisible" class="ems-dialog" :title="mode === 'add'?'新增模板':`编辑模板` ">
:title="mode === 'add'?'新增模板':`编辑模板` "> <el-form ref="addTempForm" :model="formData" :rules="rules" size="medium" label-width="100px">
<el-form ref="addTempForm" :model="formData" :rules="rules" size="medium" label-width="100px"> <el-form-item label="模板名称" prop="templateName">
<el-form-item label="模板名称" prop="templateName"> <el-input v-model="formData.templateName" placeholder="请输入" clearable :style="{width: '100%'}">
<el-input v-model="formData.templateName" placeholder="请输入" clearable :style="{width: '100%'}"> </el-input>
</el-input> </el-form-item>
</el-form-item> <el-form-item label="soc限制" prop="sdcLimit" required>
<el-form-item label="soc限制" prop="sdcLimit" required> <el-switch :active-value="1" :inactive-value="0" v-model="formData.sdcLimit"></el-switch>
<el-switch :active-value="1" :inactive-value="0" v-model="formData.sdcLimit"></el-switch> </el-form-item>
</el-form-item> <!-- <template v-if="formData.sdcLimit === 1">-->
</el-form>
<el-button type="primary" size="mini" @click="addTime">新增</el-button>
<!-- 新增时间段表单-->
<el-collapse-transition>
<el-card v-show="showAddTime" shadow="always" class="common-card-container" style="margin-top:25px;">
<el-form class="add-time-form transition-box" ref="addTimeForm" :model="formInline" :rules="formInlineRule"
label-width="100px" style="margin-top:25px">
<!-- <el-form-item label="开始时间" prop="startTime">-->
<!-- <el-time-select-->
<!-- placeholder="开始时间"-->
<!-- v-model="formInline.startTime"-->
<!-- :picker-options="{-->
<!-- start: '00:00',-->
<!-- step: '00:01',-->
<!-- end: '23:00',-->
<!-- }">-->
<!-- </el-time-select>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="结束时间" prop="endTime">-->
<!-- <el-time-select-->
<!-- placeholder="结束时间"-->
<!-- v-model="formInline.endTime"-->
<!-- :picker-options="{-->
<!-- start: '00:00',-->
<!-- step: '00:01',-->
<!-- end: '23:00',-->
<!-- minTime: formInline.startTime-->
<!-- }">-->
<!-- </el-time-select>-->
<!-- </el-form-item>-->
<el-form-item label="时间范围" prop="timeRange">
<el-time-picker
is-range
v-model="formInline.timeRange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
placeholder="选择时间范围"
format="HH:mm"
value-format="HH:mm"
:style="{width: '100%'}">
</el-time-picker>
</el-form-item>
<el-form-item label="冲放功率" prop="chargeDischargePower">
<el-input v-model="formInline.chargeDischargePower" placeholder="请输入"
:style="{width: '100%'}"></el-input>
</el-form-item>
<el-form-item label="soc下限" prop="sdcDown"> <el-form-item label="soc下限" prop="sdcDown">
<el-input v-model="formInline.sdcDown" placeholder="请输入" clearable :style="{width: '100%'}"></el-input> <el-input v-model="formData.sdcDown" placeholder="请输入" clearable :style="{width: '100%'}"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="soc上限" prop="sdcUp"> <el-form-item label="soc上限" prop="sdcUp">
<el-input v-model="formInline.sdcUp" placeholder="请输入" clearable :style="{width: '100%'}"></el-input> <el-input v-model="formData.sdcUp" placeholder="请输入" clearable :style="{width: '100%'}"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="充电状态" prop="chargeStatus"> <!-- </template>-->
<el-select v-model="formInline.chargeStatus" placeholder="请选择" :style="{width: '100%'}"> </el-form>
<el-option v-for="(value,key) in chargeStatusOptions" :key="key+'chargeStatusOptions'" :label="value" <el-button type="primary" size="mini" @click="addTime">新增</el-button>
:value="key"></el-option> <!-- 新增时间段表单-->
</el-select> <el-collapse-transition>
</el-form-item> <el-card v-show="showAddTime" shadow="always" class="common-card-container" style="margin-top:25px;">
<el-form-item> <el-form class="add-time-form transition-box" ref="addTimeForm" :model="formInline" :rules="formInlineRule" label-width="100px" style="margin-top:25px">
<el-button type="primary" size="mini" @click="saveTime">保存</el-button> <el-form-item label="开始时间" prop="startTime">
<el-button size="mini" @click="cancelAddTime">取消</el-button> <el-time-select
</el-form-item> placeholder="开始时间"
</el-form> v-model="formInline.startTime"
</el-card> :picker-options="{
</el-collapse-transition> start: '00:00',
<el-table step: '01:00',
end: '23:00',
}">
</el-time-select>
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-time-select
placeholder="结束时间"
v-model="formInline.endTime"
:picker-options="{
start: '00:00',
step: '01:00',
end: '23:00',
minTime: formInline.startTime
}">
</el-time-select>
</el-form-item>
<el-form-item label="冲放功率" prop="chargeDischargePower">
<el-input v-model="formInline.chargeDischargePower" placeholder="请输入"></el-input>
</el-form-item>
<el-form-item label="充电状态" prop="chargeStatus">
<el-select v-model="formInline.chargeStatus" placeholder="请选择">
<el-option v-for="(value,key) in chargeStatusOptions" :key="key+'chargeStatusOptions'" :label="value" :value="key"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" @click="saveTime">保存</el-button>
<el-button size="mini" @click="cancelAddTime">取消</el-button>
</el-form-item>
</el-form>
</el-card>
</el-collapse-transition>
<el-table
:data="tableData" :data="tableData"
border border
style="width: 100%;margin-top:25px"> style="width: 100%;margin-top:25px">
<el-table-column <!-- todo 如果要在span-method中使用column.property 在表格中必须定义prop="xxx"属性-->
<el-table-column
prop="startTime" prop="startTime"
label="开始时间"> label="开始时间">
<template slot-scope="scope"> </el-table-column>
<el-time-select <el-table-column
v-if="mode === 'edit'"
v-model="scope.row.startTime"
placeholder="开始时间"
:picker-options="{
start: '00:00',
step: '00:01',
end: '23:59'
}"
:style="{width: '100%'}"
/>
<span v-else>{{ scope.row.startTime || '-' }}</span>
</template>
</el-table-column>
<el-table-column
prop="endTime" prop="endTime"
label="结束时间"> label="结束时间">
<template slot-scope="scope"> </el-table-column>
<el-time-select <el-table-column
v-if="mode === 'edit'"
v-model="scope.row.endTime"
placeholder="结束时间"
:picker-options="{
start: '00:00',
step: '00:01',
end: '23:59',
minTime: scope.row.startTime
}"
:style="{width: '100%'}"
/>
<span v-else>{{ scope.row.endTime || '-' }}</span>
</template>
</el-table-column>
<el-table-column
prop="chargeDischargePower" prop="chargeDischargePower"
label="充放功率kW"> label="充放功率kW">
<template slot-scope="scope"> </el-table-column>
<el-input <el-table-column
v-if="mode === 'edit'"
v-model.trim="scope.row.chargeDischargePower"
placeholder="请输入"
clearable
:style="{width: '100%'}"
/>
<span v-else>{{ scope.row.chargeDischargePower || '-' }}</span>
</template>
</el-table-column>
<el-table-column
prop="sdcDown"
label="SOC下限">
<template slot-scope="scope">
<el-input
v-if="mode === 'edit'"
v-model.trim="scope.row.sdcDown"
placeholder="请输入"
clearable
:style="{width: '100%'}"
/>
<span v-else>{{ scope.row.sdcDown === null || scope.row.sdcDown === undefined || scope.row.sdcDown === '' ? '-' : scope.row.sdcDown + '%' }}</span>
</template>
</el-table-column>
<el-table-column
prop="sdcUp"
label="SOC上限">
<template slot-scope="scope">
<el-input
v-if="mode === 'edit'"
v-model.trim="scope.row.sdcUp"
placeholder="请输入"
clearable
:style="{width: '100%'}"
/>
<span v-else>{{ scope.row.sdcUp === null || scope.row.sdcUp === undefined || scope.row.sdcUp === '' ? '-' : scope.row.sdcUp + '%' }}</span>
</template>
</el-table-column>
<el-table-column
prop="chargeStatus" prop="chargeStatus"
label="充电状态"> label="充电状态">
<template slot-scope="scope"> <template slot-scope="scope">
<el-select {{chargeStatusOptions[scope.row.chargeStatus]}}
v-if="mode === 'edit'" </template>
v-model="scope.row.chargeStatus" </el-table-column>
placeholder="请选择" <el-table-column
:style="{width: '100%'}"
>
<el-option
v-for="(value,key) in chargeStatusOptions"
:key="key+'chargeStatusEditOptions'"
:label="value"
:value="key"
/>
</el-select>
<span v-else>{{ chargeStatusOptions[scope.row.chargeStatus] }}</span>
</template>
</el-table-column>
<el-table-column
fixed="right" fixed="right"
label="操作" label="操作"
width="120"> width="120">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button <el-button
@click.native.prevent="deleteRow(scope.$index, tableData)" @click.native.prevent="deleteRow(scope.$index, tableData)"
type="warning" type="warning"
size="mini"> size="mini">
删除 删除
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div slot="footer"> <div slot="footer">
<el-button @click="closeDialog">取消</el-button> <el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="saveDialog">确定</el-button> <el-button type="primary" @click="saveDialog">确定</el-button>
</div> </div>
</el-dialog> </el-dialog>
</template> </template>
<script> <script>
import {mapState} from 'vuex' import {mapState} from 'vuex'
import {addStrategyTemp, editStrategyTemp, getStrategyTempDetail} from '@/api/ems/dzjk' import {addStrategyTemp,editStrategyTemp,getStrategyTempDetail} from '@/api/ems/dzjk'
export default { export default {
inject: ['$home'], inject:['$home'],
data() { data() {
const now = new Date()
const year = now.getFullYear(), month = now.getMonth(), day = now.getDate()
const range = [new Date(year, month, day, 0), new Date(year, month, day, 23)]
return { return {
mode: '', mode:'',
editTempId: '', editTempId:'',
dialogTableVisible: false, dialogTableVisible:false,
secondRange: range,
formData: { formData: {
templateName: '', templateName: '',
sdcLimit: false, sdcLimit: false,
sdcDown: '',
sdcUp: '',
}, },
rules: { rules: {
templateName: [{ templateName: [{
@ -223,44 +126,45 @@ export default {
message: '请输入', message: '请输入',
trigger: 'blur' trigger: 'blur'
}], }],
sdcDown: [
{required: true,message: '请输入', trigger: 'blur'},
{ pattern: /^(0|[1-9]\d*)(\.\d+)?$/, message: '请输入合法数字或小数' }
],
sdcUp: [
{required: true,message: '请输入', trigger: 'blur'},
{ pattern: /^(0|[1-9]\d*)(\.\d+)?$/, message: '请输入合法数字或小数' }
],
}, },
showAddTime: false, showAddTime: false,
formInline: { formInline:{
timeRange: range, startTime:'',endTime:'',chargeDischargePower:'',chargeStatus:''
chargeDischargePower: '',
sdcDown: '',
sdcUp: '',
chargeStatus: ''
}, },
formInlineRule: { formInlineRule:{
timeRange: [{ startTime: [{
required: true, required: true,
message: '请选择时间范围', message: '请选择开始时间',
trigger: 'change'
}],
endTime: [{
required: true,
message: '请选择结束时间',
trigger: 'change' trigger: 'change'
}], }],
chargeDischargePower: [{ chargeDischargePower: [{
required: true, required: true,
message: '请输入冲放功率', message: '请输入冲放功率',
trigger: 'blur' trigger: 'blur'
}, },
{pattern: /^-?\d*\.?\d*$/, message: '请输入合法数字或小数'} { pattern: /^(0|[1-9]\d*)(\.\d+)?$/, message: '请输入合法数字或小数' }
], ],
sdcDown: [ chargeStatus:[{
{required: true, message: '请输入', trigger: 'blur'},
{pattern: /^(0|[1-9]\d*)(\.\d+)?$/, message: '请输入合法数字或小数'}
],
sdcUp: [
{required: true, message: '请输入', trigger: 'blur'},
{pattern: /^(0|[1-9]\d*)(\.\d+)?$/, message: '请输入合法数字或小数'}
],
chargeStatus: [{
required: true, required: true,
message: '请选择充放状态', message: '请选择充放状态',
trigger: ['blur', 'change'] trigger: ['blur','change']
} }
] ]
}, },
tableData: [], tableData:[],
} }
}, },
computed: { computed: {
@ -268,183 +172,145 @@ export default {
chargeStatusOptions: state => state?.ems?.chargeStatusOptions || {}, chargeStatusOptions: state => state?.ems?.chargeStatusOptions || {},
}) })
}, },
watch: {
"formInline.startTime":{
handler(newVal){
if(newVal && this.formInline.endTime){
const endTime = parseInt((this.formInline.endTime).split(':')[0] || 0)
const startTime =parseInt(newVal.split(':')[0])
if(endTime<=startTime){
this.formInline.endTime = `${startTime+1 <=9 ? '0'+(startTime+1) : startTime+1}:00`
}
}
},
deep:true
},
},
methods: { methods: {
changeSiteId() { changeSiteId(){
this.dialogTableVisible = false this.dialogTableVisible=false
this.mode = '' this.mode=''
this.editTempId = '' this.editTempId=''
this.formData = { this.formData={
templateName: '', templateName: '',
sdcLimit: false, sdcLimit: false,
sdcDown: '',
sdcUp: '',
}
this.formInline={
startTime:'',endTime:'',chargeDischargePower:'',chargeStatus:''
} }
this.formInline = {
timeRange: this.secondRange, chargeDischargePower: '', sdcDown: '', sdcUp: '', chargeStatus: ''
}//startTime: '', endTime: '',
this.showAddTime = false this.showAddTime = false
this.tableData = [] this.tableData=[]
}, },
show({mode = 'add', editTempId = ''}) { show({mode = 'add', editTempId = ''}){
this.$nextTick(() => { this.$nextTick(() => {
this.dialogTableVisible = true this.dialogTableVisible = true
this.mode = mode this.mode = mode
this.editTempId = editTempId this.editTempId=editTempId
if (mode === 'edit' && editTempId) { if(mode === 'edit' && editTempId){
getStrategyTempDetail(this.editTempId).then(response => { getStrategyTempDetail(this.editTempId).then(response => {
const data = JSON.parse(JSON.stringify(response?.data || [])); const data=JSON.parse(JSON.stringify(response?.data || []));
if (data.length > 0) { if(data.length>0){
const {templateName, sdcLimit} = JSON.parse(JSON.stringify(data[0])); const {templateName,sdcLimit,sdcDown,sdcUp} =JSON.parse(JSON.stringify( data[0]));
this.formData.templateName = templateName this.formData.templateName=templateName
this.formData.sdcLimit = sdcLimit this.formData.sdcLimit=sdcLimit
this.formData.sdcDown=sdcDown
this.formData.sdcUp=sdcUp
} }
if (data.length === 1) { if(data.length === 1){
const {startTime, endTime} = data[0]; const {startTime,endTime}=data;
if (!startTime || !endTime) { if(!startTime || !endTime){
this.tableData = [] this.tableData = []
} else { }else{
this.tableData = data this.tableData= data
} }
} else { }else{
this.tableData = data this.tableData= data
} }
}) })
} }
}) })
}, },
addTime() { addTime(){
this.showAddTime = true this.showAddTime=true
}, },
cancelAddTime() { cancelAddTime(){
this.$refs.addTimeForm.resetFields() this.$refs.addTimeForm.resetFields()
this.showAddTime = false this.showAddTime=false
this.formInline = {timeRange: this.secondRange, chargeDischargePower: '', sdcDown: '', sdcUp: '', chargeStatus: ''}//startTime: '', endTime: '', this.formInline = {startTime:'',endTime:'',chargeDischargePower:'',chargeStatus:''}
}, },
saveTime() { saveTime(){
//表单校验校验成功添加到tableData里 //表单校验校验成功添加到tableData里
this.$refs.addTimeForm.validate(valid => { this.$refs.addTimeForm.validate(valid => {
if (!valid) return if (!valid) return
const {timeRange: [startTime, endTime], chargeDischargePower, sdcDown, sdcUp, chargeStatus} = this.formInline this.tableData.push(JSON.parse(JSON.stringify(this.formInline)));
this.$nextTick(() => {this.cancelAddTime()})
this.tableData.push({startTime, endTime, chargeDischargePower, sdcDown, sdcUp, chargeStatus})
this.$nextTick(() => {
this.cancelAddTime()
})
}) })
}, },
deleteRow(index) { deleteRow(index){
this.tableData.splice(index, 1) this.tableData.splice(index,1)
}, },
saveDialog() { saveDialog() {
this.$refs.addTempForm.validate(valid => { this.$refs.addTempForm.validate(valid => {
if (!valid) return if (!valid) return
const {templateName, sdcLimit} = this.formData //校验时间选择范围是否冲突
const {siteId, updateStrategyId} = this.$home let status = true
const tableData = this.tableData.map(item => ({ this.tableData.forEach((outer,outerIndex)=>{
...item, const {startTime, endTime}=outer
sdcDown: this.normalizeSocValue(item.sdcDown), const outerStart = parseInt(startTime),outerEnd = parseInt(endTime)
sdcUp: this.normalizeSocValue(item.sdcUp) if(outerStart>outerEnd){
})) status = false
if (!this.validateTableData(tableData)) return }else{
if (this.mode === 'edit') { this.tableData.forEach((inner,innerIndex)=>{
editStrategyTemp({ if(innerIndex !== outerIndex){
siteId, const {startTime:innerStartTime, endTime:innerEndTime}=inner
strategyId: updateStrategyId, const innerStart = parseInt(innerStartTime),innerEnd = parseInt(innerEndTime)
templateId: this.editTempId, if((innerStart<outerStart && innerEnd>outerEnd) || !((innerStart<outerStart && innerEnd<=outerStart) || (innerStart>=outerEnd && innerEnd>outerEnd))){
templateName, status=false
sdcLimit, }
timeConfigList: tableData }
}).then(response => { })
if (response?.code === 200) {
this.closeDialog()
this.$emit('update')
this.$emit('updateTimeSetting')
} }
}) })
} else { if(!status){
addStrategyTemp({ return this.$message.error('时间选择范围冲突');
siteId, }
strategyId: updateStrategyId, const {templateName,sdcLimit,sdcDown,sdcUp} = this.formData
templateName, const {siteId,updateStrategyId} =this.$home
sdcLimit, const {tableData} = this
timeConfigList: tableData if(this.mode==='edit'){
}).then(response => { editStrategyTemp({siteId,strategyId:updateStrategyId,templateId:this.editTempId,templateName,sdcLimit,sdcDown,sdcUp,timeConfigList:tableData}).then(response=>{
if (response?.code === 200) { if(response?.code === 200){
this.closeDialog() this.closeDialog()
this.$emit('update') this.$emit('update')
} this.$emit('updateTimeSetting')
}) }
} })
}else{
addStrategyTemp({siteId,strategyId:updateStrategyId,templateName,sdcLimit,sdcDown,sdcUp,timeConfigList:tableData}).then(response=>{
if(response?.code === 200){
this.closeDialog()
this.$emit('update')
}
})
}
}) })
}, },
normalizeSocValue(value) { closeDialog(){
if (value === null || value === undefined) return null
const normalized = String(value).replace('%', '').trim()
return normalized === '' ? null : normalized
},
toMinutes(timeValue) {
if (!timeValue || String(timeValue).indexOf(':') < 0) return -1
const [h, m] = String(timeValue).split(':')
const hour = Number(h), minute = Number(m)
if (!Number.isInteger(hour) || !Number.isInteger(minute)) return -1
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return -1
return hour * 60 + minute
},
validateTableData(list = []) {
const numberPattern = /^-?\d+(\.\d+)?$/
const socPattern = /^(0|[1-9]\d*)(\.\d+)?$/
for (let i = 0; i < list.length; i++) {
const row = list[i]
const rowNo = i + 1
if (!row.startTime || !row.endTime) {
this.$message.error(`${rowNo}行:开始时间和结束时间不能为空`)
return false
}
const startMinute = this.toMinutes(row.startTime)
const endMinute = this.toMinutes(row.endTime)
if (startMinute < 0 || endMinute < 0 || startMinute >= endMinute) {
this.$message.error(`${rowNo}行:时间范围不合法`)
return false
}
if (!numberPattern.test(String(row.chargeDischargePower ?? '').trim())) {
this.$message.error(`${rowNo}行:充放功率格式不正确`)
return false
}
if (!socPattern.test(String(row.sdcDown ?? '').trim())) {
this.$message.error(`${rowNo}SOC下限格式不正确`)
return false
}
if (!socPattern.test(String(row.sdcUp ?? '').trim())) {
this.$message.error(`${rowNo}SOC上限格式不正确`)
return false
}
if (Number(row.sdcDown) > Number(row.sdcUp)) {
this.$message.error(`${rowNo}SOC下限不能大于SOC上限`)
return false
}
if (row.chargeStatus === undefined || row.chargeStatus === null || row.chargeStatus === '') {
this.$message.error(`${rowNo}行:请选择充电状态`)
return false
}
}
return true
},
closeDialog() {
// 清空所有数据 // 清空所有数据
this.$refs.addTempForm.resetFields() this.$refs.addTempForm.resetFields()
this.formData = { this.formData={
templateName: '', templateName: '',
sdcLimit: 0, sdcLimit:0,
sdcDown: '',
sdcUp: '',
} }
this.tableData = [] this.tableData=[]
this.cancelAddTime() this.cancelAddTime()
this.dialogTableVisible = false this.dialogTableVisible=false
} }
} }
} }
</script> </script>
<style lang="scss" scoped>
.add-template-dialog {
max-height: 90vh;
overflow-y: auto;
}
</style>

View File

@ -47,34 +47,11 @@ export default {
}, },
setOption(data) { setOption(data) {
if(!this.chart) return if(!this.chart) return
let obj = {}
for(var i=0;i<=23;i++){
obj[i] = {
title:i<=9?`0${i}:00` : `${i}:00`
}
}
const nowMonth = new Date().getMonth()+1;
const localMonth = data.find(item=>item.month === nowMonth)?.powerList || []
localMonth.forEach(item => {
const startHours = parseInt(item.startTime.split(':')[0], 10)
const endHours =parseInt(item.endTime.split(':')[0], 10)
for(let i=startHours;i<=endHours;i++){
obj[i].value = item.powerData
}
})
let source = [['时间','冲放功率']]
Object.values(obj).forEach(item => {
const {title,value} = item
source.push([title,value])
})
this.chart.setOption({ this.chart.setOption({
color:['#FFBD00','#3C81FF'], color:['#FFBD00','#3C81FF'],
grid: {
containLabel: true
},
legend: { legend: {
left: 'center', left: 'center',
bottom: '15', bottom: '10',
}, },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
@ -86,7 +63,7 @@ export default {
color:"#333333", color:"#333333",
}, },
xAxis: { xAxis: {
type: 'category', data: ['01:00','02:00','03:00','05:00','06:00','07:00','08:00','09:00','10:00'],
axisLine: { axisLine: {
lineStyle:{ lineStyle:{
color: '#333333', color: '#333333',
@ -101,14 +78,16 @@ export default {
} }
} }
}, },
dataset: {
source
},
series: [ series: [
{ {
name:'冲放功率', name:'模板一',
data: [80,92,1,34,90,130,320,80,9,91],
type: 'line', type: 'line',
}] },{
name:'模板二',
data: [820,932,901,934,1290,1330,1320,820,932,901],
type: 'line',
}]
}) })
} }
} }

View File

@ -39,14 +39,14 @@
prop="sdcDown" prop="sdcDown"
label="SOC下限"> label="SOC下限">
<template slot-scope="scope"> <template slot-scope="scope">
{{scope.row.sdcDown === null || scope.row.sdcDown === undefined || scope.row.sdcDown === '' ? '-' : scope.row.sdcDown + '%'}} {{scope.row.sdcDown ? scope.row. sdcDown + '%' : '-'}}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
prop="sdcUp" prop="sdcUp"
label="SOC上限"> label="SOC上限">
<template slot-scope="scope"> <template slot-scope="scope">
{{scope.row.sdcUp === null || scope.row.sdcUp === undefined || scope.row.sdcUp === '' ? '-' : scope.row.sdcUp + '%'}} {{scope.row.sdcUp ? scope.row.sdcUp + '%' : '-'}}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
@ -92,7 +92,7 @@ export default {
activeBtn:'', activeBtn:'',
tempList:[], tempList:[],
tableData:[], tableData:[],
mixinPrototype:['templateName','sdcLimit'] mixinPrototype:['templateName','sdcLimit','sdcDown','sdcUp']
} }
}, },
computed:{ computed:{

View File

@ -2,7 +2,7 @@
<template> <template>
<!-- <cl-container :hideSettingBtn="true">--> <!-- <cl-container :hideSettingBtn="true">-->
<!-- <template v-slot:default>--> <!-- <template v-slot:default>-->
<div> <div class="ems-dashboard-editor-container ems-content-container-padding">
<temp-table ref="tempTable" @updateTimeSetting="updateTimeSetting"/> <temp-table ref="tempTable" @updateTimeSetting="updateTimeSetting"/>
<time-setting ref="timeSetting"/> <time-setting ref="timeSetting"/>
<temp-power-chart ref="tomePowerChart"/> <temp-power-chart ref="tomePowerChart"/>
@ -40,6 +40,7 @@ export default {
this.$refs.tomePowerChart.changeSiteId() this.$refs.tomePowerChart.changeSiteId()
}) })
}, },
//在编辑、删除模板后更新时间配置、echart的数据todo
updateTimeSetting(){ updateTimeSetting(){
this.$refs.timeSetting.init() this.$refs.timeSetting.init()
this.$refs.tomePowerChart.init() this.$refs.tomePowerChart.init()

View File

@ -1,30 +1,27 @@
<template> <template>
<el-card v-loading="loading" gshadow="always" class="common-card-container common-card-container-no-title-bg"> <div v-loading="loading" class="ems-dashboard-editor-container ems-content-container-padding">
<!-- 搜索栏--> <!-- 搜索栏-->
<el-form :inline="true" class="select-container"> <el-form :inline="true" class="select-container">
<el-form-item label="设备清单"> <el-form-item label="设备类型">
<el-select v-model="search.deviceId" clearable placeholder="请选择" :loading="loading" <el-select v-model="search.deviceType" clearable placeholder="请选择" :loading="loading" loading-text="正在加载数据">
loading-text="正在加载数据"> <el-option :label="value" :value="key" v-for="(value,key) in $store.state.ems.deviceTypeOptions" :key="key+'deviceTypeOptions'"></el-option>
<el-option :label="item.deviceName" :value="item.deviceId" v-for="(item,key) in deviceOptions"
:key="key+'deviceIdOptions'"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="告警等级"> <el-form-item label="告警等级">
<el-select v-model="search.alarmLevel" clearable placeholder="请选择" :loading="loading" <el-select v-model="search.alarmLevel" clearable placeholder="请选择" :loading="loading" loading-text="正在加载数据">
loading-text="正在加载数据" style="width: 130px"> <el-option :label="value" :value="key" v-for="(value,key) in $store.state.ems.alarmLevelOptions" :key="key+'alarmLevelOptions'"></el-option>
<el-option :label="value" :value="key" v-for="(value,key) in $store.state.ems.alarmLevelOptions"
:key="key+'alarmLevelOptions'"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="时间选择"> <el-form-item label="时间选择">
<el-date-picker <el-date-picker
v-model="dateRange" v-model="dateRange"
type="daterange" type="daterange"
range-separator="" range-separator=""
start-placeholder="开始时间" start-placeholder="开始时间"
:picker-options="pickerOptions" :picker-options="pickerOptions"
:default-value="defaultDateRange" :default-value="defaultDateRange"
end-placeholder="结束时间"> end-placeholder="结束时间">
</el-date-picker> </el-date-picker>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
@ -39,9 +36,7 @@
<el-row style=""> <el-row style="">
<el-col :xs="24" :sm="24" :lg="24"> <el-col :xs="24" :sm="24" :lg="24">
<el-button-group class="ems-btns-group"> <el-button-group class="ems-btns-group">
<el-button v-for="(item,index) in btnList" :key="index+'dtdcBtns'" <el-button v-for="(item,index) in btnList" :key="index+'dtdcBtns'" :class="{'activeBtn' : activeBtn === item.id}" @click="changeDataType(item.id)">{{item.name}}</el-button>
:class="{'activeBtn' : activeBtn === item.id}" @click="changeDataType(item.id)">{{ item.name }}
</el-button>
</el-button-group> </el-button-group>
</el-col> </el-col>
</el-row> </el-row>
@ -52,226 +47,203 @@
stripe stripe
max-height="500" max-height="500"
style="width: 100%;margin-top:25px;"> style="width: 100%;margin-top:25px;">
<el-table-column <el-table-column
prop="deviceName" prop="deviceName"
label="设备名称"> label="设备名称">
</el-table-column> </el-table-column>
<el-table-column <el-table-column
label="告警等级" label="告警等级"
> >
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ $store.state.ems.alarmLevelOptions[scope.row.alarmLevel] }}</span> <span>{{$store.state.ems.alarmLevelOptions[scope.row.alarmLevel]}}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
prop="alarmContent" prop="alarmContent"
show-overflow-tooltip show-overflow-tooltip
label="告警内容"> label="告警内容">
</el-table-column> </el-table-column>
<el-table-column <el-table-column
prop="alarmStartTime" prop="alarmStartTime"
label="告警发生时间"> label="告警发生时间">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ formatDate(scope.row.alarmStartTime, true) }}</span> <span>{{formatDate(scope.row.alarmStartTime,true)}}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
prop="alarmEndTime" prop="alarmEndTime"
label="告警结束时间"> label="告警结束时间">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ formatDate(scope.row.alarmEndTime, true) }}</span> <span>{{formatDate(scope.row.alarmEndTime,true)}}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
label="状态"> label="状态">
<template slot-scope="scope"> <template slot-scope="scope">
<span <span :class="['0','2'].includes(scope.row.status) ? 'warning-status' : ''">{{$store.state.ems.alarmStatusOptions[scope.row.status]}}</span>
:class="['0','2'].includes(scope.row.status) ? 'warning-status' : ''">{{ </template>
$store.state.ems.alarmStatusOptions[scope.row.status] </el-table-column>
}}</span> <el-table-column
</template>
</el-table-column>
<el-table-column
label="工单" label="工单"
fixed="right" fixed="right"
width="320" width="250"
> >
<template slot-scope="scope"> <template slot-scope="scope">
<el-button type="text" size="mini" v-if="scope.row.ticketNo" @click="toTicket"> <el-button type="text" size="mini" v-if="scope.row.ticketNo" @click="toTicket">已生成工单(工单号:{{scope.row.ticketNo}})</el-button>
已生成工单(工单号:{{ scope.row.ticketNo }}) <el-button type="primary" size="mini" v-else @click="toTicket">生成工单</el-button>
</el-button> </template>
<el-button type="primary" size="mini" v-else @click="createTicket(scope.row.id)">生成工单</el-button> </el-table-column>
<el-button </el-table>
v-if="scope.row.status !== '1'"
type="success"
size="mini"
style="margin-left: 8px;"
@click="closeAlarmRecord(scope.row.id)">
确认关闭
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination <el-pagination
v-show="tableData.length>0" v-show="tableData.length>0"
background background
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
:current-page="pageNum" :current-page="pageNum"
:page-size="pageSize" :page-size="pageSize"
:page-sizes="[10, 20, 30, 40]" :page-sizes="[10, 20, 30, 40]"
layout="total, sizes, prev, pager, next, jumper" layout="total, sizes, prev, pager, next, jumper"
:total="totalSize" :total="totalSize"
style="margin-top:15px;text-align: center" style="margin-top:15px;text-align: center"
> >
</el-pagination> </el-pagination>
</div> </div>
</el-card> </div>
</template> </template>
<script> <script>
import {closeAlarm, createTicketNo, getAlarmDetailList} from '@/api/ems/dzjk' import {getAlarmDetailList} from'@/api/ems/dzjk'
import {getDeviceList} from '@/api/ems/site'
import getQuerySiteId from "@/mixins/ems/getQuerySiteId"; import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {formatDate} from '@/filters/ems' import { formatDate } from '@/filters/ems'
export default { export default {
name: 'DzjkGzgj', name:'DzjkGzgj',
mixins: [getQuerySiteId], mixins:[getQuerySiteId],
data() { data() {
return { return {
loading: false, loading:false,
btnList: [ btnList:[
{name: '未处理告警', id: 'today'}, {name:'今日告警',id:'today'},
{name: '历史告警', id: 'history'}, {name:'历史告警',id:'history'},
], ],
deviceOptions: [],//设备列表 pickerOptions:{
pickerOptions: {
disabledDate(time) { disabledDate(time) {
return time.getTime() > Date.now(); return time.getTime() > Date.now();
}, },
}, },
defaultDateRange: [],//默认展示的时间 defaultDateRange:[],//默认展示的时间
dateRange: [],//alarmStartTime,alarmEndTime dateRange:[],//alarmStartTime,alarmEndTime
activeBtn: 'today', activeBtn:'today',
search: {deviceId: '', alarmLevel: ''}, search:{deviceType:'',alarmLevel:''},
// 表格、分页 // 表格、分页
tableData: [], tableData:[],
pageSize: 10,//分页栏当前每个数据总数 pageSize:10,//分页栏当前每个数据总数
pageNum: 1,//分页栏当前页数 pageNum:1,//分页栏当前页数
totalSize: 0,//table表格数据总数 totalSize:0,//table表格数据总数
} }
}, },
methods: { methods:{
formatDate, formatDate,
toTicket() { toTicket(){
this.$router.push({path: '/ticket'}) this.$router.push({path:'/ticket'})
},
//生成工单
createTicket(id) {
this.loading = true
createTicketNo({id}).then(response => {
response?.data && this.toTicket()
}).finally(() => {
this.loading = false
})
},
//确认关闭告警
closeAlarmRecord(id) {
this.$confirm('确认关闭该故障告警吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.loading = true
closeAlarm({id}).then(() => {
this.$message.success('关闭成功')
this.getData()
}).finally(() => {
this.loading = false
})
}).catch(() => {
})
}, },
// 判断是否是同一天 // 判断是否是同一天
isSameDay(day1, day2) { isSameDay(day1, day2) {
const date1 = new Date(day1), date2 = new Date(day2) const date1 = new Date(day1),date2 = new Date(day2)
return date1.getFullYear() === date2.getFullYear() && return date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() && date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate(); date1.getDate() === date2.getDate();
}, },
// 分页 // 分页
handleSizeChange(val) { handleSizeChange(val) {
this.pageSize = val; this.pageSize = val;
this.$nextTick(() => { this.$nextTick(()=>{
this.getData() this.getData()
}) })
}, },
handleCurrentChange(val) { handleCurrentChange(val) {
this.pageNum = val this.pageNum = val
this.$nextTick(() => { this.$nextTick(()=>{
this.getData() this.getData()
}) })
}, },
// 搜索 // 搜索
onSearch() { onSearch(){
this.pageNum = 1//每次搜索从1开始搜索 this.pageNum =1//每次搜索从1开始搜索
const [alarmStartTime='',alarmEndTime='']=(this.dateRange || [])
// 选中了时间范围
if(alarmStartTime && alarmStartTime){
// 如果选择的时间范围是今天
if(this.isSameDay(alarmStartTime,alarmEndTime) && this.isSameDay(alarmStartTime,new Date())){
this.activeBtn = 'today'
}else {
this.activeBtn = 'history'
}
}else{
//没有选择时间范围 还是按照选中的今日告警、历史告警查询
}
this.getData() this.getData()
}, },
// 重置 // 重置
onReset() { onReset(){
this.search = {deviceId: '', alarmLevel: ''} this.search={deviceType:'',alarmLevel:''}
this.dateRange = [] this.dateRange=[]
this.pageNum = 1//每次搜索从1开始搜索 this.pageNum =1//每次搜索从1开始搜索
this.getData() this.getData()
}, },
// 切换今日、历史告警 // 切换今日、历史告警
changeDataType(id) { changeDataType(id){
if (id !== this.activeBtn) { if(id !== this.activeBtn){
console.log('点击了不同的菜单,更新数据') console.log('点击了不同的菜单,更新数据')
this.activeBtn = id; this.activeBtn=id;
const [alarmStartTime,alarmEndTime]=(this.dateRange || [])
// 切换到今日告警,如果已经选择了时间范围清空
if(alarmStartTime && alarmEndTime){
// 如果切换到了今日告警,时间范围不相等或者相等但是不是今天 清空时间选择范围
if(id === 'today' && !this.isSameDay(alarmStartTime,alarmEndTime) || (this.isSameDay(alarmStartTime,alarmEndTime) && !this.isSameDay(alarmStartTime,new Date()))){
this.dateRange = []
}else if(id === 'history' && this.isSameDay(alarmStartTime,alarmEndTime) && this.isSameDay(alarmStartTime,new Date())){
// 切换成历史告警,但是选择时间范围是当天,清空时间范围
this.dateRange = []
}
}
this.getData() this.getData()
} }
}, },
// 获取数据 // 获取数据
getData() { getData(){
this.$store.dispatch('getSiteAlarmNum', this.siteId) this.loading=true
this.loading = true const {deviceType,alarmLevel} = this.search
const {deviceId, alarmLevel} = this.search const {siteId,pageNum,pageSize,activeBtn} =this
const {siteId, pageNum, pageSize, activeBtn} = this const [alarmStartTime='',alarmEndTime='']=(this.dateRange || [])
const [alarmStartTime = '', alarmEndTime = ''] = (this.dateRange || []) let start='',end = '',now =new Date()
let status = activeBtn === 'today' ? '0' : '1,2' if(activeBtn === 'today'){
getAlarmDetailList({ start = end = now
status, }else{
deviceId, if(alarmStartTime && alarmEndTime){
alarmLevel, start = alarmStartTime
siteId, end = alarmEndTime
pageSize, }else{
pageNum, start=''
alarmStartTime: formatDate(alarmStartTime), end = ''
alarmEndTime: formatDate(alarmEndTime) // now
}).then(response => { // end.setDate(end.getDate() - 1);
this.tableData = response?.rows || []; }
}
getAlarmDetailList({deviceType,alarmLevel,siteId,pageSize,pageNum,alarmStartTime:formatDate(start),alarmEndTime:formatDate(end)}).then(response => {
this.tableData=response?.rows || [];
this.totalSize = response?.total || 0 this.totalSize = response?.total || 0
}).finally(() => { }).finally(() => {this.loading=false})
this.loading = false
})
}, },
getDeviceOptions() { init(){
getDeviceList(this.siteId).then(response => {
this.deviceOptions = JSON.parse(JSON.stringify(response?.data || []))
})
},
init() {
this.getDeviceOptions()
this.onReset() this.onReset()
}, },
}, },
mounted() { mounted(){
const now = new Date(); const now = new Date();
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
this.defaultDateRange = [lastMonth, now]; this.defaultDateRange = [lastMonth, now];
} }
} }
</script> </script>

View File

@ -1,167 +0,0 @@
<template>
<el-card shadow="always" class="common-card-container common-card-container-body-no-padding time-range-card">
<div slot="header" class="time-range-header">
<span class="card-title">当日功率曲线</span>
<date-range-select ref="dateRangeSelect" :showIcon="true" :mini-time-picker="true" @updateDate="updateDate"/>
</div>
<div class="card-main">
<div id="activeChart" class="active-chart-canvas"></div>
</div>
</el-card>
</template>
<script>
import * as echarts from 'echarts'
import resize from '@/mixins/ems/resize'
import DateRangeSelect from '@/components/Ems/DateRangeSelect/index.vue'
import {getPointConfigCurve} from '@/api/ems/site'
import intervalUpdate from "@/mixins/ems/intervalUpdate";
export default {
mixins: [resize, intervalUpdate],
components: {DateRangeSelect},
props: {
displayData: {
type: Array,
default: () => []
}
},
data() {
return {
chart: null,
timeRange: [],
siteId: '',
isInit: true
}
},
watch: {
displayData() {
if (this.siteId && this.timeRange.length === 2) {
this.getGVQXData()
}
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
})
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
// 更新时间范围 重置图表
updateDate(data) {
this.timeRange = data
!this.isInit && this.getGVQXData()
this.isInit = false
},
getGVQXData() {
const {siteId, timeRange} = this
const displayData = this.displayData || []
const sectionRows = displayData.filter(item =>
item && item.sectionName === '当日功率曲线' && item.useFixedDisplay !== 1 && item.dataPoint
)
const tasks = sectionRows.map(row => {
const pointId = String(row.dataPoint || '').trim()
if (!pointId) return Promise.resolve(null)
return getPointConfigCurve({
siteId,
pointId,
pointType: 'data',
rangeType: 'custom',
startTime: this.normalizeDateTime(timeRange[0], false),
endTime: this.normalizeDateTime(timeRange[1], true)
}).then(curveResponse => {
const list = curveResponse?.data || []
return {
name: row.fieldName || row.fieldCode || pointId,
data: list
.map(item => [this.parseToTimestamp(item.dataTime), Number(item.pointValue)])
.filter(item => item[0] && !Number.isNaN(item[1]))
}
}).catch(() => null)
})
Promise.all(tasks).then(series => {
this.setOption((series || []).filter(Boolean))
})
},
init(siteId) {
//初始化 清空数据
this.siteId = siteId
this.isInit = true
this.timeRange = []
this.$refs.dateRangeSelect.init(true)
this.getGVQXData()
this.updateInterval(this.getGVQXData)
},
initChart() {
this.chart = echarts.init(document.querySelector('#activeChart'))
},
normalizeDateTime(value, endOfDay) {
const raw = String(value || '').trim()
if (!raw) return ''
if (raw.includes(' ')) return raw
return `${raw} ${endOfDay ? '23:59:59' : '00:00:00'}`
},
parseToTimestamp(value) {
if (!value) return null
const t = new Date(value).getTime()
return Number.isNaN(t) ? null : t
},
setOption(seriesData = []) {
this.chart.setOption({
grid: {
containLabel: true
},
legend: {
left: 'center',
bottom: '15',
},
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' }
},
textStyle: {
color: "#333333",
},
xAxis: {
type: 'time',
},
yAxis: [{
type: 'value',
}],
series: seriesData.map((item) => {
return {
name: item.name,
type: 'line',
showSymbol: false,
symbolSize: 2,
smooth: true,
areaStyle: {
opacity: 0.5,
},
data: item.data
}
})
})
},
}
}
</script>
<style scoped lang="scss">
.card-main {
padding: 0 16px 12px;
}
.active-chart-canvas {
height: 310px;
}
</style>

View File

@ -1,86 +0,0 @@
<template>
<!-- <el-card shadow="always" class="common-card-container common-card-container-body-no-padding">
<div slot="header">
<span class="card-title">当前报警</span>
</div>
<div class="ssgj-container">
<el-table
class="common-table"
:data="tableData"
height="100%"
stripe
style="width: 100%">
<el-table-column
prop="deviceName"
label="名称">
</el-table-column>
<el-table-column
label="状态"
>
<template slot-scope="scope">
<span :class="{'circle warning-status' : scope.row.status !== 0}">{{ $store.state.ems.warnOptions[scope.row.status]}}</span>
</template>
</el-table-column>
<el-table-column
class-name="alarm-content"
prop="alarmContent"
show-overflow-tooltip
label="告警内容">
</el-table-column>
<el-table-column
label="工单"
fixed="right"
show-overflow-tooltip
>
<template slot-scope="scope">
<el-button type="text" size="mini" v-if="scope.row.ticketNo" @click="toTicket">已生成工单(工单号:{{scope.row.ticketNo}})</el-button>
<el-button type="primary" size="mini" v-else @click="toTicket">生成工单</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-card> -->
<el-alert type="error" :closable="false">
<template>
<div style="cursor: pointer" @click="toAlarm">设备告警</div>
</template>
</el-alert>
</template>
<script>
export default {
props: {
tableData: {
require: true,
type: Array,
default: () => {
return [];
},
},
},
data() {
return {};
},
methods: {
toAlarm() {
this.$router.push({ path: "/dzjk/gzgj", query: this.$route.query });
},
toTicket() {
this.$router.push({ path: "/ticket" });
},
},
};
</script>
<style lang="scss" scoped>
//实时告警
.ssgj-container {
padding: 20px;
height: 250px;
box-sizing: border-box;
::v-deep {
.el-table .el-table__header-wrapper th,
.el-table .el-table__fixed-header-wrapper th {
background: #fff2cb;
}
}
}
</style>

View File

@ -1,82 +0,0 @@
<template>
<el-card
shadow="always"
class="common-card-container common-card-container-body-no-padding"
>
<div slot="header">
<span class="card-title">策略信息</span>
</div>
<!-- <el-empty :image-size="100" ></el-empty> -->
<div
style="
box-sizing: border-box;
height: 250px;
padding: 20px 15px;
overflow-y: auto;
"
>
<el-descriptions class="home-normal-info" :column="2">
<el-descriptions-item size="mini" label="模板名称">{{
info.mainStrategyName || "-"
}}</el-descriptions-item>
<el-descriptions-item size="mini" label="SOC限制">{{
mainInfo.sdcLimit === 1 ? "开" : mainInfo.sdcLimit === 0 ? "关" : "-"
}}</el-descriptions-item>
<el-descriptions-item size="mini" label="SOC下限%">{{
formatNumber(mainInfo.sdcDown)
}}</el-descriptions-item>
<el-descriptions-item size="mini" label="SOC上限%">{{
formatNumber(mainInfo.sdcUp)
}}</el-descriptions-item>
</el-descriptions>
<el-table
:data="info.siteMonitorDataVo || []"
border
size="mini"
style="width: 100%; margin-top: 15px"
>
<el-table-column prop="startTime" label="开始时间"> </el-table-column>
<el-table-column prop="endTime" label="结束时间"> </el-table-column>
<el-table-column prop="chargeDischargePower" label="充放功率kW">
</el-table-column>
<el-table-column prop="chargeStatus" label="充电状态">
<template slot-scope="scope">
{{ chargeStatusOptions[scope.row.chargeStatus] }}
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</template>
<script>
import { mapState } from "vuex";
import { formatNumber } from "@/filters/ems";
export default {
props: {
info: {
require: true,
type: Object,
default: () => {
return {};
},
},
},
computed: {
...mapState({
chargeStatusOptions: (state) => state?.ems?.chargeStatusOptions || {},
}),
mainInfo() {
return this.info?.siteMonitorDataVo?.length > 0
? this.info.siteMonitorDataVo[0]
: {};
},
},
data() {
return {};
},
methods: {
formatNumber,
},
};
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,148 @@
<template>
<el-row style="background:#fff;margin-top:30px;">
<el-col :xs="24" :sm="24" :lg="24">
<el-card shadow="always" class="common-card-container common-card-container-body-no-padding">
<div slot="header">
<span class="card-title">能量流转</span>
</div>
<div style="height: 310px" id="nllzChart"></div>
</el-card>
</el-col>
</el-row>
</template>
<script>
import * as echarts from 'echarts'
import resize from '@/mixins/ems/resize'
import {formatNumber} from "@/filters/ems";
export default {
mixins: [resize],
data() {
return {
chart: null
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
})
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(document.querySelector('#nllzChart'))
},
setOption(data) {
const {siteMonitorDataVo=[],gridNrtPower,loadNrtPower,energyStorageNrtPower,energyStorageAvailElec} = data
const source = [['日期','充电量','放电量']]
siteMonitorDataVo.forEach(item=>{
source.push([item.ammeterDate, item.chargedCap,item.disChargedCap])
})
this.chart.setOption({
title: [
// 右上角
{
text: `电网 实时功率:${formatNumber(gridNrtPower)} kW`,
top: 10,
right: 10,
textStyle:{
fontSize:12,
color:'#666666'
}
},
// 右下角
{
text: `负载 实时功率:${formatNumber(loadNrtPower)} kW`,
bottom: 10,
right: 10,
textStyle:{
fontSize:12,
color:'#666666'
}
},
// 左下角第一行
{
text: '储能',
bottom: 40,
left: 10,
textStyle:{
fontSize:12,
color:'#666666'
}
},
// 左下角第二行
{
text: `实时功率:${formatNumber(energyStorageNrtPower)} kW`,
bottom: 26,
left: 10,
textStyle:{
fontSize:12,
color:'#666666'
}
},
// 左下角第三行
{
text: `可用电量:${formatNumber(energyStorageAvailElec)} kWh`,
bottom: 10,
left: 10,
textStyle:{
fontSize:12,
color:'#666666'
}
}
],
grid:{
left:200
},
tooltip: {
trigger: 'axis',
axisPointer: { // 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
},
textStyle:{
color:"#333333",
},
xAxis: {
type: 'category',
axisLine: {
lineStyle:{
color: '#333333',
}
}
},
yAxis: {
type: 'value',
axisLine: {
lineStyle:{
color: '#333333',
}
}
},
dataset:{
source
// source: [['日期','充电量','放电量']]
},
series: [
{
name:'充电量',
type: 'line',
},{
name:'放电量',
type: 'line',
}]
})
}
}
}
</script>

View File

@ -1,371 +0,0 @@
<template>
<el-card shadow="always" class="common-card-container common-card-container-body-no-padding time-range-card">
<div slot="header" class="time-range-header">
<span class="card-title">{{ cardTitle }}</span>
<date-range-select ref="dateRangeSelect" :showIcon="true" :mini-time-picker="true" @updateDate="updateDate" />
</div>
<div class="card-main">
<div ref="weekChartRef" class="week-chart-canvas"></div>
</div>
</el-card>
</template>
<script>
import * as echarts from 'echarts'
import resize from '@/mixins/ems/resize'
import DateRangeSelect from '@/components/Ems/DateRangeSelect/index.vue'
import { getPointConfigCurve } from '@/api/ems/site'
const DAY = 24 * 60 * 60 * 1000
const TEXT = {
cardTitle: '\u4e00\u5468\u5145\u653e\u67f1\u72b6\u56fe',
sectionName: '\u4e00\u5468\u5145\u653e\u66f2\u7ebf',
empty: '\u6682\u65e0\u6570\u636e',
date: '\u65e5\u671f',
charge: '\u5145\u7535\u91cf',
discharge: '\u653e\u7535\u91cf',
yAxis: '\u5145\u7535\u91cf/\u653e\u7535\u91cf\uff08kWh\uff09',
xAxis: '\u5355\u4f4d\uff1a\u65e5'
}
function createEmptySummary() {
return {
totalChargedCap: '',
totalDisChargedCap: '',
efficiency: ''
}
}
export default {
mixins: [resize],
components: { DateRangeSelect },
props: {
displayData: {
type: Array,
default: () => []
}
},
data() {
return {
chart: null,
timeRange: [],
siteId: '',
summary: createEmptySummary(),
cardTitle: TEXT.cardTitle
}
},
watch: {
displayData() {
if (this.siteId && this.timeRange.length === 2) {
this.getWeekKData()
}
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
})
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
updateDate(data) {
this.timeRange = data
this.getWeekKData()
},
getWeekKData() {
const { siteId, timeRange } = this
const displayData = this.displayData || []
const sectionRows = displayData.filter(item =>
item && item.sectionName === TEXT.sectionName && item.useFixedDisplay !== 1 && item.dataPoint
)
const tasks = sectionRows.map(row => {
const pointId = String(row.dataPoint || '').trim()
if (!pointId) return Promise.resolve(null)
return getPointConfigCurve({
siteId,
pointId,
pointType: 'data',
rangeType: 'custom',
startTime: this.normalizeDateTime(timeRange[0], false),
endTime: this.normalizeDateTime(timeRange[1], true)
}).then(curveResponse => {
const list = curveResponse?.data || []
return {
name: row.fieldName || row.fieldCode || pointId,
fieldCode: row.fieldCode || '',
data: list
.map(item => [this.parseToTimestamp(item.dataTime), Number(item.pointValue)])
.filter(item => item[0] && !Number.isNaN(item[1]))
}
}).catch(() => null)
})
Promise.all(tasks).then(series => {
this.setOption((series || []).filter(Boolean))
})
},
init(siteId) {
this.siteId = siteId
this.timeRange = []
this.summary = createEmptySummary()
this.$refs.dateRangeSelect.init()
},
initChart() {
this.chart = echarts.init(this.$refs.weekChartRef)
},
normalizeDateTime(value, endOfDay) {
const raw = String(value || '').trim()
if (!raw) return ''
if (raw.includes(' ')) return raw
return `${raw} ${endOfDay ? '23:59:59' : '00:00:00'}`
},
parseToTimestamp(value) {
if (!value) return null
const t = new Date(value).getTime()
return Number.isNaN(t) ? null : t
},
startOfDay(timestamp) {
const date = new Date(timestamp)
date.setHours(0, 0, 0, 0)
return date.getTime()
},
formatNumber(value) {
const num = Number(value)
if (Number.isNaN(num)) return '--'
return num.toLocaleString('zh-CN', {
minimumFractionDigits: 0,
maximumFractionDigits: 2
})
},
formatDateLabel(timestamp) {
const date = new Date(timestamp)
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${month}-${day}`
},
formatTooltipDate(timestamp) {
const date = new Date(timestamp)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
buildDatasetSource(labels = [], chargeData = [], dischargeData = []) {
const source = [[TEXT.date, TEXT.charge, TEXT.discharge]]
labels.forEach((label, index) => {
source.push([
label,
Number(chargeData[index]?.value || 0),
Number(dischargeData[index]?.value || 0)
])
})
return source
},
resolveSeriesType(item = {}) {
const text = `${item?.name || ''} ${item?.fieldCode || ''}`.toLowerCase()
if (text.includes('\u653e') || text.includes('discharge') || text.includes('discharged')) {
return 'discharge'
}
if (text.includes('\u5145') || text.includes('charge') || text.includes('charged')) {
return 'charge'
}
return ''
},
buildDailyChartData(seriesData = []) {
const normalizedRange = this.timeRange || []
const startTime = this.parseToTimestamp(this.normalizeDateTime(normalizedRange[0], false))
const endTime = this.parseToTimestamp(this.normalizeDateTime(normalizedRange[1], true))
if (!startTime || !endTime || endTime < startTime) {
return {
labels: [],
chargeData: [],
dischargeData: [],
summary: createEmptySummary()
}
}
const bucketStarts = []
for (let cursor = this.startOfDay(startTime); cursor <= this.startOfDay(endTime); cursor += DAY) {
bucketStarts.push(cursor)
}
const chargeMap = {}
const dischargeMap = {}
;(seriesData || []).forEach(item => {
const seriesType = this.resolveSeriesType(item)
if (!seriesType) return
;(item?.data || []).forEach(([timestamp, pointValue]) => {
if (!timestamp || Number.isNaN(pointValue)) return
if (timestamp < startTime || timestamp > endTime) return
const bucketStart = this.startOfDay(timestamp)
const normalizedValue = Math.abs(Number(pointValue) || 0)
if (seriesType === 'charge') {
chargeMap[bucketStart] = (chargeMap[bucketStart] || 0) + normalizedValue
} else if (seriesType === 'discharge') {
dischargeMap[bucketStart] = (dischargeMap[bucketStart] || 0) + normalizedValue
}
})
})
const labels = []
const chargeData = []
const dischargeData = []
bucketStarts.forEach(bucketStart => {
const chargedCap = Number(chargeMap[bucketStart] || 0)
const disChargedCap = Number(dischargeMap[bucketStart] || 0)
labels.push(this.formatDateLabel(bucketStart))
chargeData.push({
value: chargedCap,
bucketStart
})
dischargeData.push({
value: disChargedCap,
bucketStart
})
})
const totalChargedCap = chargeData.reduce((sum, item) => sum + Number(item.value || 0), 0)
const totalDisChargedCap = dischargeData.reduce((sum, item) => sum + Number(item.value || 0), 0)
const efficiency = totalChargedCap > 0
? Number(((totalDisChargedCap / totalChargedCap) * 100).toFixed(2))
: 0
return {
labels,
chargeData,
dischargeData,
summary: {
totalChargedCap,
totalDisChargedCap,
efficiency
}
}
},
renderEmptyState(message = TEXT.empty) {
if (!this.chart) return
this.chart.clear()
this.chart.setOption({
graphic: {
type: 'text',
left: 'center',
top: 'middle',
style: {
text: message,
fill: '#909399',
fontSize: 14
}
}
})
},
setOption(seriesData = []) {
if (!this.chart) return
const { labels, chargeData, dischargeData, summary } = this.buildDailyChartData(seriesData)
const hasValue = chargeData.some(item => Number(item?.value || 0) > 0) || dischargeData.some(item => Number(item?.value || 0) > 0)
if (!labels.length || !hasValue) {
this.summary = createEmptySummary()
this.renderEmptyState()
return
}
this.summary = summary
const source = this.buildDatasetSource(labels, chargeData, dischargeData)
this.chart.clear()
this.chart.setOption({
color: ['#4472c4', '#70ad47'],
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: (params = []) => {
if (!params.length) return ''
const dataIndex = Number(params[0]?.dataIndex)
const bucketStart = chargeData[dataIndex]?.bucketStart
const lines = [this.formatTooltipDate(bucketStart)]
params.forEach(item => {
const rawValue = Array.isArray(item?.value) ? item.value[item.seriesIndex + 1] : item?.value
const value = Number(rawValue || 0)
lines.push(`${item.marker}${item.seriesName}: ${this.formatNumber(value)}kWh`)
})
return lines.join('<br/>')
},
extraCssText: 'max-width: 420px; white-space: normal;'
},
legend: {
left: 'center',
bottom: 15
},
grid: {
top: 40,
containLabel: true,
left: 20,
right: 20,
bottom: 60
},
yAxis: [{
type: 'value',
name: TEXT.yAxis,
axisLine: {
lineStyle: {
color: '#333333'
},
onZero: false
}
}],
xAxis: [{
type: 'category',
name: TEXT.xAxis,
nameLocation: 'center',
nameGap: 30,
axisTick: {
show: false
},
axisLabel: {
interval: 0
}
}],
dataset: {
source
},
series: [
{
name: TEXT.charge,
type: 'bar',
color: '#4472c4',
barMaxWidth: 22
},
{
name: TEXT.discharge,
type: 'bar',
color: '#70ad47',
barMaxWidth: 22
}
]
})
}
}
}
</script>
<style scoped lang="scss">
.card-main {
padding: 0 16px 12px;
}
.week-chart-canvas {
height: 310px;
}
</style>

View File

@ -1,841 +1,148 @@
<template> <template>
<div> <div v-loading="loading" class="ems-dashboard-editor-container">
<el-row style="background: #fff" class="row-container" :gutter="15"> <el-row :gutter="32" style="background:#fff;">
<el-col :xs="24" :sm="24" :lg="5"> <el-col :xs="24" :sm="24" :lg="10">
<!-- 站点信息--> <el-card shadow="always" class="common-card-container common-card-container-body-no-padding">
<el-card
shadow="always"
class="common-card-container common-card-container-body-no-padding"
>
<div slot="header"> <div slot="header">
<span class="card-title">站点信息</span> <span class="card-title">数据概览</span>
<div class="alarm-msg" v-if="tableData.length > 0" @click="toAlarm">
<i class="el-icon-message-solid"></i> 设备告警
</div>
</div> </div>
<div <div style="height: 310px" >
style="box-sizing: border-box; height: 218px; padding: 20px 15px" <el-row :gutter="20">
> <el-col :span="12" v-for="(item,index) in sjglData" :key="index+'sjglData'" class="sjgl-data">
<!-- 地址运行时间--> <div class="sjgl-title">{{item.title}}</div>
<div class="site-info site-info-address"> <div class="sjgl-value">{{item.value | formatNumber}}</div>
<div class="title">
<i class="el-icon-location"></i>
</div>
<div class="value">
<i v-if="isBaseInfoLoading" class="el-icon-loading"></i>
<span v-else>{{ info.siteAddress || '-' }}</span>
</div>
</div>
<div class="site-info">
<div class="title">
<i class="el-icon-date"></i>
</div>
<div class="value">
<i v-if="isBaseInfoLoading" class="el-icon-loading"></i>
<span v-else>{{ info.runningTime || '-' }}</span>
</div>
</div>
<!-- 装机功率容量 -->
<el-row :gutter="10" style="margin-top:20px;">
<el-col
:span="12"
class="sjgl-col power-col"
>
<div class="sjgl-wrapper">
<div class="sjgl-title">装机功率(MWh)</div>
<div class="sjgl-value">
<i v-if="isBaseInfoLoading" class="el-icon-loading"></i>
<span v-else>{{ info.installPower | formatNumber }}</span>
</div>
</div>
</el-col>
<el-col
:span="12"
class="sjgl-col power-col"
>
<div class="sjgl-wrapper">
<div class="sjgl-title">装机容量(MWh)</div>
<div class="sjgl-value">
<i v-if="isBaseInfoLoading" class="el-icon-loading"></i>
<span v-else>{{ info.installCapacity | formatNumber }}</span>
</div>
</div>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
</el-card> </el-card>
</el-col> </el-col>
<!-- 总累计运行数据--> <el-col :xs="24" :sm="24" :lg="14">
<el-col :xs="24" :sm="24" :lg="19"> <el-card shadow="always" class="common-card-container common-card-container-body-no-padding">
<el-card
shadow="always"
class="common-card-container common-card-container-body-no-padding"
>
<div slot="header"> <div slot="header">
<span class="card-title">总累计运行数据</span> <span class="card-title">实时告警</span>
<div class="total-count"> <!-- <el-button style="float: right; padding: 3px 0" type="text" size="small">通讯状态<span style="color:red">超时</span></el-button>-->
<span class="title pointer-field" @click="handleTotalRevenueClick">总收入</span>
<span
class="value pointer-field"
:class="{ 'field-disabled': !hasPointId(totalRevenueDisplayItem) }"
@click="handleTotalRevenueClick"
>
<i v-if="isRunningInfoLoading" class="el-icon-loading"></i>
<span v-else>{{ totalRevenueDisplayValue | formatNumber }}</span>
</span>
<span class="unit"></span>
</div>
</div> </div>
<div <div class="ssgj-container">
style="box-sizing: border-box; height: 218px; padding: 20px 15px" <el-table
> class="common-table"
<el-row :gutter="10"> :data="tableData"
<el-col height="100%"
:span="6" stripe
v-for="(item, index) in runningDataCards" style="width: 100%">
:key="index + 'sjglData'" <el-table-column
class="sjgl-col" prop="deviceName"
> label="名称">
<div </el-table-column>
class="sjgl-wrapper pointer-field" <el-table-column
:class="{ 'field-disabled': !hasPointId(item.raw) }" label="状态"
@click="handleRunningFieldClick(item)"
> >
<div class="sjgl-title">{{ item.title }}</div> <template slot-scope="scope">
<div class="sjgl-value" :style="{color:item.color}"> <span :class="{'circle warning-status' : scope.row.status !== 0}">{{ $store.state.ems.warnOptions[scope.row.status]}}</span>
<i v-if="item.loading" class="el-icon-loading"></i> </template>
<span v-else>{{ item.value | formatNumber }}</span> </el-table-column>
</div> <el-table-column
</div> class-name="alarm-content"
</el-col> prop="alarmContent"
</el-row> label="告警内容">
</el-table-column>
<el-table-column
label="工单"
fixed="right"
width="250"
>
<template slot-scope="scope">
<el-button type="text" size="mini" v-if="scope.row.ticketNo" @click="toTicket">已生成工单(工单号:{{scope.row.ticketNo}})</el-button>
<el-button type="primary" size="mini" v-else @click="toTicket">生成工单</el-button>
</template>
</el-table-column>
</el-table>
</div> </div>
</el-card> </el-card>
</el-col> </el-col>
<el-col :xs="24" :sm="24" :lg="12">
<week-chart ref="weekChart" :display-data="runningDisplayData"/>
</el-col>
<el-col :xs="24" :sm="24" :lg="12">
<active-chart ref="activeChart" :display-data="runningDisplayData"/>
</el-col>
</el-row> </el-row>
<nllz-chart ref="nllzChart"/>
<el-dialog
:visible.sync="curveDialogVisible"
:title="curveDialogTitle"
width="1000px"
append-to-body
class="ems-dialog"
:close-on-click-modal="false"
destroy-on-close
@opened="handleCurveDialogOpened"
@closed="handleCurveDialogClosed"
>
<div class="curve-tools">
<el-date-picker
v-model="curveCustomRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 440px"
/>
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button>
</div>
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import * as echarts from "echarts"; import {getDzjkHomeView} from '@/api/ems/dzjk'
import {getSingleSiteBaseInfo} from "@/api/ems/zddt"; import NllzChart from "./NllzChart.vue";
import {getAmmeterData, getDzjkHomeTotalView, getProjectDisplayData} from "@/api/ems/dzjk"; import getQuerySiteId from '@/mixins/ems/getQuerySiteId'
import {getPointConfigCurve} from "@/api/ems/site";
import WeekChart from "./WeekChart.vue";
import ActiveChart from "./ActiveChart.vue";
import AlarmTable from "./AlarmTable.vue";
import ClInfo from "./ClInfo.vue";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
export default { export default {
name: "DzjkSbjkHome", name:'DzjkSbjkHome',
components: {WeekChart, ActiveChart, AlarmTable, ClInfo}, components: {NllzChart},
mixins: [getQuerySiteId, intervalUpdate], mixins: [getQuerySiteId],
data() { data() {
return { return {
loading: false, loading:false,
baseInfoLoading: false, sjglData:[{
runningInfoLoading: false, title:'今日充电量MWh',
runningUpdateSpinning: false, value:'',
runningUpdateTimer: null, attr:'dayChargedCap'
curveDialogVisible: false, },{
curveDialogTitle: "点位曲线", title:'今日放电量MWh',
curveChart: null, value:'',
curveLoading: false, attr:'dayDisChargedCap'
curveCustomRange: [], },{
curveQuery: { title:'总充电量MWh',
siteId: "", value:'',
pointId: "", attr:'totalChargedCap'
pointType: "data", },{
rangeType: "custom", title:'总放电量MWh',
startTime: "", value:'',
endTime: "", attr:'totalDischargedCap'
}, }],
fallbackSjglData: [ // todo 表格里的不同状态可能需要显示不同颜色 确定表格内容
{ tableData:[]
title: "今日充电量kWh",
attr: "dayChargedCap",
color: '#4472c4'
},
{
title: "今日放电量kWh",
attr: "dayDisChargedCap",
color: '#70ad47'
},
{
title: "总充电量kWh",
attr: "totalChargedCap",
color: '#4472c4'
},
{
title: "今日实时收入(元)",
attr: "dayRevenue",
color: '#f67438'
},
{
title: "昨日充电量kWh",
attr: "yesterdayChargedCap",
color: '#4472c4'
},
{
title: "昨日放电量kWh",
attr: "yesterdayDisChargedCap",
color: '#70ad47'
},
{
title: "总放电量kWh",
attr: "totalDischargedCap",
color: '#70ad47'
},
{
title: "昨日实时收入(元)",
attr: "yesterdayRevenue",
color: '#f67438'
},
],
info: {}, //基本信息
runningInfo: {}, //总累计运行数据+报警表格
runningDisplayData: [], //单站监控项目配置展示数据
ammeterDailySummary: {},
};
},
computed: {
isBaseInfoLoading() {
return false;
},
isRunningInfoLoading() {
const state = this.$data || {};
return !!(state.runningInfoLoading || state.runningUpdateSpinning || state.loading);
},
tableData() {
return this.runningInfo?.siteMonitorHomeAlarmVo || [];
},
totalRunningSectionData() {
return (this.runningDisplayData || []).filter(item => item.sectionName === "总累计运行数据");
},
totalRevenueDisplayItem() {
const sectionData = this.totalRunningSectionData || [];
const byFieldCode = sectionData.find(item => item.fieldCode === "totalRevenue");
if (byFieldCode) {
return byFieldCode;
}
return sectionData.find(item => item.fieldName === "总收入");
},
totalRevenueDisplayValue() {
return this.totalRevenueDisplayItem ? this.totalRevenueDisplayItem.fieldValue : this.runningInfo.totalRevenue;
},
runningDataCards() {
const sectionData = this.totalRunningSectionData || [];
if (sectionData.length > 0) {
const revenueFieldCode = this.totalRevenueDisplayItem ? this.totalRevenueDisplayItem.fieldCode : "";
return sectionData
.filter(item => item.fieldCode !== revenueFieldCode)
.map((item, index) => ({
title: item.fieldName,
value: this.getRunningCardValue(item),
color: this.getCardColor(index),
loading: this.isRunningInfoLoading,
raw: item,
}));
}
return this.fallbackSjglData.map(item => ({
title: item.title,
value: this.getRunningCardValue(item),
color: item.color,
loading: this.isRunningInfoLoading,
raw: item,
}));
},
},
beforeDestroy() {
if (this.runningUpdateTimer) {
clearTimeout(this.runningUpdateTimer);
this.runningUpdateTimer = null;
}
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
} }
}, },
methods: { methods:{
hasPointId(item) { toTicket(){
return !!String(item?.dataPoint || "").trim(); this.$router.push({path:'/ticket'})
},
handleTotalRevenueClick() {
const item = this.totalRevenueDisplayItem;
const pointId = String(item?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: item?.fieldName || "总收入",
});
},
handleRunningFieldClick(card) {
const item = card?.raw || {};
const pointId = String(item?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: card?.title || item?.fieldName || item?.fieldCode || pointId,
});
},
openCurveDialog({pointId, title}) {
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || pointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId,
pointType: "data",
rangeType: "custom",
startTime: range[0],
endTime: range[1],
};
this.curveDialogVisible = true;
},
handleCurveDialogOpened() {
if (!this.curveChart && this.$refs.curveChartRef) {
this.curveChart = echarts.init(this.$refs.curveChartRef);
}
this.loadCurveData();
},
handleCurveDialogClosed() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
this.curveLoading = false;
},
getDefaultCurveRange() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return [this.formatDateTime(start), this.formatDateTime(end)];
},
formatDateTime(date) {
const d = new Date(date);
const pad = (num) => String(num).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
},
formatCurveTime(value) {
if (value === undefined || value === null || value === "") {
return "";
}
const raw = String(value).trim();
const normalized = raw
.replace("T", " ")
.replace(/\.\d+/, "")
.replace(/Z$/, "")
.replace(/([+-]\d{2}:?\d{2})$/, "")
.trim();
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
if (matched) {
return `${matched[1]} ${matched[2]}`;
}
return normalized.slice(0, 16);
},
loadCurveData() {
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
this.$message.warning("点位信息不完整,无法查询曲线");
return;
}
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
this.$message.warning("请选择查询时间范围");
return;
}
this.curveQuery.startTime = this.curveCustomRange[0];
this.curveQuery.endTime = this.curveCustomRange[1];
const query = {
siteId: this.curveQuery.siteId,
pointId: this.curveQuery.pointId,
pointType: "data",
rangeType: "custom",
startTime: this.curveQuery.startTime,
endTime: this.curveQuery.endTime,
};
this.curveLoading = true;
getPointConfigCurve(query).then((response) => {
const rows = response?.data || [];
this.renderCurveChart(rows);
}).catch(() => {
this.renderCurveChart([]);
}).finally(() => {
this.curveLoading = false;
});
},
renderCurveChart(rows = []) {
if (!this.curveChart) return;
const xData = rows.map(item => this.formatCurveTime(item.dataTime));
const yData = rows.map(item => item.pointValue);
this.curveChart.clear();
this.curveChart.setOption({
legend: {},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
data: xData,
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: [
{
name: this.curveDialogTitle,
type: "line",
data: yData,
connectNulls: true,
},
],
});
if (!rows.length) {
this.$message.warning("当前时间范围暂无曲线数据");
}
},
setBaseInfoLoading(loading) {
if (Object.prototype.hasOwnProperty.call(this.$data, "baseInfoLoading")) {
this.baseInfoLoading = loading;
return;
}
this.$set(this.$data, "baseInfoLoading", loading);
},
setRunningInfoLoading(loading) {
if (Object.prototype.hasOwnProperty.call(this.$data, "runningInfoLoading")) {
this.runningInfoLoading = loading;
return;
}
this.$set(this.$data, "runningInfoLoading", loading);
},
getCardColor(index) {
const colors = ['#4472c4', '#70ad47', '#4472c4', '#f67438', '#4472c4', '#70ad47', '#70ad47', '#f67438'];
return colors[index % colors.length];
},
formatDateOnly(date) {
const value = new Date(date);
if (isNaN(value.getTime())) {
return "";
}
const pad = (num) => String(num).padStart(2, "0");
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
},
normalizeDateOnly(value) {
if (!value) {
return "";
}
const raw = String(value).trim();
const matched = raw.match(/\d{4}-\d{2}-\d{2}/);
if (matched) {
return matched[0];
}
return this.formatDateOnly(raw);
},
toDisplayNumber(value) {
if (value === null || value === undefined || value === "") {
return value;
}
const num = Number(value);
return Number.isNaN(num) ? value : num;
},
getAmmeterSummaryAttr(item) {
const fieldCode = String(item?.fieldCode || item?.attr || "").trim();
const fieldName = String(item?.fieldName || item?.title || "").trim();
if (["dayChargedCap", "今日充电量kWh"].includes(fieldCode) || fieldName === "今日充电量kWh") {
return "dayChargedCap";
}
if (["dayDisChargedCap", "今日放电量kWh"].includes(fieldCode) || fieldName === "今日放电量kWh") {
return "dayDisChargedCap";
}
if (["yesterdayChargedCap", "昨日充电量kWh"].includes(fieldCode) || fieldName === "昨日充电量kWh") {
return "yesterdayChargedCap";
}
if (["yesterdayDisChargedCap", "昨日放电量kWh"].includes(fieldCode) || fieldName === "昨日放电量kWh") {
return "yesterdayDisChargedCap";
}
if (["totalChargedCap", "总充电量kWh"].includes(fieldCode) || fieldName === "总充电量kWh") {
return "totalChargedCap";
}
if (["totalDischargedCap", "总放电量kWh"].includes(fieldCode) || fieldName === "总放电量kWh") {
return "totalDischargedCap";
}
return "";
},
getRunningCardValue(item) {
const summaryAttr = this.getAmmeterSummaryAttr(item);
if (summaryAttr) {
const summaryValue = this.ammeterDailySummary?.[summaryAttr];
if (summaryValue !== undefined && summaryValue !== null && summaryValue !== "") {
return summaryValue;
}
}
const rawValue = item?.fieldValue !== undefined ? item.fieldValue : this.runningInfo?.[item?.attr];
return this.toDisplayNumber(rawValue);
},
queryAllAmmeterDailyRows({ startTime = "", endTime = "", pageSize = 500, pageNum = 1, rows = [] } = {}) {
return getAmmeterData({
siteId: this.siteId,
startTime,
endTime,
pageSize,
pageNum,
}).then((response) => {
const currentRows = Array.isArray(response?.rows) ? response.rows : [];
const allRows = rows.concat(currentRows);
const total = Number(response?.total) || 0;
if (allRows.length >= total || currentRows.length < pageSize) {
return allRows;
}
return this.queryAllAmmeterDailyRows({
startTime,
endTime,
pageSize,
pageNum: pageNum + 1,
rows: allRows,
});
});
},
getAmmeterDailySummary() {
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const startTime = this.formatDateOnly(yesterday);
const endTime = this.formatDateOnly(today);
return Promise.all([
this.queryAllAmmeterDailyRows({
startTime,
endTime,
pageSize: 20,
pageNum: 1,
}),
this.queryAllAmmeterDailyRows(),
]).then(([recentRows, allRows]) => {
const rowMap = recentRows.reduce((result, row) => {
const dateKey = this.normalizeDateOnly(row?.dataTime);
if (dateKey) {
result[dateKey] = row;
}
return result;
}, {});
const todayKey = this.formatDateOnly(today);
const yesterdayKey = this.formatDateOnly(yesterday);
const todayRow = rowMap[todayKey] || {};
const yesterdayRow = rowMap[yesterdayKey] || {};
const totalChargedCap = allRows.reduce((result, row) => {
return result + (Number(row?.activeTotalKwh) || 0);
}, 0);
const totalDischargedCap = allRows.reduce((result, row) => {
return result + (Number(row?.reActiveTotalKwh) || 0);
}, 0);
return {
dayChargedCap: this.toDisplayNumber(todayRow.activeTotalKwh),
dayDisChargedCap: this.toDisplayNumber(todayRow.reActiveTotalKwh),
yesterdayChargedCap: this.toDisplayNumber(yesterdayRow.activeTotalKwh),
yesterdayDisChargedCap: this.toDisplayNumber(yesterdayRow.reActiveTotalKwh),
totalChargedCap: this.toDisplayNumber(totalChargedCap),
totalDischargedCap: this.toDisplayNumber(totalDischargedCap),
};
}).catch(() => ({}));
},
toAlarm() {
this.$router.push({path: "/dzjk/gzgj", query: this.$route.query});
},
getBaseInfo() {
return getSingleSiteBaseInfo(this.siteId).then((response) => {
this.info = response?.data || {};
});
},
getRunningInfo() {
const hasOldData = Object.keys(this.runningInfo || {}).length > 0 || (this.runningDisplayData || []).length > 0;
if (!hasOldData) {
this.setRunningInfoLoading(true);
}
return Promise.all([
getDzjkHomeTotalView(this.siteId),
getProjectDisplayData(this.siteId),
this.getAmmeterDailySummary(),
]).then(([homeResponse, displayResponse, ammeterDailySummary]) => {
const nextRunningInfo = homeResponse?.data || {};
const nextRunningDisplayData = displayResponse?.data || [];
const changed = hasOldData && this.hasTotalRunningChanged(nextRunningInfo, nextRunningDisplayData, ammeterDailySummary || {});
this.runningInfo = nextRunningInfo;
this.runningDisplayData = nextRunningDisplayData;
this.ammeterDailySummary = ammeterDailySummary || {};
if (changed) {
this.triggerRunningUpdateSpinner();
}
}).finally(() => {
if (!hasOldData) {
this.setRunningInfoLoading(false);
}
});
},
triggerRunningUpdateSpinner() {
if (this.runningUpdateTimer) {
clearTimeout(this.runningUpdateTimer);
}
this.runningUpdateSpinning = true;
this.runningUpdateTimer = setTimeout(() => {
this.runningUpdateSpinning = false;
this.runningUpdateTimer = null;
}, 800);
},
hasTotalRunningChanged(nextRunningInfo, nextRunningDisplayData, nextAmmeterDailySummary = {}) {
const oldSnapshot = this.getTotalRunningSnapshot(this.runningInfo, this.runningDisplayData, this.ammeterDailySummary || {});
const newSnapshot = this.getTotalRunningSnapshot(nextRunningInfo, nextRunningDisplayData, nextAmmeterDailySummary);
return JSON.stringify(oldSnapshot) !== JSON.stringify(newSnapshot);
},
getTotalRunningSnapshot(runningInfo, runningDisplayData, ammeterDailySummary = {}) {
const snapshot = {};
const sectionData = (runningDisplayData || []).filter(item => item.sectionName === "总累计运行数据");
if (sectionData.length > 0) {
sectionData.forEach(item => {
const key = item.fieldCode || item.fieldName;
if (!key) return;
const summaryAttr = this.getAmmeterSummaryAttr(item);
const value = summaryAttr ? ammeterDailySummary?.[summaryAttr] : item.fieldValue;
snapshot[`cfg:${key}`] = this.normalizeRunningCompareValue(value);
});
return snapshot;
}
this.fallbackSjglData.forEach(item => {
const summaryAttr = this.getAmmeterSummaryAttr(item);
const value = summaryAttr
? ammeterDailySummary?.[summaryAttr]
: runningInfo?.[item.attr];
snapshot[`fallback:${item.attr}`] = this.normalizeRunningCompareValue(value);
});
snapshot["fallback:totalRevenue"] = this.normalizeRunningCompareValue(runningInfo?.totalRevenue);
return snapshot;
},
normalizeRunningCompareValue(value) {
if (value === null || value === undefined) return "";
if (typeof value === "number") return Number.isNaN(value) ? "" : value;
const text = String(value).trim();
if (text === "") return "";
const num = Number(text);
return Number.isNaN(num) ? text : num;
},
init() {
// 功率曲线
this.$refs.activeChart.init(this.siteId);
// 一周冲放曲线
this.$refs.weekChart.init(this.siteId);
// 静态信息 this.getBaseInfo()
// 总累计运行数据+故障告警 this.getRunningInfo()
Promise.all([this.getBaseInfo(), this.getRunningInfo()]);
// 一分钟循环一次总累计运行数据
this.updateInterval(this.getRunningInfo);
}, },
init(){
this.loading = true
getDzjkHomeView(this.siteId).then(response => {
const data = response?.data || {}
this.sjglData.forEach(item=>{
item.value =data[item.attr]
})
this.tableData = data?.siteMonitorHomeAlarmVo || []
this.$refs.nllzChart.setOption(data)
}).finally(() => {this.loading = false})
}
}, },
}; }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
//设备告警
.alarm-msg {
background: #ff4949;
padding: 2px 5px;
font-size: 10px;
font-weight: bolder;
border-radius: 3px;
line-height: 20px;
cursor: pointer;
color: #fff;
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
}
//基本信息-地址 运行️时间
.site-info {
display: flex;
font-size: 12px;
line-height: 20px;
margin-bottom: 10px;
align-items: center;
&.site-info-address {
height: 40px;
}
.title {
color: #1791ff;
font-size: 18px;
line-height: 20px;
margin-right: 7px;
}
.value {
color: #000;
font-size: 12px;
max-height: 40px;
overflow: hidden;
}
}
//总收入
.total-count {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 12px;
font-weight: bolder;
color: #333;
line-height: 14px;
.unit {
font-style: italic;
}
.value {
font-size: 22px;
font-weight: bolder;
color: #ed2f1d;
font-style: italic;
padding: 0 5px;
}
}
.pointer-field {
cursor: pointer;
}
.field-disabled {
cursor: not-allowed;
opacity: 0.75;
}
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.row-container {
& > .el-col {
margin-bottom: 20px;
}
}
//数据概览 //数据概览
.sjgl-col { .sjgl-data{
.sjgl-wrapper { text-align: center;
text-align: left; margin-top:20px;
padding: 15px 20px; .sjgl-title{
background-color: #f2f7fb; color: #666666;
line-height: 14px;
padding-top: 18px;
}
.sjgl-value{
color: rgba(51,51,51,1);
font-size: 26px;
line-height: 26px;
font-weight: 500;
margin-top: 14px;
}
} }
//实时告警
&.power-col { .ssgj-container{
.sjgl-wrapper { padding:20px;
padding: 10px; height: 310px;
box-sizing: border-box;
.sjgl-value { ::v-deep{
color: #c44444; .el-table .el-table__header-wrapper th, .el-table .el-table__fixed-header-wrapper th{
background:#FFF2CB ;
} }
} }
} }
&:nth-child(4),
&:nth-child(2),
&:nth-child(3),
&:nth-child(4) {
margin-bottom: 10px;
}
.sjgl-title {
color: #717171;
line-height: 14px;
font-weight: bold;
}
.sjgl-value {
color: rgba(51, 51, 51, 1);
font-size: 22px;
line-height: 26px;
font-weight: bolder;
font-style: italic;
margin-top: 14px;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style> </style>
<style lang="scss">
.home-normal-info {
font-size: 12px;
.el-descriptions-item__container {
.el-descriptions-item__label {
color: #666666;
}
.el-descriptions-item__content {
color: #333333;
}
}
}
</style>

View File

@ -2,6 +2,7 @@
<template> <template>
<div class="ems-dashboard-editor-container"> <div class="ems-dashboard-editor-container">
<zd-select :get-list-by-store="true" :default-site-id="$route.query.siteId" @submitSite="submitSite"/>
<el-menu <el-menu
class="ems-second-menu" class="ems-second-menu"
:default-active="$route.meta.activeSecondMenuName" :default-active="$route.meta.activeSecondMenuName"
@ -10,10 +11,10 @@
active-text-color="#ffffff" active-text-color="#ffffff"
mode="horizontal" mode="horizontal"
> >
<el-menu-item :index="item.name" v-for="(item,index) in childrenRoute" :key="index+'dzjkChildrenRoute'" :class="{'lighting':item.path.indexOf('gzgj')>-1 && dzjkAlarmLighting}"> <el-menu-item :index="item.name" v-for="(item,index) in childrenRoute" :key="index+'dzjkChildrenRoute'">
<router-link style="height: 100%;width: 100%;display: block;padding:0 20px;" :to="{path:item.path,query:$route.query}"> <router-link style="height: 100%;width: 100%;display: block;padding:0 20px;" :to="{path:item.path,query:$route.query}">
{{item.meta.title}} {{item.meta.title}}
</router-link> </router-link>
</el-menu-item> </el-menu-item>
</el-menu> </el-menu>
<div class="ems-content-container ems-content-container-padding dzjk-ems-content-container"> <div class="ems-content-container ems-content-container-padding dzjk-ems-content-container">
@ -27,18 +28,25 @@
<script> <script>
import { dzjk } from '@/router/ems' import { dzjk } from '@/router/ems'
const childrenRoute = dzjk[0].children[0].children//获取到单站监控下面的字路由 const childrenRoute = dzjk[0].children[0].children//获取到单站监控下面的字路由
import {mapState} from "vuex"; console.log('childrenRoute',childrenRoute)
import ZdSelect from '@/components/Ems/ZdSelect/index.vue'
export default { export default {
components:{ZdSelect},
data(){ data(){
return { return {
childrenRoute, childrenRoute,
activeMenu:'' activeMenu:''
} }
}, },
computed:{ methods:{
...mapState({ submitSite(id){
dzjkAlarmLighting:state=>state.ems.dzjkAlarmLighting if(id != this.$route.query.siteId){
}) console.log('单站监控选择了其他的站点id=',id,'并更新页面地址参数')
this.$router.push({query:{...this.$route.query,siteId:id}})
}else{
console.log('单站监控选择了相同的其他的站点id=',id,'页面地址不发生改变')
}
}
}, },
beforeRouteLeave(to,from, next){ beforeRouteLeave(to,from, next){
//从单站监控下面的所有子页面跳出时会触发 //从单站监控下面的所有子页面跳出时会触发
@ -46,30 +54,15 @@ export default {
this.$store.commit('SET_ZD_LIST',[]) this.$store.commit('SET_ZD_LIST',[])
next() next()
}, },
mounted() {
console.log('单站监控一级页面路由',this.$route)
}
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.ems-dashboard-editor-container{
padding-top: 12px;
}
.dzjk-ems-content-container{ .dzjk-ems-content-container{
margin-top:0; margin-top:0;
min-height: 60vh;
}
.lighting{
position: relative;
z-index: 1;
&::after{
content:"";
display: block;
background-color: red;
height: 10px;
width: 10px;
border-radius: 100%;
position: absolute;
right: -2px;
top: -2px;
}
} }
</style> </style>

View File

@ -1,420 +0,0 @@
<!--电位展示图表-->
<template>
<el-dialog
:visible.sync="show"
:title="pointName"
:close-on-click-modal="false"
show-close
destroy-on-close
lock-scroll
append-to-body
width="1000px"
class="ems-dialog"
:before-close="handleClosed"
>
<el-card
shadow="always"
class="common-card-container common-card-container-body-no-padding time-range-card"
>
<div slot="header" class="time-range-header">
<el-radio-group class="card-title" v-model="dataUnit">
<el-radio :label="1">分钟</el-radio>
<el-radio :label="2">小时</el-radio>
<el-radio :label="3"></el-radio>
</el-radio-group>
<date-time-select
ref="dateTimeSelect"
:data-unit="dataUnit"
@initDate="(e) => (dataRange = e || [])"
@updateDate="updateDate"
/>
</div>
<div style="height: 350px" id="searchChart"></div>
</el-card>
</el-dialog>
</template>
<script>
import * as echarts from "echarts";
import resize from "@/mixins/ems/resize";
import DateTimeSelect from "@/views/ems/search/DateTimeSelect.vue";
import {getPointValueList} from "@/api/ems/search";
import DateRangeSelect from "@/components/Ems/DateRangeSelect/index.vue";
export default {
components: {DateRangeSelect, DateTimeSelect},
mixins: [resize],
props: {
siteId: {
type: String,
required: true,
},
},
computed: {
isDtdc() {
return this.deviceCategory === "BATTERY";
},
},
watch: {
show(val) {
if (!val) {
this.pointName = "";
this.deviceCategory = "";
this.deviceId = "";
this.dataUnit = 1;
this.child = "";
if (!this.chart) {
return;
}
this.hideLoading();
this.chart.dispose();
this.chart = null;
}
},
dataUnit: {
handler(newVal, oldVal) {
if (!this.show) return;
console.log("wacth到了dataUnit的变化", newVal, oldVal);
this.$nextTick(() => {
this.$refs.dateTimeSelect.init();
this.getDate();
});
},
},
},
data() {
return {
show: false,
chart: null,
dataUnit: 1,
dataRange: [],
child: "", //单体电池需要数据
pointName: "",
deviceCategory: "",
deviceId: "",
};
},
methods: {
showChart({pointName, deviceCategory, deviceId, child = ""}) {
//初始化数据
this.pointName = pointName;
this.deviceCategory = deviceCategory;
this.deviceId = deviceId;
child && (this.child = child);
this.show = true;
this.$nextTick(() => {
this.$refs.dateTimeSelect.init();
this.initChart();
this.getDate();
});
},
initChart() {
this.chart = echarts.init(document.querySelector("#searchChart"));
},
showLoading() {
this.chart && this.chart.showLoading();
},
hideLoading() {
this.chart && this.chart.hideLoading();
},
getDate() {
this.showLoading();
const {
siteId,
dataUnit,
dataRange: [start = "", end = ""],
child,
deviceId,
deviceCategory,
pointName,
} = this;
let siteDeviceMap = {};
child && (siteDeviceMap[siteId] = child);
let startDate, endDate;
if (start && dataUnit === 3) {
// startDate= start + `${dataUnit === 2 ? ':00' : ' 00:00:00'}`
startDate = start + " 00:00:00";
} else {
startDate = start;
}
if (end && dataUnit === 3) {
// endDate= end + `${dataUnit === 2 ? ':00' : ' 00:00:00'}`
endDate = end + " 00:00:00";
} else {
endDate = end;
}
getPointValueList({
siteIds: [siteId],
deviceId,
dataUnit,
deviceCategory,
pointName,
startDate,
endDate,
siteDeviceMap,
})
.then((response) => {
this.setOption(response?.data || []);
})
.finally(() => {
this.hideLoading();
});
},
setOption(data) {
if (!this.chart) return;
this.chart.clear();
console.log("返回的数据", data);
if (!data || data.length <= 0) {
this.$message.warning("暂无数据");
}
console.log('展示的图表类型chartType', data[0].chartType)
if (data[0].chartType === 2) {
// 箱型图
this.setBoxOption(data)
} else {
//折线图
this.setLineOption(data)
}
},
setLineOption(data) {
let dataset = [];
data.forEach((item, index) => {
item.deviceList.forEach((inner) => {
dataset.push({
name: `${
this.isDtdc
? `${inner.parentDeviceId ? inner.parentDeviceId + "-" : ""}${inner.deviceId}`
: `${inner.deviceId}`
}`,
type: "line",
markPoint: {
symbolSize: 30,
emphasis: {
disabled: false//打开 鼠标高亮
},
data: [//最大值、最小值
{
// type: 'max',
name: `最大值`,
coord: [inner.maxDate, inner.maxValue],
relativeTo: 'coordinate',
label: {
position: "top",
formatter: item.dataType === 2 ? ([
`最大值:${inner.maxValue}`,
// `平均值:${inner.avgValue}`,
`差值:${inner.diffValue}`,
]).join('\n') : ([
`最大值:${inner.maxValue}`,
// `平均值:${inner.avgValue}`,
]).join('\n'),
},
},
{
// type: 'min',
name: `最小值`,
coord: [inner.minDate, inner.minValue],
relativeTo: 'coordinate',
label: {
position: "top",
formatter: item.dataType === 2 ? ([
`最小值:${inner.minValue}`,
// `平均值:${inner.avgValue}`,
`差值:${inner.diffValue}`,
]).join('\n') : ([
`最小值:${inner.minValue}`,
// `平均值:${inner.avgValue}`,
]).join('\n'),
}
}
]
},
xdata: [],
data: [],
});
const length = dataset.length;
inner.pointValueList.forEach((value) => {
dataset[length - 1].xdata.push(value.valueDate);
dataset[length - 1].data.push(value.pointValue);
});
});
});
console.log("折线图图表数据", dataset);
this.chart.setOption({
legend: {
// left: 'center',
// top: '10',
},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: 'cross',
},
// axisPointer: {
// // 坐标轴指示器,坐标轴触发有效
// type: "shadow", // 默认为直线,可选为:'line' | 'shadow'
// },
},
textStyle: {
color: "#333333",
},
xAxis: {type: "category", data: dataset?.[0]?.xdata || []},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: dataset,
});
},
setBoxOption(data) {
let dataset = [];
data.forEach((item, index) => {
item.deviceList.forEach((inner) => {
dataset.push({
name: `${
this.isDtdc
? `${inner.parentDeviceId ? inner.parentDeviceId + "-" : ""}${inner.deviceId}`
: `${inner.deviceId}`
}`,
type: "boxplot",
// markPoint: {
// symbolSize: 30,
// emphasis: {
// disabled: false//打开 鼠标高亮
// },
// data: [//最大值、最小值
// {
// // type: 'max',
// name: `最大值`,
// coord: [inner.maxDate, inner.maxValue],
// relativeTo: 'coordinate',
// label: {
// position: "top",
// formatter: item.dataType === 2 ? ([
// `最大值:${inner.maxValue}`,
// // `平均值:${inner.avgValue}`,
// `差值:${inner.diffValue}`,
// ]).join('\n') : ([
// `最大值:${inner.maxValue}`,
// // `平均值:${inner.avgValue}`,
// ]).join('\n'),
// },
// },
// {
// // type: 'min',
// name: `最小值`,
// coord: [inner.minDate, inner.minValue],
// relativeTo: 'coordinate',
// label: {
// position: "top",
// formatter: item.dataType === 2 ? ([
// `最小值:${inner.minValue}`,
// // `平均值:${inner.avgValue}`,
// `差值:${inner.diffValue}`,
// ]).join('\n') : ([
// `最小值:${inner.minValue}`,
// // `平均值:${inner.avgValue}`,
// ]).join('\n'),
// }
// }
// ]
// },
xdata: [],
data: [],
});
const length = dataset.length;
inner.pointValueList.forEach((value) => {
const {valueDate, min, q1, median, q3, max} = value
// const mid = (max - min) / 2, minLine = min + Math.abs(median / 2),
// maxLine = max - Math.abs(median / 2)
dataset[length - 1].xdata.push(valueDate);
dataset[length - 1].data.push([min, q1, median, q3, max]);
});
});
});
console.log("箱型图图表数据", dataset);
this.chart.setOption({
legend: {
// left: 'center',
// top: '10',
},
grid: {
containLabel: true,
},
tooltip: {
trigger: 'item',
formatter: function (params) {
let data = params.data;
let result = params.marker + params.name + ' ' + params.seriesName + '<br/>';
result += '最小值: ' + data[1] + '<br/>';
result += '平均值: ' + data[3] + '<br/>';
result += '最大值: ' + data[5];
return result;
}
// trigger: "axis",
// axisPointer: {
// type: 'cross',
// },
// axisPointer: {
// // 坐标轴指示器,坐标轴触发有效
// type: "shadow", // 默认为直线,可选为:'line' | 'shadow'
// },
},
textStyle: {
color: "#333333",
},
xAxis: {type: "category", data: dataset?.[0]?.xdata || []},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: dataset,
});
},
updateDate(val) {
this.dataRange = val || [];
this.getDate();
},
handleClosed(done) {
if (!this.chart) {
return done();
}
this.chart.dispose();
this.chart = null;
done();
},
},
};
</script>
<style scoped lang="scss">
::v-deep {
.card-title .el-radio {
line-height: 40px;
}
}
</style>

View File

@ -2,14 +2,8 @@
<template> <template>
<!-- 6个方块--> <!-- 6个方块-->
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :xs="12" :sm="8" :lg="4" style="margin-bottom: 10px;" class="single-square-box-container" v-for="(item,index) in displaySquares" :key="index+'singleSquareBox'"> <el-col :xs="12" :sm="8" :lg="4" class="single-square-box-container" v-for="(item,index) in singleZdSqaure" :key="index+'singleSquareBox'">
<div <single-square-box :data="{...item,value:formatNumber(data[item.attr])}" ></single-square-box>
class="square-click-wrapper"
:class="{ 'field-disabled': !item.pointId }"
@click="handleSquareClick(item)"
>
<single-square-box :data="{...item,value:item.value,loading:item.valueLoading}" ></single-square-box>
</div>
</el-col> </el-col>
</el-row> </el-row>
</template> </template>
@ -17,74 +11,52 @@
<script> <script>
import SingleSquareBox from "@/components/Ems/SingleSquareBox/index.vue"; import SingleSquareBox from "@/components/Ems/SingleSquareBox/index.vue";
import {formatNumber} from '@/filters/ems'
export default { export default {
components:{SingleSquareBox}, components:{SingleSquareBox},
props:{ props:{
displayData: { data:{
type: Array, type:Object,
required: false, required:false,
default: () => [], default:()=>{return {}}
},
loading: {
type: Boolean,
required: false,
default: false,
}, },
}, },
computed: { methods:{formatNumber},
displaySquares() { data() {
const sourceList = (this.displayData || []).filter((item) => { return {
if (!item) return false; // 单个电站 四个方块数据
return item.menuCode === "SBJK_SSYX" || item.sectionName === "运行概览"; singleZdSqaure:[{
}); title:'实时有功功率kW',
const sourceMap = {}; value:'',
sourceList.forEach((item) => { bgColor:'#FFF2CB',
if (!item) return; attr:'totalActivePower'
const key = this.getFieldName(item.fieldCode); },{
if (key) { title:'实时无功功率kVar',
sourceMap[key] = item; value:'',
} bgColor:'#CBD6FF',
}); attr:'totalReactivePower'
const defaults = [ },{
{fieldCode: "totalActivePower", fieldName: "实时有功功率kW"}, title:'电池堆SOC',
{fieldCode: "totalReactivePower", fieldName: "实时无功功率kVar"}, value:'',
{fieldCode: "soc", fieldName: "电池堆SOC"}, bgColor:'#DCCBFF',
{fieldCode: "soh", fieldName: "电池堆SOH"}, attr:'soc'
{fieldCode: "dayChargedCap_rt", fieldName: "今日充电量kWh"}, },{
{fieldCode: "dayDisChargedCap_rt", fieldName: "今日放电量kWh"}, title:'电池堆SOH',
]; value:'',
return defaults.map((def, index) => { bgColor:'#FFD4CB',
const row = sourceMap[def.fieldCode] || {}; attr:'soh'
const pointId = String(row.dataPoint || "").trim(); },{
return { title:'今日充电量kWh',
title: row.fieldName || def.fieldName, value:'',
value: row.fieldValue, bgColor:'#FFD6F8',
valueLoading: this.loading && this.isEmptyValue(row.fieldValue), attr:'dayChargedCap'
bgColor: this.getBgColor(index), },{
pointId, title:'今日放电量kWh',
fieldCode: row.fieldCode || def.fieldCode, value:'',
raw: row, bgColor:'#E1FFCA',
}; attr:'dayDisChargedCap'
}); }]
}, }
},
methods: {
handleSquareClick(item) {
this.$emit("field-click", item || {});
},
getFieldName(fieldCode) {
const raw = String(fieldCode || "").trim();
if (!raw) return "";
const index = raw.lastIndexOf("__");
return index >= 0 ? raw.slice(index + 2) : raw;
},
getBgColor(index) {
const bgColors = ['#FFF2CB', '#CBD6FF', '#DCCBFF', '#FFD4CB', '#FFD6F8', '#E1FFCA'];
return bgColors[index % bgColors.length];
},
isEmptyValue(value) {
return value === undefined || value === null || value === "" || value === "-";
},
}, },
} }
@ -92,19 +64,5 @@ export default {
<style scoped lang="scss"> <style scoped lang="scss">
@media only screen and (min-width: 1200px) {
.single-square-box-container {
min-width: 16.6666666667%;
width: fit-content;
}
}
.square-click-wrapper {
cursor: pointer;
}
.square-click-wrapper.field-disabled {
cursor: not-allowed;
opacity: 0.8;
}
</style> </style>

View File

@ -1,731 +1,137 @@
<template> <template>
<div> <div v-loading="loading">
<div class="pcs-tags"> <div v-for="(baseInfo,index) in baseInfoList" :key="index+'bmsdccContainer'" style="margin-bottom:25px;">
<el-tag <el-card shadow="always" class="common-card-container common-card-container-body-no-padding common-card-container-no-title-bg">
size="small"
:type="selectedClusterId ? 'info' : 'primary'"
:effect="selectedClusterId ? 'plain' : 'dark'"
class="pcs-tag-item"
@click="handleTagClick('')"
>
全部
</el-tag>
<el-tag
v-for="(item, index) in clusterDeviceList"
:key="index + 'clusterTag'"
size="small"
:type="selectedClusterId === (item.deviceId || item.id) ? 'primary' : 'info'"
:effect="selectedClusterId === (item.deviceId || item.id) ? 'dark' : 'plain'"
class="pcs-tag-item"
@click="handleTagClick(item.deviceId || item.id || '')"
>
{{ item.deviceName || item.name || item.deviceId || item.id || 'BMS电池簇' }}
</el-tag>
</div>
<div v-for="(baseInfo,index) in filteredBaseInfoList" :key="index+'bmsdccContainer'" style="margin-bottom:25px;">
<el-card shadow="always"
class="sbjk-card-container common-card-container-body-no-padding common-card-container-no-title-bg"
:class="handleCardClass(baseInfo)">
<div slot="header"> <div slot="header">
<span <span class="large-title">{{index+1}}#{{baseInfo.deviceName}}</span>
class="large-title">{{
baseInfo.parentDeviceName ? `${baseInfo.parentDeviceName} -> ` : ''
}}{{ baseInfo.deviceName }}</span>
<div class="info">
<div>数据更新时间{{ baseInfo.dataUpdateTime || '-' }}</div>
</div>
<div class="alarm">
<el-button type="primary" round size="small" style="margin-right:20px;"
@click="pointDetail(baseInfo,'point')">详细
</el-button>
<el-badge :hidden="!baseInfo.alarmNum" :value="baseInfo.alarmNum || 0" class="item">
<i
class="el-icon-message-solid alarm-icon"
@click="pointDetail(baseInfo,'alarmPoint')"
></i>
</el-badge>
</div>
</div> </div>
<div class="descriptions-main"> <div class="descriptions-main">
<el-descriptions direction="vertical" :column="3" :colon="false"> <el-descriptions direction="vertical" :column="3" :colon="false">
<el-descriptions-item <el-descriptions-item labelClassName="descriptions-label" :contentClassName="`descriptions-direction ${baseInfo.workStatus === '0' ? 'save' :'danger'}`" :span="1" label="工作状态" >{{$store.state.ems.workStatusOptions[baseInfo.workStatus]}}</el-descriptions-item>
contentClassName="descriptions-direction work-status" <el-descriptions-item labelClassName="descriptions-label" contentClassName="descriptions-direction" :span="1" label="与PCS通信">{{$store.state.ems.communicationStatusOptions[baseInfo.pcsCommunicationStatus]}}</el-descriptions-item>
:span="1" label="工作状态"> <el-descriptions-item labelClassName="descriptions-label" contentClassName="descriptions-direction" :span="1" label="与EMS通信">{{$store.state.ems.communicationStatusOptions[baseInfo.emsCommunicationStatus]}}</el-descriptions-item>
<span </el-descriptions>
class="pointer" </div>
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'workStatus') }" <div class="descriptions-main descriptions-main-bg-color">
@click="handleFieldClick(baseInfo, 'workStatus', '工作状态')" <el-descriptions direction="vertical" :column="3" :colon="false">
> <el-descriptions-item labelClassName="descriptions-label" contentClassName="descriptions-direction" v-for="(item,index) in infoData" :key="index+'pcsInfoData'" :span="1" :label="item.label">{{baseInfo[item.attr] | formatNumber}} <span v-if="item.unit" v-html="item.unit"></span></el-descriptions-item>
{{ formatDictValue(clusterWorkStatusOptions, baseInfo.workStatus) }} </el-descriptions>
</span> <!-- 进度-->
</el-descriptions-item> <div class="process-container">
<el-descriptions-item contentClassName="descriptions-direction" <div class="process-line-bg">
:span="1" label="与PCS通信"> <div class="process-line":style="{height:baseInfo.currentSoc+'%'}"></div>
<span </div>
class="pointer" <div class="process">当前SOC : {{baseInfo.currentSoc}}%</div>
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'pcsCommunicationStatus') }"
@click="handleFieldClick(baseInfo, 'pcsCommunicationStatus', '与PCS通信')"
>
{{ formatDictValue(clusterPcsCommunicationStatusOptions, baseInfo.pcsCommunicationStatus) }}
</span>
</el-descriptions-item>
<el-descriptions-item contentClassName="descriptions-direction"
:span="1" label="与EMS通信">
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'emsCommunicationStatus') }"
@click="handleFieldClick(baseInfo, 'emsCommunicationStatus', '与EMS通信')"
>
{{ formatDictValue(clusterEmsCommunicationStatusOptions, baseInfo.emsCommunicationStatus) }}
</span>
</el-descriptions-item>
</el-descriptions>
</div>
<div class="descriptions-main descriptions-main-bg-color">
<el-descriptions direction="vertical" :column="3" :colon="false">
<el-descriptions-item labelClassName="descriptions-label" contentClassName="descriptions-direction"
v-for="(item,index) in infoData" :key="index+'pcsInfoData'" :span="1"
:label="item.label">
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, item.attr) }"
@click="handleFieldClick(baseInfo, item.attr, item.label)"
>
<i v-if="isPointLoading(baseInfo[item.attr])" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(baseInfo[item.attr]) | formatNumber }}</span>
<span v-if="item.unit" v-html="item.unit"></span>
</span>
</el-descriptions-item>
</el-descriptions>
<!-- 进度-->
<div class="process-container">
<div class="process-line-bg">
<div class="process-line" :style="{height:baseInfo.currentSoc+'%'}"></div>
</div>
<div
class="process pointer"
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'currentSoc') }"
@click="handleFieldClick(baseInfo, 'currentSoc', '当前SOC')"
>当前SOC :
{{ baseInfo.currentSoc }}%
</div> </div>
</div> </div>
</div>
<el-table <el-table
class="common-table" class="common-table"
:data="baseInfo.batteryDataList" :data="baseInfo.batteryDataList"
stripe stripe
style="width: 100%;margin-top:25px;"> style="width: 100%;margin-top:25px;">
<el-table-column <el-table-column
prop="dataName" prop="dataName"
label="名称"> label="名称">
<template slot-scope="scope"> <template slot-scope="scope">
<span v-html="scope.row.dataName+''+unitObj[scope.row.dataName]+''"></span> <span v-html="scope.row.dataName+''+unitObj[scope.row.dataName]+''"></span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
prop="avgData" prop="avgData"
label="单体平均值" label="单体平均值"
> >
<template slot-scope="scope">
<span class="pointer"
:class="{ 'field-disabled': !hasTableFieldPointId(baseInfo, scope.row.dataName, scope.column.label) }"
@click="handleTableFieldClick(baseInfo, scope.row.dataName, scope.column.label)">{{
scope.row.avgData
}}</span>
</template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
prop="minData" prop="minData"
label="单体最小值"> label="单体最小值">
<template slot-scope="scope">
<span class="pointer"
:class="{ 'field-disabled': !hasTableFieldPointId(baseInfo, scope.row.dataName, scope.column.label) }"
@click="handleTableFieldClick(baseInfo, scope.row.dataName, scope.column.label)">{{
scope.row.minData
}}</span>
</template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
prop="minDataID" prop="minDataID"
label="单体最小值ID"> label="单体最小值ID">
</el-table-column> </el-table-column>
<el-table-column <el-table-column
prop="maxData" prop="maxData"
label="单体最大值"> label="单体最大值">
<template slot-scope="scope">
<span class="pointer "
:class="{ 'field-disabled': !hasTableFieldPointId(baseInfo, scope.row.dataName, scope.column.label) }"
@click="handleTableFieldClick(baseInfo, scope.row.dataName, scope.column.label)">{{
scope.row.maxData
}}</span>
</template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
prop="maxDataID" prop="maxDataID"
label="单体最大值ID"> label="单体最大值ID">
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-card> </el-card>
</div> </div>
<point-table ref="pointTable"/> <el-empty v-show="baseInfoList.length<=0" :image-size="200"></el-empty>
<el-dialog
:visible.sync="curveDialogVisible"
:title="curveDialogTitle"
width="1000px"
append-to-body
class="ems-dialog"
:close-on-click-modal="false"
destroy-on-close
@opened="handleCurveDialogOpened"
@closed="handleCurveDialogClosed"
>
<div class="curve-tools">
<el-date-picker
v-model="curveCustomRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 440px"
/>
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button>
</div>
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import * as echarts from "echarts"; import {getBMSBatteryCluster} from '@/api/ems/dzjk'
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
import {getProjectDisplayData, getStackNameList, getClusterNameList} from '@/api/ems/dzjk'
import getQuerySiteId from "@/mixins/ems/getQuerySiteId"; import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import {mapState} from "vuex";
import {getPointConfigCurve, getSingleMonitorWorkStatusEnumMappings} from "@/api/ems/site";
export default { export default {
name: 'DzjkSbjkBmsdcc', name:'DzjkSbjkBmsdcc',
mixins: [getQuerySiteId, intervalUpdate], mixins:[getQuerySiteId],
components: {PointTable}, components:{},
computed: {
...mapState({
CLUSTERWorkStatusOptions: state => state?.ems?.CLUSTERWorkStatusOptions || {},
}),
clusterWorkStatusOptions() {
return this.getEnumOptions("CLUSTER", "workStatus", this.CLUSTERWorkStatusOptions || {});
},
clusterPcsCommunicationStatusOptions() {
return this.getEnumOptions("CLUSTER", "pcsCommunicationStatus", (this.$store.state.ems && this.$store.state.ems.communicationStatusOptions) || {});
},
clusterEmsCommunicationStatusOptions() {
return this.getEnumOptions("CLUSTER", "emsCommunicationStatus", (this.$store.state.ems && this.$store.state.ems.communicationStatusOptions) || {});
},
filteredBaseInfoList() {
if (!this.selectedClusterId) {
return this.baseInfoList || [];
}
return (this.baseInfoList || []).filter(item => item.deviceId === this.selectedClusterId);
},
},
data() { data() {
return { return {
loading: false, loading:false,
displayData: [], unitObj:{
clusterDeviceList: [], '电压':'V',
siteEnumOptionMap: {}, '温度':'&#8451;',
selectedClusterId: "", 'SOC':'%'
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
}, },
unitObj: { baseInfoList:[],
'电压': 'V', infoData:[
'温度': '&#8451;', {label:'簇电压',attr:'clusterVoltage',unit:'V'},
'SOC': '%' {label:'可充电量',attr:'chargeableCapacity',unit:'kWh'},
}, {label:'累计充电量',attr:'totalChargedCapacity',unit:'kWh'},
tablePointNameMap: { {label:'簇电流',attr:'clusterCurrent',unit:'A'},
'电压单体最小值': '最低单体电压', {label:'可放电量',attr:'dischargeableCapacity',unit:'kWh'},
'电压单体平均值': '电压平均值', {label:'累计放电量',attr:'totalDischargedCapacity',unit:'kWh'},
'电压单体最大值': '最高单体电压', {label:'SOH',attr:'soh',unit:'%'},
'温度单体最小值': '最低单体温度', {label:'平均温度',attr:'averageTemperature',unit:'&#8451;'},
'温度单体平均值': '平均单体温度', {label:'绝缘电阻',attr:'insulationResistance',unit:'&Omega;'},
'温度单体最大值': '最高单体温度',
'SOC单体最小值': '最低单体SOC',
'SOC单体平均值': '当前SOC',
'SOC单体最大值': '最高单体SOC',
},
baseInfoList: [{
siteId: "",
deviceId: "",
parentDeviceName: "",
deviceName: "BMS电池簇",
dataUpdateTime: "-",
alarmNum: 0,
batteryDataList: [],
}],
infoData: [
{label: '簇电压', attr: 'clusterVoltage', unit: 'V', pointName: '簇电压'},
{label: '可充电量', attr: 'chargeableCapacity', unit: 'kWh', pointName: '可充电量'},
{label: '累计充电量', attr: 'totalChargedCapacity', unit: 'kWh', pointName: '累计充电量'},
{label: '簇电流', attr: 'clusterCurrent', unit: 'A', pointName: '簇电流'},
{label: '可放电量', attr: 'dischargeableCapacity', unit: 'kWh', pointName: '可放电量'},
{label: '累计放电量', attr: 'totalDischargedCapacity', unit: 'kWh', pointName: '累计放电量'},
{label: 'SOH', attr: 'soh', unit: '%', pointName: 'SOH'},
{label: '平均温度', attr: 'averageTemperature', unit: '&#8451;', pointName: '平均温度'},
{label: '绝缘电阻', attr: 'insulationResistance', unit: '&Omega;', pointName: '绝缘电阻'},
], ],
} }
}, },
methods: { methods:{
displayValue(value) { init(){
return value === undefined || value === null || value === "" ? "-" : value;
},
isPointLoading(value) {
return this.loading && (value === undefined || value === null || value === "" || value === "-");
},
normalizeDictKey(value) {
const raw = String(value == null ? "" : value).trim();
if (!raw) return "";
if (/^-?\d+(\.0+)?$/.test(raw)) {
return String(parseInt(raw, 10));
}
return raw;
},
formatDictValue(options, value) {
const dict = (options && typeof options === "object") ? options : {};
const key = this.normalizeDictKey(value);
if (!key) return "-";
return dict[key] || key;
},
buildEnumScopeKey(deviceCategory, matchField) {
return `${String(deviceCategory || "").trim()}|${String(matchField || "").trim()}`;
},
buildSiteEnumOptionMap(mappings = []) {
return (mappings || []).reduce((acc, item) => {
const scopeKey = this.buildEnumScopeKey(item?.deviceCategory, item?.matchField);
const dataEnumCode = this.normalizeDictKey(item?.dataEnumCode);
const enumCode = this.normalizeDictKey(item?.enumCode);
const enumName = String(item?.enumName || "").trim();
const optionKey = dataEnumCode || enumCode;
if (!scopeKey || !optionKey || !enumName) {
return acc;
}
if (!acc[scopeKey]) {
acc[scopeKey] = {};
}
acc[scopeKey][optionKey] = enumName;
return acc;
}, {});
},
loadSiteEnumOptions() {
if (!this.siteId) {
this.siteEnumOptionMap = {};
return Promise.resolve({});
}
return getSingleMonitorWorkStatusEnumMappings(this.siteId).then(response => {
const optionMap = this.buildSiteEnumOptionMap(response?.data || []);
this.siteEnumOptionMap = optionMap;
return optionMap;
}).catch(() => {
this.siteEnumOptionMap = {};
return {};
});
},
getEnumOptions(deviceCategory, matchField, fallback = {}) {
const scopeKey = this.buildEnumScopeKey(deviceCategory, matchField);
const siteOptions = this.siteEnumOptionMap[scopeKey];
if (siteOptions && Object.keys(siteOptions).length > 0) {
return siteOptions;
}
return fallback || {};
},
handleCardClass(item) {
const workStatus = this.normalizeDictKey((item && item.workStatus) || "");
const statusOptions = (this.clusterWorkStatusOptions && typeof this.clusterWorkStatusOptions === "object")
? this.clusterWorkStatusOptions
: {};
const hasStatus = Object.prototype.hasOwnProperty.call(statusOptions, workStatus);
return !hasStatus ? "timing-card-container" : workStatus === '9' ? 'warning-card-container' : 'running-card-container';
},
// 查看设备电位表格
pointDetail(row, dataType) {
const {siteId, deviceId} = row
this.$refs.pointTable.showTable({siteId, deviceId, deviceCategory: 'CLUSTER'}, dataType)
},
hasFieldPointId(baseInfo, fieldName) {
const row = this.getFieldRow(baseInfo, fieldName);
return !!String(row?.dataPoint || "").trim();
},
hasTableFieldPointId(baseInfo, dataName, columnLabel) {
const pointName = this.tablePointNameMap[String(dataName || "") + String(columnLabel || "")];
if (!pointName) {
return false;
}
return this.hasFieldPointId(baseInfo, pointName);
},
getFieldRow(baseInfo, fieldName) {
const key = String(fieldName || "").trim();
const map = baseInfo?._fieldRowMap || {};
return map[key] || null;
},
handleFieldClick(baseInfo, fieldName, title) {
const row = this.getFieldRow(baseInfo, fieldName);
const pointId = String(row?.dataPoint || "").trim();
this.openCurveDialogByPointId(pointId, title || fieldName);
},
handleTableFieldClick(baseInfo, dataName, columnLabel) {
const pointName = this.tablePointNameMap[String(dataName || "") + String(columnLabel || "")];
if (!pointName) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.handleFieldClick(baseInfo, pointName, pointName);
},
openCurveDialogByPointId(pointId, title) {
const normalizedPointId = String(pointId || "").trim();
if (!normalizedPointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || normalizedPointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId: normalizedPointId,
pointType: "data",
rangeType: "custom",
startTime: range[0],
endTime: range[1],
};
this.curveDialogVisible = true;
},
handleCurveDialogOpened() {
if (!this.curveChart && this.$refs.curveChartRef) {
this.curveChart = echarts.init(this.$refs.curveChartRef);
}
this.loadCurveData();
},
handleCurveDialogClosed() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
this.curveLoading = false;
},
getDefaultCurveRange() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return [this.formatDateTime(start), this.formatDateTime(end)];
},
formatDateTime(date) {
const d = new Date(date);
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
},
formatCurveTime(value) {
if (value === undefined || value === null || value === "") {
return "";
}
const raw = String(value).trim();
const normalized = raw
.replace("T", " ")
.replace(/\.\d+/, "")
.replace(/Z$/, "")
.replace(/([+-]\d{2}:?\d{2})$/, "")
.trim();
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
if (matched) {
return `${matched[1]} ${matched[2]}`;
}
return normalized.slice(0, 16);
},
loadCurveData() {
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
this.$message.warning("点位信息不完整,无法查询曲线");
return;
}
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
this.$message.warning("请选择查询时间范围");
return;
}
this.curveQuery.startTime = this.curveCustomRange[0];
this.curveQuery.endTime = this.curveCustomRange[1];
const query = {
siteId: this.curveQuery.siteId,
pointId: this.curveQuery.pointId,
pointType: "data",
rangeType: "custom",
startTime: this.curveQuery.startTime,
endTime: this.curveQuery.endTime,
};
this.curveLoading = true;
getPointConfigCurve(query).then((response) => {
const rows = response?.data || [];
this.renderCurveChart(rows);
}).catch(() => {
this.renderCurveChart([]);
}).finally(() => {
this.curveLoading = false;
});
},
renderCurveChart(rows = []) {
if (!this.curveChart) return;
const xData = rows.map(item => this.formatCurveTime(item.dataTime));
const yData = rows.map(item => item.pointValue);
this.curveChart.clear();
this.curveChart.setOption({
legend: {},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
data: xData,
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: [
{
name: this.curveDialogTitle,
type: "line",
data: yData,
connectNulls: true,
},
],
});
if (!rows.length) {
this.$message.warning("当前时间范围暂无曲线数据");
}
},
handleTagClick(deviceId) {
this.selectedClusterId = deviceId || "";
},
init() {
this.updateData()
this.updateInterval(this.updateData)
},
getModuleRows(menuCode, sectionName) {
return (this.displayData || []).filter(item => item.menuCode === menuCode && item.sectionName === sectionName);
},
getFieldName(fieldCode) {
const raw = String(fieldCode || "").trim();
if (!raw) return "";
const index = raw.lastIndexOf("__");
return index >= 0 ? raw.slice(index + 2) : raw;
},
getFieldMap(rows = [], deviceId = "") {
const rowMap = this.getFieldRowMap(rows, deviceId);
return Object.keys(rowMap).reduce((acc, fieldName) => {
const row = rowMap[fieldName];
if (acc[fieldName] === undefined) {
acc[fieldName] = row?.fieldValue;
}
return acc;
}, {});
},
getFieldRowMap(rows = [], deviceId = "") {
const map = {};
const targetDeviceId = String(deviceId || "");
rows.forEach(item => {
if (!item || !item.fieldCode) return;
const itemDeviceId = String(item.deviceId || "");
if (itemDeviceId !== targetDeviceId) return;
const fieldName = this.getFieldName(item.fieldCode);
map[fieldName] = item;
const displayName = String(item.fieldName || "").trim();
if (displayName && !map[displayName]) {
map[displayName] = item;
}
});
rows.forEach(item => {
if (!item || !item.fieldCode) return;
const itemDeviceId = String(item.deviceId || "");
if (itemDeviceId !== "") return;
const fieldName = this.getFieldName(item.fieldCode);
if (map[fieldName] === undefined || map[fieldName] === null || map[fieldName] === "") {
map[fieldName] = item;
}
const displayName = String(item.fieldName || "").trim();
if (displayName && !map[displayName]) {
map[displayName] = item;
}
});
return map;
},
getLatestTime(menuCode) {
const times = (this.displayData || [])
.filter(item => item.menuCode === menuCode && item.valueTime)
.map(item => new Date(item.valueTime).getTime())
.filter(ts => !isNaN(ts));
if (times.length === 0) {
return '-';
}
const date = new Date(Math.max(...times));
const p = (n) => String(n).padStart(2, '0');
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}`;
},
getClusterDeviceList() {
return getStackNameList(this.siteId)
.then(response => {
const stackList = response?.data || [];
if (!stackList.length) {
this.clusterDeviceList = [];
return;
}
const requests = stackList.map(stack => {
const stackDeviceId = stack.deviceId || stack.id || '';
return getClusterNameList({stackDeviceId, siteId: this.siteId})
.then(clusterResponse => {
const clusterList = clusterResponse?.data || [];
return clusterList.map(cluster => ({
...cluster,
parentDeviceName: stack.deviceName || stack.name || stackDeviceId || '',
}));
})
.catch(() => []);
});
return Promise.all(requests).then(results => {
this.clusterDeviceList = results.flat();
});
})
.catch(() => {
this.clusterDeviceList = [];
});
},
buildBaseInfoList() {
const devices = (this.clusterDeviceList && this.clusterDeviceList.length > 0)
? this.clusterDeviceList
: [{deviceId: this.siteId, deviceName: 'BMS电池簇', parentDeviceName: ''}];
this.baseInfoList = devices.map(device => ({
...(() => {
const id = device.deviceId || device.id || this.siteId;
const infoMap = this.getFieldMap(this.getModuleRows('SBJK_BMSDCC', '簇信息'), id);
const statusMap = this.getFieldMap(this.getModuleRows('SBJK_BMSDCC', '状态'), id);
const currentSoc = Number(infoMap.currentSoc);
return {
...infoMap,
workStatus: statusMap.workStatus,
pcsCommunicationStatus: statusMap.pcsCommunicationStatus,
emsCommunicationStatus: statusMap.emsCommunicationStatus,
currentSoc: isNaN(currentSoc) ? 0 : currentSoc,
_fieldRowMap: {
...this.getFieldRowMap(this.getModuleRows('SBJK_BMSDCC', '簇信息'), id),
...this.getFieldRowMap(this.getModuleRows('SBJK_BMSDCC', '状态'), id),
...this.getFieldRowMap(this.getModuleRows('SBJK_BMSDCC', '单体数据'), id),
},
};
})(),
siteId: this.siteId,
deviceId: device.deviceId || device.id || this.siteId,
parentDeviceName: device.parentDeviceName || '',
deviceName: device.deviceName || device.name || device.deviceId || device.id || 'BMS电池簇',
dataUpdateTime: this.getLatestTime('SBJK_BMSDCC'),
alarmNum: 0,
batteryDataList: [],
}));
},
updateData() {
this.loading = true this.loading = true
// 先渲染卡片框架,字段值走单点位 loading getBMSBatteryCluster(this.siteId).then(response => {
this.buildBaseInfoList(); this.baseInfoList = JSON.parse(JSON.stringify(response?.data || []));
Promise.all([ }).finally(() => {this.loading = false})
getProjectDisplayData(this.siteId),
this.getClusterDeviceList(),
this.loadSiteEnumOptions(),
]).then(([response]) => {
this.displayData = response?.data || [];
this.buildBaseInfoList();
}).finally(() => {
this.loading = false
})
} }
}, }
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.pcs-tags {
margin: 0 0 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-start;
align-items: center;
}
.pcs-tag-item {
cursor: pointer;
}
::v-deep { ::v-deep {
//描述列表样式 //描述列表样式
.descriptions-main { .descriptions-main{
padding: 24px 300px 24px 24px; padding:24px 300px 24px 24px;
} }
.descriptions-main-bottom{
.descriptions-main-bottom { padding:14px 300px 14px 24px;
padding: 14px 300px 14px 24px;
} }
} }
// 进度条样式 // 进度条样式
.process-container { .process-container{
width: 100px; width:100px;
position: absolute; position: absolute;
right: 70px; right:70px;
top: 50%; top:50%;
transform: translateY(-50%); transform: translateY(-50%);
.process-line-bg{
.process-line-bg {
position: relative; position: relative;
width: 100%; width:100%;
height: 110px; height: 110px;
background-color: #fff2cb; background-color:#fff2cb ;
border-radius: 6px; border-radius: 6px;
box-shadow: 0 0 10px #fff2cb, 0 0 0 rgba(255, 242, 203, 0.5); box-shadow: 0 0 10px #fff2cb, 0 0 0 rgba(255, 242, 203, 0.5);
.process-line{
.process-line {
position: absolute; position: absolute;
left: 0; left: 0;
bottom: 0; bottom: 0;
@ -736,23 +142,10 @@ export default {
box-shadow: 0 0 10px #ffbf14, 0 0 0 rgba(255, 191, 20, 0.5); box-shadow: 0 0 10px #ffbf14, 0 0 0 rgba(255, 191, 20, 0.5);
} }
} }
.process{
.process { margin-top:15px;
margin-top: 15px; color:#666666;
color: #666666;
} }
} }
.point-loading-icon {
color: #409eff;
display: inline-block;
transform-origin: center;
animation: pointLoadingSpinPulse 1.1s linear infinite;
}
@keyframes pointLoadingSpinPulse {
0% { opacity: 0.45; transform: rotate(0deg) scale(0.9); }
50% { opacity: 1; transform: rotate(180deg) scale(1.08); }
100% { opacity: 0.45; transform: rotate(360deg) scale(0.9); }
}
</style> </style>

View File

@ -1,603 +1,130 @@
<template> <template>
<div> <div v-loading="loading">
<div class="pcs-tags"> <div v-for="(baseInfo,index) in baseInfoList" :key="index+'bmszlContainer'" style="margin-bottom:25px;">
<el-tag <el-card shadow="always" class="common-card-container common-card-container-body-no-padding common-card-container-no-title-bg">
size="small"
:type="selectedStackId ? 'info' : 'primary'"
:effect="selectedStackId ? 'plain' : 'dark'"
class="pcs-tag-item"
@click="handleTagClick('')"
>
全部
</el-tag>
<el-tag
v-for="(item, index) in stackDeviceList"
:key="index + 'stackTag'"
size="small"
:type="selectedStackId === (item.deviceId || item.id) ? 'primary' : 'info'"
:effect="selectedStackId === (item.deviceId || item.id) ? 'dark' : 'plain'"
class="pcs-tag-item"
@click="handleTagClick(item.deviceId || item.id || '')"
>
{{ item.deviceName || item.name || item.deviceId || item.id || 'BMS总览' }}
</el-tag>
</div>
<div v-for="(baseInfo,index) in filteredBaseInfoList" :key="index+'bmszlContainer'" style="margin-bottom:25px;">
<el-card
:class="handleCardClass(baseInfo)"
class="sbjk-card-container common-card-container-body-no-padding common-card-container-no-title-bg"
shadow="always">
<div slot="header"> <div slot="header">
<span class="large-title">{{ baseInfo.deviceName }}</span> <span class="large-title">{{index+1}}#{{baseInfo.deviceName}}</span>
<div class="info">
<div>数据更新时间{{ baseInfo.dataUpdateTime || '-' }}</div>
</div>
<div class="alarm">
<el-button type="primary" round size="small" style="margin-right:20px;"
@click="pointDetail(baseInfo,'point')">详细
</el-button>
<el-badge :hidden="!baseInfo.alarmNum" :value="baseInfo.alarmNum || 0" class="item">
<i
class="el-icon-message-solid alarm-icon"
@click="pointDetail(baseInfo,'alarmPoint')"
></i>
</el-badge>
</div>
</div> </div>
<div class="descriptions-main"> <div class="descriptions-main">
<el-descriptions :colon="false" :column="3" direction="vertical"> <el-descriptions direction="vertical" :column="3" :colon="false">
<el-descriptions-item <el-descriptions-item labelClassName="descriptions-label" :contentClassName="`descriptions-direction ${baseInfo.workStatus === '0' ? 'save' :'danger'}`" :span="1" label="工作状态" >{{$store.state.ems.workStatusOptions[baseInfo.workStatus]}}</el-descriptions-item>
contentClassName="descriptions-direction work-status" <el-descriptions-item labelClassName="descriptions-label" contentClassName="descriptions-direction" :span="1" label="与PCS通信">{{$store.state.ems.communicationStatusOptions[baseInfo.pcsCommunicationStatus]}}</el-descriptions-item>
label="工作状态" labelClassName="descriptions-label"> <el-descriptions-item labelClassName="descriptions-label" contentClassName="descriptions-direction" :span="1" label="与EMS通信">{{$store.state.ems.communicationStatusOptions[baseInfo.emsCommunicationStatus]}}</el-descriptions-item>
<span class="pointer" @click="handleStatusFieldClick(baseInfo, 'workStatus', '工作状态')">
{{ formatDictValue(stackWorkStatusOptions, baseInfo.workStatus) }}
</span>
</el-descriptions-item>
<el-descriptions-item :span="1" contentClassName="descriptions-direction" label="与PCS通信"
labelClassName="descriptions-label">
<span class="pointer" @click="handleStatusFieldClick(baseInfo, 'pcsCommunicationStatus', '与PCS通信')">
{{ formatDictValue(stackPcsCommunicationStatusOptions, baseInfo.pcsCommunicationStatus) }}
</span>
</el-descriptions-item>
<el-descriptions-item :span="1" contentClassName="descriptions-direction" label="与EMS通信"
labelClassName="descriptions-label">
<span class="pointer" @click="handleStatusFieldClick(baseInfo, 'emsCommunicationStatus', '与EMS通信')">
{{ formatDictValue(stackEmsCommunicationStatusOptions, baseInfo.emsCommunicationStatus) }}
</span>
</el-descriptions-item>
</el-descriptions> </el-descriptions>
</div> </div>
<div class="descriptions-main descriptions-main-bg-color"> <div class="descriptions-main descriptions-main-bg-color">
<el-descriptions :colon="false" :column="3" direction="vertical"> <el-descriptions direction="vertical" :column="3" :colon="false">
<el-descriptions-item v-for="(item,index) in infoData" :key="index+'pcsInfoData'" :label="item.label" <el-descriptions-item labelClassName="descriptions-label" contentClassName="descriptions-direction" v-for="(item,index) in infoData" :key="index+'pcsInfoData'" :span="1" :label="item.label">{{baseInfo[item.attr] | formatNumber}} <span v-if="item.unit" v-html="item.unit"></span></el-descriptions-item>
:span="1" contentClassName="descriptions-direction"
labelClassName="descriptions-label">
<span class="pointer" @click="handleStackFieldClick(baseInfo, item)">
<i v-if="isPointLoading(baseInfo[item.attr])" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(baseInfo[item.attr]) | formatNumber }}</span>
<span v-if="item.unit" v-html="item.unit"></span>
</span>
</el-descriptions-item>
</el-descriptions> </el-descriptions>
<!-- 进度--> <!-- 进度-->
<div class="process-container"> <div class="process-container">
<div class="process-line-bg"> <div class="process-line-bg">
<div :style="{height:baseInfo.stackSoc+'%'}" class="process-line"></div> <div class="process-line" :style="{height:baseInfo.stackSoc+'%'}"></div>
</div>
<div class="process pointer" @click="handleStackSocClick(baseInfo)">当前SOC :
{{ baseInfo.stackSoc }}%
</div> </div>
<div class="process">当前SOC : {{baseInfo.stackSoc}}%</div>
</div> </div>
</div> </div>
<el-table
class="common-table"
:data="baseInfo.batteryDataList"
stripe
max-height="500"
style="width: 100%;margin-top:25px;">
<el-table-column
prop="clusterId"
label="簇号">
</el-table-column>
<el-table-column
label="簇电压"
>
<template slot-scope="scope">
<span>{{scope.row.clusterVoltage}} V</span>
</template>
</el-table-column>
<el-table-column
label="簇电流">
<template slot-scope="scope">
<span>{{scope.row.clusterCurrent}} A</span>
</template>
</el-table-column>
<el-table-column
label="簇SOC">
<template slot-scope="scope">
<span>{{scope.row.currentSoc}} %</span>
</template>
</el-table-column>
<el-table-column
prop="maxVoltage"
label="单体最高电压">
<template slot-scope="scope">
<span>{{scope.row.maxCellVoltage}} V</span>
</template>
</el-table-column>
<el-table-column
prop="minVoltage"
label="单体最低电压">
<template slot-scope="scope">
<span>{{scope.row.minCellVoltage}} V</span>
</template>
</el-table-column>
<el-table-column
label="单体最高温度">
<template slot-scope="scope">
<span>{{scope.row.maxCellTemp}} &#8451;</span>
</template>
</el-table-column>
<el-table-column
prop="minTemperature"
label="单体最低温度">
<template slot-scope="scope">
<span>{{scope.row.minCellTemp}} &#8451;</span>
</template>
</el-table-column>
</el-table>
</el-card> </el-card>
</div> </div>
<el-dialog <el-empty v-show="baseInfoList.length<=0" :image-size="200"></el-empty>
:visible.sync="curveDialogVisible"
:title="curveDialogTitle"
width="1000px"
append-to-body
class="ems-dialog"
:close-on-click-modal="false"
destroy-on-close
@opened="handleCurveDialogOpened"
@closed="handleCurveDialogClosed"
>
<div class="curve-tools">
<el-date-picker
v-model="curveCustomRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 440px"
/>
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button>
</div>
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
</el-dialog>
<point-table ref="pointTable"/>
</div> </div>
</template> </template>
<script> <script>
import * as echarts from "echarts"; import {getBMSOverView} from '@/api/ems/dzjk'
import {getProjectDisplayData, getStackNameList} from '@/api/ems/dzjk'
import {getPointConfigCurve, getSingleMonitorWorkStatusEnumMappings} from "@/api/ems/site";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId"; import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
import {mapState} from "vuex";
export default { export default {
name: 'DzjkSbjkBmszl', name:'DzjkSbjkBmszl',
components: {PointTable}, mixins:[getQuerySiteId],
mixins: [getQuerySiteId, intervalUpdate],
computed: {
...mapState({
STACKWorkStatusOptions: state => state?.ems?.STACKWorkStatusOptions || {},
}),
stackWorkStatusOptions() {
return this.getEnumOptions("STACK", "workStatus", this.STACKWorkStatusOptions || {});
},
stackPcsCommunicationStatusOptions() {
return this.getEnumOptions("STACK", "pcsCommunicationStatus", (this.$store.state.ems && this.$store.state.ems.communicationStatusOptions) || {});
},
stackEmsCommunicationStatusOptions() {
return this.getEnumOptions("STACK", "emsCommunicationStatus", (this.$store.state.ems && this.$store.state.ems.communicationStatusOptions) || {});
},
filteredBaseInfoList() {
if (!this.selectedStackId) {
return this.baseInfoList || [];
}
return (this.baseInfoList || []).filter(item => item.deviceId === this.selectedStackId);
},
},
data() { data() {
return { return {
loading: false, loading:false,
displayData: [], baseInfoList:[],
stackDeviceList: [], infoData:[
siteEnumOptionMap: {}, {label:'电池堆总电压',attr:'stackVoltage',unit:'V'},
selectedStackId: "", {label:'可充电量',attr:'availableChargeCapacity',unit:'kWh'},
curveDialogVisible: false, {label:'累计充电量',attr:'totalChargeCapacity',unit:'kWh'},
curveDialogTitle: "点位曲线", {label:'电池堆总电流',attr:'stackCurrent',unit:'A'},
curveChart: null, {label:'可放电量',attr:'availableDischargeCapacity',unit:'kWh'},
curveLoading: false, {label:'累计放电量',attr:'totalDischargeCapacity',unit:'kWh'},
curveCustomRange: [], {label:'SOH',attr:'stackSoh',unit:'%'},
curveQuery: { {label:'平均温度',attr:'operatingTemp',unit:'&#8451;'},
siteId: "", {label:'绝缘电阻',attr:'stackInsulationResistance',unit:'&Omega;'},
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
baseInfoList: [{
siteId: "",
deviceId: "",
deviceName: "BMS总览",
dataUpdateTime: "-",
alarmNum: 0,
batteryDataList: [],
}],
infoData: [
{label: '电池堆总电压', attr: 'stackVoltage', unit: 'V'},
{label: '可充电量', attr: 'availableChargeCapacity', unit: 'kWh'},
{label: '累计充电量', attr: 'totalChargeCapacity', unit: 'kWh'},
{label: '电池堆总电流', attr: 'stackCurrent', unit: 'A'},
{label: '可放电量', attr: 'availableDischargeCapacity', unit: 'kWh'},
{label: '累计放电量', attr: 'totalDischargeCapacity', unit: 'kWh'},
{label: 'SOH', attr: 'stackSoh', unit: '%'},
{label: '平均温度', attr: 'operatingTemp', unit: '&#8451;'},
{label: '绝缘电阻', attr: 'stackInsulationResistance', unit: '&Omega;'},
] ]
} }
}, },
methods: { methods:{
displayValue(value) { init(){
return value === undefined || value === null || value === "" ? "-" : value; this.loading=true;
}, getBMSOverView(this.siteId).then(response => {
isPointLoading(value) { this.baseInfoList = JSON.parse(JSON.stringify(response?.data || []));
return this.loading && (value === undefined || value === null || value === "" || value === "-"); }).finally(() => {this.loading = false})
}, }
normalizeDictKey(value) {
const raw = String(value == null ? "" : value).trim();
if (!raw) return "";
if (/^-?\d+(\.0+)?$/.test(raw)) {
return String(parseInt(raw, 10));
}
return raw;
},
formatDictValue(options, value) {
const dict = (options && typeof options === "object") ? options : {};
const key = this.normalizeDictKey(value);
if (!key) return "-";
return dict[key] || key;
},
buildEnumScopeKey(deviceCategory, matchField) {
return `${String(deviceCategory || "").trim()}|${String(matchField || "").trim()}`;
},
buildSiteEnumOptionMap(mappings = []) {
return (mappings || []).reduce((acc, item) => {
const scopeKey = this.buildEnumScopeKey(item?.deviceCategory, item?.matchField);
const dataEnumCode = this.normalizeDictKey(item?.dataEnumCode);
const enumCode = this.normalizeDictKey(item?.enumCode);
const enumName = String(item?.enumName || "").trim();
const optionKey = dataEnumCode || enumCode;
if (!scopeKey || !optionKey || !enumName) {
return acc;
}
if (!acc[scopeKey]) {
acc[scopeKey] = {};
}
acc[scopeKey][optionKey] = enumName;
return acc;
}, {});
},
loadSiteEnumOptions() {
if (!this.siteId) {
this.siteEnumOptionMap = {};
return Promise.resolve({});
}
return getSingleMonitorWorkStatusEnumMappings(this.siteId).then(response => {
const optionMap = this.buildSiteEnumOptionMap(response?.data || []);
this.siteEnumOptionMap = optionMap;
return optionMap;
}).catch(() => {
this.siteEnumOptionMap = {};
return {};
});
},
getEnumOptions(deviceCategory, matchField, fallback = {}) {
const scopeKey = this.buildEnumScopeKey(deviceCategory, matchField);
const siteOptions = this.siteEnumOptionMap[scopeKey];
if (siteOptions && Object.keys(siteOptions).length > 0) {
return siteOptions;
}
return fallback || {};
},
handleCardClass(item) {
const workStatus = this.normalizeDictKey((item && item.workStatus) || "");
const statusOptions = (this.stackWorkStatusOptions && typeof this.stackWorkStatusOptions === "object")
? this.stackWorkStatusOptions
: {};
const hasStatus = Object.prototype.hasOwnProperty.call(statusOptions, workStatus);
return !hasStatus ? "timing-card-container" : workStatus === '9' ? 'warning-card-container' : 'running-card-container';
},
// 查看设备电位表格
pointDetail(row, dataType) {
const {siteId, deviceId} = row
this.$refs.pointTable.showTable({siteId, deviceId, deviceCategory: 'STACK'}, dataType)
},
handleStatusFieldClick(baseInfo, fieldKey, title) {
const pointId = this.resolvePointId(baseInfo, fieldKey, "status");
this.openCurveDialogByPointId(pointId, title || fieldKey);
},
handleStackFieldClick(baseInfo, item) {
const fieldKey = item?.attr || "";
const pointId = this.resolvePointId(baseInfo, fieldKey, "info");
this.openCurveDialogByPointId(pointId, item?.label || fieldKey);
},
handleStackSocClick(baseInfo) {
const pointId = this.resolvePointId(baseInfo, "stackSoc", "info");
this.openCurveDialogByPointId(pointId, "当前SOC");
},
handleClusterFieldClick(row = {}, fieldKey = "", title = "") {
const directKeys = [
"pointId",
"dataPoint",
`${fieldKey}PointId`,
`${fieldKey}DataPoint`,
];
let pointId = "";
directKeys.some((key) => {
const value = String(row?.[key] || "").trim();
if (value) {
pointId = value;
return true;
}
return false;
});
if (!pointId && row?.pointIdMap && fieldKey) {
pointId = String(row.pointIdMap[fieldKey] || "").trim();
}
this.openCurveDialogByPointId(pointId, title || fieldKey);
},
resolvePointId(baseInfo = {}, fieldKey = "", source = "info") {
const mapKey = source === "status" ? "statusPointIdMap" : "pointIdMap";
return String(baseInfo?.[mapKey]?.[fieldKey] || "").trim();
},
openCurveDialogByPointId(pointId, title) {
const normalizedPointId = String(pointId || "").trim();
if (!normalizedPointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || normalizedPointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId: normalizedPointId,
pointType: "data",
rangeType: "custom",
startTime: range[0],
endTime: range[1],
};
this.curveDialogVisible = true;
},
handleCurveDialogOpened() {
if (!this.curveChart && this.$refs.curveChartRef) {
this.curveChart = echarts.init(this.$refs.curveChartRef);
}
this.loadCurveData();
},
handleCurveDialogClosed() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
this.curveLoading = false;
},
getDefaultCurveRange() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return [this.formatDateTime(start), this.formatDateTime(end)];
},
formatDateTime(date) {
const d = new Date(date);
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
},
formatCurveTime(value) {
if (value === undefined || value === null || value === "") {
return "";
}
const raw = String(value).trim();
const normalized = raw
.replace("T", " ")
.replace(/\.\d+/, "")
.replace(/Z$/, "")
.replace(/([+-]\d{2}:?\d{2})$/, "")
.trim();
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
if (matched) {
return `${matched[1]} ${matched[2]}`;
}
return normalized.slice(0, 16);
},
loadCurveData() {
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
this.$message.warning("点位信息不完整,无法查询曲线");
return;
}
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
this.$message.warning("请选择查询时间范围");
return;
}
this.curveQuery.startTime = this.curveCustomRange[0];
this.curveQuery.endTime = this.curveCustomRange[1];
const query = {
siteId: this.curveQuery.siteId,
pointId: this.curveQuery.pointId,
pointType: "data",
rangeType: "custom",
startTime: this.curveQuery.startTime,
endTime: this.curveQuery.endTime,
};
this.curveLoading = true;
getPointConfigCurve(query).then((response) => {
const rows = response?.data || [];
this.renderCurveChart(rows);
}).catch(() => {
this.renderCurveChart([]);
}).finally(() => {
this.curveLoading = false;
});
},
renderCurveChart(rows = []) {
if (!this.curveChart) return;
const xData = rows.map((item) => this.formatCurveTime(item.dataTime));
const yData = rows.map((item) => item.pointValue);
this.curveChart.clear();
this.curveChart.setOption({
legend: {},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
data: xData,
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: [
{
name: this.curveDialogTitle,
type: "line",
data: yData,
connectNulls: true,
},
],
});
if (!rows.length) {
this.$message.warning("当前时间范围暂无曲线数据");
}
},
init() {
this.updateData()
this.updateInterval(this.updateData)
},
getModuleRows(menuCode, sectionName) {
return (this.displayData || []).filter(item => item.menuCode === menuCode && item.sectionName === sectionName);
},
getFieldName(fieldCode) {
const raw = String(fieldCode || "").trim();
if (!raw) return "";
const index = raw.lastIndexOf("__");
return index >= 0 ? raw.slice(index + 2) : raw;
},
isEmptyValue(value) {
return value === undefined || value === null || value === "";
},
getFieldRowMap(rows = [], deviceId = "") {
const map = {};
const targetDeviceId = String(deviceId || "");
rows.forEach(item => {
if (!item || !item.fieldCode) return;
const itemDeviceId = String(item.deviceId || "");
if (itemDeviceId !== targetDeviceId) return;
map[this.getFieldName(item.fieldCode)] = item;
});
rows.forEach(item => {
if (!item || !item.fieldCode) return;
const itemDeviceId = String(item.deviceId || "");
if (itemDeviceId !== "") return;
const fieldName = this.getFieldName(item.fieldCode);
const existRow = map[fieldName];
if (!existRow || this.isEmptyValue(existRow.fieldValue)) {
map[fieldName] = item;
}
});
return map;
},
getFieldMap(rowMap = {}) {
const map = {};
Object.keys(rowMap || {}).forEach((fieldName) => {
map[fieldName] = rowMap[fieldName]?.fieldValue;
});
return map;
},
getPointIdMap(rowMap = {}) {
const map = {};
Object.keys(rowMap || {}).forEach((fieldName) => {
map[fieldName] = String(rowMap[fieldName]?.dataPoint || "").trim();
});
return map;
},
getLatestTime(menuCode) {
const times = (this.displayData || [])
.filter(item => item.menuCode === menuCode && item.valueTime)
.map(item => new Date(item.valueTime).getTime())
.filter(ts => !isNaN(ts));
if (times.length === 0) {
return '-';
}
const date = new Date(Math.max(...times));
const p = (n) => String(n).padStart(2, '0');
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}`;
},
handleTagClick(deviceId) {
this.selectedStackId = deviceId || "";
},
getStackDeviceList() {
return getStackNameList(this.siteId).then(response => {
this.stackDeviceList = response?.data || [];
}).catch(() => {
this.stackDeviceList = [];
});
},
buildBaseInfoList() {
const devices = (this.stackDeviceList && this.stackDeviceList.length > 0)
? this.stackDeviceList
: [{deviceId: this.siteId, deviceName: 'BMS总览'}];
this.baseInfoList = devices.map(device => ({
...(() => {
const id = device.deviceId || device.id || this.siteId;
const infoRowMap = this.getFieldRowMap(this.getModuleRows('SBJK_BMSZL', '堆信息'), id);
const statusRowMap = this.getFieldRowMap(this.getModuleRows('SBJK_BMSZL', '状态'), id);
const infoMap = this.getFieldMap(infoRowMap);
const statusMap = this.getFieldMap(statusRowMap);
const stackSoc = Number(infoMap.stackSoc);
return {
...infoMap,
workStatus: statusMap.workStatus,
pcsCommunicationStatus: statusMap.pcsCommunicationStatus,
emsCommunicationStatus: statusMap.emsCommunicationStatus,
stackSoc: isNaN(stackSoc) ? 0 : stackSoc,
pointIdMap: this.getPointIdMap(infoRowMap),
statusPointIdMap: this.getPointIdMap(statusRowMap),
};
})(),
siteId: this.siteId,
deviceId: device.deviceId || device.id || this.siteId,
deviceName: device.deviceName || device.name || device.deviceId || device.id || 'BMS总览',
dataUpdateTime: this.getLatestTime('SBJK_BMSZL'),
alarmNum: 0,
batteryDataList: [],
}));
},
updateData() {
this.loading = true
// 先渲染卡片框架,字段值走单点位 loading
this.buildBaseInfoList();
Promise.all([
getProjectDisplayData(this.siteId),
this.getStackDeviceList(),
this.loadSiteEnumOptions(),
]).then(([displayResponse]) => {
this.displayData = displayResponse?.data || [];
this.buildBaseInfoList();
}).finally(() => {
this.loading = false
})
},
} }
,
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
} }
</script> </script>
<style lang="scss" scoped> <style scoped lang="scss">
.pcs-tags {
margin: 0 0 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-start;
align-items: center;
}
.pcs-tag-item {
cursor: pointer;
}
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
::v-deep { ::v-deep {
//描述列表样式 //描述列表样式
.descriptions-main { .descriptions-main {
@ -608,24 +135,21 @@ export default {
} }
} }
} }
// 进度条样式 // 进度条样式
.process-container { .process-container{
width: 100px; width:100px;
position: absolute; position: absolute;
right: 70px; right:70px;
top: 50%; top:50%;
transform: translateY(-50%); transform: translateY(-50%);
.process-line-bg{
.process-line-bg {
position: relative; position: relative;
width: 100%; width:100%;
height: 110px; height: 110px;
background-color: #fff2cb; background-color:#fff2cb ;
border-radius: 6px; border-radius: 6px;
box-shadow: 0 0 10px #fff2cb, 0 0 0 rgba(255, 242, 203, 0.5); box-shadow: 0 0 10px #fff2cb, 0 0 0 rgba(255, 242, 203, 0.5);
.process-line{
.process-line {
position: absolute; position: absolute;
left: 0; left: 0;
bottom: 0; bottom: 0;
@ -636,23 +160,10 @@ export default {
box-shadow: 0 0 10px rgb(252 108 108), 0 0 0 rgba(252, 108, 108, 0.5); box-shadow: 0 0 10px rgb(252 108 108), 0 0 0 rgba(252, 108, 108, 0.5);
} }
} }
.process{
.process { margin-top:15px;
margin-top: 15px; color:#666666;
color: #666666;
} }
} }
.point-loading-icon {
color: #409eff;
display: inline-block;
transform-origin: center;
animation: pointLoadingSpinPulse 1.1s linear infinite;
}
@keyframes pointLoadingSpinPulse {
0% { opacity: 0.45; transform: rotate(0deg) scale(0.9); }
50% { opacity: 1; transform: rotate(180deg) scale(1.08); }
100% { opacity: 0.45; transform: rotate(360deg) scale(0.9); }
}
</style> </style>

View File

@ -1,497 +1,134 @@
<template>
<div>
<div class="pcs-tags">
<el-tag
size="small"
:type="selectedSectionKey ? 'info' : 'primary'"
:effect="selectedSectionKey ? 'plain' : 'dark'"
class="pcs-tag-item"
@click="handleTagClick('')"
>
全部
</el-tag>
<el-tag
v-for="(group, index) in sectionGroups"
:key="index + 'dbTag'"
size="small"
:type="selectedSectionKey === group.sectionKey ? 'primary' : 'info'"
:effect="selectedSectionKey === group.sectionKey ? 'dark' : 'plain'"
class="pcs-tag-item"
@click="handleTagClick(group.sectionKey)"
>
{{ group.displayName || "电表" }}
</el-tag>
</div>
<el-card
v-for="(group, index) in filteredSectionGroups"
:key="index + 'dbSection'"
class="sbjk-card-container list running-card-container"
shadow="always"
>
<div slot="header">
<span class="large-title">{{ group.displayName || "电表" }}</span>
<div class="info">
<div>状态{{ group.statusText }}</div>
<div>数据更新时间{{ group.updateTimeText }}</div>
</div>
</div>
<el-row class="device-info-row">
<el-col
v-for="(item, dataIndex) in group.items"
:key="dataIndex + 'dbField'"
:span="8"
class="device-info-col"
:class="{ 'field-disabled': !item.pointId }"
>
<div class="field-click-wrapper" @click="handleFieldClick(item)">
<span class="left">{{ item.fieldName }}</span>
<span class="right">
<i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span>
</span>
</div>
</el-col>
</el-row>
</el-card>
<el-dialog <template>
:visible.sync="curveDialogVisible" <div v-loading="loading">
:title="curveDialogTitle" <el-card shadow="always" class="common-card-container" :class="zbInfo.emsCommunicationStatus === '1' ? 'zb-common-card-container' : 'cnb-common-card-container'">
width="1000px" <div slot="header">
append-to-body <span class="large-title">1#{{zbInfo.deviceName}}</span>
class="ems-dialog" <div class="status">
:close-on-click-modal="false" <div>{{$store.state.ems.communicationStatusOptions[zbInfo.emsCommunicationStatus]}}</div>
destroy-on-close <div>数据更新时间{{zbInfo.dataUpdateTime}}</div>
@opened="handleCurveDialogOpened" </div>
@closed="handleCurveDialogClosed" </div>
> <el-table
<div class="curve-tools"> class="common-table"
<el-date-picker :data="zbInfo.loadDataDetailInfo"
v-model="curveCustomRange" stripe
type="datetimerange" style="width: 100%;">
value-format="yyyy-MM-dd HH:mm:ss" <el-table-column
range-separator="" prop="category"
start-placeholder="开始时间" label="类别">
end-placeholder="结束时间" </el-table-column>
style="width: 440px" <el-table-column
/> prop="totalKwh"
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button> label="总/kWh"
>
</el-table-column>
<el-table-column
prop="peakKwh"
label="尖/kWh">
</el-table-column>
<el-table-column
prop="highKwh"
label="峰/kWh">
</el-table-column>
<el-table-column
prop="flatKwh"
label="平/kWh">
</el-table-column>
<el-table-column
prop="valleyKwh"
label="谷/kWh">
</el-table-column>
</el-table>
</el-card>
<el-card shadow="always" class="common-card-container" style="margin-top:20px" :class="cnbInfo.emsCommunicationStatus === '1' ? 'zb-common-card-container' : 'cnb-common-card-container'">
<div slot="header">
<span class="large-title">2#{{cnbInfo.deviceName}}</span>
<div class="status">
<div>{{$store.state.ems.communicationStatusOptions[cnbInfo.emsCommunicationStatus]}}</div>
<div>数据更新时间{{cnbInfo.dataUpdateTime}}</div>
</div>
</div>
<el-table
class="common-table"
:data="cnbInfo.meteDataDetailInfo"
stripe
style="width: 100%;">
<el-table-column
prop="category"
label="类别">
</el-table-column>
<el-table-column
prop="activePower"
label="有功功率"
>
</el-table-column>
<el-table-column
prop="reactivePower"
label="无功功率">
</el-table-column>
</el-table>
</el-card>
</div> </div>
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
</el-dialog>
</div>
</template> </template>
<script>
import * as echarts from "echarts";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import { getProjectDisplayData } from "@/api/ems/dzjk";
import { getDeviceList, getPointConfigCurve } from "@/api/ems/site";
<script>
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getAmmeterDataList} from '@/api/ems/dzjk'
export default { export default {
name: "DzjkSbjkDb", name:'DzjkSbjkDb',
mixins: [getQuerySiteId, intervalUpdate], mixins:[getQuerySiteId],
data() { data() {
return { return {
loading: false, loading:false,
displayData: [], zbInfo:{},
selectedSectionKey: "", cnbInfo:{},
ammeterDeviceList: [],
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
};
},
computed: {
moduleDisplayData() {
return (this.displayData || []).filter((item) => item.menuCode === "SBJK_DB");
},
dbTemplateFields() {
const source = this.moduleDisplayData || [];
const result = [];
const seen = new Set();
source.forEach((item) => {
const fieldName = String(item?.fieldName || "").trim();
if (!fieldName || seen.has(fieldName)) {
return;
}
seen.add(fieldName);
result.push(fieldName);
});
return result.length > 0 ? result : this.fallbackFields;
},
sectionGroups() {
const source = this.moduleDisplayData || [];
const devices = (this.ammeterDeviceList || []).length > 0
? this.ammeterDeviceList
: [{ deviceId: "", deviceName: "电表" }];
return devices.map((device, index) => {
const deviceId = String(device?.deviceId || device?.id || "").trim();
const sectionKey = deviceId || `AMMETER_${index}`;
const displayName = String(device?.deviceName || device?.name || deviceId || `电表${index + 1}`).trim();
const exactRows = source.filter((item) => String(item?.deviceId || "").trim() === deviceId);
const fallbackRows = source.filter((item) => !String(item?.deviceId || "").trim());
const exactValueMap = {};
exactRows.forEach((item) => {
const key = String(item?.fieldName || "").trim();
if (key) {
exactValueMap[key] = item;
}
});
const fallbackValueMap = {};
fallbackRows.forEach((item) => {
const key = String(item?.fieldName || "").trim();
if (key && fallbackValueMap[key] === undefined) {
fallbackValueMap[key] = item;
}
});
const items = (this.dbTemplateFields || []).map((fieldName) => {
const row = exactValueMap[fieldName] || fallbackValueMap[fieldName] || {};
return {
fieldName,
fieldValue: row.fieldValue,
valueTime: row.valueTime,
pointId: String(row?.dataPoint || "").trim(),
raw: row,
};
});
const statusItem = (items || []).find((it) => String(it.fieldName || "").includes("状态"));
const timestamps = [...exactRows, ...fallbackRows]
.map((it) => new Date(it?.valueTime).getTime())
.filter((ts) => !isNaN(ts));
return {
sectionName: displayName,
sectionKey,
displayName,
deviceId,
items,
statusText: this.displayValue(statusItem ? statusItem.fieldValue : "-"),
updateTimeText: timestamps.length > 0 ? this.formatDate(new Date(Math.max(...timestamps))) : "-",
};
});
},
displaySectionGroups() {
if (this.sectionGroups.length > 0) {
return this.sectionGroups;
}
return [
{
sectionName: "电参量",
sectionKey: "电参量",
displayName: "电表",
items: this.fallbackFields.map((fieldName) => ({ fieldName, fieldValue: "-" })),
statusText: "-",
updateTimeText: "-",
},
];
},
filteredSectionGroups() {
const groups = this.displaySectionGroups || [];
if (!this.selectedSectionKey) {
return groups;
}
return groups.filter((group) => group.sectionKey === this.selectedSectionKey);
},
fallbackFields() {
return [
"正向有功电能",
"反向有功电能",
"正向无功电能",
"反向无功电能",
"有功功率",
"无功功率",
];
},
},
methods: {
handleFieldClick(item) {
const pointId = String(item?.pointId || item?.raw?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: item?.fieldName || pointId,
});
},
openCurveDialog({ pointId, title }) {
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || pointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId,
pointType: "data",
rangeType: "custom",
startTime: range[0],
endTime: range[1],
};
this.curveDialogVisible = true;
},
handleCurveDialogOpened() {
if (!this.curveChart && this.$refs.curveChartRef) {
this.curveChart = echarts.init(this.$refs.curveChartRef);
}
this.loadCurveData();
},
handleCurveDialogClosed() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
this.curveLoading = false;
},
getDefaultCurveRange() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return [this.formatDateTime(start), this.formatDateTime(end)];
},
formatDateTime(date) {
const d = new Date(date);
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
},
formatCurveTime(value) {
if (value === undefined || value === null || value === "") {
return "";
}
const raw = String(value).trim();
const normalized = raw
.replace("T", " ")
.replace(/\.\d+/, "")
.replace(/Z$/, "")
.replace(/([+-]\d{2}:?\d{2})$/, "")
.trim();
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
if (matched) {
return `${matched[1]} ${matched[2]}`;
}
return normalized.slice(0, 16);
},
loadCurveData() {
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
this.$message.warning("点位信息不完整,无法查询曲线");
return;
}
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
this.$message.warning("请选择查询时间范围");
return;
}
this.curveQuery.startTime = this.curveCustomRange[0];
this.curveQuery.endTime = this.curveCustomRange[1];
const query = {
siteId: this.curveQuery.siteId,
pointId: this.curveQuery.pointId,
pointType: "data",
rangeType: "custom",
startTime: this.curveQuery.startTime,
endTime: this.curveQuery.endTime,
};
this.curveLoading = true;
getPointConfigCurve(query)
.then((response) => {
const rows = response?.data || [];
this.renderCurveChart(rows);
})
.catch(() => {
this.renderCurveChart([]);
})
.finally(() => {
this.curveLoading = false;
});
},
renderCurveChart(rows = []) {
if (!this.curveChart) return;
const xData = rows.map((item) => this.formatCurveTime(item.dataTime));
const yData = rows.map((item) => item.pointValue);
this.curveChart.clear();
this.curveChart.setOption({
legend: {},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
data: xData,
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: [
{
name: this.curveDialogTitle,
type: "line",
data: yData,
connectNulls: true,
},
],
});
if (!rows.length) {
this.$message.warning("当前时间范围暂无曲线数据");
}
},
handleTagClick(sectionKey) {
this.selectedSectionKey = sectionKey || "";
},
displayValue(value) {
return value === undefined || value === null || value === "" ? "-" : value;
},
isPointLoading(value) {
return this.loading && (value === undefined || value === null || value === "" || value === "-");
},
formatDate(date) {
if (!(date instanceof Date) || isNaN(date.getTime())) {
return "-";
}
const p = (n) => String(n).padStart(2, "0");
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(
date.getMinutes()
)}:${p(date.getSeconds())}`;
},
resolveDbDisplayName(sectionName) {
const key = String(sectionName || "").trim();
if (!key) {
return "电表";
}
const list = this.ammeterDeviceList || [];
const matched = list.find((item) => {
const deviceId = String(item.deviceId || item.id || "").trim();
const deviceName = String(item.deviceName || item.name || "").trim();
return key === deviceId || key === deviceName;
});
if (matched) {
return matched.deviceName || matched.name || key;
}
return key;
},
getAmmeterDeviceList() {
return getDeviceList(this.siteId)
.then((response) => {
const list = response?.data || [];
this.ammeterDeviceList = list.filter((item) => item.deviceCategory === "AMMETER");
})
.catch(() => {
this.ammeterDeviceList = [];
});
},
updateData() {
this.loading = true;
Promise.all([getProjectDisplayData(this.siteId), this.getAmmeterDeviceList()])
.then(([response]) => {
this.displayData = response?.data || [];
})
.finally(() => {
this.loading = false;
});
},
init() {
this.updateData();
this.updateInterval(this.updateData);
},
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
} }
}, },
}; methods:{
init(){
this.loading = true
getAmmeterDataList(this.siteId).then(response => {
this.zbInfo =JSON.parse(JSON.stringify(response?.data?.ammeterLoadData || {}));
this.cnbInfo =JSON.parse(JSON.stringify(response?.data?.ammeterMeteData || {}));
}).finally(() => {this.loading = false})
}
},
mounted(){
}
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.sbjk-card-container { .zb-common-card-container,.cnb-common-card-container{
&.list:not(:last-child) { ::v-deep{
margin-bottom: 25px; .el-card__header{
} padding:10px 14px;
background-color: #FC6B69;
.info { color:#ffffff;
float: right; position: relative;
text-align: right; }
font-size: 12px;
color: #666;
line-height: 18px;
} }
} }
.cnb-common-card-container{
.pcs-tags { margin-top:25px;
margin: 0 0 12px; ::v-deep{
display: flex; .el-card__header{
flex-wrap: wrap; background-color: #05AEA3;
gap: 8px; }
justify-content: flex-start; }
align-items: center; }
.status{
position: absolute;
right:14px;
top:50%;
transform: translateY(-50%);
color: #ffffff;
font-size: 12px;
line-height: 20px;
} }
.pcs-tag-item {
cursor: pointer;
}
.device-info-col {
cursor: pointer;
}
.field-click-wrapper {
width: 100%;
}
.device-info-col.field-disabled {
cursor: not-allowed;
opacity: 0.8;
}
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.point-loading-icon {
color: #409eff;
display: inline-block;
transform-origin: center;
animation: pointLoadingSpinPulse 1.1s linear infinite;
}
@keyframes pointLoadingSpinPulse {
0% { opacity: 0.45; transform: rotate(0deg) scale(0.9); }
50% { opacity: 1; transform: rotate(180deg) scale(1.08); }
100% { opacity: 0.45; transform: rotate(360deg) scale(0.9); }
}
</style> </style>

View File

@ -1,140 +0,0 @@
<template>
<div>
<el-card
v-for="(item,index) in list"
:key="index+'ylLise'"
class="sbjk-card-container running-card-container"
shadow="always">
<div slot="header">
<span class="large-title">{{ item.deviceName }}</span>
<div class="info">
<div>数据更新时间{{ item.dataUpdateTime || '-' }}</div>
</div>
<div class="alarm">
<el-button type="primary" round size="small" style="margin-right:20px;" @click="pointDetail(item,'point')">
详细
</el-button>
<el-badge :hidden="!item.alarmNum" :value="item.alarmNum || 0" class="item">
<i
class="el-icon-message-solid alarm-icon"
@click="pointDetail(item,'alarmPoint')"
></i>
</el-badge>
</div>
</div>
<el-row class="device-info-row">
<el-col v-for="(tempDataItem,tempDataIndex) in tempData" :key="tempDataIndex+'hdTempData'" :span="12"
class="device-info-col">
<span class="pointer" @click="showChart(tempDataItem.title,item.deviceId)">
<span class="left">{{ tempDataItem.title }}</span>
<span class="right">
<i v-if="isPointLoading(item[tempDataItem.attr])" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item[tempDataItem.attr]) }}<span v-html="tempDataItem.unit"></span></span>
</span>
</span>
</el-col>
</el-row>
</el-card>
<el-empty v-show="list.length<=0" :image-size="200"></el-empty>
<point-chart ref="pointChart" :site-id="siteId"/>
<point-table ref="pointTable"/>
</div>
</template>
<script>
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getDhDataList} from '@/api/ems/dzjk'
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import pointChart from "./../PointChart.vue";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
export default {
name: 'DzjkSbjkDh',
mixins: [getQuerySiteId, intervalUpdate],
components: {pointChart, PointTable},
data() {
return {
loading: false,
list: [],
tempData: [
{title: '湿度', attr: 'humidity', unit: ''},
{title: '温度', attr: 'temperature', unit: '&#8451;'},
]
}
},
methods: {
displayValue(value) {
return value === undefined || value === null || value === "" ? "-" : value;
},
isPointLoading(value) {
return this.loading && (value === undefined || value === null || value === "" || value === "-");
},
// 查看设备电位表格
pointDetail(row, dataType) {
const {deviceId} = row
this.$refs.pointTable.showTable({siteId: this.siteId, deviceId, deviceCategory: 'DH'}, dataType)
},
showChart(pointName, deviceId) {
pointName && this.$refs.pointChart.showChart({pointName, deviceCategory: 'DH', deviceId})
},
updateData() {
this.loading = true
getDhDataList(this.siteId).then(response => {
this.list = JSON.parse(JSON.stringify(response?.data || []));
}).finally(() => {
this.loading = false
})
},
init() {
this.updateData()
this.updateInterval(this.updateData)
}
},
mounted() {
}
}
</script>
<style scoped lang="scss">
.sbjk-card-container {
&:not(:last-child) {
margin-bottom: 25px;
}
.el-row {
background-color: #ffffff;
border: 1px solid #eeeeee;
font-size: 14px;
line-height: 16px;
color: #333333;
.el-col {
padding: 12px 0;
text-align: center;
position: relative;
}
.el-col {
border-bottom: 1px solid #eeeeee;
}
.el-col:not(:nth-child(3n)) {
border-right: 1px solid #eeeeee;
}
}
}
.point-loading-icon {
color: #409eff;
display: inline-block;
transform-origin: center;
animation: pointLoadingSpinPulse 1.1s linear infinite;
}
@keyframes pointLoadingSpinPulse {
0% { opacity: 0.45; transform: rotate(0deg) scale(0.9); }
50% { opacity: 1; transform: rotate(180deg) scale(1.08); }
100% { opacity: 0.45; transform: rotate(360deg) scale(0.9); }
}
</style>

View File

@ -7,193 +7,167 @@
lock-scroll lock-scroll
append-to-body append-to-body
width="700px" width="700px"
class="ems-dialog chart-detail-dialog" class="ems-dialog"
:before-close="handleColsed" :before-close="handleColsed"
> >
<el-card <div>
shadow="always" <el-form size="medium" label-width="100px" inline>
class="common-card-container time-range-card" <el-form-item label="时间选择">
style="margin-top: 20px" <el-date-picker
> v-model="dateRange"
<div slot="header" class="time-range-header"> type="daterange"
<span class="card-title"></span> range-separator=""
<date-range-select ref="dateRangeSelect" @updateDate="updateDate" /> start-placeholder="开始时间"
</div> value-format="yyyy-MM-dd"
<div class="card-main" v-loading="loading"> :picker-options="pickerOptions"
<div id="lineChart" style="height: 310px"></div> :default-value="defaultDateRange"
</div> end-placeholder="结束时间">
</el-card> </el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getData">搜索</el-button>
<el-button @click="onReset">重置</el-button>
</el-form-item>
</el-form>
<div id="lineChart" style="height: 360px;width: 100%;"></div>
</div>
</el-dialog> </el-dialog>
</template> </template>
<script> <script>
import * as echarts from "echarts"; import * as echarts from 'echarts'
import resize from "@/mixins/ems/resize"; import resize from '@/mixins/ems/resize'
import { getSingleBatteryData } from "@/api/ems/dzjk"; import {getSingleBatteryData} from '@/api/ems/dzjk'
import DateRangeSelect from "@/components/Ems/DateRangeSelect/index.vue";
export default { export default {
components: { DateRangeSelect },
mixins: [resize], mixins: [resize],
data() { data() {
return { return {
loading: false, loading: false,
siteId: "", siteId:'',
deviceId: "", deviceId:'',
clusterDeviceId: "", clusterDeviceId:'',
dataType: "", //展示的数据类型 空值展示所有数据 dataType:'',//展示的数据类型 空值展示所有数据
pickerOptions: { pickerOptions:{
disabledDate(time) { disabledDate(time) {
return time.getTime() > Date.now(); return time.getTime() > Date.now();
}, },
}, },
dialogTableVisible: false, dialogTableVisible: false,
dateRange: [], dateRange: [],
}; defaultDateRange:[]
}
}, },
methods: { methods: {
// 更新时间范围 重置图表 handleColsed(done){
updateDate(data) {
this.dateRange = data || [];
this.getData();
},
handleColsed(done) {
if (!this.chart) { if (!this.chart) {
return done(); return done()
} }
this.chart.dispose(); this.chart.dispose()
this.chart = null; this.chart = null
done(); done()
}, },
getData() { getData(){
if (this.loading) return; if(this.loading) return
this.loading = true; this.loading = true;
this.chart.showLoading(); this.chart.showLoading()
const { const {siteId, deviceId,clusterDeviceId,dateRange:[startDate='',endDate='']}=this;
siteId, getSingleBatteryData({siteId, deviceId,clusterDeviceId,startDate,endDate}).then(response => {
deviceId, this.setOption(response?.data || [])
clusterDeviceId, }).finally(()=>{
dateRange: [startDate = "", endDate = ""], this.loading = false;
} = this; this.chart.hideLoading()
getSingleBatteryData({
siteId,
deviceId,
clusterDeviceId,
startDate,
endDate,
}) })
.then((response) => {
this.setOption(response?.data || []);
})
.finally(() => {
this.loading = false;
this.chart.hideLoading();
});
}, },
initChart({ siteId, clusterDeviceId, deviceId }, dataType) { // 重置
this.siteId = siteId; onReset(){
this.clusterDeviceId = clusterDeviceId; this.dateRange=[]
this.deviceId = deviceId; this.getData()
this.dataType = dataType; },
this.dialogTableVisible = true; initChart({siteId, clusterDeviceId, deviceId},dataType) {
this.$nextTick(() => { this.siteId=siteId
!this.chart && this.clusterDeviceId=clusterDeviceId
(this.chart = echarts.init(document.querySelector("#lineChart"))); this.deviceId=deviceId
this.$refs.dateRangeSelect.init(); this.dataType=dataType
}); this.dateRange=[]
this.dialogTableVisible = true
this.$nextTick(()=>{
!this.chart && (this.chart = echarts.init(document.querySelector('#lineChart')))
this.getData()
})
}, },
setOption(data) { setOption(data) {
const obj = { const obj = {
voltage: "电压", voltage:'电压',
temperature: "温度", temperature:'温度',
soc: "SOC", soc:'SOC',
soh: "SOH", soh:'SOH',
};
let source,
series,
{ dataType } = this;
if (dataType) {
source = [["日期", obj[dataType]]];
data.forEach((item) => {
source.push([item.dataTimestamp, item[dataType]]);
});
series = [
{
name: obj[dataType],
type: "line",
},
];
} else {
source = [["日期", "电压", "温度", "SOC", "SOH"]];
data.forEach((item) => {
source.push([
item.dataTimestamp,
item.voltage,
item.temperature,
item.soc,
item.soh,
]);
});
series = [
{
name: "电压",
type: "line",
},
{
name: "温度",
type: "line",
},
{
name: "SOC",
type: "line",
},
{
name: "SOH",
type: "line",
},
];
} }
this.chart && let source,series,{dataType} = this
this.chart.setOption({ if(dataType){
color: ["#FFBD00", "#3C81FF", "#05AEA3", "#F86F70"], source = [['日期',obj[dataType]]]
grid: { data.forEach(item => {
containLabel: true, source.push([item.dataTimestamp,item[dataType]])
})
series=[{
name:obj[dataType],
type: 'line',
}]
}else{
source = [['日期','电压','温度','SOC','SOH']]
data.forEach(item => {
source.push([item.dataTimestamp,item.voltage,item.temperature,item.soc,item.soh])
})
series=[
{
name:'电压',
type: 'line',
},{
name:'温度',
type: 'line',
}, },
legend: { {
left: "center", name:'SOC',
bottom: "15", type: 'line',
},
tooltip: { },{
trigger: "axis", name:'SOH',
axisPointer: { type: 'line',
// 坐标轴指示器,坐标轴触发有效 }]
type: "shadow", // 默认为直线,可选为:'line' | 'shadow' }
},
},
textStyle: { this.chart && this.chart.setOption({
color: "#333333", color:['#FFBD00','#3C81FF','#05AEA3','#F86F70'],
}, legend: {
xAxis: { bottom: '10',
type: "category", },
}, tooltip: {
yAxis: { trigger: 'axis',
type: "value", axisPointer: { // 坐标轴指示器,坐标轴触发有效
}, type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
dataset: { }
source, },
}, textStyle:{
series, color:"#333333",
}); },
}, xAxis: {
}, type: 'category',
mounted() {}, },
}; yAxis: {
</script> type: 'value',
<style lang="scss" scoped> },
.chart-detail-dialog { dataset:{
::v-deep { source
.el-dialog__body { },
padding-top: 0; series
})
} }
},
mounted(){
const now = new Date();
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
this.defaultDateRange = [lastMonth, now];
} }
} }
</style> </script>

View File

@ -1,192 +0,0 @@
<template>
<div>
<template v-if="totalSize.length === 0">
<el-empty :size="200"></el-empty>
</template>
<template v-else>
<div class="lists-container clearfix">
<div
class="lists"
v-for="(item, index) in tableData"
:key="index + 'dtdcList'"
:class="handleListClass(item)"
>
<div style="font-size: 10px; font-weight: 600">
{{ item.clusterDeviceId }}
</div>
<div>#{{ item.deviceId }}</div>
<div class="dy pointer" @click="chartDetail(item, 'voltage')">
{{ item.voltage }}V
</div>
<div class="wd pointer" @click="chartDetail(item, 'temperature')">
{{ item.temperature }}
</div>
</div>
</div>
<!-- <el-pagination
v-show="tableData.length > 0"
background
@size-change="(val) => $emit('handleSizeChange', val)"
@current-change="(val) => $emit('handleCurrentChange', val)"
:current-page="pageNum"
:page-size="pageSize"
:page-sizes="[10, 20, 30, 40]"
layout="total, sizes, prev, pager, next, jumper"
:total="totalSize"
style="margin-top: 15px; text-align: center"
>
</el-pagination> -->
</template>
</div>
</template>
<script>
export default {
props: {
pointIdList: {
require: true,
type: Object,
default: () => {
return {};
},
},
tableData: {
require: true,
type: Array,
default: () => {
return [];
},
},
totalSize: {
require: true,
type: Number,
default: 0,
},
// pageNum: {
// require: true,
// type: Number,
// default: 1,
// },
// pageSize: {
// require: true,
// type: Number,
// default: 10,
// },
},
data() {
return {
//最低单体温度 最高温度 最低电压 最高电压 todo 这里的顺序需要和图形组件里的顺序保持一致,
colorMap: {
0: "minwd",
1: "maxwd",
2: "mindy",
3: "maxdy",
},
};
},
methods: {
//处理图形class 对应高亮设置
handleListClass(item) {
let className = "";
const { clusterDeviceId, deviceId } = item,
clusterIdList = Object.keys(this.pointIdList);
if (clusterIdList.includes(clusterDeviceId)) {
const index = this.pointIdList[clusterDeviceId].findIndex(
(ids) => ids === parseInt(deviceId)
);
if (index > -1) {
className = this.colorMap[index];
}
}
return className;
},
//查看表格行图表
chartDetail(row, fieldKey = "") {
this.$emit("chart", { ...row, fieldKey });
},
},
};
</script>
<style lang="scss" scoped>
.lists-container {
padding: 20px 0;
.lists {
margin: 10px 5px;
padding: 5px 9px;
font-size: 11px;
line-height: 20px;
border: 1.6px solid #09ada3;
border-radius: 5px;
position: relative;
color: #333333;
float: left;
box-sizing: content-box;
min-width: 60px;
width: auto;
&::before {
display: block;
content: "";
top: -7px;
left: 50%;
transform: translateX(-50%);
position: absolute;
width: 45%;
height: 0;
border-bottom: 7px solid #09ada3;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
}
&.minwd {
border-color: #3794ff;
.wd {
color: #3794ff;
}
&::before {
border-bottom-color: #3794ff;
}
}
&.maxwd {
border-color: #ff3a3b;
.wd {
color: #ff3a3b;
}
&::before {
border-bottom-color: #ff3a3b;
}
}
&.mindy {
border-color: #de6902;
.dy {
color: #de6902;
}
&::before {
border-bottom-color: #de6902;
}
}
&.maxdy {
border-color: #ffb521;
.dy {
color: #ffb521;
}
&::before {
border-bottom-color: #ffb521;
}
}
}
}
.dtdc-pagination {
::v-deep {
.el-button {
padding: 2px 10px !important;
font-size: 11px;
line-height: 16px;
}
.activeBtn {
background-color: #09ada3;
border-color: #09ada3;
}
}
}
</style>

View File

@ -1,117 +0,0 @@
<template>
<div>
<el-table
class="common-table"
:data="tableData"
stripe
style="width: 100%; margin-top: 25px"
>
<el-table-column prop="deviceId" label="单体编号"></el-table-column>
<el-table-column prop="clusterDeviceId" label="簇号"></el-table-column>
<el-table-column prop="voltage" label="电压 (V)">
<template slot-scope="scope">
<el-button
@click="chartDetail(scope.row, 'voltage')"
type="text"
size="small"
>
{{ scope.row.voltage }}
</el-button>
</template>
</el-table-column>
<el-table-column prop="temperature" label="温度 (℃)">
<template slot-scope="scope">
<el-button
@click="chartDetail(scope.row, 'temperature')"
type="text"
size="small"
>
{{ scope.row.temperature }}
</el-button>
</template>
</el-table-column>
<el-table-column prop="soc" label="SOC (%)">
<template slot-scope="scope">
<el-button
@click="chartDetail(scope.row, 'soc')"
type="text"
size="small"
>
{{ scope.row.soc }}
</el-button>
</template>
</el-table-column>
<el-table-column prop="soh" label="SOH (%)">
<template slot-scope="scope">
<el-button
@click="chartDetail(scope.row, 'soh')"
type="text"
size="small"
>
{{ scope.row.soh }}
</el-button>
</template>
</el-table-column>
</el-table>
<!-- <el-pagination
v-show="tableData.length > 0"
background
@size-change="(val) => $emit('handleSizeChange', val)"
@current-change="(val) => $emit('handleCurrentChange', val)"
:current-page="pageNum"
:page-size="pageSize"
:page-sizes="[10, 20, 30, 40]"
layout="total, sizes, prev, pager, next, jumper"
:total="totalSize"
style="margin-top: 15px; text-align: center"
>
</el-pagination> -->
</div>
</template>
<script>
export default {
props: {
tableData: {
require: true,
type: Array,
default: () => {
return [];
},
},
pointIdList: {
require: true,
type: Object,
default: () => {
return {};
},
},
totalSize: {
require: true,
type: Number,
default: 0,
},
// pageNum: {
// require: true,
// type: Number,
// default: 1,
// },
// pageSize: {
// require: true,
// type: Number,
// default: 10,
// },
},
data() {
return {};
},
methods: {
//查看表格行图表
chartDetail(row, fieldKey = "") {
this.$emit("chart", {...row, fieldKey});
},
},
};
</script>
<style></style>

View File

@ -1,547 +1,224 @@
<template> <template>
<el-card <el-card v-loading="loading" shadow="always" class="common-card-container common-card-container-no-title-bg">
v-loading="loading" <div slot="header">
shadow="always" <span class="large-title">单体电池实时数据</span>
class="sbjk-card-container common-card-container-no-title-bg running-card-container" </div>
> <!-- 搜索栏-->
<div slot="header"> <el-form :inline="true" class="select-container">
<span class="large-title">单体电池实时数据</span> <el-form-item label="电池堆">
</div> <el-select v-model="search.stackId" placeholder="请选择" @change="changeStackId">
<!-- 搜索栏--> <el-option :label="item.deviceName" :value="item.id" v-for="(item,index) in stackOptions" :key="index+'stackOptions'"></el-option>
<el-form :inline="true" class="select-container"> </el-select>
<el-form-item label="编号"> </el-form-item>
<el-input <el-form-item label="电池簇">
v-model="search.batteryId" <el-select v-model="search.clusterId" :no-data-text="!search.stackId && stackOptions.length > 0 ? '请先选择电池堆':'无数据'" placeholder="请选择" :loading="clusterloading" loading-text="正在加载数据">
placeholder="请输入" <el-option :label="item.deviceName" :value="item.id" v-for="(item,index) in clusterOptions" :key="index+'clusterOptions'"></el-option>
clearable </el-select>
style="width: 150px" </el-form-item>
/> <el-form-item>
</el-form-item> <el-button type="primary" @click="onSearch" native-type="button">搜索</el-button>
<el-form-item label="电池堆"> </el-form-item>
<el-select <el-form-item>
v-model="search.stackId" <el-button @click="onReset" native-type="button">重置</el-button>
placeholder="请选择" </el-form-item>
@change="changeStackId" </el-form>
> <!-- 图表-->
<el-option <!-- <div style="margin:30px 0;box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);">-->
:label="item.deviceName" <!-- <el-row style="background:#fff;margin:30px 0;">-->
:value="item.id" <!-- <el-col :xs="24" :sm="24" :lg="24">-->
v-for="(item, index) in stackOptions" <!-- <bar-chart ref="barChart"/>-->
:key="index + 'stackOptions'" <!-- </el-col>-->
></el-option> <!-- </el-row>-->
</el-select> <!-- </div>-->
</el-form-item> <el-table
<el-form-item label="电池簇"> class="common-table"
<el-select :data="tableData"
v-model="search.clusterId" stripe
:no-data-text=" style="width: 100%;margin-top: 25px">
!search.stackId && stackOptions.length > 0 <el-table-column
? '请先选择电池堆' prop="deviceId"
: '无数据' label="单体编号">
" </el-table-column>
placeholder="请选择" <el-table-column
:loading="clusterloading" prop="clusterDeviceId"
loading-text="正在加载数据" label="簇号">
> </el-table-column>
<el-option <el-table-column
:label="item.deviceName" prop="voltage"
:value="item.id" label="电压V"
v-for="(item, index) in clusterOptions" >
:key="index + 'clusterOptions'" <template slot-scope="scope">
></el-option> <el-button
</el-select> @click="chartDetail(scope.row,'voltage')"
</el-form-item> type="text"
<el-form-item> size="small">
<el-button type="primary" @click="onSearch" native-type="button" {{scope.row.voltage}}
>搜索</el-button </el-button>
> </template>
</el-form-item> </el-table-column>
<el-form-item> <el-table-column
<el-button @click="onReset" native-type="button">重置</el-button> prop="temperature"
</el-form-item> label="温度(℃)">
</el-form> <template slot-scope="scope">
<!-- 切换 --> <el-button
<div class="tip-container"> @click="chartDetail(scope.row,'temperature')"
<div class="color-tip" v-show="activeBtn === 'list'"> type="text"
单体信息 size="small">
<span class="tip minwd">最低单体温度</span> {{scope.row.temperature}}
<span class="tip maxwd">最高单体温度</span> </el-button>
<span class="tip mindy">单体最低电压</span> </template>
<span class="tip maxdy">单体最高电压</span> </el-table-column>
</div> <el-table-column
<el-button-group class="ems-btns-group"> prop="soc"
<el-button label="SOC%">
:class="{ activeBtn: activeBtn === 'table' }" <template slot-scope="scope">
@click="changeMenu('table')" <el-button
>图表</el-button @click="chartDetail(scope.row,'soc')"
> type="text"
<el-button size="small">
:class="{ activeBtn: activeBtn === 'list' }" {{scope.row.soc}}
@click="changeMenu('list')" </el-button>
>图形</el-button </template>
> </el-table-column>
</el-button-group> <el-table-column
</div> prop="soh"
<component label="SOH%">
:is="activeBtn === 'table' ? 'DtdcTable' : 'DtdcList'" <template slot-scope="scope">
:tableData="tableData" <el-button
:totalSize="totalSize" @click="chartDetail(scope.row,'soh')"
:pointIdList="pointIdList" type="text"
@chart="chartDetail" size="small">
></component> {{scope.row.soh}}
<el-pagination </el-button>
v-show="tableData.length > 0" </template>
background </el-table-column>
@size-change="handleSizeChange" <el-table-column
@current-change="handleCurrentChange" label="曲线图">
:current-page="pageNum" <template slot-scope="scope">
:page-size="pageSize" <el-button
:page-sizes="[10, 20, 30, 40]" @click="chartDetail(scope.row)"
layout="total, sizes, prev, pager, next, jumper" type="text"
:total="totalSize" size="small">
style="margin-top: 15px; text-align: center" 展示
> </el-button>
</el-pagination> </template>
<chart-detail ref="chartDetail" /> </el-table-column>
<el-dialog </el-table>
:visible.sync="curveDialogVisible" <el-pagination
:title="curveDialogTitle" v-show="tableData.length>0"
width="1000px" background
append-to-body @size-change="handleSizeChange"
class="ems-dialog" @current-change="handleCurrentChange"
:close-on-click-modal="false" :current-page="pageNum"
destroy-on-close :page-size="pageSize"
@opened="handleCurveDialogOpened" :page-sizes="[10, 20, 30, 40]"
@closed="handleCurveDialogClosed" layout="total, sizes, prev, pager, next, jumper"
> :total="totalSize"
<div class="curve-tools"> style="margin-top:15px;text-align: center"
<el-date-picker >
v-model="curveCustomRange" </el-pagination>
type="datetimerange" <chart-detail ref="chartDetail"/>
value-format="yyyy-MM-dd HH:mm:ss" </el-card>
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 440px"
/>
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button>
</div>
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
</el-dialog>
</el-card>
</template> </template>
<script> <script>
import * as echarts from "echarts"; import BarChart from './BarChart'
import BarChart from "./BarChart"; import {getStackNameList, getClusterNameList, getClusterDataInfoList} from '@/api/ems/dzjk'
import {
getClusterDataInfoList,
getClusterNameList,
getStackNameList,
} from "@/api/ems/dzjk";
import { getPointConfigCurve } from "@/api/ems/site";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId"; import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import ChartDetail from "./ChartDetail.vue"; import ChartDetail from "./ChartDetail.vue";
import Table from "./Table.vue";
import List from "./List.vue";
export default { export default {
name: "DzjkSbjkDtdc", name:'DzjkSbjkDtdc',
mixins: [getQuerySiteId], mixins:[getQuerySiteId],
components: { components:{BarChart, ChartDetail},
BarChart,
ChartDetail,
DtdcTable: Table,
DtdcList: List,
},
computed: {
pointIdList() {
let obj = {};
this.pointData.forEach((item) => {
const {
maxCellTempId,
maxCellVoltageId,
minCellTempId,
minCellVoltageId,
} = item;
obj[item.clusterId] = [
parseInt(minCellTempId || 0),
parseInt(maxCellTempId || 0),
parseInt(minCellVoltageId || 0),
parseInt(maxCellVoltageId || 0),
]; //最低单体温度 最高温度 最低电压 最高电压 todo 这里的顺序需要和图形组件里的顺序保持一致,
});
return obj;
},
},
data() { data() {
return { return {
loading: false, loading:false,
clusterloading: false, clusterloading:false,
search: { stackId: "", clusterId: "", batteryId: "" }, search:{stackId:'',clusterId:''},
stackOptions: [], //{id:'',deviceName:''} stackOptions:[],//{id:'',deviceName:''}
clusterOptions: [], //{id:'',deviceName:''} clusterOptions:[],//{id:'',deviceName:''}
tableData: [], tableData:[],
pointData: [], pageSize:10,//分页栏当前每个数据总数
pageSize: 40, //分页栏当前每个数据总 pageNum:1,//分页栏当前
pageNum: 1, //分页栏当前页 totalSize:0,//table表格数据总
totalSize: 0, //table表格数据总数
activeBtn: "table",
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
};
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
} }
}, },
methods: { methods:{
getFieldPointConfig(fieldKey) {
const pointMap = {
voltage: { pointIdKey: "voltagePointId", title: "电压 (V)" },
temperature: { pointIdKey: "temperaturePointId", title: "温度 ()" },
soc: { pointIdKey: "socPointId", title: "SOC (%)" },
soh: { pointIdKey: "sohPointId", title: "SOH (%)" },
};
return pointMap[String(fieldKey || "").trim()] || null;
},
openCurveDialogByPointId(pointId, title) {
const normalizedPointId = String(pointId || "").trim();
if (!normalizedPointId) {
this.$message.warning("该字段未配置点位无法查询曲线");
return;
}
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || normalizedPointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId: normalizedPointId,
pointType: "data",
rangeType: "custom",
startTime: range[0],
endTime: range[1],
};
this.curveDialogVisible = true;
},
handleCurveDialogOpened() {
if (!this.curveChart && this.$refs.curveChartRef) {
this.curveChart = echarts.init(this.$refs.curveChartRef);
}
this.loadCurveData();
},
handleCurveDialogClosed() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
this.curveLoading = false;
},
getDefaultCurveRange() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return [this.formatDateTime(start), this.formatDateTime(end)];
},
formatDateTime(date) {
const d = new Date(date);
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(
d.getHours()
)}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
},
formatCurveTime(value) {
if (value === undefined || value === null || value === "") {
return "";
}
const raw = String(value).trim();
const normalized = raw
.replace("T", " ")
.replace(/\.\d+/, "")
.replace(/Z$/, "")
.replace(/([+-]\d{2}:?\d{2})$/, "")
.trim();
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
if (matched) {
return `${matched[1]} ${matched[2]}`;
}
return normalized.slice(0, 16);
},
loadCurveData() {
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
this.$message.warning("点位信息不完整无法查询曲线");
return;
}
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
this.$message.warning("请选择查询时间范围");
return;
}
this.curveQuery.startTime = this.curveCustomRange[0];
this.curveQuery.endTime = this.curveCustomRange[1];
const query = {
siteId: this.curveQuery.siteId,
pointId: this.curveQuery.pointId,
pointType: "data",
rangeType: "custom",
startTime: this.curveQuery.startTime,
endTime: this.curveQuery.endTime,
};
this.curveLoading = true;
getPointConfigCurve(query)
.then((response) => {
const rows = response?.data || [];
this.renderCurveChart(rows);
})
.catch(() => {
this.renderCurveChart([]);
})
.finally(() => {
this.curveLoading = false;
});
},
renderCurveChart(rows = []) {
if (!this.curveChart) return;
const xData = rows.map((item) => this.formatCurveTime(item.dataTime));
const yData = rows.map((item) => item.pointValue);
this.curveChart.clear();
this.curveChart.setOption({
legend: {},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
data: xData,
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: [
{
name: this.curveDialogTitle,
type: "line",
data: yData,
connectNulls: true,
},
],
});
if (!rows.length) {
this.$message.warning("当前时间范围暂无曲线数据");
}
},
changeMenu(menu) {
const { activeBtn } = this;
activeBtn !== menu && (this.activeBtn = menu);
},
//查看表格行图表 //查看表格行图表
chartDetail(row = {}) { chartDetail(row,dataType = ''){
const config = this.getFieldPointConfig(row.fieldKey); const { clusterDeviceId, deviceId} = row,{siteId} = this
if (!config) return; this.$refs.chartDetail.initChart({siteId,clusterDeviceId,deviceId},dataType)
const pointId = row[config.pointIdKey];
this.openCurveDialogByPointId(pointId, config.title);
}, },
// 分页 // 分页
handleSizeChange(val) { handleSizeChange(val) {
this.pageSize = val; this.pageSize = val;
this.$nextTick(() => { this.$nextTick(()=>{
this.getTableData(); this.getTableData()
}); })
}, },
handleCurrentChange(val) { handleCurrentChange(val) {
this.pageNum = val; this.pageNum = val
this.$nextTick(() => { this.$nextTick(()=>{
this.getTableData(); this.getTableData()
}); })
}, },
// 搜索 // 搜索
onSearch() { onSearch(){
this.pageNum = 1; //每次搜索从1开始搜索 this.pageNum =1//每次搜索从1开始搜索
this.getTableData(); this.getTableData()
}, },
// 重置 // 重置
// 清空搜索栏选中数据 // 清空搜索栏选中数据
// 清空电池簇列表,保留电池堆列表 // 清空电池簇列表,保留电池堆列表
onReset() { onReset(){
this.search = { stackId: "", clusterId: "", batteryId: "" }; this.search={stackId:'',clusterId:''}
this.clusterOptions = []; this.clusterOptions=[]
this.pageNum = 1; this.pageNum = 1
this.getTableData(); this.getTableData()
}, },
changeStackId(val) { changeStackId(val){
if (val) { if(val){
console.log( console.log('选择了电池堆,需要获取对应的电池簇',val,this.search.stackId)
"选择了电池堆需要获取对应的电池簇", this.search.clusterId=''
val, this.getClusterList()
this.search.stackId
);
this.search.clusterId = "";
this.getClusterList();
} else {
this.search.clusterId = "";
this.clusterOptions = [];
} }
}, },
//表格数据 //表格数据
getTableData() { getTableData(){
this.loading = true; this.loading=true;
const { const {stackId:stackDeviceId,clusterId:clusterDeviceId} =this.search
stackId: stackDeviceId, const {siteId,pageNum,pageSize}=this
clusterId: clusterDeviceId, getClusterDataInfoList({stackDeviceId,clusterDeviceId,siteId,pageNum,pageSize}).then(response => {
batteryId, this.tableData=response?.rows || [];
} = this.search; this.totalSize = response?.total || 0
const { siteId, pageNum, pageSize } = this; }).finally(()=>{
getClusterDataInfoList({ this.loading=false;
stackDeviceId,
clusterDeviceId,
siteId,
batteryId,
pageNum,
pageSize,
}) })
.then((response) => {
this.tableData = response?.rows?.[0]?.batteryList || []; //todo check
this.pointData = response?.rows?.[0]?.clusterList || []; //todo check
this.totalSize = response?.total || 0;
})
.finally(() => {
this.loading = false;
});
}, },
getStackList() { getStackList(){
getStackNameList(this.siteId).then((response) => { getStackNameList(this.siteId).then(response => {
const list = JSON.parse(JSON.stringify(response?.data || [])); this.stackOptions = JSON.parse(JSON.stringify(response?.data || []))
this.stackOptions = list;
});
},
getClusterList() {
const { stackId } = this.search;
if (!stackId) {
this.clusterOptions = [];
return Promise.resolve();
}
this.clusterloading = true;
const currentStackId = String(stackId);
return getClusterNameList({
stackDeviceId: stackId,
siteId: this.siteId,
}) })
.then((response) => {
// 避免用户快速切换电池堆时旧请求覆盖新数据
if (String(this.search.stackId || "") !== currentStackId) return;
this.clusterOptions = JSON.parse(JSON.stringify(response?.data || []));
})
.finally(() => {
this.clusterloading = false;
});
}, },
init() { getClusterList(){
this.clusterloading =true
getClusterNameList(this.search.stackId).then(response => {
this.clusterOptions = JSON.parse(JSON.stringify(response?.data || []))
}).finally(() => {this.clusterloading =false})
},
init(){
// 只有页面初次加载或切换站点的时候调用电池堆列表,其他情况不需要 // 只有页面初次加载或切换站点的时候调用电池堆列表,其他情况不需要
this.search = { stackId: "", clusterId: "", batteryId: "" }; //保证切换站点时,清空选择项 this.search={stackId:'',clusterId:''}//保证切换站点时,清空选择项
this.clusterOptions = []; this.getStackList()
this.pageNum = 1; this.getTableData()
this.totalSize = 0; }
this.getStackList();
this.getTableData();
},
}, },
mounted() {}, mounted(){
};
</script>
<style scoped lang="scss">
.tip-container {
text-align: right;
position: relative;
.color-tip {
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
font-size: 11px;
line-height: 12px;
color: #333;
.tip {
padding-left: 30px;
position: relative;
&::before {
display: block;
content: "";
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
width: 8px;
height: 8px;
border-radius: 50%;
}
&.minwd {
color: #3794ff;
&::before {
background: #3794ff;
}
}
&.maxwd {
color: #ff3a3b;
&::before {
background: #ff3a3b;
}
}
&.mindy {
color: #de6902;
&::before {
background: #de6902;
}
}
&.maxdy {
color: #ffb521;
&::before {
background: #ffb521;
}
}
}
}
::v-deep {
.el-button-group.ems-btns-group {
& > .el-button {
padding: 5px 30px !important;
font-size: 11px;
line-height: 16px;
// padding-left: 50px;
// padding-right: 50px;
// font-size: 16px;
// line-height: 24px;
}
}
} }
} }
</style> </script>

View File

@ -1,481 +0,0 @@
<template>
<div>
<div class="pcs-tags">
<el-tag
size="small"
:type="selectedSectionKey ? 'info' : 'primary'"
:effect="selectedSectionKey ? 'plain' : 'dark'"
class="pcs-tag-item"
@click="handleTagClick('')"
>
全部
</el-tag>
<el-tag
v-for="(group, index) in sectionGroups"
:key="index + 'emsTag'"
size="small"
:type="selectedSectionKey === group.sectionKey ? 'primary' : 'info'"
:effect="selectedSectionKey === group.sectionKey ? 'dark' : 'plain'"
class="pcs-tag-item"
@click="handleTagClick(group.sectionKey)"
>
{{ group.displayName || group.sectionName || "EMS" }}
</el-tag>
</div>
<el-card
v-for="(group, index) in filteredSectionGroups"
:key="index + 'emsSection'"
class="sbjk-card-container list running-card-container"
shadow="always"
>
<div slot="header">
<span class="large-title">{{ group.displayName || group.sectionName || "EMS" }}</span>
<div class="info">
<div>状态{{ group.statusText }}</div>
<div>数据更新时间{{ group.updateTimeText }}</div>
</div>
</div>
<el-row class="device-info-row">
<el-col
v-for="(item, dataIndex) in group.items"
:key="dataIndex + 'emsField'"
:span="6"
class="device-info-col"
:class="{ 'field-disabled': !item.pointId }"
>
<div class="field-click-wrapper" @click="handleFieldClick(item)">
<span class="left">{{ item.fieldName }}</span>
<span class="right">
<i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span>
</span>
</div>
</el-col>
</el-row>
</el-card>
<el-dialog
:visible.sync="curveDialogVisible"
:title="curveDialogTitle"
width="1000px"
append-to-body
class="ems-dialog"
:close-on-click-modal="false"
destroy-on-close
@opened="handleCurveDialogOpened"
@closed="handleCurveDialogClosed"
>
<div class="curve-tools">
<el-date-picker
v-model="curveCustomRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 440px"
/>
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button>
</div>
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
</el-dialog>
</div>
</template>
<script>
import * as echarts from "echarts";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import { getProjectDisplayData } from "@/api/ems/dzjk";
import { getDeviceList, getPointConfigCurve } from "@/api/ems/site";
export default {
name: "DzjkSbjkEms",
mixins: [getQuerySiteId, intervalUpdate],
data() {
return {
loading: false,
displayData: [],
selectedSectionKey: "",
emsDeviceList: [],
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
};
},
computed: {
moduleDisplayData() {
return (this.displayData || []).filter((item) => item.menuCode === "SBJK_EMS");
},
emsTemplateFields() {
const source = this.moduleDisplayData || [];
const result = [];
const seen = new Set();
source.forEach((item) => {
const fieldName = String(item?.fieldName || "").trim();
if (!fieldName || seen.has(fieldName)) {
return;
}
seen.add(fieldName);
result.push(fieldName);
});
return result.length > 0 ? result : this.fallbackFields;
},
sectionGroups() {
const source = this.moduleDisplayData || [];
const devices = (this.emsDeviceList || []).length > 0
? this.emsDeviceList
: [{ deviceId: "", deviceName: "EMS" }];
return devices.map((device, index) => {
const deviceId = String(device?.deviceId || device?.id || "").trim();
const sectionKey = deviceId || `EMS_${index}`;
const displayName = String(device?.deviceName || device?.name || deviceId || `EMS${index + 1}`).trim();
const exactRows = source.filter((item) => String(item?.deviceId || "").trim() === deviceId);
const fallbackRows = source.filter((item) => !String(item?.deviceId || "").trim());
const exactValueMap = {};
exactRows.forEach((item) => {
const key = String(item?.fieldName || "").trim();
if (key) {
exactValueMap[key] = item;
}
});
const fallbackValueMap = {};
fallbackRows.forEach((item) => {
const key = String(item?.fieldName || "").trim();
if (key && fallbackValueMap[key] === undefined) {
fallbackValueMap[key] = item;
}
});
const items = (this.emsTemplateFields || []).map((fieldName) => {
const row = exactValueMap[fieldName] || fallbackValueMap[fieldName] || {};
return {
fieldName,
fieldValue: row.fieldValue,
valueTime: row.valueTime,
pointId: String(row?.dataPoint || "").trim(),
raw: row,
};
});
const statusItem = (items || []).find((it) => String(it.fieldName || "").includes("状态"));
const timestamps = [...exactRows, ...fallbackRows]
.map((it) => new Date(it?.valueTime).getTime())
.filter((ts) => !isNaN(ts));
return {
sectionName: displayName,
sectionKey,
displayName,
deviceId,
items,
statusText: this.displayValue(statusItem ? statusItem.fieldValue : "-"),
updateTimeText: timestamps.length > 0 ? this.formatDate(new Date(Math.max(...timestamps))) : "-",
};
});
},
displaySectionGroups() {
if (this.sectionGroups.length > 0) {
return this.sectionGroups;
}
return [
{
sectionName: "EMS",
items: this.fallbackFields.map((fieldName) => ({ fieldName, fieldValue: "-" })),
statusText: "-",
updateTimeText: "-",
},
];
},
filteredSectionGroups() {
const groups = this.displaySectionGroups || [];
if (!this.selectedSectionKey) {
return groups;
}
return groups.filter((group) => group.sectionKey === this.selectedSectionKey);
},
fallbackFields() {
return [
"BMS1SOC",
"BMS2SOC",
"BMS3SOC",
"BMS4SOC",
"PCS-1有功功率",
"PCS-2有功功率",
"PCS-3有功功率",
"PCS-4有功功率",
];
},
},
methods: {
handleFieldClick(item) {
const pointId = String(item?.pointId || item?.raw?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: item?.fieldName || pointId,
});
},
openCurveDialog({ pointId, title }) {
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || pointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId,
pointType: "data",
rangeType: "custom",
startTime: range[0],
endTime: range[1],
};
this.curveDialogVisible = true;
},
handleCurveDialogOpened() {
if (!this.curveChart && this.$refs.curveChartRef) {
this.curveChart = echarts.init(this.$refs.curveChartRef);
}
this.loadCurveData();
},
handleCurveDialogClosed() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
this.curveLoading = false;
},
getDefaultCurveRange() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return [this.formatDateTime(start), this.formatDateTime(end)];
},
formatDateTime(date) {
const d = new Date(date);
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
},
formatCurveTime(value) {
if (value === undefined || value === null || value === "") {
return "";
}
const raw = String(value).trim();
const normalized = raw
.replace("T", " ")
.replace(/\.\d+/, "")
.replace(/Z$/, "")
.replace(/([+-]\d{2}:?\d{2})$/, "")
.trim();
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
if (matched) {
return `${matched[1]} ${matched[2]}`;
}
return normalized.slice(0, 16);
},
loadCurveData() {
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
this.$message.warning("点位信息不完整,无法查询曲线");
return;
}
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
this.$message.warning("请选择查询时间范围");
return;
}
this.curveQuery.startTime = this.curveCustomRange[0];
this.curveQuery.endTime = this.curveCustomRange[1];
const query = {
siteId: this.curveQuery.siteId,
pointId: this.curveQuery.pointId,
pointType: "data",
rangeType: "custom",
startTime: this.curveQuery.startTime,
endTime: this.curveQuery.endTime,
};
this.curveLoading = true;
getPointConfigCurve(query)
.then((response) => {
const rows = response?.data || [];
this.renderCurveChart(rows);
})
.catch(() => {
this.renderCurveChart([]);
})
.finally(() => {
this.curveLoading = false;
});
},
renderCurveChart(rows = []) {
if (!this.curveChart) return;
const xData = rows.map((item) => this.formatCurveTime(item.dataTime));
const yData = rows.map((item) => item.pointValue);
this.curveChart.clear();
this.curveChart.setOption({
legend: {},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
data: xData,
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: [
{
name: this.curveDialogTitle,
type: "line",
data: yData,
connectNulls: true,
},
],
});
if (!rows.length) {
this.$message.warning("当前时间范围暂无曲线数据");
}
},
handleTagClick(sectionKey) {
this.selectedSectionKey = sectionKey || "";
},
displayValue(value) {
return value === undefined || value === null || value === "" ? "-" : value;
},
isPointLoading(value) {
return this.loading && (value === undefined || value === null || value === "" || value === "-");
},
formatDate(date) {
if (!(date instanceof Date) || isNaN(date.getTime())) {
return "-";
}
const p = (n) => String(n).padStart(2, "0");
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(
date.getMinutes()
)}:${p(date.getSeconds())}`;
},
getEmsDeviceList() {
return getDeviceList(this.siteId)
.then((response) => {
const list = response?.data || [];
this.emsDeviceList = list.filter((item) => item.deviceCategory === "EMS");
})
.catch(() => {
this.emsDeviceList = [];
});
},
updateData() {
this.loading = true;
Promise.all([getProjectDisplayData(this.siteId), this.getEmsDeviceList()])
.then(([response]) => {
this.displayData = response?.data || [];
})
.finally(() => {
this.loading = false;
});
},
init() {
this.updateData();
this.updateInterval(this.updateData);
},
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
};
</script>
<style scoped lang="scss">
.sbjk-card-container {
&.list:not(:last-child) {
margin-bottom: 25px;
}
.info {
float: right;
text-align: right;
font-size: 12px;
color: #666;
line-height: 18px;
}
}
.pcs-tags {
margin: 0 0 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-start;
align-items: center;
}
.pcs-tag-item {
cursor: pointer;
}
.device-info-col {
cursor: pointer;
}
.field-click-wrapper {
width: 100%;
}
.device-info-col.field-disabled {
cursor: not-allowed;
opacity: 0.8;
}
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.point-loading-icon {
color: #409eff;
display: inline-block;
transform-origin: center;
animation: pointLoadingSpinPulse 1.1s linear infinite;
}
@keyframes pointLoadingSpinPulse {
0% { opacity: 0.45; transform: rotate(0deg) scale(0.9); }
50% { opacity: 1; transform: rotate(180deg) scale(1.08); }
100% { opacity: 0.45; transform: rotate(360deg) scale(0.9); }
}
</style>

View File

@ -1,70 +1,51 @@
<template> <template>
<div class="ems-dashboard-editor-container ems-third-menu-container" v-loading="loading"> <div class="ems-dashboard-editor-container ems-content-container-padding sbjk-ems-dashboard-editor-container">
<el-menu <el-menu
class="ems-third-menu" class="ems-third-menu"
:default-active="$route.name" :default-active="$route.name"
background-color="#ffffff" background-color="#ffffff"
text-color="#666666" text-color="#666666"
active-text-color="#ffffff" active-text-color="#ffffff"
> >
<el-menu-item :index="item.name" v-for="(item,index) in categoryRouter" :key="index+'dzjkChildrenRoute'"> <el-menu-item :index="item.name" v-for="(item,index) in childrenRoute" :key="index+'dzjkChildrenRoute'">
<router-link style="height: 100%;width: 100%;display: block" :to="{path:item.path,query:$route.query}"> <router-link style="height: 100%;width: 100%;display: block" :to="{path:item.path,query:$route.query}">
{{item.meta.title}} {{item.meta.title}}
</router-link> </router-link>
</el-menu-item> </el-menu-item>
</el-menu> </el-menu>
<div class="ems-content-container ems-content-container-padding sbjk-ems-content-container"> <div class="ems-content-container ems-content-container-padding sbjk-ems-content-container">
<keep-alive> <keep-alive>
<router-view></router-view> <router-view></router-view>
</keep-alive> </keep-alive>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import { dzjk } from '@/router/ems' import { dzjk } from '@/router/ems'
import {mapState} from "vuex"; const childrenRoute = dzjk[0].children.find(item=> item.name==='DzjkSbjk').children//获取到单站监控-设备监控下面的字路由
const childrenRoute = dzjk[0].children[0].children.find(item=> item.name==='DzjkSbjk').children//获取到单站监控-设备监控下面的字路由 console.log('设备监控子路由',childrenRoute)
export default { export default {
name:'DzjkSbjk', name:'DzjkSbjk',
mixins:[getQuerySiteId],
computed:{
...mapState({
zdDeviceCategoryOptions: state => state.ems.zdDeviceCategoryOptions,
}),
locationSiteCategory(){
return this.zdDeviceCategoryOptions[this.siteId] || []
},
categoryRouter(){
const routeData =this.childrenRoute.filter(item=>this.locationSiteCategory.includes(item.meta.deviceCategory))
if(this.siteId && routeData.length > 0 && this.locationSiteCategory && this.locationSiteCategory.length >1){
const locationPageDeviceCategory = this.$route.meta?.deviceCategory || ''
if(!routeData.some(item=> item.meta.deviceCategory===locationPageDeviceCategory)){
this.$router.replace({path:'/dzjk/sbjk/ssyx',query:this.$route.query})
}
}
return routeData
}
},
data(){ data(){
return { return {
childrenRoute, childrenRoute,
activeMenu:'', activeMenu:''
loading:false,
} }
}, },
methods:{ mounted() {
init(){ console.log('当前设备监控页面路由',this.$route)
this.loading=true
this.$store.dispatch('getSiteDeviceCategory',this.siteId).finally(()=>this.loading=false)
}
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.sbjk-ems-dashboard-editor-container{
display: flex;
background: #FFFFFF;
}
.sbjk-ems-content-container{ .sbjk-ems-content-container{
margin-top:0; margin-top:0;
padding-top:0; padding-top:0;
@ -72,3 +53,4 @@ export default {
flex: 1; flex: 1;
} }
</style> </style>

View File

@ -1,765 +1,208 @@
<template> <template>
<div class="pcs-ems-dashboard-editor-container"> <div class="pcs-ems-dashboard-editor-container" v-loading="loading">
<div class="pcs-tags"> <!-- 顶部六个方块-->
<el-tag <real-time-base-info :data="runningHeadData"/>
size="small" <!-- 内容-->
:type="selectedPcsId ? 'info' : 'primary'" <el-container class="pcs-container" v-for="(pcsItem,pcsIndex) in pcsList" :key="pcsIndex+'PcsHome'">
:effect="selectedPcsId ? 'plain' : 'dark'" <!-- 背景颜色根据工作状态来展示-->
class="pcs-tag-item" <el-header class="pcs-header" :class="pcsItem.workStatus === '1' ? 'warn' : pcsItem.workStatus === '2' ? 'close' : ''">
@click="handleTagClick('')" <div class="pcs-title">{{pcsItem.deviceName}}</div>
> <div class="pcs-status">
全部 <div>{{$store.state.ems.communicationStatusOptions[pcsItem.communicationStatus]}}</div>
</el-tag> <div>数据更新时间{{pcsItem.dataUpdateTime}}</div>
<el-tag
v-for="(item, index) in pcsDeviceList"
:key="index + 'pcsTag'"
size="small"
:type="selectedPcsId === (item.deviceId || item.id) ? 'primary' : 'info'"
:effect="selectedPcsId === (item.deviceId || item.id) ? 'dark' : 'plain'"
class="pcs-tag-item"
@click="handleTagClick(item.deviceId || item.id || '')"
>
{{ item.deviceName || item.deviceId || item.id || 'PCS' }}
</el-tag>
</div>
<div
v-for="(pcsItem, pcsIndex) in filteredPcsList"
:key="pcsIndex + 'PcsHome'"
style="margin-bottom: 25px"
>
<el-card
:class="handleCardClass(pcsItem)"
class="sbjk-card-container common-card-container-body-no-padding common-card-container-no-title-bg"
shadow="always"
>
<div slot="header">
<span class="large-title"
>{{ pcsItem.deviceName }}</span
>
<div class="info">
<div v-if="(($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[pcsItem.communicationStatus]">
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[pcsItem.communicationStatus] }}
</div>
<div>数据更新时间{{ pcsItem.dataUpdateTime }}</div>
</div>
<div class="alarm">
<el-badge :hidden="!pcsItem.alarmNum" :value="pcsItem.alarmNum || 0" class="item">
<i
class="el-icon-message-solid alarm-icon"
@click="pointDetail(pcsItem,'alarmPoint')"
></i>
</el-badge>
</div>
</div> </div>
<!-- <div class="pcs-btns">-->
<!-- <el-button type="warning" size="small" @click="problemSaved">故障复位</el-button>-->
<!-- <el-button size="small" @click="machineClosed">关机</el-button>-->
<!-- </div>-->
</el-header>
<el-main style="padding: 0">
<div class="descriptions-main"> <div class="descriptions-main">
<el-descriptions :colon="false" :column="4" direction="vertical"> <el-descriptions direction="vertical" :column="4" :colon="false">
<el-descriptions-item <el-descriptions-item labelClassName="descriptions-label" :contentClassName="`descriptions-direction ${pcsItem.workStatus === '0' ? 'save' :'danger'}`" :span="1" label="工作状态">{{$store.state.ems.workStatusOptions[pcsItem.workStatus]}}</el-descriptions-item>
contentClassName="descriptions-direction work-status" <el-descriptions-item labelClassName="descriptions-label" contentClassName="descriptions-direction" :span="1" label="并网状态">{{$store.state.ems.gridStatusOptions[pcsItem.gridStatus]}}</el-descriptions-item>
:span="1" <el-descriptions-item labelClassName="descriptions-label" :contentClassName="`descriptions-direction ${pcsItem.deviceStatus === '0' ? 'save' : 'danger'}`" :span="1" label="设备状态">{{$store.state.ems.deviceStatusOptions[pcsItem.deviceStatus]}}</el-descriptions-item>
label="工作状态" <el-descriptions-item labelClassName="descriptions-label" contentClassName="descriptions-direction" :span="1" label="控制模式">{{$store.state.ems.controlModeOptions[pcsItem.controlMode]}}</el-descriptions-item>
labelClassName="descriptions-label"
>
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'workStatus') }"
@click="handlePcsFieldClick(pcsItem, 'workStatus', '工作状态')"
>
{{ formatDictValue(pcsWorkStatusOptions, pcsItem.workStatus) }}
</span>
</el-descriptions-item
>
<el-descriptions-item
:span="1"
contentClassName="descriptions-direction"
label="并网状态"
labelClassName="descriptions-label"
>
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'gridStatus') }"
@click="handlePcsFieldClick(pcsItem, 'gridStatus', '并网状态')"
>
{{ formatDictValue(pcsGridStatusOptions, pcsItem.gridStatus) }}
</span>
</el-descriptions-item
>
<el-descriptions-item
:contentClassName="`descriptions-direction ${
pcsItem.deviceStatus === '1' ? 'save' : 'danger'
}`"
:span="1"
label="设备状态"
labelClassName="descriptions-label"
>
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'deviceStatus') }"
@click="handlePcsFieldClick(pcsItem, 'deviceStatus', '设备状态')"
>
{{ formatDictValue(pcsDeviceStatusOptions, pcsItem.deviceStatus) }}
</span>
</el-descriptions-item
>
<el-descriptions-item
:span="1"
contentClassName="descriptions-direction"
label="控制模式"
labelClassName="descriptions-label"
>
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'controlMode') }"
@click="handlePcsFieldClick(pcsItem, 'controlMode', '控制模式')"
>
{{ formatDictValue(pcsControlModeOptions, pcsItem.controlMode) }}
</span>
</el-descriptions-item
>
</el-descriptions> </el-descriptions>
</div> </div>
<div class="descriptions-main descriptions-main-bg-color"> <div class="descriptions-main descriptions-main-bg-color">
<el-descriptions <el-descriptions labelClassName="descriptions-label" contentClassName="descriptions-direction" direction="vertical" :column="4" :colon="false">
:colon="false" <el-descriptions-item v-for="(item,index) in infoData" :key="index+'pcsInfoData'" :span="1" :label="item.label">{{pcsItem[item.attr] | formatNumber}} <span v-if="item.unit" v-html="item.unit"></span></el-descriptions-item>
:column="4"
contentClassName="descriptions-direction"
direction="vertical"
labelClassName="descriptions-label"
>
<el-descriptions-item
v-for="(item, index) in infoData"
:key="index + 'pcsInfoData'"
:label="item.label"
:span="1"
>
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, item.attr) }"
@click="handlePcsFieldClick(pcsItem, item.attr, item.label)"
>
<i v-if="isPointLoading(pcsItem[item.attr])" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(pcsItem[item.attr]) | formatNumber }}</span>
<span v-if="item.unit" v-html="item.unit"></span>
</span>
</el-descriptions-item>
</el-descriptions> </el-descriptions>
</div> </div>
<div <div class="descriptions-main" v-for="(item,index) in pcsItem.pcsBranchInfoList" :key="index+'pcsBranchInfoList'">
v-for="(item, index) in pcsItem.pcsBranchInfoList" <el-descriptions labelClassName="descriptions-label" contentClassName="descriptions-direction keep" direction="vertical" :column="4" :colon="false">
:key="index + 'pcsBranchInfoList'" <el-descriptions-item labelClassName="descriptions-label" contentClassName="descriptions-direction keep" :span="4" :label="'支路'+(index+1)">{{item.dischargeStatus}}</el-descriptions-item>
class="descriptions-main" <el-descriptions-item labelClassName="descriptions-label" contentClassName="descriptions-direction" :span="1" label="直流功率">{{item.dcPower}}kW</el-descriptions-item>
> <el-descriptions-item labelClassName="descriptions-label" contentClassName="descriptions-direction" :span="1" label="直流电压">{{item.dcVoltage}}V</el-descriptions-item>
<el-descriptions <el-descriptions-item labelClassName="descriptions-label" contentClassName="descriptions-direction" :span="1" label="直流电流">{{item.dcCurrent}}A</el-descriptions-item>
:colon="false"
:column="4"
contentClassName="descriptions-direction keep"
direction="vertical"
labelClassName="descriptions-label"
>
<el-descriptions-item
:label="'支路' + (index + 1)"
:span="4"
contentClassName="descriptions-direction keep"
labelClassName="descriptions-label"
>{{ item.dischargeStatus }}
</el-descriptions-item
>
<el-descriptions-item
:span="1"
contentClassName="descriptions-direction"
label="直流功率"
labelClassName="descriptions-label"
>
<span
class="pointer"
:class="{ 'field-disabled': !item.dcPowerPointId }"
@click="openCurveDialogByPointId(item.dcPowerPointId, '直流功率')"
>{{ item.dcPower }}kW</span
>
</el-descriptions-item>
<el-descriptions-item
:span="1"
contentClassName="descriptions-direction"
label="直流电压"
labelClassName="descriptions-label"
>
<span
class="pointer"
:class="{ 'field-disabled': !item.dcVoltagePointId }"
@click="openCurveDialogByPointId(item.dcVoltagePointId, '直流电压')"
>{{ item.dcVoltage }}V</span
>
</el-descriptions-item>
<el-descriptions-item
:span="1"
contentClassName="descriptions-direction"
label="直流电流"
labelClassName="descriptions-label"
>
<span
class="pointer"
:class="{ 'field-disabled': !item.dcCurrentPointId }"
@click="openCurveDialogByPointId(item.dcCurrentPointId, '直流电流')"
>{{ item.dcCurrent }}A</span
>
</el-descriptions-item>
</el-descriptions> </el-descriptions>
</div> </div>
</el-card> </el-main>
</div> </el-container>
<point-table ref="pointTable"/> <el-empty v-show="pcsList.length<=0" :image-size="200"></el-empty>
<el-dialog
:visible.sync="curveDialogVisible"
:title="curveDialogTitle"
width="1000px"
append-to-body
class="ems-dialog"
:close-on-click-modal="false"
destroy-on-close
@opened="handleCurveDialogOpened"
@closed="handleCurveDialogClosed"
>
<div class="curve-tools">
<el-date-picker
v-model="curveCustomRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 440px"
/>
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button>
</div>
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import * as echarts from "echarts"; import RealTimeBaseInfo from "./../RealTimeBaseInfo.vue";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId"; import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getPcsNameList, getProjectDisplayData} from "@/api/ems/dzjk"; import {getRunningHeadInfo,getPcsDetailInfo} from '@/api/ems/dzjk'
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import {mapState} from "vuex";
import {getPointConfigCurve, getSingleMonitorWorkStatusEnumMappings} from "@/api/ems/site";
export default { export default {
name: "DzjkSbjkPcs", name:'DzjkSbjkPcs',
components: {PointTable}, components:{RealTimeBaseInfo},
mixins: [getQuerySiteId, intervalUpdate], mixins:[getQuerySiteId],
computed: {
...mapState({
PCSWorkStatusOptions: state => state?.ems?.PCSWorkStatusOptions || {},
}),
pcsWorkStatusOptions() {
return this.getEnumOptions("PCS", "workStatus", this.PCSWorkStatusOptions || {});
},
pcsGridStatusOptions() {
return this.getEnumOptions("PCS", "gridStatus", (this.$store.state.ems && this.$store.state.ems.gridStatusOptions) || {});
},
pcsDeviceStatusOptions() {
return this.getEnumOptions("PCS", "deviceStatus", (this.$store.state.ems && this.$store.state.ems.deviceStatusOptions) || {});
},
pcsControlModeOptions() {
return this.getEnumOptions("PCS", "controlMode", (this.$store.state.ems && this.$store.state.ems.controlModeOptions) || {});
},
filteredPcsList() {
if (!this.selectedPcsId) {
return this.pcsList || [];
}
return (this.pcsList || []).filter(item => item.deviceId === this.selectedPcsId);
},
},
data() { data() {
return { return {
loading: false, loading:false,
displayData: [], runningHeadData:{},//运行信息
pcsDeviceList: [], pcsList:[],
siteEnumOptionMap: {}, infoData:[
selectedPcsId: "", {label:'总交流有功电率',attr:'totalActivePower',unit:'kW'},
curveDialogVisible: false, {label:'当天交流充电量',attr:'dailyAcChargeEnergy',unit:'kWh'},
curveDialogTitle: "点位曲线", {label:'A相电压',attr:'aPhaseVoltage',unit:'V'},
curveChart: null, {label:'A相电流',attr:'aPhaseCurrent',unit:'A'},
curveLoading: false, {label:'总交流无功电率',attr:'totalReactivePower',unit:'kVar'},
curveCustomRange: [], {label:'当天交流放电量',attr:'dailyAcDischargeEnergy',unit:'kWh'},
curveQuery: { {label:'B相电压',attr:'bPhaseVoltage',unit:'V'},
siteId: "", {label:'B相电流',attr:'bPhaseCurrent',unit:'A'},
pointId: "", {label:'总交流视在功率',attr:'totalApparentPower',unit:'kVA'},
pointType: "data", {label:'PCS模块温度',attr:'pcsModuleTemperature',unit:'&#8451;'},
rangeType: "custom", {label:'C相电压',attr:'cPhaseVoltage',unit:'V'},
startTime: "", {label:'C相电流',attr:'cPhaseCurrent',unit:'A'},
endTime: "", {label:'总交流功率因数',attr:'totalPowerFactor',unit:''},
}, {label:'PCS环境温度',attr:'pcsEnvironmentTemperature',unit:'&#8451;'},
pcsList: [{ {label:'交流频率',attr:'acFrequency',unit:'Hz'}
deviceId: "",
deviceName: "PCS",
dataUpdateTime: "-",
alarmNum: 0,
pcsBranchInfoList: [],
}],
infoData: [
{
label: "总交流有功功率",
attr: "totalActivePower",
unit: "kW",
pointName: "总交流有功功率",
},
{
label: "当天交流充电量",
attr: "dailyAcChargeEnergy",
unit: "kWh",
pointName: "当天交流充电量 (kWh)",
},
{label: "A相电压", attr: "aPhaseVoltage", unit: "V", pointName: ""},
{
label: "A相电流",
attr: "aPhaseCurrent",
unit: "A",
pointName: "A相电流",
},
{
label: "总交流无功功率",
attr: "totalReactivePower",
unit: "kVar",
pointName: "总交流无功功率",
},
{
label: "当天交流放电量",
attr: "dailyAcDischargeEnergy",
unit: "kWh",
pointName: "当天交流放电量 (kWh)",
},
{label: "B相电压", attr: "bPhaseVoltage", unit: "V", pointName: ""},
{
label: "B相电流",
attr: "bPhaseCurrent",
unit: "A",
pointName: "B相电流",
},
{
label: "总交流视在功率",
attr: "totalApparentPower",
unit: "kVA",
pointName: "总交流视在功率",
},
{
label: "PCS模块温度",
attr: "pcsModuleTemperature",
unit: "&#8451;",
pointName: "",
},
{label: "C相电压", attr: "cPhaseVoltage", unit: "V", pointName: ""},
{
label: "C相电流",
attr: "cPhaseCurrent",
unit: "A",
pointName: "C相电流",
},
{
label: "总交流功率因数",
attr: "totalPowerFactor",
unit: "",
pointName: "总交流功率因数",
},
{
label: "PCS环境温度",
attr: "pcsEnvironmentTemperature",
unit: "&#8451;",
pointName: "",
},
{
label: "交流频率",
attr: "acFrequency",
unit: "Hz",
pointName: "交流频率",
},
], ],
}; pcsBranchList:[],//pcs的支路列表
},
methods: {
displayValue(value) {
return value === undefined || value === null || value === "" ? "-" : value;
},
isPointLoading(value) {
return this.loading && (value === undefined || value === null || value === "" || value === "-");
},
normalizeDictKey(value) {
const raw = String(value == null ? "" : value).trim();
if (!raw) return "";
if (/^-?\d+(\.0+)?$/.test(raw)) {
return String(parseInt(raw, 10));
}
return raw;
},
formatDictValue(options, value) {
const dict = (options && typeof options === "object") ? options : {};
const key = this.normalizeDictKey(value);
if (!key) return "-";
return dict[key] || key;
},
normalizeDeviceId(value) {
return String(value == null ? "" : value).trim().toUpperCase();
},
buildEnumScopeKey(deviceCategory, matchField) {
return `${String(deviceCategory || "").trim()}|${String(matchField || "").trim()}`;
},
buildSiteEnumOptionMap(mappings = []) {
return (mappings || []).reduce((acc, item) => {
const scopeKey = this.buildEnumScopeKey(item?.deviceCategory, item?.matchField);
const dataEnumCode = this.normalizeDictKey(item?.dataEnumCode);
const enumCode = this.normalizeDictKey(item?.enumCode);
const enumName = String(item?.enumName || "").trim();
const optionKey = dataEnumCode || enumCode;
if (!scopeKey || !optionKey || !enumName) {
return acc;
}
if (!acc[scopeKey]) {
acc[scopeKey] = {};
}
acc[scopeKey][optionKey] = enumName;
return acc;
}, {});
},
loadSiteEnumOptions() {
if (!this.siteId) {
this.siteEnumOptionMap = {};
return Promise.resolve({});
}
return getSingleMonitorWorkStatusEnumMappings(this.siteId).then(response => {
const optionMap = this.buildSiteEnumOptionMap(response?.data || []);
this.siteEnumOptionMap = optionMap;
return optionMap;
}).catch(() => {
this.siteEnumOptionMap = {};
return {};
});
},
getEnumOptions(deviceCategory, matchField, fallback = {}) {
const scopeKey = this.buildEnumScopeKey(deviceCategory, matchField);
const siteOptions = this.siteEnumOptionMap[scopeKey];
if (siteOptions && Object.keys(siteOptions).length > 0) {
return siteOptions;
}
return fallback || {};
},
handleCardClass(item) {
const workStatus = this.normalizeDictKey((item && item.workStatus) || "");
const statusOptions = (this.pcsWorkStatusOptions && typeof this.pcsWorkStatusOptions === 'object')
? this.pcsWorkStatusOptions
: {};
const hasStatus = Object.prototype.hasOwnProperty.call(statusOptions, workStatus);
return workStatus === '1' || !hasStatus
? "timing-card-container"
: workStatus === '2'
? 'warning-card-container'
: 'running-card-container';
},
// 查看设备电位表格
pointDetail(row, dataType) {
const {deviceId} = row
this.$refs.pointTable.showTable({siteId: this.siteId, deviceId, deviceCategory: 'PCS'}, dataType)
},
hasFieldPointId(pcsItem, fieldName) {
const row = this.getFieldRow(pcsItem, fieldName);
return !!String(row?.dataPoint || "").trim();
},
getFieldRow(pcsItem, fieldName) {
const key = String(fieldName || "").trim();
const map = pcsItem?._fieldRowMap || {};
return map[key] || null;
},
handlePcsFieldClick(pcsItem, fieldName, title) {
const row = this.getFieldRow(pcsItem, fieldName);
const pointId = String(row?.dataPoint || "").trim();
this.openCurveDialogByPointId(pointId, title || fieldName);
},
openCurveDialogByPointId(pointId, title) {
const normalizedPointId = String(pointId || "").trim();
if (!normalizedPointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || normalizedPointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId: normalizedPointId,
pointType: "data",
rangeType: "custom",
startTime: range[0],
endTime: range[1],
};
this.curveDialogVisible = true;
},
handleCurveDialogOpened() {
if (!this.curveChart && this.$refs.curveChartRef) {
this.curveChart = echarts.init(this.$refs.curveChartRef);
}
this.loadCurveData();
},
handleCurveDialogClosed() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
this.curveLoading = false;
},
getDefaultCurveRange() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return [this.formatDateTime(start), this.formatDateTime(end)];
},
formatDateTime(date) {
const d = new Date(date);
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
},
formatCurveTime(value) {
if (value === undefined || value === null || value === "") {
return "";
}
const raw = String(value).trim();
const normalized = raw
.replace("T", " ")
.replace(/\.\d+/, "")
.replace(/Z$/, "")
.replace(/([+-]\d{2}:?\d{2})$/, "")
.trim();
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
if (matched) {
return `${matched[1]} ${matched[2]}`;
}
return normalized.slice(0, 16);
},
loadCurveData() {
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
this.$message.warning("点位信息不完整,无法查询曲线");
return;
}
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
this.$message.warning("请选择查询时间范围");
return;
}
this.curveQuery.startTime = this.curveCustomRange[0];
this.curveQuery.endTime = this.curveCustomRange[1];
const query = {
siteId: this.curveQuery.siteId,
pointId: this.curveQuery.pointId,
pointType: "data",
rangeType: "custom",
startTime: this.curveQuery.startTime,
endTime: this.curveQuery.endTime,
};
this.curveLoading = true;
getPointConfigCurve(query).then((response) => {
const rows = response?.data || [];
this.renderCurveChart(rows);
}).catch(() => {
this.renderCurveChart([]);
}).finally(() => {
this.curveLoading = false;
});
},
renderCurveChart(rows = []) {
if (!this.curveChart) return;
const xData = rows.map(item => this.formatCurveTime(item.dataTime));
const yData = rows.map(item => item.pointValue);
this.curveChart.clear();
this.curveChart.setOption({
legend: {},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
data: xData,
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: [
{
name: this.curveDialogTitle,
type: "line",
data: yData,
connectNulls: true,
},
],
});
if (!rows.length) {
this.$message.warning("当前时间范围暂无曲线数据");
}
},
handleTagClick(deviceId) {
this.selectedPcsId = deviceId || "";
},
getModuleRows(menuCode, sectionName) {
return (this.displayData || []).filter(item => item.menuCode === menuCode && item.sectionName === sectionName);
},
getFieldName(fieldCode) {
if (!fieldCode) {
return "";
}
const index = fieldCode.lastIndexOf("__");
return index >= 0 ? fieldCode.slice(index + 2) : fieldCode;
},
getFieldMap(rows = [], deviceId = "") {
const rowMap = this.getFieldRowMap(rows, deviceId);
return Object.keys(rowMap).reduce((acc, fieldName) => {
const row = rowMap[fieldName] || {};
acc[fieldName] = row.fieldValue;
return acc;
}, {});
},
getFieldRowMap(rows = [], deviceId = "") {
const map = {};
const targetDeviceId = this.normalizeDeviceId(deviceId || "");
// 设备维度优先:先吃 device_id 对应值,再用默认值(空 device_id)补齐
rows.forEach(item => {
if (!item || !item.fieldCode) {
return;
}
const itemDeviceId = this.normalizeDeviceId(item.deviceId || "");
if (itemDeviceId !== targetDeviceId) {
return;
}
map[this.getFieldName(item.fieldCode)] = item;
});
rows.forEach(item => {
if (!item || !item.fieldCode) {
return;
}
const itemDeviceId = this.normalizeDeviceId(item.deviceId || "");
if (itemDeviceId !== "") {
return;
}
const fieldName = this.getFieldName(item.fieldCode);
if (map[fieldName] === undefined || map[fieldName] === null || map[fieldName] === "") {
map[fieldName] = item;
}
});
return map;
},
getLatestTime(menuCode) {
const times = (this.displayData || [])
.filter(item => item.menuCode === menuCode && item.valueTime)
.map(item => new Date(item.valueTime).getTime())
.filter(ts => !isNaN(ts));
if (times.length === 0) {
return '-';
}
const date = new Date(Math.max(...times));
const p = (n) => String(n).padStart(2, '0');
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}`;
},
getPcsDeviceList() {
return getPcsNameList(this.siteId).then((response) => {
this.pcsDeviceList = response?.data || [];
}).catch(() => {
this.pcsDeviceList = [];
});
},
buildPcsList() {
const devices = (this.pcsDeviceList && this.pcsDeviceList.length > 0)
? this.pcsDeviceList
: [{deviceId: this.siteId, deviceName: 'PCS'}];
this.pcsList = devices.map((device) => ({
...this.getFieldMap(this.getModuleRows('SBJK_PCS', '电参量'), device.deviceId || device.id || this.siteId),
deviceId: device.deviceId || device.id || this.siteId,
deviceName: device.deviceName || device.name || device.deviceId || device.id || 'PCS',
...this.getFieldMap(this.getModuleRows('SBJK_PCS', '状态'), device.deviceId || device.id || this.siteId),
_fieldRowMap: {
...this.getFieldRowMap(this.getModuleRows('SBJK_PCS', '电参量'), device.deviceId || device.id || this.siteId),
...this.getFieldRowMap(this.getModuleRows('SBJK_PCS', '状态'), device.deviceId || device.id || this.siteId),
},
dataUpdateTime: this.getLatestTime('SBJK_PCS'),
alarmNum: 0,
pcsBranchInfoList: [],
}));
},
updateData() {
this.loading = true;
// 先渲染卡片框架,字段值走单点位 loading
this.buildPcsList();
Promise.all([
getProjectDisplayData(this.siteId),
this.getPcsDeviceList(),
this.loadSiteEnumOptions(),
]).then(([displayResponse]) => {
this.displayData = displayResponse?.data || [];
this.buildPcsList();
}).finally(() => (this.loading = false));
},
init() {
this.updateData();
this.updateInterval(this.updateData);
},
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
} }
}, },
}; methods:{
problemSaved(){
this.$confirm('确认故障已复位?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
showClose:false,
closeOnClickModal:false,
type: 'warning',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true;
setTimeout(() => {
// todo 调用接口如果关机成功 调用done方法 否则不关闭弹窗
done();
// setTimeout(() => {
instance.confirmButtonLoading = false;
// }, 300);
}, 3000);
} else {
done();
}
}
}).then(() => {
//只有在故障复位成功的情况下会走到这里
this.$message({
type: 'success',
message: '故障复位成功!'
});
}).catch(() => {
//取消复位
});
},
machineClosed(){
this.$confirm('确认要关机吗?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
showClose:false,
closeOnClickModal:false,
type: 'warning',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true;
setTimeout(() => {
// todo 调用接口如果关机成功 调用done方法 否则不关闭弹窗
done();
// setTimeout(() => {
instance.confirmButtonLoading = false;
// }, 300);
}, 3000);
} else {
done();
}
}
}).then(() => {
//只有在关机成功的情况下会走到这里
this.$message({
type: 'success',
message: '关机成功!'
});
}).catch(() => {
//取消关机
});
},
//6个方块数据
getRunningHeadData(){
getRunningHeadInfo(this.siteId).then(response => {
this.runningHeadData = response?.data || {}
})
},
getPcsList(){
this.loading = true
getPcsDetailInfo(this.siteId).then(response => {
const data = response?.data || {}
this.pcsList = JSON.parse(JSON.stringify(data))
}).finally(()=>this.loading = false)
},
init(){
this.getRunningHeadData()
this.getPcsList()
}
},
}
</script> </script>
<style lang="scss" scoped>
.pcs-tags {
margin: 0 0 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-start;
align-items: center;
}
.pcs-tag-item { <style scoped lang="scss">
cursor: pointer; .pcs-container{
} margin-top: 25px;
border:1px solid #eeeeee;
.field-disabled { border-radius: 6px 6px 0 0;
cursor: not-allowed; //红色标题
opacity: 0.8; .pcs-header{
} background: #05AEA3;
display: flex;
.curve-tools { position: relative;
margin-bottom: 10px; justify-content: flex-start;
display: flex; align-items: center;
align-items: center; padding: 0;
gap: 10px; height: 60px;
} border-radius: 6px 6px 0 0;
.pcs-title{
.point-loading-icon { color: #ffffff;
color: #409eff; font-size: 20px;
display: inline-block; font-weight: 500;
transform-origin: center; line-height: 20px;
animation: pointLoadingSpinPulse 1.1s linear infinite; padding: 0 50px 0 25px;
} }
@keyframes pointLoadingSpinPulse { .pcs-status{
0% { opacity: 0.45; transform: rotate(0deg) scale(0.9); } color: #ffffff;
50% { opacity: 1; transform: rotate(180deg) scale(1.08); } font-size: 12px;
100% { opacity: 0.45; transform: rotate(360deg) scale(0.9); } line-height: 20px;
}
.pcs-btns{
position: absolute;
right: 25px;
top: 50%;
transform: translateY(-50%);
}
}
.pcs-header.warn{
background-color:#FC6B69 ;
}
.pcs-header.close{
background-color:#666666 ;
}
} }
</style> </style>

View File

@ -1,151 +1,108 @@
<template> <template>
<el-card <el-card shadow="always" class="common-card-container common-card-container-body-no-padding">
shadow="always"
class="common-card-container common-card-container-body-no-padding"
>
<div slot="header"> <div slot="header">
<span class="card-title">PCS有功功率/PCS无功功率</span> <span class="card-title">储能功率曲线</span>
</div> </div>
<div ref="chartRef" style="height: 360px" /> <div style="height: 360px" id="cnglqxChart"/>
</el-card> </el-card>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>
<script> <script>
import * as echarts from "echarts"; import * as echarts from 'echarts'
import resize from "@/mixins/ems/resize"; import resize from '@/mixins/ems/resize'
import { getPointConfigCurve } from "@/api/ems/site"; import {formatDate} from "@/filters/ems";
import {storagePower} from '@/api/ems/dzjk'
export default { export default {
mixins: [resize], mixins: [resize],
props: {
displayData: {
type: Array,
default: () => []
}
},
data() { data() {
return { return {
chart: null, chart: null
}; }
}, },
mounted() { mounted() {
this.chart = echarts.init(this.$refs.chartRef); this.chart = echarts.init(document.querySelector('#cnglqxChart'))
}, },
beforeDestroy() { beforeDestroy() {
if (!this.chart) { if (!this.chart) {
return; return
} }
this.chart.dispose(); this.chart.dispose()
this.chart = null; this.chart = null
}, },
methods: { methods: {
init(siteId, timeRange) { init(siteId){
const [startTime = "", endTime = ""] = timeRange; this.chart.showLoading()
const query = { const x = []
rangeType: "custom", const data1 =[],data2 =[]
startTime: this.normalizeDateTime(startTime, false), storagePower(siteId).then(response => {
endTime: this.normalizeDateTime(endTime, true), const source = response?.data?.energyStoragePowList || []
siteId source.forEach(item=>{
}; x.push(formatDate(item.createDate,false,true))
const rows = (this.displayData || []).filter( data1.push(item.pcsTotalActPower)
(item) => data2.push(item.pcsTotalReactivePower)
item &&
item.useFixedDisplay !== 1 &&
[
"SBJK_SSYX__curvePcsActivePower",
"SBJK_SSYX__curvePcsReactivePower"
].includes(item.fieldCode) &&
item.dataPoint
);
const tasks = rows.map((row) => {
const pointId = String(row.dataPoint || "").trim();
if (!pointId) return Promise.resolve(null);
return getPointConfigCurve({
...query,
pointId
}) })
.then((response) => { this.setOption(x,data1,data2)
const list = response?.data || []; }).finally(()=>{
return { this.chart.hideLoading()
name: (row.deviceName || "") + (row.fieldName || row.fieldCode || pointId), })
data: list
.map((item) => [
this.parseToTimestamp(item.dataTime),
Number(item.pointValue)
])
.filter((item) => item[0] && !Number.isNaN(item[1]))
};
})
.catch(() => null);
});
Promise.all(tasks)
.then((series) => {
this.setOption((series || []).filter(Boolean));
});
}, },
normalizeDateTime(value, endOfDay) { setOption(x,data1,data2) {
const raw = String(value || "").trim(); this.chart.setOption({
if (!raw) return ""; color:['#FFBD00','#3C81FF'],
if (raw.includes(" ")) return raw;
return `${raw} ${endOfDay ? "23:59:59" : "00:00:00"}`;
},
parseToTimestamp(value) {
if (!value) return null;
const t = new Date(value).getTime();
return Number.isNaN(t) ? null : t;
},
setOption(seriesData = []) {
this.chart && this.chart.setOption({
legend: { legend: {
left: "center", left: 'center',
top: "5", top: '10',
itemWidth: 10,
itemHeight: 5,
textStyle: {
fontSize: 9,
},
}, },
grid: { grid: {
containLabel: true, left: "15%"
}, },
tooltip: { tooltip: {
show: true, trigger: 'axis',
trigger: "axis", axisPointer: { // 坐标轴指示器,坐标轴触发有效
axisPointer: { type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
type: "cross",
} }
}, },
textStyle: { textStyle:{
color: "#333333", color:"#333333",
}, },
xAxis: { type: "time" }, xAxis: {type:'category',data:x},
yAxis: { yAxis: {
type: "value", type: 'value',
}, },
dataZoom: [ dataZoom: [
{ {
type: "inside", type: 'inside',
start: 0, start: 0,
end: 100, end: 100
}, },
{ {
start: 0, start: 0,
end: 100, end: 100
}, }
], ],
series: seriesData.map((item) => ({ // POC昨日有功功率、POC昨日无功功率
type: "line", series: [
name: item.name, {
showSymbol: false, name:'POC实时有功功率',
smooth: true, type: 'line',
areaStyle: { areaStyle: {
opacity: 0.35 color:'#FFBD00'
}, },
data: item.data data: data1,
})), },{
}, true); name:'POC实时无功功率',
}, type: 'line',
}, areaStyle: {
}; color: '#3C81FF'
},
data: data2
}]
})
}
}
}
</script> </script>

View File

@ -1,141 +1,99 @@
<template> <template>
<el-card <el-card shadow="always" class="common-card-container common-card-container-body-no-padding">
shadow="always"
class="common-card-container common-card-container-body-no-padding"
>
<div slot="header"> <div slot="header">
<span class="card-title">平均SOC</span> <span class="card-title">电池平均SOC</span>
</div> </div>
<div ref="chartRef" style="height: 360px" /> <div style="height: 360px" id="dcpjsocChart"/>
</el-card> </el-card>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>
<script> <script>
import * as echarts from "echarts"; import * as echarts from 'echarts'
import resize from "@/mixins/ems/resize"; import resize from '@/mixins/ems/resize'
import { getPointConfigCurve } from "@/api/ems/site"; import {formatDate} from "@/filters/ems";
import {batteryAveSoc} from '@/api/ems/dzjk'
export default { export default {
mixins: [resize], mixins: [resize],
props: {
displayData: {
type: Array,
default: () => []
}
},
data() { data() {
return { return {
chart: null, chart: null
}; }
}, },
mounted() { mounted() {
this.chart = echarts.init(this.$refs.chartRef); this.chart = echarts.init(document.querySelector('#dcpjsocChart'))
}, },
beforeDestroy() { beforeDestroy() {
if (!this.chart) { if (!this.chart) {
return; return
} }
this.chart.dispose(); this.chart.dispose()
this.chart = null; this.chart = null
}, },
methods: { methods: {
init(siteId,timeRange) { init(siteId){
const [startTime='', endTime=''] = timeRange; this.chart.showLoading()
const query = { const x = []
siteId, const data =[]
rangeType: "custom", batteryAveSoc(siteId).then(response => {
startTime: this.normalizeDateTime(startTime, false), const source = response?.data?.batteryAveSOCList || []
endTime: this.normalizeDateTime(endTime, true) source.forEach(item=>{
}; x.push(formatDate(item.createDate,false,true))
const rows = (this.displayData || []).filter( data.push(item.batterySOC)
(item) => })
item && this.setOption(x,data)
item.fieldCode === "SBJK_SSYX__curveBatteryAveSoc" && }).finally(()=>{
item.useFixedDisplay !== 1 && this.chart.hideLoading()
item.dataPoint })
);
const tasks = rows.map((row) => {
const pointId = String(row.dataPoint || "").trim();
if(!pointId) return Promise.resolve(null);
return getPointConfigCurve({
...query,
pointId
}).then((response) => {
const list = response?.data || [];
return {
name: (row.deviceName || "") + (row.fieldName || row.fieldCode || pointId),
data: list
.map((item) => [this.parseToTimestamp(item.dataTime), Number(item.pointValue)])
.filter((item) => item[0] && !Number.isNaN(item[1]))
};
}).catch(() => null);
});
Promise.all(tasks).then((series) => {
this.setOption((series || []).filter(Boolean));
});
}, },
normalizeDateTime(value, endOfDay) { setOption(x,data) {
const raw = String(value || "").trim(); this.chart.setOption({
if (!raw) return ""; color:['#FFBD00','#3C81FF'],
if (raw.includes(" ")) return raw; // legend: {
return `${raw} ${endOfDay ? "23:59:59" : "00:00:00"}`; // left: 'center',
}, // bottom: '10',
parseToTimestamp(value) { // },
if (!value) return null;
const t = new Date(value).getTime();
return Number.isNaN(t) ? null : t;
},
setOption(seriesData = []) {
this.chart && this.chart.setOption({
legend: {
left: "center",
top: "5",
itemWidth: 10,
itemHeight: 5,
textStyle: {
fontSize: 9,
},
},
grid: {
containLabel: true,
},
tooltip: { tooltip: {
show:true, trigger: 'axis',
trigger: "axis", axisPointer: { // 坐标轴指示器,坐标轴触发有效
axisPointer: { type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
type: "cross",
} }
}, },
textStyle: { grid: {
color: "#333333", left: "15%"
}, },
xAxis: { type: "time" }, textStyle:{
color:"#333333",
},
xAxis: {type:'category',data:x},
yAxis: { yAxis: {
type: "value", type: 'value',
}, },
dataZoom: [ dataZoom: [
{ {
type: "inside", type: 'inside',
start: 0, start: 0,
end: 100, end: 100
}, },
{ {
start: 0, start: 0,
end: 100, end: 100
}, }
], ],
series: seriesData.map(item => ({ series: [
type: "line", {
name: item.name, name:'电池平均SOC',
showSymbol: false, data: data,
smooth: true, type: 'line',
areaStyle: { areaStyle: {
opacity: 0.35 color:'#FFBD00'
}, }
data: item.data
})), }]
},true); })
}, }
}, }
}; }
</script> </script>

View File

@ -1,142 +1,99 @@
<template> <template>
<el-card <el-card shadow="always" class="common-card-container common-card-container-body-no-padding">
shadow="always"
class="common-card-container common-card-container-body-no-padding"
>
<div slot="header"> <div slot="header">
<span class="card-title">电池平均温度</span> <span class="card-title">电池平均温度</span>
</div> </div>
<div ref="chartRef" style="height: 360px" /> <div style="height: 360px" id="dcpjwdChart"/>
</el-card> </el-card>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>
<script> <script>
import * as echarts from "echarts"; import * as echarts from 'echarts'
import resize from "@/mixins/ems/resize"; import resize from '@/mixins/ems/resize'
import { getPointConfigCurve } from "@/api/ems/site"; import {formatDate} from "@/filters/ems";
import {batteryAveTemp} from '@/api/ems/dzjk'
export default { export default {
mixins: [resize], mixins: [resize],
props: {
displayData: {
type: Array,
default: () => []
}
},
data() { data() {
return { return {
chart: null, chart: null
}; }
}, },
mounted() { mounted() {
this.chart = echarts.init(this.$refs.chartRef); this.chart = echarts.init(document.querySelector('#dcpjwdChart'))
}, },
beforeDestroy() { beforeDestroy() {
if (!this.chart) { if (!this.chart) {
return; return
} }
this.chart.dispose(); this.chart.dispose()
this.chart = null; this.chart = null
}, },
methods: { methods: {
init(siteId,timeRange) { init(siteId){
const [startTime='', endTime=''] = timeRange; this.chart.showLoading()
const query = { const x = []
siteId, const data1 =[],data2 =[]
rangeType: "custom", batteryAveTemp(siteId).then(response => {
startTime: this.normalizeDateTime(startTime, false), const source = response?.data?.batteryAveTempList || []
endTime: this.normalizeDateTime(endTime, true) source.forEach(item=>{
}; x.push(formatDate(item.createDate,false,true))
const rows = (this.displayData || []).filter( data1.push(item.batteryTemp)
(item) => })
item && this.setOption(x,data1,data2)
item.fieldCode === "SBJK_SSYX__curveBatteryAveTemp" && }).finally(()=>{
item.useFixedDisplay !== 1 && this.chart.hideLoading()
item.dataPoint })
);
const tasks = rows.map((row) => {
const pointId = String(row.dataPoint || "").trim();
if(!pointId) return Promise.resolve(null);
return getPointConfigCurve({
...query,
pointId
}).then((response) => {
const list = response?.data || [];
return {
name: (row.deviceName || "") + (row.fieldName || row.fieldCode || pointId),
data: list
.map((item) => [this.parseToTimestamp(item.dataTime), Number(item.pointValue)])
.filter((item) => item[0] && !Number.isNaN(item[1]))
};
}).catch(() => null);
});
Promise.all(tasks).then((series) => {
this.setOption((series || []).filter(Boolean));
});
}, },
normalizeDateTime(value, endOfDay) { setOption(x,data) {
const raw = String(value || "").trim(); this.chart.setOption({
if (!raw) return ""; color:['#3C81FF'],
if (raw.includes(" ")) return raw; // legend: {
return `${raw} ${endOfDay ? "23:59:59" : "00:00:00"}`; // left: 'center',
}, // bottom: '10',
parseToTimestamp(value) { // },
if (!value) return null;
const t = new Date(value).getTime();
return Number.isNaN(t) ? null : t;
},
setOption(seriesData = []) {
this.chart && this.chart.setOption({
legend: {
left: "center",
top: "5",
itemWidth: 10,
itemHeight: 5,
textStyle: {
fontSize: 9,
},
},
grid: {
containLabel: true,
},
tooltip: { tooltip: {
show:true, trigger: 'axis',
trigger: "axis", axisPointer: { // 坐标轴指示器,坐标轴触发有效
axisPointer: { type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
type: "cross",
} }
}, },
textStyle: { grid: {
color: "#333333", left: "15%"
}, },
xAxis: { type: "time" }, textStyle:{
color:"#333333",
},
xAxis: {type:'category',data:x},
yAxis: { yAxis: {
type: "value", type: 'value',
}, },
dataZoom: [ dataZoom: [
{ {
type: "inside", type: 'inside',
start: 0, start: 0,
end: 100, end: 100
}, },
{ {
start: 0, start: 0,
end: 100, end: 100
}, }
], ],
series: seriesData.map(item => ({ series: [
type: "line", {
name: item.name, name:'电池平均温度',
showSymbol: false, data: data,
smooth: true, type: 'line',
areaStyle: { areaStyle: {
opacity: 0.35 color:'#3C81FF'
}, },
data: item.data }]
})), })
},true); }
}, }
}, }
};
</script> </script>

View File

@ -1,142 +1,96 @@
<template> <template>
<el-card <el-card shadow="always" class="common-card-container common-card-container-body-no-padding">
shadow="always"
class="common-card-container common-card-container-body-no-padding"
>
<div slot="header"> <div slot="header">
<span class="card-title">PCS最高温度</span> <span class="card-title">Poc平均温度</span>
</div> </div>
<div ref="chartRef" style="height: 360px" /> <div style="height: 360px" id="pocpjwdChart"/>
</el-card> </el-card>
</template> </template>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>
<script> <script>
import * as echarts from "echarts"; import * as echarts from 'echarts'
import resize from "@/mixins/ems/resize"; import resize from '@/mixins/ems/resize'
import { getPointConfigCurve } from "@/api/ems/site"; import {formatDate} from "@/filters/ems";
import {stackAveTemp} from '@/api/ems/dzjk'
export default { export default {
mixins: [resize], mixins: [resize],
props: {
displayData: {
type: Array,
default: () => []
}
},
data() { data() {
return { return {
chart: null, chart: null
}; }
}, },
mounted() { mounted() {
this.chart = echarts.init(this.$refs.chartRef); this.chart = echarts.init(document.querySelector('#pocpjwdChart'))
}, },
beforeDestroy() { beforeDestroy() {
if (!this.chart) { if (!this.chart) {
return; return
} }
this.chart.dispose(); this.chart.dispose()
this.chart = null; this.chart = null
}, },
methods: { methods: {
init(siteId,timeRange) { init(siteId){
const [startTime='', endTime=''] = timeRange; this.chart.showLoading()
const query = { const x = []
siteId, const data =[]
rangeType: "custom", stackAveTemp(siteId).then(response => {
startTime: this.normalizeDateTime(startTime, false), const source = response?.data?.stackAveTempList || []
endTime: this.normalizeDateTime(endTime, true) source.forEach(item=>{
}; x.push(formatDate(item.createDate,false,true))
const rows = (this.displayData || []).filter( data.push(item.temp)
(item) => })
item && this.setOption(x,data)
item.fieldCode === "SBJK_SSYX__curvePcsMaxTemp" && }).finally(()=>{
item.useFixedDisplay !== 1 && this.chart.hideLoading()
item.dataPoint })
);
const tasks = rows.map((row) => {
const pointId = String(row.dataPoint || "").trim();
if(!pointId) return Promise.resolve(null);
return getPointConfigCurve({
...query,
pointId
}).then((response) => {
const list = response?.data || [];
return {
name: (row.deviceName || "") + (row.fieldName || row.fieldCode || pointId),
data: list
.map((item) => [this.parseToTimestamp(item.dataTime), Number(item.pointValue)])
.filter((item) => item[0] && !Number.isNaN(item[1]))
};
}).catch(() => null);
});
Promise.all(tasks).then((series) => {
this.setOption((series || []).filter(Boolean));
});
}, },
normalizeDateTime(value, endOfDay) { setOption(x,data) {
const raw = String(value || "").trim(); this.chart.setOption({
if (!raw) return ""; color:['#FFBD00','#3C81FF'],
if (raw.includes(" ")) return raw;
return `${raw} ${endOfDay ? "23:59:59" : "00:00:00"}`;
},
parseToTimestamp(value) {
if (!value) return null;
const t = new Date(value).getTime();
return Number.isNaN(t) ? null : t;
},
setOption(seriesData = []) {
this.chart && this.chart.setOption({
legend: {
left: "center",
top: "5",
itemWidth: 10,
itemHeight: 5,
textStyle: {
fontSize: 9,
},
},
grid: {
containLabel: true,
},
tooltip: { tooltip: {
show:true, trigger: 'axis',
trigger: "axis", axisPointer: { // 坐标轴指示器,坐标轴触发有效
axisPointer: { type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
type: "cross",
} }
}, },
textStyle: { grid: {
color: "#333333", left: "15%"
}, },
xAxis: { type: "time" }, textStyle:{
color:"#333333",
},
xAxis: {type:'category',data:x},
yAxis: { yAxis: {
type: "value", type: 'value',
}, },
dataZoom: [ dataZoom: [
{ {
type: "inside", type: 'inside',
start: 0, start: 0,
end: 100, end: 100
}, },
{ {
start: 0, start: 0,
end: 100, end: 100
}, }
], ],
series: seriesData.map(item => ({ series: [
type: "line", {
name: item.name, name:'Poc平均温度',
showSymbol: false, data: data,
smooth: true, type: 'line',
areaStyle: { areaStyle: {
opacity: 0.35 color:'#FFBD00'
}, }
data: item.data
})), }]
},true); })
}, }
}, }
}; }
</script> </script>

View File

@ -2,284 +2,64 @@
<template> <template>
<div class="ssyx-ems-dashboard-editor-container"> <div class="ssyx-ems-dashboard-editor-container">
<!-- 6个方块--> <!-- 6个方块-->
<real-time-base-info :display-data="runningDisplayData" :loading="runningHeadLoading" @field-click="handleHeadFieldClick"/> <real-time-base-info :data="runningHeadData"/>
<!-- 时间选择 -->
<date-range-select ref="dateRangeSelect" @updateDate="updateDate" style="margin-top:20px;"/>
<!-- echart图表--> <!-- echart图表-->
<el-row :gutter="32" style="background:#fff;margin:30px 0;"> <el-row :gutter="32" style="background:#fff;margin:30px 0;">
<el-col :xs="24" :sm="12" :lg="12"> <el-col :xs="24" :sm="12" :lg="12">
<cnglqx-chart ref='cnglqx' :display-data="runningDisplayData"/> <cnglqx-chart ref='cnglqx'/>
</el-col> </el-col>
<el-col :xs="24" :sm="12" :lg="12"> <el-col :xs="24" :sm="12" :lg="12">
<pocpjwd-chart ref='pocpjwd' :display-data="runningDisplayData"/> <pocpjwd-chart ref='pocpjwd'/>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="32" style="margin:30px 0;"> <el-row :gutter="32" style="margin:30px 0;">
<el-col :xs="24" :sm="12" :lg="12"> <el-col :xs="24" :sm="12" :lg="12">
<dcpjsoc-chart ref="dcpjsoc" :display-data="runningDisplayData"/> <dcpjsoc-chart ref="dcpjsoc"/>
</el-col> </el-col>
<el-col :xs="24" :sm="12" :lg="12"> <el-col :xs="24" :sm="12" :lg="12">
<dcpjwd-chart ref="dcpjwd" :display-data="runningDisplayData"/> <dcpjwd-chart ref="dcpjwd"/>
</el-col> </el-col>
</el-row> </el-row>
<el-dialog
:visible.sync="curveDialogVisible"
:title="curveDialogTitle"
width="1000px"
append-to-body
class="ems-dialog"
:close-on-click-modal="false"
destroy-on-close
@opened="handleCurveDialogOpened"
@closed="handleCurveDialogClosed"
>
<div class="curve-tools">
<el-date-picker
v-model="curveCustomRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 440px"
/>
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button>
</div>
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
</el-dialog>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
</style> </style>
<script> <script>
import * as echarts from "echarts";
import DateRangeSelect from '@/components/Ems/DateRangeSelect/index.vue'
import RealTimeBaseInfo from "./../RealTimeBaseInfo.vue"; import RealTimeBaseInfo from "./../RealTimeBaseInfo.vue";
import CnglqxChart from './CnglqxChart.vue' import CnglqxChart from './CnglqxChart.vue'
import PocpjwdChart from './PocpjwdChart.vue' import PocpjwdChart from './PocpjwdChart.vue'
import DcpjwdChart from './DcpjwdChart.vue' import DcpjwdChart from './DcpjwdChart.vue'
import DcpjsocChart from './DcpjsocChart.vue' import DcpjsocChart from './DcpjsocChart.vue'
import getQuerySiteId from "@/mixins/ems/getQuerySiteId"; import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getProjectDisplayData} from '@/api/ems/dzjk' import {getRunningHeadInfo} from '@/api/ems/dzjk'
import {getPointConfigCurve} from "@/api/ems/site";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
export default { export default {
name:'DzjkSbjkSsyx', name:'DzjkSbjkSsyx',
components:{RealTimeBaseInfo,CnglqxChart,PocpjwdChart,DcpjwdChart,DcpjsocChart,DateRangeSelect}, components:{RealTimeBaseInfo,CnglqxChart,PocpjwdChart,DcpjwdChart,DcpjsocChart},
mixins:[getQuerySiteId,intervalUpdate], mixins:[getQuerySiteId],
data() { data() {
return { return {
runningDisplayData: [], //单站监控项目配置展示数据 runningHeadData:{},//运行信息
timeRange:[],
isInit:true,
runningHeadLoading: false,
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
}
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
} }
}, },
methods:{ methods:{
handleHeadFieldClick(item) {
const pointId = String(item?.pointId || item?.raw?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: item?.title || item?.raw?.fieldName || pointId,
});
},
openCurveDialog({pointId, title}) {
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || pointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId,
pointType: "data",
rangeType: "custom",
startTime: range[0],
endTime: range[1],
};
this.curveDialogVisible = true;
},
handleCurveDialogOpened() {
if (!this.curveChart && this.$refs.curveChartRef) {
this.curveChart = echarts.init(this.$refs.curveChartRef);
}
this.loadCurveData();
},
handleCurveDialogClosed() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
this.curveLoading = false;
},
getDefaultCurveRange() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return [this.formatDateTime(start), this.formatDateTime(end)];
},
formatDateTime(date) {
const d = new Date(date);
const pad = (num) => String(num).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
},
formatCurveTime(value) {
if (value === undefined || value === null || value === "") {
return "";
}
const raw = String(value).trim();
const normalized = raw
.replace("T", " ")
.replace(/\.\d+/, "")
.replace(/Z$/, "")
.replace(/([+-]\d{2}:?\d{2})$/, "")
.trim();
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
if (matched) {
return `${matched[1]} ${matched[2]}`;
}
return normalized.slice(0, 16);
},
loadCurveData() {
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
this.$message.warning("点位信息不完整,无法查询曲线");
return;
}
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
this.$message.warning("请选择查询时间范围");
return;
}
this.curveQuery.startTime = this.curveCustomRange[0];
this.curveQuery.endTime = this.curveCustomRange[1];
const query = {
siteId: this.curveQuery.siteId,
pointId: this.curveQuery.pointId,
pointType: "data",
rangeType: "custom",
startTime: this.curveQuery.startTime,
endTime: this.curveQuery.endTime,
};
this.curveLoading = true;
getPointConfigCurve(query).then((response) => {
const rows = response?.data || [];
this.renderCurveChart(rows);
}).catch(() => {
this.renderCurveChart([]);
}).finally(() => {
this.curveLoading = false;
});
},
renderCurveChart(rows = []) {
if (!this.curveChart) return;
const xData = rows.map(item => this.formatCurveTime(item.dataTime));
const yData = rows.map(item => item.pointValue);
this.curveChart.clear();
this.curveChart.setOption({
legend: {},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
data: xData,
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: [
{
name: this.curveDialogTitle,
type: "line",
data: yData,
connectNulls: true,
},
],
});
if (!rows.length) {
this.$message.warning("当前时间范围暂无曲线数据");
}
},
//6个方块数据 //6个方块数据
getRunningHeadData(){ getRunningHeadData(){
this.runningHeadLoading = true getRunningHeadInfo(this.siteId).then(response => {
return getProjectDisplayData(this.siteId).then((displayResponse) => { this.runningHeadData = response?.data || {}
this.runningDisplayData = displayResponse?.data || []
}).finally(() => {
this.runningHeadLoading = false
})
},
// 更新时间范围 重置图表
updateDate(data){
this.timeRange=data
!this.isInit && this.updateChart()
this.isInit = false
},
updateChart(){
this.$refs.cnglqx.init(this.siteId,this.timeRange||[])
this.$refs.pocpjwd.init(this.siteId,this.timeRange||[])
this.$refs.dcpjsoc.init(this.siteId,this.timeRange||[])
this.$refs.dcpjwd.init(this.siteId,this.timeRange||[])
this.updateInterval(this.updateData)
},
updateData(){
this.getRunningHeadData().finally(() => {
this.updateChart()
}) })
}, },
init(){ init(){
this.$refs.dateRangeSelect.init(true) this.getRunningHeadData()
this.$nextTick(()=>{ this.$nextTick(()=>{
this.updateData() this.$refs.cnglqx.init(this.siteId)
this.$refs.pocpjwd.init(this.siteId)
this.$refs.dcpjsoc.init(this.siteId)
this.$refs.dcpjwd.init(this.siteId)
}) })
} }
} }

View File

@ -1,153 +0,0 @@
<template>
<div>
<el-card
v-for="(item,index) in list"
:key="index+'ylLise'"
class="sbjk-card-container running-card-container"
:class="{
'warning-card-container':item.emsCommunicationStatus && item.emsCommunicationStatus !== '0',
'running-card-container':item.emsCommunicationStatus === '0'
}"
shadow="always">
<div slot="header">
<span class="large-title">{{ item.deviceName }}</span>
<div class="info">
<div>
{{
$store.state.ems.communicationStatusOptions[
item.emsCommunicationStatus
]
}}
</div>
<div>数据更新时间{{ item.dataUpdateTime || '-' }}</div>
</div>
<div class="alarm">
<el-button type="primary" round size="small" style="margin-right:20px;" @click="pointDetail(item,'point')">
详细
</el-button>
<el-badge :hidden="!item.alarmNum" :value="item.alarmNum || 0" class="item">
<i
class="el-icon-message-solid alarm-icon"
@click="pointDetail(item,'alarmPoint')"
></i>
</el-badge>
</div>
</div>
<el-row class="device-info-row">
<el-col v-for="(tempDataItem,tempDataIndex) in tempData" :key="tempDataIndex+'hdTempData'" :span="12"
class="device-info-col">
<span class="pointer" @click="showChart(tempDataItem.title,item.deviceId)">
<span class="left">{{ tempDataItem.title }}</span>
<span class="right">
<i v-if="isPointLoading(item[tempDataItem.attr])" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item[tempDataItem.attr]) }}<span v-html="tempDataItem.unit"></span></span>
</span>
</span>
</el-col>
</el-row>
</el-card>
<el-empty v-show="list.length<=0" :image-size="200"></el-empty>
<point-chart ref="pointChart" :site-id="siteId"/>
<point-table ref="pointTable"/>
</div>
</template>
<script>
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getXfDataList} from '@/api/ems/dzjk'
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import pointChart from "./../PointChart.vue";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
export default {
name: 'DzjkSbjkXf',
mixins: [getQuerySiteId, intervalUpdate],
components: {pointChart, PointTable},
data() {
return {
loading: false,
list: [],
tempData: [
{title: '主电源备用电池状态', attr: 'dczt', unit: ''},
{title: '手自动状态延时状态', attr: 'yszt', unit: ''},
{title: '启动喷洒气体喷洒状态', attr: 'pszt', unit: ''},
{title: '压力开关状态电磁阀状态', attr: 'dcfzt', unit: ''},
]
}
},
methods: {
displayValue(value) {
return value === undefined || value === null || value === "" ? "-" : value;
},
isPointLoading(value) {
return this.loading && (value === undefined || value === null || value === "" || value === "-");
},
// 查看设备电位表格
pointDetail(row, dataType) {
const {deviceId} = row
this.$refs.pointTable.showTable({siteId: this.siteId, deviceId, deviceCategory: 'XF'}, dataType)
},
showChart(pointName, deviceId) {
pointName && this.$refs.pointChart.showChart({pointName, deviceCategory: 'XF', deviceId})
},
updateData() {
this.loading = true
getXfDataList(this.siteId).then(response => {
this.list = JSON.parse(JSON.stringify(response?.data || []));
}).finally(() => {
this.loading = false
})
},
init() {
this.updateData()
this.updateInterval(this.updateData)
}
},
mounted() {
}
}
</script>
<style scoped lang="scss">
.sbjk-card-container {
&:not(:last-child) {
margin-bottom: 25px;
}
.el-row {
background-color: #ffffff;
border: 1px solid #eeeeee;
font-size: 14px;
line-height: 16px;
color: #333333;
.el-col {
padding: 12px 0;
text-align: center;
position: relative;
}
.el-col {
border-bottom: 1px solid #eeeeee;
}
.el-col:not(:nth-child(3n)) {
border-right: 1px solid #eeeeee;
}
}
}
.point-loading-icon {
color: #409eff;
display: inline-block;
transform-origin: center;
animation: pointLoadingSpinPulse 1.1s linear infinite;
}
@keyframes pointLoadingSpinPulse {
0% { opacity: 0.45; transform: rotate(0deg) scale(0.9); }
50% { opacity: 1; transform: rotate(180deg) scale(1.08); }
100% { opacity: 0.45; transform: rotate(360deg) scale(0.9); }
}
</style>

View File

@ -1,471 +1,118 @@
<template> <template>
<div> <div v-loading="loading">
<div class="pcs-tags"> <!-- todo 判断条件是否需要更新-->
<el-tag <div class="yl-item-container" :class="{'yl-warn-item-container':item.workMode !== '0'}" v-for="(item,index) in list" :key="index+'ylLise'">
size="small" <div class="header">
:type="selectedSectionKey ? 'info' : 'primary'" <div class="header-title">{{item.systemName}}</div>
:effect="selectedSectionKey ? 'plain' : 'dark'" <div>工作模式<span class="header-values">{{$store.state.ems.workModeOptions[item.workMode]}}</span></div>
class="pcs-tag-item" <div>当前温度<span class="header-values">{{item.currentTemperature}}&#8451;</span></div>
@click="handleTagClick('')" </div>
> <div class="content">
全部 <el-row>
</el-tag> <el-col v-for="(tempDataItem,tempDataIndex) in tempData" :key="tempDataIndex+'ylTempData'" :span="8">{{tempDataItem.title}}{{item[tempDataItem.attr]}}&#8451;</el-col>
<el-tag </el-row>
v-for="(group, index) in sectionGroups"
:key="index + 'ylTag'"
size="small"
:type="selectedSectionKey === group.sectionKey ? 'primary' : 'info'"
:effect="selectedSectionKey === group.sectionKey ? 'dark' : 'plain'"
class="pcs-tag-item"
@click="handleTagClick(group.sectionKey)"
>
{{ group.displayName || group.sectionName || "冷却" }}
</el-tag>
</div>
<el-card
v-for="(group, index) in filteredSectionGroups"
:key="index + 'ylSection'"
class="sbjk-card-container list running-card-container"
shadow="always"
>
<div slot="header">
<span class="large-title">{{ group.displayName || group.sectionName || "冷却" }}</span>
<div class="info">
<div>状态{{ group.statusText }}</div>
<div>数据更新时间{{ group.updateTimeText }}</div>
</div> </div>
</div> </div>
<el-row class="device-info-row"> <el-empty v-show="list.length<=0" :image-size="200"></el-empty>
<el-col </div>
v-for="(item, dataIndex) in group.items"
:key="dataIndex + 'ylField'"
:span="8"
class="device-info-col"
:class="{ 'field-disabled': !item.pointId }"
>
<div class="field-click-wrapper" @click="handleFieldClick(item)">
<span class="left">{{ item.fieldName }}</span>
<span class="right">
<i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span>
</span>
</div>
</el-col>
</el-row>
</el-card>
<el-dialog
:visible.sync="curveDialogVisible"
:title="curveDialogTitle"
width="1000px"
append-to-body
class="ems-dialog"
:close-on-click-modal="false"
destroy-on-close
@opened="handleCurveDialogOpened"
@closed="handleCurveDialogClosed"
>
<div class="curve-tools">
<el-date-picker
v-model="curveCustomRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 440px"
/>
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button>
</div>
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
</el-dialog>
</div>
</template> </template>
<script>
import * as echarts from "echarts";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import { getProjectDisplayData } from "@/api/ems/dzjk";
import { getDeviceList, getPointConfigCurve } from "@/api/ems/site";
<script>
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getCoolingDataList} from '@/api/ems/dzjk'
export default { export default {
name: "DzjkSbjkYl", name:'DzjkSbjkYl',
mixins: [getQuerySiteId, intervalUpdate], mixins:[getQuerySiteId],
data() { data() {
return { return {
loading: false, loading:false,
displayData: [], list:[],
selectedSectionKey: "", tempData:[
coolingDeviceList: [], {title:'制热开启点',attr:'heatingStartPoint'},
curveDialogVisible: false, {title:'制冷开启点',attr:'coolingStartPoint'},
curveDialogTitle: "点位曲线", {title:'高温告警点',attr:'highTempAlarmPoint'},
curveChart: null, {title:'制热停止点',attr:'heatingStopPoint'},
curveLoading: false, {title:'制冷停止点',attr:'coolingStopPoint'},
curveCustomRange: [], {title:'低温告警点',attr:'lowTempAlarmPoint'},
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
};
},
computed: {
moduleDisplayData() {
return (this.displayData || []).filter((item) => item.menuCode === "SBJK_YL");
},
ylTemplateFields() {
const source = this.moduleDisplayData || [];
const result = [];
const seen = new Set();
source.forEach((item) => {
const fieldName = String(item?.fieldName || "").trim();
if (!fieldName || seen.has(fieldName)) {
return;
}
seen.add(fieldName);
result.push(fieldName);
});
return result.length > 0 ? result : this.fallbackFields;
},
sectionGroups() {
const source = this.moduleDisplayData || [];
const devices = (this.coolingDeviceList || []).length > 0
? this.coolingDeviceList
: [{ deviceId: "", deviceName: "冷却" }];
return devices.map((device, index) => {
const deviceId = String(device?.deviceId || device?.id || "").trim();
const sectionKey = deviceId || `COOLING_${index}`;
const displayName = String(device?.deviceName || device?.name || deviceId || `冷却${index + 1}`).trim();
const exactRows = source.filter((item) => String(item?.deviceId || "").trim() === deviceId);
const fallbackRows = source.filter((item) => !String(item?.deviceId || "").trim());
const exactValueMap = {}; ]
exactRows.forEach((item) => {
const key = String(item?.fieldName || "").trim();
if (key) {
exactValueMap[key] = item;
}
});
const fallbackValueMap = {};
fallbackRows.forEach((item) => {
const key = String(item?.fieldName || "").trim();
if (key && fallbackValueMap[key] === undefined) {
fallbackValueMap[key] = item;
}
});
const items = (this.ylTemplateFields || []).map((fieldName) => {
const row = exactValueMap[fieldName] || fallbackValueMap[fieldName] || {};
return {
fieldName,
fieldValue: row.fieldValue,
valueTime: row.valueTime,
pointId: String(row?.dataPoint || "").trim(),
raw: row,
};
});
const statusItem = (items || []).find((it) => String(it.fieldName || "").includes("状态"));
const timestamps = [...exactRows, ...fallbackRows]
.map((it) => new Date(it?.valueTime).getTime())
.filter((ts) => !isNaN(ts));
return {
sectionName: displayName,
sectionKey,
displayName,
deviceId,
items,
statusText: this.displayValue(statusItem ? statusItem.fieldValue : "-"),
updateTimeText: timestamps.length > 0 ? this.formatDate(new Date(Math.max(...timestamps))) : "-",
};
});
},
displaySectionGroups() {
if (this.sectionGroups.length > 0) {
return this.sectionGroups;
}
return [
{
sectionName: "冷却参数",
items: this.fallbackFields.map((fieldName) => ({ fieldName, fieldValue: "-" })),
statusText: "-",
updateTimeText: "-",
},
];
},
filteredSectionGroups() {
const groups = this.displaySectionGroups || [];
if (!this.selectedSectionKey) {
return groups;
}
return groups.filter((group) => group.sectionKey === this.selectedSectionKey);
},
fallbackFields() {
return ["供水温度", "回水温度", "供水压力", "回水压力", "冷源水温度", "VB01开度", "VB02开度"];
},
},
methods: {
handleFieldClick(item) {
const pointId = String(item?.pointId || item?.raw?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: item?.fieldName || pointId,
});
},
openCurveDialog({ pointId, title }) {
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || pointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId,
pointType: "data",
rangeType: "custom",
startTime: range[0],
endTime: range[1],
};
this.curveDialogVisible = true;
},
handleCurveDialogOpened() {
if (!this.curveChart && this.$refs.curveChartRef) {
this.curveChart = echarts.init(this.$refs.curveChartRef);
}
this.loadCurveData();
},
handleCurveDialogClosed() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
this.curveLoading = false;
},
getDefaultCurveRange() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return [this.formatDateTime(start), this.formatDateTime(end)];
},
formatDateTime(date) {
const d = new Date(date);
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
},
formatCurveTime(value) {
if (value === undefined || value === null || value === "") {
return "";
}
const raw = String(value).trim();
const normalized = raw
.replace("T", " ")
.replace(/\.\d+/, "")
.replace(/Z$/, "")
.replace(/([+-]\d{2}:?\d{2})$/, "")
.trim();
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
if (matched) {
return `${matched[1]} ${matched[2]}`;
}
return normalized.slice(0, 16);
},
loadCurveData() {
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
this.$message.warning("点位信息不完整,无法查询曲线");
return;
}
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
this.$message.warning("请选择查询时间范围");
return;
}
this.curveQuery.startTime = this.curveCustomRange[0];
this.curveQuery.endTime = this.curveCustomRange[1];
const query = {
siteId: this.curveQuery.siteId,
pointId: this.curveQuery.pointId,
pointType: "data",
rangeType: "custom",
startTime: this.curveQuery.startTime,
endTime: this.curveQuery.endTime,
};
this.curveLoading = true;
getPointConfigCurve(query)
.then((response) => {
const rows = response?.data || [];
this.renderCurveChart(rows);
})
.catch(() => {
this.renderCurveChart([]);
})
.finally(() => {
this.curveLoading = false;
});
},
renderCurveChart(rows = []) {
if (!this.curveChart) return;
const xData = rows.map((item) => this.formatCurveTime(item.dataTime));
const yData = rows.map((item) => item.pointValue);
this.curveChart.clear();
this.curveChart.setOption({
legend: {},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
data: xData,
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: [
{
name: this.curveDialogTitle,
type: "line",
data: yData,
connectNulls: true,
},
],
});
if (!rows.length) {
this.$message.warning("当前时间范围暂无曲线数据");
}
},
handleTagClick(sectionKey) {
this.selectedSectionKey = sectionKey || "";
},
displayValue(value) {
return value === undefined || value === null || value === "" ? "-" : value;
},
isPointLoading(value) {
return this.loading && (value === undefined || value === null || value === "" || value === "-");
},
formatDate(date) {
if (!(date instanceof Date) || isNaN(date.getTime())) {
return "-";
}
const p = (n) => String(n).padStart(2, "0");
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(
date.getMinutes()
)}:${p(date.getSeconds())}`;
},
getCoolingDeviceList() {
return getDeviceList(this.siteId)
.then((response) => {
const list = response?.data || [];
this.coolingDeviceList = list.filter((item) => item.deviceCategory === "COOLING");
})
.catch(() => {
this.coolingDeviceList = [];
});
},
updateData() {
this.loading = true;
Promise.all([getProjectDisplayData(this.siteId), this.getCoolingDeviceList()])
.then(([response]) => {
this.displayData = response?.data || [];
})
.finally(() => {
this.loading = false;
});
},
init() {
this.updateData();
this.updateInterval(this.updateData);
},
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
} }
}, },
}; methods:{
init(){
this.loading = true
getCoolingDataList(this.siteId).then(response => {
this.list = JSON.parse(JSON.stringify(response?.data || []));
}).finally(() => {this.loading = false})
}
},
mounted(){
}
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.sbjk-card-container { .yl-item-container{
&.list:not(:last-child) { border-radius: 5px;
background-color: #EBF6F6;
&:not(:last-child){
margin-bottom: 25px; margin-bottom: 25px;
} }
.header{
.info { line-height: 40px;
float: right; font-size: 14px;
text-align: right; >div{
font-size: 12px; display: inline-block;
color: #666; margin-right: 40px;
line-height: 18px; }
.header-title{
border-radius: 5px 0 5px 0;
color:#ffffff;
width: 120px;
height: 40px;
font-size: 16px;
background-color: #05AEA3;
text-align: center;
}
.header-values{
color: #05AEA3;
font-weight: 500;
}
}
.content{
padding:25px;
.el-row{
background-color: #ffffff;
border:1px solid #eeeeee;
line-height: 14px;
color: #333333;
font-size: 12px;
.el-col{
padding:10px 0;
text-align: center;
}
.el-col:nth-child(-n+3){
border-bottom: 1px solid #eeeeee;
}
.el-col:not(:nth-child(3n)){
border-right: 1px solid #eeeeee;
}
}
}
}
.yl-warn-item-container{
background-color: #FFF1F0;
.header{
.header-title{
background-color: #FC6B69;
}
.header-values{
color: #FC6B69;
}
} }
} }
.pcs-tags {
margin: 0 0 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-start;
align-items: center;
}
.pcs-tag-item {
cursor: pointer;
}
.device-info-col {
cursor: pointer;
}
.field-click-wrapper {
width: 100%;
}
.device-info-col.field-disabled {
cursor: not-allowed;
opacity: 0.8;
}
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.point-loading-icon {
color: #409eff;
display: inline-block;
transform-origin: center;
animation: pointLoadingSpinPulse 1.1s linear infinite;
}
@keyframes pointLoadingSpinPulse {
0% { opacity: 0.45; transform: rotate(0deg) scale(0.9); }
50% { opacity: 1; transform: rotate(180deg) scale(1.08); }
100% { opacity: 0.45; transform: rotate(360deg) scale(0.9); }
}
</style> </style>

View File

@ -1,18 +1,25 @@
<template> <template>
<div style="width:100%" v-loading="loading"> <div style="width:100%" v-loading="loading">
<!-- 搜索栏-->
<el-form :inline="true" class="select-container"> <el-form :inline="true" class="select-container">
<el-form-item label="电表">
<el-select v-model="search.deviceId" placeholder="请选择" loading-text="正在加载数据">
<el-option :label="item.deviceName" :value="item.id" v-for="(item,index) in deviceOptions" :key="index+'dbOptions'"></el-option>
</el-select>
</el-form-item>
<!-- <el-form-item label="日报">-->
<!-- <el-select v-model="search.rb" placeholder="请选择" :loading="loading" loading-text="正在加载数据">-->
<!-- <el-option :label="item.name" :value="item.id" v-for="(item,index) in rbOptions" :key="index+'rbOptions'"></el-option>-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<el-form-item label="时间选择"> <el-form-item label="时间选择">
<el-date-picker <el-date-picker
v-model="dateRange" v-model="search.date"
type="daterange" type="date"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
:clearable="false"
:picker-options="pickerOptions" :picker-options="pickerOptions"
:default-value="defaultDateRange" :default-value="defaultDate">
></el-date-picker> </el-date-picker>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="onSearch" native-type="button">搜索</el-button> <el-button type="primary" @click="onSearch" native-type="button">搜索</el-button>
@ -20,221 +27,159 @@
<el-form-item> <el-form-item>
<el-button @click="onReset" native-type="button">重置</el-button> <el-button @click="onReset" native-type="button">重置</el-button>
</el-form-item> </el-form-item>
<el-form-item>
<el-button type="primary" @click="exportTable" native-type="button">导出</el-button>
</el-form-item>
</el-form> </el-form>
<!--表格-->
<el-table <el-table
class="common-table" class="common-table"
:data="tableData" :data="tableData"
stripe stripe
style="width: 100%; margin-top: 25px;" style="width: 100%;margin-top:25px;">
> <!-- 汇总列-->
<el-table-column label="汇总"> <el-table-column label="汇总">
<el-table-column prop="dataTime" label="日期" width="120"></el-table-column> <el-table-column
</el-table-column> prop="dataTime"
label="日期"
<el-table-column label="充电量" align="center"> width="120">
<el-table-column align="center" prop="activePeakKwh" label="尖"></el-table-column> <template slot-scope="scope">
<el-table-column align="center" prop="activeHighKwh" label="峰"></el-table-column> <span>{{scope.row.dataTime}}{{scope.row.dataTime === '汇总' ? '' : ':00'}}</span>
<el-table-column align="center" prop="activeFlatKwh" label="平"></el-table-column> </template>
<el-table-column align="center" prop="activeValleyKwh" label="谷"></el-table-column> </el-table-column>
<el-table-column align="center" prop="activeTotalKwh" label="总"></el-table-column> </el-table-column>
</el-table-column> <!--充电量列-->
<el-table-column label="充电量">
<el-table-column label="放电量" align="center"> <el-table-column
<el-table-column align="center" prop="reActivePeakKwh" label="尖"></el-table-column> prop="activePeakKwh"
<el-table-column align="center" prop="reActiveHighKwh" label=""></el-table-column> label="">
<el-table-column align="center" prop="reActiveFlatKwh" label="平"></el-table-column> </el-table-column>
<el-table-column align="center" prop="reActiveValleyKwh" label="谷"></el-table-column> <el-table-column
<el-table-column align="center" prop="reActiveTotalKwh" label="总"></el-table-column> prop="activeHighKwh"
</el-table-column> label="峰">
</el-table-column>
<el-table-column label="效率(%)" align="center"> <el-table-column
<el-table-column align="center" prop="effect"></el-table-column> prop="activeFlatKwh"
</el-table-column> label="平">
</el-table-column>
<el-table-column label="备注" align="center" fixed="right" min-width="260"> <el-table-column
<template slot-scope="scope"> prop="activeValleyKwh"
<div class="remark-cell"> label="">
<span class="remark-text">{{ scope.row.remark || "-" }}</span> </el-table-column>
<el-button type="text" @click="editRemark(scope.row)">编辑</el-button> <el-table-column
</div> prop="activeTotalKwh"
</template> label="总">
</el-table-column> </el-table-column>
</el-table> </el-table-column>
<!--充电量列-->
<el-pagination <el-table-column label="放电量">
v-show="tableData.length > 0" <el-table-column
background prop="reActivePeakKwh"
@size-change="handleSizeChange" label="尖">
@current-change="handleCurrentChange" </el-table-column>
:current-page="pageNum" <el-table-column
:page-size="pageSize" prop="reActiveHighKwh"
:page-sizes="[10, 20, 30, 40]" label="峰">
layout="total, sizes, prev, pager, next, jumper" </el-table-column>
:total="totalSize" <el-table-column
style="margin-top: 15px; text-align: center" prop="reActiveFlatKwh"
> label="平">
</el-pagination> </el-table-column>
<el-table-column
prop="reActiveValleyKwh"
label="谷">
</el-table-column>
<el-table-column
prop="reActiveTotalKwh"
label="总">
</el-table-column>
</el-table-column>
<!-- 效率-->
<el-table-column label="效率(%)">
<el-table-column
prop="effect">
</el-table-column>
</el-table-column>
</el-table>
</div> </div>
</template> </template>
<script> <script>
import getQuerySiteId from "@/mixins/ems/getQuerySiteId"; import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import { batchGetBizRemark, getAmmeterData, saveBizRemark } from "@/api/ems/dzjk"; import { getAmmeterData, getLoadNameList} from '@/api/ems/dzjk'
import { formatDate } from "@/filters/ems"; import {formatDate} from "@/filters/ems";
const BIZ_TYPE = "stats_report";
const REPORT_KEY = "DBBB";
export default { export default {
name: "DzjkTjbbDbbb", name:'DzjkTjbbDbbb',
mixins: [getQuerySiteId], mixins: [getQuerySiteId],
data() { data() {
return { return {
loading: false, loading:false,
pickerOptions: { pickerOptions:{
disabledDate(time) { disabledDate(time) {
return time.getTime() > Date.now(); return time.getTime() > Date.now();
}, },
}, },
defaultDateRange: [], defaultDate:'',//默认展示的时间
dateRange: [], search:{deviceId:'',date:''},
tableData: [], deviceOptions:[],
pageSize: 10, // rbOptions:[
pageNum: 1, // {name:'日报1',id:1},
totalSize: 0, // {name:'日报2',id:2},
}; // ],
tableData:[]
}
}, },
methods: { methods:{
buildRemarkKey(dataTime) { // 搜索
return `${this.siteId}_${dataTime || ""}`; onSearch(){
this.getData()
}, },
loadRemarks(rows) { // 重置
if (!rows.length) return Promise.resolve({}); onReset(){
return batchGetBizRemark({ this.search.date = ''
bizType: BIZ_TYPE, this.getData()
bizKey1: REPORT_KEY,
bizKey2List: rows.map(row => this.buildRemarkKey(row.dataTime)),
}).then(response => response?.data || {});
}, },
applyRemarks(rows, remarkMap) { // 获取数据
rows.forEach(row => { getData(){
this.$set(row, "remark", remarkMap[this.buildRemarkKey(row.dataTime)] || ""); if(!this.search.deviceId) return
}); this.loading=true
}, getAmmeterData({siteId:this.siteId,deviceId:this.search.deviceId,dateTime:formatDate(this.search.date)}).then(response=>{
exportTable() { this.tableData=response?.data || [];
if (!this.dateRange?.length) return; }).finally(()=> {
const [startTime, endTime] = this.dateRange; this.loading = false
this.download(
"ems/statsReport/exportAmmeterDataFromDaily",
{
siteId: this.siteId,
startTime,
endTime,
},
`电表报表_${startTime}-${endTime}.xlsx`
);
},
onSearch() {
this.pageNum = 1;
this.getData();
},
onReset() {
this.dateRange = this.defaultDateRange;
this.pageNum = 1;
this.getData();
},
handleSizeChange(val) {
this.pageSize = val;
this.$nextTick(() => {
this.getData();
});
},
handleCurrentChange(val) {
this.pageNum = val;
this.$nextTick(() => {
this.getData();
});
},
editRemark(row) {
this.$prompt("请输入备注", "编辑备注", {
inputValue: row.remark || "",
inputType: "textarea",
inputPlaceholder: "可输入该日报表备注",
confirmButtonText: "保存",
cancelButtonText: "取消",
}) })
.then(({ value }) => {
return saveBizRemark({
bizType: BIZ_TYPE,
bizKey1: REPORT_KEY,
bizKey2: this.buildRemarkKey(row.dataTime),
remark: value || "",
}).then(() => {
this.$set(row, "remark", value || "");
this.$message.success("备注保存成功");
});
})
.catch(() => {});
}, },
getData() { getDbList(){
this.loading = true; return getLoadNameList(this.siteId).then(response=>{
const { siteId, pageNum, pageSize } = this; this.deviceOptions=response?.data || [];
const [startTime = "", endTime = ""] = this.dateRange || []; this.deviceOptions.length > 0 && (this.search.deviceId = this.deviceOptions[0].id);
getAmmeterData({ siteId, startTime, endTime, pageSize, pageNum }) })
.then(response => {
const rows = response?.rows || [];
this.tableData = rows;
this.totalSize = response?.total || 0;
return this.loadRemarks(rows);
})
.then(remarkMap => {
this.applyRemarks(this.tableData, remarkMap || {});
})
.finally(() => {
this.loading = false;
});
}, },
init() { init(){
this.dateRange = []; this.loading = true
this.tableData = []; this.deviceOptions = []
this.totalSize = 0; this.search.deviceId=''
this.pageSize = 10; this.search.date=''
this.pageNum = 1; this.tableData=[]
const now = new Date(); this.getDbList().then(()=>{
const lastDay = now.getTime(); if(this.search.deviceId){
const firstDay = new Date(new Date().setDate(1)).getTime(); this.onReset()
this.defaultDateRange = [formatDate(firstDay), formatDate(lastDay)]; }else{
this.dateRange = [formatDate(firstDay), formatDate(lastDay)]; this.loading = false
this.getData(); }
})
}, },
}, },
};
mounted(){
this.defaultDate = new Date()
}
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
::v-deep { ::v-deep{
.common-table.el-table .el-table__header-wrapper th, .common-table.el-table .el-table__header-wrapper th, .common-table.el-table .el-table__fixed-header-wrapper th{
.common-table.el-table .el-table__fixed-header-wrapper th {
border-bottom: 1px solid #dfe6ec; border-bottom: 1px solid #dfe6ec;
} }
} }
.remark-cell {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.remark-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
</style> </style>

View File

@ -1,209 +1,182 @@
<template> <template>
<el-card <div v-loading="loading">
shadow="always" <div class="select-container">
class="common-card-container time-range-card" <el-form :inline="true">
style="margin-top: 20px" <el-form-item label="电池堆">
> <el-select v-model="pcs" placeholder="请选择" :loading="loading" loading-text="正在加载数据">
<div slot="header" class="time-range-header"> <el-option :label="item.deviceName" :value="item.id" v-for="(item,index) in pcsOptions" :key="index+'pcsListOptions'"></el-option>
<span class="card-title"> </span> </el-select>
<date-range-select ref="dateRangeSelect" @updateDate="updateDate"/> </el-form-item>
<el-form-item label="时间选择">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始时间"
:picker-options="pickerOptions"
:default-value="defaultDateRange"
end-placeholder="结束时间">
</el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSearch" native-type="button">搜索</el-button>
</el-form-item>
<el-form-item>
<el-button @click="onReset" native-type="button">重置</el-button>
</el-form-item>
</el-form>
</div> </div>
<div class="card-main" v-loading="loading"> <div style="margin:30px 0;">
<el-button-group class="ems-btns-group"> <!-- 二个选择按钮-->
<el-button <el-row style="">
v-for="(item, index) in btnList" <el-col :xs="24" :sm="24" :lg="24">
:key="index + 'dcdqxBtns'" <el-button-group class="ems-btns-group">
size="mini" <el-button v-for="(item,index) in btnList" :key="index+'dcdqxBtns'" :class="{'activeBtn' : activeBtn === item.id}" @click="changeDataType(item.id)">{{item.name}}</el-button>
:class="{ activeBtn: activeBtn === item.id }" </el-button-group>
@click="changeDataType(item.id)" </el-col>
>{{ item.name }} </el-row>
</el-button <!--echart-->
> <div id="dcdEchart" style="height:360px;"></div>
</el-button-group>
<div id="dcdEchart" style="height: 310px"></div>
</div> </div>
</el-card> </div>
</template> </template>
<script> <script>
import * as echarts from "echarts"; import * as echarts from 'echarts'
import resize from "@/mixins/ems/resize"; import resize from "@/mixins/ems/resize";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId"; import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getStackData} from "@/api/ems/dzjk"; import { getStackData, getStackNameList} from '@/api/ems/dzjk'
import {formatDate} from "@/filters/ems"; import {formatDate} from "@/filters/ems";
import DateRangeSelect from "@/components/Ems/DateRangeSelect/index.vue";
export default { export default {
name: "DzjkTjbbDcdqx", name:'DzjkTjbbDcdqx',
components: {DateRangeSelect}, mixins: [resize,getQuerySiteId],
mixins: [resize, getQuerySiteId],
data() { data() {
return { return {
pickerOptions: { pickerOptions:{
disabledDate(time) { disabledDate(time) {
return time.getTime() > Date.now(); return time.getTime() > Date.now();
}, },
}, },
dateRange: [], defaultDateRange:[],//默认展示的时间
loading: false, dateRange:[],
activeBtn: "1", loading:false,
btnList: [ pcs:'',
{name: "堆平均维度", id: "1", attr: ["temp"], source: ["有功功率"]}, pcsOptions: [],
{name: "堆电压", id: "2", attr: ["voltage"], source: ["堆电压"]}, activeBtn:'1',
{name: "堆电流", id: "3", attr: ["current"], source: ["堆电流"]}, btnList:[
{name: "堆soc", id: "4", attr: ["soc"], source: ["堆soc"]}, {name:'堆平均维度',id:'1',attr:['temp'],source:[['日期','有功功率']]},
{name:'堆电压',id:'2',attr:['voltage'],source:[['日期','堆电压']]},
{name:'堆电流',id:'3',attr:['current'],source:[['日期','堆电流']]},
{name:'堆soc',id:'4',attr:['soc'],source:[['日期','堆soc']]},
], ],
}; }
}, },
methods: { methods: {
changeDataType(id) { changeDataType(id){
if (id !== this.activeBtn) { if(id !== this.activeBtn){
this.activeBtn = id; this.activeBtn=id;
this.getData(); this.getData()
} }
}, },
// 更新时间范围 重置图表 // 搜索
updateDate(data) { onSearch(){
this.dateRange = data || []; this.getData()
this.getData();
}, },
getData() { // 重置
const {siteId, activeBtn} = this; onReset(){
const [start = "", end = ""] = this.dateRange || []; // this.pcs = this.pcsOptions.length > 0 ?this.pcsOptions[0].id : ''
//接口调用完成之后 设置图表、结束loading this.dateRange=''
this.loading = true; this.getData()
getStackData({ },
siteId, getPcsList(){
startTime: formatDate(start), return getStackNameList(this.siteId).then(response => {
endTime: formatDate(end), const data = JSON.parse(JSON.stringify(response?.data || []))
dataType: activeBtn, this.pcsOptions = data
this.pcs = data.length>0?data[0].id:'';
}) })
.then((response) => {
this.setOption(response?.data || []);
})
.finally(() => {
this.loading = false;
});
}, },
compareDate(date1, date2) { getData(){
console.log("比较时间", date1, date2); const {siteId,pcs,activeBtn}=this;
// 年2025-09/天2025-09-15/时2025-09-15/10:00 const [start='',end='']=(this.dateRange || [])
if (date1.indexOf(":") > -1 && date2.indexOf(":") > -1) { if(!pcs) return
return parseInt(date1) - parseInt(date2); //接口调用完成之后 设置图表、结束loading
} this.loading=true;
const [date1_Y = "", date1_M = "", date1_D = ""] = date1.split("-"); //根据空格区分[年月日,小时] getStackData({siteId,deviceId:pcs,startTime:formatDate(start),endTime:formatDate(end),dataType:activeBtn}).then(response => {
const [date2_Y = "", date2_M = "", date2_D = ""] = date2.split("-"); //根据空格区分[年月日,小时] this.setOption(response?.data || [])
return ( }).finally(()=>{this.loading=false;})
(date1_Y === date2_Y && date1_M === date2_M && date1_D - date2_D) ||
(date1_Y === date2_Y && date1_M - date2_M) ||
date1_Y - date2_Y
);
}, },
setOption(data) { setOption(data) {
const ele = this.btnList.find((item) => { const ele = this.btnList.find((item)=>{return item.id === this.activeBtn})
return item.id === this.activeBtn; const source = JSON.parse(JSON.stringify(ele.source))
}); const length = ele.attr.length
const sourceBase = JSON.parse(JSON.stringify(ele.source)); const series = []
// sourceBase={name:'堆平均维度',id:'1',attr:['temp'],source:['有功功率']}, data.forEach((item)=>{
const source = []; const arr = ele.attr.map(key=>item[key])
const sourceTop = ["日期"]; source.push([item.statisDate,...arr])
let map = {}, })
mapArr = []; ele.attr.forEach((item)=>{
// 生成所有{日期:[],日期:[]}格式的对象和所有包含所有日期的[日期1,日期2...] series.push({
data.forEach((item) => { name:length>1?item:ele.name,
item.dataList.forEach((inner) => { type:ele.type || 'scatter'
// 日期格式 })
// 年2025-09/天2025-09-15/时2025-09-15/10:00 })
// 所有数据的日期格式一致 this.chart.setOption({
if (!map[inner.statisDate]) { color:['#FFBD00','#3C81FF'],
map[inner.statisDate] = []; legend: {
mapArr.push(inner.statisDate); left: 'center',
bottom: '10',
},
tooltip: {
trigger: 'axis',
axisPointer: { // 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
} }
}); },
}); textStyle:{
data.forEach((item, itemIndex) => { color:"#333333",
const dataTimeList = item.dataList.map((i) => i.statisDate); },
const noDataTime = mapArr.filter((i) => !dataTimeList.includes(i)); xAxis: {
sourceBase.forEach((outer, outerIndex) => { type: 'category',
sourceTop.push(`${item.deviceId}-${outer}`); },
noDataTime.forEach((i) => map[i].push("")); yAxis: {
item.dataList.forEach((inner, innerIndex) => { type: 'value',
map[inner.statisDate].push(inner[ele.attr[outerIndex]]); },
}); dataset: {source},
}); series
}); },true)
mapArr = mapArr.sort((a, b) => this.compareDate(a, b));
mapArr.forEach((item) => {
source.push([item, ...map[item]]);
});
source.unshift(sourceTop);
this.chart.setOption(
{
grid: {
containLabel: true,
},
legend: {
left: "center",
top: "10",
},
tooltip: {
trigger: "axis",
axisPointer: {
// 坐标轴指示器,坐标轴触发有效
type: "shadow", // 默认为直线,可选为:'line' | 'shadow'
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
dataset: {source},
series: source[0].slice(1).map((item) => {
return {
type: "line",
smooth: true,
connectNulls: true,
areaStyle: {
opacity: 0.7,
},
};
}),
},
true
);
}, },
initChart() { initChart() {
this.chart = echarts.init(document.querySelector("#dcdEchart")); this.chart = echarts.init(document.querySelector('#dcdEchart'));
},
init() {
this.$nextTick(() => {
this.initChart();
this.$refs.dateRangeSelect.init(true);
});
}, },
init(){
this.loading = true
this.pcs=''
this.pcsOptions=[]
this.initChart()
this.getPcsList().then(()=>{
if(this.pcs){
this.onReset()
}else{
this.loading = false
}
})
}
},
mounted(){
const now = new Date();
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
this.defaultDateRange = [lastMonth, now];
}, },
beforeDestroy() { beforeDestroy() {
if (!this.chart) { if (!this.chart) {
return; return
} }
this.chart.dispose(); this.chart.dispose()
this.chart = null; this.chart = null
}, },
}; }
</script> </script>

View File

@ -147,9 +147,6 @@ export default {
if(val){ if(val){
this.search.clusterId='' this.search.clusterId=''
this.getClusterList() this.getClusterList()
} else {
this.search.clusterId=''
this.clusterOptions=[]
} }
}, },
//表格数据 //表格数据
@ -173,15 +170,8 @@ export default {
}) })
}, },
async getClusterList(){ async getClusterList(){
const currentStackId = String(this.search.stackId || '')
if (!currentStackId) {
this.clusterOptions = []
this.search.clusterId = ''
return
}
this.clusterloading =true this.clusterloading =true
await getClusterNameList({stackDeviceId: this.search.stackId, siteId: this.siteId}).then(response => { await getClusterNameList(this.search.stackId).then(response => {
if (String(this.search.stackId || '') !== currentStackId) return
const data = JSON.parse(JSON.stringify(response?.data || [])) const data = JSON.parse(JSON.stringify(response?.data || []))
this.clusterOptions = data this.clusterOptions = data
this.search.clusterId = data.length > 0 ? data[0].id : '' this.search.clusterId = data.length > 0 ? data[0].id : ''
@ -210,3 +200,4 @@ export default {
} }
} }
</script> </script>

View File

@ -1,172 +1,183 @@
<template> <template>
<el-card <div v-loading="loading">
shadow="always" <div class="select-container">
class="common-card-container time-range-card" <el-form :inline="true">
style="margin-top: 20px" <!-- <el-form-item label="网点">-->
> <!-- <el-select v-model="wd" placeholder="请选择" :loading="loading" loading-text="正在加载数据">-->
<div slot="header" class="time-range-header"> <!-- <el-option :label="item.name" :value="item.id" v-for="(item,index) in wdOptions" :key="index+'sblxOptions'"></el-option>-->
<span class="card-title">功率曲线</span> <!-- </el-select>-->
<date-range-select <!-- </el-form-item>-->
ref="dateRangeSelect" <el-form-item label="PCS">
@updateDate="updateDate" <el-select v-model="pcs" placeholder="请选择" :loading="loading" loading-text="正在加载数据">
/> <el-option :label="item.deviceName" :value="item.id" v-for="(item,index) in pcsOptions" :key="index+'pcsListOptions'"></el-option>
</el-select>
</el-form-item>
<el-form-item label="时间选择">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始时间"
:picker-options="pickerOptions"
:default-value="defaultDateRange"
end-placeholder="结束时间">
</el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSearch" native-type="button">搜索</el-button>
</el-form-item>
<el-form-item>
<el-button @click="onReset" native-type="button">重置</el-button>
</el-form-item>
</el-form>
</div> </div>
<div class="card-main" v-loading="loading"> <div style="margin:30px 0;">
<div id="glqxEchart" style="height: 310px"></div> <!-- 二个选择按钮-->
<el-row style="">
<el-col :xs="24" :sm="24" :lg="24">
<el-button-group class="ems-btns-group">
<el-button v-for="(item,index) in btnList" :key="index+'flqxcBtns'" :class="{'activeBtn' : activeBtn === item.id}" @click="changeDataType(item.id)">{{item.name}}</el-button>
</el-button-group>
</el-col>
</el-row>
<!--echart-->
<div id="glqxEchart" style="height:360px;"></div>
</div> </div>
</el-card> </div>
</template> </template>
<script> <script>
import * as echarts from "echarts"; import * as echarts from 'echarts'
import resize from "@/mixins/ems/resize"; import resize from "@/mixins/ems/resize";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId"; import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getPowerData} from "@/api/ems/dzjk"; import {getPcsNameList, getPowerData} from "@/api/ems/dzjk";
import {formatDate} from "@/filters/ems"; import {formatDate} from "@/filters/ems";
import DateRangeSelect from "@/components/Ems/DateRangeSelect/index.vue";
export default { export default {
name: "DzjkTjbbGlqx", name:'DzjkTjbbGlqx',
components: {DateRangeSelect}, mixins: [resize,getQuerySiteId],
mixins: [resize, getQuerySiteId],
data() { data() {
return { return {
pickerOptions: { pickerOptions:{
disabledDate(time) { disabledDate(time) {
return time.getTime() > Date.now(); return time.getTime() > Date.now();
}, },
}, },
dateRange: [], defaultDateRange:[],//默认展示的时间
loading: false, dateRange:[],
}; loading:false,
pcs:'',
pcsOptions: [],
activeBtn:'1',
btnList:[
{name:'电网功率',id:'1',attr:'gridPower'},
{name:'负载功率',id:'2',attr:'loadPower'},
{name:'储能功率',id:'3',attr:'storagePower'},
{name:'光伏功率',id:'4',attr:'pvPower'},
],
}
}, },
methods: { methods: {
// 更新时间范围 重置图表 changeDataType(id){
updateDate(data) { if(id !== this.activeBtn){
this.dateRange = data || []; console.log('点击了不同的菜单,更新数据')
this.getData(); this.activeBtn=id;
this.getData()
}
}, },
getData() { // 搜索
const {siteId} = this; onSearch(){
let [start = "", end = ""] = this.dateRange || []; this.getData()
//接口调用完成之后 设置图表、结束loading },
this.loading = true; // 重置
getPowerData({ onReset(){
siteId, this.dateRange=[]
startDate: formatDate(start), this.getData()
endDate: formatDate(end), },
getPcsList(){
return getPcsNameList(this.siteId).then(response => {
const data = response?.data || [];
this.pcsOptions = data
this.pcs = data.length>0?data[0].id:'';
}) })
.then((response) => { },
this.setOption(response?.data || []); getData(){
}) const {siteId,pcs,activeBtn}=this;
.finally(() => { const [start='',end='']=(this.dateRange || [])
this.loading = false; if(!pcs) return
}); //接口调用完成之后 设置图表、结束loading
this.loading=true;
getPowerData({siteId,deviceId:pcs,startDate:formatDate(start),endDate:formatDate(end),dataType:activeBtn}).then(response => {
this.setOption(response?.data || [])
}).finally(()=>{this.loading=false;})
}, },
setOption(data) { setOption(data) {
const source = [["日期", "电网功率", "负载功率", "储能功率", "光伏功率"]]; const {name,attr} =this.btnList.find(item=>item.id===this.activeBtn)
data.forEach((item) => { const source = [['日期',name]]
source.push([ data.forEach((item,index)=>{
item.statisDate, source.push([item.statisDate,item[attr]])
item.gridPower, })
item.loadPower, this.chart.setOption({
item.storagePower, color:['#FFBD00','#3C81FF'],
item.pvPower, legend: {
]); left: 'center',
}); bottom: '10',
this.chart.setOption( },
tooltip: {
trigger: 'axis',
axisPointer: { // 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
},
textStyle:{
color:"#333333",
},
xAxis: {
type: 'category',
},
yAxis: {
type: 'value',
},
dataset:{source},
series: [
{ {
grid: { name,
containLabel: true, type: 'scatter',
}, }
legend: { ]
left: "center", },true)
top: "10",
},
tooltip: {
trigger: "axis",
axisPointer: {
// 坐标轴指示器,坐标轴触发有效
type: "shadow", // 默认为直线,可选为:'line' | 'shadow'
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
},
yAxis: {
type: "value",
},
dataset: {source},
dataZoom: [
{
type: "inside",
start: data.length > 500 ? 90 : 0,
end: 100,
},
{
start: data.length > 500 ? 90 : 0,
end: 100,
},
],
series: [
{
type: "line",
smooth: true,
connectNulls: true,
areaStyle: {
opacity: 0.7,
},
},
{
type: "line",
smooth: true,
connectNulls: true,
areaStyle: {
opacity: 0.7,
},
},
{
type: "line",
smooth: true,
connectNulls: true,
areaStyle: {
opacity: 0.7,
},
},
{
type: "line",
smooth: true,
connectNulls: true,
areaStyle: {
opacity: 0.7,
},
},
],
},
true
);
}, },
initChart() { initChart() {
if (this.chart) return; this.chart = echarts.init(document.querySelector('#glqxEchart'));
this.chart = echarts.init(document.querySelector("#glqxEchart"));
},
init() {
this.$nextTick(() => {
this.initChart();
this.$refs.dateRangeSelect.init();
});
}, },
init(){
this.loading = true
this.pcs=''
this.pcsOptions=[]
this.initChart()
this.getPcsList().then(()=>{
if(this.pcs){
this.onReset()
}else{
this.loading = false
}
})
}
},
mounted(){
const now = new Date();
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
this.defaultDateRange = [lastMonth, now];
}, },
beforeDestroy() { beforeDestroy() {
if (!this.chart) { if (!this.chart) {
return; return
} }
this.chart.dispose(); this.chart.dispose()
this.chart = null; this.chart = null
}, },
}; }
</script> </script>

View File

@ -1,16 +1,37 @@
<template> <template>
<el-card shadow="always" class="common-card-container time-range-card" style="margin-top:20px"> <el-card shadow="always" class="common-card-container" style="margin-top:20px">
<div slot="header" class="time-range-header"> <div slot="header">
<span class="card-title">电量指标</span> <span class="card-title">电量指标</span>
<date-range-select ref="dateRangeSelect" @updateDate="updateDate"/>
</div> </div>
<div class="card-main" v-loading="loading"> <div class="card-main" v-loading="loading">
<!-- 搜索栏-->
<div class="select-container">
<el-form :inline="true">
<el-form-item label="时间选择">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始时间"
:picker-options="pickerOptions"
:default-value="defaultDateRange"
end-placeholder="结束时间">
</el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getData" native-type="button">搜索</el-button>
</el-form-item>
<el-form-item>
<el-button @click="onReset" native-type="button">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="total-data"> <div class="total-data">
<div>总充电量:<span class="point">{{totalChargedCap | formatNumber}}kWh</span></div> <div>总充电量:<span class="point">{{totalChargedCap | formatNumber}}kWh</span></div>
<div>总放电量:<span class="point">{{totalDisChargedCap | formatNumber}}kWh</span></div> <div>总放电量:<span class="point">{{totalDisChargedCap | formatNumber}}kWh</span></div>
<div>综合效率:<span class="point">{{efficiency | formatNumber}}%</span></div> <div>综合效率:<span class="point">{{efficiency | formatNumber}}%</span></div>
</div> </div>
<div id="dlzbChart" style="height: 310px;"></div> <div id="dlzbChart" style="height: 310px"></div>
</div> </div>
</el-card> </el-card>
</template> </template>
@ -21,9 +42,7 @@ import resize from "@/mixins/ems/resize";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId"; import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getElectricData} from '@/api/ems/dzjk' import {getElectricData} from '@/api/ems/dzjk'
import {formatDate} from '@/filters/ems' import {formatDate} from '@/filters/ems'
import DateRangeSelect from '@/components/Ems/DateRangeSelect/index.vue'
export default { export default {
components: {DateRangeSelect},
mixins: [resize,getQuerySiteId], mixins: [resize,getQuerySiteId],
data() { data() {
return { return {
@ -32,6 +51,7 @@ export default {
return time.getTime() > Date.now(); return time.getTime() > Date.now();
}, },
}, },
defaultDateRange:[],//默认展示的时间
dateRange:[], dateRange:[],
loading:false, loading:false,
chart: null, chart: null,
@ -41,9 +61,9 @@ export default {
} }
}, },
methods: { methods: {
// 更新时间范围 重置图表 // 重置
updateDate(data){ onReset(){
this.dateRange=data || [] this.dateRange=[]
this.getData() this.getData()
}, },
setOption(data,unit){ setOption(data,unit){
@ -52,26 +72,17 @@ export default {
source.push([item.ammeterDate, item.chargedCap,item.disChargedCap,item.dailyEfficiency]) source.push([item.ammeterDate, item.chargedCap,item.disChargedCap,item.dailyEfficiency])
}) })
this.chart.setOption({ this.chart.setOption({
legend: { color:['#FFBD00','#3C81FF','#05AEA3'],
left: 'center', // legend: {
bottom: '15', // left: 'right',
}, // bottom: '10',
grid: { // },
top:40, tooltip: {},
containLabel: true xAxis: {
},
tooltip: {
trigger: 'axis',
axisPointer: { // 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
},
xAxis: [{
type: 'category', type: 'category',
name:`单位:${unit}`, name:unit,
nameLocation:'center', nameLocation:'center'
nameGap:30 },
}],
yAxis: [{ yAxis: [{
type: 'value', type: 'value',
name:'充电量/放电量kWh', name:'充电量/放电量kWh',
@ -91,25 +102,22 @@ export default {
onZero:false onZero:false
} }
}], }],
grid:{top:40},
dataset:{ dataset:{
source source
}, },
//所有充放电颜色保持统一
series: [ series: [
{ {
yAxisIndex:0, yAxisIndex:0,
type: 'bar',//柱状图 type: 'bar',//柱状图
color:'#4472c4'
}, },
{ {
yAxisIndex:0, yAxisIndex:0,
type: 'bar',//柱状图 type: 'bar',//柱状图
color:'#70ad47'
}, },
{ {
yAxisIndex:1, yAxisIndex:1,
type: 'line',//柱状图 type: 'line',//柱状图
color:'#FFBD00'
}, },
] ]
}) })
@ -125,24 +133,20 @@ export default {
this.totalChargedCap=totalChargedCap this.totalChargedCap=totalChargedCap
this.totalDisChargedCap=totalDisChargedCap this.totalDisChargedCap=totalDisChargedCap
this.efficiency=efficiency this.efficiency=efficiency
}).catch(() => {
this.setOption([], '')
this.totalChargedCap=''
this.totalDisChargedCap=''
this.efficiency=''
// 错误提示由全局请求拦截器处理,这里兜底避免出现 Uncaught (in promise)
}).finally(() => { }).finally(() => {
this.loading=false; this.loading=false;
}) })
}, },
init(){ init(){
this.$nextTick(()=>{ this.chart = echarts.init(document.querySelector('#dlzbChart'));
this.chart = echarts.init(document.querySelector('#dlzbChart')); this.onReset()
this.$refs.dateRangeSelect.init()
})
} }
}, },
mounted(){
const now = new Date();
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
this.defaultDateRange = [lastMonth, now];
},
beforeDestroy() { beforeDestroy() {
if (!this.chart) { if (!this.chart) {
return return
@ -159,7 +163,7 @@ export default {
line-height: 18px; line-height: 18px;
color: #333333; color: #333333;
font-size: 16px; font-size: 16px;
padding:20px 0; padding:10px 0;
>div{ >div{
display: inline-block; display: inline-block;
margin-right: 20px; margin-right: 20px;

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="ems-dashboard-editor-container ems-third-menu-container"> <div class="ems-dashboard-editor-container ems-content-container-padding tjbb-ems-dashboard-editor-container">
<el-menu <el-menu
class="ems-third-menu" class="ems-third-menu"
:default-active="$route.name" :default-active="$route.name"
@ -25,7 +25,7 @@
<script> <script>
import { dzjk } from '@/router/ems' import { dzjk } from '@/router/ems'
const childrenRoute = dzjk[0].children[0].children.find(item=> item.name==='DzjkTjbb').children//获取到统计报表下面的字路由 const childrenRoute = dzjk[0].children.find(item=> item.name==='DzjkTjbb').children//获取到统计报表下面的字路由
console.log('设备监控子路由',childrenRoute) console.log('设备监控子路由',childrenRoute)
export default { export default {
name:'DzjkTjbb', name:'DzjkTjbb',
@ -43,6 +43,10 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.tjbb-ems-dashboard-editor-container{
display: flex;
background: #FFFFFF;
}
.tjbb-ems-content-container{ .tjbb-ems-content-container{
margin-top:0; margin-top:0;
padding-top:0; padding-top:0;

View File

@ -1,225 +1,188 @@
<template> <template>
<el-card <div v-loading="loading">
shadow="always" <div class="select-container">
class="common-card-container time-range-card" <el-form :inline="true">
style="margin-top: 20px" <el-form-item label="PCS">
> <el-select v-model="pcs" placeholder="请选择" :loading="loading" loading-text="正在加载数据">
<div slot="header" class="time-range-header"> <el-option :label="item.deviceName" :value="item.id" v-for="(item,index) in pcsOptions" :key="index+'pcsListOptions'"></el-option>
<span class="card-title"> </span> </el-select>
<date-range-select ref="dateRangeSelect" @updateDate="updateDate"/> </el-form-item>
<el-form-item label="时间选择">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始时间"
:picker-options="pickerOptions"
:default-value="defaultDateRange"
end-placeholder="结束时间">
</el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSearch" native-type="button">搜索</el-button>
</el-form-item>
<el-form-item>
<el-button @click="onReset" native-type="button">重置</el-button>
</el-form-item>
</el-form>
</div> </div>
<div class="card-main" v-loading="loading"> <div style="margin:30px 0;">
<el-button-group class="ems-btns-group"> <!-- 二个选择按钮-->
<el-button <el-row style="">
v-for="(item, index) in btnList" <el-col :xs="24" :sm="24" :lg="24">
:key="index + 'flqxcBtns'" <el-button-group class="ems-btns-group">
size="mini" <el-button v-for="(item,index) in btnList" :key="index+'flqxcBtns'" :class="{'activeBtn' : activeBtn === item.id}" @click="changeDataType(item.id)">{{item.name}}</el-button>
:class="{ activeBtn: activeBtn === item.id }" </el-button-group>
@click="changeDataType(item.id)" </el-col>
>{{ item.name }} </el-row>
</el-button <!--echart-->
> <div id="pcsEchart" style="height:360px;"></div>
</el-button-group>
<div id="pcsEchart" style="height: 310px"></div>
</div> </div>
</el-card> </div>
</template> </template>
<script> <script>
import * as echarts from "echarts"; import * as echarts from 'echarts'
import resize from "@/mixins/ems/resize"; import resize from "@/mixins/ems/resize";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId"; import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getPCSData} from "@/api/ems/dzjk"; import { getPCSData, getPcsNameList} from '@/api/ems/dzjk'
import {formatDate} from "@/filters/ems"; import {formatDate} from "@/filters/ems";
import DateRangeSelect from "@/components/Ems/DateRangeSelect/index.vue";
export default { export default {
name: "DzjkTjbbPcsqx", name:'DzjkTjbbPcsqx',
components: {DateRangeSelect}, mixins: [resize,getQuerySiteId],
mixins: [resize, getQuerySiteId],
data() { data() {
return { return {
pickerOptions: { pickerOptions:{
disabledDate(time) { disabledDate(time) {
return time.getTime() > Date.now(); return time.getTime() > Date.now();
}, },
}, },
dateRange: [], defaultDateRange:[],//默认展示的时间
loading: false, dateRange:[],
activeBtn: "1", loading:false,
btnList: [ pcs:'',
{ pcsOptions: [],
name: "有功功率", activeBtn:'1',
id: "1", btnList:[
attr: ["activePower"], {name:'有功功率',id:'1',attr:['activePower'],source:[['日期','有功功率']]},
source: ["有功功率"], {name:'无功功率',id:'2',attr:['reactivePower'],source:[['日期','无功功率']]},
}, // {name:'温度',id:'wd'},
{ // {name:'三相电压',id:'sxdy'},
name: "无功功率", {name:'三相电流',id:'3',attr:['uCurrent','vCurrent','wCurrent'],source:[['日期','u电流','v电流','w电流']],type:'bar'},
id: "2",
attr: ["reactivePower"],
source: ["无功功率"],
},
{
name: "三相电流",
id: "3",
attr: ["uCurrent", "vCurrent", "wCurrent"],
source: ["u电流", "v电流", "w电流"],
type: "bar",
},
], ],
};
}
}, },
methods: { methods: {
changeDataType(id) { changeDataType(id){
if (id !== this.activeBtn) { if(id !== this.activeBtn){
console.log("点击了不同的菜单,更新数据"); console.log('点击了不同的菜单,更新数据')
this.activeBtn = id; this.activeBtn=id;
this.getData(); this.getData()
} }
}, },
// 更新时间范围 重置图表 // 搜索
updateDate(data) { onSearch(){
this.dateRange = data || []; this.getData()
this.getData();
}, },
getData() { // 重置
const {siteId, activeBtn} = this; onReset(){
const [start = "", end = ""] = this.dateRange || []; // this.pcs = this.pcsOptions.length > 0 ?this.pcsOptions[0].id : ''
this.loading = true; this.dateRange=[]
//接口调用完成之后 设置图表、结束loading this.getData()
getPCSData({ },
siteId, getPcsList(){
startTime: formatDate(start), return getPcsNameList(this.siteId).then(response => {
endTime: formatDate(end), const data = response?.data || [];
dataType: activeBtn, this.pcsOptions = data
this.pcs = data.length>0?data[0].id:'';
}) })
.then((response) => {
this.setOption(response?.data || []);
})
.finally(() => {
this.loading = false;
});
}, },
compareDate(date1, date2) { getData(){
console.log("比较时间", date1, date2); const {siteId,pcs,activeBtn}=this;
// 年2025-09/天2025-09-15/时2025-09-15/10:00 const [start='',end='']=(this.dateRange || [])
if (date1.indexOf(":") > -1 && date2.indexOf(":") > -1) { if(!pcs) return
return parseInt(date1) - parseInt(date2); this.loading=true;
} //接口调用完成之后 设置图表、结束loading
const [date1_Y = "", date1_M = "", date1_D = ""] = date1.split("-"); //根据空格区分[年月日,小时] getPCSData({siteId,deviceId:pcs,startTime:formatDate(start),endTime:formatDate(end),dataType:activeBtn}).then(response => {
const [date2_Y = "", date2_M = "", date2_D = ""] = date2.split("-"); //根据空格区分[年月日,小时] this.setOption(response?.data || [])
return ( }).finally(()=>{this.loading=false;})
(date1_Y === date2_Y && date1_M === date2_M && date1_D - date2_D) ||
(date1_Y === date2_Y && date1_M - date2_M) ||
date1_Y - date2_Y
);
}, },
setOption(data) { setOption(data) {
const ele = this.btnList.find((item) => { const ele = this.btnList.find((item)=>{return item.id === this.activeBtn})
return item.id === this.activeBtn; const source = JSON.parse(JSON.stringify(ele.source))
}); const length = ele.attr.length
const sourceBase = JSON.parse(JSON.stringify(ele.source)); const series = []
// sourceBase={name:'堆平均维度',id:'1',attr:['temp'],source:['有功功率']}, data.forEach((item)=>{
const source = []; const arr = ele.attr.map(key=>item[key])
const sourceTop = ["日期"]; source.push([item.statisDate,...arr])
let map = {}, })
mapArr = []; ele.attr.forEach((item)=>{
// 生成所有{日期:[],日期:[]}格式的对象和所有包含所有日期的[日期1,日期2...] series.push({
data.forEach((item) => { name:length>1?item:ele.name,
item.dataList.forEach((inner) => { type:ele.type || 'scatter'
// 日期格式 })
// 年2025-09/天2025-09-15/时2025-09-15/10:00 })
// 所有数据的日期格式一致 this.chart.setOption({
if (!map[inner.statisDate]) { color:['#FFBD00','#3C81FF','#91cc74'],
map[inner.statisDate] = []; legend: {
mapArr.push(inner.statisDate); left: 'center',
bottom: '10',
},
tooltip: {
trigger: 'axis',
axisPointer: { // 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
} }
}); },
}); textStyle:{
data.forEach((item, itemIndex) => { color:"#333333",
const dataTimeList = item.dataList.map((i) => i.statisDate); },
const noDataTime = mapArr.filter((i) => !dataTimeList.includes(i)); xAxis: {
sourceBase.forEach((outer, outerIndex) => { type: 'category',
sourceTop.push(`${item.deviceId}-${outer}`); },
noDataTime.forEach((i) => map[i].push("")); yAxis: {
item.dataList.forEach((inner, innerIndex) => { type: 'value',
map[inner.statisDate].push(inner[ele.attr[outerIndex]]); },
}); dataset: {source},
}); series
});
mapArr = mapArr.sort((a, b) => this.compareDate(a, b)); },true)
mapArr.forEach((item) => {
source.push([item, ...map[item]]);
});
source.unshift(sourceTop);
this.chart.setOption(
{
grid: {
containLabel: true,
},
legend: {
left: "center",
top: "10",
},
tooltip: {
trigger: "axis",
axisPointer: {
// 坐标轴指示器,坐标轴触发有效
type: "shadow", // 默认为直线,可选为:'line' | 'shadow'
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
dataset: {source},
series: source[0].slice(1).map((item) => {
return {
type: "line",
smooth: true,
connectNulls: true,
areaStyle: {
opacity: 0.7,
},
};
}),
},
true
);
}, },
initChart() { initChart() {
this.chart = echarts.init(document.querySelector("#pcsEchart")); this.chart = echarts.init(document.querySelector('#pcsEchart'));
},
init() {
this.$nextTick(() => {
this.initChart();
this.$refs.dateRangeSelect.init(true);
});
}, },
init(){
this.loading = true
this.pcs=''
this.pcsOptions=[]
this.dateRange =[]
this.initChart()
this.getPcsList().then(()=>{
if(this.pcs){
this.onReset()
}else{
this.loading=false;
}
})
}
},
mounted(){
const now = new Date();
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
this.defaultDateRange = [lastMonth, now];
}, },
beforeDestroy() { beforeDestroy() {
if (!this.chart) { if (!this.chart) {
return; return
} }
this.chart.dispose(); this.chart.dispose()
this.chart = null; this.chart = null
}, },
}; }
</script> </script>

View File

@ -1,316 +0,0 @@
<template>
<div style="width:100%" v-loading="loading">
<el-form :inline="true" class="select-container">
<el-form-item label="时间选择">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
:clearable="false"
:picker-options="pickerOptions"
:default-value="defaultDateRange"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSearch" native-type="button">搜索</el-button>
</el-form-item>
<el-form-item>
<el-button @click="onReset" native-type="button">重置</el-button>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="exportTable" native-type="button">导出</el-button>
</el-form-item>
</el-form>
<el-table
class="common-table"
:data="tableData"
:summary-method="getSummaries"
show-summary
stripe
style="width: 100%; margin-top: 25px;"
>
<el-table-column label="汇总" min-width="100px" align="center">
<el-table-column prop="dataTime" label="日期" min-width="100px" align="center"></el-table-column>
<el-table-column prop="dayType" label="日期类型" min-width="100px" align="center"></el-table-column>
<el-table-column prop="weatherDesc" label="天气情况" min-width="180px" align="center"></el-table-column>
</el-table-column>
<el-table-column label="充电价格" align="center">
<el-table-column align="center" prop="activePeakPrice" label="尖"></el-table-column>
<el-table-column align="center" prop="activeHighPrice" label="峰"></el-table-column>
<el-table-column align="center" prop="activeFlatPrice" label="平"></el-table-column>
<el-table-column align="center" prop="activeValleyPrice" label="谷"></el-table-column>
<el-table-column align="center" prop="activeTotalPrice" label="总"></el-table-column>
</el-table-column>
<el-table-column label="放电价格" align="center">
<el-table-column align="center" prop="reActivePeakPrice" label="尖"></el-table-column>
<el-table-column align="center" prop="reActiveHighPrice" label="峰"></el-table-column>
<el-table-column align="center" prop="reActiveFlatPrice" label="平"></el-table-column>
<el-table-column align="center" prop="reActiveValleyPrice" label="谷"></el-table-column>
<el-table-column align="center" prop="reActiveTotalPrice" label="总"></el-table-column>
</el-table-column>
<el-table-column label="" align="center">
<el-table-column prop="actualRevenue" label="实际收益" align="center"></el-table-column>
</el-table-column>
<el-table-column label="备注" align="center" fixed="right" min-width="260">
<template slot-scope="scope">
<div class="remark-cell">
<span class="remark-text">{{ scope.row.remark || "-" }}</span>
<el-button type="text" @click="editRemark(scope.row)">编辑</el-button>
</div>
</template>
</el-table-column>
</el-table>
<el-pagination
v-show="tableData.length > 0"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-size="pageSize"
:page-sizes="[10, 20, 30, 40]"
layout="total, sizes, prev, pager, next, jumper"
:total="totalSize"
style="margin-top: 15px; text-align: center"
>
</el-pagination>
</div>
</template>
<script>
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import { batchGetBizRemark, getAmmeterRevenueData, saveBizRemark } from "@/api/ems/dzjk";
import { formatDate } from "@/filters/ems";
const BIZ_TYPE = "stats_report";
const REPORT_KEY = "SYBB";
export default {
name: "DzjkTjbbSybb",
mixins: [getQuerySiteId],
data() {
return {
loading: false,
pickerOptions: {
disabledDate(time) {
return time.getTime() > Date.now();
},
},
defaultDateRange: [],
dateRange: [],
tableData: [],
summaryTotals: {},
pageSize: 10,
pageNum: 1,
totalSize: 0,
};
},
methods: {
buildRemarkKey(dataTime) {
return `${this.siteId}_${dataTime || ""}`;
},
loadRemarks(rows) {
if (!rows.length) return Promise.resolve({});
return batchGetBizRemark({
bizType: BIZ_TYPE,
bizKey1: REPORT_KEY,
bizKey2List: rows.map(row => this.buildRemarkKey(row.dataTime)),
}).then(response => response?.data || {});
},
applyRemarks(rows, remarkMap) {
rows.forEach(row => {
this.$set(row, "remark", remarkMap[this.buildRemarkKey(row.dataTime)] || "");
});
},
toScaledInt(value) {
const num = Number(value);
return Number.isFinite(num) ? Math.round(num * 1000) : 0;
},
sumRowsByProp(rows, prop) {
const total = (rows || []).reduce((sum, row) => sum + this.toScaledInt(row?.[prop]), 0);
return total / 1000;
},
formatSummaryNumber(value) {
const num = Number(value);
if (!Number.isFinite(num)) return "";
return num.toFixed(3).replace(/\.?0+$/, "");
},
buildSummaryTotals(rows) {
const numericProps = [
"activePeakPrice",
"activeHighPrice",
"activeFlatPrice",
"activeValleyPrice",
"activeTotalPrice",
"reActivePeakPrice",
"reActiveHighPrice",
"reActiveFlatPrice",
"reActiveValleyPrice",
"reActiveTotalPrice",
"actualRevenue",
];
return numericProps.reduce((result, prop) => {
result[prop] = this.sumRowsByProp(rows, prop);
return result;
}, {});
},
getSummaries({ columns, data }) {
return columns.map((column, index) => {
if (index === 0) return "合计";
const prop = column.property;
if (!prop) return "";
if (Object.prototype.hasOwnProperty.call(this.summaryTotals, prop)) {
return this.formatSummaryNumber(this.summaryTotals[prop]);
}
const hasNumericValue = (data || []).some(item => Number.isFinite(Number(item?.[prop])));
return hasNumericValue ? this.formatSummaryNumber(this.sumRowsByProp(data, prop)) : "";
});
},
exportTable() {
if (!this.dateRange?.length) return;
const [startTime, endTime] = this.dateRange;
this.download(
"ems/statsReport/exportAmmeterRevenueData",
{
siteId: this.siteId,
startTime,
endTime,
},
`收益报表_${startTime}-${endTime}.xlsx`
);
},
onSearch() {
this.pageNum = 1;
this.getData();
},
onReset() {
this.dateRange = this.defaultDateRange;
this.pageNum = 1;
this.getData();
},
handleSizeChange(val) {
this.pageSize = val;
this.$nextTick(() => {
this.getData();
});
},
handleCurrentChange(val) {
this.pageNum = val;
this.$nextTick(() => {
this.getData();
});
},
editRemark(row) {
this.$prompt("请输入备注", "编辑备注", {
inputValue: row.remark || "",
inputType: "textarea",
inputPlaceholder: "可输入该日报表备注",
confirmButtonText: "保存",
cancelButtonText: "取消",
})
.then(({ value }) => {
return saveBizRemark({
bizType: BIZ_TYPE,
bizKey1: REPORT_KEY,
bizKey2: this.buildRemarkKey(row.dataTime),
remark: value || "",
}).then(() => {
this.$set(row, "remark", value || "");
this.$message.success("备注保存成功");
});
})
.catch(() => {});
},
getData() {
this.loading = true;
const { siteId, pageNum, pageSize } = this;
const [startTime = "", endTime = ""] = this.dateRange || [];
getAmmeterRevenueData({ siteId, startTime, endTime, pageSize, pageNum })
.then(pageResponse => {
const rows = pageResponse?.rows || [];
const total = Number(pageResponse?.total || 0);
this.totalSize = total;
this.tableData = rows;
const tasks = [this.loadRemarks(rows)];
if (total > 0) {
tasks.push(
getAmmeterRevenueData({
siteId,
startTime,
endTime,
pageNum: 1,
pageSize: total,
})
);
}
return Promise.all(tasks);
})
.then(([remarkMap, allResponse]) => {
this.applyRemarks(this.tableData, remarkMap || {});
const allRows = allResponse?.rows || allResponse?.data || [];
this.summaryTotals = this.buildSummaryTotals(allRows);
})
.finally(() => {
this.loading = false;
});
},
init() {
this.dateRange = [];
this.tableData = [];
this.summaryTotals = {};
this.totalSize = 0;
this.pageSize = 10;
this.pageNum = 1;
const now = new Date();
const lastDay = now.getTime();
const firstDay = new Date(new Date().setDate(1)).getTime();
this.defaultDateRange = [formatDate(firstDay), formatDate(lastDay)];
this.dateRange = [formatDate(firstDay), formatDate(lastDay)];
this.getData();
},
},
};
</script>
<style scoped lang="scss">
::v-deep {
.common-table.el-table {
.el-table__header-wrapper th,
.common-table.el-table .el-table__fixed-header-wrapper th {
border-bottom: 1px solid #dfe6ec;
}
.el-table__footer-wrapper {
tbody td.el-table__cell {
color: #000;
font-weight: bolder;
}
}
}
}
.remark-cell {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.remark-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -39,12 +39,9 @@ export default {
source.push([item.dateMonth,item.chargeEnergy,item.disChargeEnergy]) source.push([item.dateMonth,item.chargeEnergy,item.disChargeEnergy])
}) })
this.chart.setOption({ this.chart.setOption({
grid: {
containLabel: true
},
legend: { legend: {
left: 'center', left: 'center',
bottom: '15', bottom: '10',
}, },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',

View File

@ -39,12 +39,9 @@ export default {
}) })
this.chart.setOption({ this.chart.setOption({
color:['#3C81FF','#FFBE29'], color:['#3C81FF','#FFBE29'],
grid: {
containLabel: true
},
legend: { legend: {
left: 'center', left: 'center',
bottom: '15', bottom: '10',
}, },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',

View File

@ -39,12 +39,9 @@ export default {
}) })
this.chart.setOption({ this.chart.setOption({
color:['#F86F70'], color:['#F86F70'],
grid: {
containLabel: true
},
legend: { legend: {
left: 'center', left: 'center',
bottom: '15', bottom: '10',
}, },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',

View File

@ -42,12 +42,9 @@ export default {
tooltip: { tooltip: {
trigger: 'item' trigger: 'item'
}, },
grid: {
containLabel: true
},
legend: { legend: {
left: 'center', left: 'center',
bottom: '15', bottom:'10'
}, },
series: [ series: [
{ {

View File

@ -39,12 +39,9 @@ export default {
}) })
this.chart.setOption({ this.chart.setOption({
color:['#FFBE01'], color:['#FFBE01'],
grid: {
containLabel: true
},
legend: { legend: {
left: 'center', left: 'center',
bottom: '15', bottom: '10',
}, },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',

View File

@ -1,25 +1,25 @@
<template> <template>
<div class="ems-dashboard-editor-container"> <div class="ems-dashboard-editor-container" v-loading="loading">
<zd-info></zd-info> <zd-info></zd-info>
<div class="ems-content-container ems-content-container-padding"> <div class="ems-content-container ems-content-container-padding">
<div class="content-title">数据概览</div> <div class="content-title">数据概览</div>
<el-row :gutter="15" style="background:#fff;margin:30px 0;"> <el-row :gutter="32" style="background:#fff;margin:30px 0;">
<el-col :xs="24" :sm="12" :lg="12" v-loading="chartLoading.dlzbchart"> <el-col :xs="24" :sm="12" :lg="12">
<dlzb-chart ref="dlzbchart"/> <dlzb-chart ref="dlzbchart"/>
</el-col> </el-col>
<el-col :xs="24" :sm="12" :lg="12" v-loading="chartLoading.xtxlchart"> <el-col :xs="24" :sm="12" :lg="12">
<xtxl-chart ref="xtxlchart"/> <xtxl-chart ref="xtxlchart"/>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="15" style="background:#fff;margin:0;"> <el-row :gutter="32" style="background:#fff;margin:0;">
<el-col :xs="24" :sm="8" :lg="8" v-loading="chartLoading.gjqsChart"> <el-col :xs="24" :sm="8" :lg="8">
<gjqs-chart ref="gjqsChart"/> <gjqs-chart ref="gjqsChart"/>
</el-col> </el-col>
<el-col :xs="24" :sm="8" :lg="8" v-loading="chartLoading.sbgjzbChart"> <el-col :xs="24" :sm="8" :lg="8">
<sbgjzb-chart ref="sbgjzbChart"/> <sbgjzb-chart ref="sbgjzbChart"/>
</el-col> </el-col>
<el-col :xs="24" :sm="8" :lg="8" v-loading="chartLoading.gjdjfbChart"> <el-col :xs="24" :sm="8" :lg="8">
<gjdjfb-chart ref="gjdjfbChart"/> <gjdjfb-chart ref="gjdjfbChart"/>
</el-col> </el-col>
</el-row> </el-row>
@ -47,45 +47,24 @@ export default {
}, },
data() { data() {
return { return {
chartLoading: { loading:false,
dlzbchart: false,
xtxlchart: false,
gjqsChart: false,
sbgjzbChart: false,
gjdjfbChart: false
}
} }
}, },
methods: { methods: {
setChartLoading(loading) {
this.chartLoading = {
dlzbchart: loading,
xtxlchart: loading,
gjqsChart: loading,
sbgjzbChart: loading,
gjdjfbChart: loading
}
},
hideChartLoading(key) {
this.$set(this.chartLoading, key, false)
}
}, },
mounted() { mounted() {
this.setChartLoading(true) this.loading = true
dataList().then(response => { dataList().then(response => {
const data = JSON.parse(JSON.stringify(response?.data || {})) const data = JSON.parse(JSON.stringify(response?.data || {}))
this.$refs.dlzbchart.initChart(data?.elecDataList || []) this.$refs.dlzbchart.initChart(data?.elecDataList || [])
this.hideChartLoading('dlzbchart')
this.$refs.xtxlchart.initChart(data?.sysEfficList || []) this.$refs.xtxlchart.initChart(data?.sysEfficList || [])
this.hideChartLoading('xtxlchart')
this.$refs.gjqsChart.initChart(data?.alarmDataList || []) this.$refs.gjqsChart.initChart(data?.alarmDataList || [])
this.hideChartLoading('gjqsChart')
this.$refs.sbgjzbChart.initChart(data?.deviceAlarmList || []) this.$refs.sbgjzbChart.initChart(data?.deviceAlarmList || [])
this.hideChartLoading('sbgjzbChart')
this.$refs.gjdjfbChart.initChart(data?.alarmLevelList || []) this.$refs.gjdjfbChart.initChart(data?.alarmLevelList || [])
this.hideChartLoading('gjdjfbChart')
}).catch(() => { }).finally(() => {
this.setChartLoading(false) this.loading = false
}) })
} }
} }

View File

@ -1,136 +0,0 @@
<template>
<div class="time-range">
<el-date-picker
v-model="dateRange"
:type="type"
range-separator=""
start-placeholder="开始时间"
:format="valueFormat"
:value-format="valueFormat"
:clearable="false"
:picker-options="pickerOptions"
end-placeholder="结束时间">
</el-date-picker>
<el-button size="mini" style="margin-left: 10px;" :loading="loading" @click="reset">重置</el-button>
<el-button type="primary" size="mini" :loading="loading" @click="search">搜索</el-button>
<el-button type="primary" size="mini" :loading="loading" @click="timeLine('before')">上一时段</el-button>
<el-button type="primary" size="mini" :loading="loading" @click="timeLine('next')" :disabled="disabledNextBtn">下一时段</el-button>
</div>
</template>
<script>
import {formatDate} from '@/filters/ems'
export default {
props:{
dataUnit:{
type:Number,
default:1
}
},
computed:{
type(){
return this.dataUnit === 3 ? 'daterange' : 'datetimerange'
},
valueFormat(){
return this.dataUnit === 3 ? 'yyyy-MM-dd' : 'yyyy-MM-dd HH:mm:ss'
},
disabledNextBtn(){
if(this.dateRange && this.dateRange.length ===2){
return new Date(this.dateRange[1]) >= new Date(this.defaultDateRange[1])
}else{
return true
}
}
},
data() {
return {
loading:false,
dateRange:[],
defaultDateRange:[],
pickerOptions:{
disabledDate(time) {
return time.getTime() > Date.now();
},
},
}
},
methods: {
init(){
const {dataUnit} = this
const timeDis= dataUnit === 3? 30 * 24 * 60 * 60 * 1000 :dataUnit === 2 ? 24 * 60 * 60 * 1000 : 60 * 60 * 1000
const now = new Date(),formatNow = formatDate(now.getTime(),dataUnit !== 3 );
const timeAgo = formatDate(new Date(now.getTime() - timeDis),dataUnit !== 3)
this.dateRange = [timeAgo, formatNow];
this.defaultDateRange=[timeAgo, formatNow];
console.log('init',timeAgo,formatNow)
this.$emit('initDate',this.dateRange || [])
},
showBtnLoading(status){
this.loading = status
},
// 切换分、时、天 重置时间可选范围
resetDate(){
this.dateRange = this.defaultDateRange
},
//重置 设置时间范围为初始化时间段
reset(){
this.resetDate()
this.$emit('updateDate',this.dateRange || [])
},
// 搜索
search(){
if(this.dateRange && this.dateRange.length>0){
const {dataUnit} = this
const [start,end] = this.dateRange
if([1,2].includes(dataUnit)){
const startTime = new Date(start),endTime=new Date(end)
const timeDis= 7 * 24 * 60 * 60 * 1000
if(endTime - startTime > timeDis){
this.$message.error(`按分钟或小时查询数据,时间范围不能超过7天`)
return
}
}
this.$emit('updateDate',this.dateRange || [])
}else{
this.$emit('updateDate',this.dateRange || [])
}
},
timeLine(type){
if(!this.dateRange || !this.dateRange[0] || !this.dateRange[1]) return
const nowStartTimes = new Date(this.dateRange[0]).getTime(),nowEndTimes = new Date(this.dateRange[1]).getTime(),maxTime = new Date(this.defaultDateRange[1]).getTime()
const nowDis = nowEndTimes - nowStartTimes//用户当前选择时间差 可能=0
//baseTime,maxTime 毫秒数
const baseDis = this.dataUnit === 3 ? 24 * 60 * 60 * 1000 :60 * 60 * 1000
const calcDis = nowDis === 0 ? baseDis : nowDis
let start = type === 'before' ? nowStartTimes - calcDis : nowStartTimes + calcDis
if(start>maxTime) start=maxTime
let end = type === 'before' ? nowEndTimes - calcDis : nowEndTimes + calcDis
if(end>maxTime) end=maxTime
this.dateRange = [formatDate(start,this.dataUnit !== 3),formatDate(end,this.dataUnit !== 3)]
this.$emit('updateDate',this.dateRange)
},
}
}
</script>
<style lang="scss" scoped>
.time-range{
display: flex;
::v-deep {
.el-range-editor--medium .el-range__icon, .el-range-editor--medium .el-range__close-icon{
line-height: 22px;
}
.el-range-editor--medium.el-input__inner{
height: 30px;
}
.el-range-editor--medium .el-range-separator{
line-height: 24px;
}
.el-button--mini{
padding:3px 10px;
}
}
}
</style>

View File

@ -1,946 +0,0 @@
<template>
<div class="ems-dashboard-editor-container" style="background-color: #ffffff">
<el-form ref="form" :model="form" label-position="top">
<div class="query-groups-toolbar">
<span class="query-groups-count">当前 {{ form.queryGroups.length }} 个点位</span>
<el-button type="primary" size="mini" plain @click="addQueryGroup">新增点位</el-button>
</div>
<div class="query-groups-row">
<div
v-for="(group, index) in form.queryGroups"
:key="group.key"
class="query-group"
>
<el-form-item :label="`点位 ${index + 1}`" class="group-point-item">
<div class="point-select-wrapper">
<el-select
v-model="group.pointId"
filterable
remote
clearable
reserve-keyword
:disabled="!canSelectPoint(group)"
:placeholder="pointSelectPlaceholder(group)"
:remote-method="(query) => remotePointSearch(index, query)"
:loading="group.pointLoading"
:no-data-text="pointNoDataText(group)"
class="point-select"
@change="(value) => handlePointChange(index, value)"
@visible-change="(visible) => handlePointDropdownVisible(index, visible)"
>
<el-option
v-for="item in group.pointOptions"
:key="`${group.key}-${item.value}`"
:label="item.label"
:value="item.value"
/>
</el-select>
<div class="point-select-toolbar">
<span class="point-select-tip">{{ group.pointId ? "已选择点位" : "未选择点位" }}</span>
<div>
<el-button
type="text"
size="mini"
:disabled="!canSelectPoint(group)"
@click="refreshPointOptions(index)"
>
刷新点位
</el-button>
<el-button
type="text"
size="mini"
:disabled="!group.pointId"
@click="clearPointSelection(index)"
>
清空选择
</el-button>
<el-button
type="text"
size="mini"
:disabled="form.queryGroups.length <= 1"
@click="removeQueryGroup(index)"
>
删除点位
</el-button>
</div>
</div>
</div>
</el-form-item>
</div>
</div>
<el-form-item>
<el-button type="primary" @click="submitForm">生成图表</el-button>
<el-button style="margin-left: 8px" @click="handleExportData">导出数据</el-button>
</el-form-item>
</el-form>
<el-card
shadow="always"
class="common-card-container common-card-container-body-no-padding time-range-card"
>
<div slot="header" class="time-range-header">
<span class="card-title">
<el-radio-group v-model="form.dataUnit">
<el-radio :label="1">分钟</el-radio>
<el-radio :label="2">小时</el-radio>
<el-radio :label="3">天</el-radio>
</el-radio-group>
</span>
<date-time-select
ref="dateTimeSelect"
:data-unit="form.dataUnit"
@initDate="(e) => (form.dataRange = e || [])"
@updateDate="updateDate"
/>
</div>
<div style="height: 350px" id="searchChart"></div>
</el-card>
</div>
</template>
<script>
import * as echarts from "echarts";
import resize from "@/mixins/ems/resize";
import {getPointValueList, pointFuzzyQuery} from "@/api/ems/search";
import DateTimeSelect from "./DateTimeSelect.vue";
export default {
name: "Search",
mixins: [resize],
components: {DateTimeSelect},
watch: {
"$route.query.siteId": {
handler(newVal) {
this.syncQuerySiteIds(newVal);
},
},
"form.dataUnit": {
handler() {
this.$nextTick(() => {
this.$refs.dateTimeSelect.init();
this.getDate();
});
},
},
},
data() {
return {
chart: null,
form: {
dataRange: [],
siteIds: [],
queryGroups: [],
dataUnit: 1,
},
queryGroupSeed: 0,
lastQueryResult: [],
sitePointOptionsCache: {},
sitePointRequestId: 0,
};
},
created() {
this.form.queryGroups = [];
for (let i = 0; i < 5; i += 1) {
this.addQueryGroup();
}
},
methods: {
getChartColor(index = 0) {
const palette = [
"#5470C6",
"#91CC75",
"#FAC858",
"#EE6666",
"#73C0DE",
"#3BA272",
"#FC8452",
"#9A60B4",
"#EA7CCC",
];
return palette[index % palette.length];
},
getSelectedPointAxisKeys() {
const keys = [];
const seen = new Set();
(this.form.queryGroups || []).forEach((group) => {
const key = String(this.resolveSelectedPointName(group) || group?.pointId || "").trim();
if (!key || seen.has(key)) return;
seen.add(key);
keys.push(key);
});
return keys;
},
buildDynamicYAxisConfig(data = []) {
const axisIndexMap = {};
const yAxis = [];
const orderedAxisKeys = [];
const seenAxisKeys = new Set();
const pushAxisKey = (key) => {
const normalized = String(key || "").trim();
if (!normalized || seenAxisKeys.has(normalized)) return;
seenAxisKeys.add(normalized);
orderedAxisKeys.push(normalized);
};
this.getSelectedPointAxisKeys().forEach((key) => pushAxisKey(key));
data.forEach((item) => {
const pointId = String(item?.pointId || "").trim();
const pointName = String(item?.pointName || "").trim();
pushAxisKey(pointName || pointId);
(item?.deviceList || []).forEach((inner) => {
pushAxisKey(inner?.deviceId);
});
});
orderedAxisKeys.forEach((pointKey) => {
const axisIndex = yAxis.length;
const sideIndex = Math.floor(axisIndex / 2);
const position = axisIndex % 2 === 0 ? "left" : "right";
const color = this.getChartColor(axisIndex);
axisIndexMap[pointKey] = axisIndex;
yAxis.push({
type: "value",
name: pointKey || `点位${axisIndex + 1}`,
position,
offset: sideIndex * 55,
alignTicks: true,
axisLine: {
show: true,
lineStyle: {color},
},
axisLabel: {
color,
},
nameTextStyle: {
color,
},
splitLine: {
show: axisIndex === 0,
},
});
});
if (yAxis.length === 0) {
yAxis.push({type: "value"});
}
const axisCount = yAxis.length;
const leftCount = Math.ceil(axisCount / 2);
const rightCount = Math.floor(axisCount / 2);
return {
yAxis,
axisIndexMap,
grid: {
containLabel: true,
left: 40 + Math.max(0, leftCount - 1) * 55,
right: 40 + Math.max(0, rightCount - 1) * 55,
},
};
},
createEmptyQueryGroup(key) {
return {
key,
pointId: "",
selectedPointName: "",
pointOptions: [],
pointOptionsCache: {},
pointRequestId: 0,
pointLoading: false,
};
},
addQueryGroup() {
this.queryGroupSeed += 1;
this.form.queryGroups.push(this.createEmptyQueryGroup(this.queryGroupSeed));
},
removeQueryGroup(groupIndex) {
if (this.form.queryGroups.length <= 1) {
this.$message.warning("至少保留1个点位");
return;
}
this.form.queryGroups.splice(groupIndex, 1);
},
getQueryGroup(index) {
return this.form.queryGroups[index];
},
canSelectPoint(group) {
return !!(group && this.form.siteIds && this.form.siteIds.length > 0);
},
pointSelectPlaceholder(group) {
return this.canSelectPoint(group)
? "支持关键字搜索展开可查看点位列表"
: "暂无可用站点点位";
},
pointNoDataText(group) {
return this.canSelectPoint(group) ? "暂无匹配点位" : "暂无可用站点点位";
},
showLoading() {
this.chart && this.chart.showLoading();
},
hideLoading() {
this.chart && this.chart.hideLoading();
},
initChart() {
this.chart = echarts.init(document.querySelector("#searchChart"));
},
updateDate(val) {
this.form.dataRange = val || [];
this.getDate();
},
setOption(data) {
if (!this.chart) return;
this.chart.clear();
if (!data || data.length <= 0) {
this.$message.warning("暂无数据");
return;
}
if (data[0].chartType === 2) {
this.setBoxOption(data);
} else {
this.setLineOption(data);
}
},
setLineOption(data) {
let dataset = [];
const axisConfig = this.buildDynamicYAxisConfig(data);
data.forEach((item) => {
const pointId = String(item?.pointId || "").trim();
const pointName = String(item?.pointName || "").trim();
const pointKey = pointId || pointName;
item.deviceList.forEach((inner) => {
const seriesKey = String(inner?.deviceId || "").trim() || pointKey;
const yAxisIndex = axisConfig.axisIndexMap[seriesKey] ?? 0;
const axisColor = this.getChartColor(yAxisIndex);
dataset.push({
name: inner.deviceId,
type: "line",
yAxisIndex,
itemStyle: {
color: axisColor,
},
lineStyle: {
color: axisColor,
},
markPoint: {
symbolSize: 30,
emphasis: {
disabled: false,
},
data: [
{
name: "最大值",
coord: [inner.maxDate, inner.maxValue],
relativeTo: "coordinate",
label: {
position: "top",
formatter: item.dataType === 2
? [
`最大值:${inner.maxValue}`,
`差值:${inner.diffValue}`,
].join("\n")
: [`最大值:${inner.maxValue}`].join("\n"),
},
},
{
name: "最小值",
coord: [inner.minDate, inner.minValue],
relativeTo: "coordinate",
label: {
position: "top",
formatter: item.dataType === 2
? [
`最小值:${inner.minValue}`,
`差值:${inner.diffValue}`,
].join("\n")
: [`最小值:${inner.minValue}`].join("\n"),
},
},
],
},
xdata: [],
data: [],
});
const length = dataset.length;
inner.pointValueList.forEach((value) => {
dataset[length - 1].xdata.push(value.valueDate);
dataset[length - 1].data.push(value.pointValue);
});
});
});
this.chart.setOption({
legend: {},
grid: axisConfig.grid,
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {type: "category", data: dataset?.[0]?.xdata || []},
yAxis: axisConfig.yAxis,
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: dataset,
});
},
setBoxOption(data) {
let dataset = [];
const axisConfig = this.buildDynamicYAxisConfig(data);
data.forEach((item) => {
const pointId = String(item?.pointId || "").trim();
const pointName = String(item?.pointName || "").trim();
const pointKey = pointId || pointName;
item.deviceList.forEach((inner) => {
const seriesKey = String(inner?.deviceId || "").trim() || pointKey;
const yAxisIndex = axisConfig.axisIndexMap[seriesKey] ?? 0;
const axisColor = this.getChartColor(yAxisIndex);
dataset.push({
name: inner.deviceId,
type: "boxplot",
yAxisIndex,
itemStyle: {
color: axisColor,
},
xdata: [],
data: [],
});
const length = dataset.length;
inner.pointValueList.forEach((value) => {
const {valueDate, min, q1, median, q3, max} = value;
dataset[length - 1].xdata.push(valueDate);
dataset[length - 1].data.push([min, q1, median, q3, max]);
});
});
});
this.chart.setOption({
legend: {},
grid: axisConfig.grid,
tooltip: {
trigger: "item",
formatter: function (params) {
let itemData = params.data;
let result = params.marker + params.name + " " + params.seriesName + "<br/>";
result += "最小值: " + itemData[1] + "<br/>";
result += "平均值: " + itemData[3] + "<br/>";
result += "最大值: " + itemData[5];
return result;
},
},
textStyle: {
color: "#333333",
},
xAxis: {type: "category", data: dataset?.[0]?.xdata || []},
yAxis: axisConfig.yAxis,
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: dataset,
});
},
submitForm() {
this.getDate();
},
buildQueryPayload() {
const activeGroups = this.form.queryGroups
.map((group) => ({group}))
.filter(({group}) => !!group.pointId);
if (activeGroups.length === 0) {
this.$message.error("请至少选择1组点位");
return null;
}
if (!this.form.siteIds || this.form.siteIds.length === 0) {
this.$message.error("请先在顶部选择站点");
return null;
}
const {
siteIds,
dataUnit,
dataRange: [start = "", end = ""],
} = this.form;
if (!start || !end) {
this.$message.error("请选择时间");
return null;
}
let startDate = start;
let endDate = end;
if (start && dataUnit === 3) {
startDate = `${start} 00:00:00`;
}
if (end && dataUnit === 3) {
endDate = `${end} 00:00:00`;
}
const selectedPoints = [];
const pointIdSet = new Set();
activeGroups.forEach(({group}) => {
const pointId = String(group.pointId || "").trim();
if (!pointId || pointIdSet.has(pointId)) return;
pointIdSet.add(pointId);
selectedPoints.push({
pointId,
pointName: this.resolveSelectedPointName(group) || pointId,
});
});
if (selectedPoints.length === 0) {
this.$message.error("请至少选择1组点位");
return null;
}
const pointIds = selectedPoints.map((item) => item.pointId);
const pointNames = selectedPoints.map((item) => item.pointName);
return {
siteIds,
dataUnit,
pointIds,
pointNames,
pointId: pointIds.join(","),
startDate,
endDate,
};
},
requestPointData(payload) {
return getPointValueList(payload).then((response) => response?.data || []);
},
resolveExportPointValue(value = {}) {
if (value?.pointValue !== undefined && value?.pointValue !== null && value?.pointValue !== "") {
return value.pointValue;
}
if (value?.median !== undefined && value?.median !== null && value?.median !== "") {
return value.median;
}
return "";
},
normalizeExportRows(data = [], payload = {}) {
const pointIds = payload?.pointIds || [];
const pointNames = payload?.pointNames || [];
const selectedPoints = pointIds
.map((pointId, index) => {
const normalizedId = String(pointId || "").trim();
if (!normalizedId) return null;
return {
pointId: normalizedId,
pointName: String(pointNames[index] || normalizedId).trim(),
pointOrder: index,
};
})
.filter((item) => !!item);
const selectedPointById = {};
const selectedPointByName = {};
selectedPoints.forEach((point) => {
selectedPointById[point.pointId] = point;
if (point.pointName) {
selectedPointByName[point.pointName] = point;
}
});
const groups = [];
const groupMap = {};
const rowMap = {};
const rowDates = [];
data.forEach((item, itemIndex) => {
const itemPointId = String(item?.pointId || "").trim();
const itemPointName = String(item?.pointName || "").trim();
const pointByItemId = itemPointId ? selectedPointById[itemPointId] : null;
const pointByItemName = itemPointName ? selectedPointByName[itemPointName] : null;
(item?.deviceList || []).forEach((device) => {
const deviceId = String(device?.deviceId || "").trim();
const pointByDeviceId = deviceId ? selectedPointById[deviceId] : null;
const pointByDeviceName = deviceId ? selectedPointByName[deviceId] : null;
const pointByOrder = selectedPoints[itemIndex] || null;
const matchedPoint = pointByItemId || pointByItemName || pointByDeviceId || pointByDeviceName || pointByOrder || null;
const pointId = itemPointId || matchedPoint?.pointId || "";
const pointName = itemPointName || matchedPoint?.pointName || pointId;
const pointOrder = matchedPoint?.pointOrder ?? Number.MAX_SAFE_INTEGER;
const pointKey = pointId || pointName || `point_${itemIndex + 1}`;
const deviceKey = deviceId || "unknown";
const groupKey = `${pointKey}__${deviceKey}`;
if (!groupMap[groupKey]) {
groupMap[groupKey] = true;
groups.push({
groupKey,
pointId,
pointName,
deviceId,
pointOrder,
});
}
(device?.pointValueList || []).forEach((value) => {
const valueDate = String(value?.valueDate || "").trim();
if (!valueDate) return;
if (!rowMap[valueDate]) {
rowMap[valueDate] = {valueDate};
rowDates.push(valueDate);
}
rowMap[valueDate][groupKey] = this.resolveExportPointValue(value);
});
});
});
groups.sort((a, b) => {
const ao = a.pointOrder === -1 ? Number.MAX_SAFE_INTEGER : a.pointOrder;
const bo = b.pointOrder === -1 ? Number.MAX_SAFE_INTEGER : b.pointOrder;
if (ao !== bo) return ao - bo;
if (a.pointId !== b.pointId) return a.pointId.localeCompare(b.pointId);
return a.deviceId.localeCompare(b.deviceId);
});
return {
groups,
rows: rowDates.sort().map((valueDate) => rowMap[valueDate]),
};
},
csvCell(value) {
const text = value === undefined || value === null ? "" : String(value);
return `"${text.replace(/"/g, '""')}"`;
},
exportRowsToCsv({groups = [], rows = []} = {}) {
const headers = ["时间"];
groups.forEach((_, index) => {
const order = index + 1;
headers.push(`点位ID${order}`, `点位名称${order}`, `设备ID${order}`, `点位值${order}`);
});
const lines = [headers.map((item) => this.csvCell(item)).join(",")];
rows.forEach((row) => {
const line = [row.valueDate || ""];
groups.forEach((group) => {
line.push(group.pointId || "", group.pointName || "", group.deviceId || "", row[group.groupKey] ?? "");
});
lines.push(line.map((item) => this.csvCell(item)).join(","));
});
const csv = `\uFEFF${lines.join("\n")}`;
this.$download.saveAs(new Blob([csv], {type: "text/csv;charset=utf-8;"}), `综合查询数据_${new Date().getTime()}.csv`);
},
handleExportData() {
this.$refs.form.validate((valid) => {
if (!valid) return;
const payload = this.buildQueryPayload();
if (!payload) return;
this.requestPointData(payload).then((data) => {
if (!data || data.length === 0) {
this.$message.warning("暂无可导出数据");
return;
}
const exportData = this.normalizeExportRows(data, payload);
if (!exportData?.rows?.length || !exportData?.groups?.length) {
this.$message.warning("暂无可导出数据");
return;
}
this.exportRowsToCsv(exportData);
this.$message.success("导出成功");
}).catch((error) => {
if (error?.code === "ECONNABORTED") {
this.$message.error("查询超时请缩短时间范围后重试");
return;
}
this.$message.error("导出失败请稍后重试");
});
});
},
getPointCacheKey(query = "") {
return `${this.form.siteIds.join(",")}_${query.trim()}`;
},
getSitePointCacheKey() {
return this.form.siteIds.join(",");
},
formatPointLabel({pointId = "", pointName = "", dataKey = ""} = {}) {
return `${pointId || "-"}-${pointName || "-"}(${dataKey || "-"})`;
},
normalizePointOptions(data = []) {
return (data || []).map((item) => {
if (typeof item === "string") {
return {value: item, label: this.formatPointLabel({pointName: item}), pointId: "", pointName: item, dataKey: "", pointDesc: ""};
}
const pointId = item?.pointId || "";
const pointName = item?.pointName || item?.value || "";
const dataKey = item?.dataKey || "";
const pointDesc = item?.pointDesc || "";
const label = this.formatPointLabel({pointId, pointName, dataKey});
return {
value: pointId || pointName,
label,
pointId,
pointName,
dataKey,
pointDesc,
};
}).filter((item) => item.value);
},
fuzzyFilterPointOptions(options = [], query = "") {
const keyword = String(query || "").trim().toLowerCase();
if (!keyword) return options;
return (options || []).filter((item) => {
const marker = `${item?.pointId || ""} ${item?.pointName || ""} ${item?.dataKey || ""} ${item?.pointDesc || ""}`.toLowerCase();
return marker.includes(keyword);
});
},
setPointOptions(group, data = []) {
const normalized = this.normalizePointOptions(data);
const selected = group.pointId
? (group.pointOptions || []).find((item) => item?.value === group.pointId)
|| {
value: group.pointId,
label: this.formatPointLabel({pointId: group.pointId}),
pointId: group.pointId,
pointName: group.selectedPointName || "",
dataKey: "",
pointDesc: "",
}
: null;
const nextOptions = selected ? [...normalized, selected] : normalized;
const seen = {};
group.pointOptions = nextOptions.filter((item) => {
if (!item?.value || seen[item.value]) return false;
seen[item.value] = true;
return true;
});
},
applyPointOptionsToGroups(pointOptions = []) {
(this.form.queryGroups || []).forEach((group) => {
if (!group) return;
const baseCacheKey = this.getPointCacheKey("");
group.pointOptionsCache[baseCacheKey] = pointOptions;
this.setPointOptions(group, pointOptions);
});
},
loadSitePointOptions({force = false} = {}) {
const siteCacheKey = this.getSitePointCacheKey();
if (!siteCacheKey) {
return Promise.resolve([]);
}
if (!force && this.sitePointOptionsCache[siteCacheKey]) {
const cached = this.sitePointOptionsCache[siteCacheKey];
this.applyPointOptionsToGroups(cached);
return Promise.resolve(cached);
}
const requestId = ++this.sitePointRequestId;
(this.form.queryGroups || []).forEach((group) => {
group.pointLoading = true;
});
return pointFuzzyQuery({
siteIds: this.form.siteIds,
pointName: "",
})
.then((response) => {
if (requestId !== this.sitePointRequestId) return [];
const data = this.normalizePointOptions(response?.data || []);
this.sitePointOptionsCache[siteCacheKey] = data;
this.applyPointOptionsToGroups(data);
return data;
})
.finally(() => {
if (requestId !== this.sitePointRequestId) return;
(this.form.queryGroups || []).forEach((group) => {
group.pointLoading = false;
});
});
},
remotePointSearch(groupIndex, query) {
const group = this.getQueryGroup(groupIndex);
if (!group || !this.canSelectPoint(group)) return;
const baseCacheKey = this.getPointCacheKey("");
const baseOptions = group.pointOptionsCache?.[baseCacheKey] || group.pointOptions || [];
const localFiltered = this.fuzzyFilterPointOptions(baseOptions, query);
this.setPointOptions(group, localFiltered);
},
handlePointDropdownVisible(groupIndex, visible) {
const group = this.getQueryGroup(groupIndex);
if (visible && group && this.canSelectPoint(group)) {
this.loadSitePointOptions();
}
},
refreshPointOptions(groupIndex) {
const group = this.getQueryGroup(groupIndex);
if (!group || !this.canSelectPoint(group)) return;
this.loadSitePointOptions({force: true});
},
clearPointSelection(groupIndex) {
const group = this.getQueryGroup(groupIndex);
if (!group) return;
group.pointId = "";
group.selectedPointName = "";
},
resolveSelectedPointName(group) {
if (!group) return "";
if (group.selectedPointName) return String(group.selectedPointName).trim();
const selectedOption = (group.pointOptions || []).find((item) => item?.value === group.pointId);
if (selectedOption?.pointName) {
return String(selectedOption.pointName).trim();
}
return "";
},
handlePointChange(groupIndex, value) {
const group = this.getQueryGroup(groupIndex);
if (!group) return;
if (!value) {
group.selectedPointName = "";
return;
}
const selectedOption = (group.pointOptions || []).find((item) => item?.value === value);
group.selectedPointName = String(selectedOption?.pointName || "").trim();
},
syncQuerySiteIds(routeSiteId) {
const siteId = routeSiteId || this.$route?.query?.siteId;
const normalizedSiteId = siteId === undefined || siteId === null ? "" : String(siteId).trim();
const prevSiteIds = (this.form.siteIds || []).join(",");
this.form.siteIds = normalizedSiteId ? [normalizedSiteId] : [];
const nextSiteIds = (this.form.siteIds || []).join(",");
if (prevSiteIds !== nextSiteIds) {
(this.form.queryGroups || []).forEach((group) => {
group.pointOptions = [];
group.pointOptionsCache = {};
group.pointLoading = false;
});
}
if (nextSiteIds) {
this.loadSitePointOptions();
}
},
getDate() {
this.$refs.form.validate((valid) => {
if (!valid) {
return;
}
const payload = this.buildQueryPayload();
if (!payload) return;
this.requestPointData(payload).then((data) => {
this.lastQueryResult = data || [];
this.setOption(this.lastQueryResult);
}).catch((error) => {
if (error?.code === "ECONNABORTED") {
this.$message.error("查询超时请缩短时间范围后重试");
return;
}
this.$message.error("查询失败请稍后重试");
});
});
},
},
beforeDestroy() {
if (!this.chart) {
return;
}
this.chart.dispose();
this.chart = null;
},
mounted() {
this.$nextTick(() => {
this.initChart();
this.$refs.dateTimeSelect.init();
this.syncQuerySiteIds();
});
},
};
</script>
<style lang="scss" scoped>
.query-groups-row {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
gap: 8px;
}
.query-group {
flex: 0 0 calc(20% - 8px);
max-width: calc(20% - 8px);
}
.group-point-item {
margin-right: 0;
}
.query-groups-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
gap: 10px;
}
.query-groups-count {
color: #909399;
font-size: 12px;
}
.point-select-wrapper {
width: 100%;
}
.point-select {
width: 100%;
}
.point-select-toolbar {
margin-top: 6px;
display: flex;
align-items: center;
justify-content: space-between;
}
.point-select-tip {
font-size: 12px;
color: #909399;
}
@media (max-width: 1600px) {
.query-group {
flex-basis: calc(33.3% - 8px);
max-width: calc(33.3% - 8px);
}
}
@media (max-width: 1200px) {
.query-group {
flex-basis: calc(50% - 8px);
max-width: calc(50% - 8px);
}
}
@media (max-width: 768px) {
.query-groups-row {
gap: 0;
}
.query-group {
flex-basis: 100%;
max-width: 100%;
}
}
</style>

View File

@ -1,183 +0,0 @@
<template>
<el-dialog
:visible.sync="dialogVisible"
:close-on-press-escape="false"
:close-on-click-modal="false"
:show-close="false"
destroy-on-close
lock-scroll
append-to-body
width="760px"
class="ems-dialog"
:title="mode === 'add' ? '新增充放电收益修正' : '编辑充放电收益修正'"
>
<el-form
ref="formRef"
v-loading="loading"
:model="formData"
:rules="rules"
label-width="120px"
size="medium"
class="form-grid"
>
<el-form-item label="站点" prop="siteId">
<el-input v-model="formData.siteId" placeholder="请先在顶部选择站点" disabled />
</el-form-item>
<el-form-item label="数据日期" prop="dateTime">
<el-date-picker
v-model="formData.dateTime"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择数据日期"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="总充电量" prop="totalChargeData">
<el-input-number v-model="formData.totalChargeData" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
</el-form-item>
<el-form-item label="总放电量" prop="totalDischargeData">
<el-input-number v-model="formData.totalDischargeData" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
</el-form-item>
<el-form-item label="当日充电量" prop="chargeData">
<el-input-number v-model="formData.chargeData" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
</el-form-item>
<el-form-item label="当日放电量" prop="dischargeData">
<el-input-number v-model="formData.dischargeData" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
</el-form-item>
<el-form-item label="累计收益" prop="totalRevenue">
<el-input-number v-model="formData.totalRevenue" :controls="false" :min="-999999999" :max="999999999" :step="0.0001" :precision="4" style="width: 100%" />
</el-form-item>
<el-form-item label="当日收益" prop="dayRevenue">
<el-input-number v-model="formData.dayRevenue" :controls="false" :min="-999999999" :max="999999999" :step="0.0001" :precision="4" style="width: 100%" />
</el-form-item>
<el-form-item label="备注" prop="remark" class="full-row">
<el-input v-model="formData.remark" type="textarea" :rows="3" maxlength="300" show-word-limit />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" :loading="saving" @click="saveDialog">确定</el-button>
</div>
</el-dialog>
</template>
<script>
import {
addDailyChargeData,
getDailyChargeDataDetail,
updateDailyChargeData,
} from '@/api/ems/site'
const buildEmptyForm = () => ({
id: '',
siteId: '',
dateTime: '',
totalChargeData: null,
totalDischargeData: null,
chargeData: null,
dischargeData: null,
totalRevenue: null,
dayRevenue: null,
remark: '',
})
export default {
name: 'AddChargeDataCorrection',
data() {
return {
dialogVisible: false,
mode: 'add',
loading: false,
saving: false,
formData: buildEmptyForm(),
rules: {
siteId: [{ required: true, message: '请先在顶部选择站点', trigger: 'blur' }],
dateTime: [{ required: true, message: '请选择数据日期', trigger: 'change' }],
},
}
},
methods: {
getRouteSiteId() {
const siteId = this.$route?.query?.siteId
return siteId === undefined || siteId === null ? '' : String(siteId).trim()
},
showDialog(id, siteId = '') {
this.dialogVisible = true
if (id) {
this.mode = 'edit'
this.formData.id = id
this.fetchDetail(id)
} else {
this.mode = 'add'
this.formData = buildEmptyForm()
this.formData.siteId = siteId || this.getRouteSiteId()
}
},
fetchDetail(id) {
this.loading = true
getDailyChargeDataDetail(id)
.then((res) => {
this.formData = Object.assign(buildEmptyForm(), res?.data || {})
})
.finally(() => {
this.loading = false
})
},
saveDialog() {
this.$refs.formRef.validate((valid) => {
if (!valid) return
this.saving = true
const request = this.mode === 'add' ? addDailyChargeData : updateDailyChargeData
const payload = {
id: this.formData.id,
siteId: this.formData.siteId,
dateTime: this.formData.dateTime,
totalChargeData: this.formData.totalChargeData,
totalDischargeData: this.formData.totalDischargeData,
chargeData: this.formData.chargeData,
dischargeData: this.formData.dischargeData,
totalRevenue: this.formData.totalRevenue,
dayRevenue: this.formData.dayRevenue,
remark: this.formData.remark,
}
request(payload)
.then((res) => {
if (res?.code === 200) {
this.$emit('update')
this.closeDialog()
}
})
.finally(() => {
this.saving = false
})
})
},
closeDialog() {
this.dialogVisible = false
this.formData = buildEmptyForm()
this.$nextTick(() => {
this.$refs.formRef && this.$refs.formRef.resetFields()
})
},
},
}
</script>
<style lang="scss" scoped>
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
column-gap: 16px;
.full-row {
grid-column: 1 / span 2;
}
}
</style>

View File

@ -1,218 +0,0 @@
<template>
<el-dialog
:visible.sync="dialogVisible"
:close-on-press-escape="false"
:close-on-click-modal="false"
:show-close="false"
destroy-on-close
lock-scroll
append-to-body
width="760px"
class="ems-dialog"
:title="mode === 'add' ? '新增数据修正' : '编辑数据修正'"
>
<el-form
ref="formRef"
v-loading="loading"
:model="formData"
:rules="rules"
label-width="120px"
size="medium"
class="form-grid"
>
<el-form-item label="站点" prop="siteId">
<el-input v-model="formData.siteId" placeholder="请先在顶部选择站点" disabled />
</el-form-item>
<el-form-item label="数据日期" prop="dataDate">
<el-date-picker
v-model="formData.dataDate"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择数据日期"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="数据小时" prop="dataHour">
<el-input-number
v-model="formData.dataHour"
:controls="false"
:min="0"
:max="23"
:step="1"
:precision="0"
placeholder="0-23"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="尖充电差值" prop="peakChargeDiff">
<el-input-number v-model="formData.peakChargeDiff" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
</el-form-item>
<el-form-item label="尖放电差值" prop="peakDischargeDiff">
<el-input-number v-model="formData.peakDischargeDiff" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
</el-form-item>
<el-form-item label="峰充电差值" prop="highChargeDiff">
<el-input-number v-model="formData.highChargeDiff" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
</el-form-item>
<el-form-item label="峰放电差值" prop="highDischargeDiff">
<el-input-number v-model="formData.highDischargeDiff" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
</el-form-item>
<el-form-item label="平充电差值" prop="flatChargeDiff">
<el-input-number v-model="formData.flatChargeDiff" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
</el-form-item>
<el-form-item label="平放电差值" prop="flatDischargeDiff">
<el-input-number v-model="formData.flatDischargeDiff" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
</el-form-item>
<el-form-item label="谷充电差值" prop="valleyChargeDiff">
<el-input-number v-model="formData.valleyChargeDiff" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
</el-form-item>
<el-form-item label="谷放电差值" prop="valleyDischargeDiff">
<el-input-number v-model="formData.valleyDischargeDiff" :controls="false" :min="-999999999" :max="999999999" :step="0.001" :precision="3" style="width: 100%" />
</el-form-item>
<el-form-item label="计算时间" prop="calcTime" class="full-row">
<el-date-picker
v-model="formData.calcTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择计算时间"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="备注" prop="remark" class="full-row">
<el-input v-model="formData.remark" type="textarea" :rows="3" maxlength="300" show-word-limit />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" :loading="saving" @click="saveDialog">确定</el-button>
</div>
</el-dialog>
</template>
<script>
import {
addDailyEnergyData,
getDailyEnergyDataDetail,
updateDailyEnergyData,
} from '@/api/ems/site'
const buildEmptyForm = () => ({
id: '',
siteId: '',
dataDate: '',
dataHour: null,
peakChargeDiff: null,
peakDischargeDiff: null,
highChargeDiff: null,
highDischargeDiff: null,
flatChargeDiff: null,
flatDischargeDiff: null,
valleyChargeDiff: null,
valleyDischargeDiff: null,
calcTime: '',
remark: '',
})
export default {
name: 'AddDataCorrection',
data() {
return {
dialogVisible: false,
mode: 'add',
loading: false,
saving: false,
formData: buildEmptyForm(),
rules: {
siteId: [{ required: true, message: '请先在顶部选择站点', trigger: 'blur' }],
dataDate: [{ required: true, message: '请选择数据日期', trigger: 'change' }],
dataHour: [
{
validator: (rule, value, callback) => {
if (value === '' || value === null || value === undefined) {
callback()
return
}
if (Number.isInteger(value) && value >= 0 && value <= 23) {
callback()
return
}
callback(new Error('数据小时需为 0-23 的整数'))
},
trigger: 'change',
},
],
},
}
},
methods: {
getRouteSiteId() {
const siteId = this.$route?.query?.siteId
return siteId === undefined || siteId === null ? '' : String(siteId).trim()
},
showDialog(id, siteId = '') {
this.dialogVisible = true
if (id) {
this.mode = 'edit'
this.formData.id = id
this.fetchDetail(id)
} else {
this.mode = 'add'
this.formData = buildEmptyForm()
this.formData.siteId = siteId || this.getRouteSiteId()
}
},
fetchDetail(id) {
this.loading = true
getDailyEnergyDataDetail(id)
.then((res) => {
this.formData = Object.assign(buildEmptyForm(), res?.data || {})
})
.finally(() => {
this.loading = false
})
},
saveDialog() {
this.$refs.formRef.validate((valid) => {
if (!valid) return
this.saving = true
const request = this.mode === 'add' ? addDailyEnergyData : updateDailyEnergyData
request(this.formData)
.then((res) => {
if (res?.code === 200) {
this.$emit('update')
this.closeDialog()
}
})
.finally(() => {
this.saving = false
})
})
},
closeDialog() {
this.dialogVisible = false
this.formData = buildEmptyForm()
this.$nextTick(() => {
this.$refs.formRef && this.$refs.formRef.resetFields()
})
},
},
}
</script>
<style lang="scss" scoped>
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
column-gap: 16px;
.full-row {
grid-column: 1 / span 2;
}
}
</style>

View File

@ -1,187 +0,0 @@
<template>
<div style="background-color: #ffffff" v-loading="loading">
<el-form :inline="true" class="select-container" @submit.native.prevent>
<el-form-item label="数据日期">
<el-date-picker
v-model="form.dateTime"
type="date"
value-format="yyyy-MM-dd"
clearable
placeholder="请选择日期"
style="width: 180px"
/>
</el-form-item>
<el-form-item>
<el-button native-type="button" type="primary" @click="onSearch">搜索</el-button>
<el-button native-type="button" @click="onReset">重置</el-button>
</el-form-item>
</el-form>
<el-button type="primary" @click="openDialog('')">新增</el-button>
<el-table
:data="tableData"
class="common-table"
max-height="620px"
stripe
style="width: 100%; margin-top: 20px"
>
<el-table-column label="站点" prop="siteId" min-width="120" />
<el-table-column label="数据日期" prop="dateTime" width="120" />
<el-table-column label="总充电量" prop="totalChargeData" min-width="110" />
<el-table-column label="总放电量" prop="totalDischargeData" min-width="110" />
<el-table-column label="当日充电量" prop="chargeData" min-width="110" />
<el-table-column label="当日放电量" prop="dischargeData" min-width="110" />
<el-table-column label="累计收益" prop="totalRevenue" min-width="110" />
<el-table-column label="当日收益" prop="dayRevenue" min-width="110" />
<el-table-column label="备注" prop="remark" min-width="180" show-overflow-tooltip />
<el-table-column fixed="right" label="操作" width="150">
<template slot-scope="scope">
<el-button size="mini" type="warning" @click="openDialog(scope.row.id)">编辑</el-button>
<el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-show="tableData.length > 0"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-size="pageSize"
:page-sizes="[10, 20, 30, 40]"
layout="total, sizes, prev, pager, next, jumper"
:total="totalSize"
style="margin-top: 15px; text-align: center"
/>
<add-charge-data-correction ref="addChargeDataCorrection" @update="getData" />
</div>
</template>
<script>
import {
deleteDailyChargeData,
getDailyChargeDataList,
} from '@/api/ems/site'
import AddChargeDataCorrection from './AddChargeDataCorrection.vue'
export default {
name: 'DailyChargeDataTab',
components: { AddChargeDataCorrection },
data() {
return {
loading: false,
form: {
siteId: '',
dateTime: '',
},
tableData: [],
pageSize: 10,
pageNum: 1,
totalSize: 0,
}
},
watch: {
'$route.query.siteId'(newSiteId) {
const normalizedSiteId = this.normalizeSiteId(newSiteId)
if (normalizedSiteId === this.form.siteId) {
return
}
this.form.siteId = normalizedSiteId
this.onSearch()
},
},
mounted() {
this.form.siteId = this.normalizeSiteId(this.$route.query.siteId)
this.getData()
},
methods: {
normalizeSiteId(siteId) {
return siteId === undefined || siteId === null ? '' : String(siteId).trim()
},
handleSizeChange(val) {
this.pageSize = val
this.pageNum = 1
this.getData()
},
handleCurrentChange(val) {
this.pageNum = val
this.getData()
},
onSearch() {
this.pageNum = 1
this.getData()
},
onReset() {
this.form = {
siteId: this.form.siteId,
dateTime: '',
}
this.pageNum = 1
this.getData()
},
getData() {
if (!this.form.siteId) {
this.tableData = []
this.totalSize = 0
return
}
this.loading = true
const params = {
pageNum: this.pageNum,
pageSize: this.pageSize,
siteId: this.form.siteId,
}
if (this.form.dateTime) {
params.dateTime = this.form.dateTime
}
getDailyChargeDataList(params)
.then((res) => {
this.tableData = res?.rows || []
this.totalSize = res?.total || 0
})
.finally(() => {
this.loading = false
})
},
openDialog(id) {
this.$refs.addChargeDataCorrection.showDialog(id, this.form.siteId)
},
handleDelete(row) {
this.$confirm('确认要删除该条数据吗?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
showClose: false,
closeOnClickModal: false,
type: 'warning',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true
deleteDailyChargeData(row.id)
.then((res) => {
if (res?.code === 200) {
done()
}
})
.finally(() => {
instance.confirmButtonLoading = false
})
} else {
done()
}
},
})
.then(() => {
this.$message.success('删除成功!')
this.getData()
})
.catch(() => {})
},
},
}
</script>
<style scoped>
</style>

View File

@ -1,191 +0,0 @@
<template>
<div style="background-color: #ffffff" v-loading="loading">
<el-form :inline="true" class="select-container" @submit.native.prevent>
<el-form-item label="数据日期">
<el-date-picker
v-model="form.dataDate"
type="date"
value-format="yyyy-MM-dd"
clearable
placeholder="请选择日期"
style="width: 180px"
/>
</el-form-item>
<el-form-item>
<el-button native-type="button" type="primary" @click="onSearch">搜索</el-button>
<el-button native-type="button" @click="onReset">重置</el-button>
</el-form-item>
</el-form>
<el-button type="primary" @click="openDialog('')">新增</el-button>
<el-table
:data="tableData"
class="common-table"
max-height="620px"
stripe
style="width: 100%; margin-top: 20px"
>
<el-table-column label="站点" prop="siteId" width="130" />
<el-table-column label="数据日期" prop="dataDate" width="120" />
<el-table-column label="data_hour" prop="dataHour" width="100" />
<el-table-column label="尖充" prop="peakChargeDiff" min-width="90" />
<el-table-column label="尖放" prop="peakDischargeDiff" min-width="90" />
<el-table-column label="峰充" prop="highChargeDiff" min-width="90" />
<el-table-column label="峰放" prop="highDischargeDiff" min-width="90" />
<el-table-column label="平充" prop="flatChargeDiff" min-width="90" />
<el-table-column label="平放" prop="flatDischargeDiff" min-width="90" />
<el-table-column label="谷充" prop="valleyChargeDiff" min-width="90" />
<el-table-column label="谷放" prop="valleyDischargeDiff" min-width="90" />
<el-table-column label="计算时间" prop="calcTime" min-width="170" />
<el-table-column label="备注" prop="remark" min-width="180" show-overflow-tooltip />
<el-table-column fixed="right" label="操作" width="150">
<template slot-scope="scope">
<el-button size="mini" type="warning" @click="openDialog(scope.row.id)">编辑</el-button>
<el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-show="tableData.length > 0"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-size="pageSize"
:page-sizes="[10, 20, 30, 40]"
layout="total, sizes, prev, pager, next, jumper"
:total="totalSize"
style="margin-top: 15px; text-align: center"
/>
<add-data-correction ref="addDataCorrection" @update="getData" />
</div>
</template>
<script>
import {
deleteDailyEnergyData,
getDailyEnergyDataList,
} from '@/api/ems/site'
import AddDataCorrection from './AddDataCorrection.vue'
export default {
name: 'DailyEnergyDataTab',
components: { AddDataCorrection },
data() {
return {
loading: false,
form: {
siteId: '',
dataDate: '',
},
tableData: [],
pageSize: 10,
pageNum: 1,
totalSize: 0,
}
},
watch: {
'$route.query.siteId'(newSiteId) {
const normalizedSiteId = this.normalizeSiteId(newSiteId)
if (normalizedSiteId === this.form.siteId) {
return
}
this.form.siteId = normalizedSiteId
this.onSearch()
},
},
mounted() {
this.form.siteId = this.normalizeSiteId(this.$route.query.siteId)
this.getData()
},
methods: {
normalizeSiteId(siteId) {
return siteId === undefined || siteId === null ? '' : String(siteId).trim()
},
handleSizeChange(val) {
this.pageSize = val
this.pageNum = 1
this.getData()
},
handleCurrentChange(val) {
this.pageNum = val
this.getData()
},
onSearch() {
this.pageNum = 1
this.getData()
},
onReset() {
this.form = {
siteId: this.form.siteId,
dataDate: '',
}
this.pageNum = 1
this.getData()
},
getData() {
if (!this.form.siteId) {
this.tableData = []
this.totalSize = 0
return
}
this.loading = true
const params = {
pageNum: this.pageNum,
pageSize: this.pageSize,
siteId: this.form.siteId,
}
if (this.form.dataDate) {
params.dataDate = this.form.dataDate
}
getDailyEnergyDataList(params)
.then((res) => {
this.tableData = res?.rows || []
this.totalSize = res?.total || 0
})
.finally(() => {
this.loading = false
})
},
openDialog(id) {
this.$refs.addDataCorrection.showDialog(id, this.form.siteId)
},
handleDelete(row) {
this.$confirm('确认要删除该条数据吗?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
showClose: false,
closeOnClickModal: false,
type: 'warning',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true
deleteDailyEnergyData(row.id)
.then((res) => {
if (res?.code === 200) {
done()
}
})
.finally(() => {
instance.confirmButtonLoading = false
})
} else {
done()
}
},
})
.then(() => {
this.$message.success('删除成功!')
this.getData()
})
.catch(() => {})
},
},
}
</script>
<style scoped>
</style>

View File

@ -1,30 +0,0 @@
<template>
<div class="ems-dashboard-editor-container" style="background-color: #ffffff">
<el-tabs v-model="activeTab">
<el-tab-pane label="充放电量修正" name="energy" lazy>
<daily-energy-data-tab />
</el-tab-pane>
<el-tab-pane label="充放电收益修正" name="charge" lazy>
<daily-charge-data-tab />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import DailyChargeDataTab from './DailyChargeDataTab.vue'
import DailyEnergyDataTab from './DailyEnergyDataTab.vue'
export default {
name: 'DataCorrection',
components: { DailyEnergyDataTab, DailyChargeDataTab },
data() {
return {
activeTab: 'energy',
}
},
}
</script>
<style scoped>
</style>

View File

@ -1,140 +0,0 @@
<template>
<el-dialog :visible.sync="dialogTableVisible" :close-on-press-escape="false" :close-on-click-modal="false" :show-close="false" destroy-on-close lock-scroll append-to-body width="400px" class="ems-dialog" :title="mode === 'add'?'新增配置':`编辑配置` " >
<el-form v-loading="loading>0" ref="addTempForm" :model="formData" :rules="rules" size="medium" label-width="140px">
<el-form-item label="站点" prop="siteId">
<el-input v-model="formData.siteId" placeholder="请先在顶部选择站点" disabled />
</el-form-item>
<el-form-item label="消息等级" prop="qos">
<el-select v-model="formData.qos" placeholder="请选择消息等级">
<el-option :value="1">1</el-option>
<el-option :value="2">2</el-option>
<el-option :value="3">3</el-option>
</el-select>
</el-form-item>
<el-form-item label="订阅topic" prop="mqttTopic">
<el-input v-model="formData.mqttTopic" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="topic描述" prop="topicName">
<el-input v-model="formData.topicName" type="textarea" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="primary" @click="saveDialog">确定</el-button>
</div>
</el-dialog>
</template>
<script>
import {editMqtt,addMqtt,getMqttDetail} from "@/api/ems/site";
export default {
data() {
return {
loading:0,
dialogTableVisible:false,
mode:'',
formData: {
id:'',//设备唯一标识
siteId:'',
qos:'',
topicName:'',
mqttTopic:''
},
rules: {
siteId:[
{ required: true, message: '请先在顶部选择站点', trigger: 'blur'},
],
qos:[
{ required: true, message: '请选择消息等级', trigger: 'blur'},
],
mqttTopic:[
{ required: true, message: '请输入订阅topic', trigger: 'blur'},
],
topicName:[
{ required: true, message: '请输入topic描述', trigger: 'blur'},
],
},
}
},
methods: {
getRouteSiteId() {
const siteId = this.$route?.query?.siteId
return siteId === undefined || siteId === null ? '' : String(siteId).trim()
},
showDialog(id, siteId = ''){
this.dialogTableVisible = true
if(id){
this.mode = 'edit'
this.formData.id = id
this.getDetail(id)
}else{
this.mode = 'add'
this.formData.siteId = siteId || this.getRouteSiteId()
}
},
getDetail(id){
getMqttDetail(id).then(response => {
const {topicName='',mqttTopic='',qos='',siteId=''} = JSON.parse(JSON.stringify(response?.data || {}));
this.formData.mqttTopic=mqttTopic;
this.formData.topicName=topicName;
this.formData.qos=qos;
this.formData.siteId=siteId;
})
},
saveDialog() {
this.$refs.addTempForm.validate(valid => {
if (!valid) return
this.loading+=1
const {
id='',
siteId='',
qos='',
mqttTopic='',//站点ID
topicName='',//设备id
}= this.formData;
if(this.mode === 'add'){
addMqtt( {mqttTopic,topicName,siteId,qos}).then(response => {
if(response.code === 200){
//新增成功
// 关闭弹窗 更新表格
this.$emit('update')
this.closeDialog()
}
}).finally(() => {
this.loading-=1
})
}else{
editMqtt({mqttTopic,topicName,id,siteId,qos}).then(response => {
if(response.code === 200){
//新增成功
// 关闭弹窗 更新表格
this.$emit('update')
this.closeDialog()
}
}).finally(() => {
this.loading-=1
})
}
})
},
closeDialog(){
this.$emit('clear')
// 清空所有数据
this.formData= {
id:'',//设备唯一标识
siteId:'',
qos:'',
mqttTopic:'',
topicName:''
}
this.$refs.addTempForm.resetFields()
this.dialogTableVisible=false
}
}
}
</script>
<style scoped>
</style>

View File

@ -1,202 +0,0 @@
<template>
<div class="ems-dashboard-editor-container" style="background-color: #ffffff" v-loading="loading">
<el-form :inline="true" class="select-container">
<el-form-item label="订阅topic">
<el-input
v-model="form.mqttTopic"
clearable
placeholder="请输入订阅topic"
style="width: 150px"
></el-input>
</el-form-item>
<el-form-item label="topic描述">
<el-input
v-model="form.topicName"
clearable
placeholder="请输入topic描述"
style="width: 150px"
></el-input>
</el-form-item>
<el-form-item>
<el-button native-type="button" type="primary" @click="onSearch">搜索</el-button>
<el-button native-type="button" @click="onReset">重置</el-button>
</el-form-item>
</el-form>
<el-button type="primary" @click="addPowerConfig('')">新增</el-button>
<el-table
:data="tableData"
class="common-table"
max-height="600px"
stripe
style="width: 100%;margin-top: 25px">
<el-table-column
label="站点"
prop="siteId">
</el-table-column>
<el-table-column
label="消息等级"
prop="qos">
</el-table-column>
<el-table-column
label="订阅topic"
prop="mqttTopic">
</el-table-column>
<el-table-column
label="topic描述"
prop="topicName">
</el-table-column>
<el-table-column
fixed="right"
label="操作">
<template slot-scope="scope">
<el-button
size="mini"
style="margin-top:10px;"
type="warning"
@click="addPowerConfig(scope.row.id)">
编辑
</el-button>
<el-button
size="mini"
style="margin-top:10px;"
type="danger"
@click="deleteMqtt(scope.row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-show="tableData.length>0"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-size="pageSize"
:page-sizes="[10, 20, 30, 40]"
layout="total, sizes, prev, pager, next, jumper"
:total="totalSize"
style="margin-top:15px;text-align: center"
>
</el-pagination>
<add-mqtt ref="addMqtt" @update="getData"/>
</div>
</template>
<script>
import {deleteMqtt,getMqttList} from '@/api/ems/site'
import AddMqtt from './AddMqtt.vue'
export default {
name: "Mqtt",
components: {AddMqtt},
computed: { },
watch: {
'$route.query.siteId'(newSiteId) {
const normalizedSiteId = this.hasValidSiteId(newSiteId) ? String(newSiteId).trim() : ''
if (normalizedSiteId === this.form.siteId) {
return
}
this.form.siteId = normalizedSiteId
this.onSearch()
}
},
data() {
return {
form:{
siteId:"",
topicName:'',
mqttTopic:''
},
loading:false,
tableData:[],
pageSize:10,//分页栏当前每个数据总数
pageNum:1,//分页栏当前页数
totalSize:0,//table表格数据总数
}
},
methods:{
hasValidSiteId(siteId) {
return !!(siteId !== undefined && siteId !== null && String(siteId).trim())
},
// 分页
handleSizeChange(val) {
this.pageSize = val;
this.$nextTick(()=>{
this.getData()
})
},
handleCurrentChange(val) {
this.pageNum = val
this.$nextTick(()=>{
this.getData()
})
},
// 搜索
onSearch(){
this.pageNum =1//每次搜索从1开始搜索
this.getData()
},
onReset(){
this.form={
siteId:this.form.siteId,
topicName:'',
mqttTopic:''
}
this.pageNum =1//每次搜索从1开始搜索
this.getData()
},
getData(){
if (!this.form.siteId) {
this.tableData = []
this.totalSize = 0
return
}
this.loading=true;
const {mqttTopic,topicName,siteId} = this.form;
const {pageNum,pageSize} = this;
getMqttList({pageNum,pageSize,mqttTopic,topicName,siteId}).then(response => {
this.tableData=response?.rows || [];
this.totalSize = response?.total || 0
}).finally(() => {this.loading=false})
},
addPowerConfig(id=''){
this.$refs.addMqtt.showDialog(id, this.form.siteId);
},
deleteMqtt(row){
this.$confirm(`确认要删除该配置吗?`, {
confirmButtonText: '确定',
cancelButtonText: '取消',
showClose:false,
closeOnClickModal:false,
type: 'warning',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true;
deleteMqtt(row.id).then(response => {
response.code === 200 && done();
}).finally(() => {
instance.confirmButtonLoading = false;
})
} else {
done();
}
}
}).then(() => {
//只有在废弃成功的情况下会走到这里
this.$message({
type: 'success',
message: '删除成功!'
});
this.getData()
//调用接口 更新表格数据
}).catch(() => {
//取消关机
});
},
},
mounted() {
this.form.siteId = this.hasValidSiteId(this.$route.query.siteId) ? String(this.$route.query.siteId).trim() : ''
this.getData()
}
}
</script>

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More