[HTML] 纯文本查看 复制代码
<!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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/sql-wasm.js"></script>
<script src="https://cdn.sheetjs.com/xlsx-0.20.2/package/dist/xlsx.full.min.js"></script>
<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; }
.tab-bar { display: flex; gap: 12px; margin-bottom: 28px; background: white; padding: 8px 20px; border-radius: 60px; width: fit-content; flex-wrap: wrap; }
.tab-btn { border: none; background: transparent; padding: 12px 28px; font-size: 1rem; font-weight: 600; border-radius: 40px; cursor: pointer; color: #475569; }
.tab-btn.active { background: #1e3c72; color: white; }
.card { background: white; border-radius: 28px; border: 1px solid #e9edf2; overflow: hidden; margin-bottom: 24px; }
.card-header { padding: 18px 24px; border-bottom: 1px solid #edf2f7; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; }
.card-header h2 { font-size: 1.4rem; font-weight: 600; }
.btn { border: none; background: #f1f5f9; padding: 8px 20px; border-radius: 40px; font-weight: 500; cursor: pointer; transition: 0.2s; }
.btn-primary { background: #1e3c72; color: white; }
.btn-primary:hover { background: #0f2b4f; }
.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; }
.btn-info { background: #3b82f6; color: white; }
.btn-purple { background: #8b5cf6; color: white; }
.btn-sm { padding: 2px 10px; font-size: 0.7rem; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 14px 16px; border-bottom: 1px solid #ecf3fa; vertical-align: middle; }
th { background: #fafcff; font-weight: 600; }
.url-link { color: #2563eb; text-decoration: none; font-weight: 500; }
.url-link:hover { text-decoration: underline; }
.url-cell { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.password-masked { font-family: monospace; background: #f1f5f9; padding: 4px 12px; border-radius: 30px; display: inline-block; }
.password-plain { font-family: monospace; background: #eef2ff; padding: 4px 12px; border-radius: 30px; display: inline-block; }
.input-group { margin-bottom: 16px; display: flex; flex-direction: column; gap: 6px; }
.input-group label { font-weight: 600; font-size: 0.85rem; color: #334155; }
input, select { padding: 10px 14px; border-radius: 20px; border: 1px solid #cfdfed; font-family: inherit; background: white; }
input:focus, select:focus { outline: none; border-color: #1e3c72; }
.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: 550px; width: 90%; border-radius: 32px; padding: 28px; }
.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; }
.toast-msg.show { opacity: 1; }
.empty-row td { text-align: center; padding: 48px; color: #6c757d; }
.hidden { display: none; }
.flex-btns { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.admin-auth-overlay { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.75); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 2000; }
.admin-auth-box { background: white; border-radius: 32px; padding: 32px; width: 90%; max-width: 400px; text-align: center; }
.admin-auth-box input { width: 100%; margin: 12px 0; }
.error-text { color: #b91c1c; font-size: 0.8rem; margin-top: 8px; }
.verify-panel { display: flex; gap: 8px; align-items: center; }
.file-input-label { background: #f1f5f9; border: 1px solid #cbd5e1; padding: 8px 18px; border-radius: 40px; cursor: pointer; font-size: 0.85rem; }
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: 8px; }
.browser-badge { display: inline-block; padding: 2px 8px; border-radius: 20px; font-size: 0.7rem; margin-right: 8px; }
.browser-default { background: #e2e8f0; color: #475569; }
.browser-edge { background: #0078d4; color: white; }
.browser-chrome { background: #4285f4; color: white; }
.browser-firefox { background: #ff9400; color: white; }
.copy-url-btn { background: #e2e8f0; border: none; padding: 2px 8px; border-radius: 16px; cursor: pointer; font-size: 0.65rem; }
.copy-url-btn:hover { background: #cbd5e1; }
.db-status { background: #d1fae5; padding: 8px 16px; border-radius: 12px; font-size: 0.75rem; color: #065f46; margin: 0 24px 16px 24px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; }
</style>
</head>
<body>
<div class="app-container">
<div class="tab-bar">
<button class="tab-btn active" data-tab="employee">👥 员工门户 (首页)</button>
<button class="tab-btn" data-tab="admin">🔐 管理员后台</button>
</div>
<div id="employeePanel" class="tab-panel">
<div class="card">
<div class="card-header">
<h2>🌐 员工门户 · 快捷访问</h2>
<div class="verify-panel">
<input type="password" id="empViewPwd" placeholder="请输入查看密码" style="width: 180px;" autocomplete="off">
<button class="btn btn-primary" id="verifyEmpBtn">🔓 验证并显示密码</button>
</div>
</div>
<div style="overflow-x: auto;">
<table id="employeeTable">
<thead><tr><th>网址链接</th><th>用户名</th><th>密码</th><th>打开方式</th></thead>
<tbody id="employeeTableBody"></tbody>
</table>
</div>
<div style="padding: 12px 20px; border-top:1px solid #edf2f7; font-size:0.75rem; color:#475569;">
🔐 员工查看密码: <strong>employee2024</strong>
</div>
</div>
</div>
<div id="adminPanel" class="tab-panel hidden">
<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">📎 导出 Excel</button>
<label class="file-input-label" for="importExcelInput">📂 导入 Excel</label>
<input type="file" id="importExcelInput" accept=".xls, .xlsx">
<button class="btn btn-info" id="exportDbBtn">💾 导出数据库</button>
<label class="file-input-label" for="importDbInput" style="background:#8b5cf6; color:white;">📁 导入数据库</label>
<input type="file" id="importDbInput" accept=".db">
</div>
</div>
<div class="db-status">
<span>📀 数据库状态:已连接</span>
<span id="dbStatusText">SQLite 持久化存储</span>
</div>
<div style="overflow-x: auto;">
<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>⚙️ 系统安全设置</h4>
<div class="settings-row">
<span style="width: 140px;">🔐 员工查看密码:</span>
<input type="password" id="newEmployeePwd" placeholder="新员工查看密码" style="width: 180px;">
<button class="btn btn-warning" id="updateEmployeePwdBtn">修改</button>
</div>
<div class="settings-row">
<span style="width: 140px;">🔑 管理员登录密码:</span>
<input type="password" id="newAdminPwd" placeholder="新管理员密码" style="width: 180px;">
<button class="btn btn-warning" id="updateAdminPwdBtn">修改</button>
</div>
</div>
<div class="card-header" style="border-top:1px solid #edf2f7;"><h2>📌 说明</h2></div>
<div style="padding: 16px 24px; color: #334155;">
• 管理员密码: <strong>admin123</strong> | 员工密码: <strong>employee2024</strong><br>
• 数据存储在浏览器 OPFS 中的真实 SQLite 文件,刷新/关闭不丢失<br>
• 可导出 .db 文件备份,或导入之前备份的数据库文件
</div>
</div>
</div>
</div>
<div id="credModal" class="modal">
<div class="modal-content">
<h3 id="modalTitle">➕ 添加凭证</h3>
<div class="input-group"><label>🔗 网址</label><input type="text" id="credUrl" placeholder="https://..."></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>
<div class="input-group">
<label>🌐 指定浏览器打开</label>
<select id="credBrowser">
<option value="default">🌍 默认浏览器</option>
<option value="edge">🔷 Edge浏览器</option>
<option value="chrome">🔴 谷歌浏览器</option>
<option value="firefox">🦊 火狐浏览器</option>
</select>
</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(); }
function getBrowserCommand(browser, url) {
switch(browser) {
case 'edge': return `start msedge "${url}"`;
case 'chrome': return `start chrome "${url}"`;
case 'firefox': return `start firefox "${url}"`;
default: return `start "${url}"`;
}
}
function getBrowserName(browser) {
switch(browser) {
case 'edge': return 'Edge浏览器';
case 'chrome': return '谷歌浏览器';
case 'firefox': return '火狐浏览器';
default: return '默认浏览器';
}
}
function getBrowserClass(browser) {
switch(browser) {
case 'edge': return 'browser-edge';
case 'chrome': return 'browser-chrome';
case 'firefox': return 'browser-firefox';
default: return 'browser-default';
}
}
async function copyToClipboard(text, successMsg = "已复制") {
try {
await navigator.clipboard.writeText(text);
showToast(successMsg);
} catch(e) {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
showToast(successMsg);
}
}
// ==================== OPFS 持久化存储 ====================
const DB_FILE_NAME = 'password_manager.db';
let db = null;
let SQL = null;
let employeePasswordVisible = false;
let adminAuthenticated = false;
let currentEmployeePwdHash = "";
let currentAdminPwdHash = "";
let opfsRoot = null;
// 默认密码哈希值
const DEFAULT_EMPLOYEE_HASH = hashPassword("employee2024");
const DEFAULT_ADMIN_HASH = hashPassword("admin123");
async function initOPFSDatabase() {
try {
if (!navigator.storage || !navigator.storage.getDirectory) {
throw new Error("当前浏览器不支持 OPFS");
}
opfsRoot = await navigator.storage.getDirectory();
let fileHandle;
let isNewFile = false;
try {
fileHandle = await opfsRoot.getFileHandle(DB_FILE_NAME);
} catch (e) {
fileHandle = await opfsRoot.getFileHandle(DB_FILE_NAME, { create: true });
isNewFile = true;
}
const file = await fileHandle.getFile();
const fileBuffer = await file.arrayBuffer();
if (!isNewFile && fileBuffer.byteLength > 0) {
db = new SQL.Database(new Uint8Array(fileBuffer));
// 验证数据库是否有效
try {
db.exec("SELECT 1 FROM credentials");
db.exec("SELECT 1 FROM system_config");
} catch(e) {
// 数据库损坏,重新创建
createNewDatabase();
}
} else {
createNewDatabase();
}
// 刷新密码哈希
let empHash = getConfig('employee_view_password_hash');
let adminHash = getConfig('admin_login_password_hash');
// 如果配置不存在或无效,使用默认值并保存
if (!empHash) {
setConfig('employee_view_password_hash', DEFAULT_EMPLOYEE_HASH);
empHash = DEFAULT_EMPLOYEE_HASH;
}
if (!adminHash) {
setConfig('admin_login_password_hash', DEFAULT_ADMIN_HASH);
adminHash = DEFAULT_ADMIN_HASH;
}
currentEmployeePwdHash = empHash;
currentAdminPwdHash = adminHash;
return true;
} catch (err) {
console.error("OPFS 初始化失败,使用内存模式:", err);
createNewDatabase();
currentEmployeePwdHash = DEFAULT_EMPLOYEE_HASH;
currentAdminPwdHash = DEFAULT_ADMIN_HASH;
return false;
}
}
async function saveDatabaseToOPFS() {
if (!db) return;
try {
if (!opfsRoot) {
opfsRoot = await navigator.storage.getDirectory();
}
const exportedData = db.export();
const buffer = new Uint8Array(exportedData);
const fileHandle = await opfsRoot.getFileHandle(DB_FILE_NAME, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(buffer);
await writable.close();
} catch (err) {
console.error("保存失败:", err);
}
}
function createNewDatabase() {
db = new SQL.Database();
db.run(`CREATE TABLE IF NOT EXISTS credentials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL UNIQUE,
username TEXT NOT NULL,
password TEXT NOT NULL,
browser TEXT DEFAULT 'default',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
db.run(`CREATE TABLE IF NOT EXISTS system_config (key TEXT PRIMARY KEY, value TEXT NOT NULL)`);
// 检查是否有演示数据
const cnt = db.exec("SELECT COUNT(*) FROM credentials");
if (!cnt.length || cnt[0].values[0][0] === 0) {
const demo = [
['https://mail.company.com', 'zhang.san@company.com', 'Mail@2024Secure', 'default'],
['https://hrsystem.company.com', 'hr_admin', 'HrP@ss#2024', 'edge'],
['https://gitlab.company.com', 'dev_lihua', 'GitLab!7890', 'chrome'],
['https://jira.company.com', 'project_manager', 'Jira@2025', 'firefox']
];
const stmt = db.prepare("INSERT INTO credentials (url, username, password, browser) VALUES (?, ?, ?, ?)");
for(let row of demo) stmt.run(row);
stmt.free();
}
// 检查配置表
let empExists = db.exec("SELECT value FROM system_config WHERE key = 'employee_view_password_hash'");
if (!empExists.length) {
db.run("INSERT INTO system_config VALUES ('employee_view_password_hash', ?)", [DEFAULT_EMPLOYEE_HASH]);
}
let adminExists = db.exec("SELECT value FROM system_config WHERE key = 'admin_login_password_hash'");
if (!adminExists.length) {
db.run("INSERT INTO system_config VALUES ('admin_login_password_hash', ?)", [DEFAULT_ADMIN_HASH]);
}
saveDatabaseToOPFS();
}
function getConfig(key) {
try {
const res = db.exec(`SELECT value FROM system_config WHERE key = '${key}'`);
return (res.length && res[0].values.length) ? res[0].values[0][0] : null;
} catch(e) { return null; }
}
function setConfig(key, value) {
db.run("INSERT OR REPLACE INTO system_config VALUES (?, ?)", [key, value]);
saveDatabaseToOPFS();
}
function getAllCredentials() {
const res = db.exec("SELECT id, url, username, password, browser FROM credentials ORDER BY id DESC");
if(!res.length) return [];
return res[0].values.map(row => ({
id: row[0], url: row[1], username: row[2], password: row[3], browser: row[4] || 'default'
}));
}
function upsertCredential(id, url, username, password, browser) {
try {
if(id === null) {
db.run("INSERT OR REPLACE INTO credentials (url, username, password, browser) VALUES (?, ?, ?, ?)", [url, username, password, browser]);
} else {
db.run("UPDATE credentials SET url = ?, username = ?, password = ?, browser = ? WHERE id = ?", [url, username, password, browser, id]);
}
saveDatabaseToOPFS();
return true;
} catch(e) { return false; }
}
function deleteCredential(id) {
try {
db.run("DELETE FROM credentials WHERE id = ?", [id]);
saveDatabaseToOPFS();
return true;
} catch(e) { return false; }
}
function batchUpsertCredentials(records) {
let success = 0;
const stmt = db.prepare("INSERT OR REPLACE INTO credentials (url, username, password, browser) VALUES (?, ?, ?, ?)");
for(let rec of records) {
if(rec.url && rec.username && rec.password) {
try {
stmt.run([rec.url, rec.username, rec.password, rec.browser || 'default']);
success++;
} catch(e) {}
}
}
stmt.free();
saveDatabaseToOPFS();
return success;
}
async function exportDatabaseFile() {
if(!db) return;
const exported = db.export();
const blob = new Blob([exported], { type: "application/x-sqlite3" });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `password_manager_${new Date().toISOString().slice(0,19).replace(/:/g, '-')}.db`;
a.click();
URL.revokeObjectURL(url);
showToast("数据库已导出");
}
async function importDatabaseFile(file) {
const reader = new FileReader();
reader.onload = async function(e) {
try {
const buffer = new Uint8Array(e.target.result);
const newDb = new SQL.Database(buffer);
newDb.exec("SELECT 1 FROM credentials");
newDb.exec("SELECT 1 FROM system_config");
db = newDb;
await saveDatabaseToOPFS();
currentEmployeePwdHash = getConfig('employee_view_password_hash') || DEFAULT_EMPLOYEE_HASH;
currentAdminPwdHash = getConfig('admin_login_password_hash') || DEFAULT_ADMIN_HASH;
employeePasswordVisible = false;
adminAuthenticated = false;
renderEmployeeTable();
renderAdminTable();
showToast("数据库导入成功");
switchTab('employee');
} catch(err) {
showToast("无效的数据库文件", true);
}
};
reader.readAsArrayBuffer(file);
}
// 渲染员工门户
function renderEmployeeTable() {
const tbody = document.getElementById('employeeTableBody');
const creds = getAllCredentials();
if(!creds.length) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="4">暂无凭证数据</td></tr>';
return;
}
let html = '';
for(let item of creds) {
const browserName = getBrowserName(item.browser);
const browserClass = getBrowserClass(item.browser);
const cmd = getBrowserCommand(item.browser, item.url);
const pwdDisplay = employeePasswordVisible ?
`<span class="password-plain">${escapeHtml(item.password)}</span>` :
`<span class="password-masked">●●●●●●●●</span>`;
html += `<tr>
<td><div class="url-cell"><a href="${escapeHtml(item.url)}" target="_blank" class="url-link">🔗 ${escapeHtml(item.url)}</a><button class="copy-url-btn" data-url="${escapeHtml(item.url)}">📋 复制</button></div></td>
<td>${escapeHtml(item.username)}</td>
<td>${pwdDisplay}</td>
<td><span class="browser-badge ${browserClass}">${browserName}</span></td>
</tr>`;
}
tbody.innerHTML = html;
document.querySelectorAll('.copy-url-btn').forEach(btn => btn.addEventListener('click', () => copyToClipboard(btn.dataset.url, "网址已复制")));
}
function verifyEmployee() {
const input = document.getElementById('empViewPwd');
const inputHash = hashPassword(input.value);
if(inputHash === currentEmployeePwdHash) {
employeePasswordVisible = true;
showToast("验证成功!密码列已显示明文");
renderEmployeeTable();
} else {
employeePasswordVisible = false;
renderEmployeeTable();
showToast("查看密码错误,密码保持隐藏", true);
}
input.value = '';
}
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 = '';
for(let item of creds) {
const browserName = getBrowserName(item.browser);
const browserClass = getBrowserClass(item.browser);
html += `<tr>
<td><a href="${escapeHtml(item.url)}" target="_blank" class="url-link">${escapeHtml(item.url)}</a></td>
<td>${escapeHtml(item.username)}</td>
<td><span class="password-plain">${escapeHtml(item.password)}</span></td>
<td><span class="browser-badge ${browserClass}">${browserName}</span></td>
<td class="flex-btns"><button class="btn btn-outline edit-btn" data-id="${item.id}">✏️ 编辑</button><button class="btn btn-danger delete-btn" data-id="${item.id}">🗑️ 删除</button></td>
</tr>`;
}
tbody.innerHTML = html;
document.querySelectorAll('.edit-btn').forEach(btn => btn.addEventListener('click', () => openEditModal(parseInt(btn.dataset.id))));
document.querySelectorAll('.delete-btn').forEach(btn => btn.addEventListener('click', () => { if(confirm('确定删除?')) { deleteCredential(parseInt(btn.dataset.id)); renderAdminTable(); renderEmployeeTable(); showToast("已删除"); } }));
}
function openEditModal(id) {
const entry = getAllCredentials().find(c => c.id === id);
if(entry) openModalForEdit(entry);
}
function exportToExcel() {
if(!adminAuthenticated) { showToast("请先登录", true); return; }
const creds = getAllCredentials();
if(!creds.length) { showToast("无数据", true); return; }
const sheetData = [["网址", "用户名", "密码", "浏览器"]];
for(let item of creds) sheetData.push([item.url, item.username, item.password, item.browser]);
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_${Date.now()}.xls`, { bookType: 'xls' });
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, userIdx=-1, pwdIdx=-1, browserIdx=-1;
for(let i=0;i<headers.length;i++) {
const s = String(headers[i]).toLowerCase();
if(s.includes('网址')||s.includes('url')) urlIdx=i;
if(s.includes('用户名')||s.includes('账号')) userIdx=i;
if(s.includes('密码')) pwdIdx=i;
if(s.includes('浏览器')) browserIdx=i;
}
if(urlIdx===-1||userIdx===-1||pwdIdx===-1) { showToast("缺少必要列", true); return; }
const records = [];
for(let i=1;i<rows.length;i++) {
const row = rows[i];
if(row && row.length > Math.max(urlIdx,userIdx,pwdIdx)) {
const url = row[urlIdx] ? String(row[urlIdx]).trim() : '';
const username = row[userIdx] ? String(row[userIdx]).trim() : '';
const password = row[pwdIdx] ? String(row[pwdIdx]).trim() : '';
const browser = (browserIdx!==-1 && row[browserIdx]) ? String(row[browserIdx]).trim() : 'default';
if(url && username && password && (url.startsWith('http://')||url.startsWith('https://'))) {
records.push({url, username, password, browser});
}
}
}
if(!records.length) { showToast("无有效数据", true); return; }
const cnt = batchUpsertCredentials(records);
renderAdminTable(); renderEmployeeTable();
showToast(`导入 ${cnt} 条`);
};
reader.readAsArrayBuffer(file);
}
function showAdminLogin() {
if(adminAuthenticated) return;
const overlay = document.createElement('div');
overlay.className = 'admin-auth-overlay';
overlay.innerHTML = `<div class="admin-auth-box"><h3>🔐 管理员验证</h3><input type="password" id="adminLoginPwd" placeholder="管理员密码" autocomplete="off"><div id="adminLoginError" class="error-text"></div><div style="display:flex; gap:12px; margin-top:20px;"><button class="btn btn-outline" id="adminLoginCancel">取消</button><button class="btn btn-primary" id="adminLoginConfirm">确认</button></div></div>`;
document.body.appendChild(overlay);
const pwdInput = document.getElementById('adminLoginPwd');
pwdInput.focus();
const doAuth = () => {
const inputHash = hashPassword(pwdInput.value);
if(inputHash === currentAdminPwdHash) {
adminAuthenticated = true;
document.body.removeChild(overlay);
showToast("验证成功");
renderAdminTable();
if(!document.getElementById('adminPanel').classList.contains('hidden')) renderAdminTable();
} else {
document.getElementById('adminLoginError').innerText = "密码错误";
pwdInput.value = '';
pwdInput.focus();
}
};
const cancel = () => {
document.body.removeChild(overlay);
if(!adminAuthenticated) { switchTab('employee'); showToast("未验证", true); }
};
document.getElementById('adminLoginConfirm').addEventListener('click', doAuth);
document.getElementById('adminLoginCancel').addEventListener('click', cancel);
pwdInput.addEventListener('keypress', (e) => { if(e.key === 'Enter') doAuth(); });
}
function switchTab(tab) {
const adminDiv = document.getElementById('adminPanel'), empDiv = document.getElementById('employeePanel');
if(tab === 'employee') {
adminDiv.classList.add('hidden'); empDiv.classList.remove('hidden');
renderEmployeeTable();
document.querySelectorAll('.tab-btn').forEach(btn=>btn.classList.remove('active'));
document.querySelector('.tab-btn[data-tab="employee"]').classList.add('active');
} else {
if(!adminAuthenticated) {
showAdminLogin();
const iv = setInterval(() => { if(adminAuthenticated) { clearInterval(iv); adminDiv.classList.remove('hidden'); empDiv.classList.add('hidden'); renderAdminTable(); document.querySelectorAll('.tab-btn').forEach(btn=>btn.classList.remove('active')); document.querySelector('.tab-btn[data-tab="admin"]').classList.add('active'); } }, 200);
setTimeout(() => clearInterval(iv), 30000);
} else {
adminDiv.classList.remove('hidden'); empDiv.classList.add('hidden');
renderAdminTable();
document.querySelectorAll('.tab-btn').forEach(btn=>btn.classList.remove('active'));
document.querySelector('.tab-btn[data-tab="admin"]').classList.add('active');
}
}
}
const modal = document.getElementById('credModal');
const urlInput = document.getElementById('credUrl'), usernameInput = document.getElementById('credUsername'), passwordInput = document.getElementById('credPassword'), browserSelect = document.getElementById('credBrowser'), editIdField = document.getElementById('editId'), modalTitle = document.getElementById('modalTitle');
function openModalForAdd() { if(!adminAuthenticated) { showToast("请先登录", true); return; } modalTitle.innerText = '➕ 新增凭证'; urlInput.value = ''; usernameInput.value = ''; passwordInput.value = ''; browserSelect.value = 'default'; editIdField.value = ''; modal.style.display = 'flex'; }
function openModalForEdit(entry) { modalTitle.innerText = '✏️ 编辑凭证'; urlInput.value = entry.url; usernameInput.value = entry.username; passwordInput.value = entry.password; browserSelect.value = entry.browser || 'default'; editIdField.value = entry.id; modal.style.display = 'flex'; }
function closeModal() { modal.style.display = 'none'; }
function saveCredential() { if(!adminAuthenticated) { showToast("未授权", true); return; } const url = urlInput.value.trim(), username = usernameInput.value.trim(), password = passwordInput.value.trim(), browser = browserSelect.value; if(!url || !username || !password) { showToast("请填完整", true); return; } if(!url.startsWith('http')) { showToast("网址需http开头", true); return; } const editId = editIdField.value; if(upsertCredential(editId ? parseInt(editId) : null, url, username, password, browser)) { showToast(editId ? "修改成功" : "添加成功"); closeModal(); renderAdminTable(); renderEmployeeTable(); } else showToast("失败", true); }
function updateEmployeePassword() { const inp = document.getElementById('newEmployeePwd'); const newPwd = inp.value; if(!newPwd || newPwd.length < 4) { showToast("密码至少4位", true); inp.value = ''; return; } setConfig('employee_view_password_hash', hashPassword(newPwd)); currentEmployeePwdHash = hashPassword(newPwd); employeePasswordVisible = false; renderEmployeeTable(); showToast("员工密码已修改,员工需重新验证"); inp.value = ''; }
function updateAdminPassword() { const inp = document.getElementById('newAdminPwd'); const newPwd = inp.value; if(!newPwd || newPwd.length < 4) { showToast("密码至少4位", true); inp.value = ''; return; } setConfig('admin_login_password_hash', hashPassword(newPwd)); currentAdminPwdHash = hashPassword(newPwd); showToast("管理员密码已修改,下次登录请使用新密码"); inp.value = ''; }
function showToast(msg, err=false) { const t = document.getElementById('toastMsg'); t.textContent = msg; t.style.backgroundColor = err ? '#b91c1c' : '#1f2937'; t.classList.add('show'); setTimeout(() => t.classList.remove('show'), 2200); }
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', saveCredential);
document.getElementById('verifyEmpBtn')?.addEventListener('click', verifyEmployee);
document.getElementById('empViewPwd')?.addEventListener('keypress', (e) => { if(e.key === 'Enter') verifyEmployee(); });
document.getElementById('exportExcelBtn')?.addEventListener('click', exportToExcel);
document.getElementById('exportDbBtn')?.addEventListener('click', exportDatabaseFile);
document.getElementById('importExcelInput')?.addEventListener('change', (e) => { if(e.target.files.length) importExcel(e.target.files[0]); e.target.value = ''; });
document.getElementById('importDbInput')?.addEventListener('change', (e) => { if(e.target.files.length) importDatabaseFile(e.target.files[0]); e.target.value = ''; });
document.getElementById('updateEmployeePwdBtn')?.addEventListener('click', updateEmployeePassword);
document.getElementById('updateAdminPwdBtn')?.addEventListener('click', updateAdminPassword);
document.querySelectorAll('.tab-btn').forEach(btn => btn.addEventListener('click', () => switchTab(btn.dataset.tab)));
window.onclick = (e) => { if(e.target === modal) closeModal(); };
async function bootstrap() {
try {
await new Promise((resolve, reject) => {
window.initSqlJs({ locateFile: () => "https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/sql-wasm.wasm" })
.then(async (sql) => {
SQL = sql;
await initOPFSDatabase();
resolve();
}).catch(reject);
});
employeePasswordVisible = false;
adminAuthenticated = false;
renderEmployeeTable();
renderAdminTable();
switchTab('employee');
console.log("系统初始化完成,员工密码: employee2024, 管理员密码: admin123");
} catch(err) { showToast("初始化失败,请刷新重试", true); }
}
bootstrap();
</script>
</body>
</html>