Merge pull request #17 from IliaMiheev/develop

Develop
This commit was merged in pull request #17.
This commit is contained in:
Alisa Gerasimova
2026-03-30 13:22:09 +03:00
committed by GitHub
27 changed files with 701 additions and 172 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Probody</title> <title>SkyVue</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/izitoast@1/dist/css/iziToast.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/izitoast@1/dist/css/iziToast.min.css">
</head> </head>
<body> <body>
+12 -2
View File
@@ -1,11 +1,11 @@
{ {
"name": "probody", "name": "SkyVue",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "probody", "name": "SkyVue",
"version": "1.0.0", "version": "1.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -14,6 +14,7 @@
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"electron-squirrel-startup": "^1.0.1", "electron-squirrel-startup": "^1.0.1",
"izitoast": "^1.4.0", "izitoast": "^1.4.0",
"prismjs": "^1.30.0",
"vue": "^3.5.28" "vue": "^3.5.28"
}, },
"devDependencies": { "devDependencies": {
@@ -6832,6 +6833,15 @@
"url": "https://github.com/prettier/prettier?sponsor=1" "url": "https://github.com/prettier/prettier?sponsor=1"
} }
}, },
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/proc-log": { "node_modules/proc-log": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz",
+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "probody", "name": "SkyVue",
"productName": "probody", "productName": "SkyVue",
"version": "1.0.0", "version": "1.0.0",
"description": "My Electron application description", "description": "My Electron application description",
"main": ".vite/build/main.js", "main": ".vite/build/main.js",
@@ -35,6 +35,7 @@
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"electron-squirrel-startup": "^1.0.1", "electron-squirrel-startup": "^1.0.1",
"izitoast": "^1.4.0", "izitoast": "^1.4.0",
"prismjs": "^1.30.0",
"vue": "^3.5.28" "vue": "^3.5.28"
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

