You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
besure_web/src/views/iot/runoverview/index.vue

509 lines
16 KiB
Vue

<template>
<div
ref="fullscreenTargetRef"
class="run-overview-page"
:class="{ 'is-fullscreen': isFullscreenLayout }"
>
<div class="run-overview-page__floating-tools">
<el-tooltip
:content="
isFullscreen
? t('DataCollection.RunOverview.exitFullscreen')
: t('DataCollection.RunOverview.enterFullscreen')
"
>
<button class="run-overview-page__screenfull" type="button" @click="toggleFullscreen">
<Icon :icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'" color="#344054" />
</button>
</el-tooltip>
</div>
<OverviewFilterBar
v-model="queryParams"
:group-options="groupOptions"
:device-options="deviceOptions"
:quick-ranges="quickRanges"
@quick-range-change="handleQuickRangeChange"
@query="handleQuery"
@reset="resetQuery"
/>
<OverviewMetricCards :metrics="overviewData.metrics" />
<StatusDistributionChart
:hourly-status="overviewData.hourlyStatus"
:summary="overviewData.summary"
:summary-total-hours="overviewData.summaryTotalHours"
:statistics-start="queryParams.timeRange[0]"
:statistics-end="queryParams.timeRange[1]"
/>
<OperationTimelineChart
:rows="overviewData.timelineRows"
:total="overviewData.totalDevices"
:page-no="pageNo"
:page-size="pageSize"
:statistics-start="queryParams.timeRange[0]"
:statistics-end="queryParams.timeRange[1]"
@update:page-no="pageNo = $event"
@update:page-size="handlePageSizeChange"
/>
</div>
</template>
<script setup lang="ts">
import dayjs from 'dayjs'
import { useFullscreen } from '@vueuse/core'
import { useRoute } from 'vue-router'
import { DeviceOperationOverviewApi } from '@/api/iot/deviceOperationOverview'
import { DeviceLedgerApi, DeviceLedgerVO } from '@/api/mes/deviceledger'
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'
import StatusDistributionChart from './components/StatusDistributionChart.vue'
import { buildDefaultQueryParams } from './mock'
import type {
OrganizationTreeOption,
OverviewOption,
QuickRangeKey,
RunOverviewData,
RunOverviewQueryParams
} from './components/types'
defineOptions({ name: 'IotRunOverview' })
const { t } = useI18n()
const route = useRoute()
const fullscreenTargetRef = ref()
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen(fullscreenTargetRef)
const deviceLedgerList = ref<DeviceLedgerVO[]>([])
const groupOptions = ref<OrganizationTreeOption[]>([])
let deviceLedgerRequestSeq = 0
const isEnabledQueryValue = (value: unknown) => {
const rawValue = Array.isArray(value) ? value[0] : value
return ['1', 'true', 'yes', 'on'].includes(String(rawValue ?? '').toLowerCase())
}
const isFullscreenLayout = computed(
() => isFullscreen.value || isEnabledQueryValue(route.query.fullscreen ?? route.query.full)
)
const createDateRangeByQuickKey = (key: QuickRangeKey): [string, string] => {
const format = 'YYYY-MM-DD HH:mm:ss'
const now = dayjs()
if (key === 'today')
return [now.startOf('day').format(format), now.format(format)]
if (key === 'yesterday') {
return [
dayjs().subtract(1, 'day').startOf('day').format(format),
dayjs().subtract(1, 'day').endOf('day').format(format)
]
}
if (key === 'last7Days')
return [
now.subtract(6, 'day').startOf('day').format(format),
now.format(format)
]
if (key === 'last30Days') {
return [
now.subtract(29, 'day').startOf('day').format(format),
now.format(format)
]
}
return [now.startOf('day').format(format), now.format(format)]
}
const quickRanges = computed(() => [
{ label: t('DataCollection.RunOverview.quickRange.today'), value: 'today' as const },
{ label: t('DataCollection.RunOverview.quickRange.yesterday'), value: 'yesterday' as const },
{ label: t('DataCollection.RunOverview.quickRange.last7Days'), value: 'last7Days' as const },
{ label: t('DataCollection.RunOverview.quickRange.last30Days'), value: 'last30Days' as const }
/* { label: t('DataCollection.RunOverview.quickRange.custom'), value: 'custom' as const }*/
])
const queryParams = ref<RunOverviewQueryParams>(buildDefaultQueryParams())
const pageNo = ref(1)
const pageSize = ref(10)
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 = node?.id
const parentId =
node?.parentId == null || node?.parentId === ''
? 0
: node.parentId
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 && node.parentId !== '' && 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 || String(node.filterId ?? '') === lineId) {
const collectStack = [node]
while (collectStack.length > 0) {
const current = collectStack.pop()!
if (current.nodeType === 'org' && current.filterId != null) ids.add(String(current.filterId))
if (Array.isArray(current.children)) collectStack.push(...current.children)
}
break
}
if (Array.isArray(node.children)) stack.push(...node.children)
}
return ids
}
const hasIotDeviceId = (item: DeviceLedgerVO) => String(item.dvId ?? '').trim() !== ''
const getDeviceOptionLabel = (item: DeviceLedgerVO) => {
const deviceName = String(item.deviceName ?? '').trim()
const deviceCode = String(item.deviceCode ?? '').trim()
return deviceName || deviceCode || String(item.iotDeviceName ?? item.iotDeviceCode ?? '').trim()
}
const collectDeviceOptionsByGroup = (groupId?: string) => {
const deviceMap = new Map<string, OverviewOption>()
const groupIds = collectGroupIds(groupId)
const selectedNode = findGroupNode(groupId)
deviceLedgerList.value.forEach((item) => {
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, {
label: getDeviceOptionLabel(item),
value: deviceId
})
})
return Array.from(deviceMap.values())
}
const deviceOptions = computed(() => collectDeviceOptionsByGroup(queryParams.value.groupId))
const overviewData = ref<RunOverviewData>({
metrics: [],
hourlyStatus: [],
summary: [],
summaryTotalHours: 0,
timelineRows: [],
totalDevices: 0
})
const currentDeviceIds = computed(() =>
queryParams.value.deviceId.length > 0
? queryParams.value.deviceId
: deviceOptions.value.map((item) => item.value)
)
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
const IOT_DEVICE_TYPE = 2
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(','),
deviceType: IOT_DEVICE_TYPE,
startTime,
endTime,
timelinePageNo: pageNo.value,
timelinePageSize: pageSize.value
}
}
const refreshData = async () => {
const response = await DeviceOperationOverviewApi.getRunOverview(buildOverviewRequestParams())
overviewData.value = {
metrics: response?.metrics || [],
hourlyStatus: response?.hourlyStatus || [],
summary: response?.summary || [],
summaryTotalHours: response?.summaryTotalHours || 0,
timelineRows: response?.timelineRows || [],
totalDevices: response?.totalDevices || 0
}
const maxPage = Math.max(1, Math.ceil(overviewData.value.totalDevices / pageSize.value))
if (pageNo.value > maxPage) pageNo.value = maxPage
}
const resetToFirstPageAndRefresh = () => {
if (pageNo.value !== 1) {
pageNo.value = 1
return
}
void refreshData()
}
const handleQuickRangeChange = (key: QuickRangeKey) => {
queryParams.value = {
...queryParams.value,
quickRange: key,
timeRange: createDateRangeByQuickKey(key)
}
pageNo.value = 1
}
const handleQuery = () => {
resetToFirstPageAndRefresh()
}
const resetQuery = () => {
queryParams.value = buildDefaultQueryParams()
pageSize.value = 10
resetToFirstPageAndRefresh()
}
const handlePageSizeChange = (size: number) => {
pageSize.value = size
pageNo.value = 1
void refreshData()
}
watch(pageNo, () => {
void refreshData()
})
const getDeviceLedgerOptions = async (groupId?: string) => {
const requestSeq = ++deviceLedgerRequestSeq
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 OrganizationApi.deviceParameterAnalysis({
keyword: undefined,
deviceType: IOT_DEVICE_TYPE,
showDevices: 1,
filterNoDeviceLine: 1
})
groupOptions.value = normalizeDeviceParameterAnalysisTree(
extractDeviceParameterAnalysisNodes(deviceLineTree)
)
await getDeviceLedgerOptions(queryParams.value.groupId)
}
watch(
() => queryParams.value.groupId,
(groupId) => {
void getDeviceLedgerOptions(groupId)
}
)
watch(
deviceOptions,
(options) => {
if (queryParams.value.deviceId.length === 0) return
const validDeviceIds = queryParams.value.deviceId.filter((deviceId) =>
options.some((item) => item.value === deviceId)
)
if (validDeviceIds.length !== queryParams.value.deviceId.length) {
queryParams.value = {
...queryParams.value,
deviceId: validDeviceIds
}
}
},
{ immediate: true }
)
onMounted(async () => {
await getFilterOptions()
await refreshData()
})
</script>
<style scoped lang="scss">
.run-overview-page {
position: relative;
min-width: 0;
//padding-top: 8px;
}
.run-overview-page.is-fullscreen {
position: fixed;
inset: 0;
z-index: 2000;
width: 100vw;
height: 100vh;
padding: 16px;
overflow: auto;
background: #f5f7fa;
}
.run-overview-page__floating-tools {
position: absolute;
top: 8px;
right: 12px;
z-index: 20;
display: flex;
align-items: center;
gap: 8px;
}
.run-overview-page__screenfull {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
padding: 0;
background: #fff;
border: 1px solid #e4e7ec;
border-radius: 8px;
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.08);
cursor: pointer;
transition: all 0.2s ease;
}
.run-overview-page__screenfull:hover {
background: #f8fbff;
border-color: #bfd3ff;
box-shadow: 0 8px 18px rgba(61, 132, 255, 0.12);
}
</style>