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

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>