diff --git a/dashboard/src/router/ChatBoxRoutes.ts b/dashboard/src/router/ChatBoxRoutes.ts new file mode 100644 index 000000000..f19fb204b --- /dev/null +++ b/dashboard/src/router/ChatBoxRoutes.ts @@ -0,0 +1,14 @@ +const ChatBoxRoutes = { + path: '/chatbox', + component: () => import('@/layouts/blank/BlankLayout.vue'), + children: [ + { + name: 'ChatBox', + path: '/chatbox', + component: () => import('@/views/ChatBoxPage.vue') + } + ] + }; + + export default ChatBoxRoutes; + \ No newline at end of file diff --git a/dashboard/src/router/index.ts b/dashboard/src/router/index.ts index c44e0249d..738fdda1d 100644 --- a/dashboard/src/router/index.ts +++ b/dashboard/src/router/index.ts @@ -1,13 +1,15 @@ import { createRouter, createWebHistory } from 'vue-router'; import MainRoutes from './MainRoutes'; import AuthRoutes from './AuthRoutes'; +import ChatBoxRoutes from './ChatBoxRoutes'; import { useAuthStore } from '@/stores/auth'; export const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ MainRoutes, - AuthRoutes + AuthRoutes, + ChatBoxRoutes ] }); diff --git a/dashboard/src/views/ChatBoxPage.vue b/dashboard/src/views/ChatBoxPage.vue new file mode 100644 index 000000000..5bbbfe08d --- /dev/null +++ b/dashboard/src/views/ChatBoxPage.vue @@ -0,0 +1,36 @@ + + + + + \ No newline at end of file diff --git a/dashboard/src/views/ChatPage.vue b/dashboard/src/views/ChatPage.vue index 0171e3e7a..f0acac487 100644 --- a/dashboard/src/views/ChatPage.vue +++ b/dashboard/src/views/ChatPage.vue @@ -1,24 +1,52 @@ - 语音转文本 + 语音转文本 @@ -81,6 +113,20 @@ marked.setOptions({
+ +
+
+

{{ getCurrentConversation.title || '新对话' }}

+
{{ formatDate(getCurrentConversation.updated_at) }}
+
+
+ + mdi-fullscreen +
+
+ +
@@ -200,21 +246,14 @@ marked.setOptions({
- + 编辑对话标题 - + @@ -258,11 +297,26 @@ export default { ctrlKeyLongPressThreshold: 300, // 长按阈值,单位毫秒 mediaCache: {}, // Add a cache to store media blobs - + // 添加对话标题编辑相关变量 editTitleDialog: false, editingTitle: '', editingCid: '', + + // 侧边栏折叠状态 + sidebarCollapsed: false, + sidebarHovered: false, + sidebarHoverTimer: null, + sidebarHoverExpanded: false, + sidebarHoverDelay: 100, // 悬停延迟,单位毫秒 + } + }, + + computed: { + // Get the current conversation from the conversations array + getCurrentConversation() { + if (!this.currCid) return null; + return this.conversations.find(c => c.cid === this.currCid); } }, @@ -281,6 +335,12 @@ export default { // 添加keyup事件监听 document.addEventListener('keyup', this.handleInputKeyUp); + + // 从 localStorage 获取侧边栏折叠状态 + const savedCollapseState = localStorage.getItem('sidebarCollapsed'); + if (savedCollapseState !== null) { + this.sidebarCollapsed = JSON.parse(savedCollapseState); + } }, beforeUnmount() { @@ -292,40 +352,86 @@ export default { // 移除keyup事件监听 document.removeEventListener('keyup', this.handleInputKeyUp); + // 清除悬停定时器 + if (this.sidebarHoverTimer) { + clearTimeout(this.sidebarHoverTimer); + } + // Cleanup blob URLs this.cleanupMediaCache(); }, methods: { + // 切换侧边栏折叠状态 + toggleSidebar() { + if (this.sidebarHoverExpanded) { + this.sidebarHoverExpanded = false; + return + } + this.sidebarCollapsed = !this.sidebarCollapsed; + // 保存折叠状态到 localStorage + localStorage.setItem('sidebarCollapsed', JSON.stringify(this.sidebarCollapsed)); + }, + + // 侧边栏鼠标悬停处理 + handleSidebarMouseEnter() { + if (!this.sidebarCollapsed) return; + + this.sidebarHovered = true; + + // 设置延迟定时器 + this.sidebarHoverTimer = setTimeout(() => { + if (this.sidebarHovered) { + this.sidebarHoverExpanded = true; + this.sidebarCollapsed = false; + } + }, this.sidebarHoverDelay); + }, + + handleSidebarMouseLeave() { + this.sidebarHovered = false; + + // 清除定时器 + if (this.sidebarHoverTimer) { + clearTimeout(this.sidebarHoverTimer); + this.sidebarHoverTimer = null; + } + + if (this.sidebarHoverExpanded) { + this.sidebarCollapsed = true; + } + this.sidebarHoverExpanded = false; + }, + // 显示编辑对话标题对话框 showEditTitleDialog(cid, title) { this.editingCid = cid; this.editingTitle = title || ''; // 如果标题为空,则设置为空字符串 this.editTitleDialog = true; }, - + // 保存对话标题 saveTitle() { if (!this.editingCid) return; - + const trimmedTitle = this.editingTitle.trim(); axios.post('/api/chat/rename_conversation', { conversation_id: this.editingCid, title: trimmedTitle }) - .then(response => { - // 更新本地对话列表中的标题 - const conversation = this.conversations.find(c => c.cid === this.editingCid); - if (conversation) { - conversation.title = trimmedTitle; - } - this.editTitleDialog = false; - }) - .catch(err => { - console.error('重命名对话失败:', err); - }); + .then(response => { + // 更新本地对话列表中的标题 + const conversation = this.conversations.find(c => c.cid === this.editingCid); + if (conversation) { + conversation.title = trimmedTitle; + } + this.editTitleDialog = false; + }) + .catch(err => { + console.error('重命名对话失败:', err); + }); }, - + async getMediaFile(filename) { if (this.mediaCache[filename]) { return this.mediaCache[filename]; @@ -336,7 +442,7 @@ export default { params: { filename }, responseType: 'blob' }); - + const blobUrl = URL.createObjectURL(response.data); this.mediaCache[filename] = blobUrl; return blobUrl; @@ -671,15 +777,15 @@ export default { audio_url: this.stagedAudioUrl ? [this.stagedAudioUrl] : [] // Already contains just filename }) }) - .then(response => { - this.prompt = ''; - this.stagedImagesName = []; - this.stagedAudioUrl = ""; - this.loadingChat = false; - }) - .catch(err => { - console.error(err); - }); + .then(response => { + this.prompt = ''; + this.stagedImagesName = []; + this.stagedAudioUrl = ""; + this.loadingChat = false; + }) + .catch(err => { + console.error(err); + }); }, scrollToBottom() { this.$nextTick(() => { @@ -773,29 +879,43 @@ export default { } } +/* 添加淡入动画 */ +@keyframes fadeInContent { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +.fade-in { + animation: fadeInContent 0.2s ease-in forwards; +} + /* 聊天页面布局 */ /* todo: 聊天页面背景颜色有问题 */ .chat-page-card { margin-bottom: 16px; width: 100%; height: 100%; - border-radius: 12px; + border-radius: 16px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05) !important; } .chat-page-container { width: 100%; - height: calc(100vh - 120px); + height: 100%; + max-height: calc(100vh - 120px); padding: 0; } .chat-layout { height: 100%; display: flex; - gap: 24px; } -/* 侧边栏样式 - 优化版 */ .sidebar-panel { max-width: 270px; min-width: 240px; @@ -806,57 +926,34 @@ export default { background-color: var(--v-theme-containerBg); height: 100%; position: relative; + transition: all 0.3s ease; + overflow: hidden; + /* 防止内容溢出 */ } -.sidebar-header { - padding: 16px; +/* 侧边栏折叠状态 */ +.sidebar-collapsed { + max-width: 75px; + min-width: 75px; + transition: all 0.3s ease; } -.conversations-container { - flex-grow: 1; - overflow-y: auto; - padding: 16px; +/* 当悬停展开时 */ +.sidebar-collapsed.sidebar-hovered { + max-width: 270px; + min-width: 240px; + transition: all 0.3s ease; } -.sidebar-footer { - padding: 16px; - border-top: 1px solid rgba(0, 0, 0, 0.04); +/* 侧边栏折叠按钮 */ +.sidebar-collapse-btn-container { + margin: 16px; + margin-bottom: 0px; + z-index: 10; } -.sidebar-section-title { - font-size: 12px; - font-weight: 500; - color: var(--v-theme-secondaryText); - text-transform: uppercase; - letter-spacing: 0.5px; - margin-bottom: 12px; - padding-left: 4px; -} - -.new-chat-btn { - width: 100%; - background-color: #673ab7 !important; - color: white !important; - font-weight: 500; - box-shadow: 0 2px 8px rgba(103, 58, 183, 0.25) !important; - transition: all 0.2s ease; - text-transform: none; - letter-spacing: 0.25px; -} - -.new-chat-btn:hover { - background-color: #7e57c2 !important; - box-shadow: 0 4px 12px rgba(103, 58, 183, 0.3) !important; - transform: translateY(-1px); -} - -.conversation-list-card { - border-radius: 8px; - box-shadow: none !important; - background-color: var(--v-theme-containerBg); -} - -.conversation-list { +.sidebar-collapse-btn { + opacity: 0.6; max-height: none; overflow-y: visible; padding: 0; @@ -869,7 +966,8 @@ export default { height: auto !important; min-height: 56px; padding: 8px 12px !important; - position: relative; /* 确保相对定位,便于添加编辑按钮 */ + position: relative; + /* 确保相对定位,便于添加编辑按钮 */ } .conversation-item:hover { @@ -881,12 +979,25 @@ export default { font-size: 14px; line-height: 1.3; margin-bottom: 2px; + transition: opacity 0.25s ease; } .timestamp { font-size: 11px; color: var(--v-theme-secondaryText); line-height: 1; + transition: opacity 0.25s ease; +} + +.sidebar-section-title { + font-size: 12px; + font-weight: 500; + color: var(--v-theme-secondaryText); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 12px; + padding-left: 4px; + transition: opacity 0.25s ease; } .status-chips { @@ -894,6 +1005,7 @@ export default { flex-wrap: wrap; gap: 8px; margin-bottom: 16px; + transition: opacity 0.25s ease; } .status-chip { @@ -911,6 +1023,7 @@ export default { letter-spacing: 0.25px; font-size: 12px; line-height: 1.2em; + transition: opacity 0.25s ease; } .delete-chat-btn:hover { @@ -930,6 +1043,7 @@ export default { .no-conversations-text { font-size: 14px; color: var(--v-theme-secondaryText); + transition: opacity 0.25s ease; } /* 聊天内容区域 */ @@ -1230,4 +1344,48 @@ export default { font-weight: 500; padding-bottom: 8px; } + +/* 对话标题和时间样式 */ +.conversation-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 16px 16px 16px; + border-bottom: 1px solid var(--v-theme-border); + width: 100%; + padding-right: 32px; +} + +.conversation-header-content { + display: flex; + flex-direction: column; +} + +.conversation-header-title { + font-size: 18px; + font-weight: 600; + margin: 0; + color: var(--v-theme-primaryText); +} + +.conversation-header-time { + font-size: 12px; + color: var(--v-theme-secondaryText); + margin-top: 4px; +} + +.conversation-header-actions { + display: flex; + align-items: center; +} + +.fullscreen-icon { + opacity: 0.7; + transition: opacity 0.2s; + cursor: pointer; +} + +.fullscreen-icon:hover { + opacity: 1; +} \ No newline at end of file