main: 增加全局暗黑模式支持及 Markdown 渲染功能
- 实现暗黑模式切换逻辑,支持系统主题适配 - 优化全局样式,添加动态主题变量及暗黑样式 - 引入 `markdown-it` 和 `mermaid`,支持 Markdown 内容及流程图渲染 - 更新 ChatView 测试用例,验证 Markdown 和 Mermaid 渲染效果 - 修改 package.json,增加新依赖
This commit is contained in:
1270
package-lock.json
generated
1270
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,8 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"element-plus": "^2.12.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"mermaid": "^11.12.2",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
|
||||
37
src/App.vue
37
src/App.vue
@@ -68,8 +68,8 @@ const miniMenuOpen = ref(false)
|
||||
<style scoped>
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
background: 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));
|
||||
background: var(--app-bg);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.auth-shell {
|
||||
@@ -83,7 +83,7 @@ const miniMenuOpen = ref(false)
|
||||
.auth-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, #e9eef7, #f6f8fc);
|
||||
background: var(--auth-bg);
|
||||
filter: blur(0);
|
||||
z-index: 0;
|
||||
}
|
||||
@@ -92,10 +92,10 @@ const miniMenuOpen = ref(false)
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: min(960px, 92vw);
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 30px 80px rgba(36, 49, 89, 0.16);
|
||||
box-shadow: var(--shadow-1);
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 32px;
|
||||
}
|
||||
@@ -105,6 +105,7 @@ const miniMenuOpen = ref(false)
|
||||
grid-template-columns: 300px 1fr;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
background: var(--bg-page);
|
||||
}
|
||||
|
||||
.main {
|
||||
@@ -119,7 +120,7 @@ const miniMenuOpen = ref(false)
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.page {
|
||||
@@ -130,15 +131,15 @@ const miniMenuOpen = ref(false)
|
||||
height: 46px;
|
||||
padding: 0 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 8px 22px rgba(17, 24, 39, 0.14);
|
||||
border: 1px solid var(--border-soft);
|
||||
background: var(--surface-strong);
|
||||
box-shadow: var(--shadow-2);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||||
color: #111827;
|
||||
color: var(--text-primary);
|
||||
min-width: 104px;
|
||||
}
|
||||
|
||||
@@ -149,13 +150,13 @@ const miniMenuOpen = ref(false)
|
||||
|
||||
.mini-icon {
|
||||
font-size: 15px;
|
||||
color: #1d4ed8;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.mini-label {
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
color: #111827;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.mini-menu {
|
||||
@@ -168,25 +169,25 @@ const miniMenuOpen = ref(false)
|
||||
.mini-title {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mini-link {
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
color: #1f2937;
|
||||
color: var(--text-primary);
|
||||
transition: all 0.16s ease;
|
||||
}
|
||||
|
||||
.mini-link:hover {
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
color: #1d4ed8;
|
||||
background: var(--chip-bg);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.mini-popper {
|
||||
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) {
|
||||
|
||||
@@ -109,19 +109,19 @@ const createSession = async () => {
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
box-shadow: 0 8px 20px rgba(17, 24, 39, 0.12);
|
||||
border: 1px solid var(--border-soft);
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow-2);
|
||||
cursor: pointer;
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||||
color: #111827;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.new-chat-btn.inline {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
box-shadow: none;
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.new-chat-btn:hover {
|
||||
@@ -148,7 +148,7 @@ const createSession = async () => {
|
||||
.title {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
@@ -159,7 +159,7 @@ const createSession = async () => {
|
||||
.last {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #4b5563;
|
||||
color: var(--text-subtle);
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -219,16 +219,16 @@ onMounted(() => {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: 18px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: var(--surface-glass);
|
||||
border-right: 1px solid var(--border-soft);
|
||||
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;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
overflow-y: auto;
|
||||
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 {
|
||||
@@ -236,7 +236,11 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -261,7 +265,7 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: 0.02em;
|
||||
font-size: 18px;
|
||||
}
|
||||
@@ -288,9 +292,9 @@ onMounted(() => {
|
||||
.session-item {
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 6px 14px rgba(17, 24, 39, 0.06);
|
||||
border: 1px solid var(--border-soft);
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow-2);
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.16s ease;
|
||||
@@ -315,12 +319,12 @@ onMounted(() => {
|
||||
|
||||
.name {
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.preview {
|
||||
margin: 6px 0 4px;
|
||||
color: #4b5563;
|
||||
color: var(--text-subtle);
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
max-height: 38px;
|
||||
@@ -329,7 +333,7 @@ onMounted(() => {
|
||||
|
||||
.meta {
|
||||
margin: 0;
|
||||
color: #9ca3af;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -338,7 +342,7 @@ onMounted(() => {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.error {
|
||||
@@ -349,20 +353,20 @@ onMounted(() => {
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
border: 1px dashed rgba(0, 0, 0, 0.1);
|
||||
color: #1f2937;
|
||||
border: 1px dashed var(--border-contrast);
|
||||
color: var(--text-primary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
letter-spacing: 0.02em;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
background: var(--surface);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.load-more:hover {
|
||||
border-color: rgba(37, 99, 235, 0.35);
|
||||
color: #1d4ed8;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.load-more:disabled {
|
||||
@@ -382,8 +386,8 @@ onMounted(() => {
|
||||
position: static;
|
||||
overflow: visible;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 8px 24px rgba(17, 24, 39, 0.08);
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.session-item {
|
||||
|
||||
63
src/composables/useTheme.js
Normal file
63
src/composables/useTheme.js
Normal 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,
|
||||
}
|
||||
}
|
||||
25
src/main.js
25
src/main.js
@@ -1,8 +1,33 @@
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
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')
|
||||
|
||||
105
src/style.css
105
src/style.css
@@ -3,11 +3,107 @@
|
||||
Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
font-weight: 400;
|
||||
color: #111827;
|
||||
background-color: #e9edf3;
|
||||
color: var(--text-primary);
|
||||
background-color: var(--bg-page);
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-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 {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background-color: #e9edf3;
|
||||
color: #111827;
|
||||
background-color: var(--bg-page);
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.25s ease, color 0.25s ease;
|
||||
}
|
||||
|
||||
a {
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import axios from 'axios'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import mermaid from 'mermaid'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -29,6 +31,49 @@ const sseRetryTimer = ref(null)
|
||||
const archiveLoading = ref(false)
|
||||
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>'
|
||||
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(
|
||||
route.params.id
|
||||
@@ -759,9 +804,24 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
messages,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
setupMermaid()
|
||||
renderMermaid()
|
||||
})
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
setupMermaid()
|
||||
window.addEventListener('scroll', handleWindowScroll, { passive: true })
|
||||
nextTick(() => handleWindowScroll())
|
||||
nextTick(() => {
|
||||
handleWindowScroll()
|
||||
renderMermaid()
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -892,7 +952,10 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="bubble-text">{{ msg.text }}</p>
|
||||
<div
|
||||
class="bubble-text"
|
||||
v-html="renderMarkdown(msg.text)"
|
||||
/>
|
||||
</template>
|
||||
<div v-if="msg.attachments?.length" class="attachment-list">
|
||||
<div
|
||||
@@ -1006,6 +1069,7 @@ onBeforeUnmount(() => {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 80vh;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.session-hero {
|
||||
@@ -1014,14 +1078,14 @@ onBeforeUnmount(() => {
|
||||
gap: 18px;
|
||||
padding: 18px 20px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, #f5f8ff, #edf2ff);
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
box-shadow: 0 18px 38px rgba(17, 24, 39, 0.08);
|
||||
background: var(--panel-hero-bg);
|
||||
border: 1px solid var(--panel-hero-border);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
|
||||
.hero-left h2 {
|
||||
margin: 6px 0;
|
||||
color: #0f172a;
|
||||
color: var(--text-primary);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
@@ -1029,7 +1093,7 @@ onBeforeUnmount(() => {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.08em;
|
||||
color: #6b7280;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@@ -1037,12 +1101,12 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #6b7280;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #6b7280;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.chips {
|
||||
@@ -1055,8 +1119,8 @@ onBeforeUnmount(() => {
|
||||
.chip {
|
||||
padding: 6px 10px;
|
||||
border-radius: 10px;
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
color: #1d4ed8;
|
||||
background: var(--chip-bg);
|
||||
color: var(--chip-text);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
@@ -1071,14 +1135,14 @@ onBeforeUnmount(() => {
|
||||
.stat {
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-soft);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
margin: 0 0 4px;
|
||||
color: #6b7280;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@@ -1086,7 +1150,7 @@ onBeforeUnmount(() => {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.chat-window {
|
||||
@@ -1095,9 +1159,9 @@ onBeforeUnmount(() => {
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
box-shadow: 0 18px 32px rgba(17, 24, 39, 0.08);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-soft);
|
||||
box-shadow: var(--shadow-1);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -1105,8 +1169,8 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: min(980px, 100%);
|
||||
padding: 0 12px;
|
||||
width: min(1120px, 100%);
|
||||
padding: 0 16px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@@ -1127,13 +1191,13 @@ onBeforeUnmount(() => {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
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 {
|
||||
border: 1px solid rgba(37, 99, 235, 0.2);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: #1d4ed8;
|
||||
background: var(--surface-strong);
|
||||
color: var(--accent);
|
||||
border-radius: 999px;
|
||||
padding: 6px 12px;
|
||||
box-shadow: 0 10px 20px rgba(17, 24, 39, 0.08);
|
||||
@@ -1149,28 +1213,27 @@ onBeforeUnmount(() => {
|
||||
.bubble {
|
||||
padding: 14px 16px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
max-width: 820px;
|
||||
box-shadow: 0 10px 20px rgba(17, 24, 39, 0.06);
|
||||
width: calc(100% - 48px);
|
||||
border: 1px solid var(--border-soft);
|
||||
max-width: 1040px;
|
||||
box-shadow: var(--shadow-2);
|
||||
width: min(1040px, calc(100% - 24px));
|
||||
align-self: center;
|
||||
background: #ffffff;
|
||||
background: var(--surface-strong);
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bubble-user {
|
||||
border-color: rgba(37, 99, 235, 0.18);
|
||||
background: linear-gradient(135deg, #edf2ff, #e5edff);
|
||||
border-color: var(--bubble-user-border);
|
||||
background: var(--bubble-user-bg);
|
||||
}
|
||||
|
||||
.bubble-agent {
|
||||
border-color: rgba(0, 0, 0, 0.04);
|
||||
border-color: var(--bubble-agent-border);
|
||||
}
|
||||
.bubble-system {
|
||||
border-color: rgba(239, 68, 68, 0.18);
|
||||
background: linear-gradient(135deg, #fff1f2, #ffe4e6);
|
||||
border-color: var(--bubble-system-border);
|
||||
background: var(--bubble-system-bg);
|
||||
}
|
||||
|
||||
.bubble-head {
|
||||
@@ -1182,29 +1245,93 @@ onBeforeUnmount(() => {
|
||||
|
||||
.author {
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.bubble-text {
|
||||
margin: 0 0 8px;
|
||||
color: #111827;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.7;
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.12), rgba(248, 113, 113, 0.1));
|
||||
border: 1px solid rgba(239, 68, 68, 0.22);
|
||||
box-shadow: 0 8px 16px rgba(239, 68, 68, 0.12);
|
||||
background: var(--error-card-bg);
|
||||
border: 1px solid var(--error-card-border);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.error-title {
|
||||
color: #b91c1c;
|
||||
@@ -1214,15 +1341,15 @@ onBeforeUnmount(() => {
|
||||
align-self: flex-start;
|
||||
padding: 6px 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(185, 28, 28, 0.4);
|
||||
background: #fff5f5;
|
||||
color: #b91c1c;
|
||||
border: 1px solid var(--error-card-border);
|
||||
background: var(--surface-subtle);
|
||||
color: #f87171;
|
||||
cursor: pointer;
|
||||
transition: all 0.12s ease;
|
||||
font-weight: 600;
|
||||
}
|
||||
.payload-toggle:hover {
|
||||
background: #fee2e2;
|
||||
background: rgba(248, 113, 113, 0.12);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
@@ -1230,8 +1357,8 @@ onBeforeUnmount(() => {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
background: var(--code-bg);
|
||||
color: var(--code-text);
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 13px;
|
||||
overflow: auto;
|
||||
@@ -1252,7 +1379,7 @@ onBeforeUnmount(() => {
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
border-radius: 10px;
|
||||
background: rgba(15, 23, 42, 0.03);
|
||||
background: var(--attachment-bg);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@@ -1266,19 +1393,19 @@ onBeforeUnmount(() => {
|
||||
border-radius: 12px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-image: linear-gradient(135deg, #e5edff, #dbeafe);
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
background-image: linear-gradient(135deg, var(--mermaid-bg1), var(--mermaid-bg2));
|
||||
border: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.attachment-meta .name {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.attachment-meta .size {
|
||||
display: block;
|
||||
color: #6b7280;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -1289,21 +1416,21 @@ onBeforeUnmount(() => {
|
||||
align-items: flex-start;
|
||||
padding: 14px;
|
||||
border-radius: 14px;
|
||||
background: #f7f9fc;
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
width: min(980px, 100%);
|
||||
box-shadow: 0 12px 30px rgba(17, 24, 39, 0.12);
|
||||
background: var(--composer-bg);
|
||||
border: 1px solid var(--border-soft);
|
||||
width: min(1120px, 100%);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border-soft);
|
||||
background: var(--surface-strong);
|
||||
box-shadow: 0 8px 18px rgba(17, 24, 39, 0.08);
|
||||
font-size: 24px;
|
||||
color: #1d4ed8;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
@@ -1314,9 +1441,9 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.composer-input :deep(.el-textarea__inner) {
|
||||
background: #ffffff;
|
||||
background: var(--input-bg);
|
||||
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);
|
||||
min-height: 110px;
|
||||
}
|
||||
@@ -1337,7 +1464,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
.send-hint {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
@@ -1347,23 +1474,23 @@ onBeforeUnmount(() => {
|
||||
justify-content: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.12);
|
||||
background: #ffffff;
|
||||
color: #6b7280;
|
||||
border: 1px solid var(--border-soft);
|
||||
background: var(--surface-strong);
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pending-attachments {
|
||||
padding: 10px 0 0;
|
||||
border-top: 1px dashed rgba(0, 0, 0, 0.08);
|
||||
border-top: 1px dashed var(--border-soft);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.pending-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.pending-list {
|
||||
@@ -1379,8 +1506,8 @@ onBeforeUnmount(() => {
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
border-radius: 12px;
|
||||
background: rgba(37, 99, 235, 0.06);
|
||||
border: 1px solid rgba(37, 99, 235, 0.15);
|
||||
background: var(--pending-bg);
|
||||
border: 1px solid var(--pending-border);
|
||||
}
|
||||
|
||||
.pending-thumb {
|
||||
@@ -1395,18 +1522,18 @@ onBeforeUnmount(() => {
|
||||
.pending-meta .name {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.pending-meta .size {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #4b5563;
|
||||
color: var(--text-subtle);
|
||||
}
|
||||
|
||||
.remove {
|
||||
cursor: pointer;
|
||||
color: #111827;
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -115,6 +115,7 @@ const handleLogin = async () => {
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 32px;
|
||||
align-items: center;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.copy {
|
||||
@@ -124,7 +125,7 @@ const handleLogin = async () => {
|
||||
.eyebrow {
|
||||
margin: 0 0 6px;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -132,24 +133,25 @@ const handleLogin = async () => {
|
||||
h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 32px;
|
||||
color: #0f172a;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.lede {
|
||||
margin: 0 0 12px;
|
||||
color: #4b5563;
|
||||
color: var(--text-subtle);
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
box-shadow: 0 18px 45px rgba(17, 24, 39, 0.08);
|
||||
border: 1px solid var(--border-soft);
|
||||
box-shadow: var(--shadow-1);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
@@ -157,7 +159,7 @@ h1 {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dot {
|
||||
@@ -178,7 +180,7 @@ h1 {
|
||||
.tips {
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.mt {
|
||||
@@ -190,17 +192,17 @@ h1 {
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
color: #334155;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
background: #f8fafc;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
background: var(--surface-subtle);
|
||||
border: 1px solid var(--border-soft);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
color: #0f172a;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -315,19 +315,20 @@ onMounted(() => {
|
||||
<style scoped>
|
||||
.users {
|
||||
position: relative;
|
||||
max-width: 1280px;
|
||||
width: 100%;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
z-index: 1;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.hero {
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
box-shadow: 0 18px 45px rgba(17, 24, 39, 0.08);
|
||||
border: 1px solid var(--border-soft);
|
||||
box-shadow: var(--shadow-1);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.hero-head {
|
||||
@@ -341,7 +342,7 @@ onMounted(() => {
|
||||
.eyebrow {
|
||||
margin: 0 0 6px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -349,18 +350,18 @@ onMounted(() => {
|
||||
h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 30px;
|
||||
color: #0f172a;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.lede {
|
||||
margin: 0 0 8px;
|
||||
color: #4b5563;
|
||||
color: var(--text-subtle);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0;
|
||||
color: #6b7280;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@@ -372,8 +373,9 @@ h1 {
|
||||
|
||||
.panel {
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
box-shadow: 0 14px 35px rgba(17, 24, 39, 0.06);
|
||||
border: 1px solid var(--border-soft);
|
||||
box-shadow: var(--shadow-2);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
@@ -381,7 +383,7 @@ h1 {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.user-cell {
|
||||
@@ -392,12 +394,12 @@ h1 {
|
||||
|
||||
.name {
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.email {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useTheme } from '../composables/useTheme'
|
||||
|
||||
const router = useRouter()
|
||||
const apiBase = import.meta.env.VITE_API_BASE || 'http://localhost:8000/api'
|
||||
@@ -18,6 +19,7 @@ const loadingUser = ref(false)
|
||||
const greeting = computed(
|
||||
() => user.value?.name || user.value?.email || '访客'
|
||||
)
|
||||
const { themeMode, resolvedTheme, setTheme } = useTheme()
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('ars-token')
|
||||
@@ -65,6 +67,18 @@ onMounted(() => {
|
||||
</p>
|
||||
</div>
|
||||
<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
|
||||
color="#1d4ed8"
|
||||
:loading="loadingUser"
|
||||
@@ -126,12 +140,14 @@ onMounted(() => {
|
||||
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
||||
gap: 18px;
|
||||
z-index: 1;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.hero {
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
box-shadow: 0 18px 45px rgba(17, 24, 39, 0.08);
|
||||
border: 1px solid var(--border-soft);
|
||||
box-shadow: var(--shadow-1);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.hero-header {
|
||||
@@ -144,7 +160,7 @@ onMounted(() => {
|
||||
.eyebrow {
|
||||
margin: 0 0 6px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -152,12 +168,12 @@ onMounted(() => {
|
||||
h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 30px;
|
||||
color: #0f172a;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.lede {
|
||||
margin: 0 0 12px;
|
||||
color: #4b5563;
|
||||
color: var(--text-subtle);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@@ -167,10 +183,27 @@ h1 {
|
||||
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 {
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
box-shadow: 0 14px 35px rgba(17, 24, 39, 0.06);
|
||||
border: 1px solid var(--border-soft);
|
||||
box-shadow: var(--shadow-2);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
@@ -178,7 +211,7 @@ h1 {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.links {
|
||||
@@ -194,14 +227,14 @@ h1 {
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
background: var(--surface-subtle);
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 10px;
|
||||
color: #111827;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.links code {
|
||||
color: #475569;
|
||||
color: var(--text-subtle);
|
||||
font-size: 13px;
|
||||
word-break: break-all;
|
||||
text-align: right;
|
||||
@@ -223,14 +256,14 @@ h1 {
|
||||
|
||||
.stat-label {
|
||||
margin: 0 0 4px;
|
||||
color: #6b7280;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.mono {
|
||||
@@ -239,7 +272,7 @@ h1 {
|
||||
}
|
||||
|
||||
.link-accent {
|
||||
color: #2563eb;
|
||||
color: var(--accent-strong);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,6 +12,12 @@ vi.mock('axios', () => ({
|
||||
post: vi.fn(),
|
||||
},
|
||||
}))
|
||||
vi.mock('mermaid', () => ({
|
||||
default: {
|
||||
initialize: vi.fn(),
|
||||
run: vi.fn().mockResolvedValue(),
|
||||
},
|
||||
}))
|
||||
|
||||
const mockedAxios = axios
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
|
||||
Reference in New Issue
Block a user