From ba8349b04c13662bc18f771843d1812ce669c3e1 Mon Sep 17 00:00:00 2001 From: anghunk Date: Tue, 27 Jan 2026 11:07:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E6=B7=BB=E5=8A=A0=E6=B7=B1?= =?UTF-8?q?=E8=89=B2=E4=B8=BB=E9=A2=98=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增主题切换功能,支持浅色、深色和跟随系统三种模式 - 创建主题管理组合式函数 useTheme 和 CSS 变量定义 - 在所有视图组件中应用 CSS 变量以实现主题适配 - 在布局头部添加主题切换按钮,支持循环切换主题 - 更新 .gitignore 添加 .trae 文件忽略 --- .gitignore | 1 + cwd-admin/src/App.vue | 14 ++- cwd-admin/src/assets/theme.css | 63 +++++++++++ cwd-admin/src/composables/useTheme.ts | 57 ++++++++++ cwd-admin/src/main.ts | 1 + cwd-admin/src/views/AnalyticsVisitView.vue | 62 ++++++----- cwd-admin/src/views/CommentsView.vue | 124 +++++++++++---------- cwd-admin/src/views/DataView.vue | 64 +++++------ cwd-admin/src/views/LayoutView.vue | 89 ++++++++++----- cwd-admin/src/views/LoginView.vue | 30 ++--- cwd-admin/src/views/SettingsView.vue | 65 +++++------ cwd-admin/src/views/StatsView.vue | 61 +++++----- 12 files changed, 411 insertions(+), 220 deletions(-) create mode 100644 cwd-admin/src/assets/theme.css create mode 100644 cwd-admin/src/composables/useTheme.ts diff --git a/.gitignore b/.gitignore index 766621b..418b24f 100644 --- a/.gitignore +++ b/.gitignore @@ -172,3 +172,4 @@ package-lock.json wrangler.jsonc .claude +.trae \ No newline at end of file diff --git a/cwd-admin/src/App.vue b/cwd-admin/src/App.vue index 08cde1c..1ab9f8b 100644 --- a/cwd-admin/src/App.vue +++ b/cwd-admin/src/App.vue @@ -4,7 +4,16 @@ - + diff --git a/cwd-admin/src/assets/theme.css b/cwd-admin/src/assets/theme.css new file mode 100644 index 0000000..4d946a0 --- /dev/null +++ b/cwd-admin/src/assets/theme.css @@ -0,0 +1,63 @@ +:root { + /* Light Theme (Default) */ + --bg-body: #f5f5f5; + --bg-card: #ffffff; + --bg-header: #24292f; + --bg-sider: #f6f8fa; + --bg-input: #ffffff; + --bg-hover: #eaeef2; + --bg-active: #d0ebff; + --bg-select: #24292f; /* Header select bg */ + + --text-primary: #24292f; + --text-secondary: #57606a; + --text-inverse: #ffffff; + --text-link: #0969da; + + --border-color: #d0d7de; + --border-input: #d0d7de; + --border-header: #57606a; + + --color-success: #1a7f37; + --color-warning: #9a6700; + --color-danger: #d1242f; + + --chart-line: #0ea5e9; + --chart-area-start: rgba(56, 189, 248, 0.80); + --chart-area-end: rgba(56, 189, 248, 0.2); + + --shadow-card: 0 1px 3px rgba(0,0,0,0.1); + --shadow-login: 0 18px 45px rgba(15, 23, 42, 0.12); +} + +:root.dark { + /* Dark Theme */ + --bg-body: #0d1117; + --bg-card: #161b22; + --bg-header: #161b22; + --bg-sider: #010409; + --bg-input: #0d1117; + --bg-hover: #21262d; + --bg-active: #1f6feb33; /* Semi-transparent blue */ + --bg-select: #161b22; + + --text-primary: #c9d1d9; + --text-secondary: #8b949e; + --text-inverse: #f0f6fc; + --text-link: #58a6ff; + + --border-color: #30363d; + --border-input: #30363d; + --border-header: #30363d; + + --color-success: #3fb950; + --color-warning: #d29922; + --color-danger: #f85149; + + --chart-line: #38bdf8; + --chart-area-start: rgba(56, 189, 248, 0.50); + --chart-area-end: rgba(56, 189, 248, 0.1); + + --shadow-card: 0 1px 3px rgba(0,0,0,0.5); + --shadow-login: 0 18px 45px rgba(0, 0, 0, 0.4); +} diff --git a/cwd-admin/src/composables/useTheme.ts b/cwd-admin/src/composables/useTheme.ts new file mode 100644 index 0000000..608d3a3 --- /dev/null +++ b/cwd-admin/src/composables/useTheme.ts @@ -0,0 +1,57 @@ +import { ref } from 'vue'; + +export type Theme = 'light' | 'dark' | 'system'; + +const STORAGE_KEY = 'cwd_admin_theme'; +const theme = ref('system'); + +export function useTheme() { + function applyTheme() { + if (typeof window === 'undefined') return; + + const root = document.documentElement; + const isDark = + theme.value === 'dark' || + (theme.value === 'system' && + window.matchMedia('(prefers-color-scheme: dark)').matches); + + if (isDark) { + root.classList.add('dark'); + } else { + root.classList.remove('dark'); + } + } + + function setTheme(newTheme: Theme) { + theme.value = newTheme; + localStorage.setItem(STORAGE_KEY, newTheme); + applyTheme(); + } + + function initTheme() { + if (typeof window === 'undefined') return; + + const stored = localStorage.getItem(STORAGE_KEY) as Theme | null; + if (stored && ['light', 'dark', 'system'].includes(stored)) { + theme.value = stored; + } else { + theme.value = 'system'; + } + applyTheme(); + + // Listen for system changes + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + // Remove existing listener to avoid duplicates if init is called multiple times (though it shouldn't be) + mediaQuery.onchange = () => { + if (theme.value === 'system') { + applyTheme(); + } + }; + } + + return { + theme, + setTheme, + initTheme + }; +} diff --git a/cwd-admin/src/main.ts b/cwd-admin/src/main.ts index 69181f8..f68f42b 100644 --- a/cwd-admin/src/main.ts +++ b/cwd-admin/src/main.ts @@ -1,4 +1,5 @@ import { createApp } from 'vue'; +import './assets/theme.css'; import App from './App.vue'; import { router } from './router'; diff --git a/cwd-admin/src/views/AnalyticsVisitView.vue b/cwd-admin/src/views/AnalyticsVisitView.vue index b682fe2..e5a3fe4 100644 --- a/cwd-admin/src/views/AnalyticsVisitView.vue +++ b/cwd-admin/src/views/AnalyticsVisitView.vue @@ -489,7 +489,7 @@ watch(domainFilter, () => { .page-title { margin: 0; font-size: 18px; - color: #24292f; + color: var(--text-primary); } .toolbar { @@ -509,21 +509,23 @@ watch(domainFilter, () => { padding: 8px 8px; box-sizing: border-box; font-size: 13px; - border: 1px solid #d0d7de; + border: 1px solid var(--border-color); border-radius: 4px; - background-color: #ffffff; + background-color: var(--bg-input); + color: var(--text-primary); } .card { - background-color: #ffffff; + background-color: var(--bg-card); border-radius: 6px; - border: 1px solid #d0d7de; + border: 1px solid var(--border-color); padding: 16px 18px; } .card-title { margin: 0 0 12px; font-size: 16px; + color: var(--text-primary); } .card-title-row { @@ -536,12 +538,12 @@ watch(domainFilter, () => { .page-hint { font-size: 14px; - color: #57606a; + color: var(--text-secondary); } .page-error { font-size: 14px; - color: #d1242f; + color: var(--color-danger); } .stats-grid { @@ -553,20 +555,20 @@ watch(domainFilter, () => { .stats-item { padding: 10px 12px; border-radius: 6px; - background-color: #f6f8fa; - border: 1px solid #d0d7de; + background-color: var(--bg-sider); + border: 1px solid var(--border-color); } .stats-label { font-size: 14px; - color: #57606a; + color: var(--text-secondary); margin-bottom: 4px; } .stats-value { font-size: 26px; font-weight: 600; - color: #24292f; + color: var(--text-primary); } .domain-table-wrapper { @@ -576,7 +578,7 @@ watch(domainFilter, () => { } .domain-table { - border: 1px solid #d0d7de; + border: 1px solid var(--border-color); border-radius: 6px; min-width: 520px; overflow: hidden; @@ -584,24 +586,30 @@ watch(domainFilter, () => { .domain-table-header { display: flex; - background-color: #f6f8fa; + background-color: var(--bg-sider); + color: var(--text-secondary); } .domain-table-row { display: flex; - border-top: 1px solid #eaeae0; + border-top: 1px solid var(--border-color); } .domain-cell { flex: 1; padding: 10px 10px; font-size: 14px; - color: #24292f; + color: var(--text-primary); box-sizing: border-box; } .domain-cell a { - color: #57606a; + color: var(--text-link); + text-decoration: none; +} + +.domain-cell a:hover { + text-decoration: underline; } .domain-cell-title { @@ -688,24 +696,24 @@ watch(domainFilter, () => { padding: 10px 14px; border-radius: 6px; font-size: 13px; - box-shadow: 0 8px 24px rgba(140, 149, 159, 0.2); + box-shadow: var(--shadow-card); z-index: 1000; } .toast-success { - background-color: #1a7f37; - color: #ffffff; + background-color: var(--color-success); + color: var(--text-inverse); } .toast-error { - background-color: #d1242f; - color: #ffffff; + background-color: var(--color-danger); + color: var(--text-inverse); } .visit-tabs { display: inline-flex; border-radius: 999px; - border: 1px solid #d0d7de; + border: 1px solid var(--border-color); overflow: hidden; } @@ -713,18 +721,18 @@ watch(domainFilter, () => { padding: 4px 10px; font-size: 12px; border: none; - background-color: #ffffff; - color: #57606a; + background-color: var(--bg-card); + color: var(--text-secondary); cursor: pointer; } .visit-tab + .visit-tab { - border-left: 1px solid #d0d7de; + border-left: 1px solid var(--border-color); } .visit-tab-active { - background-color: #0969da; - color: #ffffff; + background-color: var(--primary-color); + color: var(--text-inverse); } @media (max-width: 768px) { diff --git a/cwd-admin/src/views/CommentsView.vue b/cwd-admin/src/views/CommentsView.vue index 4e19b3a..8fab2d8 100644 --- a/cwd-admin/src/views/CommentsView.vue +++ b/cwd-admin/src/views/CommentsView.vue @@ -577,7 +577,7 @@ watch(domainFilter, () => { .page-title { margin: 0; font-size: 18px; - color: #24292f; + color: var(--text-primary); } .toolbar { @@ -602,16 +602,18 @@ watch(domainFilter, () => { padding: 8px 8px; box-sizing: border-box; font-size: 13px; - border: 1px solid #d0d7de; + border: 1px solid var(--border-color); border-radius: 4px; - background-color: #ffffff; + background-color: var(--bg-input); + color: var(--text-primary); } .toolbar-button { padding: 6px 10px; border-radius: 4px; - border: 1px solid #d0d7de; - background-color: #f6f8fa; + border: 1px solid var(--border-color); + background-color: var(--bg-sider); + color: var(--text-primary); cursor: pointer; font-size: 14px; min-width: 70px; @@ -619,17 +621,17 @@ watch(domainFilter, () => { .page-hint { font-size: 14px; - color: #57606a; + color: var(--text-secondary); } .page-error { font-size: 14px; - color: #d1242f; + color: var(--color-danger); } .comment-table { - background-color: #ffffff; - border: 1px solid #d0d7de; + background-color: var(--bg-card); + border: 1px solid var(--border-color); border-radius: 6px; overflow: hidden; overflow-x: auto; @@ -637,12 +639,12 @@ watch(domainFilter, () => { .table-header { display: flex; - background-color: #f6f8fa; + background-color: var(--bg-sider); } .table-row { display: flex; - /* border-bottom: 1px solid #eaeae0; */ + /* border-bottom: 1px solid var(--border-color); */ } .table-row:last-child { @@ -650,18 +652,18 @@ watch(domainFilter, () => { } .table-row:hover .table-cell { - background-color: #f8f9fa; + background-color: var(--bg-hover); } .table-cell { padding: 10px 12px; font-size: 13px; - color: #24292f; + color: var(--text-primary); display: flex; align-items: center; box-sizing: border-box; overflow: hidden; - border-bottom: 1px solid #eaeae0; + border-bottom: 1px solid var(--border-color); } .table-cell-id { @@ -715,15 +717,15 @@ watch(domainFilter, () => { .table-header .table-cell { font-weight: 500; - color: #57606a; + color: var(--text-secondary); align-items: center; - background-color: #f6f8fa; - border-bottom: 1px solid #d0d7de; + background-color: var(--bg-sider); + border-bottom: 1px solid var(--border-color); } .cell-id { font-size: 12px; - color: #57606a; + color: var(--text-secondary); } .cell-author-name { @@ -734,7 +736,7 @@ watch(domainFilter, () => { .cell-author-email { font-size: 12px; - color: #57606a; + color: var(--text-secondary); word-break: break-all; margin-bottom: 2px; } @@ -757,7 +759,7 @@ watch(domainFilter, () => { .cell-path { font-size: 13px; - color: #2774cb; + color: var(--text-link); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -770,7 +772,7 @@ watch(domainFilter, () => { .cell-time { font-size: 12px; - color: #57606a; + color: var(--text-secondary); } .cell-author-wrapper { @@ -805,23 +807,23 @@ watch(domainFilter, () => { } .cell-status-approved { - color: #1a7f37; - background-color: #e7f5eb; + color: var(--color-success); + background-color: rgba(26, 127, 55, 0.1); /* Simplified bg based on color */ } .cell-status-pending { - color: #9a6700; - background-color: #fff8c5; + color: var(--color-warning); + background-color: rgba(154, 103, 0, 0.1); } .cell-status-rejected { - color: #d1242f; - background-color: #ffebe9; + color: var(--color-danger); + background-color: rgba(209, 36, 47, 0.1); } .cell-likes-number { - color: #d1242f; - background-color: #ffebe9; + color: var(--color-danger); + background-color: rgba(209, 36, 47, 0.1); display: inline-flex; align-items: center; justify-content: center; @@ -829,14 +831,16 @@ watch(domainFilter, () => { } .cell-pin-flag { - color: #825802; + color: #825802; /* Maybe var(--color-warning)? */ background-color: #f7c848; } .status-select { padding: 3px 8px; border-radius: 4px; - border: 1px solid #d0d7de; + border: 1px solid var(--border-color); + background-color: var(--bg-input); + color: var(--text-primary); font-size: 13px; cursor: pointer; } @@ -850,14 +854,15 @@ watch(domainFilter, () => { .table-action { padding: 3px 8px; border-radius: 4px; - border: 1px solid #d0d7de; - background-color: #ffffff; + border: 1px solid var(--border-color); + background-color: var(--bg-input); + color: var(--text-primary); font-size: 13px; cursor: pointer; } .table-action:hover { - background-color: #f6f8fa; + background-color: var(--bg-hover); } .table-action:disabled { @@ -866,18 +871,18 @@ watch(domainFilter, () => { } .table-action-danger { - border-color: #d1242f; - color: #d1242f; + border-color: var(--color-danger); + color: var(--color-danger); } .table-action-danger:hover { - background-color: #ffebe9; + background-color: rgba(209, 36, 47, 0.1); } .table-empty { padding: 32px; text-align: center; - color: #57606a; + color: var(--text-secondary); font-size: 13px; } @@ -895,8 +900,9 @@ watch(domainFilter, () => { min-width: 28px; padding: 4px 8px; border-radius: 4px; - border: 1px solid #d0d7de; - background-color: #f6f8fa; + border: 1px solid var(--border-color); + background-color: var(--bg-sider); + color: var(--text-primary); font-size: 12px; cursor: pointer; box-sizing: border-box; @@ -908,15 +914,15 @@ watch(domainFilter, () => { } .pagination-button-active { - background-color: #0969da; - color: #ffffff; - border-color: #0969da; + background-color: var(--primary-color); + color: var(--text-inverse); + border-color: var(--primary-color); } .pagination-ellipsis { padding: 0 2px; font-size: 12px; - color: #57606a; + color: var(--text-secondary); } .pagination-jump { @@ -924,7 +930,7 @@ watch(domainFilter, () => { align-items: center; gap: 4px; font-size: 12px; - color: #57606a; + color: var(--text-secondary); } .pagination-input { @@ -933,7 +939,9 @@ watch(domainFilter, () => { box-sizing: border-box; padding: 2px 4px; border-radius: 4px; - border: 1px solid #d0d7de; + border: 1px solid var(--border-color); + background-color: var(--bg-input); + color: var(--text-primary); font-size: 12px; } @@ -959,9 +967,9 @@ watch(domainFilter, () => { } .modal { - background-color: #ffffff; + background-color: var(--bg-card); border-radius: 0; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); + box-shadow: var(--shadow-card); width: 100%; max-width: 600px; padding: 20px 20px 24px; @@ -977,7 +985,7 @@ watch(domainFilter, () => { margin: 0; font-size: 16px; font-weight: 600; - color: #24292f; + color: var(--text-primary); } .modal-body { @@ -1002,14 +1010,14 @@ watch(domainFilter, () => { } .modal-btn.primary { - background-color: #0969da; - color: #ffffff; + background-color: var(--primary-color); + color: var(--text-inverse); } .modal-btn.secondary { - background-color: #f6f8fa; - border-color: #d0d7de; - color: #24292f; + background-color: var(--bg-sider); + border-color: var(--border-color); + color: var(--text-primary); } .modal-btn:hover { @@ -1024,19 +1032,21 @@ watch(domainFilter, () => { .form-label { font-size: 13px; - color: #57606a; + color: var(--text-secondary); } .form-input { padding: 8px 10px; border-radius: 4px; - border: 1px solid #d0d7de; + border: 1px solid var(--border-color); + background-color: var(--bg-input); + color: var(--text-primary); font-size: 13px; outline: none; } .form-input:focus { - border-color: #0969da; + border-color: var(--primary-color); box-shadow: 0 0 0 1px rgba(9, 105, 218, 0.2); } diff --git a/cwd-admin/src/views/DataView.vue b/cwd-admin/src/views/DataView.vue index a4c256f..9a9babe 100644 --- a/cwd-admin/src/views/DataView.vue +++ b/cwd-admin/src/views/DataView.vue @@ -409,13 +409,13 @@ async function executeImport(comments: any[]) { .page-title { margin: 0; font-size: 18px; - color: #24292f; + color: var(--text-primary); } .card { - background-color: #ffffff; + background-color: var(--bg-card); border-radius: 6px; - border: 1px solid #d0d7de; + border: 1px solid var(--border-color); padding: 16px 18px; margin-bottom: 1em; } @@ -423,6 +423,7 @@ async function executeImport(comments: any[]) { .card-title { margin: 0 0 12px; font-size: 15px; + color: var(--text-primary); } .stats-grid { @@ -434,55 +435,56 @@ async function executeImport(comments: any[]) { .stats-item { padding: 10px 12px; border-radius: 6px; - background-color: #f6f8fa; - border: 1px solid #d0d7de; + background-color: var(--bg-sider); + border: 1px solid var(--border-color); } .stats-label { font-size: 12px; - color: #57606a; + color: var(--text-secondary); margin-bottom: 4px; } .stats-value { font-size: 18px; font-weight: 600; - color: #24292f; + color: var(--text-primary); } .stats-value-approved { - color: #1a7f37; + color: var(--color-success); } .stats-value-pending { - color: #9a6700; + color: var(--color-warning); } .stats-value-rejected { - color: #d1242f; + color: var(--color-danger); } .domain-table { - border: 1px solid #d0d7de; + border: 1px solid var(--border-color); border-radius: 6px; overflow: hidden; } .domain-table-header { display: flex; - background-color: #f6f8fa; + background-color: var(--bg-sider); + color: var(--text-secondary); } .domain-table-row { display: flex; - border-top: 1px solid #eaeae0; + border-top: 1px solid var(--border-color); } .domain-cell { flex: 1; padding: 8px 10px; font-size: 13px; - color: #24292f; + color: var(--text-primary); box-sizing: border-box; } @@ -493,7 +495,7 @@ async function executeImport(comments: any[]) { .card-desc { font-size: 14px; - color: #57606a; + color: var(--text-secondary); margin: 0 0 16px; } @@ -506,8 +508,8 @@ async function executeImport(comments: any[]) { padding: 8px 14px; border-radius: 4px; border: none; - background-color: #0969da; - color: #ffffff; + background-color: var(--primary-color); + color: var(--text-inverse); font-size: 14px; cursor: pointer; min-width: 100px; @@ -527,22 +529,22 @@ async function executeImport(comments: any[]) { .form-label { font-size: 14px; - color: #24292f; + color: var(--text-primary); font-weight: 500; } .form-select { padding: 6px 8px; - border: 1px solid #d0d7de; + border: 1px solid var(--border-color); border-radius: 4px; font-size: 14px; - color: #24292f; - background-color: #f6f8fa; + color: var(--text-primary); + background-color: var(--bg-input); outline: none; } .form-select:focus { - border-color: #0969da; + border-color: var(--primary-color); box-shadow: 0 0 0 2px rgba(9, 105, 218, 0.3); } @@ -556,32 +558,32 @@ async function executeImport(comments: any[]) { padding: 10px 14px; border-radius: 6px; font-size: 13px; - box-shadow: 0 8px 24px rgba(140, 149, 159, 0.2); + box-shadow: var(--shadow-card); z-index: 1000; } .toast-success { - background-color: #1a7f37; - color: #ffffff; + background-color: var(--color-success); + color: var(--text-inverse); } .toast-error { - background-color: #d1242f; - color: #ffffff; + background-color: var(--color-danger); + color: var(--text-inverse); } .log-container { margin-top: 16px; padding: 12px; - background-color: #f6f8fa; + background-color: var(--bg-sider); border-radius: 6px; - border: 1px solid #d0d7de; + border: 1px solid var(--border-color); } .log-title { font-size: 13px; font-weight: 600; - color: #24292f; + color: var(--text-primary); margin-bottom: 8px; } @@ -597,7 +599,7 @@ async function executeImport(comments: any[]) { font-size: 12px; font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; - color: #57606a; + color: var(--text-secondary); line-height: 1.5; } diff --git a/cwd-admin/src/views/LayoutView.vue b/cwd-admin/src/views/LayoutView.vue index f5cb116..66c6a87 100644 --- a/cwd-admin/src/views/LayoutView.vue +++ b/cwd-admin/src/views/LayoutView.vue @@ -31,8 +31,20 @@ Github + +