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

<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>