6 Commits

Author SHA1 Message Date
f88e9bedc2 重构 2026-04-01 14:27:35 +08:00
9272a0162a 重构 2026-02-17 21:44:12 +08:00
c7c1b416ee 重构 2026-02-16 13:41:41 +08:00
41a3ab45b3 重构 2026-02-15 16:24:29 +08:00
50c72d6989 重构 2026-02-13 21:46:12 +08:00
7fdb6e2ad3 重构 2026-02-12 21:19:23 +08:00
77 changed files with 13379 additions and 3337 deletions

View File

@ -5,9 +5,8 @@ VUE_APP_TITLE = 上动新能源-EMS管理系统
NODE_ENV = 'production'
# EMS管理系统/生产环境
VUE_APP_BASE_API= 'http://127.0.0.1:8089'
VUE_APP_BASE_API= 'http://1.15.120.242:8089'
# EMS管理系统/生产环境 图片拼接地址
VUE_APP_IMG_URL = 'http://127.0.0.1:8089'
VUE_APP_IMG_URL = 'http://1.15.120.242:8089'

View File

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

View File

@ -8,6 +8,39 @@ export function getDzjkHomeView(siteId) {
})
}
//获取单个站点总累计运行数据(基于日表)
export function getDzjkHomeTotalView(siteId) {
return request({
url: `/ems/siteMonitor/homeTotalView?siteId=${siteId}`,
method: 'get'
})
}
// 单站监控项目点位配置(供单站监控功能查询)
export function getProjectPointMapping(siteId) {
return request({
url: `/ems/siteMonitor/getProjectPointMapping?siteId=${siteId}`,
method: 'get'
})
}
// 单站监控项目展示数据(字段配置 + 最新值)
export function getProjectDisplayData(siteId) {
return request({
url: `/ems/siteMonitor/getProjectDisplayData?siteId=${siteId}`,
method: 'get'
})
}
// 单站监控项目展示数据写入(批量)
export function saveProjectDisplayData(data) {
return request({
url: `/ems/siteMonitor/saveProjectDisplayData`,
method: 'post',
data
})
}
//站点首页 冲放曲线
export function getSevenChargeData({siteId, startDate, endDate}) {
return request({
@ -156,12 +189,328 @@ export function createTicketNo(data) {
})
}
// 概率统计
//获取概率统计 电量指标接口
export function getElectricData({siteId, startDate, endDate}) {
// 告警确认关闭
export function closeAlarm(data) {
return request({
url: `/ems/statsReport/getElectricData?siteId=${siteId}&startDate=${startDate}&endDate=${endDate}`,
method: 'get'
url: `/ems/siteAlarm/closeAlarm`,
method: 'post',
data
})
}
function getFieldNameByCode(fieldCode) {
const raw = String(fieldCode || '').trim()
if (!raw) return ''
const idx = raw.lastIndexOf('__')
return idx >= 0 ? raw.slice(idx + 2) : raw
}
function normalizeRows(displayResponse) {
const rows = displayResponse?.data || []
return Array.isArray(rows) ? rows : []
}
function filterByMenu(rows, menuCode) {
return (rows || []).filter(item => item && item.menuCode === menuCode)
}
function toDateLabel(valueTime) {
if (!valueTime) return ''
const date = new Date(valueTime)
if (isNaN(date.getTime())) return ''
const p = (n) => String(n).padStart(2, '0')
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())}`
}
function toDateTimeLabel(valueTime) {
if (!valueTime) return ''
const date = new Date(valueTime)
if (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())}`
}
function toValueMap(rows) {
const map = {}
;(rows || []).forEach(item => {
const fieldName = getFieldNameByCode(item?.fieldCode)
if (fieldName) {
map[fieldName] = item?.fieldValue
}
})
return map
}
function groupRowsByDevice(rows) {
const map = new Map()
;(rows || []).forEach(item => {
const deviceId = String(item?.deviceId || '').trim() || 'DEFAULT'
if (!map.has(deviceId)) {
map.set(deviceId, [])
}
map.get(deviceId).push(item)
})
return map
}
function paginateRows(rows, pageNum = 1, pageSize = 10) {
const safePageNum = Number(pageNum) > 0 ? Number(pageNum) : 1
const safePageSize = Number(pageSize) > 0 ? Number(pageSize) : 10
const start = (safePageNum - 1) * safePageSize
return (rows || []).slice(start, start + safePageSize)
}
function toNumber(value) {
const num = Number(value)
return Number.isFinite(num) ? num : null
}
function normalizeDateInput(dateStr) {
if (dateStr) return dateStr
return toDateLabel(new Date())
}
function resolveElectricUnit(startDate, endDate) {
const start = new Date(`${normalizeDateInput(startDate)} 00:00:00`)
const end = new Date(`${normalizeDateInput(endDate)} 00:00:00`)
if (isNaN(start.getTime()) || isNaN(end.getTime())) return '日'
const diffDays = Math.floor((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000))
if (diffDays <= 0) return '时'
if (diffDays < 30) return '日'
return '月'
}
function formatByUnit(date, unit) {
const p = (n) => String(n).padStart(2, '0')
if (unit === '时') return `${p(date.getHours())}:${p(date.getMinutes())}`
if (unit === '月') return `${date.getFullYear()}-${p(date.getMonth() + 1)}`
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())}`
}
function aggregateCurveByUnit(curveRows, unit) {
const result = new Map()
;(curveRows || []).forEach(item => {
const time = item?.dataTime ? new Date(item.dataTime) : null
if (!time || isNaN(time.getTime())) return
const value = toNumber(item?.pointValue)
if (value == null) return
const label = formatByUnit(time, unit)
result.set(label, value)
})
return result
}
function getLatestCurveValue(curveRows) {
let latestTime = null
let latestValue = null
;(curveRows || []).forEach(item => {
const time = item?.dataTime ? new Date(item.dataTime) : null
if (!time || isNaN(time.getTime())) return
const value = toNumber(item?.pointValue)
if (value == null) return
if (latestTime == null || time.getTime() > latestTime) {
latestTime = time.getTime()
latestValue = value
}
})
return latestValue
}
function findMappingByField(rows, fieldNames = []) {
const targetSet = new Set((fieldNames || []).map(name => String(name || '').trim()))
return (rows || []).find(item => targetSet.has(getFieldNameByCode(item?.fieldCode)))
}
function getDataPointFromMapping(mapping) {
if (!mapping) return ''
const useFixedDisplay = Number(mapping?.useFixedDisplay) === 1
const fixedDataPoint = String(mapping?.fixedDataPoint || '').trim()
const dataPoint = String(mapping?.dataPoint || '').trim()
if (useFixedDisplay && fixedDataPoint) return fixedDataPoint
return dataPoint || fixedDataPoint
}
function queryPointCurveByPointId({siteId, pointId, startDate, endDate}) {
if (!siteId || !pointId) return Promise.resolve([])
const start = `${normalizeDateInput(startDate)} 00:00:00`
const end = `${normalizeDateInput(endDate)} 23:59:59`
return request({
url: `/ems/pointConfig/curve`,
method: 'post',
headers: {
repeatSubmit: false
},
data: {
siteId,
pointId,
rangeType: 'custom',
startTime: start,
endTime: end
}
}).then(resp => (Array.isArray(resp?.data) ? resp.data : []))
}
function sortCurveRows(curveRows = []) {
return [...(curveRows || [])].sort((a, b) => {
const ta = new Date(a?.dataTime).getTime()
const tb = new Date(b?.dataTime).getTime()
return (isNaN(ta) ? 0 : ta) - (isNaN(tb) ? 0 : tb)
})
}
function resolveRangeKind(startDate, endDate) {
const start = new Date(`${normalizeDateInput(startDate)} 00:00:00`)
const end = new Date(`${normalizeDateInput(endDate || startDate)} 00:00:00`)
if (isNaN(start.getTime()) || isNaN(end.getTime())) return 'day'
const diffDays = Math.floor((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000))
if (diffDays <= 0) return 'minute'
if (diffDays < 30) return 'day'
return 'month'
}
function formatTimeLabelByKind(date, kind = 'day') {
const p = (n) => String(n).padStart(2, '0')
if (kind === 'minute') return `${p(date.getHours())}:${p(date.getMinutes())}`
if (kind === 'month') return `${date.getFullYear()}-${p(date.getMonth() + 1)}`
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())}`
}
function buildSortedLabels(labelSet, kind = 'day') {
const labels = Array.from(labelSet || [])
if (kind === 'minute') {
return labels.sort((a, b) => {
const [ah = 0, am = 0] = String(a || '').split(':').map(v => Number(v) || 0)
const [bh = 0, bm = 0] = String(b || '').split(':').map(v => Number(v) || 0)
return ah * 60 + am - (bh * 60 + bm)
})
}
return labels.sort()
}
function normalizePointValue(raw) {
const num = toNumber(raw)
if (num != null) return num
return raw == null ? '' : raw
}
function resolveAliasByField(aliasMap, fieldName) {
const raw = String(fieldName || '').trim()
if (!raw) return ''
if (aliasMap[raw]) return aliasMap[raw]
const withoutStat = raw.replace(/_stat$/, '')
return aliasMap[withoutStat] || ''
}
function queryMenuPointCurves({siteId, menuCode, startDate, endDate, mappingFilter}) {
return getProjectPointMapping(siteId).then((mappingResp) => {
const allMappings = Array.isArray(mappingResp?.data) ? mappingResp.data : []
let menuMappings = allMappings.filter(item => item?.menuCode === menuCode)
if (typeof mappingFilter === 'function') {
menuMappings = menuMappings.filter(mappingFilter)
}
const tasks = menuMappings.map((mapping) => {
const pointId = getDataPointFromMapping(mapping)
if (!pointId) return Promise.resolve(null)
return queryPointCurveByPointId({siteId, pointId, startDate, endDate})
.then(curve => ({
deviceId: String(mapping?.deviceId || '').trim(),
fieldName: getFieldNameByCode(mapping?.fieldCode),
curve: sortCurveRows(curve || []),
}))
.catch(() => ({
deviceId: String(mapping?.deviceId || '').trim(),
fieldName: getFieldNameByCode(mapping?.fieldCode),
curve: [],
}))
})
return Promise.all(tasks).then(rows => rows.filter(Boolean))
})
}
// 电量指标
export function getElectricData({siteId, startDate, endDate}) {
return getProjectPointMapping(siteId).then((mappingResp) => {
const allMappings = Array.isArray(mappingResp?.data) ? mappingResp.data : []
const gltjMappings = allMappings.filter(item => item?.menuCode === 'TJBB_GLTJ')
const chargedMap = findMappingByField(gltjMappings, ['chargedCap_stat', 'chargedCap'])
const disChargedMap = findMappingByField(gltjMappings, ['disChargedCap_stat', 'disChargedCap'])
const dailyEfficiencyMap = findMappingByField(gltjMappings, ['dailyEfficiency'])
const totalChargedMap = findMappingByField(gltjMappings, ['totalChargedCap_stat', 'totalChargedCap'])
const totalDisChargedMap = findMappingByField(gltjMappings, ['totalDisChargedCap_stat', 'totalDisChargedCap'])
const totalEfficiencyMap = findMappingByField(gltjMappings, ['efficiency'])
const pointMap = {
charged: getDataPointFromMapping(chargedMap),
disCharged: getDataPointFromMapping(disChargedMap),
dailyEfficiency: getDataPointFromMapping(dailyEfficiencyMap),
totalCharged: getDataPointFromMapping(totalChargedMap),
totalDisCharged: getDataPointFromMapping(totalDisChargedMap),
totalEfficiency: getDataPointFromMapping(totalEfficiencyMap),
}
const queryTasks = Object.keys(pointMap).map((key) => {
const pointId = pointMap[key]
return queryPointCurveByPointId({siteId, pointId, startDate, endDate})
.then(curve => ({key, curve}))
.catch(() => ({key, curve: []}))
})
return Promise.all(queryTasks).then((queryResult) => {
const curveMap = {}
queryResult.forEach(item => {
curveMap[item.key] = item.curve || []
})
const unit = resolveElectricUnit(startDate, endDate)
const chargedSeries = aggregateCurveByUnit(curveMap.charged, unit)
const disChargedSeries = aggregateCurveByUnit(curveMap.disCharged, unit)
const efficiencySeries = aggregateCurveByUnit(curveMap.dailyEfficiency, unit)
const labels = Array.from(new Set([
...chargedSeries.keys(),
...disChargedSeries.keys(),
...efficiencySeries.keys(),
])).sort()
const sevenDayDisChargeStats = labels.map((label) => {
const chargedCap = chargedSeries.get(label)
const disChargedCap = disChargedSeries.get(label)
let dailyEfficiency = efficiencySeries.get(label)
if (dailyEfficiency == null && chargedCap != null && chargedCap !== 0 && disChargedCap != null) {
dailyEfficiency = Number(((disChargedCap / chargedCap) * 100).toFixed(2))
}
return {
ammeterDate: label,
chargedCap: chargedCap == null ? '' : chargedCap,
disChargedCap: disChargedCap == null ? '' : disChargedCap,
dailyEfficiency: dailyEfficiency == null ? '' : dailyEfficiency,
}
})
const fallbackTotalCharged = sevenDayDisChargeStats.reduce((acc, item) => acc + (toNumber(item.chargedCap) || 0), 0)
const fallbackTotalDisCharged = sevenDayDisChargeStats.reduce((acc, item) => acc + (toNumber(item.disChargedCap) || 0), 0)
const totalChargedCap = getLatestCurveValue(curveMap.totalCharged)
const totalDisChargedCap = getLatestCurveValue(curveMap.totalDisCharged)
const totalEfficiency = getLatestCurveValue(curveMap.totalEfficiency)
const resultTotalCharged = totalChargedCap == null ? fallbackTotalCharged : totalChargedCap
const resultTotalDisCharged = totalDisChargedCap == null ? fallbackTotalDisCharged : totalDisChargedCap
const resultEfficiency = totalEfficiency == null
? (resultTotalCharged > 0 ? Number(((resultTotalDisCharged / resultTotalCharged) * 100).toFixed(2)) : 0)
: totalEfficiency
return {
data: {
totalChargedCap: resultTotalCharged,
totalDisChargedCap: resultTotalDisCharged,
efficiency: resultEfficiency,
unit,
sevenDayDisChargeStats,
}
}
})
})
}
@ -173,27 +522,147 @@ export function getPcsNameList(siteId) {
})
}
//pcs曲线
export function getPCSData({siteId, startTime, endTime, dataType}) {
return request({
url: `/ems/statsReport/getPCSData?siteId=${siteId}&startDate=${startTime}&endDate=${endTime}&dataType=${dataType}`,
method: 'get'
// pcs曲线
export function getPCSData({siteId, startTime, endTime}) {
const kind = resolveRangeKind(startTime, endTime)
const aliasMap = {
activePower_stat: 'activePower',
activePower: 'activePower',
reactivePower_stat: 'reactivePower',
reactivePower: 'reactivePower',
uCurrent: 'uCurrent',
vCurrent: 'vCurrent',
wCurrent: 'wCurrent',
}
return queryMenuPointCurves({
siteId,
menuCode: 'TJBB_PCSQX',
startDate: startTime,
endDate: endTime,
}).then((records) => {
const byDevice = new Map()
records.forEach((record) => {
const alias = resolveAliasByField(aliasMap, record.fieldName)
if (!alias) return
const deviceId = record.deviceId || ''
if (!byDevice.has(deviceId)) byDevice.set(deviceId, new Map())
const rowMap = byDevice.get(deviceId)
;(record.curve || []).forEach((point) => {
const time = point?.dataTime ? new Date(point.dataTime) : null
if (!time || isNaN(time.getTime())) return
const label = formatTimeLabelByKind(time, kind)
if (!rowMap.has(label)) rowMap.set(label, {statisDate: label})
rowMap.get(label)[alias] = normalizePointValue(point?.pointValue)
})
})
const data = []
byDevice.forEach((rowMap, deviceId) => {
const labels = buildSortedLabels(new Set(Array.from(rowMap.keys())), kind)
data.push({
deviceId,
dataList: labels.map(label => rowMap.get(label)),
})
})
return {data}
})
}
//电池堆曲线
export function getStackData({siteId, startTime, endTime, dataType}) {
return request({
url: `/ems/statsReport/getStackData?siteId=${siteId}&startDate=${startTime}&endDate=${endTime}&dataType=${dataType}`,
method: 'get'
// 电池堆曲线
export function getStackData({siteId, startTime, endTime}) {
const kind = resolveRangeKind(startTime, endTime)
const aliasMap = {
temp: 'temp',
voltage_stat: 'voltage',
voltage: 'voltage',
current: 'current',
soc_stat: 'soc',
soc: 'soc',
}
return queryMenuPointCurves({
siteId,
menuCode: 'TJBB_DCDQX',
startDate: startTime,
endDate: endTime,
}).then((records) => {
const byDevice = new Map()
records.forEach((record) => {
const alias = resolveAliasByField(aliasMap, record.fieldName)
if (!alias) return
const deviceId = record.deviceId || ''
if (!byDevice.has(deviceId)) byDevice.set(deviceId, new Map())
const rowMap = byDevice.get(deviceId)
;(record.curve || []).forEach((point) => {
const time = point?.dataTime ? new Date(point.dataTime) : null
if (!time || isNaN(time.getTime())) return
const label = formatTimeLabelByKind(time, kind)
if (!rowMap.has(label)) rowMap.set(label, {statisDate: label})
rowMap.get(label)[alias] = normalizePointValue(point?.pointValue)
})
})
const data = []
byDevice.forEach((rowMap, deviceId) => {
const labels = buildSortedLabels(new Set(Array.from(rowMap.keys())), kind)
data.push({
deviceId,
dataList: labels.map(label => rowMap.get(label)),
})
})
return {data}
})
}
//电池温度
// 电池温度
export function getClusterData({siteId, stackId, clusterId, dateTime, pageNum, pageSize}) {
return request({
url: `/ems/statsReport/getClusterData?siteId=${siteId}&stackId=${stackId}&clusterId=${clusterId}&dateTime=${dateTime}&pageNum=${pageNum}&pageSize=${pageSize}`,
method: 'get'
const startDate = dateTime || normalizeDateInput('')
const endDate = dateTime || normalizeDateInput('')
const kind = 'minute'
const aliasMap = {
maxTemp: 'maxTemp',
maxTempId: 'maxTempId',
minTemp: 'minTemp',
minTempId: 'minTempId',
maxVoltage: 'maxVoltage',
maxVoltageId: 'maxVoltageId',
minVoltage: 'minVoltage',
minVoltageId: 'minVoltageId',
}
const queryClusterCurves = (withClusterFilter) => queryMenuPointCurves({
siteId,
menuCode: 'TJBB_DCWD',
startDate,
endDate,
mappingFilter: withClusterFilter
? (item) => {
if (!clusterId) return true
return String(item?.deviceId || '').trim() === String(clusterId || '').trim()
}
: undefined
})
return queryClusterCurves(true).then((records) => {
if (clusterId && (!records || records.length === 0)) {
return queryClusterCurves(false)
}
return records
}).then((records) => {
const rowMap = new Map()
records.forEach((record) => {
const alias = resolveAliasByField(aliasMap, record.fieldName)
if (!alias) return
;(record.curve || []).forEach((point) => {
const time = point?.dataTime ? new Date(point.dataTime) : null
if (!time || isNaN(time.getTime())) return
const label = formatTimeLabelByKind(time, kind)
if (!rowMap.has(label)) rowMap.set(label, {statisDate: label})
rowMap.get(label)[alias] = normalizePointValue(point?.pointValue)
})
})
const labels = buildSortedLabels(new Set(Array.from(rowMap.keys())), kind)
const fullRows = labels.map(label => rowMap.get(label))
return {
rows: paginateRows(fullRows, pageNum, pageSize),
total: fullRows.length,
}
})
}
@ -233,9 +702,37 @@ export function batteryAveTemp(siteId, startTime, endTime) {
// 功率曲线
export function getPowerData({siteId, startDate, endDate}) {
return request({
url: `/ems/statsReport/getPowerData?siteId=${siteId}&startDate=${startDate}&endDate=${endDate}`,
method: 'get'
const kind = resolveRangeKind(startDate, endDate)
const aliasMap = {
gridPower_stat: 'gridPower',
gridPower: 'gridPower',
loadPower_stat: 'loadPower',
loadPower: 'loadPower',
storagePower_stat: 'storagePower',
storagePower: 'storagePower',
pvPower_stat: 'pvPower',
pvPower: 'pvPower',
}
return queryMenuPointCurves({
siteId,
menuCode: 'TJBB_GLQX',
startDate,
endDate,
}).then((records) => {
const rowMap = new Map()
records.forEach((record) => {
const alias = resolveAliasByField(aliasMap, record.fieldName)
if (!alias) return
;(record.curve || []).forEach((point) => {
const time = point?.dataTime ? new Date(point.dataTime) : null
if (!time || isNaN(time.getTime())) return
const label = formatTimeLabelByKind(time, kind)
if (!rowMap.has(label)) rowMap.set(label, {statisDate: label})
rowMap.get(label)[alias] = normalizePointValue(point?.pointValue)
})
})
const labels = buildSortedLabels(new Set(Array.from(rowMap.keys())), kind)
return {data: labels.map(label => rowMap.get(label))}
})
}
@ -250,8 +747,15 @@ export function getLoadNameList(siteId) {
// 电表报表
export function getAmmeterData({siteId, startTime, endTime, pageSize, pageNum}) {
return request({
url: `/ems/statsReport/getAmmeterData?siteId=${siteId}&startTime=${startTime}&endTime=${endTime}&pageSize=${pageSize}&pageNum=${pageNum}`,
method: 'get'
url: `/ems/statsReport/getAmmeterDataFromDaily`,
method: 'get',
params: {
siteId,
startTime,
endTime,
pageSize,
pageNum,
}
})
}
@ -305,6 +809,23 @@ export function configStrategy(data) {
})
}
// 获取策略运行参数配置(按站点)
export function getStrategyRuntimeConfig(siteId) {
return request({
url: `/system/strategyRuntimeConfig/getBySiteId?siteId=${siteId}`,
method: 'get'
})
}
// 保存策略运行参数配置(按站点)
export function saveStrategyRuntimeConfig(data) {
return request({
url: `/system/strategyRuntimeConfig/save`,
method: 'post',
data
})
}
//http://localhost:8089/strategy/temp/getTempNameList?strategyId=1&siteId=021_FXX_01
//获取策略下的所有模板列表
export function getTempNameList({siteId, strategyId}) {

View File

@ -29,4 +29,12 @@ export function getAllBatteryIdsBySites(data) {
url: `/ems/generalQuery/getAllBatteryIdsBySites/${data}`,
method: 'get',
})
}
}
// 综合查询-按站点获取配置设备列表
export function getGeneralQueryDeviceList(siteId) {
return request({
url: `/ems/siteConfig/getDeviceList?siteId=${siteId}`,
method: 'get',
})
}

View File

@ -8,6 +8,33 @@ export function getSiteInfoList({siteName, startTime, endTime, pageSize, pageNum
})
}
// 手动同步站点天气(收益报表)
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) {
return request({
@ -102,6 +129,46 @@ export function getDeviceListBySiteAndCategory({siteId, deviceCategory}) {
})
}
// 获取单站监控项目点位映射
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({
@ -163,6 +230,234 @@ export function importPointList(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 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}) {

View File

@ -4,8 +4,8 @@
//右侧内容区域
//父元素
.ems-dashboard-editor-container{
background-color: #FFFFFF;//#F1F5FC
.ems-dashboard-editor-container {
background-color: #F1F5FC;
padding: 24px;
font-size: 12px;
}
@ -280,7 +280,7 @@
.ems-third-menu-container {
position: relative;
padding-left: 160px;
padding-left: 140px;
background-color: #ffffff;
.ems-third-menu {
@ -288,9 +288,10 @@
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
height: fit-content;
position: absolute;
top:20px;
left:20px;
.el-menu-item{
top: 0;
left: 0;
.el-menu-item {
line-height: 45px;
height: 45px;
padding: 0 !important;

View File

@ -2,7 +2,10 @@
<template>
<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-value">{{ data.value | formatNumber }}</div>
<div class="single-square-box-value">
<i v-if="data.loading" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ data.value | formatNumber }}</span>
</div>
</el-card>
</template>
@ -14,18 +17,29 @@
color:#666666;
text-align: left;
.single-square-box-title{
font-size: 12px;
line-height: 12px;
padding-bottom: 12px;
font-size: 10px;
line-height: 10px;
padding-bottom: 8px;
}
.single-square-box-value{
font-size: 26px;
line-height: 26px;
font-size: 18px;
line-height: 18px;
font-weight: 500;
}
::v-deep .el-card__body{
padding: 12px 10px;
.point-loading-icon{
color: #409eff;
display: inline-block;
transform-origin: center;
animation: pointLoadingSpinPulse 1.1s linear infinite;
}
::v-deep .el-card__body{
padding: 8px 7px;
}
}
@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>
<script>

View File

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

View File

@ -2,8 +2,16 @@
<template>
<div class="zd-select-container">
<el-form :inline="true">
<el-form-item label="站点选择">
<el-select v-model="id" placeholder="请选择换电站名称" :loading="loading" loading-text="正在加载数据" @change="onSubmit">
<el-form-item :label="showLabel ? '站点选择' : ''" :class="{'no-label': !showLabel}">
<el-select
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-select>
</el-form-item>
@ -15,7 +23,21 @@
</template>
<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>
<script>
import {getAllSites} from '@/api/ems/zddt'
@ -31,6 +53,26 @@ import {mapGetters} from "vuex"
type:String,
default:'',
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() {
@ -44,10 +86,23 @@ import {mapGetters} from "vuex"
computed:{
...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:{
onSubmit(){
this.$emit('submitSite',this.id)
},
emitSitesLoaded() {
this.$emit('sitesLoaded', this.siteList || [])
},
setDefaultSite(){
const defaultSite = this.defaultSiteId
if(defaultSite && this.siteList.find(item=>item.siteId === defaultSite)){
@ -60,7 +115,7 @@ import {mapGetters} from "vuex"
getList(){
return getAllSites().then(response => {
this.siteList = response.data || []
console.log("获取站点列表返回数据",response,this.siteList)
this.emitSitesLoaded()
this.setDefaultSite()
}).finally(() => {this.loading=false;this.searchLoading=false})
}
@ -71,15 +126,14 @@ import {mapGetters} from "vuex"
this.$nextTick(()=>{
if(this.getListByStore){
if(this.zdList.length === 0){
this.getList().then(()=>{
this.$store.commit('SET_ZD_LIST', this.siteList)
console.log("从store中获取站点列表数据,但是store中的zdList=[],所以从接口获取数据",this.zdList,this.siteList)
this.getList().then(()=>{
this.$store.commit('SET_ZD_LIST', this.siteList)
})
}else{
this.siteList = this.zdList
this.emitSitesLoaded()
this.loading=false
this.searchLoading=false
console.log("从store中获取站点列表数据",this.zdList,this.siteList)
this.setDefaultSite()
}
}else{

View File

@ -22,6 +22,16 @@
</el-tooltip>
</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">
<div class="avatar-wrapper">
@ -54,6 +64,7 @@ import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect'
import Search from '@/components/HeaderSearch'
import BigDataPopup from '@/components/BigDataPopup'
import ZdSelect from '@/components/Ems/ZdSelect/index.vue'
export default {
emits: ['setLayout'],
@ -64,7 +75,8 @@ export default {
Screenfull,
SizeSelect,
Search,
BigDataPopup
BigDataPopup,
ZdSelect
},
computed: {
...mapGetters([
@ -85,6 +97,25 @@ export default {
}
},
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
},
@ -150,6 +181,22 @@ export default {
height: 100%;
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;
@ -215,6 +262,7 @@ export default {
}
}
}
}
}
</style>

View File

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

View File

@ -10,11 +10,16 @@ import { isRelogin } from '@/utils/request'
NProgress.configure({ showSpinner: false })
const whiteList = ['/login', '/register']
const GLOBAL_SITE_STORAGE_KEY = 'global_site_id'
const isWhiteList = (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) => {
NProgress.start()
if (getToken()) {
@ -26,12 +31,29 @@ router.beforeEach((to, from, next) => {
} else if (isWhiteList(to.path)) {
next()
} 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) {
isRelogin.show = true
// 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(() => {
isRelogin.show = false
store.dispatch('getZdList')
store.dispatch('GenerateRoutes').then(accessRoutes => {
// 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表

View File

@ -7,261 +7,318 @@ export const dzjk = [
path: '/dzjk',
component: Layout,
redirect: '/dzjk/home',
meta: {title: '单站监控', icon: 'server',},
meta: {title: '单站监控', icon: 'dashboard',},
alwaysShow: false,
name: 'Dzjk',
name: 'DzjkLocal',
hidden: true,
children: [
{
path: '/dzjk/home',
component: () => import('@/views/ems/dzjk/home/index.vue'),
name: 'DzjkHome',
meta: {title: '站点首页', breadcrumb: false, activeMenu: '/dzjk/home', activeSecondMenuName: 'DzjkHome'}
},
{
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',
path: '',
component: () => import('@/views/ems/dzjk/index'),
name: 'DzjkRoot',
redirect: '/dzjk/home',
hidden: true,
children: [
{
path: 'ssyx',
component: () => import('@/views/ems/dzjk/sbjk/ssyx/index.vue'),
name: 'DzjkSbjkSsyx',
hidden: true,
path: '/dzjk/home',
component: () => import('@/views/ems/dzjk/home/index.vue'),
name: 'DzjkHome',
meta: {
title: '实时运行',
breadcrumb: false,
activeMenu: '/dzjk/sbjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'SSYX'
},
},
{
path: 'ems',
component: () => import('@/views/ems/dzjk/sbjk/ems/index.vue'),
name: 'DzjkSbjkEms',
hidden: true,
meta: {
title: 'EMS',
breadcrumb: false,
activeMenu: '/dzjk/sbjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'EMS'
},
},
{
path: 'pcs',
component: () => import('@/views/ems/dzjk/sbjk/pcs/index.vue'),
name: 'DzjkSbjkPcs',
hidden: true,
meta: {
title: 'PCS',
breadcrumb: false,
activeMenu: '/dzjk/sbjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'PCS'
},
},
{
path: 'bmszl',
component: () => import('@/views/ems/dzjk/sbjk/bmszl/index.vue'),
name: 'DzjkSbjkBmszl',
hidden: true,
meta: {
title: 'BMS总览',
breadcrumb: false,
activeMenu: '/dzjk/sbjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'STACK'
},
},
{
path: 'bmsdcc',
component: () => import('@/views/ems/dzjk/sbjk/bmsdcc/index.vue'),
name: 'DzjkSbjkBmsdcc',
hidden: true,
meta: {
title: 'BMS电池簇',
breadcrumb: false,
activeMenu: '/dzjk/sbjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'CLUSTER'
},
},
{
path: 'dtdc',
component: () => import('@/views/ems/dzjk/sbjk/dtdc/index.vue'),
name: 'DzjkSbjkDtdc',
hidden: true,
meta: {
title: '单体电池',
breadcrumb: false,
activeMenu: '/dzjk/sbjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'BATTERY'
},
},
{
path: 'db',
component: () => import('@/views/ems/dzjk/sbjk/db/index.vue'),
name: 'DzjkSbjkDb',
hidden: true,
meta: {
title: '电表',
breadcrumb: false,
activeMenu: '/dzjk/sbjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'AMMETER'
},
},
{
path: 'yl',
component: () => import('@/views/ems/dzjk/sbjk/yl/index.vue'),
name: 'DzjkSbjkYl',
hidden: true,
meta: {
title: '液冷',
breadcrumb: false,
activeMenu: '/dzjk/sbjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'COOLING'
},
},
{
path: 'dh',
component: () => import('@/views/ems/dzjk/sbjk/dh/index.vue'),
name: 'DzjkSbjkDh',
hidden: true,
meta: {
title: '动环',
title: '站点首页',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'DH'
},
activeSecondMenuName: 'DzjkHome'
}
},
{
path: 'xf',
component: () => import('@/views/ems/dzjk/sbjk/xf/index.vue'),
name: 'DzjkSbjkXf',
hidden: true,
path: '/dzjk/zxlt',
component: () => import('@/views/ems/dzjk/zxlt/index.vue'),
name: 'DzjkZxlt',
meta: {
title: '消防',
title: '主线路图',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'XF'
},
activeSecondMenuName: 'DzjkZxlt'
}
},
{
path: '/dzjk/sbjk',
component: () => import('@/views/ems/dzjk/sbjk/index.vue'),
name: 'DzjkSbjk',
meta: {title: '设备监控', breadcrumb: false, activeMenu: '/dzjk'},
redirect: '/dzjk/sbjk/ssyx',
children: [
{
path: 'ssyx',
component: () => import('@/views/ems/dzjk/sbjk/ssyx/index.vue'),
name: 'DzjkSbjkSsyx',
meta: {
title: '实时运行',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'SSYX'
},
},
{
path: 'ems',
component: () => import('@/views/ems/dzjk/sbjk/ems/index.vue'),
name: 'DzjkSbjkEms',
meta: {
title: 'EMS',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'EMS'
},
},
{
path: 'pcs',
component: () => import('@/views/ems/dzjk/sbjk/pcs/index.vue'),
name: 'DzjkSbjkPcs',
meta: {
title: 'PCS',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'PCS'
},
},
{
path: 'bmszl',
component: () => import('@/views/ems/dzjk/sbjk/bmszl/index.vue'),
name: 'DzjkSbjkBmszl',
meta: {
title: 'BMS总览',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'STACK'
},
},
{
path: 'bmsdcc',
component: () => import('@/views/ems/dzjk/sbjk/bmsdcc/index.vue'),
name: 'DzjkSbjkBmsdcc',
meta: {
title: 'BMS电池簇',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'CLUSTER'
},
},
{
path: 'dtdc',
component: () => import('@/views/ems/dzjk/sbjk/dtdc/index.vue'),
name: 'DzjkSbjkDtdc',
meta: {
title: 'BMS单体电池',
breadcrumb: false,
activeMenu: '/dzjk',
activeSecondMenuName: 'DzjkSbjk',
deviceCategory: 'BATTERY'
},
},
{
path: 'db',
component: () => import('@/views/ems/dzjk/sbjk/db/index.vue'),
name: 'DzjkSbjkDb',
meta: {
title: '电表',
breadcrumb: false,
activeMenu: '/dzjk',
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'},
// }
]
}
]
},
{
path: '/dzjk/gzgj',
component: () => import('@/views/ems/dzjk/gzgj/index.vue'),
name: 'DzjkGzgj',
meta: {title: '故障告警', breadcrumb: false, activeMenu: '/dzjk/gzgj', activeSecondMenuName: 'DzjkGzgj'}
},
{
path: '/dzjk/tjbb',
component: () => import('@/views/ems/dzjk/tjbb/index.vue'),
name: 'DzjkTjbb',
alwaysShow: false,
meta: {title: '统计报表', breadcrumb: false, activeMenu: '/dzjk/tjbb'},
hidden: false,
redirect: '/dzjk/tjbb/gltj',
children: [
{
path: 'gltj',
component: () => import('@/views/ems/dzjk/tjbb/gltj/index.vue'),
name: 'DzjkTjbbGltj',
hidden: true,
meta: {
title: '运行统计',
breadcrumb: false,
activeMenu: '/dzjk/tjbb',
activeSecondMenuName: 'DzjkTjbb'
},
},
{
path: 'glqx',
component: () => import('@/views/ems/dzjk/tjbb/glqx/index.vue'),
name: 'DzjkTjbbGlqx',
hidden: true,
meta: {
title: '功率曲线',
breadcrumb: false,
activeMenu: '/dzjk/tjbb',
activeSecondMenuName: 'DzjkTjbb'
},
},
{
path: 'pcsqx',
component: () => import('@/views/ems/dzjk/tjbb/pcsqx/index.vue'),
name: 'DzjkTjbbPcsqx',
hidden: true,
meta: {
title: 'PCS曲线',
breadcrumb: false,
activeMenu: '/dzjk/tjbb',
activeSecondMenuName: 'DzjkTjbb'
},
},
{
path: 'dcdqx',
component: () => import('@/views/ems/dzjk/tjbb/dcdqx/index.vue'),
name: 'DzjkTjbbDcdqx',
hidden: true,
meta: {
title: '电池堆曲线',
breadcrumb: false,
activeMenu: '/dzjk/tjbb',
activeSecondMenuName: 'DzjkTjbb'
},
},
{
path: 'dcwd',
component: () => import('@/views/ems/dzjk/tjbb/dcwd/index.vue'),
name: 'DzjkTjbbDcwd',
hidden: true,
meta: {
title: '电池温度',
breadcrumb: false,
activeMenu: '/dzjk/tjbb',
activeSecondMenuName: 'DzjkTjbb'
},
},
{
path: 'dbbb',
component: () => import('@/views/ems/dzjk/tjbb/dbbb/index.vue'),
name: 'DzjkTjbbDbbb',
hidden: true,
meta: {
title: '电表报表',
breadcrumb: false,
activeMenu: '/dzjk/tjbb',
activeSecondMenuName: 'DzjkTjbb'
},
}
]
},
{
path: '/dzjk/clpz',
component: () => import('@/views/ems/dzjk/clpz/clyx/index.vue'),
name: 'DzjkClpz',
meta: {title: '策略配置', breadcrumb: false, activeMenu: '/dzjk/clpz'},
}
]
}
]

View File

@ -89,6 +89,19 @@ 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
...dzjk
]

View File

@ -7,7 +7,7 @@ module.exports = {
/**
* 侧边栏主题 深色主题theme-dark浅色主题theme-light
*/
sideTheme: 'theme-dark',
sideTheme: 'theme-light',
/**
* 系统布局配置

View File

@ -1,4 +1,3 @@
import {getAllSites} from '@/api/ems/zddt'
import {getAlarmDetailList, getSiteAllDeviceCategory} from '@/api/ems/dzjk'
const ems = {
@ -44,14 +43,6 @@ const ems = {
}
},
actions: {
getZdList({commit, state}) {
if (state.zdList.length === 0) {
getAllSites().then(response => {
commit('SET_ZD_LIST', response?.data || [])
console.log('store action getZdList 获取站点数据', state.zdList)
})
}
},
//查询站点的所有待处理0的告警 存在展示红点标志
getSiteAlarmNum({state, commit}, siteId) {
getAlarmDetailList({

View File

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

View File

@ -1,6 +1,6 @@
<template>
<div v-loading="loading" class="ems-dashboard-editor-container ems-content-container-padding">
<div v-loading="loading">
<template v-if="!showTemp">
<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)">

View File

@ -0,0 +1,52 @@
<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

@ -0,0 +1,248 @@
<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

@ -9,14 +9,6 @@
<el-form-item label="soc限制" prop="sdcLimit" required>
<el-switch :active-value="1" :inactive-value="0" v-model="formData.sdcLimit"></el-switch>
</el-form-item>
<!-- <template v-if="formData.sdcLimit === 1">-->
<el-form-item label="soc下限" prop="sdcDown">
<el-input v-model="formData.sdcDown" placeholder="请输入" clearable :style="{width: '100%'}"></el-input>
</el-form-item>
<el-form-item label="soc上限" prop="sdcUp">
<el-input v-model="formData.sdcUp" placeholder="请输入" clearable :style="{width: '100%'}"></el-input>
</el-form-item>
<!-- </template>-->
</el-form>
<el-button type="primary" size="mini" @click="addTime">新增</el-button>
<!-- 新增时间段表单-->
@ -64,6 +56,12 @@
<el-input v-model="formInline.chargeDischargePower" placeholder="请输入"
:style="{width: '100%'}"></el-input>
</el-form-item>
<el-form-item label="soc下限" prop="sdcDown">
<el-input v-model="formInline.sdcDown" placeholder="请输入" clearable :style="{width: '100%'}"></el-input>
</el-form-item>
<el-form-item label="soc上限" prop="sdcUp">
<el-input v-model="formInline.sdcUp" placeholder="请输入" clearable :style="{width: '100%'}"></el-input>
</el-form-item>
<el-form-item label="充电状态" prop="chargeStatus">
<el-select v-model="formInline.chargeStatus" placeholder="请选择" :style="{width: '100%'}">
<el-option v-for="(value,key) in chargeStatusOptions" :key="key+'chargeStatusOptions'" :label="value"
@ -84,20 +82,100 @@
<el-table-column
prop="startTime"
label="开始时间">
<template slot-scope="scope">
<el-time-select
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"
label="结束时间">
<template slot-scope="scope">
<el-time-select
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"
label="充放功率kW">
<template slot-scope="scope">
<el-input
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"
label="充电状态">
<template slot-scope="scope">
{{ chargeStatusOptions[scope.row.chargeStatus] }}
<el-select
v-if="mode === 'edit'"
v-model="scope.row.chargeStatus"
placeholder="请选择"
: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
@ -138,8 +216,6 @@ export default {
formData: {
templateName: '',
sdcLimit: false,
sdcDown: '',
sdcUp: '',
},
rules: {
templateName: [{
@ -147,19 +223,13 @@ export default {
message: '请输入',
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,
formInline: {
timeRange: range,
chargeDischargePower: '',
sdcDown: '',
sdcUp: '',
chargeStatus: ''
},
formInlineRule: {
@ -175,6 +245,14 @@ export default {
},
{pattern: /^-?\d*\.?\d*$/, message: '请输入合法数字或小数'}
],
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: '请输入合法数字或小数'}
],
chargeStatus: [{
required: true,
message: '请选择充放状态',
@ -198,11 +276,9 @@ export default {
this.formData = {
templateName: '',
sdcLimit: false,
sdcDown: '',
sdcUp: '',
}
this.formInline = {
timeRange: this.secondRange, chargeDischargePower: '', chargeStatus: ''
timeRange: this.secondRange, chargeDischargePower: '', sdcDown: '', sdcUp: '', chargeStatus: ''
}//startTime: '', endTime: '',
this.showAddTime = false
this.tableData = []
@ -216,14 +292,12 @@ export default {
getStrategyTempDetail(this.editTempId).then(response => {
const data = JSON.parse(JSON.stringify(response?.data || []));
if (data.length > 0) {
const {templateName, sdcLimit, sdcDown, sdcUp} = JSON.parse(JSON.stringify(data[0]));
const {templateName, sdcLimit} = JSON.parse(JSON.stringify(data[0]));
this.formData.templateName = templateName
this.formData.sdcLimit = sdcLimit
this.formData.sdcDown = sdcDown
this.formData.sdcUp = sdcUp
}
if (data.length === 1) {
const {startTime, endTime} = data;
const {startTime, endTime} = data[0];
if (!startTime || !endTime) {
this.tableData = []
} else {
@ -242,15 +316,15 @@ export default {
cancelAddTime() {
this.$refs.addTimeForm.resetFields()
this.showAddTime = false
this.formInline = {timeRange: this.secondRange, chargeDischargePower: '', chargeStatus: ''}//startTime: '', endTime: '',
this.formInline = {timeRange: this.secondRange, chargeDischargePower: '', sdcDown: '', sdcUp: '', chargeStatus: ''}//startTime: '', endTime: '',
},
saveTime() {
//表单校验校验成功添加到tableData里
this.$refs.addTimeForm.validate(valid => {
if (!valid) return
const {timeRange: [startTime, endTime], chargeDischargePower, chargeStatus} = this.formInline
const {timeRange: [startTime, endTime], chargeDischargePower, sdcDown, sdcUp, chargeStatus} = this.formInline
this.tableData.push({startTime, endTime, chargeDischargePower, chargeStatus})
this.tableData.push({startTime, endTime, chargeDischargePower, sdcDown, sdcUp, chargeStatus})
this.$nextTick(() => {
this.cancelAddTime()
})
@ -262,9 +336,14 @@ export default {
saveDialog() {
this.$refs.addTempForm.validate(valid => {
if (!valid) return
const {templateName, sdcLimit, sdcDown, sdcUp} = this.formData
const {templateName, sdcLimit} = this.formData
const {siteId, updateStrategyId} = this.$home
const {tableData} = this
const tableData = this.tableData.map(item => ({
...item,
sdcDown: this.normalizeSocValue(item.sdcDown),
sdcUp: this.normalizeSocValue(item.sdcUp)
}))
if (!this.validateTableData(tableData)) return
if (this.mode === 'edit') {
editStrategyTemp({
siteId,
@ -272,8 +351,6 @@ export default {
templateId: this.editTempId,
templateName,
sdcLimit,
sdcDown,
sdcUp,
timeConfigList: tableData
}).then(response => {
if (response?.code === 200) {
@ -288,8 +365,6 @@ export default {
strategyId: updateStrategyId,
templateName,
sdcLimit,
sdcDown,
sdcUp,
timeConfigList: tableData
}).then(response => {
if (response?.code === 200) {
@ -300,14 +375,64 @@ export default {
}
})
},
normalizeSocValue(value) {
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.formData = {
templateName: '',
sdcLimit: 0,
sdcDown: '',
sdcUp: '',
}
this.tableData = []
this.cancelAddTime()
@ -322,4 +447,4 @@ export default {
max-height: 90vh;
overflow-y: auto;
}
</style>
</style>

View File

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

View File

@ -2,7 +2,7 @@
<template>
<!-- <cl-container :hideSettingBtn="true">-->
<!-- <template v-slot:default>-->
<div class="ems-dashboard-editor-container ems-content-container-padding">
<div>
<temp-table ref="tempTable" @updateTimeSetting="updateTimeSetting"/>
<time-setting ref="timeSetting"/>
<temp-power-chart ref="tomePowerChart"/>

View File

@ -1,5 +1,5 @@
<template>
<div v-loading="loading" class="ems-dashboard-editor-container ems-content-container-padding">
<el-card v-loading="loading" gshadow="always" class="common-card-container common-card-container-no-title-bg">
<!-- 搜索栏-->
<el-form :inline="true" class="select-container">
<el-form-item label="设备清单">
@ -86,19 +86,29 @@
label="状态">
<template slot-scope="scope">
<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' : ''">{{
$store.state.ems.alarmStatusOptions[scope.row.status]
}}</span>
</template>
</el-table-column>
<el-table-column
label="工单"
fixed="right"
width="250"
width="320"
>
<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="createTicket(scope.row.id)">生成工单</el-button>
<el-button
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>
@ -116,12 +126,12 @@
>
</el-pagination>
</div>
</div>
</el-card>
</template>
<script>
import {createTicketNo, getAlarmDetailList} from '@/api/ems/dzjk'
import {closeAlarm, createTicketNo, getAlarmDetailList} from '@/api/ems/dzjk'
import {getDeviceList} from '@/api/ems/site'
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {formatDate} from '@/filters/ems'
@ -167,6 +177,23 @@ export default {
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) {
const date1 = new Date(day1), date2 = new Date(day2)
@ -248,4 +275,3 @@ export default {
}
}
</script>

View File

@ -12,12 +12,18 @@
import * as echarts from 'echarts'
import resize from '@/mixins/ems/resize'
import DateRangeSelect from '@/components/Ems/DateRangeSelect/index.vue'
import {getPointData} from '@/api/ems/dzjk'
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,
@ -26,6 +32,13 @@ export default {
isInit: true
}
},
watch: {
displayData() {
if (this.siteId && this.timeRange.length === 2) {
this.getGVQXData()
}
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
@ -46,11 +59,34 @@ export default {
this.isInit = false
},
getGVQXData() {
this.showLoading()
const {siteId, timeRange} = this
getPointData({siteId, startDate: timeRange[0], endDate: timeRange[1]}).then(response => {
this.setOption(response?.data || [])
}).finally(() => this.hideLoading())
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) {
//初始化 清空数据
@ -64,18 +100,18 @@ export default {
initChart() {
this.chart = echarts.init(document.querySelector('#activeChart'))
},
showLoading() {
this.chart && this.chart.showLoading()
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'}`
},
hideLoading() {
this.chart && this.chart.hideLoading()
parseToTimestamp(value) {
if (!value) return null
const t = new Date(value).getTime()
return Number.isNaN(t) ? null : t
},
setOption(data) {
const source = [['日期', '电网功率', '负载功率', '储能功率', '光伏功率', 'soc平均值', 'soh平均值', '电池平均温度平均值']]
console.log('source.slice(1)', source[0].slice(1))
this.chart && data.forEach((item) => {
source.push([item.statisDate, item.gridPower, item.loadPower, item.storagePower, item.pvPower, item.avgSoc, item.avgSoh, item.avgTemp])
})
setOption(seriesData = []) {
this.chart.setOption({
grid: {
containLabel: true
@ -86,35 +122,28 @@ export default {
},
tooltip: {
trigger: 'axis',
axisPointer: { // 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
axisPointer: { type: 'cross' }
},
textStyle: {
color: "#333333",
},
xAxis: {
type: 'category',
type: 'time',
},
yAxis: [
{
type: 'value',
},
{
type: 'value',
},
],
dataset: {source},
series: source[0].slice(1).map((item, index) => {
yAxis: [{
type: 'value',
}],
series: seriesData.map((item) => {
return {
type: 'line',//index === 5 ? 'bar' : 'line',
name: item.name,
type: 'line',
showSymbol: false,
symbolSize: 2,
smooth: true,
areaStyle: {
opacity: 0.5,
},
yAxisIndex: index <= 4 ? 0 : 1
data: item.data
}
})
})
@ -124,4 +153,3 @@ export default {
}
</script>

View File

@ -12,16 +12,29 @@
import * as echarts from 'echarts'
import resize from '@/mixins/ems/resize'
import DateRangeSelect from '@/components/Ems/DateRangeSelect/index.vue'
import {getSevenChargeData} from '@/api/ems/dzjk'
import {getPointConfigCurve} from '@/api/ems/site'
export default {
mixins: [resize],
components: {DateRangeSelect},
props: {
displayData: {
type: Array,
default: () => []
}
},
data() {
return {
chart: null,
timeRange: [],
siteId: '',
siteId: ''
}
},
watch: {
displayData() {
if (this.siteId && this.timeRange.length === 2) {
this.getWeekKData()
}
}
},
mounted() {
@ -43,11 +56,34 @@ export default {
this.getWeekKData()
},
getWeekKData() {
this.showLoading()
const {siteId, timeRange} = this
getSevenChargeData({siteId, startDate: timeRange[0], endDate: timeRange[1]}).then(response => {
this.setOption(response?.data || [])
}).finally(() => this.hideLoading())
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) {
//初始化 清空数据
@ -59,24 +95,23 @@ export default {
initChart() {
this.chart = echarts.init(document.querySelector('#weekChart'))
},
showLoading() {
this.chart && this.chart.showLoading()
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'}`
},
hideLoading() {
this.chart && this.chart.hideLoading()
parseToTimestamp(value) {
if (!value) return null
const t = new Date(value).getTime()
return Number.isNaN(t) ? null : t
},
setOption(data, unit) {
const source = [['日期', '充电量', '放电量']]
data.forEach(item => {
source.push([item.ammeterDate, item.chargedCap, item.disChargedCap])
})
setOption(seriesData = []) {
this.chart && this.chart.setOption({
color: ['#4472c4', '#70ad47'],//所有充放电颜色保持统一
tooltip: {
trigger: 'axis',
axisPointer: { // 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
axisPointer: { type: 'cross' }
},
grid: {
containLabel: true
@ -86,9 +121,7 @@ export default {
bottom: '15',
},
xAxis: {
type: 'category',
name: unit,
nameLocation: 'center'
type: 'time'
},
yAxis: [{
type: 'value',
@ -100,19 +133,12 @@ export default {
onZero: false
}
}],
dataset: {
source
},
series: [
{
yAxisIndex: 0,
type: 'bar',
},
{
yAxisIndex: 0,
type: 'bar',
},
]
series: seriesData.map(item => ({
name: item.name,
yAxisIndex: 0,
type: 'bar',
data: item.data
}))
})
}
}

View File

@ -1,8 +1,5 @@
<template>
<div
class="ems-dashboard-editor-container ems-content-container-padding"
v-loading="loading"
>
<div>
<el-row style="background: #fff" class="row-container" :gutter="15">
<el-col :xs="24" :sm="24" :lg="5">
<!-- 站点信息-->
@ -24,13 +21,19 @@
<div class="title">
<i class="el-icon-location"></i>
</div>
<div class="value">{{ info.siteAddress }}</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">{{ info.runningTime || '-' }}</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;">
@ -39,9 +42,10 @@
class="sjgl-col power-col"
>
<div class="sjgl-wrapper">
<div class="sjgl-title">装机功率(MW)</div>
<div class="sjgl-title">装机功率(MWh)</div>
<div class="sjgl-value">
{{ info.installPower | formatNumber }}
<i v-if="isBaseInfoLoading" class="el-icon-loading"></i>
<span v-else>{{ info.installPower | formatNumber }}</span>
</div>
</div>
</el-col>
@ -50,9 +54,10 @@
class="sjgl-col power-col"
>
<div class="sjgl-wrapper">
<div class="sjgl-title">装机容量(MW)</div>
<div class="sjgl-title">装机容量(MWh)</div>
<div class="sjgl-value">
{{ info.installCapacity | formatNumber }}
<i v-if="isBaseInfoLoading" class="el-icon-loading"></i>
<span v-else>{{ info.installCapacity | formatNumber }}</span>
</div>
</div>
</el-col>
@ -69,8 +74,15 @@
<div slot="header">
<span class="card-title">总累计运行数据</span>
<div class="total-count">
<span class="title">总收入</span>
<span class="value">{{ runningInfo.totalRevenue | formatNumber }}</span>
<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>
@ -80,14 +92,19 @@
<el-row :gutter="10">
<el-col
:span="6"
v-for="(item, index) in sjglData"
v-for="(item, index) in runningDataCards"
:key="index + 'sjglData'"
class="sjgl-col"
>
<div class="sjgl-wrapper">
<div
class="sjgl-wrapper pointer-field"
:class="{ 'field-disabled': !hasPointId(item.raw) }"
@click="handleRunningFieldClick(item)"
>
<div class="sjgl-title">{{ item.title }}</div>
<div class="sjgl-value" :style="{color:item.color}">
{{ runningInfo[item.attr] | formatNumber }}
<i v-if="item.loading" class="el-icon-loading"></i>
<span v-else>{{ item.value | formatNumber }}</span>
</div>
</div>
</el-col>
@ -96,18 +113,46 @@
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :lg="12">
<week-chart ref="weekChart"/>
<week-chart ref="weekChart" :display-data="runningDisplayData"/>
</el-col>
<el-col :xs="24" :sm="24" :lg="12">
<active-chart ref="activeChart"/>
<active-chart ref="activeChart" :display-data="runningDisplayData"/>
</el-col>
</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>
</template>
<script>
import * as echarts from "echarts";
import {getSingleSiteBaseInfo} from "@/api/ems/zddt";
import {getDzjkHomeView} from "@/api/ems/dzjk";
import {getDzjkHomeTotalView, getProjectDisplayData} from "@/api/ems/dzjk";
import {getPointConfigCurve} from "@/api/ems/site";
import WeekChart from "./WeekChart.vue";
import ActiveChart from "./ActiveChart.vue";
import AlarmTable from "./AlarmTable.vue";
@ -122,7 +167,24 @@ export default {
data() {
return {
loading: false,
sjglData: [
baseInfoLoading: false,
runningInfoLoading: false,
runningUpdateSpinning: false,
runningUpdateTimer: null,
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
fallbackSjglData: [
{
title: "今日充电量kWh",
attr: "dayChargedCap",
@ -166,14 +228,246 @@ export default {
],
info: {}, //基本信息
runningInfo: {}, //总累计运行数据+报警表格
runningDisplayData: [], //单站监控项目配置展示数据
};
},
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: item.fieldValue,
color: this.getCardColor(index),
loading: this.isRunningInfoLoading,
raw: item,
}));
}
return this.fallbackSjglData.map(item => ({
title: item.title,
value: this.runningInfo[item.attr],
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: {
hasPointId(item) {
return !!String(item?.dataPoint || "").trim();
},
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];
},
toAlarm() {
this.$router.push({path: "/dzjk/gzgj", query: this.$route.query});
},
@ -183,21 +477,76 @@ export default {
});
},
getRunningInfo() {
return getDzjkHomeView(this.siteId).then((response) => {
this.runningInfo = response?.data || {};
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),
]).then(([homeResponse, displayResponse]) => {
const nextRunningInfo = homeResponse?.data || {};
const nextRunningDisplayData = displayResponse?.data || [];
const changed = hasOldData && this.hasTotalRunningChanged(nextRunningInfo, nextRunningDisplayData);
this.runningInfo = nextRunningInfo;
this.runningDisplayData = nextRunningDisplayData;
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) {
const oldSnapshot = this.getTotalRunningSnapshot(this.runningInfo, this.runningDisplayData);
const newSnapshot = this.getTotalRunningSnapshot(nextRunningInfo, nextRunningDisplayData);
return JSON.stringify(oldSnapshot) !== JSON.stringify(newSnapshot);
},
getTotalRunningSnapshot(runningInfo, runningDisplayData) {
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;
snapshot[`cfg:${key}`] = this.normalizeRunningCompareValue(item.fieldValue);
});
return snapshot;
}
this.fallbackSjglData.forEach(item => {
snapshot[`fallback:${item.attr}`] = this.normalizeRunningCompareValue(runningInfo?.[item.attr]);
});
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.loading = true;
// 功率曲线
this.$refs.activeChart.init(this.siteId);
// 一周冲放曲线
this.$refs.weekChart.init(this.siteId);
// 静态信息 this.getBaseInfo()
// 总累计运行数据+故障告警 this.getRunningInfo()
Promise.all([this.getBaseInfo(), this.getRunningInfo()]).finally(() => {
this.loading = false;
});
Promise.all([this.getBaseInfo(), this.getRunningInfo()]);
// 一分钟循环一次总累计运行数据
this.updateInterval(this.getRunningInfo);
},
@ -273,6 +622,22 @@ export default {
}
}
.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;

View File

@ -2,7 +2,6 @@
<template>
<div class="ems-dashboard-editor-container">
<zd-select :get-list-by-store="true" :default-site-id="$route.query.siteId" @submitSite="submitSite"/>
<el-menu
class="ems-second-menu"
:default-active="$route.meta.activeSecondMenuName"
@ -28,11 +27,8 @@
<script>
import { dzjk } from '@/router/ems'
const childrenRoute = dzjk[0].children[0].children//获取到单站监控下面的字路由
console.log('childrenRoute',childrenRoute)
import ZdSelect from '@/components/Ems/ZdSelect/index.vue'
import {mapState} from "vuex";
export default {
components:{ZdSelect},
data(){
return {
childrenRoute,
@ -44,18 +40,6 @@ export default {
dzjkAlarmLighting:state=>state.ems.dzjkAlarmLighting
})
},
methods:{
submitSite(id){
if(id !== this.$route.query.siteId){
// console.log('单站监控选择了其他的站点id=',id,'并更新页面地址参数')
this.$router.push({query:{...this.$route.query,siteId:id}})
}else{
// console.log('单站监控选择了相同的其他的站点id=',id,'页面地址不发生改变')
}
//获取告警列表数据
this.$store.dispatch('getSiteAlarmNum',id)
}
},
beforeRouteLeave(to,from, next){
//从单站监控下面的所有子页面跳出时会触发
// 清空store中的zdList 保障下次进入到单站监控会重新调用接口获取数据
@ -66,6 +50,9 @@ export default {
</script>
<style scoped lang="scss">
.ems-dashboard-editor-container{
padding-top: 12px;
}
.dzjk-ems-content-container{
margin-top:0;
min-height: 60vh;

View File

@ -2,8 +2,14 @@
<template>
<!-- 6个方块-->
<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 singleZdSqaure" :key="index+'singleSquareBox'">
<single-square-box :data="{...item,value:formatNumber(data[item.attr])}" ></single-square-box>
<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'">
<div
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-row>
</template>
@ -11,52 +17,74 @@
<script>
import SingleSquareBox from "@/components/Ems/SingleSquareBox/index.vue";
import {formatNumber} from '@/filters/ems'
export default {
components:{SingleSquareBox},
props:{
data:{
type:Object,
required:false,
default:()=>{return {}}
displayData: {
type: Array,
required: false,
default: () => [],
},
loading: {
type: Boolean,
required: false,
default: false,
},
},
methods:{formatNumber},
data() {
return {
// 单个电站 四个方块数据
singleZdSqaure:[{
title:'实时有功功率kW',
value:'',
bgColor:'#FFF2CB',
attr:'totalActivePower'
},{
title:'实时无功功率kVar',
value:'',
bgColor:'#CBD6FF',
attr:'totalReactivePower'
},{
title:'电池堆SOC',
value:'',
bgColor:'#DCCBFF',
attr:'soc'
},{
title:'电池堆SOH',
value:'',
bgColor:'#FFD4CB',
attr:'soh'
},{
title:'今日充电量kWh',
value:'',
bgColor:'#FFD6F8',
attr:'dayChargedCap'
},{
title:'今日放电量kWh',
value:'',
bgColor:'#E1FFCA',
attr:'dayDisChargedCap'
}]
}
computed: {
displaySquares() {
const sourceList = (this.displayData || []).filter((item) => {
if (!item) return false;
return item.menuCode === "SBJK_SSYX" || item.sectionName === "运行概览";
});
const sourceMap = {};
sourceList.forEach((item) => {
if (!item) return;
const key = this.getFieldName(item.fieldCode);
if (key) {
sourceMap[key] = item;
}
});
const defaults = [
{fieldCode: "totalActivePower", fieldName: "实时有功功率kW"},
{fieldCode: "totalReactivePower", fieldName: "实时无功功率kVar"},
{fieldCode: "soc", fieldName: "电池堆SOC"},
{fieldCode: "soh", fieldName: "电池堆SOH"},
{fieldCode: "dayChargedCap_rt", fieldName: "今日充电量kWh"},
{fieldCode: "dayDisChargedCap_rt", fieldName: "今日放电量kWh"},
];
return defaults.map((def, index) => {
const row = sourceMap[def.fieldCode] || {};
const pointId = String(row.dataPoint || "").trim();
return {
title: row.fieldName || def.fieldName,
value: row.fieldValue,
valueLoading: this.loading && this.isEmptyValue(row.fieldValue),
bgColor: this.getBgColor(index),
pointId,
fieldCode: row.fieldCode || def.fieldCode,
raw: row,
};
});
},
},
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 === "-";
},
},
}
@ -70,4 +98,13 @@ export default {
width: fit-content;
}
}
.square-click-wrapper {
cursor: pointer;
}
.square-click-wrapper.field-disabled {
cursor: not-allowed;
opacity: 0.8;
}
</style>

View File

@ -1,6 +1,28 @@
<template>
<div v-loading="loading">
<div v-for="(baseInfo,index) in baseInfoList" :key="index+'bmsdccContainer'" style="margin-bottom:25px;">
<div>
<div class="pcs-tags">
<el-tag
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)">
@ -29,15 +51,33 @@
<el-descriptions-item
contentClassName="descriptions-direction work-status"
:span="1" label="工作状态">
{{ CLUSTERWorkStatusOptions[baseInfo.workStatus] }}
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'workStatus') }"
@click="handleFieldClick(baseInfo, 'workStatus', '工作状态')"
>
{{ CLUSTERWorkStatusOptions[baseInfo.workStatus] || '-' }}
</span>
</el-descriptions-item>
<el-descriptions-item contentClassName="descriptions-direction"
:span="1" label="与PCS通信">
{{ $store.state.ems.communicationStatusOptions[baseInfo.pcsCommunicationStatus] }}
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'pcsCommunicationStatus') }"
@click="handleFieldClick(baseInfo, 'pcsCommunicationStatus', '与PCS通信')"
>
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.pcsCommunicationStatus] || '-' }}
</span>
</el-descriptions-item>
<el-descriptions-item contentClassName="descriptions-direction"
:span="1" label="与EMS通信">
{{ $store.state.ems.communicationStatusOptions[baseInfo.emsCommunicationStatus] }}
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'emsCommunicationStatus') }"
@click="handleFieldClick(baseInfo, 'emsCommunicationStatus', '与EMS通信')"
>
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.emsCommunicationStatus] || '-' }}
</span>
</el-descriptions-item>
</el-descriptions>
</div>
@ -46,8 +86,14 @@
<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" @click="showChart(item.pointName || '',baseInfo.deviceId)">
{{ baseInfo[item.attr] | formatNumber }} <span v-if="item.unit" v-html="item.unit"></span>
<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>
@ -56,7 +102,11 @@
<div class="process-line-bg">
<div class="process-line" :style="{height:baseInfo.currentSoc+'%'}"></div>
</div>
<div class="process pointer" @click="showChart( '当前SOC',baseInfo.deviceId)">当前SOC :
<div
class="process pointer"
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'currentSoc') }"
@click="handleFieldClick(baseInfo, 'currentSoc', '当前SOC')"
>当前SOC :
{{ baseInfo.currentSoc }}%
</div>
</div>
@ -79,7 +129,8 @@
>
<template slot-scope="scope">
<span class="pointer"
@click="showChart( tablePointNameMap[scope.row.dataName+scope.column.label],baseInfo.deviceId)">{{
: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>
@ -89,7 +140,8 @@
label="单体最小值">
<template slot-scope="scope">
<span class="pointer"
@click="showChart( tablePointNameMap[scope.row.dataName+scope.column.label],baseInfo.deviceId)">{{
: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>
@ -103,7 +155,8 @@
label="单体最大值">
<template slot-scope="scope">
<span class="pointer "
@click="showChart( tablePointNameMap[scope.row.dataName+scope.column.label],baseInfo.deviceId)">{{
: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>
@ -115,33 +168,79 @@
</el-table>
</el-card>
</div>
<el-empty v-show="baseInfoList.length<=0" :image-size="200"></el-empty>
<point-chart ref="pointChart" :site-id="siteId"/>
<point-table ref="pointTable"/>
<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 pointChart from "./../PointChart.vue";
import * as echarts from "echarts";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
import {getBMSBatteryCluster} from '@/api/ems/dzjk'
import {getProjectDisplayData, getStackNameList, getClusterNameList} from '@/api/ems/dzjk'
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import {mapState} from "vuex";
import {getPointConfigCurve} from "@/api/ems/site";
export default {
name: 'DzjkSbjkBmsdcc',
mixins: [getQuerySiteId, intervalUpdate],
components: {PointTable, pointChart},
components: {PointTable},
computed: {
...mapState({
CLUSTERWorkStatusOptions: state => state?.ems?.CLUSTERWorkStatusOptions || {},
})
}),
filteredBaseInfoList() {
if (!this.selectedClusterId) {
return this.baseInfoList || [];
}
return (this.baseInfoList || []).filter(item => item.deviceId === this.selectedClusterId);
},
},
data() {
return {
loading: false,
displayData: [],
clusterDeviceList: [],
selectedClusterId: "",
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
unitObj: {
'电压': 'V',
'温度': '&#8451;',
@ -158,7 +257,15 @@ export default {
'SOC单体平均值': '当前SOC',
'SOC单体最大值': '最高单体SOC',
},
baseInfoList: [],
baseInfoList: [{
siteId: "",
deviceId: "",
parentDeviceName: "",
deviceName: "BMS电池簇",
dataUpdateTime: "-",
alarmNum: 0,
batteryDataList: [],
}],
infoData: [
{label: '簇电压', attr: 'clusterVoltage', unit: 'V', pointName: '簇电压'},
{label: '可充电量', attr: 'chargeableCapacity', unit: 'kWh', pointName: '可充电量'},
@ -173,6 +280,12 @@ export default {
}
},
methods: {
displayValue(value) {
return value === undefined || value === null || value === "" ? "-" : value;
},
isPointLoading(value) {
return this.loading && (value === undefined || value === null || value === "" || value === "-");
},
handleCardClass(item) {
const {workStatus = ''} = item
return !(Object.keys(this.CLUSTERWorkStatusOptions).includes(item.workStatus)) ? "timing-card-container" : workStatus === '9' ? 'warning-card-container' : 'running-card-container'
@ -182,26 +295,338 @@ export default {
const {siteId, deviceId} = row
this.$refs.pointTable.showTable({siteId, deviceId, deviceCategory: 'CLUSTER'}, dataType)
},
showChart(pointName, deviceId) {
pointName && this.$refs.pointChart.showChart({pointName, deviceCategory: 'CLUSTER', deviceId})
hasFieldPointId(baseInfo, fieldName) {
const row = this.getFieldRow(baseInfo, fieldName);
return !!String(row?.dataPoint || "").trim();
},
updateData() {
this.loading = true
getBMSBatteryCluster(this.siteId).then(response => {
this.baseInfoList = JSON.parse(JSON.stringify(response?.data || []));
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.loading = false
})
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
// 先渲染卡片框架,字段值走单点位 loading
this.buildBaseInfoList();
Promise.all([
getProjectDisplayData(this.siteId),
this.getClusterDeviceList(),
]).then(([response]) => {
this.displayData = response?.data || [];
this.buildBaseInfoList();
}).finally(() => {
this.loading = false
})
}
}
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
}
</script>
<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 {
//描述列表样式
.descriptions-main {
@ -247,4 +672,16 @@ export default {
}
}
.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,6 +1,28 @@
<template>
<div v-loading="loading">
<div v-for="(baseInfo,index) in baseInfoList" :key="index+'bmszlContainer'" style="margin-bottom:25px;">
<div>
<div class="pcs-tags">
<el-tag
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"
@ -27,15 +49,21 @@
<el-descriptions-item
contentClassName="descriptions-direction work-status"
label="工作状态" labelClassName="descriptions-label">
{{ STACKWorkStatusOptions[baseInfo.workStatus] }}
<span class="pointer" @click="handleStatusFieldClick(baseInfo, 'workStatus', '工作状态')">
{{ STACKWorkStatusOptions[baseInfo.workStatus] || '-' }}
</span>
</el-descriptions-item>
<el-descriptions-item :span="1" contentClassName="descriptions-direction" label="与PCS通信"
labelClassName="descriptions-label">
{{ $store.state.ems.communicationStatusOptions[baseInfo.pcsCommunicationStatus] }}
<span class="pointer" @click="handleStatusFieldClick(baseInfo, 'pcsCommunicationStatus', '与PCS通信')">
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.pcsCommunicationStatus] || '-' }}
</span>
</el-descriptions-item>
<el-descriptions-item :span="1" contentClassName="descriptions-direction" label="与EMS通信"
labelClassName="descriptions-label">
{{ $store.state.ems.communicationStatusOptions[baseInfo.emsCommunicationStatus] }}
<span class="pointer" @click="handleStatusFieldClick(baseInfo, 'emsCommunicationStatus', '与EMS通信')">
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.emsCommunicationStatus] || '-' }}
</span>
</el-descriptions-item>
</el-descriptions>
</div>
@ -44,8 +72,10 @@
<el-descriptions-item v-for="(item,index) in infoData" :key="index+'pcsInfoData'" :label="item.label"
:span="1" contentClassName="descriptions-direction"
labelClassName="descriptions-label">
<span class="pointer" @click="showChart(item.pointName || '',baseInfo.deviceId)">
{{ baseInfo[item.attr] | formatNumber }}<span v-if="item.unit" v-html="item.unit"></span>
<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>
@ -54,143 +84,113 @@
<div class="process-line-bg">
<div :style="{height:baseInfo.stackSoc+'%'}" class="process-line"></div>
</div>
<div class="process pointer" @click="showChart('当前SOC',baseInfo.deviceId)">当前SOC :
<div class="process pointer" @click="handleStackSocClick(baseInfo)">当前SOC :
{{ baseInfo.stackSoc }}%
</div>
</div>
</div>
<el-table
:data="baseInfo.batteryDataList"
class="common-table"
max-height="500"
stripe
style="width: 100%;margin-top:25px;">
<el-table-column
label="簇号"
prop="clusterId">
</el-table-column>
<el-table-column
label="簇电压"
>
<template slot-scope="scope">
<span class="pointer"
@click="showChart('簇电压',scope.row.clusterId,'CLUSTER')">{{ scope.row.clusterVoltage }} V</span>
</template>
</el-table-column>
<el-table-column
label="簇电流">
<template slot-scope="scope">
<span class="pointer"
@click="showChart('簇电流',scope.row.clusterId,'CLUSTER')">{{ scope.row.clusterCurrent }} A</span>
</template>
</el-table-column>
<el-table-column
label="簇SOC">
<template slot-scope="scope">
<span class="pointer"
@click="showChart('当前SOC',scope.row.clusterId,'CLUSTER')">{{ scope.row.currentSoc }} %</span>
</template>
</el-table-column>
<el-table-column
label="单体最高电压"
prop="maxVoltage">
<template slot-scope="scope">
<span class="pointer"
@click="showChart('最高单体电压',scope.row.clusterId,'CLUSTER')">{{
scope.row.maxCellVoltage
}} V</span>
</template>
</el-table-column>
<el-table-column
label="电池号码"
prop="maxCellVoltageId">
</el-table-column>
<el-table-column
label="单体最低电压"
prop="minVoltage">
<template slot-scope="scope">
<span class="pointer"
@click="showChart('最低单体电压',scope.row.clusterId,'CLUSTER')">{{
scope.row.minCellVoltage
}} V</span>
</template>
</el-table-column>
<el-table-column
label="电池号码"
prop="minCellVoltageId">
</el-table-column>
<el-table-column
label="单体最高温度">
<template slot-scope="scope">
<span class="pointer"
@click="showChart('最高单体温度',scope.row.clusterId,'CLUSTER')">{{
scope.row.maxCellTemp
}} &#8451;</span>
</template>
</el-table-column>
<el-table-column
label="电池号码"
prop="maxCellTempId">
</el-table-column>
<el-table-column
label="单体最低温度"
prop="minTemperature">
<template slot-scope="scope">
<span class="pointer"
@click="showChart('最低单体温度',scope.row.clusterId,'CLUSTER')">{{
scope.row.minCellTemp
}} &#8451;</span>
</template>
</el-table-column>
<el-table-column
label="电池号码"
prop="minCellTempId">
</el-table-column>
</el-table>
</el-card>
</div>
<el-empty v-show="baseInfoList.length<=0" :image-size="200"></el-empty>
<point-chart ref="pointChart" :site-id="siteId"/>
<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>
<point-table ref="pointTable"/>
</div>
</template>
<script>
import {getBMSOverView} from '@/api/ems/dzjk'
import * as echarts from "echarts";
import {getProjectDisplayData, getStackNameList} from '@/api/ems/dzjk'
import {getPointConfigCurve} from "@/api/ems/site";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import pointChart from "./../PointChart.vue";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
import {mapState} from "vuex";
export default {
name: 'DzjkSbjkBmszl',
components: {pointChart, PointTable},
components: {PointTable},
mixins: [getQuerySiteId, intervalUpdate],
computed: {
...mapState({
STACKWorkStatusOptions: state => state?.ems?.STACKWorkStatusOptions || {},
})
}),
filteredBaseInfoList() {
if (!this.selectedStackId) {
return this.baseInfoList || [];
}
return (this.baseInfoList || []).filter(item => item.deviceId === this.selectedStackId);
},
},
data() {
return {
loading: false,
baseInfoList: [],
displayData: [],
stackDeviceList: [],
selectedStackId: "",
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
baseInfoList: [{
siteId: "",
deviceId: "",
deviceName: "BMS总览",
dataUpdateTime: "-",
alarmNum: 0,
batteryDataList: [],
}],
infoData: [
{label: '电池堆总电压', attr: 'stackVoltage', unit: 'V', pointName: '电池堆电压'},
{label: '可充电量', attr: 'availableChargeCapacity', unit: 'kWh', pointName: '可充电量'},
{label: '累计充电量', attr: 'totalChargeCapacity', unit: 'kWh', pointName: '累计充电量'},
{label: '电池堆总电流', attr: 'stackCurrent', unit: 'A', pointName: '电池堆总电流'},
{label: '可放电量', attr: 'availableDischargeCapacity', unit: 'kWh', pointName: '可放电量'},
{label: '累计放电量', attr: 'totalDischargeCapacity', unit: 'kWh', pointName: '累计放电量'},
{label: 'SOH', attr: 'stackSoh', unit: '%', pointName: 'SOH'},
{label: '平均温度', attr: 'operatingTemp', unit: '&#8451;', pointName: '平均温度'},
{label: '绝缘电阻', attr: 'stackInsulationResistance', unit: '&Omega;', pointName: '绝缘电阻'},
{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: {
displayValue(value) {
return value === undefined || value === null || value === "" ? "-" : value;
},
isPointLoading(value) {
return this.loading && (value === undefined || value === null || value === "" || value === "-");
},
handleCardClass(item) {
const {workStatus = ''} = item
return !Object.keys(this.STACKWorkStatusOptions).find(i => i === workStatus) ? "timing-card-container" : workStatus === '9' ? 'warning-card-container' : 'running-card-container'
@ -201,27 +201,332 @@ export default {
const {siteId, deviceId} = row
this.$refs.pointTable.showTable({siteId, deviceId, deviceCategory: 'STACK'}, dataType)
},
showChart(pointName, deviceId, deviceCategory = 'STACK') {
pointName && this.$refs.pointChart.showChart({pointName, deviceCategory, deviceId})
handleStatusFieldClick(baseInfo, fieldKey, title) {
const pointId = this.resolvePointId(baseInfo, fieldKey, "status");
this.openCurveDialogByPointId(pointId, title || fieldKey);
},
updateData() {
this.loading = true
getBMSOverView(this.siteId).then(response => {
this.baseInfoList = JSON.parse(JSON.stringify(response?.data || []));
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.loading = false
})
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(),
]).then(([displayResponse]) => {
this.displayData = displayResponse?.data || [];
this.buildBaseInfoList();
}).finally(() => {
this.loading = false
})
},
}
,
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
}
</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 {
cursor: pointer;
}
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
::v-deep {
//描述列表样式
.descriptions-main {
@ -267,4 +572,16 @@ export default {
}
}
.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,228 +1,436 @@
<template>
<div v-loading="loading">
<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="(item,index) in list"
:key="index+'dbList'"
shadow="always"
class="sbjk-card-container list"
:class="{
'timing-card-container':!['0','2'].includes(item.emsCommunicationStatus),
'warning-card-container':item.emsCommunicationStatus === '2',
'running-card-container':item.emsCommunicationStatus === '0'
}"
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">{{ item.deviceName }}</span>
<span class="large-title">{{ group.displayName || "电表" }}</span>
<div class="info">
<div>
{{
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>状态{{ group.statusText }}</div>
<div>数据更新时间{{ group.updateTimeText }}</div>
</div>
</div>
<el-row class="device-info-row">
<el-col v-for="(tempDataItem,tempDataIndex) in (deviceIdTypeMsg[item.deviceId] || otherTypeMsg)"
:key="tempDataIndex+'dbTempData'"
:span="8" class="device-info-col">
<span class="pointer" @click="showChart(tempDataItem.pointName,item.deviceId)">
<span class="left">{{ tempDataItem.name }}</span> <span class="right">{{ item[tempDataItem.attr] || '-' }}<span
v-html="tempDataItem.unit"></span></span>
<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-empty v-show="list.length<=0" :image-size="200"></el-empty>
<point-chart ref="pointChart" :site-id="siteId"/>
<point-table ref="pointTable"/>
<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 pointChart from "./../PointChart.vue";
import * as echarts from "echarts";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getAmmeterDataList} from "@/api/ems/dzjk";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
import {mapState} from "vuex";
import { getProjectDisplayData } from "@/api/ems/dzjk";
import { getDeviceList, getPointConfigCurve } from "@/api/ems/site";
export default {
name: "DzjkSbjkDb",
mixins: [getQuerySiteId, intervalUpdate],
components: {PointTable, pointChart},
computed: {
...mapState({
communicationStatusOptions: state => state?.ems?.communicationStatusOptions || {},
})
},
data() {
return {
loading: false,
list: [],
deviceIdTypeMsg: {
'LOAD': [
{
name: '正向有功电能',
attr: 'forwardActive',
pointName: '正向有功电能',
unit: 'kWh'
},
{
name: '反向有功电能',
attr: 'reverseActive',
pointName: '反向有功电能',
unit: 'kWh'
},
{
name: '正向无功电能',
attr: 'forwardReactive',
pointName: '正向无功电能',
unit: 'kvarh'
},
{
name: '反向无功电能',
attr: 'reverseReactive',
pointName: '反向无功电能',
unit: 'kvarh'
},
{
name: '有功功率',
attr: 'activePower',
pointName: '总有功功率',
unit: 'kW'
},
{
name: '无功功率',
attr: 'reactivePower',
pointName: '总无功功率',
unit: 'kvar'
}
],
'METE': [
{
name: '正向有功电能',
attr: 'forwardActive',
pointName: '正向有功电能',
unit: 'kWh'
},
{
name: '反向有功电能',
attr: 'reverseActive',
pointName: '反向有功电能',
unit: 'kWh'
},
{
name: '正向无功电能',
attr: 'forwardReactive',
pointName: '正向无功电能',
unit: 'kvarh'
},
{
name: '反向无功电能',
attr: 'reverseReactive',
pointName: '反向无功电能',
unit: 'kvarh'
},
{
name: '有功功率',
attr: 'activePower',
pointName: '总有功功率',
unit: 'kW'
},
{
name: '无功功率',
attr: 'reactivePower',
pointName: '总无功功率',
unit: 'kvar'
}
],
'METEGF': [
{
name: '有功电能',
attr: 'activeEnergy',
pointName: '有功电能',
unit: 'kWh'
},
{
name: '无功电能',
attr: 'reactiveEnergy',
pointName: '无功电能',
unit: 'kvarh'
},
{
name: '有功功率',
attr: 'activePower',
pointName: '总有功功率',
unit: 'kW'
},
{
name: '无功功率',
attr: 'reactivePower',
pointName: '总无功功率',
unit: 'kvar'
}
]
displayData: [],
selectedSectionKey: "",
ammeterDeviceList: [],
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
otherTypeMsg: [
{
name: '正向有功电能',
attr: 'forwardActive',
pointName: '正向有功电能',
unit: 'kWh'
},
{
name: '反向有功电能',
attr: 'reverseActive',
pointName: '反向有功电能',
unit: 'kWh'
},
{
name: '有功功率',
attr: 'activePower',
pointName: '总有功功率',
unit: 'kW'
},
]
};
},
methods: {
// 查看设备电位表格
pointDetail(row, dataType) {
const {deviceId} = row
this.$refs.pointTable.showTable({siteId: this.siteId, deviceId, deviceCategory: 'AMMETER'}, dataType)
computed: {
moduleDisplayData() {
return (this.displayData || []).filter((item) => item.menuCode === "SBJK_DB");
},
showChart(pointName, deviceId) {
pointName && this.$refs.pointChart.showChart({pointName, deviceCategory: 'AMMETER', deviceId})
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;
getAmmeterDataList(this.siteId)
.then((response) => {
this.list = response?.data || []
})
.finally(() => {
this.loading = false;
});
Promise.all([getProjectDisplayData(this.siteId), this.getAmmeterDeviceList()])
.then(([response]) => {
this.displayData = response?.data || [];
})
.finally(() => {
this.loading = false;
});
},
init() {
this.updateData()
this.updateInterval(this.updateData)
this.updateData();
this.updateInterval(this.updateData);
},
},
mounted() {
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
};
</script>
@ -232,6 +440,58 @@ export default {
&.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,5 +1,5 @@
<template>
<div v-loading="loading">
<div>
<el-card
v-for="(item,index) in list"
:key="index+'ylLise'"
@ -26,9 +26,11 @@
<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">{{ item[tempDataItem.attr] || '-' }}<span
v-html="tempDataItem.unit"></span></span>
<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>
@ -62,6 +64,12 @@ export default {
}
},
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
@ -117,4 +125,16 @@ export default {
}
}
}
.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

@ -15,10 +15,10 @@
{{ item.clusterDeviceId }}
</div>
<div>#{{ item.deviceId }}</div>
<div class="dy pointer" @click="chartDetail(item, '电压 (V)')">
<div class="dy pointer" @click="chartDetail(item, 'voltage')">
{{ item.voltage }}V
</div>
<div class="wd pointer" @click="chartDetail(item, '温度 (℃)')">
<div class="wd pointer" @click="chartDetail(item, 'temperature')">
{{ item.temperature }}
</div>
</div>
@ -102,9 +102,8 @@ export default {
return className;
},
//查看表格行图表
chartDetail(row, dataType = "") {
const { clusterDeviceId, deviceId } = row;
this.$emit("chart", { ...row, dataType });
chartDetail(row, fieldKey = "") {
this.$emit("chart", { ...row, fieldKey });
},
},
};

View File

@ -11,7 +11,7 @@
<el-table-column prop="voltage" label="电压 (V)">
<template slot-scope="scope">
<el-button
@click="chartDetail(scope.row, '电压 (V)')"
@click="chartDetail(scope.row, 'voltage')"
type="text"
size="small"
>
@ -22,7 +22,7 @@
<el-table-column prop="temperature" label="温度 (℃)">
<template slot-scope="scope">
<el-button
@click="chartDetail(scope.row, '温度 (℃)')"
@click="chartDetail(scope.row, 'temperature')"
type="text"
size="small"
>
@ -33,7 +33,7 @@
<el-table-column prop="soc" label="SOC (%)">
<template slot-scope="scope">
<el-button
@click="chartDetail(scope.row, 'SOC (%)')"
@click="chartDetail(scope.row, 'soc')"
type="text"
size="small"
>
@ -44,7 +44,7 @@
<el-table-column prop="soh" label="SOH (%)">
<template slot-scope="scope">
<el-button
@click="chartDetail(scope.row, 'SOH (%)')"
@click="chartDetail(scope.row, 'soh')"
type="text"
size="small"
>
@ -52,16 +52,6 @@
</el-button>
</template>
</el-table-column>
<el-table-column label="操作" width="160">
<template slot-scope="scope">
<el-button @click="$emit('pointDetail',scope.row,'point')" type="text" size="small">
详细
</el-button>
<el-button @click="$emit('pointDetail',scope.row,'alarmPoint')" type="text" size="small">
报警点位详细
</el-button>
</template>
</el-table-column>
</el-table>
<!-- <el-pagination
v-show="tableData.length > 0"
@ -117,8 +107,8 @@ export default {
},
methods: {
//查看表格行图表
chartDetail(row, dataType = "") {
this.$emit("chart", {...row, dataType});
chartDetail(row, fieldKey = "") {
this.$emit("chart", {...row, fieldKey});
},
},
};

View File

@ -88,7 +88,6 @@
:totalSize="totalSize"
:pointIdList="pointIdList"
@chart="chartDetail"
@pointDetail="pointDetail"
></component>
<el-pagination
v-show="tableData.length > 0"
@ -104,34 +103,55 @@
>
</el-pagination>
<chart-detail ref="chartDetail" />
<point-chart ref="pointChart" :site-id="siteId" />
<point-table ref="pointTable"/>
<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>
</el-card>
</template>
<script>
import * as echarts from "echarts";
import BarChart from "./BarChart";
import {
getStackNameList,
getClusterNameList,
getClusterDataInfoList,
getClusterNameList,
getStackNameList,
} from "@/api/ems/dzjk";
import { getPointConfigCurve } from "@/api/ems/site";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import ChartDetail from "./ChartDetail.vue";
import Table from "./Table.vue";
import List from "./List.vue";
import pointChart from "./../PointChart.vue";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
export default {
name: "DzjkSbjkDtdc",
mixins: [getQuerySiteId],
components: {
PointTable,
BarChart,
ChartDetail,
DtdcTable: Table,
DtdcList: List,
pointChart,
},
computed: {
pointIdList() {
@ -166,27 +186,190 @@ export default {
pageNum: 1, //分页栏当前页数
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: {
// 查看设备电位表格
pointDetail(row,dataType){
const {deviceId,clusterDeviceId} = row
this.$refs.pointTable.showTable({siteId:this.siteId,deviceId,deviceCategory:'BATTERY',parentId:clusterDeviceId},dataType)
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({ deviceId, clusterDeviceId, dataType = "" }) {
dataType &&
this.$refs.pointChart.showChart({
pointName: dataType,
deviceCategory:'BATTERY',
deviceId: clusterDeviceId,
child: [deviceId],
});
chartDetail(row = {}) {
const config = this.getFieldPointConfig(row.fieldKey);
if (!config) return;
const pointId = row[config.pointIdKey];
this.openCurveDialogByPointId(pointId, config.title);
},
// 分页
handleSizeChange(val) {
@ -224,6 +407,9 @@ export default {
);
this.search.clusterId = "";
this.getClusterList();
} else {
this.search.clusterId = "";
this.clusterOptions = [];
}
},
//表格数据
@ -254,19 +440,26 @@ export default {
},
getStackList() {
getStackNameList(this.siteId).then((response) => {
this.stackOptions = JSON.parse(JSON.stringify(response?.data || []));
const list = JSON.parse(JSON.stringify(response?.data || []));
this.stackOptions = list;
});
},
getClusterList() {
const { stackId } = this.search;
if (!stackId) {
this.clusterOptions = [];
return Promise.resolve();
}
this.clusterloading = true;
getClusterNameList({
stackDeviceId: this.search.stackId,
const currentStackId = String(stackId);
return getClusterNameList({
stackDeviceId: stackId,
siteId: this.siteId,
})
.then((response) => {
this.clusterOptions = JSON.parse(
JSON.stringify(response?.data || [])
);
// 避免用户快速切换电池堆时旧请求覆盖新数据
if (String(this.search.stackId || "") !== currentStackId) return;
this.clusterOptions = JSON.parse(JSON.stringify(response?.data || []));
})
.finally(() => {
this.clusterloading = false;

View File

@ -1,133 +1,481 @@
<template>
<div v-loading="loading" class="ems">
<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="(item,index) in list"
:key="index+'emsList'"
class="sbjk-card-container list running-card-container"
shadow="always"
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">{{ item.deviceName }}</span>
<span class="large-title">{{ group.displayName || group.sectionName || "EMS" }}</span>
<div class="info">
<div>
EMS控制模式: {{
item.emsStatus === 0 ? '自动' : '手动'
}}
</div>
<div>数据更新时间{{ item.dataUpdateTime }}</div>
</div>
<div class="alarm">
<el-button size="small" round style="margin-right:20px;" type="primary" @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>状态{{ group.statusText }}</div>
<div>数据更新时间{{ group.updateTimeText }}</div>
</div>
</div>
<el-row class="device-info-row">
<el-col v-for="(tempDataItem,tempDataIndex) in bmsDataList" :key="tempDataIndex+'bmsTempData'"
:span="6" class="device-info-col">
<span class="pointer" @click="showChart(tempDataItem.name,item.deviceId)">
<span class="left">{{ tempDataItem.name }}</span> <span class="right">{{ item[tempDataItem.attr] }}<span
v-html="tempDataItem.unit"></span></span>
</span>
</el-col>
<el-col v-for="(tempDataItem,tempDataIndex) in pcsDataList" :key="tempDataIndex+'pcsTempData'"
:span="6" class="device-info-col">
<span class="pointer" @click="showChart(tempDataItem.name,item.deviceId)">
<span class="left">{{ tempDataItem.name }}</span> <span class="right">{{ item[tempDataItem.attr] }}<span
v-html="tempDataItem.unit"></span></span>
<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-empty v-show="list.length <= 0" :image-size="200"></el-empty>
<point-chart ref="pointChart" :site-id="siteId"/>
<point-table ref="pointTable"/>
<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 pointChart from "./../PointChart.vue";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
import * as echarts from "echarts";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getEmsDataList} from "@/api/ems/dzjk";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import { getProjectDisplayData } from "@/api/ems/dzjk";
import { getDeviceList, getPointConfigCurve } from "@/api/ems/site";
export default {
name: "DzjkSbjkEms",
components: {pointChart, PointTable},
mixins: [getQuerySiteId, intervalUpdate],
data() {
return {
loading: false,
list: [],
bmsDataList: [{
name: 'BMS1SOC',
attr: 'bms1Soc'
displayData: [],
selectedSectionKey: "",
emsDeviceList: [],
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
{
name: 'BMS2SOC',
attr: 'bms2Soc'
},
{
name: 'BMS3SOC',
attr: 'bms3Soc'
},
{
name: 'BMS4SOC',
attr: 'bms4Soc'
}],
pcsDataList: [{
name: 'PCS-1有功功率',
attr: 'pcs1Yggl'
},
{
name: 'PCS-2有功功率',
attr: 'pcs2Yggl'
},
{
name: 'PCS-3有功功率',
attr: 'pcs3Yggl'
},
{
name: 'PCS-4有功功率',
attr: 'pcs4Yggl'
}]
};
},
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: {
// 查看设备电位表格
pointDetail(row, dataType) {
const {deviceId} = row
this.$refs.pointTable.showTable({siteId: this.siteId, deviceId, deviceCategory: 'EMS'}, dataType)
handleFieldClick(item) {
const pointId = String(item?.pointId || item?.raw?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: item?.fieldName || pointId,
});
},
showChart(pointName, deviceId) {
pointName &&
this.$refs.pointChart.showChart({pointName, deviceCategory: 'EMS', deviceId});
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;
},
getData() {
this.loading = true;
getEmsDataList(this.siteId)
.then((response) => {
const data = response?.data || {};
this.list = JSON.parse(JSON.stringify(data));
})
.finally(() => (this.loading = false));
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.getData();
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 lang="scss" scoped></style>
<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

@ -19,13 +19,14 @@
</keep-alive>
</div>
</div>
</template>
<script>
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
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//获取到单站监控-设备监控下面的字路由
export default {
name:'DzjkSbjk',
mixins:[getQuerySiteId],
@ -64,13 +65,9 @@ export default {
</script>
<style scoped lang="scss">
.sbjk-ems-dashboard-editor-container {
display: flex;
background: #ffffff;
}
.sbjk-ems-content-container {
margin-top: 0;
padding-top: 0;
.sbjk-ems-content-container{
margin-top:0;
padding-top:0;
padding-right: 0;
flex: 1;
}

View File

@ -1,9 +1,29 @@
<template>
<div v-loading="loading" class="pcs-ems-dashboard-editor-container">
<!-- 顶部六个方块-->
<real-time-base-info :data="runningHeadData"/>
<div class="pcs-ems-dashboard-editor-container">
<div class="pcs-tags">
<el-tag
size="small"
:type="selectedPcsId ? 'info' : 'primary'"
:effect="selectedPcsId ? 'plain' : 'dark'"
class="pcs-tag-item"
@click="handleTagClick('')"
>
全部
</el-tag>
<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 pcsList"
v-for="(pcsItem, pcsIndex) in filteredPcsList"
:key="pcsIndex + 'PcsHome'"
style="margin-bottom: 25px"
>
@ -17,26 +37,12 @@
>{{ pcsItem.deviceName }}</span
>
<div class="info">
<div>
{{
$store.state.ems.communicationStatusOptions[
pcsItem.communicationStatus
]
}}
<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">
<pcs-switch style="margin-right:10px;"
:round="true"
size="small"
type="danger"
:data="pcsItem"
@updateSuccess="init"/>
<el-button type="primary" round size="small" style="margin-right:20px;"
@click="pointDetail(pcsItem,'point')">
详细
</el-button>
<el-badge :hidden="!pcsItem.alarmNum" :value="pcsItem.alarmNum || 0" class="item">
<i
class="el-icon-message-solid alarm-icon"
@ -52,9 +58,14 @@
:span="1"
label="工作状态"
labelClassName="descriptions-label"
>{{
PCSWorkStatusOptions[pcsItem.workStatus]
}}
>
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'workStatus') }"
@click="handlePcsFieldClick(pcsItem, 'workStatus', '工作状态')"
>
{{ formatDictValue((PCSWorkStatusOptions || {}), pcsItem.workStatus) }}
</span>
</el-descriptions-item
>
<el-descriptions-item
@ -62,9 +73,14 @@
contentClassName="descriptions-direction"
label="并网状态"
labelClassName="descriptions-label"
>{{
$store.state.ems.gridStatusOptions[pcsItem.gridStatus]
}}
>
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'gridStatus') }"
@click="handlePcsFieldClick(pcsItem, 'gridStatus', '并网状态')"
>
{{ formatDictValue((($store.state.ems && $store.state.ems.gridStatusOptions) || {}), pcsItem.gridStatus) }}
</span>
</el-descriptions-item
>
<el-descriptions-item
@ -74,9 +90,14 @@
:span="1"
label="设备状态"
labelClassName="descriptions-label"
>{{
$store.state.ems.deviceStatusOptions[pcsItem.deviceStatus]
}}
>
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'deviceStatus') }"
@click="handlePcsFieldClick(pcsItem, 'deviceStatus', '设备状态')"
>
{{ formatDictValue((($store.state.ems && $store.state.ems.deviceStatusOptions) || {}), pcsItem.deviceStatus) }}
</span>
</el-descriptions-item
>
<el-descriptions-item
@ -84,9 +105,14 @@
contentClassName="descriptions-direction"
label="控制模式"
labelClassName="descriptions-label"
>{{
$store.state.ems.controlModeOptions[pcsItem.controlMode]
}}
>
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'controlMode') }"
@click="handlePcsFieldClick(pcsItem, 'controlMode', '控制模式')"
>
{{ formatDictValue((($store.state.ems && $store.state.ems.controlModeOptions) || {}), pcsItem.controlMode) }}
</span>
</el-descriptions-item
>
</el-descriptions>
@ -107,11 +133,11 @@
>
<span
class="pointer"
@click="
showChart(item.pointName || '', pcsItem.deviceId)
"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, item.attr) }"
@click="handlePcsFieldClick(pcsItem, item.attr, item.label)"
>
{{ pcsItem[item.attr] | formatNumber }}
<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>
@ -145,7 +171,8 @@
>
<span
class="pointer"
@click="showChart('直流功率', item.deviceId,true)"
:class="{ 'field-disabled': !item.dcPowerPointId }"
@click="openCurveDialogByPointId(item.dcPowerPointId, '直流功率')"
>{{ item.dcPower }}kW</span
>
</el-descriptions-item>
@ -157,7 +184,8 @@
>
<span
class="pointer"
@click="showChart('直流电压', item.deviceId,true)"
:class="{ 'field-disabled': !item.dcVoltagePointId }"
@click="openCurveDialogByPointId(item.dcVoltagePointId, '直流电压')"
>{{ item.dcVoltage }}V</span
>
</el-descriptions-item>
@ -169,7 +197,8 @@
>
<span
class="pointer"
@click="showChart('直流电流', item.deviceId,true)"
:class="{ 'field-disabled': !item.dcCurrentPointId }"
@click="openCurveDialogByPointId(item.dcCurrentPointId, '直流电流')"
>{{ item.dcCurrent }}A</span
>
</el-descriptions-item>
@ -177,36 +206,85 @@
</div>
</el-card>
</div>
<el-empty v-show="pcsList.length <= 0" :image-size="200"></el-empty>
<point-chart ref="pointChart" :site-id="siteId"/>
<point-table ref="pointTable"/>
<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 pointChart from "./../PointChart.vue";
import * as echarts from "echarts";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
import RealTimeBaseInfo from "./../RealTimeBaseInfo.vue";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getPcsDetailInfo, getRunningHeadInfo} from "@/api/ems/dzjk";
import {getPcsNameList, getProjectDisplayData} from "@/api/ems/dzjk";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import PcsSwitch from "@/views/ems/site/sblb/PcsSwitch.vue";
import {mapState} from "vuex";
import {getPointConfigCurve} from "@/api/ems/site";
export default {
name: "DzjkSbjkPcs",
components: {RealTimeBaseInfo, pointChart, PointTable, PcsSwitch},
components: {PointTable},
mixins: [getQuerySiteId, intervalUpdate],
computed: {
...mapState({
PCSWorkStatusOptions: state => state?.ems?.PCSWorkStatusOptions || {},
})
}),
filteredPcsList() {
if (!this.selectedPcsId) {
return this.pcsList || [];
}
return (this.pcsList || []).filter(item => item.deviceId === this.selectedPcsId);
},
},
data() {
return {
loading: false,
runningHeadData: {}, //运行信息
pcsList: [],
displayData: [],
pcsDeviceList: [],
selectedPcsId: "",
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
pcsList: [{
deviceId: "",
deviceName: "PCS",
dataUpdateTime: "-",
alarmNum: 0,
pcsBranchInfoList: [],
}],
infoData: [
{
label: "总交流有功功率",
@ -284,48 +362,348 @@ export default {
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();
},
handleCardClass(item) {
const {workStatus = ''} = item
return workStatus === '1' || !Object.keys(this.PCSWorkStatusOptions).find(i => i === workStatus) ? "timing-card-container" : workStatus === '2' ? 'warning-card-container' : 'running-card-container'
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)
},
showChart(pointName, deviceId, isBranch = false) {
pointName &&
this.$refs.pointChart.showChart({pointName, deviceCategory: isBranch ? 'BRANCH' : 'PCS', deviceId});
hasFieldPointId(pcsItem, fieldName) {
const row = this.getFieldRow(pcsItem, fieldName);
return !!String(row?.dataPoint || "").trim();
},
//6个方块数据
getRunningHeadData() {
getRunningHeadInfo(this.siteId).then((response) => {
this.runningHeadData = response?.data || {};
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;
});
},
getPcsList() {
this.loading = true;
getPcsDetailInfo(this.siteId)
.then((response) => {
const data = response?.data || {};
this.pcsList = JSON.parse(JSON.stringify(data));
})
.finally(() => (this.loading = 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.getRunningHeadData();
this.getPcsList();
this.loading = true;
// 先渲染卡片框架,字段值走单点位 loading
this.buildPcsList();
Promise.all([
getProjectDisplayData(this.siteId),
this.getPcsDeviceList(),
]).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;
}
},
};
</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;
}
<style lang="scss" scoped></style>
.pcs-tag-item {
cursor: pointer;
}
.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

@ -6,7 +6,7 @@
<div slot="header">
<span class="card-title">PCS有功功率/PCS无功功率</span>
</div>
<div style="height: 360px" id="cnglqxChart"/>
<div ref="chartRef" style="height: 360px" />
</el-card>
</template>
@ -14,17 +14,23 @@
<script>
import * as echarts from "echarts";
import resize from "@/mixins/ems/resize";
import {storagePower} from "@/api/ems/dzjk";
import { getPointConfigCurve } from "@/api/ems/site";
export default {
mixins: [resize],
props: {
displayData: {
type: Array,
default: () => []
}
},
data() {
return {
chart: null,
};
},
mounted() {
this.chart = echarts.init(document.querySelector("#cnglqxChart"));
this.chart = echarts.init(this.$refs.chartRef);
},
beforeDestroy() {
if (!this.chart) {
@ -35,56 +41,61 @@ export default {
},
methods: {
init(siteId, timeRange) {
this.chart.showLoading();
const [startTime = '', endTime = ''] = timeRange;
storagePower(siteId, startTime, endTime)
const [startTime = "", endTime = ""] = timeRange;
const query = {
rangeType: "custom",
startTime: this.normalizeDateTime(startTime, false),
endTime: this.normalizeDateTime(endTime, true),
siteId
};
const rows = (this.displayData || []).filter(
(item) =>
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(response?.data?.pcsPowerList || []);
const list = response?.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]))
};
})
.finally(() => {
this.chart.hideLoading();
});
},
setOption(data) {
let xdata = [],
series = [];
data.forEach((element, index) => {
if (index === 0) {
xdata = (element.energyStoragePowList || []).map((i) => i.createDate);
}
series.push(
{
type: "line",
name: `${element.deviceId}有功功率`,
areaStyle: {
// color:'#FFBD00'
},
data: (element.energyStoragePowList || []).map(
(i) => {
return {
value: i.pcsTotalActPower,
year: i.dateDay || ''
}
}
)
},
{
type: "line",
name: `${element.deviceId}无功功率`,
areaStyle: {
// color:'#FFBD00'
},
data: (element.energyStoragePowList || []).map(
(i) => {
return {
value: i.pcsTotalReactivePower,
year: i.dateDay || ''
}
}
),
}
);
.catch(() => null);
});
Promise.all(tasks)
.then((series) => {
this.setOption((series || []).filter(Boolean));
});
},
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 && this.chart.setOption({
legend: {
left: "center",
@ -102,26 +113,13 @@ export default {
show: true,
trigger: "axis",
axisPointer: {
// 坐标轴指示器,坐标轴触发有效
type: "shadow", // 默认为直线,可选为:'line' | 'shadow'
},
formatter: (params) => {
if (params.length <= 0) return
let result = (params[0].data.year || '') + ' ' + params[0].name + '<div>'
params.forEach(item => {
const {color, seriesName, value} = item
result += `<div style="position: relative;padding-left:20px;line-height: 20px;">
<div style="position: absolute;top:50%;left:0;width:12px;height:12px;border-radius:100%;background: ${color};transform: translateY(-50%)"></div>
<span>${seriesName}</span><span style="margin-left:20px;font-weight: 700">${value}</span></div>`
})
result += '</div>'
return result
type: "cross",
}
},
textStyle: {
color: "#333333",
},
xAxis: {type: "category", data: xdata},
xAxis: { type: "time" },
yAxis: {
type: "value",
},
@ -136,7 +134,16 @@ export default {
end: 100,
},
],
series,
series: seriesData.map((item) => ({
type: "line",
name: item.name,
showSymbol: false,
smooth: true,
areaStyle: {
opacity: 0.35
},
data: item.data
})),
}, true);
},
},

View File

@ -6,7 +6,7 @@
<div slot="header">
<span class="card-title">平均SOC</span>
</div>
<div style="height: 360px" id="dcpjsocChart" />
<div ref="chartRef" style="height: 360px" />
</el-card>
</template>
@ -14,16 +14,22 @@
<script>
import * as echarts from "echarts";
import resize from "@/mixins/ems/resize";
import { batteryAveSoc } from "@/api/ems/dzjk";
import { getPointConfigCurve } from "@/api/ems/site";
export default {
mixins: [resize],
props: {
displayData: {
type: Array,
default: () => []
}
},
data() {
return {
chart: null,
};
},
mounted() {
this.chart = echarts.init(document.querySelector("#dcpjsocChart"));
this.chart = echarts.init(this.$refs.chartRef);
},
beforeDestroy() {
if (!this.chart) {
@ -34,26 +40,47 @@ export default {
},
methods: {
init(siteId,timeRange) {
this.chart.showLoading();
const [startTime='', endTime=''] = timeRange;
batteryAveSoc(siteId,startTime,endTime)
const row = (this.displayData || []).find(
(item) =>
item &&
item.fieldCode === "SBJK_SSYX__curveBatteryAveSoc" &&
item.useFixedDisplay !== 1 &&
item.dataPoint
);
const pointId = String(row?.dataPoint || "").trim();
if (!pointId) {
this.setOption([]);
return;
}
getPointConfigCurve({
siteId,
pointId,
rangeType: "custom",
startTime: this.normalizeDateTime(startTime, false),
endTime: this.normalizeDateTime(endTime, true)
})
.then((response) => {
this.setOption(response?.data?.batteryAveSOCList || []);
})
.finally(() => {
this.chart.hideLoading();
const list = response?.data || [];
this.setOption(
list
.map((item) => [this.parseToTimestamp(item.dataTime), Number(item.pointValue)])
.filter((item) => item[0] && !Number.isNaN(item[1]))
);
});
},
setOption(data) {
let xdata = [],
ydata = [];
data.forEach((element) => {
xdata.push(element.createDate);
ydata.push({
value:element.batterySOC,
year:element.dateDay,
});
});
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(data = []) {
this.chart && this.chart.setOption({
legend: {
left: "center",
@ -71,26 +98,13 @@ export default {
show:true,
trigger: "axis",
axisPointer: {
// 坐标轴指示器,坐标轴触发有效
type: "shadow", // 默认为直线,可选为:'line' | 'shadow'
},
formatter :(params)=>{
if(params.length <= 0) return
let result = (params[0].data.year || '')+' '+params[0].name + '<div>'
params.forEach(item=>{
const {color,seriesName,value} = item
result += `<div style="position: relative;padding-left:20px;line-height: 20px;">
<div style="position: absolute;top:50%;left:0;width:12px;height:12px;border-radius:100%;background: ${color};transform: translateY(-50%)"></div>
<span>${seriesName}</span><span style="margin-left:20px;font-weight: 700">${value}</span></div>`
})
result+='</div>'
return result
type: "cross",
}
},
textStyle: {
color: "#333333",
},
xAxis: { type: "category", data: xdata },
xAxis: { type: "time" },
yAxis: {
type: "value",
},
@ -109,10 +123,12 @@ export default {
{
type: "line",
name: `平均SOC`,
showSymbol: false,
smooth: true,
areaStyle: {
// color:'#FFBD00'
opacity: 0.35
},
data: ydata,
data,
},
],
},true);

View File

@ -6,7 +6,7 @@
<div slot="header">
<span class="card-title">电池平均温度</span>
</div>
<div style="height: 360px" id="dcpjwdChart" />
<div ref="chartRef" style="height: 360px" />
</el-card>
</template>
@ -14,17 +14,23 @@
<script>
import * as echarts from "echarts";
import resize from "@/mixins/ems/resize";
import { batteryAveTemp } from "@/api/ems/dzjk";
import { getPointConfigCurve } from "@/api/ems/site";
export default {
mixins: [resize],
props: {
displayData: {
type: Array,
default: () => []
}
},
data() {
return {
chart: null,
};
},
mounted() {
this.chart = echarts.init(document.querySelector("#dcpjwdChart"));
this.chart = echarts.init(this.$refs.chartRef);
},
beforeDestroy() {
if (!this.chart) {
@ -35,28 +41,47 @@ export default {
},
methods: {
init(siteId,timeRange) {
this.chart.showLoading();
const [startTime='', endTime=''] = timeRange;
batteryAveTemp(siteId,startTime,endTime)
const row = (this.displayData || []).find(
(item) =>
item &&
item.fieldCode === "SBJK_SSYX__curveBatteryAveTemp" &&
item.useFixedDisplay !== 1 &&
item.dataPoint
);
const pointId = String(row?.dataPoint || "").trim();
if (!pointId) {
this.setOption([]);
return;
}
getPointConfigCurve({
siteId,
pointId,
rangeType: "custom",
startTime: this.normalizeDateTime(startTime, false),
endTime: this.normalizeDateTime(endTime, true)
})
.then((response) => {
this.setOption(response?.data?.batteryAveTempList || []);
})
.finally(() => {
this.chart.hideLoading();
const list = response?.data || [];
this.setOption(
list
.map((item) => [this.parseToTimestamp(item.dataTime), Number(item.pointValue)])
.filter((item) => item[0] && !Number.isNaN(item[1]))
);
});
},
setOption(data) {
let xdata = [],
ydata = [];
data.forEach((element) => {
xdata.push(element.createDate);
ydata.push(
{
value: element.batteryTemp,
year: element.dateDay
}
);
});
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(data = []) {
this.chart && this.chart.setOption({
legend: {
left: "center",
@ -74,26 +99,13 @@ export default {
show:true,
trigger: "axis",
axisPointer: {
// 坐标轴指示器,坐标轴触发有效
type: "shadow", // 默认为直线,可选为:'line' | 'shadow'
},
formatter :(params)=>{
if(params.length <= 0) return
let result = (params[0].data.year || '')+' '+params[0].name + '<div>'
params.forEach(item=>{
const {color,seriesName,value} = item
result += `<div style="position: relative;padding-left:20px;line-height: 20px;">
<div style="position: absolute;top:50%;left:0;width:12px;height:12px;border-radius:100%;background: ${color};transform: translateY(-50%)"></div>
<span>${seriesName}</span><span style="margin-left:20px;font-weight: 700">${value}</span></div>`
})
result+='</div>'
return result
type: "cross",
}
},
textStyle: {
color: "#333333",
},
xAxis: { type: "category", data: xdata },
xAxis: { type: "time" },
yAxis: {
type: "value",
},
@ -112,10 +124,12 @@ export default {
{
type: "line",
name: `电池平均温度`,
showSymbol: false,
smooth: true,
areaStyle: {
// color:'#FFBD00'
opacity: 0.35
},
data: ydata,
data,
},
],
},true);

View File

@ -6,7 +6,7 @@
<div slot="header">
<span class="card-title">PCS最高温度</span>
</div>
<div style="height: 360px" id="pocpjwdChart" />
<div ref="chartRef" style="height: 360px" />
</el-card>
</template>
@ -14,17 +14,24 @@
<script>
import * as echarts from "echarts";
import resize from "@/mixins/ems/resize";
import { pcsMaxTemp } from "@/api/ems/dzjk";
import { getPointConfigCurve } from "@/api/ems/site";
export default {
mixins: [resize],
props: {
displayData: {
type: Array,
default: () => []
}
},
data() {
return {
chart: null,
seriesName: "PCS最高温度"
};
},
mounted() {
this.chart = echarts.init(document.querySelector("#pocpjwdChart"));
this.chart = echarts.init(this.$refs.chartRef);
},
beforeDestroy() {
if (!this.chart) {
@ -35,37 +42,48 @@ export default {
},
methods: {
init(siteId,timeRange) {
this.chart.showLoading();
const [startTime='', endTime=''] = timeRange;
pcsMaxTemp(siteId,startTime,endTime)
const row = (this.displayData || []).find(
(item) =>
item &&
item.fieldCode === "SBJK_SSYX__curvePcsMaxTemp" &&
item.useFixedDisplay !== 1 &&
item.dataPoint
);
const pointId = String(row?.dataPoint || "").trim();
this.seriesName = row?.fieldName || "PCS最高温度";
if (!pointId) {
this.setOption([]);
return;
}
getPointConfigCurve({
siteId,
pointId,
rangeType: "custom",
startTime: this.normalizeDateTime(startTime, false),
endTime: this.normalizeDateTime(endTime, true)
})
.then((response) => {
this.setOption(response?.data?.pcsMaxTempList || []);
})
.finally(() => {
this.chart.hideLoading();
const list = response?.data || [];
this.setOption(
list
.map((item) => [this.parseToTimestamp(item.dataTime), Number(item.pointValue)])
.filter((item) => item[0] && !Number.isNaN(item[1]))
);
});
},
setOption(data) {
let xdata = [],
series = [];
data.forEach((element, index) => {
if (index === 0) {
xdata = (element.maxTempVoList || []).map((i) => i.createDate);
}
series.push({
type: "line",
name: `${element.deviceId}最高温度`,
areaStyle: {
// color:'#FFBD00'
},
data: (element.maxTempVoList || []).map((i) => {
return {
value: i.temp,
year: i.dateDay
}
}),
});
});
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(data = []) {
this.chart && this.chart.setOption({
legend: {
left: "center",
@ -83,26 +101,13 @@ export default {
show:true,
trigger: "axis",
axisPointer: {
// 坐标轴指示器,坐标轴触发有效
type: "shadow", // 默认为直线,可选为:'line' | 'shadow'
},
formatter :(params)=>{
if(params.length <= 0) return
let result = (params[0].data.year || '')+' '+params[0].name + '<div>'
params.forEach(item=>{
const {color,seriesName,value} = item
result += `<div style="position: relative;padding-left:20px;line-height: 20px;">
<div style="position: absolute;top:50%;left:0;width:12px;height:12px;border-radius:100%;background: ${color};transform: translateY(-50%)"></div>
<span>${seriesName}</span><span style="margin-left:20px;font-weight: 700">${value}</span></div>`
})
result+='</div>'
return result
type: "cross",
}
},
textStyle: {
color: "#333333",
},
xAxis: { type: "category", data: xdata },
xAxis: { type: "time" },
yAxis: {
type: "value",
},
@ -117,7 +122,18 @@ export default {
end: 100,
},
],
series,
series: [
{
type: "line",
name: this.seriesName,
showSymbol: false,
smooth: true,
areaStyle: {
opacity: 0.35
},
data
}
],
},true);
},
},

View File

@ -2,33 +2,65 @@
<template>
<div class="ssyx-ems-dashboard-editor-container">
<!-- 6个方块-->
<real-time-base-info :data="runningHeadData"/>
<real-time-base-info :display-data="runningDisplayData" :loading="runningHeadLoading" @field-click="handleHeadFieldClick"/>
<!-- 时间选择 -->
<date-range-select ref="dateRangeSelect" @updateDate="updateDate" style="margin-top:20px;"/>
<!-- echart图表-->
<el-row :gutter="32" style="background:#fff;margin:30px 0;">
<el-col :xs="24" :sm="12" :lg="12">
<cnglqx-chart ref='cnglqx'/>
<cnglqx-chart ref='cnglqx' :display-data="runningDisplayData"/>
</el-col>
<el-col :xs="24" :sm="12" :lg="12">
<pocpjwd-chart ref='pocpjwd'/>
<pocpjwd-chart ref='pocpjwd' :display-data="runningDisplayData"/>
</el-col>
</el-row>
<el-row :gutter="32" style="margin:30px 0;">
<el-col :xs="24" :sm="12" :lg="12">
<dcpjsoc-chart ref="dcpjsoc"/>
<dcpjsoc-chart ref="dcpjsoc" :display-data="runningDisplayData"/>
</el-col>
<el-col :xs="24" :sm="12" :lg="12">
<dcpjwd-chart ref="dcpjwd"/>
<dcpjwd-chart ref="dcpjwd" :display-data="runningDisplayData"/>
</el-col>
</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>
</template>
<style scoped lang="scss">
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
</style>
<script>
import * as echarts from "echarts";
import DateRangeSelect from '@/components/Ems/DateRangeSelect/index.vue'
import RealTimeBaseInfo from "./../RealTimeBaseInfo.vue";
import CnglqxChart from './CnglqxChart.vue'
@ -36,7 +68,8 @@ import PocpjwdChart from './PocpjwdChart.vue'
import DcpjwdChart from './DcpjwdChart.vue'
import DcpjsocChart from './DcpjsocChart.vue'
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getRunningHeadInfo} from '@/api/ems/dzjk'
import {getProjectDisplayData} from '@/api/ems/dzjk'
import {getPointConfigCurve} from "@/api/ems/site";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
export default {
@ -45,16 +78,183 @@ export default {
mixins:[getQuerySiteId,intervalUpdate],
data() {
return {
runningHeadData:{},//运行信息
runningDisplayData: [], //单站监控项目配置展示数据
timeRange:[],
isInit:true
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:{
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个方块数据
getRunningHeadData(){
getRunningHeadInfo(this.siteId).then(response => {
this.runningHeadData = response?.data || {}
this.runningHeadLoading = true
return getProjectDisplayData(this.siteId).then((displayResponse) => {
this.runningDisplayData = displayResponse?.data || []
}).finally(() => {
this.runningHeadLoading = false
})
},
// 更新时间范围 重置图表
@ -71,8 +271,9 @@ export default {
this.updateInterval(this.updateData)
},
updateData(){
this.getRunningHeadData()
this.updateChart()
this.getRunningHeadData().finally(() => {
this.updateChart()
})
},
init(){
this.$refs.dateRangeSelect.init(true)

View File

@ -1,5 +1,5 @@
<template>
<div v-loading="loading">
<div>
<el-card
v-for="(item,index) in list"
:key="index+'ylLise'"
@ -37,10 +37,11 @@
<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">{{
item[tempDataItem.attr] || '-'
}}<span
v-html="tempDataItem.unit"></span></span>
<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>
@ -76,6 +77,12 @@ export default {
}
},
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
@ -131,4 +138,16 @@ export default {
}
}
}
.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,103 +1,471 @@
<template>
<div v-loading="loading">
<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 + '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="(item,index) in list"
:key="index+'ylLise'"
class="sbjk-card-container running-card-container"
shadow="always">
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">{{ item.deviceName }}</span>
<span class="large-title">{{ group.displayName || group.sectionName || "冷却" }}</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>状态{{ group.statusText }}</div>
<div>数据更新时间{{ group.updateTimeText }}</div>
</div>
</div>
<el-row class="device-info-row">
<el-col v-for="(tempDataItem,tempDataIndex) in tempData" :key="tempDataIndex+'ylTempData'" :span="8"
class="device-info-col">
<span class="pointer" @click="showChart(tempDataItem.title,item.deviceId)">
<span class="left">{{ tempDataItem.title }}</span> <span
class="right">{{ item[tempDataItem.attr] || '-' }}<span
v-html="tempDataItem.unit"></span></span>
<el-col
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-empty v-show="list.length<=0" :image-size="200"></el-empty>
<point-chart ref="pointChart" :site-id="siteId"/>
<point-table ref="pointTable"/>
<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 {getCoolingDataList} from '@/api/ems/dzjk'
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import pointChart from "./../PointChart.vue";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
import { getProjectDisplayData } from "@/api/ems/dzjk";
import { getDeviceList, getPointConfigCurve } from "@/api/ems/site";
export default {
name: 'DzjkSbjkYl',
name: "DzjkSbjkYl",
mixins: [getQuerySiteId, intervalUpdate],
components: {pointChart, PointTable},
data() {
return {
loading: false,
list: [],
tempData: [
{title: '供水温度', attr: 'gsTemp', unit: '&#8451;'},
{title: '回水温度', attr: 'hsTemp', unit: '&#8451;'},
{title: '供水压力', attr: 'gsPressure', unit: 'bar'},
{title: '回水压力', attr: 'hsPressure', unit: 'bar'},
{title: '冷源水温度', attr: 'lysTemp', unit: '&#8451;'},
{title: 'VB01开度', attr: 'vb01Kd', unit: '%'},
{title: 'VB02开度', attr: 'vb02Kd', unit: '%'},
]
}
displayData: [],
selectedSectionKey: "",
coolingDeviceList: [],
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_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: {
// 查看设备电位表格
pointDetail(row, dataType) {
const {deviceId} = row
this.$refs.pointTable.showTable({siteId: this.siteId, deviceId, deviceCategory: 'COOLING'}, dataType)
handleFieldClick(item) {
const pointId = String(item?.pointId || item?.raw?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: item?.fieldName || pointId,
});
},
showChart(pointName, deviceId) {
pointName && this.$refs.pointChart.showChart({pointName, deviceCategory: 'COOLING', deviceId})
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
getCoolingDataList(this.siteId).then(response => {
this.list = JSON.parse(JSON.stringify(response?.data || []));
}).finally(() => {
this.loading = false
})
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)
this.updateData();
this.updateInterval(this.updateData);
},
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
mounted() {
}
}
};
</script>
<style scoped lang="scss">
.sbjk-card-container {
&:not(:last-child) {
&.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

@ -149,7 +149,7 @@ export default {
exportTable() {
if (!this.dateRange?.length) return
const [startTime, endTime] = this.dateRange
this.download('ems/statsReport/exportAmmeterData', {
this.download('ems/statsReport/exportAmmeterDataFromDaily', {
siteId: this.siteId,
startTime,
endTime,

View File

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

View File

@ -125,6 +125,12 @@ export default {
this.totalChargedCap=totalChargedCap
this.totalDisChargedCap=totalDisChargedCap
this.efficiency=efficiency
}).catch(() => {
this.setOption([], '')
this.totalChargedCap=''
this.totalDisChargedCap=''
this.efficiency=''
// 错误提示由全局请求拦截器处理,这里兜底避免出现 Uncaught (in promise)
}).finally(() => {
this.loading=false;
})

View File

@ -7,55 +7,47 @@
text-color="#666666"
active-text-color="#ffffff"
>
<el-menu-item
:index="item.name"
v-for="(item, index) in childrenRoute"
:key="index + 'tjbbChildrenRoute'"
>
<router-link
style="height: 100%; width: 100%; display: block"
:to="{ path: item.path, query: $route.query }"
>
{{ item.meta.title }}
<el-menu-item :index="item.name" v-for="(item,index) in childrenRoute" :key="index+'tjbbChildrenRoute'">
<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 tjbb-ems-content-container"
>
<div class="ems-content-container ems-content-container-padding tjbb-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.find(
(item) => item.name === "DzjkTjbb"
).children; //获取到统计报表下面的字路由
console.log("设备监控子路由", childrenRoute);
import { dzjk } from '@/router/ems'
const childrenRoute = dzjk[0].children[0].children.find(item=> item.name==='DzjkTjbb').children//获取到统计报表下面的字路由
console.log('设备监控子路由',childrenRoute)
export default {
name: "DzjkTjbb",
data() {
name:'DzjkTjbb',
data(){
return {
childrenRoute,
activeMenu: "",
};
activeMenu:''
}
},
mounted() {
console.log("当前统计报表页面路由", this.$route);
},
};
console.log('当前统计报表页面路由',this.$route)
}
}
</script>
<style scoped lang="scss">
.tjbb-ems-content-container {
margin-top: 0;
padding-top: 0;
.tjbb-ems-content-container{
margin-top:0;
padding-top:0;
padding-right: 0;
flex: 1;
}
</style>

View File

@ -39,6 +39,17 @@
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">
@ -187,8 +198,9 @@ export default {
const {siteId, pageNum, pageSize} = this
const [startTime = '', endTime = ''] = (this.dateRange || [])
getAmmeterRevenueData({siteId: siteId, startTime, endTime, pageSize, pageNum}).then(response => {
this.tableData = response?.rows || [];
const rows = response?.rows || [];
this.totalSize = response?.total || 0
this.tableData = rows
}).finally(() => {
this.loading = false
})

View File

@ -323,7 +323,7 @@ $lineColor: #86bcc7;
.ems-dashboard-editor-container {
background-color: #ffffff;
padding: 30px;
padding: 0;
color: #666666;
.container {

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,183 @@
<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

@ -0,0 +1,218 @@
<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

@ -0,0 +1,187 @@
<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

@ -0,0 +1,191 @@
<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

@ -0,0 +1,30 @@
<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

@ -2,9 +2,7 @@
<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-select v-model="formData.siteId" :disabled="mode === 'edit'" placeholder="请选择站点" :loading="searchLoading" loading-text="正在加载数据">
<el-option v-for="(item,index) in siteList" :key="index+'zdxeSelect'" :label="item.siteName" :value="item.siteId" ></el-option>
</el-select>
<el-input v-model="formData.siteId" placeholder="请先在顶部选择站点" disabled />
</el-form-item>
<el-form-item label="消息等级" prop="qos">
<el-select v-model="formData.qos" placeholder="请选择消息等级">
@ -30,13 +28,10 @@
</template>
<script>
import {editMqtt,addMqtt,getMqttDetail} from "@/api/ems/site";
import {getAllSites} from '@/api/ems/zddt'
export default {
data() {
return {
loading:0,
siteList:[],
searchLoading:false,
dialogTableVisible:false,
mode:'',
formData: {
@ -48,7 +43,7 @@ export default {
},
rules: {
siteId:[
{ required: true, message: '请选择站点', trigger: 'blur'},
{ required: true, message: '请先在顶部选择站点', trigger: 'blur'},
],
qos:[
{ required: true, message: '请选择消息等级', trigger: 'blur'},
@ -63,23 +58,20 @@ export default {
}
},
methods: {
showDialog(id){
getRouteSiteId() {
const siteId = this.$route?.query?.siteId
return siteId === undefined || siteId === null ? '' : String(siteId).trim()
},
showDialog(id, siteId = ''){
this.dialogTableVisible = true
this.getZdList()
if(id){
this.mode = 'edit'
this.formData.id = id
this.getDetail(id)
}else{
this.mode = 'add'
this.formData.siteId = siteId || this.getRouteSiteId()
}
},
//获取站点列表
getZdList(){
this.searchLoading=true
getAllSites().then(response => {
this.siteList = response?.data || []
}).finally(() => {this.searchLoading=false})
},
getDetail(id){
getMqttDetail(id).then(response => {
@ -132,6 +124,8 @@ export default {
// 清空所有数据
this.formData= {
id:'',//设备唯一标识
siteId:'',
qos:'',
mqttTopic:'',
topicName:''
}

View File

@ -1,11 +1,6 @@
<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="站点选择">
<el-select v-model="form.siteId" placeholder="请选择换电站名称" :loading="searchLoading" loading-text="正在加载数据" clearable>
<el-option :label="item.siteName" :value="item.siteId" v-for="(item,index) in siteList" :key="index+'zdxeSelect'"></el-option>
</el-select>
</el-form-item>
<el-form-item label="订阅topic">
<el-input
v-model="form.mqttTopic"
@ -90,12 +85,21 @@
<script>
import {deleteMqtt,getMqttList} from '@/api/ems/site'
import {getAllSites} from '@/api/ems/zddt'
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:{
@ -103,8 +107,6 @@ export default {
topicName:'',
mqttTopic:''
},
siteList:[],
searchLoading:false,
loading:false,
tableData:[],
pageSize:10,//分页栏当前每个数据总数
@ -113,6 +115,9 @@ export default {
}
},
methods:{
hasValidSiteId(siteId) {
return !!(siteId !== undefined && siteId !== null && String(siteId).trim())
},
// 分页
handleSizeChange(val) {
this.pageSize = val;
@ -133,22 +138,19 @@ export default {
},
onReset(){
this.form={
siteId:'',
siteId:this.form.siteId,
topicName:'',
mqttTopic:''
}
this.pageNum =1//每次搜索从1开始搜索
this.getData()
},
//获取站点列表
getZdList(){
this.searchLoading=true
return getAllSites().then(response => {
this.siteList = response?.data || []
// if( this.siteList.length>0 ) this.siteId = this.siteList[0].siteId
}).finally(() => {this.searchLoading=false})
},
getData(){
if (!this.form.siteId) {
this.tableData = []
this.totalSize = 0
return
}
this.loading=true;
const {mqttTopic,topicName,siteId} = this.form;
const {pageNum,pageSize} = this;
@ -158,7 +160,7 @@ export default {
}).finally(() => {this.loading=false})
},
addPowerConfig(id=''){
this.$refs.addMqtt.showDialog(id);
this.$refs.addMqtt.showDialog(id, this.form.siteId);
},
deleteMqtt(row){
this.$confirm(`确认要删除该配置吗?`, {
@ -193,8 +195,7 @@ export default {
},
},
mounted() {
this.loading=true
this.getZdList()
this.form.siteId = this.hasValidSiteId(this.$route.query.siteId) ? String(this.$route.query.siteId).trim() : ''
this.getData()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -5,9 +5,7 @@
<div class="items-container">
<div class="item-title">站点:</div>
<div class="item-content">
<el-select v-model="siteId" :disabled="mode === 'edit'" placeholder="请选择站点" :loading="searchLoading" loading-text="正在加载数据">
<el-option :label="item.siteName" :value="item.siteId" v-for="(item,index) in siteList" :key="index+'zdxeSelect'"></el-option>
</el-select>
<el-input v-model="siteId" placeholder="请先在顶部选择站点" disabled />
</div>
</div>
<div class="items-container">
@ -103,15 +101,12 @@
</template>
<script>
import {addPriceConfig,editPriceConfig,detailPriceConfig} from '@/api/ems/powerTariff'
import {getAllSites} from '@/api/ems/zddt'
export default {
data() {
return {
mode:'',
id:'',
searchLoading:false,
siteId:'',
siteList:[],
powerDate:'',//时间
//尖-peak,峰-high,平-flat,谷=valley
priceTypeOptions:[{
@ -137,6 +132,10 @@ export default {
}
},
methods: {
getRouteSiteId() {
const siteId = this.$route?.query?.siteId
return siteId === undefined || siteId === null ? '' : String(siteId).trim()
},
addRow(){
this.hoursOptions.push({
startTime:'',
@ -147,15 +146,7 @@ export default {
deleteRow(index){
this.hoursOptions.splice(index,1)
},
//获取站点列表
getZdList(){
this.searchLoading=true
getAllSites().then(response => {
this.siteList = response?.data || []
}).finally(() => {this.searchLoading=false})
},
showDialog(id){
this.getZdList()
showDialog(id, siteId = ''){
this.id = id
if(id) {
this.mode='edit'
@ -172,11 +163,12 @@ export default {
}).finally(()=>this.loading = false)
}else {
this.mode='add'
this.siteId = siteId || this.getRouteSiteId()
}
this.dialogTableVisible=true
},
saveDialog() {
if(this.siteId === '') return this.$message.error('请选择站点')
if(this.siteId === '') return this.$message.error('请先在顶部选择站点')
if(this.powerDate === '') return this.$message.error('请选择时间')
let priceArr=[]
this.priceTypeOptions.forEach(item=>{
@ -244,8 +236,6 @@ export default {
this.mode=''
this.id=''
this.siteId=''
this.siteList=[]
this.searchLoading=false
this.powerDate=''
this.hoursOptions=[]
this.priceTypeOptions.forEach(item=>{
@ -303,4 +293,4 @@ export default {
}
}
</style>
</style>

View File

@ -1,11 +1,6 @@
<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="站点选择">
<el-select v-model="siteId" placeholder="请选择换电站名称" :loading="searchLoading" loading-text="正在加载数据" @change="onSearch" clearable>
<el-option :label="item.siteName" :value="item.siteId" v-for="(item,index) in siteList" :key="index+'zdxeSelect'"></el-option>
</el-select>
</el-form-item>
<el-form-item label="年份选择">
<el-date-picker
v-model="defaultYear"
@ -33,7 +28,7 @@
:key="item.id"
>
<div slot="header" class="time-range-header">
<span class="card-title">{{siteList.find(i=>i.siteId===item.siteId).siteName || item.siteId || ''}}-{{item.month}}月电价时段划分</span>
<span class="card-title">{{item.siteId || ''}}-{{item.month}}月电价时段划分</span>
<div>
<el-button type="primary" size="mini" @click="addPowerConfig(item.id)">编辑</el-button>
<el-button type="warning" size="mini" @click="deletePowerConfig(item)">删除</el-button>
@ -61,21 +56,28 @@
<script>
import {energyPriceConfig,listPriceConfig} from '@/api/ems/powerTariff'
import {getAllSites} from '@/api/ems/zddt'
import AddPowerTariff from './AddPowerTariff.vue'
import DateTimeSelect from "@/views/ems/search/DateTimeSelect.vue";
export default {
name: "PowerTariff",
components: {DateTimeSelect, AddPowerTariff},
computed: { },
watch: {
'$route.query.siteId'(newSiteId) {
const normalizedSiteId = this.hasValidSiteId(newSiteId) ? String(newSiteId).trim() : ''
if (normalizedSiteId === this.siteId) {
return
}
this.siteId = normalizedSiteId
this.onSearch()
}
},
data() {
return {
loading:false,
pageNum:1,
pageSize:40,
searchLoading:false,
siteId:'',
siteList:[],
tableData:[],
tableTotal:0,
defaultYear:'',
@ -93,6 +95,9 @@ export default {
}
},
methods:{
hasValidSiteId(siteId) {
return !!(siteId !== undefined && siteId !== null && String(siteId).trim())
},
resetTableData(){
this.tableData=[]
this.tableTotal=0
@ -102,19 +107,16 @@ export default {
onSearch(){
this.getData(true)
},
//获取站点列表
getZdList(){
this.searchLoading=true
return getAllSites().then(response => {
this.siteList = response?.data || []
if( this.siteList.length>0 ) this.siteId = this.siteList[0].siteId
}).finally(() => {this.searchLoading=false})
},
changeDefaultYear(){
this.getData(true)
},
getData(reset=false){
reset && this.resetTableData()
if (!this.siteId) {
this.tableData = []
this.tableTotal = 0
return
}
if(!reset && this.tableData.length>=this.tableTotal) return
this.loading=true;
const date = new Date(this.defaultYear).getFullYear()
@ -127,7 +129,7 @@ export default {
}).finally(() => {this.loading=false})
},
addPowerConfig(id=''){
this.$refs.addPowerTariff.showDialog(id);
this.$refs.addPowerTariff.showDialog(id, this.siteId);
},
deletePowerConfig(row){
this.$confirm(`确认要删除${row.month}月的电价配置吗?`, {
@ -163,10 +165,8 @@ export default {
},
mounted() {
this.defaultYear = new Date()
this.loading=true
this.getZdList().then(()=>{
this.getData(true)
})
this.siteId = this.hasValidSiteId(this.$route.query.siteId) ? String(this.$route.query.siteId).trim() : ''
this.getData(true)
}
}
</script>

View File

@ -1,815 +0,0 @@
<template>
<el-dialog
v-loading="loading"
width="90%"
:visible.sync="dialogTableVisible"
class="ems-dialog"
title="保护方案"
:close-on-click-modal="false"
:show-close="false"
>
<el-form
v-loading="loading > 0"
ref="addTempForm"
:model="formData"
:rules="rules"
size="medium"
label-width="140px"
>
<el-form-item label="站点" prop="siteId">
<el-select
v-model="formData.siteId"
placeholder="请选择"
:style="{ width: '50%' }"
@change="changeType"
>
<el-option
:label="item.siteName"
:value="item.siteId"
v-for="(item, index) in siteList"
:key="index + 'siteOptions'"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="设备保护名称" prop="faultName">
<el-input
v-model="formData.faultName"
placeholder="请输入"
clearable
:style="{ width: '50%' }"
>
</el-input>
</el-form-item>
<el-form-item label="处理方案描述" prop="description">
<el-input
v-model="formData.description"
type="textarea"
:rows="2"
placeholder="请输入"
clearable
:style="{ width: '50%' }"
>
</el-input>
</el-form-item>
<el-form-item label="是否告警" prop="isAlert">
<el-checkbox
v-model="formData.isAlert"
:true-label="1"
:false-label="0"
></el-checkbox>
</el-form-item>
<el-form-item label="告警等级" prop="faultLevel">
<el-radio-group v-model="formData.faultLevel" :style="{ width: '50%' }" :disabled="mode === 'edit'">
<el-radio :label="1">等级1</el-radio>
<el-radio :label="2">等级2</el-radio>
<el-radio :label="3">等级3</el-radio>
</el-radio-group>
</el-form-item>
<div class="items-container">
<div class="item-title">
保护前提:
<div style="display: inline-block; margin-left: 20px">
<el-form-item label="延时" prop="faultDelaySeconds">
<el-input
v-model="formData.faultDelaySeconds"
placeholder="请输入"
clearable
:style="{ width: '200px', display: 'inline-block' }"
></el-input>
</el-form-item>
</div>
</div>
<div>
<el-button
@click.native.prevent="addRow('protectionSettings')"
block
type="primary"
size="mini"
style="margin-bottom: 20px"
>
新增保护前提
</el-button>
</div>
<div class="item-content">
<div class="time-lists-container">
<div class="time-lists time-lists-title">
<div>设备类型</div>
<div>点位</div>
<div>故障值比较符号</div>
<div>故障值</div>
<div>释放值比较符号</div>
<div>释放值</div>
<div>关系</div>
<div>操作</div>
</div>
<div
class="time-lists"
v-for="(item, index) in protectionSettings"
:key="'protectionSettings' + index"
>
<div>
<el-cascader
v-model="item.deviceId"
:options="childOptions"
:props="props"
:show-all-levels="false"
:ref="'protectionSettings'+index"
@change="(v)=>handleChange(v,'protectionSettings',index)"
></el-cascader>
</div>
<div>
<el-autocomplete
v-model="item.point"
placeholder="请输入点位"
clearable
:fetch-suggestions="
(q, c) =>
querySearchAsync(q, c, index, 'protectionSettings')
"
@select="(v) => handleSelect(v, index, 'protectionSettings')"
></el-autocomplete>
</div>
<div>
<el-select v-model="item.faultOperator" placeholder="请选择">
<el-option
v-for="(value, key) in comparisonOperatorOptions"
:key="key + 'faultOperator'"
:label="key"
:value="value"
></el-option>
</el-select>
</div>
<div>
<el-input placeholder="请输入故障值" v-model="item.faultValue">
</el-input>
</div>
<div>
<el-select v-model="item.releaseOperator" placeholder="请选择">
<el-option
v-for="(value, key) in comparisonOperatorOptions"
:key="key + 'releaseOperator'"
:label="key"
:value="value"
></el-option>
</el-select>
</div>
<div>
<el-input
placeholder="请输入释放值"
v-model="item.releaseValue"
>
</el-input>
</div>
<div>
<el-select v-model="item.relationNext" placeholder="请选择">
<el-option
v-for="(value, key) in relationWithPoint"
:key="key + 'relation'"
:label="key"
:value="value"
></el-option>
</el-select>
</div>
<div>
<el-button
@click.native.prevent="deleteRow(index,'protectionSettings')"
type="warning"
size="mini"
>
删除
</el-button>
</div>
</div>
</div>
</div>
</div>
<div class="items-container">
<div class="item-title">
保护方案:
<div style="display: inline-block; margin-left: 20px">
<el-form-item label="延时" prop="releaseDelaySeconds">
<el-input
v-model="formData.releaseDelaySeconds"
placeholder="请输入"
clearable
:style="{ width: '200px', display: 'inline-block' }"
></el-input>
</el-form-item>
</div>
</div>
<div>
<el-button
@click.native.prevent="addRow('protectionPlan')"
block
type="primary"
size="mini"
style="margin-bottom: 20px"
>
新增保护方案
</el-button>
</div>
<div class="item-content">
<div class="time-lists-container">
<div class="time-lists time-lists-title">
<div>设备类型</div>
<div>点位</div>
<div>故障值比较符号</div>
<div>故障值</div>
<div>操作</div>
</div>
<div
class="time-lists"
v-for="(item, index) in protectionPlan"
:key="'protectionPlan' + index"
>
<div>
<el-cascader
v-model="item.deviceId"
:show-all-levels="false"
:options="childOptions"
:props="props"
:ref="'protectionPlan'+index"
@change="(v)=>handleChange(v,'protectionPlan',index)"
></el-cascader>
</div>
<div>
<el-autocomplete
v-model="item.point"
placeholder="请输入点位"
clearable
:fetch-suggestions="
(q, c) => querySearchAsync(q, c, index, 'protectionPlan')
"
@select="(v) => handleSelect(v, index, 'protectionPlan')"
></el-autocomplete>
</div>
<div>=</div>
<div>
<el-input placeholder="请输入故障值" v-model="item.value">
</el-input>
</div>
<div>
<el-button
@click.native.prevent="deleteRow(index,'protectionPlan')"
type="warning"
size="mini"
>
删除
</el-button>
</div>
</div>
</div>
</div>
</div>
</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 {mapState} from "vuex";
import {getAllSites} from "@/api/ems/zddt";
import {validText} from "@/utils/validate";
import {addProtectPlan, getDeviceListBySiteAndCategory, getProtectPlan, updateProtectPlan} from "@/api/ems/site";
import {getAllDeviceCategory, pointFuzzyQuery} from "@/api/ems/search";
export default {
data() {
const validateText = (rule, value, callback) => {
if (value !== "" && !validText(value)) {
callback(new Error("只能输入中文英文数字和特殊字符!"));
} else {
callback();
}
};
return {
props: {
// emitPath: false,//在选中节点改变时,是否返回由该节点所在的各级菜单的值所组成的数组,若设置 false则只返回该节点的值
lazy: true,
lazyLoad: (node, resolve) => {
console.log('---------node', node)
this.getDeviceList(node, resolve)
}
},
mode: '',
loading: 0,
childOptions: [],
protectionSettings: [],
protectionPlan: [],
dialogTableVisible: false,
siteList: [], //站点列表 从接口获取数据
formData: {
id: "", //设备唯一标识
siteId: "", //站点ID
faultName: "", //设备保护名称
isAlert: 0, //是否告警
faultLevel: 1, //告警等级
faultDelaySeconds: "", //故障延时
releaseDelaySeconds: "", //释放延时
description: '',//方案描述
},
rules: {
siteId: [
{
required: true,
message: "请选择站点",
trigger: ["blur", "change"],
},
],
faultName: [
{required: true, message: "请输入设备保护名称", trigger: "blur"},
],
isAlert: [
{required: true, message: "请选择是否告警", trigger: "blur"},
],
description: [
{required: true, message: "请输入设备描述", trigger: "blur"},
{validator: validateText, trigger: "blur"},
],
faultDelaySeconds: [
{required: true, message: "请输入保护前提延时", trigger: "blur"},
{validator: validateText, trigger: "blur"},
],
releaseDelaySeconds: [
{required: true, message: "请输入保护方案延时", trigger: "blur"},
{validator: validateText, trigger: "blur"},
],
},
};
},
computed: {
...mapState({
communicationStatusOptions: (state) =>
state?.ems?.communicationStatusOptions || {},
deviceTypeOptions: (state) => state?.ems?.deviceTypeOptions || {},
comparisonOperatorOptions: (state) =>
state?.ems?.comparisonOperatorOptions || {},
relationWithPoint: (state) => state?.ems?.relationWithPoint || {},
}),
},
methods: {
open(id, siteId) {
this.dialogTableVisible = true
this.getZdList();
this.getDeviceCategoryList().then(() => {
if (id && siteId) {
// this.getDeviceList('PCS', siteId)
// this.getDeviceList('STACK', siteId)
}
});
if (id) {
this.formData.id = id
this.mode = 'edit'
getProtectPlan(id).then(response => {
const data = response?.data || {}
this.formData = {
id,
siteId: data?.siteId || '', //站点ID
faultName: data?.faultName || '', //设备保护名称
isAlert: data?.isAlert || 0, //是否告警
faultLevel: data?.faultLevel || 1, //告警等级
faultDelaySeconds: data?.faultDelaySeconds || "", //故障延时
releaseDelaySeconds: data?.releaseDelaySeconds || "", //释放延时
description: data?.description || '',//方案描述
}
const plan = (JSON.parse(data?.protectionPlan || [])).map(item => {
const index = this.childOptions.findIndex(i => i.value === item.deviceCategory)
if (index > -1) {
!this.childOptions[index].children.find(i => i.value === item.deviceId) && this.childOptions[index].children.push({
value: item.deviceId,
label: item.deviceName,
leaf: true
})
}
return Object.assign({}, item, {
deviceId: [item.deviceCategory || '', item.deviceId || ''],
deviceName: [item.categoryName || '', item.deviceName || ''],
})
})
const settings = (JSON.parse(data?.protectionSettings || [])).map(item => {
const index = this.childOptions.findIndex(i => i.value === item.deviceCategory)
if (index > -1) {
!this.childOptions[index].children.find(i => i.value === item.deviceId) && this.childOptions[index].children.push({
value: item.deviceId,
label: item.deviceName,
leaf: true
})
}
return Object.assign({}, item, {
deviceId: [item.deviceCategory || '', item.deviceId || ''],
deviceName: [item.categoryName || '', item.deviceName || ''],
})
})
this.$nextTick(() => {
this.protectionPlan.splice(0, 0, ...plan)
this.protectionSettings.splice(0, 0, ...settings)
})
console.log('获取设备保护详情并初始化', this.formData, this.protectionPlan, this.protectionSettings, this.childOptions)
})
} else {
this.mode = 'add'
}
},
// 新增设备保护前提、设备保护方案
addRow(type) {
const item = type === 'protectionSettings' ? {
deviceId: [],//设备ID
deviceName: [],
deviceCategory: "",//设备类型 英文
categoryName: '',//设备类型名称 中文
point: "",//点位 英文
pointName: "",//点位 中文
faultValue: "",//故障值
releaseValue: "",//释放值
faultOperator: "",//故障值比较关系
releaseOperator: "",//释放值比较关系
relationNext: "",//与下一个点位的关系
} : {
deviceId: [],
deviceName: [],
deviceCategory: "",//设备类型 英文
categoryName: '',//设备类型名称 中文
point: "",
pointName: "",
value: "",//设置值
}
// this.$set(this[type], this[type].length, item);
this[type].splice(this[type].length, 0, item)
console.log('新增设备保护前提、方案', type, this[type])
},
// 删除设备保护前提、设备保护方案
deleteRow(index, type) {
this[type].splice(index, 1);
},
// 设备保护前提、设备保护方案点位选择
querySearchAsync(query, cb, index, type) {
console.log("查询数据", query, index);
if (!this.formData.siteId || !this[type][index].deviceCategory) {
this.$message({
type: "warning",
message: "请先选择站点和设备",
});
return cb([]);
}
pointFuzzyQuery({
siteIds: [this.formData.siteId],
deviceCategory: this[type][index].deviceCategory,
pointName: query,
}).then((response) => {
const data = response?.data || [];
cb(
data.map((item) => {
return {name: item, value: item};
})
);
});
},
// 点位选择
handleSelect(data, index, type) {
console.log('选择点位', data, index, type)
// this.$set(this[type], index, Object.assign({},this[type][index],{
// point:data.value,
// pointName:data.value,
// }));
let line = Object.assign({}, this[type][index], {
point: data.value,
pointName: data.value,
})
this[type].splice(index, 1, line);
console.log('选择点位配置完成', this[type][index])
},
// 获取设备类别-不区分站点
getDeviceCategoryList() {
this.loading += 1;
return getAllDeviceCategory()
.then((response) => {
const data = (response?.data || []);
this.$set(this, 'childOptions', [])
let arr = []
data.forEach((item) => {
arr.push({
value: item.code,
label: item.name,
leaf: false,
children: []
})
})
this.childOptions.splice(0, 0, ...arr)
console.log('获取设备类型', data, this.childOptions)
})
.finally(() => {
this.loading -= 1;
});
},
//获取设备列表-区分站点
getDeviceList(node, resolve) {
if (!this.formData.siteId) {
this.$message({
type: "warning",
message: "请先选择站点",
});
return resolve([])
}
getDeviceListBySiteAndCategory({
siteId: this.formData.siteId,
deviceCategory: node.value
}).then((response) => {
const data = (response?.data || []).map(item => {
return {
label: item.deviceName,
value: item.id,
leaf: true
}
})
resolve(data)
}).catch(() => {
resolve([])
})
},
//更新站点下面的设备列表
updateSiteDeviceList() {
this.childOptions.forEach(item => {
const length = item.children.length
item.children.splice(0, length)
})
},
//选中设备类型、设备
handleChange(data, type, index) {
console.log('设置选中设备类型、设备', data, type, index)
const {label = '', parent = {}} = this.$refs[type + index][0].getCheckedNodes()[0]
console.log('选中设备的名称', parent.label, label)
const item = Object.assign({}, this[type][index], {
deviceId: data,
deviceName: [parent.label, label],
deviceCategory: data[0],
categoryName: parent.label,
pointName: '',
point: ''
})
this.$nextTick(() => {
// this.$set(this[type], index, item);
this[type].splice(index, 1, item);
})
console.log('设置选中设备类型、设备配置完成', this[type][index])
},
//获取站点列表
getZdList() {
this.loading += 1;
getAllSites()
.then((response) => {
this.siteList = response?.data || [];
})
.finally(() => {
this.loading -= 1;
});
},
// 切换站点
// 重新获取设备列表
// 清空选中的设备、点位信息
changeType() {
//获取当前站点下的pcs和bms
this.updateSiteDeviceList()
if (this.protectionSettings.length > 0) {
const list = this.protectionSettings
list.forEach((item) => {
item.point = ""
item.pointName = ""
item.deviceId = []
item.deviceName = []
item.categoryName = ''
item.deviceCategory = ''
});
// this.$set(this,'protectionSettings',list)
this.$nextTick(() => {
this.protectionSettings.splice(0, this.protectionSettings.length, ...list)
})
}
if (this.protectionPlan.length > 0) {
const list = this.protectionPlan
list.forEach((item) => {
item.point = ""
item.pointName = ""
item.deviceId = []
item.deviceName = []
item.categoryName = ''
item.deviceCategory = ''
});
// this.$set(this,'protectionPlan',list)
this.$nextTick(() => {
this.protectionPlan.splice(0, this.protectionPlan.length, ...list)
})
}
},
saveDialog() {
function getToastMsg(name, type, index) {
return {
protectionSettings: {
deviceId: `请选择保护前提第${index}行的设备`,//设备ID
deviceCategory: `请选择保护前提第${index}行的设备类型`,//设备类型 英文
categoryName: `请选择保护前提第${index}行的设备类型`,//设备类型名称 中文
point: `请选择保护前提第${index}行的点位`,//点位 英文
pointName: `请选择保护前提第${index}行的点位`,//点位 中文
faultValue: `请输入保护前提第${index}行的故障值`,//故障值
releaseValue: `请输入保护前提第${index}行的释放值`,//释放值
faultOperator: `请选择保护前提第${index}行的故障值比较关系`,//故障值比较关系
releaseOperator: `请选择保护前提第${index}行的释放值比较关系`,//释放值比较关系
relationNext: `请选择保护前提第${index}行与下一个点位的关系`,//与下一个点位的关系
},
protectionPlan: {
deviceId: `请选择保护方案第${index}行的设备`,
deviceCategory: `请选择保护方案第${index}行的设备类型`,//设备类型 英文
categoryName: `请选择保护方案第${index}行的设备类型`,//设备类型名称 中文
point: `请选择保护方案第${index}行的点位`,
pointName: `请选择保护方案第${index}行的点位`,
value: `请输入保护方案第${index}行的故障值`,//设置值
}
}[type][name]
}
this.$refs.addTempForm.validate((valid) => {
if (!valid) return;
const {
id = "", //设备唯一标识
siteId = "", //站点ID
faultName = "", //设备保护名称
isAlert = 0, //是否告警
faultLevel = 1, //告警等级
faultDelaySeconds = "", //故障延时
releaseDelaySeconds = "", //释放延时
description = "",//方案描述
} = this.formData;
const {protectionSettings, protectionPlan} = this
let protectionSettingsValidateStatus = true, protectionPlanValidateStatus = true
for (let i = 0; i < protectionSettings.length; i++) {
let valueMap = Object.entries(protectionSettings[i]);
for (let inner = 0; inner < valueMap.length; inner++) {
const key = valueMap[inner][0], value = valueMap[inner][1]
if (key === 'relationNext') {
if (protectionSettings[i + 1] && !value) {//有下一个点位
this.$message.error(getToastMsg(key, 'protectionSettings', i + 1))
protectionSettingsValidateStatus = false
break
}
} else {
if (![0, '0'].includes(value) && !value) {
this.$message.error(getToastMsg(key, 'protectionSettings', i + 1))
protectionSettingsValidateStatus = false
break
}
}
}
if (!protectionSettingsValidateStatus) break
}
for (let i = 0; i < protectionPlan.length; i++) {
let valueMap = Object.entries(protectionPlan[i]);
for (let inner = 0; inner < valueMap.length; inner++) {
const key = valueMap[inner][0], value = valueMap[inner][1]
if (key === 'relationNext') {
if (protectionPlan[i + 1] && !value) {//有下一个点位
this.$message.error(getToastMsg(key, 'protectionPlan', i + 1))
protectionPlanValidateStatus = false
break
} else {
// protectionPlan[i][key] = ''//清空选择的关系
}
} else {
if (![0, '0'].includes(value) && !value) {
this.$message.error(getToastMsg(key, 'protectionPlan', i + 1))
protectionPlanValidateStatus = false
break
}
}
}
if (!protectionPlanValidateStatus) break
}
if (!protectionSettingsValidateStatus || !protectionPlanValidateStatus) return
const settings = protectionSettings.map(item => {
return Object.assign({}, item, {
deviceId: item.deviceId[1],
deviceName: item.deviceName[1],
})
})
const plan = protectionPlan.map(item => {
return Object.assign({}, item, {
deviceId: item.deviceId[1],
deviceName: item.deviceName[1],
})
})
this.loading += 1;
const params = {
siteId,
faultName,
isAlert,
faultLevel,
faultDelaySeconds,
releaseDelaySeconds,
description,
protectionSettings: JSON.stringify(settings),
protectionPlan: JSON.stringify(plan),
}
if (this.mode === "add") {
addProtectPlan(params)
.then((response) => {
if (response.code === 200) {
//新增成功
// 关闭弹窗 更新表格
this.$emit("update");
this.closeDialog();
}
})
.finally(() => {
this.loading -= 1;
});
} else {
params.id = id
updateProtectPlan(params)
.then((response) => {
if (response.code === 200) {
//新增成功
// 关闭弹窗 更新表格
this.$emit("update");
this.closeDialog();
}
})
.finally(() => {
this.loading -= 1;
});
}
});
},
closeDialog() {
this.$emit("clear");
// 清空所有数据
for (let key in this.formData) {
this.formData[key] = key === 'isAlert' ? 0 : key === 'faultLevel' ? 1 : ''
}
this.$refs.addTempForm.resetFields();
this.$set(this, 'protectionSettings', [])
this.$set(this, 'protectionPlan', [])
this.$set(this, 'childOptions', [])
this.dialogTableVisible = false;
},
},
};
</script>
<style scoped lang="scss">
.items-container {
margin-top: 40px;
margin-bottom: 20px;
.item-title {
line-height: 16px;
padding: 10px 0;
color: #000;
}
}
.time-lists-container {
width: 100%;
border: 1px solid #eee;
.time-lists {
&:not(:last-child) {
border-bottom: 1px solid #eee;
}
display: flex;
& > div {
width: 16%;
box-sizing: border-box;
text-align: center;
padding: 10px 15px;
&:not(:last-child) {
width: 28%;
border-right: 1px solid #eee;
}
.el-date-editor.el-input,
.el-date-editor.el-input__inner {
width: 100%;
}
}
}
.time-lists-title {
color: #000;
font-size: 12px;
font-weight: bold;
line-height: 20px;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -1,160 +1,185 @@
<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="站点选择">
<el-select
v-model="form.siteId"
placeholder="请选择换电站名称"
:loading="searchLoading"
loading-text="正在加载数据"
clearable
>
<el-option
:label="item.siteName"
:value="item.siteId"
v-for="(item, index) in siteList"
:key="index + 'zdxeSelect'"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="故障名称">
<el-input
<div class="protect-plan-page" v-loading="loading">
<el-card class="query-card" shadow="never">
<div class="query-head">
<div class="query-title">设备保护方案</div>
<el-button type="primary" @click="addPlan" native-type="button">新增方案</el-button>
</div>
<el-form :inline="true" class="query-form" @submit.native.prevent>
<el-form-item label="故障名称">
<el-input
v-model="form.faultName"
clearable
placeholder="请输入故障名称"
style="width: 150px"
></el-input>
</el-form-item>
<el-form-item>
style="width: 220px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSearch" native-type="button">搜索</el-button>
<el-button @click="onReset" native-type="button">重置</el-button>
</el-form-item>
</el-form>
<el-button type="primary" @click="addDevice" native-type="button"
>新增设备</el-button
>
<el-table
class="common-table"
:data="tableData"
stripe
max-height="600px"
style="width: 100%; margin-top: 25px"
>
<el-table-column prop="siteId" label="站点" width="100"> </el-table-column>
<el-table-column prop="faultName" label="设备保护名称" width="100"> </el-table-column>
<el-table-column prop="faultLevel" label="故障等级" width="100">
<template slot-scope="scope">等级{{scope.row.faultLevel}}</template>
</el-table-column>
<el-table-column prop="isAlert" label="是否告警" width="100">
<template slot-scope="scope">{{scope.row.isAlert === 1 ? '是' : '否'}}</template>
</el-table-column>
<el-table-column prop="description" label="处理方案描述" width="200" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="protectionSettings" label="保护前提" show-overflow-tooltip width="400">
<template slot-scope="scope">
<div v-html="handleProtectionSettings(scope.row.protectionSettings)"></div>
</template>
</el-table-column>
<el-table-column prop="faultDelaySeconds" label="保护前提延时(s)" width="120">
</el-table-column>
<el-table-column prop="protectionPlan" label="保护方案" show-overflow-tooltip width="200">
<template slot-scope="scope">
<div v-html="handleProtectionPlan(scope.row.protectionPlan)"></div>
</template>
</el-table-column>
<el-table-column prop="releaseDelaySeconds" label="保护方案延时(s)" width="120">
</el-table-column>
<el-table-column fixed="right" label="操作" width="150">
<template slot-scope="scope">
<el-button @click="editDevice(scope.row)" type="warning" size="mini">
编辑
</el-button>
<el-button type="danger" @click="deleteDevice(scope.row)" size="mini">
删除
</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-device
ref="addDevice"
@update="getData"
/>
</el-form-item>
</el-form>
</el-card>
<el-card class="table-card" shadow="never">
<el-table class="common-table" :data="tableData" stripe max-height="620px">
<el-table-column prop="faultName" label="设备保护名称" min-width="140" />
<el-table-column prop="faultLevel" label="故障等级" width="100">
<template slot-scope="scope">等级{{ scope.row.faultLevel }}</template>
</el-table-column>
<el-table-column prop="isAlert" label="是否告警" width="100">
<template slot-scope="scope">{{ scope.row.isAlert === 1 ? '是' : '否' }}</template>
</el-table-column>
<el-table-column prop="description" label="处理方案描述" min-width="180" show-overflow-tooltip />
<el-table-column prop="protectionSettings" label="故障/释放保护" min-width="360" show-overflow-tooltip>
<template slot-scope="scope">
<div class="rich-lines" v-html="handleProtectionSettings(scope.row.protectionSettings)"></div>
</template>
</el-table-column>
<el-table-column prop="faultDelaySeconds" label="前提延时(s)" width="110" />
<el-table-column prop="protectionPlan" label="执行保护" min-width="260" show-overflow-tooltip>
<template slot-scope="scope">
<div class="rich-lines" v-html="handleProtectionPlan(scope.row.protectionPlan)"></div>
</template>
</el-table-column>
<el-table-column prop="releaseDelaySeconds" label="方案延时(s)" width="110" />
<el-table-column fixed="right" label="操作" width="150">
<template slot-scope="scope">
<el-button @click="editDevice(scope.row)" type="warning" size="mini">编辑</el-button>
<el-button type="danger" @click="deleteDevice(scope.row)" size="mini">删除</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"
class="pager"
/>
</el-card>
<add-plan ref="addPlan" @update="getData" />
</div>
</template>
<script>
import {
protectPlanList,
deleteProtectPlan,
} from "@/api/ems/site";
import { getAllSites } from "@/api/ems/zddt";
import AddDevice from "./AddDevice.vue";
import { protectPlanList, deleteProtectPlan } from "@/api/ems/site";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import AddPlan from "./AddPlan.vue";
export default {
name: "SBBH",
components: { AddDevice },
components: { AddPlan },
mixins: [getQuerySiteId],
data() {
return {
form:{
siteId:'',
faultName:''
form: {
faultName: "",
},
loading: false,
searchLoading: false,
siteList: [],
tableData: [],
pageSize: 10, //分页栏当前每个数据总数
pageNum: 1, //分页栏当前页数
totalSize: 0, //table表格数据总数
pageSize: 10,
pageNum: 1,
totalSize: 0,
dialogTableVisible: false,
};
},
methods: {
handleProtectionSettings(data){
if(!data || !JSON.parse(data)) return
const arr = JSON.parse(data),
str= arr.map((item,index)=>{
const {categoryName='',deviceId='',point='',faultOperator='',faultValue='',releaseOperator='',releaseValue='',relationNext=''} = item
return `<div>${index+1}、 <span>${categoryName ? categoryName + '-' : ''}${deviceId ? deviceId + '-' : ''}${ point || ''}</span> <span>故障:${faultOperator || ''}${ faultValue || ''}</span> <span>释放:${releaseOperator || ''}${releaseValue || ''}</span> ${arr[index+1] ? '<span>关系:'+(relationNext || '')+'</span>' : ''}</div>`
})
return str.join('')
init() {
this.pageNum = 1;
this.getData();
},
handleProtectionPlan(data){
if(!data || !JSON.parse(data)) return
const arr = JSON.parse(data),
str= arr.map((item,index)=>{
const {categoryName='',deviceId='',point='',value=''} = item
return `<div>${index+1}、 <span>${categoryName ? categoryName + '-' : ''}${deviceId ? deviceId + '-' : ''}${ point || ''}</span> <span>故障:=${ value || ''}</span> </div>`
})
return str.join('')
handleProtectionSettings(data) {
if (!data) return;
let parsed = null;
try {
parsed = JSON.parse(data);
} catch (e) {
return "";
}
const faultSettings = Array.isArray(parsed) ? parsed : parsed?.faultSettings || [];
const releaseSettings = Array.isArray(parsed) ? parsed : parsed?.releaseSettings || [];
const buildLine = (item, index, total, key, value, relationKey) => {
const {
categoryName = "",
deviceId = "",
point = "",
[key]: operator = "",
[value]: val = "",
relationNext = "",
} = item;
return `<div>${index + 1}、 <span>${categoryName ? categoryName + "-" : ""}${
deviceId ? deviceId + "-" : ""
}${point || ""}</span> <span>${relationKey}:${operator || ""}${val || ""}</span> ${
total[index + 1] ? "<span>关系:" + (relationNext || "") + "</span>" : ""
}</div>`;
};
const faultStr = faultSettings.map((item, index) =>
buildLine(item, index, faultSettings, "faultOperator", "faultValue", "故障")
);
const releaseStr = releaseSettings.map((item, index) =>
buildLine(item, index, releaseSettings, "releaseOperator", "releaseValue", "释放")
);
const groups = [];
if (faultStr.length) {
groups.push(`<div><strong>故障保护</strong></div>${faultStr.join("")}`);
}
if (releaseStr.length) {
groups.push(`<div><strong>释放保护</strong></div>${releaseStr.join("")}`);
}
return groups.join("");
},
// 新增设备 展示弹窗
addDevice() {
this.$refs.addDevice.open()
handleProtectionPlan(data) {
if (!data) return;
let arr = [];
try {
const parsed = JSON.parse(data);
arr = Array.isArray(parsed) ? parsed : parsed ? [parsed] : [];
} catch (e) {
return "";
}
const actionLabelMap = {
derate: "降功率",
shutdown: "关机/停机/切断",
forbid_charge: "禁止充电",
allow_discharge: "允许放电",
forbid_discharge: "禁止放电",
allow_charge: "允许充电",
forbid_charge_discharge: "禁止充放电",
standby: "待机",
};
const str = arr.map((item, index) => {
const action = item?.action || "";
const point = item?.point || "";
const pointName = item?.pointName || "";
const actionName = item?.actionName || actionLabelMap[action] || pointName || point || "未配置";
const value = item?.value;
if ((action === "derate" || actionName.includes("降功率")) && value !== null && value !== undefined && value !== "") {
return `<div>${index + 1}、 <span>动作:${actionName}</span> <span>比例:${value}%</span></div>`;
}
return `<div>${index + 1}、 <span>动作:${actionName}</span></div>`;
});
return str.join("");
},
addPlan() {
if (!this.siteId) {
this.$message.warning("请先在顶部选择站点");
return;
}
this.$refs.addPlan.open("", this.siteId);
},
// 编辑设备
editDevice(row) {
this.$refs.addDevice.open(row.id,row.siteId)
this.$refs.addPlan.open(row.id, this.siteId);
},
//删除设备
deleteDevice(row) {
console.log('删除')
this.$confirm(`确认要设备保护${row.faultName}吗?`, {
confirmButtonText: "确定",
cancelButtonText: "取消",
@ -177,19 +202,14 @@ export default {
},
})
.then(() => {
//只有在废弃成功的情况下会走到这里
this.$message({
type: "success",
message: "删除成功!",
});
this.getData();
//调用接口 更新表格数据
})
.catch(() => {
//取消关机
});
.catch(() => {});
},
// 分页
handleSizeChange(val) {
this.pageSize = val;
this.$nextTick(() => {
@ -202,25 +222,27 @@ export default {
this.getData();
});
},
// 搜索
onSearch() {
this.pageNum = 1; //每次搜索从1开始搜索
this.pageNum = 1;
this.getData();
},
// 重置
onReset() {
this.form={
siteId: "",
this.form = {
faultName: "",
}
this.pageNum = 1; //每次搜索从1开始搜索
};
this.pageNum = 1;
this.getData();
},
// 获取数据
getData() {
if (!this.siteId) {
this.tableData = [];
this.totalSize = 0;
return;
}
this.loading = true;
const { pageNum, pageSize } = this,{siteId,faultName=''}=this.form;
protectPlanList({ siteId, faultName,pageNum, pageSize })
const { pageNum, pageSize } = this;
const { faultName = "" } = this.form;
protectPlanList({ siteId: this.siteId, faultName, pageNum, pageSize })
.then((response) => {
this.tableData = response?.rows || [];
this.totalSize = response?.total || 0;
@ -229,31 +251,50 @@ export default {
this.loading = false;
});
},
//获取站点列表
getZdList() {
this.searchLoading = true;
return getAllSites()
.then((response) => {
this.siteList = response?.data || [];
if (this.siteList.length > 0) this.form.siteId = this.siteList[0].siteId;
})
.finally(() => {
this.searchLoading = false;
});
},
},
mounted() {
this.loading = true;
this.form = {
siteId: "",
faultName: "",
};
this.pageNum = 1; //每次搜索从1开始搜索
this.getZdList().then(() => {
this.getData();
});
},
};
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.protect-plan-page {
padding: 16px;
background: linear-gradient(180deg, #f5f8ff 0%, #f7f9fc 100%);
.query-card,
.table-card {
border-radius: 12px;
border: 1px solid #e7edf7;
margin-bottom: 12px;
}
.query-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.query-title {
font-size: 16px;
font-weight: 600;
color: #1d2a3a;
letter-spacing: 0.5px;
}
.query-form {
display: flex;
flex-wrap: wrap;
margin-bottom: -18px;
}
.rich-lines {
color: #3e4b5a;
line-height: 1.45;
}
.pager {
margin-top: 16px;
text-align: center;
}
}
</style>

View File

@ -3,133 +3,124 @@
:show-close="false" destroy-on-close lock-scroll append-to-body width="800px" class="ems-dialog"
:title="mode === 'add'?'新增设备':`编辑设备` ">
<div v-loading="loading>0">
<el-form v-loading="loading>0" ref="addTempForm" inline :model="formData" :rules="rules" size="medium"
label-width="120px" class="device-form">
<el-form-item label="站点" prop="siteId">
<el-select v-model="formData.siteId" placeholder="请选择" :style="{width: '100%'}" @change="changeType">
<el-option :label="item.siteName" :value="item.siteId" v-for="(item,index) in siteList"
:key="index+'siteOptions'"></el-option>
</el-select>
</el-form-item>
<el-form-item label="设备id" prop="deviceId">
<el-input v-model="formData.deviceId" placeholder="请输入" maxlength="60" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="设备名称" prop="deviceName">
<el-input v-model="formData.deviceName" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="设备描述" prop="description">
<el-input v-model="formData.description" type="textarea" placeholder="请输入" clearable
:style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="工作状态" prop="communicationStatus">
<el-select v-model="formData.communicationStatus" placeholder="请选择" :style="{width: '100%'}">
<el-option :label="value" :value="key" v-for="(value,key) in communicationStatusOptions"
:key="key+'communicationStatusOptions'"></el-option>
</el-select>
</el-form-item>
<el-form-item label="设备类型" prop="deviceType">
<el-select v-model="formData.deviceType" placeholder="请选择" :style="{width: '100%'}">
<el-option :label="value" :value="key" v-for="(value,key) in deviceTypeOptions"
:key="key+'deviceTypeOptions'"></el-option>
</el-select>
</el-form-item>
<el-form-item label="设备类别" prop="deviceCategory">
<el-select v-model="formData.deviceCategory" placeholder="请选择" :style="{width: '100%'}"
@change="changeType">
<el-option :label="item.name" :value="item.code" v-for="(item,index) in deviceCategoryList"
:key="index+'deviceCategoryList'"></el-option>
</el-select>
</el-form-item>
<el-form-item label="上级设备" prop="parentId" v-if="dccDeviceCategoryList.includes(formData.deviceCategory)">
<el-select v-model="formData.parentId"
:placeholder="parentDeviceList.length === 0 && !formData.siteId ? '请先选择站点' : '请选择'"
:style="{width: '100%'}">
<el-option :label="item.deviceName" :value="item.id" v-for="(item,index) in parentDeviceList"
:key="index+'parentDeviceList'"></el-option>
</el-select>
</el-form-item>
<el-form-item label="TCP设备的ip地址" prop="ipAddress" v-if="formData.deviceType === 'TCP'">
<el-input v-model="formData.ipAddress" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="TCP设备的端口号" prop="ipPort" v-if="formData.deviceType === 'TCP'">
<el-input v-model="formData.ipPort" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="从站地址" prop="slaveId" v-if="formData.deviceType === 'TCP'">
<el-input v-model="formData.slaveId" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="串口路径" prop="serialPort">
<el-input v-model="formData.serialPort" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="波特率" prop="baudRate">
<el-input v-model="formData.baudRate" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="数据位" prop="dataBits">
<el-input v-model="formData.dataBits" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="停止位" prop="stopBits">
<el-input v-model="formData.stopBits" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="校验位" prop="parity">
<el-input v-model="formData.parity" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="图片" prop="pictureUrl">
<image-upload :limit="1" :drag="false" @input="uploadImage" :value="formData.pictureUrl"/>
</el-form-item>
</el-form>
<!-- pcs配置-->
<el-form v-if="isPcs" ref="pcsSettingForm" inline :model="pcsSetting" size="medium"
label-width="120px" class="device-form" :rules="pcsSettingRules">
<div style="font-size: 14px;padding: 10px 0 20px;font-weight: 600;">PCS配置</div>
<el-form-item label="开关机地址" prop="pointAddress">
<el-input v-model="pcsSetting.pointAddress" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="功率地址" prop="powerAddress">
<el-input v-model="pcsSetting.powerAddress" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="开机指令" prop="startCommand">
<el-input v-model="pcsSetting.startCommand" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="开机目标功率" prop="startPower">
<el-input v-model="pcsSetting.startPower" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="关机指令" prop="stopCommand">
<el-input v-model="pcsSetting.stopCommand" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="关机目标功率" prop="stopPower">
<el-input v-model="pcsSetting.stopPower" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="电池簇数" prop="clusterNum">
<el-input v-model="pcsSetting.clusterNum" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<br>
<template v-for="index in parseInt(pcsSetting.clusterNum) || 0">
<el-form-item :label="'电池簇'+(index)+'地址'"
prop="clusterPointAddress">
<el-input v-model="pcsSetting.clusterPointAddress[index-1]" placeholder="请输入" clearable
<div class="form-layout" :class="{ 'has-pcs': isPcs }">
<el-form v-loading="loading>0" ref="addTempForm" inline :model="formData" :rules="rules" size="medium"
label-width="120px" class="device-form base-form">
<el-form-item label="站点" prop="siteId">
<el-input
v-model="formData.siteId"
placeholder="请先在顶部选择站点"
disabled
:style="{width: '100%'}"
/>
</el-form-item>
<el-form-item label="设备id" prop="deviceId">
<el-input v-model="formData.deviceId" placeholder="请输入" maxlength="60" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="设备名称" prop="deviceName">
<el-input v-model="formData.deviceName" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="设备描述" prop="description">
<el-input v-model="formData.description" type="textarea" placeholder="请输入" clearable
:style="{width: '100%'}">
</el-input>
</el-form-item>
</template>
</el-form>
<el-form-item label="设备类型" prop="deviceType">
<el-select v-model="formData.deviceType" placeholder="请选择" :style="{width: '100%'}">
<el-option :label="value" :value="key" v-for="(value,key) in deviceTypeOptions"
:key="key+'deviceTypeOptions'"></el-option>
</el-select>
</el-form-item>
<el-form-item label="设备类别" prop="deviceCategory">
<el-select v-model="formData.deviceCategory" placeholder="请选择" :style="{width: '100%'}"
@change="changeType">
<el-option :label="item.name" :value="item.code" v-for="(item,index) in deviceCategoryList"
:key="index+'deviceCategoryList'"></el-option>
</el-select>
</el-form-item>
<el-form-item label="上级设备" prop="parentId" v-if="dccDeviceCategoryList.includes(formData.deviceCategory)">
<el-select v-model="formData.parentId"
:placeholder="parentDeviceList.length === 0 && !formData.siteId ? '请先在顶部选择站点' : '请选择'"
:style="{width: '100%'}">
<el-option :label="item.deviceName" :value="item.id" v-for="(item,index) in parentDeviceList"
:key="index+'parentDeviceList'"></el-option>
</el-select>
</el-form-item>
<el-form-item label="TCP设备的ip地址" prop="ipAddress" v-if="formData.deviceType === 'TCP'">
<el-input v-model="formData.ipAddress" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="TCP设备的端口号" prop="ipPort" v-if="formData.deviceType === 'TCP'">
<el-input v-model="formData.ipPort" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="从站地址" prop="slaveId" v-if="formData.deviceType === 'TCP'">
<el-input v-model="formData.slaveId" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="串口路径" prop="serialPort">
<el-input v-model="formData.serialPort" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="波特率" prop="baudRate">
<el-input v-model="formData.baudRate" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="数据位" prop="dataBits">
<el-input v-model="formData.dataBits" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="停止位" prop="stopBits">
<el-input v-model="formData.stopBits" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
<el-form-item label="校验位" prop="parity">
<el-input v-model="formData.parity" placeholder="请输入" clearable :style="{width: '100%'}">
</el-input>
</el-form-item>
</el-form>
<!-- pcs配置-->
<el-form v-if="isPcs" ref="pcsSettingForm" :model="pcsSetting" size="medium"
label-position="top" class="pcs-form" :rules="pcsSettingRules">
<div class="pcs-form__title">PCS配置</div>
<div class="pcs-form__grid">
<el-form-item label="开关机地址" prop="pointAddress" class="pcs-form__item">
<el-input v-model="pcsSetting.pointAddress" placeholder="请输入" clearable :style="{width: '100%'}" />
</el-form-item>
<el-form-item label="功率地址" prop="powerAddress" class="pcs-form__item">
<el-input v-model="pcsSetting.powerAddress" placeholder="请输入" clearable :style="{width: '100%'}" />
</el-form-item>
<el-form-item label="开机指令" prop="startCommand" class="pcs-form__item">
<el-input v-model="pcsSetting.startCommand" placeholder="请输入" clearable :style="{width: '100%'}" />
</el-form-item>
<el-form-item label="关机指令" prop="stopCommand" class="pcs-form__item">
<el-input v-model="pcsSetting.stopCommand" placeholder="请输入" clearable :style="{width: '100%'}" />
</el-form-item>
<el-form-item label="开机目标功率" prop="startPower" class="pcs-form__item">
<el-input v-model="pcsSetting.startPower" placeholder="请输入" clearable :style="{width: '100%'}" />
</el-form-item>
<el-form-item label="关机目标功率" prop="stopPower" class="pcs-form__item">
<el-input v-model="pcsSetting.stopPower" placeholder="请输入" clearable :style="{width: '100%'}" />
</el-form-item>
<el-form-item label="倍率" prop="powerMultiplier" class="pcs-form__item">
<el-input v-model="pcsSetting.powerMultiplier" placeholder="请输入" clearable :style="{width: '100%'}" />
</el-form-item>
<el-form-item label="电池簇数" prop="clusterNum" class="pcs-form__item">
<el-input v-model="pcsSetting.clusterNum" placeholder="请输入" clearable :style="{width: '100%'}" />
</el-form-item>
</div>
<div v-if="(parseInt(pcsSetting.clusterNum) || 0) > 0" class="pcs-form__cluster">
<div class="pcs-form__cluster-title">电池簇地址</div>
<template v-for="index in parseInt(pcsSetting.clusterNum) || 0">
<el-form-item :key="'clusterAddress' + index" :label="'电池簇' + index + '地址'" prop="clusterPointAddress">
<el-input v-model="pcsSetting.clusterPointAddress[index - 1]" placeholder="请输入" clearable :style="{width: '100%'}" />
</el-form-item>
</template>
</div>
</el-form>
</div>
</div>
<div slot="footer">
<el-button @click="closeDialog">取消</el-button>
@ -139,7 +130,6 @@
</template>
<script>
import {mapState} from "vuex";
import {getAllSites} from '@/api/ems/zddt'
import {validText} from '@/utils/validate'
import {addDevice, getDeviceDetailInfo, getParentDeviceId, updateDevice} from "@/api/ems/site";
import {getAllDeviceCategory} from '@/api/ems/search'
@ -177,12 +167,18 @@ export default {
callback();
}
}
const validateDecimal = (rule, value, callback) => {
if (value !== '' && !/^(0|[1-9]\d*)(\.\d+)?$/.test(value)) {
callback(new Error('只能输入非负数字!'));
} else {
callback();
}
}
return {
loading: 0,
dccDeviceCategoryList: ['CLUSTER', 'BATTERY'],//需要展示上级设备的设备类型
dialogTableVisible: false,
parentDeviceList: [],//上级设备列表 从接口获取数据
siteList: [],//站点列表 从接口获取数据
deviceCategoryList: [],//设备类别列表 从接口获取数据
formData: {
id: '',//设备唯一标识
@ -190,7 +186,6 @@ export default {
deviceId: '',//设备id
deviceName: '',//设备名称
description: '',//设备描述
communicationStatus: '',//工作状态
deviceType: '',//设备类型
deviceCategory: '',//设备类别
parentId: '',//上级设备id
@ -201,7 +196,6 @@ export default {
dataBits: '',//数据位
stopBits: '',//停止位
parity: '',//校验位
pictureUrl: '',//设备图片
slaveId: '',//从站地址
},
pcsSetting: {
@ -212,12 +206,13 @@ export default {
stopCommand: "",//关机指令
startPower: '',//开机目标功率
stopPower: '',//关机目标功率
powerMultiplier: '',//目标功率倍率
clusterNum: '',//电池簇数
clusterPointAddress: []//电池簇地址
},
rules: {
siteId: [
{required: true, message: '请选择站点', trigger: ['blur', 'change']}
{required: true, message: '请先在顶部选择站点', trigger: ['blur', 'change']}
],
deviceId: [
{required: true, message: '请输入设备id', trigger: 'blur'},
@ -231,9 +226,6 @@ export default {
{required: true, message: '请输入设备描述', trigger: 'blur'},
{validator: validateText, trigger: 'blur'}
],
communicationStatus: [
{required: true, message: '请选择工作状态', trigger: ['blur', 'change']}
],
deviceType: [
{required: true, message: '请选择设备类型', trigger: ['blur', 'change']}
],
@ -264,9 +256,6 @@ export default {
parity: [
{validator: validateText, trigger: 'blur'}
],
// pictureUrl:[
// { required: true, message: '请上传图片', trigger: ['blur', 'change']}
// ],
},
pcsSettingRules: {
pointAddress: [
@ -290,6 +279,9 @@ export default {
stopPower: [
{validator: validateText, trigger: 'blur'}
],
powerMultiplier: [
{validator: validateDecimal, trigger: 'blur'}
],
clusterNum: [
{required: true, message: '请输入电池簇数', trigger: 'blur'},
{validator: validateNumber, trigger: 'blur'}
@ -303,7 +295,6 @@ export default {
},
computed: {
...mapState({
communicationStatusOptions: state => state?.ems?.communicationStatusOptions || {},
deviceTypeOptions: state => state?.ems?.deviceTypeOptions || {}
}),
isPcs() {
@ -315,12 +306,22 @@ export default {
handler(newVal) {
//打开弹窗
if (newVal) {
this.getZdList()
if (this.mode === 'add') {
this.syncSiteFromRoute(true)
}
this.getDeviceCategoryList()
}
},
immediate: true,
},
'$route.query.siteId': {
handler() {
if (!this.dialogTableVisible || this.mode !== 'add') {
return
}
this.syncSiteFromRoute(true)
}
},
id: {
handler(newVal) {
if ((newVal || newVal === 0) && this.mode !== 'add') {
@ -346,23 +347,24 @@ export default {
}
},
methods: {
syncSiteFromRoute(force = false) {
const routeSiteId = this.$route?.query?.siteId
const normalizedSiteId = routeSiteId === undefined || routeSiteId === null ? '' : String(routeSiteId).trim()
if (!normalizedSiteId) {
if (force) {
this.formData.siteId = ''
}
return
}
if (force || !this.formData.siteId) {
this.formData.siteId = normalizedSiteId
}
},
changeType() {
if (this.dccDeviceCategoryList.includes(this.formData.deviceCategory)) {
this.getParentDeviceList()
}
},
uploadImage(data) {
this.formData.pictureUrl = data
},
//获取站点列表
getZdList() {
this.loading += 1
getAllSites().then(response => {
this.siteList = response?.data || []
}).finally(() => {
this.loading -= 1
})
},
// 获取设备类别
getDeviceCategoryList() {
this.loading += 1
@ -375,7 +377,8 @@ export default {
//获取上级id列表
getParentDeviceList(init = false) {
if (!this.formData.siteId) {
return console.log('请先选择站点')
this.$message.warning('请先在顶部选择站点')
return
}
!init && (this.formData.parentId = '')
this.loading = this.loading + 1
@ -393,7 +396,6 @@ export default {
deviceId = '',//设备id
deviceName = '',//设备名称
description = '',//设备描述
communicationStatus = '',//工作状态
deviceType = '',//设备类型
deviceCategory = '',//设备类别
parentId = '',//上级设备id
@ -404,7 +406,6 @@ export default {
dataBits = '',//数据位
stopBits = '',//停止位
parity = '',//校验位
pictureUrl = '',//设备图片
slaveId = '',//从站地址
} = this.formData;
const {
@ -415,6 +416,7 @@ export default {
stopCommand,
startPower,
stopPower,
powerMultiplier,
clusterNum,
clusterPointAddress
} = this.pcsSetting
@ -423,7 +425,6 @@ export default {
deviceId,
deviceName,
description,
communicationStatus,
deviceType,
deviceCategory,
parentId,
@ -434,7 +435,6 @@ export default {
dataBits,
stopBits,
parity,
pictureUrl,
slaveId,
}
if (this.isPcs) {
@ -445,6 +445,7 @@ export default {
stopCommand,
startPower,
stopPower,
powerMultiplier,
clusterNum,
clusterPointAddress: JSON.stringify(!clusterNum ? [] : (clusterPointAddress || []).slice(0, clusterNum))
}
@ -497,7 +498,6 @@ export default {
deviceId: '',//设备id
deviceName: '',//设备名称
description: '',//设备描述
communicationStatus: '',//工作状态
deviceType: '',//设备类型
deviceCategory: '',//设备类别
parentId: '',//上级设备id
@ -508,9 +508,9 @@ export default {
dataBits: '',//数据位
stopBits: '',//停止位
parity: '',//校验位
pictureUrl: '',//设备图片
slaveId: '',//从站地址
}
this.parentDeviceList = []
this.pcsSetting = {
deviceSettingId: '',
powerAddress: '',//功率地址
@ -519,6 +519,7 @@ export default {
stopCommand: "",//关机指令
startPower: '',//开机目标功率
stopPower: '',//关机目标功率
powerMultiplier: '',//目标功率倍率
clusterNum: '',//电池簇数
clusterPointAddress: []//电池簇地址
}
@ -531,6 +532,18 @@ export default {
</script>
<style scoped lang="scss">
.form-layout {
position: relative;
}
.form-layout.has-pcs {
overflow: visible;
}
.base-form {
width: 100%;
}
.device-form {
::v-deep .el-form-item--medium .el-form-item__content {
width: 260px;
@ -541,4 +554,93 @@ export default {
margin-right: 0;
}
}
.pcs-form {
position: absolute;
top: 0;
right: -360px;
width: 340px;
max-height: 520px;
overflow-y: auto;
padding: 14px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background: #fff;
box-shadow: none;
z-index: 2;
}
.pcs-form__title {
margin-bottom: 12px;
padding-bottom: 10px;
font-size: 14px;
font-weight: 600;
color: #303133;
border-bottom: 1px solid #ebeef5;
}
.pcs-form__grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0 10px;
}
.pcs-form__item {
margin-bottom: 0;
}
.pcs-form__cluster {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #ebeef5;
}
.pcs-form__cluster-title {
margin-bottom: 8px;
font-size: 13px;
font-weight: 600;
color: #606266;
}
.pcs-form {
::v-deep .el-form-item {
width: 100%;
margin-right: 0;
margin-bottom: 18px;
}
::v-deep .el-form-item__label {
line-height: 20px;
padding-bottom: 4px;
font-size: 14px;
color: #606266;
}
::v-deep .el-form-item__content {
width: 100%;
line-height: normal;
}
::v-deep .el-input__inner {
height: 34px;
line-height: 34px;
}
::v-deep .el-form-item__error {
position: static;
line-height: 1.2;
padding-top: 4px;
}
::v-deep .el-form-item.is-required:not(.is-no-asterisk) > .el-form-item__label:before {
margin-right: 3px;
}
}
.ems-dialog {
::v-deep .el-dialog,
::v-deep .el-dialog__body {
overflow: visible;
}
}
</style>

View File

@ -40,18 +40,14 @@ export default {
required: true
}
},
data() {
return {
runningStatusMap: ['0', '3', '4', '6']
}
},
computed: {
label() {
return this.runningStatusMap.includes(this.data.workStatus) ? '关机' : '开机'
return this.data.workStatus === '0' ? '关机' : '开机'
}
},
methods: {
switchStatus() {
console.log(this.data, 11111111)
const {workStatus, deviceId, deviceName, siteId} = this.data
this.$confirm(`确认要${this.label}设备${deviceName || ''}吗?`, {
confirmButtonText: "确定",
@ -65,7 +61,7 @@ export default {
//做开关机操作,更新成功后刷新表格
updateDeviceStatus({
siteId,
workStatus: this.runningStatusMap.includes(workStatus) ? "1" : '0',//1 关机 0开机
workStatus: workStatus === '0' ? "1" : '0',
deviceId
})
.then((response) => {

View File

@ -110,6 +110,11 @@
sortable="custom"
>
</el-table-column>
<el-table-column v-if="selectable" label="操作" width="90" align="center">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="selectPoint(scope.row)">选择</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-show="tableData.length > 0"
@ -147,6 +152,7 @@ export default {
this.pageNum = 1;
this.totalSize = 0;
this.dataType = '';
this.selectable = false;
this.form = {
sortMethod: "desc", //升序不传或者asc、降序desc
sortData: this.defaultSort.prop,
@ -191,9 +197,14 @@ export default {
pageSize: 10, //分页栏当前每个数据总数
pageNum: 1, //分页栏当前页数
totalSize: 0, //table表格数据总数
selectable: false
};
},
methods: {
selectPoint(row) {
this.$emit('select-point', row || {})
this.show = false
},
showChart({pointName}) {
if (pointName) {
const {deviceCategory, deviceId} = this;
@ -235,12 +246,13 @@ export default {
this.getData()
});
},
showTable({deviceCategory, siteId, deviceId, parentId = ""}, dataType) {
showTable({deviceCategory, siteId, deviceId, parentId = ""}, dataType, options = {}) {
this.dataType = dataType;
this.deviceCategory = deviceCategory;
this.siteId = siteId;
this.deviceId = deviceId;
this.parentId = deviceCategory === "BATTERY" ? parentId : ""; //只有单体电池需要这个值
this.selectable = !!options.selectable
this.show = true;
this.getData()
},

View File

@ -1,13 +1,6 @@
<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="站点选择">
<el-select v-model="siteId" placeholder="请选择换电站名称" :loading="searchLoading" loading-text="正在加载数据"
@change="onSearch" clearable>
<el-option :label="item.siteName" :value="item.siteId" v-for="(item,index) in siteList"
:key="index+'zdxeSelect'"></el-option>
</el-select>
</el-form-item>
<el-form-item label="设备类型">
<el-select v-model="deviceCategory" placeholder="请选择设备类型" @change="onSearch" clearable>
<el-option
@ -24,34 +17,6 @@
</el-form-item>
</el-form>
<el-button type="primary" @click="addDevice" native-type="button">新增设备</el-button>
<el-dropdown @command="(val)=>downloadPointDetail(val,false)">
<el-button
style="margin-left:10px;"
type="primary"
plain>
下载点位清单
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="(item,index) in deviceCategoryList" :key="index+'deviceCategoryList'"
:command="item">
{{ item.name }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<!-- <el-dropdown @command="(val)=>uploadPointDetail(val,false)">-->
<!-- <el-button-->
<!-- style="margin-left:10px;"-->
<!-- type="success"-->
<!-- plain>-->
<!-- 上传点位清单-->
<!-- </el-button>-->
<!-- <el-dropdown-menu slot="dropdown">-->
<!-- <el-dropdown-item v-for="(item,index) in deviceCategoryList" :key="index+'deviceCategoryList'"-->
<!-- :command="item">-->
<!-- {{ item.name }}-->
<!-- </el-dropdown-item>-->
<!-- </el-dropdown-menu>-->
<!-- </el-dropdown>-->
<el-table
class="common-table"
:data="tableData"
@ -62,10 +27,6 @@
prop="siteId"
label="站点ID">
</el-table-column>
<el-table-column
prop="siteName"
label="站点名称">
</el-table-column>
<el-table-column
prop="deviceId"
label="设备ID"
@ -79,52 +40,11 @@
prop="categoryName"
label="设备类别">
</el-table-column>
<el-table-column
prop="deviceStatus"
label="在线状态">
<template slot-scope="scope">
<span>{{ $store.state.ems.deviceStatusOptions[scope.row.deviceStatus] }}</span>
<pcs-switch v-if="scope.row.deviceCategory === 'PCS' && ![null,'',undefined].includes(scope.row.workStatus)"
style="margin-left:5px;"
:data="{siteId:scope.row.siteId,workStatus:scope.row.workStatus,deviceId:scope.row.deviceId,deviceName:scope.row.deviceName}"
@updateSuccess="getData"/>
</template>
</el-table-column>
<el-table-column
fixed="right"
label="操作"
width="250">
width="180">
<template slot-scope="scope">
<el-button
@click="pointDetail(scope.row,'point')"
type="primary"
size="mini">
点位清单
</el-button>
<el-button
@click="pointDetail(scope.row,'alarmPoint')"
type="primary"
size="mini">
报警点位清单
</el-button>
<br>
<el-button
@click="downloadPointDetail(scope.row,true)"
style="margin-top:10px;"
type="primary"
plain
size="mini">
下载点位清单
</el-button>
<el-button
@click="uploadPointDetail(scope.row,true)"
style="margin-top:10px;"
type="success"
plain
size="mini">
上传点位清单
</el-button>
<br>
<el-button
@click="editDevice(scope.row)"
style="margin-top:10px;"
@ -167,32 +87,46 @@
</div>
</el-dialog>
<add-device ref="addDevice" :mode="mode" :id="editDeviceId" @update="getData" @clear="clearEditDeviceData"/>
<point-table ref="pointTable"/>
<point-upload ref="pointUpload" @update="getData"/>
</div>
</template>
<script>
import {deleteService, getDeviceDetailInfo, getDeviceInfoList} from '@/api/ems/site'
import {getAllSites} from '@/api/ems/zddt'
import {formatNumber} from "@/filters/ems";
import {getAllDeviceCategory} from '@/api/ems/search'
import PointTable from './PointTable.vue'
import AddDevice from "./AddDevice.vue";
import PointUpload from "./PointUpload.vue";
import PcsSwitch from "./PcsSwitch.vue";
export default {
name: "Sblb",
components: {AddDevice, PointTable, PointUpload, PcsSwitch},
components: {AddDevice},
watch: {
'$route.query.siteId'(newSiteId) {
const normalizedSiteId = this.hasValidSiteId(newSiteId) ? String(newSiteId).trim() : ''
if (normalizedSiteId === this.siteId) {
return
}
this.siteId = normalizedSiteId
this.onSearch()
},
'$route.query.siteName'(newSiteName) {
const normalizedSiteName = this.getSelectedSiteName(newSiteName)
if (normalizedSiteName === this.selectedSiteName) {
return
}
this.selectedSiteName = normalizedSiteName
this.tableData = (this.tableData || []).map(item => ({
...item,
siteName: normalizedSiteName
}))
}
},
data() {
return {
loading: false,
searchLoading: false,
mode: '',//新增、编辑设备
editDeviceId: '',//编辑设备id
siteId: '',
siteList: [],
selectedSiteName: '',
deviceCategory: '',//搜索栏设备类型
deviceCategoryList: [],//设备类别
tableData: [],
@ -224,38 +158,26 @@ export default {
}
},
methods: {
hasValidSiteId(siteId) {
return !!(siteId !== undefined && siteId !== null && String(siteId).trim())
},
getSelectedSiteName(routeSiteName) {
const name = routeSiteName === undefined || routeSiteName === null ? '' : String(routeSiteName).trim()
if (name) {
return name
}
const matchedSite = (this.$store.getters.zdList || []).find(item => item.siteId === this.siteId)
if (matchedSite && matchedSite.siteName) {
return matchedSite.siteName
}
return this.siteId || ''
},
// 获取设备类别
getDeviceCategoryList() {
getAllDeviceCategory().then(response => {
this.deviceCategoryList = response?.data || []
})
},
// 查看设备电位表格
pointDetail(row, dataType) {
this.$refs.pointTable.showTable(row, dataType)
},
// 下载点位清单
downloadPointDetail(command, isDetail = false) {
const siteId = isDetail ? command.siteId : this.siteId
const deviceCategory = isDetail ? command.deviceCategory : command.code
const categoryName = isDetail ? command.categoryName : command.name
const deviceId = isDetail ? command.deviceId : null
console.log('下载', command, isDetail)
this.download('ems/pointMatch/export', {
siteId,
deviceCategory,
deviceId,
}, `点位清单_${categoryName}_${new Date().getTime()}.xlsx`)
},
// 上传点位清单
uploadPointDetail(command, isDetail = false) {
const siteId = isDetail ? command.siteId : this.siteId
const deviceCategory = isDetail ? command.deviceCategory : command.code
const categoryName = isDetail ? command.categoryName : command.name
const deviceId = isDetail ? command.deviceId : ''
console.log('上传', command, isDetail)
this.$refs.pointUpload.showDialog({siteId, deviceCategory, categoryName, deviceId})
},
clearEditDeviceData() {
this.mode = '';
this.editDeviceId = ''
@ -356,31 +278,24 @@ export default {
this.loading = true
const {siteId, deviceCategory, pageNum, pageSize} = this
getDeviceInfoList({siteId, deviceCategory, pageNum, pageSize}).then(response => {
this.tableData = response?.rows || [];
const selectedSiteName = this.getSelectedSiteName(this.$route.query.siteName)
this.selectedSiteName = selectedSiteName
this.tableData = (response?.rows || []).map(item => ({
...item,
siteName: selectedSiteName
}));
this.totalSize = response?.total || 0
}).finally(() => {
this.loading = false
})
},
//获取站点列表
getZdList() {
this.searchLoading = true
return getAllSites().then(response => {
this.siteList = response?.data || []
if (this.siteList.length > 0) this.siteId = this.siteList[0].siteId
}).finally(() => {
this.searchLoading = false
})
}
},
mounted() {
this.loading = true
this.siteId = ''
this.siteId = this.hasValidSiteId(this.$route.query.siteId) ? String(this.$route.query.siteId).trim() : ''
this.selectedSiteName = this.getSelectedSiteName(this.$route.query.siteName)
this.pageNum = 1//每次搜索从1开始搜索
this.getDeviceCategoryList()
this.getZdList().then(() => {
this.getData()
})
this.getData()
}
}
</script>

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
<template>
<div class="ems-dashboard-editor-container" style="background-color: #ffffff" v-loading="loading">
<el-form :inline="true" class="select-container">
@ -24,6 +23,7 @@
<el-form-item>
<el-button type="primary" @click="onSearch" native-type="button">搜索</el-button>
<el-button @click="onReset" native-type="button">重置</el-button>
<el-button type="success" @click="openAddDialog" native-type="button">新增站点</el-button>
</el-form-item>
</el-form>
<el-table
@ -40,6 +40,10 @@
prop="siteName"
label="站点名称">
</el-table-column>
<el-table-column
prop="siteShortName"
label="站点简称">
</el-table-column>
<el-table-column
prop="siteAddress"
label="站点地址"
@ -57,6 +61,21 @@
prop="installCapacity"
label="装机容量">
</el-table-column>
<el-table-column
label="操作"
width="220"
fixed="right">
<template slot-scope="scope">
<el-button type="text" size="small" @click="openEditDialog(scope.row)">编辑</el-button>
<el-button
type="text"
size="small"
:loading="syncingSiteId === scope.row.siteId"
:disabled="syncingSiteId === scope.row.siteId"
@click="syncWeather(scope.row)"
>同步天气</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-show="tableData.length>0"
@ -71,67 +90,250 @@
style="margin-top:15px;text-align: center"
>
</el-pagination>
<el-dialog
:title="isEdit ? '编辑站点' : '新增站点'"
:visible.sync="dialogVisible"
width="640px"
:close-on-click-modal="false">
<el-form ref="siteForm" :model="siteForm" :rules="siteRules" label-width="100px">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="站点ID" prop="siteId">
<el-input v-model.trim="siteForm.siteId" :disabled="isEdit" placeholder="仅支持字母/数字/下划线" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="站点名称" prop="siteName">
<el-input v-model.trim="siteForm.siteName" placeholder="请输入站点名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="站点简称" prop="siteShortName">
<el-input v-model.trim="siteForm.siteShortName" placeholder="请输入站点简称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="运营时间" prop="runningTime">
<el-date-picker
v-model="siteForm.runningTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择运营时间"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="站点地址" prop="siteAddress">
<el-input v-model.trim="siteForm.siteAddress" placeholder="请输入站点地址" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="装机功率" prop="installPower">
<el-input v-model="siteForm.installPower" placeholder="请输入装机功率" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="装机容量" prop="installCapacity">
<el-input v-model="siteForm.installCapacity" placeholder="请输入装机容量" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="纬度" prop="latitude">
<el-input v-model="siteForm.latitude" placeholder="请输入纬度" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="经度" prop="longitude">
<el-input v-model="siteForm.longitude" placeholder="请输入经度" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="remark">
<el-input v-model.trim="siteForm.remark" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" :loading="submitLoading" @click="submitSite"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import {getSiteInfoList} from '@/api/ems/site'
import {addSite, getSiteInfoList, syncSiteWeatherByDateRange, updateSite} from '@/api/ems/site'
import { formatDate } from '@/filters/ems'
const emptySiteForm = () => ({
id: undefined,
siteId: '',
siteName: '',
siteShortName: '',
siteAddress: '',
runningTime: '',
installPower: '',
installCapacity: '',
latitude: '',
longitude: '',
remark: ''
})
export default {
name: "Zdlb",
name: 'Zdlb',
data() {
return {
loading:false,
siteName:"",
pickerOptions:{
loading: false,
submitLoading: false,
siteName: '',
pickerOptions: {
disabledDate(time) {
return time.getTime() > Date.now();
},
},
defaultDateRange:[],//默认展示的时间
dateRange:[],//startTime,endTime
tableData:[],
pageSize:10,//分页栏当前每个数据总数
pageNum:1,//分页栏当前页数
totalSize:0,//table表格数据总数
defaultDateRange: [],
dateRange: [],
tableData: [],
pageSize: 10,
pageNum: 1,
totalSize: 0,
syncingSiteId: '',
dialogVisible: false,
isEdit: false,
siteForm: emptySiteForm(),
siteRules: {
siteId: [
{ required: true, message: '请输入站点ID', trigger: 'blur' },
{ pattern: /^[A-Za-z0-9_]+$/, message: '站点ID仅支持字母、数字、下划线', trigger: 'blur' }
],
siteName: [
{ required: true, message: '请输入站点名称', trigger: 'blur' }
],
runningTime: [
{ required: true, message: '请选择运营时间', trigger: 'change' }
]
}
}
},
methods:{
// 分页
methods: {
handleSizeChange(val) {
this.pageSize = val;
this.$nextTick(()=>{
this.$nextTick(() => {
this.getData()
})
},
handleCurrentChange(val) {
this.pageNum = val
this.$nextTick(()=>{
this.$nextTick(() => {
this.getData()
})
},
// 搜索
onSearch(){
this.pageNum =1//每次搜索从1开始搜索
onSearch() {
this.pageNum = 1
this.getData()
},
// 重置
onReset(){
this.siteName=''
this.dateRange=[]
this.pageNum =1//每次搜索从1开始搜索
onReset() {
this.siteName = ''
this.dateRange = []
this.pageNum = 1
this.getData()
},
// 获取数据
getData(){
this.loading=true
const {siteName,pageNum,pageSize} =this
const [startTime='',endTime='']=(this.dateRange || [])
getSiteInfoList({siteName,pageSize,pageNum,startTime:formatDate(startTime),endTime:formatDate(endTime)}).then(response => {
this.tableData=response?.rows || [];
getSyncDateRange() {
const now = new Date()
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1)
return [formatDate(monthStart), formatDate(now)]
},
syncWeather(row) {
const siteId = row?.siteId
if (!siteId) {
this.$message.warning('站点ID为空无法同步天气')
return
}
const [startTime, endTime] = this.getSyncDateRange()
this.$confirm(`将同步站点 ${siteId}${startTime}${endTime} 的天气数据,是否继续?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.syncingSiteId = siteId
return syncSiteWeatherByDateRange({siteId, startTime, endTime})
}).then((response) => {
const result = response?.data || {}
const successDays = result.successDays ?? 0
const totalDays = result.totalDays ?? 0
this.$message.success(`天气同步完成(${successDays}/${totalDays}天)`)
}).catch((err) => {
if (err !== 'cancel') {
this.$message.error('天气同步失败')
}
}).finally(() => {
this.syncingSiteId = ''
})
},
getData() {
this.loading = true
const {siteName, pageNum, pageSize} = this
const [startTime = '', endTime = ''] = (this.dateRange || [])
getSiteInfoList({siteName, pageSize, pageNum, startTime: formatDate(startTime), endTime: formatDate(endTime)}).then(response => {
this.tableData = response?.rows || [];
this.totalSize = response?.total || 0
}).finally(() => {this.loading=false})
}).finally(() => {
this.loading = false
})
},
openAddDialog() {
this.isEdit = false
this.siteForm = emptySiteForm()
this.dialogVisible = true
this.$nextTick(() => {
this.$refs.siteForm && this.$refs.siteForm.clearValidate()
})
},
openEditDialog(row) {
this.isEdit = true
this.siteForm = {
id: row.id,
siteId: row.siteId || '',
siteName: row.siteName || '',
siteShortName: row.siteShortName || '',
siteAddress: row.siteAddress || '',
runningTime: row.runningTime || '',
installPower: row.installPower || '',
installCapacity: row.installCapacity || '',
latitude: row.latitude || '',
longitude: row.longitude || '',
remark: row.remark || ''
}
this.dialogVisible = true
this.$nextTick(() => {
this.$refs.siteForm && this.$refs.siteForm.clearValidate()
})
},
submitSite() {
this.$refs.siteForm.validate(valid => {
if (!valid) {
return
}
this.submitLoading = true
const request = this.isEdit ? updateSite : addSite
request(this.siteForm).then(() => {
this.$message.success(this.isEdit ? '编辑成功' : '新增成功')
this.dialogVisible = false
this.getData()
}).finally(() => {
this.submitLoading = false
})
})
}
},
mounted() {
this.onReset()

View File

@ -1,95 +1,188 @@
<template>
<div id="zddtChart" style="height: 100%;width:100%"></div>
<div class="map-wrapper">
<div ref="mapRef" class="map-canvas"></div>
<div v-if="selectedAddress" class="map-center-address">{{ selectedAddress }}</div>
<div v-if="!hasPoint" class="map-empty">暂无站点坐标</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
import resize from '@/mixins/ems/resize'
import china from '@/data/ems/china.json'//中国地图数据
import 'echarts/lib/chart/map';
echarts.registerMap('china', { geoJSON: china }); //注册可用地图
const TDT_SCRIPT_ID = 'tianditu-js-sdk'
let tdtScriptLoading = null
function loadTdtScript(tk) {
if (window.T) return Promise.resolve(window.T)
if (tdtScriptLoading) return tdtScriptLoading
tdtScriptLoading = new Promise((resolve, reject) => {
const oldScript = document.getElementById(TDT_SCRIPT_ID)
if (oldScript) {
oldScript.addEventListener('load', () => resolve(window.T))
oldScript.addEventListener('error', () => reject(new Error('天地图脚本加载失败')))
return
}
const script = document.createElement('script')
script.id = TDT_SCRIPT_ID
script.src = `https://api.tianditu.gov.cn/api?v=4.0&tk=${tk}`
script.async = true
script.onload = () => {
if (window.T) resolve(window.T)
else reject(new Error('天地图对象未初始化'))
}
script.onerror = () => reject(new Error('天地图脚本加载失败'))
document.body.appendChild(script)
})
return tdtScriptLoading
}
export default {
mixins: [resize],
data() {
return {
chart: null,
hasPoint: false,
selectedAddress: '',
map: null,
overlays: [],
pendingPayload: null,
mapConfig: {
zoom: 12,
selectedZoom: 15,
tk: '01e99ab4472430e1c7dbfe4b5db99787'
}
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
})
this.initMap()
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
this.clearOverlays()
this.map = null
},
methods: {
initChart() {
// ECharts 默认有提供了一个简单的加载动画。只需要调用 showLoading 方法显示。数据加载完成后再调用 hideLoading 方法隐藏加载动画。
this.chart = echarts.init(document.querySelector('#zddtChart'))
async initMap() {
try {
await loadTdtScript(this.mapConfig.tk)
if (!this.$refs.mapRef || !window.T) return
this.map = new window.T.Map(this.$refs.mapRef)
const defaultCenter = new window.T.LngLat(104.1, 35.9)
this.map.centerAndZoom(defaultCenter, 5)
if (this.pendingPayload) {
this.renderPayload(this.pendingPayload)
}
} catch (e) {
// 页面可继续使用,地图只显示空态
this.hasPoint = false
}
},
setOption(data) {
this.chart.setOption({
color:['#FFBD00'],
backgroundColor: 'transparent', //背景色
geo: { //地理坐标系组件 地理坐标系组件用于地图的绘制,支持在地理坐标系上绘制
map: 'china', //地图类型 这儿展示的是中国地图
aspectScale: 0.85,
selectedMode: "single",// 开启单选
label: {
show: true, //是否显示标签 此处指是否显示地图上的地区名字
color: '#ffffff',
fontSize: 12
},
roam: true, //是否开启鼠标缩放和平移漫游
itemStyle: {
areaColor: "#03365b",
borderColor: "#4bf3f9",
shadowColor: '#03365b', //阴影颜色
shadowOffsetX: 0, //阴影偏移量
shadowOffsetY: 0, //阴影偏移量
},
emphasis: {
label: {
show: true,
color: '#ffffff',
},
itemStyle: {
areaColor: "#0f5d9d",
}
}
},
series: [
{
type: "effectScatter",
coordinateSystem: "geo",
showEffectOn: "render",
data,
rippleEffect: {
brushType: "stroke",
scale: 5,
period: 2, // 秒数
},
symbolSize: 12,
clickable: false,
zlevel: 1,
label: {
formatter: "{b}",
position: "right",
show: true,
},
}
]
normalizePoint(site = {}) {
const name = site.siteName || site.name || ''
const address = site.siteAddress || site.address || ''
const value = site.value || site.siteLocation || []
const lonSource = site.longitude !== undefined && site.longitude !== null ? site.longitude : value[0]
const latSource = site.latitude !== undefined && site.latitude !== null ? site.latitude : value[1]
const lon = Number(lonSource)
const lat = Number(latSource)
if (!lon || !lat) return null
return { name, address, lon, lat }
},
clearOverlays() {
if (!this.map || !this.overlays.length) return
this.overlays.forEach(item => this.map.removeOverLay(item))
this.overlays = []
},
renderPayload(payload = {}) {
const isArrayPayload = Array.isArray(payload)
const selectedRaw = isArrayPayload ? ((payload || [])[0] || {}) : (payload.selected || {})
const sitesRaw = isArrayPayload ? [] : (payload.sites || [])
const selected = this.normalizePoint(selectedRaw)
const points = (Array.isArray(sitesRaw) ? sitesRaw : [])
.map(item => this.normalizePoint(item))
.filter(Boolean)
this.selectedAddress = selected?.address || ''
if (selected && !points.find(item => item.lon === selected.lon && item.lat === selected.lat)) {
points.push(selected)
}
this.clearOverlays()
this.hasPoint = points.length > 0
if (!this.map || !points.length || !window.T) return
const viewPoints = []
points.forEach(item => {
const lngLat = new window.T.LngLat(item.lon, item.lat)
const marker = new window.T.Marker(lngLat)
this.map.addOverLay(marker)
this.overlays.push(marker)
viewPoints.push(lngLat)
})
if (selected && selected.name) {
const label = new window.T.Label({
text: selected.name,
position: new window.T.LngLat(selected.lon, selected.lat),
offset: new window.T.Point(8, -34)
})
this.map.addOverLay(label)
this.overlays.push(label)
}
if (selected) {
this.map.centerAndZoom(new window.T.LngLat(selected.lon, selected.lat), this.mapConfig.selectedZoom)
} else if (viewPoints.length === 1) {
this.map.centerAndZoom(viewPoints[0], this.mapConfig.zoom)
} else {
this.map.setViewport(viewPoints)
}
},
setOption(payload = {}) {
this.pendingPayload = payload
if (!this.map) return
this.renderPayload(payload)
}
}
}
</script>
<style scoped lang="scss">
.map-wrapper {
position: relative;
width: 100%;
height: 100%;
min-height: 0;
border-radius: 8px;
overflow: hidden;
background: #f5f7fa;
}
.map-canvas {
width: 100%;
height: 100%;
}
.map-empty {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-size: 14px;
background: rgba(245, 247, 250, 0.9);
pointer-events: none;
}
.map-center-address {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, calc(-50% - 26px));
max-width: min(70%, 520px);
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
line-height: 18px;
color: #ffffff;
background: rgba(0, 0, 0, 0.65);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
text-align: center;
pointer-events: none;
z-index: 10;
}
</style>

View File

@ -1,33 +1,55 @@
<template>
<div class="ems-dashboard-editor-container" v-loading="loading">
<zd-info></zd-info>
<div class="ems-content-container ">
<div class="ems-content-container">
<div class="map-container">
<map-chart ref="mapChart"/>
</div>
<div class="zd-msg-container">
<div class="zd-msg-top">
<zd-select ref="zdSelect" @submitSite="submitSite"></zd-select>
<el-card class="common-card-container">
<div slot="header">
<span class="card-title">基本信息</span>
<el-button style="float: right; padding: 3px 0" type="text" size="small" @click="toDzjk">查看详情</el-button>
<div class="site-cards-wrapper" v-if="allSites.length > 0">
<button
class="site-cards-arrow site-cards-arrow--left"
type="button"
:disabled="!canScrollLeft"
@click="scrollSiteCards('left')"
>
<i class="el-icon-arrow-left"></i>
</button>
<div ref="siteCards" class="site-cards" @scroll="updateScrollButtons">
<div
v-for="item in allSites"
:key="item.siteId"
class="site-card"
:class="{ active: isSameSite(item.siteId, singleSiteId) }"
@click="submitSite(item.siteId)"
>
<div class="site-card-name">{{ item.siteName || '-' }}</div>
<div class="site-card-info-row">
<span class="site-card-label">电站位置</span>
<span class="site-card-value site-card-value--address">{{ formatSiteCardField(item.siteAddress) }}</span>
</div>
<div class="site-card-info-row">
<span class="site-card-label">投运时间</span>
<span class="site-card-value">{{ formatSiteCardDate(item.runningTime) }}</span>
</div>
<div class="site-card-info-row">
<span class="site-card-label">装机功率(MWh)</span>
<span class="site-card-value">{{ formatSiteCardField(item.installPower) }}</span>
</div>
<div class="site-card-info-row">
<span class="site-card-label">装机容量(MWh)</span>
<span class="site-card-value">{{ formatSiteCardField(item.installCapacity) }}</span>
</div>
</div>
<div class="single-zd-name">{{singleSiteName}}</div>
<!-- 四个方块-->
<el-row :gutter="14">
<el-col :span="12" class="single-square-box-container" v-for="(item,index) in singleZdSqaure" :key="index+'singleSquareBox'">
<single-square-box :data="item"></single-square-box>
</el-col>
</el-row>
<!-- 基本信息 -->
<el-descriptions class="single-zd-info-container" :column="1" >
<el-descriptions-item v-for="(item,index) in singleZdInfo" :key="index+'singleZdInfo'" :label="item.title">{{item.value | formatNumber }}</el-descriptions-item>
</el-descriptions>
<!-- echarts柱状图-->
<bar-chart ref="barChart"></bar-chart>
</el-card>
</div>
<button
class="site-cards-arrow site-cards-arrow--right"
type="button"
:disabled="!canScrollRight"
@click="scrollSiteCards('right')"
>
<i class="el-icon-arrow-right"></i>
</button>
</div>
<div class="map-view">
<map-chart ref="mapChart"/>
</div>
</div>
</div>
@ -36,94 +58,103 @@
<script>
import ZdInfo from '@/components/Ems/ZdBaseInfo/index.vue'
import ZdSelect from '@/components/Ems/ZdSelect/index.vue'
import SingleSquareBox from '@/components/Ems/SingleSquareBox/index.vue'
import BarChart from './BarChart.vue'
import MapChart from './MapChart.vue'
import {getSingleSiteBaseInfo} from '@/api/ems/zddt'
import { getAllSites } from '@/api/ems/zddt'
export default {
components:{ZdSelect,ZdInfo,SingleSquareBox,BarChart,MapChart},
components: { ZdInfo, MapChart },
data() {
return {
loading:false,
singleSiteId:'',
singleSiteName:'',
singleSiteLocation:[],
// 单个电站 四个方块数据
singleZdSqaure:[
{
title:'今日充电kWh',
value:'',
bgColor:'#FFE5E5',
attr:'dayChargedCap'
},{
title:'累计充电kWh',
value:'',
bgColor:'#FFE5E5',
attr:'totalChargedCap'
},{
title:'今日放电kWh',
value:'',
bgColor:'#EEEBFF',
attr:'dayDisChargedCap'
},{
title:'累计放电kWh',
value:'',
bgColor:'#EEEBFF',
attr:'totalDisChargedCap'
}
],
// 单个电站 基本信息
singleZdInfo:[{
title:'电站位置',
value:'',
attr:'siteAddress'
},{
title:'投运时间',
value:'',
attr:'runningTime'
},{
title:'装机功率(MW)',
value:'',
attr:'installPower'
},{
title:'装机容量(MW)',
value:'',
attr:'installCapacity',
}]
loading: false,
singleSiteId: '',
singleSiteName: '',
singleSiteAddress: '',
singleSiteLocation: [],
allSites: [],
canScrollLeft: false,
canScrollRight: false
}
},
mounted() {
this.loadSites()
window.addEventListener('resize', this.updateScrollButtons)
},
beforeDestroy() {
window.removeEventListener('resize', this.updateScrollButtons)
},
methods:{
isSameSite(siteId, selectedId) {
return String(siteId) === String(selectedId)
},
formatSiteCardField(value) {
if (value === null || value === undefined || value === '') {
return '-'
}
return value
},
formatSiteCardDate(value) {
if (!value) {
return '-'
}
const text = String(value)
if (text.includes('T')) {
return text.slice(0, 10)
}
return text.length > 10 ? text.slice(0, 10) : text
},
loadSites() {
this.loading = true
getAllSites().then(response => {
this.allSites = response?.data || []
if (this.allSites.length > 0) {
this.submitSite(this.allSites[0].siteId)
} else {
this.updateMapMarkers()
}
this.$nextTick(() => {
this.updateScrollButtons()
})
}).finally(() => {
this.loading = false
})
},
updateScrollButtons() {
const container = this.$refs.siteCards
if (!container) {
this.canScrollLeft = false
this.canScrollRight = false
return
}
const maxScrollLeft = container.scrollWidth - container.clientWidth
this.canScrollLeft = container.scrollLeft > 0
this.canScrollRight = maxScrollLeft > 0 && container.scrollLeft < maxScrollLeft - 1
},
scrollSiteCards(direction) {
const container = this.$refs.siteCards
if (!container) {
return
}
const amount = Math.max(container.clientWidth * 0.8, 240)
const delta = direction === 'left' ? -amount : amount
container.scrollBy({ left: delta, behavior: 'smooth' })
},
updateMapMarkers(){
this.$refs.mapChart && this.$refs.mapChart.setOption({
selected: {name:this.singleSiteName,address:this.singleSiteAddress,value:this.singleSiteLocation},
sites:this.allSites
})
},
// 站点选中
submitSite(id){
if(this.singleSiteId === id){return console.log(`点击搜索按钮 搜索相同的站点id= ${id}不再调用获取基本信息接口`)}
this.loading=true
console.log('点击搜索按钮 选中的站点id',id)
this.singleSiteId = id
this.$refs.zdSelect.searchLoading = true
getSingleSiteBaseInfo(id).then(response => {
console.log('单个站点详情数据',response)
const res = response?.data || {}
this.singleSiteName = res?.siteName || ''//站点名称
this.singleSiteLocation = res?.siteLocation || []//站点坐标
this.singleZdSqaure.forEach(item=>{
item.value =res[item.attr]
})
this.singleZdInfo.forEach(item=>{
item.value = res[item.attr]
})
this.$refs.barChart.setOption(res?.sevenDayDisChargeStats || [])
this.$refs.mapChart.setOption([{name:this.singleSiteName,value:this.singleSiteLocation}])
}).finally(() => {this.$refs.zdSelect.searchLoading = false;this.loading=false})
},
//跳转单站监控页面
toDzjk(){
this.$router.push({
path:'/dzjk',
query:{
siteId:this.singleSiteId,
}
const currentSite = this.allSites.find(item => this.isSameSite(item.siteId, id)) || {}
this.singleSiteName = currentSite.siteName || ''
this.singleSiteAddress = currentSite.siteAddress || ''
this.singleSiteLocation = currentSite.siteLocation || []
if (!this.singleSiteLocation.length) {
this.singleSiteLocation = [currentSite.longitude, currentSite.latitude].filter(item => item !== undefined && item !== null)
}
this.$nextTick(() => {
this.updateMapMarkers()
})
}
},
@ -134,29 +165,119 @@ export default {
.ems-content-container{
display: flex;
padding:24px;
padding-right: 0;
.map-container{
flex:auto;
}
.zd-msg-container{
width: 500px;
padding: 24px;
.single-zd-name{
font-weight: 500;
line-height: 23px;
color: #FFBD00;
font-size: 16px;
margin-bottom: 24px;
min-width: 0;
display: flex;
flex-direction: column;
gap: 12px;
.site-cards-wrapper{
display: flex;
align-items: center;
gap: 8px;
.site-cards-arrow{
width: 28px;
height: 28px;
border: none;
border-radius: 50%;
background: #ffffff;
box-shadow: 0 0 0 1px #dcdfe6 inset;
color: #606266;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all .2s ease;
&:hover:not(:disabled){
color: #2b74ff;
box-shadow: 0 0 0 1px #2b74ff inset;
}
&:disabled{
cursor: not-allowed;
color: #c0c4cc;
box-shadow: 0 0 0 1px #ebeef5 inset;
}
}
}
.single-square-box-container{
height: 78px;
box-sizing: border-box;
margin-bottom: 10px;
.site-cards{
display: flex;
gap: 10px;
overflow-x: auto;
padding-bottom: 2px;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar{
display: none;
}
.site-card{
flex: 0 0 360px;
height: 166px;
border-radius: 8px;
background: #ffffff;
border: 1px solid #e4e7ed;
padding: 10px;
box-sizing: border-box;
cursor: pointer;
transition: all .2s ease;
.site-card-name{
font-size: 14px;
font-weight: 600;
color: #303133;
line-height: 22px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.site-card-info-row{
display: flex;
align-items: flex-start;
gap: 8px;
margin-top: 6px;
font-size: 12px;
line-height: 18px;
}
.site-card-label{
flex: 0 0 84px;
font-size: 12px;
color: #909399;
white-space: nowrap;
}
.site-card-value{
flex: 1;
color: #606266;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.site-card-value--address{
white-space: normal;
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-overflow: unset;
min-height: 36px;
}
&:hover{
border-color: #c0d3ff;
}
&.active{
border-color: #2b74ff;
box-shadow: 0 0 0 1px rgba(43,116,255,.15) inset;
background: #f5f9ff;
.site-card-name{
color: #2b74ff;
}
.site-card-value{
color: #5f8ee3;
}
}
}
}
.single-zd-info-container{
font-size: 12px;
margin-top: 10px;
color:#666666;
.map-view{
height: 70vh;
min-height: 520px;
}
}
}

View File

@ -1,6 +1,8 @@
<template>
<div class="login">
<img :src="loginBg" alt="" srcset="" class="login-bg" />
<transition :name="bgTransitionName">
<img :key="bgNum" :src="loginBg" alt="" srcset="" class="login-bg" />
</transition>
<el-form
ref="loginForm"
:model="loginForm"
@ -107,6 +109,8 @@ export default {
data() {
return {
bgNum: 1,
bgTransitionName: "bg-slide",
bgTransitionNames: ["bg-slide", "bg-zoom", "bg-blur"],
codeUrl: "",
loginForm: {
username: "admin",
@ -148,7 +152,12 @@ export default {
this.updateInterval(this.updateBgNum, 5000);
},
methods: {
randomBgTransitionName() {
const index = Math.floor(Math.random() * this.bgTransitionNames.length);
return this.bgTransitionNames[index];
},
updateBgNum() {
this.bgTransitionName = this.randomBgTransitionName();
if (this.bgNum >= 4) this.bgNum = 0;
this.bgNum += 1;
},
@ -210,6 +219,10 @@ export default {
<style rel="stylesheet/scss" lang="scss">
.login {
position: relative;
overflow: hidden;
width: 100%;
min-height: 100vh;
padding-left: 180px;
display: flex;
justify-content: left;
@ -224,6 +237,60 @@ export default {
display: block;
height: 100%;
width: 100%;
object-fit: cover;
object-position: center;
backface-visibility: hidden;
will-change: opacity, transform, filter;
}
.bg-fade-enter-active,
.bg-fade-leave-active {
transition: opacity 0.9s ease, transform 0.9s ease;
}
.bg-fade-enter {
opacity: 0;
transform: scale(1.04);
}
.bg-fade-leave-to {
opacity: 0;
transform: scale(0.98);
}
.bg-slide-enter-active,
.bg-slide-leave-active {
transition: opacity 0.85s ease, transform 0.85s ease;
}
.bg-slide-enter {
opacity: 0;
transform: translate3d(3.5%, 0, 0) scale(1.01);
}
.bg-slide-leave-to {
opacity: 0;
transform: translate3d(-3.5%, 0, 0) scale(0.99);
}
.bg-zoom-enter-active,
.bg-zoom-leave-active {
transition: opacity 1s ease, transform 1s ease;
}
.bg-zoom-enter {
opacity: 0;
transform: scale(1.08);
}
.bg-zoom-leave-to {
opacity: 0;
transform: scale(0.94);
}
.bg-blur-enter-active,
.bg-blur-leave-active {
transition: opacity 0.9s ease, filter 0.9s ease, transform 0.9s ease;
}
.bg-blur-enter {
opacity: 0;
filter: blur(8px);
transform: scale(1.02);
}
.bg-blur-leave-to {
opacity: 0;
filter: blur(6px);
transform: scale(0.98);
}
.login-logo {
display: block;

View File

@ -47,7 +47,7 @@ module.exports = {
// }
// 当请求前缀是/dev-api时,使用下面的代理
'/dev-api': {
target: 'http://110.40.171.179:8089',
target: 'http://localhost:8089',
changeOrigin: true,
pathRewrite: {
'^/dev-api': ''