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.
271 lines
6.8 KiB
Vue
271 lines
6.8 KiB
Vue
<template>
|
|
<ContentWrap class="timeline-panel">
|
|
<div class="timeline-panel__header">
|
|
<div class="timeline-panel__title">{{ t('DataCollection.RunOverview.timelineTitle') }}</div>
|
|
<div class="timeline-panel__actions">
|
|
<el-select model-value="" class="!w-140px">
|
|
<el-option value="" :label="t('DataCollection.RunOverview.deviceFilterAll')" />
|
|
</el-select>
|
|
<el-select model-value="expand" class="!w-120px">
|
|
<el-option value="expand" :label="t('DataCollection.RunOverview.expandAllText')" />
|
|
</el-select>
|
|
<el-button class="timeline-panel__icon-btn">
|
|
<Icon icon="ep:data-analysis" />
|
|
</el-button>
|
|
<el-button class="timeline-panel__icon-btn">
|
|
<Icon icon="ep:download" />
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="timeline-legend">
|
|
<div v-for="item in legendItems" :key="item.status" class="timeline-legend__item">
|
|
<span class="timeline-legend__dot" :style="{ background: item.color }"></span>
|
|
<span>{{ item.label }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="timeline-table">
|
|
<div class="timeline-table__head">
|
|
<div class="timeline-table__meta">
|
|
<div class="timeline-table__device">{{ t('DataCollection.RunOverview.tableDeviceNameColumn') }}</div>
|
|
<div class="timeline-table__rate">{{ t('DataCollection.RunOverview.tableUtilizationRateColumn') }}</div>
|
|
</div>
|
|
<div class="timeline-scale">
|
|
<div v-for="hour in hourTicks" :key="hour" class="timeline-scale__tick">
|
|
{{ hour }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-for="row in pagedRows" :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>
|
|
</div>
|
|
<div class="timeline-track">
|
|
<div v-for="hour in 24" :key="hour" class="timeline-track__hour"></div>
|
|
<div
|
|
v-for="(segment, index) in row.segments"
|
|
:key="`${row.id}-${index}`"
|
|
class="timeline-segment"
|
|
:style="{
|
|
left: `${(segment.startHour / 24) * 100}%`,
|
|
width: `${((segment.endHour - segment.startHour) / 24) * 100}%`,
|
|
background: statusColors[segment.status]
|
|
}"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="timeline-panel__footer">
|
|
<div class="timeline-panel__summary">
|
|
{{ t('DataCollection.RunOverview.statisticsTimeText', { start: statisticsStart, end: statisticsEnd }) }}
|
|
</div>
|
|
<div class="timeline-panel__pager">
|
|
<span>{{ t('DataCollection.RunOverview.totalDevicesText', { total }) }}</span>
|
|
<el-select :model-value="pageSize" class="!w-100px" @update:model-value="emit('update:pageSize', Number($event))">
|
|
<el-option :value="10" :label="`10${t('DataCollection.RunOverview.pageUnit')}`" />
|
|
<el-option :value="20" :label="`20${t('DataCollection.RunOverview.pageUnit')}`" />
|
|
</el-select>
|
|
<el-pagination
|
|
layout="prev, pager, next"
|
|
:page-size="pageSize"
|
|
:current-page="pageNo"
|
|
:total="total"
|
|
small
|
|
@current-change="emit('update:pageNo', $event)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</ContentWrap>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import type { DeviceTimelineRow, RunStatus } from './types'
|
|
|
|
const props = defineProps<{
|
|
rows: DeviceTimelineRow[]
|
|
total: number
|
|
pageNo: number
|
|
pageSize: number
|
|
statisticsStart: string
|
|
statisticsEnd: string
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:pageNo', value: number): void
|
|
(e: 'update:pageSize', value: number): void
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
|
|
const statusColors: Record<RunStatus, string> = {
|
|
running: '#67c35b',
|
|
standby: '#f5c243',
|
|
fault: '#ff6b6b',
|
|
offline: '#d5dae3'
|
|
}
|
|
|
|
const legendItems = computed(() => [
|
|
{ status: 'running' as const, color: statusColors.running, label: t('DataCollection.RunOverview.legend.running') },
|
|
{ status: 'standby' as const, color: statusColors.standby, label: t('DataCollection.RunOverview.legend.standby') },
|
|
{ status: 'fault' as const, color: statusColors.fault, label: t('DataCollection.RunOverview.legend.fault') },
|
|
{ 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)
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.timeline-panel__header,
|
|
.timeline-panel__footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
|
|
.timeline-panel__title {
|
|
color: #101828;
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.timeline-panel__actions,
|
|
.timeline-panel__pager {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.timeline-panel__icon-btn {
|
|
width: 36px;
|
|
padding: 0;
|
|
}
|
|
|
|
.timeline-legend {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 32px;
|
|
margin: 10px 0 18px;
|
|
}
|
|
|
|
.timeline-legend__item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: #475467;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.timeline-legend__dot {
|
|
width: 18px;
|
|
height: 10px;
|
|
border-radius: 999px;
|
|
}
|
|
|
|
.timeline-table {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.timeline-table__head,
|
|
.timeline-table__row {
|
|
display: grid;
|
|
grid-template-columns: 210px minmax(720px, 1fr);
|
|
gap: 16px;
|
|
}
|
|
|
|
.timeline-table__head {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.timeline-table__row {
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.timeline-table__meta {
|
|
display: grid;
|
|
grid-template-columns: 1fr 70px;
|
|
gap: 12px;
|
|
align-items: center;
|
|
}
|
|
|
|
.timeline-table__device,
|
|
.timeline-table__rate {
|
|
color: #344054;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.timeline-table__head .timeline-table__device,
|
|
.timeline-table__head .timeline-table__rate {
|
|
color: #475467;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.timeline-scale,
|
|
.timeline-track {
|
|
position: relative;
|
|
display: grid;
|
|
grid-template-columns: repeat(24, 1fr);
|
|
}
|
|
|
|
.timeline-scale {
|
|
color: #667085;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.timeline-scale__tick {
|
|
position: relative;
|
|
}
|
|
|
|
.timeline-scale__tick::after {
|
|
position: absolute;
|
|
top: 18px;
|
|
right: 0;
|
|
width: 1px;
|
|
height: 12px;
|
|
background: #e4e7ec;
|
|
content: '';
|
|
}
|
|
|
|
.timeline-track {
|
|
height: 20px;
|
|
background: #f5f7fb;
|
|
border-radius: 999px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.timeline-track__hour {
|
|
border-right: 1px solid #edf1f7;
|
|
}
|
|
|
|
.timeline-segment {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
border-radius: 999px;
|
|
}
|
|
|
|
.timeline-panel__summary {
|
|
color: #667085;
|
|
font-size: 13px;
|
|
}
|
|
|
|
@media (max-width: 1100px) {
|
|
.timeline-panel__header,
|
|
.timeline-panel__footer {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
}
|
|
}
|
|
</style>
|