Files
emsapp/pages/work/index.vue

603 lines
17 KiB
Vue
Raw Normal View History

2026-04-01 14:28:09 +08:00
<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" />
2026-01-19 17:30:03 +08:00
<!-- 静态信息 -->
<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">
2026-04-01 14:28:09 +08:00
<view class="title">装机功率(MWh)</view>
2026-01-21 10:22:50 +08:00
<view class="text">{{baseInfo.installPower | formatNumber}}</view>
2026-01-19 17:30:03 +08:00
</view>
</uni-grid-item>
<uni-grid-item>
2025-08-15 17:41:26 +08:00
<view class="grid-item-box">
2026-04-01 14:28:09 +08:00
<view class="title">装机容量(MWh)</view>
2026-01-21 10:22:50 +08:00
<view class="text">{{baseInfo.installCapacity | formatNumber}}</view>
2025-08-15 17:41:26 +08:00
</view>
</uni-grid-item>
</uni-grid>
2026-01-19 17:30:03 +08:00
</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>
2025-07-29 23:05:58 +08:00
</view>
</template>
<script>
2026-04-01 14:28:09 +08:00
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
},
2025-07-29 23:05:58 +08:00
data() {
return {
2025-08-15 17:41:26 +08:00
// 图表数据
2025-08-16 19:31:59 +08:00
weekChartTimeRange: [],
activeChartTimeRange: [],
weekChartData: {},
2026-04-01 14:28:09 +08:00
activeChartData: {},
curveDisplayData: [],
curveDisplayLoadingPromise: null,
pageScrollTop: 0,
2025-10-15 18:16:56 +08:00
glqxOptions: {
2026-01-16 17:54:42 +08:00
padding: [10, 5, 0, 10],
2025-10-15 18:16:56 +08:00
dataLabel: false,
enableScroll: true,
xAxis: {
scrollShow: true,
itemCount: 3,
disableGrid: true
},
2026-01-16 17:54:42 +08:00
extra: {
area: {
type: "curve",
opacity: 0.2,
addLine: true,
width: 2,
gradient: true,
activeType: "hollow"
}
}
2025-10-15 18:16:56 +08:00
// update: true,
// duration: 2,
// animation: false,
// enableScroll: true,
// padding: [10, 15, 10, 15]
},
2025-08-15 17:41:26 +08:00
options: {
2026-01-16 17:54:42 +08:00
padding: [10, 5, 0, 10],
2025-08-17 01:13:04 +08:00
dataLabel: false,
2025-08-15 17:41:26 +08:00
enableScroll: true,
xAxis: {
scrollShow: true,
itemCount: 5,
2025-08-15 17:41:26 +08:00
disableGrid: true
},
2026-01-16 17:54:42 +08:00
extra: {
area: {
type: "curve",
opacity: 0.2,
addLine: true,
width: 2,
gradient: true,
activeType: "hollow"
}
}
2025-08-17 01:13:04 +08:00
// update: true,
2025-08-15 17:41:26 +08:00
// duration: 2,
// animation: false,
// enableScroll: true,
// padding: [10, 15, 10, 15]
},
// 图表数据结束
2026-04-01 14:28:09 +08:00
deviceCategoryOptions: [], //当前站点包含的设备类别
siteTypeOptions: createSiteTypeOptions(),
2025-07-29 23:05:58 +08:00
siteId: '', //选择的站点ID
2025-08-15 17:41:26 +08:00
baseInfo: {}, //站点基本信息
2025-07-29 23:05:58 +08:00
gridList: [{
page: 'bmszl',
2025-08-07 15:19:49 +08:00
icon: 'icon-BMS',
2025-07-29 23:05:58 +08:00
text: 'BMS总览',
2025-10-15 18:16:56 +08:00
categoryName: 'STACK'
2025-07-29 23:05:58 +08:00
},
{
page: 'bmsdcc',
2025-08-07 15:19:49 +08:00
icon: 'icon-a-dianchicunengliangkuai',
2025-07-29 23:05:58 +08:00
text: 'BMS电池簇',
2025-10-15 18:16:56 +08:00
categoryName: 'CLUSTER'
2025-07-29 23:05:58 +08:00
},
{
page: 'pcs',
2025-08-07 15:19:49 +08:00
icon: 'icon-PCS',
2025-07-29 23:05:58 +08:00
text: 'PCS',
2025-10-15 18:16:56 +08:00
categoryName: 'PCS'
2025-07-29 23:05:58 +08:00
},
2026-03-05 16:34:25 +08:00
{
page: 'db',
icon: 'icon-dianbiao4',
text: '电表',
categoryName: 'AMMETER'
},
{
page: 'yl',
icon: 'icon-gongneng-diandongji',
text: '冷却',
categoryName: 'COOLING'
},
{
page: 'dtdc',
icon: 'icon-dantidianchi',
text: '单体电池',
2025-10-15 18:16:56 +08:00
categoryName: 'BATTERY'
2025-07-29 23:05:58 +08:00
}
]
}
},
2026-03-05 16:34:25 +08:00
computed: {
...mapGetters(['belongSite', 'currentSiteId']),
siteGirdList() {
return this.gridList.filter(i => this.deviceCategoryOptions.includes(i.categoryName))
}
},
methods: {
isAvailableSite(siteId) {
2026-04-01 14:28:09 +08:00
const allSites = this.siteTypeOptions.reduce((result, typeItem) => {
return result.concat(typeItem.children || [])
}, [])
const site = allSites.find(item => item.value === siteId)
2026-03-05 16:34:25 +08:00
return !!(site && !site.disable)
},
// 更新一周冲放曲线时间范围 重置图表
updateWeekChartDate(data) {
this.weekChartTimeRange = data || []
this.siteId && this.getWeekChartData()
2025-08-16 19:31:59 +08:00
},
// 更新当日功率曲线时间范围 重置图表
updateActiveChartDate(data) {
2025-10-15 18:16:56 +08:00
this.activeChartTimeRange = data || []
this.siteId && this.getGVQXData()
2025-08-16 19:31:59 +08:00
},
2025-07-29 23:05:58 +08:00
toDetail(e) {
2025-08-15 17:41:26 +08:00
if (!this.siteId) return uni.showToast({
title: "请选择清单",
icon: 'none'
})
2025-07-29 23:05:58 +08:00
const {
index
} = e.detail
2026-01-21 16:53:05 +08:00
this.$tab.navigateTo(`/pages/work/${this.siteGirdList[index].page}/index?siteId=${this.siteId}`)
2025-07-29 23:05:58 +08:00
},
2026-03-05 16:34:25 +08:00
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()
},
2026-04-01 14:28:09 +08:00
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
2026-03-05 16:34:25 +08:00
return {
2026-04-01 14:28:09 +08:00
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({
2026-03-05 16:34:25 +08:00
text: item.siteName,
value: item.siteId,
2026-04-01 14:28:09 +08:00
id: item.id
})
2026-03-05 16:34:25 +08:00
})
2026-04-01 14:28:09 +08:00
this.siteTypeOptions = siteTypeOptions.filter(item => (item.children || []).length > 0)
const siteChildren = this.siteTypeOptions.reduce((result, typeItem) => {
return result.concat(typeItem.children || [])
}, [])
2026-03-05 16:34:25 +08:00
// 设置默认展示的站点
2026-04-01 14:28:09 +08:00
const defaultSiteId = this.isAvailableSite(this.currentSiteId) ? this.currentSiteId : (siteChildren[0]?.value || '')
2026-03-05 16:34:25 +08:00
if (defaultSiteId) {
this.siteId = defaultSiteId
this.$store.commit('SET_CURRENTSITEID', defaultSiteId)
this.updateSiteInfo()
}
})
},
2025-10-15 18:16:56 +08:00
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}`
},
2026-04-01 14:28:09 +08:00
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))
2026-03-05 16:34:25 +08:00
}
},
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()
2025-08-16 19:31:59 +08:00
this.$refs.weekChartDateRangeSelect.init()
2025-10-15 18:16:56 +08:00
this.$refs.activeChartDateRangeSelect.init(true)
2025-08-16 19:31:59 +08:00
})
2025-08-15 17:41:26 +08:00
},
2025-08-16 19:31:59 +08:00
// 页面滚动 设置pageScrollTop chart显示需要
2025-08-15 17:41:26 +08:00
onPageScroll(e) {
this.pageScrollTop = e.scrollTop
},
2025-07-29 23:05:58 +08:00
}
</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;
2026-01-09 19:10:32 +08:00
font-size: 26rpx;
line-height: 30rpx;
2025-07-29 23:05:58 +08:00
}
view {
line-height: inherit;
}
/* #endif */
.text {
text-align: center;
margin-top: 10rpx;
}
2026-04-01 14:28:09 +08:00
// 基本信息
.base-info {
2026-01-19 17:30:03 +08:00
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;
}
}
2026-01-09 19:10:32 +08:00
}
2026-01-19 17:30:03 +08:00
.sections-list {
margin-bottom: 10rpx;
::v-deep &>.uni-section-header {
font-weight: 700;
font-size: 26rpx;
line-height: 30rpx;
2026-01-09 19:10:32 +08:00
}
2025-11-01 13:04:26 +08:00
}
2026-01-19 17:30:03 +08:00
.sections-list:not(:first-child) {
margin-top: 40rpx;
2025-11-01 13:04:26 +08:00
}
2026-01-19 17:30:03 +08:00
::v-deep {
.uni-section__content-title {
font-size: 26rpx !important;
}
.uni-select__input-box {
width: 100%;
.uni-select__input-text {
font-size: 24rpx;
}
}
2025-07-29 23:05:58 +08:00
2026-01-19 17:30:03 +08:00
.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;
// }
2026-01-09 19:10:32 +08:00
}
2026-01-19 17:30:03 +08:00
.work-box {
.icon {
font-size: 52rpx;
color: #547ef4;
}
.text {
font-size: 26rpx;
padding-top: 10rpx;
color: #000;
}
2026-01-09 19:10:32 +08:00
}
2025-08-07 15:19:49 +08:00
2026-01-09 19:10:32 +08:00
2026-01-19 17:30:03 +08:00
.base-lists {
font-size: 24rpx;
line-height: 40rpx;
padding: 10rpx 20rpx;
padding-left: 40rpx;
2025-08-15 17:41:26 +08:00
2026-01-19 17:30:03 +08:00
.left {
width: 220rpx;
display: inline-block;
color: #666;
}
2025-08-15 17:41:26 +08:00
2026-01-19 17:30:03 +08:00
.right {
color: #333;
}
2025-08-15 17:41:26 +08:00
}
}
2025-07-29 23:05:58 +08:00
@media screen and (min-width: 500px) {}
2026-03-05 16:34:25 +08:00
</style>