320 lines
15 KiB
HTML
320 lines
15 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Менеджер паролей</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<style>
|
||
body {
|
||
background: linear-gradient(to bottom, #fff5f5, #ffffff);
|
||
}
|
||
.password-cell {
|
||
-webkit-text-security: disc; /* Скрываем пароли по умолчанию */
|
||
font-family: monospace;
|
||
}
|
||
.reveal-btn {
|
||
cursor: pointer;
|
||
margin-left: 8px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="min-h-screen font-sans">
|
||
<!-- Main Section -->
|
||
<section class="container mx-auto px-4 py-12 max-w-3xl">
|
||
<h1 class="text-4xl font-bold text-red-600 mb-4">Менеджер паролей</h1>
|
||
<p class="text-gray-600 mb-8">Введите свои данные в формате: Сервис Логин Пароль. Пароли сохраняются автоматически.</p>
|
||
|
||
<div class="bg-white p-6 rounded-3xl shadow-lg">
|
||
<textarea id="inputData" class="w-full p-4 border rounded-2xl mb-4 bg-gray-50 focus:outline-none focus:ring-2 focus:ring-red-400 text-sm resize-y" rows="5" placeholder="Введите данные в формате: Сервис Логин Пароль (каждая запись с новой строки)"></textarea>
|
||
<div class="flex gap-2">
|
||
<button onclick="parsePasswords()" class="flex-1 bg-red-500 text-white p-3 rounded-2xl hover:bg-red-600 transition duration-300 font-semibold">Добавить</button>
|
||
<button onclick="clearAllPasswords()" class="bg-gray-200 text-gray-700 p-3 rounded-2xl hover:bg-gray-300 transition duration-300 font-semibold">Очистить всё</button>
|
||
<button onclick="exportPasswords()" class="bg-green-500 text-white p-3 rounded-2xl hover:bg-green-600 transition duration-300 font-semibold">Экспорт</button>
|
||
<button onclick="importPasswords()" class="bg-blue-500 text-white p-3 rounded-2xl hover:bg-blue-600 transition duration-300 font-semibold">Импорт</button>
|
||
</div>
|
||
<div id="output" class="mt-6"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<script>
|
||
let tableData = [];
|
||
|
||
// Load existing passwords from local storage on page load
|
||
window.onload = function() {
|
||
loadPasswords();
|
||
};
|
||
|
||
function loadPasswords() {
|
||
const savedData = localStorage.getItem('passwords');
|
||
if (savedData) {
|
||
try {
|
||
tableData = JSON.parse(savedData);
|
||
renderTable();
|
||
} catch (e) {
|
||
console.error("Ошибка загрузки паролей:", e);
|
||
alert("Ошибка загрузки сохранённых паролей");
|
||
}
|
||
}
|
||
}
|
||
|
||
function savePasswords() {
|
||
localStorage.setItem('passwords', JSON.stringify(tableData));
|
||
}
|
||
|
||
function parsePasswords() {
|
||
const input = document.getElementById('inputData').value.trim();
|
||
const output = document.getElementById('output');
|
||
output.innerHTML = '';
|
||
|
||
if (!input) {
|
||
renderTable();
|
||
return;
|
||
}
|
||
|
||
const lines = input.split('\n');
|
||
let addedCount = 0;
|
||
|
||
lines.forEach(line => {
|
||
const trimmedLine = line.trim();
|
||
if (!trimmedLine) return;
|
||
|
||
// Поддержка разделителей: пробел, табуляция или запятая
|
||
const parts = trimmedLine.split(/[\s\t,]+/).filter(part => part);
|
||
|
||
if (parts.length >= 3) {
|
||
const password = parts.pop();
|
||
const login = parts.pop();
|
||
const service = parts.join(' ');
|
||
|
||
if (service && login && password) {
|
||
// Проверка на дубликаты
|
||
const isDuplicate = tableData.some(item =>
|
||
item.service === service && item.login === login
|
||
);
|
||
|
||
if (!isDuplicate) {
|
||
tableData.push({ service, login, password });
|
||
addedCount++;
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
if (addedCount > 0) {
|
||
savePasswords();
|
||
document.getElementById('inputData').value = '';
|
||
renderTable();
|
||
alert(`Добавлено ${addedCount} новых записей`);
|
||
} else {
|
||
alert("Не удалось добавить новые записи. Проверьте формат ввода или возможно записи уже существуют.");
|
||
}
|
||
}
|
||
|
||
function clearAllPasswords() {
|
||
if (confirm("Вы уверены, что хотите удалить все сохранённые пароли? Это действие нельзя отменить.")) {
|
||
tableData = [];
|
||
savePasswords();
|
||
renderTable();
|
||
}
|
||
}
|
||
|
||
function exportPasswords() {
|
||
const dataStr = JSON.stringify(tableData, null, 2);
|
||
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
|
||
|
||
const exportFileDefaultName = 'passwords-backup.json';
|
||
|
||
const linkElement = document.createElement('a');
|
||
linkElement.setAttribute('href', dataUri);
|
||
linkElement.setAttribute('download', exportFileDefaultName);
|
||
linkElement.click();
|
||
}
|
||
|
||
function importPasswords() {
|
||
const fileInput = document.createElement('input');
|
||
fileInput.type = 'file';
|
||
fileInput.accept = '.json';
|
||
|
||
fileInput.onchange = e => {
|
||
const file = e.target.files[0];
|
||
if (!file) return;
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = event => {
|
||
try {
|
||
const importedData = JSON.parse(event.target.result);
|
||
if (Array.isArray(importedData) && importedData.length > 0) {
|
||
if (confirm(`Найдено ${importedData.length} записей. Добавить их к существующим?`)) {
|
||
tableData = [...tableData, ...importedData];
|
||
savePasswords();
|
||
renderTable();
|
||
alert(`Импортировано ${importedData.length} записей`);
|
||
}
|
||
} else {
|
||
alert("Файл не содержит данных для импорта");
|
||
}
|
||
} catch (error) {
|
||
alert("Ошибка при чтении файла. Убедитесь, что выбран правильный файл.");
|
||
console.error(error);
|
||
}
|
||
};
|
||
reader.readAsText(file);
|
||
};
|
||
|
||
fileInput.click();
|
||
}
|
||
|
||
function renderTable() {
|
||
const output = document.getElementById('output');
|
||
output.innerHTML = '';
|
||
|
||
if (tableData.length === 0) {
|
||
output.innerHTML = '<p class="text-red-500 text-center font-medium">Нет сохранённых паролей. Введите данные в поле выше.</p>';
|
||
return;
|
||
}
|
||
|
||
// Создаем контейнер для таблицы и поиска
|
||
const tableContainer = document.createElement('div');
|
||
tableContainer.className = 'overflow-x-auto';
|
||
|
||
// Добавляем поле поиска
|
||
const searchDiv = document.createElement('div');
|
||
searchDiv.className = 'mb-4';
|
||
searchDiv.innerHTML = `
|
||
<input type="text" id="searchInput" placeholder="Поиск по сервису или логину..."
|
||
class="w-full p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-red-300">
|
||
<div class="text-sm text-gray-500 mt-1">Найдено записей: <span id="matchCount">${tableData.length}</span></div>
|
||
`;
|
||
tableContainer.appendChild(searchDiv);
|
||
|
||
const searchInput = searchDiv.querySelector('#searchInput');
|
||
searchInput.addEventListener('input', () => filterTable(searchInput.value.toLowerCase()));
|
||
|
||
const table = document.createElement('table');
|
||
table.className = 'w-full border-collapse rounded-2xl overflow-hidden shadow-md';
|
||
|
||
let header = table.insertRow();
|
||
const headers = ['Сервис', 'Логин', 'Пароль', 'Действия'];
|
||
let sortDirection = { 0: 1, 1: 1, 2: 1 }; // 1 for ascending, -1 for descending
|
||
|
||
headers.forEach((text, index) => {
|
||
const th = document.createElement('th');
|
||
th.textContent = text;
|
||
th.className = 'border border-gray-200 p-4 bg-red-50 text-gray-700 font-semibold text-left cursor-pointer';
|
||
if (index < 3) {
|
||
th.addEventListener('click', () => {
|
||
sortTable(index, sortDirection[index]);
|
||
sortDirection[index] *= -1;
|
||
renderTable();
|
||
});
|
||
}
|
||
header.appendChild(th);
|
||
});
|
||
|
||
tableData.forEach((row, rowIndex) => {
|
||
const tableRow = table.insertRow();
|
||
tableRow.className = 'hover:bg-gray-50';
|
||
tableRow.dataset.service = row.service.toLowerCase();
|
||
tableRow.dataset.login = row.login.toLowerCase();
|
||
|
||
// Ячейка сервиса
|
||
const serviceCell = tableRow.insertCell();
|
||
serviceCell.className = 'border border-gray-200 p-4 bg-white text-sm break-words max-w-xs';
|
||
serviceCell.textContent = row.service;
|
||
|
||
// Ячейка логина
|
||
const loginCell = tableRow.insertCell();
|
||
loginCell.className = 'border border-gray-200 p-4 bg-white text-sm break-words max-w-xs';
|
||
loginCell.textContent = row.login;
|
||
|
||
// Ячейка пароля
|
||
const passwordCell = tableRow.insertCell();
|
||
passwordCell.className = 'border border-gray-200 p-4 bg-white text-sm break-words max-w-xs password-cell';
|
||
passwordCell.textContent = '••••••••';
|
||
passwordCell.dataset.password = row.password;
|
||
|
||
// Ячейка действий
|
||
const actionsCell = tableRow.insertCell();
|
||
actionsCell.className = 'border border-gray-200 p-4 bg-white text-sm flex gap-2';
|
||
|
||
// Кнопка показать/скрыть пароль
|
||
const toggleBtn = document.createElement('button');
|
||
toggleBtn.textContent = '👁️';
|
||
toggleBtn.className = 'reveal-btn text-blue-500 hover:text-blue-700';
|
||
toggleBtn.onclick = () => {
|
||
if (passwordCell.classList.contains('password-cell')) {
|
||
passwordCell.textContent = passwordCell.dataset.password;
|
||
passwordCell.classList.remove('password-cell');
|
||
toggleBtn.textContent = '🙈';
|
||
} else {
|
||
passwordCell.textContent = '••••••••';
|
||
passwordCell.classList.add('password-cell');
|
||
toggleBtn.textContent = '👁️';
|
||
}
|
||
};
|
||
|
||
// Кнопка копирования пароля
|
||
const copyBtn = document.createElement('button');
|
||
copyBtn.textContent = '📋';
|
||
copyBtn.className = 'text-blue-500 hover:text-blue-700';
|
||
copyBtn.onclick = () => {
|
||
navigator.clipboard.writeText(row.password).then(() => {
|
||
const originalText = copyBtn.textContent;
|
||
copyBtn.textContent = '✓';
|
||
setTimeout(() => copyBtn.textContent = originalText, 2000);
|
||
});
|
||
};
|
||
|
||
// Кнопка удаления
|
||
const deleteBtn = document.createElement('button');
|
||
deleteBtn.textContent = '🗑️';
|
||
deleteBtn.className = 'text-red-500 hover:text-red-700';
|
||
deleteBtn.onclick = () => {
|
||
if (confirm(`Удалить запись для ${row.service}?`)) {
|
||
tableData.splice(rowIndex, 1);
|
||
savePasswords();
|
||
renderTable();
|
||
}
|
||
};
|
||
|
||
actionsCell.appendChild(toggleBtn);
|
||
actionsCell.appendChild(copyBtn);
|
||
actionsCell.appendChild(deleteBtn);
|
||
});
|
||
|
||
tableContainer.appendChild(table);
|
||
output.appendChild(tableContainer);
|
||
}
|
||
|
||
function filterTable(searchTerm) {
|
||
const rows = document.querySelectorAll('tbody tr');
|
||
let matchCount = 0;
|
||
|
||
rows.forEach(row => {
|
||
const service = row.dataset.service;
|
||
const login = row.dataset.login;
|
||
const isVisible = service.includes(searchTerm) || login.includes(searchTerm);
|
||
|
||
row.style.display = isVisible ? '' : 'none';
|
||
if (isVisible) matchCount++;
|
||
});
|
||
|
||
document.getElementById('matchCount').textContent = matchCount;
|
||
}
|
||
|
||
function sortTable(columnIndex, direction) {
|
||
const columns = ['service', 'login', 'password'];
|
||
const key = columns[columnIndex];
|
||
|
||
tableData.sort((a, b) => {
|
||
const valueA = a[key].toLowerCase();
|
||
const valueB = b[key].toLowerCase();
|
||
return direction * valueA.localeCompare(valueB);
|
||
});
|
||
|
||
savePasswords();
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |