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
+
+