Files
mycode/password-manager/index.html
2026-02-04 12:13:56 +08:00

1062 lines
37 KiB
HTML
Raw Permalink 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="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>密码管理器</title>
<style>
/* 基本样式 - 更简洁的设计 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
h1 {
text-align: center;
color: #2d3748;
margin-bottom: 20px;
font-size: 24px;
}
h2 {
font-size: 16px;
color: #4a5568;
margin-bottom: 15px;
font-weight: 600;
}
/* 数据管理区域 - 缩小版 */
.import-export {
background-color: white;
border-radius: 6px;
padding: 10px 15px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
}
.import-export h2 {
margin-bottom: 0;
font-size: 14px;
}
.import-export > div {
gap: 8px;
}
/* 主内容区域 */
.main-content {
display: grid;
grid-template-columns: 220px 1fr;
gap: 20px;
margin-bottom: 20px;
}
/* 分类侧边栏 */
.categories {
background-color: white;
border-radius: 6px;
padding: 15px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.category-list {
list-style: none;
margin-bottom: 15px;
}
.category-item {
padding: 8px 10px;
margin-bottom: 6px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
}
.category-item:hover {
background-color: #f7fafc;
}
.category-item.active {
background-color: #3182ce;
color: white;
}
.category-actions {
display: flex;
gap: 4px;
}
.add-category {
display: flex;
gap: 8px;
}
/* 统一按钮样式 */
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
font-weight: 500;
}
/* 小按钮样式 - 统一所有小按钮 */
.btn-small, .btn-copy {
padding: 3px 6px;
font-size: 11px;
border: none;
border-radius: 3px;
cursor: pointer;
transition: all 0.2s;
font-weight: 500;
}
.btn-primary {
background-color: #3182ce;
color: white;
}
.btn-primary:hover {
background-color: #2c5282;
}
.btn-secondary {
background-color: #718096;
color: white;
}
.btn-secondary:hover {
background-color: #4a5568;
}
.btn-danger {
background-color: #e53e3e;
color: white;
}
.btn-danger:hover {
background-color: #c53030;
}
.btn-edit {
background-color: #ed8936;
color: white;
}
.btn-edit:hover {
background-color: #dd6b20;
}
.btn-delete {
background-color: #e53e3e;
color: white;
}
.btn-delete:hover {
background-color: #c53030;
}
.btn-copy {
background-color: #38a169;
color: white;
}
.btn-copy:hover {
background-color: #2f855a;
}
/* 密码列表 */
.passwords {
background-color: white;
border-radius: 6px;
padding: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.passwords-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.password-list {
list-style: none;
}
/* 密码项 - 更紧凑的设计 */
.password-item {
padding: 8px 10px;
margin-bottom: 8px;
border: 1px solid #e2e8f0;
border-radius: 5px;
transition: all 0.2s;
}
.password-item:hover {
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
border-color: #cbd5e0;
}
.password-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.password-title {
font-weight: 600;
font-size: 14px;
color: #2d3748;
}
.password-details {
margin-bottom: 0;
}
.password-row {
display: flex;
margin-bottom: 4px;
align-items: center;
font-size: 13px;
line-height: 1.4;
}
.password-row:last-child {
margin-bottom: 0;
}
.password-label {
width: 60px;
font-weight: 500;
color: #718096;
font-size: 12px;
}
.password-value {
flex: 1;
display: flex;
align-items: center;
gap: 6px;
color: #2d3748;
}
/* 密码输入框样式优化 */
.password-value input[type="password"] {
font-size: 13px;
padding: 0;
}
/* 表单样式 */
.form-section {
background-color: white;
border-radius: 6px;
padding: 15px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 12px;
}
.form-group {
margin-bottom: 12px;
}
.form-group label {
display: block;
margin-bottom: 4px;
font-weight: 500;
color: #4a5568;
font-size: 14px;
}
input, select, textarea {
width: 100%;
padding: 8px 10px;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.2s;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: #3182ce;
box-shadow: 0 0 0 1px rgba(49, 130, 206, 0.2);
}
input[type="password"] {
font-family: monospace;
}
/* 显示/隐藏密码按钮 */
.toggle-password {
background: none;
border: none;
cursor: pointer;
color: #3182ce;
font-size: 13px;
padding: 0;
}
/* 搜索框 */
.search-box {
margin-bottom: 15px;
}
/* 新的布局样式 */
.layout-grid {
display: grid;
grid-template-columns: 300px 1fr;
gap: 20px;
margin-bottom: 20px;
}
.left-panel {
display: flex;
flex-direction: column;
gap: 20px;
}
.right-panel {
display: flex;
flex-direction: column;
}
/* 更大的密码列表 - 带滚动功能 */
.passwords.large-list {
flex: 1;
min-height: 400px;
max-height: 600px;
overflow-y: auto;
}
/* 自定义滚动条样式 */
.passwords.large-list::-webkit-scrollbar {
width: 6px;
}
.passwords.large-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.passwords.large-list::-webkit-scrollbar-thumb {
background: #cbd5e0;
border-radius: 3px;
}
.passwords.large-list::-webkit-scrollbar-thumb:hover {
background: #a0aec0;
}
/* 更小的表单 */
.form-section.small-form {
padding: 12px;
}
.small-form .form-row {
grid-template-columns: 1fr;
gap: 8px;
margin-bottom: 8px;
}
.small-form .form-group {
margin-bottom: 8px;
}
.small-form input,
.small-form select,
.small-form textarea {
padding: 6px 8px;
font-size: 13px;
}
/* 复制提示样式 */
.copy-toast {
position: fixed;
top: 20px;
right: 20px;
background-color: #38a169;
color: white;
padding: 12px 20px;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
font-size: 14px;
font-weight: 500;
z-index: 1000;
opacity: 0;
transform: translateY(-20px);
transition: all 0.3s ease;
}
.copy-toast.show {
opacity: 1;
transform: translateY(0);
}
/* 响应式设计 */
@media (max-width: 768px) {
.layout-grid {
grid-template-columns: 1fr;
}
.form-row {
grid-template-columns: 1fr;
}
.passwords-header {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>密码管理器</h1>
<!-- 复制提示 -->
<div class="copy-toast" id="copyToast">已复制到剪贴板</div>
<!-- 导入/导出功能 -->
<div class="import-export" style="background-color: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); margin-bottom: 20px;">
<h2>数据管理</h2>
<div style="display: flex; gap: 10px;">
<button class="btn btn-primary" onclick="exportData()">导出数据</button>
<label class="btn btn-secondary" style="cursor: pointer;">
导入数据
<input type="file" id="importFile" accept=".json" onchange="importData()" style="display: none;" />
</label>
</div>
</div>
<!-- 分类管理区域 -->
<div class="category-manager" style="background-color: white; border-radius: 6px; padding: 10px 15px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center;">
<h2>分类管理</h2>
<div style="display: flex; gap: 10px; align-items: center;">
<!-- 分类选择下拉框 -->
<select id="categorySelect" onchange="switchCategory(this.value)" style="padding: 6px 8px; border: 1px solid #e2e8f0; border-radius: 4px; font-size: 14px;">
<!-- 分类选项将通过JavaScript动态生成 -->
</select>
<!-- 分类管理操作 -->
<div style="display: flex; gap: 8px; align-items: center;">
<input type="text" id="newCategoryName" placeholder="新分类" style="padding: 6px 8px; border: 1px solid #e2e8f0; border-radius: 4px; font-size: 14px; width: 120px;" />
<button class="btn btn-small btn-primary" onclick="addCategory()">添加</button>
<button class="btn btn-small btn-edit" onclick="editSelectedCategory()">编辑</button>
<button class="btn btn-small btn-delete" onclick="deleteSelectedCategory()">删除</button>
</div>
</div>
</div>
<!-- 主内容区域 - 调整为更大的密码列表 -->
<div class="layout-grid">
<!-- 左侧:仅表单 -->
<div class="left-panel">
<!-- 添加/编辑密码表单 - 更小的尺寸 -->
<div class="form-section small-form">
<h2 id="formTitle">添加密码</h2>
<form id="passwordForm" onsubmit="savePassword(event)">
<input type="hidden" id="passwordId" />
<div class="form-row">
<div class="form-group">
<label for="title">标题</label>
<input type="text" id="title" required />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="category">分类</label>
<select id="category" required>
<!-- 分类选项将通过JavaScript动态生成 -->
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" required />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="password">密码</label>
<div style="position: relative;">
<input type="password" id="password" required />
<button type="button" class="toggle-password" onclick="togglePassword()" style="position: absolute; right: 10px; top: 50%; transform: translateY(-50%);">
显示
</button>
</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="url">网址</label>
<input type="url" id="url" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="notes">备注</label>
<textarea id="notes" rows="2"></textarea>
</div>
</div>
<div style="display: flex; gap: 8px; margin-top: 10px;">
<button type="submit" class="btn btn-primary btn-small">保存</button>
<button type="button" class="btn btn-secondary btn-small" onclick="resetForm()">重置</button>
<button type="button" class="btn btn-danger btn-small" id="deletePasswordBtn" onclick="deletePassword()" style="display: none;">删除</button>
</div>
</form>
</div>
</div>
<!-- 右侧:密码列表 - 更大的尺寸 -->
<div class="right-panel">
<div class="passwords large-list">
<div class="passwords-header">
<h2>密码列表</h2>
<div class="search-box">
<input type="text" id="searchInput" placeholder="搜索密码..." oninput="searchPasswords()" />
</div>
</div>
<ul class="password-list" id="passwordList">
<!-- 密码项将通过JavaScript动态生成 -->
</ul>
</div>
</div>
</div>
</div>
<script>
// 数据存储
let categories = [
{ id: 'default', name: '默认分类' }
];
let passwords = [];
let currentCategory = 'default';
let editingPasswordId = null;
const DATA_FILE_NAME = 'passwords.json';
// DOM加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
loadData();
renderCategories();
renderPasswords();
});
// 加载数据
function loadData() {
try {
// 尝试从localStorage加载数据作为备选方案
const savedData = localStorage.getItem('passwordManagerData');
if (savedData) {
const parsedData = JSON.parse(savedData);
categories = parsedData.categories || categories;
passwords = parsedData.passwords || passwords;
console.log('从localStorage加载数据成功');
} else {
console.log('没有找到保存的数据,使用默认数据');
}
} catch (error) {
console.error('加载数据失败:', error);
}
}
// 保存数据到localStorage作为备选方案
function saveData() {
try {
const data = {
categories: categories,
passwords: passwords
};
localStorage.setItem('passwordManagerData', JSON.stringify(data));
console.log('数据已保存到localStorage');
} catch (error) {
console.error('保存数据失败:', error);
}
}
// 导出数据到JSON文件
function exportData() {
try {
const data = {
categories: categories,
passwords: passwords
};
const jsonString = JSON.stringify(data, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = DATA_FILE_NAME;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('数据已导出到文件:', DATA_FILE_NAME);
} catch (error) {
console.error('导出数据失败:', error);
alert('导出数据失败,请检查浏览器权限');
}
}
// 从JSON文件导入数据
function importData() {
try {
const fileInput = document.getElementById('importFile');
const file = fileInput.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const jsonString = e.target.result;
const data = JSON.parse(jsonString);
// 验证数据格式
if (Array.isArray(data.categories) && Array.isArray(data.passwords)) {
categories = data.categories;
passwords = data.passwords;
// 确保默认分类存在
if (!categories.some(c => c.id === 'default')) {
categories.push({ id: 'default', name: '默认分类' });
}
// 确保当前分类有效
if (!categories.some(c => c.id === currentCategory)) {
currentCategory = 'default';
}
renderCategories();
renderPasswords();
saveData(); // 同时保存到localStorage
console.log('数据已从文件导入');
alert('数据导入成功');
} else {
alert('无效的数据格式');
}
};
reader.readAsText(file);
// 重置文件输入
fileInput.value = '';
} catch (error) {
console.error('导入数据失败:', error);
alert('导入数据失败,请检查文件格式');
}
}
// 渲染分类列表和下拉框
function renderCategories() {
const categorySelect = document.getElementById('category');
const mainCategorySelect = document.getElementById('categorySelect');
// 清空现有内容
categorySelect.innerHTML = '';
mainCategorySelect.innerHTML = '';
// 渲染分类选项
categories.forEach(category => {
// 密码表单中的分类下拉选项
const option = document.createElement('option');
option.value = category.id;
option.textContent = category.name;
categorySelect.appendChild(option);
// 主分类选择下拉选项
const mainOption = document.createElement('option');
mainOption.value = category.id;
mainOption.textContent = category.name;
if (category.id === currentCategory) {
mainOption.selected = true;
}
mainCategorySelect.appendChild(mainOption);
});
}
// 渲染密码列表
function renderPasswords() {
const passwordList = document.getElementById('passwordList');
// 筛选当前分类的密码
let filteredPasswords = passwords.filter(password => password.category === currentCategory);
// 清空现有内容
passwordList.innerHTML = '';
// 渲染密码项
filteredPasswords.forEach(password => {
const li = document.createElement('li');
li.className = 'password-item';
li.innerHTML = `
<div class="password-header">
<div class="password-title">${password.title}</div>
<div class="password-actions">
<button class="btn-small btn-edit" onclick="editPassword('${password.id}')">编辑</button>
<button class="btn-small btn-delete" onclick="deletePassword('${password.id}')">删除</button>
</div>
</div>
<div class="password-details">
<div class="password-row">
<div class="password-label">用户名:</div>
<div class="password-value">
<span>${password.username}</span>
<button class="btn-copy" onclick="copyToClipboard('${password.username}', '用户名')">复制</button>
</div>
</div>
<div class="password-row">
<div class="password-label">密码:</div>
<div class="password-value">
<input type="password" value="${password.password}" readonly style="border: none; background: transparent;" />
<button class="toggle-password" onclick="togglePasswordVisibility(this)">显示</button>
<button class="btn-copy" onclick="copyToClipboard('${password.password}', '密码')">复制</button>
</div>
</div>
${password.url ? `
<div class="password-row">
<div class="password-label">网址:</div>
<div class="password-value">
<a href="${password.url}" target="_blank">${password.url}</a>
<button class="btn-copy" onclick="copyToClipboard('${password.url}', '网址')">复制</button>
</div>
</div>
` : ''}
${password.notes ? `
<div class="password-row">
<div class="password-label">备注:</div>
<div class="password-value">
<span>${password.notes}</span>
<button class="btn-copy" onclick="copyToClipboard('${password.notes}', '备注')">复制</button>
</div>
</div>
` : ''}
</div>
`;
passwordList.appendChild(li);
});
}
// 添加分类
function addCategory() {
const categoryName = document.getElementById('newCategoryName').value.trim();
if (categoryName) {
const newCategory = {
id: Date.now().toString(),
name: categoryName
};
categories.push(newCategory);
renderCategories();
saveData();
document.getElementById('newCategoryName').value = '';
}
}
// 编辑分类
function editCategory(categoryId) {
const category = categories.find(c => c.id === categoryId);
if (category) {
const newName = prompt('请输入新的分类名称:', category.name);
if (newName && newName.trim()) {
category.name = newName.trim();
renderCategories();
saveData();
}
}
}
// 删除分类
function deleteCategory(categoryId) {
if (categoryId === 'default') {
alert('默认分类不能删除!');
return;
}
if (confirm('确定要删除这个分类吗? 该分类下的所有密码也会被删除。')) {
categories = categories.filter(c => c.id !== categoryId);
passwords = passwords.filter(p => p.category !== categoryId);
// 如果当前分类被删除,切换到默认分类
if (currentCategory === categoryId) {
currentCategory = 'default';
}
renderCategories();
renderPasswords();
saveData();
}
}
// 编辑选中的分类
function editSelectedCategory() {
const select = document.getElementById('categorySelect');
const categoryId = select.value;
editCategory(categoryId);
}
// 删除选中的分类
function deleteSelectedCategory() {
const select = document.getElementById('categorySelect');
const categoryId = select.value;
deleteCategory(categoryId);
}
// 切换分类
function switchCategory(categoryId) {
currentCategory = categoryId;
renderCategories();
renderPasswords();
}
// 保存密码
function savePassword(event) {
event.preventDefault();
const id = document.getElementById('passwordId').value;
const title = document.getElementById('title').value;
const category = document.getElementById('category').value;
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const url = document.getElementById('url').value;
const notes = document.getElementById('notes').value;
if (id) {
// 编辑现有密码
const passwordIndex = passwords.findIndex(p => p.id === id);
if (passwordIndex !== -1) {
passwords[passwordIndex] = {
...passwords[passwordIndex],
title,
category,
username,
password,
url,
notes
};
}
} else {
// 添加新密码
const newPassword = {
id: Date.now().toString(),
title,
category,
username,
password,
url,
notes,
createdAt: new Date().toISOString()
};
passwords.push(newPassword);
}
// 如果添加的密码不属于当前显示的分类,切换到对应分类
const categorySelect = document.getElementById('category');
const selectedCategory = categorySelect.value;
if (currentCategory !== selectedCategory) {
switchCategory(selectedCategory);
} else {
renderPasswords();
}
saveData();
resetForm();
}
// 编辑密码
function editPassword(passwordId) {
const password = passwords.find(p => p.id === passwordId);
if (password) {
document.getElementById('passwordId').value = password.id;
document.getElementById('title').value = password.title;
document.getElementById('category').value = password.category;
document.getElementById('username').value = password.username;
document.getElementById('password').value = password.password;
document.getElementById('url').value = password.url;
document.getElementById('notes').value = password.notes;
document.getElementById('formTitle').textContent = '编辑密码';
document.getElementById('deletePasswordBtn').style.display = 'inline-block';
}
}
// 删除密码
function deletePassword(passwordId) {
const id = passwordId || document.getElementById('passwordId').value;
if (id) {
if (confirm('确定要删除这个密码吗?')) {
passwords = passwords.filter(p => p.id !== id);
renderPasswords();
saveData();
resetForm();
}
}
}
// 重置表单
function resetForm() {
document.getElementById('passwordForm').reset();
document.getElementById('passwordId').value = '';
document.getElementById('formTitle').textContent = '添加密码';
document.getElementById('deletePasswordBtn').style.display = 'none';
}
// 切换密码可见性
function togglePassword() {
const passwordInput = document.getElementById('password');
const toggleBtn = passwordInput.nextElementSibling;
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
toggleBtn.textContent = '隐藏';
} else {
passwordInput.type = 'password';
toggleBtn.textContent = '显示';
}
}
// 切换密码列表中的密码可见性
function togglePasswordVisibility(btn) {
const passwordInput = btn.previousElementSibling;
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
btn.textContent = '隐藏';
} else {
passwordInput.type = 'password';
btn.textContent = '显示';
}
}
// 搜索密码
function searchPasswords() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const passwordItems = document.querySelectorAll('.password-item');
passwordItems.forEach(item => {
const title = item.querySelector('.password-title').textContent.toLowerCase();
const username = item.querySelectorAll('.password-value')[0].textContent.toLowerCase();
const url = item.querySelectorAll('.password-value')[2]?.textContent.toLowerCase() || '';
if (title.includes(searchTerm) || username.includes(searchTerm) || url.includes(searchTerm)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
}
// 复制到剪贴板 - 支持多种浏览器环境
function copyToClipboard(text, type) {
// 尝试使用现代Clipboard API
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text)
.then(() => {
showCopySuccess(event.target);
})
.catch(err => {
console.error('Clipboard API失败:', err);
// 失败时回退到传统方法
fallbackCopyTextToClipboard(text, event.target);
});
} else {
// 使用传统方法
fallbackCopyTextToClipboard(text, event.target);
}
}
// 传统复制方法(备选方案)
function fallbackCopyTextToClipboard(text, btn) {
try {
// 创建临时输入元素
const textArea = document.createElement('textarea');
textArea.value = text;
// 确保元素不在可视区域
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
// 执行复制命令
const successful = document.execCommand('copy');
if (successful) {
showCopySuccess(btn);
} else {
throw new Error('execCommand失败');
}
} catch (err) {
console.error('传统复制方法失败:', err);
alert('复制失败,请手动复制');
} finally {
// 清理临时元素
const textArea = document.querySelector('textarea[style*="-999999px"]');
if (textArea) {
document.body.removeChild(textArea);
}
}
}
// 显示复制成功提示
function showCopySuccess(btn) {
// 更新按钮状态
const originalText = btn.textContent;
btn.textContent = '已复制';
btn.style.backgroundColor = '#2f855a';
setTimeout(() => {
btn.textContent = originalText;
btn.style.backgroundColor = '#38a169';
}, 1000);
// 显示Toast提示
const toast = document.getElementById('copyToast');
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 2000);
}
</script>
</body>
</html>