7 Commits

Author SHA1 Message Date
alisagerasimova fd7408016f добавила ещё несколько компонентов 2026-06-01 13:04:53 +03:00
alisagerasimova 6eec8f097a добавила панельку с компонентами дрона 2026-05-31 19:40:16 +03:00
IliaMiheev b6597f235e Исправили проблему с табуляццией 2026-05-28 09:57:24 +03:00
Ilia Miheev e213475de5 Фиксы ошибок
Увеличил шрифт
Сделал иконку удаления
Убрал обводку после нажатия у кнопок на панели инструментов
2026-03-31 18:16:15 +03:00
alisagerasimova dc31ec1cb6 добавили тултипы 2026-03-30 14:42:09 +03:00
Alisa Gerasimova e24fa92a84 Merge pull request #17 from IliaMiheev/develop
Develop
2026-03-30 13:22:09 +03:00
Ilia Miheev d1cc7875a8 Merge pull request #5 from IliaMiheev/develop
Develop
2026-03-09 19:14:27 +03:00
34 changed files with 811 additions and 73 deletions
+1 -4
View File
@@ -4,15 +4,12 @@ const { FuseV1Options, FuseVersion } = require('@electron/fuses');
module.exports = { module.exports = {
packagerConfig: { packagerConfig: {
asar: true, asar: true,
icon: './mainicon',
}, },
rebuildConfig: {}, rebuildConfig: {},
makers: [ makers: [
{ {
name: '@electron-forge/maker-squirrel', name: '@electron-forge/maker-squirrel',
config: { config: {},
setupIcon: './mainicon.ico',
},
}, },
{ {
name: '@electron-forge/maker-zip', name: '@electron-forge/maker-zip',
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

+156
View File
@@ -0,0 +1,156 @@
[
{
"id": 1,
"name": "Пропеллеры (винты) дрона",
"category": "Силовая установка",
"image": "/droneComponents/images/prop.png",
"description": "Пропеллеры (воздушные винты) — это лопасти, которые вращаются моторами и создают подъёмную силу и тягу для движения дрона.\n\nДва основных размера — это две цифры: первые две — диаметр (дюймы), последние две — шаг (расстояние, которое винт проходит за один оборот). Например, 5030: диаметр 5.0 дюймов, шаг 3.0 дюйма.\n\nОсновные правила:\n• Большой диаметр + низкий KV мотора = тяжёлые грузы и стабильность\n• Маленький диаметр + высокий KV = скорость и манёвренность\n• Чем больше шаг — тем быстрее дрон, но выше нагрузка на мотор\n\nПропеллеры бывают двух направлений вращения: CW (по часовой стрелке) и CCW (против часовой). У дрона всегда два винта крутятся в одну сторону, два — в другую для компенсации реактивного момента."
},
{
"id": 2,
"name": "Регулятор оборотов (ESC)",
"category": "Электроника",
"image": "/droneComponents/images/esc.png",
"description": "ESC (Electronic Speed Controller) — это устройство, которое получает сигнал от полётного контроллера и преобразует постоянный ток от АКБ в трёхфазный переменный ток для управления бесколлекторным мотором.\n\nФактически ESC — это «педаль газа» для каждого мотора: он меняет скорость вращения тысячи раз в секунду.\n\nОсновные характеристики:\n• Ток (A, амперы): максимальный ток, который ESC может пропустить без перегрева (например, 30A, 50A)\n• Напряжение (S): сколько банок LiPo поддерживает (2-6S и выше)\n• Прошивка (firmware): BLHeli_S, BLHeli_32, AM32 — влияет на плавность и отклик\n\nНа квадрокоптере 4 ESC: один на каждый мотор. Современные дроны часто используют 4-в-1 ESC (одна плата на все 4 мотора) — это компактнее, но если сгорает один канал, менять нужно всю плату."
},
{
"id": 3,
"name": "АКБ дрона (аккумулятор)",
"category": "Батареи",
"image": "/droneComponents/images/AKB.png",
"description": "АКБ дрона — это аккумуляторная батарея (химический источник тока), которая обеспечивает питанием все системы БПЛА: двигатели, полетный контроллер, телеметрию, камеру.\n\nКоротко: это «топливный бак» дрона, от которого зависит время полёта.\n\nОсновные характеристики:\n• Тип: Li-Po (литий-полимерные) или Li-Ion (для дальних полётов)\n• Напряжение (S): 3S (11.1В), 4S (14.8В) — чем больше банок, тем мощнее\n• Ёмкость (mAh): влияет на время полёта (1500-5000 mAh)\n• Токоотдача (C): способность мгновенно отдавать энергию"
},
{
"id": 4,
"name": "Моторы дрона (электродвигатели)",
"category": "Силовая установка",
"image": "/droneComponents/images/motor.png",
"description": "Моторы дрона — это электродвигатели, которые преобразуют энергию от АКБ во вращение пропеллеров, создавая подъёмную силу и управляя движением коптера.\n\nСуществует два типа: бесколлекторные (Brushless) — стандарт для всех современных дронов (FPV, DJI), высокий КПД и мощность; коллекторные (Brushed) — только в дешёвых игрушках, малый ресурс.\n\nКлючевые характеристики:\n• Размер (4 цифры, например 2207): первые две — диаметр статора (мм), последние две — высота статора (мм)\n• KV (оборотов на вольт): низкий KV (1700-2000) — для тяжёлых грузов и больших винтов, высокий KV (2500-5000+) — для скорости и маленьких винтов\n• ESC (регулятор оборотов): преобразует постоянный ток батареи в трёхфазный для мотора\n\nПравило подбора: чем тяжелее дрон и длиннее винты, тем ниже должно быть KV. Перегрев мотора после полёта — признак перегрузки."
},
{
"id": 5,
"name": "PDB (Плата распределения питания)",
"category": "Электроника",
"image": "/droneComponents/images/pdb.png",
"description": "PDB (Power Distribution Board) — это плата, которая принимает питание от АКБ и распределяет его между всеми потребителями: регуляторами оборотов (ESC), полётным контроллером, видеопередатчиком и другой периферией.\n\nЧасто PDB выполняет роль центральной «силовой магистрали» дрона, к ней сходятся все толстые провода.\n\nОсновные возможности:\n• Медные дорожки рассчитаны на большой ток (до 100+ Ампер)\n• Имеет посадочные места для пайки ESC и других устройств\n• Может включать встроенный BEC для питания электроники (5В или 12В)\n• На современных дронах часто интегрирована прямо в полётный контроллер (FC) — тогда отдельная PDB не нужна"
},
{
"id": 6,
"name": "BEC (Блок стабилизации напряжения)",
"category": "Электроника",
"image": "/droneComponents/images/bec.png",
"description": "BEC (Battery Elimination Circuit) — это преобразователь напряжения, который понижает высокое напряжение от АКБ (например, 14.8В от 4S) до стабильного низкого напряжения (обычно 5В или 12В) для питания чувствительной электроники: полётного контроллера, приёмника, GPS, LED-лент.\n\nНазвание произошло от того, что раньше моделистам нужна была отдельная батарея для электроники, а BEC «устранил» эту батарею.\n\nТипы BEC:\n• Линейный (Linear): простой и дешёвый, но греется и неэффективен при большом перепаде напряжения\n• Импульсный (Switching / UBEC): дороже, но холодный и эффективный — используется в большинстве дронов\n\nГде встречается:\n• Встроен в PDB\n• Встроен в ESC (обычно BEC есть в одном из ESC для питания FC)\n• Отдельный модуль (UBEC) для мощных или особо чувствительных систем\n\nВажно: BEC должен выдавать достаточно тока (ампер) для всей подключаемой электроники."
},
{
"id": 7,
"name": "Зарядное устройство (балансировочное)",
"category": "Зарядные устройства",
"image": "/droneComponents/images/zaryadka.png",
"description": "Зарядное устройство (LiPo балансировочный зарядник) — это внешний прибор для безопасной зарядки и обслуживания литий-полимерных (LiPo) и литий-ионных (Li-Ion) аккумуляторов дронов.\n\nПростые USB-зарядки из комплекта — это «медленные и опасные» зарядки без балансировки. Хорошее зарядное устройство обязательно выполняет балансировку — выравнивает напряжение на каждой банке (ячейке) АКБ, продлевая срок службы и предотвращая пожар.\n\nОсновные характеристики:\n• Тип: балансировочное (обязательно для LiPo)\n• Мощность: измеряется в Ваттах (например, 50W, 200W, 500W) — влияет на скорость зарядки\n• Ток заряда: 1С (рекомендуемый) — для АКБ 1500mAh это 1.5A\n• Режимы: заряд, разряд, хранение (storage), балансировка\n• Количество каналов: одноканальные или многоканальные (можно заряжать несколько АКБ сразу)\n\nПопулярные модели: ToolkitRC M6D, HOTA D6 Pro, SkyRC B6, ISDT Q6\n\nПравило безопасности: никогда не оставляйте LiPo без присмотра во время зарядки — используйте огнестойкий мешок (LiPo safe bag)."
},
{
"id": 8,
"name": "Полётный контроллер (FC)",
"category": "Электроника",
"image": "/droneComponents/images/controller.png",
"description": "Полётный контроллер (Flight Controller, FC) — это главный процессор дрона, который обрабатывает сигналы с датчиков и управляет моторами через ESC, чтобы стабилизировать полёт и выполнять команды пилота.\n\nЭто «мозг» дрона: он получает данные о положении, угле крена, высоте, читает команды с пульта (газ, крен, тангаж, рыскание) и мгновенно подкручивает обороты каждого мотора тысячи раз в секунду.\n\nЧто современные FC умеют:\n• Стабилизация (режимы Angle/Horizon для новичков и Acro для профессионалов)\n• Поддержка Betaflight, INAV, ArduPilot (прошивки)\n• Блэкбокс (запись полётных данных для анализа)\n• Встроенный барометр, гироскоп, иногда OSD (наложение данных на видео)\n\nПопулярные модели: SpeedyBee F405, MATEK F405, Kakute H7, Pixhawk (для тяжёлых дронов)."
},
{
"id": 9,
"name": "Гироскоп и акселерометр (IMU)",
"category": "Датчики",
"image": "/droneComponents/images/imu.png",
"description": "Гироскоп и акселерометр — это датчики положения дрона в пространстве. Оба распаяны прямо внутри корпуса полётного контроллера и вместе образуют IMU (Inertial Measurement Unit).\n\nКак работают:\n• Гироскоп измеряет угловую скорость (как быстро дрон вращается вокруг осей: крен, тангаж, рыскание)\n• Акселерометр измеряет линейные ускорения (включая гравитацию — чтобы определить, где «низ»)\n\nСовместная работа: FC объединяет данные обоих датчиков (сенсорная фьюжн) и получает точные углы наклона (attitude).\n\nВажно: акселерометр нужен только для режимов стабилизации (Angle/Horizon). В режиме Acro (свободный полёт) многие пилоты отключают акселерометр для более чистой работы гироскопа.\n\nПопулярные модели датчиков: MPU6000, MPU6050, BMI270, ICM42688."
},
{
"id": 10,
"name": "Барометр (датчик высоты)",
"category": "Датчики",
"image": "/droneComponents/images/barometr.png",
"description": "Барометр — это датчик, который измеряет высоту дрона по изменению атмосферного давления. Чаще всего он уже встроен прямо в полётный контроллер.\n\nПринцип работы: чем выше поднимается дрон, тем ниже давление воздуха. Барометр фиксирует эту разницу и вычисляет высоту.\n\nОсобенности:\n• Работает только на открытом воздухе (давление не меняется в помещении из-за закрытого объёма)\n• Даёт абсолютную высоту относительно стартовой точки\n• Основной датчик для удержания высоты в GPS-режиме и при автономных полётах (INAV, ArduPilot)\n• Точность обычно до ±0.5-1 метра (но чувствителен к ветру и изменениям погоды)\n\nПопулярные модели: BMP280, BMP388, MS5611 (более дорогой и точный)."
},
{
"id": 11,
"name": "Дальномер (ультразвуковой или лазерный)",
"category": "Датчики",
"image": "/droneComponents/images/dalnomer.png",
"description": "Дальномер — это датчик для точного измерения расстояния от дрона до земли или препятствия. Используется для удержания высоты на малых высотах (до нескольких метров), где барометр даёт грубую ошибку.\n\nДва основных типа:\n• Ультразвуковой (SONAR): работает по принципу эхолокации — излучает звук и замеряет время возврата эха. Дёшево, но чувствителен к шуму и работает плохо над травой/водой. Дальность: 0.1-5 м.\n• Лазерный (LiDAR / TOF): измеряет время пролёта лазерного луча. Точный, не боится шума, работает на любых поверхностях. Дальность: до 10-30 метров. Дороже.\n\nГде применяется: дроны для съёмки, агродроны, дроны для полётов в помещениях (где нет GPS). В FPV-гоночных дронах почти не встречается."
},
{
"id": 12,
"name": "GPS / Компас (магнитометр)",
"category": "Датчики",
"image": "/droneComponents/images/gps.png",
"description": "GPS/Компас — это модуль, который позволяет дрону знать своё местоположение на Земле и направление относительно магнитных полюсов (север/юг). Рекомендуется для любого дрона, который летает на улице.\n\nЧто даёт:\n• Удержание точки (Position Hold) — дрон зависает на месте даже при ветре\n• Возврат домой (RTH — Return To Home) — при потере сигнала или низком заряде дрон возвращается на точку взлёта\n• Полёты по точкам (Waypoints) — автономные миссии\n• Telemetry (скорость, высота, расстояние до дома, координаты)\n\nСостав:\n• GPS приёмник: определяет координаты, высоту (по спутникам), скорость\n• Магнитометр (компас): определяет направление по магнитному полю Земли (нужен для RTH, иначе дрон не поймёт, куда лететь)\n\nВажно: компас требует калибровки перед полётом и страдает от электромагнитных помех от проводов с током. Модуль должен стоять подальше от силовой проводки.\n\nФорматы: M8N, M10, M8Q (чем новее, тем лучше). Рекомендуется M10 с защитой от помех."
},
{
"id": 13,
"name": "Аппаратура управления (пульт пилота)",
"category": "Управление",
"image": "/droneComponents/images/appa.png",
"description": "Аппаратура управления (Transmitter, TX, пульт) — это устройство в руках пилота, которое отправляет команды на дрон: газ, крен, тангаж, рыскание, а также переключение режимов полёта и дополнительные функции.\n\nЭто «руль и педали» дрона. Современные пульты работают в цифровых протоколах и имеют телеметрию — обратную связь с дроном (уровень батареи, сигнал, высота).\n\nОсновные характеристики:\n• Количество каналов: минимально 4 (газ, крен, тангаж, рыскание), для FPV нужно 6-8 каналов (режимы, бипер, арм)\n• Протокол связи: ELRS (ExpressLRS) — современный стандарт для FPV (дальность километры), также Crossfire, Ghost, FrSky (ACCST, ACCESS)\n• Мощность передатчика: 100mW, 250mW, 1W — влияет на дальность\n• Частота: 2.4 ГГц (стандарт) или 900 МГц (лучшее проникновение через препятствия)\n\nФорм-факторы:\n• Геймпад (игровой стиль): Radiomaster Zorro, Jumper T-Pro\n• Боксёр (классический): Radiomaster TX16S, TBS Mambo, FrSky X18\n• Для новичков: LiteRadio 3, BetaFPV LiteRadio\n\nВажно: пульт должен поддерживать тот же протокол, что и приёмник на дроне (например, оба на ELRS)."
},
{
"id": 14,
"name": "Приёмник (Receiver, RX)",
"category": "Управление",
"image": "/droneComponents/images/rx.png",
"description": "Приёмник (Receiver, RX) — это устройство, которое крепится на дрон, принимает радиосигнал с пульта пилота и передаёт команды на полётный контроллер.\n\nЭто «уши» дрона. Без него вы не сможете управлять дроном — он просто не услышит команды.\n\nКак работает:\n1. Пилот двигает стики на пульте → пульт кодирует команды в радиосигнал\n2. Приёмник на дроне ловит сигнал и расшифровывает его\n3. Полётный контроллер читает данные с приёмника (через протоколы типа SBUS, CRSF) и управляет моторами\n\nОсновные характеристики:\n• Протокол связи: должен совпадать с пультом (ELRS, Crossfire, FrSky, DJI, Spektrum)\n• Количество каналов: обычно 6-16 (чем больше, тем больше функций)\n• Дальность: зависит от мощности и протокола — у ELRS на 100mW можно летать на километры\n• Антенны: один или два штырька (диверсити — две антенны для лучшего приёма)\n\nПопулярные модели:\n• ELRS: Radiomaster RP1, Happymodel EP1/EP2, BetaFPV ELRS Lite\n• Crossfire: TBS Crossfire Nano RX\n• FrSky: XM+, R-XSR (устаревающий, но ещё встречается)\n\nВажно: приёмник должен быть совместим с полётным контроллером (обычно подключается через UART и протокол CRSF для ELRS или SBUS для FrSky)."
},
{
"id": 15,
"name": "FPV-камера",
"category": "Видеосистема",
"image": "/droneComponents/images/camera.png",
"description": "FPV-камера — это устройство, которое крепится на дрон и передаёт картинку с его «глаз» в реальном времени. Пилот видит эту картинку в очках или на экране и управляет дроном от первого лица.\n\nЭто «глаза» дрона. Без FPV-камеры полёт от первого лица невозможен.\n\nОсновные характеристики:\n• Размер: самый популярный — 19x19 мм для дронов 3-5 дюймов, также 14x14 мм и 27x27 мм\n• Сенсор: CCD (устаревший, плавная картинка при изменении освещения) или CMOS (современный, лучшее качество)\n• Соотношение сторон: 4:3 (классика) или 16:9 (широкоэкранный)\n• Разрешение: стандартное ~600TVL (аналог), 1080p (цифровые системы типа DJI O4)\n• WDR (широкий динамический диапазон): позволяет видеть детали и в тени, и на свету — критично для полётов против солнца\n• Задержка (латентность): для аналога <5-10 мс, для цифры ~20-40 мс — чем меньше, тем лучше для быстрых маневров\n\nПопулярные модели:\n• Аналог: Caddx Ratel 2, Foxeer T-Rex, Runcam Phoenix 2\n• HDZero: Runcam HDZero\n• Walksnail: Avatar HD\n• DJI: O3 Air Unit (камера + VTX в одном корпусе)"
},
{
"id": 16,
"name": "Видеопередатчик (VTX)",
"category": "Видеосистема",
"image": "/droneComponents/images/videopered.png",
"description": "Видеопередатчик (Video Transmitter, VTX) — это устройство, которое получает видеосигнал с FPV-камеры и транслирует его по радиочастоте на землю (в очки или монитор пилота).\n\nЭто «радиостанция» дрона для видео. VTX определяет, насколько далеко и с каким качеством вы увидите картинку.\n\nОсновные характеристики:\n• Мощность: 25mW, 200mW, 600mW, 1W (1000mW) и выше. Чем выше мощность, тем дальше полёт, но сильнее нагрев\n• Частота: 5.8 ГГц (стандарт для FPV). Бывают ещё 1.3 ГГц и 2.4 ГГц, но редко\n• Каналы: 40+ каналов, включая Raceband (самые чистые частоты для групповых полётов)\n• Тип: аналоговый (дешевле, меньше задержка) или цифровой (качество как на телевизоре, дороже)\n• SmartAudio / Tramp: протоколы для управления VTX с пульта (переключать каналы и мощность в полёте)\n\nПопулярные модели:\n• Аналог: TBS Unify Pro32, Rush Tank Ultimate, Eachine Nano\n• Цифровые системы: DJI O3/O4 Air Unit, Walksnail Avatar, HDZero\n\nВажно: VTX сильно греется — его нельзя включать без антенны (сгорит). Нужен обдув от пропеллеров или радиатор."
},
{
"id": 17,
"name": "Антенна видеопередатчика",
"category": "Видеосистема",
"image": "/droneComponents/images/antena.png",
"description": "Антенна видеопередатчика (VTX Antenna) — это устройство, которое излучает видеосигнал VTX в окружающее пространство. Качество антенны напрямую влияет на дальность и надёжность видеосвязи.\n\nЭто «рупор» дрона для видео. Без антенны VTX не только не передаёт сигнал, но и сгорает от перегрева.\n\nТипы антенн:\n• Штыревая (Whip / Linear): простая, дешёвая, но плохая помехозащищённость — подходит только для очень маленьких дронов\n• Клеверный лист (Cloverleaf / RHCP): круговая поляризация, лучше проникает через препятствия\n• Пагода (Pagoda): дешёвая и эффективная круговая антенна\n• Лутц (Lumenier AXII, TrueRC): современные высококачественные антенны с лучшим коэффициентом усиления\n\nПоляризация:\n• RHCP (Right-Hand Circular Polarized) — правая круговая (стандарт FPV)\n• LHCP (Left-Hand Circular Polarized) — левая круговая (несовместима с RHCP!)\n\nВажно: антенна на дроне и антенны на очках должны иметь одинаковую поляризацию (например, обе RHCP). Иначе дальность упадёт в 10-20 раз.\n\nФорм-факторы: короткая (для минидронов) или длинная (лучше сигнал на большом дроне)."
},
{
"id": 18,
"name": "FPV-шлем / очки",
"category": "Видеосистема",
"image": "/droneComponents/images/ochki.png",
"description": "FPV-шлем или очки (Goggles / Video Receiver) — это устройство, которое пилот надевает на голову. Оно принимает видеосигнал с видеопередатчика (VTX) дрона и показывает картинку с камеры прямо перед глазами, создавая эффект полного погружения «от первого лица».\n\nЭто «окно в кабину» дрона. Без них FPV-полёт теряет смысл.\n\nДва основных типа:\n• Очки-«бабочка» (Box Goggles): один большой экран внутри корпуса. Дешевле, подходят для новичков и людей в очках. Пример: Eachine EV800D.\n• Компактные очки (Slim Goggles): два маленьких экрана, выглядят как лыжная маска. Дороже, компактнее, легче. Пример: FatShark, DJI Goggles.\n\nОсновные характеристики:\n• Приёмник (аналоговый или цифровой): должен совпадать с VTX дрона — аналог не примет цифру и наоборот\n• Разрешение экранов: от 480p до 1080p\n• Угол обзора (FOV): от 30° до 60° — чем больше, тем сильнее погружение\n• Запись видео: многие очки пишут DVR (то, что видел пилот) на SD-карту\n• HDMI-вход: позволяет подключать симулятор или цифровые системы сторонних производителей\n• Диоптрии / регулировка линз: для близоруких пилотов\n\nПопулярные модели:\n• Аналоговые: Eachine EV800D (начальный), FatShark Attitude V6, Skyzone Cobra X\n• Цифровые: DJI Goggles 2 / Integra / V2, Walksnail Avatar Goggles X, HDZero Goggles\n\nВажно: антенны на очках должны совпадать по поляризации с антенной на дроне (RHCP с RHCP, LHCP с LHCP)."
},
{
"id": 19,
"name": "Рама и лучи (несущая основа)",
"category": "Конструкция",
"image": "/droneComponents/images/frame.png",
"description": "Рама — это несущая основа дрона, на которую крепятся все компоненты: моторы, полётный контроллер, АКБ, видеопередатчик и остальная электроника. Лучи (arms) — это «руки» дрона, на концах которых устанавливаются моторы. Лучи обычно являются частью рамы (бывают цельные или съёмные).\n\nЭто «скелет» дрона. От жёсткости и веса рамы зависит стабильность полёта и устойчивость к вибрациям.\n\nОсновные материалы:\n• Карбон (углепластик) — самый популярный для FPV: лёгкий, жёсткий, не гнётся. Классическая толщина — 3-4 мм для рамы, 5-6 мм для лучей.\n• Пластик — для игрушек и дешёвых дронов (хрупкий).\n• Алюминий/сталь — для тяжёлых промышленных дронов (редко).\n\nХарактеристики рамы:\n• Размер (wheelbase): расстояние по диагонали между центрами моторов (например, 250 мм для 5-дюймового дрона)\n• Форма: X (симметричный), True X (равные плечи), Wide X (широкий), Deadcat (передние лучи разведены — камера не попадает в кадр)\n• Совместимость с моторами: крепление 12x12 мм (микро), 16x16 мм, 19x19 мм (стандарт M3)\n\nПопулярные производители: TBS, iFlight, GEPRC, Armattan (гарантия на поломку), Source One (дешёвый и хороший)."
},
{
"id": 20,
"name": "Фюзеляж (защитный капот)",
"category": "Конструкция",
"image": "/droneComponents/images/fuzelazh.png",
"description": "Фюзеляж — это защитный капот (корпус), который закрывает и защищает электронику дрона: полётный контроллер, приёмник, видеопередатчик и провода от пыли, влаги и ударов. Нужен не всем дронам — чаще используется на самолётах (крылатых БПЛА) или готовых consumer-дронах (DJI, Autel).\n\nЭто «кузов» или «кабина» дрона. В FPV-коптерах почти не встречается, так как карбоновая рама сама по себе защищает компоненты, а открытый доступ упрощает ремонт.\n\nГде применяется:\n• Самолёты и крылья: обтекаемый фюзеляж для аэродинамики\n• Промышленные октокоптеры: пыле- и брызгозащита (степень IP54+)\n• Готовые дроны (DJI Mavic, DJI Phantom, Autel Evo): полный пластиковый корпус для защиты и красоты\n• Детские и игрушечные дроны: весь дрон — один большой корпус\n\nМатериалы: пластик (ABS, поликарбонат), пенопласт (для самолётов), иногда карбон с накладками.\n\nВажно: в FPV квадрокоптерах вместо фюзеляжа используют 3D-печатные фиксаторы для АКБ, кейсы для GoPro и небольшой «капот» (top plate) на верхней плате рамы — это минималистичный вариант."
},
{
"id": 21,
"name": "Светодиодная лента (LED / подсветка)",
"category": "Электроника",
"image": "/droneComponents/images/ledstrip.png",
"description": "Светодиодная лента (LED-подсветка) — это адресные или обычные светодиоды на гибкой плате, которые крепятся на раму дрона. Используется для индикации статуса (вооружён/не вооружён, режим полёта, уровень заряда АКБ) и для визуальной ориентации дрона в темноте.\n\nЭто «световая сигнализация» и «габаритные огни» дрона. Позволяет понять, куда повёрнут дрон ночью и готов ли он к взлёту.\n\nТипы:\n• Обычные одноцветные: просто горят красным/синим/белым (дешёвые)\n• Адресные (WS2812B / NeoPixel): каждый светодиод можно управлять индивидуально — менять цвет, яркость, эффекты (бегущие огни, мигание)\n\nЧто можно делать:\n• Подключить к полётному контроллеру (UART или LED-порт) и настроить в Betaflight\n• Включить разные цвета для задних (обычно красные, тёплые) и передних (синие/белые/холодные) лучей для ориентации\n• Мигать при низком заряде АКБ\n• Светить ярко-синим/зелёным при вооружении (arm)\n\nПараметры: напряжение 5В (питается от BEC), количество светодиодов: 4-8 шт на дрон, длина обычно 1-2 метра на дрон.\n\nВажно: в Betaflight есть встроенный режим для WS2812B — не требует программирования."
},
{
"id": 22,
"name": "Вольтметр (пищалка / LiPo Buzzer)",
"category": "Аксессуары",
"image": "/droneComponents/images/volt.png",
"description": "Вольтметр (называется также пищалкой, LiPo Buzzer, checker) — это маленькое устройство, которое подключается к балансировочному разъёму АКБ. Оно измеряет напряжение на каждой банке (ячейке) аккумулятора и громко пищит, когда напряжение падает ниже заданного порога, предупреждая пилота о скором разряде.\n\nЭто «сигнализация разряда» для аккумулятора. Предотвращает глубокий разряд LiPo — одну из главных причин выхода батареи из строя и пожара при зарядке.\n\nКак работает:\n1. Вставляешь в балансировочный разъём АКБ (тот, что с несколькими тонкими проводами)\n2. Устанавливаешь порог писка (для LiPo это 3.3-3.5В на банку)\n3. Когда напряжение на любой банке падает ниже порога — пищалка орёт (сигнал слышен с дрона на 50-100 метров)\n4. Пилот слышит звук и сажает дрон, чтобы не убить батарею\n\nХарактеристики:\n• Диапазон измерения: 1-8 банок (S) (чаще всего 2-6S)\n• Настройка порога срабатывания: кнопками на корпусе (например, 3.3В, 3.4В, 3.5В)\n• Режимы: звуковой сигнал (непрерывный или прерывистый), иногда дисплей с напряжением\n• Размер: маленький (2x3 см), вес 5-10 грамм\n\nВажно: в современных полётных контроллерах есть встроенный функционал пищалки через моторы (dshot beeper) — моторы сами издают звук, и отдельная пищалка не нужна. Но внешний вольтметр надёжнее: он работает даже при отказе полётного контроллера или разряде, когда ESC уже не могут пищать.\n\nПравило: всегда начинающим рекомендуется иметь такой buzzer — он стоит 50-100 рублей и спасёт не один аккумулятор."
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px"><path d="M 24 4 C 20.704135 4 18 6.7041348 18 10 L 11.746094 10 A 1.50015 1.50015 0 0 0 11.476562 9.9785156 A 1.50015 1.50015 0 0 0 11.259766 10 L 7.5 10 A 1.50015 1.50015 0 1 0 7.5 13 L 10 13 L 10 38.5 C 10 41.519774 12.480226 44 15.5 44 L 32.5 44 C 35.519774 44 38 41.519774 38 38.5 L 38 13 L 40.5 13 A 1.50015 1.50015 0 1 0 40.5 10 L 36.746094 10 A 1.50015 1.50015 0 0 0 36.259766 10 L 30 10 C 30 6.7041348 27.295865 4 24 4 z M 24 7 C 25.674135 7 27 8.3258652 27 10 L 21 10 C 21 8.3258652 22.325865 7 24 7 z M 13 13 L 35 13 L 35 38.5 C 35 39.898226 33.898226 41 32.5 41 L 15.5 41 C 14.101774 41 13 39.898226 13 38.5 L 13 13 z M 20.476562 17.978516 A 1.50015 1.50015 0 0 0 19 19.5 L 19 34.5 A 1.50015 1.50015 0 1 0 22 34.5 L 22 19.5 A 1.50015 1.50015 0 0 0 20.476562 17.978516 z M 27.476562 17.978516 A 1.50015 1.50015 0 0 0 26 19.5 L 26 34.5 A 1.50015 1.50015 0 1 0 29 34.5 L 29 19.5 A 1.50015 1.50015 0 0 0 27.476562 17.978516 z"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+102 -43
View File
@@ -1,7 +1,7 @@
<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";
@@ -14,7 +14,8 @@ import Preview from "@/components/UI/svg/Preview.vue";
import Save from "@/components/UI/svg/Save.vue"; import Save from "@/components/UI/svg/Save.vue";
import Trash from "@/components/UI/svg/Trash.vue"; import Trash from "@/components/UI/svg/Trash.vue";
import Back from "@/components/UI/svg/Back.vue"; import Back from "@/components/UI/svg/Back.vue";
import { NTooltip } from 'naive-ui'
import DroneParts from "@/components/DroneParts.vue";
const numbers = ref([]); // Инициализируем массив с пустыми строками const numbers = ref([]); // Инициализируем массив с пустыми строками
const clickCount = ref(1); // Счетчик нажатий const clickCount = ref(1); // Счетчик нажатий
@@ -23,6 +24,7 @@ const isModalVisible = ref(false);
const isCodeViewVisible = ref(false); const isCodeViewVisible = ref(false);
const finallyText = ref(""); const finallyText = ref("");
const dropMenu = ref(null) const dropMenu = ref(null)
const showDroneParts = ref(false);
onMounted(() => { onMounted(() => {
for (let i = 0; i < 100; i++) { for (let i = 0; i < 100; i++) {
@@ -60,7 +62,8 @@ function setNumber(item) {
} }
} }
function openPreview() { function openPreview(event) {
event.currentTarget.blur()
generateCode() generateCode()
if (!listOfTrips.value.length) { if (!listOfTrips.value.length) {
izitoast.error({ izitoast.error({
@@ -72,8 +75,9 @@ function openPreview() {
isCodeViewVisible.value = true; isCodeViewVisible.value = true;
} }
function saveResult() { function saveResult(event) {
isModalVisible.value = true; isModalVisible.value = true;
event.currentTarget.blur()
generateCode() generateCode()
} }
@@ -94,18 +98,18 @@ function generateCode() {
let textOfCode = color let textOfCode = color
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 += ` ${textOfCode[1].trim()}\n`;
} }
if (isUsedQR) { if (isUsedQR) {
let textOfCode = qr let textOfCode = qr
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 += ` ${textOfCode[1].trim()}\n`;
} }
for (const item of listOfTrips.value) { for (const item of listOfTrips.value) {
mainCode.value += `\tnavigate_wait(${item.x}, ${item.y})\n`; mainCode.value += ` navigate_wait(${item.x}, ${item.y})\n`;
if (item.isPlatform) { if (item.isPlatform) {
mainCode.value += colorDetectCode.value + '\n' mainCode.value += colorDetectCode.value + '\n'
} }
@@ -127,7 +131,8 @@ function generateCode() {
} }
} }
function cancel() { function cancel(event) {
event.currentTarget.blur()
if (clickCount.value === 1) { if (clickCount.value === 1) {
return; return;
} }
@@ -139,7 +144,8 @@ function cancel() {
clickCount.value--; clickCount.value--;
} }
function cleanResult() { function cleanResult(event) {
event.currentTarget.blur()
clickCount.value = 1; clickCount.value = 1;
numbers.value.forEach(item => { numbers.value.forEach(item => {
item.isPlatform = false; item.isPlatform = false;
@@ -184,7 +190,7 @@ function copyAndToast(text) {
function handleDownloadBtn() { function handleDownloadBtn() {
// Создание файла // Создание файла
const downloadUrl = ref(''); const downloadUrl = ref('');
const blob = new Blob([finallyText.value], {type: 'text/plain'}); const blob = new Blob([finallyText.value], { type: 'text/plain' });
downloadUrl.value = URL.createObjectURL(blob); downloadUrl.value = URL.createObjectURL(blob);
// Задержка для избежания ошибок в некоторых браузерах // Задержка для избежания ошибок в некоторых браузерах
setTimeout(() => { setTimeout(() => {
@@ -300,7 +306,7 @@ function onContextMenu(e, item) {
{ {
label: "Удалить", label: "Удалить",
onClick: () => deletePlatform(item), onClick: () => deletePlatform(item),
icon: createIcon('icons/trash.svg'), icon: h(Trash),
}, },
] ]
}, },
@@ -316,14 +322,14 @@ function onContextMenu(e, item) {
{ {
label: "Удалить", label: "Удалить",
onClick: () => deleteQR(item), onClick: () => deleteQR(item),
icon: createIcon('icons/trash.svg') icon: h(Trash)
}, },
], ],
}, },
{ {
label: "Удалить", label: "Удалить",
icon: createIcon('icons/trash.svg'), icon: h(Trash),
onClick: () => deletePointMap(item), onClick: () => deletePointMap(item),
}, },
] ]
@@ -332,7 +338,16 @@ function onContextMenu(e, item) {
</script> </script>
<template> <template>
<div v-if="!isCodeViewVisible" class="wraper"> <button
v-if="!showDroneParts"
@click="showDroneParts = true"
class="show-parts-btn"
>
<img src="/images/drone.png" alt="menu" class="btn-icon" />
</button>
<DroneParts v-if="showDroneParts" @back="showDroneParts = false" />
<div v-if="!showDroneParts && !isCodeViewVisible" class="wraper">
<div class="grid"> <div class="grid">
<div class="square" v-for="(item, index) in numbers" :data-key="index" :key="index" @click="setNumber(item)" <div class="square" v-for="(item, index) in numbers" :data-key="index" :key="index" @click="setNumber(item)"
@contextmenu.prevent="onContextMenu($event, item)" :style="{ backgroundImage: `url(${item.BgImg})` }"> @contextmenu.prevent="onContextMenu($event, item)" :style="{ backgroundImage: `url(${item.BgImg})` }">
@@ -341,12 +356,32 @@ function onContextMenu(e, item) {
</div> </div>
<div class="actions"> <div class="actions">
<Save @click="saveResult"/> <n-tooltip trigger="hover">
<Trash @click="cleanResult"/> <template #trigger>
<Back @click="cancel"/> <Save @click="saveResult" />
<Preview @click="openPreview"/> </template>
Сохранить
</n-tooltip>
<n-tooltip trigger="hover">
<template #trigger>
<Trash @click="cleanResult" />
</template>
Удалить
</n-tooltip>
<n-tooltip trigger="hover">
<template #trigger>
<Back @click="cancel" />
</template>
Отменить
</n-tooltip>
<n-tooltip trigger="hover">
<template #trigger>
<Preview @click="openPreview" />
</template>
Превью
</n-tooltip>
</div> </div>
<MyDropMenu ref="dropMenu"/> <MyDropMenu ref="dropMenu" />
<MyDialog v-model:show="isModalVisible" class="dialogWindow" :is-success=Boolean(listOfTrips.length)> <MyDialog v-model:show="isModalVisible" class="dialogWindow" :is-success=Boolean(listOfTrips.length)>
<template #header> <template #header>
<strong v-if="listOfTrips.length">Код готов!</strong> <strong v-if="listOfTrips.length">Код готов!</strong>
@@ -356,14 +391,16 @@ function onContextMenu(e, item) {
<p v-if="listOfTrips.length">Скопируйте или скачайте ваш код</p> <p v-if="listOfTrips.length">Скопируйте или скачайте ваш код</p>
<p v-else>Выстройте маршрут перед сохранением кода.</p> <p v-else>Выстройте маршрут перед сохранением кода.</p>
</main> </main>
<template v-if="listOfTrips.length" #footer> <template #footer>
<div v-if="listOfTrips.length">
<button @click="isCodeViewVisible = true">Предпросмотр</button> <button @click="isCodeViewVisible = true">Предпросмотр</button>
<button @click="handleDownloadBtn">Скачать код</button> <button @click="handleDownloadBtn">Скачать код</button>
<button @click="copyAndToast(finallyText)">Скопировать код</button> <button @click="copyAndToast(finallyText)">Скопировать код</button>
</div>
</template> </template>
</MyDialog> </MyDialog>
</div> </div>
<CodeView v-model:show="isCodeViewVisible" :code="finallyText" :show="isCodeViewVisible"/> <CodeView v-model:show="isCodeViewVisible" :code="finallyText" :show="isCodeViewVisible" />
</template> </template>
<style> <style>
@@ -373,16 +410,43 @@ function onContextMenu(e, item) {
html { html {
background-image: radial-gradient(#23aae3e2, #222222); background-image: radial-gradient(#23aae3e2, #222222);
font-size: 18px;
} }
body { body {
font-size: 18px;
margin: 0; margin: 0;
} }
:root { :root {
--side: 8vh; --side: 75px;
--sizeActions: 5vh; }
.show-parts-btn {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
width: 50px;
height: 50px;
padding: 0;
background: rgba(12, 12, 12, 0.034);
border: none;
cursor: pointer;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
transition: transform 0.2s;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.btn-icon {
width: 90%;
height: 90%;
object-fit: cover;
}
.show-parts-btn:hover {
transform: scale(1.05);
} }
.wraper { .wraper {
@@ -395,40 +459,36 @@ body {
display: grid; display: grid;
padding: 3px; padding: 3px;
grid-template-columns: repeat(10, var(--side)); grid-template-columns: repeat(10, var(--side));
grid-template-rows: repeat(10, var(--side));
gap: 5px; gap: 5px;
user-select: none; user-select: none;
} }
.square { .square {
width: 100%; width: var(--side);
height: 100%; height: var(--side);
text-align: center;
background-size: cover; background-size: cover;
border: 3px solid #ffffff; border: 3px solid rgb(251, 251, 251);
cursor: pointer; cursor: pointer;
overflow-y: scroll; overflow-y: scroll;
background-blend-mode: multiply; background-blend-mode: multiply;
background-color: #ffffff; background-color: transparent;
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
color: #2267fce2; color: white;
text-shadow: -1px -1px 0 #fff, text-shadow: -1px -1px 0 black,
1px -1px 0 #fff, 1px -1px 0 black,
-1px 1px 0 #fff, -1px 1px 0 black,
1px 1px 0 #fff; 1px 1px 0 black;
box-sizing: border-box; box-sizing: border-box;
font-size: 1.1rem;
font-weight: bold
} }
.square:hover { .square:hover {
background-color: #949494; background-color: rgb(166, 162, 162);
} }
.square:active { .square:active {
background-color: #747474; background-color: rgb(164, 162, 162);
} }
.square::-webkit-scrollbar { .square::-webkit-scrollbar {
@@ -456,8 +516,8 @@ body {
} }
.actions svg { .actions svg {
width: var(--sizeActions); width: 40px;
height: var(--sizeActions); height: 40px;
transition: color 0.4s; transition: color 0.4s;
} }
@@ -476,6 +536,5 @@ path {
.actions button { .actions button {
padding: 20px 35px; padding: 20px 35px;
/* background-color: red; */
} }
</style> </style>
+2 -2
View File
@@ -18,13 +18,13 @@ const props = defineProps({
<div class="code-view" v-if="show"> <div class="code-view" v-if="show">
<CustomButton <CustomButton
@click.stop="$emit('update:show', false)"> @click.stop="$emit('update:show', false)">
Вернуться к генерации маршрута Вернуться к сохранению сгенерированного кода
</CustomButton> </CustomButton>
<CodeBlock :code="code" /> <CodeBlock :code="code" />
<CustomButton <CustomButton
@click.stop="$emit('update:show', false)"> @click.stop="$emit('update:show', false)">
Вернуться к генерации маршрута Вернуться к сохранению сгенерированного кода
</CustomButton> </CustomButton>
<MovementButtons/> <MovementButtons/>
</div> </div>
+530
View File
@@ -0,0 +1,530 @@
<template>
<div class="drone-parts-container">
<!-- Кнопка возврата в левом верхнем углу -->
<button class="back-btn" @click="$emit('back')">
Вернуться к маршруту
</button>
<!-- Левая панель - список компонентов -->
<div class="parts-list">
<div
v-for="part in parts"
:key="part.id"
class="part-item"
:class="{ active: selectedPart?.id === part.id }"
@click="selectPart(part)"
>
<div class="part-image-wrapper">
<img :src="part.image" :alt="part.name" class="part-thumb" />
</div>
<div class="part-info">
<span class="part-name">{{ part.name }}</span>
<span class="part-category">{{ part.category }}</span>
</div>
</div>
</div>
<!-- Правая часть - детали -->
<div class="main-area">
<div v-if="selectedPart" class="part-detail">
<div class="detail-image">
<img :src="selectedPart.image" :alt="selectedPart.name" />
</div>
<div class="detail-info">
<h2>{{ selectedPart.name }}</h2>
<p class="description">{{ selectedPart.description }}</p>
</div>
<div class="comments-section">
<div class="comments-header">
<h3>📝 Мой комментарий</h3>
</div>
<div v-if="myComment" class="existing-comment">
<div class="comment-text">{{ myComment.text }}</div>
<div class="comment-meta">
<span class="comment-date">{{ myComment.date }}</span>
<button class="edit-comment" @click="editComment"> Редактировать</button>
<button class="delete-comment" @click="deleteComment">🗑 Удалить</button>
</div>
</div>
<div v-else class="add-comment">
<textarea
v-model="commentText"
:placeholder="`Мой комментарий к «${selectedPart.name}»...`"
rows="3"
></textarea>
<div class="comment-actions">
<button class="save-btn" @click="saveComment" :disabled="!commentText.trim()">
💾 Сохранить комментарий
</button>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<div class="empty-icon">🛸</div>
<h3>Выберите компонент дрона</h3>
<p>Нажмите на любой компонент из списка слева</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const emit = defineEmits(['back'])
let db = null
const openDB = () => {
return new Promise((resolve, reject) => {
const request = indexedDB.open('DroneCommentsDB', 1)
request.onerror = () => reject(request.error)
request.onsuccess = () => resolve(request.result)
request.onupgradeneeded = (event) => {
const database = event.target.result
if (!database.objectStoreNames.contains('comments')) {
const store = database.createObjectStore('comments', { keyPath: 'id', autoIncrement: true })
store.createIndex('part_id', 'part_id', { unique: false })
}
}
})
}
const saveCommentToDB = async (partId, text) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['comments'], 'readwrite')
const store = transaction.objectStore('comments')
const comment = {
part_id: partId,
user_id: 'me',
text: text,
date: new Date().toLocaleString('ru-RU'),
timestamp: Date.now()
}
const request = store.add(comment)
request.onsuccess = () => resolve(request.result)
request.onerror = () => reject(request.error)
})
}
const loadCommentFromDB = async (partId) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['comments'], 'readonly')
const store = transaction.objectStore('comments')
const index = store.index('part_id')
const request = index.getAll(partId)
request.onsuccess = () => {
const comments = request.result
const myComment = comments.length > 0 ? comments[comments.length - 1] : null
resolve(myComment)
}
request.onerror = () => reject(request.error)
})
}
const updateCommentInDB = async (id, newText) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['comments'], 'readwrite')
const store = transaction.objectStore('comments')
const request = store.get(id)
request.onsuccess = () => {
const comment = request.result
if (comment) {
comment.text = newText
comment.date = new Date().toLocaleString('ru-RU')
comment.timestamp = Date.now()
const updateRequest = store.put(comment)
updateRequest.onsuccess = () => resolve()
updateRequest.onerror = () => reject(updateRequest.error)
} else {
reject('Comment not found')
}
}
request.onerror = () => reject(request.error)
})
}
const deleteCommentFromDB = async (id) => {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['comments'], 'readwrite')
const store = transaction.objectStore('comments')
const request = store.delete(id)
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
}
const parts = ref([])
const selectedPart = ref(null)
const myComment = ref(null)
const commentText = ref('')
const dbReady = ref(false)
const loadParts = async () => {
try {
const response = await fetch('/droneComponents/components.json')
if (response.ok) {
parts.value = await response.json()
} else {
console.error('Файл components.json не найден')
parts.value = []
}
} catch (error) {
console.error('Ошибка загрузки компонентов:', error)
parts.value = []
}
}
const selectPart = async (part) => {
selectedPart.value = part
if (dbReady.value) {
const comment = await loadCommentFromDB(part.id)
myComment.value = comment
commentText.value = ''
}
}
// ========== КОММЕНТАРИИ ==========
const saveComment = async () => {
if (!commentText.value.trim() || !selectedPart.value) return
try {
await saveCommentToDB(selectedPart.value.id, commentText.value.trim())
const comment = await loadCommentFromDB(selectedPart.value.id)
myComment.value = comment
commentText.value = ''
} catch (error) {
console.error('Ошибка сохранения:', error)
}
}
const editComment = () => {
if (myComment.value) {
commentText.value = myComment.value.text
myComment.value = null
}
}
const deleteComment = async () => {
if (!confirm('Удалить комментарий?')) return
try {
await deleteCommentFromDB(myComment.value.id)
myComment.value = null
commentText.value = ''
} catch (error) {
console.error('Ошибка удаления:', error)
}
}
onMounted(async () => {
try {
db = await openDB()
dbReady.value = true
await loadParts()
} catch (error) {
console.error('Ошибка открытия IndexedDB:', error)
}
})
</script>
<style scoped>
.drone-parts-container {
display: flex;
height: 100vh;
width: 100%;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
position: relative;
}
/* Кнопка возврата */
.back-btn {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
padding: 12px 20px;
background: linear-gradient(135deg, #ff6b6b, #c92a2a);
border: none;
border-radius: 10px;
color: white;
font-weight: bold;
cursor: pointer;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
transition: transform 0.2s;
}
.back-btn:hover {
transform: scale(1.02);
}
/* Левая панель с отступом сверху, чтобы не перекрывалась кнопкой */
.parts-list {
width: 300px;
height: 100%;
overflow-y: auto;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(10px);
border-right: 1px solid rgba(255, 255, 255, 0.1);
margin-top: 0;
}
.parts-list::-webkit-scrollbar {
width: 6px;
}
.parts-list::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
.parts-list::-webkit-scrollbar-thumb {
background: #23aae3;
border-radius: 3px;
}
.part-item {
display: flex;
align-items: center;
gap: 15px;
padding: 15px;
margin: 8px 12px;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
background: rgba(255, 255, 255, 0.05);
}
.part-item:hover {
background: rgba(35, 170, 227, 0.2);
transform: translateX(5px);
}
.part-item.active {
background: linear-gradient(135deg, #23aae3, #1a6d8f);
box-shadow: 0 4px 15px rgba(35, 170, 227, 0.3);
}
.part-image-wrapper {
width: 50px;
height: 50px;
flex-shrink: 0;
}
.part-thumb {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.part-info {
flex: 1;
min-width: 0;
}
.part-name {
display: block;
font-size: 14px;
font-weight: 600;
color: #fff;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.part-category {
display: block;
font-size: 11px;
color: rgba(255, 255, 255, 0.6);
}
/* Правая область */
.main-area {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.part-detail {
max-width: 800px;
margin: 0 auto;
background: rgba(0, 0, 0, 0.5);
border-radius: 24px;
padding: 30px;
backdrop-filter: blur(5px);
}
.detail-image {
text-align: center;
margin-bottom: 30px;
}
.detail-image img {
max-width: 100%;
max-height: 350px;
border-radius: 20px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.detail-info h2 {
font-size: 28px;
color: #fff;
margin-bottom: 15px;
text-align: center;
}
.description {
font-size: 16px;
line-height: 1.6;
color: rgba(255, 255, 255, 0.85);
text-align: center;
margin-bottom: 30px;
white-space: pre-wrap;
}
.comments-section {
border-top: 1px solid rgba(255, 255, 255, 0.2);
padding-top: 25px;
}
.comments-header h3 {
font-size: 18px;
color: #fff;
margin: 0 0 20px 0;
}
.existing-comment {
background: rgba(35, 170, 227, 0.15);
border-radius: 12px;
padding: 15px;
border-left: 3px solid #23aae3;
}
.comment-text {
color: #fff;
font-size: 15px;
line-height: 1.5;
margin-bottom: 10px;
white-space: pre-wrap;
}
.comment-meta {
display: flex;
gap: 15px;
align-items: center;
font-size: 11px;
color: rgba(255, 255, 255, 0.5);
}
.comment-meta button {
background: none;
border: none;
cursor: pointer;
font-size: 12px;
padding: 4px 8px;
border-radius: 6px;
transition: 0.2s;
}
.edit-comment {
color: #23aae3;
}
.edit-comment:hover {
background: rgba(35, 170, 227, 0.2);
}
.delete-comment {
color: #ff6b6b;
}
.delete-comment:hover {
background: rgba(255, 107, 107, 0.2);
}
.add-comment textarea {
width: 100%;
padding: 12px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
color: #fff;
font-size: 14px;
font-family: inherit;
resize: vertical;
}
.add-comment textarea:focus {
outline: none;
border-color: #23aae3;
}
.add-comment textarea::placeholder {
color: rgba(255, 255, 255, 0.4);
}
.comment-actions {
margin-top: 10px;
}
.save-btn {
background: linear-gradient(135deg, #23aae3, #1a6d8f);
border: none;
padding: 10px 24px;
border-radius: 24px;
color: white;
font-size: 14px;
cursor: pointer;
transition: 0.2s;
}
.save-btn:hover:not(:disabled) {
transform: scale(1.02);
box-shadow: 0 2px 10px rgba(35, 170, 227, 0.4);
}
.save-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
}
.empty-icon {
font-size: 80px;
margin-bottom: 20px;
opacity: 0.5;
}
.empty-state h3 {
font-size: 24px;
color: #fff;
margin-bottom: 10px;
}
.empty-state p {
color: rgba(255, 255, 255, 0.6);
}
</style>
+5 -6
View File
@@ -80,9 +80,9 @@ useEventListener("keydown", handleKeydown);
overflow-x: auto; overflow-x: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 55%; max-width: 500px;
width: 90%;
border-radius: 5px; border-radius: 5px;
font-size: 1.5rem;
} }
.modal-header, .modal-header,
@@ -110,17 +110,16 @@ useEventListener("keydown", handleKeydown);
} }
.modal-footer { .modal-footer {
justify-content: flex-end;
border-top: 1px solid white; border-top: 1px solid white;
display: flex;
gap: 5px;
} }
.modal-footer button { .modal-footer button {
width: 33%; justify-content: flex-end;
border-top: 1px solid white; border-top: 1px solid white;
padding: 5px; padding: 5px;
color: black; color: black;
font-size: 20px; font-size: 15px;
border-radius: 5px; border-radius: 5px;
} }
+2 -8
View File
@@ -99,8 +99,8 @@ defineExpose({open: () => (isOpen.value = true), close: closeMenu});
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-around; justify-content: space-around;
min-width: 40px; width: 40px;
min-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;
@@ -109,18 +109,12 @@ defineExpose({open: () => (isOpen.value = true), close: closeMenu});
h1 { h1 {
text-align: center; text-align: center;
font-size: 3rem;
} }
.menu-content { .menu-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
padding-bottom: 20px;
}
.menu-content button{
font-size: 1.2rem;
} }
.line { .line {
+6 -2
View File
@@ -17,6 +17,10 @@
</template> </template>
<style scoped> <style scoped>
.cls-1{fill:none;} .cls-1{
fill:none;
}
#Layer_2 {
color: black;
}
</style> </style>
+2 -2
View File
@@ -41,7 +41,7 @@ def emergency_stop():
"""Функция экстренной остановки, срабатывает при нажатии Ctrl + C""" """Функция экстренной остановки, срабатывает при нажатии Ctrl + C"""
print("\n" + "="*50) print("="*50)
print("EMERGENCY STOP ACTIVATED!") print("EMERGENCY STOP ACTIVATED!")
print("="*50) print("="*50)
@@ -81,7 +81,7 @@ def signal_handler(sig, frame):
"""Перехват Ctrl+C: вместо аварийного завершения программы """Перехват Ctrl+C: вместо аварийного завершения программы
выполняет безопасную посадку дрона.""" выполняет безопасную посадку дрона."""
print("\nCtrl+C detected - Emergency stop!") print("Ctrl+C detected - Emergency stop!")
emergency_stop() emergency_stop()
sys.exit(0) sys.exit(0)