+144 -64
View File
@@ -1,17 +1,28 @@
<script setup> <script setup>
import {ref, onMounted, h} from "vue"; import {ref, onMounted, h} from "vue";
import izitoast from "./shared/izitoast"; import izitoast from "@/shared/izitoast";
import {useEventListener} from "@vueuse/core"; import {useEventListener} from "@vueuse/core";
import MyDialog from "./components/MyDialog.vue"; import MyDialog from "@/components/MyDialog.vue";
import MyDropMenu from "./components/MyDropMenu.vue"; import MyDropMenu from "@/components/MyDropMenu.vue";
import copyToclipboard from "./shared/copyToclipboard"; import copyToclipboard from "@/shared/copyToclipboard";
import ContextMenu from "@imengyu/vue3-context-menu"; import ContextMenu from "@imengyu/vue3-context-menu";
import CodeView from "@/components/CodeView.vue";
import emergencyAndNavigate from '@/shablons/emergency_and_navigate.js'
import color from '@/shablons/color.js'
import qr from '@/shablons/qr.js'
import Preview from "@/components/UI/svg/Preview.vue";
import Save from "@/components/UI/svg/Save.vue";
import Trash from "@/components/UI/svg/Trash.vue";
import Back from "@/components/UI/svg/Back.vue";
const numbers = ref([]); // Инициализируем массив с пустыми строками const numbers = ref([]); // Инициализируем массив с пустыми строками
const clickCount = ref(1); // Счетчик нажатий const clickCount = ref(1); // Счетчик нажатий
const listOfTrips = ref([]); const listOfTrips = ref([]);
const isModalVisible = ref(false); const isModalVisible = ref(false);
let finallyText = ref(""); const isCodeViewVisible = ref(false);
const finallyText = ref("");
const dropMenu = ref(null)
onMounted(() => { onMounted(() => {
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
@@ -49,7 +60,24 @@ function setNumber(item) {
} }
} }
async function saveResult() { function openPreview() {
generateCode()
if (!listOfTrips.value.length) {
izitoast.error({
title: 'Ошибка',
message: 'Для предпросмотра нужно создать маршрут'
})
return
}
isCodeViewVisible.value = true;
}
function saveResult() {
isModalVisible.value = true;
generateCode()
}
function generateCode() {
finallyText.value = ""; finallyText.value = "";
const mainCode = ref(""); const mainCode = ref("");
const colorDetectClass = ref(""); const colorDetectClass = ref("");
@@ -59,17 +87,18 @@ async function saveResult() {
const isUsedPlatform = listOfTrips.value.some(item => item.isPlatform); const isUsedPlatform = listOfTrips.value.some(item => item.isPlatform);
const isUsedQR = listOfTrips.value.some(item => item.isQR); const isUsedQR = listOfTrips.value.some(item => item.isQR);
if (!listOfTrips.value.length) {
return
}
if (isUsedPlatform) { if (isUsedPlatform) {
const response = await fetch("/shablons/color.txt") let textOfCode = color
let textOfCode = await response.text()
textOfCode = textOfCode.split('<<separator>>') textOfCode = textOfCode.split('<<separator>>')
colorDetectClass.value = textOfCode[0] colorDetectClass.value = textOfCode[0]
colorDetectCode.value += `\t${textOfCode[1].trim()}\n`; colorDetectCode.value += `\t${textOfCode[1].trim()}\n`;
} }
if (isUsedQR) { if (isUsedQR) {
const response = await fetch("/shablons/qr.txt") let textOfCode = qr
let textOfCode = await response.text()
textOfCode = textOfCode.split('<<separator>>') textOfCode = textOfCode.split('<<separator>>')
QRDetectClass.value = textOfCode[0] QRDetectClass.value = textOfCode[0]
QRDetectCode.value += `\t${textOfCode[1].trim()}\n`; QRDetectCode.value += `\t${textOfCode[1].trim()}\n`;
@@ -84,8 +113,7 @@ async function saveResult() {
mainCode.value += QRDetectCode.value + '\n' mainCode.value += QRDetectCode.value + '\n'
} }
} }
const response = await fetch("/shablons/emergency_and_navigate.txt") let textOfCode = emergencyAndNavigate
let textOfCode = await response.text()
finallyText.value = textOfCode.replace("<<main>>", mainCode.value); finallyText.value = textOfCode.replace("<<main>>", mainCode.value);
if (isUsedPlatform) { if (isUsedPlatform) {
finallyText.value = finallyText.value.replace("<<ColorDetect>>", colorDetectClass.value); finallyText.value = finallyText.value.replace("<<ColorDetect>>", colorDetectClass.value);
@@ -97,13 +125,15 @@ async function saveResult() {
} else { } else {
finallyText.value = finallyText.value.replace("<<qr_function>>", ''); finallyText.value = finallyText.value.replace("<<qr_function>>", '');
} }
isModalVisible.value = true;
} }
function cancel() { function cancel() {
if (clickCount.value === 1) { if (clickCount.value === 1) {
return; return;
} }
if (listOfTrips.value.at(-1).variable.length === 1) {
listOfTrips.value.at(-1).BgImg = 'images/ArUco-marker.jpg'
}
listOfTrips.value.at(-1).variable.pop(); listOfTrips.value.at(-1).variable.pop();
listOfTrips.value.pop(); listOfTrips.value.pop();
clickCount.value--; clickCount.value--;
@@ -111,13 +141,17 @@ function cancel() {
function cleanResult() { function cleanResult() {
clickCount.value = 1; clickCount.value = 1;
listOfTrips.value.forEach(item => { numbers.value.forEach(item => {
item.isPlatform = false; item.isPlatform = false;
item.isQR = false; item.isQR = false;
item.variable = []; item.variable = [];
item.BgImg = 'images/ArUco-marker.jpg' item.BgImg = 'images/ArUco-marker.jpg'
}) })
listOfTrips.value = []; listOfTrips.value = []
izitoast.info({
title: 'Поле очищено',
message: 'Поле очищено. Можно строить маршрут заново',
})
} }
// Обрабатываем нажатие Ctrl + Z // Обрабатываем нажатие Ctrl + Z
@@ -125,6 +159,15 @@ const handleKeydown = (event) => {
if (event.ctrlKey && event.code === "KeyZ" && listOfTrips.value.length) { if (event.ctrlKey && event.code === "KeyZ" && listOfTrips.value.length) {
event.preventDefault(); event.preventDefault();
cancel(); cancel();
} else if (event.ctrlKey && event.code === "KeyS") {
event.preventDefault();
saveResult();
} else if (event.ctrlKey && event.code === "KeyN") {
event.preventDefault();
dropMenu.value.open();
} else if (event.code === "Delete") {
event.preventDefault();
cleanResult();
} }
}; };
useEventListener("keydown", handleKeydown); useEventListener("keydown", handleKeydown);
@@ -173,8 +216,10 @@ function createPlatform(item, color) {
} }
function deletePlatform(item) { function deletePlatform(item) {
item.BgImg = 'images/ArUco-marker.jpg' if (item.isPlatform) {
item.isPlatform = false item.BgImg = 'images/ArUco-marker.jpg'
item.isPlatform = false
}
} }
function createQR(item) { function createQR(item) {
@@ -189,8 +234,10 @@ function createQR(item) {
} }
function deleteQR(item) { function deleteQR(item) {
item.BgImg = 'images/ArUco-marker.jpg' if (item.isQR) {
item.isQR = true item.BgImg = 'images/ArUco-marker.jpg'
item.isQR = false
}
} }
function deletePointMap(item) { function deletePointMap(item) {
@@ -214,6 +261,7 @@ function deletePointMap(item) {
item.BgImg = 'images/ArUco-marker.jpg' item.BgImg = 'images/ArUco-marker.jpg'
} }
} }
function createIcon(src) { function createIcon(src) {
return h('img', { return h('img', {
src: src, src: src,
@@ -223,6 +271,7 @@ function createIcon(src) {
} }
}) })
} }
function onContextMenu(e, item) { function onContextMenu(e, item) {
ContextMenu.showContextMenu({ ContextMenu.showContextMenu({
theme: 'mac dark', theme: 'mac dark',
@@ -251,7 +300,7 @@ function onContextMenu(e, item) {
{ {
label: "Удалить", label: "Удалить",
onClick: () => deletePlatform(item), onClick: () => deletePlatform(item),
icon: createIcon('icons/trash.webp'), icon: createIcon('icons/trash.svg'),
}, },
] ]
}, },
@@ -267,14 +316,14 @@ function onContextMenu(e, item) {
{ {
label: "Удалить", label: "Удалить",
onClick: () => deleteQR(item), onClick: () => deleteQR(item),
icon: createIcon('icons/trash.webp') icon: createIcon('icons/trash.svg')
}, },
], ],
}, },
{ {
label: "Удалить", label: "Удалить",
icon: createIcon('icons/trash.webp'), icon: createIcon('icons/trash.svg'),
onClick: () => deletePointMap(item), onClick: () => deletePointMap(item),
}, },
] ]
@@ -283,63 +332,71 @@ function onContextMenu(e, item) {
</script> </script>
<template> <template>
<div class="wraper"> <div v-if="!isCodeViewVisible" class="wraper">
<div class="grid"> <div class="grid">
<div class="square" v-for="(item, index) in numbers" :data-key="index" :key="index" <div class="square" v-for="(item, index) in numbers" :data-key="index" :key="index" @click="setNumber(item)"
@click="setNumber(item)" @contextmenu.prevent="onContextMenu($event, item)" @contextmenu.prevent="onContextMenu($event, item)" :style="{ backgroundImage: `url(${item.BgImg})` }">
:style="{backgroundImage: `url(${item.BgImg})`}">
{{ numbers[index].variable.join(", ") }} {{ numbers[index].variable.join(", ") }}
</div> </div>
</div> </div>
<div class="actions"> <div class="actions">
<button @click="saveResult">Сохранить</button> <Save @click="saveResult"/>
<button @click="cleanResult">Очистить</button> <Trash @click="cleanResult"/>
<button @click="cancel">Отменить</button> <Back @click="cancel"/>
<Preview @click="openPreview"/>
</div> </div>
<MyDropMenu/> <MyDropMenu ref="dropMenu"/>
<MyDialog v-model:show="isModalVisible" class="dialogWindow" :is-success=Boolean(listOfTrips.length)>
<template #header>
<strong v-if="listOfTrips.length">Код готов!</strong>
<strong v-else>Код не готов!</strong>
</template>
<main>
<p v-if="listOfTrips.length">Скопируйте или скачайте ваш код</p>
<p v-else>Выстройте маршрут перед сохранением кода.</p>
</main>
<template #footer>
<div v-if="listOfTrips.length">
<button @click="isCodeViewVisible = true">Предпросмотр</button>
<button @click="handleDownloadBtn">Скачать код</button>
<button @click="copyAndToast(finallyText)">Скопировать код</button>
</div>
</template>
</MyDialog>
</div> </div>
<CodeView v-model:show="isCodeViewVisible" :code="finallyText" :show="isCodeViewVisible"/>
<MyDialog v-show="isModalVisible" @close="isModalVisible = false" class="dialogWindow"
:is-success=Boolean(listOfTrips.length)>
<template #header>
<strong v-if="listOfTrips.length">Код готов!</strong>
<strong v-else>Код не готов!</strong>
</template>
<template #body>
<p v-if="listOfTrips.length">Скопируйте или скачайте ваш код</p>
<p v-else>Выстройте маршрут перед сохранением кода.</p>
</template>
<template #footer>
<div v-if="listOfTrips.length">
<button @click="handleDownloadBtn">Скачать код</button>
<button @click="copyAndToast(finallyText)">Скопировать код</button>
<button @click="isModalVisible = false">ОК</button>
</div>
</template>
</MyDialog>
</template> </template>
<style> <style>
* {
font-family: 'Courier New', Courier, monospace;
}
html { html {
background-image: radial-gradient(#383838e2, #222222); background-image: radial-gradient(#23aae3e2, #222222);
font-size: 18px;
}
body {
margin: 0;
} }
:root { :root {
--side: 70px; --side: 75px;
} }
.wraper { .wraper {
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: 20px 0 80px 0;
} }
.grid { .grid {
display: grid; display: grid;
padding: 3px; padding: 3px;
grid-template-columns: repeat(10, var(--side)); grid-template-columns: repeat(10, var(--side));
gap: 10px; gap: 5px;
user-select: none; user-select: none;
} }
@@ -356,11 +413,11 @@ html {
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
color: white; color: white;
font-size: 18px;
text-shadow: -1px -1px 0 black, text-shadow: -1px -1px 0 black,
1px -1px 0 black, 1px -1px 0 black,
-1px 1px 0 black, -1px 1px 0 black,
1px 1px 0 black; 1px 1px 0 black;
box-sizing: border-box;
} }
.square:hover { .square:hover {
@@ -380,19 +437,42 @@ html {
} }
.actions { .actions {
margin-left: 10px; position: fixed;
bottom: 10px;
display: flex; display: flex;
flex-direction: column; gap: 10px;
padding: 10px 20px;
border-radius: 10px;
background-color: #2f2f2f;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(5px);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
border: 2px solid rgba(255, 255, 255, 0.18);
}
.actions svg {
width: 40px;
height: 40px;
transition: color 0.4s;
}
.actions svg:hover {
color: rgba(20, 104, 140, 0.89);
}
.actions svg:active {
transition: color 0s;
color: #23aae3e2;
}
path {
fill: currentColor;
} }
.actions button { .actions button {
border: 2px solid rgb(251, 251, 251); padding: 20px 35px;
padding: 15px 30px; /* background-color: red; */
font-size: 15px;
background-color: #383838;
color: white;
cursor: pointer;
margin: 5px;
transition: 0.3s;
} }
</style> </style>
+32
View File
@@ -0,0 +1,32 @@
<template>
<pre><code class="language-python" v-html="highlighted"></code></pre>
</template>
<script setup>
import {ref, onMounted} from 'vue'
import Prism from 'prismjs'
import 'prismjs/components/prism-python'
const props = defineProps({
code: {type: String, required: true},
})
const highlighted = ref('')
function highlight() {
// безопасно экранируем HTML и подсвечиваем через Prism
const escaped = props.code
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
highlighted.value = Prism.highlight(escaped, Prism.languages['python'], 'python')
}
onMounted(highlight)
</script>
<style scoped>
pre {
padding-left: 10px;
}
</style>
+42
View File
@@ -0,0 +1,42 @@
<script setup>
import CustomButton from '@/components/UI/CustomButton.vue'
import CodeBlock from "./CodeBlock.vue";
import MovementButtons from "./MovementButtons.vue";
const props = defineProps({
code: {
required: true,
type: String
},
show: {
type: Boolean,
default: false
}
})
</script>
<template>
<div class="code-view" v-if="show">
<CustomButton
@click.stop="$emit('update:show', false)">
Вернуться к сохранению сгенерированного кода
</CustomButton>
<CodeBlock :code="code" />
<CustomButton
@click.stop="$emit('update:show', false)">
Вернуться к сохранению сгенерированного кода
</CustomButton>
<MovementButtons/>
</div>
</template>
<style scoped>
.code-view {
background-color: #222222;
overflow: auto;
color: #eeeeee;
width: 100%;
height: 100%;
z-index: 999;
}
</style>
+62
View File
@@ -0,0 +1,62 @@
<template>
<div class="movement-buttons">
<button @click="scrollToTop" class="up"></button>
<button @click="scrollToBottom" class="down"></button>
</div>
</template>
<script setup>
function scrollToTop() {
window.scrollTo({
top: 0,
behavior: 'smooth' // Плавная прокрутка
});
}
function scrollToBottom() {
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth'
});
}
</script>
<style scoped>
:root {
--btnHeight: 50px;
}
.movement-buttons {
display: flex;
gap: 2px;
flex-direction: column;
position: fixed;
right: 15px;
bottom: calc(50% - 50px);
}
.movement-buttons > button {
width: auto;
font-size: 15px;
border: 2px solid #d0d0d0;
color: white;
background-color: #383838;
height: 50px;
}
@media (max-width: 900px) {
.movement-buttons > button {
width: 30px;
font-size: 15px;
}
}
.up {
border-radius: 10px 10px 0 0;
}
.down {
border-radius: 0 0 10px 10px;
}
</style>
+71 -14
View File
@@ -1,21 +1,22 @@
<template> <template>
<transition name="modal-fade"> <transition name="modal-fade">
<div class="modal-backdrop" @click="closeModal"> <div class="modal-backdrop" v-if="show" @click.stop="onClose">
<div class="modal" @click.stop> <div class="modal" @click.stop>
<header :class="['modal-header', isSuccess ? 'modal-header-success': 'modal-header-error']"> <header :class="['modal-header', isSuccess ? 'modal-header-success' : 'modal-header-error']">
<slot name="header"> <slot name="header">
<strong>Заголовок по умолчанию</strong> <strong>Заголовок по умолчанию</strong>
</slot> </slot>
<button class="modal-btn-close" @click="closeModal" aria-label="Закрыть модальное окно"> <button class="modal-btn-close" @click.stop="onClose"
aria-label="Закрыть модальное окно">
× ×
</button> </button>
</header> </header>
<section class="modal-body"> <section class="modal-body">
<slot name="body">Тело по умолчанию</slot> <slot>Тело по умолчанию</slot>
</section> </section>
<footer :class="['modal-footer', isSuccess ? 'modal-footer-success': 'modal-footer-error']"> <footer :class="['modal-footer', isSuccess ? 'modal-footer-success' : 'modal-footer-error']">
<slot name="footer"> <slot name="footer">
<button @click="closeModal">Закрыть</button> <button @click.stop="onClose">Закрыть</button>
</slot> </slot>
</footer> </footer>
</div> </div>
@@ -24,20 +25,42 @@
</template> </template>
<script setup> <script setup>
const emit = defineEmits(['close']) import {defineEmits} from 'vue'
const props = defineProps({ import {useEventListener} from "@vueuse/core";
const emit = defineEmits({
close: null,
'update:show': false,
})
defineProps({
isSuccess: { isSuccess: {
type: Boolean, type: Boolean,
required: true default: true,
},
show: {
type: Boolean,
default: false
} }
}) })
function closeModal() { function onClose() {
emit('close'); emit('update:show')
emit('close')
} }
const handleKeydown = (event) => {
if (event.code === "Escape") {
emit('update:show')
}
}
useEventListener("keydown", handleKeydown);
</script> </script>
<style> <style>
:root {
--successColor: #4ee882
}
.modal-backdrop { .modal-backdrop {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -48,6 +71,7 @@ function closeModal() {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 998;
} }
.modal { .modal {
@@ -73,7 +97,7 @@ function closeModal() {
} }
.modal-header-success { .modal-header-success {
color: #4AAE9B; color: var(--successColor);
} }
.modal-header-error { .modal-header-error {
@@ -94,11 +118,17 @@ function closeModal() {
justify-content: flex-end; justify-content: flex-end;
border-top: 1px solid white; border-top: 1px solid white;
padding: 5px; padding: 5px;
color: white; color: black;
font-size: 15px;
border-radius: 5px;
}
.modal-footer a {
border-radius: 5px;
} }
.modal-footer-success button { .modal-footer-success button {
background-color: #4AAE9B; background-color: var(--successColor);
border: none; border: none;
margin: 3px; margin: 3px;
} }
@@ -123,4 +153,31 @@ function closeModal() {
color: red; color: red;
background: transparent; background: transparent;
} }
/* Начало появления: ниже и прозрачный */
.modal-fade-enter-from {
opacity: 0;
}
/* Конец появления: на месте и видим */
.modal-fade-enter-to {
opacity: 1;
}
/* Начало исчезания: на месте и видим */
.modal-fade-leave-from {
opacity: 1;
}
/* Конец исчезания: выше и прозрачный */
.modal-fade-leave-to {
opacity: 0;
}
/* Общие настройки анимации (скорость + плавность) */
.modal-fade-enter-active,
.modal-fade-leave-active {
transition: transform 0.5s cubic-bezier(.22, .9, .31, 1), opacity 0.5s ease;
will-change: transform, opacity;
}
</style> </style>
+159 -79
View File
@@ -1,120 +1,200 @@
<template> <template>
<div class="navigation-wrapper"> <div class="navigation-wrapper">
<!-- Кнопка Бургер --> <!-- Кнопка Бургер -->
<button <button class="burger-btn" @click="toggleMenu" :class="{ 'is-active': isOpen }" aria-label="Menu">
class="burger-btn" <span class="line"></span>
@click="toggleMenu" <span class="line"></span>
:class="{ 'is-active': isOpen }" <span class="line"></span>
aria-label="Menu" </button>
>
<span class="line"></span>
<span class="line"></span>
<span class="line"></span>
</button>
<!-- Затемнение заднего фона (Overlay) --> <!-- Затемнение заднего фона (Overlay) -->
<transition name="fade"> <transition name="fade">
<div v-if="isOpen" class="overlay" @click="closeMenu"></div> <div v-if="isOpen" class="overlay" @click="closeMenu"></div>
</transition> </transition>
<!-- Выпадающая панель --> <!-- Выпадающая панель -->
<transition name="slide"> <transition name="slide">
<aside v-if="isOpen" class="side-panel"> <aside v-if="isOpen" class="side-panel">
<div class="menu-header"> <div class="menu-header">
<h1>SkyVue</h1> <h1>SkyVue</h1>
</div> </div>
<nav class="menu-content"> <nav class="menu-content">
<a href="https://clover.coex.tech/ru/simple_offboard.html" target="_blank">COEX Автономный полет</a> <custom-button
<slot> @click="openInfoCard(item)"
<p class="placeholder">Здесь будут ваши ссылки...</p> v-for="item in informationForDirectory"
</slot> >
</nav> {{ item.title }}
</aside> </custom-button>
</transition> </nav>
</div> </aside>
</transition>
<MyDialog isSuccess v-model:show="isModalVisible" @close="handleOkBtn">
<template #header>
<strong>{{ infoCard.title }}</strong>
</template>
<main>
<p>{{ infoCard.body }}</p>
</main>
<template #footer>
<div class="navigation-wrapper__footer">
<custom-button :to="infoCard.hrefToCourse" title="Посмотреть на Stepik">Курс</custom-button>
<custom-button :to="infoCard.hrefToDoc" title="Посмотреть в официальной документации">Документация
</custom-button>
<custom-button @click="handleOkBtn">ОК</custom-button>
</div>
</template>
</MyDialog>
</div>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import {ref} from 'vue';
import MyDialog from "@/components/MyDialog.vue";
import CustomButton from '@/components/UI/CustomButton.vue'
import informationForDirectory from "../data/informationForDirectory";
import {useEventListener} from "@vueuse/core";
const isOpen = ref(false); const isOpen = ref(false);
const isModalVisible = ref(false);
const infoCard = ref({})
function handleOkBtn() {
isModalVisible.value = false
isOpen.value = true
}
const toggleMenu = () => { const toggleMenu = () => {
isOpen.value = !isOpen.value; isOpen.value = !isOpen.value;
}; };
const closeMenu = () => { const closeMenu = () => {
isOpen.value = false; isOpen.value = false;
}; };
function openInfoCard(obj) {
isModalVisible.value = true
isOpen.value = false
infoCard.value = obj
}
const handleKeydown = (event) => {
if (event.code === "Escape" && isOpen.value) {
event.preventDefault();
closeMenu();
}
};
useEventListener("keydown", handleKeydown);
// Экспортируем методы, если захотим управлять меню извне // Экспортируем методы, если захотим управлять меню извне
defineExpose({ open: () => (isOpen.value = true), close: closeMenu }); defineExpose({open: () => (isOpen.value = true), close: closeMenu});
</script> </script>
<style scoped> <style scoped>
.burger-btn { .burger-btn {
position: fixed; /* Она всегда на одном месте при скролле */ position: fixed;
top: 20px; top: 20px;
left: 20px; left: 20px;
z-index: 9999; /* Максимальный приоритет, чтобы быть ПОВЕРХ всего */ z-index: 3;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-around; justify-content: space-around;
width: 40px; width: 40px;
height: 30px; height: 30px;
background: rgba(255, 255, 255, 0.508); background: rgba(255, 255, 255, 0.508);
border: 1px solid #383838; border: 1px solid #383838;
cursor: pointer; cursor: pointer;
padding: 5px; padding: 5px;
} }
h1{
text-align: center; h1 {
text-align: center;
} }
.menu-content {
display: flex;
flex-direction: column;
gap: 10px;
}
.line { .line {
width: 100%; width: 100%;
height: 4px; height: 4px;
background-color: #383838; /* Ярко-красный цвет для теста */ background-color: #383838;
transition: all 0.3s ease; /* Ярко-красный цвет для теста */
transition: all 0.3s ease;
} }
/* Анимация бургера в крестик */ /* Анимация бургера в крестик */
.is-active .line:nth-child(1) { transform: translateY(6px) rotate(45deg); } .is-active .line:nth-child(1) {
.is-active .line:nth-child(2) { opacity: 0; } transform: translateY(6px) rotate(45deg);
.is-active .line:nth-child(3) { transform: translateY(-6px) rotate(-45deg); } }
.is-active .line:nth-child(2) {
opacity: 0;
}
.is-active .line:nth-child(3) {
transform: translateY(-6px) rotate(-45deg);
}
/* Боковая панель */ /* Боковая панель */
.side-panel { .side-panel {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 300px; width: 25%;
height: 100vh; height: 100%;
background: white; background-image: radial-gradient(#23aae3e2, #222222);
z-index: 1000; color: white;
padding: 0 20px 20px; z-index: 2;
padding: 0 20px 20px;
overflow-y: scroll;
overflow: auto;
border-right: white 2px solid;
} }
/* Затемнение фона */ /* Затемнение фона */
.overlay { .overlay {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
z-index: 999; z-index: 2;
background-color: #333333;
opacity: 90%;
display: flex;
}
main {
text-align: justify;
}
.navigation-wrapper__footer {
display: flex;
gap: 10px;
width: 100%;
} }
/* Анимации появления */ /* Анимации появления */
.slide-enter-active, .slide-leave-active { transition: transform 0.3s ease; } .slide-enter-active,
.slide-enter-from, .slide-leave-to { transform: translateX(-100%); } .slide-leave-active {
transition: transform 0.39s ease;
}
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; } .slide-enter-from,
.fade-enter-from, .fade-leave-to { opacity: 0; } .slide-leave-to {
transform: translateX(-100%);
}
.placeholder { .fade-enter-active,
color: #00e44c; .fade-leave-active {
/* цвет текста */ transition: opacity 0.39s ease;
font-style: italic; }
.fade-enter-from,
.fade-leave-to {
opacity: 0;
} }
</style> </style>
+43
View File
@@ -0,0 +1,43 @@
<script setup>
const props = defineProps({
isSuccess: {
type: Boolean,
required: false
},
to: {
type: String,
default: null
}
})
</script>
<template>
<a v-if="to" :href="props.to" target="_blank" class="btn">
<slot />
</a>
<button v-else class="btn">
<slot />
</button>
</template>
<style scoped>
.btn {
border: 2px solid white;
cursor: pointer;
transition: 0.3s;
padding: 5px;
background-color: #383838;
color: white;
width: 100%;
font-size: 17px;
text-decoration: none;
box-sizing: border-box;
text-align: center;
font-family: 'Courier New', Courier, monospace;
margin: 0;
}
.success {
background-color: var(--successColor);
}
</style>
+6
View File
@@ -0,0 +1,6 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M7.29292 11.7071L2.58582 7L7.29292 2.29289L8.70714 3.70711L6.41424 6H14C17.866 6 21 9.13401 21 13C21 16.866 17.866 20 14 20H6V18H14C16.7614 18 19 15.7614 19 13C19 10.2386 16.7614 8 14 8H6.41424L8.70714 10.2929L7.29292 11.7071Z"
fill="black"/>
</svg>
</template>
+7
View File
@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
<g id="Reading_Mode" data-name="Reading Mode">
<path d="M12,20C6,20,1.35,12.84,1.16,12.54a1,1,0,0,1,0-1.08C1.35,11.16,6,4,12,4s10.65,7.16,10.84,7.46a1,1,0,0,1,0,1.08C22.65,12.84,18,20,12,20ZM3.22,12c1.14,1.56,4.72,6,8.78,6s7.65-4.43,8.78-6C19.64,10.44,16.06,6,12,6S4.35,10.43,3.22,12ZM12,16a4,4,0,1,1,4-4A4,4,0,0,1,12,16Zm0-6a2,2,0,1,0,2,2A2,2,0,0,0,12,10Z"/>
</g>
</svg>
</template>
+7
View File
@@ -0,0 +1,7 @@
<template>
<svg data-name="Livello 1" id="Livello_1" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<title/>
<path d="M61.88,93.12h0a3,3,0,0,0,.44.36l.24.13a1.74,1.74,0,0,0,.59.24l.25.07h0a3,3,0,0,0,1.16,0l.26-.08.3-.09a3,3,0,0,0,.3-.16l.21-.12a3,3,0,0,0,.46-.38L93,66.21A3,3,0,1,0,88.79,62L67,83.76V3a3,3,0,0,0-6,0V83.76L39.21,62A3,3,0,0,0,35,66.21Z"/>
<path d="M125,88a3,3,0,0,0-3,3v22a9,9,0,0,1-9,9H15a9,9,0,0,1-9-9V91a3,3,0,0,0-6,0v22a15,15,0,0,0,15,15h98a15,15,0,0,0,15-15V91A3,3,0,0,0,125,88Z"/>
</svg>
</template>
+22
View File
@@ -0,0 +1,22 @@
<script setup>
</script>
<template>
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<title/>
<g data-name="Layer 2" id="Layer_2">
<path d="M20,29H12a5,5,0,0,1-5-5V12a1,1,0,0,1,2,0V24a3,3,0,0,0,3,3h8a3,3,0,0,0,3-3V12a1,1,0,0,1,2,0V24A5,5,0,0,1,20,29Z"/>
<path d="M26,9H6A1,1,0,0,1,6,7H26a1,1,0,0,1,0,2Z"/>
<path d="M20,9H12a1,1,0,0,1-1-1V6a3,3,0,0,1,3-3h4a3,3,0,0,1,3,3V8A1,1,0,0,1,20,9ZM13,7h6V6a1,1,0,0,0-1-1H14a1,1,0,0,0-1,1Z"/>
<path d="M14,23a1,1,0,0,1-1-1V15a1,1,0,0,1,2,0v7A1,1,0,0,1,14,23Z"/>
<path d="M18,23a1,1,0,0,1-1-1V15a1,1,0,0,1,2,0v7A1,1,0,0,1,18,23Z"/>
</g>
<g id="frame">
<rect class="cls-1" height="32" width="32"/>
</g>
</svg>
</template>
<style scoped>
.cls-1{fill:none;}
</style>
+74
View File
@@ -0,0 +1,74 @@
export default [
{
title: 'ROS',
body: 'ROS (Robot Operating System) — экосистема для программирования роботов. В приложении через ROS дрон получает команды, передает телеметрию и данные с камеры.',
hrefToDoc: 'https://www.ros.org/',
hrefToCourse: 'https://stepik.org/lesson/2260739/step/1?unit=2294748'
},
{
title: 'OpenCV',
body: 'OpenCV (Open Source Computer Vision Library) — библиотека компьютерного зрения. С её помощью дрон распознает цвета платформ и считывает QR-коды.',
hrefToDoc: 'https://opencv.org/',
hrefToCourse: 'https://stepik.org/lesson/2272700/step/1?unit=2306893'
},
{
title: 'navigate_wait',
body: 'Navigate_wait — функция из библиотеки Clover. Отправляет дрон в точку и ждёт, пока он туда долетит, и только потом выполняет следующую команду. Это важно, потому что обычная команда navigate не ждет: дрон ещё летит, а скрипт уже бежит дальше — из-за этого следующие команды могут выполняться раньше времени.',
hrefToDoc: 'https://clover.coex.tech/ru/snippets.html',
hrefToCourse: 'https://stepik.org/lesson/2264712/step/1?unit=2298803'
},
{
title: 'Нода (Node)',
body: 'Нода (Node/узел) в ROS - исполняемая программа, которая выполняет одну конкретную задачу. В сгенерированном коде вся логика полета и обработки камеры находится внутри одной ноды. Это позволяет запускать, останавливать и отлаживать программу как единое целое. ',
hrefToDoc: 'https://docs.voltbro.ru/starting-ros/basics.html',
hrefToCourse: 'https://stepik.org/lesson/2261765/step/1?unit=2295758'
},
{
title: 'Топик (Topic)',
body: 'Топик (Topic) в ROS — канал обмена данными. Например, камера публикует изображение в топик, а ваш скрипт подписывается на него и получает кадры.',
hrefToDoc: 'https://docs.voltbro.ru/starting-ros/basics.html',
hrefToCourse: 'https://stepik.org/lesson/2262844/step/1?unit=2296866'
},
{
title: 'Издатель (Publisher)',
body: 'Издатель (Publisher) в ROS — часть программы, которая отправляет данные в топик. Например, если нужно передать команду другому узлу или опубликовать координаты дрона — создается издатель, который публикует сообщения в нужный канал. Подписчики других узлов могут эти данные получать. ',
hrefToDoc: 'https://docs.voltbro.ru/starting-ros/basics.html',
hrefToCourse: 'https://stepik.org/lesson/2262844/step/1?unit=2296866'
},
{
title: 'Подписчик (Subscriber)',
body: 'Подписчик (Subscriber) в ROS — часть программы, которая получает данные из топика. Например, чтобы дрон "видел" изображение с камеры или считывал свои координаты, создаётся подписчик на соответствующий топик. Как только в топике появляются новые данные, подписчик автоматически их обрабатывает.',
hrefToDoc: 'https://docs.voltbro.ru/starting-ros/basics.html',
hrefToCourse: 'https://stepik.org/lesson/2262844/step/1?unit=2296866'
},
{
title: 'Сервис',
body: 'Сервис в ROS - способ вызвать функцию робота и сразу получить ответ. В отличие от топика, где данные идут постоянно, сервис работает по принципу "запрос — ответ". Например, команда navigate — это сервис: вы отправляете координаты, а робот отвечает "команда принята". Команда get_telemetry возвращает текущие координаты дрона.',
hrefToDoc: 'https://docs.voltbro.ru/starting-ros/basics.html',
hrefToCourse: 'https://stepik.org/lesson/2263182/step/1?unit=2297206'
},
{
title: 'Системы координат',
body: 'Определяют, откуда отсчитывать координаты дрона. body — относительно самого дрона (вперед/назад/влево/вправо). aruco_map — относительно карты с ArUco-метками, где координаты привязаны к неподвижным маркерам. Выбор правильной системы координат позволяет дрону точно выполнять команды: лететь относительно себя для коротких маневров или по глобальной карте для точной навигации.',
hrefToCourse: 'https://stepik.org/lesson/2264336/step/1?unit=2298407',
hrefToDoc: 'https://clover.coex.tech/ru/frames.html'
},
{
title: 'ArUco-маркер',
body: 'ArUco-маркер — квадратный визуальный маркер, который камера дрона может распознавать. Используется для навигации: по положению маркера дрон определяет свои координаты в пространстве.',
hrefToCourse: 'https://stepik.org/lesson/2264336/step/1?unit=2298407',
hrefToDoc: 'https://klever-doc.tech/ROS1/ru/aruco.html'
},
{
title: 'Симулятор (Gazebo)',
body: 'Симулятор (Gazebo) — виртуальная среда, где дрон ведет себя как в реальности. Позволяет безопасно тестировать полетные задания перед запуском на реальном дроне.',
hrefToCourse: 'https://stepik.org/lesson/2278310/step/1?unit=2312564',
hrefToDoc: 'https://klever-doc.tech/ROS1/ru/simulation.html'
},
{
title: 'unregister()',
body: 'unregister() — метод, который отключает подписчика от топика. Используется, чтобы перестать получать данные, когда они больше не нужны (например, после сканирования QR-кода).',
hrefToCourse: 'https://stepik.org/lesson/2272845/step/1?unit=2307058',
hrefToDoc: 'https://docs.ros.org/en/diamondback/api/rospy/html/rospy.topics.Subscriber-class.html'
},
]
+2
View File
@@ -15,10 +15,12 @@ const createWindow = () => {
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
width: width, width: width,
height: height, height: height,
icon: './public/images/logo32x32.jpg',
webPreferences: { webPreferences: {
preload: path.join(__dirname, 'preload.js'), preload: path.join(__dirname, 'preload.js'),
}, },
}); });
mainWindow.menuBarVisible = false;
// and load the index.html of the app. // and load the index.html of the app.
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
+4
View File
@@ -29,5 +29,9 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
import App from './App.vue'; import App from './App.vue';
import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css' import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
import Prism from 'prismjs'
import 'prismjs/themes/prism-okaidia.css'
import ContextMenu from '@imengyu/vue3-context-menu' import ContextMenu from '@imengyu/vue3-context-menu'
createApp(App).use(ContextMenu).mount('#app'); createApp(App).use(ContextMenu).mount('#app');
@@ -1,4 +1,4 @@
def color_detection(data, print_type='str'): export default `def color_detection(data, print_type='str'):
'''определение цвета''' '''определение цвета'''
frame = bridge.imgmsg_to_cv2(data, 'bgr8') frame = bridge.imgmsg_to_cv2(data, 'bgr8')
@@ -34,7 +34,7 @@ def color_detection(data, print_type='str'):
color = 'green' color = 'green'
elif r > b + 20 and r > g + 20 and r > 100: elif r > b + 20 and r > g + 20 and r > 100:
color = 'red' color = 'red'
elif r > 150 and g > 150 and g > 150: elif r > 150 and g > 150 and b > 150:
color= 'white' color= 'white'
else: else:
color = None color = None
@@ -42,4 +42,4 @@ def color_detection(data, print_type='str'):
<<separator>> <<separator>>
color_sub = rospy.Subscriber('main_camera/image_raw', Image, lambda img: color_detection(img, print_type='rgb'), queue_size=1) color_sub = rospy.Subscriber('main_camera/image_raw', Image, lambda img: color_detection(img, print_type='rgb'), queue_size=1)
rospy.sleep(2) rospy.sleep(2)
color_sub.unregister() color_sub.unregister()`
@@ -1,4 +1,4 @@
#!/usr/bin/env python3 export default `#!/usr/bin/env python3
import rospy import rospy
import time import time
@@ -102,4 +102,4 @@ def main():
land() #посадка land() #посадка
if __name__ == "__main__": if __name__ == "__main__":
main() main()`
@@ -1,4 +1,4 @@
def qr_detection(data): export default `def qr_detection(data):
cv_image = bridge.imgmsg_to_cv2(data, 'bgr8') cv_image = bridge.imgmsg_to_cv2(data, 'bgr8')
barcodes = pyzbar.decode(cv_image) barcodes = pyzbar.decode(cv_image)
@@ -9,4 +9,4 @@ def qr_detection(data):
<<separator>> <<separator>>
qr_sub = rospy.Subscriber('main_camera/image_raw', Image, qr_detection, queue_size=1) qr_sub = rospy.Subscriber('main_camera/image_raw', Image, qr_detection, queue_size=1)
rospy.sleep(2) rospy.sleep(2)
qr_sub.unregister() qr_sub.unregister()`
+1 -1
View File
@@ -1,7 +1,7 @@
import iziToast from 'iziToast'; import iziToast from 'iziToast';
iziToast.settings({ iziToast.settings({
timeout: 3000, timeout: 2500,
closeOnEscape: true, closeOnEscape: true,
closeOnClick : true, closeOnClick : true,
resetOnHover: false, resetOnHover: false,