main: 增加全局暗黑模式支持及 Markdown 渲染功能
- 实现暗黑模式切换逻辑,支持系统主题适配 - 优化全局样式,添加动态主题变量及暗黑样式 - 引入 `markdown-it` 和 `mermaid`,支持 Markdown 内容及流程图渲染 - 更新 ChatView 测试用例,验证 Markdown 和 Mermaid 渲染效果 - 修改 package.json,增加新依赖
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user