作者 lixiang

1、智能助手修改回答方式为流式回答

2、修改智能助手页面样式
1 import { defHttp } from '/@/utils/http/axios'; 1 import { defHttp } from '/@/utils/http/axios';
2 import { useMessage } from '/@/hooks/web/useMessage'; 2 import { useMessage } from '/@/hooks/web/useMessage';
  3 +import { useUserStore } from '/@/store/modules/user'; // 引入用户store获取token
3 4
4 const { createMessage } = useMessage(); 5 const { createMessage } = useMessage();
5 6
@@ -9,43 +10,76 @@ enum Api { @@ -9,43 +10,76 @@ enum Api {
9 } 10 }
10 11
11 /** 12 /**
12 - * 发送消息 13 + * 发送消息(流式处理)
13 * @param params 14 * @param params
14 */ 15 */
15 export const sendMessage = async (params: { questionText: string }) => { 16 export const sendMessage = async (params: { questionText: string }) => {
16 try { 17 try {
17 - const res = await defHttp.get({  
18 - url: Api.send,  
19 - params,  
20 - timeout: 60000  
21 - });  
22 - console.log("res...",res)  
23 - // 确保返回的数据结构正确  
24 - if (res ) {  
25 - return {  
26 - answer: res.answer || '',  
27 - similarity: res.similarity || 0,  
28 - fileName: res.fileName || '',  
29 - fileBase64: res.fileBase64 || null  
30 - }; 18 + const userStore = useUserStore();
  19 + const token = userStore.getToken;
  20 +
  21 + const response = await fetch(
  22 + `/jeecgboot/airag/zdyRag/sendStream?questionText=${encodeURIComponent(params.questionText)}`,
  23 + {
  24 + method: 'GET',
  25 + headers: {
  26 + 'x-access-token': token,
  27 + 'Accept': 'text/event-stream'
  28 + },
  29 + credentials: 'include',
31 } 30 }
32 - return null; 31 + );
  32 +
  33 + if (!response.ok || !response.body) {
  34 + throw new Error(`请求失败: ${response.status} ${response.statusText}`);
  35 + }
  36 +
  37 + // 返回异步生成器函数
  38 + return (async function* () {
  39 + const reader = response.body.getReader();
  40 + const decoder = new TextDecoder('utf-8');
  41 + let buffer = '';
  42 +
  43 + try {
  44 + while (true) {
  45 + const { done, value } = await reader.read();
  46 + if (done) break;
  47 +
  48 + const chunk = decoder.decode(value, { stream: true });
  49 + buffer += chunk;
  50 +
  51 + const lines = buffer.split('\n');
  52 + buffer = lines.pop() || '';
  53 +
  54 + for (const line of lines) {
  55 + if (line.startsWith('data:')) {
  56 + try {
  57 + const jsonStr = line.substring(5).trim();
  58 + if (jsonStr) {
  59 + yield JSON.parse(jsonStr); // 直接 yield 解析后的数据
  60 + }
  61 + } catch (e) {
  62 + console.error('JSON解析错误:', e);
  63 + }
  64 + }
  65 + }
  66 + }
  67 + } finally {
  68 + reader.releaseLock();
  69 + }
  70 + })();
33 } catch (error) { 71 } catch (error) {
34 - console.error("Error sending message:", error); 72 + console.error('Error sending message:', error);
35 createMessage.error('发送消息失败'); 73 createMessage.error('发送消息失败');
36 - return null; 74 + throw error; // 抛出错误让调用方处理
37 } 75 }
38 }; 76 };
39 -  
40 -  
41 -export const getButtonList = async () =>{ 77 +export const getButtonList = async () => {
42 const res = await defHttp.get({ 78 const res = await defHttp.get({
43 url: Api.buttonList, 79 url: Api.buttonList,
44 }); 80 });
45 - console.log("res",res);  
46 - if (res){ 81 + if (res) {
47 return res; 82 return res;
48 } 83 }
49 return null; 84 return null;
50 -  
51 } 85 }
@@ -11,11 +11,17 @@ @@ -11,11 +11,17 @@
11 相似度: {{ (message.similarity * 100).toFixed(2) }}% 11 相似度: {{ (message.similarity * 100).toFixed(2) }}%
12 <div v-if="message.fileBase64"> 12 <div v-if="message.fileBase64">
13 相关资料: 13 相关资料:
14 - <a @click="showPreview(message)">{{message.fileName}}</a> 14 + <a @click="showPreview(message)">{{ message.fileName }}</a>
15 </div> 15 </div>
16 </div> 16 </div>
17 </div> 17 </div>
18 </div> 18 </div>
  19 + <!-- 流式响应时显示的临时消息 -->
  20 + <div v-if="streamingMessage" class="message assistant">
  21 + <div class="message-content">
  22 + <div class="message-text">{{ streamingMessage }}</div>
  23 + </div>
  24 + </div>
