From 3e6b0c262a3e07e033251ee8d177761d0de083fc Mon Sep 17 00:00:00 2001 From: john Date: Thu, 10 Sep 2026 20:10:06 +0800 Subject: [PATCH] feat(portal): add light/dark theme sync and OA drive navigation 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 --- public/oa-drive/app.css | 233 +++++++++++++++++++++------------ public/oa-drive/app.js | 111 ++++++++++++++-- public/oa-drive/index.html | 1 + src/App.tsx | 2 + src/components/ChatView.tsx | 18 +++ src/components/ThemeToggle.tsx | 32 +++++ src/index.css | 198 +++++++++++++++++++++++----- src/main.tsx | 3 + src/routes/OaDriveRoute.tsx | 24 +++- src/utils/theme.ts | 26 ++++ 10 files changed, 520 insertions(+), 128 deletions(-) create mode 100644 src/components/ThemeToggle.tsx create mode 100644 src/utils/theme.ts diff --git a/public/oa-drive/app.css b/public/oa-drive/app.css index 45036c4..f2cc884 100644 --- a/public/oa-drive/app.css +++ b/public/oa-drive/app.css @@ -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 { diff --git a/public/oa-drive/app.js b/public/oa-drive/app.js index 73c7f1a..6cb3d94 100644 --- a/public/oa-drive/app.js +++ b/public/oa-drive/app.js @@ -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 = [`${escapeHtml(name)} 的 OA 网盘`]; + if (quota) { + chips.push(`剩余 ${fmtSize(quota.availableBytes)} / ${fmtSize(quota.quotaBytes)}`); + } + 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 = `剩余 ${fmtSize(quota.availableBytes)} / ${fmtSize(quota.quotaBytes)}`; - } 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(); diff --git a/public/oa-drive/index.html b/public/oa-drive/index.html index cb4c54d..ca6fad1 100644 --- a/public/oa-drive/index.html +++ b/public/oa-drive/index.html @@ -14,6 +14,7 @@ 云端同步 · 拖放/⌘V粘贴上传
+
diff --git a/src/App.tsx b/src/App.tsx index 463686b..0f25e4f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -91,6 +91,7 @@ function AuthenticatedApp({ } : undefined } + onOpenOaDrive={user ? () => navigate('/space/oa/drive') : undefined} onOpenPage={user ? (pageId) => navigate(`/space/page/${pageId}`) : undefined} onOpenAdmin={ user?.role === 'admin' @@ -121,6 +122,7 @@ function AuthenticatedApp({ element={} /> } /> + } /> } /> diff --git a/src/components/ChatView.tsx b/src/components/ChatView.tsx index b07796b..eb77de4 100644 --- a/src/components/ChatView.tsx +++ b/src/components/ChatView.tsx @@ -18,6 +18,7 @@ import { WechatBindPrompt } from './WechatBindPrompt'; import { WechatAccountButton } from './WechatAccountButton'; import { ChatHeaderMoreMenu } from './ChatHeaderMoreMenu'; import { NotificationCenter } from './NotificationCenter'; +import { ThemeToggle } from './ThemeToggle'; import { GoalRunAwaitingBanner } from './GoalRunAwaitingBanner'; import { HealthChannelBanner } from './HealthChannelBanner'; import { HealthAlertsPanel } from './HealthAlertsPanel'; @@ -173,6 +174,7 @@ export function ChatView({ onGrantedSkillsUpdate, onLogout, onOpenSpace, + onOpenOaDrive, onOpenPage, onOpenAdmin, onOpenFeedback, @@ -184,6 +186,7 @@ export function ChatView({ onGrantedSkillsUpdate?: (skills: string[]) => void; onLogout?: () => void; onOpenSpace?: (target?: { categoryCode?: MindSpaceSaveCategory; pageId?: string }) => void; + onOpenOaDrive?: () => void; onOpenPage?: (pageId: string) => void; onOpenAdmin?: () => void; onOpenFeedback?: () => void; @@ -327,6 +330,9 @@ export function ChatView({ : pendingSessionTitle; const moreMenuItems = [ + ...(onOpenOaDrive + ? [{ id: 'oa-drive', label: 'OA 网盘', onClick: () => onOpenOaDrive() }] + : []), ...(onOpenAdmin ? [{ id: 'admin', label: '管理', onClick: () => onOpenAdmin() }] : []), @@ -427,6 +433,16 @@ export function ChatView({ 管理 )} + {onOpenOaDrive && ( + + )} {onOpenSpace && ( )} + (() => getStoredTheme()); + + useEffect(() => { + const onChange = (event: Event) => { + const detail = (event as CustomEvent<{ theme: MemindTheme }>).detail; + if (detail?.theme) setTheme(detail.theme); + }; + window.addEventListener('memind-theme-change', onChange); + return () => window.removeEventListener('memind-theme-change', onChange); + }, []); + + return ( + + ); +} diff --git a/src/index.css b/src/index.css index ad02a7b..3135eab 100644 --- a/src/index.css +++ b/src/index.css @@ -1,7 +1,54 @@ -:root { - color-scheme: dark; +:root, +[data-theme='light'] { + color-scheme: light; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --color-bg-base: #f4f1ea; + --color-bg-surface: #fffaf0; + --color-bg-elevated: #ffffff; + --color-bg-active: #e8f0ea; + --color-bg-code: #f0ebe0; + --color-bg-warning: #fff6e5; + --color-bg-error: #fff0f0; + --color-bg-info: #eef6ff; + + --color-text-primary: #18211d; + --color-text-secondary: #3d4a44; + --color-text-muted: #52605a; + --color-text-faint: #6b7872; + --color-text-meta: #7a8780; + --color-text-link: #2f6f57; + --color-text-error: #b42318; + --color-text-info: #2458a6; + + --color-border: rgba(24, 33, 29, 0.12); + --color-border-input: rgba(24, 33, 29, 0.18); + --color-border-warning: rgba(238, 176, 78, 0.45); + --color-border-input-focus: #2f6f57; + + --color-accent: #2f6f57; + --color-accent-user: #2458a6; + --color-danger: #c4453d; + + --radius-sm: 8px; + --radius-xs: 4px; + --radius-md: 10px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-pill: 999px; + + --space-xs: 4px; + --space-sm: 8px; + --space-md: 12px; + --space-lg: 16px; + + background: var(--color-bg-base); + color: var(--color-text-primary); +} + +[data-theme='dark'] { + color-scheme: dark; + --color-bg-base: #0f1419; --color-bg-surface: #121820; --color-bg-elevated: #1c2735; @@ -28,21 +75,6 @@ --color-accent: #3d8bfd; --color-accent-user: #2458a6; --color-danger: #c4453d; - - --radius-sm: 8px; - --radius-xs: 4px; - --radius-md: 10px; - --radius-lg: 12px; - --radius-xl: 16px; - --radius-pill: 999px; - - --space-xs: 4px; - --space-sm: 8px; - --space-md: 12px; - --space-lg: 16px; - - background: var(--color-bg-base); - color: var(--color-text-primary); } * { @@ -76,11 +108,9 @@ body, position: relative; overflow: hidden; background: - radial-gradient(circle at 50% 22%, rgba(86, 128, 255, 0.12), transparent 18%), - radial-gradient(circle at 50% 56%, rgba(107, 138, 209, 0.08), transparent 34%), - radial-gradient(circle at 12% 92%, rgba(67, 107, 180, 0.08), transparent 28%), - radial-gradient(circle at 88% 8%, rgba(160, 97, 255, 0.05), transparent 22%), - linear-gradient(180deg, #08101d 0%, #0a1220 44%, #0c1320 100%); + radial-gradient(circle at 50% 22%, rgba(47, 111, 87, 0.08), transparent 18%), + radial-gradient(circle at 50% 56%, rgba(238, 176, 78, 0.06), transparent 34%), + linear-gradient(180deg, #fffaf0 0%, #f4f1ea 44%, #ece6d9 100%); } .app-home::before { @@ -88,6 +118,21 @@ body, position: absolute; inset: 0; pointer-events: none; + background: + linear-gradient(90deg, rgba(24, 33, 29, 0.02), transparent 18%, transparent 82%, rgba(24, 33, 29, 0.02)), + radial-gradient(circle at center, transparent 0%, rgba(244, 241, 234, 0.18) 72%, rgba(244, 241, 234, 0.42) 100%); +} + +[data-theme='dark'] .app-home { + background: + radial-gradient(circle at 50% 22%, rgba(86, 128, 255, 0.12), transparent 18%), + radial-gradient(circle at 50% 56%, rgba(107, 138, 209, 0.08), transparent 34%), + radial-gradient(circle at 12% 92%, rgba(67, 107, 180, 0.08), transparent 28%), + radial-gradient(circle at 88% 8%, rgba(160, 97, 255, 0.05), transparent 22%), + linear-gradient(180deg, #08101d 0%, #0a1220 44%, #0c1320 100%); +} + +[data-theme='dark'] .app-home::before { background: linear-gradient(90deg, rgba(255, 255, 255, 0.015), transparent 18%, transparent 82%, rgba(255, 255, 255, 0.015)), radial-gradient(circle at center, transparent 0%, rgba(2, 8, 18, 0.18) 72%, rgba(2, 8, 18, 0.42) 100%); @@ -897,16 +942,32 @@ body, .header-home .ghost-btn, .header-home .header-icon-btn, .header-home .header-menu-btn { + background: rgba(24, 33, 29, 0.04); + color: rgba(24, 33, 29, 0.82); + border: 1px solid rgba(24, 33, 29, 0.12); +} + +.header-home .header-title { + color: rgba(24, 33, 29, 0.96); +} + +.header-home .header-sub { + color: rgba(24, 33, 29, 0.62); +} + +[data-theme='dark'] .header-home .ghost-btn, +[data-theme='dark'] .header-home .header-icon-btn, +[data-theme='dark'] .header-home .header-menu-btn { background: rgba(255, 255, 255, 0.04); color: rgba(220, 231, 248, 0.82); border: 1px solid rgba(103, 123, 156, 0.22); } -.header-home .header-title { +[data-theme='dark'] .header-home .header-title { color: rgba(238, 243, 255, 0.96); } -.header-home .header-sub { +[data-theme='dark'] .header-home .header-sub { color: rgba(180, 193, 219, 0.74); } @@ -3188,8 +3249,18 @@ body, } .chat-input-shell-home { - border: 1px solid rgba(96, 118, 153, 0.38); + border: 1px solid rgba(24, 33, 29, 0.14); border-radius: 18px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 250, 240, 0.96)), + rgba(255, 255, 255, 0.94); + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.6), + 0 16px 36px rgba(24, 33, 29, 0.08); +} + +[data-theme='dark'] .chat-input-shell-home { + border: 1px solid rgba(96, 118, 153, 0.38); background: linear-gradient(180deg, rgba(18, 27, 42, 0.94), rgba(13, 20, 32, 0.96)), rgba(10, 18, 30, 0.94); @@ -3216,12 +3287,20 @@ body, padding-top: 14px; padding-bottom: 14px; font-size: 14px; - color: #edf4ff; + color: var(--color-text-primary); background: transparent; border-color: transparent; } .chat-input-row-home textarea.input.chat-input-field::placeholder { + color: var(--color-text-muted); +} + +[data-theme='dark'] .chat-input-row-home textarea.input.chat-input-field { + color: #edf4ff; +} + +[data-theme='dark'] .chat-input-row-home textarea.input.chat-input-field::placeholder { color: rgba(175, 191, 216, 0.72); } @@ -3367,10 +3446,18 @@ body, } .chat-input-row-home .chat-input-task-status-text { - color: rgba(175, 191, 216, 0.72); + color: var(--color-text-muted); } .chat-input-row-home .chat-input-task-status-dots span { + background: var(--color-text-muted); +} + +[data-theme='dark'] .chat-input-row-home .chat-input-task-status-text { + color: rgba(175, 191, 216, 0.72); +} + +[data-theme='dark'] .chat-input-row-home .chat-input-task-status-dots span { background: rgba(175, 191, 216, 0.72); } @@ -3766,6 +3853,13 @@ body, .chat-input-row-home .chat-image-upload-trigger, .chat-input-row-home .voice-btn { + border-color: var(--color-border-input); + background: var(--color-bg-elevated); + color: var(--color-text-secondary); +} + +[data-theme='dark'] .chat-input-row-home .chat-image-upload-trigger, +[data-theme='dark'] .chat-input-row-home .voice-btn { border-color: rgba(110, 132, 168, 0.3); background: rgba(170, 192, 228, 0.08); color: #d7e4fb; @@ -12910,19 +13004,61 @@ body, display: flex; flex-direction: column; height: 100dvh; - background: #f4f1ea; + background: var(--color-bg-base); + color: var(--color-text-primary); } .oa-drive-toolbar { flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; padding: 10px 16px; - border-bottom: 1px solid rgba(24, 33, 29, 0.08); - background: #fffaf0; + border-bottom: 1px solid var(--color-border); + background: var(--color-bg-surface); +} + +.oa-drive-toolbar-start { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} + +.oa-drive-toolbar-secondary { + opacity: 0.88; +} + +.oa-drive-toolbar-end { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.oa-drive-toolbar-label { + color: var(--color-text-muted); + font-size: 13px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .oa-drive-frame { flex: 1 1 auto; width: 100%; border: 0; - background: #fff; + background: var(--color-bg-base); +} + +.theme-toggle-btn { + min-width: 44px; + padding-inline: 12px; +} + +.theme-toggle-btn-mobile { + min-width: 0; + padding: 8px 10px; + font-size: 12px; } diff --git a/src/main.tsx b/src/main.tsx index 8f6b53a..2b166bd 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,8 +2,11 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import { App } from './App'; +import { initTheme } from './utils/theme'; import './index.css'; +initTheme(); + createRoot(document.getElementById('root')!).render( diff --git a/src/routes/OaDriveRoute.tsx b/src/routes/OaDriveRoute.tsx index c2eb6db..e96d6eb 100644 --- a/src/routes/OaDriveRoute.tsx +++ b/src/routes/OaDriveRoute.tsx @@ -1,6 +1,11 @@ import { Navigate, useNavigate } from 'react-router-dom'; +import { ThemeToggle } from '../components/ThemeToggle'; import type { PortalUser } from '../types'; +function resolveUserLabel(user: PortalUser) { + return user.displayName?.trim() || user.username?.trim() || user.slug?.trim() || '用户'; +} + export function OaDriveRoute({ user, }: { @@ -15,9 +20,22 @@ export function OaDriveRoute({ return (
- +
+ + +
+
+ {resolveUserLabel(user)} 的 OA 网盘 + +