设备运行概览调整

main
liutao 1 day ago
parent f88765448b
commit 2872f52d57

@ -2,22 +2,28 @@
<ContentWrap class="run-overview-filter">
<el-form :model="modelValue" inline label-width="auto">
<el-form-item :label="t('DataCollection.RunOverview.groupLabel')">
<el-select
<el-tree-select
:model-value="modelValue.groupId"
:placeholder="t('DataCollection.RunOverview.groupPlaceholder')"
:data="groupOptions"
:props="treeProps"
check-strictly
clearable
default-expand-all
class="!w-220px"
@update:model-value="updateField('groupId', $event)"
>
<el-option v-for="item in groupOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
:placeholder="t('DataCollection.RunOverview.groupPlaceholder')"
@update:model-value="handleGroupChange"
/>
</el-form-item>
<el-form-item :label="t('DataCollection.RunOverview.deviceLabel')">
<el-select
:model-value="modelValue.deviceId"
multiple
collapse-tags
collapse-tags-tooltip
clearable
:placeholder="t('DataCollection.RunOverview.devicePlaceholder')"
class="!w-240px"
@update:model-value="updateField('deviceId', $event || '')"
@update:model-value="updateField('deviceId', $event || [])"
>
<el-option v-for="item in deviceOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
@ -62,11 +68,16 @@
</template>
<script setup lang="ts">
import type { OverviewOption, QuickRangeKey, RunOverviewQueryParams } from './types'
import type {
OrganizationTreeOption,
OverviewOption,
QuickRangeKey,
RunOverviewQueryParams
} from './types'
const props = defineProps<{
modelValue: RunOverviewQueryParams
groupOptions: OverviewOption[]
groupOptions: OrganizationTreeOption[]
deviceOptions: OverviewOption[]
quickRanges: Array<{ label: string; value: QuickRangeKey }>
}>()
@ -80,6 +91,11 @@ const emit = defineEmits<{
}>()
const { t } = useI18n()
const treeProps = {
value: 'id',
label: 'name',
children: 'children'
}
const updateField = <K extends keyof RunOverviewQueryParams>(key: K, value: RunOverviewQueryParams[K]) => {
emit('update:modelValue', {
@ -87,6 +103,15 @@ const updateField = <K extends keyof RunOverviewQueryParams>(key: K, value: RunO
[key]: value
})
}
const handleGroupChange = (value: string | number | undefined) => {
const nextGroupId = value == null ? '' : String(value)
emit('update:modelValue', {
...props.modelValue,
groupId: nextGroupId,
deviceId: props.modelValue.deviceId
})
}
</script>
<style scoped lang="scss">

@ -1,15 +1,31 @@
export type RunStatus = 'running' | 'standby' | 'fault' | 'offline'
export type QuickRangeKey = 'today' | 'yesterday' | 'last7Days' | 'last30Days' | 'custom'
export type QuickRangeKey = 'today' | 'yesterday' | 'last7Days' | 'last30Days'
export interface OverviewOption {
label: string
value: string
}
export interface OrganizationFilterItem {
id: number | string
name: string
parentId?: number | string | null
dvId?: number | string | null
machineName?: string
}
export interface OrganizationTreeOption {
id: number | string
name: string
parentId?: number | string | null
dvId?: number | string | null
children?: OrganizationTreeOption[]
}
export interface RunOverviewQueryParams {
groupId: string
deviceId: string
deviceId: string[]
quickRange: QuickRangeKey
timeRange: [string, string]
}

@ -45,12 +45,21 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { useFullscreen } from '@vueuse/core'
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, buildRunOverviewData, DEVICE_OPTIONS, GROUP_OPTIONS } from './mock'
import type { QuickRangeKey, RunOverviewData, RunOverviewQueryParams } from './components/types'
import { buildDefaultQueryParams, buildRunOverviewData } from './mock'
import type {
OrganizationFilterItem,
OrganizationTreeOption,
OverviewOption,
QuickRangeKey,
RunOverviewData,
RunOverviewQueryParams
} from './components/types'
defineOptions({ name: 'IotRunOverview' })
@ -58,9 +67,8 @@ const { t } = useI18n()
const message = useMessage()
const fullscreenTargetRef = ref()
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen(fullscreenTargetRef)
const groupOptions = GROUP_OPTIONS
const deviceOptions = DEVICE_OPTIONS
const organizationList = ref<OrganizationFilterItem[]>([])
const groupOptions = ref<OrganizationTreeOption[]>([])
const createDateRangeByQuickKey = (key: QuickRangeKey): [string, string] => {
const format = 'YYYY-MM-DD HH:mm:ss'
@ -83,16 +91,63 @@ const quickRanges = computed(() => [
{ 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 }
/* { label: t('DataCollection.RunOverview.quickRange.custom'), value: 'custom' as const }*/
])
const queryParams = ref<RunOverviewQueryParams>(buildDefaultQueryParams())
const overviewData = ref<RunOverviewData>(buildRunOverviewData(queryParams.value))
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>(buildRunOverviewData(queryParams.value, deviceOptions.value))
const refreshData = () => {
overviewData.value = buildRunOverviewData(queryParams.value)
overviewData.value = buildRunOverviewData(queryParams.value, deviceOptions.value)
const maxPage = Math.max(1, Math.ceil(overviewData.value.totalDevices / pageSize.value))
if (pageNo.value > maxPage) pageNo.value = maxPage
}
@ -127,6 +182,46 @@ const handlePageSizeChange = (size: number) => {
pageSize.value = size
pageNo.value = 1
}
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()
refreshData()
})
</script>
<style scoped lang="scss">

