Files
emsapp/pages/work/index.vue
2026-04-01 14:28:09 +08:00

603 lines
17 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="work-container">
<site-switch-header :site-id="siteId" :site-type-options="siteTypeOptions" :site-address="baseInfo.siteAddress"
:running-time="baseInfo.runningTime" @change="selectedSite" />
<!-- 静态信息 -->
<view class="base-info">
<uni-group mode="card" class="install-data">
<uni-grid :column="2" :showBorder="false" :square="false" :highlight="false">
<uni-grid-item>
<view class="grid-item-box">
<view class="title">装机功率(MWh)</view>
<view class="text">{{baseInfo.installPower | formatNumber}}</view>
</view>
</uni-grid-item>
<uni-grid-item>
<view class="grid-item-box">
<view class="title">装机容量(MWh)</view>
<view class="text">{{baseInfo.installCapacity | formatNumber}}</view>
</view>
</uni-grid-item>
</uni-grid>
</uni-group>
<!-- 工作台 -->
<uni-section title="工作台" type="line" class="sections-list">
<view class="grid-body">
<uni-grid :column="4" :showBorder="false" @change="toDetail">
<uni-grid-item v-for="(item,index) in siteGirdList" :index="index" :key="index+'work'">
<view class="grid-item-box work-box">
<view class="icon iconfont" :class="item.icon" size="30"></view>
<text class="text">{{item.text}}</text>
</view>
</uni-grid-item>
</uni-grid>
</view>
</uni-section>
<!-- 一周充放曲线 uchart的组件最好放在同级-->
<uni-section title="一周充放曲线" type="line" class="sections-list">
<date-range-select ref="weekChartDateRangeSelect" @updateDate="updateWeekChartDate" />
<view style="width:100%;height: 250px;">
<qiun-data-charts type="area" :chartData="weekChartData" :optsWatch='false' :inScrollView="true"
:pageScrollTop="pageScrollTop" :opts="options" :ontouch="true" />
</view>
</uni-section>
<!-- 当日功率曲线 uchart的组件最好放在同级-->
<uni-section title="当日功率曲线" type="line" class="sections-list">
<date-range-select ref="activeChartDateRangeSelect" @updateDate="updateActiveChartDate" />
<view style="width:100%;height: 250px;">
<qiun-data-charts type="area" :chartData="activeChartData" :optsWatch='false' :inScrollView="true"
:pageScrollTop="pageScrollTop" :opts="glqxOptions" :ontouch="true" />
</view>
</uni-section>
</view>
</view>
</template>
<script>
import {
mapGetters
} from 'vuex'
import DateRangeSelect from './DateRangeSelect.vue'
import SiteSwitchHeader from '@/components/SiteSwitchHeader/index.vue'
import {
getAllSites,
getSingleSiteBaseInfo,
getProjectDisplayData,
getPointConfigCurve,
getSiteAllDeviceCategory
} from '@/api/ems/site.js'
const createSiteTypeOptions = () => ([{
text: '储能',
value: 'cn',
children: []
},
{
text: '光能',
value: 'gn',
children: []
},
{
text: '岸电',
value: 'ad',
children: []
}
])
export default {
components: {
DateRangeSelect,
SiteSwitchHeader
},
data() {
return {
// 图表数据
weekChartTimeRange: [],
activeChartTimeRange: [],
weekChartData: {},
activeChartData: {},
curveDisplayData: [],
curveDisplayLoadingPromise: null,
pageScrollTop: 0,
glqxOptions: {
padding: [10, 5, 0, 10],
dataLabel: false,
enableScroll: true,
xAxis: {
scrollShow: true,
itemCount: 3,
disableGrid: true
},
extra: {
area: {
type: "curve",
opacity: 0.2,
addLine: true,
width: 2,
gradient: true,
activeType: "hollow"
}
}
// update: true,
// duration: 2,
// animation: false,
// enableScroll: true,
// padding: [10, 15, 10, 15]
},
options: {
padding: [10, 5, 0, 10],
dataLabel: false,
enableScroll: true,
xAxis: {
scrollShow: true,
itemCount: 5,
disableGrid: true
},
extra: {
area: {
type: "curve",
opacity: 0.2,
addLine: true,
width: 2,
gradient: true,
activeType: "hollow"
}
}
// update: true,
// duration: 2,
// animation: false,
// enableScroll: true,
// padding: [10, 15, 10, 15]
},
// 图表数据结束
deviceCategoryOptions: [], //当前站点包含的设备类别
siteTypeOptions: createSiteTypeOptions(),
siteId: '', //选择的站点ID
baseInfo: {}, //站点基本信息
gridList: [{
page: 'bmszl',
icon: 'icon-BMS',
text: 'BMS总览',
categoryName: 'STACK'
},
{
page: 'bmsdcc',
icon: 'icon-a-dianchicunengliangkuai',
text: 'BMS电池簇',
categoryName: 'CLUSTER'
},
{
page: 'pcs',
icon: 'icon-PCS',
text: 'PCS',
categoryName: 'PCS'
},
{
page: 'db',
icon: 'icon-dianbiao4',
text: '电表',
categoryName: 'AMMETER'
},
{
page: 'yl',
icon: 'icon-gongneng-diandongji',
text: '冷却',
categoryName: 'COOLING'
},
{
page: 'dtdc',
icon: 'icon-dantidianchi',
text: '单体电池',
categoryName: 'BATTERY'
}
]
}
},
computed: {
...mapGetters(['belongSite', 'currentSiteId']),
siteGirdList() {
return this.gridList.filter(i => this.deviceCategoryOptions.includes(i.categoryName))
}
},
methods: {
isAvailableSite(siteId) {
const allSites = this.siteTypeOptions.reduce((result, typeItem) => {
return result.concat(typeItem.children || [])
}, [])
const site = allSites.find(item => item.value === siteId)
return !!(site && !site.disable)
},
// 更新一周冲放曲线时间范围 重置图表
updateWeekChartDate(data) {
this.weekChartTimeRange = data || []
this.siteId && this.getWeekChartData()
},
// 更新当日功率曲线时间范围 重置图表
updateActiveChartDate(data) {
this.activeChartTimeRange = data || []
this.siteId && this.getGVQXData()
},
toDetail(e) {
if (!this.siteId) return uni.showToast({
title: "请选择清单",
icon: 'none'
})
const {
index
} = e.detail
this.$tab.navigateTo(`/pages/work/${this.siteGirdList[index].page}/index?siteId=${this.siteId}`)
},
selectedSite(data) {
const siteObj = (data.detail.value || [])[1]
const value = siteObj?.value
if (!value) return
if (value === this.siteId) return
this.siteId = value
this.$store.commit('SET_CURRENTSITEID', value)
this.updateSiteInfo()
},
updateSiteInfo() {
if (!this.siteId) return
this.curveDisplayData = []
this.curveDisplayLoadingPromise = null
this.getSiteBaseInfo()
this.getWeekChartData()
this.getGVQXData()
this.getSiteDeviceCategory()
},
getFieldName(fieldCode) {
const raw = String(fieldCode || '').trim()
if (!raw) return ''
const index = raw.lastIndexOf('__')
return index >= 0 ? raw.slice(index + 2) : raw
},
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 timestamp = new Date(value).getTime()
return Number.isNaN(timestamp) ? null : timestamp
},
ensureCurveDisplayData() {
if (!this.siteId) return Promise.resolve([])
if (this.curveDisplayData.length > 0) return Promise.resolve(this.curveDisplayData)
if (this.curveDisplayLoadingPromise) return this.curveDisplayLoadingPromise
this.curveDisplayLoadingPromise = getProjectDisplayData(this.siteId)
.then(response => {
this.curveDisplayData = response?.data || []
return this.curveDisplayData
})
.catch(() => {
this.curveDisplayData = []
return []
})
.finally(() => {
this.curveDisplayLoadingPromise = null
})
return this.curveDisplayLoadingPromise
},
fetchCurveSeries(pointId, name, startTime, endTime) {
if (!pointId) return Promise.resolve(null)
return getPointConfigCurve({
siteId: this.siteId,
pointId,
pointType: 'data',
rangeType: 'custom',
startTime,
endTime
}).then(response => {
const list = response?.data || []
const points = list.map(item => {
const timestamp = this.parseToTimestamp(item.dataTime)
const value = Number(item.pointValue)
if (!timestamp || Number.isNaN(value)) return null
return {
timestamp,
label: item.dataTime,
value
}
}).filter(Boolean)
return {
name,
points
}
}).catch(() => null)
},
buildLineChartData(seriesList) {
const validSeries = (seriesList || []).filter(item => item && (item.points || []).length > 0)
if (validSeries.length === 0) {
return {
categories: [],
series: []
}
}
const labelByTimestamp = {}
const timestampSet = new Set()
validSeries.forEach(item => {
item.points.forEach(point => {
timestampSet.add(point.timestamp)
if (!labelByTimestamp[point.timestamp]) {
labelByTimestamp[point.timestamp] = point.label
}
})
})
const sortedTimestamps = Array.from(timestampSet).sort((a, b) => a - b)
const categories = sortedTimestamps.map(item => labelByTimestamp[item])
const series = validSeries.map(item => {
const pointMap = {}
item.points.forEach(point => {
pointMap[point.timestamp] = point.value
})
return {
name: item.name,
data: sortedTimestamps.map(timestamp => pointMap[timestamp] ?? null)
}
})
return {
categories,
series
}
},
findActiveCurveRow(sectionRows, keywords = []) {
const keywordSet = new Set((keywords || []).map(item => String(item || '').toLowerCase()))
return (sectionRows || []).find(row => {
const fieldCode = this.getFieldName(row?.fieldCode).toLowerCase()
const fieldName = String(row?.fieldName || '').toLowerCase()
if (keywordSet.has(fieldCode) || keywordSet.has(fieldName)) return true
return Array.from(keywordSet).some(keyword => fieldCode.includes(keyword) || fieldName.includes(keyword))
})
},
getSiteList() {
getAllSites().then(response => {
const data = response?.data || []
const canAccessAll = !this.belongSite || this.belongSite.length === 0 || this.belongSite.includes('all')
const siteTypeOptions = createSiteTypeOptions().map(item => ({
...item,
children: []
}))
data.forEach(item => {
if (!canAccessAll && !this.belongSite.includes(item.siteId)) return
const siteType = (item.siteType || item.type || 'cn').toString().toLowerCase()
const typeOption = siteTypeOptions.find(i => i.value === siteType) || siteTypeOptions.find(i => i.value === 'cn')
if (!typeOption) return
typeOption.children.push({
text: item.siteName,
value: item.siteId,
id: item.id
})
})
this.siteTypeOptions = siteTypeOptions.filter(item => (item.children || []).length > 0)
const siteChildren = this.siteTypeOptions.reduce((result, typeItem) => {
return result.concat(typeItem.children || [])
}, [])
// 设置默认展示的站点
const defaultSiteId = this.isAvailableSite(this.currentSiteId) ? this.currentSiteId : (siteChildren[0]?.value || '')
if (defaultSiteId) {
this.siteId = defaultSiteId
this.$store.commit('SET_CURRENTSITEID', defaultSiteId)
this.updateSiteInfo()
}
})
},
getSiteBaseInfo() {
getSingleSiteBaseInfo({
siteId: this.siteId
}).then(response => {
console.log('获取站点基本信息', response)
this.baseInfo = response?.data || {}
})
},
getSiteDeviceCategory() {
getSiteAllDeviceCategory({
siteId: this.siteId
}).then(response => {
this.deviceCategoryOptions = response?.data || []
})
},
handleDate(date) {
if (!date) return date
const time = new Date(date)
const month = time.getMonth() + 1,
day = time.getDate()
return `${month<10?'0'+month : month}/${day<10 ? '0'+day : day}`
},
getGVQXData() {
this.$refs.activeChartDateRangeSelect.showBtnLoading(true)
const startTime = this.normalizeDateTime(this.activeChartTimeRange[0], false)
const endTime = this.normalizeDateTime(this.activeChartTimeRange[1], true)
this.ensureCurveDisplayData().then(displayData => {
const sectionRows = (displayData || []).filter(item =>
item && item.sectionName === '当日功率曲线' && item.useFixedDisplay !== 1 && item.dataPoint
)
const targetRows = [{
name: '电网功率',
row: this.findActiveCurveRow(sectionRows, ['gridpower', '电网功率'])
},
{
name: '负载功率',
row: this.findActiveCurveRow(sectionRows, ['loadpower', '负载功率'])
},
{
name: '储能功率',
row: this.findActiveCurveRow(sectionRows, ['storagepower', '储能功率'])
}
].filter(item => item.row && item.row.dataPoint)
const tasks = targetRows.map(item => this.fetchCurveSeries(String(item.row.dataPoint).trim(), item.name, startTime,
endTime))
return Promise.all(tasks).then(series => {
this.activeChartData = JSON.parse(JSON.stringify(this.buildLineChartData((series || []).filter(Boolean))))
})
}).finally(() => this.$refs.activeChartDateRangeSelect.showBtnLoading(false))
},
getWeekChartData() {
this.$refs.weekChartDateRangeSelect.showBtnLoading(true)
const startTime = this.normalizeDateTime(this.weekChartTimeRange[0], false)
const endTime = this.normalizeDateTime(this.weekChartTimeRange[1], true)
this.ensureCurveDisplayData().then(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)
const name = row.fieldName || this.getFieldName(row.fieldCode) || pointId
return this.fetchCurveSeries(pointId, name, startTime, endTime)
})
return Promise.all(tasks).then(series => {
this.weekChartData = JSON.parse(JSON.stringify(this.buildLineChartData((series || []).filter(Boolean))))
})
}).finally(() => this.$refs.weekChartDateRangeSelect.showBtnLoading(false))
}
},
watch: {
currentSiteId(newSiteId) {
if (!newSiteId || newSiteId === this.siteId) return
if (!this.isAvailableSite(newSiteId)) return
this.siteId = newSiteId
this.updateSiteInfo()
}
},
// 页面切换不会重新调用如果希望每次切换页面都重新调接口使用onShow
onLoad() {
this.$nextTick(() => {
this.getSiteList()
this.$refs.weekChartDateRangeSelect.init()
this.$refs.activeChartDateRangeSelect.init(true)
})
},
// 页面滚动 设置pageScrollTop chart显示需要
onPageScroll(e) {
this.pageScrollTop = e.scrollTop
},
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #fff;
min-height: 100%;
height: auto;
font-size: 26rpx;
line-height: 30rpx;
}
view {
line-height: inherit;
}
/* #endif */
.text {
text-align: center;
margin-top: 10rpx;
}
// 基本信息
.base-info {
margin-top: -80rpx;
border-radius: 80rpx 80rpx 0 0;
padding: 30rpx;
background-color: #fff;
// 装机功率
.install-data {
.grid-item-box {
padding-top: 6rpx;
padding-bottom: 6rpx;
.text {
margin-top: 20rpx;
color: #000;
}
}
}
.sections-list {
margin-bottom: 10rpx;
::v-deep &>.uni-section-header {
font-weight: 700;
font-size: 26rpx;
line-height: 30rpx;
}
}
.sections-list:not(:first-child) {
margin-top: 40rpx;
}
::v-deep {
.uni-section__content-title {
font-size: 26rpx !important;
}
.uni-select__input-box {
width: 100%;
.uni-select__input-text {
font-size: 24rpx;
}
}
.uni-select__selector-empty,
.uni-select__selector-item {
font-size: 24rpx;
line-height: 36rpx;
padding-top: 10rpx;
padding-bottom: 10rpx;
text-align: left;
}
// .uni-date__x-input {
// height: 50rpx;
// line-height: 50rpx;
// font-size: 26rpx;
// }
}
.work-box {
.icon {
font-size: 52rpx;
color: #547ef4;
}
.text {
font-size: 26rpx;
padding-top: 10rpx;
color: #000;
}
}
.base-lists {
font-size: 24rpx;
line-height: 40rpx;
padding: 10rpx 20rpx;
padding-left: 40rpx;
.left {
width: 220rpx;
display: inline-block;
color: #666;
}
.right {
color: #333;
}
}
}
@media screen and (min-width: 500px) {}
</style>