- Реализованы функции для закрепления и архивирования заметок, а также их восстановления. - Добавлены новые индексы в базу данных для улучшения производительности запросов. - Внедрено логирование действий пользователей, включая регистрацию, вход, создание, обновление и удаление заметок. - Обновлены интерфейсы для поддержки новых функций, включая кнопки для архивирования и закрепления заметок. - Оптимизированы стили и добавлены новые элементы управления для улучшения пользовательского опыта.
360 lines
12 KiB
JavaScript
360 lines
12 KiB
JavaScript
// Переменные для пагинации логов
|
||
let logsOffset = 0;
|
||
const logsLimit = 50;
|
||
let hasMoreLogs = true;
|
||
|
||
// Проверка аутентификации
|
||
async function checkAuthentication() {
|
||
try {
|
||
const response = await fetch("/api/auth/status");
|
||
if (!response.ok) {
|
||
localStorage.removeItem("isAuthenticated");
|
||
localStorage.removeItem("username");
|
||
window.location.href = "/";
|
||
return false;
|
||
}
|
||
const authData = await response.json();
|
||
if (!authData.authenticated) {
|
||
localStorage.removeItem("isAuthenticated");
|
||
localStorage.removeItem("username");
|
||
window.location.href = "/";
|
||
return false;
|
||
}
|
||
return true;
|
||
} catch (error) {
|
||
console.error("Ошибка проверки аутентификации:", error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Загрузка информации о пользователе для применения accent color
|
||
async function loadUserInfo() {
|
||
try {
|
||
const response = await fetch("/api/user");
|
||
if (response.ok) {
|
||
const user = await response.json();
|
||
const accentColor = user.accent_color || "#007bff";
|
||
if (
|
||
getComputedStyle(document.documentElement)
|
||
.getPropertyValue("--accent-color")
|
||
.trim() !== accentColor
|
||
) {
|
||
document.documentElement.style.setProperty("--accent-color", accentColor);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("Ошибка загрузки информации о пользователе:", error);
|
||
}
|
||
}
|
||
|
||
// Переключение табов
|
||
function initTabs() {
|
||
const tabs = document.querySelectorAll(".settings-tab");
|
||
const contents = document.querySelectorAll(".tab-content");
|
||
|
||
tabs.forEach((tab) => {
|
||
tab.addEventListener("click", () => {
|
||
const tabName = tab.dataset.tab;
|
||
|
||
// Убираем активный класс со всех табов и контентов
|
||
tabs.forEach((t) => t.classList.remove("active"));
|
||
contents.forEach((c) => c.classList.remove("active"));
|
||
|
||
// Добавляем активный класс к выбранному табу и контенту
|
||
tab.classList.add("active");
|
||
document.getElementById(`${tabName}-tab`).classList.add("active");
|
||
|
||
// Загружаем данные для таба
|
||
if (tabName === "archive") {
|
||
loadArchivedNotes();
|
||
} else if (tabName === "logs") {
|
||
loadLogs(true);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Загрузка архивных заметок
|
||
async function loadArchivedNotes() {
|
||
const container = document.getElementById("archived-notes-container");
|
||
container.innerHTML = '<p style="text-align: center; color: #999;">Загрузка...</p>';
|
||
|
||
try {
|
||
const response = await fetch("/api/notes/archived");
|
||
if (!response.ok) {
|
||
throw new Error("Ошибка загрузки архивных заметок");
|
||
}
|
||
|
||
const notes = await response.json();
|
||
|
||
if (notes.length === 0) {
|
||
container.innerHTML =
|
||
'<p style="text-align: center; color: #999;">Архив пуст</p>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = "";
|
||
|
||
notes.forEach((note) => {
|
||
const noteDiv = document.createElement("div");
|
||
noteDiv.className = "archived-note-item";
|
||
noteDiv.dataset.noteId = note.id;
|
||
|
||
// Форматируем дату
|
||
const created = new Date(note.created_at.replace(" ", "T") + "Z");
|
||
const dateStr = new Intl.DateTimeFormat("ru-RU", {
|
||
day: "2-digit",
|
||
month: "2-digit",
|
||
year: "numeric",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
}).format(created);
|
||
|
||
// Преобразуем markdown в HTML для предпросмотра
|
||
const htmlContent = marked.parse(note.content);
|
||
const preview = htmlContent.substring(0, 200) + (htmlContent.length > 200 ? "..." : "");
|
||
|
||
// Изображения
|
||
let imagesHtml = "";
|
||
if (note.images && note.images.length > 0) {
|
||
imagesHtml = `<div class="archived-note-images">${note.images.length} изображений</div>`;
|
||
}
|
||
|
||
noteDiv.innerHTML = `
|
||
<div class="archived-note-header">
|
||
<span class="archived-note-date">${dateStr}</span>
|
||
<div class="archived-note-actions">
|
||
<button class="btn-restore" data-id="${note.id}" title="Восстановить">
|
||
<span class="iconify" data-icon="mdi:restore"></span> Восстановить
|
||
</button>
|
||
<button class="btn-delete-permanent" data-id="${note.id}" title="Удалить навсегда">
|
||
<span class="iconify" data-icon="mdi:delete-forever"></span> Удалить
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="archived-note-content">${preview}</div>
|
||
${imagesHtml}
|
||
`;
|
||
|
||
container.appendChild(noteDiv);
|
||
});
|
||
|
||
// Добавляем обработчики событий
|
||
addArchivedNotesEventListeners();
|
||
} catch (error) {
|
||
console.error("Ошибка загрузки архивных заметок:", error);
|
||
container.innerHTML =
|
||
'<p style="text-align: center; color: #dc3545;">Ошибка загрузки архивных заметок</p>';
|
||
}
|
||
}
|
||
|
||
// Добавление обработчиков для архивных заметок
|
||
function addArchivedNotesEventListeners() {
|
||
// Восстановление
|
||
document.querySelectorAll(".btn-restore").forEach((btn) => {
|
||
btn.addEventListener("click", async (e) => {
|
||
const noteId = e.target.closest("button").dataset.id;
|
||
if (confirm("Восстановить эту заметку из архива?")) {
|
||
try {
|
||
const response = await fetch(`/api/notes/${noteId}/unarchive`, {
|
||
method: "PUT",
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error("Ошибка восстановления заметки");
|
||
}
|
||
|
||
// Удаляем элемент из списка
|
||
document
|
||
.querySelector(`[data-note-id="${noteId}"]`)
|
||
?.remove();
|
||
|
||
// Проверяем, остались ли заметки
|
||
const container = document.getElementById("archived-notes-container");
|
||
if (container.children.length === 0) {
|
||
container.innerHTML =
|
||
'<p style="text-align: center; color: #999;">Архив пуст</p>';
|
||
}
|
||
|
||
alert("Заметка восстановлена!");
|
||
} catch (error) {
|
||
console.error("Ошибка:", error);
|
||
alert("Ошибка восстановления заметки");
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// Окончательное удаление
|
||
document.querySelectorAll(".btn-delete-permanent").forEach((btn) => {
|
||
btn.addEventListener("click", async (e) => {
|
||
const noteId = e.target.closest("button").dataset.id;
|
||
if (
|
||
confirm(
|
||
"Удалить эту заметку НАВСЕГДА? Это действие нельзя отменить!"
|
||
)
|
||
) {
|
||
try {
|
||
const response = await fetch(`/api/notes/archived/${noteId}`, {
|
||
method: "DELETE",
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error("Ошибка удаления заметки");
|
||
}
|
||
|
||
// Удаляем элемент из списка
|
||
document
|
||
.querySelector(`[data-note-id="${noteId}"]`)
|
||
?.remove();
|
||
|
||
// Проверяем, остались ли заметки
|
||
const container = document.getElementById("archived-notes-container");
|
||
if (container.children.length === 0) {
|
||
container.innerHTML =
|
||
'<p style="text-align: center; color: #999;">Архив пуст</p>';
|
||
}
|
||
|
||
alert("Заметка удалена окончательно");
|
||
} catch (error) {
|
||
console.error("Ошибка:", error);
|
||
alert("Ошибка удаления заметки");
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Загрузка логов
|
||
async function loadLogs(reset = false) {
|
||
if (reset) {
|
||
logsOffset = 0;
|
||
hasMoreLogs = true;
|
||
}
|
||
|
||
const tbody = document.getElementById("logsTableBody");
|
||
const loadMoreContainer = document.getElementById("logsLoadMore");
|
||
const filterValue = document.getElementById("logTypeFilter").value;
|
||
|
||
if (reset) {
|
||
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center;">Загрузка...</td></tr>';
|
||
}
|
||
|
||
try {
|
||
let url = `/api/logs?limit=${logsLimit}&offset=${logsOffset}`;
|
||
if (filterValue) {
|
||
url += `&action_type=${filterValue}`;
|
||
}
|
||
|
||
const response = await fetch(url);
|
||
if (!response.ok) {
|
||
throw new Error("Ошибка загрузки логов");
|
||
}
|
||
|
||
const logs = await response.json();
|
||
|
||
if (reset) {
|
||
tbody.innerHTML = "";
|
||
}
|
||
|
||
if (logs.length === 0 && logsOffset === 0) {
|
||
tbody.innerHTML =
|
||
'<tr><td colspan="4" style="text-align: center; color: #999;">Логов пока нет</td></tr>';
|
||
loadMoreContainer.style.display = "none";
|
||
return;
|
||
}
|
||
|
||
if (logs.length < logsLimit) {
|
||
hasMoreLogs = false;
|
||
}
|
||
|
||
logs.forEach((log) => {
|
||
const row = document.createElement("tr");
|
||
|
||
// Форматируем дату
|
||
const created = new Date(log.created_at.replace(" ", "T") + "Z");
|
||
const dateStr = new Intl.DateTimeFormat("ru-RU", {
|
||
day: "2-digit",
|
||
month: "2-digit",
|
||
year: "numeric",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
second: "2-digit",
|
||
}).format(created);
|
||
|
||
// Форматируем тип действия
|
||
const actionTypes = {
|
||
login: "Вход",
|
||
logout: "Выход",
|
||
register: "Регистрация",
|
||
note_create: "Создание заметки",
|
||
note_update: "Редактирование",
|
||
note_delete: "Удаление",
|
||
note_pin: "Закрепление",
|
||
note_archive: "Архивирование",
|
||
note_unarchive: "Восстановление",
|
||
note_delete_permanent: "Окончательное удаление",
|
||
profile_update: "Обновление профиля",
|
||
};
|
||
|
||
const actionText = actionTypes[log.action_type] || log.action_type;
|
||
|
||
row.innerHTML = `
|
||
<td>${dateStr}</td>
|
||
<td><span class="log-action-badge log-action-${log.action_type}">${actionText}</span></td>
|
||
<td>${log.details || "-"}</td>
|
||
<td>${log.ip_address || "-"}</td>
|
||
`;
|
||
|
||
tbody.appendChild(row);
|
||
});
|
||
|
||
logsOffset += logs.length;
|
||
|
||
if (hasMoreLogs && logs.length > 0) {
|
||
loadMoreContainer.style.display = "block";
|
||
} else {
|
||
loadMoreContainer.style.display = "none";
|
||
}
|
||
} catch (error) {
|
||
console.error("Ошибка загрузки логов:", error);
|
||
if (reset) {
|
||
tbody.innerHTML =
|
||
'<tr><td colspan="4" style="text-align: center; color: #dc3545;">Ошибка загрузки логов</td></tr>';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Инициализация при загрузке страницы
|
||
document.addEventListener("DOMContentLoaded", async function () {
|
||
// Проверяем аутентификацию
|
||
const isAuth = await checkAuthentication();
|
||
if (!isAuth) return;
|
||
|
||
// Загружаем информацию о пользователе
|
||
await loadUserInfo();
|
||
|
||
// Инициализируем табы
|
||
initTabs();
|
||
|
||
// Загружаем архивные заметки по умолчанию
|
||
loadArchivedNotes();
|
||
|
||
// Обработчик фильтра логов
|
||
document.getElementById("logTypeFilter").addEventListener("change", () => {
|
||
loadLogs(true);
|
||
});
|
||
|
||
// Обработчик кнопки обновления логов
|
||
document.getElementById("refreshLogs").addEventListener("click", () => {
|
||
loadLogs(true);
|
||
});
|
||
|
||
// Обработчик кнопки "Загрузить еще"
|
||
document.getElementById("loadMoreLogsBtn").addEventListener("click", () => {
|
||
loadLogs(false);
|
||
});
|
||
});
|
||
|
||
|