You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

552 lines
15 KiB

4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
  1. <script setup>
  2. import { ref, nextTick, watch, onMounted } from 'vue'
  3. import { useUserInfo } from '../store/userPermissionCode'
  4. import axios from 'axios'
  5. import { ElMessage } from 'element-plus';
  6. import { Loading, Position } from '@element-plus/icons-vue'; // 引入图标组件
  7. import { marked } from 'marked'; // 引入marked库
  8. import katex from 'katex'; // 引入 KaTeX 库
  9. import 'katex/dist/katex.min.css'; // 引入 KaTeX 样式
  10. const { getQueryVariable } = useUserInfo()
  11. // 假设 getSessionId 已经正确定义
  12. const { getSessionId } = useUserInfo()
  13. const isTokenValid = ref(false)
  14. const fnGetToken = () => {
  15. localStorage.setItem('localToken', decodeURIComponent(String(getQueryVariable('token'))))
  16. console.log(localStorage.getItem('localToken'));
  17. }
  18. setTimeout(() => {
  19. fnGetToken()
  20. }, 800)
  21. // 验证 token
  22. const validateToken = async () => {
  23. const token = localStorage.getItem('localToken')
  24. console.log('token',token);
  25. if (!token) {
  26. console.error('未找到 token,请重新登录')
  27. return false
  28. }
  29. return true
  30. }
  31. // 创建新对话
  32. const sessionId = ref(localStorage.getItem('sessionId') || {});
  33. const add = async () => {
  34. try {
  35. const result = await axios.post(
  36. "http://wnxvxx.natappfree.cc/api/v1/chats/8b37cd9cf0c811efa4210242ac120003/completions",
  37. {},
  38. {
  39. headers: {
  40. 'Content-Type': 'application/json',
  41. Authorization: 'Bearer ragflow-hkNjEwYjcwZjBlMDExZWZiYjYzMDI0Mm'
  42. }
  43. }
  44. );
  45. const data = result.data;
  46. console.log(data);
  47. console.log(data, 'data11111111');
  48. // 判断 data 是否为空
  49. if (Object.keys(data).length === 0) {
  50. return;
  51. }
  52. // 假设 getSessionId 函数已经正确定义
  53. const sss = String(getSessionId("session_id", data));
  54. sessionId.value = sss;
  55. localStorage.setItem('sessionId', sss);
  56. console.log('sss', sss);
  57. } catch (error) {
  58. console.log("请求失败", error);
  59. ElMessage.error("添加失败,请检查输入内容是否正确");
  60. }
  61. };
  62. // 页面加载时验证 token
  63. validateToken().then((isValid) => {
  64. isTokenValid.value = isValid
  65. if (isValid) {
  66. console.log('Token 验证成功')
  67. if (!localStorage.getItem('sessionId')) {
  68. console.log('没有sessionId,创建新对话')
  69. add()
  70. }
  71. }
  72. })
  73. // 定义Props(可配置参数)
  74. const props = defineProps({
  75. apiUrl: {
  76. type: String,
  77. default: 'http://localhost:5000/ask'
  78. },
  79. initialGreeting: {
  80. type: String,
  81. default: '您好!我是夺宝奇兵ai智能客服,有什么可以帮助您的?'
  82. }
  83. })
  84. // 响应式数据
  85. const messages = ref([
  86. {
  87. content: props.initialGreeting,
  88. sender: 'bot',
  89. timestamp: new Date()
  90. }
  91. ])
  92. const inputMessage = ref('')
  93. const messageContainer = ref(null)
  94. const isLoading = ref(false) // 新增:加载状态
  95. // 自动滚动到底部
  96. const scrollToBottom = () => {
  97. nextTick(() => {
  98. if (messageContainer.value) {
  99. messageContainer.value.scrollTop = messageContainer.value.scrollHeight
  100. }
  101. })
  102. }
  103. // 发送消息
  104. const sendMessage = async () => {
  105. if (!isTokenValid.value) {
  106. console.error('Token 验证失败,无法发送消息');
  107. return;
  108. }
  109. if (isLoading.value) return;
  110. const content = inputMessage.value.trim();
  111. if (!content) return;
  112. // 添加用户消息
  113. messages.value.push({
  114. content,
  115. sender: 'user',
  116. timestamp: new Date()
  117. });
  118. // 清空输入框
  119. inputMessage.value = '';
  120. scrollToBottom();
  121. // 显示加载动画
  122. isLoading.value = true;
  123. messages.value.push({
  124. content: '我正在思考...',
  125. sender: 'bot',
  126. timestamp: new Date(),
  127. isLoading: true
  128. });
  129. scrollToBottom();
  130. try {
  131. // 调用API获取回复
  132. const response = await fetch("http://wnxvxx.natappfree.cc/api/v1/chats/8b37cd9cf0c811efa4210242ac120003/completions", {
  133. method: 'POST',
  134. headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ragflow-hkNjEwYjcwZjBlMDExZWZiYjYzMDI0Mm' },
  135. body: JSON.stringify({ question: content, stream: false, session_id: sessionId.value })
  136. });
  137. const data = await response.json();
  138. console.log(data, 'data211111');
  139. // 移除加载消息
  140. messages.value = messages.value.filter(msg => !msg.isLoading);
  141. // 检查 data 对象的结构,确保正确提取所需属性
  142. if (data && data.data && typeof data.data.answer === 'string') {
  143. // 提取推理思考部分和结果部分
  144. const regex = /<think>(.*?)<\/think>(.*)/s;
  145. const match = data.data.answer.match(regex);
  146. let thinking = '';
  147. let result = data.data.answer;
  148. if (match) {
  149. thinking = match[1].trim();
  150. result = match[2].trim();
  151. }
  152. // 组合推理思考内容和结果内容,并添加 HTML 标签
  153. let combinedContent = '';
  154. // if (thinking) {
  155. // combinedContent += `<span class="thinking-content">推理思考:${thinking}</span><br>`;
  156. // }
  157. combinedContent += `<span class="result-content">${result}</span>`;
  158. // 使用marked库将Markdown转换为HTML
  159. let markdownContent = marked(combinedContent);
  160. // 使用 KaTeX 渲染数学公式
  161. const katexRegex = /\$\$(.*?)\$\$/g;
  162. markdownContent = markdownContent.replace(katexRegex, (match, formula) => {
  163. try {
  164. return katex.renderToString(formula, { throwOnError: false });
  165. } catch (error) {
  166. console.error('KaTeX 渲染错误:', error);
  167. return match;
  168. }
  169. });
  170. // 添加初始空消息
  171. const botMessageIndex = messages.value.length;
  172. messages.value.push({
  173. content: '',
  174. sender: 'bot',
  175. timestamp: new Date()
  176. });
  177. // 使用定时器逐个字符更新消息内容
  178. let currentIndex = 0;
  179. const intervalId = setInterval(() => {
  180. if (currentIndex < markdownContent.length) {
  181. messages.value[botMessageIndex].content += markdownContent[currentIndex];
  182. scrollToBottom();
  183. currentIndex++;
  184. } else {
  185. clearInterval(intervalId);
  186. }
  187. }, 100); // 每100毫秒添加一个字符,可以根据需要调整
  188. } else {
  189. console.error('API 返回的数据格式不正确:', data);
  190. messages.value.push({
  191. content: '服务返回数据格式错误,请稍后再试',
  192. sender: 'bot',
  193. timestamp: new Date()
  194. });
  195. scrollToBottom();
  196. }
  197. } catch (error) {
  198. console.error('API请求失败:', error);
  199. // 移除加载消息
  200. messages.value = messages.value.filter(msg => !msg.isLoading);
  201. messages.value.push({
  202. content: '服务暂时不可用,请稍后再试',
  203. sender: 'bot',
  204. timestamp: new Date()
  205. });
  206. scrollToBottom();
  207. } finally {
  208. // 隐藏加载动画
  209. isLoading.value = false;
  210. }
  211. };
  212. // 格式化时间显示
  213. const formatTime = (date) => {
  214. return new Date(date).toLocaleTimeString([], {
  215. hour: '2-digit',
  216. minute: '2-digit'
  217. })
  218. }
  219. // 自适应输入框高度
  220. const adjustInputHeight = () => {
  221. const textarea = document.querySelector('.message-input')
  222. textarea.style.height = 'auto'
  223. textarea.style.height = `${textarea.scrollHeight}px`
  224. }
  225. // 监听输入内容变化
  226. watch(inputMessage, adjustInputHeight)
  227. // 在组件挂载后按顺序执行操作
  228. onMounted(async () => {
  229. // 先获取 token
  230. fnGetToken()
  231. // 再验证 token
  232. const isValid = await validateToken()
  233. isTokenValid.value = isValid
  234. if (!isValid) {
  235. console.error('Token 验证失败,请重新登录')
  236. }
  237. })
  238. </script>
  239. <template>
  240. <!-- 聊天容器 -->
  241. <div class="chat-container">
  242. <!-- 聊天框头部 -->
  243. <div class="chat-header">夺宝奇兵智能客服</div>
  244. <!-- 消息展示区域 -->
  245. <div class="message-list" ref="messageContainer">
  246. <div v-for="(message, index) in messages" :key="index" class="message-item" :class="[message.sender]">
  247. <!-- 机器人头像 -->
  248. <div v-if="message.sender === 'bot'" class="bot-avatar">
  249. <img src="/src/assets/img/avatar/超级云脑按钮.png" alt="Bot Avatar">
  250. </div>
  251. <div class="message-bubble">
  252. <div class="message-content" v-html="message.content">
  253. </div>
  254. <div class="message-time">{{ formatTime(message.timestamp) }}</div>
  255. </div>
  256. <!-- 用户头像 -->
  257. <div v-if="message.sender === 'user'" class="user-avatar">
  258. <img src="/src/assets/img/avatar/小柒.png" alt="User Avatar">
  259. </div>
  260. </div>
  261. </div>
  262. <!-- 输入区域 -->
  263. <div class="input-area">
  264. <textarea v-model="inputMessage" @keydown.enter.exact.prevent="isLoading ? null : sendMessage()"
  265. placeholder="输入您的问题..." rows="1" class="message-input"></textarea>
  266. <el-tooltip content="机器人正在思考" :disabled="!isLoading">
  267. <template #content>
  268. 机器人正在思考
  269. </template>
  270. <button @click="sendMessage" :disabled="!isTokenValid || isLoading" class="send-button">
  271. <!-- 使用ElementPlus的发送图标 -->
  272. <span v-if="isLoading">
  273. <el-icon class="is-loading">
  274. <Loading />
  275. </el-icon>
  276. </span>
  277. <span v-else class="send-button-content">
  278. <el-icon>
  279. <Position />
  280. </el-icon>
  281. <span> 发送</span>
  282. </span>
  283. </button>
  284. </el-tooltip>
  285. </div>
  286. <!-- 未登录覆盖层 -->
  287. <div v-if="!isTokenValid" class="overlay">
  288. <div class="overlay-content">用户未登录</div>
  289. </div>
  290. </div>
  291. </template>
  292. <style scoped>
  293. table{
  294. border-color: black
  295. }
  296. /* 使用更具体的选择器 */
  297. .message-bubble .message-content .thinking-content {
  298. color: #c8c4c4;
  299. font-style: italic;
  300. }
  301. /* 新增表格样式 */
  302. .message-bubble .message-content table {
  303. border-collapse: collapse; /* 合并边框 */
  304. width: 100%; /* 表格宽度为 100% */
  305. }
  306. .message-bubble .message-content th,
  307. .message-bubble .message-content td {
  308. border: 1px solid #e0e0e0; /* 单元格边框 */
  309. padding: 8px; /* 单元格内边距 */
  310. text-align: left; /* 文本左对齐 */
  311. }
  312. .message-bubble .message-content th {
  313. background-color: #f1f3f5; /* 表头背景颜色 */
  314. }
  315. /* 其他样式保持不变 */
  316. .message-item.bot {
  317. justify-content: flex-start; /* 让机器人消息靠左对齐 */
  318. text-align: left; /* 新增:设置文本左对齐 */
  319. }
  320. .chat-container {
  321. display: flex;
  322. flex-direction: column;
  323. height: 90vh;
  324. width: 90vw;
  325. max-width: 800px;
  326. margin: 0;
  327. border: 1px solid #e0e0e0;
  328. border-radius: 12px;
  329. background: #f8f9fa;
  330. overflow: hidden;
  331. /* 新增样式,实现水平和垂直居中 */
  332. position: absolute;
  333. top: 50%;
  334. left: 50%;
  335. transform: translate(-50%, -50%);
  336. }
  337. /* 聊天框头部样式 */
  338. .chat-header {
  339. background-color: #007bff;
  340. color: white;
  341. padding: 16px;
  342. font-size: 1.2rem;
  343. text-align: center;
  344. }
  345. .message-list {
  346. flex: 1;
  347. padding: 20px;
  348. overflow-y: auto;
  349. background: white;
  350. }
  351. .message-item {
  352. display: flex;
  353. margin-bottom: 16px;
  354. }
  355. .message-item.user {
  356. justify-content: flex-end;
  357. }
  358. .bot-avatar {
  359. margin-right: 10px;
  360. }
  361. .bot-avatar img {
  362. width: 40px;
  363. height: 40px;
  364. border-radius: 50%;
  365. object-fit: cover;
  366. }
  367. .user-avatar {
  368. margin-left: 10px;
  369. }
  370. .user-avatar img {
  371. width: 40px;
  372. height: 40px;
  373. border-radius: 50%;
  374. object-fit: cover;
  375. }
  376. .message-bubble {
  377. max-width: 80%;
  378. padding: 12px 16px;
  379. border-radius: 15px;
  380. position: relative;
  381. }
  382. .message-content span {
  383. display: block;
  384. /* 确保元素显示 */
  385. }
  386. .message-item.user .message-bubble {
  387. background: #007bff;
  388. color: white;
  389. border-bottom-right-radius: 4px;
  390. }
  391. .message-item.bot .message-bubble {
  392. background: #f1f3f5;
  393. color: #212529;
  394. border-bottom-left-radius: 4px;
  395. }
  396. .message-time {
  397. font-size: 0.75rem;
  398. color: rgba(255, 255, 255, 0.8);
  399. margin-top: 4px;
  400. text-align: right;
  401. }
  402. .message-item.bot .message-time {
  403. color: rgba(0, 0, 0, 0.6);
  404. }
  405. .input-area {
  406. display: flex;
  407. align-items: center;
  408. gap: 12px;
  409. padding: 16px;
  410. border-top: 1px solid #e0e0e0;
  411. background: white;
  412. }
  413. .message-input {
  414. flex: 1;
  415. padding: 10px 16px;
  416. border: 1px solid #e0e0e0;
  417. border-radius: 20px;
  418. resize: none;
  419. max-height: 120px;
  420. font-family: inherit;
  421. }
  422. .send-button {
  423. display: flex;
  424. align-items: center;
  425. justify-content: center;
  426. width: 100px;
  427. /* 调整宽度以适应文字 */
  428. height: 40px;
  429. border: none;
  430. border-radius: 20px;
  431. /* 调整圆角 */
  432. background: #007bff;
  433. color: white;
  434. cursor: pointer;
  435. transition: all 0.3s ease;
  436. /* 添加过渡效果 */
  437. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  438. /* 添加阴影 */
  439. font-size: 16px;
  440. /* 调整字体大小 */
  441. font-weight: 600;
  442. /* 调整字体粗细 */
  443. }
  444. .send-button:hover {
  445. background: #0056b3;
  446. transform: translateY(-2px);
  447. /* 悬停时向上移动 */
  448. box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
  449. /* 悬停时增加阴影 */
  450. }
  451. /* 新增加载状态样式 */
  452. .loading-state {
  453. background: #ccc;
  454. cursor: not-allowed;
  455. }
  456. .send-button-content {
  457. display: flex;
  458. align-items: center;
  459. justify-content: center;
  460. gap: 8px;
  461. /* 调整文字和图标间距 */
  462. }
  463. /* .send-button {
  464. display: flex;
  465. align-items: center;
  466. justify-content: center;
  467. width: 40px;
  468. height: 40px;
  469. border: none;
  470. border-radius: 50%;
  471. background: #007bff;
  472. color: white;
  473. cursor: pointer;
  474. transition: background 0.2s;
  475. }
  476. .send-button:hover {
  477. background: #0056b3;
  478. } */
  479. .send-button svg {
  480. width: 20px;
  481. height: 20px;
  482. }
  483. .overlay {
  484. position: absolute;
  485. top: 0;
  486. left: 0;
  487. width: 100%;
  488. height: 100%;
  489. background-color: rgba(255, 255, 255, 0.8);
  490. /* 透明度 50% 的白色背景 */
  491. display: flex;
  492. justify-content: center;
  493. align-items: center;
  494. z-index: 1;
  495. /* 确保覆盖层在聊天框上方 */
  496. }
  497. .overlay-content {
  498. background-color: white;
  499. padding: 20px;
  500. border-radius: 8px;
  501. box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
  502. text-align: center;
  503. }
  504. </style>