commit 106b4bf1c06698d01e00db6376dedde8908b2a42 Author: Kavalar Date: Mon Sep 14 14:32:41 2026 +0300 WordPress-плагин Med Diagnostics: шорткод, настройки, камерное сканирование diff --git a/README.md b/README.md new file mode 100644 index 0000000..e120a44 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# Med Diagnostics — WordPress-плагин + +Интеграция сервиса «Диагностика по видео» (API-бэкенд med.my-dpr.ru) в WordPress. +Плагин выводит вёрстку сервиса (загрузка видео, сканирование с камеры, результат) прямо на странице сайта — без iframe. + +## Установка + +### Вариант 1: через админку (zip) + +```bash +# из корня репозитория (папка репозитория = папка плагина) +zip -r med-diagnostics.zip . -x README.md +``` + +1. В админке WordPress: **Плагины → Добавить новый → Загрузить плагин** → выберите `med-diagnostics.zip` → **Установить** → **Активировать**. + +### Вариант 2: копированием файлов + +1. Скопируйте содержимое репозитория в `wp-content/plugins/med-diagnostics/` вашего WordPress. +2. В админке: **Плагины → Установленные** → активируйте **Med Diagnostics**. + +### Использование + +Вставьте шорткод в нужную страницу/запись: + +``` +[med_diagnostics] +``` + +Дополнительно можно задать заголовок: + +``` +[med_diagnostics title="Проверь своё здоровье"] +``` + +## Настройки + +**Настройки → Med Diagnostics** — поле «URL API-бэкенда» (по умолчанию `https://med.my-dpr.ru`). +Можно также переопределить кодом: + +```php +add_filter('med_diag_api_url', fn() => 'https://другой-домен.ru'); +``` + +## Как это работает + +- Плагин выводит ту же вёрстку, что и главная страница сервиса (стили с префиксом `med-`, чтобы не конфликтовать с темой). +- JS отправляет видео на API-бэкенд (`POST /analyze`) с абсолютным URL — поэтому нужен CORS на стороне Flask. +- Результат (сводка, показатели, рекомендации, кнопки PDF/DOCX/видео/поделиться) рендерится на странице WordPress. +- Сканирование с камеры работает на мобильных (canvas 720×720 + MediaRecorder). + +## Требования к бэкенду (Flask) + +В `app.py` серверного репозитория уже добавлена поддержка CORS: + +- `Access-Control-Allow-Origin` — по умолчанию `*`, можно ограничить через env `CORS_ORIGINS` (список через запятую, например `https://site1.ru,https://site2.ru`). +- Preflight `OPTIONS /analyze` возвращает `204`. + +## Локальное тестирование + +1. Поднимите API-бэкенд локально (см. README серверного репозитория) или используйте прод `https://med.my-dpr.ru`. +2. Установите плагин в локальный WordPress (например, на `http://wp.loc` через Apache vhost). +3. Создайте страницу с шорткодом `[med_diagnostics]` и откройте её. +4. Проверьте в консоли браузера, что `MED_DIAG.apiUrl` указывает на нужный бэкенд, и что запрос `POST /analyze` проходит (CORS). + +## Ограничения + +- Шорткод используйте **один раз на странице** (id элементов фиксированы). +- Для работы сканирования с камеры нужен HTTPS на сайте WordPress (getUserMedia требует secure context). +- Загрузка видео идёт напрямую в API-бэкенд — убедитесь, что `client_max_body_size` в nginx бэкенда ≥ 100 МБ. + +## Структура + +Корень репозитория = папка плагина: + +``` +med-diagnostics/ +├── med-diagnostics.php — главный файл плагина (шорткод, настройки, ассеты) +├── README.md — этот файл (в zip не попадает) +└── assets/ + ├── css/med-diag.css — стили сервиса (префикс med-) + └── js/med-diag.js — логика (fetch на API, сканирование, рендер результата) +``` \ No newline at end of file diff --git a/assets/css/med-diag.css b/assets/css/med-diag.css new file mode 100644 index 0000000..6b9c077 --- /dev/null +++ b/assets/css/med-diag.css @@ -0,0 +1,357 @@ +/* Med Diagnostics — стили сервиса (префикс med- для изоляции от темы WordPress) */ + +.med-diag { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + max-width: 900px; + margin: 0 auto; + color: #333; + box-sizing: border-box; +} + +.med-diag *, .med-diag *::before, .med-diag *::after { box-sizing: border-box; } + +.med-diag-title { + color: #061A40; + text-align: center; + margin-bottom: 10px; + font-size: 28px; + line-height: 1.3; +} + +.med-diag-subtitle { + color: #555; + text-align: center; + margin-bottom: 25px; + font-size: 15px; +} + +.med-card { + background: #faf6ec; + border-radius: 16px; + padding: 30px; + box-shadow: 0 10px 40px rgba(0,0,0,0.2); + margin-bottom: 20px; +} + +.med-card h3 { + color: #333; + margin-bottom: 15px; + font-size: 20px; + line-height: 1.3; +} + +.med-upload-area { + border: 2px dashed #ccc; + border-radius: 12px; + padding: 40px; + text-align: center; + cursor: pointer; + transition: all 0.3s; + background: #fafafa; +} + +.med-upload-area:hover, .med-upload-area.dragover { + border-color: #0014FC; + background: #f0f2ff; +} + +.med-upload-area .med-icon { font-size: 48px; margin-bottom: 10px; } +.med-upload-area p { color: #666; margin: 5px 0; } +.med-upload-area .med-hint { font-size: 13px; color: #999; } + +.med-diag input[type="file"] { display: none; } + +.med-btn { + background: linear-gradient(135deg, #061A40, #0B4F9C, #2D9CDB); + color: #fff; + border: none; + padding: 12px 30px; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + transition: background 0.3s; + margin-top: 15px; + display: inline-block; + line-height: 1.5; +} + +.med-btn:hover { filter: brightness(1.15); } +.med-btn:disabled { background: #ccc; cursor: not-allowed; } + +.med-btn-secondary { + background: #eee; + color: #333; + border: none; + padding: 10px 20px; + border-radius: 8px; + font-size: 14px; + cursor: pointer; + transition: background 0.3s; +} + +.med-btn-secondary:hover { background: #ddd; } + +/* Кнопки скачивания */ +.med-download-buttons { + display: flex; + gap: 10px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.med-btn-download { + flex: 1; + min-width: 140px; + text-align: center; + background: linear-gradient(135deg, #061A40, #0B4F9C, #2D9CDB); + color: #fff; + border: none; + padding: 12px 20px; + border-radius: 8px; + font-size: 15px; + cursor: pointer; + transition: background 0.3s; + text-decoration: none; + display: inline-block; + line-height: 1.5; +} + +.med-btn-download:hover { filter: brightness(1.15); color: #fff; } +.med-btn-download:active { transform: scale(0.98); } + +/* Сканирование с камеры */ +.med-scan-section { display: none; margin-bottom: 15px; } +.med-scan-desc { color: #666; font-size: 14px; margin-bottom: 15px; } + +.med-scan-btn { + width: 100%; + background: linear-gradient(135deg, #061A40, #0B4F9C, #2D9CDB); + color: #fff; + border: none; + padding: 16px 20px; + border-radius: 12px; + font-size: 17px; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + min-height: 52px; + box-shadow: 0 4px 15px rgba(102,126,234,0.4); + transition: transform 0.2s, box-shadow 0.2s; +} + +.med-scan-btn:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(102,126,234,0.5); } +.med-scan-btn:active { transform: scale(0.98); } +.med-scan-btn:disabled { background: #ccc; box-shadow: none; cursor: not-allowed; } + +.med-scan-modal { + display: none; + position: fixed; + inset: 0; + background: rgba(10, 10, 30, 0.92); + z-index: 100000; + align-items: center; + justify-content: center; + flex-direction: column; + padding: 20px; +} + +.med-scan-modal.open { display: flex; } + +.med-scan-video-wrap { + width: 280px; + height: 280px; + border-radius: 50%; + overflow: hidden; + border: 4px solid #0014FC; + background: #000; + box-shadow: 0 0 40px rgba(102,126,234,0.5); + position: relative; +} + +.med-scan-video { + width: 100%; + height: 100%; + object-fit: cover; + transform: scaleX(-1); +} + +.med-scan-progress { color: #fff; margin-top: 20px; font-size: 16px; text-align: center; } + +.med-scan-progress-bar { + width: 280px; + height: 8px; + background: rgba(255,255,255,0.2); + border-radius: 4px; + margin-top: 10px; + overflow: hidden; +} + +.med-scan-progress-fill { + height: 100%; + width: 0%; + background: linear-gradient(90deg, #061A40, #0B4F9C, #2D9CDB); + transition: width 0.2s; + border-radius: 4px; +} + +.med-scan-cancel { + margin-top: 20px; + background: rgba(255,255,255,0.12); + color: #fff; + border: 1px solid rgba(255,255,255,0.35); + padding: 12px 30px; + border-radius: 8px; + font-size: 15px; + cursor: pointer; + transition: background 0.3s; +} + +.med-scan-cancel:hover { background: rgba(255,255,255,0.2); } +.med-scan-hint { color: rgba(255,255,255,0.7); margin-top: 15px; font-size: 13px; text-align: center; max-width: 300px; } + +/* Прогресс */ +.med-progress-container { display: none; margin-top: 20px; } + +.med-progress-bar { + width: 100%; + height: 20px; + background: #eee; + border-radius: 10px; + overflow: hidden; +} + +.med-progress-fill { + height: 100%; + width: 0%; + background: linear-gradient(90deg, #061A40, #0B4F9C, #2D9CDB); + transition: width 0.3s; + border-radius: 10px; +} + +.med-progress-text { + text-align: center; + margin-top: 10px; + color: #666; + font-size: 14px; +} + +/* Результат */ +.med-result { display: none; } + +.med-summary-box { + background: linear-gradient(135deg, #061A40, #0B4F9C, #2D9CDB); + color: #fff; + padding: 15px 20px; + border-radius: 8px; + margin-bottom: 20px; + font-size: 16px; + line-height: 1.6; +} + +.med-metrics-table { + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; +} + +.med-metrics-table th, .med-metrics-table td { + padding: 10px 15px; + text-align: left; + border-bottom: 1px solid #eee; +} + +.med-metrics-table th { + background: #f8f9fa; + font-weight: 600; + color: #555; +} + +.med-metrics-table tr:hover { background: #fafafa; } + +.med-status-badge { + display: inline-block; + padding: 3px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; +} + +.med-status-ok { background: #d4edda; color: #155724; } +.med-status-warning { background: #ffe0d6; color: #a04a2f; } +.med-status-danger { background: #f8d7da; color: #721c24; } +.med-status-info { background: #dbe9ff; color: #1e4a8a; } + +.med-warnings-box { + background: #ffe0d6; + border-left: 4px solid #ff7f50; + padding: 15px 20px; + border-radius: 8px; + margin-bottom: 20px; +} + +.med-warnings-box h3 { color: #a04a2f; margin-bottom: 10px; font-size: 16px; } +.med-warnings-box ul { padding-left: 20px; margin: 0; } +.med-warnings-box li { margin: 5px 0; color: #a04a2f; } + +.med-advice-box { + background: #d4edda; + border-left: 4px solid #28a745; + padding: 15px 20px; + border-radius: 8px; + margin-bottom: 20px; +} + +.med-advice-box h3 { color: #155724; margin-bottom: 10px; font-size: 16px; } +.med-advice-box ul { padding-left: 20px; margin: 0; } +.med-advice-box li { margin: 5px 0; color: #155724; } + +.med-disclaimer { + background: #f8f9fa; + border-radius: 8px; + padding: 12px 15px; + font-size: 13px; + color: #888; + margin-top: 20px; +} + +.med-error-box { + background: #f8d7da; + border-left: 4px solid #dc3545; + padding: 15px 20px; + border-radius: 8px; + margin-bottom: 20px; + color: #721c24; +} + +.med-result-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + gap: 10px; +} + +/* Мобильная версия */ +@media (max-width: 600px) { + .med-diag-title { font-size: 22px; margin-bottom: 12px; } + .med-diag-subtitle { font-size: 14px; margin-bottom: 18px; } + .med-card { padding: 18px; border-radius: 14px; margin-bottom: 14px; } + .med-card h3 { font-size: 18px; } + .med-upload-area { padding: 25px 15px; } + .med-upload-area .med-icon { font-size: 40px; } + .med-btn, .med-btn-secondary { width: 100%; padding: 14px 20px; font-size: 16px; min-height: 48px; } + .med-download-buttons { flex-direction: column; } + .med-btn-download { width: 100%; min-width: 0; padding: 14px 20px; font-size: 16px; min-height: 48px; } + .med-result-header { flex-direction: column; align-items: stretch; } + .med-result-header h3 { text-align: center; } + .med-metrics-table thead { display: none; } + .med-metrics-table, .med-metrics-table tbody, .med-metrics-table tr, .med-metrics-table td { display: block; width: 100%; } + .med-metrics-table tr { border: 1px solid #eee; border-radius: 10px; margin-bottom: 10px; padding: 10px 12px; } + .med-metrics-table td { border: none; padding: 5px 0; } + .med-metrics-table td:first-child { font-weight: 600; color: #555; } + .med-summary-box, .med-warnings-box, .med-advice-box { padding: 12px 14px; font-size: 15px; } + .med-progress-text { font-size: 13px; } +} \ No newline at end of file diff --git a/assets/js/med-diag.js b/assets/js/med-diag.js new file mode 100644 index 0000000..3fc0ee3 --- /dev/null +++ b/assets/js/med-diag.js @@ -0,0 +1,355 @@ +/* Med Diagnostics — логика сервиса (адаптирована для WordPress) */ +(function () { + 'use strict'; + + var API_URL = (window.MED_DIAG && MED_DIAG.apiUrl) || 'https://med.my-dpr.ru'; + + // Абсолютный URL: если ссылка относительная — добавляем домен API + function absUrl(url) { + if (!url) return url; + return url.indexOf('http') === 0 ? url : API_URL + url; + } + + var uploadArea = document.getElementById('uploadArea'); + var fileInput = document.getElementById('fileInput'); + var analyzeBtn = document.getElementById('analyzeBtn'); + var progressContainer = document.getElementById('progressContainer'); + var progressFill = document.getElementById('progressFill'); + var progressText = document.getElementById('progressText'); + var resultCard = document.getElementById('resultCard'); + var resultContent = document.getElementById('resultContent'); + + if (!uploadArea || !fileInput || !analyzeBtn) return; // шорткод не выведен + + var selectedFile = null; + + uploadArea.addEventListener('click', function () { fileInput.click(); }); + uploadArea.addEventListener('dragover', function (e) { + e.preventDefault(); + uploadArea.classList.add('dragover'); + }); + uploadArea.addEventListener('dragleave', function () { uploadArea.classList.remove('dragover'); }); + uploadArea.addEventListener('drop', function (e) { + e.preventDefault(); + uploadArea.classList.remove('dragover'); + if (e.dataTransfer.files.length > 0) { + handleFile(e.dataTransfer.files[0]); + } + }); + fileInput.addEventListener('change', function (e) { + if (e.target.files.length > 0) handleFile(e.target.files[0]); + }); + + function handleFile(file) { + selectedFile = file; + uploadArea.querySelector('p strong').textContent = '📁 ' + file.name + ' (' + (file.size / 1024 / 1024).toFixed(1) + ' МБ)'; + uploadArea.querySelector('.med-hint').textContent = 'Готово к анализу'; + analyzeBtn.disabled = false; + } + + analyzeBtn.addEventListener('click', function () { + if (!selectedFile) return; + sendVideo(selectedFile, selectedFile.name); + }); + + async function sendVideo(blob, filename) { + analyzeBtn.disabled = true; + progressContainer.style.display = 'block'; + resultCard.style.display = 'none'; + setProgress(5, 'Загрузка видео...'); + + var formData = new FormData(); + formData.append('video', blob, filename); + + // Автоповтор при обрыве соединения (нестабильная мобильная сеть) + var MAX_ATTEMPTS = 3; + for (var attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + try { + var response = await fetch(API_URL + '/analyze', { + method: 'POST', + body: formData + }); + + if (!response.ok) { + var err = await response.json(); + var e = new Error(err.error || 'Ошибка анализа'); + e.videoUrl = err.video_url; + throw e; + } + + setProgress(90, 'Формирование результата...'); + var data = await response.json(); + + if (data.error) { + showError(data.error); + return; + } + + setProgress(100, 'Готово!'); + setTimeout(function () { + progressContainer.style.display = 'none'; + renderResult(data); + }, 300); + return; + } catch (err) { + // TypeError "Load failed" = обрыв соединения — пробуем ещё раз + if (err instanceof TypeError && attempt < MAX_ATTEMPTS) { + setProgress(5, 'Соединение прервано, повторная попытка (' + attempt + '/' + (MAX_ATTEMPTS - 1) + ')...'); + await new Promise(function (r) { setTimeout(r, 1500); }); + continue; + } + if (err instanceof TypeError) { + showError('Соединение прервано при загрузке видео. Проверьте интернет и попробуйте ещё раз.'); + } else { + showError(err.message, err.videoUrl); + } + return; + } + } + } + + function setProgress(pct, text) { + progressFill.style.width = pct + '%'; + progressText.textContent = text; + } + + function showError(msg, videoUrl) { + progressContainer.style.display = 'none'; + resultCard.style.display = 'block'; + var html = '
Ошибка: ' + msg + '
'; + if (videoUrl) { + html += '
' + + '🎬 Скачать записанное видео' + + '
'; + } + resultContent.innerHTML = html; + analyzeBtn.disabled = false; + } + + function renderResult(data) { + resultCard.style.display = 'block'; + + var html = ''; + + // Кнопки скачивания (вверху блока результатов) + if (data.share_id) { + html += '
' + + '📄 Скачать PDF' + + '📝 Скачать DOCX'; + if (data.video_url) { + html += '🎬 Скачать видео'; + } + html += '🏥 Запись на диспанцеризацию' + + '
'; + } + + // Сводка + html += '
' + data.summary + '
'; + + // Предупреждения + if (data.warnings && data.warnings.length > 0) { + html += '

