From 63954dddf498097144b5164901491ad842affcd5 Mon Sep 17 00:00:00 2001
From: wenjinbo <599483010@qq.com>
Date: Tue, 4 Nov 2025 09:50:36 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=95=B4=E5=8A=A0=E5=85=AB=E5=A4=A7?=
=?UTF-8?q?=E7=B1=BB=E8=BF=BD=E9=97=AE=E8=81=8A=E5=A4=A9=E7=AA=97=E4=BD=93?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../library/components/VabTopBar/index.vue | 12 +-
.../src/components/AIResponseDrawer.vue | 1077 +++++++++++++++++
.../src/components/dashboard/ModuleCard.vue | 503 ++++++--
chat-client/src/constants/modules.ts | 3 +-
chat-client/src/services/dashboardService.ts | 20 +-
chat-client/src/types/dashboard.ts | 3 +-
.../src/views/chatweb/homePage/index.vue | 974 ++++++++++++++-
.../src/views/guest/homePage/index.vue | 83 ++
.../brichat/service/impl/ChatServiceImpl.java | 7 +-
9 files changed, 2532 insertions(+), 150 deletions(-)
create mode 100644 chat-client/src/components/AIResponseDrawer.vue
diff --git a/chat-client/library/components/VabTopBar/index.vue b/chat-client/library/components/VabTopBar/index.vue
index c36b06a..6519ed4 100644
--- a/chat-client/library/components/VabTopBar/index.vue
+++ b/chat-client/library/components/VabTopBar/index.vue
@@ -209,6 +209,11 @@
window.addEventListener('homepage-mounted', handleHomePageMounted)
window.addEventListener('homepage-unmounted', handleHomePageUnmounted)
+ // 监听来自homePage的拖拽状态更新
+ window.addEventListener('update-drag-mode', ((e: CustomEvent) => {
+ isDragEnabled.value = e.detail.enabled
+ }) as EventListener)
+
// 初始化时检查是否已经在 homePage
if ((window as any).__isHomePage__) {
isHomePageActive.value = true
@@ -236,6 +241,9 @@
window.removeEventListener('scroll', handleScroll)
window.removeEventListener('homepage-mounted', handleHomePageMounted)
window.removeEventListener('homepage-unmounted', handleHomePageUnmounted)
+ window.removeEventListener('update-drag-mode', ((e: CustomEvent) => {
+ isDragEnabled.value = e.detail.enabled
+ }) as EventListener)
// 取消订阅
if ($unsub) {
@@ -427,7 +435,7 @@
top: 0;
left: 0;
right: 0;
- z-index: 1000;
+ z-index: 1003;
pointer-events: none; // 允许点击穿透到下方内容
// 子元素恢复 pointer-events
@@ -720,7 +728,7 @@
position: fixed;
top: 12px;
left: 12px;
- z-index: 1001;
+ z-index: 1004;
background: linear-gradient(135deg, rgba(0, 212, 255, 0.15) 0%, rgba(24, 144, 255, 0.15) 100%);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 8px;
diff --git a/chat-client/src/components/AIResponseDrawer.vue b/chat-client/src/components/AIResponseDrawer.vue
new file mode 100644
index 0000000..235ad5e
--- /dev/null
+++ b/chat-client/src/components/AIResponseDrawer.vue
@@ -0,0 +1,1077 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ unreadCount > 99 ? '99+' : unreadCount }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ message.content }}
+
{{ message.timestamp }}
+
+
+
+
+
+
+
+
+
{{ message.timestamp }}
+
+
+
+
+
+
+
+
+
+
+
{{ formatTimestamp(new Date()) }}
+
+
+
+
+
+
+
+
+
开始对话
+
AI助手将在这里回答您的问题
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chat-client/src/components/dashboard/ModuleCard.vue b/chat-client/src/components/dashboard/ModuleCard.vue
index 7425686..3d204c1 100644
--- a/chat-client/src/components/dashboard/ModuleCard.vue
+++ b/chat-client/src/components/dashboard/ModuleCard.vue
@@ -699,98 +699,7 @@
-
-
+
+
+
+
@@ -243,6 +250,16 @@
>
这是一个测试弹窗,用于验证Element Plus是否正常工作
+
+
+
+
@@ -253,6 +270,7 @@ import { useAclStore } from '@/store/modules/acl';
import { MODULE_CONFIGS } from '@/constants/modules';
import ModuleCard from '@/components/dashboard/ModuleCard.vue';
import DetailModal from '@/components/dashboard/DetailModal.vue';
+import AIResponseDrawer from '@/components/AIResponseDrawer.vue';
import type { ModuleConfig, ModuleType, SearchResult, StatusStats, Position, Size, RuleRegulation, TypicalCase } from '@/types/dashboard';
import { webSocketService } from '@/utils/websocket';
import { baseURL } from '@/config';
@@ -275,7 +293,8 @@ import {
MessageSquare,
Activity,
Phone,
- GraduationCap
+ GraduationCap,
+ Minimize
} from 'lucide-vue-next';
// 使用store
@@ -304,6 +323,20 @@ const questionTextarea = ref(); // 输入框引用
const atSuggestionsStyle = ref({}); // @推荐框的样式定位
const selectedCards = ref([]); // 选中的卡片列表,用于显示tag标签
+// AI响应抽屉相关
+const showAIDrawer = ref(false); // 控制AI响应抽屉显示
+const aiDrawerRef = ref>(); // AI抽屉组件引用
+const pendingUserMessage = ref(''); // 缓存待添加的用户消息
+
+// AI消息相关(保留接口定义以兼容现有代码)
+interface AIMessage {
+ type: 'user' | 'ai';
+ content: string;
+ timestamp: string;
+}
+
+// 监听抽屉状态变化(已移至AIResponseDrawer组件内部处理)
+
// 监听弹窗状态变化
watch(showCardSelectorDialog, (newVal, oldVal) => {
// 弹窗状态变化处理
@@ -1432,6 +1465,12 @@ const toggleDragMode = () => {
isDragEnabled.value = !isDragEnabled.value;
};
+/**
+ * 根据抽屉状态自动控制拖拽功能
+ * 现在抽屉展开时也允许拖拽,只有在缩略状态时才可能影响拖拽
+ */
+// updateDragBasedOnDrawer方法已移除,拖拽控制现在完全由用户手动控制
+
/**
* 保存当前布局到文件
*/
@@ -1550,6 +1589,76 @@ const showNotification = (message: string, type: 'success' | 'error' | 'info' =
}, 3000);
};
+/**
+ * 处理AI用户消息事件
+ */
+const handleAIUserMessage = (message: string) => {
+ // 缓存用户消息,等待流式响应开始时再显示抽屉和添加消息
+ pendingUserMessage.value = message;
+};
+
+/**
+ * 处理AI流式响应开始事件
+ */
+const handleAIStreamStart = () => {
+ // 只在流式响应开始时才显示抽屉
+ showAIDrawer.value = true;
+
+ nextTick(() => {
+ // 如果有缓存的用户消息,先添加到抽屉
+ if (pendingUserMessage.value) {
+ aiDrawerRef.value?.addUserMessage(pendingUserMessage.value);
+ pendingUserMessage.value = ''; // 清空缓存
+ }
+
+ // 拖拽状态现在完全由用户手动控制
+
+ // 开始流式响应
+ aiDrawerRef.value?.startStreaming();
+ });
+};
+
+/**
+ * 处理AI流式响应更新事件
+ */
+const handleAIStreamUpdate = (content: string) => {
+ // 更新流式内容
+ aiDrawerRef.value?.updateStreamContent(content);
+};
+
+/**
+ * 处理AI流式响应完成事件
+ */
+const handleAIStreamFinish = (finalContent?: string) => {
+ // 完成流式响应
+ aiDrawerRef.value?.finishStreaming(finalContent);
+};
+
+/**
+ * 处理AI抽屉关闭事件
+ */
+const handleAIDrawerClose = () => {
+ showAIDrawer.value = false;
+ console.log('AI抽屉关闭');
+};
+
+/**
+ * 处理AI抽屉缩略事件
+ */
+const handleAIDrawerMinimize = () => {
+ console.log('AI抽屉已缩略');
+};
+
+/**
+ * 处理AI抽屉展开事件
+ */
+const handleAIDrawerMaximize = () => {
+ console.log('AI抽屉已展开');
+};
+
+// AI相关方法已移至AIResponseDrawer组件中
+
+
@@ -1568,9 +1677,12 @@ onMounted(async () => {
// 监听来自 VabTopBar 的事件
window.addEventListener('toggle-drag-mode', ((e: CustomEvent) => {
+ // 允许用户随时手动切换拖拽模式,不受抽屉状态限制
isDragEnabled.value = e.detail.enabled;
}) as EventListener);
+ // 监听抽屉状态变化,自动控制拖拽功能(已移至AIResponseDrawer组件内部处理)
+
window.addEventListener('save-layout', (() => {
saveCurrentLayout();
}) as EventListener);
@@ -1592,6 +1704,8 @@ onUnmounted(() => {
window.removeEventListener('save-layout', (() => {}) as EventListener);
window.removeEventListener('restore-layout', (() => {}) as EventListener);
+ // 打字机定时器已移至AIResponseDrawer组件中
+
// 断开 WebSocket 连接并清理监听器
webSocketService.off('message', handleWebSocketMessage);
webSocketService.off('connect');
@@ -2638,4 +2752,862 @@ watch(rulePdfDialogVisible, (newValue) => {
text-align: center;
line-height: 1.2;
}
+
+/* AI响应抽屉样式 - 参考ModuleCard背景色 */
+.ai-response-drawer {
+ z-index: 1005 !important;
+
+ /* 全局强制覆盖 - 最高优先级 */
+ &.el-drawer,
+ & .el-drawer,
+ & .el-drawer *,
+ & .el-drawer__wrapper,
+ & .el-drawer__container,
+ & .el-drawer__body {
+ background: linear-gradient(135deg, rgba(15, 23, 42, 0.8) 0%, rgba(30, 41, 59, 0.6) 100%) !important;
+ background-color: transparent !important;
+ }
+
+ /* 强制重写所有可能的Element Plus CSS变量 */
+ :deep(.el-drawer),
+ :deep(.el-drawer *),
+ :deep(.el-drawer__wrapper),
+ :deep(.el-drawer__container) {
+ --el-drawer-bg-color: transparent !important;
+ --el-dialog-bg-color: transparent !important;
+ --el-bg-color: transparent !important;
+ --el-color-white: transparent !important;
+ --el-drawer-padding-primary: 0px !important;
+ }
+
+ :deep(.el-drawer) {
+ background: linear-gradient(135deg, rgba(15, 23, 42, 0.8) 0%, rgba(30, 41, 59, 0.6) 100%) !important;
+ background-color: transparent !important;
+ backdrop-filter: none !important;
+ border-left: 2px solid rgba(0, 212, 255, 0.3);
+ box-shadow:
+ -8px 0 32px rgba(0, 212, 255, 0.15),
+ inset 1px 0 0 rgba(0, 212, 255, 0.1);
+ z-index: 1005 !important;
+ transition: all 0.3s ease;
+ pointer-events: auto;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background:
+ radial-gradient(circle at 20% 20%, rgba(0, 212, 255, 0.08) 0%, transparent 50%),
+ radial-gradient(circle at 80% 80%, rgba(0, 162, 255, 0.06) 0%, transparent 50%),
+ linear-gradient(45deg, transparent 30%, rgba(0, 212, 255, 0.02) 50%, transparent 70%);
+ pointer-events: none;
+ }
+ }
+
+ :deep(.el-overlay) {
+ z-index: 3999 !important;
+ background: transparent !important;
+ backdrop-filter: none !important;
+ }
+
+ :deep(.el-drawer__header) {
+ padding: 0;
+ margin-bottom: 0;
+ border-bottom: 1px solid rgba(0, 212, 255, 0.3);
+ box-shadow: 0 2px 8px rgba(0, 212, 255, 0.1);
+ }
+
+ :deep(.el-drawer__body) {
+ padding: 0;
+ height: calc(100% - 60px);
+ overflow: hidden;
+ position: relative;
+ background: transparent !important;
+ background-color: transparent !important;
+ }
+
+ /* 强制覆盖所有可能的白色背景元素 */
+ :deep(.el-drawer__wrapper) {
+ background: transparent !important;
+ background-color: transparent !important;
+ }
+
+ :deep(.el-drawer__container) {
+ background: transparent !important;
+ background-color: transparent !important;
+ }
+
+ /* 如果还有其他内部元素 */
+ :deep(.el-drawer *) {
+ background-color: transparent !important;
+ }
+
+ /* 重新设置主要内容区域的背景 */
+ :deep(.el-drawer__body),
+ :deep(.drawer-content),
+ :deep(.messages-container) {
+ background: transparent !important;
+ background-color: transparent !important;
+ }
+}
+
+.drawer-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 18px 24px;
+ background: linear-gradient(135deg,
+ rgba(0, 212, 255, 0.1) 0%,
+ rgba(0, 162, 255, 0.08) 50%,
+ rgba(0, 212, 255, 0.05) 100%
+ );
+ border-bottom: 1px solid rgba(0, 212, 255, 0.3);
+ position: relative;
+ z-index: 1005;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(90deg,
+ transparent 0%,
+ rgba(0, 212, 255, 0.05) 50%,
+ transparent 100%
+ );
+ pointer-events: none;
+ }
+
+ .header-title {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ color: #ffffff;
+ font-size: 17px;
+ font-weight: 600;
+ text-shadow: 0 0 8px rgba(0, 212, 255, 0.3);
+ position: relative;
+ z-index: 1;
+
+ svg {
+ color: #00D4FF;
+ filter: drop-shadow(0 0 4px rgba(0, 212, 255, 0.5));
+ }
+ }
+
+ .header-actions {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ :deep(.el-button) {
+ color: rgba(255, 255, 255, 0.8);
+ background: rgba(0, 212, 255, 0.1);
+ border: 1px solid rgba(0, 212, 255, 0.3);
+ border-radius: 6px;
+ transition: all 0.3s ease;
+
+ &:hover {
+ color: #ffffff;
+ background: rgba(0, 212, 255, 0.2);
+ border-color: rgba(0, 212, 255, 0.5);
+ box-shadow: 0 0 8px rgba(0, 212, 255, 0.3);
+ }
+
+ &:disabled {
+ color: rgba(255, 255, 255, 0.3);
+ background: rgba(0, 212, 255, 0.05);
+ border-color: rgba(0, 212, 255, 0.1);
+ }
+ }
+
+ .drawer-minimize-btn-header {
+ width: 32px;
+ height: 32px;
+ background: rgba(0, 212, 255, 0.1);
+ border: 1px solid rgba(0, 212, 255, 0.3);
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ color: rgba(255, 255, 255, 0.8);
+
+ &:hover {
+ color: #ffffff;
+ background: rgba(0, 212, 255, 0.2);
+ border-color: rgba(0, 212, 255, 0.5);
+ box-shadow: 0 0 8px rgba(0, 212, 255, 0.3);
+ transform: scale(1.05);
+ }
+
+ svg {
+ transition: all 0.3s ease;
+ }
+
+ &:hover svg {
+ transform: rotate(180deg);
+ }
+ }
+ }
+}
+
+.drawer-content {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ z-index: 1;
+}
+
+.messages-container {
+ flex: 1;
+ padding: 24px;
+ overflow-y: auto;
+ scroll-behavior: smooth;
+ position: relative;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background:
+ radial-gradient(circle at 10% 10%, rgba(0, 212, 255, 0.03) 0%, transparent 50%),
+ radial-gradient(circle at 90% 90%, rgba(0, 162, 255, 0.02) 0%, transparent 50%);
+ pointer-events: none;
+ z-index: -1;
+ }
+
+ &::-webkit-scrollbar {
+ width: 8px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: linear-gradient(180deg,
+ rgba(0, 212, 255, 0.08) 0%,
+ rgba(0, 162, 255, 0.05) 100%
+ );
+ border-radius: 4px;
+ border: 1px solid rgba(0, 212, 255, 0.1);
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: linear-gradient(180deg,
+ rgba(0, 212, 255, 0.4) 0%,
+ rgba(0, 162, 255, 0.3) 100%
+ );
+ border-radius: 4px;
+ border: 1px solid rgba(0, 212, 255, 0.2);
+ box-shadow: inset 0 0 4px rgba(0, 212, 255, 0.2);
+
+ &:hover {
+ background: linear-gradient(180deg,
+ rgba(0, 212, 255, 0.6) 0%,
+ rgba(0, 162, 255, 0.5) 100%
+ );
+ box-shadow:
+ inset 0 0 4px rgba(0, 212, 255, 0.3),
+ 0 0 8px rgba(0, 212, 255, 0.2);
+ }
+ }
+}
+
+.message-item {
+ margin-bottom: 20px;
+
+ &.user {
+ .user-message {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+
+ .message-avatar {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ background: rgba(0, 212, 255, 0.2);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+
+ svg {
+ color: #00D4FF;
+ }
+ }
+
+ .message-content {
+ flex: 1;
+ background: linear-gradient(135deg,
+ rgba(0, 212, 255, 0.12) 0%,
+ rgba(0, 162, 255, 0.08) 100%
+ );
+ border: 1px solid rgba(0, 212, 255, 0.3);
+ border-radius: 16px 16px 16px 6px;
+ padding: 14px 18px;
+ box-shadow:
+ 0 2px 8px rgba(0, 212, 255, 0.1),
+ inset 0 1px 0 rgba(0, 212, 255, 0.1);
+ backdrop-filter: blur(5px);
+
+ .message-text {
+ color: #ffffff;
+ line-height: 1.5;
+ word-wrap: break-word;
+ }
+
+ .message-time {
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.5);
+ margin-top: 8px;
+ }
+ }
+ }
+ }
+
+ &.ai {
+ .ai-message {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+
+ .message-avatar {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ background: rgba(82, 196, 26, 0.2);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+
+ svg {
+ color: #00FFC8;
+ filter: drop-shadow(0 0 4px rgba(0, 255, 200, 0.5));
+ }
+
+ .typing-indicator {
+ display: flex;
+ gap: 2px;
+
+ span {
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ background: #00FFC8;
+ animation: typing 1.4s infinite ease-in-out;
+
+ &:nth-child(1) { animation-delay: 0s; }
+ &:nth-child(2) { animation-delay: 0.2s; }
+ &:nth-child(3) { animation-delay: 0.4s; }
+ }
+ }
+ }
+
+ .message-content {
+ flex: 1;
+ background: linear-gradient(135deg,
+ rgba(0, 255, 200, 0.12) 0%,
+ rgba(0, 212, 255, 0.08) 100%
+ );
+ border: 1px solid rgba(0, 255, 200, 0.3);
+ border-radius: 16px 16px 6px 16px;
+ padding: 14px 18px;
+ position: relative;
+ box-shadow:
+ 0 2px 8px rgba(0, 255, 200, 0.1),
+ inset 0 1px 0 rgba(0, 255, 200, 0.1);
+ backdrop-filter: blur(5px);
+
+ .message-text {
+ color: #ffffff;
+ line-height: 1.5;
+ word-wrap: break-word;
+
+ :deep(strong) {
+ font-weight: 600;
+ color: #00D4FF;
+ }
+
+ :deep(em) {
+ font-style: italic;
+ color: #FAAD14;
+ }
+
+ :deep(code) {
+ background: rgba(0, 0, 0, 0.3);
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-family: 'Courier New', monospace;
+ font-size: 0.9em;
+ color: #00D4FF;
+ }
+
+ :deep(pre) {
+ background: rgba(0, 0, 0, 0.3);
+ padding: 12px;
+ border-radius: 6px;
+ margin: 8px 0;
+ overflow-x: auto;
+
+ code {
+ background: none;
+ padding: 0;
+ color: #ffffff;
+ }
+ }
+ }
+
+ &.streaming-text {
+ min-height: 20px;
+ }
+
+ .streaming-cursor {
+ display: inline-block;
+ color: #00FFC8;
+ animation: blink 1s infinite;
+ margin-left: 2px;
+ text-shadow: 0 0 4px rgba(0, 255, 200, 0.5);
+ }
+
+ .message-time {
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.5);
+ margin-top: 8px;
+ }
+ }
+ }
+ }
+
+ &.streaming {
+ .ai-message .message-content {
+ border-color: rgba(0, 255, 200, 0.5);
+ box-shadow:
+ 0 0 12px rgba(0, 255, 200, 0.3),
+ 0 2px 8px rgba(0, 255, 200, 0.1),
+ inset 0 1px 0 rgba(0, 255, 200, 0.2);
+ animation: streamingGlow 2s ease-in-out infinite alternate;
+ }
+ }
+}
+
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ text-align: center;
+ color: rgba(255, 255, 255, 0.6);
+
+ .empty-icon {
+ margin-bottom: 20px;
+ opacity: 0.5;
+
+ svg {
+ color: rgba(0, 212, 255, 0.5);
+ }
+ }
+
+ .empty-text {
+ h3 {
+ font-size: 18px;
+ font-weight: 500;
+ margin: 0 0 8px 0;
+ color: rgba(255, 255, 255, 0.8);
+ }
+
+ p {
+ font-size: 14px;
+ margin: 0;
+ line-height: 1.5;
+ }
+ }
+}
+
+@keyframes typing {
+ 0%, 60%, 100% {
+ transform: translateY(0);
+ opacity: 0.5;
+ }
+ 30% {
+ transform: translateY(-10px);
+ opacity: 1;
+ }
+}
+
+@keyframes blink {
+ 0%, 50% {
+ opacity: 1;
+ }
+ 51%, 100% {
+ opacity: 0;
+ }
+}
+
+@keyframes streamingGlow {
+ 0% {
+ box-shadow:
+ 0 0 12px rgba(0, 255, 200, 0.3),
+ 0 2px 8px rgba(0, 255, 200, 0.1),
+ inset 0 1px 0 rgba(0, 255, 200, 0.2);
+ }
+ 100% {
+ box-shadow:
+ 0 0 20px rgba(0, 255, 200, 0.5),
+ 0 4px 16px rgba(0, 255, 200, 0.2),
+ inset 0 1px 0 rgba(0, 255, 200, 0.3);
+ }
+}
+
+
+/* AI悬浮图标样式 - 右下角固定位置 */
+.ai-floating-icon {
+ position: fixed;
+ bottom: 24px;
+ right: 24px;
+ z-index: 1006;
+ width: 64px;
+ height: 64px;
+ cursor: pointer;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ animation: slideInFromRight 0.5s cubic-bezier(0.4, 0, 0.2, 1);
+
+ .floating-icon-inner {
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(135deg, rgba(15, 23, 42, 0.9) 0%, rgba(30, 41, 59, 0.8) 100%);
+ border: 2px solid rgba(0, 212, 255, 0.4);
+ border-radius: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ backdrop-filter: blur(15px);
+ box-shadow:
+ 0 8px 32px rgba(0, 212, 255, 0.2),
+ 0 0 0 1px rgba(0, 212, 255, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
+ transition: all 0.3s ease;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background:
+ radial-gradient(circle at 30% 30%, rgba(0, 212, 255, 0.1) 0%, transparent 50%),
+ radial-gradient(circle at 70% 70%, rgba(0, 162, 255, 0.08) 0%, transparent 50%);
+ pointer-events: none;
+ }
+
+ svg {
+ color: #00D4FF;
+ filter: drop-shadow(0 0 8px rgba(0, 212, 255, 0.6));
+ transition: all 0.3s ease;
+ position: relative;
+ z-index: 1;
+ }
+ }
+
+ .activity-indicator {
+ position: absolute;
+ top: -2px;
+ right: -2px;
+
+ .pulse-dot {
+ width: 12px;
+ height: 12px;
+ background: #00FFC8;
+ border-radius: 50%;
+ animation: pulse 2s infinite;
+ box-shadow:
+ 0 0 8px rgba(0, 255, 200, 0.8),
+ 0 0 16px rgba(0, 255, 200, 0.4);
+ border: 2px solid rgba(15, 23, 42, 0.8);
+ }
+ }
+
+ .message-count {
+ position: absolute;
+ top: -8px;
+ right: -8px;
+ background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%);
+ color: white;
+ font-size: 12px;
+ font-weight: 600;
+ min-width: 20px;
+ height: 20px;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 6px;
+ box-shadow:
+ 0 2px 8px rgba(255, 77, 79, 0.4),
+ 0 0 0 2px rgba(15, 23, 42, 0.8);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ }
+
+ &:hover {
+ transform: translateY(-4px) scale(1.05);
+
+ .floating-icon-inner {
+ background: linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.9) 100%);
+ border-color: rgba(0, 212, 255, 0.6);
+ box-shadow:
+ 0 12px 48px rgba(0, 212, 255, 0.3),
+ 0 0 0 1px rgba(0, 212, 255, 0.2),
+ inset 0 1px 0 rgba(255, 255, 255, 0.2);
+
+ svg {
+ transform: scale(1.1);
+ filter: drop-shadow(0 0 12px rgba(0, 212, 255, 0.8));
+ }
+ }
+ }
+
+ &:active {
+ transform: translateY(-2px) scale(1.02);
+ }
+
+ &.has-activity {
+ .floating-icon-inner {
+ border-color: rgba(0, 255, 200, 0.5);
+ box-shadow:
+ 0 8px 32px rgba(0, 255, 200, 0.2),
+ 0 0 0 1px rgba(0, 255, 200, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
+ animation: activityGlow 2s ease-in-out infinite alternate;
+ }
+ }
+}
+
+@keyframes slideInFromRight {
+ 0% {
+ opacity: 0;
+ transform: translateX(100px) scale(0.8);
+ }
+ 60% {
+ opacity: 1;
+ transform: translateX(-5px) scale(1.05);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateX(0) scale(1);
+ }
+}
+
+@keyframes activityGlow {
+ 0% {
+ box-shadow:
+ 0 8px 32px rgba(0, 255, 200, 0.2),
+ 0 0 0 1px rgba(0, 255, 200, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
+ }
+ 100% {
+ box-shadow:
+ 0 12px 48px rgba(0, 255, 200, 0.4),
+ 0 0 0 1px rgba(0, 255, 200, 0.3),
+ inset 0 1px 0 rgba(255, 255, 255, 0.2);
+ }
+}
+
+/* 悬浮图标的淡入淡出动画 */
+.float-fade-enter-active {
+ transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.float-fade-leave-active {
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.float-fade-enter-from {
+ opacity: 0;
+ transform: translateX(100px) scale(0.8);
+}
+
+.float-fade-leave-to {
+ opacity: 0;
+ transform: translateX(100px) scale(0.8);
+}
+
+@keyframes pulse {
+ 0% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ 50% {
+ transform: scale(1.2);
+ opacity: 0.7;
+ }
+ 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+// AI抽屉和悬浮图标响应式设计
+@media (max-width: 768px) {
+ .ai-response-drawer {
+ :deep(.el-drawer) {
+ width: 90% !important;
+ }
+ }
+
+ .messages-container {
+ padding: 16px;
+ }
+
+ .message-item {
+ .user-message,
+ .ai-message {
+ gap: 8px;
+
+ .message-avatar {
+ width: 28px;
+ height: 28px;
+
+ svg {
+ width: 14px;
+ height: 14px;
+ }
+ }
+
+ .message-content {
+ padding: 10px 12px;
+ font-size: 14px;
+ }
+ }
+ }
+
+ // 移动端悬浮图标调整
+ .ai-floating-icon {
+ bottom: 16px;
+ right: 16px;
+ width: 56px;
+ height: 56px;
+
+ .floating-icon-inner {
+ border-radius: 14px;
+
+ svg {
+ width: 20px;
+ height: 20px;
+ }
+ }
+
+ .activity-indicator .pulse-dot {
+ width: 10px;
+ height: 10px;
+ }
+
+ .message-count {
+ top: -6px;
+ right: -6px;
+ min-width: 18px;
+ height: 18px;
+ font-size: 11px;
+ }
+ }
+}
+
+@media (max-width: 480px) {
+ .ai-floating-icon {
+ bottom: 12px;
+ right: 12px;
+ width: 48px;
+ height: 48px;
+
+ .floating-icon-inner {
+ border-radius: 12px;
+
+ svg {
+ width: 18px;
+ height: 18px;
+ }
+ }
+
+ .activity-indicator .pulse-dot {
+ width: 8px;
+ height: 8px;
+ }
+
+ .message-count {
+ top: -4px;
+ right: -4px;
+ min-width: 16px;
+ height: 16px;
+ font-size: 10px;
+ }
+ }
+}
+
+
+
diff --git a/chat-client/src/views/guest/homePage/index.vue b/chat-client/src/views/guest/homePage/index.vue
index 2d601f5..88254a2 100644
--- a/chat-client/src/views/guest/homePage/index.vue
+++ b/chat-client/src/views/guest/homePage/index.vue
@@ -163,6 +163,10 @@
@open-case-pdf="handleOpenCasePdf"
@show-card-selector="showCardSelector"
@remove-selected-card="removeSelectedCard"
+ @ai-user-message="handleAIUserMessage"
+ @ai-stream-start="handleAIStreamStart"
+ @ai-stream-update="handleAIStreamUpdate"
+ @ai-stream-finish="handleAIStreamFinish"
/>
@@ -237,6 +241,15 @@
+
+
+
@@ -246,6 +259,7 @@ import { useRoute } from 'vue-router';
import { MODULE_CONFIGS } from '@/constants/modules';
import ModuleCard from '@/components/dashboard/ModuleCard.vue';
import DetailModal from '@/components/dashboard/DetailModal.vue';
+import AIResponseDrawer from '@/components/AIResponseDrawer.vue';
import type { ModuleConfig, ModuleType, SearchResult, StatusStats, Position, Size, RuleRegulation, TypicalCase } from '@/types/dashboard';
import { webSocketService } from '@/utils/websocket';
import { baseURL } from '@/config';
@@ -305,6 +319,10 @@ const questionTextarea = ref(); // 输入框引用
const atSuggestionsStyle = ref({}); // @推荐框的样式定位
const selectedCards = ref([]); // 选中的卡片列表,用于显示tag标签
+// AI响应抽屉相关
+const showAIDrawer = ref(false); // 控制AI响应抽屉显示
+const aiDrawerRef = ref>(); // AI抽屉组件引用
+
// 监听弹窗状态变化
watch(showCardSelectorDialog, (newVal, oldVal) => {
// 弹窗状态变化处理
@@ -1545,6 +1563,71 @@ const showNotification = (message: string, type: 'success' | 'error' | 'info' =
}, 3000);
};
+/**
+ * 处理AI用户消息事件
+ */
+const handleAIUserMessage = (message: string) => {
+ console.log('收到用户消息:', message);
+ // 显示AI抽屉
+ showAIDrawer.value = true;
+ // 添加用户消息到抽屉
+ nextTick(() => {
+ aiDrawerRef.value?.addUserMessage(message);
+ });
+};
+
+/**
+ * 处理AI流式响应开始事件
+ */
+const handleAIStreamStart = () => {
+ console.log('AI流式响应开始');
+ // 确保抽屉是打开的
+ showAIDrawer.value = true;
+ // 开始流式响应
+ nextTick(() => {
+ aiDrawerRef.value?.startStreaming();
+ });
+};
+
+/**
+ * 处理AI流式响应更新事件
+ */
+const handleAIStreamUpdate = (content: string) => {
+ console.log('AI流式响应更新:', content.length, '字符');
+ // 更新流式内容
+ aiDrawerRef.value?.updateStreamContent(content);
+};
+
+/**
+ * 处理AI流式响应完成事件
+ */
+const handleAIStreamFinish = (finalContent?: string) => {
+ console.log('AI流式响应完成,最终内容长度:', finalContent?.length || 0);
+ // 完成流式响应
+ aiDrawerRef.value?.finishStreaming(finalContent);
+};
+
+/**
+ * 处理AI抽屉关闭事件
+ */
+const handleAIDrawerClose = () => {
+ showAIDrawer.value = false;
+};
+
+/**
+ * 处理AI抽屉缩略事件
+ */
+const handleAIDrawerMinimize = () => {
+ console.log('AI抽屉已缩略');
+};
+
+/**
+ * 处理AI抽屉展开事件
+ */
+const handleAIDrawerMaximize = () => {
+ console.log('AI抽屉已展开');
+};
+
diff --git a/chat-server/src/main/java/com/bjtds/brichat/service/impl/ChatServiceImpl.java b/chat-server/src/main/java/com/bjtds/brichat/service/impl/ChatServiceImpl.java
index 7b9d44d..ce3c264 100644
--- a/chat-server/src/main/java/com/bjtds/brichat/service/impl/ChatServiceImpl.java
+++ b/chat-server/src/main/java/com/bjtds/brichat/service/impl/ChatServiceImpl.java
@@ -4,6 +4,7 @@ import com.bjtds.brichat.entity.chat.AgentInputCardReq;
import com.bjtds.brichat.entity.chat.ChatMessage;
import com.bjtds.brichat.entity.chat.ChatTypeVo;
import com.bjtds.brichat.entity.dataset.TUserDataset;
+import com.bjtds.brichat.entity.sys.ApiKey;
import com.bjtds.brichat.mapper.opengauss.TUserDatasetMapper;
import com.bjtds.brichat.service.KnowledgeBaseService;
import com.bjtds.brichat.service.TApiKeyService;
@@ -131,14 +132,16 @@ public class ChatServiceImpl implements ChatService {
datasetIds.add(datasetId);
}
}
- UUID appId = UUID.fromString("f940e32f-e302-49b8-b030-591539c26779");
+
+ ApiKey workFlowInfo = apiKeyService.queryByName("agentInput");
+ UUID appId = UUID.fromString(workFlowInfo.getRemark());
final boolean b = knowledgeBaseService.addDatasetsToWorkflow(appId, datasetIds);
//启动对话流
ChatMessageSendRequest sendRequest = new ChatMessageSendRequest();
sendRequest.setUserId(agentInputCardReq.getUserId());
sendRequest.setContent(agentInputCardReq.getContent());
- sendRequest.setApiKey("app-jCKfKpPW8f9C7ZXLElAq1DSS");
+ sendRequest.setApiKey(workFlowInfo.getValue());
Map inputMap = new HashMap<>();
inputMap.put("sys_message_id", sysMessage);
inputMap.put("context", agentInputCardReq.getContext());