修改设备及能源概览

main
liutao 2 days ago
parent ba0cfccff9
commit 44ca9e458c

@ -33,10 +33,11 @@
:model-value="modelValue.timeRange"
type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss"
:disabled-date="disabledFutureDate"
:start-placeholder="t('DataCollection.RunOverview.timeRangeStartPlaceholder')"
:end-placeholder="t('DataCollection.RunOverview.timeRangeEndPlaceholder')"
class="!w-360px"
@update:model-value="updateField('timeRange', $event)"
@update:model-value="handleTimeRangeChange"
/>
</el-form-item>
<el-form-item class="run-overview-filter__actions">
@ -68,6 +69,7 @@
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import type {
OrganizationTreeOption,
OverviewOption,
@ -91,10 +93,12 @@ const emit = defineEmits<{
}>()
const { t } = useI18n()
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
const treeProps = {
value: 'id',
label: 'name',
children: 'children'
children: 'children',
disabled: 'disabled'
}
const updateField = <K extends keyof RunOverviewQueryParams>(key: K, value: RunOverviewQueryParams[K]) => {
@ -104,6 +108,28 @@ const updateField = <K extends keyof RunOverviewQueryParams>(key: K, value: RunO
})
}
const disabledFutureDate = (date: Date) => dayjs(date).startOf('day').isAfter(dayjs().startOf('day'))
const normalizeDateTime = (value: unknown, fallback: string) => {
const parsed = dayjs(value)
if (!parsed.isValid()) return fallback
const now = dayjs()
return (parsed.isAfter(now) ? now : parsed).format(DATE_TIME_FORMAT)
}
const normalizeTimeRange = (value: unknown): [string, string] => {
if (!Array.isArray(value) || value.length < 2) return props.modelValue.timeRange
const [fallbackStart, fallbackEnd] = props.modelValue.timeRange
let start = normalizeDateTime(value[0], fallbackStart)
let end = normalizeDateTime(value[1], fallbackEnd)
if (dayjs(start).isAfter(dayjs(end))) start = end
return [start, end]
}
const handleTimeRangeChange = (value: unknown) => {
updateField('timeRange', normalizeTimeRange(value))
}
const handleGroupChange = (value: string | number | undefined) => {
const nextGroupId = value == null ? '' : String(value)
emit('update:modelValue', {

@ -1,4 +1,4 @@
<template>
<template>
<ContentWrap class="chart-panel">
<div class="chart-panel__header">
<div class="chart-panel__title">
@ -42,6 +42,7 @@
</template>
<script setup lang="ts">
import dayjs, { type Dayjs } from 'dayjs'
import { computed } from 'vue'
import type { EChartsOption } from 'echarts'
import { Echart as EchartChart } from '@/components/Echart'
@ -53,6 +54,8 @@ const props = defineProps<{
hourlyStatus: HourlyStatusItem[]
summary: StatusSummaryItem[]
summaryTotalHours?: number
statisticsStart: string
statisticsEnd: string
}>()
const { t } = useI18n()
@ -71,6 +74,80 @@ const statusLabelMap = computed<Record<RunStatus, string>>(() => ({
offline: t('DataCollection.RunOverview.legend.offline')
}))
const parseDateTime = (value?: string) => {
const parsed = dayjs(value)
return parsed.isValid() ? parsed : undefined
}
const buildTimeInDay = (base: Dayjs, hour: number, minute = 0, second = 0) =>
base.hour(hour).minute(minute).second(second).millisecond(0)
const isHourBucketInRange = (time: Dayjs, rangeStart: Dayjs, rangeEnd: Dayjs) => {
if (!time?.isValid?.()) return false
const bucketStart = time.startOf('hour')
return !bucketStart.isBefore(rangeStart.startOf('hour')) && !bucketStart.isAfter(rangeEnd.startOf('hour'))
}
const getHourTime = (hour: string, rangeStart: Dayjs, rangeEnd: Dayjs) => {
const text = String(hour || '').trim()
if (!text) return undefined
if (/\d{4}[-/]\d{1,2}[-/]\d{1,2}/.test(text)) {
const parsed = dayjs(text)
if (parsed.isValid()) return parsed
}
const monthDayMatch = text.match(/(?:^|\D)(\d{1,2})[-/](\d{1,2})\s+(\d{1,2}):(\d{2})(?::(\d{2}))?/)
if (monthDayMatch) {
const [, month, date, hourText, minuteText, secondText] = monthDayMatch
const candidates = [rangeStart.year(), rangeEnd.year()].map((year) =>
dayjs(
`${year}-${month.padStart(2, '0')}-${date.padStart(2, '0')} ${hourText.padStart(2, '0')}:${minuteText}:${secondText || '00'}`
)
)
return candidates.find((item) => isHourBucketInRange(item, rangeStart, rangeEnd)) || candidates[0]
}
const hourMatch =
text.match(/^(\d{1,2})(?::(\d{2}))?(?::(\d{2}))?$/) ||
text.match(/(\d{1,2}):(\d{2})(?::(\d{2}))?/) ||
text.match(/(\d{1,2})\s*(?:\u65f6|\u70b9)/)
if (!hourMatch) return undefined
const hourNumber = Number(hourMatch[1])
const minuteNumber = Number(hourMatch[2] || 0)
const secondNumber = Number(hourMatch[3] || 0)
if (
!Number.isInteger(hourNumber) ||
hourNumber < 0 ||
hourNumber > 23 ||
!Number.isInteger(minuteNumber) ||
minuteNumber < 0 ||
minuteNumber > 59
) {
return undefined
}
if (rangeStart.isSame(rangeEnd, 'day')) {
return buildTimeInDay(rangeStart, hourNumber, minuteNumber, secondNumber)
}
const startCandidate = buildTimeInDay(rangeStart, hourNumber, minuteNumber, secondNumber)
const endCandidate = buildTimeInDay(rangeEnd, hourNumber, minuteNumber, secondNumber)
return [startCandidate, endCandidate].find((item) => isHourBucketInRange(item, rangeStart, rangeEnd)) || startCandidate
}
const chartHourlyStatus = computed(() => {
const rangeStart = parseDateTime(props.statisticsStart)
const rangeEnd = parseDateTime(props.statisticsEnd)
if (!rangeStart || !rangeEnd || rangeStart.isAfter(rangeEnd)) return props.hourlyStatus
return props.hourlyStatus.filter((item) => {
const hourTime = getHourTime(item.hour, rangeStart, rangeEnd)
return hourTime ? isHourBucketInRange(hourTime, rangeStart, rangeEnd) : false
})
})
const barOption = computed<EChartsOption>(() => ({
color: [
statusColors.running,
@ -88,7 +165,7 @@ const barOption = computed<EChartsOption>(() => ({
grid: { left: 48, right: 18, top: 46, bottom: 36 },
xAxis: {
type: 'category',
data: props.hourlyStatus.map((item) => item.hour),
data: chartHourlyStatus.value.map((item) => item.hour),
axisTick: { show: false },
axisLine: { lineStyle: { color: '#d0d5dd' } },
axisLabel: { color: '#667085', fontSize: 11 }
@ -107,28 +184,28 @@ const barOption = computed<EChartsOption>(() => ({
type: 'bar',
stack: 'status',
barWidth: 18,
data: props.hourlyStatus.map((item) => item.running)
data: chartHourlyStatus.value.map((item) => item.running)
},
{
name: statusLabelMap.value.standby,
type: 'bar',
stack: 'status',
barWidth: 18,
data: props.hourlyStatus.map((item) => item.standby)
data: chartHourlyStatus.value.map((item) => item.standby)
},
{
name: statusLabelMap.value.fault,
type: 'bar',
stack: 'status',
barWidth: 18,
data: props.hourlyStatus.map((item) => item.fault)
data: chartHourlyStatus.value.map((item) => item.fault)
},
{
name: statusLabelMap.value.offline,
type: 'bar',
stack: 'status',
barWidth: 18,
data: props.hourlyStatus.map((item) => item.offline)
data: chartHourlyStatus.value.map((item) => item.offline)
}
]
}))

@ -20,6 +20,12 @@ export interface OrganizationTreeOption {
name: string
parentId?: number | string | null
dvId?: number | string | null
lineId?: number | string | null
filterId?: number | string | null
deviceId?: number | string | null
paramId?: number | string | null
nodeType?: 'org' | 'equipment' | 'param'
disabled?: boolean
children?: OrganizationTreeOption[]
}

@ -34,6 +34,8 @@
:hourly-status="overviewData.hourlyStatus"
:summary="overviewData.summary"
:summary-total-hours="overviewData.summaryTotalHours"
:statistics-start="queryParams.timeRange[0]"
:statistics-end="queryParams.timeRange[1]"
/>
<OperationTimelineChart
@ -54,7 +56,9 @@ import dayjs from 'dayjs'
import { useFullscreen } from '@vueuse/core'
import { DeviceOperationOverviewApi } from '@/api/iot/deviceOperationOverview'
import { DeviceLedgerApi, DeviceLedgerVO } from '@/api/mes/deviceledger'
import { DeviceLineApi, DeviceLineTreeVO } from '@/api/mes/deviceline'
import { OrganizationApi } from '@/api/mes/organization'
import type { DeviceParameterAnalysisNodeVO } from '@/api/mes/organization'
import { handleTree } from '@/utils/tree'
import OverviewFilterBar from './components/OverviewFilterBar.vue'
import OverviewMetricCards from './components/OverviewMetricCards.vue'
import OperationTimelineChart from './components/OperationTimelineChart.vue'
@ -71,7 +75,6 @@ import type {
defineOptions({ name: 'IotRunOverview' })
const { t } = useI18n()
const message = useMessage()
const fullscreenTargetRef = ref()
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen(fullscreenTargetRef)
const deviceLedgerList = ref<DeviceLedgerVO[]>([])
@ -80,8 +83,9 @@ let deviceLedgerRequestSeq = 0
const createDateRangeByQuickKey = (key: QuickRangeKey): [string, string] => {
const format = 'YYYY-MM-DD HH:mm:ss'
const now = dayjs()
if (key === 'today')
return [dayjs().startOf('day').format(format), dayjs().endOf('day').format(format)]
return [now.startOf('day').format(format), now.format(format)]
if (key === 'yesterday') {
return [
dayjs().subtract(1, 'day').startOf('day').format(format),
@ -90,16 +94,16 @@ const createDateRangeByQuickKey = (key: QuickRangeKey): [string, string] => {
}
if (key === 'last7Days')
return [
dayjs().subtract(6, 'day').startOf('day').format(format),
dayjs().endOf('day').format(format)
now.subtract(6, 'day').startOf('day').format(format),
now.format(format)
]
if (key === 'last30Days') {
return [
dayjs().subtract(29, 'day').startOf('day').format(format),
dayjs().endOf('day').format(format)
now.subtract(29, 'day').startOf('day').format(format),
now.format(format)
]
}
return [dayjs().startOf('day').format(format), dayjs().endOf('day').format(format)]
return [now.startOf('day').format(format), now.format(format)]
}
const quickRanges = computed(() => [
@ -114,26 +118,137 @@ const queryParams = ref<RunOverviewQueryParams>(buildDefaultQueryParams())
const pageNo = ref(1)
const pageSize = ref(10)
const normalizeDeviceLineTree = (nodes: DeviceLineTreeVO[] = []): OrganizationTreeOption[] =>
nodes.map((node) => ({
id: String(node.id),
name: node.name,
parentId: node.parentId != null ? String(node.parentId) : node.parentId,
children: Array.isArray(node.children) ? normalizeDeviceLineTree(node.children) : undefined
}))
const extractDeviceParameterAnalysisNodes = (res: any): DeviceParameterAnalysisNodeVO[] => {
if (Array.isArray(res)) return res
if (Array.isArray(res?.data)) return res.data
if (Array.isArray(res?.result)) return res.result
if (Array.isArray(res?.list)) return res.list
return []
}
const normalizeDeviceParameterAnalysisTree = (
nodes: DeviceParameterAnalysisNodeVO[] = []
): OrganizationTreeOption[] => {
const normalizeNode = (node: DeviceParameterAnalysisNodeVO): any => {
const rawId = Number(node?.id) || node?.id
const parentId =
node?.parentId == null || node?.parentId === ''
? 0
: typeof node.parentId === 'number'
? node.parentId
: Number(node.parentId) || 0
return {
...node,
id: rawId,
name: String(node?.name ?? ''),
parentId,
children: Array.isArray(node?.children) ? node.children.map(normalizeNode) : undefined
}
}
const normalizedNodes = Array.isArray(nodes) ? nodes.map(normalizeNode) : []
const isNestedTree = normalizedNodes.some(
(node) => Array.isArray(node?.children) && node.children.length > 0
)
const orgTree = isNestedTree
? normalizedNodes
: (handleTree(normalizedNodes, 'id', 'parentId', 'children') as any[])
const toOption = (node: any): OrganizationTreeOption => {
const orgChildren = Array.isArray(node.children) ? node.children.map(toOption) : []
const equipmentChildren = Array.isArray(node.equipments)
? node.equipments.map((equipment: any) => {
const parameterChildren = Array.isArray(equipment.parameters)
? equipment.parameters.map((param: any) => ({
id: `param-${equipment.id}-${param.id}`,
name: String(param?.name ?? param?.code ?? param?.id ?? ''),
parentId: `equipment-${node.id}-${equipment.id}`,
lineId: String(node.id),
filterId: String(param.id),
deviceId: String(equipment.id),
paramId: String(param.id),
nodeType: 'param' as const
}))
: []
return {
id: `equipment-${node.id}-${equipment.id}`,
name: String(equipment?.name ?? equipment?.id ?? ''),
parentId: `org-${node.id}`,
lineId: String(node.id),
filterId: String(equipment.id),
deviceId: String(equipment.id),
nodeType: 'equipment' as const,
children: parameterChildren.length ? parameterChildren : undefined
}
})
: []
const children = [...orgChildren, ...equipmentChildren]
return {
id: `org-${node.id}`,
name: node.name,
parentId: node.parentId != null && Number(node.parentId) > 0 ? `org-${node.parentId}` : node.parentId,
lineId: String(node.id),
filterId: String(node.id),
nodeType: 'org',
children: children.length ? children : undefined
}
}
return Array.isArray(orgTree) ? orgTree.map(toOption) : []
}
const findGroupNode = (groupId?: string): OrganizationTreeOption | undefined => {
if (!groupId) return undefined
const stack = [...groupOptions.value]
while (stack.length > 0) {
const node = stack.pop()!
if (String(node.id) === String(groupId)) return node
if (Array.isArray(node.children)) stack.push(...node.children)
}
return undefined
}
const getGroupLineId = (groupId?: string) => {
if (!groupId) return undefined
const node = findGroupNode(groupId)
if (node?.nodeType === 'org' && node.filterId != null) return String(node.filterId)
const matched = /^org-(.+)$/.exec(String(groupId))
return matched?.[1] || groupId
}
const getDeviceLedgerRequestParams = (groupId?: string) => {
const node = findGroupNode(groupId)
if (!node) return groupId ? { deviceLine: getGroupLineId(groupId) } : {}
if (node.nodeType === 'equipment' && node.filterId != null) return { dvId: node.filterId }
if (node.nodeType === 'param' && node.deviceId != null) return { dvId: node.deviceId }
if (node.filterId != null) return { deviceLine: node.filterId }
return {}
}
const collectGroupIds = (groupId?: string) => {
if (!groupId) return undefined
const selectedNode = findGroupNode(groupId)
if (selectedNode?.nodeType === 'equipment' && selectedNode.filterId != null) {
return new Set([String(selectedNode.filterId)])
}
if (selectedNode?.nodeType === 'param' && selectedNode.deviceId != null) {
return new Set([String(selectedNode.deviceId)])
}
const ids = new Set<string>()
const lineId = getGroupLineId(groupId)
const stack = [...groupOptions.value]
while (stack.length > 0) {
const node = stack.pop()!
if (String(node.id) === groupId) {
if (String(node.id) === groupId || String(node.filterId ?? '') === lineId) {
const collectStack = [node]
while (collectStack.length > 0) {
const current = collectStack.pop()!
ids.add(String(current.id))
if (current.nodeType === 'org' && current.filterId != null) ids.add(String(current.filterId))
if (Array.isArray(current.children)) collectStack.push(...current.children)
}
break
@ -155,8 +270,15 @@ const getDeviceOptionLabel = (item: DeviceLedgerVO) => {
const collectDeviceOptionsByGroup = (groupId?: string) => {
const deviceMap = new Map<string, OverviewOption>()
const groupIds = collectGroupIds(groupId)
const selectedNode = findGroupNode(groupId)
deviceLedgerList.value.forEach((item) => {
if (groupIds && !groupIds.has(String(item.deviceLine ?? ''))) return
if (groupIds) {
const targetId =
selectedNode?.nodeType === 'equipment' || selectedNode?.nodeType === 'param'
? String(item.dvId ?? '')
: String(item.deviceLine ?? '')
if (!groupIds.has(targetId)) return
}
const deviceId = String(item.dvId ?? '').trim()
if (!deviceId || deviceMap.has(deviceId)) return
deviceMap.set(deviceId, {
@ -182,13 +304,47 @@ const currentDeviceIds = computed(() =>
: deviceOptions.value.map((item) => item.value)
)
const buildOverviewRequestParams = () => ({
ids: currentDeviceIds.value.join(','),
startTime: queryParams.value.timeRange[0],
endTime: queryParams.value.timeRange[1],
timelinePageNo: pageNo.value,
timelinePageSize: pageSize.value
})
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
const normalizeDateTime = (value: string, fallback: string) => {
const parsed = dayjs(value)
if (!parsed.isValid()) return fallback
const now = dayjs()
return (parsed.isAfter(now) ? now : parsed).format(DATE_TIME_FORMAT)
}
const normalizeTimeRange = (timeRange: [string, string]): [string, string] => {
const nowText = dayjs().format(DATE_TIME_FORMAT)
let start = normalizeDateTime(timeRange?.[0], dayjs().startOf('day').format(DATE_TIME_FORMAT))
let end = normalizeDateTime(timeRange?.[1], nowText)
if (dayjs(start).isAfter(dayjs(end))) start = end
return [start, end]
}
const syncSafeTimeRange = () => {
const safeTimeRange = normalizeTimeRange(queryParams.value.timeRange)
if (
safeTimeRange[0] !== queryParams.value.timeRange[0] ||
safeTimeRange[1] !== queryParams.value.timeRange[1]
) {
queryParams.value = {
...queryParams.value,
timeRange: safeTimeRange
}
}
return safeTimeRange
}
const buildOverviewRequestParams = () => {
const [startTime, endTime] = syncSafeTimeRange()
return {
ids: currentDeviceIds.value.join(','),
startTime,
endTime,
timelinePageNo: pageNo.value,
timelinePageSize: pageSize.value
}
}
const refreshData = async () => {
const response = await DeviceOperationOverviewApi.getRunOverview(buildOverviewRequestParams())
@ -231,10 +387,6 @@ const resetQuery = () => {
resetToFirstPageAndRefresh()
}
const handleExport = () => {
message.success(t('DataCollection.RunOverview.exportPlaceholderMessage'))
}
const handlePageSizeChange = (size: number) => {
pageSize.value = size
pageNo.value = 1
@ -247,16 +399,20 @@ watch(pageNo, () => {
const getDeviceLedgerOptions = async (groupId?: string) => {
const requestSeq = ++deviceLedgerRequestSeq
const ledgerList = await DeviceLedgerApi.getDeviceLedgerList({
deviceLine: groupId || undefined
})
const ledgerList = await DeviceLedgerApi.getDeviceLedgerList(getDeviceLedgerRequestParams(groupId))
if (requestSeq !== deviceLedgerRequestSeq) return
deviceLedgerList.value = Array.isArray(ledgerList) ? ledgerList.filter(hasIotDeviceId) : []
}
const getFilterOptions = async () => {
const deviceLineTree = await DeviceLineApi.getDeviceLineTree({ filterNoDeviceLine: 1 })
groupOptions.value = normalizeDeviceLineTree(Array.isArray(deviceLineTree) ? deviceLineTree : [])
const deviceLineTree = await OrganizationApi.deviceParameterAnalysis({
keyword: undefined,
showDevices: 1,
filterNoDeviceLine: 1
})
groupOptions.value = normalizeDeviceParameterAnalysisTree(
extractDeviceParameterAnalysisNodes(deviceLineTree)
)
await getDeviceLedgerOptions(queryParams.value.groupId)
}

@ -5,7 +5,7 @@ const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
export const buildDefaultQueryParams = (): RunOverviewQueryParams => {
const start = dayjs().startOf('day')
const end = dayjs().endOf('day')
const end = dayjs()
return {
groupId: '',
deviceId: [],

@ -9,15 +9,17 @@
label-width="auto"
>
<el-form-item :label="t('EnergyOverview.filters.org')" prop="orgId">
<el-select
<el-tree-select
v-model="queryParams.orgId"
:data="orgOptions"
:props="orgTreeProps"
check-strictly
clearable
filterable
default-expand-all
:placeholder="t('EnergyOverview.filters.orgPlaceholder')"
class="!w-220px"
>
<el-option v-for="item in orgOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
/>
</el-form-item>
<el-form-item :label="t('EnergyOverview.filters.energyType')" prop="energyTypeId">
<el-select
@ -31,15 +33,20 @@
</el-form-item>
<el-form-item :label="t('EnergyOverview.filters.timeRange')" prop="timeRange">
<el-date-picker
v-model="queryParams.timeRange"
:model-value="queryParams.timeRange"
type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm:ss"
:disabled-date="disabledFutureDate"
:disabled-hours="disabledFutureHours"
:disabled-minutes="disabledFutureMinutes"
:disabled-seconds="disabledFutureSeconds"
:start-placeholder="t('common.startTimeText')"
:end-placeholder="t('common.endTimeText')"
range-separator="~"
:default-time="[new Date('2000-01-01 00:00:00'), new Date('2000-01-01 23:59:59')]"
:default-time="datePickerDefaultTime"
class="!w-420px"
@update:model-value="handleTimeRangeChange"
/>
</el-form-item>
<el-form-item>
@ -92,7 +99,7 @@
<Icon icon="ep:info-filled" class="panel-info" />
</div>
</div>
<Echart :height="300" :options="trendChartOptions" />
<Echart :height="300" :options="trendChartDisplayOptions" />
</ContentWrap>
</el-col>
<el-col :xl="12" :lg="12" :md="24" :sm="24" :xs="24">
@ -103,7 +110,7 @@
<Icon icon="ep:info-filled" class="panel-info" />
</div>
</div>
<Echart :height="300" :options="regionPieOptions" />
<Echart :height="300" :options="regionPieDisplayOptions" />
</ContentWrap>
</el-col>
</el-row>
@ -183,7 +190,9 @@ import {
EnergyOverviewRankVO,
EnergyOverviewRespVO
} from '@/api/mes/energydevice'
import { OrganizationApi, OrganizationVO } from '@/api/mes/organization'
import { OrganizationApi } from '@/api/mes/organization'
import type { DeviceParameterAnalysisNodeVO } from '@/api/mes/organization'
import { handleTree } from '@/utils/tree'
defineOptions({ name: 'EnergyOverview' })
@ -202,16 +211,41 @@ interface MetricCard {
type DetailRow = EnergyOverviewDetailVO
interface OrganizationTreeOption {
id: number | string
name: string
parentId?: number | string | null
lineId?: number | string | null
filterId?: number | string | null
deviceId?: number | string | null
paramId?: number | string | null
nodeType?: 'org' | 'equipment' | 'param'
disabled?: boolean
children?: OrganizationTreeOption[]
}
const { t } = useI18n()
const message = useMessage()
const padTimeUnit = (value: number) => String(value).padStart(2, '0')
const formatDateTime = (date: Date) => {
const year = date.getFullYear()
const month = padTimeUnit(date.getMonth() + 1)
const day = padTimeUnit(date.getDate())
const hour = padTimeUnit(date.getHours())
const minute = padTimeUnit(date.getMinutes())
const second = padTimeUnit(date.getSeconds())
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
}
const getTodayTimeRange = () => {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const month = padTimeUnit(now.getMonth() + 1)
const day = padTimeUnit(now.getDate())
const date = `${year}-${month}-${day}`
return [`${date} 00:00:00`, `${date} 23:59:59`]
return [`${date} 00:00:00`, formatDateTime(now)]
}
const parseRangeTime = (value?: string) => {
@ -232,8 +266,90 @@ const isSingleDayRange = (timeRange?: string[]) => {
)
}
const isSameDay = (left: Date, right: Date) =>
left.getFullYear() === right.getFullYear() &&
left.getMonth() === right.getMonth() &&
left.getDate() === right.getDate()
const disabledFutureDate = (date: Date) => {
const today = new Date()
today.setHours(0, 0, 0, 0)
return date.getTime() > today.getTime()
}
const getComparingDate = (value?: unknown) => {
if (!value) return undefined
if (value instanceof Date) return value
const maybeDate = value as { toDate?: () => Date; valueOf?: () => number }
if (typeof maybeDate.toDate === 'function') {
const date = maybeDate.toDate()
return Number.isNaN(date.getTime()) ? undefined : date
}
if (typeof maybeDate.valueOf === 'function') {
const date = new Date(maybeDate.valueOf())
return Number.isNaN(date.getTime()) ? undefined : date
}
return undefined
}
const getDisabledFutureUnits = (max: number, current: number) =>
Array.from({ length: max + 1 }, (_, index) => index).filter((value) => value > current)
const shouldDisableFutureTime = (roleOrDate?: unknown, comparingDate?: unknown) => {
const role = typeof roleOrDate === 'string' ? roleOrDate : undefined
if (role && role !== 'end' && role !== 'start') return false
const date = getComparingDate(role ? comparingDate : roleOrDate)
return !!date && isSameDay(date, new Date())
}
const disabledFutureHours = (roleOrDate?: unknown, comparingDate?: unknown) => {
if (!shouldDisableFutureTime(roleOrDate, comparingDate)) return []
return getDisabledFutureUnits(23, new Date().getHours())
}
const disabledFutureMinutes = (hour: number, roleOrDate?: unknown, comparingDate?: unknown) => {
const now = new Date()
if (!shouldDisableFutureTime(roleOrDate, comparingDate) || hour !== now.getHours()) return []
return getDisabledFutureUnits(59, now.getMinutes())
}
const disabledFutureSeconds = (
hour: number,
minute: number,
roleOrDate?: unknown,
comparingDate?: unknown
) => {
const now = new Date()
if (
!shouldDisableFutureTime(roleOrDate, comparingDate) ||
hour !== now.getHours() ||
minute !== now.getMinutes()
) {
return []
}
return getDisabledFutureUnits(59, now.getSeconds())
}
const defaultTimeRange = getTodayTimeRange()
const orgOptions = ref<{ label: string; value: string | number; raw?: OrganizationVO }[]>([])
const defaultPickerEndTime = new Date()
const datePickerDefaultTime = [
new Date(2000, 0, 1, 0, 0, 0),
new Date(
2000,
0,
1,
defaultPickerEndTime.getHours(),
defaultPickerEndTime.getMinutes(),
defaultPickerEndTime.getSeconds()
)
]
const orgOptions = ref<OrganizationTreeOption[]>([])
const orgTreeProps = {
value: 'id',
label: 'name',
children: 'children',
disabled: 'disabled'
}
const loading = ref(false)
const queryFormRef = ref()
@ -242,11 +358,39 @@ const defaultEnergyTypeId = ref<number>()
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
orgId: undefined as number | undefined,
orgId: undefined as string | undefined,
energyTypeId: undefined as number | undefined,
timeRange: [...defaultTimeRange]
})
const normalizeTimeRange = (value?: unknown) => {
const range = Array.isArray(value) && value.length >= 2 ? value : defaultTimeRange
const now = new Date()
let start = parseRangeTime(String(range[0] || '')) || parseRangeTime(defaultTimeRange[0])!
let end = parseRangeTime(String(range[1] || '')) || now
if (start.getTime() > now.getTime()) start = now
if (end.getTime() > now.getTime()) end = now
if (start.getTime() > end.getTime()) start = end
return [formatDateTime(start), formatDateTime(end)]
}
const syncSafeTimeRange = () => {
const safeTimeRange = normalizeTimeRange(queryParams.timeRange)
if (
safeTimeRange[0] !== queryParams.timeRange[0] ||
safeTimeRange[1] !== queryParams.timeRange[1]
) {
queryParams.timeRange = safeTimeRange
}
return safeTimeRange
}
const handleTimeRangeChange = (value: unknown) => {
queryParams.timeRange = normalizeTimeRange(value)
}
const metricCards = ref<MetricCard[]>([])
const rankList = ref<EnergyOverviewRankVO[]>([])
const pageDetailList = ref<DetailRow[]>([])
@ -262,7 +406,7 @@ const trendSeries = ref<number[]>([])
const regionItems = ref<{ name: string; value: number; percent: string }[]>([])
const regionTotal = ref('0')
const trendChartOptions = reactive<EChartsOption>({
const trendChartOptions = ref<EChartsOption>({
color: ['#2f7df6'],
tooltip: {
trigger: 'axis',
@ -325,7 +469,7 @@ const trendChartOptions = reactive<EChartsOption>({
]
})
const regionPieOptions = reactive<EChartsOption>({
const regionPieOptions = ref<EChartsOption>({
color: ['#2f7df6', '#52c41a', '#faad14', '#13c2c2', '#fa8c16'],
tooltip: {
trigger: 'item',
@ -371,6 +515,9 @@ const regionPieOptions = reactive<EChartsOption>({
]
})
const trendChartDisplayOptions = computed<EChartsOption>(() => trendChartOptions.value)
const regionPieDisplayOptions = computed<EChartsOption>(() => regionPieOptions.value)
const metricThemeMap: Record<string, { icon: string; theme: string }> = {
total: { icon: 'ep:lightning', theme: 'blue' },
deviceCount: { icon: 'ep:cpu', theme: 'green' },
@ -379,8 +526,172 @@ const metricThemeMap: Record<string, { icon: string; theme: string }> = {
range: { icon: 'ep:calendar', theme: 'orange' }
}
const extractDeviceParameterAnalysisNodes = (res: any): DeviceParameterAnalysisNodeVO[] => {
if (Array.isArray(res)) return res
if (Array.isArray(res?.data)) return res.data
if (Array.isArray(res?.result)) return res.result
if (Array.isArray(res?.list)) return res.list
return []
}
const normalizeDeviceParameterAnalysisTree = (
nodes: DeviceParameterAnalysisNodeVO[] = []
): OrganizationTreeOption[] => {
const normalizeNode = (node: DeviceParameterAnalysisNodeVO): any => {
const rawId = Number(node?.id) || node?.id
const parentId =
node?.parentId == null || node?.parentId === ''
? 0
: typeof node.parentId === 'number'
? node.parentId
: Number(node.parentId) || 0
return {
...node,
id: rawId,
name: String(node?.name ?? ''),
parentId,
children: Array.isArray(node?.children) ? node.children.map(normalizeNode) : undefined
}
}
const normalizedNodes = Array.isArray(nodes) ? nodes.map(normalizeNode) : []
const isNestedTree = normalizedNodes.some(
(node) => Array.isArray(node?.children) && node.children.length > 0
)
const orgTree = isNestedTree
? normalizedNodes
: (handleTree(normalizedNodes, 'id', 'parentId', 'children') as any[])
const toOption = (node: any): OrganizationTreeOption => {
const orgChildren = Array.isArray(node.children) ? node.children.map(toOption) : []
const equipmentChildren = Array.isArray(node.equipments)
? node.equipments.map((equipment: any) => {
const parameterChildren = Array.isArray(equipment.parameters)
? equipment.parameters.map((param: any) => ({
id: `param-${equipment.id}-${param.id}`,
name: String(param?.name ?? param?.code ?? param?.id ?? ''),
parentId: `equipment-${node.id}-${equipment.id}`,
lineId: String(node.id),
filterId: String(param.id),
deviceId: String(equipment.id),
paramId: String(param.id),
nodeType: 'param' as const
}))
: []
return {
id: `equipment-${node.id}-${equipment.id}`,
name: String(equipment?.name ?? equipment?.id ?? ''),
parentId: `org-${node.id}`,
lineId: String(node.id),
filterId: String(equipment.id),
deviceId: String(equipment.id),
nodeType: 'equipment' as const,
children: parameterChildren.length ? parameterChildren : undefined
}
})
: []
const children = [...orgChildren, ...equipmentChildren]
return {
id: `org-${node.id}`,
name: node.name,
parentId: node.parentId != null && Number(node.parentId) > 0 ? `org-${node.parentId}` : node.parentId,
lineId: String(node.id),
filterId: String(node.id),
nodeType: 'org',
children: children.length ? children : undefined
}
}
return Array.isArray(orgTree) ? orgTree.map(toOption) : []
}
const findOrgNode = (orgId?: string): OrganizationTreeOption | undefined => {
if (!orgId) return undefined
const stack = [...orgOptions.value]
while (stack.length > 0) {
const node = stack.pop()!
if (String(node.id) === String(orgId)) return node
if (Array.isArray(node.children)) stack.push(...node.children)
}
return undefined
}
const getRequestOrgId = (orgId?: string) => {
if (!orgId) return undefined
const node = findOrgNode(orgId)
if (node?.nodeType === 'org' && node.filterId != null) return node.filterId
if (node?.lineId != null) return node.lineId
const matched = /^org-(.+)$/.exec(String(orgId))
return matched?.[1] || orgId
}
const isDateTimeInRange = (date: Date, start: Date, end: Date) =>
date.getTime() >= start.getTime() && date.getTime() <= end.getTime()
const createDateInRangeDay = (base: Date, hour = 0, minute = 0, second = 0) =>
new Date(base.getFullYear(), base.getMonth(), base.getDate(), hour, minute, second)
const parseTrendAxisTime = (label: string, rangeStart: Date, rangeEnd: Date) => {
const text = String(label || '').trim()
if (!text) return undefined
if (/\d{4}[-/]\d{1,2}[-/]\d{1,2}/.test(text)) {
const parsed = parseRangeTime(text)
if (parsed) return parsed
}
const monthDayMatch = text.match(/(?:^|\D)(\d{1,2})[-/](\d{1,2})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?/)
if (monthDayMatch) {
const [, month, day, hourText = '0', minuteText = '0', secondText = '0'] = monthDayMatch
const candidates = [rangeStart.getFullYear(), rangeEnd.getFullYear()].map(
(year) =>
new Date(
year,
Number(month) - 1,
Number(day),
Number(hourText),
Number(minuteText),
Number(secondText)
)
)
return candidates.find((item) => isDateTimeInRange(item, rangeStart, rangeEnd)) || candidates[0]
}
const hourMatch = text.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/)
if (hourMatch) {
const [, hourText, minuteText, secondText = '0'] = hourMatch
return createDateInRangeDay(rangeStart, Number(hourText), Number(minuteText), Number(secondText))
}
return undefined
}
const filterTrendChartData = (axis: string[], data: number[]) => {
const [startTime, endTime] = syncSafeTimeRange()
const rangeStart = parseRangeTime(startTime)
const rangeEnd = parseRangeTime(endTime)
if (!rangeStart || !rangeEnd) return { axis, data }
const filtered = axis
.map((label, index) => ({
label,
value: data[index] ?? 0,
time: parseTrendAxisTime(label, rangeStart, rangeEnd)
}))
.filter((item) => item.time && isDateTimeInRange(item.time, rangeStart, rangeEnd))
if (!filtered.length && axis.length) return { axis: [], data: [] }
return {
axis: filtered.map((item) => item.label),
data: filtered.map((item) => item.value)
}
}
const buildFallbackTrendXAxis = () => {
const timeRange = queryParams.timeRange?.length === 2 ? queryParams.timeRange : defaultTimeRange
const timeRange = syncSafeTimeRange()
const [startTime, endTime] = timeRange
const start = new Date(startTime.replace(/-/g, '/'))
const end = new Date(endTime.replace(/-/g, '/'))
@ -396,7 +707,7 @@ const buildFallbackTrendXAxis = () => {
const axis: string[] = []
if (sameDay) {
const current = new Date(start)
current.setMinutes(0, 0, 0)
current.setMinutes(current.getMinutes() === 0 && current.getSeconds() === 0 ? 0 : 60, 0, 0)
const limit = new Date(end)
limit.setMinutes(0, 0, 0)
while (current <= limit) {
@ -437,12 +748,15 @@ const normalizeMetricCards = (metrics: EnergyOverviewMetricVO[]) => {
const updateCharts = (data: EnergyOverviewRespVO) => {
const fallbackXAxis = buildFallbackTrendXAxis()
trendXAxis.value = data.trendChart?.xAxis?.length ? data.trendChart.xAxis : fallbackXAxis
const responseSeries = (data.trendChart?.data || []).map((item) => Number(item) || 0)
trendSeries.value =
const rawTrendAxis = data.trendChart?.xAxis?.length ? data.trendChart.xAxis : fallbackXAxis
const rawTrendSeries =
responseSeries.length > 0
? responseSeries
: new Array(trendXAxis.value.length).fill(0)
: new Array(rawTrendAxis.length).fill(0)
const filteredTrend = filterTrendChartData(rawTrendAxis, rawTrendSeries)
trendXAxis.value = filteredTrend.axis
trendSeries.value = filteredTrend.data
regionItems.value = (data.regionChart?.items || []).map((item) => ({
name: item.name,
value: Number(item.value) || 0,
@ -450,27 +764,28 @@ const updateCharts = (data: EnergyOverviewRespVO) => {
}))
regionTotal.value = data.regionChart?.totalValue || '0'
trendChartOptions.xAxis = {
...(trendChartOptions.xAxis as any),
trendChartOptions.value.xAxis = {
...(trendChartOptions.value.xAxis as any),
data: trendXAxis.value,
name: t('EnergyOverview.chart.time')
}
trendChartOptions.yAxis = {
...(trendChartOptions.yAxis as any),
trendChartOptions.value.yAxis = {
...(trendChartOptions.value.yAxis as any),
name: selectedEnergyUnit.value
}
if (Array.isArray(trendChartOptions.legend?.data)) {
trendChartOptions.legend.data = [t('EnergyOverview.chart.usage')]
const trendLegend = trendChartOptions.value.legend as any
if (Array.isArray(trendLegend?.data)) {
trendLegend.data = [t('EnergyOverview.chart.usage')]
}
if (Array.isArray(trendChartOptions.series) && trendChartOptions.series[0]) {
;(trendChartOptions.series[0] as any).name = t('EnergyOverview.chart.usage')
;(trendChartOptions.series[0] as any).data = trendSeries.value
if (Array.isArray(trendChartOptions.value.series) && trendChartOptions.value.series[0]) {
;(trendChartOptions.value.series[0] as any).name = t('EnergyOverview.chart.usage')
;(trendChartOptions.value.series[0] as any).data = trendSeries.value
}
if (Array.isArray(regionPieOptions.series) && regionPieOptions.series[0]) {
;(regionPieOptions.series[0] as any).name = t('EnergyOverview.chart.regionEnergy')
;(regionPieOptions.series[0] as any).data = regionItems.value
;(regionPieOptions.series[0] as any).label = {
...(regionPieOptions.series[0] as any).label,
if (Array.isArray(regionPieOptions.value.series) && regionPieOptions.value.series[0]) {
;(regionPieOptions.value.series[0] as any).name = t('EnergyOverview.chart.regionEnergy')
;(regionPieOptions.value.series[0] as any).data = regionItems.value
;(regionPieOptions.value.series[0] as any).label = {
...(regionPieOptions.value.series[0] as any).label,
formatter: `${t('EnergyOverview.chart.totalUsage')}\n{total|${regionTotal.value} ${selectedEnergyUnit.value}}`
}
}
@ -487,9 +802,10 @@ const getList = async () => {
loading.value = true
try {
const timeRange = queryParams.timeRange?.length === 2 ? queryParams.timeRange : defaultTimeRange
const timeRange = syncSafeTimeRange()
const requestOrgId = getRequestOrgId(queryParams.orgId)
const res = await EnergyDeviceApi.queryOverviewData({
orgId: queryParams.orgId,
orgId: requestOrgId,
energyTypeId: queryParams.energyTypeId,
startTime: timeRange[0],
endTime: timeRange[1],
@ -521,8 +837,9 @@ const normalizeEnergyTypeList = (data: unknown): EnergyTypeVO[] => {
const getDefaultEnergyType = (list: EnergyTypeVO[]) => list[0]
const getEnergyTypes = async () => {
const requestOrgId = getRequestOrgId(queryParams.orgId)
try {
const data = await EnergyTypeApi.getEnergyTypeList({ orgId: queryParams.orgId })
const data = await EnergyTypeApi.getEnergyTypeList({ orgId: requestOrgId })
energyTypeOptions.value = normalizeEnergyTypeList(data)
} catch (error) {
energyTypeOptions.value = []
@ -530,7 +847,7 @@ const getEnergyTypes = async () => {
let defaultEnergyType = getDefaultEnergyType(energyTypeOptions.value)
try {
const deviceRes = await EnergyDeviceApi.getList({ orgId: queryParams.orgId })
const deviceRes = await EnergyDeviceApi.getList({ orgId: requestOrgId })
const devices = ((deviceRes as any)?.data || deviceRes || []) as Array<{ deviceTypeId?: number }>
const deviceTypeIds = new Set(devices.map((item) => item.deviceTypeId).filter(Boolean))
defaultEnergyType =
@ -543,7 +860,8 @@ const getEnergyTypes = async () => {
}
const handleQuery = () => {
if (!isSingleDayRange(queryParams.timeRange)) {
const safeTimeRange = syncSafeTimeRange()
if (!isSingleDayRange(safeTimeRange)) {
message.warning(t('EnergyOverview.messages.singleDayOnly'))
return
}
@ -559,27 +877,16 @@ const resetQuery = () => {
handleQuery()
}
const handleExport = async () => {
if (!isSingleDayRange(queryParams.timeRange)) {
message.warning(t('EnergyOverview.messages.singleDayOnly'))
return
}
const timeRange = queryParams.timeRange?.length === 2 ? queryParams.timeRange : defaultTimeRange
await EnergyDeviceApi.exportQueryDataRecords({
orgId: queryParams.orgId,
startTime: timeRange[0],
endTime: timeRange[1]
})
message.success(t('EnergyOverview.exportSuccess'))
}
const loadOrgOptions = async () => {
if (orgOptions.value.length) return
const data: any = await OrganizationApi.getOrganizationList({})
const rows = (Array.isArray(data) ? data : data?.list ?? data?.data ?? []) as OrganizationVO[]
orgOptions.value = rows
.filter((r) => r?.id !== undefined && r?.name)
.map((r) => ({ label: r.name, value: r.id, raw: r }))
const deviceLineTree = await OrganizationApi.deviceParameterAnalysis({
keyword: undefined,
showDevices: 1,
filterNoDeviceLine: 1
})
orgOptions.value = normalizeDeviceParameterAnalysisTree(
extractDeviceParameterAnalysisNodes(deviceLineTree)
)
}
onMounted(async () => {

Loading…
Cancel
Save