index.vue 27 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072
  1. <template>
  2. <view class="chat-container">
  3. <scroll-view class="messages" scroll-y :scroll-top="scrollTop" scroll-with-animation>
  4. <block v-for="message in messages" :key="message.id">
  5. <view :class="['message', message.user]">
  6. <view class="avatar-container">
  7. <image :src="message.user === 'user' ? userAvatar : '/static/机器人.png'" class="avatar"
  8. mode="aspectFill" />
  9. <text class="sender-name">{{ message.user === 'user' ? userInfo.nickName : 'AI助理' }}</text>
  10. </view>
  11. <view class="message-content"> <!-- 改为 message-content 类 -->
  12. <template v-if="message.message.includes('相关资料来源')">
  13. <view>{{ getMainContent(message.message) }}</view>
  14. <view class="source-section" v-if="minioUrls && minioUrls.length > 0">
  15. <view class="source-title">相关资料来源:</view>
  16. <view class="source-links">
  17. <view v-for="(item, index) in minioUrls" :key="index" class="source-link"
  18. @tap="copyLink(item.url)">
  19. {{ item.object_name }}
  20. </view>
  21. </view>
  22. </view>
  23. </template>
  24. <template v-else>
  25. {{ message.message }}
  26. </template>
  27. </view>
  28. </view>
  29. </block>
  30. </scroll-view>
  31. <!-- 添加新建对话按钮 -->
  32. <view class="new-chat-btn">
  33. <button @tap="startNewChat" class="btn-new">新建对话</button>
  34. </view>
  35. <view class="input-container">
  36. <textarea v-model="userInput" @keydown="handleKeydown" placeholder="有问题可以咨询我..." maxlength="100"></textarea>
  37. <button @tap="sendMessage" class="btn">发送</button>
  38. </view>
  39. </view>
  40. </template>
  41. <script>
  42. import MarkdownIt from "markdown-it";
  43. const md = new MarkdownIt();
  44. export default {
  45. data() {
  46. return {
  47. messages: [{
  48. user: "bot",
  49. messageType: "TEXT",
  50. message: "欢迎使用智能AI助理",
  51. html: "",
  52. time: "",
  53. done: true,
  54. }, ],
  55. userInput: "",
  56. websocket: null,
  57. AiUrl: '',
  58. apiUrl: 'https://ylaiapi.raycos.com.cn',
  59. scrollTop: 0,
  60. tweaks: {},
  61. userAvatar: '/static/用户.png',
  62. userInfo: {},
  63. level: 1,
  64. idArray: [],
  65. minioUrls: [], // 新增:用于存储 Minio URL
  66. generating: false, // 是否正在生成回复
  67. typewriterTimer: null,
  68. currentIndex: 0,
  69. fullMessage: '',
  70. session_id: "",
  71. platform: {
  72. type: '', // 平台类型
  73. name: '', // 平台名称
  74. isWeixin: false
  75. }
  76. };
  77. },
  78. created() {
  79. },
  80. onLoad() {
  81. // this.getKbmUrl()
  82. this.loadUserInfo()
  83. this.getPlatformInfo();
  84. },
  85. onShow() {
  86. this.getKbmUrl()
  87. },
  88. watch: {
  89. messages: {
  90. handler() {
  91. this.$nextTick(() => {
  92. this.scrollToBottom();
  93. });
  94. },
  95. deep: true
  96. }
  97. },
  98. methods: {
  99. // 添加新建对话方法
  100. startNewChat() {
  101. uni.showModal({
  102. title: '确认新建对话',
  103. content: '是否清空当前对话内容开始新的对话?',
  104. success: (res) => {
  105. if (res.confirm) {
  106. // 重置消息列表,只保留欢迎消息
  107. this.messages = [{
  108. user: "bot",
  109. messageType: "TEXT",
  110. message: "欢迎使用智能AI助理",
  111. html: "",
  112. time: "",
  113. done: true,
  114. }];
  115. // 清空session_id
  116. this.session_id = "";
  117. // 清空MinioUrls
  118. this.minioUrls = [];
  119. // 重置其他相关状态
  120. this.generating = false;
  121. this.currentIndex = 0;
  122. this.fullMessage = '';
  123. // 显示提示
  124. uni.showToast({
  125. title: '已开始新对话',
  126. icon: 'success',
  127. duration: 1500
  128. });
  129. }
  130. }
  131. });
  132. },
  133. // 获取平台信息
  134. getPlatformInfo() {
  135. try {
  136. const systemInfo = uni.getSystemInfoSync();
  137. // console.log('系统信息:', systemInfo);
  138. // 判断平台类型
  139. // #ifdef MP-WEIXIN
  140. this.platform.type = 'mp-weixin';
  141. this.platform.name = '微信小程序';
  142. this.platform.isWeixin = true;
  143. // #endif
  144. // #ifdef MP-ALIPAY
  145. this.platform.type = 'mp-alipay';
  146. this.platform.name = '支付宝小程序';
  147. // #endif
  148. // #ifdef MP-BAIDU
  149. this.platform.type = 'mp-baidu';
  150. this.platform.name = '百度小程序';
  151. // #endif
  152. // #ifdef MP-TOUTIAO
  153. this.platform.type = 'mp-toutiao';
  154. this.platform.name = '头条小程序';
  155. // #endif
  156. // #ifdef H5
  157. this.platform.type = 'h5';
  158. this.platform.name = 'H5';
  159. // 在H5中判断是否在微信浏览器中
  160. if (navigator.userAgent.toLowerCase().indexOf('micromessenger') !== -1) {
  161. this.platform.isWeixin = true;
  162. this.platform.name = 'H5微信浏览器';
  163. }
  164. // #endif
  165. // #ifdef APP-PLUS
  166. this.platform.type = 'app';
  167. this.platform.name = 'App';
  168. // #endif
  169. // console.log('当前平台:', this.platform);
  170. // 获取更详细的平台信息
  171. const platformInfo = {
  172. brand: systemInfo.brand, // 手机品牌
  173. model: systemInfo.model, // 手机型号
  174. system: systemInfo.system, // 操作系统版本
  175. platform: systemInfo.platform, // 客户端平台
  176. SDKVersion: systemInfo.SDKVersion, // 小程序基础库版本
  177. version: systemInfo.version, // 微信版本号
  178. };
  179. // console.log('平台详细信息:', platformInfo);
  180. return this.platform;
  181. } catch (error) {
  182. console.error('获取平台信息失败:', error);
  183. return {
  184. type: 'unknown',
  185. name: '未知平台',
  186. isWeixin: false
  187. };
  188. }
  189. },
  190. getMainContent(message) {
  191. const parts = message.split('相关资料来源');
  192. return parts[0].trim();
  193. },
  194. extractUrls(message) {
  195. const urlRegex = /href="([^"]+)"/g;
  196. const urls = [];
  197. let match;
  198. while ((match = urlRegex.exec(message)) !== null) {
  199. urls.push(match[1]);
  200. }
  201. return urls;
  202. },
  203. getFileName(url) {
  204. try {
  205. const decodedUrl = decodeURIComponent(url);
  206. const parts = decodedUrl.split('/');
  207. let fileName = parts[parts.length - 1];
  208. // 如果文件名过长,截断它
  209. if (fileName.length > 30) {
  210. fileName = fileName.substring(0, 27) + '...';
  211. }
  212. return fileName;
  213. } catch (e) {
  214. return '文件';
  215. }
  216. },
  217. // 修改 copyLink 方法
  218. copyLink(url) {
  219. if (!url) return;
  220. uni.setClipboardData({
  221. data: url,
  222. success: () => {
  223. uni.showToast({
  224. title: '链接已复制',
  225. icon: 'none',
  226. duration: 1500
  227. });
  228. }
  229. });
  230. },
  231. loadUserInfo() {
  232. const userDataString = uni.getStorageSync('userInfo');
  233. if (userDataString) {
  234. this.userInfo = JSON.parse(userDataString);
  235. this.userAvatar = this.userInfo.avatarUrl || '/static/用户.png'; // Fallback to default if no avatar
  236. }
  237. },
  238. scrollToBottom() {
  239. // 使用一个很大的值来确保滚动到底部
  240. this.scrollTop += 9999;
  241. // 重置 scrollTop 以便下次滚动生效
  242. // this.$nextTick(() => {
  243. // this.scrollTop = 0;
  244. // });
  245. },
  246. /* 请求链接 */
  247. getKbmUrl() {
  248. let userDataString;
  249. if (uni.getStorageSync('userData') == '') {
  250. console.log('userData is empty');
  251. // 跳转到个人页面
  252. uni.switchTab({
  253. url: '/pages/my/index',
  254. success: function() {
  255. console.log('Successfully switched to personal page');
  256. },
  257. fail: function(err) {
  258. console.error('Failed to switch to personal page:', err);
  259. }
  260. });
  261. return;
  262. } else {
  263. userDataString = JSON.parse(uni.getStorageSync('userData'));
  264. }
  265. uni.request({
  266. url: this.apiUrl + '/wechat/getUserInfo',
  267. method: 'POST',
  268. data: {
  269. openid: userDataString.openid
  270. },
  271. header: {
  272. 'content-type': 'application/x-www-form-urlencoded'
  273. },
  274. success: (res) => {
  275. console.log('获取用户信息成功', res.data);
  276. this.AiUrl = this.apiUrl + '/chatbot/chat'
  277. this.level = res.data.data.level
  278. },
  279. fail: (err) => {
  280. console.error('获取用户信息失败', err);
  281. uni.showToast({
  282. title: '获取用户信息失败,请重试',
  283. icon: 'none'
  284. });
  285. }
  286. });
  287. /* uni.request({
  288. url: this.apiUrl + '/wechat/getKbmUrl', // 确保这是正确的URL
  289. method: 'POST',
  290. data: {
  291. openid: userDataString.openid
  292. }, //JSON.stringify()
  293. header: {
  294. 'content-type': 'application/x-www-form-urlencoded' // 设置正确的内容类型
  295. },
  296. success: (res) => {
  297. console.log('Login response:', res.data);
  298. if (res.data && res.data.status == 200) {
  299. this.tweaks = res.data.data.tweaks
  300. this.connectWebSocket();
  301. }
  302. },
  303. fail: (err) => {
  304. console.error('请求失败', err);
  305. uni.showToast({
  306. title: '请求失败,请重试',
  307. icon: 'none'
  308. });
  309. }
  310. }) */
  311. },
  312. getUuid() {
  313. return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
  314. /[xy]/g,
  315. function(c) {
  316. var r = (Math.random() * 16) | 0,
  317. v = c == "x" ? r : (r & 0x3) | 0x8;
  318. return v.toString(16);
  319. }
  320. );
  321. },
  322. connectWebSocket() {
  323. const wsUrl = this.AiUrl
  324. //'http://ai.raycos.net/api/v1/run/c7ea3a2d-f50c-4936-b96c-3fe7c34fa062?stream=false';
  325. // 使用 XMLHttpRequest 替代 fetch
  326. uni.request({
  327. url: wsUrl,
  328. method: 'POST',
  329. data: JSON.stringify({}), // 如果需要发送数据,可以在这里添加
  330. header: {
  331. 'content-type': 'application/json'
  332. },
  333. success: (res) => {
  334. if (res.statusCode === 200) {
  335. const result = res.data;
  336. this.handleWebSocketMessage(result);
  337. } else {
  338. console.error("请求失败:", res.statusCode);
  339. }
  340. },
  341. fail: (err) => {
  342. console.error("网络连接错误:", err);
  343. }
  344. });
  345. // 如果需要发送数据,可以在这里添加
  346. },
  347. // 新增方法来处理消息
  348. handleWebSocketMessage(result) {
  349. console.log(result.content);
  350. // 检查消息是否完结
  351. if (result.done) {
  352. this.messages[this.messages.length - 1].done = true;
  353. return;
  354. }
  355. if (this.messages[this.messages.length - 1].done) {
  356. // 添加新的消息
  357. this.messages.push({
  358. time: Date.now(),
  359. message: result.content,
  360. messageType: "TEXT",
  361. user: "bot",
  362. done: false,
  363. });
  364. } else {
  365. // 更新最后一条消息
  366. let lastMessage = this.messages[this.messages.length - 1];
  367. lastMessage.message += result.content;
  368. this.messages[this.messages.length - 1] = lastMessage;
  369. }
  370. },
  371. // 修改 sendMessage 方法
  372. async sendMessage() {
  373. const wsUrl = this.AiUrl;
  374. let message = this.userInput.trim();
  375. if (!message || this.typewriterTimer) return; // 防止重复发送
  376. try {
  377. // 添加用户消息
  378. const userMessage = {
  379. id: this.getUuid(),
  380. user: "user",
  381. messageType: "TEXT",
  382. message: message,
  383. time: Date.now(),
  384. done: true,
  385. };
  386. this.messages.push(userMessage);
  387. // 清空输入框
  388. this.userInput = "";
  389. this.scrollToBottom();
  390. // 添加AI助理的消息
  391. const botMessage = {
  392. id: this.getUuid(),
  393. user: "bot",
  394. messageType: "TEXT",
  395. message: "正在思考中...",
  396. time: Date.now(),
  397. done: false,
  398. };
  399. this.messages.push(botMessage);
  400. const response = await uni.request({
  401. url: wsUrl,
  402. method: 'POST',
  403. header: {
  404. 'Content-Type': 'application/json'
  405. },
  406. data: {
  407. message: message,
  408. chat_config_id: '17',
  409. user_id: this.userInfo.phoneNumber,
  410. session_id: this.session_id || "",
  411. source: this.platform.type //uni.getAccountInfoSync().miniProgram.appName,
  412. }
  413. });
  414. // 检查响应状态和数据
  415. if (response.statusCode !== 200 || !response.data || !response.data.data?.answer) {
  416. throw new Error('Invalid response from server');
  417. }
  418. if (this.level === '0' && response.data.data?.milvus_ids) {
  419. await this.getMinioUrls({
  420. ids: response.data.data.milvus_ids
  421. });
  422. }
  423. this.session_id = response.data.data.session_id;
  424. await this.handleResponse(response.data, botMessage);
  425. } catch (error) {
  426. console.error("Error sending message:", error);
  427. // 找到最后一条消息(应该是机器人的消息)并更新它
  428. const lastMessage = this.messages[this.messages.length - 1];
  429. if (lastMessage && lastMessage.user === 'bot') {
  430. lastMessage.message = "抱歉,处理消息时出现错误,请重试。";
  431. lastMessage.done = true;
  432. }
  433. uni.showToast({
  434. title: '发送消息失败,请重试',
  435. icon: 'none'
  436. });
  437. }
  438. },
  439. // 修改 getMinioUrls 方法
  440. async getMinioUrls(idObject) {
  441. try {
  442. const response = await uni.request({
  443. url: `${this.apiUrl}/milvus/getMinioURl`,
  444. method: 'POST',
  445. data: idObject,
  446. header: {
  447. 'Content-Type': 'application/json'
  448. }
  449. });
  450. if (response.statusCode === 200 && response.data?.data) {
  451. this.minioUrls = response.data.data;
  452. console.log("Received Minio URLs:", this.minioUrls);
  453. } else {
  454. this.minioUrls = [];
  455. console.error("Invalid response format:", response);
  456. }
  457. } catch (error) {
  458. console.error("Error fetching Minio URLs:", error);
  459. this.minioUrls = [];
  460. uni.showToast({
  461. title: '获取资源链接失败',
  462. icon: 'none'
  463. });
  464. }
  465. },
  466. // 修改 handleResponse 方法
  467. async handleResponse(value, existingMessage) {
  468. try {
  469. if (!value?.data?.answer) {
  470. throw new Error('Invalid response format');
  471. }
  472. const data = value.data.answer;
  473. this.fullMessage = data;
  474. this.currentIndex = 0;
  475. existingMessage.message = '';
  476. // 开始打字效果
  477. this.startTypewriter(existingMessage);
  478. } catch (error) {
  479. console.error("Error handling response:", error);
  480. existingMessage.message = "抱歉,处理响应时出现错误。";
  481. existingMessage.done = true;
  482. }
  483. },
  484. // 新的打字效果实现
  485. startTypewriter(existingMessage) {
  486. // 清除可能存在的旧计时器
  487. if (this.typewriterTimer) {
  488. clearInterval(this.typewriterTimer);
  489. this.typewriterTimer = null;
  490. }
  491. const doTypewriter = () => {
  492. if (this.currentIndex < this.fullMessage.length) {
  493. existingMessage.message = this.fullMessage.slice(0, this.currentIndex + 1);
  494. this.currentIndex++;
  495. // 使用 nextTick 确保视图更新
  496. this.$nextTick(() => {
  497. this.scrollToBottom();
  498. });
  499. // 使用 setTimeout 代替 setInterval
  500. this.typewriterTimer = setTimeout(() => {
  501. doTypewriter();
  502. }, 30);
  503. } else {
  504. // 打字效果完成
  505. if (this.minioUrls && this.minioUrls.length > 0) {
  506. existingMessage.message += '\n\n相关资料来源';
  507. }
  508. existingMessage.done = true;
  509. this.typewriterTimer = null;
  510. this.scrollToBottom();
  511. }
  512. };
  513. // 立即开始打字效果
  514. doTypewriter();
  515. },
  516. // 修改 renderMarkdown 方法以保留 HTML 标签
  517. /* renderMarkdown(rawMarkdown) {
  518. if (typeof rawMarkdown !== 'string') {
  519. console.warn('Invalid input for renderMarkdown:', rawMarkdown);
  520. return '';
  521. }
  522. try {
  523. const md = new MarkdownIt({
  524. html: true, // 允许 HTML 标签
  525. breaks: true,
  526. linkify: true
  527. });
  528. // 自定义链接渲染
  529. md.renderer.rules.link_open = (tokens, idx) => {
  530. const token = tokens[idx];
  531. const hrefIndex = token.attrIndex('href');
  532. const href = token.attrs[hrefIndex][1];
  533. return `<text class="link" style="color: #1296db; text-decoration: underline;" data-href="${href}">`;
  534. };
  535. md.renderer.rules.link_close = () => '</text>';
  536. // 自定义段落渲染
  537. md.renderer.rules.paragraph_open = () =>
  538. '<p style="font-size: 12px; margin: 5px 0 10px; padding: 0; line-height: 22px;">';
  539. return md.render(rawMarkdown);
  540. } catch (error) {
  541. console.error('Markdown rendering error:', error);
  542. return rawMarkdown;
  543. }
  544. }, */
  545. // 修改 sendMessage 方法
  546. /* async sendMessage() {
  547. const wsUrl = this.AiUrl;
  548. let message = this.userInput.trim();
  549. if (message) {
  550. // 添加用户消息
  551. const userMessage = {
  552. id: this.getUuid(),
  553. user: "user",
  554. messageType: "TEXT",
  555. message: message,
  556. time: Date.now(),
  557. done: true,
  558. };
  559. this.messages.push(userMessage);
  560. // 清空输入框并滚动
  561. this.userInput = "";
  562. this.scrollToBottom();
  563. // 添加AI助理的消息
  564. const botMessage = {
  565. id: this.getUuid(),
  566. user: "bot",
  567. messageType: "TEXT",
  568. message: "正在思考中...", // 添加加载提示
  569. time: Date.now(),
  570. done: false,
  571. };
  572. this.messages.push(botMessage);
  573. try {
  574. const response = await uni.request({
  575. url: wsUrl,
  576. method: 'POST',
  577. header: {
  578. 'Content-Type': 'application/json'
  579. },
  580. data: {
  581. message: message,
  582. chat_config_id: '17',
  583. user_id: this.userInfo.phoneNumber,
  584. session_id: this.session_id || "",
  585. }
  586. });
  587. if (response.statusCode === 200 && response.data) {
  588. // 如果需要获取 MinioUrls,先获取
  589. if (this.level === '0' && response.data.data?.milvus_ids) {
  590. await this.getMinioUrls({
  591. ids: response.data.data.milvus_ids
  592. });
  593. }
  594. // 然后处理完整响应
  595. await this.handleResponse(response.data, botMessage);
  596. }
  597. } catch (error) {
  598. console.error("Error sending message:", error);
  599. botMessage.message = "抱歉,发送消息失败,请重试。";
  600. botMessage.done = true;
  601. uni.showToast({
  602. title: '发送消息失败,请重试',
  603. icon: 'none'
  604. });
  605. }
  606. }
  607. }, */
  608. // 修改 renderMarkdown 方法
  609. /* renderMarkdown(rawMarkdown) {
  610. if (typeof rawMarkdown !== 'string') {
  611. console.warn('Invalid input for renderMarkdown:', rawMarkdown);
  612. return '';
  613. }
  614. try {
  615. const md = new MarkdownIt({
  616. html: true,
  617. breaks: true,
  618. linkify: true
  619. });
  620. // 自定义链接渲染
  621. md.renderer.rules.link_open = (tokens, idx) => {
  622. const token = tokens[idx];
  623. const hrefIndex = token.attrIndex('href');
  624. const href = token.attrs[hrefIndex][1];
  625. return `<text class="link" data-href="${href}">`;
  626. };
  627. md.renderer.rules.link_close = () => '</text>';
  628. return md.render(rawMarkdown);
  629. } catch (error) {
  630. console.error('Markdown rendering error:', error);
  631. return rawMarkdown;
  632. }
  633. }, */
  634. // 添加链接点击处理方法
  635. // 修改 handleLinkClick 方法
  636. // 修改 renderMarkdown 方法
  637. renderMarkdown(rawMarkdown) {
  638. if (typeof rawMarkdown !== 'string') {
  639. console.warn('Invalid input for renderMarkdown:', rawMarkdown);
  640. return '';
  641. }
  642. try {
  643. const md = new MarkdownIt({
  644. html: true,
  645. breaks: true,
  646. linkify: true
  647. });
  648. // 自定义链接渲染
  649. md.renderer.rules.link_open = (tokens, idx) => {
  650. const token = tokens[idx];
  651. const hrefIndex = token.attrIndex('href');
  652. const href = token.attrs[hrefIndex][1];
  653. // 使用自定义的类名和数据属性
  654. return `<view class="custom-link" data-url="${href}">`;
  655. };
  656. md.renderer.rules.link_close = () => '</view>';
  657. const html = md.render(rawMarkdown);
  658. return html;
  659. } catch (error) {
  660. console.error('Markdown rendering error:', error);
  661. return rawMarkdown;
  662. }
  663. },
  664. // 修改 handleLinkClick 方法
  665. handleLinkClick(e) {
  666. // 查找最近的带有 custom-link 类的父元素
  667. let target = e.target;
  668. while (target && !target.classList?.contains('custom-link')) {
  669. target = target.parentElement;
  670. }
  671. if (target && target.dataset.url) {
  672. const url = target.dataset.url;
  673. console.log('Clicked URL:', url);
  674. // 复制链接到剪贴板
  675. uni.setClipboardData({
  676. data: url,
  677. success: () => {
  678. uni.showToast({
  679. title: '链接已复制',
  680. icon: 'none'
  681. });
  682. }
  683. });
  684. }
  685. },
  686. /* renderMarkdown(rawMarkdown) {
  687. if (typeof rawMarkdown !== 'string') {
  688. console.warn('Invalid input for renderMarkdown:', rawMarkdown);
  689. return '';
  690. }
  691. return md.render(rawMarkdown);
  692. }, */
  693. handleKeydown(event) {
  694. // Check if 'Enter' is pressed without the 'Alt' key
  695. if (event.key === "Enter" && !(event.shiftKey || event.altKey)) {
  696. event.preventDefault(); // Prevent the default action to avoid line break in textarea
  697. this.sendMessage();
  698. } else if (event.key === "Enter" && event.altKey) {
  699. // Allow 'Alt + Enter' to insert a newline
  700. const cursorPosition = event.target.selectionStart;
  701. const textBeforeCursor = this.userInput.slice(0, cursorPosition);
  702. const textAfterCursor = this.userInput.slice(cursorPosition);
  703. // Insert the newline character at the cursor position
  704. this.userInput = textBeforeCursor + "\n" + textAfterCursor;
  705. // Move the cursor to the right after the inserted newline
  706. this.$nextTick(() => {
  707. event.target.selectionStart = cursorPosition + 1;
  708. event.target.selectionEnd = cursorPosition + 1;
  709. });
  710. }
  711. },
  712. beforeDestroy() {
  713. if (this.websocket) {
  714. this.websocket.close();
  715. }
  716. },
  717. },
  718. // updated() {
  719. // const messagesContainer = this.$el.querySelector(".messages");
  720. // messagesContainer.scrollTop = messagesContainer.scrollHeight;
  721. // },
  722. };
  723. </script>
  724. <style scoped>
  725. /* 这里引入 ChatBox.css 的内容 */
  726. /* @import '/static/css/ChatBox.css'; */
  727. .chat-container {
  728. display: flex;
  729. flex-direction: column;
  730. height: 100vh;
  731. /* 或者按照实际需要调整高度 */
  732. }
  733. .messages {
  734. width: 90%;
  735. /* 设置消息列表宽度为屏幕宽度的 80% */
  736. max-width: 90%;
  737. /* 限制最大宽度,避免超出视口 */
  738. margin: 0 auto;
  739. /* 上下保持原有的 margin,左右自动调整以居中 */
  740. overflow-y: auto;
  741. /* 如果消息太多,允许滚动 */
  742. height: calc(100vh - 190px);
  743. /* 高度需要根据输入框和其他元素的高度调整 */
  744. padding: 10px 10px 0;
  745. /* 或你希望的内边距大小 */
  746. }
  747. .message {
  748. display: flex;
  749. padding: 10px;
  750. align-items: flex-start;
  751. }
  752. .avatar-container {
  753. display: flex;
  754. flex-direction: column;
  755. align-items: center;
  756. min-width: 50px;
  757. /* 确保头像容器有固定宽度 */
  758. }
  759. .messages li {
  760. display: flex;
  761. align-items: flex-start;
  762. /* 这将确保所有子元素对齐到容器的顶部 */
  763. margin-bottom: 10px;
  764. }
  765. .avatar-container {
  766. display: flex;
  767. /* flex-direction: column; */
  768. flex-direction: row;
  769. margin-right: 10px;
  770. }
  771. .avatar {
  772. width: 30px;
  773. height: 30px;
  774. border-radius: 50%;
  775. margin-bottom: 5px;
  776. }
  777. .sender-name {
  778. margin-left: 15px;
  779. font-size: 12px;
  780. color: #666;
  781. margin-top: 2px;
  782. }
  783. .message {
  784. display: flex;
  785. align-items: flex-start;
  786. flex-direction: column;
  787. /* 增加消息间距 */
  788. }
  789. .user-avatar {
  790. /* background-image: url('/static/用户.png'); */
  791. /* 替换为您的 GIF 文件路径 */
  792. background-size: cover;
  793. /* 确保 GIF 覆盖整个头像区域 */
  794. background-repeat: no-repeat;
  795. /* 防止 GIF 重复 */
  796. background-position: center;
  797. /* 将 GIF 居中 */
  798. border-radius: 50%;
  799. }
  800. .bot-avatar {
  801. background-image: url('/static/机器人.png');
  802. /* 替换为您的 GIF 文件路径 */
  803. background-size: cover;
  804. /* 确保 GIF 覆盖整个头像区域 */
  805. background-repeat: no-repeat;
  806. /* 防止 GIF 重复 */
  807. background-position: center;
  808. /* 将 GIF 居中 */
  809. }
  810. .message-content {
  811. background-color: #f0f0f0;
  812. /* 消息背景 */
  813. padding-left: 20px;
  814. /* 上下左右的内边距 */
  815. padding-right: 10px;
  816. /* 上下左右的内边距 */
  817. padding-top: 10px;
  818. /* 上下左右的内边距 */
  819. padding-bottom: 10px;
  820. /* 上下左右的内边距 */
  821. max-width: calc(90% - 15px);
  822. /* 计算头像和间距 */
  823. border-radius: 10px 10px 10px 10px;
  824. /* 边框圆角 */
  825. margin-bottom: 10px;
  826. /* 和下一条消息之间的间距 */
  827. /* 消息内容样式 */
  828. display: flex;
  829. flex-direction: column;
  830. /* 排列文本 */
  831. margin-left: 30px;
  832. margin-top: 0;
  833. font-size: 12px;
  834. }
  835. .message-text {
  836. margin: 0;
  837. white-space: normal;
  838. /* 确保文本会换行 */
  839. word-wrap: break-word;
  840. /* 长单词或 URL 会被断开以适应容器宽度 */
  841. }
  842. .input-container {
  843. position: fixed;
  844. bottom: 0;
  845. left: 50%;
  846. /* 把容器移动到屏幕中间 */
  847. transform: translateX(-50%);
  848. /* 通过变换抵消容器宽度的一半,实现居中 */
  849. width: 100%;
  850. /* 容器宽度设置为屏幕宽度的 80% */
  851. display: flex;
  852. justify-content: space-between;
  853. /* 确保按钮和输入框间有间隔 */
  854. align-items: center;
  855. padding: 20px;
  856. background: white;
  857. /* 根据你的设计调整 */
  858. box-shadow: 0px -2px 5px rgba(0, 0, 0, 0.1);
  859. }
  860. textarea {
  861. width: 90%;
  862. /* 可以根据需要调整 */
  863. height: 100px;
  864. padding: 15px;
  865. border-radius: 7px;
  866. }
  867. .input-container textarea {
  868. width: 80%;
  869. /* 输入框占据所有可用空间 */
  870. padding: 10px;
  871. margin-right: 10px;
  872. /* 与按钮的间隔 */
  873. box-sizing: border-box;
  874. }
  875. .input-container button {
  876. min-width: 40px;
  877. /* 例如,按钮的最小宽度为 100px */
  878. width: 40px;
  879. height: 30px;
  880. line-height: 20px;
  881. font-size: 12px;
  882. white-space: nowrap;
  883. /* 防止文字折行 */
  884. text-align: center;
  885. padding: 5px;
  886. cursor: pointer;
  887. }
  888. .sender-name {
  889. font-weight: bold;
  890. color: #333;
  891. font-size: 12px;
  892. margin-bottom: 5px;
  893. }
  894. .btn {
  895. border: none;
  896. background-color: #1296db;
  897. color: #fff;
  898. }
  899. .message-content :deep(p) {
  900. font-size: 12px !important;
  901. margin: 5px 0 10px;
  902. padding: 0;
  903. line-height: 22px;
  904. font-weight: inherit;
  905. }
  906. .message-content :deep(ol) {
  907. padding-bottom: 10px;
  908. }
  909. .message-content :deep(.source-section) {
  910. margin: 20px 0 3px;
  911. color: #333;
  912. font-size: 16px;
  913. }
  914. .message-content :deep(ol li) {
  915. padding-top: 3px;
  916. }
  917. .message-content :deep(ol li a) {
  918. font-size: 14px;
  919. color: #1296db;
  920. }
  921. /* 添加资料来源相关的新样式 */
  922. .source-section {
  923. border-top: 1px solid #e5e5e5;
  924. padding-top: 10px;
  925. margin-top: 10px;
  926. }
  927. .source-title {
  928. font-size: 14px;
  929. color: #666;
  930. margin-bottom: 8px;
  931. }
  932. .source-links {
  933. display: flex;
  934. flex-direction: column;
  935. gap: 5px;
  936. }
  937. .source-link {
  938. color: #1296db;
  939. font-size: 12px;
  940. cursor: pointer;
  941. /* text-decoration: underline; */
  942. }
  943. .source-link:hover {
  944. opacity: 0.8;
  945. }
  946. /* 添加新建对话按钮样式 */
  947. .new-chat-btn {
  948. padding: 0px 20px;
  949. display: flex;
  950. justify-content: flex-start;
  951. background-color: #fff;
  952. }
  953. .btn-new {
  954. background-color: #1296db;
  955. color: #fff;
  956. font-size: 14px;
  957. border-radius: 5px;
  958. border: none;
  959. height: 35px;
  960. margin: 0;
  961. }
  962. </style>