Files
Zilant2025/templates/edit_post.html
T
gitadmin a50fd3823a
ci/woodpecker/push/woodpecker Pipeline was successful
Ред пост 9
2025-10-12 21:02:26 +03:00

565 lines
29 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% if post_id %}Редактирование поста{% else %}Добавление поста{% endif %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.form-container {
max-width: 800px;
margin: 0 auto;
}
.preview-image {
max-width: 200px;
max-height: 200px;
object-fit: contain;
}
.required:after {
content: " *";
color: red;
}
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 1050;
}
/* Скрытие крутилок у числовых полей */
input[type="text"][pattern="[0-9]*"]::-webkit-outer-spin-button,
input[type="text"][pattern="[0-9]*"]::-webkit-inner-spin-button {
-webkit-appearance: none;
appearance: none;
margin: 0;
}
input[type="text"][pattern="[0-9]*"] {
-moz-appearance: textfield;
appearance: textfield;
}
/* Принудительное отображение 24-часового формата */
input[type="datetime-local"] {
font-family: monospace;
}
/* Жирные заголовки и выравнивание */
.form-label {
font-weight: bold;
display: flex;
align-items: center;
margin-bottom: 0;
height: 38px; /* Высота стандартного поля ввода Bootstrap */
}
/* Выравнивание полей с заголовками */
.form-control, .form-select {
height: 38px;
}
textarea.form-control {
height: auto;
}
/* Убираем отступы слева у полей для выравнивания */
.form-control, .form-select {
margin-left: 0 !important;
}
</style>
</head>
<body>
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>{% if post_id %}Редактирование поста{% else %}Добавление поста{% endif %}</h2>
<a href="#" id="back-button" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> Назад
</a>
</div>
<div class="card form-container">
<div class="card-body">
<form id="post-form">
<input type="hidden" id="post-id" value="{{ post_id if post_id else '' }}">
<!-- Заголовок и поле Название в одну строку -->
<div class="row mb-3">
<div class="col-md-2">
<label for="shortname" class="form-label">Название:</label>
</div>
<div class="col-md-10">
<input type="text" class="form-control" id="shortname" maxlength="100">
</div>
</div>
<!-- Секция с площадкой и VK ID слева, изображением справа -->
<div class="row mb-3">
<div class="col-md-8">
<!-- Площадка -->
<div class="row mb-2">
<div class="col-md-2">
<label for="auto_unit" class="form-label">Площадка:</label>
</div>
<div class="col-md-10">
<select class="form-control" id="auto_unit">
<option value="">Выберите площадку</option>
</select>
</div>
</div>
<!-- VK ID -->
<div class="row mb-2">
<div class="col-md-2">
<label for="vk_post_id" class="form-label">VK ID:</label>
</div>
<div class="col-md-4">
<input type="text" class="form-control" id="vk_post_id" maxlength="8" required>
</div>
</div>
<!-- Переключатели -->
<div class="row mb-2">
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="is_event">
<label class="form-check-label" for="is_event">Событие</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="is_poll">
<label class="form-check-label" for="is_poll">Опрос</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="marked_for_publication">
<label class="form-check-label" for="marked_for_publication">К публикации</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="published_in_tg">
<label class="form-check-label" for="published_in_tg">Опубликовано в TG</label>
</div>
</div>
</div>
</div>
<div class="col-md-4 text-end">
<div id="image-preview-container" style="display: none;">
<img id="image-preview" class="preview-image img-thumbnail" style="max-width: 200px; max-height: 200px; object-fit: contain;">
</div>
</div>
</div>
<!-- Текстовое поле поста -->
<div class="mb-3">
<textarea class="form-control" id="text" rows="4" required placeholder="Введите текст поста"></textarea>
</div>
<!-- URL изображения -->
<div class="row mb-3">
<div class="col-md-2">
<label for="image_url" class="form-label">Картинка:</label>
</div>
<div class="col-md-10">
<input type="url" class="form-control" id="image_url">
</div>
</div>
<!-- URL поста ВК с датой -->
<div class="row mb-3">
<div class="col-md-2">
<label for="vk_post_url" class="form-label">Пост в ВК:</label>
</div>
<div class="col-md-4">
<input type="url" class="form-control" id="vk_post_url">
</div>
<div class="col-md-2">
<label for="published_at" class="form-label">датирован:</label>
</div>
<div class="col-md-4">
<input type="datetime-local" class="form-control" id="published_at" step="60">
</div>
</div>
<!-- Дата публикации в TG -->
<div class="row mb-3">
<div class="col-md-4">
<label for="tg_publication_date" class="form-label">Опубликовано в TG:</label>
</div>
<div class="col-md-8">
<input type="datetime-local" class="form-control" id="tg_publication_date" step="60">
</div>
</div>
<!-- ID TG в одну строку -->
<div class="row mb-3">
<div class="col-md-2">
<label for="tg_message_id" class="form-label">ID TG:</label>
</div>
<div class="col-md-2">
<input type="text" class="form-control" id="tg_message_id" pattern="[0-9]*" inputmode="numeric">
</div>
<div class="col-md-2">
<label for="tg_poll_id" class="form-label">ID опроса TG:</label>
</div>
<div class="col-md-2">
<input type="text" class="form-control" id="tg_poll_id" pattern="[0-9]*" inputmode="numeric">
</div>
<div class="col-md-2">
<label for="tg_markpost_id" class="form-label">ID отметок TG:</label>
</div>
<div class="col-md-2">
<input type="text" class="form-control" id="tg_markpost_id" pattern="[0-9]*" inputmode="numeric">
</div>
</div>
<!-- Блок параметров опроса -->
<div id="poll-fields" style="display: none;">
<div class="border-top mt-4 pt-3">
<h5>Параметры опроса</h5>
<div class="mb-3">
<label for="poll_question" class="form-label">Вопрос опроса</label>
<input type="text" class="form-control" id="poll_question">
</div>
<div class="mb-3">
<label for="poll_options" class="form-label">Варианты ответов (JSON)</label>
<textarea class="form-control" id="poll_options" rows="2"></textarea>
<div class="form-text">Формат: ["Вариант 1", "Вариант 2", ...]</div>
</div>
<div class="mb-3 form-check form-switch">
<input class="form-check-input" type="checkbox" id="poll_multiple">
<label class="form-check-label" for="poll_multiple">Множественный выбор</label>
</div>
<div class="mb-3">
<label for="poll_end_date" class="form-label">Дата окончания опроса</label>
<input type="datetime-local" class="form-control" id="poll_end_date" step="60">
</div>
<div class="mb-3">
<label for="tg_poll_results" class="form-label">Результаты опроса</label>
<textarea class="form-control" id="tg_poll_results" rows="4"></textarea>
</div>
</div>
</div>
<div class="d-flex justify-content-end mt-4">
<button type="button" class="btn btn-secondary me-2" id="cancel-btn">Отмена</button>
<button type="submit" class="btn btn-primary">
{% if post_id %}Обновить{% else %}Создать{% endif %}
</button>
</div>
</form>
</div>
</div>
<!-- Контейнер для уведомлений -->
<div class="toast-container">
<div id="success-toast" class="toast align-items-center text-white bg-success border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
<i class="fas fa-check-circle me-2"></i> Пост успешно сохранен!
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const PREFIX = "{{ g.prefix }}";
console.log("Editor Prefix is:", PREFIX); // Должно выводить "/zilant"
document.addEventListener('DOMContentLoaded', () => {
const postId = document.getElementById('post-id').value;
const form = document.getElementById('post-form');
const isPollCheckbox = document.getElementById('is_poll');
const pollFields = document.getElementById('poll-fields');
const imageUrlInput = document.getElementById('image_url');
const imagePreviewContainer = document.getElementById('image-preview-container');
const imagePreview = document.getElementById('image-preview');
const successToast = new bootstrap.Toast(document.getElementById('success-toast'));
// Обработчики кнопок отмены и назад
const cancelHandler = () => {
if (postId) {
window.location.href = `${PREFIX}/?selected_post=${postId}&tab=posts`;
} else {
window.location.href = `${PREFIX}/?tab=posts`;
}
};
document.getElementById('cancel-btn').addEventListener('click', cancelHandler);
document.getElementById('back-button').addEventListener('click', cancelHandler);
// Показать/скрыть поля опроса
isPollCheckbox.addEventListener('change', () => {
pollFields.style.display = isPollCheckbox.checked ? 'block' : 'none';
});
// Предпросмотр изображения
imageUrlInput.addEventListener('change', () => {
const url = imageUrlInput.value.trim();
if (url) {
imagePreview.src = `${PREFIX}/image_proxy?url=${encodeURIComponent(url)}`;
imagePreviewContainer.style.display = 'block';
} else {
imagePreviewContainer.style.display = 'none';
}
});
// Валидация поля названия
const shortnameInput = document.getElementById('shortname');
shortnameInput.addEventListener('input', () => {
const value = shortnameInput.value;
if (value.length > 100) {
alert('Название не может превышать 100 символов');
shortnameInput.value = value.substring(0, 100);
}
});
// Валидация поля VK ID
const vkPostIdInput = document.getElementById('vk_post_id');
vkPostIdInput.addEventListener('input', () => {
const value = vkPostIdInput.value;
if (value.length > 8) {
alert('VK ID не может превышать 8 символов');
vkPostIdInput.value = value.substring(0, 8);
}
});
// Загрузка списка площадок
loadVenues();
// Загрузка данных поста (если редактирование)
if (postId) {
loadPostData(postId);
}
// Принудительная установка 24-часового формата для полей даты-времени
const datetimeInputs = document.querySelectorAll('input[type="datetime-local"]');
datetimeInputs.forEach(input => {
// Устанавливаем атрибут step для округления до минут
input.step = '60';
// Добавляем обработчик для принудительного отображения 24-часового формата
input.addEventListener('change', function() {
if (this.value) {
const date = new Date(this.value);
// Форматируем в 24-часовом формате
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
this.value = `${year}-${month}-${day}T${hours}:${minutes}`;
}
});
});
// Обработка отправки формы
form.addEventListener('submit', (e) => {
e.preventDefault();
savePost();
});
});
// Загрузка списка площадок
async function loadVenues() {
try {
const response = await fetch(`${PREFIX}/api/venues`);
if (!response.ok) {
throw new Error('Ошибка загрузки списка площадок');
}
const venues = await response.json();
const select = document.getElementById('auto_unit');
// Очищаем список, оставляя только первую опцию
select.innerHTML = '<option value="">Выберите площадку</option>';
// Добавляем площадки
venues.forEach(venue => {
const option = document.createElement('option');
option.value = venue.name;
option.textContent = venue.name;
select.appendChild(option);
});
} catch (error) {
console.error('Ошибка загрузки площадок:', error);
}
}
// Загрузка данных поста
async function loadPostData(postId) {
try {
const response = await fetch(`${PREFIX}/api/posts/${postId}`);
if (!response.ok) {
throw new Error('Пост не найден');
}
const post = await response.json();
// Заполняем форму
document.getElementById('vk_post_id').value = post.vk_post_id || '';
document.getElementById('shortname').value = post.shortname || '';
document.getElementById('auto_unit').value = post.auto_unit || '';
document.getElementById('text').value = post.text || '';
document.getElementById('image_url').value = post.image_url || '';
document.getElementById('vk_post_url').value = post.vk_post_url || '';
// Форматирование дат для input[type=datetime-local]
// Конвертация UTC в локальное время с округлением до минут в 24-часовом формате
const formatForInput = (dateStr) => {
if (!dateStr) return '';
const date = new Date(dateStr);
// Округляем до минут (убираем секунды и миллисекунды)
date.setSeconds(0, 0);
// Возвращаем в локальном времени в формате YYYY-MM-DDTHH:MM (24-часовой формат)
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
};
document.getElementById('published_at').value = formatForInput(post.published_at);
document.getElementById('is_event').checked = post.is_event; // Новое поле
document.getElementById('is_poll').checked = post.is_poll;
document.getElementById('poll_question').value = post.poll_question || '';
document.getElementById('poll_options').value = post.poll_options || '';
document.getElementById('poll_multiple').checked = post.poll_multiple;
document.getElementById('poll_end_date').value = formatForInput(post.poll_end_date);
document.getElementById('marked_for_publication').checked = post.marked_for_publication;
document.getElementById('published_in_tg').checked = post.published_in_tg;
document.getElementById('tg_publication_date').value = formatForInput(post.tg_publication_date);
document.getElementById('tg_message_id').value = post.tg_message_id || '';
document.getElementById('tg_poll_id').value = post.tg_poll_id || '';
document.getElementById('tg_markpost_id').value = post.tg_markpost_id || '';
document.getElementById('tg_poll_results').value = post.tg_poll_results || '';
// Показываем поля опроса, если нужно
if (post.is_poll) {
document.getElementById('poll-fields').style.display = 'block';
}
// Показываем изображение, если есть
if (post.image_url) {
document.getElementById('image-preview').src =
`${PREFIX}/image_proxy?url=${encodeURIComponent(post.image_url)}`;
document.getElementById('image-preview-container').style.display = 'block';
}
} catch (error) {
alert(`Ошибка загрузки данных: ${error.message}`);
console.error(error);
}
}
// Функция конвертации локального времени в UTC для отправки на сервер
const convertLocalToUTC = (localString) => {
if (!localString) return null;
const date = new Date(localString);
// Округляем до минут
date.setSeconds(0, 0);
// Возвращаем в UTC формате для сервера (24-часовой формат)
return date.toISOString().slice(0, 19).replace('T', ' ');
};
// Сохранение поста
async function savePost() {
const postId = document.getElementById('post-id').value;
const form = document.getElementById('post-form');
const submitBtn = form.querySelector('button[type="submit"]');
const originalBtnText = submitBtn.innerHTML;
const isNewPost = !postId;
// Показываем индикатор загрузки
submitBtn.innerHTML = `
<span class="spinner-border spinner-border-sm" role="status"></span>
Сохранение...
`;
submitBtn.disabled = true;
try {
// Собираем данные формы
const formData = {
vk_post_id: document.getElementById('vk_post_id').value,
shortname: document.getElementById('shortname').value,
auto_unit: document.getElementById('auto_unit').value,
text: document.getElementById('text').value,
image_url: document.getElementById('image_url').value,
vk_post_url: document.getElementById('vk_post_url').value,
published_at: convertLocalToUTC(document.getElementById('published_at').value),
is_event: document.getElementById('is_event').checked, // Новое поле
is_poll: document.getElementById('is_poll').checked,
poll_question: document.getElementById('poll_question').value,
poll_options: document.getElementById('poll_options').value,
poll_multiple: document.getElementById('poll_multiple').checked,
poll_end_date: convertLocalToUTC(document.getElementById('poll_end_date').value),
marked_for_publication: document.getElementById('marked_for_publication').checked,
published_in_tg: document.getElementById('published_in_tg').checked,
tg_publication_date: convertLocalToUTC(document.getElementById('tg_publication_date').value),
tg_message_id: document.getElementById('tg_message_id').value,
tg_poll_id: document.getElementById('tg_poll_id').value,
tg_markpost_id: document.getElementById('tg_markpost_id').value,
tg_poll_results: document.getElementById('tg_poll_results').value
};
// Валидация
if (!formData.vk_post_id) {
throw new Error('VK ID поста обязателен для заполнения');
}
let response;
let result;
if (postId) {
// Обновление существующего поста
response = await fetch(`${PREFIX}/api/posts/${postId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
result = await response.json();
} else {
// Создание нового поста
response = await fetch(`${PREFIX}/api/posts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
result = await response.json();
}
if (!response.ok) {
throw new Error(result.error || 'Ошибка сохранения');
}
// Показываем уведомление об успехе
const successToast = new bootstrap.Toast(document.getElementById('success-toast'));
successToast.show();
// Ждем 1.5 секунды перед перенаправлением
setTimeout(() => {
// Перенаправляем на главную страницу с выбранным постом
window.location.href = `${PREFIX}/?selected_post=${postId || result.post_id}&tab=posts`;
}, 1500);
} catch (error) {
alert(`Ошибка: ${error.message}`);
console.error(error);
} finally {
// Восстанавливаем кнопку
submitBtn.innerHTML = originalBtnText;
submitBtn.disabled = false;
}
}
</script>
</body>
</html>