базовые настройки

This commit is contained in:
2025-12-19 01:01:35 +03:00
parent 7334aae92e
commit ad577b1f4d
13 changed files with 1670 additions and 819 deletions
+49 -33
View File
@@ -3,55 +3,71 @@
<head>
<meta charset="utf-8">
<title>{% block title %}Система{% endblock %}</title>
<title>{% block title %}Control Panel{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
padding-top: 60px;
}
.navbar-brand {
font-weight: 600;
}
pre {
background: #f8f9fa;
padding: 12px;
border-radius: 6px;
}
</style>
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="/static/css/base.css" rel="stylesheet">
{% block styles %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top shadow-sm">
<div class="container-fluid">
<a class="navbar-brand" href="/">⚙️ Control Panel</a>
<div class="dropdown">
<button class="btn btn-outline-light dropdown-toggle" type="button" data-bs-toggle="dropdown">
Навигация
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/">Главная</a></li>
<li><a class="dropdown-item" href="/medods">Medods</a></li>
<li><a class="dropdown-item" href="/vk">VK</a></li>
<li><a class="dropdown-item" href="/posts">Посты</a></li>
<!-- Логотип -->
<a class="navbar-brand fw-semibold" href="/">
<i class="bi bi-gear-fill me-1"></i> Control Panel
</a>
<!-- Кнопка для мобилок -->
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNavbar">
<span class="navbar-toggler-icon"></span>
</button>
<!-- Меню -->
<div class="collapse navbar-collapse" id="mainNavbar">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link {% if request.path == '/' %}active{% endif %}" href="/">
<i class="bi bi-speedometer2 me-1"></i> Главная
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path.startswith('/medods') %}active{% endif %}" href="/medods">
<i class="bi bi-diagram-3 me-1"></i> Medods
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path.startswith('/vk') %}active{% endif %}" href="/vk">
<i class="bi bi-chat-dots me-1"></i> VK
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path.startswith('/posts') %}active{% endif %}" href="/posts">
<i class="bi bi-file-earmark-text me-1"></i> Посты
</a>
</li>
</ul>
</div>
</div>
</nav>
<script>
const pageData = {{ data | tojson }};
</script>
<div class="container-fluid">
<main class="container-fluid pt-2">
{% block content %}{% endblock %}
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
+232 -678
View File
@@ -1,138 +1,113 @@
{% extends "base.html" %}
{% block title %}Medods{% endblock %}
{% block styles %}
<link rel="stylesheet" href="/static/css/medods.css">
{% endblock %}
{% block content %}
<div class="container-fluid">
<h3 class="mb-4">🔌 Medods API</h3>
<!-- Заголовок -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="mb-1"><i class="bi bi-plug me-2"></i>Medods API</h2>
<p class="text-muted mb-0">Управление подключением и запросами к Medods API</p>
</div>
<div class="badge bg-primary fs-6 px-3 py-2">
<i class="bi bi-activity me-1"></i>API Control
</div>
</div>
<!-- Настройка подключения -->
<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>
<!-- Настройка подключения -->
<div class="row mb-4">
<div class="col-lg-6 mb-4 mb-lg-0">
<div class="card h-100 url-status">
<div
class="card-header bg-primary bg-opacity-10 border-primary d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-server me-2"></i>Настройка сервера</h5>
<span id="urlCheck" class="status-indicator">
{% if data.url %}
<span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>Настроен</span>
{% else %}
<span class="badge bg-warning"><i class="bi bi-exclamation-triangle me-1"></i>Не настроен</span>
{% endif %}
</span>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label fw-semibold">URL адрес сервера</label>
<div class="input-group">
<span class="input-group-text bg-light"><i class="bi bi-link-45deg"></i></span>
<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>
value="{{ data.url or '' }}">
</div>
<div class="form-text small">Введите полный URL вашего сервера Medods API</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>
<button type="button" class="btn btn-success w-100" onclick="saveServerUrl()" id="saveServerUrlButton">
{% if data.url %}
<i class="bi bi-arrow-repeat me-2"></i>Обновить URL
{% else %}
<i class="bi bi-save me-2"></i>Сохранить URL
{% endif %}
</button>
</div>
</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 class="col-lg-6">
<div class="card h-100 api-status">
<div
class="card-header bg-success bg-opacity-10 border-success d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-key me-2"></i>API ключ</h5>
<span id="apiKeyCheck" class="status-indicator">
{% if data.apiKey %}
<span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>Загружен</span>
{% else %}
<span class="badge bg-warning"><i class="bi bi-exclamation-triangle me-1"></i>Не загружен</span>
{% endif %}
</span>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label fw-semibold">CSV файл с ключами</label>
<input type="file" class="form-control" id="api_key_file" accept=".csv">
<div class="form-text small">
<i class="bi bi-info-circle me-1"></i>
Файл должен содержать колонки: <code>identity;secretKey</code>
</div>
</div>
<button type="button" class="btn btn-primary w-100" onclick="uploadApiKey()" id="uploadApiKeyButton">
{% if data.apiKey %}
<i class="bi bi-arrow-repeat me-2"></i>Обновить ключ
{% else %}
<i class="bi bi-upload me-2"></i>Загрузить API ключ
{% endif %}
</button>
</div>
</div>
</div>
</div>
<!-- Список существующих запросов -->
<!-- Основной раздел: Запросы -->
<div class="card mb-4">
<div class="card-header bg-dark bg-opacity-10">
<div class="d-flex justify-content-between align-items-center">
<h4 class="mb-0"><i class="bi bi-send me-2"></i>Управление запросами</h4>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="newRequest()">
<i class="bi bi-plus-circle me-1"></i>Новый запрос
</button>
</div>
</div>
<div class="card-body p-0">
<div class="row g-0">
<!-- Левая колонка: Список запросов -->
<div class="col-md-4 border-end">
<div class="p-3 border-bottom bg-light">
<h5 class="mb-0"><i class="bi bi-list-ul me-2"></i>Сохраненные запросы</h5>
</div>
<div class="requests-list p-3">
<div id="requestsList">
<div class="text-center">
<!-- Список запросов будет загружен здесь -->
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
@@ -140,575 +115,154 @@
</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 class="col-md-8">
<div class="p-3 border-bottom bg-light">
<h5 class="mb-0">
<i class="bi bi-pencil-square me-2"></i>
<span id="editorTitle">Создание нового запроса</span>
</h5>
</div>
</form>
<div class="p-4">
<form id="requestForm">
<input type="hidden" id="requestId">
<!-- Окно с результатом -->
<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 class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label fw-semibold">Название запроса</label>
<input type="text" class="form-control form-control-lg" id="title"
placeholder="Например: Получить список пользователей" required>
</div>
<div class="col-md-3">
<label class="form-label fw-semibold">HTTP метод</label>
<select class="form-select form-select-lg" 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 fw-semibold">URL путь</label>
<input type="text" class="form-control form-control-lg" id="url_path"
placeholder="/users" required>
</div>
</div>
</div>
<!-- Query параметры -->
<div class="card mb-4">
<div class="card-header bg-body-secondary">
<div class="d-flex justify-content-between align-items-center">
<span class="fw-semibold">
<i class="bi bi-filter me-2"></i>Query параметры
</span>
<button type="button" class="btn btn-sm btn-outline-primary"
onclick="addQueryParam()">
<i class="bi bi-plus me-1"></i>Добавить
</button>
</div>
</div>
<div class="card-body">
<div id="queryParamsContainer" class="mb-2">
<!-- Параметры будут добавляться сюда -->
</div>
<div class="text-center">
<button type="button" class="btn btn-sm btn-outline-primary"
onclick="addQueryParam()">
<i class="bi bi-plus-circle me-1"></i>Добавить параметр Query
</button>
</div>
</div>
</div>
<!-- Payload параметры -->
<div class="card mb-4">
<div class="card-header bg-body-secondary">
<div class="d-flex justify-content-between align-items-center">
<span class="fw-semibold">
<i class="bi bi-code me-2"></i>Body параметры
</span>
<button type="button" class="btn btn-sm btn-outline-primary"
onclick="addPayloadParam()">
<i class="bi bi-plus me-1"></i>Добавить
</button>
</div>
</div>
<div class="card-body">
<div id="payloadParamsContainer" class="mb-2">
<!-- Параметры будут добавляться сюда -->
</div>
<div class="text-center">
<button type="button" class="btn btn-sm btn-outline-primary"
onclick="addPayloadParam()">
<i class="bi bi-plus-circle me-1"></i>Добавить параметр Body
</button>
</div>
</div>
</div>
<div class="d-flex justify-content-center align-items-center gap-2 text-muted d-none"
id="timestampDiv">
<div>
<small>Создано: </small><span class="small" id="createdAt"></span>
</div>
<div>
<small>Последнее обновление: </small><span class="small" id="updatedAt"></span>
</div>
</div>
<!-- Кнопки действий -->
<div class="d-flex gap-3 justify-content-end pt-3 border-top">
<button type="button" class="btn btn-outline-secondary" onclick="resetForm()">
<i class="bi bi-x-lg me-1"></i>Сбросить
</button>
<button type="button" class="btn btn-primary" onclick="saveRequest()"
id="saveRequestButton">
<i class="bi bi-save me-1"></i>Сохранить запрос
</button>
<button type="button" class="btn btn-warning" onclick="executeCurrentRequest()"
id="executeButton" disabled>
<i class="bi bi-play-fill me-1"></i>Запустить
</button>
</div>
</form>
</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;
}
<!-- Раздел с результатом -->
<div class="card fade-in" id="responseCard" style="display: none;">
<div class="card-header bg-info bg-opacity-10">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-arrow-return-right me-2"></i>Результат выполнения запроса</h5>
<div>
<button type="button" class="btn btn-sm btn-outline-success me-2" onclick="downloadResponse()">
<i class="bi bi-download me-1"></i>Скачать JSON
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleResponse()">
<i class="bi bi-chevron-up"></i>
</button>
</div>
</div>
</div>
<div class="card-body" id="responseBody">
<div id="responseContainer" class="response-container">
<!-- Ответ будет отображаться здесь -->
</div>
</div>
</div>
.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>
<!-- Всплывающие уведомления -->
<div id="alertContainer" style="position: fixed; top: 80px; right: 20px; z-index: 1050;"></div>
{% endblock %}
{% block scripts %}
<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);
});
const pageData = {{ data | tojson }};
</script>
<script src="/static/js/medods.js"></script>
{% endblock %}
+170 -15
View File
@@ -1,22 +1,177 @@
{% extends "base.html" %}
{% block title %}VK{% endblock %}
{% block styles %}
<link rel="stylesheet" href="/static/css/vk.css">
{% endblock %}
{% block content %}
<h3 class="mb-4">📣 VK</h3>
<div class="card">
<div class="card-body">
<div class="mb-3">
<label class="form-label">Access Token группы</label>
<input class="form-control">
</div>
<div class="mb-3">
<label class="form-label">ID сообщества</label>
<input class="form-control">
</div>
<button class="btn btn-primary">Сохранить</button>
<!-- Заголовок -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="mb-1"><i class="bi bi-megaphone vk-icon me-2"></i>VK Настройки</h2>
<p class="text-muted mb-0">Настройки для работы с VK API и сообществом</p>
</div>
<div class="badge bg-primary fs-6 px-3 py-2">
<i class="bi bi-bell me-1"></i>Социальная сеть
</div>
</div>
<!-- Настройки VK -->
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card setting-card fade-in">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-gear vk-icon me-2"></i>Настройки VK</h5>
<span id="vkStatus" class="status-badge">
{% if data.vk_settings %}
<span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>Настроено</span>
{% else %}
<span class="badge bg-warning"><i class="bi bi-exclamation-triangle me-1"></i>Не
настроено</span>
{% endif %}
</span>
</div>
<div class="card-body">
<form id="vkForm">
<!-- Access Token группы -->
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-key vk-icon me-1"></i>Access Token группы
</label>
<div class="input-group">
<span class="input-group-text bg-light"><i class="bi bi-shield-lock"></i></span>
<input type="password" class="form-control" id="access_token"
placeholder="Введите Access Token вашей группы"
value="{{ data.vk_settings.access_token if data.vk_settings else '' }}">
<button class="btn btn-outline-secondary" type="button"
onclick="togglePassword('access_token')">
<i class="bi bi-eye"></i>
</button>
</div>
<div class="form-text small">
<i class="bi bi-info-circle me-1"></i>
Access Token с правами: groups, wall
</div>
</div>
<!-- ID сообщества -->
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-people vk-icon me-1"></i>ID сообщества
</label>
<div class="input-group">
<span class="input-group-text bg-light"><i class="bi bi-hash"></i></span>
<input type="number" class="form-control" id="group_id" placeholder="Например: 123456789"
step="1" min="0" value="{{ data.vk_settings.group_id if data.vk_settings else '' }}">
</div>
<div class="form-text small">
<i class="bi bi-info-circle me-1"></i>
Числовой ID вашего сообщества VK (без знака минус)
</div>
</div>
<!-- ID Базового фото -->
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-image vk-icon me-1"></i>ID Базового фото
</label>
<div class="input-group">
<span class="input-group-text bg-light"><i class="bi bi-image-alt"></i></span>
<input type="number" class="form-control" id="base_photo_url"
placeholder="Например: 12345689123" step="1" min="0"
value="{{ data.vk_settings.base_photo_url if data.vk_settings else '' }}">
</div>
<div class="form-text small">
<i class="bi bi-info-circle me-1"></i>
ID фото в формате: <code>photo_id</code>
</div>
</div>
<!-- Информационная панель -->
<div class="alert alert-info">
<div class="d-flex">
<div class="me-3">
<i class="bi bi-lightbulb fs-4"></i>
</div>
<div>
<h6 class="alert-heading">Как получить данные:</h6>
<ul class="mb-0 small">
<li><strong>Access Token:</strong> Создайте Standalone-приложение в <a
href="https://vk.com/apps?act=manage" target="_blank">управлении
приложениями VK</a></li>
<li><strong>ID сообщества:</strong> Число в адресе сообщества после
<code>vk.com/public</code> или <code>vk.com/club</code>
</li>
<li><strong>ID Базового фото:</strong> Загрузите фото в альбом сообщества и
скопируйте ID из адреса фото</li>
</ul>
</div>
</div>
</div>
<!-- Кнопки действий -->
<div class="d-flex gap-3 pt-3 border-top">
<button type="button" class="btn btn-outline-secondary" onclick="resetForm()">
<i class="bi bi-x-lg me-1"></i>Сбросить
</button>
<button type="button" class="btn btn-primary" onclick="saveVkSettings()" id="saveButton">
<i class="bi bi-save me-1"></i>Сохранить настройки
</button>
</div>
</form>
</div>
</div>
<!-- Информация о текущих настройках -->
{% if data.vk_settings %}
<div class="card mt-4 fade-in">
<div class="card-header bg-success bg-opacity-10 text-success">
<h6 class="mb-0"><i class="bi bi-check-circle me-2"></i>Текущие настройки</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="card bg-light">
<div class="card-body">
<h6 class="card-title"><i class="bi bi-key me-2"></i>Access Token</h6>
<p class="card-text small text-truncate" id="tokenPreview">
{{ data.vk_settings.access_token[:15] }}...
</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-light">
<div class="card-body">
<h6 class="card-title"><i class="bi bi-people me-2"></i>ID сообщества</h6>
<p class="card-text">-{{ data.vk_settings.group_id }}</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-light">
<div class="card-body">
<h6 class="card-title"><i class="bi bi-image me-2"></i>ID Базового фото</h6>
<p class="card-text">photo-{{ data.vk_settings.group_id }}_{{
data.vk_settings.base_photo_url }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Контейнер для уведомлений -->
<div id="alertContainer" class="alert-fixed"></div>
{% endblock %}
{% block scripts %}
<script>
const pageData = {{ data | tojson }};
</script>
<script src="/static/js/vk.js"></script>
{% endblock %}