+ const newDivShunshi = $(``)
newDivShunshi.insertAfter($('#shushi'))
@@ -342,6 +383,13 @@ const initWave = (
const wave = document.getElementById('wave')
if (!wave) return
+ applyChartSize(wave)
+
+ const yRange = calcShuYAxisRange(Number(min), Number(max))
+
+ const existingChart = echarts.getInstanceByDom(wave)
+ if (existingChart) existingChart.dispose()
+
const myChartes = echarts.init(wave)
const echartsColor = {
WordColor: '#000',
@@ -363,11 +411,6 @@ const initWave = (
]
}
- setTimeout(() => {
- wave.style.width = '100%'
- wave.style.height = vh.value
- }, 0)
-
const option = {
tooltip: {
top: '10px',
@@ -481,10 +524,8 @@ const initWave = (
},
boundaryGap: [0, '100%'],
showLastLabel: true,
- // max: max.toFixed(2) * 1.1,
- // min: min.toFixed(2) > 0 ? min.toFixed(2) - min.toFixed(2) * 0.1 : min.toFixed(2) * 1.1,
- max: Math.floor(max.toFixed(2) * 1.1 * 10) / 10,
- min: Math.floor(min.toFixed(2) > 0 ? min.toFixed(2) - min.toFixed(2) * 0.1 : min.toFixed(2) * 1.1 * 10) / 10 ,
+ max: yRange.max,
+ min: yRange.min,
opposite: false,
nameTextStyle: {
fontSize: '12px',
@@ -501,8 +542,7 @@ const initWave = (
fontSize: '12px',
color: props.DColor ? '#000' : echartsColor.WordColor,
formatter: function (value: number) {
- // return (value - 0).toFixed(2)
- return Math.floor(value * 1000) / 1000
+ return formatAxisLabel(value)
}
},
splitLine: {
@@ -518,7 +558,6 @@ const initWave = (
right: '45px',
bottom: '40px',
top: '60px'
- // containLabel: true
},
dataZoom: [
{
@@ -578,11 +617,7 @@ const initWave = (
myChartes.setOption(option)
myChartess.value = myChartes
-
- setTimeout(() => {
- myChartes.resize()
- loading.value = false
- }, 400)
+ finishChartRender(myChartes, true)
if (waveDatas && waveDatas.length > 1) {
const waveDatasTemp = waveDatas.slice(1)
@@ -679,6 +714,13 @@ const drawPics = (
const waveIds = document.getElementById(waveId)
if (!waveIds) return
+ applyChartSize(waveIds)
+
+ const yRange = calcShuYAxisRange(Number(min), Number(max))
+
+ const existingChart = echarts.getInstanceByDom(waveIds)
+ if (existingChart) existingChart.dispose()
+
const myChartes = echarts.init(waveIds)
const echartsColor = {
WordColor: '#000',
@@ -791,8 +833,8 @@ const drawPics = (
},
boundaryGap: [0, '100%'],
showLastLabel: true,
- max: Math.floor(max.toFixed(2) * 1.1 * 10) / 10,
- min: Math.floor(min.toFixed(2) > 0 ? min.toFixed(2) - min.toFixed(2) * 0.1 : min.toFixed(2) * 1.1 * 10) / 10 ,
+ max: yRange.max,
+ min: yRange.min,
opposite: false,
nameTextStyle: {
fontSize: '12px',
@@ -809,8 +851,7 @@ const drawPics = (
fontSize: '12px',
color: props.DColor ? '#000' : echartsColor.WordColor,
formatter: function (value: number) {
- // return (value - 0).toFixed(2)
- return Math.floor(value * 1000) / 1000
+ return formatAxisLabel(value)
}
},
splitLine: {
@@ -897,10 +938,7 @@ const drawPics = (
break
}
- setTimeout(() => {
- myChartes.resize()
- loading.value = false
- }, 400)
+ finishChartRender(myChartes)
echarts.connect([myChartes1, myChartes])
}
@@ -929,12 +967,8 @@ const backbxlb = () => {
myChartess4.value = null
myChartess5.value = null
- // echarts.disconnect(charts.filter(Boolean) as echarts.ECharts[])
- charts.filter(Boolean).forEach(chart => {
- if (chart && typeof chart.dispose === 'function') {
- chart.dispose()
- }
- })
+ $('#shushi').nextAll(`.${EXTRA_PANEL_CLASS}`).remove()
+ $('div.bx1').remove()
}
const getMax = (temp: number, tempA: number, tempB: number, tempC: number): number => {
diff --git a/src/components/echarts/shushiboxi1.vue b/src/components/echarts/shushiboxi1.vue
deleted file mode 100644
index d3d8f13..0000000
--- a/src/components/echarts/shushiboxi1.vue
+++ /dev/null
@@ -1,1201 +0,0 @@
-
-
-
-
diff --git a/src/components/table/index.vue b/src/components/table/index.vue
index 84b353e..8c1fa8a 100644
--- a/src/components/table/index.vue
+++ b/src/components/table/index.vue
@@ -10,10 +10,11 @@
@checkbox-all="selectChangeEvent"
@checkbox-change="selectChangeEvent"
:showOverflow="showOverflow"
- :sort-config="{ remote: true }"
- @sort-change="handleSortChange"
+
+
>
-
+
+
diff --git a/src/components/tree/govern/analyzeTree.vue b/src/components/tree/govern/analyzeTree.vue
index 6dabb56..124f9c0 100644
--- a/src/components/tree/govern/analyzeTree.vue
+++ b/src/components/tree/govern/analyzeTree.vue
@@ -152,7 +152,7 @@ async function selectInitialNode(
treeInstance?.setCurrentKey(node.id)
- emit('init', { level, ...node })
+ emit('init', { ...node })
return
diff --git a/src/components/tree/govern/cloudDeviceEntryTree.vue b/src/components/tree/govern/cloudDeviceEntryTree.vue
index c333480..d8845de 100644
--- a/src/components/tree/govern/cloudDeviceEntryTree.vue
+++ b/src/components/tree/govern/cloudDeviceEntryTree.vue
@@ -64,7 +64,7 @@ async function loadTree() {
await selectTreeNode(treRef.value, node, {
level: 3,
onSelect: selected => {
- emit('init', { level: 3, ...selected })
+ emit('init', { ...selected, level: 3 })
changePointType('4', selected)
}
})
diff --git a/src/components/tree/govern/deviceInfoTree.vue b/src/components/tree/govern/deviceInfoTree.vue
index d0e24db..408a2eb 100644
--- a/src/components/tree/govern/deviceInfoTree.vue
+++ b/src/components/tree/govern/deviceInfoTree.vue
@@ -48,7 +48,7 @@ async function initTree(list: any[]) {
}
await selectTreeNode(treRef.value, node, {
- onSelect: selected => emit('init', { level: 2, ...selected })
+ onSelect: selected => emit('init', { ...selected, level: 2 })
})
}
diff --git a/src/components/tree/govern/deviceTree.vue b/src/components/tree/govern/deviceTree.vue
index c7a127e..ec03293 100644
--- a/src/components/tree/govern/deviceTree.vue
+++ b/src/components/tree/govern/deviceTree.vue
@@ -1,16 +1,7 @@
-
+
diff --git a/src/components/tree/govern/deviceTreeUtils.ts b/src/components/tree/govern/deviceTreeUtils.ts
index 8c9b0e1..8d2de56 100644
--- a/src/components/tree/govern/deviceTreeUtils.ts
+++ b/src/components/tree/govern/deviceTreeUtils.ts
@@ -17,7 +17,8 @@ export function decorateDeviceTree(
child.children?.forEach((grand: any) => {
applyMeta(grand, {
icon: 'el-icon-Platform',
- color: statusColor(grand.comFlag)
+ color: statusColor(grand.comFlag),
+ level: 3
})
leaves.engineering.push(grand)
})
@@ -68,6 +69,7 @@ export function decorateDeviceTree(
l3.pName = '监测设备'
applyMeta(l3, {
icon: 'el-icon-Platform',
+ level: 3,
color: l3.comFlag === 1 ? '#e26257 !important' : primary()
})
leaves.monitor.push(l3)
diff --git a/src/components/tree/govern/lineTreeUtils.ts b/src/components/tree/govern/lineTreeUtils.ts
index 55c9ee9..ce8dc42 100644
--- a/src/components/tree/govern/lineTreeUtils.ts
+++ b/src/components/tree/govern/lineTreeUtils.ts
@@ -39,6 +39,21 @@ export function createLineTreeDecorators(getPrimaryColor: () => string): LineTre
export type TreeRefKey = 'treeRef1' | 'treeRef2' | 'treeRef3' | 'treeRef4'
+/** 线路树可选叶子节点元数据 */
+export const LINE_LEAF_META = { level: 3, type: 'line' as const }
+
+/** 是否为线路树可选叶子(监测点/线路) */
+export function isLineTreeLeaf(node: any): boolean {
+ if (!node?.id) return false
+ return node.type === 'line' || node.level === 3
+}
+
+/** 是否为报告/导出可选监测点 */
+export function isReportMonitorPoint(node: any): boolean {
+ if (!node?.id) return false
+ return isLineTreeLeaf(node) || node.level === 3 || (!node.children?.length && !!node.pid)
+}
+
export interface DecorateLineTreeOptions {
/** 是否禁用父级节点(分析树隐藏父节点,测点树不禁用) */
disableParents?: boolean
@@ -69,7 +84,11 @@ export function decorateLineTree(
...parentDisabled
})
grand.children?.forEach((leaf: any) => {
- applyMeta(leaf, { icon: 'el-icon-Platform', color: statusColor(leaf.comFlag) })
+ applyMeta(leaf, {
+ icon: 'el-icon-Platform',
+ color: statusColor(leaf.comFlag),
+ ...LINE_LEAF_META
+ })
leaves.engineering.push(leaf)
})
})
@@ -90,7 +109,11 @@ export function decorateLineTree(
...parentDisabled
})
l3.children?.forEach((l4: any) => {
- applyMeta(l4, { icon: 'el-icon-Platform', color: statusColor(l4.comFlag) })
+ applyMeta(l4, {
+ icon: 'el-icon-Platform',
+ color: statusColor(l4.comFlag),
+ ...LINE_LEAF_META
+ })
leaves.govern.push(l4)
})
})
@@ -100,7 +123,11 @@ export function decorateLineTree(
item.children?.forEach((l1: any) => {
applyMeta(l1, { icon: 'el-icon-Platform', color: statusColor(l1.comFlag) })
l1.children?.forEach((l2: any) => {
- applyMeta(l2, { icon: 'el-icon-Platform', color: statusColor(l2.comFlag) })
+ applyMeta(l2, {
+ icon: 'el-icon-Platform',
+ color: statusColor(l2.comFlag),
+ ...LINE_LEAF_META
+ })
leaves.portable.push(l2)
})
})
@@ -117,7 +144,11 @@ export function decorateLineTree(
...parentDisabled
})
l3.children?.forEach((l4: any) => {
- applyMeta(l4, { icon: 'el-icon-Platform', color: statusColor(l4.comFlag) })
+ applyMeta(l4, {
+ icon: 'el-icon-Platform',
+ color: statusColor(l4.comFlag),
+ ...LINE_LEAF_META
+ })
leaves.monitor.push(l4)
})
})
diff --git a/src/components/tree/govern/pointTree.vue b/src/components/tree/govern/pointTree.vue
index 270d0a0..96d25e4 100644
--- a/src/components/tree/govern/pointTree.vue
+++ b/src/components/tree/govern/pointTree.vue
@@ -64,7 +64,7 @@ async function selectInitialNode(type: string | undefined, leaves: LineTreeLeave
const treeInstance = await waitForTreeRef(treRef.value, refKey)
treeInstance?.setCurrentKey(node.id)
- emit('init', { level, ...node })
+ emit('init', { ...node })
if (type === '2') {
changePointType('4', node)
diff --git a/src/components/tree/govern/pointTreeWx.vue b/src/components/tree/govern/pointTreeWx.vue
index 3e37123..d9964f6 100644
--- a/src/components/tree/govern/pointTreeWx.vue
+++ b/src/components/tree/govern/pointTreeWx.vue
@@ -59,7 +59,7 @@ interface Props {
const props = withDefaults(defineProps(), { template: false })
-const emit = defineEmits(['init', 'checkChange', 'nodeChange', 'editNode', 'getChart', 'Policy'])
+const emit = defineEmits(['init', 'checkChange', 'nodeChange', 'node-click', 'editNode', 'getChart', 'Policy'])
const config = useConfig()
const tree = ref([])
@@ -109,6 +109,7 @@ const clickNode = (e: any) => {
planId.value = e?.children ? e.id : e.pid
id.value = e.id
emit('nodeChange', e)
+ emit('node-click', e)
}
bootstrapWithTemplate(
diff --git a/src/components/tree/select.vue b/src/components/tree/select.vue
index a4caac0..4e8ca49 100644
--- a/src/components/tree/select.vue
+++ b/src/components/tree/select.vue
@@ -52,6 +52,7 @@ import useCurrentInstance from '@/utils/useCurrentInstance'
import { ElMessage, ElTree } from 'element-plus'
import { ref, watch } from 'vue'
import { createTreeFilterNode } from './govern/treeFilterUtils'
+import { isLineTreeLeaf } from './govern/lineTreeUtils'
defineOptions({ name: 'govern/select', inheritAttrs: false })
@@ -75,7 +76,8 @@ const filterNode = createTreeFilterNode()
const checkedNodes = ref([])
const defaultCheckedKeys = ref([])
const MAX_CHECK = 5
-const MONITOR_LEVEL = 3
+
+const isMonitorLeaf = (node: any) => isLineTreeLeaf(node)
watch(filterText, val => treeRef.value?.filter(val))
@@ -85,7 +87,7 @@ const onMenuCollapse = () => {
}
const handleCheckChange = (_data: any, checkInfo: any) => {
- const monitoringPointNodes = (checkInfo.checkedNodes as any[]).filter(node => node.level === MONITOR_LEVEL)
+ const monitoringPointNodes = (checkInfo.checkedNodes as any[]).filter(isMonitorLeaf)
if (monitoringPointNodes.length > MAX_CHECK) {
const previousCheckedNodes = checkedNodes.value
@@ -122,7 +124,7 @@ const updateNodeCheckStatus = (currentCount: number) => {
if (!treeRef.value) return
const isMaxSelected = currentCount >= MAX_CHECK
treeRef.value.store._getAllNodes().forEach((node: any) => {
- if (node.level === MONITOR_LEVEL) {
+ if (isMonitorLeaf(node.data)) {
node.data.disabled = isMaxSelected && !node.checked
}
})
diff --git a/src/layouts/admin/components/logo.vue b/src/layouts/admin/components/logo.vue
index 674d0fc..97fade5 100644
--- a/src/layouts/admin/components/logo.vue
+++ b/src/layouts/admin/components/logo.vue
@@ -1,6 +1,6 @@
-
![]()
+
![]()
@@ -8,7 +8,7 @@
-
+ />
diff --git a/src/utils/chartAxisHelper.ts b/src/utils/chartAxisHelper.ts
new file mode 100644
index 0000000..36abdde
--- /dev/null
+++ b/src/utils/chartAxisHelper.ts
@@ -0,0 +1,59 @@
+const AXIS_DECIMALS = 2
+
+export function roundAxisValue(val: number, decimals = AXIS_DECIMALS): number {
+ if (!Number.isFinite(val)) return 0
+ const factor = 10 ** decimals
+ return Math.round(val * factor) / factor
+}
+
+/** Y 轴刻度:最多 2 位小数,末尾 0 去掉(如 1.00 → 1,0.10 → 0.1) */
+export function formatAxisLabel(value: number): string {
+ if (!Number.isFinite(value)) return '0'
+ return String(Number(roundAxisValue(value).toFixed(AXIS_DECIMALS)))
+}
+
+/** 瞬间波形 Y 轴范围 */
+export function calcShuYAxisRange(dataMin: number, dataMax: number): { min: number; max: number } {
+ const min = Number(dataMin)
+ const max = Number(dataMax)
+ if (!Number.isFinite(min) || !Number.isFinite(max)) {
+ return { min: 0, max: 1 }
+ }
+
+ let axisMax = max * 1.1
+ let axisMin = min > 0 ? min - min * 0.1 : min * 1.1
+
+ if (axisMax <= axisMin) {
+ const pad = Math.abs(max) * 0.1 || 0.01
+ axisMax = max + pad
+ axisMin = min - pad
+ }
+
+ return {
+ min: roundAxisValue(axisMin),
+ max: roundAxisValue(axisMax)
+ }
+}
+
+/** RMS 波形 Y 轴范围 */
+export function calcRmsYAxisRange(dataMin: number, dataMax: number): { min: number; max: number } {
+ const min = Number(dataMin)
+ const max = Number(dataMax)
+ if (!Number.isFinite(min) || !Number.isFinite(max)) {
+ return { min: 0, max: 1 }
+ }
+
+ let axisMax = max * 1.06 * 1.1
+ let axisMin = min - min * 0.2
+
+ if (axisMax <= axisMin) {
+ const pad = Math.abs(max - min) * 0.1 || Math.abs(max) * 0.1 || 0.01
+ axisMax = max + pad
+ axisMin = min - pad
+ }
+
+ return {
+ min: roundAxisValue(axisMin),
+ max: roundAxisValue(axisMax)
+ }
+}
diff --git a/src/utils/luckysheetHelper.ts b/src/utils/luckysheetHelper.ts
new file mode 100644
index 0000000..36f42b2
--- /dev/null
+++ b/src/utils/luckysheetHelper.ts
@@ -0,0 +1,83 @@
+import { ElMessage } from 'element-plus'
+import { exportExcel } from '@/views/system/reportForms/export.js'
+
+/** 解析 Luckysheet 接口返回的 sheet 数据 */
+export function parseLuckysheetSheets(sheets: any[]) {
+ sheets.forEach((item: any) => {
+ if (item.data1) {
+ try {
+ item.data = JSON.parse(item.data1)
+ } catch {
+ /* ignore invalid json */
+ }
+ }
+ item.celldata?.forEach((cell: any) => {
+ if (item.data?.[cell.r]?.[cell.c]?.v != null) {
+ item.data[cell.r][cell.c] = cell.v
+ }
+ })
+ })
+}
+
+declare const luckysheet: any
+
+const DEFAULT_REPORT_OPTIONS = {
+ title: '',
+ lang: 'zh',
+ showtoolbar: false,
+ showinfobar: false,
+ showsheetbar: true,
+}
+
+/** 销毁已有 Luckysheet 实例,避免重复 create 导致 DOM 堆积 */
+export function destroyLuckysheet() {
+ try {
+ if (typeof luckysheet !== 'undefined' && luckysheet.destroy) {
+ luckysheet.destroy()
+ }
+ } catch {
+ /* ignore */
+ }
+}
+
+/** 解析 sheet 数据、销毁旧实例并渲染报表 */
+export function renderLuckysheetReport(
+ container: string,
+ sheets: any[],
+ options: Record = {}
+) {
+ parseLuckysheetSheets(sheets)
+ destroyLuckysheet()
+ setTimeout(() => {
+ luckysheet.create({
+ container,
+ ...DEFAULT_REPORT_OPTIONS,
+ ...options,
+ data: sheets,
+ })
+ }, 10)
+}
+
+/** 安全导出 Luckysheet,无数据时提示并返回 false */
+export function exportLuckysheetFile(filename: string, hasData = true): boolean {
+ if (!hasData) {
+ ElMessage.warning('暂无数据')
+ return false
+ }
+ try {
+ if (typeof luckysheet === 'undefined' || !luckysheet.getAllSheets) {
+ ElMessage.warning('暂无数据')
+ return false
+ }
+ const sheets = luckysheet.getAllSheets()
+ if (!sheets?.length) {
+ ElMessage.warning('暂无数据')
+ return false
+ }
+ exportExcel(sheets, filename)
+ return true
+ } catch {
+ ElMessage.warning('导出失败,请先加载报表数据')
+ return false
+ }
+}
diff --git a/src/utils/storage.ts b/src/utils/storage.ts
index 1b4d3e5..9b52e35 100644
--- a/src/utils/storage.ts
+++ b/src/utils/storage.ts
@@ -1,45 +1,60 @@
-/**
- * window.localStorage
- * @method set 设置
- * @method get 获取
- * @method remove 移除
- * @method clear 移除全部
- */
-export const Local = {
- set(key: string, val: any) {
- window.localStorage.setItem(key, JSON.stringify(val))
- },
- get(key: string) {
- const json: any = window.localStorage.getItem(key)
- return JSON.parse(json)
- },
- remove(key: string) {
- window.localStorage.removeItem(key)
- },
- clear() {
- window.localStorage.clear()
- },
-}
-
-/**
- * window.sessionStorage
- * @method set 设置会话缓存
- * @method get 获取会话缓存
- * @method remove 移除会话缓存
- * @method clear 移除全部会话缓存
- */
-export const Session = {
- set(key: string, val: any) {
- window.sessionStorage.setItem(key, JSON.stringify(val))
- },
- get(key: string) {
- const json: any = window.sessionStorage.getItem(key)
- return JSON.parse(json)
- },
- remove(key: string) {
- window.sessionStorage.removeItem(key)
- },
- clear() {
- window.sessionStorage.clear()
- },
-}
+/**
+ * window.localStorage
+ * @method set 设置
+ * @method get 获取
+ * @method remove 移除
+ * @method clear 移除全部
+ */
+export const Local = {
+ set(key: string, val: any) {
+ window.localStorage.setItem(key, JSON.stringify(val))
+ },
+ get(key: string) {
+ const json: any = window.localStorage.getItem(key)
+ return JSON.parse(json)
+ },
+ remove(key: string) {
+ window.localStorage.removeItem(key)
+ },
+ clear() {
+ window.localStorage.clear()
+ },
+}
+
+/**
+ * window.sessionStorage
+ * @method set 设置会话缓存
+ * @method get 获取会话缓存
+ * @method remove 移除会话缓存
+ * @method clear 移除全部会话缓存
+ */
+const DEFAULT_THEME_NAME = '电能质量监测系统'
+
+export function getStoredTheme(): { name?: string; logoUrl?: string; [key: string]: any } {
+ try {
+ const raw = window.localStorage.getItem('getTheme')
+ return raw ? JSON.parse(raw) : {}
+ } catch {
+ return {}
+ }
+}
+
+export function getStoredThemeName(): string {
+ return getStoredTheme().name || DEFAULT_THEME_NAME
+}
+
+export const Session = {
+ set(key: string, val: any) {
+ window.sessionStorage.setItem(key, JSON.stringify(val))
+ },
+ get(key: string) {
+ const json: any = window.sessionStorage.getItem(key)
+ return JSON.parse(json)
+ },
+ remove(key: string) {
+ window.sessionStorage.removeItem(key)
+ },
+ clear() {
+ window.sessionStorage.clear()
+ },
+}
diff --git a/src/utils/tableStore.ts b/src/utils/tableStore.ts
index 633da77..1f284ea 100644
--- a/src/utils/tableStore.ts
+++ b/src/utils/tableStore.ts
@@ -19,7 +19,7 @@ interface TableStoreParams {
resetCallback?: () => void // 重置
loadCallback?: () => void // 接口调用后的回调
exportProcessingData?: () => void //导出处理数据
- beforeSearchFun?: () => void // 接口调用前的回调
+ beforeSearchFun?: () => void | boolean // 接口调用前的回调,返回 false 中止请求
}
export default class TableStore {
@@ -75,7 +75,13 @@ export default class TableStore {
}
index() {
- this.table.beforeSearchFun && this.table.beforeSearchFun()
+ if (this.table.beforeSearchFun) {
+ const canSearch = this.table.beforeSearchFun()
+ if (canSearch === false) {
+ this.table.loading = false
+ return
+ }
+ }
this.table.data = []
this.table.loading = true
// 重置用的数据数据
diff --git a/src/utils/waveCache.ts b/src/utils/waveCache.ts
new file mode 100644
index 0000000..d114d78
--- /dev/null
+++ b/src/utils/waveCache.ts
@@ -0,0 +1,42 @@
+const MAX_CACHE_SIZE = 30
+
+const cache = new Map()
+
+function dataFingerprint(data: unknown[][] | undefined): string {
+ if (!data?.length) return '0'
+ const first = data[0]
+ const last = data[data.length - 1]
+ return `${data.length}:${first?.[0]}:${last?.[0]}`
+}
+
+export function buildWaveCacheKey(
+ type: 'shu' | 'rms',
+ wp: Record | undefined,
+ value: number,
+ isOpen: boolean,
+ boxoList: Record
+): string {
+ if (!wp) return ''
+ const waveFp =
+ type === 'shu' ? dataFingerprint(wp.listWaveData) : dataFingerprint(wp.listRmsData)
+ const boxoFp = boxoList?.startTime ?? boxoList?.lineName ?? boxoList?.equipmentName ?? ''
+ return `${type}|${wp.time}|${wp.waveType}|${wp.iphasic}|${value}|${isOpen}|${waveFp}|${boxoFp}`
+}
+
+export function getWaveCache(key: string): T | null {
+ if (!key || !cache.has(key)) return null
+ const value = cache.get(key) as T
+ cache.delete(key)
+ cache.set(key, value)
+ return value
+}
+
+export function setWaveCache(key: string, value: unknown): void {
+ if (!key) return
+ if (cache.has(key)) cache.delete(key)
+ cache.set(key, value)
+ while (cache.size > MAX_CACHE_SIZE) {
+ const oldest = cache.keys().next().value
+ if (oldest !== undefined) cache.delete(oldest)
+ }
+}
diff --git a/src/utils/waveWorkerPool.ts b/src/utils/waveWorkerPool.ts
new file mode 100644
index 0000000..0f1dd32
--- /dev/null
+++ b/src/utils/waveWorkerPool.ts
@@ -0,0 +1,96 @@
+import { toRaw } from 'vue'
+
+type WorkerMessageHandler = (data: any) => void
+
+let shuWorker: Worker | null = null
+let rmsWorker: Worker | null = null
+
+/** 递归剥离 Vue 响应式代理,得到可 structuredClone 的纯对象 */
+export function toPlainDeep(value: T): T {
+ const raw = toRaw(value as object) as T
+ if (Array.isArray(raw)) {
+ return raw.map(item => toPlainDeep(item)) as T
+ }
+ if (raw !== null && typeof raw === 'object') {
+ const out: Record = {}
+ for (const [key, val] of Object.entries(raw)) {
+ out[key] = toPlainDeep(val)
+ }
+ return out as T
+ }
+ return raw
+}
+
+const BOXO_LIST_KEYS = [
+ 'systemType',
+ 'powerStationName',
+ 'measurementPointName',
+ 'startTime',
+ 'featureAmplitude',
+ 'duration',
+ 'engineeringName',
+ 'equipmentName',
+ 'evtParamVVaDepth',
+ 'evtParamTm',
+ 'lineName',
+ 'persistTime',
+ 'subName'
+] as const
+
+export function buildWorkerPayload(
+ type: 'shu' | 'rms',
+ wp: Record,
+ boxoList: Record,
+ extras: { requestId: number; value: number; isOpen: boolean; iphasic?: number }
+) {
+ const plainWp = toPlainDeep(wp)
+ const wpPayload: Record = {
+ pt: plainWp.pt,
+ ct: plainWp.ct,
+ waveTitle: plainWp.waveTitle,
+ iphasic: plainWp.iphasic,
+ time: plainWp.time,
+ waveType: plainWp.waveType,
+ yzd: plainWp.yzd
+ }
+ if (type === 'shu') {
+ wpPayload.listWaveData = plainWp.listWaveData
+ } else {
+ wpPayload.listRmsData = plainWp.listRmsData
+ }
+
+ const plainBoxo: Record = {}
+ const rawBoxo = toPlainDeep(boxoList)
+ for (const key of BOXO_LIST_KEYS) {
+ if (rawBoxo[key] !== undefined) {
+ plainBoxo[key] = rawBoxo[key]
+ }
+ }
+
+ return {
+ requestId: extras.requestId,
+ value: extras.value,
+ isOpen: extras.isOpen,
+ iphasic: extras.iphasic,
+ wp: wpPayload,
+ boxoList: plainBoxo
+ }
+}
+
+export function getShuWorker(onMessage: WorkerMessageHandler): Worker {
+ if (!shuWorker) {
+ shuWorker = new Worker(new URL('../components/echarts/shuWorker.js', import.meta.url))
+ }
+ shuWorker.onmessage = e => onMessage(e.data)
+ shuWorker.onerror = error => console.error('Shu worker error:', error)
+ return shuWorker
+}
+
+export function getRmsWorker(onMessage: WorkerMessageHandler): Worker {
+ if (!rmsWorker) {
+ rmsWorker = new Worker(new URL('../components/echarts/rmsWorker.js', import.meta.url))
+ }
+ rmsWorker.onmessage = e => onMessage(e.data)
+ rmsWorker.onerror = error => console.error('Rms worker error:', error)
+ return rmsWorker
+}
diff --git a/src/views/govern/alarm/Abnormal.vue b/src/views/govern/alarm/Abnormal.vue
index 3dbc541..881a5dd 100644
--- a/src/views/govern/alarm/Abnormal.vue
+++ b/src/views/govern/alarm/Abnormal.vue
@@ -92,8 +92,8 @@ const tableStore = new TableStore({
return (tableStore.table.params.pageNum - 1) * tableStore.table.params.pageSize + row.rowIndex + 1
}
},
- { title: '设备名称', field: 'ndid', align: 'center' },
{ title: '异常时间', field: 'evtTime', align: 'center', sortable: true },
+ { title: '设备名称', field: 'ndid', align: 'center' },
{
title: '告警代码',
field: 'code',
diff --git a/src/views/govern/alarm/Device.vue b/src/views/govern/alarm/Device.vue
index b797703..7591cec 100644
--- a/src/views/govern/alarm/Device.vue
+++ b/src/views/govern/alarm/Device.vue
@@ -99,16 +99,17 @@ const tableStore = new TableStore({
return (tableStore.table.params.pageNum - 1) * tableStore.table.params.pageSize + row.rowIndex + 1
}
},
- { title: '设备名称', field: 'equipmentName', align: 'center', width: 120 },
- { title: '监测点名称', field: 'lineName', align: 'center', width: 140 },
- { title: '工程名称', field: 'engineeringName', align: 'center', width: 120 },
- { title: '项目名称', field: 'projectName', align: 'center', width: 120 },
- { title: '发生时刻', field: 'startTime', align: 'center', width: 180, sortable: true },
+ { title: '发生时刻', field: 'startTime', align: 'center', minWidth: 180, sortable: true },
+ { title: '工程名称', field: 'engineeringName', align: 'center', minWidth: 120 },
+ { title: '项目名称', field: 'projectName', align: 'center', minWidth: 120 },
+ { title: '设备名称', field: 'equipmentName', align: 'center', minWidth: 120 },
+ { title: '监测点名称', field: 'lineName', align: 'center', minWidth: 120 },
+
{
title: '模块信息',
field: 'moduleNo',
align: 'center',
- width: 100,
+ minWidth: 100,
formatter: (row: any) => {
return row.cellValue ? row.cellValue : '/'
}
@@ -117,7 +118,7 @@ const tableStore = new TableStore({
title: '告警代码',
field: 'code',
align: 'center',
- width: 100,
+ minWidth: 100,
formatter: (row: any) => {
return row.cellValue ? '\u200B' + row.cellValue : '/'
},
@@ -125,13 +126,13 @@ const tableStore = new TableStore({
},
{
title: '事件描述',
- minWidth: 250,
+ minWidth: 300,
field: 'showName'
},
{
title: '级别',
field: 'level',
- width: 100,
+ width: 110,
render: 'tag',
custom: {
// 1:Ⅰ级 2:Ⅱ级 3:Ⅲ级 4:DEBUG 5:NORMAL 6:WARN 7:ERROR
@@ -202,7 +203,7 @@ tableStore.table.params.deviceTypeId = ''
tableStore.table.params.deviceTypeName = ''
const deviceTreeOptions = ref(props.deviceTree)
deviceTreeOptions.value.map((item: any, index: any) => {
- if (item.children.length == 0) {
+ if (item?.children.length == 0) {
deviceTreeOptions.value.splice(index, 1)
}
})
diff --git a/src/views/govern/alarm/Front.vue b/src/views/govern/alarm/Front.vue
index cd86550..b4d921f 100644
--- a/src/views/govern/alarm/Front.vue
+++ b/src/views/govern/alarm/Front.vue
@@ -78,10 +78,10 @@ const tableStore = new TableStore({
return (tableStore.table.params.pageNum - 1) * tableStore.table.params.pageSize + row.rowIndex + 1
}
},
- { title: '前置服务器名称', field: 'lineId', align: 'center', width: 120 },
- { title: '前置服务器ip', field: 'wavePath', align: 'center', width: 120 },
- { title: '进程号', field: 'clDid', align: 'center', width: 60 },
{ title: '发生时刻', field: 'startTime', align: 'center', width: 180, sortable: true },
+ { title: '前置服务器名称', field: 'lineId', align: 'center', width: 150 },
+ { title: '前置服务器ip', field: 'wavePath', align: 'center', width: 150 },
+ { title: '进程号', field: 'clDid', align: 'center', width: 70 },
{
title: '事件描述',
@@ -102,7 +102,7 @@ const tableStore = new TableStore({
{
title: '级别',
field: 'level',
- width: 100,
+ width: 110,
render: 'tag',
custom: {
// 1:Ⅰ级 2:Ⅱ级 3:Ⅲ级 4:DEBUG 5:NORMAL 6:WARN 7:ERROR
@@ -153,7 +153,7 @@ tableStore.table.params.searchValue = ''
tableStore.table.params.level = ''
const deviceTreeOptions = ref(props.deviceTree)
deviceTreeOptions.value.map((item: any, index: any) => {
- if (item.children.length == 0) {
+ if (item?.children.length == 0) {
deviceTreeOptions.value.splice(index, 1)
}
})
diff --git a/src/views/govern/alarm/Steady.vue b/src/views/govern/alarm/Steady.vue
index 8889ab7..724f2d1 100644
--- a/src/views/govern/alarm/Steady.vue
+++ b/src/views/govern/alarm/Steady.vue
@@ -2,16 +2,9 @@
-
+
+
@@ -585,7 +585,7 @@ import { ElMessage } from 'element-plus'
import DatePicker from '@/components/form/datePicker/index.vue'
import Trend from './tabs/trend.vue' //趋势数据
import realTime from './tabs/realtime.vue' //实时数据-主界面
-import electroplating from './tabs/electroplating.vue' //电镀数据-主界面
+import electroplating from './tabs/electroplating.vue' //电度数据-主界面
import realTrend from './tabs/components/realtrend.vue' //实时数据-实时趋势
import operatingTrend from './tabs/operatingTrend.vue' //运行趋势
import harmonicSpectrum from './tabs/components/harmonicSpectrum.vue' //实时数据-谐波频谱子页面
@@ -705,6 +705,25 @@ const activeTrendName: any = ref(0)
const trendTimer: any = ref()
const trendDataTime: any = ref()
const showButton = ref(false)
+
+const decodeMqttPayload = (message: any) => {
+ try {
+ return JSON.parse(JSON.stringify(JSON.parse(new TextDecoder().decode(message))))
+ } catch {
+ return {}
+ }
+}
+
+/** 谐波频谱 MQTT 消息(命名函数,便于 off 避免重复注册) */
+const onMqttTrendMessage = (topic: any, message: any) => {
+ const obj = decodeMqttPayload(message) || {}
+ if ((obj.hasOwnProperty('data1') || obj.hasOwnProperty('data2')) && obj.dataTime) {
+ trendDataTime.value = obj.dataTime
+ realTrendRef.value?.setRealTrendData(obj)
+ tableLoading.value = false
+ }
+}
+
//谐波频谱方法
const handleTrend = async () => {
realTimeFlag.value = false
@@ -728,21 +747,7 @@ const handleTrend = async () => {
// console.log(res, '获取谐波频谱数据')
})
}, 30000)
- mqttRef.value.on('message', (topic: any, message: any) => {
- let obj = JSON.parse(JSON.stringify(JSON.parse(new TextDecoder().decode(message)))) || {}
- if ((obj.hasOwnProperty('data1') || obj.hasOwnProperty('data2')) && obj.dataTime) {
- trendDataTime.value = obj.dataTime
- realTrendRef.value && realTrendRef.value.setRealTrendData(obj)
- tableLoading.value = false
- // console.log(
- // '谐波频谱---mqtt接收到消息',
- // JSON.parse(JSON.stringify(JSON.parse(new TextDecoder().decode(message))))
- // )
- }
- // else {
- // trendDataTime.value = obj.dataTime
- // }
- })
+ bindMqttMessage(onMqttTrendMessage)
} else {
ElMessage.warning('设备应答失败')
}
@@ -885,8 +890,8 @@ const lineId: any = ref('')
const dataLevel: any = ref('')
const dataSource = ref([])
const engineeringName = ref('')
-const nodeClick = async (e: anyObj, node: any) => {
- if (e == undefined || e.level == 2) {
+const nodeClick = async (e: anyObj, node?: any) => {
+ if (e == undefined) {
return (loading.value = false)
}
searchValue.value = ''
@@ -900,7 +905,7 @@ const nodeClick = async (e: anyObj, node: any) => {
}
//选中设备名称后,点击标签页也能查询数据,要求点击设备名称后,点击标签页默认查询第一个监测点数据
- if (e.level == 3 || e.level == 2) {
+ if (e.level == 3 ) {
engineeringName.value = node?.parent.parent.data.name
await queryDictType({
@@ -940,7 +945,7 @@ const nodeClick = async (e: anyObj, node: any) => {
if (item.type === 'trenddata') {
item.id = item.id + '_trenddata'
}
- //电镀数据
+ //电度数据
if (item.type === 'kilowattHour') {
item.id = item.id + '_kilowattHour'
}
@@ -995,6 +1000,14 @@ const trendRef: any = ref()
const eventRef: any = ref()
const mqttRef = ref()
const url: any = window.localStorage.getItem('MQTTURL')
+
+/** 同一 handler 先 off 再 on,避免重复 message 监听 */
+const bindMqttMessage = (handler: (topic: any, message: any) => void) => {
+ if (!mqttRef.value) return
+ mqttRef.value.off('message', handler)
+ mqttRef.value.on('message', handler)
+}
+
const connectMqtt = () => {
if (mqttRef.value) {
if (mqttRef.value.connected) {
@@ -1035,16 +1048,44 @@ const getRealDataMqttMsg = async () => {
// console.log(res, '获取基础实时数据')
})
}, 30000)
- mqttRef.value.on('message', (topic: any, message: any) => {
- // console.log(
- // '实时数据&实时趋势---mqtt接收到消息',
- // JSON.parse(JSON.stringify(JSON.parse(new TextDecoder().decode(message))))
- // )
- let obj = JSON.parse(JSON.stringify(JSON.parse(new TextDecoder().decode(message))))
+ bindMqttMessage(onMqttRealDataMessage)
+ //2.建立mqtt通讯
+ //每隔30s调用一下接口,通知后台推送mqtt消息
- if (lineId.value != obj.lineId || adminInfo.userIndex != obj.userId) return
+ mqttRef.value.on('error', (error: any) => {
+ console.log('mqtt连接失败...', error)
+ mqttRef.value.end()
+ })
- //处理mqtt数据 1转2除 2转1乘
+ mqttRef.value.on('close', function () {
+ console.log('mqtt客户端已断开连接.....')
+ })
+ setTimeout(() => {
+ tableLoading.value = false
+ }, 6000)
+ } else {
+ ElMessage.success('设备应答失败')
+ tableLoading.value = false
+ }
+ })
+ .catch(e => {
+ setTimeout(() => {
+ tableLoading.value = false
+ }, 0)
+ })
+}
+//tab点击事件
+
+const realDataTimer: any = ref()
+const mqttMessage = ref({})
+
+/** 实时数据 / 实时趋势 MQTT 消息(命名函数,便于 off 避免重复注册) */
+const onMqttRealDataMessage = (topic: any, message: any) => {
+ let obj = decodeMqttPayload(message)
+
+ if (lineId.value != obj.lineId || adminInfo.userIndex != obj.userId) return
+
+ //处理mqtt数据 1转2除 2转1乘
//如果消息返回值是二次值,下拉框是二次值只需要单位换算 除以1000
//如果消息返回值是一次值,下拉框是一次值只需要单位换算 除以1000
if (obj.dataLevel == formInline.dataLevel) {
@@ -1178,36 +1219,8 @@ const getRealDataMqttMsg = async () => {
// sonTab.value == 1 &&
// realTrendRef.value &&
// realTrendRef.value.setRealTrendData(obj)
- })
- //2.建立mqtt通讯
- //每隔30s调用一下接口,通知后台推送mqtt消息
-
- mqttRef.value.on('error', (error: any) => {
- console.log('mqtt连接失败...', error)
- mqttRef.value.end()
- })
-
- mqttRef.value.on('close', function () {
- console.log('mqtt客户端已断开连接.....')
- })
- setTimeout(() => {
- tableLoading.value = false
- }, 6000)
- } else {
- ElMessage.success('设备应答失败')
- tableLoading.value = false
- }
- })
- .catch(e => {
- setTimeout(() => {
- tableLoading.value = false
- }, 0)
- })
}
-//tab点击事件
-const realDataTimer: any = ref()
-const mqttMessage = ref({})
const handleClick = async (tab?: any) => {
tableLoading.value = true
showButton.value = false
@@ -1272,7 +1285,7 @@ const handleClick = async (tab?: any) => {
tableLoading.value = false
}, 0)
}
- //电镀数据
+ //电度数据
if (dataSet.value.includes('_kilowattHour')) {
let obj = {
devId: deviceId.value, //e.id
@@ -1475,6 +1488,8 @@ const handleClick = async (tab?: any) => {
window.clearInterval(trendTimer.value)
}
if (mqttRef.value) {
+ mqttRef.value.off('message', onMqttTrendMessage)
+ mqttRef.value.off('message', onMqttRealDataMessage)
mqttRef.value.end()
}
}
@@ -1557,6 +1572,8 @@ onBeforeUnmount(() => {
realDataTimer.value = 0
trendTimer.value = 0
if (mqttRef.value) {
+ mqttRef.value.off('message', onMqttTrendMessage)
+ mqttRef.value.off('message', onMqttRealDataMessage)
mqttRef.value.end()
}
})
diff --git a/src/views/govern/device/control/tabs/components/realtrend.vue b/src/views/govern/device/control/tabs/components/realtrend.vue
index 0eb72dc..521466a 100644
--- a/src/views/govern/device/control/tabs/components/realtrend.vue
+++ b/src/views/govern/device/control/tabs/components/realtrend.vue
@@ -293,31 +293,31 @@ const setRealTrendData = (val: any) => {
if (selectValue.value == '2') {
if (activeName.value == 2) {
if (numberPart % 2 !== 0 && numberPart < 17) {
- tableData.value[key] = val[key]
+ tableData.value[key] = val[key].toFixed(2)
}
} else {
if (numberPart % 2 === 0) {
- tableData.value[key] = val[key]
+ tableData.value[key] = val[key].toFixed(2)
}
}
} else {
if (activeName.value == 2) {
if (numberPart % 2 === 0 && numberPart < 17) {
- tableData.value[key] = val[key]
+ tableData.value[key] = val[key].toFixed(2)
}
} else {
if (numberPart % 2 !== 0) {
- tableData.value[key] = val[key]
+ tableData.value[key] = val[key].toFixed(2)
}
}
}
} else {
if (activeName.value == 2) {
if (numberPart < 17) {
- tableData.value[key] = val[key]
+ tableData.value[key] = val[key].toFixed(2)
}
} else {
- tableData.value[key] = val[key]
+ tableData.value[key] = val[key].toFixed(2)
}
}
}
diff --git a/src/views/govern/device/control/tabs/components/waveFormAnalysis.vue b/src/views/govern/device/control/tabs/components/waveFormAnalysis.vue
index d7d6adc..aeb009e 100644
--- a/src/views/govern/device/control/tabs/components/waveFormAnalysis.vue
+++ b/src/views/govern/device/control/tabs/components/waveFormAnalysis.vue
@@ -20,25 +20,15 @@
返回
-
-