黄伟杰 23 hours ago
commit cb0ade38e0

@ -38,7 +38,7 @@
</div>
</div>
<div v-for="row in pagedRows" :key="row.id" class="timeline-table__row">
<div v-for="row in timelineRows" :key="row.id" class="timeline-table__row">
<div class="timeline-table__meta">
<div class="timeline-table__device">{{ row.name }}</div>
<div class="timeline-table__rate">{{ row.utilizationRate.toFixed(2) }}%</div>
@ -116,12 +116,20 @@ const legendItems = computed(() => [
{ status: 'offline' as const, color: statusColors.offline, label: t('DataCollection.RunOverview.legend.offline') }
])
const hourTicks = Array.from({ length: 13 }, (_, index) => `${String(index * 2).padStart(2, '0')}:00`)
const pagedRows = computed(() => {
const start = (props.pageNo - 1) * props.pageSize
return props.rows.slice(start, start + props.pageSize)
})
const hourTicks = Array.from({ length: 24 }, (_, index) => `${String(index).padStart(2, '0')}:00`)
const timelineRows = computed(() =>
props.rows.map((row) => ({
...row,
segments: (row.segments || []).filter(
(segment) =>
!!statusColors[segment.status] &&
Number.isFinite(segment.startHour) &&
Number.isFinite(segment.endHour) &&
segment.endHour > segment.startHour
)
}))
)
</script>
<style scoped lang="scss">
@ -239,7 +247,6 @@ const pagedRows = computed(() => {
.timeline-track {
height: 20px;
background: #f5f7fb;
border-radius: 999px;
overflow: hidden;
}

@ -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>
@ -52,21 +58,26 @@
<Icon icon="ep:refresh" class="mr-5px" />
{{ t('DataCollection.RunOverview.resetButtonText') }}
</el-button>
<el-button type="success" plain @click="emit('export')">
<!-- <el-button type="success" plain @click="emit('export')">
<Icon icon="ep:download" class="mr-5px" />
{{ t('DataCollection.RunOverview.exportButtonText') }}
</el-button>
</el-button>-->
</el-form-item>
</el-form>
</ContentWrap>
</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">

@ -52,6 +52,7 @@ const Echart = EchartChart
const props = defineProps<{
hourlyStatus: HourlyStatusItem[]
summary: StatusSummaryItem[]
summaryTotalHours?: number
}>()
const { t } = useI18n()
@ -137,11 +138,11 @@ const pieOption = computed<EChartsOption>(() => ({
tooltip: { trigger: 'item', formatter: '{b}: {c}%' },
graphic: [
{
type: 'text',
left: 'center',
top: '42%',
style: {
text: `${t('DataCollection.RunOverview.totalTimeLabel')}\n24.00 h`,
type: 'text',
left: 'center',
top: '42%',
style: {
text: `${t('DataCollection.RunOverview.totalTimeLabel')}\n${(props.summaryTotalHours ?? 0).toFixed(2)} h`,
textAlign: 'center',
fill: '#101828',
fontSize: 14,

@ -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]
}
@ -53,6 +69,7 @@ export interface RunOverviewData {
metrics: RunOverviewMetric[]
hourlyStatus: HourlyStatusItem[]
summary: StatusSummaryItem[]
summaryTotalHours: number
timelineRows: DeviceTimelineRow[]
totalDevices: number
}

@ -22,12 +22,15 @@
@quick-range-change="handleQuickRangeChange"
@query="handleQuery"
@reset="resetQuery"
@export="handleExport"
/>
<OverviewMetricCards :metrics="overviewData.metrics" />
<StatusDistributionChart :hourly-status="overviewData.hourlyStatus" :summary="overviewData.summary" />
<StatusDistributionChart
:hourly-status="overviewData.hourlyStatus"
:summary="overviewData.summary"
:summary-total-hours="overviewData.summaryTotalHours"
/>
<OperationTimelineChart
:rows="overviewData.timelineRows"
@ -45,12 +48,22 @@
<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, buildRunOverviewData, DEVICE_OPTIONS, GROUP_OPTIONS } from './mock'
import type { QuickRangeKey, RunOverviewData, RunOverviewQueryParams } from './components/types'
import { buildDefaultQueryParams } from './mock'
import type {
OrganizationFilterItem,
OrganizationTreeOption,
OverviewOption,
QuickRangeKey,
RunOverviewData,
RunOverviewQueryParams
} from './components/types'
defineOptions({ name: 'IotRunOverview' })
@ -58,9 +71,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,40 +95,119 @@ 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 refreshData = () => {
overviewData.value = buildRunOverviewData(queryParams.value)
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: key === 'custom' ? queryParams.value.timeRange : createDateRangeByQuickKey(key)
timeRange: createDateRangeByQuickKey(key)
}
pageNo.value = 1
refreshData()
}
const handleQuery = () => {
pageNo.value = 1
refreshData()
resetToFirstPageAndRefresh()
}
const resetQuery = () => {
queryParams.value = buildDefaultQueryParams()
pageNo.value = 1
pageSize.value = 10
refreshData()
resetToFirstPageAndRefresh()
}
const handleExport = () => {
@ -126,7 +217,52 @@ const handleExport = () => {
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">

@ -1,178 +1,15 @@
import dayjs from 'dayjs'
import type {
DeviceTimelineRow,
HourlyStatusItem,
OverviewOption,
RunOverviewData,
RunOverviewQueryParams,
RunOverviewMetric,
RunStatus
} from './components/types'
import type { RunOverviewQueryParams } 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 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 BASE_SEGMENTS = [
{ status: 'running' as const, startHour: 0, endHour: 2.7 },
{ status: 'standby' as const, startHour: 2.7, endHour: 3.1 },
{ status: 'running' as const, startHour: 3.1, endHour: 6.6 },
{ status: 'standby' as const, startHour: 6.6, endHour: 7.15 },
{ status: 'running' as const, startHour: 7.15, endHour: 13.9 },
{ status: 'standby' as const, startHour: 13.9, endHour: 14.25 },
{ status: 'offline' as const, startHour: 14.25, endHour: 14.95 },
{ status: 'running' as const, startHour: 14.95, endHour: 17.45 },
{ status: 'fault' as const, startHour: 17.45, endHour: 18.15 },
{ status: 'standby' as const, startHour: 18.15, endHour: 18.8 },
{ status: 'running' as const, startHour: 18.8, endHour: 20.55 },
{ status: 'standby' as const, startHour: 20.55, endHour: 20.95 },
{ status: 'running' as const, startHour: 20.95, endHour: 22.25 },
{ status: 'offline' as const, startHour: 22.25, endHour: 23.05 },
{ status: 'running' as const, startHour: 23.05, endHour: 23.7 },
{ 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]
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) => ({
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)
}))
const toHourlyStatus = (summaryFactor: number): HourlyStatusItem[] => {
return 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))))
const fault = Math.max(1, Math.min(8, Number(faultBase.toFixed(2))))
const offline = Number(Math.max(4, 100 - running - standby - fault).toFixed(2))
return {
hour: `${String(hour).padStart(2, '0')}:00`,
running,
standby,
fault,
offline
}
})
}
const createMetrics = (summaryFactor: number): RunOverviewMetric[] => [
{
key: 'utilizationRate',
icon: 'ep:pie-chart',
value: Number((75.42 + summaryFactor * 1.2).toFixed(2)),
unit: '%',
change: 4.32
},
{
key: 'powerOnRate',
icon: 'ep:video-play',
value: Number((90.12 + summaryFactor * 0.7).toFixed(2)),
unit: '%',
change: 2.15
},
{
key: 'faultRate',
icon: 'ep:warning',
value: Number(Math.max(1.5, 3.21 - summaryFactor * 0.35).toFixed(2)),
unit: '%',
change: -1.03
},
{
key: 'standbyRate',
icon: 'ep:timer',
value: Number(Math.max(4, 6.67 - summaryFactor * 0.25).toFixed(2)),
unit: '%',
change: -1.14
}
]
const createSummary = (summaryFactor: number) => {
const running = Number((75.42 + summaryFactor * 1.2).toFixed(2))
const standby = Number(Math.max(4, 6.67 - summaryFactor * 0.25).toFixed(2))
const fault = Number(Math.max(1.5, 3.21 - summaryFactor * 0.35).toFixed(2))
const offline = Number((100 - running - standby - fault).toFixed(2))
return [
{ status: 'running' as const, percent: running, hours: Number(((24 * running) / 100).toFixed(2)) },
{ status: 'standby' as const, percent: standby, hours: Number(((24 * standby) / 100).toFixed(2)) },
{ status: 'fault' as const, percent: fault, hours: Number(((24 * fault) / 100).toFixed(2)) },
{ status: 'offline' as const, percent: offline, hours: Number(((24 * offline) / 100).toFixed(2)) }
]
}
export const buildRunOverviewData = (query: RunOverviewQueryParams): RunOverviewData => {
const summaryFactor =
query.quickRange === 'today'
? 0
: query.quickRange === 'yesterday'
? -0.8
: query.quickRange === 'last7Days'
? 1.1
: query.quickRange === 'last30Days'
? 0.45
: 0.15
const rows = toTimelineRows()
const filteredRows = query.deviceId ? rows.filter((row) => row.id === query.deviceId) : rows
return {
metrics: createMetrics(summaryFactor),
hourlyStatus: toHourlyStatus(summaryFactor),
summary: createSummary(summaryFactor),
timelineRows: filteredRows,
totalDevices: filteredRows.length
}
}

Loading…
Cancel
Save