|
|
|
|
@ -31,9 +31,17 @@
|
|
|
|
|
<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 class="timeline-chart" :style="timelineChartStyle">
|
|
|
|
|
<div class="timeline-scale">
|
|
|
|
|
<div
|
|
|
|
|
v-for="tick in hourTicks"
|
|
|
|
|
:key="tick.key"
|
|
|
|
|
class="timeline-scale__tick"
|
|
|
|
|
:class="`is-${tick.align}`"
|
|
|
|
|
:style="{ left: `${tick.position}%` }"
|
|
|
|
|
>
|
|
|
|
|
{{ tick.label }} :00
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@ -43,18 +51,25 @@
|
|
|
|
|
<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 class="timeline-chart" :style="timelineChartStyle">
|
|
|
|
|
<div class="timeline-track">
|
|
|
|
|
<div
|
|
|
|
|
v-for="tick in hourTicks"
|
|
|
|
|
:key="tick.key"
|
|
|
|
|
class="timeline-track__hour"
|
|
|
|
|
:style="{ left: `${tick.position}%` }"
|
|
|
|
|
></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>
|
|
|
|
|
@ -84,6 +99,7 @@
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed } from 'vue'
|
|
|
|
|
import dayjs from 'dayjs'
|
|
|
|
|
import type { DeviceTimelineRow, RunStatus } from './types'
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
@ -116,7 +132,40 @@ const legendItems = computed(() => [
|
|
|
|
|
{ status: 'offline' as const, color: statusColors.offline, label: t('DataCollection.RunOverview.legend.offline') }
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const hourTicks = Array.from({ length: 24 }, (_, index) => `${String(index).padStart(2, '0')}:00`)
|
|
|
|
|
const timelineRange = computed(() => {
|
|
|
|
|
const start = dayjs(props.statisticsStart)
|
|
|
|
|
const end = dayjs(props.statisticsEnd)
|
|
|
|
|
if (start.isValid() && end.isValid() && end.isAfter(start)) return { start, end }
|
|
|
|
|
|
|
|
|
|
const fallbackStart = dayjs().startOf('day')
|
|
|
|
|
return { start: fallbackStart, end: fallbackStart.add(1, 'day') }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const rangeHours = computed(() => Math.max(timelineRange.value.end.diff(timelineRange.value.start, 'hour', true), 1))
|
|
|
|
|
|
|
|
|
|
const hourTicks = computed(() => {
|
|
|
|
|
const { start, end } = timelineRange.value
|
|
|
|
|
const ticks: Array<{ key: string; label: string; position: number; align: 'start' | 'center' | 'end' }> = []
|
|
|
|
|
const totalSeconds = end.diff(start, 'second', true)
|
|
|
|
|
let current = start.startOf('hour')
|
|
|
|
|
if (current.isBefore(start)) current = current.add(1, 'hour')
|
|
|
|
|
|
|
|
|
|
while (!current.isAfter(end)) {
|
|
|
|
|
const position = (current.diff(start, 'second', true) / totalSeconds) * 100
|
|
|
|
|
ticks.push({
|
|
|
|
|
key: current.valueOf().toString(),
|
|
|
|
|
label: current.format('HH'),
|
|
|
|
|
position,
|
|
|
|
|
align: position === 0 ? 'start' : position === 100 ? 'end' : 'center'
|
|
|
|
|
})
|
|
|
|
|
current = current.add(1, 'hour')
|
|
|
|
|
}
|
|
|
|
|
return ticks
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const timelineChartStyle = computed(() => ({
|
|
|
|
|
minWidth: `${Math.max(720, Math.ceil(rangeHours.value) * 64)}px`
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const timelineRows = computed(() =>
|
|
|
|
|
props.rows.map((row) => ({
|
|
|
|
|
@ -219,30 +268,34 @@ const timelineRows = computed(() =>
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.timeline-chart {
|
|
|
|
|
min-width: 720px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.timeline-scale,
|
|
|
|
|
.timeline-track {
|
|
|
|
|
position: relative;
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(24, 1fr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.timeline-scale {
|
|
|
|
|
height: 30px;
|
|
|
|
|
color: #667085;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.timeline-scale__tick {
|
|
|
|
|
position: relative;
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.timeline-scale__tick::after {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 18px;
|
|
|
|
|
right: 0;
|
|
|
|
|
width: 1px;
|
|
|
|
|
height: 12px;
|
|
|
|
|
background: #e4e7ec;
|
|
|
|
|
content: '';
|
|
|
|
|
.timeline-scale__tick.is-start {
|
|
|
|
|
transform: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.timeline-scale__tick.is-end {
|
|
|
|
|
transform: translateX(-100%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.timeline-track {
|
|
|
|
|
@ -252,7 +305,11 @@ const timelineRows = computed(() =>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.timeline-track__hour {
|
|
|
|
|
border-right: 1px solid #edf1f7;
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
width: 1px;
|
|
|
|
|
background: #edf1f7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.timeline-segment {
|
|
|
|
|
|