[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>🔐 密码管理系统 <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">👥 员工门户</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: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="🔍 搜索名称 / 网址 / 账号..." autocomplete="off">
<span class="result-count" id="resultCount">共 0 条</span>
<button class="btn btn-clear btn-sm" id="clearSearchBtn" style="display:none;">✕ 清除</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>💡 支持搜索名称、网址、账号</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 btn-sm" 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 btn-sm" id="exportJsonBtn">📥 JSON</button>
<label class="file-input-label" for="importJsonInput" style="background:var(--primary);color:#fff;">📤 导入 JSON</label>
<input type="file" id="importJsonInput" accept=".json">
</div>
</div>
<div class="db-status">
<span>📀 数据库已连接</span>
<span id="dbStatusText">localStorage 持久化存储</span>
</div>
<!-- 从浏览器导入 -->
<div class="import-area">
<div class="title">🌐 从浏览器导入密码</div>
<div class="row">
<select id="importBrowserSelect">
<option value="firefox">🦊 Firefox</option>
<option value="chrome">🔴 Chrome</option>
<option value="edge">🔷 Edge</option>
</select>
<label class="file-input-label" for="importBrowserCsv">📂 选择 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>📌 导出:</strong> Firefox/Chrome/Edge 密码管理器 → 导出密码 → CSV
<span style="display:block;color:var(--success);">✅ 导入后自动绑定对应浏览器打开</span>
</div>
</div>
<!-- 自动备份 -->
<div class="backup-settings">
<div>
<span class="label">💾 自动备份</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">💾 存储:</span>浏览器 localStorage</span>
<span><span class="label">📊 记录:</span><span id="recordCount">0</span> 条</span>
<span><span class="label">📌 持久化:</span><span class="status">✅ 刷新不丢失</span></span>
<span class="backup-status" id="lastBackupTime">📋 最后备份:无</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>⚙️ 安全设置</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>💡 开启「自动备份」后每次修改自动导出</span>
</div>
</div>
</div>
</div>
<!-- ===== 浏览器提示弹窗 ===== -->
<div class="browser-hint-overlay" id="browserHintOverlay">
<div class="browser-hint-box">
<div class="icon" id="hintIcon">🌐</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">⬇ 下载</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">🌍 默认</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 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: '🔷',
name: 'Edge 浏览器',
desc: '使用前提:已安装Edge浏览器。(未安装请先下载安装)。',
download: 'https://www.microsoft.com/zh-cn/edge/download',
protocol: 'microsoft-edge:'
},
chrome: {
icon: '🔴',
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: '🦊',
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 = '⬇ 下载 ' + 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 ? '✅ 已开启' : '⛔ 已关闭';
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 = '📋 最后备份:' + str;
} else {
el.textContent = '📋 最后备份:无';
}
}
// ===== 增删改查 =====
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 ? '暂无数据' : '🔍 没有找到匹配的记录') + '</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 = '✏️ 编辑凭证';
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 = '➕ 添加凭证';
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>🔐 管理员验证</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 = '❌ 密码错误,请重试';
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>