This commit is contained in:
2025-12-21 15:12:09 +03:00
parent e149d7380d
commit 865224ba5a
10 changed files with 145 additions and 156 deletions
BIN
View File
Binary file not shown.
+17 -14
View File
@@ -332,6 +332,7 @@ def api_vk():
@app.route("/api/posts", methods=["POST", "GET"]) @app.route("/api/posts", methods=["POST", "GET"])
def api_posts(): def api_posts():
response = {"status": "ok"}
match request.method: match request.method:
case "POST": case "POST":
requestData = request.json requestData = request.json
@@ -362,32 +363,34 @@ def api_posts():
logger.info("Обновление расписания публикации") logger.info("Обновление расписания публикации")
try: try:
scheduler = PostScheduler.query.first() scheduler = PostScheduler.query.first()
startTime = schedulerData.get("startTime", None) hour = schedulerData.get("hour", None)
endTime = schedulerData.get("endTime", None) minute = schedulerData.get("minute", None)
interval_minutes = schedulerData.get("interval_minutes", None)
enabled = schedulerData.get("enabled", None) enabled = schedulerData.get("enabled", None)
if scheduler: if scheduler:
if startTime: if hour is not None:
scheduler.start_hour = int(startTime) if scheduler.hour != hour:
if endTime: scheduler.hour = hour
scheduler.end_hour = int(endTime) if minute is not None:
if interval_minutes: if scheduler.minute != minute:
scheduler.interval_minutes = int(interval_minutes) scheduler.minute = minute
if enabled is not None: if enabled is not None:
scheduler.enabled = enabled if scheduler.enabled != enabled:
scheduler.enabled = enabled
else: else:
db.session.merge( db.session.merge(
PostScheduler( PostScheduler(
start_hour=int(startTime), hour=int(hour),
end_hour=int(endTime), minute=int(minute),
interval_minutes=int(interval_minutes),
enabled=enabled, enabled=enabled,
) )
) )
db.session.commit() db.session.commit()
enable_publish_job()
scheduleInfo = get_scheduler_status()
response["next_run_time"] = scheduleInfo.get("next_run_time")
except Exception as e: except Exception as e:
logger.error(f"Ошибка при обновлении расписания публикации: {e}") logger.error(f"Ошибка при обновлении расписания публикации: {e}")
return jsonify({"status": "ok"}) return jsonify(response)
case "GET": case "GET":
queryParams = request.args.to_dict() queryParams = request.args.to_dict()
+4 -6
View File
@@ -123,9 +123,8 @@ class PostScheduler(db.Model):
__tablename__ = "post_scheduler" __tablename__ = "post_scheduler"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
start_hour = db.Column(db.Integer) hour = db.Column(db.Integer)
end_hour = db.Column(db.Integer) minute = db.Column(db.Integer)
interval_minutes = db.Column(db.Integer)
enabled = db.Column(db.Boolean) enabled = db.Column(db.Boolean)
created_at = db.Column(db.DateTime, default=datetime.now) created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now) updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
@@ -133,9 +132,8 @@ class PostScheduler(db.Model):
def toDict(self): def toDict(self):
return { return {
"id": self.id, "id": self.id,
"start_hour": self.start_hour, "hour": self.hour,
"end_hour": self.end_hour, "minute": self.minute,
"interval_minutes": self.interval_minutes,
"enabled": self.enabled, "enabled": self.enabled,
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M:%S"), "created_at": self.created_at.strftime("%Y-%m-%d %H:%M:%S"),
"updated_at": self.updated_at.strftime("%Y-%m-%d %H:%M:%S"), "updated_at": self.updated_at.strftime("%Y-%m-%d %H:%M:%S"),
+2 -10
View File
@@ -75,19 +75,11 @@ def enable_publish_job():
disable_publish_job() disable_publish_job()
return return
start_hour = scheduleData.start_hour
end_hour = scheduleData.end_hour
interval_minutes = scheduleData.interval_minutes
trigger = CronTrigger( trigger = CronTrigger(
hour="12", hour=f"{scheduleData.hour}",
minute="0", minute=f"{scheduleData.minute}",
day="*", day="*",
) )
# trigger = CronTrigger(
# hour=f"{start_hour}-{end_hour - 1}",
# minute=f"*/{interval_minutes}",
# )
scheduler.add_job( scheduler.add_job(
vk_publish_job, vk_publish_job,
+1 -1
View File
@@ -17,7 +17,7 @@
/* Таблица сотрудников */ /* Таблица сотрудников */
.table-responsive { .table-responsive {
max-height: 800px; max-height: 575px;
overflow-y: auto; overflow-y: auto;
} }
+18 -30
View File
@@ -20,9 +20,8 @@ function saveOriginalSettings() {
selected_users: getSelectedUsers(), selected_users: getSelectedUsers(),
static_text: document.getElementById('static_text').value, static_text: document.getElementById('static_text').value,
full_name: document.getElementById('full_name').checked, full_name: document.getElementById('full_name').checked,
start_hour: parseInt(document.getElementById('start_hour').value), hour: parseInt(document.getElementById('hour').value),
end_hour: parseInt(document.getElementById('end_hour').value), minute: parseInt(document.getElementById('minute').value),
interval_minutes: parseInt(document.getElementById('interval_minutes').value),
enabled: document.getElementById('scheduler_enabled').checked enabled: document.getElementById('scheduler_enabled').checked
}; };
} }
@@ -38,7 +37,7 @@ function setupChangeListeners() {
}); });
// Поля формы // Поля формы
const formFields = ['static_text', 'start_hour', 'end_hour', 'interval_minutes']; const formFields = ['static_text', 'hour', 'minute'];
formFields.forEach(field => { formFields.forEach(field => {
const element = document.getElementById(field); const element = document.getElementById(field);
if (element) { if (element) {
@@ -58,9 +57,8 @@ function checkForChanges() {
selected_users: getSelectedUsers(), selected_users: getSelectedUsers(),
static_text: document.getElementById('static_text').value, static_text: document.getElementById('static_text').value,
full_name: document.getElementById('full_name').checked, full_name: document.getElementById('full_name').checked,
start_hour: parseInt(document.getElementById('start_hour').value), hour: parseInt(document.getElementById('hour').value),
end_hour: parseInt(document.getElementById('end_hour').value), minute: parseInt(document.getElementById('minute').value),
interval_minutes: parseInt(document.getElementById('interval_minutes').value),
enabled: document.getElementById('scheduler_enabled').checked enabled: document.getElementById('scheduler_enabled').checked
}; };
@@ -194,9 +192,8 @@ async function saveSettings() {
const selectedUsers = getSelectedUsers(); const selectedUsers = getSelectedUsers();
const staticText = document.getElementById('static_text').value.trim(); const staticText = document.getElementById('static_text').value.trim();
const fullName = document.getElementById('full_name').checked; const fullName = document.getElementById('full_name').checked;
const startHour = parseInt(document.getElementById('start_hour').value); const hour = parseInt(document.getElementById('hour').value);
const endHour = parseInt(document.getElementById('end_hour').value); const minute = parseInt(document.getElementById('minute').value);
const intervalMinutes = parseInt(document.getElementById('interval_minutes').value);
const schedulerEnabled = document.getElementById('scheduler_enabled').checked; const schedulerEnabled = document.getElementById('scheduler_enabled').checked;
// Валидация // Валидация
@@ -210,23 +207,13 @@ async function saveSettings() {
return; return;
} }
if (startHour < 0 || startHour > 23) { if (hour < 0 || hour > 23) {
showAlert('warning', 'Время начала должно быть от 0 до 23 часов'); showAlert('warning', 'Час должен быть от 0 до 23 часов');
return; return;
} }
if (endHour < 0 || endHour > 23) { if (minute < 0 || minute > 59) {
showAlert('warning', 'Время окончания должно быть от 0 до 23 часов'); showAlert('warning', 'Минута должна быть от 0 до 59 минут');
return;
}
if (startHour >= endHour) {
showAlert('warning', 'Время начала должно быть раньше времени окончания');
return;
}
if (intervalMinutes < 1 || intervalMinutes > 1440) {
showAlert('warning', 'Интервал должен быть от 1 до 1440 минут');
return; return;
} }
@@ -248,14 +235,12 @@ async function saveSettings() {
} }
// Только измененные данные для schedulerData // Только измененные данные для schedulerData
if (startHour !== originalSettings.start_hour || if (hour !== originalSettings.hour ||
endHour !== originalSettings.end_hour || minute !== originalSettings.minute ||
intervalMinutes !== originalSettings.interval_minutes ||
schedulerEnabled !== originalSettings.enabled) { schedulerEnabled !== originalSettings.enabled) {
postData.schedulerData = { postData.schedulerData = {
startTime: startHour.toString(), hour: hour,
endTime: endHour.toString(), minute: minute,
interval_minutes: intervalMinutes.toString(),
enabled: schedulerEnabled enabled: schedulerEnabled
}; };
} }
@@ -288,6 +273,9 @@ async function saveSettings() {
if (Object.keys(postData.schedulerData).length > 0) { if (Object.keys(postData.schedulerData).length > 0) {
setTimeout(updateSchedulerStatus, 1000); setTimeout(updateSchedulerStatus, 1000);
} }
if (data.next_run_time) {
document.getElementById('nextRunTime').textContent = data.next_run_time;
}
} else { } else {
const error = data.message || 'Ошибка сохранения'; const error = data.message || 'Ошибка сохранения';
showAlert('danger', error); showAlert('danger', error);
+38
View File
@@ -152,3 +152,41 @@ document.getElementById('base_photo_url').addEventListener('focus', function ()
showAlert('info', 'Формат ID фото: photo_id (например: 7236456789)'); showAlert('info', 'Формат ID фото: photo_id (например: 7236456789)');
} }
}); });
function handleLink() {
const link = prompt('Введите ссылку на фотографию:');
if (!link) return;
try {
const url = new URL(link);
const zParam = url.searchParams.get('z');
if (!zParam || !zParam.startsWith('photo')) {
alert('Некорректная ссылка на фото ВК');
return;
}
const decoded = decodeURIComponent(zParam);
const match = decoded.match(/photo(-?\d+)_(\d+)/);
if (!match) {
alert('Не удалось разобрать ссылку');
return;
}
const groupId = Math.abs(Number(match[1]));
const photoId = Number(match[2]);
const isConfirmed = window.confirm(
`Применить новые данные?\nГруппа: ${groupId}\nID фото: ${photoId}`
);
if (isConfirmed) {
document.getElementById('group_id').value = groupId;
document.getElementById('base_photo_url').value = photoId;
}
} catch (e) {
alert('Ошибка обработки ссылки');
}
}
+8 -8
View File
@@ -228,12 +228,12 @@
</div> </div>
</div> </div>
<div class="col-6"> <div class="col-6">
<div class="small text-muted">Следующий запуск</div> <div class="small text-muted">Отображение имен</div>
<div class="fw-semibold"> <div class="fw-semibold">
{% if exitData.vkPost.scheduler.next_run_time %} {% if exitData.vkPost.full_name %}
{{ exitData.vkPost.scheduler.next_run_time }} Полные имена
{% else %} {% else %}
Не запланирован Короткие имена
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -365,12 +365,12 @@
<div class="col-6"> <div class="col-6">
<div class="card bg-light"> <div class="card bg-light">
<div class="card-body py-2"> <div class="card-body py-2">
<div class="small text-muted">Отображение имен</div> <div class="small text-muted">Следующая публикация</div>
<div class="fw-semibold"> <div class="fw-semibold">
{% if exitData.vkPost.full_name %} {% if exitData.vkPost.scheduler.next_run_time %}
Полные имена {{ exitData.vkPost.scheduler.next_run_time }}
{% else %} {% else %}
Короткие имена Не запланирован
{% endif %} {% endif %}
</div> </div>
</div> </div>
+39 -81
View File
@@ -12,13 +12,16 @@
<h2 class="mb-1"><i class="bi bi-megaphone-fill text-primary me-2"></i>Управление постами</h2> <h2 class="mb-1"><i class="bi bi-megaphone-fill text-primary me-2"></i>Управление постами</h2>
<p class="text-muted mb-0">Создание и планирование публикаций в VK</p> <p class="text-muted mb-0">Создание и планирование публикаций в VK</p>
</div> </div>
<button type="button" class="btn btn-success btn-lg px-5" onclick="saveSettings()">
<i class="bi bi-save me-2"></i>Сохранить все настройки
</button>
<div class="badge bg-primary fs-6 px-3 py-2"> <div class="badge bg-primary fs-6 px-3 py-2">
<i class="bi bi-calendar-week me-1"></i>Публикации <i class="bi bi-calendar-week me-1"></i>Публикации
</div> </div>
</div> </div>
<!-- Основной контент --> <!-- Основной контент -->
<div class="row g-4"> <div class="row g-4 mb-3">
<!-- Левая колонка: Сотрудники --> <!-- Левая колонка: Сотрудники -->
<div class="col-lg-7"> <div class="col-lg-7">
<div class="card h-100"> <div class="card h-100">
@@ -164,95 +167,50 @@
</span> </span>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row g-3 align-items-start">
<div class="alert alert-danger"> <!-- Время запуска -->
<div class="d-flex"> <div class="col-12 col-md-4">
<div class="me-3"> <label class="form-label fw-semibold mb-1">Время запуска</label>
<i class="bi bi-exclamation-triangle fs-5"></i> <div class="input-group input-group-sm">
</div> <span class="input-group-text"><i class="bi bi-alarm"></i></span>
<div> <input type="number" class="form-control" id="hour" min="0" max="23"
<h6 class="alert-heading mb-1">Временная настройка</h6> value="{{ data.schedulerSettings.hour if data.schedulerSettings else 12 }}">
<p class="mb-0">Посты будут опубликованы в 12:00 ежедневно.<br>Настройки из расписания будут <span class="input-group-text">:</span>
проигнорированы!</p> <input type="number" class="form-control" id="minute" min="0" max="59"
value="{{ data.schedulerSettings.minute if data.schedulerSettings else 0 }}">
</div> </div>
</div> </div>
</div>
<!-- Время работы --> <!-- Статус планировщика -->
<div class="row g-3 mb-3"> <div class="col-12 col-md-8">
<div class="col-md-6"> <label class="form-label fw-semibold mb-1">Статус планировщика</label>
<label class="form-label fw-semibold">Начало публикации</label> <div class="form-check form-switch d-flex align-items-center gap-2 p-0">
<div class="input-group"> <input class="form-check-input m-0" type="checkbox" role="switch" id="scheduler_enabled" {%
<span class="input-group-text"><i class="bi bi-sun"></i></span> if data.schedulerSettings and data.schedulerSettings.enabled %}checked{% endif %}>
<input type="number" class="form-control" id="start_hour" min="0" max="23" <label class="form-check-label mb-0" for="scheduler_enabled">
value="{{ data.schedulerSettings.start_hour if data.schedulerSettings else 9 }}"> Автоматическая публикация
<span class="input-group-text">:00</span> </label>
</div> </div>
</div> </div>
<div class="col-md-6">
<label class="form-label fw-semibold">Конец публикации</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-moon"></i></span>
<input type="number" class="form-control" id="end_hour" min="0" max="23"
value="{{ data.schedulerSettings.end_hour if data.schedulerSettings else 21 }}">
<span class="input-group-text">:00</span>
</div>
</div>
</div>
<!-- Интервал -->
<div class="mb-3">
<label class="form-label fw-semibold">Интервал публикации (минут)</label>
<input type="number" class="form-control" id="interval_minutes" min="1" max="1440"
value="{{ data.schedulerSettings.interval_minutes if data.schedulerSettings else 60 }}">
<div class="form-text">
Через сколько минут публиковать следующий пост
</div>
</div>
<!-- Включить/выключить -->
<div class="mb-4">
<label class="form-label fw-semibold">Статус планировщика</label>
<div class="form-check form-switch d-flex justify-content-left gap-2 p-0">
<input class="form-check-input m-0" type="checkbox" role="switch" id="scheduler_enabled" {% if
data.schedulerSettings and data.schedulerSettings.enabled %}checked{% endif %}>
<label class="form-check-label mb-0" for="scheduler_enabled">
Автоматическая публикация по расписанию
</label>
</div>
</div>
<!-- Информация о следующем запуске -->
{% if data.schedulerStatus.next_run_time %}
<div class="alert alert-info">
<div class="d-flex">
<div class="me-3">
<i class="bi bi-info-circle fs-5"></i>
</div>
<div>
<h6 class="alert-heading mb-1">Следующая публикация</h6>
<p class="mb-0">{{ data.schedulerStatus.next_run_time }}</p>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Кнопка сохранения -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-body text-center py-3">
<button type="button" class="btn btn-success btn-lg px-5" onclick="saveSettings()">
<i class="bi bi-save me-2"></i>Сохранить все настройки
</button>
<div class="form-text mt-2">
Сохраняются: выбранные сотрудники, текст поста, настройки расписания
</div> </div>
</div> </div>
<!-- Информация о следующем запуске -->
{% if data.schedulerStatus.next_run_time %}
<div class="alert alert-success shadow mx-3">
<div class="d-flex">
<div class="me-3">
<i class="bi bi-info-circle fs-5"></i>
</div>
<div>
<h6 class="alert-heading mb-1">Следующая публикация</h6>
<p class="mb-0" id="nextRunTime">{{ data.schedulerStatus.next_run_time }}</p>
</div>
</div>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
+17 -5
View File
@@ -96,6 +96,9 @@
<button type="button" class="btn btn-primary" onclick="saveVkSettings()" id="saveButton"> <button type="button" class="btn btn-primary" onclick="saveVkSettings()" id="saveButton">
<i class="bi bi-save me-1"></i>Сохранить настройки <i class="bi bi-save me-1"></i>Сохранить настройки
</button> </button>
<button type="button" class="btn btn-outline-success" onclick="handleLink()" id="linkButton">
<i class="bi bi-link me-1"></i>Получить данные из ссылки на фото
</button>
</div> </div>
</form> </form>
</div> </div>
@@ -103,7 +106,7 @@
<!-- Информация о текущих настройках --> <!-- Информация о текущих настройках -->
{% if data.vk_settings %} {% if data.vk_settings %}
<div class="card mt-4 fade-in"> <div class="card my-4 fade-in">
<div class="card-header bg-success bg-opacity-10 text-success"> <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> <h6 class="mb-0"><i class="bi bi-check-circle me-2"></i>Текущие настройки</h6>
</div> </div>
@@ -123,16 +126,25 @@
<div class="card bg-light"> <div class="card bg-light">
<div class="card-body"> <div class="card-body">
<h6 class="card-title"><i class="bi bi-people me-2"></i>ID сообщества</h6> <h6 class="card-title"><i class="bi bi-people me-2"></i>ID сообщества</h6>
<p class="card-text">-{{ data.vk_settings.group_id }}</p> <p class="card-text">
<a href="https://vk.com/club{{ data.vk_settings.group_id }}" target="_blank">
<i class="bi bi-link me-1"></i>-{{ data.vk_settings.group_id }}
</a>
</p>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="card bg-light"> <div class="card bg-light">
<div class="card-body"> <div class="card-body">
<h6 class="card-title"><i class="bi bi-image me-2"></i>ID Базового фото</h6> <h6 class="card-title"><i class="bi bi-image me-2"></i>Базовое фото</h6>
<p class="card-text">photo-{{ data.vk_settings.group_id }}_{{ <p class="card-text">
data.vk_settings.base_photo_url }}</p> <a href="https://vk.com/photo-{{ data.vk_settings.group_id }}_{{ data.vk_settings.base_photo_url }}"
target="_blank">
<i class="bi bi-link me-1"></i>photo-{{ data.vk_settings.group_id }}_{{
data.vk_settings.base_photo_url }}
</a>
</p>
</div> </div>
</div> </div>
</div> </div>