好友
阅读权限10
听众
最后登录1970-1-1
|
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>密码管理系统 | SQLite持久化存储</title>
<!-- SQL.js (WebAssembly 版 SQLite) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/sql-wasm.js"></script>
<!-- SheetJS (XLS 导入导出) -->
<script src="https://cdn.sheetjs.com/xlsx-0.20.2/package/dist/xlsx.full.min.js"></script>
<!-- CryptoJS 用于 SHA-256 哈希加密 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', system-ui; background: #f1f5f9; padding: 24px 20px; }
.app-container { max-width: 1400px; margin: 0 auto; }
.card { background: white; border-radius: 28px; border: 1px solid #e9edf2; overflow: hidden; margin-bottom: 24px; box-shadow: 0 8px 20px rgba(0,0,0,0.02); }
.card-header { padding: 18px 24px; border-bottom: 1px solid #edf2f7; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; background: #fefefe; }
.card-header h2 { font-size: 1.4rem; font-weight: 600; color: #0f2b4f; }
.btn { border: none; background: #f1f5f9; padding: 8px 20px; border-radius: 40px; font-weight: 500; cursor: pointer; transition: 0.2s; font-size: 0.85rem; }
.btn-primary { background: #1e3c72; color: white; }
.btn-primary:hover { background: #0f2b4f; transform: translateY(-1px); }
.btn-outline { background: white; border: 1px solid #cbd5e1; }
.btn-success { background: #10b981; color: white; }
.btn-warning { background: #f59e0b; color: white; }
.btn-danger { background: #fff0f0; color: #b91c1c; border: 1px solid #ffd9d9; }
.table-wrapper { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; table-layout: fixed; min-width: 800px; }
th, td { text-align: left; padding: 14px 16px; border-bottom: 1px solid #ecf3fa; vertical-align: middle; }
th { background: #fafcff; font-weight: 600; color: #1e293b; }
/* 调整列宽:网站20%,描述18%,用户名18%,密码26%,操作18%(足够两个按钮并排) */
th:nth-child(1), td:nth-child(1) { width: 20%; }
th:nth-child(2), td:nth-child(2) { width: 18%; }
th:nth-child(3), td:nth-child(3) { width: 18%; }
th:nth-child(4), td:nth-child(4) { width: 26%; }
th:nth-child(5), td:nth-child(5) { width: 18%; white-space: nowrap; }
.url-link { color: #2563eb; text-decoration: none; font-weight: 500; word-break: break-all; }
.url-link:hover { text-decoration: underline; }
.password-plain { font-family: monospace; background: #eef2ff; padding: 4px 12px; border-radius: 30px; display: inline-block; font-size: 0.85rem; word-break: break-all; max-width: 100%; }
.desc-cell { white-space: nowrap; overflow-x: auto; max-width: 100%; color: #4b5563; line-height: 1.4; }
.desc-cell::-webkit-scrollbar { height: 4px; }
.input-group { margin-bottom: 16px; display: flex; flex-direction: column; gap: 6px; }
input, textarea { padding: 10px 14px; border-radius: 20px; border: 1px solid #cfdfed; font-family: inherit; font-size: 0.9rem; }
textarea { resize: vertical; min-height: 60px; }
input:focus, textarea:focus { outline: none; border-color: #1e3c72; box-shadow: 0 0 0 2px rgba(30,60,114,0.1); }
.modal { display: none; position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.5); align-items: center; justify-content: center; z-index: 1000; }
.modal-content { background: white; max-width: 500px; width: 90%; border-radius: 32px; padding: 28px; box-shadow: 0 20px 35px rgba(0,0,0,0.2); }
.toast-msg { position: fixed; bottom: 24px; right: 24px; background: #1f2937; color: white; padding: 10px 20px; border-radius: 40px; opacity: 0; transition: 0.2s; z-index: 1100; pointer-events: none; font-size: 0.85rem; }
.toast-msg.show { opacity: 1; }
.empty-row td { text-align: center; padding: 48px; color: #6c757d; }
.hidden { display: none; }
.flex-btns { display: flex; gap: 8px; align-items: center; }
/* 操作列内部按钮强制水平排列不换行(桌面端) */
td .flex-btns { flex-wrap: nowrap; white-space: nowrap; }
.admin-auth-overlay, .change-pwd-overlay, .setup-pwd-overlay { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.8); backdrop-filter: blur(5px); display: flex; align-items: center; justify-content: center; z-index: 2000; }
.admin-auth-box, .change-pwd-box, .setup-pwd-box { background: white; border-radius: 32px; padding: 32px; width: 90%; max-width: 400px; text-align: center; box-shadow: 0 25px 40px rgba(0,0,0,0.3); }
.admin-auth-box input, .change-pwd-box input, .setup-pwd-box input { width: 100%; margin: 12px 0; }
.error-text { color: #b91c1c; font-size: 0.8rem; margin-top: 8px; }
.file-input-label { background: #f1f5f9; border: 1px solid #cbd5e1; padding: 8px 18px; border-radius: 40px; cursor: pointer; font-size: 0.85rem; transition: 0.2s; }
.file-input-label:hover { background: #e2e8f0; }
input[type="file"] { display: none; }
.settings-group { background: #f8fafc; border-radius: 20px; padding: 16px 20px; margin: 12px 24px 20px 24px; border: 1px solid #e2e8f0; }
.settings-group h4 { margin-bottom: 12px; color: #1e293b; }
.settings-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; }
.export-db-btn { background: #475569; color: white; }
.export-db-btn:hover { background: #334155; }
.badge-info { background: #e6f0ff; border-radius: 20px; padding: 4px 12px; font-size: 0.7rem; color: #1e3c72; }
@media (max-width: 768px) {
td .flex-btns { flex-wrap: wrap; white-space: normal; }
.desc-cell { white-space: normal; word-break: break-word; }
th:nth-child(5), td:nth-child(5) { white-space: normal; }
}
</style>
</head>
<body>
<div class="app-container">
<div id="adminPanel" class="tab-panel">
<div class="card">
<div class="card-header">
<h2>📂 凭证管理</h2>
<div class="flex-btns">
<button class="btn btn-primary" id="openAddCredBtn">+ 新增凭证</button>
<button class="btn btn-success" id="exportExcelBtn">📎 导出为 XLS</button>
<label class="file-input-label" for="importExcelInput">📂 导入 XLS 文件</label>
<input type="file" id="importExcelInput" accept=".xls, .xlsx">
<button class="btn export-db-btn" id="exportDbBtn">💾 备份数据库</button>
</div>
</div>
<div class="table-wrapper">
<table id="adminTable">
<thead>
<tr>
<th>网站/系统</th>
<th>描述</th>
<th>用户名</th>
<th>密码 (明文)</th>
<th>操作</th>
</tr>
</thead>
<tbody id="adminTableBody"><tr class="empty-row"><td colspan="5">🔒 请先验证管理员权限</td></tr>
</tbody>
</table>
</div>
<div class="settings-group">
<h4>⚙️ 系统安全设置 (数据持久化存储于SQLite)</h4>
<div class="settings-row">
<span style="width: 140px;">🛡️ 管理员登录密码:</span>
<button class="btn btn-warning" id="changeAdminPwdBtn">修改管理员密码</button>
<span class="badge-info">SHA-256加密存储 | 修改前需验证原密码</span>
</div>
<div class="settings-row">
<span style="width: 140px;"></span>
<span style="font-size: 0.7rem; color: #e07c3c;">⚠️ 修改密码后立即保存至数据库,刷新不丢失</span>
</div>
</div>
<div class="card-header" style="border-top:1px solid #edf2f7;"><h2>📌 说明</h2></div>
<div style="padding: 16px 24px; color: #334155;">
• 支持 Excel (.xls, .xlsx) 导入导出,导入时自动匹配“网址/描述/用户名/密码”列(描述列可选)<br>
• 所有数据持久化存储在浏览器 SQLite 数据库中,刷新页面不会丢失<br>
• 可点击「备份数据库」下载完整数据库文件 (.db)<br>
• 首次使用请设置管理员密码,请务必牢记
</div>
</div>
</div>
</div>
<!-- 凭证编辑模态框(增加描述字段) -->
<div id="credModal" class="modal">
<div class="modal-content">
<h3 id="modalTitle">➕ 添加凭证</h3>
<div class="input-group"><label>🔗 网址 (URL)</label><input type="text" id="credUrl" placeholder="https://..."></div>
<div class="input-group"><label>📝 描述 (选填)</label><textarea id="credDescription" rows="2" placeholder="例如:公司邮箱、内部系统等"></textarea></div>
<div class="input-group"><label>👤 用户名</label><input type="text" id="credUsername" placeholder="账号"></div>
<div class="input-group"><label>🔐 密码</label><input type="text" id="credPassword" placeholder="密码"></div>
<input type="hidden" id="editId" value="">
<div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px;">
<button class="btn btn-outline" id="closeModalBtn">取消</button>
<button class="btn btn-primary" id="saveCredBtn">保存</button>
</div>
</div>
</div>
<div id="toastMsg" class="toast-msg">✅ 操作成功</div>
<script>
// ==================== SHA-256 哈希 ====================
function hashPassword(plain) { return CryptoJS.SHA256(plain).toString(); }
// ==================== 全局变量 ====================
let db = null;
let SQL = null;
let adminAuthenticated = false;
let adminAuthActive = false;
let currentAdminPwdHash = "";
const DB_STORAGE_KEY = 'password_manager_sqlite_db';
// ==================== 数据库持久化操作 ====================
function saveDatabaseToLocalStorage() {
if (!db) return;
try {
const exportedData = db.export();
const buffer = new Uint8Array(exportedData);
let binary = '';
for (let i = 0; i < buffer.byteLength; i++) binary += String.fromCharCode(buffer);
const base64 = btoa(binary);
localStorage.setItem(DB_STORAGE_KEY, base64);
} catch(e) { console.error("保存数据库失败:", e); }
}
async function loadDatabaseFromLocalStorage() {
const saved = localStorage.getItem(DB_STORAGE_KEY);
if (saved) {
try {
const binary = atob(saved);
const buffer = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) buffer = binary.charCodeAt(i);
db = new SQL.Database(buffer);
return true;
} catch(e) { console.error("恢复数据库失败", e); return false; }
}
return false;
}
// 升级表结构:增加 description 列(如果不存在)
function upgradeDatabaseSchema() {
if (!db) return;
try {
const tableInfo = db.exec("PRAGMA table_info(credentials)");
if (tableInfo.length > 0) {
const columns = tableInfo[0].values.map(row => row[1]);
if (!columns.includes('description')) {
db.run("ALTER TABLE credentials ADD COLUMN description TEXT DEFAULT ''");
console.log("已添加 description 列");
saveDatabaseToLocalStorage();
}
}
} catch(e) {
console.warn("升级表结构失败", e);
}
}
function createNewDatabase() {
db = new SQL.Database();
db.run(`CREATE TABLE IF NOT EXISTS credentials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL UNIQUE,
description TEXT DEFAULT '',
username TEXT NOT NULL,
password TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
db.run(`CREATE TABLE IF NOT EXISTS system_config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)`);
// 示例凭证数据(带描述)
const demo = [
['https://mail.company.com', '公司企业邮箱 - 日常通讯', 'zhang.san@company.com', 'Mail@2024Secure'],
['https://hrsystem.company.com', '人力资源系统 - 考勤与薪酬', 'hr_admin', 'HrP@ss#2024'],
['https://gitlab.company.com', '代码仓库 - 研发中心', 'dev_lihua', 'GitLab!7890'],
['https://jira.company.com', '项目管理 - 任务跟踪', 'project_manager', 'Jira@2025']
];
const stmt = db.prepare("INSERT INTO credentials (url, description, username, password) VALUES (?, ?, ?, ?)");
for(let row of demo) stmt.run(row);
stmt.free();
// 不插入默认管理员密码,首次使用需自行设置
saveDatabaseToLocalStorage();
}
// 检查是否已设置管理员密码
function isAdminPasswordSet() {
if (!db) return false;
const res = db.exec("SELECT value FROM system_config WHERE key = 'admin_login_password_hash'");
return (res.length && res[0].values.length && res[0].values[0][0]);
}
// 设置初始管理员密码
function setInitialAdminPassword(plainPwd) {
if (!db) return false;
if (!plainPwd || plainPwd.trim().length < 4) return false;
const newHash = hashPassword(plainPwd.trim());
try {
db.run("INSERT OR REPLACE INTO system_config (key, value) VALUES ('admin_login_password_hash', ?)", [newHash]);
saveDatabaseToLocalStorage();
refreshPasswordHashes();
return true;
} catch(e) { return false; }
}
async function initDatabase() {
return new Promise((resolve, reject) => {
if (typeof window.initSqlJs !== 'undefined') {
window.initSqlJs({ locateFile: () => "https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/sql-wasm.wasm" })
.then(async function(SQLite) {
SQL = SQLite;
const restored = await loadDatabaseFromLocalStorage();
if (!restored) createNewDatabase();
else upgradeDatabaseSchema();
refreshPasswordHashes();
resolve();
})
.catch(err => reject(err));
} else { reject("sql.js not loaded"); }
});
}
function getConfigValue(key) {
if(!db) return null;
let res = db.exec(`SELECT value FROM system_config WHERE key = ?`, [key]);
if(res.length && res[0].values.length) return res[0].values[0][0];
return null;
}
function setConfigValue(key, value) {
if(!db) return false;
try {
db.run(`INSERT OR REPLACE INTO system_config (key, value) VALUES (?, ?)`, [key, value]);
saveDatabaseToLocalStorage();
return true;
} catch(e) { return false; }
}
function refreshPasswordHashes() {
let adminHash = getConfigValue('admin_login_password_hash');
currentAdminPwdHash = adminHash ? adminHash : "";
}
function performAdminPasswordChange(newPlainPwd) {
if(!adminAuthenticated) { showToast("请先登录管理员后台", true); return false; }
if(!newPlainPwd || newPlainPwd.trim().length < 4) { showToast("新密码长度至少4位", true); return false; }
const newHash = hashPassword(newPlainPwd.trim());
if(setConfigValue('admin_login_password_hash', newHash)) {
refreshPasswordHashes();
showToast("管理员密码已修改,下次登录请使用新密码");
return true;
}
showToast("修改失败", true);
return false;
}
function verifyAdminPassword(plainPwd) {
if (!currentAdminPwdHash) return false;
return hashPassword(plainPwd) === currentAdminPwdHash;
}
// ==================== 凭证数据操作(含描述) ====================
function getAllCredentials() {
if(!db) return [];
const result = db.exec("SELECT id, url, description, username, password FROM credentials ORDER BY id DESC");
if(result.length === 0) return [];
return result[0].values.map(row => ({
id: row[0],
url: row[1],
description: row[2] || '',
username: row[3],
password: row[4]
}));
}
function isUrlUnique(url, excludeId = null) {
let query = "SELECT id FROM credentials WHERE url = ?";
let params = [url];
if(excludeId) { query += " AND id != ?"; params.push(excludeId); }
const res = db.exec(query, params);
return !(res.length && res[0].values.length);
}
function upsertCredential(id, url, description, username, password) {
if(!db) return false;
url = url.trim();
description = (description || '').trim();
username = username.trim();
password = password.trim();
if(!url.startsWith('http://') && !url.startsWith('https://')) { showToast("网址需以 http:// 或 https:// 开头", true); return false; }
if(id) {
if(!isUrlUnique(url, id)) { showToast("该网址已存在其他凭证,请修改网址", true); return false; }
try {
db.run("UPDATE credentials SET url = ?, description = ?, username = ?, password = ? WHERE id = ?",
[url, description, username, password, id]);
saveDatabaseToLocalStorage();
return true;
} catch(e) { console.error(e); showToast("更新失败", true); return false; }
} else {
if(!isUrlUnique(url, null)) { showToast("该网址已存在,请使用编辑功能或更换网址", true); return false; }
try {
db.run("INSERT INTO credentials (url, description, username, password) VALUES (?, ?, ?, ?)",
[url, description, username, password]);
saveDatabaseToLocalStorage();
return true;
} catch(e) { console.error(e); showToast("添加失败", true); return false; }
}
}
function deleteCredentialById(id) {
if(!db) return false;
try { db.run("DELETE FROM credentials WHERE id = ?", [id]); saveDatabaseToLocalStorage(); return true; }
catch(e) { return false; }
}
function batchUpsertCredentials(records) {
if(!db) return 0;
let successCount = 0;
for(let rec of records) {
if(rec.url && rec.username && rec.password && (rec.url.startsWith('http://') || rec.url.startsWith('https://'))) {
try {
const description = rec.description ? rec.description.trim() : '';
if(isUrlUnique(rec.url, null)) {
db.run("INSERT INTO credentials (url, description, username, password) VALUES (?, ?, ?, ?)",
[rec.url, description, rec.username, rec.password]);
successCount++;
} else {
db.run("UPDATE credentials SET description = ?, username = ?, password = ? WHERE url = ?",
[description, rec.username, rec.password, rec.url]);
successCount++;
}
} catch(e) {}
}
}
saveDatabaseToLocalStorage();
return successCount;
}
function exportDatabase() {
if(!adminAuthenticated) { showToast("请先登录管理员后台", true); return; }
if(!db) { showToast("数据库未就绪", true); return; }
const exportedData = db.export();
const blob = new Blob([exportedData], {type: "application/x-sqlite3"});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `pwd_manager_backup_${new Date().toISOString().slice(0,19).replace(/:/g, '-')}.db`;
a.click();
URL.revokeObjectURL(url);
showToast("数据库备份成功");
}
// ==================== 管理员后台渲染(含描述列) ====================
function renderAdminTable() {
const tbody = document.getElementById('adminTableBody');
if(!adminAuthenticated) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="5">🔒 请先验证管理员权限</td></tr>';
return;
}
const creds = getAllCredentials();
if(!creds.length) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="5">暂无凭证,点击"新增凭证"添加</td></tr>';
return;
}
let html = '';
creds.forEach(item => {
html += `<tr>
<td><a href="${escapeHtml(item.url)}" target="_blank" class="url-link">${escapeHtml(item.url)}</a></td>
<td class="desc-cell" title="${escapeHtml(item.description)}">${escapeHtml(item.description) || '—'}</td>
<td>${escapeHtml(item.username)}</td>
<td><span class="password-plain">${escapeHtml(item.password)}</span></td>
<td class="flex-btns">
<button class="btn btn-outline edit-admin-btn" data-id="${item.id}">✏️ 编辑</button>
<button class="btn btn-danger delete-admin-btn" data-id="${item.id}">🗑️ 删除</button>
</td>
</tr>`;
});
tbody.innerHTML = html;
document.querySelectorAll('.edit-admin-btn').forEach(btn => btn.addEventListener('click', () => openEditById(parseInt(btn.dataset.id))));
document.querySelectorAll('.delete-admin-btn').forEach(btn => btn.addEventListener('click', () => {
if(confirm('确定删除该凭证吗?')) { deleteCredentialById(parseInt(btn.dataset.id)); renderAdminTable(); showToast("已删除"); }
}));
}
function openEditById(id) {
const creds = getAllCredentials();
const entry = creds.find(c => c.id === id);
if(entry) openModalForEdit(entry);
}
// ==================== Excel 导入导出(支持描述列) ====================
function exportToExcel() {
if(!adminAuthenticated) { showToast("请先登录管理员后台", true); return; }
const creds = getAllCredentials();
if(creds.length === 0) { showToast("没有数据可导出", true); return; }
const sheetData = [["网址", "描述", "用户名", "密码"]];
creds.forEach(item => sheetData.push([item.url, item.description || '', item.username, item.password]));
const ws = XLSX.utils.aoa_to_sheet(sheetData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Credentials");
XLSX.writeFile(wb, `password_export_${new Date().toISOString().slice(0,19).replace(/:/g, '-')}.xlsx`);
showToast("导出成功");
}
function importExcel(file) {
if(!adminAuthenticated) { showToast("请先登录管理员后台", true); return; }
const reader = new FileReader();
reader.onload = function(e) {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: "" });
if(!rows || rows.length < 2) { showToast("文件无有效数据", true); return; }
const headers = rows[0];
let urlIdx = -1, descIdx = -1, userIdx = -1, pwdIdx = -1;
headers.forEach((h, idx) => {
const str = String(h).toLowerCase();
if(str.includes('网址') || str.includes('url')) urlIdx = idx;
if(str.includes('描述') || str.includes('description') || str.includes('备注')) descIdx = idx;
if(str.includes('用户名') || str.includes('账号') || str.includes('username')) userIdx = idx;
if(str.includes('密码') || str.includes('password')) pwdIdx = idx;
});
if(urlIdx === -1 || userIdx === -1 || pwdIdx === -1) { showToast("文件缺少必要列(网址/用户名/密码)", true); return; }
const records = [];
for(let i=1; i<rows.length; i++) {
const row = rows;
if(row && row.length > Math.max(urlIdx, userIdx, pwdIdx)) {
const url = row[urlIdx] ? String(row[urlIdx]).trim() : '';
const description = (descIdx !== -1 && row[descIdx]) ? String(row[descIdx]).trim() : '';
const username = row[userIdx] ? String(row[userIdx]).trim() : '';
const password = row[pwdIdx] ? String(row[pwdIdx]).trim() : '';
if(url && username && password && (url.startsWith('http://') || url.startsWith('https://')))
records.push({ url, description, username, password });
}
}
if(records.length === 0) { showToast("未找到有效数据行", true); return; }
const successCount = batchUpsertCredentials(records);
renderAdminTable();
showToast(`导入完成!成功处理 ${successCount} 条记录`);
};
reader.readAsArrayBuffer(file);
}
// ==================== 首次设置密码弹窗 ====================
let setupOverlay = null;
function showSetupPasswordModal(callback) {
if (isAdminPasswordSet()) {
if (callback) callback();
return;
}
const overlay = document.createElement('div');
overlay.className = 'setup-pwd-overlay';
overlay.innerHTML = `<div class="setup-pwd-box">
<h3>🔐 首次使用,请设置管理员密码</h3>
<input type="password" id="setupPwd" placeholder="新密码 (至少4位)" autocomplete="off">
<input type="password" id="setupConfirmPwd" placeholder="确认密码" autocomplete="off">
<div id="setupError" class="error-text"></div>
<div style="display:flex; gap:12px; margin-top:20px;">
<button class="btn btn-primary" id="setupConfirmBtn">确认设置</button>
</div>
</div>`;
document.body.appendChild(overlay);
setupOverlay = overlay;
const pwdInput = document.getElementById('setupPwd');
const confirmInput = document.getElementById('setupConfirmPwd');
pwdInput.focus();
const doSetup = () => {
const newPwd = pwdInput.value.trim();
const confirmPwd = confirmInput.value.trim();
if (!newPwd) { document.getElementById('setupError').innerText = "请输入密码"; return; }
if (newPwd.length < 4) { document.getElementById('setupError').innerText = "密码长度至少4位"; return; }
if (newPwd !== confirmPwd) { document.getElementById('setupError').innerText = "两次输入的密码不一致"; confirmInput.value = ''; confirmInput.focus(); return; }
if (setInitialAdminPassword(newPwd)) {
if (setupOverlay && setupOverlay.parentNode) setupOverlay.parentNode.removeChild(setupOverlay);
setupOverlay = null;
showToast("管理员密码设置成功,请使用新密码登录");
if (callback) callback();
} else {
document.getElementById('setupError').innerText = "设置失败,请重试";
}
};
document.getElementById('setupConfirmBtn').addEventListener('click', doSetup);
pwdInput.addEventListener('keypress', (e) => { if(e.key === 'Enter') doSetup(); });
confirmInput.addEventListener('keypress', (e) => { if(e.key === 'Enter') doSetup(); });
}
// ==================== 管理员登录弹窗 ====================
let activeAuthOverlay = null;
function showAdminAuthModal(callback) {
if (adminAuthenticated) { if(callback) callback(); return; }
if (!isAdminPasswordSet()) {
showSetupPasswordModal(() => {
showAdminAuthModal(callback);
});
return;
}
if (adminAuthActive) return;
adminAuthActive = true;
const overlay = document.createElement('div');
overlay.className = 'admin-auth-overlay';
overlay.innerHTML = `<div class="admin-auth-box">
<h3>🔐 管理员身份验证</h3>
<input type="password" id="adminAuthPwd" placeholder="管理员密码" autocomplete="off">
<div id="adminAuthError" class="error-text"></div>
<div style="display:flex; gap:12px; margin-top:20px;">
<button class="btn btn-outline" id="adminAuthCancelBtn">取消</button>
<button class="btn btn-primary" id="adminAuthConfirmBtn">确认</button>
</div>
</div>`;
document.body.appendChild(overlay);
activeAuthOverlay = overlay;
const pwdInput = document.getElementById('adminAuthPwd');
pwdInput.focus();
const doAuth = () => {
if(verifyAdminPassword(pwdInput.value)) {
adminAuthenticated = true;
document.body.removeChild(overlay);
adminAuthActive = false;
activeAuthOverlay = null;
showToast("管理员验证成功");
if(callback) callback();
else { renderAdminTable(); }
} else {
document.getElementById('adminAuthError').innerText = "密码错误,无权访问后台";
pwdInput.value = '';
pwdInput.focus();
}
};
const closeModal = () => {
if(activeAuthOverlay && activeAuthOverlay.parentNode) activeAuthOverlay.parentNode.removeChild(activeAuthOverlay);
adminAuthActive = false;
activeAuthOverlay = null;
if(!adminAuthenticated) {
showToast("未通过验证,无法使用系统", true);
renderAdminTable();
}
};
document.getElementById('adminAuthConfirmBtn').addEventListener('click', doAuth);
document.getElementById('adminAuthCancelBtn').addEventListener('click', closeModal);
pwdInput.addEventListener('keypress', (e) => { if(e.key === 'Enter') doAuth(); });
}
// ==================== 修改密码弹窗 ====================
function showChangePasswordModal() {
if(!adminAuthenticated) { showToast("请先登录管理员后台", true); return; }
const overlay = document.createElement('div');
overlay.className = 'change-pwd-overlay';
overlay.innerHTML = `<div class="change-pwd-box">
<h3>🔐 修改管理员密码</h3>
<input type="password" id="oldPwdInput" placeholder="当前密码" autocomplete="off">
<input type="password" id="newPwdInput" placeholder="新密码 (至少4位)" autocomplete="off">
<input type="password" id="confirmPwdInput" placeholder="确认新密码" autocomplete="off">
<div id="changePwdError" class="error-text"></div>
<div style="display:flex; gap:12px; margin-top:20px;">
<button class="btn btn-outline" id="changePwdCancelBtn">取消</button>
<button class="btn btn-primary" id="changePwdConfirmBtn">确认修改</button>
</div>
</div>`;
document.body.appendChild(overlay);
const oldInput = document.getElementById('oldPwdInput');
const newInput = document.getElementById('newPwdInput');
const confirmInput = document.getElementById('confirmPwdInput');
const errorDiv = document.getElementById('changePwdError');
const doChange = () => {
const oldPwd = oldInput.value.trim();
const newPwd = newInput.value.trim();
const confirmPwd = confirmInput.value.trim();
if(!oldPwd || !newPwd || !confirmPwd) { errorDiv.innerText = "请填写所有字段"; return; }
if(!verifyAdminPassword(oldPwd)) { errorDiv.innerText = "当前密码错误"; oldInput.value = ''; oldInput.focus(); return; }
if(newPwd.length < 4) { errorDiv.innerText = "新密码长度至少4位"; newInput.value = ''; confirmInput.value = ''; newInput.focus(); return; }
if(newPwd !== confirmPwd) { errorDiv.innerText = "两次输入的新密码不一致"; confirmInput.value = ''; confirmInput.focus(); return; }
if(performAdminPasswordChange(newPwd)) {
document.body.removeChild(overlay);
showToast("密码修改成功!请牢记新密码,下次登录使用新密码。");
} else { errorDiv.innerText = "修改失败,请重试"; }
};
const closeModal = () => { if(overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); };
document.getElementById('changePwdConfirmBtn').addEventListener('click', doChange);
document.getElementById('changePwdCancelBtn').addEventListener('click', closeModal);
[oldInput, newInput, confirmInput].forEach(inp => inp.addEventListener('keypress', (e) => { if(e.key === 'Enter') doChange(); }));
oldInput.focus();
}
// ==================== 初始化显示 ====================
function initializeUI() {
showAdminAuthModal(() => { renderAdminTable(); });
}
// ==================== 模态框逻辑 ====================
const modal = document.getElementById('credModal');
const urlInput = document.getElementById('credUrl');
const descInput = document.getElementById('credDescription');
const userInput = document.getElementById('credUsername');
const pwdInputField = document.getElementById('credPassword');
const editIdField = document.getElementById('editId');
const modalTitle = document.getElementById('modalTitle');
function openModalForAdd() {
if(!adminAuthenticated) { showToast("请先登录管理员后台", true); return; }
modalTitle.innerText = '➕ 新增凭证';
urlInput.value = '';
descInput.value = '';
userInput.value = '';
pwdInputField.value = '';
editIdField.value = '';
modal.style.display = 'flex';
}
function openModalForEdit(entry) {
modalTitle.innerText = '✏️ 编辑凭证';
urlInput.value = entry.url;
descInput.value = entry.description || '';
userInput.value = entry.username;
pwdInputField.value = entry.password;
editIdField.value = entry.id;
modal.style.display = 'flex';
}
function closeModal() { modal.style.display = 'none'; }
function saveCredentialHandler() {
if(!adminAuthenticated) { showToast("未授权", true); return; }
const url = urlInput.value.trim();
const description = descInput.value.trim();
const username = userInput.value.trim();
const password = pwdInputField.value.trim();
if(!url || !username || !password) { showToast("请填写完整信息 (网址、用户名、密码为必填)", true); return; }
const editId = editIdField.value;
const success = upsertCredential(editId ? parseInt(editId) : null, url, description, username, password);
if(success) { showToast(editId ? "修改成功" : "添加成功"); closeModal(); renderAdminTable(); }
else { showToast("操作失败,可能网址重复或格式错误", true); }
}
function showToast(msg, isError = false) {
const toast = document.getElementById('toastMsg');
toast.textContent = msg;
toast.style.backgroundColor = isError ? '#b91c1c' : '#1f2937';
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 2500);
}
function escapeHtml(str) { if(!str) return ''; return str.replace(/[&<>]/g, m => ({ '&':'&', '<':'<', '>':'>' }[m])); }
// 事件绑定
document.getElementById('openAddCredBtn')?.addEventListener('click', openModalForAdd);
document.getElementById('closeModalBtn')?.addEventListener('click', closeModal);
document.getElementById('saveCredBtn')?.addEventListener('click', saveCredentialHandler);
document.getElementById('exportExcelBtn')?.addEventListener('click', exportToExcel);
document.getElementById('exportDbBtn')?.addEventListener('click', exportDatabase);
document.getElementById('importExcelInput')?.addEventListener('change', (e) => { if(e.target.files.length) importExcel(e.target.files[0]); e.target.value = ''; });
document.getElementById('changeAdminPwdBtn')?.addEventListener('click', showChangePasswordModal);
window.onclick = (e) => { if(e.target === modal) closeModal(); };
async function bootstrap() {
try {
await initDatabase();
adminAuthenticated = false;
renderAdminTable();
initializeUI();
} catch(err) {
console.error(err);
showToast("初始化失败,请刷新页面重试", true);
}
}
bootstrap();
</script>
</body>
</html> |
免费评分
-
| 参与人数 1 | 吾爱币 +1 |
收起
理由
|
少污污
| + 1 |
回复直接插入源码或上传HTML文件。你这么直接发源码会给论坛吃码的。 |
查看全部评分
|