Prace s mapu, zobrazeni GPX, editace kopilota, skeletony
This commit is contained in:
@@ -1,58 +0,0 @@
|
||||
<script setup>
|
||||
import MapWrapper from "components/MapWrapper.vue";
|
||||
import {Marker, Polyline} from "vue3-google-map";
|
||||
import {computed} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
gpsLatitude: {
|
||||
type: Number,
|
||||
required: false
|
||||
},
|
||||
gpsLongitude: {
|
||||
type: Number,
|
||||
required: false
|
||||
},
|
||||
zoom: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 10
|
||||
},
|
||||
markers: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
showAsTrack: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const mapCenter = computed(() => {
|
||||
const north = Math.max(...props.markers.map(m => m.lng));
|
||||
const south = Math.min(...props.markers.map(m => m.lng));
|
||||
const east = Math.max(...props.markers.map(m => m.lat));
|
||||
const west = Math.min(...props.markers.map(m => m.lat));
|
||||
|
||||
const longitudeDiff = north - south;
|
||||
const latitudeDiff = east - west;
|
||||
|
||||
return {
|
||||
lat: west + latitudeDiff / 2,
|
||||
lng: south + longitudeDiff / 2
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MapWrapper v-bind="$attrs" :center="mapCenter" :zoom="zoom">
|
||||
<template v-if="!showAsTrack">
|
||||
<Marker v-for="(marker, idx) in markers" :key="idx" :options="{position: marker}"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Polyline :options="{path: markers, geodesic: true, strokeColor: '#ff0000', strokeOpacity: 0.3, strokeWeight: 5}"/>
|
||||
</template>
|
||||
</MapWrapper>
|
||||
</template>
|
||||
@@ -1,12 +0,0 @@
|
||||
<script setup>
|
||||
import {GoogleMap} from "vue3-google-map";
|
||||
|
||||
const key = process.env.GOOGLE_MAPS_API_KEY;
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GoogleMap :api-key="key" v-bind="$attrs">
|
||||
<slot />
|
||||
</GoogleMap>
|
||||
</template>
|
||||
@@ -6,10 +6,10 @@ import {randomInt} from "src/composables/random";
|
||||
<q-list bordered separator>
|
||||
<q-item v-ripple v-for="i in randomInt(2, 5)" :key="i">
|
||||
<q-item-section avatar>
|
||||
<q-skeleton type="QAvatar"></q-skeleton>
|
||||
<q-skeleton type="QAvatar" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-skeleton :width="randomInt(10,25)+'%'"></q-skeleton>
|
||||
<q-skeleton :width="randomInt(10,25)+'%'" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
edit form
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup>
|
||||
|
||||
import {onMounted, reactive} from "vue";
|
||||
import {setDefaultData} from "src/composables/form/formHandling";
|
||||
import FormWrapper from "components/FormWrapper.vue";
|
||||
import {commonProps} from "src/composables/inputs";
|
||||
|
||||
const props = defineProps({
|
||||
copilot: {
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
|
||||
const formData = reactive({
|
||||
name: null,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (props.copilot) {
|
||||
setDefaultData(formData, props.copilot);
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormWrapper v-model="formData">
|
||||
<q-input v-bind="commonProps" label="Jméno" v-model="formData.name" />
|
||||
</FormWrapper>
|
||||
</template>
|
||||
@@ -1,5 +1,16 @@
|
||||
<script setup></script>
|
||||
<script setup>
|
||||
import {randomInt} from "src/composables/random";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<em>Copilot skeleton</em>
|
||||
<q-list bordered separator>
|
||||
<q-item v-ripple v-for="i in randomInt(2, 5)" :key="i">
|
||||
<q-item-section avatar>
|
||||
<q-skeleton type="QAvatar"/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-skeleton :width="randomInt(10,25)+'%'"/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</template>
|
||||
|
||||
@@ -16,7 +16,7 @@ const props = defineProps({
|
||||
<q-avatar icon="person" color="primary" text-color="white"/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<router-link :to="{name: 'pilotDetail', params: {id: pilot.id }}">
|
||||
<router-link :to="{name: 'copilotDetail', params: {id: pilot.id }}">
|
||||
{{ pilot.name }}
|
||||
</router-link>
|
||||
</q-item-section>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import {flight} from "src/composables/objects/flight";
|
||||
import {formatDatetime, minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||
import WeatherInfo from "components/weather/WeatherInfo.vue";
|
||||
|
||||
const props = defineProps({
|
||||
flight: {
|
||||
@@ -20,7 +21,11 @@ const props = defineProps({
|
||||
<q-icon name="flight_takeoff"/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
{{ flight.takeoffAirport.name }}
|
||||
<div>
|
||||
{{ flight.takeoffAirport.name }}
|
||||
|
||||
<WeatherInfo class="q-mx-sm" v-if="flight.takeoffWeatherInfo" :weather="flight.takeoffWeatherInfo"/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
@@ -38,7 +43,10 @@ const props = defineProps({
|
||||
<q-icon name="flight_land"/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
{{ flight.landingAirport.name }}
|
||||
<div>
|
||||
{{ flight.landingAirport.name }}
|
||||
<WeatherInfo class="q-mx-sm" v-if="flight.landingWeatherInfo" :weather="flight.landingWeatherInfo"/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
@@ -61,20 +69,12 @@ const props = defineProps({
|
||||
{{ minutesToHumanReadable(flight.durationMinCalculated) }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="thermostat"/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
Clouds: few, Wind: 220/12kt, Temp: 12°C
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="!flight.solo">
|
||||
<q-item v-if="flight.copilot">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="people"/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<router-link :to="{name: 'pilotDetail', params: {id: flight.copilot.id}}">
|
||||
<router-link :to="{name: 'copilotDetail', params: {id: flight.copilot.id}}">
|
||||
{{ flight.copilot.name }}
|
||||
</router-link>
|
||||
</q-item-section>
|
||||
|
||||
@@ -15,34 +15,32 @@ const props = defineProps({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 col-lg-4" v-for="flight of items" :key="flight">
|
||||
<q-list bordered separator class="q-mx-md q-my-md">
|
||||
<q-item v-for="flight of items" :key="flight" ripple clickable :to="{name: 'flightDetail', params:{id: flight.id}}">
|
||||
<q-item-section avatar>
|
||||
|
||||
<q-card class="q-my-sm q-mx-sm" >
|
||||
<q-img :src="flight.coverPhoto.thumbnailUrl" v-if="flight.coverPhoto">
|
||||
<div class="absolute-bottom text-center text-h6">
|
||||
<router-link :to="{name: 'flightDetail', params:{id: flight.id}}" class="text-white">
|
||||
{{ flightName(flight) }}
|
||||
</router-link>
|
||||
</div>
|
||||
</q-img>
|
||||
<q-card-section>
|
||||
<div class="text-h6" v-if="!flight.coverPhoto">
|
||||
<router-link :to="{name: 'flightDetail', params:{id: flight.id}}">
|
||||
{{ flightName(flight) }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="text-subtitle2">{{ minutesToHumanReadable(flight.durationMinCalculated) }}</div>
|
||||
<div class="text-secondary">
|
||||
<q-icon name="today"/>
|
||||
{{ formatDatetime(flight.takeoffDatetime) }}
|
||||
</div>
|
||||
<div class="text-secondary">
|
||||
<q-icon name="plane"/>
|
||||
{{ flight.aircraft.callSign }}
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
<q-avatar size="100px">
|
||||
<div v-if="flight.coverPhoto" :style="`width: 100px; height: 100px; background-image: url('${flight.coverPhoto.thumbnailUrl}'); background-size: cover`"></div>
|
||||
</q-avatar>
|
||||
|
||||
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
{{ flightName(flight) }}
|
||||
<div class="text-secondary">
|
||||
<q-icon name="flight_takeoff"/>
|
||||
{{ formatDatetime(flight.takeoffDatetime) }}
|
||||
</div>
|
||||
<div class="text-secondary">
|
||||
<q-icon name="schedule" />
|
||||
{{ minutesToHumanReadable(flight.durationMinCalculated) }}
|
||||
|
||||
</div>
|
||||
<div class="text-secondary">
|
||||
<q-icon name="flight"/>
|
||||
{{ flight.aircraft.callSign }}
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
<script setup>
|
||||
import {randomInt} from "src/composables/random";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
Skeleton
|
||||
<q-list bordered separator class="q-mx-md q-my-md">
|
||||
<q-item v-for="i in randomInt(5, 10)" :key="i" ripple>
|
||||
<q-item-section avatar>
|
||||
<q-avatar size="100px">
|
||||
<q-skeleton type="QAvatar" size="100px"></q-skeleton>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-skeleton :width="randomInt(30,50)+'%'"/>
|
||||
|
||||
<q-skeleton type="text" :width="randomInt(10,15)+'%'"/>
|
||||
<q-skeleton type="text" :width="randomInt(5,10)+'%'"/>
|
||||
<q-skeleton type="text" width="8%"/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup>
|
||||
import {onMounted, reactive, ref, watch} from "vue";
|
||||
import {computed, onMounted, reactive, ref, watch} from "vue";
|
||||
import FormWrapper from "components/FormWrapper.vue";
|
||||
import {setDefaultData} from "src/composables/form/formHandling";
|
||||
import DatetimePicker from "components/inputs/DatetimePicker.vue";
|
||||
import AirportLocationInput from "components/inputs/AirportLocationInput.vue";
|
||||
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||
import {commonProps} from "src/composables/inputs";
|
||||
|
||||
const props = defineProps({
|
||||
aircrafts: {
|
||||
@@ -21,16 +22,14 @@ const props = defineProps({
|
||||
}
|
||||
)
|
||||
const formData = reactive({
|
||||
aircraftId: 43,
|
||||
takeoffAirportId: null,
|
||||
landingAirportId: null,
|
||||
aircraft: null,
|
||||
takeoffAirport: null,
|
||||
landingAirport: null,
|
||||
takeoffDatetime: null,
|
||||
landingDatetime: null,
|
||||
landings: 1,
|
||||
solo: true,
|
||||
|
||||
name: "",
|
||||
description: ""
|
||||
description: "",
|
||||
})
|
||||
|
||||
const recalculateDuration = (data) => {
|
||||
@@ -47,55 +46,72 @@ const isMounted = ref(false);
|
||||
onMounted(() => {
|
||||
if (props.flight) {
|
||||
setDefaultData(formData, props.flight);
|
||||
formData.aircraft = {id: formData.aircraft.id, name: formData.aircraft.callSign};
|
||||
formData.takeoffAirport = {id: formData.takeoffAirport.id, name: formData.takeoffAirport.name}
|
||||
formData.landingAirport = {id: formData.landingAirport.id, name: formData.landingAirport.name}
|
||||
}
|
||||
isMounted.value = true;
|
||||
});
|
||||
|
||||
const aircrafts = computed(() => {
|
||||
return props.aircrafts.map(a => ({id: a.id, name: a.callSign}));
|
||||
});
|
||||
|
||||
watch(() => formData.takeoffAirport, () => {
|
||||
if (!formData.landingAirport) {
|
||||
formData.landingAirport = {...formData.takeoffAirport};
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormWrapper v-model="formData" v-if="isMounted">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<q-select
|
||||
v-model="formData.aircraftId"
|
||||
option-label="callSign"
|
||||
option-value="id"
|
||||
emit-value
|
||||
map-options
|
||||
:rules="rules.aircraft"
|
||||
<ComboboxInput
|
||||
v-bind="commonProps"
|
||||
:options="aircrafts"
|
||||
v-model="formData.aircraft"
|
||||
:key="formData.aircraft"
|
||||
label="Letadlo"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="flight" />
|
||||
</template>
|
||||
</q-select>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="formData.aircraftId">
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<h3>Vzlet</h3>
|
||||
<DatetimePicker v-model="formData.takeoffDatetime" label="Datum a čas vzletu: "/>
|
||||
<AirportLocationInput v-model="formData.takeoffAirportId" :airports="airports" label="Letiště vzletu" takeoff/>
|
||||
</div>
|
||||
<div class="col col-12 col-md-6" v-if="!!formData.takeoffDatetime && !!formData.takeoffAirportId">
|
||||
|
||||
<h3>Přistání</h3>
|
||||
<DatetimePicker v-model="formData.landingDatetime" label="Datum a čas přistání"/>
|
||||
<AirportLocationInput v-model="formData.landingAirportId" :airports="airports" label="Letiště přistání" landing/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<h3>Vzlet</h3>
|
||||
<DatetimePicker
|
||||
v-model="formData.takeoffDatetime"
|
||||
label="Datum a čas vzletu: "
|
||||
v-bind="commonProps"
|
||||
/>
|
||||
<ComboboxInput
|
||||
:options="airports"
|
||||
v-model="formData.takeoffAirport"
|
||||
label="Letiště vzletu"
|
||||
v-bind="commonProps"
|
||||
:enable-new-values="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
|
||||
<div class="row" v-if="formData.landingAirportId">
|
||||
<div class="col">
|
||||
<q-checkbox v-model="formData.solo" label="Solo?"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<q-input v-model.number="formData.landings" label="Počet přistání" type="number" min="1"/>
|
||||
</div>
|
||||
<h3>Přistání</h3>
|
||||
<DatetimePicker
|
||||
v-model="formData.landingDatetime"
|
||||
:min-date="formData.takeoffDatetime"
|
||||
label="Datum a čas přistání"
|
||||
v-bind="commonProps"
|
||||
/>
|
||||
|
||||
<ComboboxInput
|
||||
:options="airports"
|
||||
v-model="formData.landingAirport"
|
||||
label="Letiště přistání"
|
||||
v-bind="commonProps"
|
||||
:enable-new-values="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</FormWrapper>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import {onMounted, reactive, ref} from "vue";
|
||||
import {onMounted, reactive, ref, watch} from "vue";
|
||||
import FormWrapper from "components/FormWrapper.vue";
|
||||
import {setDefaultData} from "src/composables/form/formHandling";
|
||||
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||
@@ -16,14 +16,20 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const formData = reactive({
|
||||
solo: true,
|
||||
withInstructor: false,
|
||||
copilot: null,
|
||||
})
|
||||
|
||||
const solo = ref(true);
|
||||
watch(() => solo.value, (newVal) => {
|
||||
if (!newVal) {
|
||||
formData.copilot = null;
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (props.flight) {
|
||||
setDefaultData(formData, props.flight);
|
||||
solo.value = !formData.copilot;
|
||||
}
|
||||
})
|
||||
|
||||
@@ -32,9 +38,15 @@ onMounted(() => {
|
||||
<template>
|
||||
<FormWrapper v-model="formData">
|
||||
|
||||
<q-btn-toggle spread no-caps v-model="formData.solo" toggle-color="primary" :options="[{label: 'Solo', value: true}, {label: 'S někým', value: false}]"/>
|
||||
<q-btn-toggle
|
||||
spread
|
||||
no-caps
|
||||
v-model="solo"
|
||||
toggle-color="primary"
|
||||
:options="[{label: 'Solo', value: true}, {label: 'S někým', value: false}]"
|
||||
/>
|
||||
|
||||
<div v-if="!formData.solo">
|
||||
<div v-if="!solo">
|
||||
<ComboboxInput
|
||||
class="q-my-lg"
|
||||
outlined
|
||||
@@ -45,9 +57,6 @@ onMounted(() => {
|
||||
:clearable="true"
|
||||
label="Druhy pilot"
|
||||
/>
|
||||
|
||||
<!-- <q-checkbox v-model="formData.withInstructor" v-if="formData.copilot" label="Instruktor"/>-->
|
||||
<!-- <q-input v-model="formData.picTime" label="PIC time"/>-->
|
||||
</div>
|
||||
|
||||
</FormWrapper>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {onMounted, ref} from "vue";
|
||||
import FormWrapper from "components/FormWrapper.vue";
|
||||
import {setDefaultData} from "src/composables/form/formHandling";
|
||||
import RichTextEditor from "components/inputs/RichTextEditor.vue";
|
||||
import {commonProps} from "src/composables/inputs";
|
||||
|
||||
const props = defineProps({
|
||||
flight: {
|
||||
@@ -28,7 +29,7 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<FormWrapper v-model="formData" v-if="isMounted">
|
||||
<q-input type="text" v-model="formData.name" label="Název"/>
|
||||
<RichTextEditor v-model="formData.description" label="Popis letu, poznámky, ..." />
|
||||
<q-input type="text" v-model="formData.name" label="Jméno letu" v-bind="commonProps" />
|
||||
<RichTextEditor v-model="formData.description" v-bind="commonProps" label="Popis letu, poznámky, ..." />
|
||||
</FormWrapper>
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import FormWrapper from "components/FormWrapper.vue";
|
||||
import {onMounted, reactive, ref, watch} from "vue";
|
||||
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||
import {commonProps} from "src/composables/inputs";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -18,7 +19,8 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const formData = reactive({
|
||||
track: []
|
||||
track: [],
|
||||
gpxTrack: null
|
||||
});
|
||||
const removeTrackItem = (index) => {
|
||||
formData.track.splice(index, 1);
|
||||
@@ -48,16 +50,22 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<FormWrapper v-model="formData">
|
||||
|
||||
<q-file v-model="formData.gpxTrack" v-bind="commonProps" label="Trasa v GPX" hint="Slouží pro vykreslení kompletní a detailní trasy"/>
|
||||
|
||||
<template v-if="flight.gpxTrackUrl">
|
||||
<q-icon name="check" color="green" /> Trasa nahrána
|
||||
</template>
|
||||
|
||||
<ComboboxInput
|
||||
outlined
|
||||
standout
|
||||
class="q-my-lg"
|
||||
v-bind="commonProps"
|
||||
v-model="poi"
|
||||
:key="formData.track.length"
|
||||
:options="locations"
|
||||
@select="addTrackItem"
|
||||
clearable
|
||||
label="Otočný bod"
|
||||
hint="Nastav významné otočné body"
|
||||
/>
|
||||
|
||||
<q-list bordered>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [Number]
|
||||
},
|
||||
airports: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
takeoff: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
landing: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const model = ref(null);
|
||||
const emitModelValue = (val) => {
|
||||
emit("update:modelValue", val);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
model.value = props.modelValue;
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-select
|
||||
v-model="model"
|
||||
emit-value
|
||||
map-options
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
clearable
|
||||
:options="airports"
|
||||
@input="emitModelValue"
|
||||
>
|
||||
<template #prepend v-if="takeoff">
|
||||
<q-icon name="flight_takeoff"/>
|
||||
</template>
|
||||
|
||||
<template #append v-if="landing">
|
||||
<q-icon name="flight_land"/>
|
||||
</template>
|
||||
</q-select>
|
||||
</template>
|
||||
@@ -10,6 +10,11 @@ const props = defineProps({
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
enableNewValues: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
required: true,
|
||||
@@ -17,9 +22,9 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const model = ref({});
|
||||
onMounted(() => {
|
||||
model.value = props.modelValue;
|
||||
})
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
model.value = newVal;
|
||||
}, {immediate: true});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'clear']);
|
||||
watch(model, (val) => {
|
||||
@@ -84,7 +89,7 @@ const filterFn = (val, update, abort) => {
|
||||
:options="availableOptions"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
new-value-mode="add"
|
||||
:new-value-mode="enableNewValues ? 'add' : 'toggle'"
|
||||
clear-icon="clear"
|
||||
:v-bind="$attrs"
|
||||
>
|
||||
|
||||
@@ -10,8 +10,11 @@ const props = defineProps({
|
||||
type: [String, Boolean],
|
||||
required: false,
|
||||
default: FORMAT_DATETIME_LONG
|
||||
},
|
||||
minDate: {
|
||||
type: [String, Date],
|
||||
required: false
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
let dateModel = ref("");
|
||||
@@ -36,6 +39,7 @@ onMounted(() => {
|
||||
emitModelUpdate();
|
||||
}
|
||||
});
|
||||
// TODO: ^^ by asi slo prepsat do watchera, viz ComboboxInput
|
||||
|
||||
|
||||
const publicModel = computed(() => {
|
||||
@@ -92,7 +96,12 @@ const myLocale = {
|
||||
pluralDay: 'dny'
|
||||
}
|
||||
|
||||
// https://quasar.dev/vue-composables/use-form-child - mozna resi ohack s onMounted()?
|
||||
const isDateValid = (date) => {
|
||||
if (!props.minDate) return true;
|
||||
|
||||
const minDate = formatDatetime(props.minDate, "YYYY/MM/DD");
|
||||
return date >= minDate;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -107,10 +116,10 @@ const myLocale = {
|
||||
<div class="proxy-wrapper">
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-6" v-if="showDatePicker">
|
||||
<q-date square v-model="dateModel" :today-btn="true" flat mask="YYYY-MM-DD" :locale="myLocale"/>
|
||||
<q-date square v-model="dateModel" :options="isDateValid" :today-btn="true" flat mask="YYYY-MM-DD" :locale="myLocale"/>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6" v-if="showTimePicker">
|
||||
<q-time square v-model="timeModel" format24h flat mask="HH:mm"/>
|
||||
<q-time square v-model="timeModel" format24h flat mask="HH:mm:ss"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row q-mb-sm">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import {Marker} from "vue3-google-map";
|
||||
import {onMounted, ref} from "vue";
|
||||
import axios from "axios";
|
||||
import MapWrapper from "components/MapWrapper.vue";
|
||||
import MapWrapper from "components/map/MapWrapper.vue";
|
||||
|
||||
const center = ref({
|
||||
lat: 50.1314,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
address: {
|
||||
type: String
|
||||
},
|
||||
gpsLatitude: {
|
||||
type: Number
|
||||
},
|
||||
gpsLongitude: {
|
||||
type: Number
|
||||
},
|
||||
zoom: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 12
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "500px"
|
||||
}
|
||||
});
|
||||
|
||||
const addressEncoded = computed(() => {
|
||||
if (!props.gpsLatitude || !props.gpsLongitude) {
|
||||
return encodeURI(props.address);
|
||||
} else {
|
||||
return `${props.gpsLatitude},${props.gpsLongitude}`
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<iframe frameborder="0" scrolling="no" marginheight="0" marginwidth="0"
|
||||
:src="`https://maps.google.com/maps?q=${addressEncoded}&t=m&z=${zoom}&output=embed&iwloc=near`"
|
||||
:style="`width: 100%; height: ${height}`"
|
||||
:aria-label="address"/>
|
||||
</template>
|
||||
@@ -0,0 +1,71 @@
|
||||
<script setup>
|
||||
import MapWrapper from "components/map/MapWrapper.vue";
|
||||
import {Marker, Polyline, InfoWindow} from "vue3-google-map";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {axios} from "boot/axios";
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
gpxTrackUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
markers: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
photos: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const track = ref([]);
|
||||
const gpxLoading = ref(!!props.gpxTrackUrl);
|
||||
onMounted(() => {
|
||||
if (!props.gpxTrackUrl) {
|
||||
return;
|
||||
}
|
||||
gpxLoading.value = true;
|
||||
axios.get(props.gpxTrackUrl)
|
||||
.then(({data}) => (new window.DOMParser()).parseFromString(data, "text/xml"))
|
||||
.then(doc => {
|
||||
const points = [];
|
||||
const nodes = [...doc.getElementsByTagName('trkpt')];
|
||||
nodes.forEach(node => {
|
||||
const lat = parseFloat(node.getAttribute("lat"));
|
||||
const lng = parseFloat(node.getAttribute("lon"));
|
||||
points.push({lat, lng});
|
||||
})
|
||||
|
||||
track.value = points; // puvodne se bounds bralo z tohodle
|
||||
})
|
||||
.finally(() => gpxLoading.value = false);
|
||||
});
|
||||
|
||||
|
||||
const bounds = computed(() => {
|
||||
const photosTransformed = props.photos.map(p => ({position: {lat: p.gpsLatitude, lng: p.gpsLongitude}}));
|
||||
const allData = [...props.markers, ...track.value, ...photosTransformed];
|
||||
return allData.filter(i => !!i.position).map(i => i.position);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="gpxLoading" v-bind="$attrs">Loading</div>
|
||||
<MapWrapper v-bind="$attrs" :bounds="bounds" v-if="!gpxLoading">
|
||||
<Marker v-for="(marker, idx) in markers" :key="idx" :options="marker"/>
|
||||
|
||||
<Marker v-for="(photo, idx) in photos" :key="idx" :options="{icon: '/mapIcons/photo-16x16.png', position: {lat: photo.gpsLatitude, lng: photo.gpsLongitude}}">
|
||||
<InfoWindow :options="{position: {lat: photo.gpsLatitude, lng: photo.gpsLongitude}}">
|
||||
<img :src="photo.thumbnailUrl" />
|
||||
</InfoWindow>
|
||||
</Marker>
|
||||
|
||||
<template v-if="track.length > 0">
|
||||
<Polyline :options="{path: track, geodesic: true, strokeColor: '#ff0000', strokeOpacity: 0.3, strokeWeight: 5}"/>
|
||||
</template>
|
||||
</MapWrapper>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup>
|
||||
import {GoogleMap} from "vue3-google-map";
|
||||
import {ref, watch} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
bounds: {
|
||||
type: Array,
|
||||
required: false
|
||||
}
|
||||
});
|
||||
|
||||
const key = process.env.GOOGLE_MAPS_API_KEY;
|
||||
|
||||
|
||||
const mapRef = ref(null);
|
||||
watch(
|
||||
() => mapRef.value?.ready,
|
||||
(ready) => {
|
||||
if (!props.bounds || !ready) return;
|
||||
const map = mapRef.value.map;
|
||||
const api = mapRef.value.api;
|
||||
|
||||
const bounds = new api.LatLngBounds();
|
||||
for (let i = 0; i < props.bounds.length; i++) {
|
||||
bounds.extend(props.bounds[i]);
|
||||
}
|
||||
map.fitBounds(bounds);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GoogleMap :api-key="key" v-bind="$attrs" :max-zoom="13" ref="mapRef">
|
||||
<slot/>
|
||||
</GoogleMap>
|
||||
</template>
|
||||
@@ -0,0 +1,105 @@
|
||||
<script setup>
|
||||
import EmbeddedMap from "components/map/EmbeddedMap.vue";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {formatDatetime} from "src/composables/datetimeUtils";
|
||||
|
||||
const props = defineProps({
|
||||
photos: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
openedIndex: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
|
||||
|
||||
const open = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(newVal) {
|
||||
emit("update:modelValue", newVal);
|
||||
}
|
||||
})
|
||||
|
||||
const photoIndex = ref(null);
|
||||
onMounted(() => {
|
||||
photoIndex.value = props.openedIndex
|
||||
});
|
||||
|
||||
const photo = computed(() => props.photos[photoIndex.value]);
|
||||
|
||||
const nextPhoto = () => {
|
||||
if (photoIndex.value < props.photos.length - 1)
|
||||
photoIndex.value++
|
||||
}
|
||||
|
||||
const prevPhoto = () => {
|
||||
if (photoIndex.value > 0) photoIndex.value--;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog v-model="open" full-width @keyup.left="prevPhoto" @keyup.right="nextPhoto">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-12 col-md-9">
|
||||
<q-img :src="photo.url"/>
|
||||
<strong v-if="photo.name !== ''">{{ photo.name }}</strong>
|
||||
|
||||
<p v-if="photo.description !== ''">{{ photo.description }}</p>
|
||||
|
||||
<div class="text-center q-my-sm">
|
||||
<q-btn @click="prevPhoto" v-if="photoIndex > 0" icon="chevron_left" flat rounded>Předchozí</q-btn>
|
||||
<q-btn @click="nextPhoto" v-if="photoIndex < photos.length - 1" icon-right="chevron_right" flat rounded>Další</q-btn>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-12 col-sm-12 col-md-3">
|
||||
<EmbeddedMap class="q-mx-sm" v-if="photo.gpsLatitude && photo.gpsLongitude" :gps-latitude="photo.gpsLatitude" :gps-longitude="photo.gpsLongitude"/>
|
||||
|
||||
<q-list>
|
||||
<q-item v-if="photo.pointOfInterest">
|
||||
<q-item-section avatar>
|
||||
<q-avatar icon="marker"></q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
{{ formatDatetime(photo.exposedAt) }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="photo.exposedAt">
|
||||
<q-item-section avatar>
|
||||
<q-avatar icon="photo_camera"></q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
{{ formatDatetime(photo.exposedAt) }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item v-if="photo.gpsAltitude">
|
||||
<q-item-section avatar>
|
||||
<q-avatar icon="height"/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
{{ photo.gpsAltitude }} m
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {computed, ref} from "vue";
|
||||
import {setDefaultData} from "src/composables/form/formHandling";
|
||||
import DialogWrapper from "components/DialogWrapper.vue";
|
||||
import FormWrapper from "components/FormWrapper.vue";
|
||||
@@ -10,7 +10,7 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(["edit", "delete", "detail"])
|
||||
const emits = defineEmits(["edit", "delete", "openDetail"])
|
||||
|
||||
const openEdit = (photo) => {
|
||||
emits("edit", photo);
|
||||
@@ -20,16 +20,16 @@ const deletePhoto = (id) => {
|
||||
emits("delete", id);
|
||||
};
|
||||
|
||||
const openDetail = (photo) => {
|
||||
emits("detail", photo);
|
||||
const openDetail = (photo, idx) => {
|
||||
emits("openDetail", {photo, idx, allPhotos: props.photos});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-4 col-lg-3" v-for="photo in photos" :key="photo.id">
|
||||
<div class="col-12 col-sm-4 col-md-4 col-lg-3" v-for="(photo, idx) in photos" :key="photo.id">
|
||||
<q-card class="q-my-sm q-mx-sm">
|
||||
<q-img :src="photo.thumbnailUrl" @click="openDetail">
|
||||
<q-img :src="photo.thumbnailUrl" @click="openDetail(photo, idx)">
|
||||
<div class="absolute-bottom">
|
||||
<div class="text-h6" v-if="photo.name !== ''">
|
||||
{{ photo.name }}
|
||||
@@ -40,8 +40,14 @@ const openDetail = (photo) => {
|
||||
</div>
|
||||
</q-img>
|
||||
|
||||
<q-card-actions v-if="showActions">
|
||||
{{ photo.isFlightCover ? 'ano' : 'ne' }}
|
||||
<q-card-actions v-if="showActions" align="right">
|
||||
<div class="col-grow">
|
||||
<q-icon name="star" v-if="photo.isFlightCover" color="red"/>
|
||||
<q-icon name="place" v-if="photo.pointOfInterestId" />
|
||||
<q-icon name="gps_fixed" v-if="photo.gpsLatitude && photo.gpsLongitude" />
|
||||
<q-icon name="gps_off" v-else />
|
||||
</div>
|
||||
|
||||
<q-btn flat icon="edit" color="blue" @click="openEdit(photo)"></q-btn>
|
||||
<q-btn flat icon="delete" color="red" @click="deletePhoto(photo.id)"></q-btn>
|
||||
</q-card-actions>
|
||||
|
||||
@@ -22,7 +22,6 @@ const formData = reactive({
|
||||
});
|
||||
|
||||
const poiNameForMap = ref("");
|
||||
|
||||
onMounted(() => {
|
||||
setDefaultData(formData, props.poi);
|
||||
poiNameForMap.value = props.poi.name;
|
||||
@@ -46,7 +45,6 @@ const handlePositionNotFound = () => {
|
||||
|
||||
<template>
|
||||
<FormWrapper v-model="formData">
|
||||
|
||||
<q-input
|
||||
class="q-my-lg"
|
||||
outlined
|
||||
@@ -72,7 +70,5 @@ const handlePositionNotFound = () => {
|
||||
/>
|
||||
|
||||
<q-checkbox v-model="formData.isPublic" label="Veřejný?"/>
|
||||
|
||||
|
||||
</FormWrapper>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
cloudcover: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-icon name="mdi-weather-sunny" v-if="cloudcover < 20"/>
|
||||
<q-icon name="mdi-weather-partly-cloudy" v-else-if="cloudcover < 75"/>
|
||||
<q-icon name="mdi-weather-cloudy" v-else/>
|
||||
</template>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import CloudCoverIcon from "components/weather/CloudCoverIcon.vue";
|
||||
|
||||
const props = defineProps({
|
||||
weather: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const kmhToKnots = (speedKmh) => {
|
||||
return Math.round(speedKmh / 0.53996);
|
||||
}
|
||||
|
||||
const openDetail = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<q-btn rounded @click="openDetail = true" v-bind="$attrs">
|
||||
<CloudCoverIcon :cloudcover="weather.cloudcoverTotal"/>
|
||||
|
||||
<q-icon name="navigation" :style="`transform: rotate(${weather.windDirectionSurface}deg)`"/>
|
||||
</q-btn>
|
||||
|
||||
<q-dialog v-model="openDetail">
|
||||
<q-card>
|
||||
<q-list>
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<CloudCoverIcon :cloudcover="weather.cloudcoverTotal"/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
Pokrytí oblačností (celkové): {{ weather.cloudcoverTotal }} %
|
||||
<br>
|
||||
Nízké mraky: {{ weather.cloudcoverLow }} %
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
QNH
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
{{ weather.qnh }} hPa
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="air"/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<div :title="`${weather.windSpeedSurface} km/h`">
|
||||
{{ kmhToKnots(weather.windSpeedSurface) }} kt, {{ weather.windDirectionSurface }} °
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="mdi-thermometer"/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
Teplota: {{ weather.temperatureSurface }} ° C
|
||||
<br>
|
||||
Rosný bod: {{ weather.dewpointSurface }} ° C
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
Reference in New Issue
Block a user