addressbook_web/templates/index.html

1287 lines
55 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Органайзер АйТи-Депо</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.12/jstree.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.12/themes/default/style.min.css">
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
height: 100vh;
background-color: #f4f4f4;
transition: background-color 0.3s;
}
body.dark-theme {
background-color: #222;
color: #fff;
}
#tree-container, #installs-container, #notes-panel {
overflow: auto;
}
#tree-container {
width: 250px;
min-width: 200px;
border-right: 1px solid #ccc;
padding: 10px;
background-color: #fff;
transition: width 0.3s;
}
#tree-container.dark-theme {
border-right-color: #444;
background-color: #333;
}
#installs-container {
flex-grow: 1;
padding: 10px;
background-color: #fff;
}
#installs-container.dark-theme {
background-color: #333;
}
#notes-panel {
width: 300px;
min-width: 200px;
border-left: 1px solid #ccc;
padding: 10px;
background-color: #fff;
}
#notes-panel.dark-theme {
border-left-color: #444;
background-color: #333;
}
#splitter {
width: 5px;
background-color: #ccc;
cursor: col-resize;
position: relative;
z-index: 10;
}
#splitter.dark-theme {
background-color: #444;
}
.header-logo {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.header-logo img {
height: 40px;
margin-right: 10px;
}
.theme-toggle, #add-record-button {
padding: 5px 10px;
cursor: pointer;
}
.theme-toggle.dark-theme, #add-record-button.dark-theme {
background-color: #555;
color: #fff;
border: 1px solid #777;
}
.export-import-container {
margin-bottom: 10px;
}
#search-container {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
#search-container.dark-theme input, #search-container.dark-theme select {
background-color: #444;
color: #fff;
border: 1px solid #666;
}
.header {
display: flex;
justify-content: space-between;
font-weight: bold;
padding: 5px;
background-color: #e0e0e0;
cursor: pointer;
}
.header.dark-theme {
background-color: #444;
}
.header div {
flex: 1;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.sort-arrow {
margin-left: 5px;
}
.install-item {
display: flex;
justify-content: space-between;
padding: 5px 8px; /* Увеличим внутренний отступ для большей "воздушности" */
border-bottom: 1px solid #ddd;
cursor: pointer;
align-items: center;
}
.install-item.dark-theme {
border-bottom-color: #555;
background-color: #444;
}
.install-item:hover {
background-color: #f0f0f0;
}
.install-item.dark-theme:hover {
background-color: #555;
}
.install-item.selected {
background-color: #d0d0d0;
}
.install-item.dark-theme.selected {
background-color: #666;
}
.computer-name, .connection-id, .install-time, .actions {
flex: 1;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: flex;
align-items: center;
justify-content: center;
}
.protocol-icon img {
height: 32px;
width: 32px;
margin-right: 8px; /* Увеличим отступ для большей читаемости */
vertical-align: middle;
}
.connection-link {
color: #007bff;
text-decoration: none;
flex-grow: 1;
margin-right: 8px;
}
.connection-link.dark-theme {
color: #66b0ff;
}
.action-buttons button {
margin: 0 2px;
padding: 2px 5px;
display: flex;
align-items: center;
background: none;
border: none;
cursor: pointer;
}
.action-buttons.dark-theme button {
background-color: #555;
color: #fff;
border: 1px solid #777;
}
/* Стили для иконок */
.custom-icon {
height: 32px;
width: 32px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
background-color: transparent;
border: none;
padding: 0;
margin: 0 4px; /* Добавляем отступы вокруг иконок */
cursor: pointer;
transition: background-color 0.2s, transform 0.2s;
}
.custom-icon:hover {
background-color: #e0e0e0;
transform: scale(1.1);
}
.custom-icon.dark-theme:hover {
background-color: #444;
}
/* Иконки для светлой темы */
.edit-icon { background-image: url('/icons/edit.png'); }
.copy-icon { background-image: url('/icons/copy.png'); }
/* Иконки для темной темы */
.edit-icon.dark-theme { background-image: url('/icons/edit-dark.png'); }
.copy-icon.dark-theme { background-image: url('/icons/copy-dark.png'); }
/* Иконки для папок (меньший размер) */
.folder-edit-icon, .folder-delete-icon {
height: 16px;
width: 16px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
background-color: transparent;
border: none;
padding: 0;
margin: 0 2px; /* Небольшой отступ для папочных иконок */
cursor: pointer;
transition: background-color 0.2s, transform 0.2s;
}
.folder-edit-icon:hover, .folder-delete-icon:hover {
background-color: #e0e0e0;
transform: scale(1.1);
}
.folder-edit-icon.dark-theme:hover, .folder-delete-icon.dark-theme:hover {
background-color: #444;
}
.folder-edit-icon { background-image: url('/icons/edit.png'); }
.folder-delete-icon { background-image: url('/icons/delete.png'); }
.folder-edit-icon.dark-theme { background-image: url('/icons/edit-dark.png'); }
.folder-delete-icon.dark-theme { background-image: url('/icons/delete-dark.png'); }
/* Стили для кнопок удаления и заметок */
.action-button {
margin: 0 4px; /* Увеличим расстояние между кнопками */
padding: 5px 8px;
font-size: 12px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #f9f9f9;
color: #333;
cursor: pointer;
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
}
.action-button:hover {
background-color: #e0e0e0;
}
.action-button.dark-theme {
background-color: #555;
color: #fff;
border-color: #777;
}
.action-button.dark-theme:hover {
background-color: #666;
}
#notes-content {
padding: 10px;
min-height: 200px;
border: 1px solid #ccc;
}
#notes-content.dark-theme {
border-color: #444;
background-color: #444;
}
#modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
#add-modal, #note-modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
padding: 20px;
border-radius: 5px;
z-index: 1001;
width: 400px;
}
#add-modal.dark-theme, #note-modal.dark-theme {
background: #333;
color: #fff;
}
#add-modal input, #add-modal select, #add-modal textarea,
#note-modal textarea {
width: 100%;
margin-bottom: 10px;
padding: 5px;
}
#add-modal.dark-theme input, #add-modal.dark-theme select, #add-modal.dark-theme textarea,
#note-modal.dark-theme textarea {
background-color: #444;
color: #fff;
border: 1px solid #666;
}
.markdown-buttons, .image-upload {
margin-bottom: 10px;
}
.time-hint {
font-size: 0.8em;
color: #666;
}
.time-hint.dark-theme {
color: #bbb;
}
.status-online {
background-color: green;
width: 10px;
height: 10px;
display: inline-block;
border-radius: 50%;
vertical-align: middle;
margin-left: 5px;
}
.status-offline {
background-color: red;
width: 10px;
height: 10px;
display: inline-block;
border-radius: 50%;
vertical-align: middle;
margin-left: 5px;
}
.highlight {
background-color: #e0e0e0;
}
.highlight.dark-theme {
background-color: #555;
}
</style>
</head>
<body>
<div id="tree-container">
<h2>Папки</h2>
<div id="folder-tree"></div>
<button onclick="addFolder()">Добавить папку</button>
</div>
<div id="splitter"></div>
<div id="installs-container">
<h1 class="header-logo">
<img src="/icons/it-depo-logo.png" alt="АйТи-Депо логотип">
Органайзер АйТи-Депо
<button class="theme-toggle" onclick="toggleTheme()">Темная тема</button>
</h1>
<div class="export-import-container">
<button onclick="exportCSV()">Экспорт CSV</button>
<form id="import-form" enctype="multipart/form-data">
<input type="file" id="import-file" accept=".csv" onchange="importCSV(this.files[0])">
<label for="import-file" id="import-label">Импорт CSV</label>
</form>
</div>
<div id="search-container">
<input type="text" id="search-input" placeholder="Поиск по ID или имени">
<select id="folder-select">
<option value="">Все папки</option>
</select>
<button id="add-record-button" onclick="openAddModal()">Добавить запись</button>
</div>
<div class="header">
<div onclick="sortInstalls('computer_name')">Имя компьютера<span class="sort-arrow"></span></div>
<div onclick="sortInstalls('rust_id')">ID подключения<span class="sort-arrow"></span></div>
<div onclick="sortInstalls('install_time')">Время установки<span class="sort-arrow"></span></div>
<div>Действия</div>
</div>
<div id="installs-list"></div>
</div>
<div id="notes-panel">
<h2>Заметка</h2>
<div id="notes-content"></div>
</div>
<div id="modal-overlay"></div>
<div id="add-modal">
<h3>Добавить запись</h3>
<input type="text" id="add-rustId" placeholder="ID подключения" required>
<input type="text" id="add-computerName" placeholder="Имя компьютера" required>
<input type="text" id="add-installTime" placeholder="Время (опционально, формат: ГГГГ-ММ-ДД ЧЧ:ММ:СС)">
<select id="add-protocol">
<option value="rustdesk" selected>RustDesk</option>
<option value="anydesk">AnyDesk</option>
<option value="ammyy">Ammyy Admin</option>
<option value="teamviewer">TeamViewer</option>
<option value="vnc">VNC</option>
<option value="rdp">RDP</option>
</select>
<textarea id="add-note" placeholder="Заметка (Markdown поддерживается)"></textarea>
<button onclick="addInstall()">Добавить</button>
<button onclick="closeAddModal()">Закрыть</button>
<div class="time-hint">Пример: 2025-03-05 14:30:00</div>
</div>
<div id="note-modal">
<h3>Редактировать заметку</h3>
<div class="markdown-buttons">
<button onclick="formatBold()">Жирный</button>
<button onclick="formatItalic()">Курсив</button>
<button onclick="formatLink()">Ссылка</button>
</div>
<div class="image-upload">
<input type="file" id="image-upload-input" accept="image/*" onchange="uploadImage()">
<button onclick="document.getElementById('image-upload-input').click()">Добавить изображение</button>
</div>
<textarea id="note-textarea" placeholder="Введите заметку (Markdown поддерживается)"></textarea>
<button onclick="saveNote()">Сохранить</button>
<button onclick="closeNoteModal()">Закрыть</button>
</div>
<script>
const API_URL = "/api";
let selectedFolderId = null;
let allInstalls = [];
let allFolders = [];
let selectedInstallId = null;
let sortField = null;
let sortDirection = 'asc';
let ws = null;
const protocolIcons = {
'rustdesk': '<img src="/icons/rustdesk.png" class="protocol-icon">',
'anydesk': '<img src="/icons/anydesk.png" class="protocol-icon">',
'ammyy': '<img src="/icons/ammyy.png" class="protocol-icon">',
'teamviewer': '<img src="/icons/teamviewer.png" class="protocol-icon">',
'vnc': '<img src="/icons/vnc.png" class="protocol-icon">',
'rdp': '<img src="/icons/rdp.png" class="protocol-icon">'
};
const protocolLinks = {
'rustdesk': 'rustdesk://',
'anydesk': 'anydesk://',
'ammyy': 'ammyy://',
'teamviewer': 'teamviewer://remote-control/',
'vnc': 'vnc://',
'rdp': 'rdp://'
};
document.addEventListener('DOMContentLoaded', () => {
const savedWidth = localStorage.getItem('treeContainerWidth');
if (savedWidth) {
document.getElementById('tree-container').style.width = savedWidth + 'px';
}
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const defaultTheme = savedTheme || (prefersDark ? 'dark' : 'light');
if (defaultTheme === 'dark') {
document.body.classList.add('dark-theme');
document.getElementById('installs-container').classList.add('dark-theme');
document.getElementById('tree-container').classList.add('dark-theme');
document.getElementById('notes-panel').classList.add('dark-theme');
document.getElementById('add-modal').classList.add('dark-theme');
document.getElementById('note-modal').classList.add('dark-theme');
document.getElementById('search-container').classList.add('dark-theme');
document.getElementById('import-label').classList.add('dark-theme');
document.getElementById('splitter').classList.add('dark-theme');
document.querySelector('.theme-toggle').classList.add('dark-theme');
document.querySelector('#add-record-button').classList.add('dark-theme');
document.querySelector('.header').classList.add('dark-theme');
document.querySelectorAll('.install-item').forEach(item => item.classList.add('dark-theme'));
document.querySelectorAll('.action-button').forEach(button => button.classList.add('dark-theme'));
$('#folder-tree').addClass('jstree-default-dark');
document.querySelector('.theme-toggle').textContent = 'Светлая тема';
} else {
document.querySelector('.theme-toggle').textContent = 'Темная тема';
}
localStorage.setItem('theme', defaultTheme);
const splitter = document.getElementById('splitter');
const treeContainer = document.getElementById('tree-container');
let isResizing = false;
splitter.addEventListener('mousedown', (e) => {
isResizing = true;
document.body.style.cursor = 'col-resize';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const containerRect = document.body.getBoundingClientRect();
let newWidth = e.clientX - containerRect.left;
const minWidth = 200;
const maxWidth = containerRect.width * 0.5;
if (newWidth < minWidth) newWidth = minWidth;
if (newWidth > maxWidth) newWidth = maxWidth;
treeContainer.style.width = `${newWidth}px`;
});
document.addEventListener('mouseup', () => {
isResizing = false;
document.body.style.cursor = 'default';
const width = treeContainer.offsetWidth;
localStorage.setItem('treeContainerWidth', width);
});
document.addEventListener('keydown', (e) => {
if (!selectedFolderId || !allInstalls.length) return;
const items = $('#installs-list .install-item');
let currentIndex = items.index($('.install-item.selected'));
if (e.key === 'ArrowUp' && currentIndex > 0) {
e.preventDefault();
currentIndex--;
} else if (e.key === 'ArrowDown' && currentIndex < items.length - 1) {
e.preventDefault();
currentIndex++;
} else {
return;
}
selectedInstallId = $(items[currentIndex]).data('id');
$('.install-item').removeClass('selected');
$(items[currentIndex]).addClass('selected');
updateNotesPanel();
$(items[currentIndex])[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
});
connectWebSocket();
});
function toggleTheme() {
const body = document.body;
const installsContainer = document.getElementById('installs-container');
const treeContainer = document.getElementById('tree-container');
const notesPanel = document.getElementById('notes-panel');
const addModal = document.getElementById('add-modal');
const noteModal = document.getElementById('note-modal');
const searchContainer = document.getElementById('search-container');
const importLabel = document.getElementById('import-label');
const splitter = document.getElementById('splitter');
const themeToggle = document.querySelector('.theme-toggle');
const addRecordButton = document.getElementById('add-record-button');
const header = document.querySelector('.header');
const installItems = document.querySelectorAll('.install-item');
const actionButtons = document.querySelectorAll('.action-button');
const folderTree = $('#folder-tree');
if (body.classList.contains('dark-theme')) {
body.classList.remove('dark-theme');
installsContainer.classList.remove('dark-theme');
treeContainer.classList.remove('dark-theme');
notesPanel.classList.remove('dark-theme');
addModal.classList.remove('dark-theme');
noteModal.classList.remove('dark-theme');
searchContainer.classList.remove('dark-theme');
importLabel.classList.remove('dark-theme');
splitter.classList.remove('dark-theme');
themeToggle.classList.remove('dark-theme');
addRecordButton.classList.remove('dark-theme');
header.classList.remove('dark-theme');
installItems.forEach(item => item.classList.remove('dark-theme'));
actionButtons.forEach(button => button.classList.remove('dark-theme'));
folderTree.removeClass('jstree-default-dark');
themeToggle.textContent = 'Темная тема';
localStorage.setItem('theme', 'light');
} else {
body.classList.add('dark-theme');
installsContainer.classList.add('dark-theme');
treeContainer.classList.add('dark-theme');
notesPanel.classList.add('dark-theme');
addModal.classList.add('dark-theme');
noteModal.classList.add('dark-theme');
searchContainer.classList.add('dark-theme');
importLabel.classList.add('dark-theme');
splitter.classList.add('dark-theme');
themeToggle.classList.add('dark-theme');
addRecordButton.classList.add('dark-theme');
header.classList.add('dark-theme');
installItems.forEach(item => item.classList.add('dark-theme'));
actionButtons.forEach(button => button.classList.add('dark-theme'));
folderTree.addClass('jstree-default-dark');
themeToggle.textContent = 'Светлая тема';
localStorage.setItem('theme', 'dark');
}
}
$(document).ready(function () {
loadFolders();
loadInstalls(null);
$('#folder-tree').jstree({
'core': {
'data': function (node, cb) {
$.getJSON(`${API_URL}/folders`, function (data) {
allFolders = data;
const sortedFolders = [...data].sort((a, b) => a.name.localeCompare(b.name));
const treeData = [
{ id: "root", text: "Корень", parent: "#" }
].concat(sortedFolders.map(folder => ({
id: folder.id,
text: `${folder.name} <span class="folder-actions">` +
`<button class="folder-edit-icon" title="Редактировать папку" onclick="editFolder(${folder.id}, '${folder.name}')"></button>` +
`<button class="folder-delete-icon" title="Удалить папку" onclick="deleteFolder(${folder.id})"></button></span>`,
parent: folder.parent_id ? folder.parent_id : "root"
})));
cb(treeData);
const unsortedFolder = sortedFolders.find(f => f.name === 'Несортированные');
if (unsortedFolder) {
setTimeout(() => {
$('#folder-tree').jstree('select_node', unsortedFolder.id);
loadInstalls(unsortedFolder.id);
updateFolderActions();
}, 100);
}
}).fail(function(jqxhr, textStatus, error) {
console.error("Error loading folders:", textStatus, error);
});
},
'check_callback': true,
'themes': {
'variant': 'default-dark'
}
},
'plugins': ['dnd', 'html_data'],
'dnd': {
'is_draggable': true
}
}).on('select_node.jstree', function (e, data) {
selectedFolderId = data.node.id === "root" ? null : data.node.id;
loadInstalls(selectedFolderId);
updateFolderActions();
}).on('move_node.jstree', function (e, data) {
const movedNode = data.node;
const newParentId = movedNode.parent === '#' ? null : movedNode.parent;
saveFolderPosition(movedNode.id, newParentId);
}).on('dragover.jstree', function (e) {
e.preventDefault();
const node = $(e.target).closest('.jstree-node');
if (node.length) {
node.addClass('highlight').addClass(document.body.classList.contains('dark-theme') ? 'dark-theme' : '');
}
}).on('dragenter.jstree', function (e) {
e.preventDefault();
const node = $(e.target).closest('.jstree-node');
if (node.length) {
node.addClass('highlight').addClass(document.body.classList.contains('dark-theme') ? 'dark-theme' : '');
}
}).on('dragleave.jstree', function (e) {
const node = $(e.target).closest('.jstree-node');
if (node.length) {
node.removeClass('highlight');
}
}).on('drop.jstree', function (e) {
const node = $(e.target).closest('.jstree-node');
if (node.length) {
node.removeClass('highlight');
}
});
$('#search-input').on('input', function () {
performSearch();
});
$('#folder-select').on('change', function () {
performSearch();
});
$('#installs-list').on('dragstart', '.install-item', function (e) {
e.originalEvent.dataTransfer.setData('text/plain', $(this).data('id'));
});
$('#folder-tree').on('dragover', function (e) {
e.preventDefault();
}).on('drop', function (e) {
e.preventDefault();
const installId = e.originalEvent.dataTransfer.getData('text');
let targetFolderId = $(e.target).closest('.jstree-node').attr('id');
if (installId && targetFolderId) {
targetFolderId = targetFolderId === "root" ? null : targetFolderId;
moveInstallToFolder(installId, targetFolderId);
}
});
$('#installs-list').on('click', '.note-button', function () {
const installId = $(this).closest('.install-item').data('id');
const install = allInstalls.find(i => i.id === installId);
openNoteModal(installId, install?.note || '');
});
});
function connectWebSocket() {
if (ws) {
ws.close();
}
ws = new WebSocket(`wss://${window.location.host}/ws`);
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.type === "update") {
const updatedInstall = data.data;
const index = allInstalls.findIndex(i => i.id === updatedInstall.id);
if (index !== -1) {
allInstalls[index] = updatedInstall;
} else {
allInstalls.push(updatedInstall);
alert(`Новое подключение добавлено: ${updatedInstall.computer_name} (ID: ${updatedInstall.rust_id})`);
}
displayInstalls(allInstalls.filter(i => !selectedFolderId || i.folder_id == selectedFolderId));
}
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
};
ws.onclose = function() {
console.log('WebSocket disconnected, retrying...');
let retryDelay = 1000;
const maxDelay = 30000;
const maxAttempts = 10;
let attempts = 0;
function reconnect() {
if (attempts < maxAttempts) {
setTimeout(() => {
connectWebSocket();
retryDelay = Math.min(retryDelay * 2, maxDelay);
attempts++;
}, retryDelay);
} else {
console.error('Max reconnect attempts reached');
}
}
reconnect();
};
}
function loadFolders() {
$.getJSON(`${API_URL}/folders`, function (data) {
allFolders = [...data].sort((a, b) => a.name.localeCompare(b.name));
updateFolderSelect();
}).fail(function(jqxhr, textStatus, error) {
console.error("Error loading folders:", textStatus, error);
});
}
function loadInstalls(folderId) {
$.getJSON(`${API_URL}/installs`, function (data) {
allInstalls = data;
let filtered = folderId ? data.filter(i => i.folder_id == folderId && i.folder_id !== null) : data;
displayInstalls(filtered);
updateNotesPanel();
if (selectedInstallId) {
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
}
}).fail(function(jqxhr, textStatus, error) {
console.error("Error loading installs:", textStatus, error);
$('#installs-list').html('<p>Ошибка загрузки данных. Проверьте консоль.</p>');
});
selectedFolderId = folderId;
}
function performSearch() {
const query = $('#search-input').val().toLowerCase().trim();
const folderId = $('#folder-select').val() || '';
let filteredInstalls = [...allInstalls];
if (folderId) {
filteredInstalls = filteredInstalls.filter(i =>
i.folder_id == folderId &&
allFolders.some(f => f.id === i.folder_id)
);
}
if (query) {
filteredInstalls = filteredInstalls.filter(i =>
i.rust_id.toLowerCase().trim().includes(query) ||
i.computer_name.toLowerCase().trim().includes(query)
);
}
sortInstalls(null, filteredInstalls);
}
function displayInstalls(installs) {
const isDarkTheme = document.body.classList.contains('dark-theme');
if (installs.length === 0) {
$('#installs-list').html('<p>Нет результатов</p>');
} else {
$('#installs-list').html(installs.map(item => {
const lastSeen = new Date(item.last_seen || '1970-01-01');
const isOnline = (new Date() - lastSeen) < 60000;
const statusClass = isOnline ? 'status-online' : 'status-offline';
return `
<div class="install-item ${isDarkTheme ? 'dark-theme' : ''}" data-id="${item.id}" draggable="true">
<div class="computer-name">${item.computer_name} <button class="custom-icon edit-icon" title="Редактировать имя компьютера" onclick="editField(${item.id}, 'computer_name', '${item.computer_name}')"></button></div>
<div class="connection-id"><span class="protocol-icon">${protocolIcons[item.protocol]}</span><a href="${protocolLinks[item.protocol]}${item.rust_id}" class="connection-link ${isDarkTheme ? 'dark-theme' : ''}" target="_blank">${item.rust_id}</a><span class="${statusClass}"></span> <button class="custom-icon copy-icon" title="Скопировать ID подключения" onclick="copyToClipboard('${item.rust_id}')"></button> <button class="custom-icon edit-icon" title="Редактировать ID подключения" onclick="editField(${item.id}, 'rust_id', '${item.rust_id}')"></button></div>
<div class="install-time">${item.install_time} <button class="custom-icon edit-icon" title="Редактировать время установки" onclick="editField(${item.id}, 'install_time', '${item.install_time}')"></button></div>
<div class="actions"><span class="action-buttons"><button class="action-button delete-button ${isDarkTheme ? 'dark-theme' : ''}" title="Удалить запись" onclick="deleteInstall(${item.id})">Удалить</button> <button class="action-button note-button ${isDarkTheme ? 'dark-theme' : ''}" title="Добавить или редактировать заметку">Заметка</button></span></div>
</div>
`;
}).join(''));
}
updateSortArrows();
if (selectedInstallId) {
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
}
}
function selectInstall(installId, rustId, protocol) {
selectedInstallId = installId;
$('.install-item').removeClass('selected');
$(`#installs-list .install-item[data-id="${installId}"]`).addClass('selected');
updateNotesPanel();
const link = protocolLinks[protocol] + rustId;
window.location.href = link;
}
function sortInstalls(field, filteredInstalls = null) {
let installs = filteredInstalls || (selectedFolderId
? allInstalls.filter(i => i.folder_id == selectedFolderId && i.folder_id !== null)
: allInstalls);
if (field) {
if (sortField === field) {
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
} else {
sortField = field;
sortDirection = 'asc';
}
installs.sort((a, b) => {
let valA = a[field], valB = b[field];
if (field === 'install_time') {
valA = new Date(valA);
valB = new Date(valB);
}
return sortDirection === 'asc' ? (valA > valB ? 1 : -1) : (valA < valB ? 1 : -1);
});
}
displayInstalls(installs);
}
function updateSortArrows() {
$('.sort-arrow').text('');
if (sortField) {
$(`.header div:contains("${sortField === 'computer_name' ? 'Имя компьютера' : sortField === 'rust_id' ? 'ID подключения' : 'Время установки'}") .sort-arrow`)
.text(sortDirection === 'asc' ? '↑' : '↓');
}
}
function addFolder() {
const name = prompt("Введите имя папки:");
if (name) {
$.ajax({
url: `${API_URL}/folders`,
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ name, parent_id: selectedFolderId }),
success: function () {
loadFolders();
$('#folder-tree').jstree(true).refresh();
}
});
}
}
function editFolder(folderId, currentName) {
const newName = prompt("Введите новое имя папки:", currentName);
if (newName) {
$.ajax({
url: `${API_URL}/folders/${folderId}`,
type: 'PUT',
contentType: 'application/json',
data: JSON.stringify({ name: newName }),
success: function () {
loadFolders();
$('#folder-tree').jstree(true).refresh();
}
});
}
}
function deleteFolder(folderId) {
if (confirm("Удалить папку? Все записи будут перемещены в корень.")) {
$.ajax({
url: `${API_URL}/folders/${folderId}`,
type: 'DELETE',
success: function () {
$.getJSON(`${API_URL}/installs`, function (data) {
const folderInstalls = data.filter(i => i.folder_id == folderId);
folderInstalls.forEach(item => {
moveInstallToFolder(item.id, null);
});
});
loadFolders();
$('#folder-tree').jstree(true).refresh();
loadInstalls(selectedFolderId);
},
error: function (xhr, status, error) {
if (xhr.status === 403) {
alert("Нельзя удалить папку 'Несортированные'.");
} else {
console.error("Ошибка удаления:", status, error);
}
}
});
}
}
function openAddModal() {
$('#add-rustId').val('');
$('#add-computerName').val('');
$('#add-installTime').val('');
$('#add-protocol').val('rustdesk');
$('#add-note').val('');
$('#add-modal').show();
$('#modal-overlay').show();
}
function closeAddModal() {
$('#add-modal').hide();
$('#modal-overlay').hide();
}
function addInstall() {
const rustId = $('#add-rustId').val();
const computerName = $('#add-computerName').val();
const installTime = $('#add-installTime').val() || '';
const protocol = $('#add-protocol').val();
const note = $('#add-note').val() || '';
if (installTime && !/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$/.test(installTime)) {
alert("Неверный формат времени. Используйте ГГГГ-ММ-ДД ЧЧ:ММ:СС (например, 2025-03-05 14:30:00)");
return;
}
$.ajax({
url: `${API_URL}/install`,
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ rust_id: rustId, computer_name: computerName, install_time: installTime, folder_id: selectedFolderId, protocol: protocol, note: note }),
success: function () {
loadInstalls(selectedFolderId);
closeAddModal();
},
error: function (xhr, status, error) {
if (xhr.status === 400) {
alert(xhr.responseJSON.detail);
} else {
console.error("Ошибка добавления:", status, error);
}
}
});
}
function editField(installId, field, currentValue) {
let newValue;
if (field === 'install_time') {
newValue = prompt(`Новое ${field === 'computer_name' ? 'Имя компьютера' : field === 'rust_id' ? 'ID подключения' : 'Время установки'} (опционально, формат для времени: ГГГГ-ММ-ДД ЧЧ:ММ:СС):`, currentValue || '');
if (newValue && field === 'install_time' && !/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$/.test(newValue)) {
alert("Неверный формат времени. Используйте ГГГГ-ММ-ДД ЧЧ:ММ:СС (например, 2025-03-05 14:30:00)");
return;
}
} else {
newValue = prompt(`Новое ${field === 'computer_name' ? 'Имя компьютера' : field === 'rust_id' ? 'ID подключения' : 'Время установки'}:`, currentValue || '');
}
if (newValue !== null && newValue !== currentValue) {
let data = {};
data[field] = newValue;
if (field === 'install_time') {
data['folder_id'] = selectedFolderId;
}
const install = allInstalls.find(i => i.id === installId);
if (install) {
data['protocol'] = install.protocol;
data['note'] = install.note;
}
$.ajax({
url: `${API_URL}/install/${installId}`,
type: 'PUT',
contentType: 'application/json',
data: JSON.stringify(data),
success: function () {
loadInstalls(selectedFolderId);
},
error: function (xhr, status, error) {
if (xhr.status === 400) {
alert(xhr.responseJSON.detail);
} else {
console.error(`Ошибка редактирования ${field}:`, status, error);
}
}
});
}
}
function deleteInstall(id) {
if (confirm("Удалить запись?")) {
$.ajax({
url: `${API_URL}/install/${id}`,
type: 'DELETE',
success: function () {
loadInstalls(selectedFolderId);
}
});
}
}
function moveInstallToFolder(installId, folderId) {
const install = allInstalls.find(i => i.id === parseInt(installId));
if (!install) {
console.error('Install not found:', installId);
return;
}
$.ajax({
url: `${API_URL}/install/${installId}`,
type: 'PUT',
contentType: 'application/json',
data: JSON.stringify({
rust_id: install.rust_id,
computer_name: install.computer_name,
install_time: install.install_time,
folder_id: folderId,
protocol: install.protocol,
note: install.note
}),
success: function () {
loadInstalls(selectedFolderId);
},
error: function (xhr, status, error) {
console.error("Ошибка переноса:", status, error);
if (xhr.status === 404) {
alert("Запись не найдена");
} else {
alert("Ошибка при перемещении записи");
}
}
});
}
function exportCSV() {
const folderId = $('#folder-select').val() || '';
window.location.href = `${API_URL}/export/csv?folder_id=${folderId}`;
}
function importCSV(file) {
if (!file) return;
const folderId = $('#folder-select').val() || '';
const formData = new FormData();
formData.append('file', file);
if (folderId) {
formData.append('folder_id', folderId);
}
$.ajax({
url: `${API_URL}/import/csv`,
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function (response) {
alert(response.message);
loadInstalls(selectedFolderId);
},
error: function (xhr, status, error) {
alert(`Ошибка импорта: ${xhr.responseJSON.detail || error}`);
}
});
}
function openNoteModal(installId, currentNote) {
selectedInstallId = installId;
$('#note-textarea').val(currentNote || '');
$('#note-modal').show();
$('#modal-overlay').show();
document.getElementById('image-upload-input').value = '';
}
function uploadImage() {
const fileInput = document.getElementById('image-upload-input');
const file = fileInput.files[0];
if (!file || !selectedInstallId) {
alert('Пожалуйста, выберите запись и изображение.');
return;
}
const formData = new FormData();
formData.append('install_id', selectedInstallId);
formData.append('file', file);
$.ajax({
url: `${API_URL}/upload-image`,
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function (response) {
const imageUrl = response.url;
const textarea = $('#note-textarea');
const cursorPos = textarea[0].selectionStart;
const text = textarea.val();
const altText = prompt('Введите альтернативный текст для изображения:', file.name);
const markdownImage = `![${altText || 'image'}](${imageUrl})`;
textarea.val(text.substring(0, cursorPos) + markdownImage + text.substring(cursorPos));
textarea[0].selectionStart = cursorPos + markdownImage.length;
textarea[0].selectionEnd = cursorPos + markdownImage.length;
},
error: function (xhr, status, error) {
console.error('Error uploading image:', status, error);
alert(`Не удалось загрузить изображение: ${xhr.responseJSON?.detail || 'Неизвестная ошибка'}`);
}
});
}
function saveNote() {
if (!selectedInstallId) {
alert('Выберите запись для редактирования заметки.');
return;
}
const note = $('#note-textarea').val() || '';
const install = allInstalls.find(i => i.id === selectedInstallId);
if (install) {
const currentSelectedId = selectedInstallId;
$.ajax({
url: `${API_URL}/install/${selectedInstallId}`,
type: 'PUT',
contentType: 'application/json',
data: JSON.stringify({
rust_id: install.rust_id,
computer_name: install.computer_name,
install_time: install.install_time,
folder_id: install.folder_id,
protocol: install.protocol,
note: note
}),
success: function () {
loadInstalls(selectedFolderId);
selectedInstallId = currentSelectedId;
closeNoteModal();
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
updateNotesPanel();
},
error: function (xhr, status, error) {
console.error("Ошибка сохранения заметки:", status, error);
alert(`Не удалось сохранить заметку: ${xhr.responseJSON?.detail || error}`);
}
});
}
}
function closeNoteModal() {
$('#note-modal').hide();
$('#modal-overlay').hide();
$('#note-textarea').val('');
document.getElementById('image-upload-input').value = '';
if (selectedInstallId) {
$(`#installs-list .install-item[data-id="${selectedInstallId}"]`).addClass('selected');
updateNotesPanel();
}
}
function updateNotesPanel() {
if (selectedInstallId) {
const install = allInstalls.find(i => i.id === selectedInstallId);
if (install && install.note) {
$('#notes-content').html(marked.parse(install.note));
} else {
$('#notes-content').html('<p>Нет заметок</p>');
}
} else {
$('#notes-content').html('<p>Выберите подключение для просмотра заметки</p>');
}
}
function updateFolderActions() {
$('.folder-actions').hide();
if (selectedFolderId) {
$(`#${selectedFolderId} .folder-actions`).show();
} else {
$('#root .folder-actions').show();
}
}
function updateFolderSelect() {
const $folderSelect = $('#folder-select');
$folderSelect.empty().append('<option value="">Все папки</option>');
allFolders.forEach(folder => {
$folderSelect.append(`<option value="${folder.id}">${folder.name}</option>`);
});
}
$('#installs-list').on('click', '.install-item', function () {
selectedInstallId = $(this).data('id');
$('.install-item').removeClass('selected');
$(this).addClass('selected');
updateNotesPanel();
});
function formatBold() {
const textarea = $('#note-textarea');
const start = textarea[0].selectionStart;
const end = textarea[0].selectionEnd;
const text = textarea.val();
const selected = text.substring(start, end);
const newText = `**${selected}**`;
textarea.val(text.substring(0, start) + newText + text.substring(end));
textarea[0].selectionStart = start;
textarea[0].selectionEnd = start + newText.length - 4;
}
function formatItalic() {
const textarea = $('#note-textarea');
const start = textarea[0].selectionStart;
const end = textarea[0].selectionEnd;
const text = textarea.val();
const selected = text.substring(start, end);
const newText = `*${selected}*`;
textarea.val(text.substring(0, start) + newText + text.substring(end));
textarea[0].selectionStart = start;
textarea[0].selectionEnd = start + newText.length - 2;
}
function formatLink() {
const textarea = $('#note-textarea');
const start = textarea[0].selectionStart;
const end = textarea[0].selectionEnd;
const text = textarea.val();
const selected = text.substring(start, end) || 'текст';
const url = prompt('Введите URL ссылки:', 'https://example.com');
if (url) {
const newText = `[${selected}](${url})`;
textarea.val(text.substring(0, start) + newText + text.substring(end));
textarea[0].selectionStart = start;
textarea[0].selectionEnd = start + newText.length - (selected ? 0 : '[текст]'.length);
}
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text).catch(err => {
console.error('Ошибка копирования: ', err);
});
}
function saveFolderPosition(folderId, newParentId) {
const folder = allFolders.find(f => f.id == folderId);
if (!folder) {
console.error(`Folder with id ${folderId} not found`);
return;
}
$.ajax({
url: `${API_URL}/folders/${folderId}`,
type: 'PUT',
contentType: 'application/json',
data: JSON.stringify({ name: folder.name, parent_id: newParentId }),
success: function () {
loadFolders();
$('#folder-tree').jstree(true).refresh();
},
error: function (xhr, status, error) {
console.error("Error saving folder position:", status, error);
console.error("Response:", xhr.responseText);
alert("Не удалось сохранить новое положение папки. Проверьте консоль.");
$('#folder-tree').jstree(true).refresh();
}
});
}
function startPeriodicSync() {
setInterval(() => {
$.getJSON(`${API_URL}/installs`, function (data) {
allInstalls = data;
displayInstalls(allInstalls.filter(i => !selectedFolderId || i.folder_id == selectedFolderId));
}).fail(function(jqxhr, textStatus, error) {
console.error("Error syncing installs:", textStatus, error);
});
}, 60000);
}
startPeriodicSync();
</script>
</body>
</html>