初始化
This commit is contained in:
701
frontend/static/css/style.css
Normal file
701
frontend/static/css/style.css
Normal file
@@ -0,0 +1,701 @@
|
||||
/* ========================================
|
||||
MoKeeText — Feishu-inspired Design System
|
||||
======================================== */
|
||||
|
||||
/* ——— Design Tokens ——— */
|
||||
:root {
|
||||
/* Surface */
|
||||
--bg-page: #F5F6F8;
|
||||
--bg-sidebar: #FCFCFD;
|
||||
--bg-surface: #FFFFFF;
|
||||
--bg-hover: #F2F3F5;
|
||||
--bg-active: #EEF3FF;
|
||||
|
||||
/* Text */
|
||||
--text-primary: #1F2329;
|
||||
--text-secondary: #646A73;
|
||||
--text-tertiary: #8F959E;
|
||||
--text-placeholder:#BCC0C6;
|
||||
|
||||
/* Border */
|
||||
--border-light: #E5E6EB;
|
||||
--border-medium: #DEE0E3;
|
||||
|
||||
/* Brand */
|
||||
--brand: #3370FF;
|
||||
--brand-hover: #245BDB;
|
||||
--brand-light: #EEF3FF;
|
||||
--brand-ghost: rgba(51,112,255,0.06);
|
||||
|
||||
/* Semantic */
|
||||
--danger: #F54A45;
|
||||
--danger-light: #FFF1F0;
|
||||
--success: #34C759;
|
||||
|
||||
/* Radius */
|
||||
--r-sm: 6px;
|
||||
--r-md: 8px;
|
||||
--r-lg: 12px;
|
||||
|
||||
/* Shadow */
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
|
||||
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
|
||||
--shadow-lg: 0 8px 24px rgba(0,0,0,0.12);
|
||||
|
||||
/* Spacing */
|
||||
--space-xs: 4px;
|
||||
--space-sm: 8px;
|
||||
--space-md: 12px;
|
||||
--space-lg: 16px;
|
||||
--space-xl: 20px;
|
||||
--space-2xl: 24px;
|
||||
}
|
||||
|
||||
/* ——— Reset ——— */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-page);
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ——— Top Navigation ——— */
|
||||
.topnav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
padding: 0 var(--space-xl);
|
||||
background: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
gap: 0;
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
}
|
||||
.topnav-brand {
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
margin-right: var(--space-2xl);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.topnav-link {
|
||||
padding: 13px var(--space-lg);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: color 0.15s;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.topnav-link:hover { color: var(--brand); }
|
||||
.topnav-link.active {
|
||||
color: var(--brand);
|
||||
border-bottom-color: var(--brand);
|
||||
}
|
||||
/* ——— User Widget ——— */
|
||||
.user-widget {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 10px 4px 4px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
.user-widget:hover { background: var(--bg-hover); }
|
||||
.user-avatar {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--brand);
|
||||
color: #fff;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 12px; font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.user-name { font-size: 13px; font-weight: 500; color: var(--text-primary); }
|
||||
|
||||
/* ——— Footer ——— */
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 32px;
|
||||
padding: 0 var(--space-xl);
|
||||
background: var(--bg-surface);
|
||||
border-top: 1px solid var(--border-light);
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.footer-icp { color: var(--text-placeholder); }
|
||||
|
||||
/* ——— Main Layout ——— */
|
||||
.layout { display: flex; height: calc(100vh - 48px - 32px); }
|
||||
|
||||
/* ——— Sidebar ——— */
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
min-width: 260px;
|
||||
background: var(--bg-sidebar);
|
||||
border-right: 1px solid var(--border-light);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--space-md);
|
||||
gap: var(--space-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Sidebar section label */
|
||||
.sidebar-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 4px 0;
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* ——— Buttons ——— */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
height: 36px;
|
||||
padding: 0 var(--space-lg);
|
||||
border: none;
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
font-family: inherit;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
|
||||
|
||||
.btn-primary {
|
||||
background: var(--brand);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-primary:hover { background: var(--brand-hover); }
|
||||
|
||||
.btn-outline {
|
||||
background: var(--bg-surface);
|
||||
color: var(--brand);
|
||||
border: 1px solid var(--brand);
|
||||
}
|
||||
.btn-outline:hover { background: var(--brand-light); }
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.btn-ghost:hover { background: var(--bg-hover); color: var(--text-primary); }
|
||||
|
||||
.btn-danger-ghost {
|
||||
background: transparent;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
.btn-danger-ghost:hover { background: var(--danger-light); color: var(--danger); }
|
||||
|
||||
.btn-block { width: 100%; }
|
||||
|
||||
.btn-sm { height: 30px; font-size: 12px; padding: 0 12px; }
|
||||
|
||||
/* ——— Input ——— */
|
||||
.input {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border-medium);
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-surface);
|
||||
outline: none;
|
||||
transition: border 0.15s;
|
||||
font-family: inherit;
|
||||
}
|
||||
.input:focus { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-light); }
|
||||
.input::placeholder { color: var(--text-placeholder); }
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border-medium);
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-surface);
|
||||
outline: none;
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
transition: border 0.15s;
|
||||
min-height: 60px;
|
||||
}
|
||||
.textarea:focus { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-light); }
|
||||
|
||||
.select {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border-medium);
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-surface);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* ——— Folder Tree ——— */
|
||||
.folder-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 32px;
|
||||
padding: 0 8px;
|
||||
border-radius: var(--r-sm);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
transition: background 0.1s;
|
||||
user-select: none;
|
||||
}
|
||||
.folder-item:hover { background: var(--bg-hover); }
|
||||
.folder-item.active { background: var(--brand-light); color: var(--brand); font-weight: 500; }
|
||||
.folder-item .count {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
.folder-item.active .count { color: var(--brand); opacity: 0.7; }
|
||||
|
||||
/* Folder menu */
|
||||
.folder-more {
|
||||
opacity: 0;
|
||||
padding: 2px 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text-tertiary);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
}
|
||||
.folder-item:hover .folder-more { opacity: 1; }
|
||||
.folder-more:hover { background: var(--bg-hover); color: var(--text-primary); }
|
||||
|
||||
.folder-menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--r-md);
|
||||
box-shadow: var(--shadow-md);
|
||||
z-index: 10;
|
||||
min-width: 120px;
|
||||
padding: 4px;
|
||||
}
|
||||
.folder-menu-item {
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
border-radius: var(--r-sm);
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.folder-menu-item:hover { background: var(--bg-hover); }
|
||||
.folder-menu-item.danger { color: var(--danger); }
|
||||
.folder-menu-item.danger:hover { background: var(--danger-light); }
|
||||
|
||||
.folder-children { margin-left: 16px; }
|
||||
.folder-arrow { font-size: 10px; color: var(--text-tertiary); width: 12px; flex-shrink: 0; }
|
||||
|
||||
/* ——— Site List ——— */
|
||||
.site-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 36px;
|
||||
padding: 0 8px;
|
||||
border-radius: var(--r-sm);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: background 0.1s;
|
||||
user-select: none;
|
||||
}
|
||||
.site-item:hover { background: var(--bg-hover); }
|
||||
.site-item.active { background: var(--brand-light); color: var(--brand); font-weight: 500; }
|
||||
.site-item .site-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.site-item .site-count { font-size: 10px; color: var(--text-tertiary); flex-shrink: 0; }
|
||||
.site-item.active .site-count { color: var(--brand); opacity: 0.7; }
|
||||
|
||||
/* ——— Content Area ——— */
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-surface);
|
||||
min-width: 0;
|
||||
}
|
||||
.content-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px var(--space-xl);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
gap: var(--space-md);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.content-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
flex: 1;
|
||||
font-family: inherit;
|
||||
}
|
||||
.content-title::placeholder { color: var(--text-placeholder); }
|
||||
|
||||
/* ——— View Toggle ——— */
|
||||
.toggle-group {
|
||||
display: flex;
|
||||
background: var(--bg-hover);
|
||||
border-radius: var(--r-sm);
|
||||
padding: 2px;
|
||||
}
|
||||
.toggle-group button {
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.15s;
|
||||
font-family: inherit;
|
||||
}
|
||||
.toggle-group button.active {
|
||||
background: var(--bg-surface);
|
||||
box-shadow: var(--shadow-sm);
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ——— Editor Area ——— */
|
||||
.editor-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
}
|
||||
.editor-pane {
|
||||
flex: 1;
|
||||
padding: var(--space-xl);
|
||||
overflow-y: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.editor-pane + .editor-pane { border-left: 1px solid var(--border-light); }
|
||||
|
||||
/* ——— Tiptap Toolbar ——— */
|
||||
.tb-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 4px 6px;
|
||||
background: var(--bg-sidebar);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--r-md);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
.tb-divider {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: var(--border-medium);
|
||||
margin: 0 4px;
|
||||
}
|
||||
.tb-btn {
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
font-family: inherit;
|
||||
transition: all 0.1s;
|
||||
}
|
||||
.tb-btn:hover { background: var(--bg-hover); }
|
||||
.tb-btn.active { background: var(--brand-light); color: var(--brand); }
|
||||
.tb-color {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid var(--border-medium);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
/* ProseMirror / Tiptap Content */
|
||||
.ProseMirror { outline: none; min-height: 300px; font-size: 14px; line-height: 1.8; }
|
||||
.ProseMirror h1 { font-size: 22px; font-weight: 700; margin: 16px 0 8px; padding-bottom: 6px; border-bottom: 1px solid var(--border-light); }
|
||||
.ProseMirror h2 { font-size: 18px; font-weight: 600; margin: 14px 0 6px; }
|
||||
.ProseMirror h3 { font-size: 15px; font-weight: 600; margin: 12px 0 4px; }
|
||||
.ProseMirror p { margin: 4px 0; }
|
||||
.ProseMirror ul, .ProseMirror ol { padding-left: 20px; margin: 4px 0; }
|
||||
.ProseMirror li { margin: 2px 0; }
|
||||
.ProseMirror blockquote { border-left: 3px solid var(--brand); padding-left: 12px; color: var(--text-secondary); margin: 8px 0; }
|
||||
.ProseMirror pre { background: #F7F8FA; padding: 12px 16px; border-radius: var(--r-sm); overflow-x: auto; font-family: "SF Mono","Fira Code","Consolas",monospace; font-size: 13px; line-height: 1.6; }
|
||||
.ProseMirror code { background: #F2F3F5; padding: 2px 6px; border-radius: 3px; font-size: 12px; font-family: "SF Mono","Fira Code","Consolas",monospace; }
|
||||
.ProseMirror table { border-collapse: collapse; width: 100%; margin: 8px 0; }
|
||||
.ProseMirror th, .ProseMirror td { border: 1px solid var(--border-light); padding: 8px 12px; text-align: left; }
|
||||
.ProseMirror th { background: #F7F8FA; font-weight: 600; font-size: 13px; }
|
||||
.ProseMirror img { max-width: 100%; border-radius: var(--r-sm); }
|
||||
.ProseMirror mark { background: #FFF3CD; padding: 2px 4px; border-radius: 2px; }
|
||||
|
||||
/* ——— Note List in Sidebar ——— */
|
||||
.note-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 34px;
|
||||
padding: 0 8px;
|
||||
border-radius: var(--r-sm);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.note-list-item:hover { background: var(--bg-hover); }
|
||||
.note-list-item.active { background: var(--brand-light); }
|
||||
.note-list-item .note-title-s {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
.note-list-item .note-date { font-size: 10px; color: var(--text-tertiary); }
|
||||
|
||||
/* ——— Account Card ——— */
|
||||
.card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--r-lg);
|
||||
padding: var(--space-lg);
|
||||
transition: box-shadow 0.15s;
|
||||
}
|
||||
.card:hover { box-shadow: var(--shadow-sm); }
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.card-title { font-size: 14px; font-weight: 600; }
|
||||
.card-body { display: grid; grid-template-columns: auto 1fr auto; gap: 8px 12px; font-size: 13px; align-items: center; }
|
||||
.card-body .label { color: var(--text-tertiary); font-size: 12px; white-space: nowrap; }
|
||||
.card-body .value { font-family: "SF Mono","Fira Code","Consolas",monospace; font-size: 13px; word-break: break-all; }
|
||||
|
||||
.card-actions { display: flex; gap: 6px; }
|
||||
.card-btn {
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--r-sm);
|
||||
background: var(--bg-surface);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.15s;
|
||||
font-family: inherit;
|
||||
}
|
||||
.card-btn:hover { background: var(--bg-hover); border-color: var(--border-medium); }
|
||||
.card-btn.edit:hover { color: var(--brand); border-color: var(--brand); background: var(--brand-light); }
|
||||
.card-btn.danger:hover { color: var(--danger); border-color: var(--danger); background: var(--danger-light); }
|
||||
|
||||
/* ——— Modal ——— */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
animation: fade-in 0.15s ease;
|
||||
}
|
||||
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
|
||||
|
||||
.modal {
|
||||
background: var(--bg-surface);
|
||||
border-radius: var(--r-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
width: 480px;
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: slide-in 0.15s ease;
|
||||
}
|
||||
@keyframes slide-in { from { transform: translateY(8px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-lg) var(--space-xl);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.modal-title { font-size: 15px; font-weight: 600; }
|
||||
.modal-close {
|
||||
width: 28px; height: 28px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border: none; background: transparent;
|
||||
border-radius: var(--r-sm);
|
||||
cursor: pointer;
|
||||
font-size: 18px; color: var(--text-tertiary);
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.modal-close:hover { background: var(--bg-hover); color: var(--text-primary); }
|
||||
|
||||
.modal-body { padding: var(--space-xl); overflow-y: auto; flex: 1; }
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-md) var(--space-xl);
|
||||
border-top: 1px solid var(--border-light);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ——— Form ——— */
|
||||
.form-group { margin-bottom: var(--space-lg); }
|
||||
.form-group:last-child { margin-bottom: 0; }
|
||||
.form-label { display: block; font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-bottom: 4px; }
|
||||
.form-row { display: flex; gap: var(--space-sm); align-items: center; }
|
||||
|
||||
/* ——— Import Area ——— */
|
||||
.import-textarea {
|
||||
width: 100%;
|
||||
height: 90px;
|
||||
padding: 10px 12px;
|
||||
border: 2px solid var(--brand);
|
||||
border-radius: var(--r-md);
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
outline: none;
|
||||
background: var(--brand-light);
|
||||
}
|
||||
.import-hint { font-size: 11px; color: var(--text-tertiary); margin: 4px 0 12px; }
|
||||
|
||||
.result-box {
|
||||
background: #F0FAF0;
|
||||
border: 1px solid #A3E4A7;
|
||||
border-radius: var(--r-md);
|
||||
padding: 14px 16px;
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
.result-grid { display: grid; grid-template-columns: 70px 1fr; gap: 6px 12px; font-size: 13px; }
|
||||
|
||||
.alert {
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 12px;
|
||||
}
|
||||
.alert-warning { background: #FFF7E6; color: #8A5D00; }
|
||||
|
||||
/* ——— Stats ——— */
|
||||
.stat-row { display: flex; gap: 6px; }
|
||||
.stat {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 8px 4px;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 11px;
|
||||
}
|
||||
.stat-num { font-size: 18px; font-weight: 700; }
|
||||
.stat-label { color: var(--text-tertiary); font-size: 10px; }
|
||||
|
||||
/* ——— Toast ——— */
|
||||
.toast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #1F2329;
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--r-sm);
|
||||
font-size: 13px;
|
||||
z-index: 9999;
|
||||
animation: fade-in 0.2s ease;
|
||||
}
|
||||
|
||||
/* ——— Tag Badge ——— */
|
||||
.tag-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
background: var(--bg-hover);
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.tag-badge:hover { background: #DEE0E3; }
|
||||
.tag-badge.active { background: var(--brand); color: #fff; }
|
||||
.tag-add { border: 1px dashed var(--border-medium); background: transparent; color: var(--text-tertiary); }
|
||||
.tag-add:hover { border-color: var(--brand); color: var(--brand); background: transparent; }
|
||||
|
||||
/* ——— Helpers ——— */
|
||||
.hidden { display: none !important; }
|
||||
.flex-1 { flex: 1; }
|
||||
.text-muted { color: var(--text-tertiary); font-size: 12px; }
|
||||
.text-sm { font-size: 12px; }
|
||||
.gap-xs { gap: var(--space-xs); }
|
||||
.gap-sm { gap: var(--space-sm); }
|
||||
.mb-sm { margin-bottom: var(--space-sm); }
|
||||
.mb-md { margin-bottom: var(--space-md); }
|
||||
|
||||
/* ——— Scrollbar ——— */
|
||||
::-webkit-scrollbar { width: 5px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #D0D3D7; border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #B0B3B7; }
|
||||
|
||||
/* ——— Brand wrapper ——— */
|
||||
.save-badge {
|
||||
font-size: 10px;
|
||||
color: var(--text-tertiary);
|
||||
background: var(--bg-hover);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
1
frontend/static/dist/assets/index-5uIxZ5nb.css
vendored
Normal file
1
frontend/static/dist/assets/index-5uIxZ5nb.css
vendored
Normal file
File diff suppressed because one or more lines are too long
166
frontend/static/dist/assets/index-BpxV2n8n.js
vendored
Normal file
166
frontend/static/dist/assets/index-BpxV2n8n.js
vendored
Normal file
File diff suppressed because one or more lines are too long
13
frontend/static/dist/index.html
vendored
Normal file
13
frontend/static/dist/index.html
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>MoKeeText</title>
|
||||
<script type="module" crossorigin src="/assets/index-BpxV2n8n.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-5uIxZ5nb.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
133
frontend/static/js/editor.js
Normal file
133
frontend/static/js/editor.js
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Tiptap 增强编辑器初始化
|
||||
* 支持:文字颜色、背景高亮、图片、表格、任务列表、代码块
|
||||
*/
|
||||
let editor = null;
|
||||
|
||||
function initEditor(content) {
|
||||
content = content || '';
|
||||
if (editor) {
|
||||
editor.destroy();
|
||||
}
|
||||
|
||||
editor = new tiptap.Editor({
|
||||
element: document.querySelector("#tiptapEditor"),
|
||||
extensions: [
|
||||
tiptap.StarterKit,
|
||||
tiptap.TextStyle,
|
||||
tiptap.Color,
|
||||
tiptap.Highlight.configure({ multicolor: true }),
|
||||
tiptap.Image.configure({ inline: true, allowBase64: true }),
|
||||
tiptap.Table.configure({ resizable: true }),
|
||||
tiptap.TableRow,
|
||||
tiptap.TableCell,
|
||||
tiptap.TableHeader,
|
||||
tiptap.TaskList,
|
||||
tiptap.TaskItem.configure({ nested: true }),
|
||||
],
|
||||
content: content,
|
||||
onUpdate: function() {
|
||||
updatePreview();
|
||||
scheduleSave();
|
||||
},
|
||||
});
|
||||
|
||||
renderToolbar();
|
||||
}
|
||||
|
||||
function renderToolbar() {
|
||||
var toolbar = document.querySelector("#tiptapToolbar");
|
||||
if (!toolbar) return;
|
||||
|
||||
var html = '';
|
||||
|
||||
// 标题
|
||||
html += '<span class="tb-group">';
|
||||
html += '<button class="tb-btn" data-action="h1">H1</button>';
|
||||
html += '<button class="tb-btn" data-action="h2">H2</button>';
|
||||
html += '<button class="tb-btn" data-action="h3">H3</button>';
|
||||
html += '</span>';
|
||||
|
||||
// 内联格式
|
||||
html += '<span class="tb-group">';
|
||||
html += '<button class="tb-btn" data-action="bold" style="font-weight:bold;">B</button>';
|
||||
html += '<button class="tb-btn" data-action="italic" style="font-style:italic;">I</button>';
|
||||
html += '<button class="tb-btn" data-action="strike" style="text-decoration:line-through;">S</button>';
|
||||
html += '</span>';
|
||||
|
||||
// 颜色
|
||||
html += '<span class="tb-group">';
|
||||
html += '<input type="color" id="textColor" title="文字颜色" style="width:22px;height:22px;border:none;cursor:pointer;padding:0;border-radius:3px;" value="#333333">';
|
||||
html += '<input type="color" id="bgColor" title="背景高亮" style="width:22px;height:22px;border:none;cursor:pointer;padding:0;border-radius:3px;" value="#fff3cd">';
|
||||
html += '</span>';
|
||||
|
||||
// 媒体
|
||||
html += '<span class="tb-group">';
|
||||
html += '<button class="tb-btn" data-action="image">🖼 图片</button>';
|
||||
html += '<button class="tb-btn" data-action="emoji">😊 表情</button>';
|
||||
html += '</span>';
|
||||
|
||||
// 块级元素
|
||||
html += '<span class="tb-group">';
|
||||
html += '<button class="tb-btn" data-action="bulletList">• 列表</button>';
|
||||
html += '<button class="tb-btn" data-action="orderedList">1. 列表</button>';
|
||||
html += '<button class="tb-btn" data-action="taskList">✅ 任务</button>';
|
||||
html += '<button class="tb-btn" data-action="blockquote">❝ 引用</button>';
|
||||
html += '<button class="tb-btn" data-action="codeBlock"></> 代码</button>';
|
||||
html += '</span>';
|
||||
|
||||
// 表格
|
||||
html += '<span class="tb-group">';
|
||||
html += '<button class="tb-btn" data-action="table">📋 表格</button>';
|
||||
html += '</span>';
|
||||
|
||||
toolbar.innerHTML = html;
|
||||
|
||||
// 绑定按钮事件
|
||||
var actions = {
|
||||
'h1': function() { editor.chain().focus().toggleHeading({ level: 1 }).run(); },
|
||||
'h2': function() { editor.chain().focus().toggleHeading({ level: 2 }).run(); },
|
||||
'h3': function() { editor.chain().focus().toggleHeading({ level: 3 }).run(); },
|
||||
'bold': function() { editor.chain().focus().toggleBold().run(); },
|
||||
'italic': function() { editor.chain().focus().toggleItalic().run(); },
|
||||
'strike': function() { editor.chain().focus().toggleStrike().run(); },
|
||||
'image': function() {
|
||||
var url = prompt('输入图片 URL(支持直接粘贴图片链接):');
|
||||
if (url) editor.chain().focus().setImage({ src: url }).run();
|
||||
},
|
||||
'emoji': function() {
|
||||
var emoji = prompt('输入表情符号(Win+. 打开系统表情面板粘贴):');
|
||||
if (emoji) editor.chain().focus().insertContent(emoji).run();
|
||||
},
|
||||
'bulletList': function() { editor.chain().focus().toggleBulletList().run(); },
|
||||
'orderedList': function() { editor.chain().focus().toggleOrderedList().run(); },
|
||||
'taskList': function() { editor.chain().focus().toggleTaskList().run(); },
|
||||
'blockquote': function() { editor.chain().focus().toggleBlockquote().run(); },
|
||||
'codeBlock': function() { editor.chain().focus().toggleCodeBlock().run(); },
|
||||
'table': function() { editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(); },
|
||||
};
|
||||
|
||||
toolbar.querySelectorAll('.tb-btn').forEach(function(btn) {
|
||||
var action = btn.dataset.action;
|
||||
if (actions[action]) {
|
||||
btn.addEventListener('click', actions[action]);
|
||||
}
|
||||
});
|
||||
|
||||
// 颜色选择器
|
||||
var textColor = document.querySelector('#textColor');
|
||||
var bgColor = document.querySelector('#bgColor');
|
||||
if (textColor) textColor.addEventListener('input', function(e) {
|
||||
editor.chain().focus().setColor(e.target.value).run();
|
||||
});
|
||||
if (bgColor) bgColor.addEventListener('input', function(e) {
|
||||
editor.chain().focus().toggleHighlight({ color: e.target.value }).run();
|
||||
});
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
var preview = document.querySelector("#previewContent");
|
||||
if (preview && editor) {
|
||||
preview.innerHTML = editor.getHTML();
|
||||
}
|
||||
}
|
||||
150
frontend/static/js/notes.js
Normal file
150
frontend/static/js/notes.js
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 笔记页面交互逻辑
|
||||
*/
|
||||
var currentNoteId = null;
|
||||
var saveTimer = null;
|
||||
var viewMode = "edit";
|
||||
|
||||
function setViewMode(mode) {
|
||||
viewMode = mode;
|
||||
var editPane = document.querySelector("#editPane");
|
||||
var previewPane = document.querySelector("#previewPane");
|
||||
document.querySelectorAll(".toggle-group button").forEach(function(b) {
|
||||
b.classList.toggle("active", b.dataset.mode === mode);
|
||||
});
|
||||
editPane.classList.remove("hidden");
|
||||
previewPane.classList.remove("hidden");
|
||||
if (mode === "edit") { previewPane.classList.add("hidden"); }
|
||||
else if (mode === "preview") { editPane.classList.add("hidden"); }
|
||||
updatePreview();
|
||||
}
|
||||
|
||||
function createNote() {
|
||||
var folderId = getActiveFolderId();
|
||||
fetch("/api/notes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title: "无标题笔记", content: "", folder_id: folderId }),
|
||||
}).then(function(r) { return r.json(); })
|
||||
.then(function(note) { loadNote(note.id); refreshFolderNotes(); });
|
||||
}
|
||||
|
||||
function loadNote(noteId) {
|
||||
fetch("/api/notes/" + noteId)
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(note) {
|
||||
currentNoteId = note.id;
|
||||
document.querySelector("#currentNoteId").value = note.id;
|
||||
document.querySelector("#noteTitle").value = note.title;
|
||||
initEditor(note.content || "");
|
||||
document.querySelectorAll(".note-list-item").forEach(function(el) {
|
||||
el.classList.toggle("active", parseInt(el.dataset.noteId) === note.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function scheduleSave() {
|
||||
var s = document.querySelector("#saveStatus");
|
||||
if (s) { s.textContent = "保存中…"; s.style.color = "#F5A623"; }
|
||||
clearTimeout(saveTimer);
|
||||
saveTimer = setTimeout(saveNote, 800);
|
||||
}
|
||||
|
||||
function saveNote() {
|
||||
if (!currentNoteId) return;
|
||||
var title = document.querySelector("#noteTitle").value;
|
||||
var content = editor ? editor.getHTML() : "";
|
||||
fetch("/api/notes/" + currentNoteId, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title: title, content: content }),
|
||||
}).then(function(r) {
|
||||
if (r.ok) { var s = document.querySelector("#saveStatus"); if (s) { s.textContent = "已保存"; s.style.color = ""; } }
|
||||
});
|
||||
}
|
||||
|
||||
function filterByFolder(folderId, el) {
|
||||
document.querySelectorAll(".folder-item").forEach(function(f) { f.classList.remove("active"); });
|
||||
el.classList.add("active");
|
||||
fetch("/api/notes?folder_id=" + folderId)
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(notes) { renderNoteList(notes); });
|
||||
}
|
||||
|
||||
function renderNoteList(notes) {
|
||||
var listDiv = document.querySelector("#folderNoteList");
|
||||
if (!listDiv) return;
|
||||
listDiv.classList.remove("hidden");
|
||||
|
||||
var html = '<div style="padding:4px 0; cursor:pointer; color:var(--brand); font-size:11px;" onclick="showFolderList()">← 返回文件夹</div>';
|
||||
if (!notes.length) {
|
||||
html += '<div style="color:#ccc; padding:8px; text-align:center; font-size:12px;">暂无笔记</div>';
|
||||
} else {
|
||||
notes.forEach(function(note) {
|
||||
var title = note.title || "无标题";
|
||||
var date = (note.updated_at || "").substr(0, 10);
|
||||
html += '<div class="note-list-item" data-note-id="' + note.id + '" onclick="loadNote(' + note.id + ')">' +
|
||||
'<span class="note-title-s">' + escapeHtml(title) + '</span>' +
|
||||
'<span class="note-date">' + date + '</span></div>';
|
||||
});
|
||||
}
|
||||
listDiv.innerHTML = html;
|
||||
}
|
||||
|
||||
function showFolderList() {
|
||||
var listDiv = document.querySelector("#folderNoteList");
|
||||
if (listDiv) listDiv.classList.add("hidden");
|
||||
}
|
||||
|
||||
function getActiveFolderId() {
|
||||
var active = document.querySelector(".folder-item.active");
|
||||
return active ? parseInt(active.dataset.folderId) : null;
|
||||
}
|
||||
|
||||
function refreshFolderNotes() {
|
||||
var af = document.querySelector(".folder-item.active");
|
||||
if (af) filterByFolder(parseInt(af.dataset.folderId), af);
|
||||
}
|
||||
|
||||
function addFolder() {
|
||||
var name = prompt("输入新文件夹名称:");
|
||||
if (!name) return;
|
||||
fetch("/api/folders", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: name, type: "note" }) })
|
||||
.then(function() { window.location.reload(); });
|
||||
}
|
||||
|
||||
function toggleFolderMenu(btn) {
|
||||
document.querySelectorAll(".folder-menu").forEach(function(m) { if (m !== btn.nextElementSibling) m.classList.add("hidden"); });
|
||||
var menu = btn.nextElementSibling;
|
||||
if (menu) menu.classList.toggle("hidden");
|
||||
setTimeout(function() {
|
||||
document.addEventListener("click", function close() { if (menu) menu.classList.add("hidden"); document.removeEventListener("click", close); }, { once: true });
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function renameFolder(folderId) {
|
||||
var name = prompt("新名称:"); if (!name) return;
|
||||
fetch("/api/folders/" + folderId, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: name }) })
|
||||
.then(function() { window.location.reload(); });
|
||||
}
|
||||
|
||||
function deleteFolder(folderId) {
|
||||
if (!confirm("确定删除此文件夹?内容将移至「未分类」")) return;
|
||||
fetch("/api/folders/" + folderId, { method: "DELETE" }).then(function() { window.location.reload(); });
|
||||
}
|
||||
|
||||
function deleteCurrentNote() {
|
||||
if (!currentNoteId || !confirm("确定删除?")) return;
|
||||
fetch("/api/notes/" + currentNoteId, { method: "DELETE" }).then(function() { window.location.reload(); });
|
||||
}
|
||||
|
||||
function filterByTag(tagId, el) { el.classList.toggle("active"); }
|
||||
function addTag() { var n = prompt("标签名:"); if (n) window.location.reload(); }
|
||||
|
||||
function escapeHtml(s) { return s ? s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""") : ""; }
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
document.querySelector("#noteTitle").addEventListener("input", scheduleSave);
|
||||
var firstId = document.querySelector("#firstNoteId");
|
||||
if (firstId && firstId.value) loadNote(parseInt(firstId.value)); else createNote();
|
||||
});
|
||||
155
frontend/static/js/vendor/tiptap-bundle.min.js
vendored
Normal file
155
frontend/static/js/vendor/tiptap-bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/static/js/vendor/tiptap-core.umd.min.js
vendored
Normal file
1
frontend/static/js/vendor/tiptap-core.umd.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Couldn't find the requested file /dist/tiptap-core.umd.min.js in @tiptap/core.
|
||||
29
frontend/static/js/vendor/tiptap-entry.js
vendored
Normal file
29
frontend/static/js/vendor/tiptap-entry.js
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
// Tiptap 打包入口 - 将所有需要的模块挂载到全局 window.tiptap
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { StarterKit } from "@tiptap/starter-kit";
|
||||
import { Color } from "@tiptap/extension-color";
|
||||
import { Highlight } from "@tiptap/extension-highlight";
|
||||
import { TextStyle } from "@tiptap/extension-text-style";
|
||||
import { Image } from "@tiptap/extension-image";
|
||||
import { Table } from "@tiptap/extension-table";
|
||||
import { TableRow } from "@tiptap/extension-table-row";
|
||||
import { TableCell } from "@tiptap/extension-table-cell";
|
||||
import { TableHeader } from "@tiptap/extension-table-header";
|
||||
import { TaskList } from "@tiptap/extension-task-list";
|
||||
import { TaskItem } from "@tiptap/extension-task-item";
|
||||
|
||||
// 挂载到全局,保持和 UMD 版本一样的 API
|
||||
window.tiptap = {
|
||||
Editor,
|
||||
StarterKit,
|
||||
Color,
|
||||
Highlight,
|
||||
TextStyle,
|
||||
Image,
|
||||
Table,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
TaskList,
|
||||
TaskItem,
|
||||
};
|
||||
1
frontend/static/js/vendor/tiptap-extension-code-block-lowlight.umd.min.js
vendored
Normal file
1
frontend/static/js/vendor/tiptap-extension-code-block-lowlight.umd.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Couldn't find the requested file /dist/tiptap-extension-code-block-lowlight.umd.min.js in @tiptap/extension-code-block-lowlight.
|
||||
1
frontend/static/js/vendor/tiptap-extension-color.umd.min.js
vendored
Normal file
1
frontend/static/js/vendor/tiptap-extension-color.umd.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Couldn't find the requested file /dist/tiptap-extension-color.umd.min.js in @tiptap/extension-color.
|
||||
1
frontend/static/js/vendor/tiptap-extension-highlight.umd.min.js
vendored
Normal file
1
frontend/static/js/vendor/tiptap-extension-highlight.umd.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Couldn't find the requested file /dist/tiptap-extension-highlight.umd.min.js in @tiptap/extension-highlight.
|
||||
1
frontend/static/js/vendor/tiptap-extension-image.umd.min.js
vendored
Normal file
1
frontend/static/js/vendor/tiptap-extension-image.umd.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Couldn't find the requested file /dist/tiptap-extension-image.umd.min.js in @tiptap/extension-image.
|
||||
1
frontend/static/js/vendor/tiptap-extension-table-cell.umd.min.js
vendored
Normal file
1
frontend/static/js/vendor/tiptap-extension-table-cell.umd.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Couldn't find the requested file /dist/tiptap-extension-table-cell.umd.min.js in @tiptap/extension-table-cell.
|
||||
1
frontend/static/js/vendor/tiptap-extension-table-header.umd.min.js
vendored
Normal file
1
frontend/static/js/vendor/tiptap-extension-table-header.umd.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Couldn't find the requested file /dist/tiptap-extension-table-header.umd.min.js in @tiptap/extension-table-header.
|
||||
1
frontend/static/js/vendor/tiptap-extension-table-row.umd.min.js
vendored
Normal file
1
frontend/static/js/vendor/tiptap-extension-table-row.umd.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Couldn't find the requested file /dist/tiptap-extension-table-row.umd.min.js in @tiptap/extension-table-row.
|
||||
1
frontend/static/js/vendor/tiptap-extension-table.umd.min.js
vendored
Normal file
1
frontend/static/js/vendor/tiptap-extension-table.umd.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Couldn't find the requested file /dist/tiptap-extension-table.umd.min.js in @tiptap/extension-table.
|
||||
1
frontend/static/js/vendor/tiptap-extension-task-item.umd.min.js
vendored
Normal file
1
frontend/static/js/vendor/tiptap-extension-task-item.umd.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Couldn't find the requested file /dist/tiptap-extension-task-item.umd.min.js in @tiptap/extension-task-item.
|
||||
1
frontend/static/js/vendor/tiptap-extension-task-list.umd.min.js
vendored
Normal file
1
frontend/static/js/vendor/tiptap-extension-task-list.umd.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Couldn't find the requested file /dist/tiptap-extension-task-list.umd.min.js in @tiptap/extension-task-list.
|
||||
1
frontend/static/js/vendor/tiptap-extension-text-style.umd.min.js
vendored
Normal file
1
frontend/static/js/vendor/tiptap-extension-text-style.umd.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Couldn't find the requested file /dist/tiptap-extension-text-style.umd.min.js in @tiptap/extension-text-style.
|
||||
1
frontend/static/js/vendor/tiptap-starter-kit.umd.min.js
vendored
Normal file
1
frontend/static/js/vendor/tiptap-starter-kit.umd.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Couldn't find the requested file /dist/tiptap-starter-kit.umd.min.js in @tiptap/starter-kit.
|
||||
249
frontend/static/js/websites.js
Normal file
249
frontend/static/js/websites.js
Normal file
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* 网站管理页面交互逻辑
|
||||
*/
|
||||
var currentSiteId = null;
|
||||
var parsedImportData = null;
|
||||
|
||||
// ===== 网站选择 =====
|
||||
function selectWebsite(siteId, el) {
|
||||
currentSiteId = siteId;
|
||||
document.querySelector("#currentSiteId").value = siteId;
|
||||
document.querySelectorAll(".site-item").forEach(function(s) { s.classList.remove("active"); });
|
||||
if (el) el.classList.add("active");
|
||||
fetch("/api/websites/" + siteId).then(function(r) { return r.json(); }).then(renderSiteDetail);
|
||||
}
|
||||
|
||||
function renderSiteDetail(site) {
|
||||
var fName = getFolderName(site.folder_id);
|
||||
document.querySelector("#siteHeaderInfo").innerHTML =
|
||||
'<span style="font-size:11px;color:var(--text-tertiary);">' + fName + ' / </span>' +
|
||||
'<span style="font-size:15px;font-weight:600;">' + site.name + '</span>' +
|
||||
'<a href="' + site.url + '" target="_blank" style="font-size:11px;color:var(--brand);margin-left:8px;text-decoration:none;">' + site.url + '</a>';
|
||||
document.querySelector("#editSiteBtn").classList.remove("hidden");
|
||||
document.querySelector("#addAccountBtn").classList.remove("hidden");
|
||||
|
||||
var cards = document.querySelector("#accountCards");
|
||||
var accounts = site.accounts || [];
|
||||
if (!accounts.length) {
|
||||
cards.innerHTML = '<div style="text-align:center;padding:80px 20px;color:var(--text-placeholder);"><p>暂无账号</p></div>';
|
||||
return;
|
||||
}
|
||||
cards.innerHTML = accounts.map(function(a) {
|
||||
return '<div class="card" style="margin-bottom:12px;">' +
|
||||
'<div class="card-header">' +
|
||||
'<span class="card-title">👤 ' + (a.remark || '账号') + '</span>' +
|
||||
'<div class="card-actions">' +
|
||||
'<button class="card-btn edit" onclick="editAccount(' + a.id + ')">✏️ 编辑</button>' +
|
||||
'<button class="card-btn danger" onclick="deleteAccount(' + a.id + ')">🗑 删除</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="card-body">' +
|
||||
'<span class="label">账号</span><span class="value">' + esc(a.username) + '</span>' +
|
||||
'<button class="card-btn" onclick="copyText(\'' + esc(a.username) + '\')">复制</button>' +
|
||||
'<span class="label">密码</span><span class="value pw-hidden" id="pw-' + a.id + '" data-pw="' + esc(a.password) + '">••••••••</span>' +
|
||||
'<div style="display:flex;gap:4px;">' +
|
||||
'<button class="card-btn" onclick="togglePassword(' + a.id + ')">显示</button>' +
|
||||
'<button class="card-btn" onclick="copyText(\'' + esc(a.password) + '\')">复制</button>' +
|
||||
'</div>' +
|
||||
'<span class="label">说明</span><span style="color:var(--text-secondary);font-size:12px;">' + (a.remark || '-') + '</span><span></span>' +
|
||||
'</div></div>';
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function esc(s) { return (s||"").replace(/'/g,"\\'"); }
|
||||
|
||||
function togglePassword(id) {
|
||||
var el = document.querySelector("#pw-" + id);
|
||||
if (!el) return;
|
||||
el.textContent = el.textContent === "••••••••" ? el.dataset.pw : "••••••••";
|
||||
}
|
||||
|
||||
function copyText(t) {
|
||||
navigator.clipboard.writeText(t).then(function() {
|
||||
var toast = document.createElement("div");
|
||||
toast.className = "toast";
|
||||
toast.textContent = "已复制";
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(function() { toast.remove(); }, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
function getFolderName(fid) {
|
||||
if (!fid) return "未分类";
|
||||
var el = document.querySelector('.folder-group[data-folder-id="' + fid + '"] .folder-item span:nth-child(2)');
|
||||
return el ? el.textContent.replace(/📁\s*/, "").trim() : "未分类";
|
||||
}
|
||||
|
||||
// ===== 文件夹 =====
|
||||
function toggleFolder(el) {
|
||||
var ch = el.parentElement.querySelector(".folder-children");
|
||||
var arrow = el.querySelector(".folder-arrow");
|
||||
if (!ch) return;
|
||||
if (ch.style.display === "none") { ch.style.display = "block"; arrow.textContent = "▼"; }
|
||||
else { ch.style.display = "none"; arrow.textContent = "▶"; }
|
||||
}
|
||||
|
||||
function toggleFolderMenu(btn) {
|
||||
document.querySelectorAll(".folder-menu").forEach(function(m) { if (m !== btn.nextElementSibling) m.classList.add("hidden"); });
|
||||
var menu = btn.nextElementSibling;
|
||||
if (menu) menu.classList.toggle("hidden");
|
||||
setTimeout(function() {
|
||||
document.addEventListener("click", function c() { if (menu) menu.classList.add("hidden"); document.removeEventListener("click", c); }, { once: true });
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function renameFolder(id) { var n = prompt("新名称:"); if (n) fetch("/api/folders/" + id, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: n }) }).then(function() { window.location.reload(); }); }
|
||||
function deleteFolder(id) { if (confirm("确定删除?内容将移至「未分类」")) fetch("/api/folders/" + id, { method: "DELETE" }).then(function() { window.location.reload(); }); }
|
||||
function showAddFolder() { var n = prompt("文件夹名:"); if (n) fetch("/api/folders", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: n, type: "website" }) }).then(function() { window.location.reload(); }); }
|
||||
|
||||
// ===== 智能导入 =====
|
||||
function showImportModal() {
|
||||
document.querySelector("#importModal").classList.remove("hidden");
|
||||
document.querySelector("#parseResult").classList.add("hidden");
|
||||
document.querySelector("#importText").value = "";
|
||||
document.querySelector("#importNewFolder").value = "";
|
||||
}
|
||||
function hideImportModal() { document.querySelector("#importModal").classList.add("hidden"); }
|
||||
|
||||
function parseImport() {
|
||||
var text = document.querySelector("#importText").value.trim();
|
||||
if (!text) return alert("请先粘贴文本");
|
||||
fetch("/api/import/parse", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: text }) })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (!d.ok) return alert(d.error);
|
||||
parsedImportData = d;
|
||||
document.querySelector("#parseResult").classList.remove("hidden");
|
||||
document.querySelector("#parseGrid").innerHTML =
|
||||
'<span style="color:var(--text-secondary);">网址</span><span>' + d.url + '</span>' +
|
||||
'<span style="color:var(--text-secondary);">网站名</span><span>' + d.site_name + '</span>' +
|
||||
'<span style="color:var(--text-secondary);">账号</span><span style="font-family:monospace;">' + d.username + '</span>' +
|
||||
'<span style="color:var(--text-secondary);">密码</span><span style="font-family:monospace;">' + d.password + '</span>' +
|
||||
'<span style="color:var(--text-secondary);">备注</span><span>' + (d.remark || '(无)') + '</span>';
|
||||
var alert = document.querySelector("#duplicateAlert");
|
||||
if (d.action === "append") {
|
||||
alert.classList.remove("hidden");
|
||||
alert.innerHTML = '⚠️ <strong>' + d.matched_site.name + '</strong> 已存在(' + d.matched_site.account_count + '个账号),将追加进去';
|
||||
if (d.matched_site.folder_id) document.querySelector("#importFolderSelect").value = d.matched_site.folder_id;
|
||||
} else { alert.classList.add("hidden"); }
|
||||
});
|
||||
}
|
||||
|
||||
function confirmImport() {
|
||||
if (!parsedImportData) return;
|
||||
var d = parsedImportData;
|
||||
var fid = document.querySelector("#importFolderSelect").value || null;
|
||||
var nf = document.querySelector("#importNewFolder").value.trim();
|
||||
if (nf) {
|
||||
fetch("/api/folders", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: nf, type: "website" }) })
|
||||
.then(function(r) { return r.json(); }).then(function(f) { doImport(d, f.id); });
|
||||
} else { doImport(d, fid); }
|
||||
}
|
||||
|
||||
function doImport(d, fid) {
|
||||
fetch("/api/websites").then(function(r) { return r.json(); }).then(function(sites) {
|
||||
var s = sites.find(function(x) { return x.url === d.url; });
|
||||
if (s) { addAccountToSite(s.id, d); }
|
||||
else {
|
||||
fetch("/api/websites", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: d.site_name, url: d.url, folder_id: fid }) })
|
||||
.then(function(r) { return r.json(); }).then(function(ns) { addAccountToSite(ns.id, d); });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addAccountToSite(sid, d) {
|
||||
fetch("/api/websites/" + sid + "/accounts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: d.username, password: d.password, remark: d.remark }) })
|
||||
.then(function() { hideImportModal(); window.location.reload(); });
|
||||
}
|
||||
|
||||
// ===== 网站弹窗 =====
|
||||
function showAddWebsite() {
|
||||
document.querySelector("#websiteModalTitle").textContent = "添加网站";
|
||||
document.querySelector("#websiteName").value = "";
|
||||
document.querySelector("#websiteUrl").value = "";
|
||||
document.querySelector("#websiteFolder").value = "";
|
||||
document.querySelector("#editWebsiteId").value = "";
|
||||
document.querySelector("#websiteModal").classList.remove("hidden");
|
||||
}
|
||||
function hideWebsiteModal() { document.querySelector("#websiteModal").classList.add("hidden"); }
|
||||
|
||||
function editWebsite() {
|
||||
if (!currentSiteId) return;
|
||||
document.querySelector("#websiteModalTitle").textContent = "编辑网站";
|
||||
document.querySelector("#editWebsiteId").value = currentSiteId;
|
||||
fetch("/api/websites/" + currentSiteId).then(function(r) { return r.json(); }).then(function(s) {
|
||||
document.querySelector("#websiteName").value = s.name;
|
||||
document.querySelector("#websiteUrl").value = s.url;
|
||||
document.querySelector("#websiteFolder").value = s.folder_id || "";
|
||||
document.querySelector("#websiteModal").classList.remove("hidden");
|
||||
});
|
||||
}
|
||||
|
||||
function saveWebsite() {
|
||||
var id = document.querySelector("#editWebsiteId").value;
|
||||
var data = { name: document.querySelector("#websiteName").value, url: document.querySelector("#websiteUrl").value, folder_id: document.querySelector("#websiteFolder").value || null };
|
||||
fetch(id ? "/api/websites/" + id : "/api/websites", { method: id ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) })
|
||||
.then(function() { hideWebsiteModal(); window.location.reload(); });
|
||||
}
|
||||
|
||||
// ===== 账号弹窗 =====
|
||||
function showAddAccount() {
|
||||
if (!currentSiteId) return alert("请先选择网站");
|
||||
document.querySelector("#accountModalTitle").textContent = "添加账号";
|
||||
document.querySelector("#accountUsername").value = "";
|
||||
document.querySelector("#accountPassword").value = "";
|
||||
document.querySelector("#accountRemark").value = "";
|
||||
document.querySelector("#editAccountId").value = "";
|
||||
document.querySelector("#accountPassword").type = "password";
|
||||
document.querySelector("#accountModal").classList.remove("hidden");
|
||||
}
|
||||
|
||||
function hideAccountModal() { document.querySelector("#accountModal").classList.add("hidden"); }
|
||||
|
||||
function editAccount(accId) {
|
||||
document.querySelector("#accountModalTitle").textContent = "编辑账号";
|
||||
document.querySelector("#editAccountId").value = accId;
|
||||
document.querySelector("#accountPassword").type = "password";
|
||||
fetch("/api/accounts/" + accId).then(function(r) { return r.json(); }).then(function(a) {
|
||||
document.querySelector("#accountUsername").value = a.username || "";
|
||||
document.querySelector("#accountPassword").value = a.password || "";
|
||||
document.querySelector("#accountRemark").value = a.remark || "";
|
||||
document.querySelector("#accountModal").classList.remove("hidden");
|
||||
});
|
||||
}
|
||||
|
||||
function togglePwVisibility() {
|
||||
var pw = document.querySelector("#accountPassword");
|
||||
var btn = pw.parentElement.querySelector("button");
|
||||
if (pw.type === "password") { pw.type = "text"; btn.textContent = "隐藏"; }
|
||||
else { pw.type = "password"; btn.textContent = "显示"; }
|
||||
}
|
||||
|
||||
function saveAccount() {
|
||||
var sid = document.querySelector("#currentSiteId").value;
|
||||
var aid = document.querySelector("#editAccountId").value;
|
||||
var data = { username: document.querySelector("#accountUsername").value, password: document.querySelector("#accountPassword").value, remark: document.querySelector("#accountRemark").value };
|
||||
if (aid) {
|
||||
fetch("/api/accounts/" + aid, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) })
|
||||
.then(function() { hideAccountModal(); selectWebsite(sid); });
|
||||
} else {
|
||||
fetch("/api/websites/" + sid + "/accounts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) })
|
||||
.then(function() { hideAccountModal(); selectWebsite(sid); });
|
||||
}
|
||||
}
|
||||
|
||||
function deleteAccount(accId) {
|
||||
if (!confirm("确定删除?")) return;
|
||||
fetch("/api/accounts/" + accId, { method: "DELETE" }).then(function() { selectWebsite(currentSiteId); });
|
||||
}
|
||||
|
||||
// ===== 搜索 =====
|
||||
function filterSites() {
|
||||
var q = document.querySelector("#siteSearch").value.toLowerCase().trim();
|
||||
document.querySelectorAll(".site-item").forEach(function(item) {
|
||||
if (!q) { item.style.display = "flex"; return; }
|
||||
var name = (item.querySelector(".site-name")?.textContent || "").toLowerCase();
|
||||
var url = (item.dataset.siteUrl || "").toLowerCase();
|
||||
item.style.display = (name.indexOf(q) !== -1 || url.indexOf(q) !== -1) ? "flex" : "none";
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user