rafactoring mostly
Gitea Actions Demo / build_and_push (push) Failing after 43s Details

This commit is contained in:
artem 2025-01-02 11:47:40 +03:00
parent aa70ce1481
commit 2253d00caa
3 changed files with 600 additions and 0 deletions

View File

@ -0,0 +1,76 @@
import { AxiosResponse, AxiosInstance, AxiosError } from "axios";
import { inject } from 'vue'
import { WorkoutItem, ChartData } from "../Definitions.vue";
let workoutItem: WorkoutItem;
let mapCenter: Array<number> = ([37.617644, 55.755819]);
let lineCoordinates: Array<Array<number>> = [];
let data: ChartData;
let dzenLink: string;
const msToKmh = (ms: number) => ms * 3.6;
export type InitWorkoutItem = {
workoutItem: WorkoutItem;
mapCenter: Array<number>;
lineCoordinates: Array<Array<number>>;
data: ChartData;
dzenLink?: string;
}
export const GetWorkout = (url: string) => {
const axiosAuth = inject('axiosAuth') as AxiosInstance;
const query = axiosAuth
.get(url)
.then((response: AxiosResponse) => {
workoutItem = 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 = coords
mapCenter = [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 = {
labels: times,
datasets: datasets
}
if (response.data.workout.external_links && response.data.workout.external_links.values) {
dzenLink = response.data.workout.external_links.values[0].value;
}
return { workoutItem, mapCenter, lineCoordinates, data, dzenLink };
});
return Promise.resolve(query);
};

View File

@ -0,0 +1,394 @@
<template>
<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 v-if="isPrivate" 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" v-if="isPrivate">
<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 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-if="isPrivate"
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-if="isPrivate"
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 '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 './LineWithLineChart.js'
import type { YMap } from '@yandex/ymaps3-types';
import { YandexMap, YandexMapDefaultSchemeLayer, YandexMapFeature, YandexMapDefaultFeaturesLayer, YandexMapDefaultMarker } from 'vue-yandex-maps';
import { WorkoutItem, distConvert, speedConvert, formatTime, ChartData } from "../Definitions.vue";
interface Props {
workoutItem?: WorkoutItem
data: ChartData,
mapCenter: Array<number>,
lineCoordinates: Array<Array<number>>,
isPrivate: boolean,
dzenLink?: string
}
const { workoutItem, data, mapCenter, lineCoordinates, dzenLink: dzenLinkProps } = defineProps<Props>()
const dzenLink = ref(dzenLinkProps);
const map = shallowRef<null | YMap>(null);
const chart = ref();
let currentCoordinates = ref<Array<number> | null>([]);
let showModalTitle = ref(false);
const showModalLink = ref(false);
ChartJS.register(
Title,
Tooltip,
Legend,
LineElement,
CategoryScale,
LogarithmicScale,
LinearScale,
PointElement,
zoomPlugin,
TimeScale
)
const { init } = useToast();
type afterEventEvent = {
"type": string
}
type afterEventArgs = {
"event": afterEventEvent,
}
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[context.dataIndex];
let show_data = [];
for (let i = 0; i < data.datasets.length; i++) {
let value = data.datasets[i].label + " :" + Math.floor(data.datasets[i].data[context.dataIndex])
if (data.datasets[i].label == "Скорость") {
value = value + " км/ч";
}
if (data.datasets[i].label == "Пульс") {
value = value + " уд/мин";
}
if (data.datasets[i].label == "Мощность") {
value = value + " Ватт";
}
if (data.datasets[i].label == "Подъем") {
value = value + " м";
}
show_data.push(value);
}
return show_data;
}
}
},
zoom: {
zoom: {
drag: {
enabled: true,
},
pinch: {
enabled: true
},
mode: 'x',
}
}
}
}
const axiosAuth = inject('axiosAuth') as AxiosInstance;
const saveLink = (hide: any) => {
if (!dzenLink.value) {
return
}
if (!workoutItem) {
return
}
if (!dzenLink.value.includes("https://dzen.ru/")) {
init({
message: "Неверный формат ссылки",
color: "error",
});
return;
}
axiosAuth
.patch(`/api/v0/workouts/${workoutItem.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) {
return
}
if (workoutItem.name.length == 0) {
init({
message: "Название не может быть пустым",
color: "error",
});
return;
}
axiosAuth
.patch(`/api/v0/workouts/${workoutItem.id}`,{ name: workoutItem.name })
.then((response: AxiosResponse) => {
hide();
}).catch((error: AxiosError) => {
console.log(error);
init({
message: "Что-то пошло не так.",
color: "error",
});
});
};
const changePublic = (value: boolean) => {
if (!workoutItem) {
return
}
axiosAuth
.patch(`/api/v0/workouts/${workoutItem.id}`,{ is_public: value })
.then((response: AxiosResponse) => {
}).catch((error: AxiosError) => {
console.log(error);
init({
message: "Что-то пошло не так.",
color: "error",
});
});
};
const resetChartZoom = () => {
resetZoom(chart.value.chart);
}
</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;
}
.workout-item-params-pointer {
cursor: pointer;
}
</style>

View File

@ -0,0 +1,130 @@
<template>
<article class="news-item">
<div class="news-item-header" v-on:click="openWorkout(item.id)">
<h3>{{ item.name }}</h3>
<!-- @vue-ignore -->
<VaIcon v-if="deleteItem" 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>
</template>
<script setup lang="ts">
import { WorkoutItem, secondsToDuration, distConvert, formatTime, speedConvert } from "../Definitions.vue";
interface Props {
item: WorkoutItem
openWorkout: (item: string) => void
deleteItem?: (item: string, event: any) => void
}
const { item, openWorkout, deleteItem } = defineProps<Props>()
</script>
<style>
.image-container {
cursor: pointer;
height: 300px;
width: 100%;
background-repeat: no-repeat;
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-item {
display: flex;
flex-direction: column;
padding: 1rem;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
transition: transform 0.2s ease-in-out;
width: 100%;
}
.news-item-header {
cursor: pointer;
display: flex;
justify-content: space-between;
}
.news-item-icon {
cursor: pointer;
}
.news-item h3 {
font-weight: bold;
color: #3d3d3d;
}
.metadata {
border-top: #9c9a9a 1px solid;
}
.metadata li {
list-style: none;
display: inline-block;
margin-left: 1rem;
}
.metadata span {
font-size: 0.9em;
opacity: 0.7;
}
</style>