style(dashboard): move version footer out of card
All checks were successful
Secrets MCP — Build & Release / 检查 / 构建 / 发版 (push) Successful in 6m30s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Successful in 1m37s

This commit is contained in:
agent
2026-04-09 15:23:16 +08:00
parent 10da51c203
commit 089d0b4b58
23 changed files with 2114 additions and 525 deletions

View File

@@ -13,45 +13,44 @@
--border: #30363d; --text: #e6edf3; --text-muted: #8b949e;
--accent: #58a6ff; --accent-hover: #79b8ff;
}
body { background: var(--bg); color: var(--text); font-family: 'Inter', sans-serif; min-height: 100vh; }
body { background: #0d1117; color: #c9d1d9; font-family: 'Inter', sans-serif; min-height: 100vh; }
.layout { display: flex; min-height: 100vh; }
.sidebar {
width: 220px; flex-shrink: 0; background: var(--surface); border-right: 1px solid var(--border);
padding: 24px 16px; display: flex; flex-direction: column; gap: 20px;
width: 200px; flex-shrink: 0; background: #0b1220; border-right: 1px solid rgba(240,246,252,0.08);
padding: 20px 12px; display: flex; flex-direction: column; gap: 20px;
}
.sidebar-logo { font-family: 'JetBrains Mono', monospace; font-size: 16px; font-weight: 600;
color: var(--text); text-decoration: none; padding: 0 10px; }
.sidebar-logo span { color: var(--accent); }
.sidebar-menu { display: flex; flex-direction: column; gap: 6px; }
.sidebar-logo { font-family: 'Inter', sans-serif; font-size: 16px; font-weight: 700;
color: #fff; text-decoration: none; padding: 0 10px; }
.sidebar-menu { display: grid; gap: 6px; }
.sidebar-link {
padding: 10px 12px; border-radius: 8px; color: var(--text-muted); text-decoration: none;
border: 1px solid transparent; font-size: 13px; font-weight: 500;
padding: 10px 12px; border-radius: 10px; color: #8b949e; text-decoration: none;
font-size: 13px; font-weight: 500;
}
.sidebar-link:hover { background: var(--surface2); color: var(--text); }
.sidebar-link:hover { background: rgba(56,139,253,0.14); color: #fff; }
.sidebar-link.active {
background: rgba(88,166,255,0.12); color: var(--text); border-color: rgba(88,166,255,0.35);
background: rgba(56,139,253,0.14); color: #fff;
}
.content-shell { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.topbar {
background: var(--surface); border-bottom: 1px solid var(--border); padding: 0 24px;
display: flex; align-items: center; gap: 12px; min-height: 52px;
background: transparent; border-bottom: none; padding: 0 24px;
display: flex; align-items: center; gap: 12px; min-height: 44px;
}
.topbar-spacer { flex: 1; }
.nav-user { font-size: 13px; color: var(--text-muted); }
.lang-bar { display: flex; gap: 2px; background: var(--surface2); border-radius: 6px; padding: 2px; }
.lang-btn { padding: 3px 9px; border: none; background: none; color: var(--text-muted);
font-size: 12px; cursor: pointer; border-radius: 4px; }
.lang-btn.active { background: var(--border); color: var(--text); }
.nav-user { font-size: 14px; color: #8b949e; }
.lang-bar { display: flex; gap: 2px; background: rgba(240,246,252,0.06); border-radius: 8px; padding: 2px; }
.lang-btn { padding: 4px 10px; border: none; background: none; color: #8b949e;
font-size: 12px; cursor: pointer; border-radius: 6px; }
.lang-btn.active { background: rgba(240,246,252,0.1); color: #fff; }
.btn-sign-out {
padding: 5px 12px; border-radius: 6px; border: 1px solid var(--border);
background: none; color: var(--text); font-size: 12px; text-decoration: none; cursor: pointer;
padding: 6px 14px; border-radius: 10px; border: 1px solid rgba(240,246,252,0.12);
background: #161b22; color: #c9d1d9; font-size: 13px; text-decoration: none; cursor: pointer;
}
.btn-sign-out:hover { background: var(--surface2); }
.main { padding: 32px 24px 40px; flex: 1; }
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
padding: 24px; width: 100%; max-width: 1480px; margin: 0 auto; }
.card-title { font-size: 20px; font-weight: 600; margin-bottom: 8px; }
.card-subtitle { color: var(--text-muted); font-size: 13px; margin-bottom: 20px; }
.btn-sign-out:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
.main { padding: 16px 16px 24px; flex: 1; }
.card { background: #111827; border: 1px solid rgba(240,246,252,0.08); border-radius: 18px;
padding: 20px; width: 100%; }
.card-title { font-size: 22px; font-weight: 700; margin-bottom: 8px; color: #fff; }
.card-subtitle { color: #8b949e; font-size: 14px; margin-bottom: 18px; }
.folder-tabs {
display: flex;
flex-wrap: wrap;
@@ -60,40 +59,40 @@
}
.folder-tab {
text-decoration: none;
color: var(--text-muted);
border: 1px solid var(--border);
color: #8b949e;
border: 1px solid rgba(240,246,252,0.08);
border-radius: 999px;
padding: 6px 10px;
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
background: var(--bg);
background: #0d1117;
}
.folder-tab:hover { color: var(--text); border-color: var(--text-muted); }
.folder-tab:hover { color: #c9d1d9; border-color: rgba(240,246,252,0.2); }
.folder-tab.active {
background: rgba(88,166,255,0.12);
border-color: rgba(88,166,255,0.35);
color: var(--text);
background: rgba(56,139,253,0.14);
border-color: rgba(56,139,253,0.3);
color: #fff;
}
.filter-bar {
display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px 16px;
margin-bottom: 20px; padding: 16px; background: var(--bg); border: 1px solid var(--border);
border-radius: 10px;
margin-bottom: 18px; padding: 16px; background: #0d1117; border: 1px solid rgba(240,246,252,0.08);
border-radius: 12px;
}
.filter-field { display: flex; flex-direction: column; gap: 6px; min-width: 140px; flex: 1; }
.filter-field label { font-size: 12px; color: var(--text-muted); font-weight: 500; }
.filter-field label { font-size: 12px; color: #8b949e; font-weight: 500; }
.filter-field input {
background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
color: var(--text); padding: 8px 10px; font-size: 13px; font-family: 'JetBrains Mono', monospace;
background: #161b22; border: 1px solid rgba(240,246,252,0.08); border-radius: 8px;
color: #c9d1d9; padding: 8px 10px; font-size: 13px; font-family: 'JetBrains Mono', monospace;
outline: none; width: 100%;
}
.filter-field select {
background-color: var(--surface);
background-color: #161b22;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.5l3 3 3-3' fill='none' stroke='%238b949e' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 14px center;
background-size: 12px 12px;
border: 1px solid var(--border); border-radius: 6px;
color: var(--text);
border: 1px solid rgba(240,246,252,0.08); border-radius: 8px;
color: #c9d1d9;
padding: 8px 2.8rem 8px 10px;
font-size: 13px; font-family: 'JetBrains Mono', monospace;
outline: none; width: 100%;
@@ -103,24 +102,24 @@
-moz-appearance: none;
}
.filter-field input:focus,
.filter-field select:focus { border-color: var(--accent); }
.filter-field select:focus { border-color: rgba(56,139,253,0.5); }
.filter-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.btn-filter {
padding: 8px 16px; border-radius: 6px; border: none; background: var(--accent); color: #0d1117;
padding: 8px 16px; border-radius: 10px; border: none; background: #388bfd; color: #fff;
font-size: 13px; font-weight: 600; cursor: pointer;
}
.btn-filter:hover { background: var(--accent-hover); }
.btn-filter:hover { background: #58a6ff; }
.btn-clear {
padding: 8px 14px; border-radius: 6px; border: 1px solid var(--border); background: transparent;
color: var(--text-muted); font-size: 13px; text-decoration: none; cursor: pointer;
padding: 8px 14px; border-radius: 10px; border: 1px solid rgba(240,246,252,0.12); background: transparent;
color: #8b949e; font-size: 13px; text-decoration: none; cursor: pointer;
}
.btn-clear:hover { background: var(--surface2); color: var(--text); }
.empty { color: var(--text-muted); font-size: 14px; padding: 20px 0; }
.btn-clear:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
.empty { color: #8b949e; font-size: 14px; padding: 20px 0; }
.table-wrap {
overflow: auto;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--bg);
border: 1px solid rgba(240,246,252,0.08);
border-radius: 12px;
background: #0d1117;
}
table {
width: 100%;
@@ -128,20 +127,20 @@
border-collapse: separate;
border-spacing: 0;
}
th, td { text-align: left; vertical-align: middle; padding: 12px 10px; border-top: 1px solid var(--border); }
th, td { text-align: left; vertical-align: middle; padding: 14px 12px; border-top: 1px solid rgba(240,246,252,0.08); }
th {
color: var(--text-muted);
color: #8b949e;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
position: sticky;
top: 0;
z-index: 2;
background: var(--surface);
background: #111827;
text-align: center;
vertical-align: middle;
}
td { font-size: 13px; line-height: 1.45; }
td { font-size: 13px; line-height: 1.45; color: #c9d1d9; }
tbody tr:nth-child(2n) td { background: rgba(255, 255, 255, 0.01); }
.mono { font-family: 'JetBrains Mono', monospace; }
.col-type { min-width: 108px; width: 1%; text-align: center; vertical-align: middle; }
@@ -163,13 +162,13 @@
white-space: pre-wrap;
word-break: break-word;
padding: 8px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
background: #0d1117;
border: 1px solid rgba(240,246,252,0.08);
border-radius: 10px;
font-size: 12px;
}
.detail {
background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
background: #0d1117; border: 1px solid rgba(240,246,252,0.08); border-radius: 10px;
padding: 10px; white-space: pre-wrap; word-break: break-word; font-size: 12px;
max-width: 360px; max-height: 120px; overflow: auto;
}
@@ -180,11 +179,11 @@
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid var(--border);
border: 1px solid rgba(240,246,252,0.08);
border-radius: 999px;
padding: 3px 8px;
font-size: 11px;
background: var(--surface2);
background: #161b22;
font-family: 'JetBrains Mono', monospace;
max-width: 100%;
min-width: 0;
@@ -196,8 +195,8 @@
white-space: nowrap;
}
.secret-type {
color: var(--text-muted);
border-left: 1px solid var(--border);
color: #8b949e;
border-left: 1px solid rgba(240,246,252,0.08);
padding-left: 6px;
}
.btn-unlink-secret {
@@ -224,17 +223,17 @@
.secret-name-input {
width: 100%;
min-width: 0;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
background: #0d1117;
border: 1px solid rgba(240,246,252,0.08);
border-radius: 8px;
color: #c9d1d9;
padding: 6px 10px;
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
outline: none;
}
.secret-name-input:focus {
border-color: var(--accent);
border-color: rgba(56,139,253,0.5);
}
.secret-name-input.invalid {
border-color: #f85149;
@@ -245,10 +244,10 @@
.secret-type-select {
width: 100%;
min-width: 0;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
background: #161b22;
border: 1px solid rgba(240,246,252,0.08);
border-radius: 8px;
color: #c9d1d9;
padding: 6px 10px;
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
@@ -256,7 +255,7 @@
cursor: pointer;
}
.secret-type-select:focus {
border-color: var(--accent);
border-color: rgba(56,139,253,0.5);
}
.secret-type-select.invalid {
border-color: #f85149;
@@ -274,13 +273,13 @@
font-size: 11px;
line-height: 1.3;
margin-top: 2px;
color: var(--text-muted);
color: #8b949e;
}
.secret-name-status:empty {
display: none;
}
.secret-name-status.checking {
color: var(--accent);
color: #58a6ff;
}
.secret-name-status.error {
color: #f85149;
@@ -289,11 +288,11 @@
color: #3fb950;
}
.btn-row {
padding: 4px 10px; border-radius: 6px; font-size: 12px; cursor: pointer;
border: 1px solid var(--border); background: var(--surface2); color: var(--text-muted);
padding: 8px 12px; border-radius: 10px; font-size: 13px; cursor: pointer;
border: 1px solid rgba(240,246,252,0.12); background: #161b22; color: #8b949e;
font-family: inherit;
}
.btn-row:hover { color: var(--text); border-color: var(--text-muted); }
.btn-row:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
.btn-row.danger:hover { border-color: #f85149; color: #f85149; }
.modal-overlay {
position: fixed; inset: 0; background: rgba(1, 4, 9, 0.65); z-index: 200;
@@ -301,19 +300,19 @@
}
.modal-overlay[hidden] { display: none !important; }
.modal {
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
background: #111827; border: 1px solid rgba(240,246,252,0.08); border-radius: 18px;
padding: 22px; width: 100%; max-width: 520px; max-height: 90vh; overflow: auto;
box-shadow: 0 16px 48px rgba(0,0,0,0.45);
}
.modal.modal-wide {
max-width: 800px;
}
.modal-title { font-size: 16px; font-weight: 600; margin-bottom: 14px; }
.modal-title { font-size: 18px; font-weight: 700; margin-bottom: 14px; color: #fff; }
.modal-field { margin-bottom: 12px; }
.modal-field label { display: block; font-size: 12px; color: var(--text-muted); margin-bottom: 5px; }
.modal-field label { display: block; font-size: 12px; color: #8b949e; margin-bottom: 5px; }
.modal-field input, .modal-field textarea {
width: 100%; background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
color: var(--text); padding: 8px 10px; font-size: 13px; font-family: 'JetBrains Mono', monospace;
width: 100%; background: #0d1117; border: 1px solid rgba(240,246,252,0.08); border-radius: 10px;
color: #c9d1d9; padding: 8px 10px; font-size: 13px; font-family: 'JetBrains Mono', monospace;
outline: none;
}
.modal-field textarea { resize: vertical; }
@@ -323,22 +322,23 @@
}
.modal-field textarea.metadata-edit { min-height: 140px; }
.modal-readonly-value {
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
color: var(--text-muted); padding: 8px 10px; font-size: 13px;
background: #0d1117; border: 1px solid rgba(240,246,252,0.08); border-radius: 10px;
color: #8b949e; padding: 8px 10px; font-size: 13px;
font-family: 'JetBrains Mono', monospace;
}
.modal-secrets .secret-list { display: flex; flex-direction: column; gap: 10px; }
.modal-error { color: #f85149; font-size: 12px; margin-bottom: 10px; display: none; }
.modal-error.visible { display: block; }
.modal-footer { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; margin-top: 16px; }
.btn-modal { padding: 8px 16px; border-radius: 6px; font-size: 13px; cursor: pointer; font-family: inherit; border: 1px solid var(--border); background: transparent; color: var(--text); }
.btn-modal.primary { background: var(--accent); color: #0d1117; border-color: transparent; font-weight: 600; }
.btn-modal.primary:hover { background: var(--accent-hover); }
.btn-modal { padding: 8px 16px; border-radius: 10px; font-size: 13px; cursor: pointer; font-family: inherit; border: 1px solid rgba(240,246,252,0.12); background: transparent; color: #c9d1d9; }
.btn-modal:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
.btn-modal.primary { background: #388bfd; color: #fff; border-color: transparent; font-weight: 600; }
.btn-modal.primary:hover { background: #58a6ff; }
.btn-modal.danger { border-color: #f85149; color: #f85149; }
@media (max-width: 900px) {
.layout { flex-direction: column; }
.sidebar {
width: 100%; border-right: none; border-bottom: 1px solid var(--border);
width: 100%; border-right: none; border-bottom: 1px solid rgba(240,246,252,0.08);
padding: 16px; gap: 14px;
}
.sidebar-menu { flex-direction: row; flex-wrap: wrap; }
@@ -349,10 +349,10 @@
.table-wrap { max-height: none; border: none; background: transparent; }
table, thead, tbody, th, td, tr { display: block; min-width: 0; width: 100%; }
thead { display: none; }
tr { border-top: 1px solid var(--border); padding: 12px 0; }
tr { border-top: 1px solid rgba(240,246,252,0.08); padding: 12px 0; }
td { border-top: none; padding: 6px 0; max-width: none; }
td::before {
display: block; color: var(--text-muted); font-size: 11px;
display: block; color: #8b949e; font-size: 11px;
margin-bottom: 4px; text-transform: uppercase;
content: attr(data-label);
}
@@ -362,26 +362,26 @@
.detail, .notes-scroll, .secret-list { max-width: none; }
}
.pagination {
display: flex; align-items: center; gap: 8px; margin-top: 20px;
display: flex; align-items: center; gap: 12px; margin-top: 18px;
justify-content: center; padding: 12px 0;
}
.page-btn {
padding: 6px 14px; border-radius: 6px; border: 1px solid var(--border);
background: var(--surface); color: var(--text); text-decoration: none;
padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(240,246,252,0.12);
background: #161b22; color: #c9d1d9; text-decoration: none;
font-size: 13px; cursor: pointer;
}
.page-btn:hover { background: var(--surface2); }
.page-btn:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
.page-btn-disabled {
padding: 6px 14px; border-radius: 6px; border: 1px solid var(--border);
background: var(--surface); color: var(--text-muted); font-size: 13px;
padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(240,246,252,0.12);
background: #161b22; color: #6e7681; font-size: 13px;
opacity: 0.5; cursor: not-allowed;
}
.page-info {
color: var(--text-muted); font-size: 13px; font-family: 'JetBrains Mono', monospace;
color: #8b949e; font-size: 13px; font-family: 'JetBrains Mono', monospace;
}
.view-secret-row {
display: flex; flex-direction: column; gap: 4px; padding: 8px 0;
border-bottom: 1px solid var(--border);
border-bottom: 1px solid rgba(240,246,252,0.08);
}
.view-secret-row:last-child { border-bottom: none; }
.view-secret-header {
@@ -389,59 +389,60 @@
}
.view-secret-name {
font-family: 'JetBrains Mono', monospace; font-size: 12px;
color: var(--text); font-weight: 600;
color: #c9d1d9; font-weight: 600;
}
.view-secret-type {
font-family: 'JetBrains Mono', monospace; font-size: 11px;
color: var(--text-muted); background: var(--surface2);
border: 1px solid var(--border); border-radius: 4px; padding: 1px 6px;
color: #8b949e; background: #161b22;
border: 1px solid rgba(240,246,252,0.08); border-radius: 6px; padding: 1px 6px;
}
.view-secret-actions { margin-left: auto; display: flex; gap: 6px; }
.view-secret-value-wrap { position: relative; }
.view-secret-value {
font-family: 'JetBrains Mono', monospace; font-size: 12px;
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
background: #0d1117; border: 1px solid rgba(240,246,252,0.08); border-radius: 10px;
padding: 7px 10px; word-break: break-all; white-space: pre-wrap;
max-height: 140px; overflow: auto; color: var(--text); line-height: 1.5;
max-height: 140px; overflow: auto; color: #c9d1d9; line-height: 1.5;
}
.view-secret-value.masked { letter-spacing: 2px; user-select: none; filter: blur(4px); }
.btn-icon {
padding: 3px 8px; border-radius: 5px; font-size: 11px; cursor: pointer;
border: 1px solid var(--border); background: var(--surface2); color: var(--text-muted);
padding: 6px 10px; border-radius: 8px; font-size: 12px; cursor: pointer;
border: 1px solid rgba(240,246,252,0.12); background: #161b22; color: #8b949e;
font-family: inherit;
}
.btn-icon:hover { color: var(--text); border-color: var(--text-muted); }
.btn-icon:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
.view-locked-msg {
font-size: 13px; color: var(--text-muted); padding: 16px 0;
font-size: 13px; color: #8b949e; padding: 16px 0;
line-height: 1.6; text-align: center;
}
.view-locked-msg a { color: var(--accent); }
.view-locked-msg a { color: #58a6ff; }
.view-secret-name-wrap { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; flex: 1; min-width: 0; }
.view-secret-name-input {
width: 180px; max-width: 100%; background: var(--bg); border: 1px solid var(--border);
border-radius: 4px; color: var(--text); padding: 2px 8px; font-size: 12px;
width: 180px; max-width: 100%; background: #0d1117; border: 1px solid rgba(240,246,252,0.08);
border-radius: 8px; color: #c9d1d9; padding: 2px 8px; font-size: 12px;
font-family: 'JetBrains Mono', monospace; outline: none;
}
.view-secret-name-input:focus { border-color: var(--accent); }
.view-secret-name-input:focus { border-color: rgba(56,139,253,0.5); }
.view-secret-type-select {
background: var(--surface2); border: 1px solid var(--border); border-radius: 4px;
color: var(--text); padding: 2px 6px; font-size: 11px;
background: #161b22; border: 1px solid rgba(240,246,252,0.08); border-radius: 8px;
color: #c9d1d9; padding: 2px 6px; font-size: 11px;
font-family: 'JetBrains Mono', monospace; outline: none; cursor: pointer;
}
.view-secret-type-select:focus { border-color: var(--accent); }
.btn-view-edit { color: var(--accent); }
.view-secret-type-select:focus { border-color: rgba(56,139,253,0.5); }
.btn-view-edit { color: #58a6ff; }
.btn-view-save { color: #3fb950; }
.btn-view-cancel { color: var(--text-muted); }
.btn-view-cancel { color: #8b949e; }
.btn-view-unlink { color: #f85149; font-size: 14px; }
</style>
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a href="/dashboard" class="sidebar-logo"><span>secrets</span></a>
<a href="/dashboard" class="sidebar-logo">secrets</a>
<nav class="sidebar-menu">
<a href="/dashboard" class="sidebar-link" data-i18n="navMcp">MCP</a>
<a href="/entries" class="sidebar-link active" data-i18n="navEntries">条目</a>
<a href="/trash" class="sidebar-link" data-i18n="navTrash">回收站</a>
<a href="/audit" class="sidebar-link" data-i18n="navAudit">审计</a>
</nav>
</aside>
@@ -480,6 +481,10 @@
<label for="filter-name" data-i18n="filterNameLabel">名称</label>
<input id="filter-name" name="name" type="text" value="{{ filter_name }}" data-i18n-ph="filterNamePlaceholder" placeholder="输入关键字" autocomplete="off">
</div>
<div class="filter-field">
<label for="filter-metadata-query" data-i18n="filterMetadataLabel">元数据值</label>
<input id="filter-metadata-query" name="metadata_query" type="text" value="{{ filter_metadata_query }}" data-i18n-ph="filterMetadataPlaceholder" placeholder="搜索元数据值" autocomplete="off">
</div>
<div class="filter-field">
<label for="filter-type" data-i18n="filterTypeLabel">类型</label>
<select id="filter-type" name="type" onchange="this.form.requestSubmit();">
@@ -506,17 +511,34 @@
<th data-i18n="colType">类型</th>
<th data-i18n="colNotes">备注</th>
<th data-i18n="colTags">标签</th>
<th data-i18n="colRelations">关联</th>
<th data-i18n="colSecrets">密文</th>
<th data-i18n="colActions">操作</th>
</tr>
</thead>
<tbody>
{% for entry in entries %}
<tr data-entry-id="{{ entry.id }}" data-entry-folder="{{ entry.folder }}" data-entry-metadata="{{ entry.metadata_json }}" data-entry-secrets="{{ entry.secrets_json }}" data-updated-at="{{ entry.updated_at_iso }}">
<tr data-entry-id="{{ entry.id }}" data-entry-folder="{{ entry.folder }}" data-entry-metadata="{{ entry.metadata_json }}" data-entry-secrets="{{ entry.secrets_json }}" data-entry-parents="{{ entry.parents_json }}" data-updated-at="{{ entry.updated_at_iso }}">
<td class="col-name mono cell-name" data-label="名称">{{ entry.name }}</td>
<td class="col-type mono cell-type" data-label="类型">{{ entry.entry_type }}</td>
<td class="col-notes cell-notes" data-label="备注">{% if !entry.notes.is_empty() %}<div class="notes-scroll cell-notes-val">{{ entry.notes }}</div>{% endif %}</td>
<td class="col-tags mono cell-tags-val" data-label="标签">{{ entry.tags }}</td>
<td class="col-relations" data-label="关联">
<div class="secret-list">
{% for parent in entry.parents %}
<a class="secret-chip" href="{{ parent.href }}" title="{{ parent.folder }} / {{ parent.name }}">
<span class="secret-name">{{ parent.name }}</span>
<span class="secret-type" data-i18n="relationParentBadge">上级</span>
</a>
{% endfor %}
{% for child in entry.children %}
<a class="secret-chip" href="{{ child.href }}" title="{{ child.folder }} / {{ child.name }}">
<span class="secret-name">{{ child.name }}</span>
<span class="secret-type" data-i18n="relationChildBadge">下级</span>
</a>
{% endfor %}
</div>
</td>
<td class="col-secrets" data-label="密文">
<div class="secret-list">
{% for s in entry.secrets %}
@@ -543,13 +565,13 @@
{% if total_count > 0 %}
<div class="pagination">
{% if current_page > 1 %}
<a href="?{% if !filter_folder.is_empty() %}folder={{ filter_folder | urlencode }}&{% endif %}{% if !filter_type.is_empty() %}type={{ filter_type | urlencode }}&{% endif %}{% if !filter_name.is_empty() %}name={{ filter_name | urlencode }}&{% endif %}page={{ current_page - 1 }}" class="page-btn" data-i18n="prevPage">上一页</a>
<a href="?{% if !filter_folder.is_empty() %}folder={{ filter_folder | urlencode }}&{% endif %}{% if !filter_type.is_empty() %}type={{ filter_type | urlencode }}&{% endif %}{% if !filter_name.is_empty() %}name={{ filter_name | urlencode }}&{% endif %}{% if !filter_metadata_query.is_empty() %}metadata_query={{ filter_metadata_query | urlencode }}&{% endif %}page={{ current_page - 1 }}" class="page-btn" data-i18n="prevPage">上一页</a>
{% else %}
<span class="page-btn page-btn-disabled" data-i18n="prevPage">上一页</span>
{% endif %}
<span class="page-info">{{ current_page }} / {{ total_pages }}</span>
{% if current_page < total_pages %}
<a href="?{% if !filter_folder.is_empty() %}folder={{ filter_folder | urlencode }}&{% endif %}{% if !filter_type.is_empty() %}type={{ filter_type | urlencode }}&{% endif %}{% if !filter_name.is_empty() %}name={{ filter_name | urlencode }}&{% endif %}page={{ current_page + 1 }}" class="page-btn" data-i18n="nextPage">下一页</a>
<a href="?{% if !filter_folder.is_empty() %}folder={{ filter_folder | urlencode }}&{% endif %}{% if !filter_type.is_empty() %}type={{ filter_type | urlencode }}&{% endif %}{% if !filter_name.is_empty() %}name={{ filter_name | urlencode }}&{% endif %}{% if !filter_metadata_query.is_empty() %}metadata_query={{ filter_metadata_query | urlencode }}&{% endif %}page={{ current_page + 1 }}" class="page-btn" data-i18n="nextPage">下一页</a>
{% else %}
<span class="page-btn page-btn-disabled" data-i18n="nextPage">下一页</span>
{% endif %}
@@ -572,6 +594,12 @@
<div class="modal-field"><label for="edit-tags" data-i18n="modalTags">标签(逗号分隔)</label><input id="edit-tags" type="text" autocomplete="off"></div>
<div class="modal-field"><label data-i18n="modalUpdated">更新</label><div id="edit-updated-at" class="modal-readonly-value" aria-live="polite"></div></div>
<div class="modal-field"><label for="edit-metadata" data-i18n="modalMetadata">元数据JSON 对象)</label><textarea id="edit-metadata" class="metadata-edit"></textarea></div>
<div class="modal-field">
<label for="edit-parent-search" data-i18n="modalParents">上级条目</label>
<div id="edit-parent-list" class="secret-list"></div>
<input id="edit-parent-search" type="text" autocomplete="off" data-i18n-ph="parentSearchPlaceholder" placeholder="按名称搜索条目">
<div id="edit-parent-results" class="secret-list" style="margin-top:8px"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn-modal" id="edit-cancel" data-i18n="modalCancel">取消</button>
<button type="button" class="btn-modal primary" id="edit-save" data-i18n="modalSave">保存</button>
@@ -579,31 +607,19 @@
</div>
</div>
<div id="delete-overlay" class="modal-overlay" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="delete-title">
<div class="modal-title" id="delete-title" data-i18n="deleteTitle">确认删除</div>
<div id="delete-error" class="modal-error"></div>
<div class="modal-field">
<div id="delete-message" style="font-size:14px;line-height:1.6;color:var(--text)"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn-modal" id="delete-cancel" data-i18n="modalCancel">取消</button>
<button type="button" class="btn-modal danger" id="delete-confirm" data-i18n="deleteConfirm">删除</button>
</div>
</div>
</div>
<div id="view-overlay" class="modal-overlay" hidden>
<div class="modal modal-wide" role="dialog" aria-modal="true" aria-labelledby="view-title">
<div class="modal-title" id="view-title" data-i18n="viewTitle">查看条目密文</div>
<div id="view-entry-name" style="font-size:13px;color:var(--text-muted);margin-bottom:14px;font-family:'JetBrains Mono',monospace;"></div>
<div id="view-entry-name" style="font-size:13px;color:#8b949e;margin-bottom:14px;font-family:'JetBrains Mono',monospace;"></div>
<div id="view-body"></div>
<div class="modal-footer">
<button type="button" class="btn-modal" id="view-close" data-i18n="modalCancel">关闭</button>
</div>
</div>
</div>
<script src="/static/i18n.js"></script>
<script src="/static/i18n.js?v={{ version }}"></script>
<script id="secret-type-options" type="application/json">{{ secret_type_options_json|safe }}</script>
<script>
var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-options').textContent);
@@ -611,10 +627,13 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
I18N_PAGE = {
'zh-CN': {
pageTitle: 'Secrets — 条目',
navTrash: '回收站',
entriesTitle: '我的条目',
allTab: '全部',
filterNameLabel: '名称',
filterNamePlaceholder: '输入关键字',
filterMetadataLabel: '元数据值',
filterMetadataPlaceholder: '搜索元数据值',
filterTypeLabel: '类型',
filterTypeAll: '全部',
filterSubmit: '筛选',
@@ -624,8 +643,11 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
colType: '类型',
colNotes: '备注',
colTags: '标签',
colRelations: '关联',
colSecrets: '密文',
colActions: '操作',
relationParentBadge: '上级',
relationChildBadge: '下级',
rowEdit: '编辑条目',
rowDelete: '删除',
modalTitle: '编辑条目信息',
@@ -636,16 +658,15 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
modalTags: '标签(逗号分隔)',
modalUpdated: '更新',
modalMetadata: '元数据JSON 对象)',
modalParents: '上级条目',
modalSecrets: '密文',
modalCancel: '取消',
modalSave: '保存',
deleteTitle: '确认删除',
deleteConfirm: '删除',
deleteMessage: '确定删除条目「{name}」?此操作不可撤销。',
mobileLabelName: '名称',
mobileLabelType: '类型',
mobileLabelNotes: '备注',
mobileLabelTags: '标签',
mobileLabelRelations: '关联',
mobileLabelSecrets: '密文',
mobileLabelActions: '操作',
errInvalidJson: '元数据不是合法 JSON',
@@ -679,13 +700,19 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
viewSaveChanges: '保存更改',
viewChangesSaved: '已保存',
viewUnlinkConfirm: '确定解除密文关联「{name}」?',
parentSearchPlaceholder: '按名称搜索条目',
parentSearchEmpty: '没有匹配的条目',
removeParent: '移除上级',
},
'zh-TW': {
pageTitle: 'Secrets — 條目',
navTrash: '回收站',
entriesTitle: '我的條目',
allTab: '全部',
filterNameLabel: '名稱',
filterNamePlaceholder: '輸入關鍵字',
filterMetadataLabel: '中繼資料值',
filterMetadataPlaceholder: '搜尋中繼資料值',
filterTypeLabel: '類型',
filterTypeAll: '全部',
filterSubmit: '篩選',
@@ -695,8 +722,11 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
colType: '類型',
colNotes: '備註',
colTags: '標籤',
colRelations: '關聯',
colSecrets: '密文',
colActions: '操作',
relationParentBadge: '上級',
relationChildBadge: '下級',
rowEdit: '編輯條目',
rowDelete: '刪除',
modalTitle: '編輯條目資訊',
@@ -707,16 +737,15 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
modalTags: '標籤(逗號分隔)',
modalUpdated: '更新時間',
modalMetadata: '中繼資料JSON 物件)',
modalParents: '上級條目',
modalSecrets: '密文',
modalCancel: '取消',
modalSave: '儲存',
deleteTitle: '確認刪除',
deleteConfirm: '刪除',
deleteMessage: '確定刪除條目「{name}」?此操作不可復原。',
mobileLabelName: '名稱',
mobileLabelType: '類型',
mobileLabelNotes: '備註',
mobileLabelTags: '標籤',
mobileLabelRelations: '關聯',
mobileLabelSecrets: '密文',
mobileLabelActions: '操作',
errInvalidJson: '中繼資料不是合法 JSON',
@@ -750,13 +779,19 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
viewSaveChanges: '儲存變更',
viewChangesSaved: '已儲存',
viewUnlinkConfirm: '確定解除密文關聯「{name}」?',
parentSearchPlaceholder: '依名稱搜尋條目',
parentSearchEmpty: '沒有符合的條目',
removeParent: '移除上級',
},
en: {
pageTitle: 'Secrets — Entries',
navTrash: 'Trash',
entriesTitle: 'My entries',
allTab: 'All',
filterNameLabel: 'Name',
filterNamePlaceholder: 'Enter keywords',
filterMetadataLabel: 'Metadata value',
filterMetadataPlaceholder: 'Search metadata values',
filterTypeLabel: 'Type',
filterTypeAll: 'All',
filterSubmit: 'Filter',
@@ -766,8 +801,11 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
colType: 'Type',
colNotes: 'Notes',
colTags: 'Tags',
colRelations: 'Relations',
colSecrets: 'Secrets',
colActions: 'Actions',
relationParentBadge: 'Parent',
relationChildBadge: 'Child',
rowEdit: 'Edit entry',
rowDelete: 'Delete',
modalTitle: 'Edit entry details',
@@ -778,16 +816,15 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
modalTags: 'Tags (comma separated)',
modalUpdated: 'Updated',
modalMetadata: 'Metadata (JSON object)',
modalParents: 'Parent entries',
modalSecrets: 'Secrets',
modalCancel: 'Cancel',
modalSave: 'Save',
deleteTitle: 'Confirm Delete',
deleteConfirm: 'Delete',
deleteMessage: 'Are you sure you want to delete entry "{name}"? This action cannot be undone.',
mobileLabelName: 'Name',
mobileLabelType: 'Type',
mobileLabelNotes: 'Notes',
mobileLabelTags: 'Tags',
mobileLabelRelations: 'Relations',
mobileLabelSecrets: 'Secrets',
mobileLabelActions: 'Actions',
errInvalidJson: 'Metadata is not valid JSON',
@@ -821,6 +858,9 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
viewSaveChanges: 'Save changes',
viewChangesSaved: 'Saved',
viewUnlinkConfirm: 'Unlink secret "{name}"?',
parentSearchPlaceholder: 'Search entries by name',
parentSearchEmpty: 'No matching entries',
removeParent: 'Remove parent',
}
};
@@ -836,6 +876,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
'.col-type': 'mobileLabelType',
'.col-notes': 'mobileLabelNotes',
'.col-tags': 'mobileLabelTags',
'.col-relations': 'mobileLabelRelations',
'.col-secrets': 'mobileLabelSecrets',
'.col-actions': 'mobileLabelActions'
};
@@ -855,11 +896,98 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
var editTags = document.getElementById('edit-tags');
var editMetadata = document.getElementById('edit-metadata');
var editUpdatedAt = document.getElementById('edit-updated-at');
var deleteOverlay = document.getElementById('delete-overlay');
var deleteError = document.getElementById('delete-error');
var deleteMessage = document.getElementById('delete-message');
var editParentList = document.getElementById('edit-parent-list');
var editParentSearch = document.getElementById('edit-parent-search');
var editParentResults = document.getElementById('edit-parent-results');
var currentEntryId = null;
var pendingDeleteId = null;
var selectedParents = [];
var parentSearchTimer = null;
function renderSelectedParents() {
editParentList.innerHTML = '';
selectedParents.forEach(function (parent) {
var chip = document.createElement('span');
chip.className = 'secret-chip';
var name = document.createElement('span');
name.className = 'secret-name';
name.textContent = parent.name;
var type = document.createElement('span');
type.className = 'secret-type';
type.textContent = parent.entry_type || parent.type || '';
var remove = document.createElement('button');
remove.type = 'button';
remove.className = 'btn-icon btn-view-unlink';
remove.textContent = '×';
remove.title = t('removeParent');
remove.addEventListener('click', function () {
selectedParents = selectedParents.filter(function (item) { return item.id !== parent.id; });
renderSelectedParents();
});
chip.appendChild(name);
chip.appendChild(type);
chip.appendChild(remove);
editParentList.appendChild(chip);
});
}
function renderParentSearchResults(options) {
editParentResults.innerHTML = '';
if (!options.length) {
var empty = document.createElement('div');
empty.className = 'view-locked-msg';
empty.textContent = t('parentSearchEmpty');
editParentResults.appendChild(empty);
return;
}
options.forEach(function (option) {
if (selectedParents.some(function (item) { return item.id === String(option.id); })) return;
var button = document.createElement('button');
button.type = 'button';
button.className = 'secret-chip';
button.style.background = '#161b22';
button.style.cursor = 'pointer';
button.innerHTML = '<span class="secret-name"></span><span class="secret-type"></span>';
button.querySelector('.secret-name').textContent = option.name;
button.querySelector('.secret-type').textContent = [option.folder, option.type].filter(Boolean).join(' / ');
button.addEventListener('click', function () {
selectedParents.push({
id: String(option.id),
name: option.name,
folder: option.folder || '',
entry_type: option.type || ''
});
editParentSearch.value = '';
editParentResults.innerHTML = '';
renderSelectedParents();
});
editParentResults.appendChild(button);
});
}
function searchParentEntries() {
var query = editParentSearch.value.trim();
if (!query || !currentEntryId) {
editParentResults.innerHTML = '';
return;
}
fetch('/api/entries/options?q=' + encodeURIComponent(query) + '&exclude_id=' + encodeURIComponent(currentEntryId), {
credentials: 'same-origin'
}).then(function (response) {
return response.json().then(function (body) {
if (!response.ok) throw new Error(body.error || ('HTTP ' + response.status));
return body;
});
}).then(function (options) {
renderParentSearchResults(options || []);
}).catch(function (error) {
showEditErr(error.message || String(error));
});
}
// ── View secrets modal ────────────────────────────────────────────────────
var viewOverlay = document.getElementById('view-overlay');
@@ -1264,6 +1392,14 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
} catch (err) {
editMetadata.value = md;
}
try {
selectedParents = JSON.parse(tr.getAttribute('data-entry-parents') || '[]');
} catch (err) {
selectedParents = [];
}
editParentSearch.value = '';
editParentResults.innerHTML = '';
renderSelectedParents();
editOverlay.hidden = false;
}
@@ -1273,37 +1409,31 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
showEditErr('');
editUpdatedAt.textContent = '';
editUpdatedAt.title = '';
editParentSearch.value = '';
editParentResults.innerHTML = '';
selectedParents = [];
renderSelectedParents();
}
editParentSearch.addEventListener('input', function () {
clearTimeout(parentSearchTimer);
parentSearchTimer = setTimeout(searchParentEntries, 180);
});
document.getElementById('edit-cancel').addEventListener('click', closeEdit);
editOverlay.addEventListener('click', function (e) {
if (e.target === editOverlay) closeEdit();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && !editOverlay.hidden) closeEdit();
if (e.key === 'Escape' && !deleteOverlay.hidden) closeDelete();
if (e.key === 'Escape' && !viewOverlay.hidden) closeView();
});
function showDeleteErr(msg) {
deleteError.textContent = msg || '';
deleteError.classList.toggle('visible', !!msg);
}
function openDelete(id, name) {
pendingDeleteId = id;
deleteMessage.textContent = tf('deleteMessage', { name: name });
showDeleteErr('');
deleteOverlay.hidden = false;
}
function closeDelete() {
deleteOverlay.hidden = true;
pendingDeleteId = null;
showDeleteErr('');
}
function refreshListAfterSave(entryId, body) {
if (body.parent_ids) {
window.location.reload();
return;
}
var tr = document.querySelector('tr[data-entry-id="' + entryId + '"]');
if (!tr) { window.location.reload(); return; }
var nameCell = tr.querySelector('.cell-name');
@@ -1368,27 +1498,6 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
}
}
document.getElementById('delete-cancel').addEventListener('click', closeDelete);
deleteOverlay.addEventListener('click', function (e) {
if (e.target === deleteOverlay) closeDelete();
});
document.getElementById('delete-confirm').addEventListener('click', function () {
if (!pendingDeleteId) return;
fetch('/api/entries/' + encodeURIComponent(pendingDeleteId), { method: 'DELETE', credentials: 'same-origin' })
.then(function (r) {
return r.json().then(function (data) {
if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
return data;
});
})
.then(function () {
var deletedId = pendingDeleteId;
closeDelete();
refreshListAfterDelete(deletedId);
})
.catch(function (e) { showDeleteErr(e.message || String(e)); });
});
document.getElementById('edit-save').addEventListener('click', function () {
if (!currentEntryId) return;
var meta;
@@ -1409,7 +1518,8 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
name: editName.value.trim(),
notes: editNotes.value,
tags: tags,
metadata: meta
metadata: meta,
parent_ids: selectedParents.map(function (parent) { return parent.id; })
};
showEditErr('');
fetch('/api/entries/' + encodeURIComponent(currentEntryId), {
@@ -1433,17 +1543,23 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
document.querySelectorAll('tr[data-entry-id]').forEach(function (tr) {
var viewBtn = tr.querySelector('.btn-view-secrets');
if (viewBtn) {
var hasSecrets = tr.querySelectorAll('.secret-chip').length > 0;
var hasSecrets = tr.querySelectorAll('.col-secrets .secret-chip').length > 0;
if (!hasSecrets) viewBtn.disabled = true;
viewBtn.addEventListener('click', function () { openView(tr); });
}
tr.querySelector('.btn-edit').addEventListener('click', function () { openEdit(tr); });
tr.querySelector('.btn-del').addEventListener('click', function () {
var id = tr.getAttribute('data-entry-id');
var nameEl = tr.querySelector('.cell-name');
var name = nameEl ? nameEl.textContent.trim() : '';
if (!id) return;
openDelete(id, name);
fetch('/api/entries/' + encodeURIComponent(id), { method: 'DELETE', credentials: 'same-origin' })
.then(function (r) {
return r.json().then(function (data) {
if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
return data;
});
})
.then(function () { refreshListAfterDelete(id); })
.catch(function (e) { alert(e.message || String(e)); });
});
});