strava-frontend/src/pages/workouts/WorkoutItem.vue

435 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<h1 class="page-title">Tренировка</h1>
<div v-if="workoutItem">
<div id="workout-container">
<div id="workout-map">
<yandex-map v-model="map" :settings="{
location: {
center: mapCenter,
zoom: 9,
},
}" width="100%" height="350px">
<yandex-map-default-scheme-layer />
<yandex-map-default-features-layer />
<yandex-map-feature :settings="{
geometry: {
type: 'LineString',
coordinates: lineCoordinates,
},
style: {
stroke: [{ color: '#007afce6', width: 4 }],
},
}" />
<yandex-map-default-marker v-if="currentCoordinates" :settings="{
coordinates: currentCoordinates,
}" />
</yandex-map>
</div>
<div id="workout-short-data">
<div class="workout-item-editable-title">
<h3>{{ workoutItem.name }}</h3>
<VaIcon name="edit" @click="showModalTitle = !showModalTitle"/>
</div>
<div class="workout-item-params" v-if="workoutItem.power">
<div class="workout-item-params-name">Средняя мощность:</div>
<div class="workout-item-params-value">{{ Math.floor(workoutItem.power) }} Вт</div>
</div>
<div class="workout-item-params" v-if="workoutItem.max_power">
<div class="workout-item-params-name">Максимальная мощность:</div>
<div class="workout-item-params-value">{{ Math.floor(workoutItem.max_power) }} Вт</div>
</div>
<div class="workout-item-params" v-if="workoutItem.distantion">
<div class="workout-item-params-name">Расстояние:</div>
<div class="workout-item-params-value">{{ distConvert(workoutItem.distantion) }} км</div>
</div>
<div class="workout-item-params" v-if="workoutItem.temperature">
<div class="workout-item-params-name">Температура:</div>
<div class="workout-item-params-value">{{ workoutItem.temperature }} °C</div>
</div>
<div class="workout-item-params" v-if="workoutItem.heart_rate">
<div class="workout-item-params-name">Средний пульс:</div>
<div class="workout-item-params-value">{{ Math.floor(workoutItem.heart_rate) }} уд./ мин.</div>
</div>
<div class="workout-item-params" v-if="workoutItem.max_heart_rate">
<div class="workout-item-params-name">Максимальный пульс:</div>
<div class="workout-item-params-value">{{ Math.floor(workoutItem.max_heart_rate) }} уд./ мин.</div>
</div>
<div class="workout-item-params" v-if="workoutItem.speed">
<div class="workout-item-params-name">Средняя скорость:</div>
<div class="workout-item-params-value">{{ speedConvert(workoutItem.speed) }} км / ч</div>
</div>
<div class="workout-item-params" v-if="workoutItem.max_speed">
<div class="workout-item-params-name">Максимальная скорость:</div>
<div class="workout-item-params-value">{{ speedConvert(workoutItem.max_speed || 0) }} км / ч</div>
</div>
<div class="workout-item-params" >
<div class="workout-item-params-name">Сделать публичной:&nbsp;</div>
<input type="checkbox" v-model="workoutItem.is_public" v-on:change="changePublic(workoutItem.is_public)">
</div>
<div class="workout-item-params" >
<div class="workout-item-params-name">Ссылки на описание:</div>
<div v-if="dzenLink"><a :href="dzenLink" target="_blank">Дзен</a></div>
<div v-else><a @click="showModalLink = !showModalLink">Добавить</a> </div>
</div>
</div>
</div>
<VaButton v-on:click="resetChartZoom()" preset="secondary" color="textPrimary" id="zoom-botton">
<VaZoomOut />
</VaButton>
<!-- @vue-ignore -->
<LineWithLineChart
id="chart"
ref="chart"
:options="chartOptions"
:plugins="chartPlugins"
:data="data"
pointerAxies="currentCoordinates"
>
</LineWithLineChart>
</div>
<div v-else>
<VaInnerLoading
loading
:size="60"
>
</VaInnerLoading>
</div>
<VaModal
v-model="showModalTitle"
ok-text="OK"
:beforeOk="saveName"
>
<h3>Редактирование названия</h3>
<input v-if="workoutItem" v-model="workoutItem.name" type="text" placeholder="Название" class="workout-item-input" />
<div v-else>Тренировка не найдена</div>
</VaModal>
<VaModal
v-model="showModalLink"
ok-text="OK"
:beforeOk="saveLink"
>
<h3>Редактирование ссылок</h3>
<input v-model="dzenLink" type="text" placeholder="Название" class="workout-item-input" />
</VaModal>
</template>
<script setup lang="ts">
import VaZoomOut from "../../components/icons/VaZoomOut.vue";
import { useRoute } from 'vue-router'
import 'chartjs-adapter-moment';
import { AxiosResponse, AxiosInstance, AxiosError } from "axios";
import { ref, inject, shallowRef } from 'vue'
import { useToast } from "vuestic-ui/web-components";
import zoomPlugin, { resetZoom } from 'chartjs-plugin-zoom';
import {
Chart as ChartJS,
Title,
Tooltip,
Legend,
LineElement,
LinearScale,
CategoryScale,
LogarithmicScale,
PointElement,
TimeScale
} from 'chart.js'
import LineWithLineChart from './components/LineWithLineChart.js'
import type { YMap } from '@yandex/ymaps3-types';
import { YandexMap, YandexMapDefaultSchemeLayer, YandexMapFeature, YandexMapDefaultFeaturesLayer, YandexMapDefaultMarker } from 'vue-yandex-maps';
import { WorkoutItem, distConvert, speedConvert, WorkoutLink } from "./Definitions.vue";
//Можно использовать для различных преобразований
const map = shallowRef<null | YMap>(null);
const chart = ref();
let mapCenter = ref<Array<number>>([37.617644, 55.755819]);
let lineCoordinates = ref<Array<Array<number>>>([]);
let currentCoordinates = ref<Array<number> | null>([]);
let showModalTitle = ref(false);
const showModalLink = ref(false);
const dzenLink = ref("");
ChartJS.register(
Title,
Tooltip,
Legend,
LineElement,
CategoryScale,
LogarithmicScale,
LinearScale,
PointElement,
zoomPlugin,
TimeScale
)
const { init } = useToast();
const route = useRoute()
type ChartDataset = {
"radius": number,
"label": string,
"backgroundColor": string,
"borderColor": string,
"data": Array<number>,
}
type ChartData = {
"labels": Array<string>,
"datasets": Array<ChartDataset>,
}
type afterEventEvent = {
"type": string
}
type afterEventArgs = {
"event": afterEventEvent,
}
let workoutItem = ref<WorkoutItem>();
const data = ref<ChartData>({
labels: [],
datasets: [],
})
const chartPlugins = [
{
id: 'eventPlugin',
afterEvent(chart: any, args: afterEventArgs, opts: any) {
if (args.event.type == "mouseout") {
currentCoordinates.value = null;
}
}
}
];
const chartOptions = {
responsive: true,
scales: {
yAxes: {
type: 'logarithmic',
position: 'right',
stacked: false,
ticks: {
beginAtZero: false
},
gridLines: {
display: true
}
},
x: {
type: 'time',
time: {
displayFormats: { hour: 'HH:mm' }
}
}
},
plugins: {
tooltip: {
enabled: true,
intersect: false,
footerMarginTop: 10,
displayColors: false,
callbacks: {
label: function (context: any) {
currentCoordinates.value = lineCoordinates.value[context.dataIndex];
let show_data = [];
if (data.value.datasets[0].data[context.dataIndex]) {
show_data.push("Скорость: " + Math.floor(data.value.datasets[0].data[context.dataIndex]));
}
if (data.value.datasets[1].data[context.dataIndex]) {
show_data.push("Пульс: " + data.value.datasets[1].data[context.dataIndex]);
}
if (data.value.datasets[2].data[context.dataIndex]) {
show_data.push("Мощность: " + data.value.datasets[2].data[context.dataIndex]);
}
if (Math.floor(data.value.datasets[3].data[context.dataIndex])) {
show_data.push("Подъем: " + Math.floor(data.value.datasets[3].data[context.dataIndex]));
}
return show_data;
}
}
},
zoom: {
zoom: {
drag: {
enabled: true,
},
pinch: {
enabled: true
},
mode: 'x',
}
}
}
}
const msToKmh = (ms: number) => ms * 3.6;
const axiosAuth = inject('axiosAuth') as AxiosInstance;
const saveLink = (hide: any) => {
if (!dzenLink.value.includes("https://dzen.ru/")) {
init({
message: "Неверный формат ссылки",
color: "error",
});
return;
}
axiosAuth
.patch(`/api/v0/workouts/${workoutItem.value?.id}`,{ links: [{"value": dzenLink.value, "type": "dzen"}] })
.then((response: AxiosResponse) => {
hide();
}).catch((error: AxiosError) => {
console.log(error);
init({
message: "Что-то пошло не так.",
color: "error",
});
});
};
const saveName = (hide: any) => {
axiosAuth
.patch(`/api/v0/workouts/${workoutItem.value?.id}`,{ name: workoutItem.value?.name })
.then((response: AxiosResponse) => {
hide();
}).catch((error: AxiosError) => {
console.log(error);
init({
message: "Что-то пошло не так.",
color: "error",
});
});
};
const changePublic = (value: boolean) => {
axiosAuth
.patch(`/api/v0/workouts/${workoutItem.value?.id}`,{ is_public: value })
.then((response: AxiosResponse) => {
}).catch((error: AxiosError) => {
console.log(error);
init({
message: "Что-то пошло не так.",
color: "error",
});
});
};
const resetChartZoom = () => {
resetZoom(chart.value.chart);
}
const initWorkout = (id: string) => {
axiosAuth
.get(`/api/v0/workouts/${id}`)
.then((response: AxiosResponse) => {
workoutItem.value = response.data.workout;
let times = [];
let speed = [];
let heart_rate = [];
let power = [];
let coords = [];
let elevation = [];
for (let i in response.data.results) {
times.push(response.data.results[i].timestamp);
coords.push([response.data.results[i].longitude, response.data.results[i].latitude]);
if (response.data.results[i].elevation) {
elevation.push(response.data.results[i].elevation);
}
if (response.data.results[i].speed) {
speed.push(msToKmh(response.data.results[i].speed));
}
if (response.data.results[i].power) {
power.push(response.data.results[i].power);
}
if (response.data.results[i].heart_rate) {
heart_rate.push(response.data.results[i].heart_rate);
}
}
lineCoordinates.value = coords
mapCenter.value = [coords[0][0], coords[0][1]];
let datasets = [];
if (speed) {
datasets.push({ radius: 0, label: 'Скорость', borderColor: '#00aa00', backgroundColor: '#00aa00', data: speed });
}
if (heart_rate) {
datasets.push({ radius: 0, label: 'Пульс', borderColor: '#990000', backgroundColor: '#990000', data: heart_rate, });
}
if (power) {
datasets.push({ radius: 0, label: 'Мощность', borderColor: '#cccccc', backgroundColor: '#cccccc', data: power, });
}
if (elevation) {
datasets.push( { radius: 0, label: 'Подъем', borderColor: '#000', backgroundColor: '#000', data: elevation, });
}
data.value = {
labels: times,
datasets: datasets
}
if (response.data.workout.external_links && response.data.workout.external_links.values) {
dzenLink.value = response.data.workout.external_links.values[0].value;
}
})
.catch((error: AxiosError) => {
console.log(error);
init({
message: "Что-то пошло не так.",
color: "error",
});
});
};
initWorkout(route.params.id as string);
</script>
<style>
#zoom-botton {
display: block;
margin-top: 20px;
margin-bottom: -35px;
margin-left: 20px;
}
#workout-container {
display: flex;
width: 100%;
justify-content: space-between;
}
#workout-map {
width: 70%;
}
#workout-short-data {
width: 30%;
padding: 0 0 0 20px;
}
h3 {
margin-bottom: 10px;
font-size: 20px;
font-family: sans-serif;
}
.workout-item-params {
display: flex;
width: 100%;
justify-content:left;
color: #333;
padding: 0 0 5px 0
}
.workout-item-params-name {
font-weight: bold;
}
.workout-item-params-value {
padding: 0 0 0 5px
}
.workout-item-editable-title {
display: flex;
width: 100%;
justify-self: start;
}
.workout-item-editable-title h3 {
padding-right: 5px;
}
.workout-item-editable-title i {
cursor: pointer;
}
.workout-item-input {
border: #333 1px solid;
padding-left: 5px;
}
</style>