⚠️ Требует внимания

'; + } + + // Таблица показателей + html += '

Показатели

'; + html += ''; + data.metrics.forEach(function (m) { + var statusText = { + 'ok': 'В норме', + 'warning': 'Обратить внимание', + 'danger': 'Отклонение', + 'info': 'Информация' + }[m.status] || m.status; + html += '' + + '' + + '' + + '' + + ''; + }); + html += '
ПоказательЗначениеОценка
' + m.name + '' + m.value + '' + (m.comment ? '
' + m.comment + '' : '') + '
' + statusText + '
'; + + // Рекомендации + if (data.advice && data.advice.length > 0) { + html += '

💡 Рекомендации

'; + } + + html += '
⚠️ Результат получен автоматически по анализу видео и не является медицинским диагнозом. Для точной оценки обратитесь к врачу.
'; + + // Кнопка "Поделиться" + if (data.share_url) { + html += '
' + + '' + + '
'; + } + + resultContent.innerHTML = html; + resultCard.scrollIntoView({ behavior: 'smooth' }); + } + + // Поделиться (глобальная функция для inline-onclick) + window.MED_DIAG_SHARE = function (url, btn) { + navigator.clipboard.writeText(url).then(function () { + var oldText = btn.textContent; + btn.textContent = '✅ Ссылка скопирована!'; + setTimeout(function () { btn.textContent = oldText; }, 2000); + }); + }; + + // ─── Сканирование с камеры ─── + var scanSection = document.getElementById('scanSection'); + var scanBtn = document.getElementById('scanBtn'); + var scanModal = document.getElementById('scanModal'); + var scanVideo = document.getElementById('scanVideo'); + var scanProgressText = document.getElementById('scanProgressText'); + var scanProgressFill = document.getElementById('scanProgressFill'); + var scanCancelBtn = document.getElementById('scanCancelBtn'); + + var SCAN_FRAMES = 310; // чуть больше 300, чтобы в файле гарантированно было >= 300 кадров + var SCAN_FPS = 30; + var FRAME_INTERVAL = 1000 / SCAN_FPS; + + var mediaStream = null; + var mediaRecorder = null; + var scanCanvas = null; + var scanCtx = null; + var scanFrameCount = 0; + var scanRafId = null; + var scanLastFrameTime = 0; + var scanChunks = []; + var scanActive = false; + + // Показываем кнопку сканирования только на мобильных устройствах + var isMobile = window.matchMedia('(max-width: 768px)').matches || ('ontouchstart' in window); + if (isMobile && scanSection) { + scanSection.style.display = 'block'; + } + + if (scanBtn) scanBtn.addEventListener('click', startScan); + if (scanCancelBtn) scanCancelBtn.addEventListener('click', cancelScan); + + async function startScan() { + if (scanActive) return; + scanActive = true; + scanBtn.disabled = true; + + // Включаем фронтальную камеру + try { + mediaStream = await navigator.mediaDevices.getUserMedia({ + video: { + facingMode: 'user', + width: { ideal: 1280 }, + height: { ideal: 720 } + }, + audio: false + }); + } catch (err) { + scanActive = false; + scanBtn.disabled = false; + showError('Не удалось получить доступ к камере: ' + (err.message || 'разрешите доступ')); + return; + } + + scanModal.classList.add('open'); + scanVideo.srcObject = mediaStream; + try { await scanVideo.play(); } catch (e) {} + + scanChunks = []; + scanFrameCount = 0; + scanLastFrameTime = 0; + updateScanProgress(0); + + // Canvas 720×720 — квадрат, лицо целиком влезает в portrait и landscape. + // Серверное extract_frames масштабирует до 1280×720 для SDK. + scanCanvas = document.createElement('canvas'); + scanCanvas.width = 720; + scanCanvas.height = 720; + scanCtx = scanCanvas.getContext('2d'); + + var captureStream; + try { + captureStream = scanCanvas.captureStream(SCAN_FPS); + } catch (e) { + // Fallback: записываем напрямую с камеры + captureStream = mediaStream; + } + + var mimeType = ['video/webm;codecs=vp9', 'video/webm;codecs=vp8', 'video/webm', 'video/mp4'] + .find(function (t) { return MediaRecorder.isTypeSupported(t); }) || ''; + mediaRecorder = new MediaRecorder(captureStream, { + mimeType: mimeType || undefined, + videoBitsPerSecond: 1500000 // 1.5 Мбит/с — маленький файл, качество достаточное + }); + mediaRecorder.ondataavailable = function (e) { if (e.data.size > 0) scanChunks.push(e.data); }; + mediaRecorder.onstop = function () { + var blob = new Blob(scanChunks, { type: mediaRecorder.mimeType || 'video/webm' }); + var ext = blob.type.indexOf('mp4') !== -1 ? 'mp4' : 'webm'; + scanModal.classList.remove('open'); + stopCamera(); + scanActive = false; + scanBtn.disabled = false; + sendVideo(blob, 'scan.' + ext); + }; + mediaRecorder.start(); + + // Рисуем кадры с ограничением ~30 fps, считаем до 300 + var drawFrame = function (ts) { + if (!scanActive) return; + if (scanFrameCount >= SCAN_FRAMES) { + if (mediaRecorder && mediaRecorder.state !== 'inactive') { + mediaRecorder.stop(); + } + return; + } + if (ts - scanLastFrameTime >= FRAME_INTERVAL) { + // Квадратный center-crop: берём центральную квадратную область из видео. + var vw = scanVideo.videoWidth; + var vh = scanVideo.videoHeight; + if (vw && vh) { + var side = Math.min(vw, vh); + var sx = (vw - side) / 2; + var sy = (vh - side) / 2; + scanCtx.drawImage(scanVideo, sx, sy, side, side, 0, 0, 720, 720); + } else { + scanCtx.drawImage(scanVideo, 0, 0, 720, 720); + } + scanFrameCount++; + scanLastFrameTime = ts; + updateScanProgress(scanFrameCount); + } + scanRafId = requestAnimationFrame(drawFrame); + }; + scanRafId = requestAnimationFrame(drawFrame); + } + + function updateScanProgress(count) { + var pct = Math.min(100, Math.round(count / SCAN_FRAMES * 100)); + scanProgressText.textContent = 'Кадров: ' + count + ' / ' + SCAN_FRAMES + ' (' + pct + '%)'; + scanProgressFill.style.width = pct + '%'; + } + + function stopCamera() { + if (mediaStream) { + mediaStream.getTracks().forEach(function (t) { t.stop(); }); + mediaStream = null; + } + if (scanRafId) cancelAnimationFrame(scanRafId); + scanRafId = null; + scanVideo.srcObject = null; + } + + function cancelScan() { + scanActive = false; + if (mediaRecorder && mediaRecorder.state !== 'inactive') { + mediaRecorder.onstop = null; + mediaRecorder.stop(); + } + stopCamera(); + scanModal.classList.remove('open'); + scanBtn.disabled = false; + } +})(); \ No newline at end of file diff --git a/med-diagnostics.php b/med-diagnostics.php new file mode 100644 index 0000000..449544a --- /dev/null +++ b/med-diagnostics.php @@ -0,0 +1,180 @@ + 'https://...'); + */ +function med_diag_api_url() { + $url = get_option( 'med_diag_api_url', 'https://med.my-dpr.ru' ); + return apply_filters( 'med_diag_api_url', untrailingslashit( $url ) ); +} + +/* ─── Регистрация ассетов ─────────────────────────────────── */ + +add_action( 'wp_enqueue_scripts', 'med_diag_register_assets' ); + +function med_diag_register_assets() { + wp_register_style( + 'med-diag-css', + MED_DIAG_PLUGIN_URL . 'assets/css/med-diag.css', + array(), + MED_DIAG_VERSION + ); + wp_register_script( + 'med-diag-js', + MED_DIAG_PLUGIN_URL . 'assets/js/med-diag.js', + array(), + MED_DIAG_VERSION, + true // в футере + ); + wp_localize_script( 'med-diag-js', 'MED_DIAG', array( + 'apiUrl' => med_diag_api_url(), + ) ); +} + +/* ─── Шорткод ─────────────────────────────────────────────── */ + +add_shortcode( 'med_diagnostics', 'med_diag_shortcode' ); + +function med_diag_shortcode( $atts ) { + $atts = shortcode_atts( + array( + 'title' => '🩺 Диагностика по видео', + ), + $atts, + 'med_diagnostics' + ); + + wp_enqueue_style( 'med-diag-css' ); + wp_enqueue_script( 'med-diag-js' ); + + ob_start(); + ?> +
+

+

Загрузите видео с лицом — получите оценку состояния здоровья

+ +
+

📷 Сканирование

+

Наведите камеру на лицо — мы сделаем 300 кадров и проведём анализ

+ +
+ +
+

Загрузка видео

+
+
📹
+

Нажмите или перетащите видео сюда

+

MP4, AVI, MOV, MKV • до 100 МБ • 10–15 секунд • лицо анфас

+ +
+
+ +
+ +
+
+
+
+
Подготовка...
+
+
+ +
+
+

📋 Результат анализа

+ +
+
+
+
+ + +
+
+ +
+
+
Кадров: 0 / 300 (0%)
+
+
+
+
+ +
Держите лицо в круге, смотрите прямо в камеру, не двигайтесь
+
+ 'string', + 'sanitize_callback' => 'esc_url_raw', + 'default' => 'https://med.my-dpr.ru', + ) ); +} + +function med_diag_settings_page() { + ?> +
+

Med Diagnostics

+
+ + + + + + + + +
+
+

Использование

+

Вставьте шорткод [med_diagnostics] в любую страницу или запись.

+

Дополнительные параметры: [med_diagnostics title="Мой заголовок"]

+

Важно: используйте шорткод один раз на странице.

+
+