perf: 前端性能优化 - 构建分包、异步组件、渲染优化
This commit is contained in:
parent
8c29f0684f
commit
de79c227e2
|
|
@ -3,6 +3,9 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||||
<title>Chat</title>
|
<title>Chat</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -57,22 +57,23 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, shallowRef, computed, onMounted, defineAsyncComponent, triggerRef } from 'vue'
|
||||||
import Sidebar from './components/Sidebar.vue'
|
import Sidebar from './components/Sidebar.vue'
|
||||||
import ChatView from './components/ChatView.vue'
|
import ChatView from './components/ChatView.vue'
|
||||||
import SettingsPanel from './components/SettingsPanel.vue'
|
|
||||||
import StatsPanel from './components/StatsPanel.vue'
|
const SettingsPanel = defineAsyncComponent(() => import('./components/SettingsPanel.vue'))
|
||||||
|
const StatsPanel = defineAsyncComponent(() => import('./components/StatsPanel.vue'))
|
||||||
import { conversationApi, messageApi } from './api'
|
import { conversationApi, messageApi } from './api'
|
||||||
|
|
||||||
// -- Conversations state --
|
// -- Conversations state --
|
||||||
const conversations = ref([])
|
const conversations = shallowRef([])
|
||||||
const currentConvId = ref(null)
|
const currentConvId = ref(null)
|
||||||
const loadingConvs = ref(false)
|
const loadingConvs = ref(false)
|
||||||
const hasMoreConvs = ref(false)
|
const hasMoreConvs = ref(false)
|
||||||
const nextConvCursor = ref(null)
|
const nextConvCursor = ref(null)
|
||||||
|
|
||||||
// -- Messages state --
|
// -- Messages state --
|
||||||
const messages = ref([])
|
const messages = shallowRef([])
|
||||||
const hasMoreMessages = ref(false)
|
const hasMoreMessages = ref(false)
|
||||||
const loadingMessages = ref(false)
|
const loadingMessages = ref(false)
|
||||||
const nextMsgCursor = ref(null)
|
const nextMsgCursor = ref(null)
|
||||||
|
|
@ -81,8 +82,8 @@ const nextMsgCursor = ref(null)
|
||||||
const streaming = ref(false)
|
const streaming = ref(false)
|
||||||
const streamContent = ref('')
|
const streamContent = ref('')
|
||||||
const streamThinking = ref('')
|
const streamThinking = ref('')
|
||||||
const streamToolCalls = ref([])
|
const streamToolCalls = shallowRef([])
|
||||||
const streamProcessSteps = ref([])
|
const streamProcessSteps = shallowRef([])
|
||||||
|
|
||||||
// 保存每个对话的流式状态
|
// 保存每个对话的流式状态
|
||||||
const streamStates = new Map()
|
const streamStates = new Map()
|
||||||
|
|
@ -151,7 +152,7 @@ async function loadConversations(reset = true) {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
conversations.value = res.data.items
|
conversations.value = res.data.items
|
||||||
} else {
|
} else {
|
||||||
conversations.value.push(...res.data.items)
|
conversations.value = [...conversations.value, ...res.data.items]
|
||||||
}
|
}
|
||||||
nextConvCursor.value = res.data.next_cursor
|
nextConvCursor.value = res.data.next_cursor
|
||||||
hasMoreConvs.value = res.data.has_more
|
hasMoreConvs.value = res.data.has_more
|
||||||
|
|
@ -173,7 +174,7 @@ async function createConversation() {
|
||||||
title: '新对话',
|
title: '新对话',
|
||||||
project_id: currentProject.value?.id || null,
|
project_id: currentProject.value?.id || null,
|
||||||
})
|
})
|
||||||
conversations.value.unshift(res.data)
|
conversations.value = [res.data, ...conversations.value]
|
||||||
await selectConversation(res.data.id)
|
await selectConversation(res.data.id)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to create conversation:', e)
|
console.error('Failed to create conversation:', e)
|
||||||
|
|
@ -282,7 +283,7 @@ function createStreamCallbacks(convId, { updateConvList = true } = {}) {
|
||||||
|
|
||||||
if (currentConvId.value === convId) {
|
if (currentConvId.value === convId) {
|
||||||
streaming.value = false
|
streaming.value = false
|
||||||
messages.value.push({
|
messages.value = [...messages.value, {
|
||||||
id: data.message_id,
|
id: data.message_id,
|
||||||
conversation_id: convId,
|
conversation_id: convId,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
|
|
@ -292,16 +293,20 @@ function createStreamCallbacks(convId, { updateConvList = true } = {}) {
|
||||||
process_steps: streamProcessSteps.value.filter(Boolean),
|
process_steps: streamProcessSteps.value.filter(Boolean),
|
||||||
token_count: data.token_count,
|
token_count: data.token_count,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
})
|
}]
|
||||||
resetStreamState()
|
resetStreamState()
|
||||||
|
|
||||||
if (updateConvList) {
|
if (updateConvList) {
|
||||||
const idx = conversations.value.findIndex(c => c.id === convId)
|
const idx = conversations.value.findIndex(c => c.id === convId)
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
const conv = idx > 0 ? conversations.value.splice(idx, 1)[0] : conversations.value[0]
|
const conv = conversations.value[idx]
|
||||||
conv.message_count = (conv.message_count || 0) + 2
|
const updated = {
|
||||||
if (data.suggested_title) conv.title = data.suggested_title
|
...conv,
|
||||||
if (idx > 0) conversations.value.unshift(conv)
|
message_count: (conv.message_count || 0) + 2,
|
||||||
|
...(data.suggested_title ? { title: data.suggested_title } : {}),
|
||||||
|
}
|
||||||
|
const newList = conversations.value.filter((_, i) => i !== idx)
|
||||||
|
conversations.value = [updated, ...newList]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -309,11 +314,13 @@ function createStreamCallbacks(convId, { updateConvList = true } = {}) {
|
||||||
const res = await messageApi.list(convId, null, 50)
|
const res = await messageApi.list(convId, null, 50)
|
||||||
const idx = conversations.value.findIndex(c => c.id === convId)
|
const idx = conversations.value.findIndex(c => c.id === convId)
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
conversations.value[idx].message_count = res.data.items.length
|
const conv = conversations.value[idx]
|
||||||
|
const updates = { message_count: res.data.items.length }
|
||||||
if (res.data.items.length > 0) {
|
if (res.data.items.length > 0) {
|
||||||
const convRes = await conversationApi.get(convId)
|
const convRes = await conversationApi.get(convId)
|
||||||
if (convRes.data.title) conversations.value[idx].title = convRes.data.title
|
if (convRes.data.title) updates.title = convRes.data.title
|
||||||
}
|
}
|
||||||
|
conversations.value = conversations.value.map((c, i) => i === idx ? { ...c, ...updates } : c)
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
@ -345,7 +352,7 @@ async function sendMessage(data) {
|
||||||
token_count: 0,
|
token_count: 0,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
messages.value.push(userMsg)
|
messages.value = [...messages.value, userMsg]
|
||||||
|
|
||||||
initStreamState()
|
initStreamState()
|
||||||
|
|
||||||
|
|
@ -410,7 +417,7 @@ async function saveSettings(data) {
|
||||||
const res = await conversationApi.update(currentConvId.value, data)
|
const res = await conversationApi.update(currentConvId.value, data)
|
||||||
const idx = conversations.value.findIndex(c => c.id === currentConvId.value)
|
const idx = conversations.value.findIndex(c => c.id === currentConvId.value)
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
conversations.value[idx] = { ...conversations.value[idx], ...res.data }
|
conversations.value = conversations.value.map((c, i) => i === idx ? { ...c, ...res.data } : c)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to save settings:', e)
|
console.error('Failed to save settings:', e)
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
v-for="msg in messages"
|
v-for="msg in messages"
|
||||||
:key="msg.id"
|
:key="msg.id"
|
||||||
:data-msg-id="msg.id"
|
:data-msg-id="msg.id"
|
||||||
|
v-memo="[msg.text, msg.thinking, msg.tool_calls, msg.process_steps, msg.attachments]"
|
||||||
>
|
>
|
||||||
<MessageBubble
|
<MessageBubble
|
||||||
:role="msg.role"
|
:role="msg.role"
|
||||||
|
|
@ -119,6 +120,7 @@ const inputRef = ref(null)
|
||||||
const modelNameMap = ref({})
|
const modelNameMap = ref({})
|
||||||
const activeMessageId = ref(null)
|
const activeMessageId = ref(null)
|
||||||
let scrollObserver = null
|
let scrollObserver = null
|
||||||
|
const observedElements = new WeakSet()
|
||||||
|
|
||||||
function formatModelName(modelId) {
|
function formatModelName(modelId) {
|
||||||
return modelNameMap.value[modelId] || modelId
|
return modelNameMap.value[modelId] || modelId
|
||||||
|
|
@ -158,7 +160,12 @@ watch(() => props.messages.length, () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (!scrollObserver || !scrollContainer.value) return
|
if (!scrollObserver || !scrollContainer.value) return
|
||||||
const wrappers = scrollContainer.value.querySelectorAll('[data-msg-id]')
|
const wrappers = scrollContainer.value.querySelectorAll('[data-msg-id]')
|
||||||
wrappers.forEach(el => scrollObserver.observe(el))
|
wrappers.forEach(el => {
|
||||||
|
if (!observedElements.has(el)) {
|
||||||
|
scrollObserver.observe(el)
|
||||||
|
observedElements.add(el)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -211,8 +211,13 @@ const processItems = computed(() => {
|
||||||
return items
|
return items
|
||||||
})
|
})
|
||||||
|
|
||||||
// 增强 processBlock 内代码块(必须在 processItems 定义之后)
|
// 增强 processBlock 内代码块
|
||||||
useCodeEnhancement(processRef, processItems, { deep: true })
|
const { enhance, debouncedEnhance } = useCodeEnhancement(processRef, processItems, { deep: true })
|
||||||
|
|
||||||
|
// 流式时使用节流的代码块增强,减少 DOM 操作
|
||||||
|
watch(() => props.streamingContent?.length, () => {
|
||||||
|
if (props.streaming) debouncedEnhance()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { watch, onMounted, nextTick } from 'vue'
|
import { watch, onMounted, nextTick, onUnmounted } from 'vue'
|
||||||
import { enhanceCodeBlocks } from '../utils/markdown'
|
import { enhanceCodeBlocks } from '../utils/markdown'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -10,15 +10,25 @@ import { enhanceCodeBlocks } from '../utils/markdown'
|
||||||
* @param {import('vue').WatchOptions} [watchOpts] - Optional watch options (e.g. { deep: true })
|
* @param {import('vue').WatchOptions} [watchOpts] - Optional watch options (e.g. { deep: true })
|
||||||
*/
|
*/
|
||||||
export function useCodeEnhancement(templateRef, dep, watchOpts) {
|
export function useCodeEnhancement(templateRef, dep, watchOpts) {
|
||||||
|
let debounceTimer = null
|
||||||
|
|
||||||
function enhance() {
|
function enhance() {
|
||||||
enhanceCodeBlocks(templateRef.value)
|
enhanceCodeBlocks(templateRef.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function debouncedEnhance() {
|
||||||
|
if (debounceTimer) clearTimeout(debounceTimer)
|
||||||
|
debounceTimer = setTimeout(() => nextTick(enhance), 150)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(enhance)
|
onMounted(enhance)
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (debounceTimer) clearTimeout(debounceTimer)
|
||||||
|
})
|
||||||
|
|
||||||
if (dep) {
|
if (dep) {
|
||||||
watch(dep, () => nextTick(enhance), watchOpts)
|
watch(dep, () => nextTick(enhance), watchOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { enhance }
|
return { enhance, debouncedEnhance }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/* highlight.js - Light theme for code blocks */
|
/* highlight.js - Light theme for code blocks */
|
||||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap');
|
/* JetBrains Mono font loaded via <link> in index.html */
|
||||||
|
|
||||||
.hljs {
|
.hljs {
|
||||||
color: #24292f;
|
color: #24292f;
|
||||||
|
|
|
||||||
|
|
@ -23,4 +23,14 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
'vendor-markdown': ['marked', 'marked-highlight', 'highlight.js'],
|
||||||
|
'vendor-katex': ['katex'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue