返回列表 发布新帖
查看: 140|回复: 0

个人整改后做的html版网页密码管理系统

4658

主题

0

回帖

1万

积分

论坛元老

积分
15130
发表于 2026-9-2 00:38:51 | 查看全部 |阅读模式
小新活动线报实时推送群:594617924复制群号 (进群第一时间获取活动线报推送)
鞋服捡漏群:327839651复制群号
主要整改:
网址指定浏览器访问:
用于销售助理平时点击添加后客户的ERP的网页进入客户的ERP系统,可以根据自己在后台添加的网址指定浏览器来实现。
点击访问客户ERP 系统的时候,会自动检测指定浏览器是否已经安装,如未安装,会弹窗提示安装浏览器。
管理员后台整改:
主要整改添加客户ERP系统是选择使用什么浏览器。数据自动保存,管理员后台还修改了数据导入及导出JSON功能。
管理员后台密码默认: admin123,员工查看密码默认: employee2024
支持 Excel (.xls, .xlsx) 导入导出
所有数据存储方式改成了 localStorage
可点击「备份数据库」下载完整数据库文件
界面主题增加:
增加有8种颜色主题切换
  
html代码:
  
  1. [code]
  2. <!DOCTYPE html>
  3. <html lang=“zh-CN“>
  4. <head>
  5.     <meta charset=“UTF-8“>
  6.     <meta name=“viewport“ content=“width=device-width, initial-scale=1.0, user-scalable=yes“>
  7.     <title>企业密码管理系统</title>
  8.     <script src=“https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/sql-wasm.js“></script>
  9.     <script src=“https://cdn.sheetjs.com/xlsx-0.20.2/package/dist/xlsx.full.min.js“></script>
  10.     <script src=“https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js“></script>
  11.     <style id=“theme-style“>
  12.         :root {
  13.             --bg: #f1f5f9;
  14.             --card: #ffffff;
  15.             --border: #e9edf2;
  16.             --text: #1e293b;
  17.             --text2: #475569;
  18.             --shadow: rgba(0,0,0,0.05);
  19.             --primary: #1e3c72;
  20.             --primary-hover: #0f2b4f;
  21.             --input: #ffffff;
  22.             --input-border: #cfdfed;
  23.             --table-header: #fafcff;
  24.             --badge-default: #e2e8f0;
  25.             --badge-edge: #0078d4;
  26.             --badge-chrome: #4285f4;
  27.             --badge-firefox: #ff9400;
  28.             --success: #10b981;
  29.             --warning: #f59e0b;
  30.             --danger: #ef4444;
  31.             --info: #3b82f6;
  32.         }
  33.         .theme-red { --bg: #fef2f2; --card: #ffffff; --primary: #b91c1c; --primary-hover: #7f1d1d; --border: #fecaca; --table-header: #fef2f2; }
  34.         .theme-orange { --bg: #fff7ed; --card: #ffffff; --primary: #c2410c; --primary-hover: #9a3412; --border: #fed7aa; --table-header: #fff7ed; }
  35.         .theme-yellow { --bg: #fefce8; --card: #ffffff; --primary: #a16207; --primary-hover: #854d0e; --border: #fde68a; --table-header: #fefce8; }
  36.         .theme-green { --bg: #f0fdf4; --card: #ffffff; --primary: #15803d; --primary-hover: #166534; --border: #bbf7d0; --table-header: #f0fdf4; }
  37.         .theme-blue { --bg: #eff6ff; --card: #ffffff; --primary: #1d4ed8; --primary-hover: #1e40af; --border: #bfdbfe; --table-header: #eff6ff; }
  38.         .theme-purple { --bg: #faf5ff; --card: #ffffff; --primary: #7e22ce; --primary-hover: #6b21a8; --border: #e9d5ff; --table-header: #faf5ff; }
  39.         .theme-pink { --bg: #fdf2f8; --card: #ffffff; --primary: #be185d; --primary-hover: #9d174d; --border: #fbcfe8; --table-header: #fdf2f8; }
  40.         * { margin: 0; padding: 0; box-sizing: border-box; }
  41.         body { font-family: ‘Segoe UI‘, system-ui; background: var(--bg); padding: 20px 16px; transition: 0.3s; }
  42.         .app { max-width: 1400px; margin: 0 auto; }
  43.         .header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; margin-bottom: 20px; }
  44.         .header h1 { font-size: 1.4rem; font-weight: 600; color: var(--text); }
  45.         .theme-bar { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
  46.         .theme-bar span { font-size: 0.75rem; color: var(--text2); margin-right: 2px; }
  47.         .theme-dot { width: 28px; height: 28px; border-radius: 50%; border: 2px solid transparent; cursor: pointer; transition: 0.2s; }
  48.         .theme-dot:hover { transform: scale(1.1); }
  49.         .theme-dot.active { border-color: var(--text); box-shadow: 0 0 0 2px var(--bg); }
  50.         .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); }
  51.         .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; }
  52.         .tab-btn.active { background: var(--primary); color: #fff; }
  53.         .tab-btn:hover:not(.active) { background: var(--bg); }
  54.         .card { background: var(--card); border-radius: 20px; border: 1px solid var(--border); overflow: hidden; margin-bottom: 20px; box-shadow: 0 2px 8px var(--shadow); }
  55.         .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; }
  56.         .card-header h2 { font-size: 1.2rem; font-weight: 600; color: var(--text); }
  57.         .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); }
  58.         .btn-primary { background: var(--primary); color: #fff; }
  59.         .btn-primary:hover { background: var(--primary-hover); }
  60.         .btn-outline { background: transparent; border: 1px solid var(--border); }
  61.         .btn-outline:hover { background: var(--bg); }
  62.         .btn-success { background: var(--success); color: #fff; }
  63.         .btn-success:hover { background: #059669; }
  64.         .btn-warning { background: var(--warning); color: #fff; }
  65.         .btn-warning:hover { background: #d97706; }
  66.         .btn-danger { background: var(--danger); color: #fff; }
  67.         .btn-danger:hover { background: #dc2626; }
  68.         .btn-info { background: var(--info); color: #fff; }
  69.         .btn-info:hover { background: #2563eb; }
  70.         .btn-sm { padding: 2px 10px; font-size: 0.7rem; }
  71.         .btn-open { background: #22d3ee; color: #0e7490; }
  72.         .btn-open:hover { background: #67e8f9; }
  73.         .btn-auto-backup { background: #8b5cf6; color: #fff; }
  74.         .btn-auto-backup:hover { background: #7c3aed; }
  75.         table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
  76.         th, td { text-align: left; padding: 12px 14px; border-bottom: 1px solid var(--border); vertical-align: middle; }
  77.         th { background: var(--table-header); font-weight: 600; color: var(--text); }
  78.         .url-link { color: var(--primary); text-decoration: none; font-weight: 500; }
  79.         .url-link:hover { text-decoration: underline; }
  80.         .url-cell { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
  81.         .password-masked { font-family: monospace; background: var(--bg); padding: 2px 12px; border-radius: 20px; display: inline-block; font-size: 0.85rem; }
  82.         .password-plain { font-family: monospace; background: #dbeafe; padding: 2px 12px; border-radius: 20px; display: inline-block; font-size: 0.85rem; }
  83.         .input-group { margin-bottom: 14px; display: flex; flex-direction: column; gap: 4px; }
  84.         .input-group label { font-weight: 600; font-size: 0.8rem; color: var(--text2); }
  85.         input, select { padding: 8px 14px; border-radius: 16px; border: 1px solid var(--input-border); font-family: inherit; background: var(--input); color: var(--text); }
  86.         input:focus, select:focus { outline: none; border-color: var(--primary); }
  87.         .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; }
  88.         .modal-content { background: var(--card); max-width: 500px; width: 92%; border-radius: 24px; padding: 24px; }
  89.         .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%; }
  90.         .toast.show { opacity: 1; }
  91.         .toast.error { background: var(--danger); }
  92.         .toast.success { background: var(--success); }
  93.         .hidden { display: none; }
  94.         .flex-btns { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
  95.         .verify-panel { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
  96.         .file-input-label { background: var(--bg); border: 1px solid var(--border); padding: 6px 16px; border-radius: 30px; cursor: pointer; font-size: 0.85rem; }
  97.         input[type=“file“] { display: none; }
  98.         .settings-group { background: var(--bg); border-radius: 16px; padding: 14px 18px; margin: 12px 20px 16px 20px; border: 1px solid var(--border); }
  99.         .settings-group h4 { margin-bottom: 10px; color: var(--text); font-size: 0.9rem; }
  100.         .settings-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 6px; }
  101.         .settings-row span { font-size: 0.85rem; color: var(--text2); min-width: 130px; }
  102.         .browser-badge { display: inline-block; padding: 2px 10px; border-radius: 16px; font-size: 0.7rem; margin-right: 6px; }
  103.         .browser-default { background: var(--badge-default); color: var(--text2); }
  104.         .browser-edge { background: var(--badge-edge); color: #fff; }
  105.         .browser-chrome { background: var(--badge-chrome); color: #fff; }
  106.         .browser-firefox { background: var(--badge-firefox); color: #fff; }
  107.         .copy-url-btn { background: var(--bg); border: none; padding: 2px 8px; border-radius: 14px; cursor: pointer; font-size: 0.6rem; color: var(--text2); }
  108.         .copy-url-btn:hover { background: var(--border); }
  109.         .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; }
  110.         .open-btn-group { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; }
  111.         .open-btn-group .btn { padding: 2px 10px; font-size: 0.65rem; }
  112.         .empty-row td { text-align: center; padding: 40px; color: var(--text2); }
  113.         .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; }
  114.         .admin-auth-box { background: var(--card); border-radius: 24px; padding: 28px; width: 92%; max-width: 380px; text-align: center; }
  115.         .admin-auth-box input { width: 100%; margin: 10px 0; }
  116.         .admin-auth-box .error { color: var(--danger); font-size: 0.8rem; }
  117.         .scroll-x { overflow-x: auto; }
  118.         .browser-hint-overlay {
  119.             position: fixed;
  120.             top: 0;
  121.             left: 0;
  122.             width: 100%;
  123.             height: 100%;
  124.             background: rgba(0,0,0,0.55);
  125.             display: none;
  126.             align-items: center;
  127.             justify-content: center;
  128.             z-index: 3000;
  129.         }
  130.         .browser-hint-overlay.show { display: flex; }
  131.         .browser-hint-box {
  132.             background: var(--card);
  133.             border-radius: 24px;
  134.             padding: 32px 28px;
  135.             max-width: 460px;
  136.             width: 92%;
  137.             text-align: center;
  138.             box-shadow: 0 20px 60px rgba(0,0,0,0.3);
  139.             animation: fadeIn 0.3s ease;
  140.         }
  141.         @keyframes fadeIn {
  142.             from { opacity: 0; transform: scale(0.95); }
  143.             to { opacity: 1; transform: scale(1); }
  144.         }
  145.         .browser-hint-box .icon { font-size: 3.2rem; margin-bottom: 12px; }
  146.         .browser-hint-box h3 { font-size: 1.3rem; color: var(--text); margin-bottom: 10px; }
  147.         .browser-hint-box p { font-size: 0.9rem; color: var(--text2); line-height: 1.6; margin-bottom: 18px; }
  148.         .browser-hint-box .btn-group { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
  149.         .browser-hint-box .btn-group .btn { min-width: 100px; padding: 10px 24px; font-size: 0.9rem; }
  150.         .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); }
  151.         .browser-hint-box .checkbox-row input[type=“checkbox“] { width: 16px; height: 16px; cursor: pointer; accent-color: var(--primary); }
  152.         .storage-info {
  153.             background: var(--bg);
  154.             border-radius: 12px;
  155.             padding: 12px 18px;
  156.             margin: 8px 20px 12px 20px;
  157.             border: 1px solid var(--border);
  158.             font-size: 0.8rem;
  159.             color: var(--text2);
  160.             display: flex;
  161.             justify-content: space-between;
  162.             align-items: center;
  163.             flex-wrap: wrap;
  164.             gap: 8px;
  165.         }
  166.         .storage-info .label { font-weight: 600; color: var(--text); }
  167.         .storage-info .status { color: var(--success); }
  168.         .storage-info .backup-status { color: var(--info); font-size: 0.75rem; }
  169.         .backup-settings {
  170.             background: var(--bg);
  171.             border-radius: 12px;
  172.             padding: 12px 18px;
  173.             margin: 8px 20px 12px 20px;
  174.             border: 1px solid var(--border);
  175.             display: flex;
  176.             justify-content: space-between;
  177.             align-items: center;
  178.             flex-wrap: wrap;
  179.             gap: 10px;
  180.         }
  181.         .backup-settings .label { font-weight: 600; color: var(--text); font-size: 0.85rem; }
  182.         .backup-settings .desc { font-size: 0.75rem; color: var(--text2); }
  183.         .backup-settings .toggle { display: flex; align-items: center; gap: 8px; }
  184.         .toggle-switch {
  185.             width: 40px;
  186.             height: 22px;
  187.             background: #cbd5e1;
  188.             border-radius: 12px;
  189.             cursor: pointer;
  190.             position: relative;
  191.             transition: 0.3s;
  192.             border: none;
  193.             padding: 0;
  194.         }
  195.         .toggle-switch.active {
  196.             background: var(--success);
  197.         }
  198.         .toggle-switch::after {
  199.             content: ‘‘;
  200.             width: 18px;
  201.             height: 18px;
  202.             background: white;
  203.             border-radius: 50%;
  204.             position: absolute;
  205.             top: 2px;
  206.             left: 2px;
  207.             transition: 0.3s;
  208.             box-shadow: 0 1px 3px rgba(0,0,0,0.2);
  209.         }
  210.         .toggle-switch.active::after {
  211.             left: 20px;
  212.         }
  213.         [url=home.php?mod=space&uid=945662]@media[/url] (max-width: 600px) {
  214.             th, td { padding: 8px 10px; font-size: 0.8rem; }
  215.             .card-header { padding: 12px 14px; }
  216.             .browser-hint-box { padding: 24px 18px; }
  217.             .browser-hint-box .btn-group .btn { min-width: 80px; padding: 8px 16px; font-size: 0.8rem; }
  218.             .storage-info, .backup-settings { flex-direction: column; align-items: flex-start; }
  219.         }
  220.     </style>
  221. </head>
  222. <body>
  223. <div class=“app“>
  224.     <div class=“header“>
  225.         <h1>密码管理系统</h1>
  226.         <div class=“theme-bar“>
  227.             <span>主题</span>
  228.             <span class=“theme-dot active“ data-theme=“default“ style=“background:#64748b;“ title=“默认“></span>
  229.             <span class=“theme-dot“ data-theme=“red“ style=“background:#dc2626;“ title=“红“></span>
  230.             <span class=“theme-dot“ data-theme=“orange“ style=“background:#ea580c;“ title=“橙“></span>
  231.             <span class=“theme-dot“ data-theme=“yellow“ style=“background:#ca8a04;“ title=“黄“></span>
  232.             <span class=“theme-dot“ data-theme=“green“ style=“background:#16a34a;“ title=“绿“></span>
  233.             <span class=“theme-dot“ data-theme=“blue“ style=“background:#2563eb;“ title=“蓝“></span>
  234.             <span class=“theme-dot“ data-theme=“purple“ style=“background:#9333ea;“ title=“紫“></span>
  235.             <span class=“theme-dot“ data-theme=“pink“ style=“background:#db2777;“ title=“粉“></span>
  236.         </div>
  237.     </div>
  238.     <div class=“tab-bar“>
  239.         <button class=“tab-btn active“ data-tab=“employee“>员工门户</button>
  240.         <button class=“tab-btn“ data-tab=“admin“>管理后台</button>
  241.     </div>
  242.     <div id=“employeePanel“>
  243.         <div class=“card“>
  244.             <div class=“card-header“>
  245.                 <h2>快捷访问</h2>
  246.                 <div class=“verify-panel“>
  247.                     <input type=“password“ id=“empViewPwd“ placeholder=“查看密码“ style=“width:150px;“ autocomplete=“off“>
  248.                     <button class=“btn btn-primary“ id=“verifyEmpBtn“>显示密码</button>
  249.                 </div>
  250.             </div>
  251.             <div class=“scroll-x“>
  252.                 <table>
  253.                     <thead><tr><th>网址</th><th>账号</th><th>密码</th><th>打开方式</th></tr></thead>
  254.                     <tbody id=“employeeTableBody“></tbody>
  255.                 </table>
  256.             </div>
  257.             <div style=“padding:10px 20px;border-top:1px solid var(--border);font-size:0.7rem;color:var(--text2);“>
  258.                 <span>查看密码: <strong>employee2024</strong></span>
  259.             </div>
  260.         </div>
  261.     </div>
  262.     <div id=“adminPanel“ class=“hidden“>
  263.         <div class=“card“>
  264.             <div class=“card-header“>
  265.                 <h2>凭证管理</h2>
  266.                 <div class=“flex-btns“>
  267.                     <button class=“btn btn-primary“ id=“openAddCredBtn“>新增</button>
  268.                     <button class=“btn btn-success“ id=“exportExcelBtn“>导出 Excel</button>
  269.                     <label class=“file-input-label“ for=“importExcelInput“>导入 Excel</label>
  270.                     <input type=“file“ id=“importExcelInput“ accept=“.xls,.xlsx“>
  271.                     <button class=“btn btn-info“ id=“exportJsonBtn“> 导出 JSON</button>
  272.                     <label class=“file-input-label“ for=“importJsonInput“ style=“background:var(--primary);color:#fff;“> 导入 JSON</label>
  273.                     <input type=“file“ id=“importJsonInput“ accept=“.json“>
  274.                 </div>
  275.             </div>
  276.             <div class=“db-status“>
  277.                 <span> 数据库已连接</span>
  278.                 <span id=“dbStatusText“>localStorage 持久化存储</span>
  279.             </div>
  280.             <!-- 自动备份设置 -->
  281.             <div class=“backup-settings“>
  282.                 <div>
  283.                     <span class=“label“> 自动备份到下载文件夹</span>
  284.                     <span class=“desc“>每次修改数据后自动导出 JSON 文件到电脑下载目录</span>
  285.                 </div>
  286.                 <div class=“toggle“>
  287.                     <span id=“autoBackupStatus“ style=“font-size:0.75rem;color:var(--text2);“>已关闭</span>
  288.                     <button class=“toggle-switch“ id=“autoBackupToggle“ title=“点击切换自动备份“></button>
  289.                 </div>
  290.             </div>
  291.             <!-- 存储信息 -->
  292.             <div class=“storage-info“>
  293.                 <span><span class=“label“> 存储位置:</span>浏览器 localStorage</span>
  294.                 <span><span class=“label“> 记录数:</span><span id=“recordCount“>0</span> 条</span>
  295.                 <span><span class=“label“> 持久化:</span><span class=“status“>✅ 刷新/关闭不丢失</span></span>
  296.                 <span class=“backup-status“ id=“lastBackupTime“> 最后备份:无</span>
  297.             </div>
  298.             <div class=“scroll-x“>
  299.                 <table>
  300.                     <thead><tr><th>网址</th><th>账号</th><th>密码</th><th>浏览器</th><th>操作</th></tr></thead>
  301.                     <tbody id=“adminTableBody“></tbody>
  302.                 </table>
  303.             </div>
  304.             <div class=“settings-group“>
  305.                 <h4>⚙️ 安全设置</h4>
  306.                 <div class=“settings-row“><span>员工查看密码</span><input type=“password“ id=“newEmployeePwd“ placeholder=“新密码“ style=“width:160px;“><button class=“btn btn-warning“ id=“updateEmployeePwdBtn“>修改</button></div>
  307.                 <div class=“settings-row“><span>管理员密码</span><input type=“password“ id=“newAdminPwd“ placeholder=“新密码“ style=“width:160px;“><button class=“btn btn-warning“ id=“updateAdminPwdBtn“>修改</button></div>
  308.             </div>
  309.             <div style=“padding:14px 20px;border-top:1px solid var(--border);font-size:0.75rem;color:var(--text2);“>
  310.                 <span>管理员: <strong>admin123</strong></span>
  311.                 <span style=“margin-left:16px;“>员工: <strong>employee2024</strong></span>
  312.                 <span style=“margin-left:16px;“> 开启「自动备份」后每次修改自动导出 JSON 到下载文件夹</span>
  313.             </div>
  314.         </div>
  315.     </div>
  316. </div>
  317. <!-- 浏览器提示弹窗 -->
  318. <div class=“browser-hint-overlay“ id=“browserHintOverlay“>
  319.     <div class=“browser-hint-box“>
  320.         <div class=“icon“ id=“hintIcon“></div>
  321.         <h3 id=“hintTitle“>浏览器</h3>
  322.         <p id=“hintDesc“>请先确认电脑已安装该浏览器,如未安装请先下载。</p>
  323.         <div class=“checkbox-row“>
  324.             <input type=“checkbox“ id=“hintDontShowAgain“>
  325.             <label for=“hintDontShowAgain“>下次不再提示</label>
  326.         </div>
  327.         <div class=“btn-group“>
  328.             <button class=“btn btn-outline“ id=“hintCancel“>取消</button>
  329.             <button class=“btn btn-warning“ id=“hintDownload“>⬇ 下载浏览器</button>
  330.             <button class=“btn btn-primary“ id=“hintConfirm“>确定,已安装</button>
  331.         </div>
  332.     </div>
  333. </div>
  334. <div id=“credModal“ class=“modal“>
  335.     <div class=“modal-content“>
  336.         <h3 id=“modalTitle“>添加凭证</h3>
  337.         <div class=“input-group“><label>网址</label><input type=“text“ id=“credUrl“ placeholder=“https://...“></div>
  338.         <div class=“input-group“><label>账号</label><input type=“text“ id=“credUsername“ placeholder=“账号“></div>
  339.         <div class=“input-group“><label>密码</label><input type=“text“ id=“credPassword“ placeholder=“密码“></div>
  340.         <div class=“input-group“>
  341.             <label>浏览器</label>
  342.             <select id=“credBrowser“>
  343.                 <option value=“default“>默认</option>
  344.                 <option value=“edge“>Edge</option>
  345.                 <option value=“chrome“>Chrome</option>
  346.                 <option value=“firefox“>Firefox</option>
  347.             </select>
  348.         </div>
  349.         <input type=“hidden“ id=“editId“ value=““>
  350.         <div style=“display:flex;gap:10px;justify-content:flex-end;margin-top:16px;“>
  351.             <button class=“btn btn-outline“ id=“closeModalBtn“>取消</button>
  352.             <button class=“btn btn-primary“ id=“saveCredBtn“>保存</button>
  353.         </div>
  354.     </div>
  355. </div>
  356. <div id=“toast“ class=“toast“>操作成功</div>
  357. <script>
  358.     function h(s) { return CryptoJS.SHA256(s).toString(); }
  359.     // ===== 浏览器配置 =====
  360.     var BROWSER_CONFIG = {
  361.         edge: {
  362.             icon: ‘‘,
  363.             name: ‘Edge 浏览器‘,
  364.             desc: ‘使用前提:已安装Edge浏览器。(未安装请先下载安装)。‘,
  365.             download: ‘https://www.microsoft.com/zh-cn/edge/download‘,
  366.             protocol: ‘microsoft-edge:‘
  367.         },
  368.         chrome: {
  369.             icon: ‘‘,
  370.             name: ‘谷歌浏览器‘,
  371.             desc: ‘使用前提:已安装谷歌浏览器。(未安装请先下载安装)。‘,
  372.             download: ‘https://sw.pcmgr.qq.com/62a4683d5d6deb5f13daf8e29b0d90dd/6a962b0c/spcmgr/download/152.0.7977.65_chrome_installer_uncompressed-win64.exe‘,
  373.             protocol: ‘googlechrome:‘
  374.         },
  375.         firefox: {
  376.             icon: ‘力‘,
  377.             name: ‘火狐浏览器‘,
  378.             desc: ‘使用前提:已安装火狐浏览器。(未安装请先下载安装)。‘,
  379.             download: ‘https://sw.pcmgr.qq.com/e38a6e930fd7892da4c3d82d122394cb/6a962af4/spcmgr/download/22931-110819-Firefox-setup-win64_116.0.3.8627.exe‘,
  380.             protocol: ‘firefox:‘
  381.         }
  382.     };
  383.     // ===== 浏览器提示弹窗控制 =====
  384.     var hintOverlay = document.getElementById(‘browserHintOverlay‘);
  385.     var hintDontShowAgain = document.getElementById(‘hintDontShowAgain‘);
  386.     var hintIcon = document.getElementById(‘hintIcon‘);
  387.     var hintTitle = document.getElementById(‘hintTitle‘);
  388.     var hintDesc = document.getElementById(‘hintDesc‘);
  389.     var hintDownload = document.getElementById(‘hintDownload‘);
  390.     var hintConfirm = document.getElementById(‘hintConfirm‘);
  391.     var hintCancel = document.getElementById(‘hintCancel‘);
  392.     var pendingBrowser = null;
  393.     var pendingUrl = null;
  394.     function getHintSetting(browser) {
  395.         return localStorage.getItem(‘hint_dismissed_‘ + browser) === ‘true‘;
  396.     }
  397.     function setHintSetting(browser, dismissed) {
  398.         localStorage.setItem(‘hint_dismissed_‘ + browser, dismissed ? ‘true‘ : ‘false‘);
  399.     }
  400.     function showBrowserHint(browser, url) {
  401.         if (getHintSetting(browser)) {
  402.             doOpenBrowser(browser, url);
  403.             return;
  404.         }
  405.         var config = BROWSER_CONFIG[browser];
  406.         if (!config) { window.open(url, ‘_blank‘); return; }
  407.         pendingBrowser = browser;
  408.         pendingUrl = url;
  409.         hintIcon.textContent = config.icon;
  410.         hintTitle.textContent = config.name;
  411.         hintDesc.textContent = config.desc;
  412.         hintDownload.textContent = ‘⬇ 下载 ‘ + config.name;
  413.         hintOverlay.classList.add(‘show‘);
  414.         hintDontShowAgain.checked = false;
  415.     }
  416.     function hideBrowserHint() {
  417.         hintOverlay.classList.remove(‘show‘);
  418.     }
  419.     function doOpenBrowser(b, u) {
  420.         var config = BROWSER_CONFIG[b];
  421.         if (!config) { window.open(u, ‘_blank‘); return; }
  422.         var url = u;
  423.         if (!url.startsWith(‘http://‘) && !url.startsWith(‘https://‘)) {
  424.             url = ‘https://‘ + url;
  425.         }
  426.         var fullUrl = config.protocol + url;
  427.         try {
  428.             var currentUrl = window.location.href;
  429.             window.location.href = fullUrl;
  430.             setTimeout(function() {
  431.                 if (window.location.href === currentUrl) {
  432.                     tryMethod2(b, url);
  433.                 }
  434.             }, 800);
  435.         } catch(e) {
  436.             tryMethod2(b, url);
  437.         }
  438.     }
  439.     function tryMethod2(b, u) {
  440.         var config = BROWSER_CONFIG[b];
  441.         var url = u;
  442.         if (!url.startsWith(‘http://‘) && !url.startsWith(‘https://‘)) {
  443.             url = ‘https://‘ + url;
  444.         }
  445.         var protocols = [
  446.             config.protocol + url,
  447.             config.protocol + ‘//open?url=‘ + encodeURIComponent(url),
  448.             config.protocol + ‘//‘ + url
  449.         ];
  450.         for (var i = 0; i < protocols.length; i++) {
  451.             try {
  452.                 var w = window.open(protocols[i], ‘_blank‘);
  453.                 if (w) {
  454.                     setTimeout(function(win) {
  455.                         try { if (win && win.closed) { tryMethod3(b, u); } } catch(e) { tryMethod3(b, u); }
  456.                     }, 1500, w);
  457.                     return;
  458.                 }
  459.             } catch(e) {}
  460.         }
  461.         tryMethod3(b, u);
  462.     }
  463.     function tryMethod3(b, u) {
  464.         var config = BROWSER_CONFIG[b];
  465.         var url = u;
  466.         if (!url.startsWith(‘http://‘) && !url.startsWith(‘https://‘)) {
  467.             url = ‘https://‘ + url;
  468.         }
  469.         var fullUrl = config.protocol + url;
  470.         try {
  471.             var iframe = document.createElement(‘iframe‘);
  472.             iframe.style.display = ‘none‘;
  473.             iframe.src = fullUrl;
  474.             document.body.appendChild(iframe);
  475.             setTimeout(function() {
  476.                 try { document.body.removeChild(iframe); } catch(e) {}
  477.                 showDownloadDialog(b, u);
  478.             }, 2000);
  479.         } catch(e) {
  480.             showDownloadDialog(b, u);
  481.         }
  482.     }
  483.     function showDownloadDialog(b, u) {
  484.         var config = BROWSER_CONFIG[b];
  485.         var name = config ? config.name : ‘该浏览器‘;
  486.         var url = config ? config.download : null;
  487.         if (url) {
  488.             if (confirm(‘未检测到 ‘ + name + ‘,是否下载?\n\n点击「确定」前往下载\n点击「取消」使用默认浏览器打开‘)) {
  489.                 window.open(url, ‘_blank‘);
  490.             } else {
  491.                 window.open(u, ‘_blank‘);
  492.             }
  493.         } else {
  494.             window.open(u, ‘_blank‘);
  495.         }
  496.     }
  497.     hintConfirm.addEventListener(‘click‘, function() {
  498.         if (hintDontShowAgain.checked && pendingBrowser) {
  499.             setHintSetting(pendingBrowser, true);
  500.         }
  501.         hideBrowserHint();
  502.         if (pendingUrl && pendingBrowser) {
  503.             setTimeout(function() { doOpenBrowser(pendingBrowser, pendingUrl); }, 150);
  504.         }
  505.     });
  506.     hintCancel.addEventListener(‘click‘, function() {
  507.         hideBrowserHint();
  508.         pendingBrowser = null;
  509.         pendingUrl = null;
  510.     });
  511.     hintDownload.addEventListener(‘click‘, function() {
  512.         if (pendingBrowser) {
  513.             var config = BROWSER_CONFIG[pendingBrowser];
  514.             if (config && config.download) {
  515.                 window.open(config.download, ‘_blank‘);
  516.             }
  517.         }
  518.     });
  519.     hintOverlay.addEventListener(‘click‘, function(e) {
  520.         if (e.target === hintOverlay) {
  521.             hideBrowserHint();
  522.             pendingBrowser = null;
  523.             pendingUrl = null;
  524.         }
  525.     });
  526.     // ===== 浏览器打开入口 =====
  527.     function bn(b) {
  528.         var map = { edge:‘Edge浏览器打开‘, chrome:‘谷歌浏览器打开‘, firefox:‘火狐浏览器打开‘ };
  529.         return map[b] || ‘默认浏览器打开‘;
  530.     }
  531.     function bc(b) {
  532.         var map = { edge:‘browser-edge‘, chrome:‘browser-chrome‘, firefox:‘browser-firefox‘ };
  533.         return map[b] || ‘browser-default‘;
  534.     }
  535.     function openWith(b, u) {
  536.         if (b === ‘default‘ || !b) { window.open(u, ‘_blank‘); return; }
  537.         showBrowserHint(b, u);
  538.     }
  539.     function openHTML(b, u) {
  540.         var name = bn(b);
  541.         var cls = bc(b);
  542.         var su = u.replace(/‘/g, “\\‘“);
  543.         var html = ‘<div class=“open-btn-group“>‘;
  544.         if (b === ‘default‘) {
  545.             html += ‘<button class=“btn btn-open btn-sm“>打开</button>‘;
  546.         } else {
  547.             html += ‘<button class=“btn btn-open btn-sm“>‘ + name + ‘</button>‘;
  548.             html += ‘<button class=“btn btn-outline btn-sm“>默认浏览器打开</button>‘;
  549.         }
  550.         html += ‘</div>‘;
  551.         return html;
  552.     }
  553.     function copyText(t, msg) {
  554.         try {
  555.             navigator.clipboard.writeText(t);
  556.             toast(msg);
  557.         } catch(e) {
  558.             var ta = document.createElement(‘textarea‘);
  559.             ta.value = t;
  560.             document.body.appendChild(ta);
  561.             ta.select();
  562.             document.execCommand(‘copy‘);
  563.             document.body.removeChild(ta);
  564.             toast(msg);
  565.         }
  566.     }
  567.     // ============================================================
  568.     // ===== 数据存储层 - localStorage 持久化存储 =====
  569.     // ============================================================
  570.     var STORAGE_KEY = ‘password_manager_data‘;
  571.     var CONFIG_KEY = ‘password_manager_config‘;
  572.     var empVisible = false;
  573.     var adminAuth = false;
  574.     var empHash = ‘‘;
  575.     var adminHash = ‘‘;
  576.     var autoBackupEnabled = false;
  577.     var lastBackupTime = null;
  578.     var credentials = [];
  579.     var nextId = 1;
  580.     // 默认密码
  581.     var DEF_EMP = h(‘employee2024‘);
  582.     var DEF_ADMIN = h(‘admin123‘);
  583.     // ===== 数据操作 =====
  584.     function loadData() {
  585.         try {
  586.             var saved = localStorage.getItem(STORAGE_KEY);
  587.             if (saved) {
  588.                 var parsed = JSON.parse(saved);
  589.                 credentials = parsed.credentials || [];
  590.                 nextId = parsed.nextId || 1;
  591.                 return true;
  592.             }
  593.         } catch(e) {
  594.             console.error(‘加载数据失败:‘, e);
  595.         }
  596.         return false;
  597.     }
  598.     function saveData() {
  599.         try {
  600.             var data = {
  601.                 credentials: credentials,
  602.                 nextId: nextId
  603.             };
  604.             localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
  605.             return true;
  606.         } catch(e) {
  607.             console.error(‘保存数据失败:‘, e);
  608.             return false;
  609.         }
  610.     }
  611.     function loadConfig() {
  612.         try {
  613.             var saved = localStorage.getItem(CONFIG_KEY);
  614.             if (saved) {
  615.                 var parsed = JSON.parse(saved);
  616.                 empHash = parsed.empHash || DEF_EMP;
  617.                 adminHash = parsed.adminHash || DEF_ADMIN;
  618.                 autoBackupEnabled = parsed.autoBackupEnabled || false;
  619.                 return true;
  620.             }
  621.         } catch(e) {
  622.             console.error(‘加载配置失败:‘, e);
  623.         }
  624.         return false;
  625.     }
  626.     function saveConfig() {
  627.         try {
  628.             var data = {
  629.                 empHash: empHash,
  630.                 adminHash: adminHash,
  631.                 autoBackupEnabled: autoBackupEnabled
  632.             };
  633.             localStorage.setItem(CONFIG_KEY, JSON.stringify(data));
  634.             return true;
  635.         } catch(e) {
  636.             console.error(‘保存配置失败:‘, e);
  637.             return false;
  638.         }
  639.     }
  640.     function initData() {
  641.         var loaded = loadData();
  642.         var configLoaded = loadConfig();
  643.         if (!loaded) {
  644.             credentials = [
  645.                 { id: 1, url: ‘https://mail.company.com‘, username: ‘zhang.san@company.com‘, password: ‘Mail@2024Secure‘, browser: ‘default‘ },
  646.                 { id: 2, url: ‘https://hrsystem.company.com‘, username: ‘hr_admin‘, password: ‘HrP@ss#2024‘, browser: ‘edge‘ },
  647.                 { id: 3, url: ‘https://gitlab.company.com‘, username: ‘dev_lihua‘, password: ‘GitLab!7890‘, browser: ‘chrome‘ },
  648.                 { id: 4, url: ‘https://jira.company.com‘, username: ‘project_manager‘, password: ‘Jira@2025‘, browser: ‘firefox‘ }
  649.             ];
  650.             nextId = 5;
  651.             saveData();
  652.         }
  653.         if (!configLoaded) {
  654.             empHash = DEF_EMP;
  655.             adminHash = DEF_ADMIN;
  656.             autoBackupEnabled = false;
  657.             saveConfig();
  658.         }
  659.         updateStorageInfo();
  660.         updateLastBackupTime();
  661.         updateAutoBackupUI();
  662.     }
  663.     // ===== 自动备份 =====
  664.     function getAutoBackupSetting() {
  665.         return autoBackupEnabled;
  666.     }
  667.     function setAutoBackupSetting(enabled) {
  668.         autoBackupEnabled = enabled;
  669.         saveConfig();
  670.         updateAutoBackupUI();
  671.     }
  672.     function updateAutoBackupUI() {
  673.         var toggle = document.getElementById(‘autoBackupToggle‘);
  674.         var status = document.getElementById(‘autoBackupStatus‘);
  675.         if (toggle) {
  676.             toggle.classList.toggle(‘active‘, autoBackupEnabled);
  677.         }
  678.         if (status) {
  679.             status.textContent = autoBackupEnabled ? ‘✅ 已开启‘ : ‘⛔ 已关闭‘;
  680.             status.style.color = autoBackupEnabled ? ‘var(--success)‘ : ‘var(--text2)‘;
  681.         }
  682.     }
  683.     function exportToDownloadFolder() {
  684.         try {
  685.             var data = {
  686.                 credentials: credentials,
  687.                 nextId: nextId,
  688.                 exportTime: new Date().toISOString(),
  689.                 version: ‘1.0‘
  690.             };
  691.             var json = JSON.stringify(data, null, 2);
  692.             var blob = new Blob([json], { type: ‘application/json‘ });
  693.             var url = URL.createObjectURL(blob);
  694.             var a = document.createElement(‘a‘);
  695.             a.href = url;
  696.             var now = new Date();
  697.             var dateStr = now.getFullYear() + ‘-‘ +
  698.                 String(now.getMonth()+1).padStart(2,‘0‘) + ‘-‘ +
  699.                 String(now.getDate()).padStart(2,‘0‘) + ‘_‘ +
  700.                 String(now.getHours()).padStart(2,‘0‘) + ‘-‘ +
  701.                 String(now.getMinutes()).padStart(2,‘0‘);
  702.             a.download = ‘password_backup_‘ + dateStr + ‘.json‘;
  703.             document.body.appendChild(a);
  704.             a.click();
  705.             document.body.removeChild(a);
  706.             setTimeout(function() {
  707.                 URL.revokeObjectURL(url);
  708.             }, 5000);
  709.             lastBackupTime = now;
  710.             updateLastBackupTime();
  711.             return true;
  712.         } catch(e) {
  713.             console.error(‘自动备份失败:‘, e);
  714.             return false;
  715.         }
  716.     }
  717.     function updateLastBackupTime() {
  718.         var el = document.getElementById(‘lastBackupTime‘);
  719.         if (!el) return;
  720.         if (lastBackupTime) {
  721.             var str = lastBackupTime.getFullYear() + ‘-‘ +
  722.                 String(lastBackupTime.getMonth()+1).padStart(2,‘0‘) + ‘-‘ +
  723.                 String(lastBackupTime.getDate()).padStart(2,‘0‘) + ‘ ‘ +
  724.                 String(lastBackupTime.getHours()).padStart(2,‘0‘) + ‘:‘ +
  725.                 String(lastBackupTime.getMinutes()).padStart(2,‘0‘);
  726.             el.textContent = ‘ 最后备份:‘ + str;
  727.         } else {
  728.             el.textContent = ‘ 最后备份:无‘;
  729.         }
  730.     }
  731.     // ===== 增删改查 =====
  732.     function allCreds() {
  733.         return credentials.slice().sort(function(a, b) { return b.id - a.id; });
  734.     }
  735.     function getCredById(id) {
  736.         for (var i = 0; i < credentials.length; i++) {
  737.             if (credentials[i].id === id) return credentials[i];
  738.         }
  739.         return null;
  740.     }
  741.     function upsert(id, url, username, password, browser) {
  742.         if (!url || !username || !password) return false;
  743.         if (id === null || id === undefined || id === ‘‘) {
  744.             var newCred = {
  745.                 id: nextId++,
  746.                 url: url,
  747.                 username: username,
  748.                 password: password,
  749.                 browser: browser || ‘default‘
  750.             };
  751.             credentials.push(newCred);
  752.         } else {
  753.             var found = false;
  754.             for (var i = 0; i < credentials.length; i++) {
  755.                 if (credentials[i].id === id) {
  756.                     credentials[i].url = url;
  757.                     credentials[i].username = username;
  758.                     credentials[i].password = password;
  759.                     credentials[i].browser = browser || ‘default‘;
  760.                     found = true;
  761.                     break;
  762.                 }
  763.             }
  764.             if (!found) return false;
  765.         }
  766.         saveData();
  767.         updateStorageInfo();
  768.         if (autoBackupEnabled) {
  769.             exportToDownloadFolder();
  770.         }
  771.         return true;
  772.     }
  773.     function delCred(id) {
  774.         for (var i = 0; i < credentials.length; i++) {
  775.             if (credentials[i].id === id) {
  776.                 credentials.splice(i, 1);
  777.                 saveData();
  778.                 updateStorageInfo();
  779.                 if (autoBackupEnabled) {
  780.                     exportToDownloadFolder();
  781.                 }
  782.                 return true;
  783.             }
  784.         }
  785.         return false;
  786.     }
  787.     function batchUpsert(records) {
  788.         var cnt = 0;
  789.         for (var i = 0; i < records.length; i++) {
  790.             var rec = records[i];
  791.             if (rec.url && rec.username && rec.password) {
  792.                 var exists = false;
  793.                 for (var j = 0; j < credentials.length; j++) {
  794.                     if (credentials[j].url === rec.url) {
  795.                         credentials[j].username = rec.username;
  796.                         credentials[j].password = rec.password;
  797.                         credentials[j].browser = rec.browser || ‘default‘;
  798.                         exists = true;
  799.                         cnt++;
  800.                         break;
  801.                     }
  802.                 }
  803.                 if (!exists) {
  804.                     credentials.push({
  805.                         id: nextId++,
  806.                         url: rec.url,
  807.                         username: rec.username,
  808.                         password: rec.password,
  809.                         browser: rec.browser || ‘default‘
  810.                     });
  811.                     cnt++;
  812.                 }
  813.             }
  814.         }
  815.         saveData();
  816.         updateStorageInfo();
  817.         if (autoBackupEnabled) {
  818.             exportToDownloadFolder();
  819.         }
  820.         return cnt;
  821.     }
  822.     function updateStorageInfo() {
  823.         var el = document.getElementById(‘recordCount‘);
  824.         if (el) el.textContent = credentials.length;
  825.     }
  826.     // ===== 密码配置 =====
  827.     function getEmpHash() { return empHash; }
  828.     function getAdminHash() { return adminHash; }
  829.     function setEmpHash(newHash) {
  830.         empHash = newHash;
  831.         saveConfig();
  832.     }
  833.     function setAdminHash(newHash) {
  834.         adminHash = newHash;
  835.         saveConfig();
  836.     }
  837.     // ===== 导出/导入 JSON =====
  838.     function exportJson() {
  839.         try {
  840.             var data = {
  841.                 credentials: credentials,
  842.                 nextId: nextId,
  843.                 exportTime: new Date().toISOString(),
  844.                 version: ‘1.0‘
  845.             };
  846.             var json = JSON.stringify(data, null, 2);
  847.             var blob = new Blob([json], { type: ‘application/json‘ });
  848.             var url = URL.createObjectURL(blob);
  849.             var a = document.createElement(‘a‘);
  850.             a.href = url;
  851.             var now = new Date();
  852.             var dateStr = now.getFullYear() + ‘-‘ +
  853.                 String(now.getMonth()+1).padStart(2,‘0‘) + ‘-‘ +
  854.                 String(now.getDate()).padStart(2,‘0‘) + ‘_‘ +
  855.                 String(now.getHours()).padStart(2,‘0‘) + ‘-‘ +
  856.                 String(now.getMinutes()).padStart(2,‘0‘);
  857.             a.download = ‘password_data_‘ + dateStr + ‘.json‘;
  858.             document.body.appendChild(a);
  859.             a.click();
  860.             document.body.removeChild(a);
  861.             setTimeout(function() {
  862.                 URL.revokeObjectURL(url);
  863.             }, 5000);
  864.             toast(‘数据已导出到下载目录‘, ‘success‘);
  865.         } catch(e) {
  866.             toast(‘导出失败: ‘ + e.message, true);
  867.         }
  868.     }
  869.     function importJson(file) {
  870.         var reader = new FileReader();
  871.         reader.onload = function(e) {
  872.             try {
  873.                 var data = JSON.parse(e.target.result);
  874.                 if (!data.credentials || !Array.isArray(data.credentials)) {
  875.                     toast(‘无效的数据文件‘, true);
  876.                     return;
  877.                 }
  878.                 if (!confirm(‘确定要恢复此数据吗?当前数据将被覆盖!‘)) {
  879.                     return;
  880.                 }
  881.                 credentials = data.credentials;
  882.                 nextId = data.nextId || (credentials.length > 0 ? Math.max.apply(null, credentials.map(function(c) { return c.id; })) + 1 : 1);
  883.                 saveData();
  884.                 updateStorageInfo();
  885.                 if (autoBackupEnabled) {
  886.                     exportToDownloadFolder();
  887.                 }
  888.                 renderEmp();
  889.                 renderAdmin();
  890.                 toast(‘数据恢复成功‘, ‘success‘);
  891.             } catch(err) {
  892.                 toast(‘无效的数据文件: ‘ + err.message, true);
  893.             }
  894.         };
  895.         reader.readAsText(file);
  896.     }
  897.     // ===== 渲染 =====
  898.     function renderEmp() {
  899.         var tbody = document.getElementById(‘employeeTableBody‘);
  900.         if (!tbody) return;
  901.         var list = allCreds();
  902.         if (!list.length) {
  903.             tbody.innerHTML = ‘<tr class=“empty-row“><td colspan=“4“>暂无数据</td></tr>‘;
  904.             return;
  905.         }
  906.         var html = ‘‘;
  907.         for (var i = 0; i < list.length; i++) {
  908.             var item = list[i];
  909.             var name = bn(item.browser);
  910.             var cls = bc(item.browser);
  911.             var pwd = empVisible ?
  912.                 ‘<span class=“password-plain“>‘ + esc(item.password) + ‘</span>‘ :
  913.                 ‘<span class=“password-masked“>●●●●●●●●</span>‘;
  914.             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>‘;
  915.             html += ‘<td>‘ + esc(item.username) + ‘</td><td>‘ + pwd + ‘</td>‘;
  916.             html += ‘<td><span class=“browser-badge ‘ + cls + ‘“>‘ + name + ‘</span><br>‘ + openHTML(item.browser, item.url) + ‘</td></tr>‘;
  917.         }
  918.         tbody.innerHTML = html;
  919.         var btns = document.querySelectorAll(‘.copy-url-btn‘);
  920.         for (var j = 0; j < btns.length; j++) {
  921.             btns[j].addEventListener(‘click‘, function() { copyText(this.dataset.url, ‘网址已复制‘); });
  922.         }
  923.     }
  924.     function renderAdmin() {
  925.         var tbody = document.getElementById(‘adminTableBody‘);
  926.         if (!tbody) return;
  927.         if (!adminAuth) {
  928.             tbody.innerHTML = ‘<tr class=“empty-row“><td colspan=“5“>请验证管理员权限</td></tr>‘;
  929.             return;
  930.         }
  931.         var list = allCreds();
  932.         if (!list.length) {
  933.             tbody.innerHTML = ‘<tr class=“empty-row“><td colspan=“5“>暂无数据,点击“新增“添加</td></tr>‘;
  934.             return;
  935.         }
  936.         var html = ‘‘;
  937.         for (var i = 0; i < list.length; i++) {
  938.             var item = list[i];
  939.             var name = bn(item.browser);
  940.             var cls = bc(item.browser);
  941.             html += ‘<tr><td><a href=“‘ + esc(item.url) + ‘“ target=“_blank“ class=“url-link“>‘ + esc(item.url) + ‘</a></td>‘;
  942.             html += ‘<td>‘ + esc(item.username) + ‘</td>‘;
  943.             html += ‘<td><span class=“password-plain“>‘ + esc(item.password) + ‘</span></td>‘;
  944.             html += ‘<td><span class=“browser-badge ‘ + cls + ‘“>‘ + name + ‘</span><br>‘ + openHTML(item.browser, item.url) + ‘</td>‘;
  945.             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>‘;
  946.         }
  947.         tbody.innerHTML = html;
  948.         var eds = document.querySelectorAll(‘.edit-btn‘);
  949.         for (var j = 0; j < eds.length; j++) {
  950.             eds[j].addEventListener(‘click‘, function() { openEdit(parseInt(this.dataset.id)); });
  951.         }
  952.         var dels = document.querySelectorAll(‘.delete-btn‘);
  953.         for (var k = 0; k < dels.length; k++) {
  954.             dels[k].addEventListener(‘click‘, function() {
  955.                 if (confirm(‘确定删除?‘)) {
  956.                     delCred(parseInt(this.dataset.id));
  957.                     renderAdmin();
  958.                     renderEmp();
  959.                     toast(‘已删除‘);
  960.                 }
  961.             });
  962.         }
  963.         updateStorageInfo();
  964.     }
  965.     function esc(s) { if (!s) return ‘‘; return s.replace(/[&<>]/g, function(m) { return { ‘&‘:‘&‘, ‘<‘:‘<‘, ‘>‘:‘>‘ }[m]; }); }
  966.     function verifyEmp() {
  967.         var inp = document.getElementById(‘empViewPwd‘);
  968.         if (!inp) return;
  969.         if (h(inp.value) === empHash) {
  970.             empVisible = true;
  971.             toast(‘验证成功‘, ‘success‘);
  972.             renderEmp();
  973.         } else {
  974.             empVisible = false;
  975.             renderEmp();
  976.             toast(‘密码错误‘, true);
  977.         }
  978.         inp.value = ‘‘;
  979.     }
  980.     function openEdit(id) {
  981.         var item = getCredById(id);
  982.         if (item) openModal(item);
  983.     }
  984.     function toast(msg, err) {
  985.         var t = document.getElementById(‘toast‘);
  986.         if (!t) return;
  987.         t.textContent = msg;
  988.         t.className = ‘toast‘;
  989.         if (err === true) t.classList.add(‘error‘);
  990.         else if (err === ‘success‘) t.classList.add(‘success‘);
  991.         t.classList.add(‘show‘);
  992.         clearTimeout(t._timer);
  993.         t._timer = setTimeout(function() { t.classList.remove(‘show‘); }, 3000);
  994.     }
  995.     // ===== 模态框 =====
  996.     var modal = document.getElementById(‘credModal‘);
  997.     var urlInp = document.getElementById(‘credUrl‘);
  998.     var userInp = document.getElementById(‘credUsername‘);
  999.     var pwdInp = document.getElementById(‘credPassword‘);
  1000.     var browserSel = document.getElementById(‘credBrowser‘);
  1001.     var editIdF = document.getElementById(‘editId‘);
  1002.     var modalTitle = document.getElementById(‘modalTitle‘);
  1003.     function openModal(entry) {
  1004.         if (!adminAuth) { toast(‘请先登录‘, true); return; }
  1005.         if (entry) {
  1006.             modalTitle.textContent = ‘编辑凭证‘;
  1007.             urlInp.value = entry.url;
  1008.             userInp.value = entry.username;
  1009.             pwdInp.value = entry.password;
  1010.             browserSel.value = entry.browser || ‘default‘;
  1011.             editIdF.value = entry.id;
  1012.         } else {
  1013.             modalTitle.textContent = ‘添加凭证‘;
  1014.             urlInp.value = ‘‘;
  1015.             userInp.value = ‘‘;
  1016.             pwdInp.value = ‘‘;
  1017.             browserSel.value = ‘default‘;
  1018.             editIdF.value = ‘‘;
  1019.         }
  1020.         modal.style.display = ‘flex‘;
  1021.     }
  1022.     function closeModal() { modal.style.display = ‘none‘; }
  1023.     function saveCred() {
  1024.         if (!adminAuth) { toast(‘未授权‘, true); return; }
  1025.         var url = urlInp.value.trim();
  1026.         var user = userInp.value.trim();
  1027.         var pwd = pwdInp.value.trim();
  1028.         var browser = browserSel.value;
  1029.         if (!url || !user || !pwd) { toast(‘请填完整‘, true); return; }
  1030.         if (!url.startsWith(‘http://‘) && !url.startsWith(‘https://‘)) {
  1031.             toast(‘网址需 http:// 或 https:// 开头‘, true);
  1032.             return;
  1033.         }
  1034.         var id = editIdF.value;
  1035.         if (upsert(id ? parseInt(id) : null, url, user, pwd, browser)) {
  1036.             toast(id ? ‘已修改‘ : ‘已添加‘, ‘success‘);
  1037.             closeModal();
  1038.             renderAdmin();
  1039.             renderEmp();
  1040.         } else { toast(‘操作失败‘, true); }
  1041.     }
  1042.     // ===== 导出导入 Excel =====
  1043.     function exportExcel() {
  1044.         if (!adminAuth) { toast(‘请先登录‘, true); return; }
  1045.         var list = allCreds();
  1046.         if (!list.length) { toast(‘无数据‘, true); return; }
  1047.         var data = [[‘网址‘, ‘账号‘, ‘密码‘, ‘浏览器‘]];
  1048.         for (var i = 0; i < list.length; i++) {
  1049.             data.push([list[i].url, list[i].username, list[i].password, list[i].browser]);
  1050.         }
  1051.         var ws = XLSX.utils.aoa_to_sheet(data);
  1052.         var wb = XLSX.utils.book_new();
  1053.         XLSX.utils.book_append_sheet(wb, ws, ‘Credentials‘);
  1054.         XLSX.writeFile(wb, ‘password_export_‘ + Date.now() + ‘.xls‘, { bookType: ‘xls‘ });
  1055.         toast(‘导出成功‘, ‘success‘);
  1056.     }
  1057.     function importExcel(file) {
  1058.         if (!adminAuth) { toast(‘请先登录‘, true); return; }
  1059.         var reader = new FileReader();
  1060.         reader.onload = function(e) {
  1061.             var data = new Uint8Array(e.target.result);
  1062.             var workbook = XLSX.read(data, { type: ‘array‘ });
  1063.             var sheet = workbook.Sheets[workbook.SheetNames[0]];
  1064.             var rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: ‘‘ });
  1065.             if (!rows || rows.length < 2) { toast(‘无效文件‘, true); return; }
  1066.             var headers = rows[0];
  1067.             var ui = -1;
  1068.             var ui2 = -1;
  1069.             var pi = -1;
  1070.             var bi = -1;
  1071.             for (var i = 0; i < headers.length; i++) {
  1072.                 var s = String(headers[i]).toLowerCase();
  1073.                 if (s.includes(‘网址‘) || s.includes(‘url‘)) ui = i;
  1074.                 if (s.includes(‘账号‘) || s.includes(‘用户名‘)) ui2 = i;
  1075.                 if (s.includes(‘密码‘)) pi = i;
  1076.                 if (s.includes(‘浏览器‘)) bi = i;
  1077.             }
  1078.             if (ui === -1 || ui2 === -1 || pi === -1) { toast(‘缺少必要列‘, true); return; }
  1079.             var records = [];
  1080.             for (var j = 1; j < rows.length; j++) {
  1081.                 var row = rows[j];
  1082.                 if (row && row.length > Math.max(ui, ui2, pi)) {
  1083.                     var url = row[ui] ? String(row[ui]).trim() : ‘‘;
  1084.                     var user = row[ui2] ? String(row[ui2]).trim() : ‘‘;
  1085.                     var pwd = row[pi] ? String(row[pi]).trim() : ‘‘;
  1086.                     var browser = (bi !== -1 && row[bi]) ? String(row[bi]).trim() : ‘default‘;
  1087.                     if (url && user && pwd && (url.startsWith(‘http://‘) || url.startsWith(‘https://‘))) {
  1088.                         records.push({ url: url, username: user, password: pwd, browser: browser });
  1089.                     }
  1090.                 }
  1091.             }
  1092.             if (!records.length) { toast(‘无有效数据‘, true); return; }
  1093.             var cnt = batchUpsert(records);
  1094.             renderAdmin();
  1095.             renderEmp();
  1096.             toast(‘导入 ‘ + cnt + ‘ 条‘, ‘success‘);
  1097.         };
  1098.         reader.readAsArrayBuffer(file);
  1099.     }
  1100.     // ===== 管理员登录 =====
  1101.     function showAdminLogin() {
  1102.         if (adminAuth) return;
  1103.         var overlay = document.createElement(‘div‘);
  1104.         overlay.className = ‘admin-auth-overlay‘;
  1105.         overlay.id = ‘adminOverlay‘;
  1106.         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>‘;
  1107.         document.body.appendChild(overlay);
  1108.         var inp = document.getElementById(‘adminLoginPwd‘);
  1109.         inp.focus();
  1110.         var doAuth = function() {
  1111.             if (h(inp.value) === adminHash) {
  1112.                 adminAuth = true;
  1113.                 document.body.removeChild(overlay);
  1114.                 toast(‘验证成功‘, ‘success‘);
  1115.                 renderAdmin();
  1116.                 var panel = document.getElementById(‘adminPanel‘);
  1117.                 if (!panel.classList.contains(‘hidden‘)) renderAdmin();
  1118.             } else {
  1119.                 document.getElementById(‘adminLoginError‘).textContent = ‘密码错误‘;
  1120.                 inp.value = ‘‘;
  1121.                 inp.focus();
  1122.             }
  1123.         };
  1124.         var cancel = function() {
  1125.             document.body.removeChild(overlay);
  1126.             if (!adminAuth) { switchTab(‘employee‘);
  1127.                 toast(‘已取消‘, true); }
  1128.         };
  1129.         document.getElementById(‘adminLoginConfirm‘).addEventListener(‘click‘, doAuth);
  1130.         document.getElementById(‘adminLoginCancel‘).addEventListener(‘click‘, cancel);
  1131.         inp.addEventListener(‘keypress‘, function(e) { if (e.key === ‘Enter‘) doAuth(); });
  1132.     }
  1133.     // ===== Tab切换 =====
  1134.     function switchTab(tab) {
  1135.         var admin = document.getElementById(‘adminPanel‘);
  1136.         var emp = document.getElementById(‘employeePanel‘);
  1137.         var btns = document.querySelectorAll(‘.tab-btn‘);
  1138.         if (tab === ‘employee‘) {
  1139.             admin.classList.add(‘hidden‘);
  1140.             emp.classList.remove(‘hidden‘);
  1141.             renderEmp();
  1142.             btns.forEach(function(b) { b.classList.remove(‘active‘); });
  1143.             document.querySelector(‘.tab-btn[data-tab=“employee“]‘).classList.add(‘active‘);
  1144.         } else {
  1145.             if (!adminAuth) {
  1146.                 showAdminLogin();
  1147.                 var iv = setInterval(function() {
  1148.                     if (adminAuth) {
  1149.                         clearInterval(iv);
  1150.                         admin.classList.remove(‘hidden‘);
  1151.                         emp.classList.add(‘hidden‘);
  1152.                         renderAdmin();
  1153.                         btns.forEach(function(b) { b.classList.remove(‘active‘); });
  1154.                         document.querySelector(‘.tab-btn[data-tab=“admin“]‘).classList.add(‘active‘);
  1155.                     }
  1156.                 }, 200);
  1157.                 setTimeout(function() { clearInterval(iv); }, 30000);
  1158.             } else {
  1159.                 admin.classList.remove(‘hidden‘);
  1160.                 emp.classList.add(‘hidden‘);
  1161.                 renderAdmin();
  1162.                 btns.forEach(function(b) { b.classList.remove(‘active‘); });
  1163.                 document.querySelector(‘.tab-btn[data-tab=“admin“]‘).classList.add(‘active‘);
  1164.             }
  1165.         }
  1166.     }
  1167.     // ===== 主题切换 =====
  1168.     function setTheme(name) {
  1169.         var root = document.documentElement;
  1170.         var prev = document.querySelector(‘.theme-dot.active‘);
  1171.         if (prev) prev.classList.remove(‘active‘);
  1172.         var dot = document.querySelector(‘.theme-dot[data-theme=“‘ + name + ‘“]‘);
  1173.         if (dot) dot.classList.add(‘active‘);
  1174.         root.className = ‘‘;
  1175.         if (name !== ‘default‘) root.className = ‘theme-‘ + name;
  1176.         localStorage.setItem(‘pwd_theme‘, name);
  1177.     }
  1178.     // ===== 事件绑定 =====
  1179.     document.addEventListener(‘DOMContentLoaded‘, function() {
  1180.         var dots = document.querySelectorAll(‘.theme-dot‘);
  1181.         dots.forEach(function(d) {
  1182.             d.addEventListener(‘click‘, function() { setTheme(this.dataset.theme); });
  1183.         });
  1184.         var saved = localStorage.getItem(‘pwd_theme‘);
  1185.         if (saved) setTheme(saved);
  1186.         // 自动备份开关
  1187.         var toggleBtn = document.getElementById(‘autoBackupToggle‘);
  1188.         if (toggleBtn) {
  1189.             toggleBtn.addEventListener(‘click‘, function() {
  1190.                 autoBackupEnabled = !autoBackupEnabled;
  1191.                 setAutoBackupSetting(autoBackupEnabled);
  1192.                 toast(autoBackupEnabled ? ‘自动备份已开启‘ : ‘自动备份已关闭‘, ‘success‘);
  1193.                 if (autoBackupEnabled && credentials.length > 0) {
  1194.                     exportToDownloadFolder();
  1195.                     toast(‘已备份到下载文件夹‘, ‘success‘);
  1196.                 }
  1197.             });
  1198.         }
  1199.         var btnOpenAdd = document.getElementById(‘openAddCredBtn‘);
  1200.         var btnCloseModal = document.getElementById(‘closeModalBtn‘);
  1201.         var btnSaveCred = document.getElementById(‘saveCredBtn‘);
  1202.         var btnVerifyEmp = document.getElementById(‘verifyEmpBtn‘);
  1203.         var inpEmpPwd = document.getElementById(‘empViewPwd‘);
  1204.         var btnExportExcel = document.getElementById(‘exportExcelBtn‘);
  1205.         var btnExportJson = document.getElementById(‘exportJsonBtn‘);
  1206.         var inpImportExcel = document.getElementById(‘importExcelInput‘);
  1207.         var inpImportJson = document.getElementById(‘importJsonInput‘);
  1208.         var btnUpdateEmpPwd = document.getElementById(‘updateEmployeePwdBtn‘);
  1209.         var btnUpdateAdminPwd = document.getElementById(‘updateAdminPwdBtn‘);
  1210.         if (btnOpenAdd) btnOpenAdd.addEventListener(‘click‘, function() { openModal(null); });
  1211.         if (btnCloseModal) btnCloseModal.addEventListener(‘click‘, closeModal);
  1212.         if (btnSaveCred) btnSaveCred.addEventListener(‘click‘, saveCred);
  1213.         if (btnVerifyEmp) btnVerifyEmp.addEventListener(‘click‘, verifyEmp);
  1214.         if (inpEmpPwd) inpEmpPwd.addEventListener(‘keypress‘, function(e) { if (e.key === ‘Enter‘) verifyEmp(); });
  1215.         if (btnExportExcel) btnExportExcel.addEventListener(‘click‘, exportExcel);
  1216.         if (btnExportJson) btnExportJson.addEventListener(‘click‘, exportJson);
  1217.         if (inpImportExcel) inpImportExcel.addEventListener(‘change‘, function(e) { if (this.files.length) importExcel(this.files[0]);
  1218.             this.value = ‘‘; });
  1219.         if (inpImportJson) inpImportJson.addEventListener(‘change‘, function(e) { if (this.files.length) importJson(this.files[0]);
  1220.             this.value = ‘‘; });
  1221.         if (btnUpdateEmpPwd) {
  1222.             btnUpdateEmpPwd.addEventListener(‘click‘, function() {
  1223.                 var inp = document.getElementById(‘newEmployeePwd‘);
  1224.                 var p = inp.value;
  1225.                 if (!p || p.length < 4) { toast(‘密码至少4位‘, true);
  1226.                     inp.value = ‘‘; return; }
  1227.                 setEmpHash(h(p));
  1228.                 empVisible = false;
  1229.                 renderEmp();
  1230.                 toast(‘员工密码已修改‘, ‘success‘);
  1231.                 inp.value = ‘‘;
  1232.             });
  1233.         }
  1234.         if (btnUpdateAdminPwd) {
  1235.             btnUpdateAdminPwd.addEventListener(‘click‘, function() {
  1236.                 var inp = document.getElementById(‘newAdminPwd‘);
  1237.                 var p = inp.value;
  1238.                 if (!p || p.length < 4) { toast(‘密码至少4位‘, true);
  1239.                     inp.value = ‘‘; return; }
  1240.                 setAdminHash(h(p));
  1241.                 toast(‘管理员密码已修改‘, ‘success‘);
  1242.                 inp.value = ‘‘;
  1243.             });
  1244.         }
  1245.         document.querySelectorAll(‘.tab-btn‘).forEach(function(btn) {
  1246.             btn.addEventListener(‘click‘, function() { switchTab(this.dataset.tab); });
  1247.         });
  1248.         window.onclick = function(e) { if (e.target === modal) closeModal(); };
  1249.     });
  1250.     // ===== 启动 =====
  1251.     function boot() {
  1252.         try {
  1253.             initData();
  1254.             empVisible = false;
  1255.             adminAuth = false;
  1256.             renderEmp();
  1257.             renderAdmin();
  1258.             switchTab(‘employee‘);
  1259.             updateStorageInfo();
  1260.             updateLastBackupTime();
  1261.             toast(‘系统已启动,数据永久保存在 localStorage‘, ‘success‘);
  1262.         } catch(err) {
  1263.             toast(‘初始化失败: ‘ + err.message, true);
  1264.             console.error(err);
  1265.         }
  1266.     }
  1267.     if (document.readyState === ‘loading‘) {
  1268.         document.addEventListener(‘DOMContentLoaded‘, boot);
  1269.     } else {
  1270.         boot();
  1271.     }
  1272. </script>
  1273. </body>
  1274. </html>
  1275. [i]复制代码[/i]
复制代码
[/code]由于个人对HTML网页代码的理解还是欠缺,大家有什么新的建议可提出,或有什么错误的地方,麻烦大家告知,我虚心求教。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Copyright © 2001-2026 52线报网 版权所有 All Rights Reserved. |网站地图 闽ICP备19006036号-4
关灯 在本版发帖
扫一扫添加微信客服
返回顶部
快速回复 返回顶部 返回列表