feat(portal): add light/dark theme sync and OA drive navigation
Memind CI / Test, build, and release guards (push) Successful in 4m31s

Unify chat and OA drive appearance with shared memind-theme storage, default light mode, and cross-frame sync so users can switch themes from chat or the drive shell.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-10 20:10:06 +08:00
parent 97a3e89ff7
commit 3e6b0c262a
10 changed files with 520 additions and 128 deletions
+149 -84
View File
@@ -1,21 +1,86 @@
:root {
--bg: #0f1117;
--panel: #171a22;
--border: #2a3140;
--text: #e8ecf4;
--muted: #8b95a8;
--accent: #5b8cff;
:root,
[data-theme='light'] {
color-scheme: light;
--bg: #f4f1ea;
--bg-gradient-start: #fffaf0;
--panel: #fffaf0;
--border: rgba(24, 33, 29, 0.12);
--text: #18211d;
--muted: #52605a;
--accent: #2f6f57;
--accent-2: #2f6f57;
--danger: #c4453d;
--drop: rgba(47, 111, 87, 0.12);
--warning: #eeb04e;
--overlay: rgba(244, 241, 234, 0.72);
--finder-bg: #fffaf0;
--finder-bg-head: #f0ebe0;
--finder-bg-body: #ffffff;
--finder-border: rgba(24, 33, 29, 0.14);
--finder-text: #18211d;
--finder-muted: #52605a;
--finder-subtle: #7a8780;
--finder-surface: #ffffff;
--finder-accent: #2f6f57;
--finder-hover: rgba(47, 111, 87, 0.08);
--finder-selected: rgba(47, 111, 87, 0.16);
--finder-drop: rgba(47, 111, 87, 0.12);
--finder-danger: #c4453d;
--finder-input-border: rgba(24, 33, 29, 0.18);
--finder-focus-ring: rgba(47, 111, 87, 0.15);
--finder-accent-ghost: rgba(47, 111, 87, 0.1);
--finder-chip-active-bg: rgba(47, 111, 87, 0.12);
--finder-chip-active-border: rgba(47, 111, 87, 0.35);
--finder-resizer: #7a8780;
--finder-row-border: rgba(24, 33, 29, 0.06);
--finder-bc-bg: #f4f1ea;
--finder-empty-bg: #faf8f4;
}
[data-theme='dark'] {
color-scheme: dark;
--bg: #0f1419;
--bg-gradient-start: #1a2236;
--panel: #121820;
--border: #243041;
--text: #e7ecf3;
--muted: #8fa3bf;
--accent: #3d8bfd;
--accent-2: #3dd68c;
--danger: #ff6b7a;
--drop: rgba(91, 140, 255, 0.12);
--warning: #ffb347;
--overlay: rgba(15, 17, 23, 0.72);
--finder-bg: #121820;
--finder-bg-head: #1c2735;
--finder-bg-body: #0f1419;
--finder-border: #243041;
--finder-text: #e7ecf3;
--finder-muted: #8fa3bf;
--finder-subtle: #6b829e;
--finder-surface: #1c2735;
--finder-accent: #3d8bfd;
--finder-hover: rgba(61, 139, 253, 0.12);
--finder-selected: rgba(61, 139, 253, 0.22);
--finder-drop: rgba(61, 139, 253, 0.18);
--finder-danger: #ff6b7a;
--finder-input-border: #2a3a50;
--finder-focus-ring: rgba(61, 139, 253, 0.25);
--finder-accent-ghost: rgba(61, 139, 253, 0.1);
--finder-chip-active-bg: rgba(61, 139, 253, 0.15);
--finder-chip-active-border: rgba(61, 139, 253, 0.45);
--finder-resizer: #6b829e;
--finder-row-border: #243041;
--finder-bc-bg: #1c2735;
--finder-empty-bg: #121820;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: radial-gradient(circle at top, #1a2236 0%, var(--bg) 45%);
background: radial-gradient(circle at top, var(--bg-gradient-start) 0%, var(--bg) 45%);
color: var(--text);
}
.wrap { max-width: min(1280px, 96vw); margin: 0 auto; padding: 10px 16px 20px; }
@@ -55,7 +120,7 @@ body {
white-space: nowrap; flex: none;
}
.link-chip:hover { border-color: var(--accent); }
.link-chip.active { border-color: var(--accent); background: #e8f0fe; color: #0066cc; font-weight: 600; }
.link-chip.active { border-color: var(--accent); background: var(--finder-chip-active-bg); color: var(--finder-accent); font-weight: 600; }
.top-actions { display: flex; gap: 4px; flex: none; }
.top-actions button { padding: 3px 8px; font-size: 11px; min-height: 0; line-height: 1.3; }
.top-actions button.primary { padding: 3px 9px; }
@@ -129,7 +194,7 @@ button:disabled { opacity: 0.45; cursor: not-allowed; filter: none; }
.drag-overlay {
position: fixed; inset: 0; z-index: 200; pointer-events: none;
display: none; align-items: center; justify-content: center;
background: rgba(15, 17, 23, 0.72); backdrop-filter: blur(4px);
background: var(--overlay); backdrop-filter: blur(4px);
}
.drag-overlay.show { display: flex; pointer-events: none; }
.drag-overlay-box {
@@ -144,8 +209,8 @@ button:disabled { opacity: 0.45; cursor: not-allowed; filter: none; }
.panel-shell { margin-top: 8px; position: relative; width: 100%; max-width: 100%; }
.panel {
display: flex; flex-direction: column;
background: #f5f5f7; border: 1px solid #d1d1d6;
border-radius: 10px; overflow: hidden; color: #1d1d1f;
background: var(--finder-bg); border: 1px solid var(--finder-border);
border-radius: 10px; overflow: hidden; color: var(--finder-text);
height: 520px; min-height: 280px; max-height: 85vh;
}
.panel-resizer {
@@ -154,57 +219,57 @@ button:disabled { opacity: 0.45; cursor: not-allowed; filter: none; }
}
.panel-resizer::after {
content: ''; position: absolute; right: 4px; bottom: 4px; width: 10px; height: 10px;
border-right: 2px solid #86868b; border-bottom: 2px solid #86868b;
border-right: 2px solid var(--finder-subtle); border-bottom: 2px solid var(--finder-subtle);
border-radius: 0 0 2px 0; opacity: 0.75; transition: opacity .15s;
}
.panel-resizer:hover::after,
.panel-shell-resizing .panel-resizer::after { opacity: 1; border-color: #0066cc; }
.panel-shell-resizing .panel-resizer::after { opacity: 1; border-color: var(--finder-accent); }
body.panel-shell-resizing { user-select: none; cursor: nwse-resize; }
/* Toolbar */
.panel-head {
display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px;
padding: 10px 14px; border-bottom: 1px solid #d1d1d6; font-weight: 600;
font-size: 13px; background: #ebebed; flex: none;
padding: 10px 14px; border-bottom: 1px solid var(--finder-border); font-weight: 600;
font-size: 13px; background: var(--finder-bg-head); flex: none;
}
.panel-head small { color: #6e6e73; font-weight: 400; }
.panel-head small { color: var(--finder-muted); font-weight: 400; }
.panel-head button {
background: #fff; border-color: #c7c7cc; color: #1d1d1f; padding: 4px 10px;
background: var(--finder-surface); border-color: var(--finder-input-border); color: var(--finder-text); padding: 4px 10px;
border-radius: 6px; font-size: 12px;
}
.panel-head .bulk-actions { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.panel-head .select-all-label {
display: inline-flex; align-items: center; gap: 4px; font-weight: 400;
font-size: 12px; color: #6e6e73; cursor: pointer; user-select: none;
font-size: 12px; color: var(--finder-muted); cursor: pointer; user-select: none;
}
.panel-head .select-all-label input { width: 14px; height: 14px; accent-color: #0066cc; cursor: pointer; }
.panel-head .select-all-label input { width: 14px; height: 14px; accent-color: var(--finder-accent); cursor: pointer; }
.panel-head .bulk-actions button.danger {
background: #fff; border-color: #ff3b30; color: #ff3b30;
background: var(--finder-surface); border-color: var(--finder-danger); color: var(--finder-danger);
}
.panel-head .bulk-actions button.danger:disabled { opacity: 0.45; cursor: not-allowed; }
.panel-head .bulk-actions button.danger:not(:disabled):hover { background: #ff3b30; color: #fff; }
.panel-head .bulk-actions button.danger:not(:disabled):hover { background: var(--finder-danger); color: #fff; }
/* Search bar */
.toolbar-search {
display: flex; align-items: center; gap: 6px; flex: 1; min-width: 140px; max-width: 220px;
}
.toolbar-search input {
flex: 1; min-width: 0; padding: 4px 8px; border: 1px solid #c7c7cc; border-radius: 6px;
font-size: 12px; background: #fff; color: #1d1d1f;
flex: 1; min-width: 0; padding: 4px 8px; border: 1px solid var(--finder-input-border); border-radius: 6px;
font-size: 12px; background: var(--finder-surface); color: var(--finder-text);
}
.toolbar-search input:focus { outline: none; border-color: #0066cc; box-shadow: 0 0 0 2px rgba(0,102,204,.15); }
.toolbar-search input::placeholder { color: #aeaeb2; }
.toolbar-search input:focus { outline: none; border-color: var(--finder-accent); box-shadow: 0 0 0 2px var(--finder-focus-ring); }
.toolbar-search input::placeholder { color: var(--finder-subtle); }
/* Conflict policy select */
.conflict-select {
padding: 4px 6px; border: 1px solid #c7c7cc; border-radius: 6px;
font-size: 11px; background: #fff; color: #1d1d1f; cursor: pointer;
padding: 4px 6px; border: 1px solid var(--finder-input-border); border-radius: 6px;
font-size: 11px; background: var(--finder-surface); color: var(--finder-text); cursor: pointer;
}
.conflict-select:focus { outline: none; border-color: #0066cc; }
.conflict-select:focus { outline: none; border-color: var(--finder-accent); }
/* Notification toggle */
.notify-btn { position: relative; }
.notify-btn.active { background: #e8f0fe; border-color: #0066cc; color: #0066cc; }
.notify-btn.active { background: var(--finder-hover); border-color: var(--finder-accent); color: var(--finder-accent); }
.notify-btn .badge {
position: absolute; top: -4px; right: -4px; width: 8px; height: 8px;
border-radius: 50%; background: var(--accent-2); display: none;
@@ -212,29 +277,29 @@ body.panel-shell-resizing { user-select: none; cursor: nwse-resize; }
.notify-btn.has-unread .badge { display: block; }
.view-toggle {
display: inline-flex; border: 1px solid #c7c7cc; border-radius: 6px; overflow: hidden; background: #fff;
display: inline-flex; border: 1px solid var(--finder-input-border); border-radius: 6px; overflow: hidden; background: var(--finder-surface);
}
.view-toggle button {
border: none; border-radius: 0; padding: 4px 8px; min-height: 0; background: #fff; color: #6e6e73;
border: none; border-radius: 0; padding: 4px 8px; min-height: 0; background: var(--finder-surface); color: var(--finder-muted);
font-size: 14px; line-height: 1;
}
.view-toggle button + button { border-left: 1px solid #c7c7cc; }
.view-toggle button.active { background: #e8f0fe; color: #0066cc; }
.view-toggle button:hover { filter: none; background: #f2f2f7; }
.view-toggle button.active:hover { background: #e8f0fe; }
.view-toggle button + button { border-left: 1px solid var(--finder-input-border); }
.view-toggle button.active { background: var(--finder-hover); color: var(--finder-accent); }
.view-toggle button:hover { filter: none; background: var(--finder-hover); }
.view-toggle button.active:hover { background: var(--finder-hover); }
.breadcrumb {
display: flex; flex-wrap: wrap; align-items: center; gap: 4px;
padding: 8px 14px; border-bottom: 1px solid #d1d1d6; background: #fafafa;
font-size: 12px; color: #6e6e73; flex: none;
padding: 8px 14px; border-bottom: 1px solid var(--finder-border); background: var(--finder-bc-bg);
font-size: 12px; color: var(--finder-muted); flex: none;
}
.breadcrumb .bc-link {
background: none; border: none; padding: 0; color: #0066cc; cursor: pointer;
background: none; border: none; padding: 0; color: var(--finder-accent); cursor: pointer;
font-size: 12px; border-radius: 0; min-height: 0;
}
.breadcrumb .bc-link:hover { text-decoration: underline; filter: none; }
.breadcrumb .bc-sep { color: #aeaeb2; user-select: none; }
.breadcrumb .bc-current { color: #1d1d1f; font-weight: 600; }
.breadcrumb .bc-sep { color: var(--finder-subtle); user-select: none; }
.breadcrumb .bc-current { color: var(--finder-text); font-weight: 600; }
/* File list */
.file-list {
@@ -252,30 +317,30 @@ body.panel-shell-resizing { user-select: none; cursor: nwse-resize; }
}
.list-header .col-check input, .list-row .col-check input,
.list-row .col-check {
width: 14px; height: 14px; cursor: pointer; accent-color: #0066cc;
width: 14px; height: 14px; cursor: pointer; accent-color: var(--finder-accent);
}
.list-row .col-check { width: 28px; margin: 0; }
.list-header {
height: 28px; background: #ebebed; border-bottom: 1px solid #d1d1d6;
color: #6e6e73; font-size: 12px; user-select: none; flex: none;
height: 28px; background: var(--finder-bg-head); border-bottom: 1px solid var(--finder-border);
color: var(--finder-muted); font-size: 12px; user-select: none; flex: none;
}
.list-header .sortable {
cursor: pointer; display: inline-flex; align-items: center; gap: 4px;
}
.list-header .sortable:hover { color: #1d1d1f; }
.list-header .sortable.active { color: #1d1d1f; font-weight: 600; }
.list-header .sortable:hover { color: var(--finder-text); }
.list-header .sortable.active { color: var(--finder-text); font-weight: 600; }
.list-header .col-size, .list-row .col-size { text-align: right; font-variant-numeric: tabular-nums; }
.list-header .col-date, .list-row .col-date { color: #6e6e73; font-variant-numeric: tabular-nums; white-space: nowrap; }
.list-header .col-date, .list-row .col-date { color: var(--finder-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.list-body {
background: #fff; flex: 1; min-height: 0; overflow: auto; position: relative;
background: var(--finder-bg-body); flex: 1; min-height: 0; overflow: auto; position: relative;
}
.list-body.drop-target { background: #d6e8ff; outline: 1px dashed #0066cc; outline-offset: -2px; }
.list-body.drop-target { background: var(--finder-drop); outline: 1px dashed var(--finder-accent); outline-offset: -2px; }
.list-row {
min-height: 28px; border-bottom: 1px solid #ececf0; position: relative;
min-height: 28px; border-bottom: 1px solid var(--finder-row-border); position: relative;
}
.list-row:last-child { border-bottom: none; }
.list-row:hover { background: #e8f0fe; }
.list-row.selected { background: #cce4ff; }
.list-row:hover { background: var(--finder-hover); }
.list-row.selected { background: var(--finder-selected); }
.list-row.folder { cursor: default; }
.list-row[draggable="true"], .icon-item[draggable="true"] { cursor: grab; }
.list-row.dragging, .icon-item.dragging { opacity: 0.45; }
@@ -283,24 +348,24 @@ body.panel-shell-resizing { user-select: none; cursor: nwse-resize; }
display: flex; align-items: center; gap: 6px; min-width: 0;
}
.list-row .chevron {
width: 12px; flex: none; color: #86868b; font-size: 10px; text-align: center;
width: 12px; flex: none; color: var(--finder-subtle); font-size: 10px; text-align: center;
}
.list-row .chevron.hidden { visibility: hidden; }
/* Origin badge */
.origin-badge {
flex: none; font-size: 9px; padding: 1px 5px; border-radius: 999px;
background: #f2f2f7; border: 1px solid #d1d1d6; color: #6e6e73;
background: var(--finder-hover); border: 1px solid var(--finder-border); color: var(--finder-muted);
max-width: 72px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.origin-badge.self { background: #e8f0fe; border-color: #99caff; color: #0066cc; }
.origin-badge.self { background: var(--finder-hover); border-color: var(--finder-chip-active-border); color: var(--finder-accent); }
/* Inline rename */
.rename-input {
flex: 1; min-width: 0; padding: 1px 4px; border: 1px solid #0066cc; border-radius: 4px;
font-size: 13px; font-family: inherit; background: #fff; color: #1d1d1f;
flex: 1; min-width: 0; padding: 1px 4px; border: 1px solid var(--finder-accent); border-radius: 4px;
font-size: 13px; font-family: inherit; background: var(--finder-surface); color: var(--finder-text);
}
.rename-input:focus { outline: none; box-shadow: 0 0 0 2px rgba(0,102,204,.2); }
.rename-input:focus { outline: none; box-shadow: 0 0 0 2px var(--finder-focus-ring); }
.file-icon {
width: 16px; height: 16px; flex: none; background-size: contain; background-repeat: no-repeat;
@@ -325,51 +390,51 @@ body.panel-shell-resizing { user-select: none; cursor: nwse-resize; }
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect fill='%235b8cff' x='2' y='3' width='12' height='10' rx='1.5'/%3E%3Cpath fill='%23fff' d='M7 6.5v3l3-1.5z'/%3E%3C/svg%3E");
}
.list-row .name-link {
color: #1d1d1f; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
color: var(--finder-text); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.list-row .name-link.previewable:hover { color: #0066cc; text-decoration: underline; }
.list-row .name-link.previewable:hover { color: var(--finder-accent); text-decoration: underline; }
.list-row .name-text {
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #1d1d1f;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--finder-text);
}
.list-row .col-size { color: #6e6e73; }
.list-row .col-size { color: var(--finder-muted); }
.list-row .col-actions {
display: flex; justify-content: flex-end; gap: 4px; opacity: 0;
}
.list-row:hover .col-actions { opacity: 1; }
.list-row .col-actions button, .list-row .col-actions a {
background: transparent; border: none; color: #0066cc; padding: 2px 4px;
background: transparent; border: none; color: var(--finder-accent); padding: 2px 4px;
font-size: 12px; border-radius: 4px; min-height: 0;
}
.list-row .col-actions button:hover, .list-row .col-actions a:hover {
background: rgba(0,102,204,.1); filter: none;
background: var(--finder-accent-ghost); filter: none;
}
.list-row .col-actions button[data-del] { color: #ff3b30; }
.list-row .col-actions button[data-info] { color: #6e6e73; }
.list-row .col-actions button[data-del] { color: var(--finder-danger); }
.list-row .col-actions button[data-info] { color: var(--finder-muted); }
/* Empty + empty folder drop zone */
.empty {
padding: 40px 18px; text-align: center; color: #86868b; background: #fff; font-size: 13px;
padding: 40px 18px; text-align: center; color: var(--finder-subtle); background: var(--finder-bg-body); font-size: 13px;
}
.empty-drop-zone {
margin: 24px auto; max-width: 360px; padding: 32px 20px;
border: 2px dashed #c7c7cc; border-radius: 12px; background: #fafafa;
color: #6e6e73; transition: .15s;
border: 2px dashed var(--finder-input-border); border-radius: 12px; background: var(--finder-bc-bg);
color: var(--finder-muted); transition: .15s;
}
.empty-drop-zone.dragover {
border-color: #0066cc; background: #d6e8ff; color: #0066cc;
border-color: var(--finder-accent); background: var(--finder-drop); color: var(--finder-accent);
}
.empty-drop-zone .icon { font-size: 32px; margin-bottom: 8px; }
/* Load more */
.load-more-wrap { padding: 10px; text-align: center; background: #fff; border-top: 1px solid #ececf0; }
.load-more-wrap { padding: 10px; text-align: center; background: var(--finder-bg-body); border-top: 1px solid var(--finder-row-border); }
.load-more-btn {
background: #fff; border: 1px solid #c7c7cc; color: #0066cc; padding: 6px 16px;
background: var(--finder-surface); border: 1px solid var(--finder-input-border); color: var(--finder-accent); padding: 6px 16px;
border-radius: 6px; font-size: 12px; cursor: pointer;
}
.load-more-btn:hover { background: #e8f0fe; filter: none; }
.load-more-btn:hover { background: var(--finder-hover); filter: none; }
input[type=file] { display: none; }
.list-row.drop-target { background: #d6e8ff !important; }
.list-row.drop-target { background: var(--finder-drop) !important; }
/* Icon mode */
.file-list.icon-mode .list-header { display: none; }
@@ -383,16 +448,16 @@ input[type=file] { display: none; }
border: 1px solid transparent; min-width: 0; text-align: center;
}
.icon-item.folder { cursor: default; }
.icon-item:hover { background: #e8f0fe; border-color: #d1e3ff; }
.icon-item.selected { background: #cce4ff; border-color: #99caff; }
.icon-item.drop-target { background: #d6e8ff !important; border-color: #0066cc; }
.icon-item:hover { background: var(--finder-hover); border-color: var(--finder-chip-active-border); }
.icon-item.selected { background: var(--finder-selected); border-color: var(--finder-chip-active-border); }
.icon-item.drop-target { background: var(--finder-drop) !important; border-color: var(--finder-accent); }
.icon-item .item-check-wrap {
position: absolute; top: 4px; left: 4px; z-index: 2;
display: flex; align-items: center; justify-content: center;
width: 22px; height: 22px; cursor: pointer;
}
.icon-item .item-check {
width: 14px; height: 14px; accent-color: #0066cc; cursor: pointer; margin: 0;
width: 14px; height: 14px; accent-color: var(--finder-accent); cursor: pointer; margin: 0;
}
.icon-item .icon-thumb {
width: 56px; height: 56px; display: flex; align-items: center; justify-content: center; flex: none;
@@ -400,24 +465,24 @@ input[type=file] { display: none; }
.icon-item .icon-thumb .file-icon { width: 48px; height: 48px; }
.icon-item .icon-thumb-img {
width: 56px; height: 56px; object-fit: cover; border-radius: 6px;
border: 1px solid #ececf0; background: #fafafa;
border: 1px solid var(--finder-row-border); background: var(--finder-bc-bg);
}
.icon-item .icon-name {
width: 100%; font-size: 11px; line-height: 1.35; color: #1d1d1f;
width: 100%; font-size: 11px; line-height: 1.35; color: var(--finder-text);
overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
word-break: break-all;
}
.icon-item .icon-name a { color: inherit; text-decoration: none; }
.icon-item .icon-name a.previewable:hover { color: #0066cc; text-decoration: underline; }
.icon-item .icon-meta { font-size: 10px; color: #86868b; line-height: 1.2; }
.icon-item .icon-name a.previewable:hover { color: var(--finder-accent); text-decoration: underline; }
.icon-item .icon-meta { font-size: 10px; color: var(--finder-subtle); line-height: 1.2; }
.icon-item .icon-actions {
display: flex; gap: 6px; opacity: 0; font-size: 11px;
}
.icon-item:hover .icon-actions { opacity: 1; }
.icon-item .icon-actions a, .icon-item .icon-actions button {
background: none; border: none; color: #0066cc; padding: 0; font-size: 11px; min-height: 0;
background: none; border: none; color: var(--finder-accent); padding: 0; font-size: 11px; min-height: 0;
}
.icon-item .icon-actions button[data-del] { color: #ff3b30; }
.icon-item .icon-actions button[data-del] { color: var(--finder-danger); }
/* Modals (shared) */
.modal {
+101 -10
View File
@@ -1,6 +1,59 @@
(() => {
'use strict';
const THEME_STORAGE_KEY = 'memind-theme';
function getStoredTheme() {
const stored = localStorage.getItem(THEME_STORAGE_KEY);
return stored === 'dark' ? 'dark' : 'light';
}
function applyTheme(theme) {
const normalized = theme === 'dark' ? 'dark' : 'light';
document.documentElement.dataset.theme = normalized;
document.documentElement.style.colorScheme = normalized;
}
function updateThemeToggleLabel() {
const themeToggle = document.getElementById('themeToggle');
if (!themeToggle) return;
themeToggle.textContent = getStoredTheme() === 'light' ? '深色' : '浅色';
themeToggle.title = getStoredTheme() === 'light' ? '切换深色模式' : '切换浅色模式';
}
function toggleTheme() {
const next = getStoredTheme() === 'light' ? 'dark' : 'light';
localStorage.setItem(THEME_STORAGE_KEY, next);
applyTheme(next);
updateThemeToggleLabel();
window.dispatchEvent(new CustomEvent('memind-theme-change', { detail: { theme: next } }));
return next;
}
function initTheme() {
applyTheme(getStoredTheme());
updateThemeToggleLabel();
window.addEventListener('storage', (event) => {
if (event.key === THEME_STORAGE_KEY) {
applyTheme(getStoredTheme());
updateThemeToggleLabel();
}
});
window.addEventListener('memind-theme-change', (event) => {
const detail = event.detail;
if (detail?.theme) {
applyTheme(detail.theme);
updateThemeToggleLabel();
}
});
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
themeToggle.addEventListener('click', () => toggleTheme());
}
}
initTheme();
const API_BASE = '/api/mindspace/v1/oa/drive';
const fetchOpts = { credentials: 'same-origin' };
@@ -90,6 +143,7 @@
let pendingConflictResolve = null;
let uploadAbort = false;
let filesHydrated = false;
let currentUser = null;
let panelSize = (() => {
try {
@@ -172,6 +226,45 @@
statusEl.className = 'status ' + type;
}
function redirectToLogin() {
const returnTo = encodeURIComponent('/space/oa/drive');
const target = window.top && window.top !== window ? window.top : window;
target.location.href = `/?return_to=${returnTo}`;
}
async function ensureAuthenticated() {
const res = await fetch('/auth/status', fetchOpts);
if (!res.ok) {
redirectToLogin();
return null;
}
const data = await res.json();
if (!data.authenticated || !data.user?.id) {
redirectToLogin();
return null;
}
return data.user;
}
function renderUserBanner(user, quota) {
if (!linksEl || !user) return;
const name = user.displayName || user.username || user.slug || '用户';
const chips = [`<span class="link-chip active">${escapeHtml(name)} 的 OA 网盘</span>`];
if (quota) {
chips.push(`<span class="link-chip">剩余 ${fmtSize(quota.availableBytes)} / ${fmtSize(quota.quotaBytes)}</span>`);
}
linksEl.innerHTML = chips.join('');
}
async function driveFetch(url, opts = {}) {
const res = await fetch(url, { ...fetchOpts, ...opts });
if (res.status === 401 || res.status === 403) {
redirectToLogin();
throw new Error('未登录');
}
return res;
}
function viewUrl(name) {
return API_BASE + '/view/' + encodeURIComponent(name).replace(/%2F/g, '/');
}
@@ -734,23 +827,16 @@
/* ── API: load info ── */
async function loadInfo() {
const res = await fetch(API_BASE + '/info', fetchOpts);
const res = await driveFetch(API_BASE + '/info');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
serverInfo = await res.json();
displayLimit = serverInfo.pageSize || 100;
if (linksEl) {
const quota = serverInfo.quota;
if (quota) {
linksEl.innerHTML = `<span class="link-chip active">剩余 ${fmtSize(quota.availableBytes)} / ${fmtSize(quota.quotaBytes)}</span>`;
} else {
linksEl.innerHTML = '';
}
}
renderUserBanner(currentUser, serverInfo.quota);
}
async function refreshFiles() {
try {
const res = await fetch(API_BASE + '/files', fetchOpts);
const res = await driveFetch(API_BASE + '/files');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
applyFilePayload({ ...data, delta: false });
@@ -1754,6 +1840,11 @@
initPanelResize();
setViewMode(viewMode);
void (async () => {
currentUser = await ensureAuthenticated();
if (!currentUser) {
setStatus('未登录,正在跳转…', 'err');
return;
}
await loadInfo();
await refreshFiles();
connectEvents();
+1
View File
@@ -14,6 +14,7 @@
<span class="top-sub">云端同步 · 拖放/<kbd>⌘V</kbd>粘贴上传</span>
<div class="links" id="links"></div>
<div class="top-actions">
<button type="button" id="themeToggle" title="切换主题">深色</button>
<button type="button" class="primary" id="pickFiles">选择文件</button>
<button type="button" id="pickFolder">选择文件夹</button>
</div>