@ -4,47 +4,31 @@ import type {
HourlyStatusItem,
OverviewOption,
RunOverviewData,
RunOverviewQueryParams,
RunOverviewMetric,
RunOverviewQueryParams,
RunStatus
} from './components/types'
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
export const GROUP_OPTIONS: OverviewOption[] = [
{ label: 'SMT一组', value: 'group-1' },
{ label: '成型二组', value: 'group-2' },
{ label: '总装三组', value: 'group-3' }
]
export const DEVICE_OPTIONS: OverviewOption[] = [
{ label: '模拟干燥设备04', value: 'device-01' },
{ label: '模拟干燥设备03', value: 'device-02' },
{ label: '模拟干燥设备02', value: 'device-03' },
{ label: '成型模拟设备05', value: 'device-04' },
{ label: '成型模拟设备04', value: 'device-05' },
{ label: '模拟干燥设备', value: 'device-06' },
{ label: '成型模拟设备02', value: 'device-07' },
{ label: '成型模拟设备01', value: 'device-08' },
{ label: '物流输送设备01', value: 'device-09' },
{ label: '包装工作站02', value: 'device-10' },
{ label: '包装工作站03', value: 'device-11' },
{ label: '拧紧设备01', value: 'device-12' },
{ label: '视觉检测设备01', value: 'device-13' }
const FALLBACK_DEVICE_OPTIONS: OverviewOption[] = [
{ label: '模拟设备01', value: 'device-01' },
{ label: '模拟设备02', value: 'device-02' },
{ label: '模拟设备03', value: 'device-03' },
{ label: '模拟设备04', value: 'device-04' },
{ label: '模拟设备05', value: 'device-05' },
{ label: '模拟设备06', value: 'device-06' },
{ label: '模拟设备07', value: 'device-07' },
{ label: '模拟设备08', value: 'device-08' },
{ label: '模拟设备09', value: 'device-09' },
{ label: '模拟设备10', value: 'device-10' },
{ label: '模拟设备11', value: 'device-11' },
{ label: '模拟设备12', value: 'device-12' },
{ label: '模拟设备13', value: 'device-13' }
]
const shiftTimeline = (segments: Array<{ status: RunStatus; startHour: number; endHour: number }>, offset: number) =>
segments.map((segment, index) => {
let nextStart = segment.startHour + offset
let nextEnd = segment.endHour + offset
if (index === 0 && nextStart < 0) nextStart = 0
if (index === segments.length - 1 && nextEnd > 24) nextEnd = 24
return {
...segment,
startHour: Math.max(0, Math.min(24, Number(nextStart.toFixed(2)))),
endHour: Math.max(0, Math.min(24, Number(nextEnd.toFixed(2))))
}
})
const UTILIZATION_RATES = [82.35, 76.12, 68.54, 75.63, 72.18, 91.24, 65.32, 78.44, 71.05, 83.67, 69.18, 87.42, 74.56]
const TIMELINE_OFFSETS = [0, -0.3, 0.8, -0.5, 0.45, 1.05, -1.1, 0.25, -0.75, 0.6, -0.2, 1.2, -0.45]
const BASE_SEGMENTS = [
{ status: 'running' as const, startHour: 0, endHour: 2.7 },
@ -65,33 +49,43 @@ const BASE_SEGMENTS = [
{ status: 'offline' as const, startHour: 23.7, endHour: 24 }
]
const TIMELINE_OFFSETS = [0, -0.3, 0.8, -0.5, 0.45, 1.05, -1.1, 0.25, -0.75, 0.6, -0.2, 1.2, -0.45]
const shiftTimeline = (segments: Array<{ status: RunStatus; startHour: number; endHour: number }>, offset: number) =>
segments.map((segment, index) => {
let nextStart = segment.startHour + offset
let nextEnd = segment.endHour + offset
if (index === 0 && nextStart < 0) nextStart = 0
if (index === segments.length - 1 && nextEnd > 24) nextEnd = 24
return {
...segment,
startHour: Math.max(0, Math.min(24, Number(nextStart.toFixed(2)))),
endHour: Math.max(0, Math.min(24, Number(nextEnd.toFixed(2))))
}
})
export const buildDefaultQueryParams = (): RunOverviewQueryParams => {
const start = dayjs().startOf('day')
const end = dayjs().endOf('day')
return {
groupId: GROUP_OPTIONS[0].value,
deviceId: '',
groupId: '',
deviceId: [],
quickRange: 'today',
timeRange: [start.format(DATE_TIME_FORMAT), end.format(DATE_TIME_FORMAT)]
}
}
const toTimelineRows = (): DeviceTimelineRow[] =>
DEVICE_OPTIONS.map((device, index) => ({
const toTimelineRows = (deviceOptions: OverviewOption[]): DeviceTimelineRow[] =>
deviceOptions.map((device, index) => ({
id: device.value,
name: device.label,
utilizationRate: [82.35, 76.12, 68.54, 75.63, 72.18, 91.24, 65.32, 78.44, 71.05, 83.67, 69.18, 87.42, 74.56][index],
segments: shiftTimeline(BASE_SEGMENTS, TIMELINE_OFFSETS[index] || 0)
utilizationRate: UTILIZATION_RATES[index % UTILIZATION_RATES.length],
segments: shiftTimeline(BASE_SEGMENTS, TIMELINE_OFFSETS[index % TIMELINE_OFFSETS.length])
}))
const toHourlyStatus = (summaryFactor: number): HourlyStatusItem[] => {
return Array.from({ length: 24 }, (_, hour) => {
const toHourlyStatus = (summaryFactor: number): HourlyStatusItem[] =>
Array.from({ length: 24 }, (_, hour) => {
const runningBase = 74 + Math.sin((hour / 24) * Math.PI * 3) * 14 + summaryFactor * 2
const standbyBase = 10 + Math.cos((hour / 24) * Math.PI * 4) * 7
const faultBase = 2 + Math.max(0, Math.sin((hour - 5) / 2.2)) * 5
const offlineBase = 100 - runningBase - standbyBase - faultBase
const running = Math.max(48, Math.min(88, Number(runningBase.toFixed(2))))
const standby = Math.max(4, Math.min(26, Number(standbyBase.toFixed(2))))
@ -106,7 +100,6 @@ const toHourlyStatus = (summaryFactor: number): HourlyStatusItem[] => {
offline
}
})
}
const createMetrics = (summaryFactor: number): RunOverviewMetric[] => [
{
@ -153,7 +146,10 @@ const createSummary = (summaryFactor: number) => {
]
}
export const buildRunOverviewData = (query: RunOverviewQueryParams): RunOverviewData => {
export const buildRunOverviewData = (
query: RunOverviewQueryParams,
deviceOptions: OverviewOption[] = FALLBACK_DEVICE_OPTIONS
): RunOverviewData => {
const summaryFactor =
query.quickRange === 'today'
? 0
@ -165,8 +161,8 @@ export const buildRunOverviewData = (query: RunOverviewQueryParams): RunOverview
? 0.45
: 0.15
const rows = toTimelineRows()
const filteredRows = query.deviceId ? rows.filter((row) => row.id === query.deviceId) : rows
const rows = toTimelineRows(deviceOptions.length > 0 ? deviceOptions : FALLBACK_DEVICE_OPTIONS)
const filteredRows = query.deviceId.length > 0 ? rows.filter((row) => query.deviceId.includes(row.id)) : rows
return {
metrics: createMetrics(summaryFactor),

Loading…
Cancel
Save