rafactoring mostly
This commit is contained in:
parent
eabc2d3e7b
commit
aa70ce1481
|
|
@ -63,5 +63,16 @@
|
|||
export const speedConvert = (speed: number) => {
|
||||
return Math.round(speed*3.6)
|
||||
}
|
||||
export type ChartDataset = {
|
||||
"radius": number,
|
||||
"label": string,
|
||||
"backgroundColor": string,
|
||||
"borderColor": string,
|
||||
"data": Array<number>,
|
||||
}
|
||||
export type ChartData = {
|
||||
"labels": Array<string>,
|
||||
"datasets": Array<ChartDataset>,
|
||||
}
|
||||
export default {}
|
||||
</script>
|
||||
|
|
@ -5,25 +5,7 @@
|
|||
<div>Публичные тренировки не найдены</div>
|
||||
</template>
|
||||
<template v-if="workoutItems.length>0" v-for="(item, index) in workoutItems" :key="item.id">
|
||||
<article class="news-item">
|
||||
<div class="news-item-header" v-on:click="openWorkout(item.id)">
|
||||
<h3>{{ item.name }}</h3>
|
||||
</div>
|
||||
<ul class="metadata">
|
||||
<li v-if="item.workouted_at"><span>Дата:</span> {{ formatTime(item.workouted_at) }}</li>
|
||||
<li v-if="item.speed"><span>Скорость:</span> {{ speedConvert(item.speed) }} км/ч</li>
|
||||
<li v-if="item.heart_rate"><span>Пульс:</span> {{ item.heart_rate }} уд /мин</li>
|
||||
<li v-if="item.distantion"><span>Расстояние:</span> {{ distConvert(item.distantion) }} км</li>
|
||||
<li v-if="item.duraion_sec"><span>Продолжительность:</span> {{ secondsToDuration(item.duraion_sec) }} </li>
|
||||
<li v-if="item.cadence"><span>Каденс:</span> {{ item.cadence }} об/мин</li>
|
||||
<li v-if="item.power"><span>Мощность:</span> {{ Math.round(item.power) }} Вт</li>
|
||||
<li v-if="item.external_links && item.external_links.values.length > 0"><span>Ссылки:</span> <a :href="item.external_links?.values[0].value" target="_blank">Дзен</a></li>
|
||||
</ul>
|
||||
<div v-if="item.attachment"
|
||||
class="image-container"
|
||||
:style="{'background-image': `url(${item.attachment.url})` }"
|
||||
v-on:click="openWorkout(item.id)"></div>
|
||||
</article>
|
||||
<WorkoutListItem :item=item :openWorkout=openWorkout></WorkoutListItem>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
|
@ -33,7 +15,8 @@
|
|||
import { ref, inject } from 'vue';
|
||||
import { useToast } from "vuestic-ui/web-components";
|
||||
import { useRouter } from "vue-router";
|
||||
import { WorkoutItem, secondsToDuration, distConvert, formatTime, speedConvert } from "./Definitions.vue";
|
||||
import { WorkoutItem } from "./Definitions.vue";
|
||||
import WorkoutListItem from "./components/WorkoutListItem.vue";
|
||||
|
||||
|
||||
const { push } = useRouter();
|
||||
|
|
@ -69,17 +52,57 @@
|
|||
cursor: pointer;
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-size: auto;
|
||||
background-position: center center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.image-container img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
@media (min-width: 1300px) {
|
||||
.image-container::before,
|
||||
.image-container::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 24%;
|
||||
height: 300px;
|
||||
background-image: inherit;
|
||||
background-size: inherit;
|
||||
filter: blur(10px);
|
||||
}
|
||||
|
||||
.image-container::before {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.image-container::after {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.image-container::before {
|
||||
left: 0;
|
||||
background-position: left center;
|
||||
}
|
||||
|
||||
.image-container:after {
|
||||
right: 0;
|
||||
background-position: right center;
|
||||
}
|
||||
}
|
||||
.news-feed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 7rem;
|
||||
}
|
||||
|
||||
.news-item {
|
||||
|
|
|
|||
|
|
@ -1,390 +1,55 @@
|
|||
<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.workouted_at">
|
||||
<div class="workout-item-params-name">Дата:</div>
|
||||
<div class="workout-item-params-value">{{ formatTime(workoutItem.workouted_at) }}</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.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" 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.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" >
|
||||
<div class="workout-item-params-name">Сделать публичной: </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 class="workout-item-params-pointer" 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>
|
||||
<WorkoutItemComponent v-if="workoutItem"
|
||||
:workoutItem=workoutItem
|
||||
:data=data
|
||||
:mapCenter=mapCenter
|
||||
:lineCoordinates=lineCoordinates
|
||||
:dzenLink=dzenLink
|
||||
:isPrivate=true
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import VaZoomOut from "../../components/icons/VaZoomOut.vue";
|
||||
|
||||
|
||||
import { AxiosError } from "axios";
|
||||
import { ref } from '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, formatTime } from "./Definitions.vue";
|
||||
import WorkoutItemComponent from "./components/WorkoutItem.vue";
|
||||
import { WorkoutItem, ChartData } from "./Definitions.vue";
|
||||
import { GetWorkout, InitWorkoutItem } from "./components/GetWorkout";
|
||||
|
||||
|
||||
//Можно использовать для различных преобразований
|
||||
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 mapCenter = ref<Array<number>>([37.617644, 55.755819]);
|
||||
const lineCoordinates = ref<Array<Array<number>>>([]);
|
||||
const 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) {
|
||||
console.log(data.value.datasets)
|
||||
currentCoordinates.value = lineCoordinates.value[context.dataIndex];
|
||||
let show_data = [];
|
||||
for (let i = 0; i < data.value.datasets.length; i++) {
|
||||
let value = data.value.datasets[i].label + " :" + Math.floor(data.value.datasets[i].data[context.dataIndex])
|
||||
if (data.value.datasets[i].label == "Скорость") {
|
||||
value = value + " км/ч";
|
||||
}
|
||||
if (data.value.datasets[i].label == "Пульс") {
|
||||
value = value + " уд/мин";
|
||||
}
|
||||
if (data.value.datasets[i].label == "Мощность") {
|
||||
value = value + " Ватт";
|
||||
}
|
||||
if (data.value.datasets[i].label == "Подъем") {
|
||||
value = value + " м";
|
||||
}
|
||||
show_data.push(value);
|
||||
}
|
||||
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) => {
|
||||
if (workoutItem.value?.name.length == 0) {
|
||||
init({
|
||||
message: "Название не может быть пустым",
|
||||
color: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
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 dzenLink = ref("");
|
||||
|
||||
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.length > 0) {
|
||||
datasets.push({ radius: 0, label: 'Скорость', borderColor: '#00aa00', backgroundColor: '#00aa00', data: speed });
|
||||
}
|
||||
if (heart_rate.length > 0) {
|
||||
datasets.push({ radius: 0, label: 'Пульс', borderColor: '#990000', backgroundColor: '#990000', data: heart_rate, });
|
||||
}
|
||||
if (power.length > 0) {
|
||||
datasets.push({ radius: 0, label: 'Мощность', borderColor: '#cccccc', backgroundColor: '#cccccc', data: power, });
|
||||
}
|
||||
if (elevation.length > 0) {
|
||||
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",
|
||||
});
|
||||
GetWorkout(`/api/v0/workouts/${id}`).then((d: InitWorkoutItem) => {
|
||||
mapCenter.value = d.mapCenter;
|
||||
lineCoordinates.value = d.lineCoordinates;
|
||||
workoutItem.value = d.workoutItem;
|
||||
data.value = d.data;
|
||||
if (d.dzenLink != undefined) {
|
||||
dzenLink.value = d.dzenLink;
|
||||
}
|
||||
})
|
||||
.catch((error: AxiosError) => {
|
||||
console.log(error);
|
||||
init({
|
||||
message: "Что-то пошло не так.",
|
||||
color: "error",
|
||||
});
|
||||
});
|
||||
};
|
||||
initWorkout(route.params.id as string);
|
||||
</script>
|
||||
|
|
@ -392,63 +57,7 @@ initWorkout(route.params.id as string);
|
|||
|
||||
|
||||
<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;
|
||||
}
|
||||
.workout-item-params-pointer {
|
||||
cursor: pointer;
|
||||
.page-title {
|
||||
color: #555;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -7,26 +7,7 @@
|
|||
<div>Тренировки не найдены</div>
|
||||
</template>
|
||||
<template v-if="workoutItems.length>0" v-for="(item, index) in workoutItems" :key="item.id">
|
||||
<article class="news-item">
|
||||
<div class="news-item-header" v-on:click="openWorkout(item.id)">
|
||||
<h3>{{ item.name }}</h3>
|
||||
<VaIcon name="delete_forever" size="22px" color="#bbc1c3" class="news-item-icon" @click="(event: any) => deleteItem(item.id, event)"/>
|
||||
</div>
|
||||
<ul class="metadata">
|
||||
<li v-if="item.workouted_at"><span>Дата:</span> {{ formatTime(item.workouted_at) }}</li>
|
||||
<li v-if="item.speed"><span>Скорость:</span> {{ speedConvert(item.speed) }} км/ч</li>
|
||||
<li v-if="item.heart_rate"><span>Пульс:</span> {{ item.heart_rate }} уд /мин</li>
|
||||
<li v-if="item.distantion"><span>Расстояние:</span> {{ distConvert(item.distantion) }} км</li>
|
||||
<li v-if="item.duraion_sec"><span>Продолжительность:</span> {{ secondsToDuration(item.duraion_sec) }} </li>
|
||||
<li v-if="item.cadence"><span>Каденс:</span> {{ item.cadence }} об/мин</li>
|
||||
<li v-if="item.power"><span>Мощность:</span> {{ Math.round(item.power) }} Вт</li>
|
||||
<li v-if="item.external_links && item.external_links.values.length > 0"><span>Ссылки:</span> <a :href="item.external_links?.values[0].value" target="_blank">Дзен</a></li>
|
||||
</ul>
|
||||
<div v-if="item.attachment"
|
||||
class="image-container"
|
||||
:style="{'background-image': `url(${item.attachment.url})` }"
|
||||
v-on:click="openWorkout(item.id)"></div>
|
||||
</article>
|
||||
<WorkoutListItem :item=item :openWorkout=openWorkout :deleteItem=deleteItem></WorkoutListItem>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
|
@ -36,7 +17,8 @@ import { AxiosResponse, AxiosInstance } from "axios";
|
|||
import { ref, inject } from 'vue';
|
||||
import { useToast } from "vuestic-ui/web-components";
|
||||
import { useRouter } from "vue-router";
|
||||
import { WorkoutItem, formatTime, secondsToDuration, distConvert, speedConvert } from "./Definitions.vue";
|
||||
import { WorkoutItem } from "./Definitions.vue";
|
||||
import WorkoutListItem from "./components/WorkoutListItem.vue";
|
||||
|
||||
|
||||
const { push } = useRouter();
|
||||
|
|
@ -103,7 +85,6 @@ initWorkouts();
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 7rem;
|
||||
}
|
||||
|
||||
.news-item {
|
||||
|
|
|
|||
|
|
@ -1,304 +1,55 @@
|
|||
<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">
|
||||
<h3>{{ workoutItem.name }}</h3>
|
||||
<div class="workout-item-params" v-if="workoutItem.workouted_at">
|
||||
<div class="workout-item-params-name">Дата:</div>
|
||||
<div class="workout-item-params-value">{{ formatTime(workoutItem.workouted_at) }}</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.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" 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.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="dzenLink">
|
||||
<div class="workout-item-params-name">Ссылки на описание:</div>
|
||||
<div><a :href="dzenLink" target="_blank">Дзен</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>
|
||||
<WorkoutItemComponent v-if="workoutItem"
|
||||
:workoutItem=workoutItem
|
||||
:data=data
|
||||
:mapCenter=mapCenter
|
||||
:lineCoordinates=lineCoordinates
|
||||
:dzenLink=dzenLink
|
||||
:isPrivate=false
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import VaZoomOut from "../../components/icons/VaZoomOut.vue";
|
||||
|
||||
|
||||
import { AxiosError } from "axios";
|
||||
import { ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import 'chartjs-adapter-moment';
|
||||
import { AxiosResponse, AxiosInstance } 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, formatTime, speedConvert } from "./Definitions.vue";
|
||||
import WorkoutItemComponent from "./components/WorkoutItem.vue";
|
||||
import { WorkoutItem, ChartData } from "./Definitions.vue";
|
||||
import { GetWorkout, InitWorkoutItem } from "./components/GetWorkout";
|
||||
|
||||
|
||||
//Можно использовать для различных преобразований
|
||||
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>([]);
|
||||
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 mapCenter = ref<Array<number>>([37.617644, 55.755819]);
|
||||
const lineCoordinates = ref<Array<Array<number>>>([]);
|
||||
const 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 axiosPublic= inject('axiosPublic') as AxiosInstance;
|
||||
|
||||
const resetChartZoom = () => {
|
||||
resetZoom(chart.value.chart);
|
||||
}
|
||||
const dzenLink = ref("");
|
||||
|
||||
const initWorkout = (id: string) => {
|
||||
axiosPublic
|
||||
.get(`/api/v0/public/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: any) => {
|
||||
console.log(error);
|
||||
init({
|
||||
message: "Что-то пошло не так.",
|
||||
color: "error",
|
||||
});
|
||||
GetWorkout(`/api/v0/public/workouts/${id}`).then((d: InitWorkoutItem) => {
|
||||
mapCenter.value = d.mapCenter;
|
||||
lineCoordinates.value = d.lineCoordinates;
|
||||
workoutItem.value = d.workoutItem;
|
||||
data.value = d.data;
|
||||
if (d.dzenLink != undefined) {
|
||||
dzenLink.value = d.dzenLink;
|
||||
}
|
||||
})
|
||||
.catch((error: AxiosError) => {
|
||||
console.log(error);
|
||||
init({
|
||||
message: "Что-то пошло не так.",
|
||||
color: "error",
|
||||
});
|
||||
});
|
||||
};
|
||||
initWorkout(route.params.id as string);
|
||||
</script>
|
||||
|
|
@ -306,45 +57,7 @@ initWorkout(route.params.id as string);
|
|||
|
||||
|
||||
<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
|
||||
.page-title {
|
||||
color: #555;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue