Password-Manager/index.html
2025-05-08 23:07:03 +00:00

320 lines
15 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>Менеджер паролей</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>