吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1466|回复: 30
上一主题 下一主题
收起左侧

[其他转载] 个人整改后做的html版网页密码管理系统

  [复制链接]
跳转到指定楼层
楼主
kissfox 发表于 2026-9-1 14:21 回帖奖励
因公司销售助理反映上一个版本里面有网址、用户、密码管理了,可有时候却不记得应该用哪个浏览器去打开网址实现正确的系统登录。销售助理主管又找我从新整改下。
为了尽快让销售助理使用上整改后的HTML版的网页密码管理系统,我根据销售助理主管提出的要求重新整改了。
主要整改:
网址指定浏览器访问:
用于销售助理平时点击添加后客户的ERP的网页进入客户的ERP系统,可以根据自己在后台添加的网址指定浏览器来实现。
点击访问客户ERP 系统的时候,会自动检测指定浏览器是否已经安装,如未安装,会弹窗提示安装浏览器。
管理员后台整改:
主要整改添加客户ERP系统是选择使用什么浏览器。数据自动保存,管理员后台还修改了数据导入及导出JSON功能。
管理员后台密码默认: admin123,员工查看密码默认: employee2024
支持 Excel (.xls, .xlsx) 导入导出
所有数据存储方式改成了 localStorage
可点击「备份数据库」下载完整数据库文件
界面主题增加:
增加有8种颜色主题切换





