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.
323 lines
9.5 KiB
Vue
323 lines
9.5 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 { DeviceLedgerApi, DeviceLedgerVO } from '@/api/mes/deviceledger'
|
|
import { DeviceLineApi, DeviceLineTreeVO } from '@/api/mes/deviceline'
|
|
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 message = useMessage()
|
|
const fullscreenTargetRef = ref()
|
|
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen(fullscreenTargetRef)
|
|
const deviceLedgerList = ref<DeviceLedgerVO[]>([])
|
|
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 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 collectGroupIds = (groupId?: string) => {
|
|
if (!groupId) return undefined
|
|
const ids = new Set<string>()
|
|
const stack = [...groupOptions.value]
|
|
|
|
while (stack.length > 0) {
|
|
const node = stack.pop()!
|
|
if (String(node.id) === groupId) {
|
|
const collectStack = [node]
|
|
while (collectStack.length > 0) {
|
|
const current = collectStack.pop()!
|
|
ids.add(String(current.id))
|
|
if (Array.isArray(current.children)) collectStack.push(...current.children)
|
|
}
|
|
break
|
|
}
|
|
if (Array.isArray(node.children)) stack.push(...node.children)
|
|
}
|
|
|
|
return ids
|
|
}
|
|
|
|
const hasIotDeviceCode = (item: DeviceLedgerVO) => String(item.iotDeviceCode ?? '').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)
|
|
deviceLedgerList.value.forEach((item) => {
|
|
if (groupIds && !groupIds.has(String(item.deviceLine ?? ''))) return
|
|
const deviceCode = String(item.iotDeviceCode ?? '').trim()
|
|
if (!deviceCode || deviceMap.has(deviceCode)) return
|
|
deviceMap.set(deviceCode, {
|
|
label: getDeviceOptionLabel(item),
|
|
value: deviceCode
|
|
})
|
|
})
|
|
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 getFilterOptions = async () => {
|
|
const [deviceLineTree, ledgerList] = await Promise.all([
|
|
DeviceLineApi.getDeviceLineTree(),
|
|
DeviceLedgerApi.getDeviceLedgerList()
|
|
])
|
|
groupOptions.value = normalizeDeviceLineTree(Array.isArray(deviceLineTree) ? deviceLineTree : [])
|
|
deviceLedgerList.value = Array.isArray(ledgerList) ? ledgerList.filter(hasIotDeviceCode) : []
|
|
}
|
|
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 {
|
|
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>
|