From 7803c46b96ef615bca1d63c880902bfe869eeaad Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 17 Jul 2026 17:28:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package-lock.json | 27 + frontend/package.json | 1 + frontend/src/App.vue | 2 + frontend/src/api/index.js | 59 +- frontend/src/assets/main.css | 119 +- frontend/src/components/ChatInput.vue | 1623 +++++++++++++++-- frontend/src/components/ChatSidebar.vue | 342 ++-- frontend/src/components/MessageItem.vue | 334 +++- frontend/src/components/MessageList.vue | 362 +++- .../src/components/NotificationCenter.vue | 292 +++ frontend/src/router/index.js | 2 +- frontend/src/stores/auth.js | 43 +- frontend/src/stores/chat.js | 19 +- frontend/src/stores/notification.js | 45 + frontend/src/stores/settings.js | 8 +- frontend/src/utils/errorPresentation.js | 126 ++ frontend/src/utils/guestSession.js | 17 + frontend/src/views/ChatView.vue | 137 +- 18 files changed, 3118 insertions(+), 440 deletions(-) create mode 100644 frontend/src/components/NotificationCenter.vue create mode 100644 frontend/src/stores/notification.js create mode 100644 frontend/src/utils/errorPresentation.js create mode 100644 frontend/src/utils/guestSession.js diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 05cc02e..bb6c89a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "ai-chat-member", "version": "1.0.0", "dependencies": { + "@tabler/icons-vue": "^3.45.0", "axios": "^1.7.9", "dompurify": "^3.2.4", "highlight.js": "^11.11.1", @@ -865,6 +866,32 @@ "win32" ] }, + "node_modules/@tabler/icons": { + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.45.0.tgz", + "integrity": "sha512-jiATwV8+zGYLTZ7gMLGivCic+KtsMZXcDmufIG8umlLxoHhI6902hGYIEt0Oa9Y9SXblNzUlrisHm5jOFMxOQA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + } + }, + "node_modules/@tabler/icons-vue": { + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-vue/-/icons-vue-3.45.0.tgz", + "integrity": "sha512-Q/cmDsx0zrzBScIL6Xxwevg7JqvkrteFW2R+NGTN9pwOrgJMzBLhyz/7ONvktjuwDn6eUKhFSM66CNAR8u3tRw==", + "license": "MIT", + "dependencies": { + "@tabler/icons": "3.45.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + }, + "peerDependencies": { + "vue": ">=3.0.1" + } + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", diff --git a/frontend/package.json b/frontend/package.json index f8927a5..d23ba6f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "@tabler/icons-vue": "^3.45.0", "axios": "^1.7.9", "dompurify": "^3.2.4", "highlight.js": "^11.11.1", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index d372e96..b799c50 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,6 +1,8 @@ diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index a4ab705..b11e6e4 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -1,10 +1,34 @@ import axios from 'axios' +import { getGuestKey } from '@/utils/guestSession' const api = axios.create({ baseURL: '/api', timeout: 60000 }) +let guestRenewal = null + +async function renewGuestSession() { + const guestKey = getGuestKey() + if (!guestKey) throw new Error('游客身份已失效') + + if (!guestRenewal) { + guestRenewal = axios.post('/api/auth/guest', { guest_key: guestKey }) + .then(res => { + const data = res.data?.data + if (!data?.token) throw new Error('游客身份续期失败') + localStorage.setItem('token', data.token) + localStorage.setItem('auth_type', 'guest') + return data.token + }) + .finally(() => { + guestRenewal = null + }) + } + + return guestRenewal +} + api.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { @@ -15,20 +39,49 @@ api.interceptors.request.use(config => { api.interceptors.response.use( res => res, - err => { + async err => { if (axios.isCancel(err)) { return Promise.reject(err) } - const message = err.response?.data?.message || '请求失败' + const message = err.response?.data?.message || err.message || '请求失败' if (err.response?.status === 401) { + const original = err.config || {} + const authType = localStorage.getItem('auth_type') + const requestUrl = String(original.url || '') + const isPublicAuthRequest = ['/auth/login', '/auth/register', '/auth/guest'] + .some(path => requestUrl.includes(path)) + + if (isPublicAuthRequest) { + const wrapped = new Error(message) + wrapped.code = err.code + wrapped.status = 401 + wrapped.detail = err.response?.data?.detail || '' + return Promise.reject(wrapped) + } + + if (authType === 'guest' && !original._guestRetry) { + original._guestRetry = true + try { + const token = await renewGuestSession() + original.headers = original.headers || {} + original.headers.Authorization = `Bearer ${token}` + return api(original) + } catch { + // Fall through so the page can present a useful retry state. + } + } + localStorage.removeItem('token') - if (!window.location.pathname.includes('/login')) { + localStorage.removeItem('auth_type') + if (authType === 'account' && !window.location.pathname.includes('/login')) { window.location.href = '/login' } } const wrapped = new Error(message) wrapped.code = err.code + wrapped.status = err.response?.status || null + wrapped.detail = err.response?.data?.detail || '' return Promise.reject(wrapped) } ) diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index 9274b24..1225f64 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -1,22 +1,29 @@ -@import 'highlight.js/styles/github-dark.css'; +@import 'highlight.js/styles/github.css'; :root { - --bg-primary: #212121; - --bg-secondary: #171717; - --bg-tertiary: #2f2f2f; - --bg-hover: #3a3a3a; - --text-primary: #ececec; - --text-secondary: #b4b4b4; - --text-muted: #8e8e8e; - --accent: #10a37f; - --accent-hover: #0d8a6a; - --border: #3a3a3a; - --danger: #ef4444; - --sidebar-width: 260px; - --header-height: 56px; - --input-max-width: 768px; - --radius: 12px; - --shadow: 0 4px 24px rgba(0, 0, 0, 0.3); + --bg-primary: #f5f7fb; + --bg-secondary: #ffffff; + --bg-tertiary: #f8fafc; + --bg-hover: #eef3f9; + --bg-soft: #f2f5f8; + --text-primary: #152033; + --text-secondary: #526077; + --text-muted: #7d899b; + --accent: #2d66da; + --accent-hover: #2458c4; + --accent-soft: #edf4ff; + --border: #e1e7ef; + --border-strong: #d4dce7; + --danger: #dc4c4c; + --sidebar-width: 268px; + --header-height: 66px; + --input-max-width: 920px; + --content-max-width: 1040px; + --radius: 18px; + --radius-sm: 12px; + --shadow: 0 18px 50px rgba(38, 59, 92, 0.08); + --shadow-soft: 0 8px 24px rgba(38, 59, 92, 0.06); + --shadow-composer: 0 14px 38px rgba(38, 59, 92, 0.09); } * { @@ -27,10 +34,16 @@ html, body, #app { height: 100%; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + min-height: 100dvh; + font-family: "Microsoft YaHei UI", "PingFang SC", "Segoe UI", sans-serif; background: var(--bg-primary); color: var(--text-primary); -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +body { + overflow: hidden; } a { @@ -46,7 +59,16 @@ button { font: inherit; } -input, textarea { +button:focus-visible, +a:focus-visible, +select:focus-visible, +textarea:focus-visible, +input:focus-visible { + outline: 2px solid rgba(45, 102, 218, 0.72); + outline-offset: 2px; +} + +input, textarea, select { font: inherit; color: inherit; background: transparent; @@ -54,47 +76,55 @@ input, textarea { outline: none; } -/* Scrollbar */ ::-webkit-scrollbar { width: 6px; } + ::-webkit-scrollbar-track { background: transparent; } + ::-webkit-scrollbar-thumb { - background: var(--bg-hover); - border-radius: 3px; + background: #ced7e3; + border-radius: 999px; } -/* Buttons */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 10px 20px; - border-radius: 8px; + border-radius: 999px; font-size: 14px; font-weight: 500; - transition: all 0.2s; + transition: color 0.16s ease, background 0.16s ease, border-color 0.16s ease, transform 0.16s ease; +} + +.btn:active { + transform: scale(0.97); } .btn-primary { background: var(--accent); color: white; } + .btn-primary:hover { background: var(--accent-hover); } + .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; } .btn-ghost { - background: transparent; + background: #fff; color: var(--text-secondary); + border: 1px solid var(--border); } + .btn-ghost:hover { background: var(--bg-hover); color: var(--text-primary); @@ -103,11 +133,11 @@ input, textarea { .btn-danger { color: var(--danger); } + .btn-danger:hover { background: rgba(239, 68, 68, 0.1); } -/* Form */ .form-group { margin-bottom: 16px; } @@ -122,15 +152,16 @@ input, textarea { .form-input { width: 100%; padding: 12px 16px; - background: var(--bg-tertiary); + background: #fff; border: 1px solid var(--border); - border-radius: 8px; + border-radius: 14px; font-size: 15px; - transition: border-color 0.2s; + transition: border-color 0.2s, box-shadow 0.2s; } .form-input:focus { border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(45, 102, 218, 0.11); } .form-error { @@ -139,7 +170,6 @@ input, textarea { margin-top: 12px; } -/* Markdown content */ .markdown-body { line-height: 1.7; font-size: 15px; @@ -149,11 +179,11 @@ input, textarea { .markdown-body p { margin-bottom: 12px; } + .markdown-body p:last-child { margin-bottom: 0; } -/* 单个换行(非空行分段)时也留出呼吸空间,避免像 OCR 结果那样一行行挤在一起 */ .markdown-body br { content: ''; display: block; @@ -161,8 +191,9 @@ input, textarea { } .markdown-body pre { - background: var(--bg-secondary); - border-radius: 8px; + background: #f8fafc; + border: 1px solid var(--border); + border-radius: 14px; padding: 16px; overflow-x: auto; margin: 12px 0; @@ -174,9 +205,9 @@ input, textarea { } .markdown-body :not(pre) > code { - background: var(--bg-tertiary); + background: #eef2ff; padding: 2px 6px; - border-radius: 4px; + border-radius: 6px; } .markdown-body ul, .markdown-body ol { @@ -204,7 +235,7 @@ input, textarea { } .markdown-body th { - background: var(--bg-tertiary); + background: var(--bg-soft); } .markdown-body a { @@ -216,12 +247,12 @@ input, textarea { border-radius: 8px; } -/* Overlay for mobile sidebar */ .sidebar-overlay { display: none; position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.5); + background: rgba(21, 32, 51, 0.34); + backdrop-filter: blur(2px); z-index: 90; } @@ -230,3 +261,13 @@ input, textarea { display: block; } } + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + } +} diff --git a/frontend/src/components/ChatInput.vue b/frontend/src/components/ChatInput.vue index 722f287..91ff904 100644 --- a/frontend/src/components/ChatInput.vue +++ b/frontend/src/components/ChatInput.vue @@ -1,6 +1,27 @@