Files
medods_vk/templates/medods.html
T
2025-12-17 22:02:26 +03:00

714 lines
28 KiB
HTML

{% extends "base.html" %}
{% block title %}Medods{% endblock %}
{% block content %}
<div class="container-fluid">
<h3 class="mb-4">🔌 Medods API</h3>
<!-- Настройка подключения -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">Подключение к серверу</h5>
</div>
<div class="card-body">
<form id="serverForm">
<div class="row g-3 align-items-end">
<div class="col-md-9">
<label class="form-label">URL адрес сервера</label>
<input type="text" class="form-control" id="server_url" placeholder="https://api.example.com"
required>
</div>
<div class="col-md-3">
<button type="button" class="btn btn-success w-100" onclick="saveServerUrl()">
💾 Сохранить
</button>
</div>
</div>
</form>
<hr class="my-4">
<form id="apiKeyForm" enctype="multipart/form-data">
<div class="row g-3 align-items-end">
<div class="col-md-9">
<label class="form-label">Загрузка API ключа</label>
<input type="file" class="form-control" id="api_key_file" accept=".csv" required>
<div class="form-text">
Файл формата CSV с колонками: identity;secretKey
</div>
</div>
<div class="col-md-3">
<button type="button" class="btn btn-primary w-100" onclick="uploadApiKey()">
📤 Загрузить apiKey.csv
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Аккордеон с запросами -->
<div class="accordion mb-4" id="requestsAccordion">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#requestsCollapse" aria-expanded="false" aria-controls="requestsCollapse">
⚙️ Настроенные запросы
</button>
</h2>
<div id="requestsCollapse" class="accordion-collapse collapse" data-bs-parent="#requestsAccordion">
<div class="accordion-body">
<!-- Форма создания/редактирования запроса -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Добавить/Редактировать запрос</h5>
</div>
<div class="card-body">
<form id="requestForm">
<input type="hidden" id="requestId">
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label">Название запроса</label>
<input type="text" class="form-control" id="title"
placeholder="Получить список пользователей" required>
</div>
<div class="col-md-3">
<label class="form-label">HTTP метод</label>
<select class="form-select" id="method" required>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="PATCH">PATCH</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">URL путь</label>
<input type="text" class="form-control" id="url_path" placeholder="/users"
required>
</div>
</div>
<!-- Динамические параметры Query -->
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label fw-bold">Параметры Query</label>
<button type="button" class="btn btn-sm btn-outline-primary"
onclick="addQueryParam()">
➕ Добавить параметр
</button>
</div>
<div id="queryParamsContainer">
<!-- Поля будут добавляться динамически -->
</div>
</div>
<!-- Динамические параметры Payload -->
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label fw-bold">Параметры Payload (для POST/PUT/PATCH)</label>
<button type="button" class="btn btn-sm btn-outline-primary"
onclick="addPayloadParam()">
➕ Добавить параметр
</button>
</div>
<div id="payloadParamsContainer">
<!-- Поля будут добавляться динамически -->
</div>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-success" onclick="saveRequest()">
💾 Сохранить запрос
</button>
<button type="button" class="btn btn-secondary" onclick="resetForm()">
🆕 Новый запрос
</button>
</div>
</form>
</div>
</div>
<!-- Список существующих запросов -->
<div id="requestsList">
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Раздел выполнения запросов -->
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="mb-0">📥 Выполнение запроса</h5>
</div>
<div class="card-body">
<form id="executeForm">
<div class="row g-3 align-items-end mb-4">
<div class="col-md-10">
<label class="form-label">Выберите запрос для выполнения</label>
<select class="form-select" id="requestSelect" required>
<option value="" disabled selected>Выберите запрос...</option>
</select>
</div>
<div class="col-md-2">
<button type="button" class="btn btn-warning w-100" onclick="executeRequest()">
🚀 Отправить запрос
</button>
</div>
</div>
</form>
<!-- Окно с результатом -->
<div id="responseSection" style="display: none;">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6>Результат выполнения:</h6>
<button type="button" class="btn btn-sm btn-outline-success" onclick="downloadResponse()">
⬇️ Скачать JSON
</button>
</div>
<div class="card">
<div class="card-body">
<div id="responseContainer" class="response-container"
style="max-height: 500px; overflow-y: auto;">
<!-- Ответ будет отображен здесь -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.response-container {
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.4;
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
}
.json-key {
color: #92278f;
font-weight: bold;
}
.json-string {
color: #3ab54a;
}
.json-number {
color: #25aae2;
}
.json-boolean {
color: #f98280;
}
.json-null {
color: #f1592a;
}
.param-row {
margin-bottom: 8px;
padding: 8px;
background-color: #f8f9fa;
border-radius: 4px;
border-left: 4px solid #0d6efd;
}
.accordion-button:not(.collapsed) {
background-color: #e7f1ff;
color: #0c63e4;
}
</style>
<script>
// Сохранение URL сервера
async function saveServerUrl() {
const serverUrl = document.getElementById('server_url').value;
if (!serverUrl) {
alert('Пожалуйста, введите URL сервера');
return;
}
try {
const response = await fetch('/settings/medods_url', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ url: serverUrl })
});
if (response.ok) {
alert('URL сервера сохранен!');
} else {
const error = await response.text();
alert('Ошибка сохранения: ' + error);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка сохранения!');
}
}
// Загрузка API ключа
async function uploadApiKey() {
const fileInput = document.getElementById('api_key_file');
const file = fileInput.files[0];
if (!file) {
alert('Пожалуйста, выберите файл');
return;
}
try {
const text = await file.text();
const lines = text.split('\n');
if (lines.length < 2) {
alert('Файл должен содержать заголовок и данные');
return;
}
const headers = lines[0].split(';').map(h => h.trim());
if (!headers.includes('identity') || !headers.includes('secretKey')) {
alert('Файл должен содержать колонки: identity и secretKey');
return;
}
const data = {};
for (let i = 1; i < lines.length; i++) {
if (lines[i].trim()) {
const values = lines[i].split(';').map(v => v.trim());
if (values.length >= 2) {
data[values[0]] = values[1];
}
}
}
const response = await fetch('/settings/medods_apikey', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (response.ok) {
alert('API ключ загружен!');
fileInput.value = '';
} else {
const error = await response.text();
alert('Ошибка загрузки: ' + error);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка загрузки файла!');
}
}
// Загрузка запросов при раскрытии аккордеона
document.getElementById('requestsAccordion').addEventListener('show.bs.collapse', function () {
loadRequests();
});
// Загрузка списка запросов
async function loadRequests() {
try {
const response = await fetch('/settings/requests');
const requests = await response.json();
// Обновляем выпадающий список для выполнения
const select = document.getElementById('requestSelect');
select.innerHTML = '<option value="" disabled selected>Выберите запрос...</option>';
requests.forEach(req => {
const option = document.createElement('option');
option.value = req.id;
option.textContent = `${req.id} - ${req.title}`;
select.appendChild(option);
});
// Отображаем список запросов
const container = document.getElementById('requestsList');
container.innerHTML = '';
if (requests.length === 0) {
container.innerHTML = '<div class="alert alert-info">Нет настроенных запросов</div>';
return;
}
requests.forEach(request => {
const card = document.createElement('div');
card.className = 'card mb-2';
card.innerHTML = `
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1">${request.title}</h6>
<small class="text-muted">
${request.method} ${request.url_path}
</small>
</div>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" onclick="editRequest(${request.id})">
✏️
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteRequest(${request.id})">
🗑️
</button>
</div>
</div>
</div>
`;
container.appendChild(card);
});
// Сохраняем запросы для использования
window.requestsData = requests;
} catch (error) {
console.error('Ошибка загрузки запросов:', error);
document.getElementById('requestsList').innerHTML =
'<div class="alert alert-danger">Ошибка загрузки запросов</div>';
}
}
// Добавление параметра Query
function addQueryParam(key = '', value = '') {
const container = document.getElementById('queryParamsContainer');
const div = document.createElement('div');
div.className = 'row g-2 param-row';
div.innerHTML = `
<div class="col-md-5">
<input type="text" class="form-control param-key" placeholder="Ключ" value="${key}">
</div>
<div class="col-md-5">
<input type="text" class="form-control param-value" placeholder="Значение" value="${value}">
</div>
<div class="col-md-2">
<button type="button" class="btn btn-sm btn-outline-danger w-100" onclick="this.parentElement.parentElement.remove()">
🗑️
</button>
</div>
`;
container.appendChild(div);
}
// Добавление параметра Payload
function addPayloadParam(key = '', value = '') {
const container = document.getElementById('payloadParamsContainer');
const div = document.createElement('div');
div.className = 'row g-2 param-row';
div.innerHTML = `
<div class="col-md-5">
<input type="text" class="form-control param-key" placeholder="Ключ" value="${key}">
</div>
<div class="col-md-5">
<input type="text" class="form-control param-value" placeholder="Значение" value="${value}">
</div>
<div class="col-md-2">
<button type="button" class="btn btn-sm btn-outline-danger w-100" onclick="this.parentElement.parentElement.remove()">
🗑️
</button>
</div>
`;
container.appendChild(div);
}
// Редактирование запроса
function editRequest(id) {
const request = window.requestsData.find(r => r.id === id);
if (!request) return;
document.getElementById('requestId').value = request.id;
document.getElementById('title').value = request.title;
document.getElementById('method').value = request.method;
document.getElementById('url_path').value = request.url_path;
// Очищаем контейнеры параметров
document.getElementById('queryParamsContainer').innerHTML = '';
document.getElementById('payloadParamsContainer').innerHTML = '';
// Добавляем query параметры
if (request.query && typeof request.query === 'object') {
Object.entries(request.query).forEach(([key, value]) => {
addQueryParam(key, value);
});
}
// Добавляем payload параметры
if (request.payload && typeof request.payload === 'object') {
Object.entries(request.payload).forEach(([key, value]) => {
addPayloadParam(key, typeof value === 'object' ? JSON.stringify(value) : value);
});
}
// Прокручиваем к форме
document.getElementById('requestForm').scrollIntoView({ behavior: 'smooth' });
}
// Сброс формы
function resetForm() {
document.getElementById('requestForm').reset();
document.getElementById('requestId').value = '';
document.getElementById('queryParamsContainer').innerHTML = '';
document.getElementById('payloadParamsContainer').innerHTML = '';
}
// Сохранение запроса
async function saveRequest() {
const id = document.getElementById('requestId').value;
const title = document.getElementById('title').value;
const method = document.getElementById('method').value;
const url_path = document.getElementById('url_path').value;
if (!title || !method || !url_path) {
alert('Пожалуйста, заполните все обязательные поля');
return;
}
// Собираем query параметры
const query = {};
document.querySelectorAll('#queryParamsContainer .param-row').forEach(row => {
const key = row.querySelector('.param-key').value;
const value = row.querySelector('.param-value').value;
if (key) query[key] = value;
});
// Собираем payload параметры
const payload = {};
document.querySelectorAll('#payloadParamsContainer .param-row').forEach(row => {
const key = row.querySelector('.param-key').value;
const value = row.querySelector('.param-value').value;
if (key) {
// Пробуем парсить JSON, если это объект
try {
payload[key] = JSON.parse(value);
} catch {
payload[key] = value;
}
}
});
const requestData = {
title,
method,
url_path,
query,
payload
};
if (id) {
requestData.id = parseInt(id);
}
try {
const response = await fetch('/settings/requests', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
if (response.ok) {
alert('Запрос сохранен!');
resetForm();
loadRequests();
} else {
const error = await response.text();
alert('Ошибка сохранения: ' + error);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка сохранения!');
}
}
// Удаление запроса
async function deleteRequest(id) {
if (!confirm('Вы уверены, что хотите удалить этот запрос?')) {
return;
}
try {
const response = await fetch(`/settings/requests/${id}`, {
method: 'DELETE'
});
if (response.ok) {
alert('Запрос удален!');
loadRequests();
} else {
const error = await response.text();
alert('Ошибка удаления: ' + error);
}
} catch (error) {
console.error('Ошибка:', error);
alert('Ошибка удаления!');
}
}
// Выполнение запроса
async function executeRequest() {
const select = document.getElementById('requestSelect');
const requestId = select.value;
if (!requestId) {
alert('Пожалуйста, выберите запрос');
return;
}
try {
const response = await fetch('/settings/requests', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id: parseInt(requestId) })
});
const data = await response.json();
displayResponse(data);
document.getElementById('responseSection').style.display = 'block';
// Прокручиваем к результату
document.getElementById('responseSection').scrollIntoView({ behavior: 'smooth' });
} catch (error) {
console.error('Ошибка:', error);
displayResponse({ error: error.message });
document.getElementById('responseSection').style.display = 'block';
}
}
// Отображение ответа
function displayResponse(data, container = document.getElementById('responseContainer'), level = 0) {
container.innerHTML = '';
function formatValue(value, indent = 0) {
const indentStr = ' '.repeat(indent);
if (value === null) {
const span = document.createElement('span');
span.className = 'json-null';
span.textContent = 'null';
return span;
} else if (typeof value === 'boolean') {
const span = document.createElement('span');
span.className = 'json-boolean';
span.textContent = value.toString();
return span;
} else if (typeof value === 'number') {
const span = document.createElement('span');
span.className = 'json-number';
span.textContent = value;
return span;
} else if (typeof value === 'string') {
const span = document.createElement('span');
span.className = 'json-string';
span.textContent = `"${value}"`;
return span;
} else if (Array.isArray(value)) {
if (value.length === 0) {
return document.createTextNode('[]');
}
const div = document.createElement('div');
div.appendChild(document.createTextNode('['));
value.forEach((item, index) => {
const itemDiv = document.createElement('div');
itemDiv.style.paddingLeft = '20px';
itemDiv.appendChild(formatValue(item, indent + 1));
if (index < value.length - 1) {
itemDiv.appendChild(document.createTextNode(','));
}
div.appendChild(itemDiv);
});
div.appendChild(document.createTextNode(']'));
return div;
} else if (typeof value === 'object') {
const entries = Object.entries(value);
if (entries.length === 0) {
return document.createTextNode('{}');
}
const div = document.createElement('div');
div.appendChild(document.createTextNode('{'));
entries.forEach(([key, val], index) => {
const itemDiv = document.createElement('div');
itemDiv.style.paddingLeft = '20px';
const keySpan = document.createElement('span');
keySpan.className = 'json-key';
keySpan.textContent = `"${key}": `;
itemDiv.appendChild(keySpan);
itemDiv.appendChild(formatValue(val, indent + 1));
if (index < entries.length - 1) {
itemDiv.appendChild(document.createTextNode(','));
}
div.appendChild(itemDiv);
});
div.appendChild(document.createTextNode('}'));
return div;
}
return document.createTextNode(String(value));
}
container.appendChild(formatValue(data));
window.lastResponse = data;
}
// Скачивание ответа
function downloadResponse() {
if (!window.lastResponse) return;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `request_${timestamp}.json`;
const jsonStr = JSON.stringify(window.lastResponse, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function setServerUrlInput(data) {
const serverUrlInput = document.getElementById('server_url');
if (serverUrlInput && data) {
serverUrlInput.value = data.url;
serverUrlInput.disabled = true;
}
}
// Инициализация
document.addEventListener('DOMContentLoaded', function () {
// Добавляем примеры параметров
addQueryParam();
addPayloadParam();
setServerUrlInput(pageData);
});
</script>
{% endblock %}