WordPress-плагин Med Diagnostics: шорткод, настройки, камерное сканирование
This commit is contained in:
commit
106b4bf1c0
4 files changed
+975
No files matched your search
@@ -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, сканирование, рендер результата)
|
||||||
|
```
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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 = '<div class="med-error-box"><strong>Ошибка:</strong> ' + msg + '</div>';
|
||||||
|
if (videoUrl) {
|
||||||
|
html += '<div style="text-align:center; margin-top:15px">' +
|
||||||
|
'<a class="med-btn-download" href="' + absUrl(videoUrl) + '">🎬 Скачать записанное видео</a>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
resultContent.innerHTML = html;
|
||||||
|
analyzeBtn.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResult(data) {
|
||||||
|
resultCard.style.display = 'block';
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
// Кнопки скачивания (вверху блока результатов)
|
||||||
|
if (data.share_id) {
|
||||||
|
html += '<div class="med-download-buttons">' +
|
||||||
|
'<a class="med-btn-download" href="' + API_URL + '/download/' + data.share_id + '/pdf">📄 Скачать PDF</a>' +
|
||||||
|
'<a class="med-btn-download" href="' + API_URL + '/download/' + data.share_id + '/docx">📝 Скачать DOCX</a>';
|
||||||
|
if (data.video_url) {
|
||||||
|
html += '<a class="med-btn-download" href="' + absUrl(data.video_url) + '">🎬 Скачать видео</a>';
|
||||||
|
}
|
||||||
|
html += '<a class="med-btn-download" href="https://max.ru/miac_dnr_bot" target="_blank" rel="noopener">🏥 Запись на диспанцеризацию</a>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сводка
|
||||||
|
html += '<div class="med-summary-box">' + data.summary + '</div>';
|
||||||
|
|
||||||
|
// Предупреждения
|
||||||
|
if (data.warnings && data.warnings.length > 0) {
|
||||||
|
html += '<div class="med-warnings-box"><h3>⚠️ Требует внимания</h3><ul>';
|
||||||
|
data.warnings.forEach(function (w) { html += '<li>' + w + '</li>'; });
|
||||||
|
html += '</ul></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Таблица показателей
|
||||||
|
html += '<h3>Показатели</h3>';
|
||||||
|
html += '<table class="med-metrics-table"><thead><tr><th>Показатель</th><th>Значение</th><th>Оценка</th></tr></thead><tbody>';
|
||||||
|
data.metrics.forEach(function (m) {
|
||||||
|
var statusText = {
|
||||||
|
'ok': 'В норме',
|
||||||
|
'warning': 'Обратить внимание',
|
||||||
|
'danger': 'Отклонение',
|
||||||
|
'info': 'Информация'
|
||||||
|
}[m.status] || m.status;
|
||||||
|
html += '<tr>' +
|
||||||
|
'<td>' + m.name + '</td>' +
|
||||||
|
'<td><strong>' + m.value + '</strong>' + (m.comment ? '<br><small style="color:#888">' + m.comment + '</small>' : '') + '</td>' +
|
||||||
|
'<td><span class="med-status-badge med-status-' + m.status + '">' + statusText + '</span></td>' +
|
||||||
|
'</tr>';
|
||||||
|
});
|
||||||
|
html += '</tbody></table>';
|
||||||
|
|
||||||
|
// Рекомендации
|
||||||
|
if (data.advice && data.advice.length > 0) {
|
||||||
|
html += '<div class="med-advice-box"><h3>💡 Рекомендации</h3><ul>';
|
||||||
|
data.advice.forEach(function (a) { html += '<li>' + a + '</li>'; });
|
||||||
|
html += '</ul></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '<div class="med-disclaimer">⚠️ Результат получен автоматически по анализу видео и не является медицинским диагнозом. Для точной оценки обратитесь к врачу.</div>';
|
||||||
|
|
||||||
|
// Кнопка "Поделиться"
|
||||||
|
if (data.share_url) {
|
||||||
|
html += '<div style="text-align:center; margin-top:20px">' +
|
||||||
|
'<button class="med-btn" onclick="window.MED_DIAG_SHARE && MED_DIAG_SHARE(\'' + data.share_url + '\', this)">🔗 Поделиться результатом</button>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Med Diagnostics
|
||||||
|
* Description: Интеграция сервиса «Диагностика по видео» (med.my-dpr.ru) в WordPress. Выводит вёрстку сервиса через шорткод [med_diagnostics].
|
||||||
|
* Version: 1.0.0
|
||||||
|
* Author: apuc
|
||||||
|
* License: GPL-2.0-or-later
|
||||||
|
* Text Domain: med-diagnostics
|
||||||
|
*
|
||||||
|
* Использование:
|
||||||
|
* [med_diagnostics] — вставить блок диагностики
|
||||||
|
* [med_diagnostics title="Мой заголовок"] — со своим заголовком
|
||||||
|
*
|
||||||
|
* URL API можно изменить в настройках: Настройки → Med Diagnostics.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit; // Прямой доступ запрещён
|
||||||
|
}
|
||||||
|
|
||||||
|
define( 'MED_DIAG_VERSION', '1.0.0' );
|
||||||
|
define( 'MED_DIAG_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL API-бэкенда (Flask-приложение med.my-dpr.ru).
|
||||||
|
* Можно переопределить фильтром: add_filter('med_diag_api_url', fn() => '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();
|
||||||
|
?>
|
||||||
|
<div class="med-diag" id="medDiagApp">
|
||||||
|
<h2 class="med-diag-title"><?php echo esc_html( $atts['title'] ); ?></h2>
|
||||||
|
<p class="med-diag-subtitle">Загрузите видео с лицом — получите оценку состояния здоровья</p>
|
||||||
|
|
||||||
|
<div class="med-card med-scan-section" id="scanSection">
|
||||||
|
<h3>📷 Сканирование</h3>
|
||||||
|
<p class="med-scan-desc">Наведите камеру на лицо — мы сделаем 300 кадров и проведём анализ</p>
|
||||||
|
<button class="med-scan-btn" id="scanBtn">📷 Сканировать</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="med-card">
|
||||||
|
<h3>Загрузка видео</h3>
|
||||||
|
<div class="med-upload-area" id="uploadArea">
|
||||||
|
<div class="med-icon">📹</div>
|
||||||
|
<p><strong>Нажмите или перетащите видео сюда</strong></p>
|
||||||
|
<p class="med-hint">MP4, AVI, MOV, MKV • до 100 МБ • 10–15 секунд • лицо анфас</p>
|
||||||
|
<input type="file" id="fileInput" accept="video/*">
|
||||||
|
</div>
|
||||||
|
<div style="text-align:center">
|
||||||
|
<button class="med-btn" id="analyzeBtn" disabled>🔍 Анализировать</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="med-progress-container" id="progressContainer">
|
||||||
|
<div class="med-progress-bar">
|
||||||
|
<div class="med-progress-fill" id="progressFill"></div>
|
||||||
|
</div>
|
||||||
|
<div class="med-progress-text" id="progressText">Подготовка...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="med-card med-result" id="resultCard">
|
||||||
|
<div class="med-result-header">
|
||||||
|
<h3 style="margin:0">📋 Результат анализа</h3>
|
||||||
|
<button class="med-btn-secondary" onclick="location.reload()">🔄 Новый анализ</button>
|
||||||
|
</div>
|
||||||
|
<div id="resultContent"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Модал сканирования с камеры -->
|
||||||
|
<div class="med-scan-modal" id="scanModal">
|
||||||
|
<div class="med-scan-video-wrap">
|
||||||
|
<video class="med-scan-video" id="scanVideo" autoplay playsinline muted></video>
|
||||||
|
</div>
|
||||||
|
<div class="med-scan-progress">
|
||||||
|
<div id="scanProgressText">Кадров: 0 / 300 (0%)</div>
|
||||||
|
<div class="med-scan-progress-bar">
|
||||||
|
<div class="med-scan-progress-fill" id="scanProgressFill"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="med-scan-cancel" id="scanCancelBtn">✖ Отмена</button>
|
||||||
|
<div class="med-scan-hint">Держите лицо в круге, смотрите прямо в камеру, не двигайтесь</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Настройки (Настройки → Med Diagnostics) ─────────────── */
|
||||||
|
|
||||||
|
add_action( 'admin_menu', 'med_diag_admin_menu' );
|
||||||
|
|
||||||
|
function med_diag_admin_menu() {
|
||||||
|
add_options_page(
|
||||||
|
'Med Diagnostics',
|
||||||
|
'Med Diagnostics',
|
||||||
|
'manage_options',
|
||||||
|
'med-diagnostics',
|
||||||
|
'med_diag_settings_page'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
add_action( 'admin_init', 'med_diag_register_settings' );
|
||||||
|
|
||||||
|
function med_diag_register_settings() {
|
||||||
|
register_setting( 'med_diag_settings', 'med_diag_api_url', array(
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'esc_url_raw',
|
||||||
|
'default' => 'https://med.my-dpr.ru',
|
||||||
|
) );
|
||||||
|
}
|
||||||
|
|
||||||
|
function med_diag_settings_page() {
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>Med Diagnostics</h1>
|
||||||
|
<form method="post" action="options.php">
|
||||||
|
<?php settings_fields( 'med_diag_settings' ); ?>
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label for="med_diag_api_url">URL API-бэкенда</label></th>
|
||||||
|
<td>
|
||||||
|
<input type="url" id="med_diag_api_url" name="med_diag_api_url"
|
||||||
|
value="<?php echo esc_attr( med_diag_api_url() ); ?>" class="regular-text" />
|
||||||
|
<p class="description">Адрес Flask-приложения, которое выполняет анализ (по умолчанию https://med.my-dpr.ru).</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<?php submit_button(); ?>
|
||||||
|
</form>
|
||||||
|
<hr>
|
||||||
|
<h2>Использование</h2>
|
||||||
|
<p>Вставьте шорткод <code>[med_diagnostics]</code> в любую страницу или запись.</p>
|
||||||
|
<p>Дополнительные параметры: <code>[med_diagnostics title="Мой заголовок"]</code></p>
|
||||||
|
<p><strong>Важно:</strong> используйте шорткод один раз на странице.</p>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
Reference in new issue
Block a user