feat: 增加流式动态渲染

This commit is contained in:
ViperEkura 2026-03-24 15:28:59 +08:00
parent 8d259e2d50
commit d7fe954098
5 changed files with 108 additions and 34 deletions

View File

@ -49,10 +49,7 @@
<div v-if="streamingThinking" class="thinking-content streaming-thinking"> <div v-if="streamingThinking" class="thinking-content streaming-thinking">
{{ streamingThinking }} {{ streamingThinking }}
</div> </div>
<div class="message-content streaming-content"> <div class="message-content streaming-content" v-html="renderedStreamContent || '<span class=\'placeholder\'>...</span>'"></div>
{{ streamingContent || '...' }}
<span class="cursor-blink">|</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -68,9 +65,22 @@
</template> </template>
<script setup> <script setup>
import { ref, watch, nextTick } from 'vue' import { ref, computed, watch, nextTick } from 'vue'
import MessageBubble from './MessageBubble.vue' import MessageBubble from './MessageBubble.vue'
import MessageInput from './MessageInput.vue' import MessageInput from './MessageInput.vue'
import { marked } from 'marked'
import hljs from 'highlight.js'
marked.setOptions({
highlight(code, lang) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value
}
return hljs.highlightAuto(code).value
},
breaks: true,
gfm: true,
})
const props = defineProps({ const props = defineProps({
conversation: { type: Object, default: null }, conversation: { type: Object, default: null },
@ -87,6 +97,11 @@ defineEmits(['sendMessage', 'deleteMessage', 'toggleSettings', 'loadMoreMessages
const scrollContainer = ref(null) const scrollContainer = ref(null)
const inputRef = ref(null) const inputRef = ref(null)
const renderedStreamContent = computed(() => {
if (!props.streamingContent) return ''
return marked.parse(props.streamingContent)
})
function scrollToBottom(smooth = true) { function scrollToBottom(smooth = true) {
nextTick(() => { nextTick(() => {
const el = scrollContainer.value const el = scrollContainer.value
@ -142,7 +157,7 @@ defineExpose({ scrollToBottom })
width: 64px; width: 64px;
height: 64px; height: 64px;
border-radius: 16px; border-radius: 16px;
background: linear-gradient(135deg, #4f46e5, #7c3aed); background: linear-gradient(135deg, #2563eb, #0ea5e9);
color: white; color: white;
display: flex; display: flex;
align-items: center; align-items: center;
@ -193,8 +208,8 @@ defineExpose({ scrollToBottom })
font-size: 11px; font-size: 11px;
padding: 2px 8px; padding: 2px 8px;
border-radius: 10px; border-radius: 10px;
background: rgba(79, 70, 229, 0.15); background: rgba(37, 99, 235, 0.15);
color: #a5b4fc; color: #60a5fa;
flex-shrink: 0; flex-shrink: 0;
} }
@ -288,16 +303,75 @@ defineExpose({ scrollToBottom })
font-size: 15px; font-size: 15px;
line-height: 1.7; line-height: 1.7;
color: #e2e8f0; color: #e2e8f0;
white-space: pre-wrap; word-break: break-word;
} }
.cursor-blink { .streaming-content :deep(p) {
animation: blink 0.8s infinite; margin: 0 0 8px;
color: #4f46e5;
} }
@keyframes blink { .streaming-content :deep(p:last-child) {
0%, 50% { opacity: 1; } margin-bottom: 0;
51%, 100% { opacity: 0; } }
.streaming-content :deep(pre) {
background: #0d1117;
border-radius: 8px;
padding: 16px;
overflow-x: auto;
margin: 8px 0;
}
.streaming-content :deep(pre code) {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 13px;
line-height: 1.5;
}
.streaming-content :deep(code) {
background: rgba(255, 255, 255, 0.06);
padding: 2px 6px;
border-radius: 4px;
font-size: 13px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
}
.streaming-content :deep(pre code) {
background: none;
padding: 0;
}
.streaming-content :deep(ul),
.streaming-content :deep(ol) {
padding-left: 20px;
margin: 8px 0;
}
.streaming-content :deep(blockquote) {
border-left: 3px solid rgba(59, 130, 246, 0.5);
padding-left: 12px;
color: #94a3b8;
margin: 8px 0;
}
.streaming-content :deep(table) {
border-collapse: collapse;
margin: 8px 0;
width: 100%;
}
.streaming-content :deep(th),
.streaming-content :deep(td) {
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 8px 12px;
text-align: left;
}
.streaming-content :deep(th) {
background: rgba(255, 255, 255, 0.04);
}
.streaming-content :deep(.placeholder) {
color: #475569;
} }
</style> </style>

View File

@ -103,12 +103,12 @@ function copyContent() {
} }
.user .avatar { .user .avatar {
background: linear-gradient(135deg, #4f46e5, #7c3aed); background: linear-gradient(135deg, #2563eb, #0ea5e9);
color: white; color: white;
} }
.assistant .avatar { .assistant .avatar {
background: linear-gradient(135deg, #059669, #10b981); background: linear-gradient(135deg, #0ea5e9, #06b6d4);
color: white; color: white;
} }
@ -212,7 +212,7 @@ function copyContent() {
} }
.message-content :deep(blockquote) { .message-content :deep(blockquote) {
border-left: 3px solid rgba(79, 70, 229, 0.5); border-left: 3px solid rgba(59, 130, 246, 0.5);
padding-left: 12px; padding-left: 12px;
color: #94a3b8; color: #94a3b8;
margin: 8px 0; margin: 8px 0;
@ -268,8 +268,8 @@ function copyContent() {
} }
.btn-copy:hover { .btn-copy:hover {
color: #a5b4fc; color: #60a5fa;
background: rgba(165, 180, 252, 0.1); background: rgba(96, 165, 250, 0.1);
} }
.btn-delete-msg:hover { .btn-delete-msg:hover {

View File

@ -88,7 +88,7 @@ defineExpose({ focus })
} }
.input-wrapper:focus-within { .input-wrapper:focus-within {
border-color: rgba(79, 70, 229, 0.5); border-color: rgba(37, 99, 235, 0.5);
} }
textarea { textarea {
@ -134,13 +134,13 @@ textarea:disabled {
} }
.btn-send.active { .btn-send.active {
background: #4f46e5; background: #2563eb;
color: white; color: white;
cursor: pointer; cursor: pointer;
} }
.btn-send.active:hover { .btn-send.active:hover {
background: #6366f1; background: #3b82f6;
} }
.input-hint { .input-hint {

View File

@ -200,7 +200,7 @@ function save() {
.value-display { .value-display {
float: right; float: right;
color: #a5b4fc; color: #60a5fa;
font-weight: 600; font-weight: 600;
} }
@ -223,7 +223,7 @@ function save() {
.form-group input[type="text"]:focus, .form-group input[type="text"]:focus,
.form-group textarea:focus, .form-group textarea:focus,
.form-group select:focus { .form-group select:focus {
border-color: rgba(79, 70, 229, 0.5); border-color: rgba(37, 99, 235, 0.5);
} }
.form-group textarea { .form-group textarea {
@ -264,7 +264,7 @@ function save() {
width: 16px; width: 16px;
height: 16px; height: 16px;
border-radius: 50%; border-radius: 50%;
background: #4f46e5; background: #2563eb;
cursor: pointer; cursor: pointer;
border: 2px solid #0f172a; border: 2px solid #0f172a;
} }
@ -346,7 +346,7 @@ function save() {
padding: 8px 20px; padding: 8px 20px;
border-radius: 8px; border-radius: 8px;
border: none; border: none;
background: #4f46e5; background: #2563eb;
color: white; color: white;
font-size: 14px; font-size: 14px;
cursor: pointer; cursor: pointer;
@ -354,7 +354,7 @@ function save() {
} }
.btn-save:hover { .btn-save:hover {
background: #6366f1; background: #3b82f6;
} }
.slide-enter-active, .slide-enter-active,

View File

@ -89,10 +89,10 @@ function onContextMenu(e, conv) {
.btn-new { .btn-new {
width: 100%; width: 100%;
padding: 10px 16px; padding: 10px 16px;
background: rgba(79, 70, 229, 0.15); background: rgba(37, 99, 235, 0.15);
border: 1px dashed rgba(79, 70, 229, 0.4); border: 1px dashed rgba(37, 99, 235, 0.4);
border-radius: 10px; border-radius: 10px;
color: #a5b4fc; color: #60a5fa;
font-size: 14px; font-size: 14px;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@ -102,8 +102,8 @@ function onContextMenu(e, conv) {
} }
.btn-new:hover { .btn-new:hover {
background: rgba(79, 70, 229, 0.25); background: rgba(37, 99, 235, 0.25);
border-color: rgba(79, 70, 229, 0.6); border-color: rgba(37, 99, 235, 0.6);
} }
.btn-new .icon { .btn-new .icon {
@ -141,7 +141,7 @@ function onContextMenu(e, conv) {
} }
.conversation-item.active { .conversation-item.active {
background: rgba(79, 70, 229, 0.2); background: rgba(37, 99, 235, 0.2);
} }
.conv-info { .conv-info {