diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..e4fba218 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/frontend/src/App.vue b/frontend/src/App.vue index b7cd71ca..05b8fa9e 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -18,8 +18,9 @@ font-family: 'JetBrains Mono', 'Space Grotesk', 'Noto Sans SC', monospace; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - color: #000000; - background-color: #ffffff; + color: var(--text-primary); + background-color: var(--bg-primary); + transition: background-color 0.2s, color 0.2s; } /* 滚动条样式 */ diff --git a/frontend/src/assets/theme.css b/frontend/src/assets/theme.css new file mode 100644 index 00000000..26803592 --- /dev/null +++ b/frontend/src/assets/theme.css @@ -0,0 +1,59 @@ +/* ===== Light mode (default) ===== */ +:root { + --bg-primary: #ffffff; + --bg-secondary: #f5f5f5; + --bg-surface: #fafafa; + --bg-input: #fafafa; + --bg-hover: #f0f0f0; + + --text-primary: #000000; + --text-secondary: #333333; + --text-muted: #666666; + --text-faint: #999999; + + --border: #e0e0e0; + --border-light: #eeeeee; + --border-medium: #cccccc; + + --accent: #ff6b35; + --status-success: #1a936f; + --status-error: #c5283d; +} + +/* ===== Dark mode ===== */ +html.dark { + --bg-primary: #111111; + --bg-secondary: #1a1a1a; + --bg-surface: #1e1e1e; + --bg-input: #1a1a1a; + --bg-hover: #2a2a2a; + + --text-primary: #e5e5e5; + --text-secondary: #cccccc; + --text-muted: #a0a0a0; + --text-faint: #555555; + + --border: #2e2e2e; + --border-light: #222222; + --border-medium: #3a3a3a; + + --accent: #ff6b35; + --status-success: #1a936f; + --status-error: #c5283d; +} + +/* ===== Global dark mode overrides ===== */ +html.dark body { + background: var(--bg-primary); + color: var(--text-primary); +} + +html.dark ::-webkit-scrollbar-track { + background: #1a1a1a; +} +html.dark ::-webkit-scrollbar-thumb { + background: #444; +} +html.dark ::-webkit-scrollbar-thumb:hover { + background: #666; +} diff --git a/frontend/src/components/GraphPanel.vue b/frontend/src/components/GraphPanel.vue index db188298..e211346d 100644 --- a/frontend/src/components/GraphPanel.vue +++ b/frontend/src/components/GraphPanel.vue @@ -818,10 +818,11 @@ onUnmounted(() => { position: relative; width: 100%; height: 100%; - background-color: #FAFAFA; - background-image: radial-gradient(#D0D0D0 1.5px, transparent 1.5px); + background-color: var(--bg-surface); + background-image: radial-gradient(var(--border) 1.5px, transparent 1.5px); background-size: 24px 24px; overflow: hidden; + transition: background-color 0.2s; } .panel-header { @@ -834,14 +835,14 @@ onUnmounted(() => { display: flex; justify-content: space-between; align-items: center; - background: linear-gradient(to bottom, rgba(255,255,255,0.95), rgba(255,255,255,0)); + background: linear-gradient(to bottom, var(--bg-surface) 60%, transparent); pointer-events: none; } .panel-title { font-size: 14px; font-weight: 600; - color: #333; + color: var(--text-secondary); pointer-events: auto; } @@ -855,24 +856,24 @@ onUnmounted(() => { .tool-btn { height: 32px; padding: 0 12px; - border: 1px solid #E0E0E0; - background: #FFF; + border: 1px solid var(--border); + background: var(--bg-primary); border-radius: 6px; display: flex; align-items: center; justify-content: center; gap: 6px; cursor: pointer; - color: #666; + color: var(--text-muted); transition: all 0.2s; - box-shadow: 0 2px 4px rgba(0,0,0,0.02); + box-shadow: 0 2px 4px rgba(0,0,0,0.04); font-size: 13px; } .tool-btn:hover { - background: #F5F5F5; - color: #000; - border-color: #CCC; + background: var(--bg-hover); + color: var(--text-primary); + border-color: var(--border-medium); } .tool-btn .btn-text { @@ -902,7 +903,7 @@ onUnmounted(() => { left: 50%; transform: translate(-50%, -50%); text-align: center; - color: #999; + color: var(--text-faint); } .empty-icon { @@ -916,11 +917,11 @@ onUnmounted(() => { position: absolute; bottom: 24px; left: 24px; - background: rgba(255,255,255,0.95); + background: var(--bg-primary); padding: 12px 16px; border-radius: 8px; - border: 1px solid #EAEAEA; - box-shadow: 0 4px 16px rgba(0,0,0,0.06); + border: 1px solid var(--border); + box-shadow: 0 4px 16px rgba(0,0,0,0.1); z-index: 10; } @@ -928,7 +929,7 @@ onUnmounted(() => { display: block; font-size: 11px; font-weight: 600; - color: #E91E63; + color: #e91e63; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; @@ -946,7 +947,7 @@ onUnmounted(() => { align-items: center; gap: 6px; font-size: 12px; - color: #555; + color: var(--text-muted); } .legend-dot { @@ -968,11 +969,11 @@ onUnmounted(() => { display: flex; align-items: center; gap: 10px; - background: #FFF; + background: var(--bg-primary); padding: 8px 14px; border-radius: 20px; - border: 1px solid #E0E0E0; - box-shadow: 0 2px 8px rgba(0,0,0,0.04); + border: 1px solid var(--border); + box-shadow: 0 2px 8px rgba(0,0,0,0.08); z-index: 10; } @@ -996,7 +997,7 @@ onUnmounted(() => { left: 0; right: 0; bottom: 0; - background-color: #E0E0E0; + background-color: var(--border-medium); border-radius: 22px; transition: 0.3s; } @@ -1008,7 +1009,7 @@ onUnmounted(() => { width: 16px; left: 3px; bottom: 3px; - background-color: white; + background-color: var(--bg-primary); border-radius: 50%; transition: 0.3s; } @@ -1023,7 +1024,7 @@ input:checked + .slider:before { .toggle-label { font-size: 12px; - color: #666; + color: var(--text-muted); } /* Detail Panel - Right Side */ @@ -1033,10 +1034,10 @@ input:checked + .slider:before { right: 20px; width: 320px; max-height: calc(100% - 100px); - background: #FFF; - border: 1px solid #EAEAEA; + background: var(--bg-primary); + border: 1px solid var(--border); border-radius: 10px; - box-shadow: 0 8px 32px rgba(0,0,0,0.1); + box-shadow: 0 8px 32px rgba(0,0,0,0.15); overflow: hidden; font-family: 'Noto Sans SC', system-ui, sans-serif; font-size: 13px; @@ -1050,14 +1051,14 @@ input:checked + .slider:before { justify-content: space-between; align-items: center; padding: 14px 16px; - background: #FAFAFA; - border-bottom: 1px solid #EEE; + background: var(--bg-surface); + border-bottom: 1px solid var(--border-light); flex-shrink: 0; } .detail-title { font-weight: 600; - color: #333; + color: var(--text-secondary); font-size: 14px; } @@ -1075,14 +1076,14 @@ input:checked + .slider:before { border: none; font-size: 20px; cursor: pointer; - color: #999; + color: var(--text-faint); line-height: 1; padding: 0; transition: color 0.2s; } .detail-close:hover { - color: #333; + color: var(--text-primary); } .detail-content { @@ -1099,14 +1100,14 @@ input:checked + .slider:before { } .detail-label { - color: #888; + color: var(--text-faint); font-size: 12px; font-weight: 500; min-width: 80px; } .detail-value { - color: #333; + color: var(--text-secondary); flex: 1; word-break: break-word; } @@ -1114,24 +1115,24 @@ input:checked + .slider:before { .detail-value.uuid-text { font-family: 'JetBrains Mono', monospace; font-size: 11px; - color: #666; + color: var(--text-muted); } .detail-value.fact-text { line-height: 1.5; - color: #444; + color: var(--text-muted); } .detail-section { margin-top: 16px; padding-top: 14px; - border-top: 1px solid #F0F0F0; + border-top: 1px solid var(--border-light); } .section-title { font-size: 12px; font-weight: 600; - color: #666; + color: var(--text-muted); margin-bottom: 10px; } @@ -1147,19 +1148,19 @@ input:checked + .slider:before { } .property-key { - color: #888; + color: var(--text-faint); font-weight: 500; min-width: 90px; } .property-value { - color: #333; + color: var(--text-secondary); flex: 1; } .summary-text { line-height: 1.6; - color: #444; + color: var(--text-muted); font-size: 12px; } @@ -1172,11 +1173,11 @@ input:checked + .slider:before { .label-tag { display: inline-block; padding: 4px 12px; - background: #F5F5F5; - border: 1px solid #E0E0E0; + background: var(--bg-secondary); + border: 1px solid var(--border); border-radius: 16px; font-size: 11px; - color: #555; + color: var(--text-muted); } .episodes-list { @@ -1188,24 +1189,24 @@ input:checked + .slider:before { .episode-tag { display: inline-block; padding: 6px 10px; - background: #F8F8F8; - border: 1px solid #E8E8E8; + background: var(--bg-surface); + border: 1px solid var(--border); border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-size: 10px; - color: #666; + color: var(--text-muted); word-break: break-all; } /* Edge relation header */ .edge-relation-header { - background: #F8F8F8; + background: var(--bg-surface); padding: 12px; border-radius: 8px; margin-bottom: 16px; font-size: 13px; font-weight: 500; - color: #333; + color: var(--text-secondary); line-height: 1.5; word-break: break-word; } @@ -1317,8 +1318,8 @@ input:checked + .slider:before { .self-loop-count { margin-left: auto; font-size: 11px; - color: #666; - background: rgba(255,255,255,0.8); + color: var(--text-muted); + background: var(--bg-surface); padding: 2px 8px; border-radius: 10px; } @@ -1330,8 +1331,8 @@ input:checked + .slider:before { } .self-loop-item { - background: #FAFAFA; - border: 1px solid #EAEAEA; + background: var(--bg-secondary); + border: 1px solid var(--border); border-radius: 8px; } @@ -1340,24 +1341,24 @@ input:checked + .slider:before { align-items: center; gap: 8px; padding: 10px 12px; - background: #F5F5F5; + background: var(--bg-hover); cursor: pointer; transition: background 0.2s; } .self-loop-item-header:hover { - background: #EEEEEE; + background: var(--border); } .self-loop-item.expanded .self-loop-item-header { - background: #E8E8E8; + background: var(--border); } .self-loop-index { font-size: 10px; font-weight: 600; - color: #888; - background: #E0E0E0; + color: var(--text-faint); + background: var(--border-medium); padding: 2px 6px; border-radius: 4px; } @@ -1365,7 +1366,7 @@ input:checked + .slider:before { .self-loop-name { font-size: 12px; font-weight: 500; - color: #333; + color: var(--text-secondary); flex: 1; } @@ -1377,20 +1378,20 @@ input:checked + .slider:before { justify-content: center; font-size: 14px; font-weight: 600; - color: #888; - background: #E0E0E0; + color: var(--text-faint); + background: var(--border-medium); border-radius: 4px; transition: all 0.2s; } .self-loop-item.expanded .self-loop-toggle { - background: #D0D0D0; - color: #666; + background: var(--border); + color: var(--text-muted); } .self-loop-item-content { padding: 12px; - border-top: 1px solid #EAEAEA; + border-top: 1px solid var(--border); } .self-loop-item-content .detail-row { diff --git a/frontend/src/components/LanguageSwitcher.vue b/frontend/src/components/LanguageSwitcher.vue index 723d64b7..7c2bd079 100644 --- a/frontend/src/components/LanguageSwitcher.vue +++ b/frontend/src/components/LanguageSwitcher.vue @@ -66,11 +66,12 @@ onUnmounted(() => { font-family: 'JetBrains Mono', monospace; } -/* Light theme (default - for white header backgrounds) */ +/* Inherits color from parent so it works on both dark navbars and light headers */ .switcher-trigger { background: transparent; - color: #333; - border: 1px solid #CCC; + color: inherit; + border: 1px solid currentColor; + opacity: 0.75; padding: 4px 12px; font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; @@ -78,11 +79,11 @@ onUnmounted(() => { display: flex; align-items: center; gap: 6px; - transition: border-color 0.2s, opacity 0.2s; + transition: opacity 0.2s; } .switcher-trigger:hover { - border-color: #999; + opacity: 1; } .caret { @@ -94,30 +95,30 @@ onUnmounted(() => { top: 100%; right: 0; margin-top: 4px; - background: #FFFFFF; - border: 1px solid #DDD; + background: var(--bg-surface, #fff); + border: 1px solid var(--border, #ddd); list-style: none; padding: 4px 0; min-width: 100%; z-index: 1000; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); } .switcher-option { padding: 6px 12px; font-size: 0.8rem; - color: #333; + color: var(--text-secondary, #333); cursor: pointer; white-space: nowrap; transition: background 0.15s; } .switcher-option:hover { - background: #F0F0F0; + background: var(--bg-hover, #f0f0f0); } .switcher-option.active { - color: var(--orange, #FF4500); + color: var(--accent, #ff6b35); } diff --git a/frontend/src/components/Step1GraphBuild.vue b/frontend/src/components/Step1GraphBuild.vue index 687d1c7b..40933c41 100644 --- a/frontend/src/components/Step1GraphBuild.vue +++ b/frontend/src/components/Step1GraphBuild.vue @@ -275,7 +275,7 @@ watch(() => props.systemLogs.length, () => { diff --git a/frontend/src/main.js b/frontend/src/main.js index cc3d101e..5557c77e 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -1,5 +1,6 @@ import { createApp } from 'vue' import App from './App.vue' +import './assets/theme.css' import router from './router' import i18n from './i18n' diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue index ca7ef6ff..4aa2744a 100644 --- a/frontend/src/views/Home.vue +++ b/frontend/src/views/Home.vue @@ -4,6 +4,7 @@ @@ -416,6 +419,7 @@ import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue' import { useRoute, useRouter } from 'vue-router' import { generateOntology, getProject, buildGraph, getTaskStatus, getGraphData } from '../api/graph' import { getPendingUpload, clearPendingUpload } from '../store/pendingUpload' +import ThemeToggle from '../components/ThemeToggle.vue' import * as d3 from 'd3' const route = useRoute() @@ -1091,24 +1095,15 @@ onUnmounted(() => { \ No newline at end of file diff --git a/frontend/src/views/ReportView.vue b/frontend/src/views/ReportView.vue index ac054e47..476a147e 100644 --- a/frontend/src/views/ReportView.vue +++ b/frontend/src/views/ReportView.vue @@ -1,4 +1,4 @@ -