19 <div v-if="loading" class="message assistant"> 25 <div v-if="loading" class="message assistant">
20 <div class="message-content"> 26 <div class="message-content">
21 <div class="message-text">思考中...</div> 27 <div class="message-text">思考中...</div>
@@ -25,7 +31,6 @@ @@ -25,7 +31,6 @@
25 31
26 <!-- 文件预览区域 --> 32 <!-- 文件预览区域 -->
27 <div v-if="previewContent" class="file-preview"> 33 <div v-if="previewContent" class="file-preview">
28 -  
29 <div class="preview-header"> 34 <div class="preview-header">
30 <span class="preview-title">文件预览</span> 35 <span class="preview-title">文件预览</span>
31 <div class="preview-actions"> 36 <div class="preview-actions">
@@ -58,24 +63,12 @@ @@ -58,24 +63,12 @@
58 </div> 63 </div>
59 </div> 64 </div>
60 </div> 65 </div>
61 - <div> 66 +
  67 + <!-- 快捷按钮区域 -->
  68 + <div class="quick-actions">
62 <a-button 69 <a-button
63 - style="margin-left: 1%;  
64 - background-color: white;  
65 - color: #666;  
66 - border: 1px solid rgba(0, 0, 0, 0.1);  
67 - border-radius: 16px;  
68 - cursor: pointer;  
69 - font-size: 14px;  
70 - padding: 2px 10px;  
71 - width: max-content;  
72 - margin-right: 6px;  
73 - white-space: nowrap;  
74 - transition: all 300ms ease;  
75 -"  
76 v-for="(btn, index) in buttonList" 70 v-for="(btn, index) in buttonList"
77 :key="index" 71 :key="index"
78 - type="primary"  
79 class="action-button" 72 class="action-button"
80 @click="sendButtonMessage(btn.buttonValues)" 73 @click="sendButtonMessage(btn.buttonValues)"
81 > 74 >
@@ -96,6 +89,7 @@ @@ -96,6 +89,7 @@
96 @click="sendMessage" 89 @click="sendMessage"
97 :loading="loading" 90 :loading="loading"
98 :disabled="!inputMessage.trim()" 91 :disabled="!inputMessage.trim()"
  92 + class="send-button"
