main: 增加全局暗黑模式支持及 Markdown 渲染功能

- 实现暗黑模式切换逻辑,支持系统主题适配
- 优化全局样式,添加动态主题变量及暗黑样式
- 引入 `markdown-it` 和 `mermaid`,支持 Markdown 内容及流程图渲染
- 更新 ChatView 测试用例,验证 Markdown 和 Mermaid 渲染效果
- 修改 package.json,增加新依赖
This commit is contained in:
2025-12-22 12:37:59 +08:00
parent 9b48ff3e3b
commit 9280fbe762
13 changed files with 1846 additions and 165 deletions

1270
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,8 @@
"dependencies": { "dependencies": {
"axios": "^1.13.2", "axios": "^1.13.2",
"element-plus": "^2.12.0", "element-plus": "^2.12.0",
"markdown-it": "^14.1.0",
"mermaid": "^11.12.2",
"vue": "^3.5.24", "vue": "^3.5.24",
"vue-router": "^4.6.4" "vue-router": "^4.6.4"
}, },

View File

@@ -68,8 +68,8 @@ const miniMenuOpen = ref(false)
<style scoped> <style scoped>
.app-shell { .app-shell {
min-height: 100vh; min-height: 100vh;
background: radial-gradient(circle at 20% 20%, #f3f6ff, #e7ebf5 40%, #dae0ec), background: var(--app-bg);
linear-gradient(135deg, rgba(255, 255, 255, 0.6), rgba(220, 226, 238, 0.9)); color: var(--text-primary);
} }
.auth-shell { .auth-shell {
@@ -83,7 +83,7 @@ const miniMenuOpen = ref(false)
.auth-bg { .auth-bg {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: linear-gradient(180deg, #e9eef7, #f6f8fc); background: var(--auth-bg);
filter: blur(0); filter: blur(0);
z-index: 0; z-index: 0;
} }
@@ -92,10 +92,10 @@ const miniMenuOpen = ref(false)
position: relative; position: relative;
z-index: 1; z-index: 1;
width: min(960px, 92vw); width: min(960px, 92vw);
background: rgba(255, 255, 255, 0.75); background: var(--surface);
border: 1px solid rgba(255, 255, 255, 0.6); border: 1px solid var(--border-soft);
border-radius: 18px; border-radius: 18px;
box-shadow: 0 30px 80px rgba(36, 49, 89, 0.16); box-shadow: var(--shadow-1);
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
padding: 32px; padding: 32px;
} }
@@ -105,6 +105,7 @@ const miniMenuOpen = ref(false)
grid-template-columns: 300px 1fr; grid-template-columns: 300px 1fr;
min-height: 100vh; min-height: 100vh;
position: relative; position: relative;
background: var(--bg-page);
} }
.main { .main {
@@ -119,7 +120,7 @@ const miniMenuOpen = ref(false)
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding-bottom: 6px; padding-bottom: 6px;
border-bottom: 1px solid rgba(0, 0, 0, 0.04); border-bottom: 1px solid var(--border-soft);
} }
.page { .page {
@@ -130,15 +131,15 @@ const miniMenuOpen = ref(false)
height: 46px; height: 46px;
padding: 0 12px; padding: 0 12px;
border-radius: 12px; border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.06); border: 1px solid var(--border-soft);
background: rgba(255, 255, 255, 0.9); background: var(--surface-strong);
box-shadow: 0 8px 22px rgba(17, 24, 39, 0.14); box-shadow: var(--shadow-2);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
cursor: pointer; cursor: pointer;
transition: transform 0.18s ease, box-shadow 0.18s ease; transition: transform 0.18s ease, box-shadow 0.18s ease;
color: #111827; color: var(--text-primary);
min-width: 104px; min-width: 104px;
} }
@@ -149,13 +150,13 @@ const miniMenuOpen = ref(false)
.mini-icon { .mini-icon {
font-size: 15px; font-size: 15px;
color: #1d4ed8; color: var(--accent);
} }
.mini-label { .mini-label {
font-weight: 700; font-weight: 700;
letter-spacing: 0.02em; letter-spacing: 0.02em;
color: #111827; color: var(--text-primary);
} }
.mini-menu { .mini-menu {
@@ -168,25 +169,25 @@ const miniMenuOpen = ref(false)
.mini-title { .mini-title {
margin: 0; margin: 0;
font-weight: 700; font-weight: 700;
color: #0f172a; color: var(--text-primary);
font-size: 14px; font-size: 14px;
} }
.mini-link { .mini-link {
padding: 8px 10px; padding: 8px 10px;
border-radius: 10px; border-radius: 10px;
color: #1f2937; color: var(--text-primary);
transition: all 0.16s ease; transition: all 0.16s ease;
} }
.mini-link:hover { .mini-link:hover {
background: rgba(37, 99, 235, 0.08); background: var(--chip-bg);
color: #1d4ed8; color: var(--accent);
} }
.mini-popper { .mini-popper {
border-radius: 12px; border-radius: 12px;
box-shadow: 0 20px 60px rgba(17, 24, 39, 0.2) !important; box-shadow: var(--shadow-1) !important;
} }
@media (max-width: 900px) { @media (max-width: 900px) {

View File

@@ -109,19 +109,19 @@ const createSession = async () => {
gap: 8px; gap: 8px;
padding: 8px 12px; padding: 8px 12px;
border-radius: 12px; border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.08); border: 1px solid var(--border-soft);
background: rgba(255, 255, 255, 0.88); background: var(--surface);
box-shadow: 0 8px 20px rgba(17, 24, 39, 0.12); box-shadow: var(--shadow-2);
cursor: pointer; cursor: pointer;
transition: transform 0.18s ease, box-shadow 0.18s ease; transition: transform 0.18s ease, box-shadow 0.18s ease;
color: #111827; color: var(--text-primary);
} }
.new-chat-btn.inline { .new-chat-btn.inline {
width: 100%; width: 100%;
justify-content: center; justify-content: center;
box-shadow: none; box-shadow: none;
border: 1px solid rgba(0, 0, 0, 0.04); border: 1px solid var(--border-soft);
} }
.new-chat-btn:hover { .new-chat-btn:hover {
@@ -148,7 +148,7 @@ const createSession = async () => {
.title { .title {
margin: 0; margin: 0;
font-weight: 700; font-weight: 700;
color: #0f172a; color: var(--text-primary);
} }
.create-btn { .create-btn {
@@ -159,7 +159,7 @@ const createSession = async () => {
.last { .last {
margin: 0; margin: 0;
font-size: 12px; font-size: 12px;
color: #4b5563; color: var(--text-subtle);
word-break: break-all; word-break: break-all;
} }
</style> </style>

View File

@@ -219,16 +219,16 @@ onMounted(() => {
position: sticky; position: sticky;
top: 0; top: 0;
padding: 18px; padding: 18px;
background: rgba(255, 255, 255, 0.8); background: var(--surface-glass);
border-right: 1px solid rgba(0, 0, 0, 0.06); border-right: 1px solid var(--border-soft);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
box-shadow: 12px 0 30px rgba(17, 24, 39, 0.08); box-shadow: 12px 0 30px rgba(0, 0, 0, 0.12);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 14px; gap: 14px;
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: rgba(37, 99, 235, 0.3) rgba(255, 255, 255, 0.6); scrollbar-color: rgba(37, 99, 235, 0.3) var(--surface-strong);
} }
.sidebar::-webkit-scrollbar { .sidebar::-webkit-scrollbar {
@@ -236,7 +236,11 @@ onMounted(() => {
} }
.sidebar::-webkit-scrollbar-track { .sidebar::-webkit-scrollbar-track {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(236, 242, 255, 0.9)); background: linear-gradient(
180deg,
var(--surface-strong),
var(--surface-glass-strong)
);
border-radius: 12px; border-radius: 12px;
} }
@@ -261,7 +265,7 @@ onMounted(() => {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-weight: 700; font-weight: 700;
color: #111827; color: var(--text-primary);
letter-spacing: 0.02em; letter-spacing: 0.02em;
font-size: 18px; font-size: 18px;
} }
@@ -288,9 +292,9 @@ onMounted(() => {
.session-item { .session-item {
padding: 10px 12px; padding: 10px 12px;
border-radius: 12px; border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid var(--border-soft);
background: rgba(255, 255, 255, 0.9); background: var(--surface);
box-shadow: 0 6px 14px rgba(17, 24, 39, 0.06); box-shadow: var(--shadow-2);
margin-bottom: 10px; margin-bottom: 10px;
cursor: pointer; cursor: pointer;
transition: all 0.16s ease; transition: all 0.16s ease;
@@ -315,12 +319,12 @@ onMounted(() => {
.name { .name {
font-weight: 700; font-weight: 700;
color: #111827; color: var(--text-primary);
} }
.preview { .preview {
margin: 6px 0 4px; margin: 6px 0 4px;
color: #4b5563; color: var(--text-subtle);
font-size: 13px; font-size: 13px;
line-height: 1.4; line-height: 1.4;
max-height: 38px; max-height: 38px;
@@ -329,7 +333,7 @@ onMounted(() => {
.meta { .meta {
margin: 0; margin: 0;
color: #9ca3af; color: var(--text-muted);
font-size: 12px; font-size: 12px;
} }
@@ -338,7 +342,7 @@ onMounted(() => {
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
font-size: 12px; font-size: 12px;
color: #6b7280; color: var(--text-muted);
} }
.error { .error {
@@ -349,20 +353,20 @@ onMounted(() => {
width: 100%; width: 100%;
height: 34px; height: 34px;
border-radius: 10px; border-radius: 10px;
border: 1px dashed rgba(0, 0, 0, 0.1); border: 1px dashed var(--border-contrast);
color: #1f2937; color: var(--text-primary);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 6px; gap: 6px;
letter-spacing: 0.02em; letter-spacing: 0.02em;
background: rgba(255, 255, 255, 0.85); background: var(--surface);
transition: all 0.15s ease; transition: all 0.15s ease;
} }
.load-more:hover { .load-more:hover {
border-color: rgba(37, 99, 235, 0.35); border-color: rgba(37, 99, 235, 0.35);
color: #1d4ed8; color: var(--accent);
} }
.load-more:disabled { .load-more:disabled {
@@ -382,8 +386,8 @@ onMounted(() => {
position: static; position: static;
overflow: visible; overflow: visible;
border-right: none; border-right: none;
border-bottom: 1px solid rgba(0, 0, 0, 0.06); border-bottom: 1px solid var(--border-soft);
box-shadow: 0 8px 24px rgba(17, 24, 39, 0.08); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
} }
.session-item { .session-item {

View File

@@ -0,0 +1,63 @@
import { onBeforeUnmount, onMounted, ref } from 'vue'
const THEME_KEY = 'ars-theme'
const getSystemTheme = () => {
if (typeof window === 'undefined') return 'light'
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
}
const applyThemeToDom = (resolved) => {
if (typeof document === 'undefined') return
document.documentElement.dataset.theme = resolved
document.documentElement.classList.toggle('dark', resolved === 'dark')
document.documentElement.style.colorScheme =
resolved === 'dark' ? 'dark' : 'light'
}
export const useTheme = () => {
const themeMode = ref('system')
const resolvedTheme = ref('light')
let mediaQuery = null
const apply = (mode = 'system') => {
const normalized = mode || 'system'
themeMode.value = normalized
if (typeof localStorage !== 'undefined') {
localStorage.setItem(THEME_KEY, normalized)
}
const resolved = normalized === 'system' ? getSystemTheme() : normalized
resolvedTheme.value = resolved
applyThemeToDom(resolved)
}
const handleMediaChange = () => {
if (themeMode.value === 'system') {
apply('system')
}
}
onMounted(() => {
const stored =
typeof localStorage !== 'undefined'
? localStorage.getItem(THEME_KEY)
: null
apply(stored || 'system')
if (typeof window !== 'undefined' && window.matchMedia) {
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
mediaQuery.addEventListener('change', handleMediaChange)
}
})
onBeforeUnmount(() => {
mediaQuery?.removeEventListener('change', handleMediaChange)
})
return {
themeMode,
resolvedTheme,
setTheme: apply,
}
}

View File

@@ -1,8 +1,33 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import ElementPlus from 'element-plus' import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import './style.css' import './style.css'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
const initTheme = () => {
if (typeof document === 'undefined') return
const stored =
typeof localStorage !== 'undefined'
? localStorage.getItem('ars-theme')
: null
const prefersDark =
typeof window !== 'undefined' &&
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
const resolved =
stored === 'light' || stored === 'dark'
? stored
: prefersDark
? 'dark'
: 'light'
document.documentElement.dataset.theme = resolved
document.documentElement.classList.toggle('dark', resolved === 'dark')
document.documentElement.style.colorScheme =
resolved === 'dark' ? 'dark' : 'light'
}
initTheme()
createApp(App).use(ElementPlus).use(router).mount('#app') createApp(App).use(ElementPlus).use(router).mount('#app')

View File

@@ -3,11 +3,107 @@
Arial, sans-serif; Arial, sans-serif;
line-height: 1.6; line-height: 1.6;
font-weight: 400; font-weight: 400;
color: #111827; color: var(--text-primary);
background-color: #e9edf3; background-color: var(--bg-page);
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
--bg-page: #e9edf3;
--app-bg: radial-gradient(circle at 20% 20%, #f3f6ff, #e7ebf5 40%, #dae0ec),
linear-gradient(135deg, rgba(255, 255, 255, 0.6), rgba(220, 226, 238, 0.9));
--auth-bg: linear-gradient(180deg, #e9eef7, #f6f8fc);
--surface: rgba(255, 255, 255, 0.9);
--surface-strong: #ffffff;
--surface-subtle: rgba(15, 23, 42, 0.03);
--surface-glass: rgba(255, 255, 255, 0.8);
--surface-glass-strong: rgba(255, 255, 255, 0.92);
--border-soft: rgba(0, 0, 0, 0.05);
--border-contrast: rgba(0, 0, 0, 0.1);
--text-primary: #111827;
--text-muted: #6b7280;
--text-subtle: #4b5563;
--shadow-1: 0 18px 38px rgba(17, 24, 39, 0.08);
--shadow-2: 0 14px 35px rgba(17, 24, 39, 0.06);
--accent: #1d4ed8;
--accent-strong: #2563eb;
--chip-bg: rgba(37, 99, 235, 0.08);
--chip-text: #1d4ed8;
--panel-hero-bg: linear-gradient(135deg, #f5f8ff, #edf2ff);
--panel-hero-border: rgba(0, 0, 0, 0.04);
--bubble-user-bg: linear-gradient(135deg, #edf2ff, #e5edff);
--bubble-user-border: rgba(37, 99, 235, 0.18);
--bubble-agent-border: rgba(0, 0, 0, 0.04);
--bubble-system-bg: linear-gradient(135deg, #fff1f2, #ffe4e6);
--bubble-system-border: rgba(239, 68, 68, 0.18);
--composer-bg: #f7f9fc;
--input-bg: #ffffff;
--table-border: #d1d5db;
--table-divider: #e5e7eb;
--code-bg: #0f172a;
--code-border: rgba(255, 255, 255, 0.06);
--code-text: #e2e8f0;
--mermaid-bg1: #eef2ff;
--mermaid-bg2: #e0f2fe;
--error-card-bg: linear-gradient(
135deg,
rgba(239, 68, 68, 0.12),
rgba(248, 113, 113, 0.1)
);
--error-card-border: rgba(239, 68, 68, 0.22);
--pending-bg: rgba(37, 99, 235, 0.06);
--pending-border: rgba(37, 99, 235, 0.15);
--attachment-bg: rgba(15, 23, 42, 0.03);
--overlay-weak: rgba(255, 255, 255, 0.82);
}
:root.dark {
color-scheme: dark;
--bg-page: #0b1221;
--app-bg: radial-gradient(circle at 18% 18%, #0f172a, #0b1221 40%, #0a1020),
linear-gradient(135deg, rgba(10, 17, 33, 0.9), rgba(8, 12, 24, 0.95));
--auth-bg: linear-gradient(180deg, #0d1525, #0a0f1d);
--surface: rgba(15, 23, 42, 0.9);
--surface-strong: #111827;
--surface-subtle: rgba(255, 255, 255, 0.04);
--surface-glass: rgba(15, 23, 42, 0.75);
--surface-glass-strong: rgba(15, 23, 42, 0.82);
--border-soft: rgba(255, 255, 255, 0.08);
--border-contrast: rgba(255, 255, 255, 0.12);
--text-primary: #e5e7eb;
--text-muted: #9ca3af;
--text-subtle: #cbd5e1;
--shadow-1: 0 18px 38px rgba(0, 0, 0, 0.55);
--shadow-2: 0 14px 35px rgba(0, 0, 0, 0.45);
--accent: #60a5fa;
--accent-strong: #93c5fd;
--chip-bg: rgba(96, 165, 250, 0.14);
--chip-text: #bfdbfe;
--panel-hero-bg: linear-gradient(135deg, #0f172a, #0b1221);
--panel-hero-border: rgba(255, 255, 255, 0.06);
--bubble-user-bg: linear-gradient(135deg, #14203b, #0f172a);
--bubble-user-border: rgba(96, 165, 250, 0.35);
--bubble-agent-border: rgba(255, 255, 255, 0.06);
--bubble-system-bg: linear-gradient(135deg, #2b1b1b, #1c0f0f);
--bubble-system-border: rgba(248, 113, 113, 0.3);
--composer-bg: #0f172a;
--input-bg: #0b1020;
--table-border: #384152;
--table-divider: #2f3645;
--code-bg: #0b1020;
--code-border: rgba(255, 255, 255, 0.08);
--code-text: #e5e7eb;
--mermaid-bg1: #101a32;
--mermaid-bg2: #10243e;
--error-card-bg: linear-gradient(
135deg,
rgba(248, 113, 113, 0.14),
rgba(127, 29, 29, 0.28)
);
--error-card-border: rgba(248, 113, 113, 0.35);
--pending-bg: rgba(96, 165, 250, 0.08);
--pending-border: rgba(96, 165, 250, 0.3);
--attachment-bg: rgba(255, 255, 255, 0.05);
--overlay-weak: rgba(8, 12, 24, 0.82);
} }
*, *,
@@ -19,8 +115,9 @@
body { body {
margin: 0; margin: 0;
min-height: 100vh; min-height: 100vh;
background-color: #e9edf3; background-color: var(--bg-page);
color: #111827; color: var(--text-primary);
transition: background-color 0.25s ease, color 0.25s ease;
} }
a { a {

View File

@@ -11,6 +11,8 @@ import {
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import axios from 'axios' import axios from 'axios'
import MarkdownIt from 'markdown-it'
import mermaid from 'mermaid'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -29,6 +31,49 @@ const sseRetryTimer = ref(null)
const archiveLoading = ref(false) const archiveLoading = ref(false)
const sampleImage = const sampleImage =
'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="220" height="140" viewBox="0 0 220 140"><defs><linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%"><stop stop-color="%23e0e9ff" offset="0%"/><stop stop-color="%23c7d8ff" offset="50%"/><stop stop-color="%23e8f1ff" offset="100%"/></linearGradient></defs><rect width="220" height="140" rx="12" fill="url(%23g)"/><text x="50%" y="52%" dominant-baseline="middle" text-anchor="middle" fill="%231d4ed8" font-family="Arial" font-size="16">mock image</text></svg>' 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="220" height="140" viewBox="0 0 220 140"><defs><linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%"><stop stop-color="%23e0e9ff" offset="0%"/><stop stop-color="%23c7d8ff" offset="50%"/><stop stop-color="%23e8f1ff" offset="100%"/></linearGradient></defs><rect width="220" height="140" rx="12" fill="url(%23g)"/><text x="50%" y="52%" dominant-baseline="middle" text-anchor="middle" fill="%231d4ed8" font-family="Arial" font-size="16">mock image</text></svg>'
const markdown = new MarkdownIt({
html: false,
linkify: true,
breaks: true,
typographer: true,
})
const defaultFence = markdown.renderer.rules.fence
markdown.renderer.rules.fence = (tokens, idx, options, env, self) => {
const token = tokens[idx]
const lang = (token.info || '').trim().toLowerCase()
if (lang === 'mermaid') {
const code = markdown.utils.escapeHtml(token.content || '')
return `<div class="mermaid">${code}</div>`
}
return defaultFence
? defaultFence(tokens, idx, options, env, self)
: self.renderToken(tokens, idx, options)
}
const defaultLinkOpen = markdown.renderer.rules.link_open
markdown.renderer.rules.link_open = (tokens, idx, options, env, self) => {
tokens[idx].attrSet('target', '_blank')
tokens[idx].attrSet('rel', 'noreferrer noopener')
return defaultLinkOpen
? defaultLinkOpen(tokens, idx, options, env, self)
: self.renderToken(tokens, idx, options)
}
const renderMarkdown = (text = '') => markdown.render(text || '')
let mermaidInitialized = false
const setupMermaid = () => {
if (mermaidInitialized || typeof window === 'undefined') return
mermaid.initialize({ startOnLoad: false, securityLevel: 'strict' })
mermaidInitialized = true
}
const renderMermaid = () => {
if (typeof window === 'undefined') return
const nodes = document.querySelectorAll('.bubble-text .mermaid:not([data-processed])')
if (!nodes.length) return
mermaid
.run({ nodes })
.catch(() => {
/* ignore render errors */
})
}
const sessionId = ref( const sessionId = ref(
route.params.id route.params.id
@@ -759,9 +804,24 @@ watch(
{ immediate: true } { immediate: true }
) )
watch(
messages,
() => {
nextTick(() => {
setupMermaid()
renderMermaid()
})
},
{ deep: true }
)
onMounted(() => { onMounted(() => {
setupMermaid()
window.addEventListener('scroll', handleWindowScroll, { passive: true }) window.addEventListener('scroll', handleWindowScroll, { passive: true })
nextTick(() => handleWindowScroll()) nextTick(() => {
handleWindowScroll()
renderMermaid()
})
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
@@ -892,7 +952,10 @@ onBeforeUnmount(() => {
</div> </div>
</template> </template>
<template v-else> <template v-else>
<p class="bubble-text">{{ msg.text }}</p> <div
class="bubble-text"
v-html="renderMarkdown(msg.text)"
/>
</template> </template>
<div v-if="msg.attachments?.length" class="attachment-list"> <div v-if="msg.attachments?.length" class="attachment-list">
<div <div
@@ -1006,6 +1069,7 @@ onBeforeUnmount(() => {
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
min-height: 80vh; min-height: 80vh;
color: var(--text-primary);
} }
.session-hero { .session-hero {
@@ -1014,14 +1078,14 @@ onBeforeUnmount(() => {
gap: 18px; gap: 18px;
padding: 18px 20px; padding: 18px 20px;
border-radius: 16px; border-radius: 16px;
background: linear-gradient(135deg, #f5f8ff, #edf2ff); background: var(--panel-hero-bg);
border: 1px solid rgba(0, 0, 0, 0.04); border: 1px solid var(--panel-hero-border);
box-shadow: 0 18px 38px rgba(17, 24, 39, 0.08); box-shadow: var(--shadow-1);
} }
.hero-left h2 { .hero-left h2 {
margin: 6px 0; margin: 6px 0;
color: #0f172a; color: var(--text-primary);
font-size: 24px; font-size: 24px;
} }
@@ -1029,7 +1093,7 @@ onBeforeUnmount(() => {
margin: 0; margin: 0;
font-size: 13px; font-size: 13px;
letter-spacing: 0.08em; letter-spacing: 0.08em;
color: #6b7280; color: var(--text-muted);
text-transform: uppercase; text-transform: uppercase;
} }
@@ -1037,12 +1101,12 @@ onBeforeUnmount(() => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
color: #6b7280; color: var(--text-muted);
font-size: 13px; font-size: 13px;
} }
.muted { .muted {
color: #6b7280; color: var(--text-muted);
} }
.chips { .chips {
@@ -1055,8 +1119,8 @@ onBeforeUnmount(() => {
.chip { .chip {
padding: 6px 10px; padding: 6px 10px;
border-radius: 10px; border-radius: 10px;
background: rgba(37, 99, 235, 0.08); background: var(--chip-bg);
color: #1d4ed8; color: var(--chip-text);
font-size: 12px; font-size: 12px;
letter-spacing: 0.01em; letter-spacing: 0.01em;
} }
@@ -1071,14 +1135,14 @@ onBeforeUnmount(() => {
.stat { .stat {
padding: 12px 14px; padding: 12px 14px;
border-radius: 12px; border-radius: 12px;
background: rgba(255, 255, 255, 0.85); background: var(--surface);
border: 1px solid rgba(0, 0, 0, 0.04); border: 1px solid var(--border-soft);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
} }
.stat-label { .stat-label {
margin: 0 0 4px; margin: 0 0 4px;
color: #6b7280; color: var(--text-muted);
font-size: 13px; font-size: 13px;
} }
@@ -1086,7 +1150,7 @@ onBeforeUnmount(() => {
margin: 0; margin: 0;
font-size: 20px; font-size: 20px;
font-weight: 700; font-weight: 700;
color: #111827; color: var(--text-primary);
} }
.chat-window { .chat-window {
@@ -1095,9 +1159,9 @@ onBeforeUnmount(() => {
gap: 12px; gap: 12px;
padding: 16px; padding: 16px;
border-radius: 16px; border-radius: 16px;
background: rgba(255, 255, 255, 0.9); background: var(--surface);
border: 1px solid rgba(0, 0, 0, 0.04); border: 1px solid var(--border-soft);
box-shadow: 0 18px 32px rgba(17, 24, 39, 0.08); box-shadow: var(--shadow-1);
align-items: center; align-items: center;
} }
@@ -1105,8 +1169,8 @@ onBeforeUnmount(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 12px;
width: min(980px, 100%); width: min(1120px, 100%);
padding: 0 12px; padding: 0 16px;
margin: 0 auto; margin: 0 auto;
} }
@@ -1127,13 +1191,13 @@ onBeforeUnmount(() => {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 8px 0; padding: 8px 0;
background: linear-gradient(180deg, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.8)); background: linear-gradient(180deg, transparent, var(--overlay-weak));
} }
.jump-down { .jump-down {
border: 1px solid rgba(37, 99, 235, 0.2); border: 1px solid rgba(37, 99, 235, 0.2);
background: rgba(255, 255, 255, 0.92); background: var(--surface-strong);
color: #1d4ed8; color: var(--accent);
border-radius: 999px; border-radius: 999px;
padding: 6px 12px; padding: 6px 12px;
box-shadow: 0 10px 20px rgba(17, 24, 39, 0.08); box-shadow: 0 10px 20px rgba(17, 24, 39, 0.08);
@@ -1149,28 +1213,27 @@ onBeforeUnmount(() => {
.bubble { .bubble {
padding: 14px 16px; padding: 14px 16px;
border-radius: 16px; border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid var(--border-soft);
max-width: 820px; max-width: 1040px;
box-shadow: 0 10px 20px rgba(17, 24, 39, 0.06); box-shadow: var(--shadow-2);
width: calc(100% - 48px); width: min(1040px, calc(100% - 24px));
align-self: center; align-self: center;
background: #ffffff; background: var(--surface-strong);
word-break: break-word; word-break: break-word;
white-space: pre-wrap;
position: relative; position: relative;
} }
.bubble-user { .bubble-user {
border-color: rgba(37, 99, 235, 0.18); border-color: var(--bubble-user-border);
background: linear-gradient(135deg, #edf2ff, #e5edff); background: var(--bubble-user-bg);
} }
.bubble-agent { .bubble-agent {
border-color: rgba(0, 0, 0, 0.04); border-color: var(--bubble-agent-border);
} }
.bubble-system { .bubble-system {
border-color: rgba(239, 68, 68, 0.18); border-color: var(--bubble-system-border);
background: linear-gradient(135deg, #fff1f2, #ffe4e6); background: var(--bubble-system-bg);
} }
.bubble-head { .bubble-head {
@@ -1182,29 +1245,93 @@ onBeforeUnmount(() => {
.author { .author {
font-weight: 700; font-weight: 700;
color: #0f172a; color: var(--text-primary);
} }
.time { .time {
font-size: 12px; font-size: 12px;
color: #9ca3af; color: var(--text-muted);
} }
.bubble-text { .bubble-text {
margin: 0 0 8px; margin: 0 0 8px;
color: #111827; color: var(--text-primary);
line-height: 1.7; line-height: 1.7;
font-size: 15px; font-size: 15px;
} }
.bubble-text :deep(p) {
margin: 0 0 10px;
}
.bubble-text :deep(p:last-child) {
margin-bottom: 0;
}
.bubble-text :deep(ul),
.bubble-text :deep(ol) {
padding-left: 20px;
margin: 0 0 12px;
}
.bubble-text :deep(code) {
background: var(--surface-subtle);
border-radius: 6px;
padding: 2px 6px;
font-size: 13px;
}
.bubble-text :deep(pre) {
margin: 10px 0;
padding: 12px;
border-radius: 10px;
background: var(--code-bg);
color: var(--code-text);
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
overflow: auto;
border: 1px solid var(--code-border);
}
.bubble-text :deep(table) {
width: 100%;
margin: 12px 0;
border-collapse: separate;
border-spacing: 0;
border: 1.5px solid var(--table-border);
border-radius: 12px;
overflow: hidden;
background: var(--surface-strong);
}
.bubble-text :deep(th),
.bubble-text :deep(td) {
padding: 10px 12px;
border-bottom: 1px solid var(--table-divider);
border-right: 1px solid var(--table-divider);
color: var(--text-primary);
text-align: left;
}
.bubble-text :deep(tr:last-child td) {
border-bottom: none;
}
.bubble-text :deep(th:last-child),
.bubble-text :deep(td:last-child) {
border-right: none;
}
.bubble-text :deep(th) {
background: linear-gradient(180deg, var(--surface), var(--surface-subtle));
font-weight: 700;
}
.bubble-text :deep(.mermaid) {
margin: 10px 0;
padding: 12px;
border-radius: 12px;
background: linear-gradient(135deg, var(--mermaid-bg1), var(--mermaid-bg2));
border: 1px solid rgba(37, 99, 235, 0.14);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
}
.error-card { .error-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
padding: 10px 12px; padding: 10px 12px;
border-radius: 12px; border-radius: 12px;
background: linear-gradient(135deg, rgba(239, 68, 68, 0.12), rgba(248, 113, 113, 0.1)); background: var(--error-card-bg);
border: 1px solid rgba(239, 68, 68, 0.22); border: 1px solid var(--error-card-border);
box-shadow: 0 8px 16px rgba(239, 68, 68, 0.12); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
} }
.error-title { .error-title {
color: #b91c1c; color: #b91c1c;
@@ -1214,15 +1341,15 @@ onBeforeUnmount(() => {
align-self: flex-start; align-self: flex-start;
padding: 6px 10px; padding: 6px 10px;
border-radius: 10px; border-radius: 10px;
border: 1px solid rgba(185, 28, 28, 0.4); border: 1px solid var(--error-card-border);
background: #fff5f5; background: var(--surface-subtle);
color: #b91c1c; color: #f87171;
cursor: pointer; cursor: pointer;
transition: all 0.12s ease; transition: all 0.12s ease;
font-weight: 600; font-weight: 600;
} }
.payload-toggle:hover { .payload-toggle:hover {
background: #fee2e2; background: rgba(248, 113, 113, 0.12);
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
} }
@@ -1230,8 +1357,8 @@ onBeforeUnmount(() => {
margin: 0; margin: 0;
padding: 12px; padding: 12px;
border-radius: 10px; border-radius: 10px;
background: #0f172a; background: var(--code-bg);
color: #e2e8f0; color: var(--code-text);
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 13px; font-size: 13px;
overflow: auto; overflow: auto;
@@ -1252,7 +1379,7 @@ onBeforeUnmount(() => {
align-items: center; align-items: center;
padding: 8px 10px; padding: 8px 10px;
border-radius: 10px; border-radius: 10px;
background: rgba(15, 23, 42, 0.03); background: var(--attachment-bg);
word-break: break-word; word-break: break-word;
} }
@@ -1266,19 +1393,19 @@ onBeforeUnmount(() => {
border-radius: 12px; border-radius: 12px;
background-size: cover; background-size: cover;
background-position: center; background-position: center;
background-image: linear-gradient(135deg, #e5edff, #dbeafe); background-image: linear-gradient(135deg, var(--mermaid-bg1), var(--mermaid-bg2));
border: 1px solid rgba(0, 0, 0, 0.04); border: 1px solid var(--border-soft);
} }
.attachment-meta .name { .attachment-meta .name {
display: block; display: block;
font-weight: 600; font-weight: 600;
color: #0f172a; color: var(--text-primary);
} }
.attachment-meta .size { .attachment-meta .size {
display: block; display: block;
color: #6b7280; color: var(--text-muted);
font-size: 12px; font-size: 12px;
} }
@@ -1289,21 +1416,21 @@ onBeforeUnmount(() => {
align-items: flex-start; align-items: flex-start;
padding: 14px; padding: 14px;
border-radius: 14px; border-radius: 14px;
background: #f7f9fc; background: var(--composer-bg);
border: 1px solid rgba(0, 0, 0, 0.04); border: 1px solid var(--border-soft);
width: min(980px, 100%); width: min(1120px, 100%);
box-shadow: 0 12px 30px rgba(17, 24, 39, 0.12); box-shadow: var(--shadow-1);
} }
.add-btn { .add-btn {
width: 44px; width: 44px;
height: 44px; height: 44px;
border-radius: 12px; border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.08); border: 1px solid var(--border-soft);
background: #ffffff; background: var(--surface-strong);
box-shadow: 0 8px 18px rgba(17, 24, 39, 0.08); box-shadow: 0 8px 18px rgba(17, 24, 39, 0.08);
font-size: 24px; font-size: 24px;
color: #1d4ed8; color: var(--accent);
cursor: pointer; cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease; transition: transform 0.15s ease, box-shadow 0.15s ease;
} }
@@ -1314,9 +1441,9 @@ onBeforeUnmount(() => {
} }
.composer-input :deep(.el-textarea__inner) { .composer-input :deep(.el-textarea__inner) {
background: #ffffff; background: var(--input-bg);
border-radius: 12px; border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.06); border: 1px solid var(--border-soft);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
min-height: 110px; min-height: 110px;
} }
@@ -1337,7 +1464,7 @@ onBeforeUnmount(() => {
.send-hint { .send-hint {
font-size: 12px; font-size: 12px;
color: #9ca3af; color: var(--text-muted);
letter-spacing: 0.02em; letter-spacing: 0.02em;
} }
@@ -1347,23 +1474,23 @@ onBeforeUnmount(() => {
justify-content: center; justify-content: center;
padding: 2px 6px; padding: 2px 6px;
border-radius: 8px; border-radius: 8px;
border: 1px solid rgba(15, 23, 42, 0.12); border: 1px solid var(--border-soft);
background: #ffffff; background: var(--surface-strong);
color: #6b7280; color: var(--text-muted);
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
} }
.pending-attachments { .pending-attachments {
padding: 10px 0 0; padding: 10px 0 0;
border-top: 1px dashed rgba(0, 0, 0, 0.08); border-top: 1px dashed var(--border-soft);
margin-top: 4px; margin-top: 4px;
} }
.pending-title { .pending-title {
margin: 0 0 8px; margin: 0 0 8px;
font-size: 13px; font-size: 13px;
color: #6b7280; color: var(--text-muted);
} }
.pending-list { .pending-list {
@@ -1379,8 +1506,8 @@ onBeforeUnmount(() => {
align-items: center; align-items: center;
padding: 8px 10px; padding: 8px 10px;
border-radius: 12px; border-radius: 12px;
background: rgba(37, 99, 235, 0.06); background: var(--pending-bg);
border: 1px solid rgba(37, 99, 235, 0.15); border: 1px solid var(--pending-border);
} }
.pending-thumb { .pending-thumb {
@@ -1395,18 +1522,18 @@ onBeforeUnmount(() => {
.pending-meta .name { .pending-meta .name {
display: block; display: block;
font-weight: 600; font-weight: 600;
color: #0f172a; color: var(--text-primary);
} }
.pending-meta .size { .pending-meta .size {
display: block; display: block;
font-size: 12px; font-size: 12px;
color: #4b5563; color: var(--text-subtle);
} }
.remove { .remove {
cursor: pointer; cursor: pointer;
color: #111827; color: var(--text-primary);
font-size: 16px; font-size: 16px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;

View File

@@ -115,6 +115,7 @@ const handleLogin = async () => {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 32px; gap: 32px;
align-items: center; align-items: center;
color: var(--text-primary);
} }
.copy { .copy {
@@ -124,7 +125,7 @@ const handleLogin = async () => {
.eyebrow { .eyebrow {
margin: 0 0 6px; margin: 0 0 6px;
font-size: 13px; font-size: 13px;
color: #6b7280; color: var(--text-muted);
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-transform: uppercase; text-transform: uppercase;
} }
@@ -132,24 +133,25 @@ const handleLogin = async () => {
h1 { h1 {
margin: 0 0 8px; margin: 0 0 8px;
font-size: 32px; font-size: 32px;
color: #0f172a; color: var(--text-primary);
} }
.lede { .lede {
margin: 0 0 12px; margin: 0 0 12px;
color: #4b5563; color: var(--text-subtle);
} }
.hint { .hint {
margin: 0; margin: 0;
font-size: 13px; font-size: 13px;
color: #6b7280; color: var(--text-muted);
} }
.login-card { .login-card {
border-radius: 14px; border-radius: 14px;
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid var(--border-soft);
box-shadow: 0 18px 45px rgba(17, 24, 39, 0.08); box-shadow: var(--shadow-1);
background: var(--surface);
} }
.card-header { .card-header {
@@ -157,7 +159,7 @@ h1 {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-weight: 700; font-weight: 700;
color: #0f172a; color: var(--text-primary);
} }
.dot { .dot {
@@ -178,7 +180,7 @@ h1 {
.tips { .tips {
margin-top: 16px; margin-top: 16px;
font-size: 13px; font-size: 13px;
color: #6b7280; color: var(--text-muted);
} }
.mt { .mt {
@@ -190,17 +192,17 @@ h1 {
} }
:deep(.el-form-item__label) { :deep(.el-form-item__label) {
color: #334155; color: var(--text-primary);
letter-spacing: 0.02em; letter-spacing: 0.02em;
} }
:deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
background: #f8fafc; background: var(--surface-subtle);
border: 1px solid rgba(0, 0, 0, 0.06); border: 1px solid var(--border-soft);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
} }
:deep(.el-input__inner) { :deep(.el-input__inner) {
color: #0f172a; color: var(--text-primary);
} }
</style> </style>

View File

@@ -315,19 +315,20 @@ onMounted(() => {
<style scoped> <style scoped>
.users { .users {
position: relative; position: relative;
max-width: 1280px; max-width: 1100px;
width: 100%;
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 14px; gap: 14px;
z-index: 1; z-index: 1;
color: var(--text-primary);
} }
.hero { .hero {
border-radius: 16px; border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid var(--border-soft);
box-shadow: 0 18px 45px rgba(17, 24, 39, 0.08); box-shadow: var(--shadow-1);
background: var(--surface);
} }
.hero-head { .hero-head {
@@ -341,7 +342,7 @@ onMounted(() => {
.eyebrow { .eyebrow {
margin: 0 0 6px; margin: 0 0 6px;
font-size: 12px; font-size: 12px;
color: #6b7280; color: var(--text-muted);
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-transform: uppercase; text-transform: uppercase;
} }
@@ -349,18 +350,18 @@ onMounted(() => {
h1 { h1 {
margin: 0 0 8px; margin: 0 0 8px;
font-size: 30px; font-size: 30px;
color: #0f172a; color: var(--text-primary);
} }
.lede { .lede {
margin: 0 0 8px; margin: 0 0 8px;
color: #4b5563; color: var(--text-subtle);
line-height: 1.6; line-height: 1.6;
} }
.hint { .hint {
margin: 0; margin: 0;
color: #6b7280; color: var(--text-muted);
font-size: 13px; font-size: 13px;
} }
@@ -372,8 +373,9 @@ h1 {
.panel { .panel {
border-radius: 16px; border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid var(--border-soft);
box-shadow: 0 14px 35px rgba(17, 24, 39, 0.06); box-shadow: var(--shadow-2);
background: var(--surface);
} }
.card-header { .card-header {
@@ -381,7 +383,7 @@ h1 {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-weight: 700; font-weight: 700;
color: #0f172a; color: var(--text-primary);
} }
.user-cell { .user-cell {
@@ -392,12 +394,12 @@ h1 {
.name { .name {
font-weight: 600; font-weight: 600;
color: #0f172a; color: var(--text-primary);
} }
.email { .email {
font-size: 13px; font-size: 13px;
color: #6b7280; color: var(--text-muted);
} }
.pagination { .pagination {

View File

@@ -3,6 +3,7 @@ import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import axios from 'axios' import axios from 'axios'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useTheme } from '../composables/useTheme'
const router = useRouter() const router = useRouter()
const apiBase = import.meta.env.VITE_API_BASE || 'http://localhost:8000/api' const apiBase = import.meta.env.VITE_API_BASE || 'http://localhost:8000/api'
@@ -18,6 +19,7 @@ const loadingUser = ref(false)
const greeting = computed( const greeting = computed(
() => user.value?.name || user.value?.email || '访客' () => user.value?.name || user.value?.email || '访客'
) )
const { themeMode, resolvedTheme, setTheme } = useTheme()
const logout = () => { const logout = () => {
localStorage.removeItem('ars-token') localStorage.removeItem('ars-token')
@@ -65,6 +67,18 @@ onMounted(() => {
</p> </p>
</div> </div>
<div class="actions"> <div class="actions">
<div class="theme-switch">
<p class="theme-label">主题 · {{ resolvedTheme === 'dark' ? '深色' : resolvedTheme === 'light' ? '浅色' : '系统' }}</p>
<el-radio-group
v-model="themeMode"
size="small"
@change="setTheme"
>
<el-radio-button label="system">跟随系统</el-radio-button>
<el-radio-button label="light">浅色</el-radio-button>
<el-radio-button label="dark">深色</el-radio-button>
</el-radio-group>
</div>
<el-button <el-button
color="#1d4ed8" color="#1d4ed8"
:loading="loadingUser" :loading="loadingUser"
@@ -126,12 +140,14 @@ onMounted(() => {
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
gap: 18px; gap: 18px;
z-index: 1; z-index: 1;
color: var(--text-primary);
} }
.hero { .hero {
border-radius: 16px; border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid var(--border-soft);
box-shadow: 0 18px 45px rgba(17, 24, 39, 0.08); box-shadow: var(--shadow-1);
background: var(--surface);
} }
.hero-header { .hero-header {
@@ -144,7 +160,7 @@ onMounted(() => {
.eyebrow { .eyebrow {
margin: 0 0 6px; margin: 0 0 6px;
font-size: 12px; font-size: 12px;
color: #6b7280; color: var(--text-muted);
letter-spacing: 0.1em; letter-spacing: 0.1em;
text-transform: uppercase; text-transform: uppercase;
} }
@@ -152,12 +168,12 @@ onMounted(() => {
h1 { h1 {
margin: 0 0 8px; margin: 0 0 8px;
font-size: 30px; font-size: 30px;
color: #0f172a; color: var(--text-primary);
} }
.lede { .lede {
margin: 0 0 12px; margin: 0 0 12px;
color: #4b5563; color: var(--text-subtle);
line-height: 1.6; line-height: 1.6;
} }
@@ -167,10 +183,27 @@ h1 {
flex-wrap: wrap; flex-wrap: wrap;
} }
.theme-switch {
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px 10px;
border-radius: 12px;
border: 1px solid var(--border-soft);
background: var(--surface-subtle);
}
.theme-label {
margin: 0;
font-size: 13px;
color: var(--text-muted);
}
.panel { .panel {
border-radius: 16px; border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid var(--border-soft);
box-shadow: 0 14px 35px rgba(17, 24, 39, 0.06); box-shadow: var(--shadow-2);
background: var(--surface);
} }
.card-header { .card-header {
@@ -178,7 +211,7 @@ h1 {
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-weight: 700; font-weight: 700;
color: #0f172a; color: var(--text-primary);
} }
.links { .links {
@@ -194,14 +227,14 @@ h1 {
justify-content: space-between; justify-content: space-between;
gap: 10px; gap: 10px;
padding: 12px; padding: 12px;
background: #f8fafc; background: var(--surface-subtle);
border: 1px solid rgba(0, 0, 0, 0.04); border: 1px solid var(--border-soft);
border-radius: 10px; border-radius: 10px;
color: #111827; color: var(--text-primary);
} }
.links code { .links code {
color: #475569; color: var(--text-subtle);
font-size: 13px; font-size: 13px;
word-break: break-all; word-break: break-all;
text-align: right; text-align: right;
@@ -223,14 +256,14 @@ h1 {
.stat-label { .stat-label {
margin: 0 0 4px; margin: 0 0 4px;
color: #6b7280; color: var(--text-muted);
font-size: 13px; font-size: 13px;
} }
.stat-value { .stat-value {
margin: 0; margin: 0;
font-weight: 700; font-weight: 700;
color: #0f172a; color: var(--text-primary);
} }
.mono { .mono {
@@ -239,7 +272,7 @@ h1 {
} }
.link-accent { .link-accent {
color: #2563eb; color: var(--accent-strong);
font-weight: 600; font-weight: 600;
} }
</style> </style>

View File

@@ -12,6 +12,12 @@ vi.mock('axios', () => ({
post: vi.fn(), post: vi.fn(),
}, },
})) }))
vi.mock('mermaid', () => ({
default: {
initialize: vi.fn(),
run: vi.fn().mockResolvedValue(),
},
}))
const mockedAxios = axios const mockedAxios = axios
let latestEventSource = null let latestEventSource = null
@@ -451,6 +457,67 @@ describe('ChatView message.delta handling', () => {
}) })
}) })
describe('ChatView markdown rendering', () => {
beforeEach(() => {
localStorage.clear()
vi.clearAllMocks()
latestEventSource = null
globalThis.EventSource = MockEventSource
globalThis.scrollTo = vi.fn()
})
it('renders markdown content and mermaid blocks', async () => {
localStorage.setItem('ars-token', 'token')
const router = makeRouter()
router.push('/chat/session-1')
await router.isReady()
const markdownText = ['Hello **world**', '', '```mermaid', 'graph TD;', 'A-->B;', '```'].join(
'\n'
)
const list = [
buildMessage({
seq: 1,
role: 'AGENT',
type: 'agent.message',
content: markdownText,
}),
]
mockedAxios.get.mockImplementation((url) => {
if (url.endsWith('/sessions/session-1')) {
return Promise.resolve({
data: { session_name: 'Demo', status: 'OPEN', last_seq: 1 },
})
}
if (url.endsWith('/sessions/session-1/messages')) {
return Promise.resolve({ data: { data: list } })
}
return Promise.resolve({ data: {} })
})
const wrapper = mount(ChatView, {
global: {
plugins: [router, ElementPlus],
},
})
await flushPromises()
await nextTick()
const bubble = wrapper.find('.bubble-agent')
expect(bubble.exists()).toBe(true)
const textBlock = bubble.find('.bubble-text')
expect(textBlock.html()).toContain('<strong>world</strong>')
const mermaidBlock = textBlock.find('.mermaid')
expect(mermaidBlock.exists()).toBe(true)
expect(mermaidBlock.text()).toContain('A-->B')
})
})
describe('ChatView system error and failed status handling', () => { describe('ChatView system error and failed status handling', () => {
beforeEach(() => { beforeEach(() => {
localStorage.clear() localStorage.clear()