| <template> |
| <div class="app"> |
| <template v-if="authChecked"> |
| <LoginPage v-if="!isLoggedIn" @loggedIn="onLogin" /> |
| <template v-else> |
| <AppHeader :connected="sseConnected" @openConfig="configDrawerVisible = true" /> |
| <div class="main"> |
| <RequestList /> |
| <DetailPanel /> |
| </div> |
| <ConfigDrawer :visible="configDrawerVisible" @close="configDrawerVisible = false" /> |
| </template> |
| </template> |
| </div> |
| </template> |
|
|
| <script setup lang="ts"> |
| import { ref, onMounted, watch } from 'vue'; |
| import { storeToRefs } from 'pinia'; |
| import { useAuthStore } from './stores/auth'; |
| import { useLogsStore } from './stores/logs'; |
| import { useStatsStore } from './stores/stats'; |
| import { useSSE } from './composables/useSSE'; |
| import LoginPage from './components/LoginPage.vue'; |
| import AppHeader from './components/AppHeader.vue'; |
| import RequestList from './components/RequestList.vue'; |
| import DetailPanel from './components/DetailPanel.vue'; |
| import ConfigDrawer from './components/ConfigDrawer.vue'; |
| |
| const auth = useAuthStore(); |
| const logsStore = useLogsStore(); |
| const statsStore = useStatsStore(); |
| const { loggedIn: isLoggedIn } = storeToRefs(auth); |
| |
| const authChecked = ref(false); |
| const sseConnected = ref(false); |
| const configDrawerVisible = ref(false); |
| |
| |
| const savedTheme = localStorage.getItem('cursor2api_theme') ?? 'light'; |
| document.documentElement.setAttribute('data-theme', savedTheme); |
| |
| const { connect: connectSSE, disconnect: disconnectSSE } = useSSE((connected) => { sseConnected.value = connected; }); |
| |
| onMounted(async () => { |
| |
| const urlToken = new URLSearchParams(location.search).get('token'); |
| if (urlToken) { |
| auth.setToken(urlToken); |
| |
| history.replaceState(null, '', location.pathname); |
| } |
| try { |
| const res = await fetch('/api/vue/stats', { |
| headers: auth.token ? { Authorization: `Bearer ${auth.token}` } : {}, |
| }); |
| if (res.ok) { |
| |
| auth.loggedIn = true; |
| } else if (res.status === 401) { |
| |
| auth.loggedIn = auth.isLoggedIn(); |
| } else { |
| auth.loggedIn = auth.isLoggedIn(); |
| } |
| } catch { |
| auth.loggedIn = auth.isLoggedIn(); |
| } |
| authChecked.value = true; |
| if (isLoggedIn.value) { |
| await Promise.all([logsStore.loadRequests(), statsStore.load()]); |
| connectSSE(); |
| } |
| }); |
| |
| |
| watch(isLoggedIn, (val) => { |
| if (!val) { |
| disconnectSSE(); |
| logsStore.resetState(); |
| } |
| }); |
| |
| async function onLogin() { |
| await Promise.all([logsStore.loadRequests(), statsStore.load()]); |
| connectSSE(); |
| } |
| </script> |
|
|
| <style> |
| @import 'highlight.js/styles/github-dark.css'; |
| |
| |
| :root, [data-theme="light"] { |
| --bg0: #f0f4f8; |
| --bg1: #ffffff; |
| --bg2: #f7f9fc; |
| --bg3: #edf2f7; |
| --bg-card: #ffffff; |
| --bdr: #e2e8f0; |
| --bdr2: #cbd5e1; |
| --t1: #1e293b; |
| --t2: #475569; |
| --t3: #94a3b8; |
| --blue: #3b82f6; |
| --cyan: #0891b2; |
| --green: #059669; |
| --yellow: #d97706; |
| --red: #dc2626; |
| --purple: #7c3aed; |
| --pink: #db2777; |
| --orange: #ea580c; |
| --mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', 'SF Mono', 'Consolas', 'Menlo', monospace; |
| --sans: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', 'Segoe UI', sans-serif; |
| --shadow-sm: 0 1px 2px rgba(0,0,0,.05); |
| --shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.04); |
| --shadow-md: 0 4px 6px rgba(0,0,0,.06), 0 2px 4px rgba(0,0,0,.04); |
| --radius: 10px; |
| --radius-sm: 6px; |
| |
| --bg: var(--bg0); |
| --card-bg: var(--bg1); |
| --border: var(--bdr); |
| --border-faint: #f1f5f9; |
| --text: var(--t1); |
| --text-muted: var(--t2); |
| --text-dim: var(--t3); |
| --accent: var(--blue); |
| --pill-bg: var(--bg3); |
| --hover-bg: var(--bg3); |
| --active-bg: #eff6ff; |
| } |
| |
| |
| [data-theme="dark"] { |
| --bg0: #0d1117; |
| --bg1: #161b27; |
| --bg2: #1c2133; |
| --bg3: #21273a; |
| --bg-card: #161b27; |
| --bdr: #2a3150; |
| --bdr2: #3a4268; |
| --t1: #e2e8f0; |
| --t2: #8892aa; |
| --t3: #546178; |
| --blue: #58a6ff; |
| --cyan: #39d0e8; |
| --green: #3dd68c; |
| --yellow: #f0b429; |
| --red: #f87171; |
| --purple: #b48efa; |
| --pink: #f472b6; |
| --orange: #fb923c; |
| --mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', 'SF Mono', 'Consolas', 'Menlo', monospace; |
| --sans: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', 'Segoe UI', sans-serif; |
| --shadow-sm: 0 1px 2px rgba(0,0,0,.3); |
| --shadow: 0 1px 3px rgba(0,0,0,.4); |
| --shadow-md: 0 4px 6px rgba(0,0,0,.4); |
| --radius: 10px; |
| --radius-sm: 6px; |
| --bg: var(--bg0); |
| --card-bg: var(--bg1); |
| --border: var(--bdr); |
| --border-faint: #1e2540; |
| --text: var(--t1); |
| --text-muted: var(--t3); |
| --accent: var(--blue); |
| --pill-bg: var(--bg3); |
| --hover-bg: var(--bg2); |
| --active-bg: #1e3a5f; |
| } |
| |
| |
| [data-theme="light"] .hljs { background: #f6f8fa; color: #24292e; } |
| [data-theme="light"] .hljs-comment, [data-theme="light"] .hljs-quote { color: #6a737d; } |
| [data-theme="light"] .hljs-keyword, [data-theme="light"] .hljs-selector-tag { color: #d73a49; font-weight: bold; } |
| [data-theme="light"] .hljs-string, [data-theme="light"] .hljs-attr { color: #032f62; } |
| [data-theme="light"] .hljs-number, [data-theme="light"] .hljs-literal { color: #005cc5; } |
| [data-theme="light"] .hljs-title, [data-theme="light"] .hljs-section { color: #6f42c1; font-weight: bold; } |
| [data-theme="light"] .hljs-built_in, [data-theme="light"] .hljs-type { color: #e36209; } |
| [data-theme="light"] .hljs-variable, [data-theme="light"] .hljs-name { color: #24292e; } |
| |
| * { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| body { |
| font-family: var(--sans); |
| background: var(--bg0); |
| color: var(--t1); |
| height: 100vh; |
| overflow: hidden; |
| } |
| |
| [data-theme="light"] body { |
| background: linear-gradient(135deg, #e8eeff 0%, #f0f4f8 40%, #eef2f8 70%, #f0f4f8 100%); |
| background-attachment: fixed; |
| } |
| |
| [data-theme="dark"] body { |
| background: |
| radial-gradient(ellipse 80% 50% at 20% -10%, rgba(88,166,255,0.07) 0%, transparent 60%), |
| radial-gradient(ellipse 60% 40% at 85% 110%, rgba(57,208,232,0.06) 0%, transparent 55%), |
| radial-gradient(ellipse 40% 30% at 50% 50%, rgba(99,102,241,0.04) 0%, transparent 50%), |
| #0d1117; |
| background-attachment: fixed; |
| } |
| |
| .app { |
| display: flex; flex-direction: column; |
| height: 100vh; color: var(--t1); |
| } |
| |
| .main { display: flex; flex: 1; overflow: hidden; } |
| |
| ::-webkit-scrollbar { width: 5px; height: 5px; } |
| ::-webkit-scrollbar-track { background: transparent; } |
| ::-webkit-scrollbar-thumb { background: var(--bdr2); border-radius: 3px; } |
| ::-webkit-scrollbar-thumb:hover { background: var(--t3); } |
| </style> |
|
|