99 > 93 >
100 发送 94 发送
101 </a-button> 95 </a-button>
@@ -105,7 +99,7 @@ @@ -105,7 +99,7 @@
105 99
106 <script lang="ts" name="zdy-rag-chat" setup> 100 <script lang="ts" name="zdy-rag-chat" setup>
107 import { ref, reactive, nextTick, onMounted, onBeforeUnmount } from 'vue'; 101 import { ref, reactive, nextTick, onMounted, onBeforeUnmount } from 'vue';
108 -import { sendMessage as apiSendMessage , getButtonList} from './ZdyRag.api'; 102 +import { sendMessage as apiSendMessage, getButtonList } from './ZdyRag.api';
109 import { useMessage } from '/@/hooks/web/useMessage'; 103 import { useMessage } from '/@/hooks/web/useMessage';
110 import { CloseOutlined, DownloadOutlined } from '@ant-design/icons-vue'; 104 import { CloseOutlined, DownloadOutlined } from '@ant-design/icons-vue';
111 import * as pdfjsLib from 'pdfjs-dist'; 105 import * as pdfjsLib from 'pdfjs-dist';
@@ -121,12 +115,9 @@ onMounted(async () => { @@ -121,12 +115,9 @@ onMounted(async () => {
121 const res = await getButtonList(); 115 const res = await getButtonList();
122 if (res && Array.isArray(res)) { 116 if (res && Array.isArray(res)) {
123 buttonList.push(...res); 117 buttonList.push(...res);
124 - } else {  
125 - // createMessage.warning('未获取到按钮列表数据');  
126 } 118 }
127 } catch (error) { 119 } catch (error) {
128 console.error('获取按钮列表失败:', error); 120 console.error('获取按钮列表失败:', error);
129 - // createMessage.error('获取按钮列表失败');  
130 } 121 }
131 }); 122 });
132 123
@@ -143,6 +134,7 @@ const messages = reactive([ @@ -143,6 +134,7 @@ const messages = reactive([
143 134
144 const inputMessage = ref(''); 135 const inputMessage = ref('');
145 const loading = ref(false); 136 const loading = ref(false);
  137 +const streamingMessage = ref(''); // 流式响应消息
146 const messagesContainer = ref<HTMLElement>(); 138 const messagesContainer = ref<HTMLElement>();
147 const previewContent = ref(''); 139 const previewContent = ref('');
148 const previewType = ref(''); 140 const previewType = ref('');
@@ -167,28 +159,52 @@ const sendMessage = async () => { @@ -167,28 +159,52 @@ const sendMessage = async () => {
167 159
168 inputMessage.value = ''; 160 inputMessage.value = '';
169 loading.value = true; 161 loading.value = true;
  162 + streamingMessage.value = '';
170 closePreview(); 163 closePreview();
  164 + scrollToBottom();
171 165
172 try { 166 try {
173 - // 调用API发送消息  
174 - const res = await apiSendMessage({ questionText: text });  
175 - console.log('API响应:', res);  
176 -  
177 - if (res) { 167 + // 创建最终消息对象
178 const newMessage = { 168 const newMessage = {
179 type: 'assistant', 169 type: 'assistant',
180 - text: formatAnswer(res.answer),  
181 - similarity: res.similarity || null,  
182 - fileBase64: res.fileBase64 || null,  
183 - fileName: res.fileName || '' 170 + text: '',
  171 + similarity: 0,
  172 + fileBase64: null,
  173 + fileName: ''
184 }; 174 };
185 - messages.push(newMessage);  
186 - } else {  
187 - // createMessage.error('获取回答失败: 返回数据为空'); 175 +
  176 + // 调用API获取异步生成器
  177 + const streamGenerator = await apiSendMessage({ questionText: text });
  178 +
  179 + // 遍历异步生成器
  180 + for await (const chunk of streamGenerator) {
  181 + if (chunk.token) {
  182 + if (!streamingMessage.value){
  183 + loading.value = false;
  184 + }
  185 + streamingMessage.value += chunk.token;
  186 + newMessage.text += chunk.token;
  187 + scrollToBottom();
  188 + } else if (chunk.event === 'END') {
  189 + newMessage.similarity = parseFloat(chunk.similarity) || 0;
  190 + newMessage.fileName = chunk.fileName || '';
  191 + newMessage.fileBase64 = chunk.fileBase64 || null;
188 } 192 }
  193 + }
  194 +
  195 + // 将流式消息添加到消息列表
  196 + messages.push(newMessage);
  197 + streamingMessage.value = '';
  198 +
189 } catch (error: any) { 199 } catch (error: any) {
190 console.error('发送消息失败:', error); 200 console.error('发送消息失败:', error);
191 - // createMessage.error('发送消息失败: ' + error.message); 201 + messages.push({
  202 + type: 'assistant',
  203 + text: '请求出错,请稍后重试',
  204 + similarity: null,
  205 + fileBase64: null,
  206 + fileName: null
  207 + });
192 } finally { 208 } finally {
193 loading.value = false; 209 loading.value = false;
194 scrollToBottom(); 210 scrollToBottom();
@@ -208,34 +224,58 @@ const sendButtonMessage = async (buttonValue: string) => { @@ -208,34 +224,58 @@ const sendButtonMessage = async (buttonValue: string) => {
208 }); 224 });
209 225
210 loading.value = true; 226 loading.value = true;
  227 + streamingMessage.value = '';
211 closePreview(); 228 closePreview();
  229 + scrollToBottom();
212 230
213 try { 231 try {
214 - // 调用API发送按钮值  
215 - const res = await apiSendMessage({ questionText: buttonValue });  
216 -  
217 - if (res) { 232 + // 创建最终消息对象
218 const newMessage = { 233 const newMessage = {
219 type: 'assistant', 234 type: 'assistant',
220 - text: formatAnswer(res.answer),  
221 - similarity: res.similarity || null,  
222 - fileBase64: res.fileBase64 || null,  
223 - fileName: res.fileName || '' 235 + text: '',
  236 + similarity: 0,
  237 + fileBase64: null,
  238 + fileName: ''
224 }; 239 };
225 - messages.push(newMessage);  
226 - } else {  
227 - console.error('获取回答失败: 返回数据为空'); 240 +
  241 + // 调用API获取异步生成器
  242 + const streamGenerator = await apiSendMessage({ questionText: buttonValue });
  243 +
  244 + // 遍历异步生成器
  245 + for await (const chunk of streamGenerator) {
  246 + if (chunk.token) {
  247 + if (!streamingMessage.value){
  248 + loading.value = false;
228 } 249 }
  250 + streamingMessage.value += chunk.token;
  251 + newMessage.text += chunk.token;
  252 + scrollToBottom();
  253 + } else if (chunk.event === 'END') {
  254 + newMessage.similarity = parseFloat(chunk.similarity) || 0;
  255 + newMessage.fileName = chunk.fileName || '';
  256 + newMessage.fileBase64 = chunk.fileBase64 || null;
  257 + }
  258 + }
  259 +
  260 + // 将流式消息添加到消息列表
  261 + messages.push(newMessage);
  262 + streamingMessage.value = '';
  263 +
229 } catch (error: any) { 264 } catch (error: any) {
230 console.error('发送按钮消息失败:', error); 265 console.error('发送按钮消息失败:', error);
231 - // createMessage.error('发送按钮消息失败: ' + error.message); 266 + messages.push({
  267 + type: 'assistant',
  268 + text: '请求出错,请稍后重试',
  269 + similarity: null,
  270 + fileBase64: null,
  271 + fileName: null
  272 + });
232 } finally { 273 } finally {
233 loading.value = false; 274 loading.value = false;
234 scrollToBottom(); 275 scrollToBottom();
235 } 276 }
236 }; 277 };
237 -  
238 -// 增强版showPreview函数 278 +// 显示文件预览
239 const showPreview = async (message: any) => { 279 const showPreview = async (message: any) => {
240 try { 280 try {
241 // 验证基本数据 281 // 验证基本数据
@@ -275,7 +315,6 @@ const showPreview = async (message: any) => { @@ -275,7 +315,6 @@ const showPreview = async (message: any) => {
275 } catch (error) { 315 } catch (error) {
276 console.error('预览失败:', error); 316 console.error('预览失败:', error);
277 previewContent.value = 'error'; 317 previewContent.value = 'error';
278 - // createMessage.error(`预览失败: ${error.message}`);  
279 } 318 }
280 }; 319 };
281 320
@@ -365,7 +404,6 @@ const decodeBase64Content = (base64: string): string => { @@ -365,7 +404,6 @@ const decodeBase64Content = (base64: string): string => {
365 // 下载文件 404 // 下载文件
366 const downloadFile = () => { 405 const downloadFile = () => {
367 if (!currentFile.value || !currentFile.value.fileBase64 || !currentFile.value.fileName) { 406 if (!currentFile.value || !currentFile.value.fileBase64 || !currentFile.value.fileName) {
368 - // createMessage.warning('无法下载文件');  
369 return; 407 return;
370 } 408 }
371 409
@@ -384,11 +422,8 @@ const downloadFile = () => { @@ -384,11 +422,8 @@ const downloadFile = () => {
384 document.body.appendChild(link); 422 document.body.appendChild(link);
385 link.click(); 423 link.click();
386 document.body.removeChild(link); 424 document.body.removeChild(link);
387 -  
388 - // createMessage.success('文件下载开始');  
389 } catch (error: any) { 425 } catch (error: any) {
390 console.error('文件下载失败:', error); 426 console.error('文件下载失败:', error);
391 - // createMessage.error('文件下载失败: ' + error.message);  
392 } 427 }
393 }; 428 };
394 429
@@ -407,12 +442,6 @@ const closePreview = () => { @@ -407,12 +442,6 @@ const closePreview = () => {
407 if (docxPreview.value) docxPreview.value.innerHTML = ''; 442 if (docxPreview.value) docxPreview.value.innerHTML = '';
408 }; 443 };
409 444
410 -// 格式化回答内容  
411 -const formatAnswer = (answer: string) => {  
412 - // 替换[n]为换行符,然后将换行符转换为<br>标签  
413 - return answer.replace(/\[n\]/g, '\n').replace(/\n/g, '<br>');  
414 -};  
415 -  
416 // 滚动到底部 445 // 滚动到底部
417 const scrollToBottom = () => { 446 const scrollToBottom = () => {
418 nextTick(() => { 447 nextTick(() => {
@@ -429,16 +458,17 @@ onBeforeUnmount(() => { @@ -429,16 +458,17 @@ onBeforeUnmount(() => {
429 </script> 458 </script>
430 459
431 <style lang="less" scoped> 460 <style lang="less" scoped>
432 -/* 样式保持不变 */  
433 .chat-container { 461 .chat-container {
434 display: flex; 462 display: flex;
435 flex-direction: column; 463 flex-direction: column;
436 - height: calc(100vh - 120px);  
437 - max-width: 1200px; 464 + height: calc(100vh - 60px);
  465 + max-width: 100%;
438 margin: 0 auto; 466 margin: 0 auto;
439 padding: 20px; 467 padding: 20px;
440 - background-color: #f5f5f5;  
441 - border-radius: 8px; 468 + background-color: #f9fafb;
  469 + border-radius: 16px;
  470 + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
  471 + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
442 } 472 }
443 473
444 .main-content { 474 .main-content {
@@ -447,151 +477,83 @@ onBeforeUnmount(() => { @@ -447,151 +477,83 @@ onBeforeUnmount(() => {
447 gap: 20px; 477 gap: 20px;
448 margin-bottom: 20px; 478 margin-bottom: 20px;
449 overflow: hidden; 479 overflow: hidden;
  480 + background-color: #fff;
  481 + border-radius: 12px;
  482 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
450 } 483 }
451 484
452 .chat-messages { 485 .chat-messages {
453 flex: 1; 486 flex: 1;
454 overflow-y: auto; 487 overflow-y: auto;
455 - padding: 15px;  
456 - background-color: white;  
457 - border-radius: 8px;  
458 - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 488 + padding: 20px;
  489 + background-color: #fff;
  490 + border-radius: 12px;
459 min-height: 300px; 491 min-height: 300px;
  492 + display: flex;
  493 + flex-direction: column;
460 } 494 }
461 495
462 -//.file-preview {  
463 -// width: 55%;  
464 -// min-width: 300px;  
465 -// display: flex;  
466 -// flex-direction: column;  
467 -// background-color: white;  
468 -// border-radius: 8px;  
469 -// box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);  
470 -// overflow: hidden;  
471 -//  
472 -// .preview-header {  
473 -// display: flex;  
474 -// justify-content: space-between;  
475 -// align-items: center;  
476 -// padding: 10px 15px;  
477 -// background-color: #f0f0f0;  
478 -// border-bottom: 1px solid #e8e8e8;  
479 -//  
480 -// .preview-actions {  
481 -// display: flex;  
482 -// align-items: center;  
483 -// gap: 8px;  
484 -// }  
485 -// }  
486 -//  
487 -// .preview-content {  
488 -// flex: 1;  
489 -// overflow: auto;  
490 -// padding: 10px;  
491 -//  
492 -// .text-preview {  
493 -// padding: 15px;  
494 -// white-space: pre-wrap;  
495 -// font-family: monospace;  
496 -// line-height: 1.6;  
497 -// }  
498 -//  
499 -// .pdf-preview {  
500 -// height: 100%;  
501 -// overflow-y: auto;  
502 -//  
503 -// .pdf-pages {  
504 -// display: flex;  
505 -// flex-direction: column;  
506 -// gap: 20px;  
507 -// padding: 10px;  
508 -//  
509 -// .pdf-page {  
510 -// border: 1px solid #e8e8e8;  
511 -// box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);  
512 -// }  
513 -// }  
514 -// }  
515 -//  
516 -// .docx-preview {  
517 -// height: 100%;  
518 -// overflow-y: auto;  
519 -// padding: 20px;  
520 -// background-color: #fff;  
521 -// }  
522 -//  
523 -// .unsupported-preview {  
524 -// height: 100%;  
525 -// padding: 20px;  
526 -// display: flex;  
527 -// align-items: center;  
528 -// justify-content: center;  
529 -// color: #ff4d4f;  
530 -// font-weight: 500;  
531 -// }  
532 -// }  
533 -//}  
534 .file-preview { 496 .file-preview {
535 - width: 55%; 497 + width: 50%;
536 min-width: 300px; 498 min-width: 300px;
537 display: flex; 499 display: flex;
538 flex-direction: column; 500 flex-direction: column;
539 background-color: white; 501 background-color: white;
540 - border-radius: 8px;  
541 - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 502 + border-radius: 12px;
  503 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
542 overflow: hidden; 504 overflow: hidden;
  505 + border-left: 1px solid #eaeaea;
543 506
544 .preview-header { 507 .preview-header {
545 display: flex; 508 display: flex;
546 justify-content: space-between; 509 justify-content: space-between;
547 align-items: center; 510 align-items: center;
548 - padding: 10px 15px;  
549 - background-color: #f0f0f0; 511 + padding: 12px 16px;
  512 + background-color: #f5f7fa;
550 border-bottom: 1px solid #e8e8e8; 513 border-bottom: 1px solid #e8e8e8;
551 - font-weight: 500;  
552 - font-size: 14px; 514 + font-weight: 600;
  515 + font-size: 16px;
  516 + color: #2d3748;
553 517
554 .preview-actions { 518 .preview-actions {
555 display: flex; 519 display: flex;
556 align-items: center; 520 align-items: center;
557 - gap: 10px; 521 + gap: 8px;
558 522
559 .ant-btn { 523 .ant-btn {
560 padding: 0; 524 padding: 0;
561 height: auto; 525 height: auto;
562 - color: #666; 526 + color: #4a5568;
563 display: flex; 527 display: flex;
564 align-items: center; 528 align-items: center;
565 transition: color 0.3s; 529 transition: color 0.3s;
566 530
567 &:hover { 531 &:hover {
568 - color: #1890ff;  
569 - background: transparent; 532 + color: #3182ce;
570 } 533 }
571 534
572 .anticon { 535 .anticon {
573 font-size: 16px; 536 font-size: 16px;
  537 + margin-right: 4px;
574 } 538 }
575 } 539 }
576 -  
577 - .ant-btn-link {  
578 - padding: 0 8px;  
579 - }  
580 } 540 }
581 } 541 }
582 542
583 .preview-content { 543 .preview-content {
584 flex: 1; 544 flex: 1;
585 overflow: auto; 545 overflow: auto;
586 - padding: 10px; 546 + padding: 16px;
587 547
588 .text-preview { 548 .text-preview {
589 - padding: 15px; 549 + padding: 16px;
590 white-space: pre-wrap; 550 white-space: pre-wrap;
591 - font-family: monospace; 551 + font-family: 'Consolas', monospace;
592 line-height: 1.6; 552 line-height: 1.6;
593 - background-color: #fafafa;  
594 - border-radius: 4px; 553 + background-color: #f8fafc;
  554 + border-radius: 8px;
  555 + font-size: 14px;
  556 + color: #2d3748;
595 } 557 }
596 558
597 .pdf-preview { 559 .pdf-preview {
@@ -606,7 +568,7 @@ onBeforeUnmount(() => { @@ -606,7 +568,7 @@ onBeforeUnmount(() => {
606 568
607 .pdf-page { 569 .pdf-page {
608 border: 1px solid #e8e8e8; 570 border: 1px solid #e8e8e8;
609 - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); 571 + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
610 background-color: white; 572 background-color: white;
611 } 573 }
612 } 574 }
@@ -627,86 +589,101 @@ onBeforeUnmount(() => { @@ -627,86 +589,101 @@ onBeforeUnmount(() => {
627 display: flex; 589 display: flex;
628 align-items: center; 590 align-items: center;
629 justify-content: center; 591 justify-content: center;
630 - color: #ff4d4f; 592 + color: #e53e3e;
631 font-weight: 500; 593 font-weight: 500;
632 - background-color: #fff2f0; 594 + background-color: #fff5f5;
633 border-radius: 4px; 595 border-radius: 4px;
634 text-align: center; 596 text-align: center;
635 } 597 }
636 } 598 }
637 } 599 }
638 600
639 -/* 响应式设计 */  
640 -@media (max-width: 768px) {  
641 - .file-preview {  
642 - width: 100%;  
643 - min-width: auto;  
644 - max-height: 300px;  
645 -  
646 - .preview-header {  
647 - padding: 8px 12px; 601 +.quick-actions {
  602 + display: flex;
  603 + flex-wrap: wrap;
  604 + gap: 10px;
  605 + margin-bottom: 20px;
  606 + padding: 0 10px;
648 607
649 - .preview-actions {  
650 - gap: 6px; 608 + .action-button {
  609 + background-color: white;
  610 + color: #4a5568;
  611 + border: 1px solid #e2e8f0;
  612 + border-radius: 16px;
  613 + cursor: pointer;
  614 + font-size: 14px;
  615 + padding: 6px 16px;
  616 + transition: all 0.3s ease;
  617 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
651 618
652 - .ant-btn-link {  
653 - padding: 0 4px;  
654 - }  
655 - } 619 + &:hover {
  620 + background-color: #edf2f7;
  621 + transform: translateY(-2px);
  622 + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
656 } 623 }
657 624
658 - .preview-content {  
659 - padding: 8px;  
660 -  
661 - .text-preview,  
662 - .docx-preview {  
663 - padding: 10px;  
664 - } 625 + &:active {
  626 + transform: translateY(0);
665 } 627 }
666 } 628 }
667 } 629 }
  630 +
668 .message { 631 .message {
669 - margin-bottom: 15px; 632 + margin-bottom: 20px;
670 display: flex; 633 display: flex;
  634 + max-width: 85%;
  635 + animation: fadeIn 0.3s ease;
671 636
672 &.user { 637 &.user {
  638 + align-self: flex-end;
673 justify-content: flex-end; 639 justify-content: flex-end;
674 640
675 .message-content { 641 .message-content {
676 - background-color: #1890ff; 642 + background-color: #3182ce;
677 color: white; 643 color: white;
678 border-radius: 18px 18px 0 18px; 644 border-radius: 18px 18px 0 18px;
679 } 645 }
680 } 646 }
681 647
682 &.assistant { 648 &.assistant {
  649 + align-self: flex-start;
683 justify-content: flex-start; 650 justify-content: flex-start;
684 651
685 .message-content { 652 .message-content {
686 - background-color: #f0f0f0;  
687 - color: #333; 653 + background-color: #edf2f7;
  654 + color: #2d3748;
688 border-radius: 18px 18px 18px 0; 655 border-radius: 18px 18px 18px 0;
689 } 656 }
690 } 657 }
691 } 658 }
692 659
  660 +@keyframes fadeIn {
  661 + from { opacity: 0; transform: translateY(10px); }
  662 + to { opacity: 1; transform: translateY(0); }
  663 +}
  664 +
693 .message-content { 665 .message-content {
694 - max-width: 80%;  
695 - padding: 12px 16px; 666 + padding: 14px 18px;
696 word-wrap: break-word; 667 word-wrap: break-word;
697 - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 668 + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
  669 + line-height: 1.5;
  670 +}
  671 +
  672 +.message-text {
  673 + font-size: 15px;
698 } 674 }
699 675
700 .message-meta { 676 .message-meta {
701 font-size: 12px; 677 font-size: 12px;
702 - color: #666; 678 + color: #718096;
703 margin-top: 8px; 679 margin-top: 8px;
704 text-align: right; 680 text-align: right;
705 681
706 a { 682 a {
707 margin-left: 10px; 683 margin-left: 10px;
708 - color: #1890ff; 684 + color: #3182ce;
709 cursor: pointer; 685 cursor: pointer;
  686 + font-weight: 500;
710 687
711 &:hover { 688 &:hover {
712 text-decoration: underline; 689 text-decoration: underline;
@@ -718,45 +695,88 @@ onBeforeUnmount(() => { @@ -718,45 +695,88 @@ onBeforeUnmount(() => {
718 display: flex; 695 display: flex;
719 gap: 12px; 696 gap: 12px;
720 padding: 10px 0; 697 padding: 10px 0;
  698 + background: white;
  699 + border-radius: 12px;
  700 + padding: 15px;
  701 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
721 702
722 :deep(.ant-input) { 703 :deep(.ant-input) {
723 flex: 1; 704 flex: 1;
724 - border-radius: 20px;  
725 - padding: 12px 18px;  
726 - border: 1px solid #d9d9d9; 705 + border-radius: 24px;
  706 + padding: 14px 20px;
  707 + border: 1px solid #e2e8f0;
727 transition: all 0.3s; 708 transition: all 0.3s;
  709 + font-size: 15px;
  710 + height: auto;
728 711
729 &:hover { 712 &:hover {
730 - border-color: #40a9ff; 713 + border-color: #a0aec0;
731 } 714 }
732 715
733 &:focus { 716 &:focus {
734 - box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); 717 + border-color: #3182ce;
  718 + box-shadow: 0 0 0 2px rgba(49, 130, 206, 0.2);
735 } 719 }
736 } 720 }
737 721
738 - .ant-btn {  
739 - border-radius: 20px;  
740 - padding: 0 24px;  
741 - height: 40px;  
742 - font-weight: 500; 722 + .send-button {
  723 + border-radius: 24px;
  724 + padding: 0 28px;
  725 + height: 48px;
  726 + font-weight: 600;
  727 + background-color: #3182ce;
  728 + border: none;
  729 + transition: all 0.3s;
  730 +
  731 + &:hover {
  732 + background-color: #2b6cb0 !important;
  733 + transform: translateY(-2px);
  734 + }
  735 +
  736 + &:active {
  737 + transform: translateY(0);
  738 + }
743 } 739 }
744 } 740 }
745 741
746 /* 响应式设计 */ 742 /* 响应式设计 */
747 @media (max-width: 768px) { 743 @media (max-width: 768px) {
  744 + .chat-container {
  745 + height: calc(100vh - 20px);
  746 + padding: 10px;
  747 + }
  748 +
748 .main-content { 749 .main-content {
749 flex-direction: column; 750 flex-direction: column;
  751 + height: auto;
750 } 752 }
751 753
752 .file-preview { 754 .file-preview {
753 width: 100%; 755 width: 100%;
754 min-width: auto; 756 min-width: auto;
755 max-height: 300px; 757 max-height: 300px;
  758 + border-left: none;
  759 + border-top: 1px solid #eaeaea;
  760 +
  761 + .preview-header {
  762 + padding: 10px 12px;
  763 + }
756 } 764 }
757 765
758 - .message-content { 766 + .message {
759 max-width: 90%; 767 max-width: 90%;
760 } 768 }
  769 +
  770 + .chat-input {
  771 + flex-direction: column;
  772 +
  773 + .send-button {
  774 + width: 100%;
  775 + }
  776 + }
  777 +
  778 + .quick-actions {
  779 + justify-content: center;
  780 + }
761 } 781 }
762 </style> 782 </style>