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.
313 lines
9.1 KiB
Vue
313 lines
9.1 KiB
Vue
<template>
|
|
<div ref="fullscreenTargetRef" class="run-overview-page" :class="{ 'is-fullscreen': isFullscreen }">
|
|
<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"
|
|
/>
|
|
|
|
<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 { DeviceOperationOverviewApi } from '@/api/iot/deviceOperationOverview'
|
|
import { OrganizationApi } 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 {
|
|
OrganizationFilterItem,
|
|
OrganizationTreeOption,
|
|
OverviewOption,
|
|
QuickRangeKey,
|
|
RunOverviewData,
|
|
RunOverviewQueryParams
|
|
} from './components/types'
|
|
|
|
defineOptions({ name: 'IotRunOverview' })
|
|
|
|
const { t } = useI18n()
|
|
const message = useMessage()
|
|
const fullscreenTargetRef = ref()
|
|
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen(fullscreenTargetRef)
|
|
const organizationList = ref<OrganizationFilterItem[]>([])
|
|
const groupOptions = ref<OrganizationTreeOption[]>([])
|
|
|
|
const createDateRangeByQuickKey = (key: QuickRangeKey): [string, string] => {
|
|
const format = 'YYYY-MM-DD HH:mm:ss'
|
|
if (key === 'today') return [dayjs().startOf('day').format(format), dayjs().endOf('day').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 [dayjs().subtract(6, 'day').startOf('day').format(format), dayjs().endOf('day').format(format)]
|
|
if (key === 'last30Days') {
|
|
return [dayjs().subtract(29, 'day').startOf('day').format(format), dayjs().endOf('day').format(format)]
|
|
}
|
|
return [dayjs().startOf('day').format(format), dayjs().endOf('day').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 organizationMap = computed(() => {
|
|
return organizationList.value.reduce<Record<string, OrganizationFilterItem>>((acc, item) => {
|
|
acc[String(item.id)] = item
|
|
return acc
|
|
}, {})
|
|
})
|
|
|
|
const childrenByParentId = computed(() => {
|
|
return organizationList.value.reduce<Record<string, OrganizationFilterItem[]>>((acc, item) => {
|
|
const parentKey = String(item.parentId ?? 0)
|
|
if (!acc[parentKey]) acc[parentKey] = []
|
|
acc[parentKey].push(item)
|
|
return acc
|
|
}, {})
|
|
})
|
|
|
|
const collectDeviceOptionsByGroup = (groupId?: string) => {
|
|
const deviceMap = new Map<string, OverviewOption>()
|
|
const pushDevice = (node?: OrganizationFilterItem) => {
|
|
if (!node?.dvId || !node.machineName?.trim()) return
|
|
const deviceId = String(node.dvId)
|
|
if (!deviceMap.has(deviceId)) {
|
|
deviceMap.set(deviceId, {
|
|
label: node.machineName.trim(),
|
|
value: deviceId
|
|
})
|
|
}
|
|
}
|
|
|
|
if (!groupId) {
|
|
organizationList.value.forEach(pushDevice)
|
|
return Array.from(deviceMap.values())
|
|
}
|
|
|
|
const queue = [groupId]
|
|
while (queue.length > 0) {
|
|
const currentId = queue.shift() as string
|
|
pushDevice(organizationMap.value[currentId])
|
|
const children = childrenByParentId.value[currentId] || []
|
|
children.forEach((child) => queue.push(String(child.id)))
|
|
}
|
|
|
|
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 buildOverviewRequestParams = () => ({
|
|
ids: currentDeviceIds.value.join(','),
|
|
startTime: queryParams.value.timeRange[0],
|
|
endTime: queryParams.value.timeRange[1],
|
|
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 handleExport = () => {
|
|
message.success(t('DataCollection.RunOverview.exportPlaceholderMessage'))
|
|
}
|
|
|
|
const handlePageSizeChange = (size: number) => {
|
|
pageSize.value = size
|
|
pageNo.value = 1
|
|
void refreshData()
|
|
}
|
|
|
|
watch(pageNo, () => {
|
|
void refreshData()
|
|
})
|
|
|
|
const getOrganizationOptions = async () => {
|
|
const data = await OrganizationApi.getOrganizationList()
|
|
organizationList.value = Array.isArray(data)
|
|
? data.map((item) => ({
|
|
id: String(item.id),
|
|
name: item.name,
|
|
parentId: item.parentId != null ? String(item.parentId) : item.parentId,
|
|
dvId: item.dvId,
|
|
machineName: item.machineName
|
|
}))
|
|
: []
|
|
groupOptions.value = handleTree(
|
|
organizationList.value.map((item) => ({ ...item })),
|
|
'id',
|
|
'parentId'
|
|
) as OrganizationTreeOption[]
|
|
}
|
|
|
|
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 getOrganizationOptions()
|
|
await refreshData()
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.run-overview-page {
|
|
position: relative;
|
|
min-width: 0;
|
|
//padding-top: 8px;
|
|
}
|
|
|
|
.run-overview-page.is-fullscreen {
|
|
height: 100%;
|
|
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>
|