[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>企业密码管理系统</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 id="theme-style">
        :root {
            --bg: #f1f5f9;
            --card: #ffffff;
            --border: #e9edf2;
            --text: #1e293b;
            --text2: #475569;
            --shadow: rgba(0,0,0,0.05);
            --primary: #1e3c72;
            --primary-hover: #0f2b4f;
            --input: #ffffff;
            --input-border: #cfdfed;
            --table-header: #fafcff;
            --badge-default: #e2e8f0;
            --badge-edge: #0078d4;
            --badge-chrome: #4285f4;
            --badge-firefox: #ff9400;
            --success: #10b981;
            --warning: #f59e0b;
            --danger: #ef4444;
            --info: #3b82f6;
        }
        .theme-red { --bg: #fef2f2; --card: #ffffff; --primary: #b91c1c; --primary-hover: #7f1d1d; --border: #fecaca; --table-header: #fef2f2; }
        .theme-orange { --bg: #fff7ed; --card: #ffffff; --primary: #c2410c; --primary-hover: #9a3412; --border: #fed7aa; --table-header: #fff7ed; }
        .theme-yellow { --bg: #fefce8; --card: #ffffff; --primary: #a16207; --primary-hover: #854d0e; --border: #fde68a; --table-header: #fefce8; }
        .theme-green { --bg: #f0fdf4; --card: #ffffff; --primary: #15803d; --primary-hover: #166534; --border: #bbf7d0; --table-header: #f0fdf4; }
        .theme-blue { --bg: #eff6ff; --card: #ffffff; --primary: #1d4ed8; --primary-hover: #1e40af; --border: #bfdbfe; --table-header: #eff6ff; }
        .theme-purple { --bg: #faf5ff; --card: #ffffff; --primary: #7e22ce; --primary-hover: #6b21a8; --border: #e9d5ff; --table-header: #faf5ff; }
        .theme-pink { --bg: #fdf2f8; --card: #ffffff; --primary: #be185d; --primary-hover: #9d174d; --border: #fbcfe8; --table-header: #fdf2f8; }
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', system-ui; background: var(--bg); padding: 20px 16px; transition: 0.3s; }
        .app { max-width: 1400px; margin: 0 auto; }
        .header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; margin-bottom: 20px; }
        .header h1 { font-size: 1.4rem; font-weight: 600; color: var(--text); }
        .theme-bar { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
        .theme-bar span { font-size: 0.75rem; color: var(--text2); margin-right: 2px; }
        .theme-dot { width: 28px; height: 28px; border-radius: 50%; border: 2px solid transparent; cursor: pointer; transition: 0.2s; }
        .theme-dot:hover { transform: scale(1.1); }
        .theme-dot.active { border-color: var(--text); box-shadow: 0 0 0 2px var(--bg); }
        .tab-bar { display: flex; gap: 8px; margin-bottom: 20px; background: var(--card); padding: 6px 16px; border-radius: 40px; width: fit-content; flex-wrap: wrap; border: 1px solid var(--border); }
        .tab-btn { border: none; background: transparent; padding: 10px 24px; font-size: 0.95rem; font-weight: 500; border-radius: 30px; cursor: pointer; color: var(--text2); transition: 0.2s; }
        .tab-btn.active { background: var(--primary); color: #fff; }
        .tab-btn:hover:not(.active) { background: var(--bg); }
        .card { background: var(--card); border-radius: 20px; border: 1px solid var(--border); overflow: hidden; margin-bottom: 20px; box-shadow: 0 2px 8px var(--shadow); }
        .card-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; }
        .card-header h2 { font-size: 1.2rem; font-weight: 600; color: var(--text); }
        .btn { border: none; background: var(--bg); padding: 6px 18px; border-radius: 30px; font-weight: 500; cursor: pointer; transition: 0.2s; font-size: 0.85rem; color: var(--text); }
        .btn-primary { background: var(--primary); color: #fff; }
        .btn-primary:hover { background: var(--primary-hover); }
        .btn-outline { background: transparent; border: 1px solid var(--border); }
        .btn-outline:hover { background: var(--bg); }
        .btn-success { background: var(--success); color: #fff; }
        .btn-success:hover { background: #059669; }
        .btn-warning { background: var(--warning); color: #fff; }
        .btn-warning:hover { background: #d97706; }
        .btn-danger { background: var(--danger); color: #fff; }
        .btn-danger:hover { background: #dc2626; }
        .btn-info { background: var(--info); color: #fff; }
        .btn-info:hover { background: #2563eb; }
        .btn-sm { padding: 2px 10px; font-size: 0.7rem; }
        .btn-open { background: #22d3ee; color: #0e7490; }
        .btn-open:hover { background: #67e8f9; }
        .btn-auto-backup { background: #8b5cf6; color: #fff; }
        .btn-auto-backup:hover { background: #7c3aed; }
        table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
        th, td { text-align: left; padding: 12px 14px; border-bottom: 1px solid var(--border); vertical-align: middle; }
        th { background: var(--table-header); font-weight: 600; color: var(--text); }
        .url-link { color: var(--primary); text-decoration: none; font-weight: 500; }
        .url-link:hover { text-decoration: underline; }
        .url-cell { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
        .password-masked { font-family: monospace; background: var(--bg); padding: 2px 12px; border-radius: 20px; display: inline-block; font-size: 0.85rem; }
        .password-plain { font-family: monospace; background: #dbeafe; padding: 2px 12px; border-radius: 20px; display: inline-block; font-size: 0.85rem; }
        .input-group { margin-bottom: 14px; display: flex; flex-direction: column; gap: 4px; }
        .input-group label { font-weight: 600; font-size: 0.8rem; color: var(--text2); }
        input, select { padding: 8px 14px; border-radius: 16px; border: 1px solid var(--input-border); font-family: inherit; background: var(--input); color: var(--text); }
        input:focus, select:focus { outline: none; border-color: var(--primary); }
        .modal { display: none; position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.4); align-items: center; justify-content: center; z-index: 1000; }
        .modal-content { background: var(--card); max-width: 500px; width: 92%; border-radius: 24px; padding: 24px; }
        .toast { position: fixed; bottom: 24px; right: 24px; background: var(--text); color: #fff; padding: 10px 20px; border-radius: 30px; opacity: 0; transition: 0.25s; z-index: 1100; font-size: 0.85rem; pointer-events: none; max-width: 90%; }
        .toast.show { opacity: 1; }
        .toast.error { background: var(--danger); }
        .toast.success { background: var(--success); }
        .hidden { display: none; }
        .flex-btns { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
        .verify-panel { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
        .file-input-label { background: var(--bg); border: 1px solid var(--border); padding: 6px 16px; border-radius: 30px; cursor: pointer; font-size: 0.85rem; }
        input[type="file"] { display: none; }
        .settings-group { background: var(--bg); border-radius: 16px; padding: 14px 18px; margin: 12px 20px 16px 20px; border: 1px solid var(--border); }
        .settings-group h4 { margin-bottom: 10px; color: var(--text); font-size: 0.9rem; }
        .settings-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 6px; }
        .settings-row span { font-size: 0.85rem; color: var(--text2); min-width: 130px; }
        .browser-badge { display: inline-block; padding: 2px 10px; border-radius: 16px; font-size: 0.7rem; margin-right: 6px; }
        .browser-default { background: var(--badge-default); color: var(--text2); }
        .browser-edge { background: var(--badge-edge); color: #fff; }
        .browser-chrome { background: var(--badge-chrome); color: #fff; }
        .browser-firefox { background: var(--badge-firefox); color: #fff; }
        .copy-url-btn { background: var(--bg); border: none; padding: 2px 8px; border-radius: 14px; cursor: pointer; font-size: 0.6rem; color: var(--text2); }
        .copy-url-btn:hover { background: var(--border); }
        .db-status { background: #d1fae5; padding: 6px 16px; border-radius: 10px; font-size: 0.75rem; color: #065f46; margin: 0 20px 12px 20px; display: flex; justify-content: space-between; flex-wrap: wrap; gap: 6px; }
        .open-btn-group { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; }
        .open-btn-group .btn { padding: 2px 10px; font-size: 0.65rem; }
        .empty-row td { text-align: center; padding: 40px; color: var(--text2); }
        .admin-auth-overlay { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 2000; }
        .admin-auth-box { background: var(--card); border-radius: 24px; padding: 28px; width: 92%; max-width: 380px; text-align: center; }
        .admin-auth-box input { width: 100%; margin: 10px 0; }
        .admin-auth-box .error { color: var(--danger); font-size: 0.8rem; }
        .scroll-x { overflow-x: auto; }

        .browser-hint-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.55);
            display: none;
            align-items: center;
            justify-content: center;
            z-index: 3000;
        }
        .browser-hint-overlay.show { display: flex; }
        .browser-hint-box {
            background: var(--card);
            border-radius: 24px;
            padding: 32px 28px;
            max-width: 460px;
            width: 92%;
            text-align: center;
            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
            animation: fadeIn 0.3s ease;
        }
        @keyframes fadeIn {
            from { opacity: 0; transform: scale(0.95); }
            to { opacity: 1; transform: scale(1); }
        }
        .browser-hint-box .icon { font-size: 3.2rem; margin-bottom: 12px; }
        .browser-hint-box h3 { font-size: 1.3rem; color: var(--text); margin-bottom: 10px; }
        .browser-hint-box p { font-size: 0.9rem; color: var(--text2); line-height: 1.6; margin-bottom: 18px; }
        .browser-hint-box .btn-group { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
        .browser-hint-box .btn-group .btn { min-width: 100px; padding: 10px 24px; font-size: 0.9rem; }
        .browser-hint-box .checkbox-row { display: flex; align-items: center; justify-content: center; gap: 8px; margin: 12px 0 16px 0; font-size: 0.8rem; color: var(--text2); }
        .browser-hint-box .checkbox-row input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; accent-color: var(--primary); }

        .storage-info {
            background: var(--bg);
            border-radius: 12px;
            padding: 12px 18px;
            margin: 8px 20px 12px 20px;
            border: 1px solid var(--border);
            font-size: 0.8rem;
            color: var(--text2);
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 8px;
        }
        .storage-info .label { font-weight: 600; color: var(--text); }
        .storage-info .status { color: var(--success); }
        .storage-info .backup-status { color: var(--info); font-size: 0.75rem; }

        .backup-settings {
            background: var(--bg);
            border-radius: 12px;
            padding: 12px 18px;
            margin: 8px 20px 12px 20px;
            border: 1px solid var(--border);
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 10px;
        }
        .backup-settings .label { font-weight: 600; color: var(--text); font-size: 0.85rem; }
        .backup-settings .desc { font-size: 0.75rem; color: var(--text2); }
        .backup-settings .toggle { display: flex; align-items: center; gap: 8px; }
        .toggle-switch {
            width: 40px;
            height: 22px;
            background: #cbd5e1;
            border-radius: 12px;
            cursor: pointer;
            position: relative;
            transition: 0.3s;
            border: none;
            padding: 0;
        }
        .toggle-switch.active {
            background: var(--success);
        }
        .toggle-switch::after {
            content: '';
            width: 18px;
            height: 18px;
            background: white;
            border-radius: 50%;
            position: absolute;
            top: 2px;
            left: 2px;
            transition: 0.3s;
            box-shadow: 0 1px 3px rgba(0,0,0,0.2);
        }
        .toggle-switch.active::after {
            left: 20px;
        }

        [url=home.php?mod=space&uid=945662]@media[/url] (max-width: 600px) {
            th, td { padding: 8px 10px; font-size: 0.8rem; }
            .card-header { padding: 12px 14px; }
            .browser-hint-box { padding: 24px 18px; }
            .browser-hint-box .btn-group .btn { min-width: 80px; padding: 8px 16px; font-size: 0.8rem; }
            .storage-info, .backup-settings { flex-direction: column; align-items: flex-start; }
        }
    </style>
</head>
<body>
<div class="app">
    <div class="header">
        <h1>密码管理系统</h1>
        <div class="theme-bar">
            <span>主题</span>
            <span class="theme-dot active" data-theme="default" style="background:#64748b;" title="默认"></span>
            <span class="theme-dot" data-theme="red" style="background:#dc2626;" title="红"></span>
            <span class="theme-dot" data-theme="orange" style="background:#ea580c;" title="橙"></span>
            <span class="theme-dot" data-theme="yellow" style="background:#ca8a04;" title="黄"></span>
            <span class="theme-dot" data-theme="green" style="background:#16a34a;" title="绿"></span>
            <span class="theme-dot" data-theme="blue" style="background:#2563eb;" title="蓝"></span>
            <span class="theme-dot" data-theme="purple" style="background:#9333ea;" title="紫"></span>
            <span class="theme-dot" data-theme="pink" style="background:#db2777;" title="粉"></span>
        </div>
    </div>
    <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">
        <div class="card">
            <div class="card-header">
                <h2>快捷访问</h2>
                <div class="verify-panel">
                    <input type="password" id="empViewPwd" placeholder="查看密码" style="width:150px;" autocomplete="off">
                    <button class="btn btn-primary" id="verifyEmpBtn">显示密码</button>
                </div>
            </div>
            <div class="scroll-x">
                <table>
                    <thead><tr><th>网址</th><th>账号</th><th>密码</th><th>打开方式</th></tr></thead>
                    <tbody id="employeeTableBody"></tbody>
                </table>
            </div>
            <div style="padding:10px 20px;border-top:1px solid var(--border);font-size:0.7rem;color:var(--text2);">
                <span>查看密码: <strong>employee2024</strong></span>
            </div>
        </div>
    </div>

    <div id="adminPanel" class="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="exportJsonBtn">&#128229; 导出 JSON</button>
                    <label class="file-input-label" for="importJsonInput" style="background:var(--primary);color:#fff;">&#128228; 导入 JSON</label>
                    <input type="file" id="importJsonInput" accept=".json">
                </div>
            </div>
            <div class="db-status">
                <span>&#128192; 数据库已连接</span>
                <span id="dbStatusText">localStorage 持久化存储</span>
            </div>

            <!-- 自动备份设置 -->
            <div class="backup-settings">
                <div>
                    <span class="label">&#128190; 自动备份到下载文件夹</span>
                    <span class="desc">每次修改数据后自动导出 JSON 文件到电脑下载目录</span>
                </div>
                <div class="toggle">
                    <span id="autoBackupStatus" style="font-size:0.75rem;color:var(--text2);">已关闭</span>
                    <button class="toggle-switch" id="autoBackupToggle" title="点击切换自动备份"></button>
                </div>
            </div>

            <!-- 存储信息 -->
            <div class="storage-info">
                <span><span class="label">&#128190; 存储位置:</span>浏览器 localStorage</span>
                <span><span class="label">&#128202; 记录数:</span><span id="recordCount">0</span> 条</span>
                <span><span class="label">&#128204; 持久化:</span><span class="status">&#9989; 刷新/关闭不丢失</span></span>
                <span class="backup-status" id="lastBackupTime">&#128203; 最后备份:无</span>
            </div>

            <div class="scroll-x">
                <table>
                    <thead><tr><th>网址</th><th>账号</th><th>密码</th><th>浏览器</th><th>操作</th></tr></thead>
                    <tbody id="adminTableBody"></tbody>
                </table>
            </div>
            <div class="settings-group">
                <h4>&#9881;&#65039; 安全设置</h4>
                <div class="settings-row"><span>员工查看密码</span><input type="password" id="newEmployeePwd" placeholder="新密码" style="width:160px;"><button class="btn btn-warning" id="updateEmployeePwdBtn">修改</button></div>
                <div class="settings-row"><span>管理员密码</span><input type="password" id="newAdminPwd" placeholder="新密码" style="width:160px;"><button class="btn btn-warning" id="updateAdminPwdBtn">修改</button></div>
            </div>
            <div style="padding:14px 20px;border-top:1px solid var(--border);font-size:0.75rem;color:var(--text2);">
                <span>管理员: <strong>admin123</strong></span>
                <span style="margin-left:16px;">员工: <strong>employee2024</strong></span>
                <span style="margin-left:16px;">&#128161; 开启「自动备份」后每次修改自动导出 JSON 到下载文件夹</span>
            </div>
        </div>
    </div>
</div>

<!-- 浏览器提示弹窗 -->
<div class="browser-hint-overlay" id="browserHintOverlay">
    <div class="browser-hint-box">
        <div class="icon" id="hintIcon">&#127760;</div>
        <h3 id="hintTitle">浏览器</h3>
        <p id="hintDesc">请先确认电脑已安装该浏览器,如未安装请先下载。</p>
        <div class="checkbox-row">
            <input type="checkbox" id="hintDontShowAgain">
            <label for="hintDontShowAgain">下次不再提示</label>
        </div>
        <div class="btn-group">
            <button class="btn btn-outline" id="hintCancel">取消</button>
            <button class="btn btn-warning" id="hintDownload">&#11015; 下载浏览器</button>
            <button class="btn btn-primary" id="hintConfirm">确定,已安装</button>
        </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">Chrome</option>
                <option value="firefox">Firefox</option>
            </select>
        </div>
        <input type="hidden" id="editId" value="">
        <div style="display:flex;gap:10px;justify-content:flex-end;margin-top:16px;">
            <button class="btn btn-outline" id="closeModalBtn">取消</button>
            <button class="btn btn-primary" id="saveCredBtn">保存</button>
        </div>
    </div>
</div>

<div id="toast" class="toast">操作成功</div>

<script>
    function h(s) { return CryptoJS.SHA256(s).toString(); }

    // ===== 浏览器配置 =====
    var BROWSER_CONFIG = {
        edge: {
            icon: '&#128311;',
            name: 'Edge 浏览器',
            desc: '使用前提:已安装Edge浏览器。(未安装请先下载安装)。',
            download: 'https://www.microsoft.com/zh-cn/edge/download',
            protocol: 'microsoft-edge:'
        },
        chrome: {
            icon: '&#128308;',
            name: '谷歌浏览器',
            desc: '使用前提:已安装谷歌浏览器。(未安装请先下载安装)。',
            download: 'https://sw.pcmgr.qq.com/62a4683d5d6deb5f13daf8e29b0d90dd/6a962b0c/spcmgr/download/152.0.7977.65_chrome_installer_uncompressed-win64.exe',
            protocol: 'googlechrome:'
        },
        firefox: {
            icon: '&#129418;',
            name: '火狐浏览器',
            desc: '使用前提:已安装火狐浏览器。(未安装请先下载安装)。',
            download: 'https://sw.pcmgr.qq.com/e38a6e930fd7892da4c3d82d122394cb/6a962af4/spcmgr/download/22931-110819-Firefox-setup-win64_116.0.3.8627.exe',
            protocol: 'firefox:'
        }
    };

    // ===== 浏览器提示弹窗控制 =====
    var hintOverlay = document.getElementById('browserHintOverlay');
    var hintDontShowAgain = document.getElementById('hintDontShowAgain');
    var hintIcon = document.getElementById('hintIcon');
    var hintTitle = document.getElementById('hintTitle');
    var hintDesc = document.getElementById('hintDesc');
    var hintDownload = document.getElementById('hintDownload');
    var hintConfirm = document.getElementById('hintConfirm');
    var hintCancel = document.getElementById('hintCancel');
    var pendingBrowser = null;
    var pendingUrl = null;

    function getHintSetting(browser) {
        return localStorage.getItem('hint_dismissed_' + browser) === 'true';
    }

    function setHintSetting(browser, dismissed) {
        localStorage.setItem('hint_dismissed_' + browser, dismissed ? 'true' : 'false');
    }

    function showBrowserHint(browser, url) {
        if (getHintSetting(browser)) {
            doOpenBrowser(browser, url);
            return;
        }
        var config = BROWSER_CONFIG[browser];
        if (!config) { window.open(url, '_blank'); return; }
        pendingBrowser = browser;
        pendingUrl = url;
        hintIcon.textContent = config.icon;
        hintTitle.textContent = config.name;
        hintDesc.textContent = config.desc;
        hintDownload.textContent = '&#11015; 下载 ' + config.name;
        hintOverlay.classList.add('show');
        hintDontShowAgain.checked = false;
    }

    function hideBrowserHint() {
        hintOverlay.classList.remove('show');
    }

    function doOpenBrowser(b, u) {
        var config = BROWSER_CONFIG[b];
        if (!config) { window.open(u, '_blank'); return; }
        var url = u;
        if (!url.startsWith('http://') && !url.startsWith('https://')) {
            url = 'https://' + url;
        }
        var fullUrl = config.protocol + url;
        try {
            var currentUrl = window.location.href;
            window.location.href = fullUrl;
            setTimeout(function() {
                if (window.location.href === currentUrl) {
                    tryMethod2(b, url);
                }
            }, 800);
        } catch(e) {
            tryMethod2(b, url);
        }
    }

    function tryMethod2(b, u) {
        var config = BROWSER_CONFIG[b];
        var url = u;
        if (!url.startsWith('http://') && !url.startsWith('https://')) {
            url = 'https://' + url;
        }
        var protocols = [
            config.protocol + url,
            config.protocol + '//open?url=' + encodeURIComponent(url),
            config.protocol + '//' + url
        ];
        for (var i = 0; i < protocols.length; i++) {
            try {
                var w = window.open(protocols[i], '_blank');
                if (w) {
                    setTimeout(function(win) {
                        try { if (win && win.closed) { tryMethod3(b, u); } } catch(e) { tryMethod3(b, u); }
                    }, 1500, w);
                    return;
                }
            } catch(e) {}
        }
        tryMethod3(b, u);
    }

    function tryMethod3(b, u) {
        var config = BROWSER_CONFIG[b];
        var url = u;
        if (!url.startsWith('http://') && !url.startsWith('https://')) {
            url = 'https://' + url;
        }
        var fullUrl = config.protocol + url;
        try {
            var iframe = document.createElement('iframe');
            iframe.style.display = 'none';
            iframe.src = fullUrl;
            document.body.appendChild(iframe);
            setTimeout(function() {
                try { document.body.removeChild(iframe); } catch(e) {}
                showDownloadDialog(b, u);
            }, 2000);
        } catch(e) {
            showDownloadDialog(b, u);
        }
    }

    function showDownloadDialog(b, u) {
        var config = BROWSER_CONFIG[b];
        var name = config ? config.name : '该浏览器';
        var url = config ? config.download : null;
        if (url) {
            if (confirm('未检测到 ' + name + ',是否下载?\n\n点击「确定」前往下载\n点击「取消」使用默认浏览器打开')) {
                window.open(url, '_blank');
            } else {
                window.open(u, '_blank');
            }
        } else {
            window.open(u, '_blank');
        }
    }

    hintConfirm.addEventListener('click', function() {
        if (hintDontShowAgain.checked && pendingBrowser) {
            setHintSetting(pendingBrowser, true);
        }
        hideBrowserHint();
        if (pendingUrl && pendingBrowser) {
            setTimeout(function() { doOpenBrowser(pendingBrowser, pendingUrl); }, 150);
        }
    });

    hintCancel.addEventListener('click', function() {
        hideBrowserHint();
        pendingBrowser = null;
        pendingUrl = null;
    });

    hintDownload.addEventListener('click', function() {
        if (pendingBrowser) {
            var config = BROWSER_CONFIG[pendingBrowser];
            if (config && config.download) {
                window.open(config.download, '_blank');
            }
        }
    });

    hintOverlay.addEventListener('click', function(e) {
        if (e.target === hintOverlay) {
            hideBrowserHint();
            pendingBrowser = null;
            pendingUrl = null;
        }
    });

    // ===== 浏览器打开入口 =====
    function bn(b) {
        var map = { edge:'Edge浏览器打开', chrome:'谷歌浏览器打开', firefox:'火狐浏览器打开' };
        return map[b] || '默认浏览器打开';
    }

    function bc(b) {
        var map = { edge:'browser-edge', chrome:'browser-chrome', firefox:'browser-firefox' };
        return map[b] || 'browser-default';
    }

    function openWith(b, u) {
        if (b === 'default' || !b) { window.open(u, '_blank'); return; }
        showBrowserHint(b, u);
    }

    function openHTML(b, u) {
        var name = bn(b);
        var cls = bc(b);
        var su = u.replace(/'/g, "\\'");
        var html = '<div class="open-btn-group">';
        if (b === 'default') {
            html += '<button class="btn btn-open btn-sm">打开</button>';
        } else {
            html += '<button class="btn btn-open btn-sm">' + name + '</button>';
            html += '<button class="btn btn-outline btn-sm">默认浏览器打开</button>';
        }
        html += '</div>';
        return html;
    }

    function copyText(t, msg) {
        try {
            navigator.clipboard.writeText(t);
            toast(msg);
        } catch(e) {
            var ta = document.createElement('textarea');
            ta.value = t;
            document.body.appendChild(ta);
            ta.select();
            document.execCommand('copy');
            document.body.removeChild(ta);
            toast(msg);
        }
    }

    // ============================================================
    // ===== 数据存储层 - localStorage 持久化存储 =====
    // ============================================================

    var STORAGE_KEY = 'password_manager_data';
    var CONFIG_KEY = 'password_manager_config';
    var empVisible = false;
    var adminAuth = false;
    var empHash = '';
    var adminHash = '';
    var autoBackupEnabled = false;
    var lastBackupTime = null;
    var credentials = [];
    var nextId = 1;

    // 默认密码
    var DEF_EMP = h('employee2024');
    var DEF_ADMIN = h('admin123');

    // ===== 数据操作 =====

    function loadData() {
        try {
            var saved = localStorage.getItem(STORAGE_KEY);
            if (saved) {
                var parsed = JSON.parse(saved);
                credentials = parsed.credentials || [];
                nextId = parsed.nextId || 1;
                return true;
            }
        } catch(e) {
            console.error('加载数据失败:', e);
        }
        return false;
    }

    function saveData() {
        try {
            var data = {
                credentials: credentials,
                nextId: nextId
            };
            localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
            return true;
        } catch(e) {
            console.error('保存数据失败:', e);
            return false;
        }
    }

    function loadConfig() {
        try {
            var saved = localStorage.getItem(CONFIG_KEY);
            if (saved) {
                var parsed = JSON.parse(saved);
                empHash = parsed.empHash || DEF_EMP;
                adminHash = parsed.adminHash || DEF_ADMIN;
                autoBackupEnabled = parsed.autoBackupEnabled || false;
                return true;
            }
        } catch(e) {
            console.error('加载配置失败:', e);
        }
        return false;
    }

    function saveConfig() {
        try {
            var data = {
                empHash: empHash,
                adminHash: adminHash,
                autoBackupEnabled: autoBackupEnabled
            };
            localStorage.setItem(CONFIG_KEY, JSON.stringify(data));
            return true;
        } catch(e) {
            console.error('保存配置失败:', e);
            return false;
        }
    }

    function initData() {
        var loaded = loadData();
        var configLoaded = loadConfig();

        if (!loaded) {
            credentials = [
                { id: 1, url: 'https://mail.company.com', username: 'zhang.san@company.com', password: 'Mail@2024Secure', browser: 'default' },
                { id: 2, url: 'https://hrsystem.company.com', username: 'hr_admin', password: 'HrP@ss#2024', browser: 'edge' },
                { id: 3, url: 'https://gitlab.company.com', username: 'dev_lihua', password: 'GitLab!7890', browser: 'chrome' },
                { id: 4, url: 'https://jira.company.com', username: 'project_manager', password: 'Jira@2025', browser: 'firefox' }
            ];
            nextId = 5;
            saveData();
        }

        if (!configLoaded) {
            empHash = DEF_EMP;
            adminHash = DEF_ADMIN;
            autoBackupEnabled = false;
            saveConfig();
        }

        updateStorageInfo();
        updateLastBackupTime();
        updateAutoBackupUI();
    }

    // ===== 自动备份 =====

    function getAutoBackupSetting() {
        return autoBackupEnabled;
    }

    function setAutoBackupSetting(enabled) {
        autoBackupEnabled = enabled;
        saveConfig();
        updateAutoBackupUI();
    }

    function updateAutoBackupUI() {
        var toggle = document.getElementById('autoBackupToggle');
        var status = document.getElementById('autoBackupStatus');
        if (toggle) {
            toggle.classList.toggle('active', autoBackupEnabled);
        }
        if (status) {
            status.textContent = autoBackupEnabled ? '&#9989; 已开启' : '&#9940; 已关闭';
            status.style.color = autoBackupEnabled ? 'var(--success)' : 'var(--text2)';
        }
    }

    function exportToDownloadFolder() {
        try {
            var data = {
                credentials: credentials,
                nextId: nextId,
                exportTime: new Date().toISOString(),
                version: '1.0'
            };
            var json = JSON.stringify(data, null, 2);
            var blob = new Blob([json], { type: 'application/json' });
            var url = URL.createObjectURL(blob);
            var a = document.createElement('a');
            a.href = url;
            var now = new Date();
            var dateStr = now.getFullYear() + '-' +
                String(now.getMonth()+1).padStart(2,'0') + '-' +
                String(now.getDate()).padStart(2,'0') + '_' +
                String(now.getHours()).padStart(2,'0') + '-' +
                String(now.getMinutes()).padStart(2,'0');
            a.download = 'password_backup_' + dateStr + '.json';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            setTimeout(function() {
                URL.revokeObjectURL(url);
            }, 5000);

            lastBackupTime = now;
            updateLastBackupTime();
            return true;
        } catch(e) {
            console.error('自动备份失败:', e);
            return false;
        }
    }

    function updateLastBackupTime() {
        var el = document.getElementById('lastBackupTime');
        if (!el) return;
        if (lastBackupTime) {
            var str = lastBackupTime.getFullYear() + '-' +
                String(lastBackupTime.getMonth()+1).padStart(2,'0') + '-' +
                String(lastBackupTime.getDate()).padStart(2,'0') + ' ' +
                String(lastBackupTime.getHours()).padStart(2,'0') + ':' +
                String(lastBackupTime.getMinutes()).padStart(2,'0');
            el.textContent = '&#128203; 最后备份:' + str;
        } else {
            el.textContent = '&#128203; 最后备份:无';
        }
    }

    // ===== 增删改查 =====

    function allCreds() {
        return credentials.slice().sort(function(a, b) { return b.id - a.id; });
    }

    function getCredById(id) {
        for (var i = 0; i < credentials.length; i++) {
            if (credentials[i].id === id) return credentials[i];
        }
        return null;
    }

    function upsert(id, url, username, password, browser) {
        if (!url || !username || !password) return false;

        if (id === null || id === undefined || id === '') {
            var newCred = {
                id: nextId++,
                url: url,
                username: username,
                password: password,
                browser: browser || 'default'
            };
            credentials.push(newCred);
        } else {
            var found = false;
            for (var i = 0; i < credentials.length; i++) {
                if (credentials[i].id === id) {
                    credentials[i].url = url;
                    credentials[i].username = username;
                    credentials[i].password = password;
                    credentials[i].browser = browser || 'default';
                    found = true;
                    break;
                }
            }
            if (!found) return false;
        }

        saveData();
        updateStorageInfo();

        if (autoBackupEnabled) {
            exportToDownloadFolder();
        }

        return true;
    }

    function delCred(id) {
        for (var i = 0; i < credentials.length; i++) {
            if (credentials[i].id === id) {
                credentials.splice(i, 1);
                saveData();
                updateStorageInfo();
                if (autoBackupEnabled) {
                    exportToDownloadFolder();
                }
                return true;
            }
        }
        return false;
    }

    function batchUpsert(records) {
        var cnt = 0;
        for (var i = 0; i < records.length; i++) {
            var rec = records[i];
            if (rec.url && rec.username && rec.password) {
                var exists = false;
                for (var j = 0; j < credentials.length; j++) {
                    if (credentials[j].url === rec.url) {
                        credentials[j].username = rec.username;
                        credentials[j].password = rec.password;
                        credentials[j].browser = rec.browser || 'default';
                        exists = true;
                        cnt++;
                        break;
                    }
                }
                if (!exists) {
                    credentials.push({
                        id: nextId++,
                        url: rec.url,
                        username: rec.username,
                        password: rec.password,
                        browser: rec.browser || 'default'
                    });
                    cnt++;
                }
            }
        }
        saveData();
        updateStorageInfo();
        if (autoBackupEnabled) {
            exportToDownloadFolder();
        }
        return cnt;
    }

    function updateStorageInfo() {
        var el = document.getElementById('recordCount');
        if (el) el.textContent = credentials.length;
    }

    // ===== 密码配置 =====

    function getEmpHash() { return empHash; }
    function getAdminHash() { return adminHash; }

    function setEmpHash(newHash) {
        empHash = newHash;
        saveConfig();
    }

    function setAdminHash(newHash) {
        adminHash = newHash;
        saveConfig();
    }

    // ===== 导出/导入 JSON =====

    function exportJson() {
        try {
            var data = {
                credentials: credentials,
                nextId: nextId,
                exportTime: new Date().toISOString(),
                version: '1.0'
            };
            var json = JSON.stringify(data, null, 2);
            var blob = new Blob([json], { type: 'application/json' });
            var url = URL.createObjectURL(blob);
            var a = document.createElement('a');
            a.href = url;
            var now = new Date();
            var dateStr = now.getFullYear() + '-' +
                String(now.getMonth()+1).padStart(2,'0') + '-' +
                String(now.getDate()).padStart(2,'0') + '_' +
                String(now.getHours()).padStart(2,'0') + '-' +
                String(now.getMinutes()).padStart(2,'0');
            a.download = 'password_data_' + dateStr + '.json';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            setTimeout(function() {
                URL.revokeObjectURL(url);
            }, 5000);
            toast('数据已导出到下载目录', 'success');
        } catch(e) {
            toast('导出失败: ' + e.message, true);
        }
    }

    function importJson(file) {
        var reader = new FileReader();
        reader.onload = function(e) {
            try {
                var data = JSON.parse(e.target.result);
                if (!data.credentials || !Array.isArray(data.credentials)) {
                    toast('无效的数据文件', true);
                    return;
                }

                if (!confirm('确定要恢复此数据吗?当前数据将被覆盖!')) {
                    return;
                }

                credentials = data.credentials;
                nextId = data.nextId || (credentials.length > 0 ? Math.max.apply(null, credentials.map(function(c) { return c.id; })) + 1 : 1);
                saveData();
                updateStorageInfo();
                if (autoBackupEnabled) {
                    exportToDownloadFolder();
                }
                renderEmp();
                renderAdmin();
                toast('数据恢复成功', 'success');
            } catch(err) {
                toast('无效的数据文件: ' + err.message, true);
            }
        };
        reader.readAsText(file);
    }

    // ===== 渲染 =====
    function renderEmp() {
        var tbody = document.getElementById('employeeTableBody');
        if (!tbody) return;
        var list = allCreds();
        if (!list.length) {
            tbody.innerHTML = '<tr class="empty-row"><td colspan="4">暂无数据</td></tr>';
            return;
        }
        var html = '';
        for (var i = 0; i < list.length; i++) {
            var item = list[i];
            var name = bn(item.browser);
            var cls = bc(item.browser);
            var pwd = empVisible ?
                '<span class="password-plain">' + esc(item.password) + '</span>' :
                '<span class="password-masked">●●●●●●●●</span>';
            html += '<tr><td><div class="url-cell"><a href="' + esc(item.url) + '" target="_blank" class="url-link">' + esc(item.url) + '</a><button class="copy-url-btn" data-url="' + esc(item.url) + '">复制</button></div></td>';
            html += '<td>' + esc(item.username) + '</td><td>' + pwd + '</td>';
            html += '<td><span class="browser-badge ' + cls + '">' + name + '</span><br>' + openHTML(item.browser, item.url) + '</td></tr>';
        }
        tbody.innerHTML = html;
        var btns = document.querySelectorAll('.copy-url-btn');
        for (var j = 0; j < btns.length; j++) {
            btns[j].addEventListener('click', function() { copyText(this.dataset.url, '网址已复制'); });
        }
    }

    function renderAdmin() {
        var tbody = document.getElementById('adminTableBody');
        if (!tbody) return;
        if (!adminAuth) {
            tbody.innerHTML = '<tr class="empty-row"><td colspan="5">请验证管理员权限</td></tr>';
            return;
        }
        var list = allCreds();
        if (!list.length) {
            tbody.innerHTML = '<tr class="empty-row"><td colspan="5">暂无数据,点击"新增"添加</td></tr>';
            return;
        }
        var html = '';
        for (var i = 0; i < list.length; i++) {
            var item = list[i];
            var name = bn(item.browser);
            var cls = bc(item.browser);
            html += '<tr><td><a href="' + esc(item.url) + '" target="_blank" class="url-link">' + esc(item.url) + '</a></td>';
            html += '<td>' + esc(item.username) + '</td>';
            html += '<td><span class="password-plain">' + esc(item.password) + '</span></td>';
            html += '<td><span class="browser-badge ' + cls + '">' + name + '</span><br>' + openHTML(item.browser, item.url) + '</td>';
            html += '<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;
        var eds = document.querySelectorAll('.edit-btn');
        for (var j = 0; j < eds.length; j++) {
            eds[j].addEventListener('click', function() { openEdit(parseInt(this.dataset.id)); });
        }
        var dels = document.querySelectorAll('.delete-btn');
        for (var k = 0; k < dels.length; k++) {
            dels[k].addEventListener('click', function() {
                if (confirm('确定删除?')) {
                    delCred(parseInt(this.dataset.id));
                    renderAdmin();
                    renderEmp();
                    toast('已删除');
                }
            });
        }
        updateStorageInfo();
    }

    function esc(s) { if (!s) return ''; return s.replace(/[&<>]/g, function(m) { return { '&':'&', '<':'<', '>':'>' }[m]; }); }

    function verifyEmp() {
        var inp = document.getElementById('empViewPwd');
        if (!inp) return;
        if (h(inp.value) === empHash) {
            empVisible = true;
            toast('验证成功', 'success');
            renderEmp();
        } else {
            empVisible = false;
            renderEmp();
            toast('密码错误', true);
        }
        inp.value = '';
    }

    function openEdit(id) {
        var item = getCredById(id);
        if (item) openModal(item);
    }

    function toast(msg, err) {
        var t = document.getElementById('toast');
        if (!t) return;
        t.textContent = msg;
        t.className = 'toast';
        if (err === true) t.classList.add('error');
        else if (err === 'success') t.classList.add('success');
        t.classList.add('show');
        clearTimeout(t._timer);
        t._timer = setTimeout(function() { t.classList.remove('show'); }, 3000);
    }

    // ===== 模态框 =====
    var modal = document.getElementById('credModal');
    var urlInp = document.getElementById('credUrl');
    var userInp = document.getElementById('credUsername');
    var pwdInp = document.getElementById('credPassword');
    var browserSel = document.getElementById('credBrowser');
    var editIdF = document.getElementById('editId');
    var modalTitle = document.getElementById('modalTitle');

    function openModal(entry) {
        if (!adminAuth) { toast('请先登录', true); return; }
        if (entry) {
            modalTitle.textContent = '编辑凭证';
            urlInp.value = entry.url;
            userInp.value = entry.username;
            pwdInp.value = entry.password;
            browserSel.value = entry.browser || 'default';
            editIdF.value = entry.id;
        } else {
            modalTitle.textContent = '添加凭证';
            urlInp.value = '';
            userInp.value = '';
            pwdInp.value = '';
            browserSel.value = 'default';
            editIdF.value = '';
        }
        modal.style.display = 'flex';
    }

    function closeModal() { modal.style.display = 'none'; }

    function saveCred() {
        if (!adminAuth) { toast('未授权', true); return; }
        var url = urlInp.value.trim();
        var user = userInp.value.trim();
        var pwd = pwdInp.value.trim();
        var browser = browserSel.value;
        if (!url || !user || !pwd) { toast('请填完整', true); return; }
        if (!url.startsWith('http://') && !url.startsWith('https://')) {
            toast('网址需 http:// 或 https:// 开头', true);
            return;
        }
        var id = editIdF.value;
        if (upsert(id ? parseInt(id) : null, url, user, pwd, browser)) {
            toast(id ? '已修改' : '已添加', 'success');
            closeModal();
            renderAdmin();
            renderEmp();
        } else { toast('操作失败', true); }
    }

    // ===== 导出导入 Excel =====
    function exportExcel() {
        if (!adminAuth) { toast('请先登录', true); return; }
        var list = allCreds();
        if (!list.length) { toast('无数据', true); return; }
        var data = [['网址', '账号', '密码', '浏览器']];
        for (var i = 0; i < list.length; i++) {
            data.push([list[i].url, list[i].username, list[i].password, list[i].browser]);
        }
        var ws = XLSX.utils.aoa_to_sheet(data);
        var wb = XLSX.utils.book_new();
        XLSX.utils.book_append_sheet(wb, ws, 'Credentials');
        XLSX.writeFile(wb, 'password_export_' + Date.now() + '.xls', { bookType: 'xls' });
        toast('导出成功', 'success');
    }

    function importExcel(file) {
        if (!adminAuth) { toast('请先登录', true); return; }
        var reader = new FileReader();
        reader.onload = function(e) {
            var data = new Uint8Array(e.target.result);
            var workbook = XLSX.read(data, { type: 'array' });
            var sheet = workbook.Sheets[workbook.SheetNames[0]];
            var rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '' });
            if (!rows || rows.length < 2) { toast('无效文件', true); return; }
            var headers = rows[0];
            var ui = -1;
            var ui2 = -1;
            var pi = -1;
            var bi = -1;
            for (var i = 0; i < headers.length; i++) {
                var s = String(headers[i]).toLowerCase();
                if (s.includes('网址') || s.includes('url')) ui = i;
                if (s.includes('账号') || s.includes('用户名')) ui2 = i;
                if (s.includes('密码')) pi = i;
                if (s.includes('浏览器')) bi = i;
            }
            if (ui === -1 || ui2 === -1 || pi === -1) { toast('缺少必要列', true); return; }
            var records = [];
            for (var j = 1; j < rows.length; j++) {
                var row = rows[j];
                if (row && row.length > Math.max(ui, ui2, pi)) {
                    var url = row[ui] ? String(row[ui]).trim() : '';
                    var user = row[ui2] ? String(row[ui2]).trim() : '';
                    var pwd = row[pi] ? String(row[pi]).trim() : '';
                    var browser = (bi !== -1 && row[bi]) ? String(row[bi]).trim() : 'default';
                    if (url && user && pwd && (url.startsWith('http://') || url.startsWith('https://'))) {
                        records.push({ url: url, username: user, password: pwd, browser: browser });
                    }
                }
            }
            if (!records.length) { toast('无有效数据', true); return; }
            var cnt = batchUpsert(records);
            renderAdmin();
            renderEmp();
            toast('导入 ' + cnt + ' 条', 'success');
        };
        reader.readAsArrayBuffer(file);
    }

    // ===== 管理员登录 =====
    function showAdminLogin() {
        if (adminAuth) return;
        var overlay = document.createElement('div');
        overlay.className = 'admin-auth-overlay';
        overlay.id = 'adminOverlay';
        overlay.innerHTML = '<div class="admin-auth-box"><h3>管理员验证</h3><input type="password" id="adminLoginPwd" placeholder="管理员密码" autocomplete="off"><div id="adminLoginError" class="error"></div><div style="display:flex;gap:10px;margin-top:16px;justify-content:center;"><button class="btn btn-outline" id="adminLoginCancel">取消</button><button class="btn btn-primary" id="adminLoginConfirm">确认</button></div></div>';
        document.body.appendChild(overlay);
        var inp = document.getElementById('adminLoginPwd');
        inp.focus();
        var doAuth = function() {
            if (h(inp.value) === adminHash) {
                adminAuth = true;
                document.body.removeChild(overlay);
                toast('验证成功', 'success');
                renderAdmin();
                var panel = document.getElementById('adminPanel');
                if (!panel.classList.contains('hidden')) renderAdmin();
            } else {
                document.getElementById('adminLoginError').textContent = '密码错误';
                inp.value = '';
                inp.focus();
            }
        };
        var cancel = function() {
            document.body.removeChild(overlay);
            if (!adminAuth) { switchTab('employee');
                toast('已取消', true); }
        };
        document.getElementById('adminLoginConfirm').addEventListener('click', doAuth);
        document.getElementById('adminLoginCancel').addEventListener('click', cancel);
        inp.addEventListener('keypress', function(e) { if (e.key === 'Enter') doAuth(); });
    }

    // ===== Tab切换 =====
    function switchTab(tab) {
        var admin = document.getElementById('adminPanel');
        var emp = document.getElementById('employeePanel');
        var btns = document.querySelectorAll('.tab-btn');
        if (tab === 'employee') {
            admin.classList.add('hidden');
            emp.classList.remove('hidden');
            renderEmp();
            btns.forEach(function(b) { b.classList.remove('active'); });
            document.querySelector('.tab-btn[data-tab="employee"]').classList.add('active');
        } else {
            if (!adminAuth) {
                showAdminLogin();
                var iv = setInterval(function() {
                    if (adminAuth) {
                        clearInterval(iv);
                        admin.classList.remove('hidden');
                        emp.classList.add('hidden');
                        renderAdmin();
                        btns.forEach(function(b) { b.classList.remove('active'); });
                        document.querySelector('.tab-btn[data-tab="admin"]').classList.add('active');
                    }
                }, 200);
                setTimeout(function() { clearInterval(iv); }, 30000);
            } else {
                admin.classList.remove('hidden');
                emp.classList.add('hidden');
                renderAdmin();
                btns.forEach(function(b) { b.classList.remove('active'); });
                document.querySelector('.tab-btn[data-tab="admin"]').classList.add('active');
            }
        }
    }

    // ===== 主题切换 =====
    function setTheme(name) {
        var root = document.documentElement;
        var prev = document.querySelector('.theme-dot.active');
        if (prev) prev.classList.remove('active');
        var dot = document.querySelector('.theme-dot[data-theme="' + name + '"]');
        if (dot) dot.classList.add('active');
        root.className = '';
        if (name !== 'default') root.className = 'theme-' + name;
        localStorage.setItem('pwd_theme', name);
    }

    // ===== 事件绑定 =====
    document.addEventListener('DOMContentLoaded', function() {
        var dots = document.querySelectorAll('.theme-dot');
        dots.forEach(function(d) {
            d.addEventListener('click', function() { setTheme(this.dataset.theme); });
        });
        var saved = localStorage.getItem('pwd_theme');
        if (saved) setTheme(saved);

        // 自动备份开关
        var toggleBtn = document.getElementById('autoBackupToggle');
        if (toggleBtn) {
            toggleBtn.addEventListener('click', function() {
                autoBackupEnabled = !autoBackupEnabled;
                setAutoBackupSetting(autoBackupEnabled);
                toast(autoBackupEnabled ? '自动备份已开启' : '自动备份已关闭', 'success');
                if (autoBackupEnabled && credentials.length > 0) {
                    exportToDownloadFolder();
                    toast('已备份到下载文件夹', 'success');
                }
            });
        }

        var btnOpenAdd = document.getElementById('openAddCredBtn');
        var btnCloseModal = document.getElementById('closeModalBtn');
        var btnSaveCred = document.getElementById('saveCredBtn');
        var btnVerifyEmp = document.getElementById('verifyEmpBtn');
        var inpEmpPwd = document.getElementById('empViewPwd');
        var btnExportExcel = document.getElementById('exportExcelBtn');
        var btnExportJson = document.getElementById('exportJsonBtn');
        var inpImportExcel = document.getElementById('importExcelInput');
        var inpImportJson = document.getElementById('importJsonInput');
        var btnUpdateEmpPwd = document.getElementById('updateEmployeePwdBtn');
        var btnUpdateAdminPwd = document.getElementById('updateAdminPwdBtn');

        if (btnOpenAdd) btnOpenAdd.addEventListener('click', function() { openModal(null); });
        if (btnCloseModal) btnCloseModal.addEventListener('click', closeModal);
        if (btnSaveCred) btnSaveCred.addEventListener('click', saveCred);
        if (btnVerifyEmp) btnVerifyEmp.addEventListener('click', verifyEmp);
        if (inpEmpPwd) inpEmpPwd.addEventListener('keypress', function(e) { if (e.key === 'Enter') verifyEmp(); });
        if (btnExportExcel) btnExportExcel.addEventListener('click', exportExcel);
        if (btnExportJson) btnExportJson.addEventListener('click', exportJson);
        if (inpImportExcel) inpImportExcel.addEventListener('change', function(e) { if (this.files.length) importExcel(this.files[0]);
            this.value = ''; });
        if (inpImportJson) inpImportJson.addEventListener('change', function(e) { if (this.files.length) importJson(this.files[0]);
            this.value = ''; });
        if (btnUpdateEmpPwd) {
            btnUpdateEmpPwd.addEventListener('click', function() {
                var inp = document.getElementById('newEmployeePwd');
                var p = inp.value;
                if (!p || p.length < 4) { toast('密码至少4位', true);
                    inp.value = ''; return; }
                setEmpHash(h(p));
                empVisible = false;
                renderEmp();
                toast('员工密码已修改', 'success');
                inp.value = '';
            });
        }
        if (btnUpdateAdminPwd) {
            btnUpdateAdminPwd.addEventListener('click', function() {
                var inp = document.getElementById('newAdminPwd');
                var p = inp.value;
                if (!p || p.length < 4) { toast('密码至少4位', true);
                    inp.value = ''; return; }
                setAdminHash(h(p));
                toast('管理员密码已修改', 'success');
                inp.value = '';
            });
        }
        document.querySelectorAll('.tab-btn').forEach(function(btn) {
            btn.addEventListener('click', function() { switchTab(this.dataset.tab); });
        });
        window.onclick = function(e) { if (e.target === modal) closeModal(); };
    });

    // ===== 启动 =====
    function boot() {
        try {
            initData();
            empVisible = false;
            adminAuth = false;
            renderEmp();
            renderAdmin();
            switchTab('employee');
            updateStorageInfo();
            updateLastBackupTime();
            toast('系统已启动,数据永久保存在 localStorage', 'success');
        } catch(err) {
            toast('初始化失败: ' + err.message, true);
            console.error(err);
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', boot);
    } else {
        boot();
    }
</script>
</body>
</html>



由于个人对HTML网页代码的理解还是欠缺,大家有什么新的建议可提出,或有什么错误的地方,麻烦大家告知,我虚心求教。

免费评分

参与人数 1热心值 +1 收起 理由
cd1688 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
 楼主| kissfox 发表于 2026-9-2 13:26 |楼主
本帖最后由 kissfox 于 2026-9-2 13:46 编辑

应各位要求修改增加:
1、利用deepseek对整个网页代码进行UI美化并实现H5自适应
2、增加站内搜索功能,站内搜索框输入关键词即可搜索所需内容
3、增加网址名称,员工门户增加网址名称显示,后台网址添加中增加网址名称填写
4、增加导入密码文件csv文件,后台增加导入火狐、谷歌、edge等浏览器导出的密码文件csv文件并选择指定浏览器方式打开

[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes">
    <title>密码管理系统</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 id="theme-style">
        /* ===== CSS 变量 ===== */
        :root {
            --bg: #f0f4f8;
            --card: #ffffff;
            --border: #e2e8f0;
            --text: #1a202c;
            --text2: #4a5568;
            --text3: #a0aec0;
            --shadow: 0 4px 20px rgba(0,0,0,0.06);
            --primary: #2d3748;
            --primary-light: #4a5568;
            --primary-hover: #1a202c;
            --input: #ffffff;
            --input-border: #e2e8f0;
            --table-header: #f7fafc;
            --badge-default: #edf2f7;
            --badge-edge: #0078d4;
            --badge-chrome: #4285f4;
            --badge-firefox: #ff9400;
            --success: #38a169;
            --warning: #d69e2e;
            --danger: #e53e3e;
            --info: #3182ce;
            --radius: 16px;
            --radius-sm: 10px;
        }
        .theme-red { --bg: #fff5f5; --card: #ffffff; --primary: #c53030; --primary-light: #e53e3e; --primary-hover: #9b2c2c; --border: #fed7d7; --table-header: #fff5f5; }
        .theme-orange { --bg: #fffaf0; --card: #ffffff; --primary: #c05621; --primary-light: #dd6b20; --primary-hover: #9c4221; --border: #feebc8; --table-header: #fffaf0; }
        .theme-yellow { --bg: #fffff0; --card: #ffffff; --primary: #975a16; --primary-light: #d69e2e; --primary-hover: #744210; --border: #fefcbf; --table-header: #fffff0; }
        .theme-green { --bg: #f0fff4; --card: #ffffff; --primary: #276749; --primary-light: #38a169; --primary-hover: #22543d; --border: #c6f6d5; --table-header: #f0fff4; }
        .theme-blue { --bg: #ebf8ff; --card: #ffffff; --primary: #2a69ac; --primary-light: #3182ce; --primary-hover: #2c5282; --border: #bee3f8; --table-header: #ebf8ff; }
        .theme-purple { --bg: #faf5ff; --card: #ffffff; --primary: #6b46c1; --primary-light: #805ad5; --primary-hover: #553c9a; --border: #e9d8fd; --table-header: #faf5ff; }
        .theme-pink { --bg: #fff5f7; --card: #ffffff; --primary: #b83280; --primary-light: #d53f8c; --primary-hover: #97266d; --border: #fed7e2; --table-header: #fff5f7; }

        /* ===== 基础样式 ===== */
        * { margin: 0; padding: 0; box-sizing: border-box; }
        html { font-size: 16px; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif;
            background: var(--bg);
            padding: 16px;
            transition: background 0.3s, color 0.3s;
            color: var(--text);
            min-height: 100vh;
        }
        .app { max-width: 1440px; margin: 0 auto; }

        /* ===== 头部 ===== */
        .header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 12px;
            margin-bottom: 20px;
            padding: 8px 4px;
        }
        .header h1 {
            font-size: clamp(1.2rem, 4vw, 1.8rem);
            font-weight: 700;
            color: var(--text);
            letter-spacing: -0.02em;
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .header h1 span { background: var(--primary); color: #fff; font-size: 0.5em; padding: 2px 10px; border-radius: 20px; font-weight: 500; }
        .theme-bar {
            display: flex;
            gap: 5px;
            flex-wrap: wrap;
            align-items: center;
            background: var(--card);
            padding: 4px 12px 4px 16px;
            border-radius: 30px;
            border: 1px solid var(--border);
            box-shadow: var(--shadow);
        }
        .theme-bar .label { font-size: 0.7rem; color: var(--text3); margin-right: 2px; }
        .theme-dot {
            width: 26px;
            height: 26px;
            border-radius: 50%;
            border: 2px solid transparent;
            cursor: pointer;
            transition: all 0.2s;
            flex-shrink: 0;
        }
        .theme-dot:hover { transform: scale(1.12); }
        .theme-dot.active { border-color: var(--text); box-shadow: 0 0 0 2px var(--card), 0 0 0 4px var(--primary); }

        /* ===== Tab 栏 ===== */
        .tab-bar {
            display: flex;
            gap: 6px;
            margin-bottom: 20px;
            background: var(--card);
            padding: 4px;
            border-radius: 40px;
            width: fit-content;
            max-width: 100%;
            flex-wrap: wrap;
            border: 1px solid var(--border);
            box-shadow: var(--shadow);
        }
        .tab-btn {
            border: none;
            background: transparent;
            padding: 10px 24px;
            font-size: clamp(0.85rem, 2vw, 0.95rem);
            font-weight: 600;
            border-radius: 30px;
            cursor: pointer;
            color: var(--text2);
            transition: all 0.25s;
            white-space: nowrap;
        }
        .tab-btn:hover:not(.active) { background: var(--bg); color: var(--text); }
        .tab-btn.active { background: var(--primary); color: #fff; box-shadow: 0 2px 12px rgba(45, 55, 72, 0.25); }

        /* ===== 卡片 ===== */
        .card {
            background: var(--card);
            border-radius: var(--radius);
            border: 1px solid var(--border);
            overflow: hidden;
            margin-bottom: 16px;
            box-shadow: var(--shadow);
            transition: background 0.3s, border 0.3s;
        }
        .card-header {
            padding: 16px 20px;
            border-bottom: 1px solid var(--border);
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 10px;
            background: var(--table-header);
        }
        .card-header h2 {
            font-size: clamp(1rem, 2.5vw, 1.2rem);
            font-weight: 600;
            color: var(--text);
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .card-body { padding: 16px 20px; }
        .card-footer {
            padding: 12px 20px;
            border-top: 1px solid var(--border);
            font-size: 0.75rem;
            color: var(--text2);
            background: var(--table-header);
            display: flex;
            justify-content: space-between;
            flex-wrap: wrap;
            gap: 6px;
        }

        /* ===== 按钮 ===== */
        .btn {
            border: none;
            background: var(--bg);
            padding: 8px 20px;
            border-radius: 30px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s;
            font-size: clamp(0.75rem, 1.5vw, 0.85rem);
            color: var(--text);
            display: inline-flex;
            align-items: center;
            gap: 6px;
            white-space: nowrap;
            border: 1px solid transparent;
        }
        .btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
        .btn:active { transform: translateY(0); }
        .btn-primary { background: var(--primary); color: #fff; }
        .btn-primary:hover { background: var(--primary-hover); }
        .btn-outline { background: transparent; border-color: var(--border); }
        .btn-outline:hover { background: var(--bg); }
        .btn-success { background: var(--success); color: #fff; }
        .btn-success:hover { background: #2f855a; }
        .btn-warning { background: var(--warning); color: #fff; }
        .btn-warning:hover { background: #b7791f; }
        .btn-danger { background: var(--danger); color: #fff; }
        .btn-danger:hover { background: #c53030; }
        .btn-info { background: var(--info); color: #fff; }
        .btn-info:hover { background: #2b6cb0; }
        .btn-sm { padding: 4px 12px; font-size: 0.7rem; }
        .btn-open { background: #e0f7fa; color: #00838f; }
        .btn-open:hover { background: #b2ebf2; }
        .btn-clear { background: var(--bg); color: var(--text3); }
        .btn-clear:hover { background: var(--border); color: var(--text); }

        /* ===== 表格 ===== */
        .table-wrap { overflow-x: auto; padding: 0; }
        table {
            width: 100%;
            border-collapse: collapse;
            font-size: clamp(0.8rem, 1.4vw, 0.9rem);
            min-width: 600px;
        }
        th, td {
            text-align: left;
            padding: 12px 14px;
            border-bottom: 1px solid var(--border);
            vertical-align: middle;
            word-break: break-word;
        }
        th {
            background: var(--table-header);
            font-weight: 600;
            color: var(--text2);
            font-size: 0.8rem;
            text-transform: uppercase;
            letter-spacing: 0.03em;
            position: sticky;
            top: 0;
            z-index: 5;
        }
        tr:hover td { background: var(--table-header); }
        .url-link {
            color: var(--primary-light);
            text-decoration: none;
            font-weight: 500;
            font-size: 0.85rem;
            transition: color 0.2s;
        }
        .url-link:hover { color: var(--primary); text-decoration: underline; }
        .url-name { font-weight: 600; color: var(--text); display: block; margin-bottom: 2px; }
        .url-cell .url-name { font-size: clamp(0.85rem, 1.2vw, 0.95rem); }
        .url-cell .url-link { font-size: clamp(0.7rem, 1vw, 0.8rem); }

        .password-masked {
            font-family: 'SF Mono', 'Menlo', 'Monaco', monospace;
            background: var(--bg);
            padding: 2px 12px;
            border-radius: 20px;
            display: inline-block;
            font-size: 0.85rem;
            letter-spacing: 2px;
        }
        .password-plain {
            font-family: 'SF Mono', 'Menlo', 'Monaco', monospace;
            background: #ebf8ff;
            padding: 2px 12px;
            border-radius: 20px;
            display: inline-block;
            font-size: 0.85rem;
            word-break: break-all;
        }

        .browser-badge {
            display: inline-block;
            padding: 2px 12px;
            border-radius: 20px;
            font-size: 0.65rem;
            font-weight: 600;
            margin-right: 4px;
            text-transform: uppercase;
            letter-spacing: 0.03em;
        }
        .browser-default { background: var(--badge-default); color: var(--text2); }
        .browser-edge { background: var(--badge-edge); color: #fff; }
        .browser-chrome { background: var(--badge-chrome); color: #fff; }
        .browser-firefox { background: var(--badge-firefox); color: #fff; }

        .copy-url-btn {
            background: var(--bg);
            border: none;
            padding: 2px 10px;
            border-radius: 14px;
            cursor: pointer;
            font-size: 0.6rem;
            color: var(--text3);
            transition: 0.2s;
        }
        .copy-url-btn:hover { background: var(--border); color: var(--text); }

        /* ===== 打开方式按钮组 - 恢复原样 ===== */
        .open-btn-group {
            display: flex;
            gap: 6px;
            flex-wrap: wrap;
            align-items: center;
            margin-top: 4px;
        }
        .open-btn-group .btn {
            padding: 4px 14px;
            font-size: 0.7rem;
            border-radius: 20px;
        }
        .open-btn-group .btn-open {
            background: #22d3ee;
            color: #0e7490;
            border: none;
        }
        .open-btn-group .btn-open:hover {
            background: #67e8f9;
        }
        .open-btn-group .btn-outline {
            background: transparent;
            border: 1px solid var(--border);
            color: var(--text2);
        }
        .open-btn-group .btn-outline:hover {
            background: var(--bg);
        }

        /* ===== 表单 ===== */
        .input-group { margin-bottom: 14px; display: flex; flex-direction: column; gap: 4px; }
        .input-group label {
            font-weight: 600;
            font-size: 0.8rem;
            color: var(--text2);
            display: flex;
            align-items: center;
            gap: 4px;
        }
        .input-group label .hint {
            font-weight: 400;
            color: var(--text3);
            font-size: 0.7rem;
        }
        input, select {
            padding: 10px 16px;
            border-radius: var(--radius-sm);
            border: 2px solid var(--input-border);
            font-family: inherit;
            background: var(--input);
            color: var(--text);
            font-size: 0.9rem;
            transition: border 0.2s, box-shadow 0.2s;
            width: 100%;
            -webkit-appearance: none;
            appearance: none;
        }
        input:focus, select:focus {
            outline: none;
            border-color: var(--primary);
            box-shadow: 0 0 0 3px rgba(45, 55, 72, 0.1);
        }
        select { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%234a5568' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 14px center; padding-right: 36px; }

        /* ===== 模态框 ===== */
        .modal {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.45);
            backdrop-filter: blur(4px);
            -webkit-backdrop-filter: blur(4px);
            align-items: center;
            justify-content: center;
            z-index: 1000;
            padding: 16px;
        }
        .modal.show { display: flex; }
        .modal-content {
            background: var(--card);
            max-width: 520px;
            width: 100%;
            border-radius: var(--radius);
            padding: 24px 28px;
            box-shadow: 0 24px 80px rgba(0,0,0,0.2);
            animation: modalIn 0.3s ease;
            max-height: 90vh;
            overflow-y: auto;
        }
        @keyframes modalIn {
            from { opacity: 0; transform: scale(0.95) translateY(10px); }
            to { opacity: 1; transform: scale(1) translateY(0); }
        }
        .modal-content h3 {
            font-size: 1.2rem;
            font-weight: 700;
            margin-bottom: 16px;
            color: var(--text);
        }
        .modal-footer {
            display: flex;
            gap: 10px;
            justify-content: flex-end;
            margin-top: 16px;
            flex-wrap: wrap;
        }

        /* ===== Toast ===== */
        .toast {
            position: fixed;
            bottom: 24px;
            right: 24px;
            background: var(--text);
            color: #fff;
            padding: 12px 24px;
            border-radius: 30px;
            opacity: 0;
            transition: all 0.35s cubic-bezier(0.68, -0.55, 0.27, 1.55);
            z-index: 1100;
            font-size: 0.85rem;
            pointer-events: none;
            max-width: 90%;
            box-shadow: 0 8px 30px rgba(0,0,0,0.2);
            transform: translateY(20px);
        }
        .toast.show { opacity: 1; transform: translateY(0); }
        .toast.error { background: var(--danger); }
        .toast.success { background: var(--success); }

        /* ===== 管理员登录覆盖 ===== */
        .admin-auth-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.55);
            backdrop-filter: blur(6px);
            -webkit-backdrop-filter: blur(6px);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 2000;
            padding: 16px;
        }
        .admin-auth-box {
            background: var(--card);
            border-radius: var(--radius);
            padding: 32px 28px;
            width: 100%;
            max-width: 380px;
            text-align: center;
            box-shadow: 0 24px 80px rgba(0,0,0,0.25);
            animation: modalIn 0.3s ease;
        }
        .admin-auth-box h3 { font-size: 1.3rem; margin-bottom: 8px; color: var(--text); }
        .admin-auth-box p { font-size: 0.85rem; color: var(--text2); margin-bottom: 16px; }
        .admin-auth-box input { width: 100%; margin-bottom: 12px; }
        .admin-auth-box .error { color: var(--danger); font-size: 0.8rem; margin-top: 4px; min-height: 20px; }
        .admin-auth-box .btn-group { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-top: 8px; }

        /* ===== 搜索栏 ===== */
        .search-bar {
            display: flex;
            gap: 10px;
            align-items: center;
            flex-wrap: wrap;
            padding: 4px 0;
        }
        .search-bar input {
            flex: 1;
            min-width: 150px;
            padding: 10px 18px;
            border-radius: 30px;
            border: 2px solid var(--input-border);
            font-size: 0.9rem;
            background: var(--input);
            color: var(--text);
            transition: 0.2s;
        }
        .search-bar input:focus {
            border-color: var(--primary);
            box-shadow: 0 0 0 3px rgba(45, 55, 72, 0.08);
        }
        .search-bar .result-count {
            font-size: 0.8rem;
            color: var(--text2);
            padding: 4px 14px;
            background: var(--bg);
            border-radius: 20px;
            white-space: nowrap;
        }
        .highlight { background: #fef3c7; padding: 0 3px; border-radius: 3px; font-weight: 600; }

        /* ===== 导入区域 ===== */
        .import-area {
            background: var(--bg);
            border-radius: var(--radius-sm);
            padding: 16px 18px;
            margin: 0 20px 12px 20px;
            border: 1px solid var(--border);
        }
        .import-area .title { font-weight: 600; color: var(--text); font-size: 0.85rem; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
        .import-area .row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
        .import-area .row select { flex: 1; min-width: 140px; }
        .import-area .row .file-input-label { background: var(--primary); color: #fff; border: none; padding: 6px 18px; border-radius: 30px; cursor: pointer; font-size: 0.85rem; transition: 0.2s; }
        .import-area .row .file-input-label:hover { background: var(--primary-hover); }
        .import-area .hint { font-size: 0.7rem; color: var(--text2); margin-top: 6px; line-height: 1.6; }
        .import-area .hint strong { color: var(--text); }

        /* ===== 存储信息 ===== */
        .storage-info {
            background: var(--bg);
            border-radius: var(--radius-sm);
            padding: 12px 18px;
            margin: 0 20px 12px 20px;
            border: 1px solid var(--border);
            font-size: 0.8rem;
            color: var(--text2);
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 8px;
        }
        .storage-info .label { font-weight: 600; color: var(--text); }
        .storage-info .status { color: var(--success); }
        .storage-info .backup-status { color: var(--info); font-size: 0.75rem; }

        .backup-settings {
            background: var(--bg);
            border-radius: var(--radius-sm);
            padding: 12px 18px;
            margin: 0 20px 12px 20px;
            border: 1px solid var(--border);
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 10px;
        }
        .backup-settings .label { font-weight: 600; color: var(--text); font-size: 0.85rem; }
        .backup-settings .desc { font-size: 0.75rem; color: var(--text2); }
        .backup-settings .toggle { display: flex; align-items: center; gap: 8px; }
        .toggle-switch {
            width: 42px;
            height: 24px;
            background: #cbd5e1;
            border-radius: 14px;
            cursor: pointer;
            position: relative;
            transition: 0.3s;
            border: none;
            padding: 0;
            flex-shrink: 0;
        }
        .toggle-switch.active { background: var(--success); }
        .toggle-switch::after {
            content: '';
            width: 18px;
            height: 18px;
            background: white;
            border-radius: 50%;
            position: absolute;
            top: 3px;
            left: 3px;
            transition: 0.3s;
            box-shadow: 0 1px 4px rgba(0,0,0,0.15);
        }
        .toggle-switch.active::after { left: 21px; }

        .db-status {
            background: #e6fffa;
            padding: 6px 16px;
            border-radius: var(--radius-sm);
            font-size: 0.75rem;
            color: #234e52;
            margin: 0 20px 12px 20px;
            display: flex;
            justify-content: space-between;
            flex-wrap: wrap;
            gap: 6px;
        }
        .settings-group {
            background: var(--bg);
            border-radius: var(--radius-sm);
            padding: 14px 18px;
            margin: 12px 20px 16px 20px;
            border: 1px solid var(--border);
        }
        .settings-group h4 { margin-bottom: 10px; color: var(--text); font-size: 0.9rem; }
        .settings-row {
            display: flex;
            gap: 10px;
            align-items: center;
            flex-wrap: wrap;
            margin-bottom: 6px;
        }
        .settings-row span { font-size: 0.85rem; color: var(--text2); min-width: 120px; }

        .flex-btns { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
        .verify-panel { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
        .file-input-label { background: var(--bg); border: 1px solid var(--border); padding: 6px 16px; border-radius: 30px; cursor: pointer; font-size: 0.85rem; transition: 0.2s; }
        .file-input-label:hover { background: var(--border); }
        input[type="file"] { display: none; }
        .hidden { display: none !important; }
        .empty-row td { text-align: center; padding: 40px 20px; color: var(--text3); font-size: 0.9rem; }

        /* ===== 浏览器提示弹窗 ===== */
        .browser-hint-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.5);
            backdrop-filter: blur(4px);
            -webkit-backdrop-filter: blur(4px);
            display: none;
            align-items: center;
            justify-content: center;
            z-index: 3000;
            padding: 16px;
        }
        .browser-hint-overlay.show { display: flex; }
        .browser-hint-box {
            background: var(--card);
            border-radius: var(--radius);
            padding: 32px 28px;
            max-width: 440px;
            width: 100%;
            text-align: center;
            box-shadow: 0 24px 80px rgba(0,0,0,0.25);
            animation: modalIn 0.3s ease;
        }
        .browser-hint-box .icon { font-size: 3rem; margin-bottom: 10px; }
        .browser-hint-box h3 { font-size: 1.2rem; color: var(--text); margin-bottom: 8px; }
        .browser-hint-box p { font-size: 0.9rem; color: var(--text2); line-height: 1.6; margin-bottom: 16px; }
        .browser-hint-box .btn-group { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
        .browser-hint-box .btn-group .btn { min-width: 90px; padding: 10px 20px; }
        .browser-hint-box .checkbox-row {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            margin: 12px 0 16px 0;
            font-size: 0.8rem;
            color: var(--text2);
        }
        .browser-hint-box .checkbox-row input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--primary); cursor: pointer; }

        /* ===== 响应式 ===== */
        @media (max-width: 1024px) {
            body { padding: 12px; }
            .card-header { padding: 14px 16px; }
            .card-body { padding: 12px 16px; }
            .card-footer { padding: 10px 16px; }
            .modal-content { padding: 20px; }
            th, td { padding: 10px 12px; }
            .storage-info, .backup-settings, .db-status, .import-area, .settings-group { margin-left: 12px; margin-right: 12px; }
        }

        @media (max-width: 768px) {
            body { padding: 10px; }
            .header { flex-direction: column; align-items: flex-start; gap: 10px; }
            .header h1 { font-size: 1.2rem; }
            .theme-bar { width: 100%; justify-content: center; padding: 4px 12px; }
            .tab-bar { width: 100%; justify-content: center; border-radius: 20px; padding: 4px; }
            .tab-btn { padding: 8px 16px; font-size: 0.8rem; flex: 1; text-align: center; min-width: 60px; }
            .card-header { flex-direction: column; align-items: flex-start; gap: 8px; padding: 12px 14px; }
            .card-header .verify-panel { width: 100%; }
            .card-header .verify-panel input { flex: 1; min-width: 100px; }
            .card-body { padding: 10px 12px; }
            .card-footer { flex-direction: column; align-items: flex-start; gap: 4px; padding: 10px 14px; }

            table { font-size: 0.75rem; min-width: 480px; }
            th, td { padding: 8px 10px; }
            .url-cell .url-name { font-size: 0.8rem; }
            .url-cell .url-link { font-size: 0.65rem; }

            .modal-content { padding: 16px; max-width: 100%; margin: 8px; border-radius: var(--radius-sm); }
            .modal-content h3 { font-size: 1rem; }

            .search-bar input { font-size: 0.8rem; padding: 8px 14px; min-width: 100px; }
            .search-bar .result-count { font-size: 0.7rem; padding: 2px 10px; }

            .storage-info, .backup-settings, .db-status, .import-area, .settings-group {
                margin-left: 8px;
                margin-right: 8px;
                padding: 10px 14px;
                flex-direction: column;
                align-items: stretch;
            }
            .backup-settings .toggle { justify-content: center; }
            .import-area .row { flex-direction: column; align-items: stretch; }
            .import-area .row select { width: 100%; }

            .settings-row { flex-direction: column; align-items: stretch; gap: 6px; }
            .settings-row span { min-width: auto; }

            .flex-btns { flex-wrap: wrap; }
            .flex-btns .btn { flex: 1; min-width: 60px; justify-content: center; padding: 6px 12px; font-size: 0.7rem; }

            .open-btn-group .btn { font-size: 0.65rem; padding: 3px 12px; }
            .password-masked, .password-plain { font-size: 0.75rem; padding: 1px 10px; }
            .browser-badge { font-size: 0.55rem; padding: 1px 8px; }

            .admin-auth-box { padding: 20px; }
            .browser-hint-box { padding: 20px; }
            .browser-hint-box .icon { font-size: 2.4rem; }
            .browser-hint-box .btn-group .btn { min-width: 70px; padding: 8px 14px; font-size: 0.8rem; }

            .toast { bottom: 16px; right: 16px; font-size: 0.8rem; padding: 10px 16px; max-width: calc(100% - 32px); }
        }

        @media (max-width: 400px) {
            body { padding: 6px; }
            .tab-btn { padding: 6px 10px; font-size: 0.7rem; }
            th, td { padding: 6px 6px; font-size: 0.7rem; }
            table { min-width: 360px; }
            .card-header h2 { font-size: 0.95rem; }
            .btn { font-size: 0.7rem; padding: 5px 12px; }
            .modal-content { padding: 12px; }
            .storage-info, .backup-settings, .db-status, .import-area, .settings-group { padding: 8px 10px; }
        }
    </style>
</head>
<body>
<div class="app">
    <!-- ===== 头部 ===== -->
    <div class="header">
        <h1>&#128272; 密码管理系统 <span>v2</span></h1>
        <div class="theme-bar">
            <span class="label">主题</span>
            <span class="theme-dot active" data-theme="default" style="background:#64748b;" title="默认"></span>
            <span class="theme-dot" data-theme="red" style="background:#dc2626;" title="红"></span>
            <span class="theme-dot" data-theme="orange" style="background:#ea580c;" title="橙"></span>
            <span class="theme-dot" data-theme="yellow" style="background:#ca8a04;" title="黄"></span>
            <span class="theme-dot" data-theme="green" style="background:#16a34a;" title="绿"></span>
            <span class="theme-dot" data-theme="blue" style="background:#2563eb;" title="蓝"></span>
            <span class="theme-dot" data-theme="purple" style="background:#9333ea;" title="紫"></span>
            <span class="theme-dot" data-theme="pink" style="background:#db2777;" title="粉"></span>
        </div>
    </div>

    <!-- ===== Tab 栏 ===== -->
    <div class="tab-bar">
        <button class="tab-btn active" data-tab="employee">&#128101; 员工门户</button>
        <button class="tab-btn" data-tab="admin">&#128272; 管理后台</button>
    </div>

    <!-- ===== 员工门户 ===== -->
    <div id="employeePanel">
        <div class="card">
            <div class="card-header">
                <h2>&#127760; 快捷访问</h2>
                <div class="verify-panel">
                    <input type="password" id="empViewPwd" placeholder="查看密码" style="width:140px;" autocomplete="off">
                    <button class="btn btn-primary btn-sm" id="verifyEmpBtn">显示密码</button>
                </div>
            </div>
            <div class="card-body" style="padding-bottom:8px;">
                <div class="search-bar">
                    <input type="text" id="searchInput" placeholder="&#128269; 搜索名称 / 网址 / 账号..." autocomplete="off">
                    <span class="result-count" id="resultCount">共 0 条</span>
                    <button class="btn btn-clear btn-sm" id="clearSearchBtn" style="display:none;">&#10005; 清除</button>
                </div>
            </div>
            <div class="table-wrap">
                <table>
                    <thead><tr><th style="min-width:180px;">网站名称 / 网址</th><th>账号</th><th>密码</th><th style="min-width:120px;">打开方式</th></tr></thead>
                    <tbody id="employeeTableBody"></tbody>
                </table>
            </div>
            <div class="card-footer">
                <span>查看密码: <strong>employee2024</strong></span>
                <span>&#128161; 支持搜索名称、网址、账号</span>
            </div>
        </div>
    </div>

    <!-- ===== 管理后台 ===== -->
    <div id="adminPanel" class="hidden">
        <div class="card">
            <div class="card-header">
                <h2>&#128203; 凭证管理</h2>
                <div class="flex-btns">
                    <button class="btn btn-primary" id="openAddCredBtn">+ 新增</button>
                    <button class="btn btn-success btn-sm" id="exportExcelBtn">&#128206; Excel</button>
                    <label class="file-input-label" for="importExcelInput">&#128194; 导入 Excel</label>
                    <input type="file" id="importExcelInput" accept=".xls,.xlsx">
                    <button class="btn btn-info btn-sm" id="exportJsonBtn">&#128229; JSON</button>
                    <label class="file-input-label" for="importJsonInput" style="background:var(--primary);color:#fff;">&#128228; 导入 JSON</label>
                    <input type="file" id="importJsonInput" accept=".json">
                </div>
            </div>

            <div class="db-status">
                <span>&#128192; 数据库已连接</span>
                <span id="dbStatusText">localStorage 持久化存储</span>
            </div>

            <!-- 从浏览器导入 -->
            <div class="import-area">
                <div class="title">&#127760; 从浏览器导入密码</div>
                <div class="row">
                    <select id="importBrowserSelect">
                        <option value="firefox">&#129418; Firefox</option>
                        <option value="chrome">&#128308; Chrome</option>
                        <option value="edge">&#128311; Edge</option>
                    </select>
                    <label class="file-input-label" for="importBrowserCsv">&#128194; 选择 CSV</label>
                    <input type="file" id="importBrowserCsv" accept=".csv">
                    <span style="font-size:0.7rem;color:var(--text2);">从浏览器导出的 CSV</span>
                </div>
                <div class="hint">
                    <strong>&#128204; 导出:</strong> Firefox/Chrome/Edge 密码管理器 → 导出密码 → CSV
                    <span style="display:block;color:var(--success);">&#9989; 导入后自动绑定对应浏览器打开</span>
                </div>
            </div>

            <!-- 自动备份 -->
            <div class="backup-settings">
                <div>
                    <span class="label">&#128190; 自动备份</span>
                    <span class="desc">每次修改自动导出 JSON 到下载目录</span>
                </div>
                <div class="toggle">
                    <span id="autoBackupStatus" style="font-size:0.75rem;color:var(--text2);">已关闭</span>
                    <button class="toggle-switch" id="autoBackupToggle" title="点击切换"></button>
                </div>
            </div>

            <!-- 存储信息 -->
            <div class="storage-info">
                <span><span class="label">&#128190; 存储:</span>浏览器 localStorage</span>
                <span><span class="label">&#128202; 记录:</span><span id="recordCount">0</span> 条</span>
                <span><span class="label">&#128204; 持久化:</span><span class="status">&#9989; 刷新不丢失</span></span>
                <span class="backup-status" id="lastBackupTime">&#128203; 最后备份:无</span>
            </div>

            <div class="table-wrap">
                <table>
                    <thead><tr><th style="min-width:100px;">名称</th><th style="min-width:150px;">网址</th><th>账号</th><th>密码</th><th style="min-width:100px;">浏览器</th><th style="min-width:120px;">操作</th></tr></thead>
                    <tbody id="adminTableBody"></tbody>
                </table>
            </div>

            <div class="settings-group">
                <h4>&#9881;&#65039; 安全设置</h4>
                <div class="settings-row"><span>员工查看密码</span><input type="password" id="newEmployeePwd" placeholder="新密码" style="flex:1;min-width:120px;"><button class="btn btn-warning btn-sm" id="updateEmployeePwdBtn">修改</button></div>
                <div class="settings-row"><span>管理员密码</span><input type="password" id="newAdminPwd" placeholder="新密码" style="flex:1;min-width:120px;"><button class="btn btn-warning btn-sm" id="updateAdminPwdBtn">修改</button></div>
            </div>

            <div class="card-footer">
                <span>管理员: <strong>admin123</strong></span>
                <span>员工: <strong>employee2024</strong></span>
                <span>&#128161; 开启「自动备份」后每次修改自动导出</span>
            </div>
        </div>
    </div>
</div>

<!-- ===== 浏览器提示弹窗 ===== -->
<div class="browser-hint-overlay" id="browserHintOverlay">
    <div class="browser-hint-box">
        <div class="icon" id="hintIcon">&#127760;</div>
        <h3 id="hintTitle">浏览器</h3>
        <p id="hintDesc">请先确认电脑已安装该浏览器,如未安装请先下载。</p>
        <div class="checkbox-row">
            <input type="checkbox" id="hintDontShowAgain">
            <label for="hintDontShowAgain">下次不再提示</label>
        </div>
        <div class="btn-group">
            <button class="btn btn-outline" id="hintCancel">取消</button>
            <button class="btn btn-warning" id="hintDownload">&#11015; 下载</button>
            <button class="btn btn-primary" id="hintConfirm">确定,已安装</button>
        </div>
    </div>
</div>

<!-- ===== 模态框 ===== -->
<div class="modal" id="credModal">
    <div class="modal-content">
        <h3 id="modalTitle">添加凭证</h3>
        <div class="input-group">
            <label>网站名称 <span class="hint">(必填)</span></label>
            <input type="text" id="credName" placeholder="如:企业邮箱、GitLab...">
        </div>
        <div class="input-group">
            <label>网址 <span class="hint">(必填,http:// 或 https://)</span></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">&#127757; 默认</option>
                <option value="edge">&#128311; Edge</option>
                <option value="chrome">&#128308; Chrome</option>
                <option value="firefox">&#129418; Firefox</option>
            </select>
        </div>
        <input type="hidden" id="editId" value="">
        <div class="modal-footer">
            <button class="btn btn-outline" id="closeModalBtn">取消</button>
            <button class="btn btn-primary" id="saveCredBtn">保存</button>
        </div>
    </div>
</div>

<!-- ===== Toast ===== -->
<div id="toast" class="toast">操作成功</div>

<script>
    function h(s) { return CryptoJS.SHA256(s).toString(); }

    // ===== 浏览器配置 =====
    var BROWSER_CONFIG = {
        edge: {
            icon: '&#128311;',
            name: 'Edge 浏览器',
            desc: '使用前提:已安装Edge浏览器。(未安装请先下载安装)。',
            download: 'https://www.microsoft.com/zh-cn/edge/download',
            protocol: 'microsoft-edge:'
        },
        chrome: {
            icon: '&#128308;',
            name: '谷歌浏览器',
            desc: '使用前提:已安装谷歌浏览器。(未安装请先下载安装)。',
            download: 'https://sw.pcmgr.qq.com/62a4683d5d6deb5f13daf8e29b0d90dd/6a962b0c/spcmgr/download/152.0.7977.65_chrome_installer_uncompressed-win64.exe',
            protocol: 'googlechrome:'
        },
        firefox: {
            icon: '&#129418;',
            name: '火狐浏览器',
            desc: '使用前提:已安装火狐浏览器。(未安装请先下载安装)。',
            download: 'https://sw.pcmgr.qq.com/e38a6e930fd7892da4c3d82d122394cb/6a962af4/spcmgr/download/22931-110819-Firefox-setup-win64_116.0.3.8627.exe',
            protocol: 'firefox:'
        }
    };

    // ===== 浏览器提示弹窗 =====
    var hintOverlay = document.getElementById('browserHintOverlay');
    var hintDontShowAgain = document.getElementById('hintDontShowAgain');
    var hintIcon = document.getElementById('hintIcon');
    var hintTitle = document.getElementById('hintTitle');
    var hintDesc = document.getElementById('hintDesc');
    var hintDownload = document.getElementById('hintDownload');
    var hintConfirm = document.getElementById('hintConfirm');
    var hintCancel = document.getElementById('hintCancel');
    var pendingBrowser = null;
    var pendingUrl = null;

    function getHintSetting(browser) {
        return localStorage.getItem('hint_dismissed_' + browser) === 'true';
    }

    function setHintSetting(browser, dismissed) {
        localStorage.setItem('hint_dismissed_' + browser, dismissed ? 'true' : 'false');
    }

    function showBrowserHint(browser, url) {
        if (getHintSetting(browser)) {
            doOpenBrowser(browser, url);
            return;
        }
        var config = BROWSER_CONFIG[browser];
        if (!config) { window.open(url, '_blank'); return; }
        pendingBrowser = browser;
        pendingUrl = url;
        hintIcon.textContent = config.icon;
        hintTitle.textContent = config.name;
        hintDesc.textContent = config.desc;
        hintDownload.textContent = '&#11015; 下载 ' + config.name;
        hintOverlay.classList.add('show');
        hintDontShowAgain.checked = false;
    }

    function hideBrowserHint() {
        hintOverlay.classList.remove('show');
    }

    function doOpenBrowser(b, u) {
        var config = BROWSER_CONFIG[b];
        if (!config) { window.open(u, '_blank'); return; }
        var url = u;
        if (!url.startsWith('http://') && !url.startsWith('https://')) {
            url = 'https://' + url;
        }
        var fullUrl = config.protocol + url;
        try {
            var currentUrl = window.location.href;
            window.location.href = fullUrl;
            setTimeout(function() {
                if (window.location.href === currentUrl) {
                    tryMethod2(b, url);
                }
            }, 800);
        } catch(e) {
            tryMethod2(b, url);
        }
    }

    function tryMethod2(b, u) {
        var config = BROWSER_CONFIG[b];
        var url = u;
        if (!url.startsWith('http://') && !url.startsWith('https://')) {
            url = 'https://' + url;
        }
        var protocols = [
            config.protocol + url,
            config.protocol + '//open?url=' + encodeURIComponent(url),
            config.protocol + '//' + url
        ];
        for (var i = 0; i < protocols.length; i++) {
            try {
                var w = window.open(protocols[i], '_blank');
                if (w) {
                    setTimeout(function(win) {
                        try { if (win && win.closed) { tryMethod3(b, u); } } catch(e) { tryMethod3(b, u); }
                    }, 1500, w);
                    return;
                }
            } catch(e) {}
        }
        tryMethod3(b, u);
    }

    function tryMethod3(b, u) {
        var config = BROWSER_CONFIG[b];
        var url = u;
        if (!url.startsWith('http://') && !url.startsWith('https://')) {
            url = 'https://' + url;
        }
        var fullUrl = config.protocol + url;
        try {
            var iframe = document.createElement('iframe');
            iframe.style.display = 'none';
            iframe.src = fullUrl;
            document.body.appendChild(iframe);
            setTimeout(function() {
                try { document.body.removeChild(iframe); } catch(e) {}
                showDownloadDialog(b, u);
            }, 2000);
        } catch(e) {
            showDownloadDialog(b, u);
        }
    }

    function showDownloadDialog(b, u) {
        var config = BROWSER_CONFIG[b];
        var name = config ? config.name : '该浏览器';
        var url = config ? config.download : null;
        if (url) {
            if (confirm('未检测到 ' + name + ',是否下载?\n\n点击「确定」前往下载\n点击「取消」使用默认浏览器打开')) {
                window.open(url, '_blank');
            } else {
                window.open(u, '_blank');
            }
        } else {
            window.open(u, '_blank');
        }
    }

    hintConfirm.addEventListener('click', function() {
        if (hintDontShowAgain.checked && pendingBrowser) {
            setHintSetting(pendingBrowser, true);
        }
        hideBrowserHint();
        if (pendingUrl && pendingBrowser) {
            setTimeout(function() { doOpenBrowser(pendingBrowser, pendingUrl); }, 150);
        }
    });

    hintCancel.addEventListener('click', function() {
        hideBrowserHint();
        pendingBrowser = null;
        pendingUrl = null;
    });

    hintDownload.addEventListener('click', function() {
        if (pendingBrowser) {
            var config = BROWSER_CONFIG[pendingBrowser];
            if (config && config.download) {
                window.open(config.download, '_blank');
            }
        }
    });

    hintOverlay.addEventListener('click', function(e) {
        if (e.target === hintOverlay) {
            hideBrowserHint();
            pendingBrowser = null;
            pendingUrl = null;
        }
    });

    // ===== 浏览器打开入口 - 恢复原样 =====
    function bn(b) {
        var map = { edge:'Edge浏览器打开', chrome:'谷歌浏览器打开', firefox:'火狐浏览器打开' };
        return map[b] || '默认浏览器打开';
    }

    function bc(b) {
        var map = { edge:'browser-edge', chrome:'browser-chrome', firefox:'browser-firefox' };
        return map[b] || 'browser-default';
    }

    function openWith(b, u) {
        if (b === 'default' || !b) { window.open(u, '_blank'); return; }
        showBrowserHint(b, u);
    }

    function openHTML(b, u) {
        var name = bn(b);
        var cls = bc(b);
        var su = u.replace(/'/g, "\\'");
        var html = '<div class="open-btn-group">';
        if (b === 'default') {
            html += '<button class="btn btn-open btn-sm">打开</button>';
        } else {
            html += '<button class="btn btn-open btn-sm">' + name + '</button>';
            html += '<button class="btn btn-outline btn-sm">默认浏览器打开</button>';
        }
        html += '</div>';
        return html;
    }

    function copyText(t, msg) {
        try {
            navigator.clipboard.writeText(t);
            toast(msg);
        } catch(e) {
            var ta = document.createElement('textarea');
            ta.value = t;
            document.body.appendChild(ta);
            ta.select();
            document.execCommand('copy');
            document.body.removeChild(ta);
            toast(msg);
        }
    }

    // ============================================================
    // ===== 数据存储层 - localStorage =====
    // ============================================================

    var STORAGE_KEY = 'password_manager_data';
    var CONFIG_KEY = 'password_manager_config';
    var empVisible = false;
    var adminAuth = false;
    var empHash = '';
    var adminHash = '';
    var autoBackupEnabled = false;
    var lastBackupTime = null;
    var credentials = [];
    var nextId = 1;

    var DEF_EMP = h('employee2024');
    var DEF_ADMIN = h('admin123');

    // ===== 数据操作 =====

    function loadData() {
        try {
            var saved = localStorage.getItem(STORAGE_KEY);
            if (saved) {
                var parsed = JSON.parse(saved);
                credentials = parsed.credentials || [];
                nextId = parsed.nextId || 1;
                return true;
            }
        } catch(e) { console.error('加载数据失败:', e); }
        return false;
    }

    function saveData() {
        try {
            var data = { credentials: credentials, nextId: nextId };
            localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
            return true;
        } catch(e) { console.error('保存数据失败:', e); return false; }
    }

    function loadConfig() {
        try {
            var saved = localStorage.getItem(CONFIG_KEY);
            if (saved) {
                var parsed = JSON.parse(saved);
                empHash = parsed.empHash || DEF_EMP;
                adminHash = parsed.adminHash || DEF_ADMIN;
                autoBackupEnabled = parsed.autoBackupEnabled || false;
                return true;
            }
        } catch(e) { console.error('加载配置失败:', e); }
        return false;
    }

    function saveConfig() {
        try {
            var data = { empHash: empHash, adminHash: adminHash, autoBackupEnabled: autoBackupEnabled };
            localStorage.setItem(CONFIG_KEY, JSON.stringify(data));
            return true;
        } catch(e) { console.error('保存配置失败:', e); return false; }
    }

    function initData() {
        var loaded = loadData();
        var configLoaded = loadConfig();

        if (!loaded) {
            credentials = [
                { id: 1, name: '企业邮箱', url: 'https://mail.company.com', username: 'zhang.san@company.com', password: 'Mail@2024Secure', browser: 'default' },
                { id: 2, name: 'HR系统', url: 'https://hrsystem.company.com', username: 'hr_admin', password: 'HrP@ss#2024', browser: 'edge' },
                { id: 3, name: 'GitLab代码库', url: 'https://gitlab.company.com', username: 'dev_lihua', password: 'GitLab!7890', browser: 'chrome' },
                { id: 4, name: 'Jira项目管理', url: 'https://jira.company.com', username: 'project_manager', password: 'Jira@2025', browser: 'firefox' }
            ];
            nextId = 5;
            saveData();
        }

        if (!configLoaded) {
            empHash = DEF_EMP;
            adminHash = DEF_ADMIN;
            autoBackupEnabled = false;
            saveConfig();
        }

        updateStorageInfo();
        updateLastBackupTime();
        updateAutoBackupUI();
    }

    // ===== 自动备份 =====

    function getAutoBackupSetting() { return autoBackupEnabled; }

    function setAutoBackupSetting(enabled) {
        autoBackupEnabled = enabled;
        saveConfig();
        updateAutoBackupUI();
    }

    function updateAutoBackupUI() {
        var toggle = document.getElementById('autoBackupToggle');
        var status = document.getElementById('autoBackupStatus');
        if (toggle) toggle.classList.toggle('active', autoBackupEnabled);
        if (status) {
            status.textContent = autoBackupEnabled ? '&#9989; 已开启' : '&#9940; 已关闭';
            status.style.color = autoBackupEnabled ? 'var(--success)' : 'var(--text2)';
        }
    }

    function exportToDownloadFolder() {
        try {
            var data = { credentials: credentials, nextId: nextId, exportTime: new Date().toISOString(), version: '1.0' };
            var json = JSON.stringify(data, null, 2);
            var blob = new Blob([json], { type: 'application/json' });
            var url = URL.createObjectURL(blob);
            var a = document.createElement('a');
            a.href = url;
            var now = new Date();
            var dateStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0') + '_' + String(now.getHours()).padStart(2,'0') + '-' + String(now.getMinutes()).padStart(2,'0');
            a.download = 'password_backup_' + dateStr + '.json';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            setTimeout(function() { URL.revokeObjectURL(url); }, 5000);
            lastBackupTime = now;
            updateLastBackupTime();
            return true;
        } catch(e) { console.error('自动备份失败:', e); return false; }
    }

    function updateLastBackupTime() {
        var el = document.getElementById('lastBackupTime');
        if (!el) return;
        if (lastBackupTime) {
            var str = lastBackupTime.getFullYear() + '-' + String(lastBackupTime.getMonth()+1).padStart(2,'0') + '-' + String(lastBackupTime.getDate()).padStart(2,'0') + ' ' + String(lastBackupTime.getHours()).padStart(2,'0') + ':' + String(lastBackupTime.getMinutes()).padStart(2,'0');
            el.textContent = '&#128203; 最后备份:' + str;
        } else {
            el.textContent = '&#128203; 最后备份:无';
        }
    }

    // ===== 增删改查 =====

    function allCreds() {
        return credentials.slice().sort(function(a, b) { return b.id - a.id; });
    }

    function searchCreds(keyword) {
        if (!keyword || keyword.trim() === '') return allCreds();
        var kw = keyword.trim().toLowerCase();
        var results = [];
        for (var i = 0; i < credentials.length; i++) {
            var item = credentials[i];
            var name = (item.name || '').toLowerCase();
            if (name.indexOf(kw) !== -1 ||
                item.url.toLowerCase().indexOf(kw) !== -1 ||
                item.username.toLowerCase().indexOf(kw) !== -1 ||
                item.password.toLowerCase().indexOf(kw) !== -1) {
                results.push(item);
            }
        }
        return results;
    }

    function getCredById(id) {
        for (var i = 0; i < credentials.length; i++) {
            if (credentials[i].id === id) return credentials[i];
        }
        return null;
    }

    function upsert(id, name, url, username, password, browser) {
        if (!name || !url || !username || !password) return false;
        var finalUrl = url;
        if (!finalUrl.startsWith('http://') && !finalUrl.startsWith('https://')) {
            finalUrl = 'https://' + finalUrl;
        }
        if (id === null || id === undefined || id === '') {
            credentials.push({ id: nextId++, name: name.trim(), url: finalUrl, username: username, password: password, browser: browser || 'default' });
        } else {
            var found = false;
            for (var i = 0; i < credentials.length; i++) {
                if (credentials[i].id === id) {
                    credentials[i].name = name.trim();
                    credentials[i].url = finalUrl;
                    credentials[i].username = username;
                    credentials[i].password = password;
                    credentials[i].browser = browser || 'default';
                    found = true;
                    break;
                }
            }
            if (!found) return false;
        }
        saveData();
        updateStorageInfo();
        if (autoBackupEnabled) exportToDownloadFolder();
        return true;
    }

    function delCred(id) {
        for (var i = 0; i < credentials.length; i++) {
            if (credentials[i].id === id) {
                credentials.splice(i, 1);
                saveData();
                updateStorageInfo();
                if (autoBackupEnabled) exportToDownloadFolder();
                return true;
            }
        }
        return false;
    }

    function batchUpsert(records) {
        var cnt = 0;
        for (var i = 0; i < records.length; i++) {
            var rec = records[i];
            if (rec.url && rec.username && rec.password) {
                var exists = false;
                for (var j = 0; j < credentials.length; j++) {
                    if (credentials[j].url === rec.url) {
                        credentials[j].username = rec.username;
                        credentials[j].password = rec.password;
                        credentials[j].browser = rec.browser || 'default';
                        if (rec.name) credentials[j].name = rec.name;
                        exists = true;
                        cnt++;
                        break;
                    }
                }
                if (!exists) {
                    credentials.push({
                        id: nextId++,
                        name: rec.name || rec.url.replace(/^https?:\/\//, '').split('/')[0] || '未命名',
                        url: rec.url,
                        username: rec.username,
                        password: rec.password,
                        browser: rec.browser || 'default'
                    });
                    cnt++;
                }
            }
        }
        saveData();
        updateStorageInfo();
        if (autoBackupEnabled) exportToDownloadFolder();
        return cnt;
    }

    function updateStorageInfo() {
        var el = document.getElementById('recordCount');
        if (el) el.textContent = credentials.length;
        updateSearchResultCount();
    }

    // ===== 密码配置 =====

    function setEmpHash(newHash) { empHash = newHash; saveConfig(); }
    function setAdminHash(newHash) { adminHash = newHash; saveConfig(); }

    // ===== 导出/导入 JSON =====

    function exportJson() {
        try {
            var data = { credentials: credentials, nextId: nextId, exportTime: new Date().toISOString(), version: '1.0' };
            var json = JSON.stringify(data, null, 2);
            var blob = new Blob([json], { type: 'application/json' });
            var url = URL.createObjectURL(blob);
            var a = document.createElement('a');
            a.href = url;
            var now = new Date();
            var dateStr = now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0') + '-' + String(now.getDate()).padStart(2,'0') + '_' + String(now.getHours()).padStart(2,'0') + '-' + String(now.getMinutes()).padStart(2,'0');
            a.download = 'password_data_' + dateStr + '.json';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            setTimeout(function() { URL.revokeObjectURL(url); }, 5000);
            toast('数据已导出到下载目录', 'success');
        } catch(e) { toast('导出失败: ' + e.message, true); }
    }

    function importJson(file) {
        var reader = new FileReader();
        reader.onload = function(e) {
            try {
                var data = JSON.parse(e.target.result);
                if (!data.credentials || !Array.isArray(data.credentials)) {
                    toast('无效的数据文件', true);
                    return;
                }
                if (!confirm('确定要恢复此数据吗?当前数据将被覆盖!')) return;
                credentials = data.credentials;
                nextId = data.nextId || (credentials.length > 0 ? Math.max.apply(null, credentials.map(function(c) { return c.id; })) + 1 : 1);
                saveData();
                updateStorageInfo();
                if (autoBackupEnabled) exportToDownloadFolder();
                renderEmp();
                renderAdmin();
                toast('数据恢复成功', 'success');
            } catch(err) { toast('无效的数据文件: ' + err.message, true); }
        };
        reader.readAsText(file);
    }

    // ===== 从浏览器 CSV 导入 =====

    function importBrowserCsv(file) {
        var browserSelect = document.getElementById('importBrowserSelect');
        var browserType = browserSelect ? browserSelect.value : 'firefox';
        var targetBrowser = { firefox: 'firefox', chrome: 'chrome', edge: 'edge' }[browserType] || 'default';

        var reader = new FileReader();
        reader.onload = function(e) {
            try {
                var text = e.target.result;
                var lines = text.split(/\r?\n/).filter(function(line) { return line.trim() !== ''; });
                if (lines.length < 2) { toast('CSV 文件为空或格式无效', true); return; }

                function parseCSVLine(line) {
                    var result = [], current = '', inQuotes = false;
                    for (var i = 0; i < line.length; i++) {
                        var ch = line[i];
                        if (inQuotes) {
                            if (ch === '"' && (i + 1 < line.length && line[i + 1] === '"')) { current += '"'; i++; }
                            else if (ch === '"') inQuotes = false;
                            else current += ch;
                        } else {
                            if (ch === '"') inQuotes = true;
                            else if (ch === ',') { result.push(current.trim()); current = ''; }
                            else current += ch;
                        }
                    }
                    result.push(current.trim());
                    return result;
                }

                var columnMaps = {
                    firefox: { url: ['url', '网址', 'web address'], username: ['username', '账号', 'user name'], password: ['password', '密码', 'pwd'] },
                    chrome: { url: ['url', '网址', 'web address'], username: ['username', '账号', 'user name', 'name', '名称'], password: ['password', '密码', 'pwd'] },
                    edge: { url: ['url', '网址', 'web address'], username: ['username', '账号', 'user name', 'name', '名称'], password: ['password', '密码', 'pwd'] }
                };
                var map = columnMaps[browserType] || columnMaps.firefox;
                var headers = parseCSVLine(lines[0]);
                var headerLower = headers.map(function(h) { return h.toLowerCase().trim(); });

                var urlIdx = -1, userIdx = -1, pwdIdx = -1, nameIdx = -1;
                for (var i = 0; i < headers.length; i++) {
                    var h = headerLower[i];
                    if (urlIdx === -1) { for (var u = 0; u < map.url.length; u++) { if (h === map.url[u].toLowerCase() || h.indexOf(map.url[u].toLowerCase()) !== -1) { urlIdx = i; break; } } }
                    if (userIdx === -1) { for (var u2 = 0; u2 < map.username.length; u2++) { if (h === map.username[u2].toLowerCase() || h.indexOf(map.username[u2].toLowerCase()) !== -1) { userIdx = i; break; } } }
                    if (pwdIdx === -1) { for (var p = 0; p < map.password.length; p++) { if (h === map.password[p].toLowerCase() || h.indexOf(map.password[p].toLowerCase()) !== -1) { pwdIdx = i; break; } } }
                    if (nameIdx === -1 && (h === 'name' || h === '名称')) nameIdx = i;
                }

                if (urlIdx === -1 || userIdx === -1 || pwdIdx === -1) {
                    if (headers.length >= 3) {
                        if (browserType === 'firefox') { urlIdx = 0; userIdx = 1; pwdIdx = 2; }
                        else {
                            if (headers.length >= 4 && (headerLower[0] === 'name' || headerLower[0] === '名称')) { nameIdx = 0; urlIdx = 1; userIdx = 2; pwdIdx = 3; }
                            else { urlIdx = 0; userIdx = 1; pwdIdx = 2; }
                        }
                    } else {
                        toast('无法识别 ' + getBrowserNameByType(browserType) + ' CSV 格式', true);
                        return;
                    }
                }

                var records = [], skipped = 0;
                for (var i = 1; i < lines.length; i++) {
                    var fields = parseCSVLine(lines[i]);
                    if (fields.length <= Math.max(urlIdx, userIdx, pwdIdx)) continue;
                    var url = fields[urlIdx] ? fields[urlIdx].trim() : '';
                    var username = fields[userIdx] ? fields[userIdx].trim() : '';
                    var password = fields[pwdIdx] ? fields[pwdIdx].trim() : '';
                    var name = (nameIdx !== -1 && fields[nameIdx]) ? fields[nameIdx].trim() : '';
                    if (!url || !username || !password) { skipped++; continue; }
                    if (!url.startsWith('http://') && !url.startsWith('https://')) url = 'https://' + url;
                    if (!name) name = url.replace(/^https?:\/\//, '').split('/')[0] || '未命名';
                    records.push({ name: name, url: url, username: username, password: password, browser: targetBrowser });
                }

                if (records.length === 0) { toast('没有找到有效的密码数据', true); return; }
                var browserName = getBrowserNameByType(browserType);
                if (!confirm('从 ' + browserName + ' 找到 ' + records.length + ' 条密码,将自动设置为「' + browserName + '」打开方式。\n\n是否导入?')) return;

                var cnt = batchUpsert(records);
                renderAdmin(); renderEmp();
                toast('成功从 ' + browserName + ' 导入 ' + cnt + ' 条密码,已自动绑定 ' + browserName + ' 打开', 'success');
            } catch(err) { toast('导入失败: ' + err.message, true); console.error(err); }
        };
        reader.readAsText(file);
    }

    function getBrowserNameByType(type) {
        return { firefox: 'Firefox', chrome: 'Chrome', edge: 'Edge' }[type] || type;
    }

    // ===== 更新搜索结果计数 =====
    function updateSearchResultCount() {
        var el = document.getElementById('resultCount');
        if (!el) return;
        var keyword = document.getElementById('searchInput').value;
        var results = searchCreds(keyword);
        var total = allCreds().length;
        if (keyword && keyword.trim() !== '') {
            el.textContent = '找到 ' + results.length + ' 条 / 共 ' + total + ' 条';
        } else {
            el.textContent = '共 ' + total + ' 条';
        }
    }

    // ===== 高亮 =====
    function highlightText(text, keyword) {
        if (!keyword || keyword.trim() === '') return text;
        var kw = keyword.trim();
        var regex = new RegExp('(' + kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
        return text.replace(regex, '<span class="highlight">$1</span>');
    }

    // ===== 渲染:员工门户 =====
    function renderEmp() {
        var tbody = document.getElementById('employeeTableBody');
        if (!tbody) return;
        var keyword = document.getElementById('searchInput').value;
        var list = searchCreds(keyword);

        if (!list.length) {
            var total = allCreds().length;
            tbody.innerHTML = '<tr class="empty-row"><td colspan="4">' + (total === 0 ? '暂无数据' : '&#128269; 没有找到匹配的记录') + '</td></tr>';
            updateSearchResultCount();
            return;
        }

        var html = '', kw = keyword.trim().toLowerCase();
        for (var i = 0; i < list.length; i++) {
            var item = list[i];
            var name = bn(item.browser), cls = bc(item.browser);

            var displayName = esc(item.name || '未命名');
            var displayUrl = esc(item.url);
            var displayUsername = esc(item.username);
            var displayPassword = esc(item.password);

            if (kw) {
                if ((item.name || '').toLowerCase().indexOf(kw) !== -1) displayName = highlightText(esc(item.name || '未命名'), kw);
                if (item.url.toLowerCase().indexOf(kw) !== -1) displayUrl = highlightText(esc(item.url), kw);
                if (item.username.toLowerCase().indexOf(kw) !== -1) displayUsername = highlightText(esc(item.username), kw);
                if (item.password.toLowerCase().indexOf(kw) !== -1) displayPassword = highlightText(esc(item.password), kw);
            }

            var pwd = empVisible ?
                '<span class="password-plain">' + displayPassword + '</span>' :
                '<span class="password-masked">●●●●●●●●</span>';

            html += '<tr><td><div class="url-cell">';
            html += '<span class="url-name">' + displayName + '</span>';
            html += '<a href="' + esc(item.url) + '" target="_blank" class="url-link">' + displayUrl + '</a>';
            html += ' <button class="copy-url-btn" data-url="' + esc(item.url) + '">复制</button>';
            html += '</div></td>';
            html += '<td>' + displayUsername + '</td><td>' + pwd + '</td>';
            html += '<td><span class="browser-badge ' + cls + '">' + name + '</span><br>' + openHTML(item.browser, item.url) + '</td></tr>';
        }
        tbody.innerHTML = html;
        updateSearchResultCount();

        document.querySelectorAll('.copy-url-btn').forEach(function(btn) {
            btn.addEventListener('click', function() { copyText(this.dataset.url, '网址已复制'); });
        });
    }

    // ===== 渲染:管理后台 =====
    function renderAdmin() {
        var tbody = document.getElementById('adminTableBody');
        if (!tbody) return;
        if (!adminAuth) {
            tbody.innerHTML = '<tr class="empty-row"><td colspan="6">请验证管理员权限</td></tr>';
            return;
        }
        var list = allCreds();
        if (!list.length) {
            tbody.innerHTML = '<tr class="empty-row"><td colspan="6">暂无数据,点击"新增"添加</td></tr>';
            return;
        }
        var html = '';
        for (var i = 0; i < list.length; i++) {
            var item = list[i];
            var name = bn(item.browser), cls = bc(item.browser);
            html += '<tr><td><span class="url-name-sm">' + esc(item.name || '未命名') + '</span></td>';
            html += '<td><a href="' + esc(item.url) + '" target="_blank" class="url-link">' + esc(item.url) + '</a></td>';
            html += '<td>' + esc(item.username) + '</td>';
            html += '<td><span class="password-plain">' + esc(item.password) + '</span></td>';
            html += '<td><span class="browser-badge ' + cls + '">' + name + '</span><br>' + openHTML(item.browser, item.url) + '</td>';
            html += '<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(function(btn) {
            btn.addEventListener('click', function() { openEdit(parseInt(this.dataset.id)); });
        });
        document.querySelectorAll('.delete-btn').forEach(function(btn) {
            btn.addEventListener('click', function() {
                if (confirm('确定删除?')) {
                    delCred(parseInt(this.dataset.id));
                    renderAdmin();
                    renderEmp();
                    toast('已删除');
                }
            });
        });
        updateStorageInfo();
    }

    function esc(s) { if (!s) return ''; return s.replace(/[&<>]/g, function(m) { return { '&':'&', '<':'<', '>':'>' }[m]; }); }

    function verifyEmp() {
        var inp = document.getElementById('empViewPwd');
        if (!inp) return;
        if (h(inp.value) === empHash) {
            empVisible = true;
            toast('验证成功', 'success');
            renderEmp();
        } else {
            empVisible = false;
            renderEmp();
            toast('密码错误', true);
        }
        inp.value = '';
    }

    function openEdit(id) {
        var item = getCredById(id);
        if (item) openModal(item);
    }

    function toast(msg, err) {
        var t = document.getElementById('toast');
        if (!t) return;
        t.textContent = msg;
        t.className = 'toast';
        if (err === true) t.classList.add('error');
        else if (err === 'success') t.classList.add('success');
        t.classList.add('show');
        clearTimeout(t._timer);
        t._timer = setTimeout(function() { t.classList.remove('show'); }, 3500);
    }

    // ===== 模态框 =====
    var modal = document.getElementById('credModal');
    var nameInp = document.getElementById('credName');
    var urlInp = document.getElementById('credUrl');
    var userInp = document.getElementById('credUsername');
    var pwdInp = document.getElementById('credPassword');
    var browserSel = document.getElementById('credBrowser');
    var editIdF = document.getElementById('editId');
    var modalTitle = document.getElementById('modalTitle');

    function openModal(entry) {
        if (!adminAuth) { toast('请先登录', true); return; }
        if (entry) {
            modalTitle.textContent = '&#9999;&#65039; 编辑凭证';
            nameInp.value = entry.name || '';
            urlInp.value = entry.url;
            userInp.value = entry.username;
            pwdInp.value = entry.password;
            browserSel.value = entry.browser || 'default';
            editIdF.value = entry.id;
        } else {
            modalTitle.textContent = '&#10133; 添加凭证';
            nameInp.value = '';
            urlInp.value = '';
            userInp.value = '';
            pwdInp.value = '';
            browserSel.value = 'default';
            editIdF.value = '';
        }
        modal.classList.add('show');
        setTimeout(function() { nameInp.focus(); }, 100);
    }

    function closeModal() { modal.classList.remove('show'); }

    function saveCred() {
        if (!adminAuth) { toast('未授权', true); return; }
        var name = nameInp.value.trim();
        var url = urlInp.value.trim();
        var user = userInp.value.trim();
        var pwd = pwdInp.value.trim();
        var browser = browserSel.value;

        if (!name) { toast('请填写网站名称', true); nameInp.focus(); return; }
        if (!url) { toast('请填写网址', true); urlInp.focus(); return; }
        if (!user) { toast('请填写账号', true); userInp.focus(); return; }
        if (!pwd) { toast('请填写密码', true); pwdInp.focus(); return; }
        if (!url.startsWith('http://') && !url.startsWith('https://')) {
            toast('网址需 http:// 或 https:// 开头', true);
            urlInp.focus();
            return;
        }

        var id = editIdF.value;
        if (upsert(id ? parseInt(id) : null, name, url, user, pwd, browser)) {
            toast(id ? '已修改' : '已添加', 'success');
            closeModal();
            renderAdmin();
            renderEmp();
        } else { toast('操作失败', true); }
    }

    // ===== 导出导入 Excel =====
    function exportExcel() {
        if (!adminAuth) { toast('请先登录', true); return; }
        var list = allCreds();
        if (!list.length) { toast('无数据', true); return; }
        var data = [['网站名称', '网址', '账号', '密码', '浏览器']];
        for (var i = 0; i < list.length; i++) {
            data.push([list[i].name || '', list[i].url, list[i].username, list[i].password, list[i].browser]);
        }
        var ws = XLSX.utils.aoa_to_sheet(data);
        var wb = XLSX.utils.book_new();
        XLSX.utils.book_append_sheet(wb, ws, 'Credentials');
        XLSX.writeFile(wb, 'password_export_' + Date.now() + '.xls', { bookType: 'xls' });
        toast('导出成功', 'success');
    }

    function importExcel(file) {
        if (!adminAuth) { toast('请先登录', true); return; }
        var reader = new FileReader();
        reader.onload = function(e) {
            var data = new Uint8Array(e.target.result);
            var workbook = XLSX.read(data, { type: 'array' });
            var sheet = workbook.Sheets[workbook.SheetNames[0]];
            var rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '' });
            if (!rows || rows.length < 2) { toast('无效文件', true); return; }
            var headers = rows[0];
            var ni = -1, ui = -1, ui2 = -1, pi = -1, bi = -1;
            for (var i = 0; i < headers.length; i++) {
                var s = String(headers[i]).toLowerCase();
                if (s.includes('网站名称') || s.includes('名称') || s.includes('name')) ni = i;
                if (s.includes('网址') || s.includes('url')) ui = i;
                if (s.includes('账号') || s.includes('用户名')) ui2 = i;
                if (s.includes('密码')) pi = i;
                if (s.includes('浏览器')) bi = i;
            }
            if (ui === -1 || ui2 === -1 || pi === -1) { toast('缺少必要列(网址、账号、密码)', true); return; }
            var records = [];
            for (var j = 1; j < rows.length; j++) {
                var row = rows[j];
                if (row && row.length > Math.max(ui, ui2, pi)) {
                    var name = (ni !== -1 && row[ni]) ? String(row[ni]).trim() : '';
                    var url = row[ui] ? String(row[ui]).trim() : '';
                    var user = row[ui2] ? String(row[ui2]).trim() : '';
                    var pwd = row[pi] ? String(row[pi]).trim() : '';
                    var browser = (bi !== -1 && row[bi]) ? String(row[bi]).trim() : 'default';
                    if (url && user && pwd && (url.startsWith('http://') || url.startsWith('https://'))) {
                        if (!name) name = url.replace(/^https?:\/\//, '').split('/')[0] || '未命名';
                        records.push({ name: name, url: url, username: user, password: pwd, browser: browser });
                    }
                }
            }
            if (!records.length) { toast('无有效数据', true); return; }
            var cnt = batchUpsert(records);
            renderAdmin(); renderEmp();
            toast('导入 ' + cnt + ' 条', 'success');
        };
        reader.readAsArrayBuffer(file);
    }

    // ===== 管理员登录 =====
    function showAdminLogin() {
        if (adminAuth) return;
        var overlay = document.createElement('div');
        overlay.className = 'admin-auth-overlay';
        overlay.id = 'adminOverlay';
        overlay.innerHTML = '<div class="admin-auth-box"><h3>&#128272; 管理员验证</h3><p>请输入管理员密码以进入后台</p><input type="password" id="adminLoginPwd" placeholder="管理员密码" autocomplete="off"><div id="adminLoginError" class="error"></div><div class="btn-group"><button class="btn btn-outline" id="adminLoginCancel">取消</button><button class="btn btn-primary" id="adminLoginConfirm">确认</button></div></div>';
        document.body.appendChild(overlay);
        var inp = document.getElementById('adminLoginPwd');
        inp.focus();
        var doAuth = function() {
            if (h(inp.value) === adminHash) {
                adminAuth = true;
                document.body.removeChild(overlay);
                toast('验证成功', 'success');
                renderAdmin();
                var panel = document.getElementById('adminPanel');
                if (!panel.classList.contains('hidden')) renderAdmin();
            } else {
                document.getElementById('adminLoginError').textContent = '&#10060; 密码错误,请重试';
                inp.value = '';
                inp.focus();
            }
        };
        var cancel = function() {
            document.body.removeChild(overlay);
            if (!adminAuth) { switchTab('employee');
                toast('已取消', true); }
        };
        document.getElementById('adminLoginConfirm').addEventListener('click', doAuth);
        document.getElementById('adminLoginCancel').addEventListener('click', cancel);
        inp.addEventListener('keypress', function(e) { if (e.key === 'Enter') doAuth(); });
    }

    // ===== Tab切换 =====
    function switchTab(tab) {
        var admin = document.getElementById('adminPanel');
        var emp = document.getElementById('employeePanel');
        var btns = document.querySelectorAll('.tab-btn');
        if (tab === 'employee') {
            admin.classList.add('hidden');
            emp.classList.remove('hidden');
            renderEmp();
            btns.forEach(function(b) { b.classList.remove('active'); });
            document.querySelector('.tab-btn[data-tab="employee"]').classList.add('active');
        } else {
            if (!adminAuth) {
                showAdminLogin();
                var iv = setInterval(function() {
                    if (adminAuth) {
                        clearInterval(iv);
                        admin.classList.remove('hidden');
                        emp.classList.add('hidden');
                        renderAdmin();
                        btns.forEach(function(b) { b.classList.remove('active'); });
                        document.querySelector('.tab-btn[data-tab="admin"]').classList.add('active');
                    }
                }, 200);
                setTimeout(function() { clearInterval(iv); }, 30000);
            } else {
                admin.classList.remove('hidden');
                emp.classList.add('hidden');
                renderAdmin();
                btns.forEach(function(b) { b.classList.remove('active'); });
                document.querySelector('.tab-btn[data-tab="admin"]').classList.add('active');
            }
        }
    }

    // ===== 主题切换 =====
    function setTheme(name) {
        var root = document.documentElement;
        var prev = document.querySelector('.theme-dot.active');
        if (prev) prev.classList.remove('active');
        var dot = document.querySelector('.theme-dot[data-theme="' + name + '"]');
        if (dot) dot.classList.add('active');
        root.className = '';
        if (name !== 'default') root.className = 'theme-' + name;
        localStorage.setItem('pwd_theme', name);
    }

    // ===== 事件绑定 =====
    document.addEventListener('DOMContentLoaded', function() {
        // 主题
        document.querySelectorAll('.theme-dot').forEach(function(d) {
            d.addEventListener('click', function() { setTheme(this.dataset.theme); });
        });
        var saved = localStorage.getItem('pwd_theme');
        if (saved) setTheme(saved);

        // 自动备份开关
        var toggleBtn = document.getElementById('autoBackupToggle');
        if (toggleBtn) {
            toggleBtn.addEventListener('click', function() {
                autoBackupEnabled = !autoBackupEnabled;
                setAutoBackupSetting(autoBackupEnabled);
                toast(autoBackupEnabled ? '自动备份已开启' : '自动备份已关闭', 'success');
                if (autoBackupEnabled && credentials.length > 0) {
                    exportToDownloadFolder();
                    toast('已备份到下载文件夹', 'success');
                }
            });
        }

        // 搜索
        var searchInput = document.getElementById('searchInput');
        var clearBtn = document.getElementById('clearSearchBtn');
        if (searchInput) {
            searchInput.addEventListener('input', function() {
                var val = this.value;
                if (val && val.trim() !== '') { clearBtn.style.display = 'inline-block'; }
                else { clearBtn.style.display = 'none'; }
                renderEmp();
            });
            searchInput.addEventListener('keydown', function(e) {
                if (e.key === 'Escape') { this.value = ''; clearBtn.style.display = 'none'; renderEmp(); this.blur(); }
            });
        }
        if (clearBtn) {
            clearBtn.addEventListener('click', function() {
                searchInput.value = '';
                this.style.display = 'none';
                renderEmp();
                searchInput.focus();
            });
        }

        // 按钮
        document.getElementById('openAddCredBtn').addEventListener('click', function() { openModal(null); });
        document.getElementById('closeModalBtn').addEventListener('click', closeModal);
        document.getElementById('saveCredBtn').addEventListener('click', saveCred);
        document.getElementById('verifyEmpBtn').addEventListener('click', verifyEmp);
        document.getElementById('empViewPwd').addEventListener('keypress', function(e) { if (e.key === 'Enter') verifyEmp(); });
        document.getElementById('exportExcelBtn').addEventListener('click', exportExcel);
        document.getElementById('exportJsonBtn').addEventListener('click', exportJson);
        document.getElementById('importExcelInput').addEventListener('change', function(e) { if (this.files.length) importExcel(this.files[0]);
            this.value = ''; });
        document.getElementById('importJsonInput').addEventListener('change', function(e) { if (this.files.length) importJson(this.files[0]);
            this.value = ''; });
        document.getElementById('importBrowserCsv').addEventListener('change', function(e) { if (this.files.length) importBrowserCsv(this.files[0]);
            this.value = ''; });

        // 密码修改
        document.getElementById('updateEmployeePwdBtn').addEventListener('click', function() {
            var inp = document.getElementById('newEmployeePwd');
            var p = inp.value;
            if (!p || p.length < 4) { toast('密码至少4位', true);
                inp.value = ''; return; }
            setEmpHash(h(p));
            empVisible = false;
            renderEmp();
            toast('员工密码已修改', 'success');
            inp.value = '';
        });
        document.getElementById('updateAdminPwdBtn').addEventListener('click', function() {
            var inp = document.getElementById('newAdminPwd');
            var p = inp.value;
            if (!p || p.length < 4) { toast('密码至少4位', true);
                inp.value = ''; return; }
            setAdminHash(h(p));
            toast('管理员密码已修改', 'success');
            inp.value = '';
        });

        // Tab
        document.querySelectorAll('.tab-btn').forEach(function(btn) {
            btn.addEventListener('click', function() { switchTab(this.dataset.tab); });
        });

        window.onclick = function(e) { if (e.target === modal) closeModal(); };
    });

    // ===== 启动 =====
    function boot() {
        try {
            initData();
            empVisible = false;
            adminAuth = false;
            renderEmp();
            renderAdmin();
            switchTab('employee');
            updateStorageInfo();
            updateLastBackupTime();
            toast('系统已启动,数据永久保存在 localStorage', 'success');
        } catch(err) {
            toast('初始化失败: ' + err.message, true);
            console.error(err);
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', boot);
    } else {
        boot();
    }
</script>
</body>
</html>
推荐
 楼主| kissfox 发表于 2026-9-2 11:22 |楼主
KILLCALL 发表于 2026-9-2 09:29
本地基本无安全一说,还是侧重便携性,毕竟账号一多容易忘记,有些还是临时账号

销售助理一直用的excel保存网址和用户密码,这也是为了让他们在工作中,可以直接点击访问连接跳转至指定浏览器来访问各个不同的erp系统
沙发
xyxst 发表于 2026-9-1 15:23
3#
KILLCALL 发表于 2026-9-1 15:57
比浏览器自带的好在哪里
4#
 楼主| kissfox 发表于 2026-9-1 16:04 |楼主
KILLCALL 发表于 2026-9-1 15:57
比浏览器自带的好在哪里

随时管理,随时使用,另外就是可根据ERP系统所需浏览器点击切换
5#
m_h 发表于 2026-9-1 16:41
不支持  firefox  edge chrome 导出的密码  导入吗?
6#
bachelor66 发表于 2026-9-1 17:39
可以做成不同员工密码查看不同内容吗?                  
7#
逐雅斋 发表于 2026-9-1 18:01
不错!本地存储和导出!
8#
布鲁惠斯 发表于 2026-9-1 18:13
本帖最后由 布鲁惠斯 于 2026-9-1 18:14 编辑

很好,不错
9#
tutu2 发表于 2026-9-1 18:27
说实话 只是做了一个UI  其实本地的还是使用db比较好
10#
YiH 发表于 2026-9-1 19:30
可以看看 简单版本的就好些
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-9-2 16:02

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表