From 975d960bac54baf3db49f573112729959e09cd53 Mon Sep 17 00:00:00 2001
From: ViperEkura <3081035982@qq.com>
Date: Tue, 14 Apr 2026 11:22:38 +0800
Subject: [PATCH] =?UTF-8?q?chore:=20=E6=B8=85=E9=99=A4=E5=86=97=E4=BD=99?=
=?UTF-8?q?=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
dashboard/src/components/EmptyState.vue | 79 -----
dashboard/src/components/ErrorMessage.vue | 103 ------
dashboard/src/index.js | 6 +-
dashboard/src/utils/useConversations.js | 261 ++++++++++++++
dashboard/src/views/ConversationView.vue | 393 ++++++----------------
5 files changed, 360 insertions(+), 482 deletions(-)
delete mode 100644 dashboard/src/components/EmptyState.vue
delete mode 100644 dashboard/src/components/ErrorMessage.vue
create mode 100644 dashboard/src/utils/useConversations.js
diff --git a/dashboard/src/components/EmptyState.vue b/dashboard/src/components/EmptyState.vue
deleted file mode 100644
index 84421e9..0000000
--- a/dashboard/src/components/EmptyState.vue
+++ /dev/null
@@ -1,79 +0,0 @@
-
-
-
-
{{ title }}
-
{{ description }}
-
-
-
-
-
-
-
diff --git a/dashboard/src/components/ErrorMessage.vue b/dashboard/src/components/ErrorMessage.vue
deleted file mode 100644
index 7c92623..0000000
--- a/dashboard/src/components/ErrorMessage.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-
-
-
-
-
{{ title }}
-
{{ message }}
-
-
-
-
-
-
-
-
diff --git a/dashboard/src/index.js b/dashboard/src/index.js
index d4d18e0..eed2331 100644
--- a/dashboard/src/index.js
+++ b/dashboard/src/index.js
@@ -1,3 +1,5 @@
// 导出组件
-export { default as ErrorMessage } from './components/ErrorMessage.vue'
-export { default as EmptyState } from './components/EmptyState.vue'
+export { default as AppHeader } from './components/AppHeader.vue'
+export { default as ProcessBlock } from './components/ProcessBlock.vue'
+export { default as MessageBubble } from './components/MessageBubble.vue'
+export { default as MessageNav } from './components/MessageNav.vue'
diff --git a/dashboard/src/utils/useConversations.js b/dashboard/src/utils/useConversations.js
new file mode 100644
index 0000000..9dcf410
--- /dev/null
+++ b/dashboard/src/utils/useConversations.js
@@ -0,0 +1,261 @@
+import { ref, computed, watch } from 'vue'
+import { conversationsAPI, messagesAPI, toolsAPI, providersAPI } from './api.js'
+import { streamManager } from './streamManager.js'
+import { useStreamStore } from './streamStore.js'
+
+// 对话管理 Composable
+export function useConversations() {
+ const streamStore = useStreamStore()
+
+ // 状态
+ const list = ref([])
+ const providers = ref([])
+ const page = ref(1)
+ const pageSize = 20
+ const total = ref(0)
+ const loading = ref(true)
+ const error = ref('')
+
+ const selectedId = ref(null)
+ const selectedConv = ref(null)
+ const convMessages = ref([])
+ const loadingMessages = ref(false)
+ const enabledTools = ref([])
+
+ // 计算属性
+ const totalPages = computed(() => Math.ceil(total.value / pageSize))
+
+ const currentStreamState = computed(() => {
+ return streamStore.getStreamState(selectedId.value)
+ })
+
+ const sending = computed(() => {
+ const state = streamStore.getStreamState(selectedId.value)
+ return state && state.status === 'streaming'
+ })
+
+ // 检查指定会话是否有活跃流
+ const hasActiveStream = (convId) => {
+ return streamStore.hasActiveStream(convId)
+ }
+
+ // 加载启用的工具列表
+ const loadEnabledTools = async () => {
+ try {
+ const res = await toolsAPI.list()
+ if (res.success) {
+ const tools = res.data?.tools || []
+ enabledTools.value = tools.map(t => t.function?.name || t.name)
+ }
+ } catch (e) {
+ console.error('Failed to load tools:', e)
+ }
+ }
+
+ // 加载会话列表
+ const fetchData = async () => {
+ loading.value = true
+ error.value = ''
+ try {
+ const [convRes, provRes, toolsRes] = await Promise.allSettled([
+ conversationsAPI.list({ page: page.value, page_size: pageSize }),
+ providersAPI.list(),
+ toolsAPI.list()
+ ])
+ if (convRes.status === 'fulfilled' && convRes.value.success) {
+ list.value = convRes.value.data?.items || []
+ total.value = convRes.value.data?.total || 0
+ // 默认选中第一个会话
+ if (list.value.length > 0 && !selectedId.value) {
+ selectConv(list.value[0])
+ }
+ }
+ if (provRes.status === 'fulfilled' && provRes.value.success) {
+ providers.value = provRes.value.data?.providers || []
+ }
+ if (toolsRes.status === 'fulfilled' && toolsRes.value.success) {
+ enabledTools.value = (toolsRes.value.data?.tools || []).map(t => t.function?.name || t.name)
+ }
+ } catch (e) {
+ error.value = e.message
+ }
+ finally {
+ loading.value = false
+ }
+ }
+
+ // 选择会话
+ const selectConv = async (c) => {
+ selectedId.value = c.id
+ selectedConv.value = c
+ await fetchConvMessages(c.id)
+ setupStreamWatch()
+ }
+
+ // 获取会话消息
+ const fetchConvMessages = async (convId) => {
+ loadingMessages.value = true
+ convMessages.value = []
+ try {
+ const res = await messagesAPI.list(convId)
+ if (res.success) {
+ convMessages.value = res.data?.messages || []
+ }
+ } catch (e) {
+ console.error('获取消息失败:', e)
+ } finally {
+ loadingMessages.value = false
+ }
+ }
+
+ // 发送消息
+ const sendMessage = async (content) => {
+ if (!content.trim() || !selectedConv.value || sending.value) return
+
+ const trimmedContent = content.trim()
+
+ // 添加用户消息到列表
+ const userMsgId = 'user-' + Date.now()
+ const userMsg = {
+ id: userMsgId,
+ role: 'user',
+ content: trimmedContent,
+ created_at: new Date().toISOString()
+ }
+ convMessages.value.push(userMsg)
+
+ // 如果还没有标题或标题为默认标题,使用第一条消息作为标题
+ const currentTitle = selectedConv.value?.title
+ const isDefaultTitle = !currentTitle || currentTitle === 'New Conversation' || currentTitle.trim() === ''
+ if (isDefaultTitle) {
+ const title = trimmedContent.slice(0, 30) + (trimmedContent.length > 30 ? '...' : '')
+ selectedConv.value.title = title
+ // 更新列表中的标题
+ const conv = list.value.find(c => c.id === selectedConv.value.id)
+ if (conv) conv.title = title
+ // 调用 API 保存
+ await conversationsAPI.update(selectedConv.value.id, { title })
+ }
+
+ // 使用 StreamManager 发送流式请求
+ await streamManager.startStream(
+ selectedConv.value.id,
+ {
+ conversation_id: selectedConv.value.id,
+ content: trimmedContent,
+ enabled_tools: enabledTools.value
+ },
+ userMsgId
+ )
+ }
+
+ // 创建会话
+ const createConv = async (form) => {
+ const res = await conversationsAPI.create(form)
+ if (res.success && res.data?.id) {
+ await fetchData()
+ const newConv = list.value.find(c => c.id === res.data.id)
+ if (newConv) {
+ await selectConv(newConv)
+ }
+ return res.data
+ }
+ throw new Error(res.message)
+ }
+
+ // 删除会话
+ const deleteConv = async (c) => {
+ if (hasActiveStream(c.id)) {
+ streamManager.cancelStream(c.id)
+ }
+ await conversationsAPI.delete(c.id)
+ if (selectedId.value === c.id) {
+ selectedId.value = null
+ selectedConv.value = null
+ }
+ await fetchData()
+ }
+
+ // 更新会话标题
+ const updateConvTitle = async (c, newTitle) => {
+ c.title = newTitle
+ // 更新列表中的标题
+ const conv = list.value.find(item => item.id === c.id)
+ if (conv) conv.title = newTitle
+ // 调用 API 保存
+ await conversationsAPI.update(c.id, { title: newTitle })
+ }
+
+ // 设置流状态监听
+ let unwatchStream = null
+ const setupStreamWatch = () => {
+ if (unwatchStream) {
+ unwatchStream()
+ }
+
+ unwatchStream = watch(
+ () => streamStore.getStreamState(selectedId.value),
+ (state) => {
+ if (!state) return
+
+ if (state.status === 'done') {
+ const completedMessage = {
+ id: state.id,
+ role: 'assistant',
+ process_steps: state.process_steps,
+ token_count: state.token_count,
+ usage: state.usage,
+ created_at: new Date().toISOString()
+ }
+ convMessages.value.push(completedMessage)
+ streamStore.clearStream(selectedId.value)
+ } else if (state.status === 'error') {
+ console.error('Stream error:', state.error)
+ streamStore.clearStream(selectedId.value)
+ }
+ },
+ { deep: true }
+ )
+ }
+
+ // 初始化
+ const init = async () => {
+ await fetchData()
+ }
+
+ // 清理
+ const cleanup = () => {
+ if (unwatchStream) {
+ unwatchStream()
+ }
+ }
+
+ return {
+ // 状态
+ list,
+ providers,
+ page,
+ totalPages,
+ loading,
+ error,
+ selectedId,
+ selectedConv,
+ convMessages,
+ loadingMessages,
+ sending,
+ currentStreamState,
+
+ // 方法
+ hasActiveStream,
+ fetchData,
+ selectConv,
+ fetchConvMessages,
+ sendMessage,
+ createConv,
+ deleteConv,
+ updateConvTitle,
+ loadEnabledTools,
+ init,
+ cleanup
+ }
+}
diff --git a/dashboard/src/views/ConversationView.vue b/dashboard/src/views/ConversationView.vue
index cfbb5de..79fa3d8 100644
--- a/dashboard/src/views/ConversationView.vue
+++ b/dashboard/src/views/ConversationView.vue
@@ -99,18 +99,18 @@
@@ -137,7 +137,7 @@
-
+
@@ -149,7 +149,7 @@
-
+
@@ -166,93 +166,38 @@
@@ -633,7 +430,7 @@ onUnmounted(() => {
.modal h2 { margin: 0 0 1.25rem; font-size: 1.1rem; color: var(--text-primary); }
.form-group { margin-bottom: 1rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-weight: 500; font-size: 0.9rem; color: var(--text-primary); }
-.form-group input, .form-group select { width: 100%; padding: 0.65rem; border: 1px solid var(--border-input); border-radius: 8px; background: var(--bg-input); box-sizing: border-box; font-size: 0.9rem; color: var(--text-primary); }
+.form-group input { width: 100%; padding: 0.65rem; border: 1px solid var(--border-input); border-radius: 8px; background: var(--bg-input); box-sizing: border-box; color: var(--text-primary); font-size: 0.9rem; }
.modal-actions { display: flex; justify-content: flex-end; gap: 0.75rem; margin-top: 1.25rem; }