Files
app-govern/pages/index/comp/monitoringPoint.vue
2026-06-18 16:34:25 +08:00

871 lines
26 KiB
Vue
Raw 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>
<page-meta :page-style="'overflow:' + (indicatorPopupShow ? 'hidden' : 'visible')"></page-meta>
<view class="itic2-page">
<view class="itic2-content">
<view class="project-header card">
<view class="project-title-row">
<view class="project-title-left">
<uni-icons custom-prefix="iconfont" type="icon-gongcheng" size="22" color="#376cf3" />
<text class="project-name">{{ engineeringName }}</text>
</view>
<view class="switch-btn" @click="switchEngineering">
<uni-icons type="loop" size="14" color="#376cf3" />
<text>切换工程</text>
</view>
</view>
<view class="stats-row">
<view class="stats-item" v-for="(item, idx) in summaryStats" :key="idx">
<text class="stats-num">{{ item.value }}</text>
<text class="stats-label">{{ item.label }}</text>
</view>
</view>
</view>
<view class="monitor-section-header">
<view class="section-title-row section-title-row--no-mb">
<uni-icons type="settings" size="18" color="#376cf3" />
<text class="section-title">已选展示指标</text>
</view>
</view>
<view class="indicators-card card">
<view class="indicator-tags">
<view v-for="(tag, idx) in selectedIndicators" :key="tag"
class="indicator-tag indicator-tag--active" @click="removeIndicator(idx)">
<text class="indicator-tag-text">{{ formatIndicatorTag(tag) }}</text>
<uni-icons type="closeempty" size="12" color="#376cf3" />
</view>
<view class="indicator-tag indicator-tag--add" @click="openIndicatorPopup">
<text>+添加指标</text>
</view>
</view>
</view>
<view class="monitor-section">
<view class="monitor-section-header">
<view class="section-title-row section-title-row--no-mb">
<uni-icons type="map-pin-ellipse" size="20" color="#376cf3" />
<text class="section-title">监测点信息</text>
</view>
<view class="legend-row">
<view class="legend-item" v-for="phase in phaseColors" :key="phase.name">
<view class="legend-dot" :style="{ background: phase.color }" />
<text class="legend-text">{{ phase.name }}</text>
</view>
</view>
</view>
<view class="monitor-list">
<view class="monitor-card card" v-for="(point, idx) in monitoringPoints" :key="idx">
<view class="card-header">
<view class="event-icon">
<Cn-icon-transient name="监测点" />
</view>
<view class="card-header-info">
<view class="point-name-row">
<text class="point-name ellipsis">{{ point.pointName }}</text>
</view>
<view class="meta-row">
<text class="meta-item ellipsis">项目{{ point.projectName }}</text>
<text class="meta-item ellipsis">设备{{ point.deviceName }}</text>
</view>
<text class="meta-time ellipsis">最新数据时间{{ point.dataTime || '-' }}</text>
</view>
</view>
<view class="params-section">
<view v-for="(rowItems, rowIdx) in chunkedChildren(getDisplayChildren(point))" :key="rowIdx"
class="double-row">
<view v-for="(child, childIdx) in rowItems" :key="childIdx" class="param-group">
<view class="param-title">
<text>{{ child.name }} {{ child.unit ? `(${child.unit})` : '' }}</text>
</view>
<view v-if="hasTPhaseData(child)" class="phase-single">
<text class="phase-value-vertical phase-value-vertical--neutral">{{ child.T
}}</text>
</view>
<view v-else class="phase-vertical">
<view class="phase-item-vertical">
<text class="phase-value-vertical"
:style="{ color: phaseColors[0].color }">{{ child.A }}</text>
</view>
<view class="phase-divider" />
<view class="phase-item-vertical">
<text class="phase-value-vertical"
:style="{ color: phaseColors[1].color }">{{ child.B }}</text>
</view>
<view class="phase-divider" />
<view class="phase-item-vertical">
<text class="phase-value-vertical"
:style="{ color: phaseColors[2].color }">{{ child.C }}</text>
</view>
</view>
</view>
</view>
</view>
<view class="more-btn" v-if="shouldShowMoreBtn(point)" @click="onMoreIndicators(point)">
<uni-icons type="list" size="16" color="#376cf3" />
<text>更多指标</text>
</view>
</view>
<uni-load-more v-if="listStatus == 'loading' || monitoringPoints.length > 0"
:status="listStatus"></uni-load-more>
<Cn-empty v-else style="top: 35%"></Cn-empty>
</view>
</view>
</view>
<view class="back-top boxClick" v-show="showBackTop" @click="backToTop">
<uni-icons type="arrow-up" size="22" color="#fff"></uni-icons>
</view>
<uni-popup ref="indicatorPopup" type="bottom" :safe-area="false" @change="onIndicatorPopupChange">
<view class="indicator-popup">
<view class="indicator-popup-header" @touchmove.stop.prevent>
<text class="indicator-popup-cancel" @click="closeIndicatorPopup">取消</text>
<text class="indicator-popup-confirm" @click="confirmIndicatorPopup">确定</text>
</view>
<scroll-view class="indicator-popup-list" scroll-y :show-scrollbar="false" @touchmove.stop>
<view v-if="targetLists.length === 0" class="indicator-popup-empty">
<text>暂无指标数据</text>
</view>
<view v-for="item in targetLists" :key="item.id || item.name" class="indicator-popup-item"
:class="{ 'indicator-popup-item--active': popupSelectedIndicators.includes(item.name) }"
@click="togglePopupIndicator(item.name)">
<text>{{ item.name }}</text>
<uni-icons v-if="popupSelectedIndicators.includes(item.name)" type="checkmarkempty" size="18"
color="#376cf3" />
</view>
</scroll-view>
</view>
</uni-popup>
</view>
</template>
<script>
import { queryByCode, queryCsDictTree } from '@/common/api/dictionary'
import { getLineDataByEngineer } from '@/common/api/harmonic'
import { getDevCount } from '@/common/api/device.js'
/** 无本地缓存时的默认展示指标:电压、电流 */
const DEFAULT_INDICATOR_CODES = ['Key_Power_Quality_V', 'Key_Power_Quality_I']
/** 治理测点(lineType=0)卡片默认展示的指标 */
const GOVERNANCE_DEFAULT_INDICATORS = [
'电网电流',
'电网电压',
'负载电流',
'总输出电流',
]
export default {
data() {
return {
targetLists: [],
popupSelectedIndicators: [],
engineeringName: '',
engineeringId: '',
summaryStats: [
{ label: '项目总数', value: 0 },
{ label: '设备总数', value: 0 },
{ label: '监测点总数', value: 0 },
],
selectedIndicators: [],
phaseColors: [
{ name: 'A相', color: '#F1B22E' },
{ name: 'B相', color: '#2BA471' },
{ name: 'C相', color: '#D54941' },
],
monitoringPoints: [],
listStatus: 'noMore',
lineDataRequestId: 0,
showBackTop: false,
indicatorPopupShow: false,
}
},
onPageScroll(e) {
this.showBackTop = e.scrollTop > 200
},
// 页面显示时同步工程名称
onShow() {
const engineering = uni.getStorageSync('engineering')
if (engineering?.id) {
this.engineeringName = engineering.name || ''
this.engineeringId = engineering.id
this.info()
}
if (this.targetLists.length) {
this.loadSelectedIndicators()
}
},
// 加载电能质量指标字典
created() {
queryByCode('Key_Power_Quality').then((res) => {
queryCsDictTree(res.data.id).then((resp) => {
this.targetLists = (resp.data || []).slice().reverse()
this.loadSelectedIndicators()
})
})
},
// 下拉刷新
onPullDownRefresh() {
this.info()
},
methods: {
// 查询接口
info() {
if (!this.engineeringId) {
uni.stopPullDownRefresh()
return
}
this.loadDevCount()
this.loadLineData()
},
loadDevCount() {
getDevCount(this.engineeringId).then((res) => {
if (res.code == 'A0000') {
this.updateSummaryStats(res.data)
}
})
},
loadLineData() {
this.listStatus = 'loading'
this.monitoringPoints = []
const requestId = ++this.lineDataRequestId
const engineerId = this.engineeringId
getLineDataByEngineer({ id: engineerId })
.then((res) => {
if (requestId !== this.lineDataRequestId) return
if (res.code == 'A0000') {
this.monitoringPoints = this.parseMonitoringPoints(res.data)
} else if (res.message) {
this.$util.toast(res.message)
}
})
.catch(() => {
if (requestId !== this.lineDataRequestId) return
this.$util.toast('加载失败,请稍后重试')
})
.finally(() => {
if (requestId !== this.lineDataRequestId) return
this.listStatus = 'noMore'
uni.stopPullDownRefresh()
})
},
// 接口 data 转为页面 monitoringPoints
parseMonitoringPoints(data = []) {
const list = Array.isArray(data) ? data : []
return list.map((point) => ({
projectName: point.projectName || '',
deviceName: point.deviceName || '',
pointName: point.pointName || '',
dataTime: point.dataTime || '',
lineType: point.lineType,
children: this.groupChildren(point.children || []),
}))
},
// 将按相别拆分的指标合并为 A/B/C 结构
groupChildren(children = []) {
const map = {}
const order = []
children.forEach((item) => {
const key = item.targetId || item.name
if (!map[key]) {
map[key] = {
targetId: item.targetId,
name: item.name,
unit: item.unit,
A: '-',
B: '-',
C: '-',
T: '-',
}
order.push(key)
}
const phase = item.phase
if (phase === 'A' || phase === 'B' || phase === 'C' || phase === 'T') {
map[key][phase] = this.formatPhaseValue(item.data)
}
})
return order.map((key) => map[key])
},
formatPhaseValue(value) {
if (value === null || value === undefined || value === '') return '-'
const num = Number(value)
if (Number.isNaN(num)) return String(value)
return num.toFixed(2)
},
hasTPhaseData(child) {
return child.T !== undefined && child.T !== null && child.T !== '-'
},
updateSummaryStats(devCount = {}) {
const deviceTotal =
(devCount.currentOnLineDevCount || 0) + (devCount.currentOffLineDevCount || 0)
this.summaryStats = [
{ label: '项目总数', value: devCount.currentProjectCount || 0 },
{ label: '设备总数', value: deviceTotal },
{ label: '监测点总数', value: devCount.lineCount || 0 },
]
},
// 指标名称超过6个字截断显示
formatIndicatorTag(name) {
if (!name || name.length <= 6) return name
return `${name.slice(0, 5)}...`
},
// 判断指标名称是否匹配(兼容括号后缀)
matchIndicator(name, selected) {
if (!name || !selected) return false
const base = (name || '').replace(/\(.*\)/, '').trim()
const selectedBase = (selected || '').replace(/\(.*\)/, '').trim()
return base === selectedBase
},
// 从缓存读取已选指标,无缓存时使用默认电压/电流指标
loadSelectedIndicators() {
const cached = uni.getStorageSync(this.$cacheKey.monitorSelectedIndicators)
if (Array.isArray(cached) && cached.length) {
const valid = cached.filter((name) =>
this.targetLists.some((item) => this.matchIndicator(item.name, name)),
)
if (valid.length) {
this.selectedIndicators = valid
return
}
}
this.selectedIndicators = this.getDefaultIndicatorsByCode()
this.saveSelectedIndicators()
},
getDefaultIndicatorsByCode() {
return this.targetLists
.filter((item) => DEFAULT_INDICATOR_CODES.includes(item.code) && item.name)
.map((item) => item.name)
},
saveSelectedIndicators() {
uni.setStorageSync(this.$cacheKey.monitorSelectedIndicators, this.selectedIndicators)
},
// 跳转切换工程
switchEngineering() {
uni.navigateTo({ url: '/pages/home/selectEngineering' })
},
// 移除已选指标
removeIndicator(idx) {
if (this.selectedIndicators.length <= 1) {
uni.showToast({ title: '至少保留一个指标', icon: 'none' })
return
}
this.selectedIndicators.splice(idx, 1)
this.saveSelectedIndicators()
},
// 打开指标选择弹窗
openIndicatorPopup() {
if (!this.targetLists.length) {
uni.showToast({ title: '暂无指标数据', icon: 'none' })
return
}
this.popupSelectedIndicators = this.targetLists
.filter((item) => this.selectedIndicators.some((name) => this.matchIndicator(item.name, name)))
.map((item) => item.name)
this.$refs.indicatorPopup.open()
},
// 关闭指标选择弹窗
closeIndicatorPopup() {
this.$refs.indicatorPopup.close()
this.popupSelectedIndicators = []
},
onIndicatorPopupChange(e) {
this.indicatorPopupShow = !!e.show
},
// 切换弹窗内指标勾选状态
togglePopupIndicator(name) {
const idx = this.popupSelectedIndicators.indexOf(name)
if (idx > -1) {
this.popupSelectedIndicators.splice(idx, 1)
} else {
this.popupSelectedIndicators.push(name)
}
},
// 确认指标选择
confirmIndicatorPopup() {
if (!this.popupSelectedIndicators.length) {
uni.showToast({ title: '至少保留一个指标', icon: 'none' })
return
}
this.selectedIndicators = [...this.popupSelectedIndicators]
this.saveSelectedIndicators()
this.closeIndicatorPopup()
},
// 跳转指标详情页;治理测点展示全部指标,普通测点展示未选中的指标
onMoreIndicators(point) {
uni.setStorageSync('monitorPointDetail', {
...point,
engineeringName: this.engineeringName,
selectedIndicators:
point.lineType === 0
? [...GOVERNANCE_DEFAULT_INDICATORS]
: [...this.selectedIndicators],
showAllIndicators: point.lineType === 0,
})
uni.navigateTo({
url: '/pages/index/comp/targetInfo',
})
},
// 治理测点默认 4 项;普通测点按顶部已选指标过滤
getDisplayChildren(point) {
const children = point.children || []
if (point.lineType === 0) {
return GOVERNANCE_DEFAULT_INDICATORS.map((name) =>
children.find((child) => this.matchIndicator(child.name, name)),
).filter(Boolean)
}
return children.filter((child) =>
this.selectedIndicators.some((name) => this.matchIndicator(child.name, name)),
)
},
// 治理测点存在默认四项以外的指标时显示「更多指标」
shouldShowMoreBtn(point) {
if (point.lineType !== 0) return true
const children = point.children || []
return children.some(
(child) =>
!GOVERNANCE_DEFAULT_INDICATORS.some((name) => this.matchIndicator(child.name, name)),
)
},
// 将指标列表按每行两个分组
chunkedChildren(children) {
const result = []
for (let i = 0; i < children.length; i += 2) {
result.push(children.slice(i, i + 2))
}
return result
},
backToTop() {
uni.pageScrollTo({
scrollTop: 0,
duration: 300,
})
this.showBackTop = false
},
},
}
</script>
<style lang="scss" scoped>
.itic2-page {
min-height: 100vh;
background: #f7f8fa;
position: relative;
}
.itic2-content {
padding: 20rpx 0 0rpx;
box-sizing: border-box;
}
.back-top {
position: fixed;
right: 30rpx;
bottom: 60rpx;
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #376cf3;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 16rpx rgba(55, 108, 243, 0.35);
z-index: 99;
}
.card {
background: #ffffff;
border-radius: 10px;
box-shadow: rgba(0, 0, 0, 0.08) 0px 0px 3px 1px;
margin: 0 10px 10px;
overflow: hidden;
}
.project-header {
padding: 20rpx;
// margin-top: 20rpx;
}
.project-title-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.project-title-left {
display: flex;
align-items: center;
gap: 12rpx;
flex: 1;
min-width: 0;
}
.project-name {
font-size: 32rpx;
font-weight: 700;
color: #333333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.switch-btn {
display: flex;
align-items: center;
gap: 6rpx;
padding: 8rpx 20rpx;
border: 1rpx solid #376cf380;
background-color: #266FFF10;
border-radius: 16rpx;
flex-shrink: 0;
text {
font-size: 24rpx;
color: #376cf3;
}
}
.stats-row {
display: flex;
gap: 20rpx;
}
.stats-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4rpx;
padding: 16rpx 12rpx;
background: #376cf3;
border-radius: 16rpx;
color: #ffffff;
}
.stats-num {
font-size: 44rpx;
font-weight: 700;
line-height: 1.2;
}
.stats-label {
font-size: 26rpx;
}
.indicators-card {
padding: 24rpx;
}
.section-title-row {
display: flex;
align-items: center;
gap: 10rpx;
margin-bottom: 20rpx;
&--no-mb {
margin-bottom: 0;
}
}
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #333333;
}
.indicator-tags {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.indicator-tag {
display: flex;
align-items: center;
gap: 6rpx;
padding: 6rpx 14rpx;
border-radius: 16rpx;
font-size: 26rpx;
max-width: 100%;
.indicator-tag-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&--active {
background: #376cf315;
color: #376cf3;
}
&--add {
background: #f5f5f5;
color: #666666;
border: 1rpx dashed #dcdfe6;
}
}
.monitor-section-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12rpx;
padding: 0 10px 16rpx;
}
.legend-row {
display: flex;
gap: 20rpx;
align-items: center;
}
.legend-item {
display: flex;
align-items: center;
gap: 8rpx;
}
.legend-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
}
.legend-text {
font-size: 24rpx;
color: #666666;
}
.monitor-list {
display: flex;
flex-direction: column;
padding-bottom: 40rpx;
}
.monitor-card {
border: 1rpx solid #eef2f6;
box-shadow: rgba(0, 0, 0, 0.08) 0px 0px 3px 1px;
}
.card-header {
padding: 20rpx 20rpx 12rpx;
display: flex;
align-items: center;
border-bottom: 1rpx solid #eef2f6;
}
.event-icon {
width: 90rpx;
height: 90rpx;
border-radius: 16rpx;
display: flex;
justify-content: center;
align-items: center;
margin-right: 20rpx;
background-color: #376cf320;
flex-shrink: 0;
}
.card-header-info {
flex: 1;
min-width: 0;
}
.point-name-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 8rpx;
min-width: 0;
}
.point-name {
flex: 1;
min-width: 0;
font-size: 30rpx;
font-weight: 700;
color: #333333;
}
.point-type-tag {
flex-shrink: 0;
padding: 4rpx 12rpx;
font-size: 22rpx;
color: #2ba471;
background: #2ba47115;
border-radius: 8rpx;
line-height: 1.2;
}
.meta-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6rpx 12rpx;
}
.meta-item {
font-size: 24rpx;
color: #666666;
line-height: 1.2;
}
.meta-time {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #666666;
line-height: 1.2;
}
.params-section {
padding: 20rpx 16rpx;
}
.double-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
.param-group {
min-width: 0;
background: #f3f3f3;
border-radius: 16rpx;
padding: 16rpx 8rpx 12rpx;
}
.param-title {
font-size: 24rpx;
color: #666666;
margin-bottom: 8rpx;
padding-left: 4rpx;
}
.phase-vertical {
display: flex;
align-items: stretch;
}
.phase-single {
display: flex;
justify-content: center;
align-items: center;
padding: 4rpx;
}
.phase-item-vertical {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 4rpx;
}
.phase-value-vertical {
font-size: 28rpx;
font-weight: 700;
&--neutral {
color: #333333;
}
}
.phase-divider {
width: 1px;
background: #d2d2d2;
margin: 8rpx 0;
}
.more-btn {
margin: 0 20rpx 20rpx;
height: 72rpx;
line-height: 72rpx;
text-align: center;
border: 1rpx solid #376cf380;
background-color: #266FFF10;
border-radius: 16rpx;
text {
font-size: 28rpx;
color: #376cf3;
font-weight: 500;
}
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.indicator-popup {
background: #ffffff;
overflow: hidden;
}
.indicator-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18rpx 32rpx;
border-bottom: 1rpx solid #eef2f6;
}
.indicator-popup-cancel {
font-size: 27rpx;
color: #666666;
min-width: 80rpx;
}
.indicator-popup-confirm {
font-size: 28rpx;
color: #376cf3;
min-width: 80rpx;
text-align: right;
}
.indicator-popup-list {
height: 60vh;
padding: 16rpx 0;
box-sizing: border-box;
}
.indicator-popup-empty {
padding: 80rpx 0;
text-align: center;
font-size: 28rpx;
color: #999999;
}
.indicator-popup-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 32rpx;
font-size: 28rpx;
color: #333333;
&--active {
color: #376cf3;
background: #376cf310;
}
}
</style>