127 lines
3.5 KiB
Vue
127 lines
3.5 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
import { RouterLink, useRoute } from 'vue-router'
|
|
import * as echarts from 'echarts'
|
|
import { controlCaseApi, type CsvChartResponse } from '@/api.ts'
|
|
import { formatApiError } from '@/api_client.ts'
|
|
|
|
const route = useRoute()
|
|
const containerRef = ref<HTMLDivElement | null>(null)
|
|
const loading = ref(false)
|
|
const error = ref('')
|
|
const data = ref<CsvChartResponse | null>(null)
|
|
let chart: echarts.ECharts | null = null
|
|
|
|
const id = computed(() => Number(route.params.id))
|
|
|
|
function buildOption(payload: CsvChartResponse): echarts.EChartsOption {
|
|
const [xKey, ...seriesKeys] = payload.columns
|
|
const rows = payload.rows
|
|
|
|
const xValues = rows.map((row) => row[0] ?? '')
|
|
const series = seriesKeys.map((name, index) => ({
|
|
name,
|
|
type: 'line' as const,
|
|
showSymbol: false,
|
|
smooth: true,
|
|
data: rows.map((row) => row[index + 1] ?? null),
|
|
}))
|
|
|
|
return {
|
|
tooltip: { trigger: 'axis' },
|
|
legend: { top: 0 },
|
|
grid: { left: 48, right: 24, top: 48, bottom: 48, containLabel: true },
|
|
dataZoom: [
|
|
{ type: 'inside', xAxisIndex: 0 },
|
|
{ type: 'slider', xAxisIndex: 0, height: 18, bottom: 10 },
|
|
],
|
|
xAxis: {
|
|
type: 'category',
|
|
name: xKey ?? 'x',
|
|
data: xValues,
|
|
},
|
|
yAxis: { type: 'value' },
|
|
series,
|
|
}
|
|
}
|
|
|
|
function renderChart() {
|
|
if (!chart || !data.value) return
|
|
chart.setOption(buildOption(data.value), true)
|
|
}
|
|
|
|
async function loadData() {
|
|
if (!Number.isFinite(id.value)) {
|
|
error.value = 'Некорректный ID'
|
|
data.value = null
|
|
return
|
|
}
|
|
|
|
loading.value = true
|
|
error.value = ''
|
|
|
|
try {
|
|
data.value = await controlCaseApi.chartData(id.value)
|
|
renderChart()
|
|
} catch (err) {
|
|
error.value = formatApiError(err, 'Не удалось загрузить данные графика.')
|
|
data.value = null
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function handleResize() {
|
|
chart?.resize()
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (containerRef.value) {
|
|
chart = echarts.init(containerRef.value)
|
|
window.addEventListener('resize', handleResize)
|
|
}
|
|
await loadData()
|
|
})
|
|
|
|
watch(id, loadData)
|
|
watch(data, renderChart)
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('resize', handleResize)
|
|
chart?.dispose()
|
|
chart = null
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<section class="d-flex flex-column gap-3">
|
|
<div class="d-flex align-items-center justify-content-between gap-2 flex-wrap">
|
|
<div>
|
|
<RouterLink class="link-secondary text-decoration-none" :to="{ name: 'control-case-detail', params: { id } }">
|
|
← Назад к кейсу
|
|
</RouterLink>
|
|
<h1 class="h3 mt-2 mb-0">График control case {{ id }}</h1>
|
|
</div>
|
|
<button class="btn btn-outline-secondary" type="button" @click="loadData" :disabled="loading">
|
|
Обновить
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="error" class="alert alert-danger mb-0" role="alert">{{ error }}</div>
|
|
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<div class="position-relative" style="min-height: 560px;">
|
|
<div ref="containerRef" style="height: 560px; width: 100%;"></div>
|
|
<div
|
|
v-if="loading"
|
|
class="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center bg-white bg-opacity-75"
|
|
>
|
|
<div class="text-muted">Загрузка...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|