TextToText.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  1. <template>
  2. <!-- 原左侧折叠面板和右侧AI问答 -->
  3. <div class="content-wrapper">
  4. <div class="people-image">
  5. <div class="selected-image">
  6. <img :src="selectedImage" alt="" />
  7. </div>
  8. </div>
  9. <!-- 右侧AI问答 -->
  10. <div class="number-people">
  11. <div class="content-box">
  12. <!-- AI对话框 -->
  13. <div class="chat-dialog">
  14. <!-- 对话消息列表 -->
  15. <div class="message-list" ref="messageListRef" @scroll="handleScroll">
  16. <div v-for="(item, index) in messageList" :key="index">
  17. <!-- AI消息 -->
  18. <div class="ai-message" v-if="item.type !== 'user'">
  19. <MarkdownView class="left-text" :content="item.content" />
  20. <!-- {{item.content}} -->
  21. </div>
  22. <!-- 用户消息 -->
  23. <div class="user-message" v-if="item.type === 'user'">
  24. {{ item.content }}
  25. </div>
  26. </div>
  27. </div>
  28. <!-- 默认消息 -->
  29. <DefaultMessage
  30. v-if="showDefaultMessages"
  31. @select-message="handleDefaultMessageSelect"
  32. :category="route.query.category"
  33. :quest-tip="route.query.default"
  34. />
  35. <!-- 输入框和发送按钮 -->
  36. <div class="input-section">
  37. <input
  38. type="text"
  39. v-model="prompt"
  40. placeholder="问我任何问题..."
  41. @keyup.enter="handleSendByKeydown"
  42. />
  43. <!-- 语音输入按钮 -->
  44. <VoiceInput
  45. @voiceRecognized="handleVoiceRecognized"
  46. lang="zh-CN"
  47. maxDuration="10"
  48. />
  49. <!-- 终止问答按钮 -->
  50. <div
  51. v-if="conversationInProgress"
  52. @click="handleStopButtonClick"
  53. class="stop-btn"
  54. title="终止问答"
  55. >
  56. <img :src="stopicon" alt="停止" />
  57. </div>
  58. <button
  59. v-if="!conversationInProgress"
  60. @click="handleSendByButton"
  61. >
  62. 发送
  63. </button>
  64. </div>
  65. </div>
  66. </div>
  67. </div>
  68. </div>
  69. </template>
  70. <script setup>
  71. import {ref, onMounted, onUnmounted, computed, watch, nextTick, defineEmits} from "vue";
  72. import { CreateDialogue, sendChatMessageStream } from "@/api/questions.js";
  73. import { useRouter, useRoute } from "vue-router";
  74. import { teacherList } from '@/api/teachers.js'
  75. // 导入全局状态
  76. import { globalState } from "@/utils/globalState.js";
  77. import VoiceInput from '../voice/VoiceInput.vue'
  78. // 终止按钮
  79. import stopicon from "@/assets/icon/stopicon.png";
  80. import MarkdownView from "@/components/MarkdownView/index.vue";
  81. import DefaultMessage from "@/components/DefaultMessage/index.vue";
  82. // 定义props
  83. const props = defineProps({
  84. //根据需求传输
  85. isCourse: { type: Boolean, default: false},
  86. cacheDataKey: { type: String, default: localStorage.getItem('token') + "_ai_textToText" },
  87. cacheDataHistoryKey: { type: String, default: localStorage.getItem('token') + "_ai_textToText_history" },
  88. preDialogueList: { type: Array, default: () => []},
  89. replySupplement: { type: String, default: ''},
  90. personId: { type: Number},
  91. personName: { type: String },
  92. personImage: { type: String},
  93. personIntroduce: { type: String},
  94. })
  95. // 定义emits
  96. const emits = defineEmits(['saveProgress'])
  97. //历史记录缓存集合
  98. const cacheDataHistoryList = ref([]);
  99. // 默认消息控制
  100. const showDefaultMessages = ref(true);
  101. const handleDefaultMessageSelect = (message) => {
  102. prompt.value = message;
  103. handleSendByButton();
  104. showDefaultMessages.value = false;
  105. };
  106. // 抽屉显示状态
  107. const drawerVisible = ref(true);
  108. // 添加切换抽屉显示状态的函数
  109. const toggleDrawer = () => {
  110. drawerVisible.value = !drawerVisible.value;
  111. };
  112. // 处理菜单展开和关闭
  113. const handleOpen = () => {};
  114. const handleClose = () => {};
  115. // 返回上一页
  116. const goBack = () => {
  117. // 停止语音播放
  118. stopPlayback();
  119. router.push("/ai-laboratory");
  120. };
  121. const router = useRouter();
  122. const route = useRoute();
  123. const personId = ref(props.personId);
  124. const personName = ref(props.personName);
  125. const personImage = ref(props.personImage);
  126. const personIntroduce = ref(props.personIntroduce);
  127. // 渲染实验室携带的人物形象图片
  128. const selectedImage = ref("");
  129. // 聊天对话
  130. const activeConversationModelPath = ref(null); // 选中的对话编号
  131. const activeConversationId = ref(null); // 选中的对话编号
  132. const activeConversation = ref(null); // 选中的 Conversation
  133. const conversationInProgress = ref(false); // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作,导致 stream 中断
  134. // 消息列表
  135. const messageRef = ref();
  136. const activeMessageList = ref([]); // 选中对话的消息列表
  137. const activeMessageListLoading = ref(false); // activeMessageList 是否正在加载中
  138. const activeMessageListLoadingTimer = ref(); // activeMessageListLoading Timer 定时器。如果加载速度很快,就不进入加载中
  139. // 消息滚动
  140. const textSpeed = ref(50); // Typing speed in milliseconds
  141. const textRoleRunning = ref(false); // Typing speed in milliseconds
  142. // 发送消息输入框
  143. const isComposing = ref(false); // 判断用户是否在输入
  144. const conversationInAbortController = ref(); // 对话进行中 abort 控制器(控制 stream 对话)
  145. const inputTimeout = ref(); // 处理输入中回车的定时器
  146. const prompt = ref(""); // prompt
  147. const enableContext = ref(true); // 是否开启上下文
  148. // 接收 Stream 消息
  149. const receiveMessageFullText = ref("");
  150. const receiveMessageDisplayedText = ref("");
  151. const messageListRef = ref(null);
  152. const userScrolled = ref(false)//是否用户手动滚动
  153. // =========== 【聊天对话】相关 ===========
  154. /** 获取对话信息 */
  155. const getConversation = async (id) => {
  156. if (!id) {
  157. return;
  158. }
  159. const conversation = ref({});
  160. if (!conversation) {
  161. return;
  162. }
  163. conversation.systemMessage = personIntroduce.value;
  164. activeConversation.value = conversation;
  165. // activeConversationId.value = personId.value
  166. activeConversationModelPath.value = personImage.value;
  167. // 获取对话消息列表【后面补充接口,现在取的浏览器缓存】
  168. // activeMessageList.value = await ChatMessageApi.getChatMessageListByConversationId(
  169. // activeConversationId.value
  170. // )
  171. // 取缓存历史记录
  172. let cacheDataHistoryStr = localStorage.getItem(props.cacheDataHistoryKey);
  173. console.log("cacheDataHistoryStr", cacheDataHistoryStr);
  174. if (cacheDataHistoryStr) {
  175. cacheDataHistoryList.value = JSON.parse(cacheDataHistoryStr) || [];
  176. activeMessageList.value.push(...cacheDataHistoryList.value);
  177. }else{
  178. // 加入预置对话
  179. activeMessageList.value.push(...props.preDialogueList);
  180. cacheDataHistoryList.value.push(...props.preDialogueList);
  181. localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
  182. }
  183. };
  184. // 语音输入识别结果处理
  185. const handleVoiceRecognized = (text) => {
  186. prompt.value += text;
  187. };
  188. // =========== 【聊天对话】相关 ===========
  189. /** 处理来自 keydown 的发送消息 */
  190. const handleSendByKeydown = async (event) => {
  191. // 判断用户是否在输入
  192. if (isComposing.value) {
  193. return;
  194. }
  195. // 进行中不允许发送
  196. if (conversationInProgress.value) {
  197. return;
  198. }
  199. const content = prompt.value?.trim();
  200. if (event.key === "Enter") {
  201. if (event.shiftKey) {
  202. // 插入换行
  203. prompt.value += "\r\n";
  204. event.preventDefault(); // 防止默认的换行行为
  205. } else {
  206. // 发送消息
  207. await doSendMessage(content);
  208. event.preventDefault(); // 防止默认的提交行为
  209. }
  210. }
  211. };
  212. /** 处理来自【发送】按钮的发送消息 */
  213. const handleSendByButton = () => {
  214. doSendMessage(prompt.value?.trim());
  215. };
  216. /** 处理 prompt 输入变化 */
  217. const handlePromptInput = (event) => {
  218. // 非输入法 输入设置为 true
  219. if (!isComposing.value) {
  220. // 回车 event data 是 null
  221. if (event.data == null) {
  222. return;
  223. }
  224. isComposing.value = true;
  225. }
  226. // 清理定时器
  227. if (inputTimeout.value) {
  228. clearTimeout(inputTimeout.value);
  229. }
  230. // 重置定时器
  231. inputTimeout.value = setTimeout(() => {
  232. isComposing.value = false;
  233. }, 400);
  234. };
  235. // TODO注:是不是可以通过 @keydown.enter、@keydown.shift.enter 来实现,回车发送、shift+回车换行;主要看看,是不是可以简化 isComposing 相关的逻辑
  236. const onCompositionstart = () => {
  237. isComposing.value = true;
  238. };
  239. const onCompositionend = () => {
  240. setTimeout(() => {
  241. isComposing.value = false;
  242. }, 200);
  243. };
  244. // 保存记录
  245. // 年级ID相关
  246. const gradeId = ref("");
  247. /** 真正执行【发送】消息操作 */
  248. const doSendMessage = async (content) => {
  249. // 校验
  250. if (content.length < 1) {
  251. console.error("发送失败,原因:内容为空!");
  252. return;
  253. }
  254. if (activeConversationId.value == null) {
  255. console.error("请刷新创建新对话!");
  256. return;
  257. }
  258. // 通过emit事件通知父组件保存进度
  259. emits('saveProgress', "aiCount", 1)
  260. console.log(props.isCourse);
  261. if (props.isCourse){
  262. emits('saveProgress', "course", 100)
  263. }
  264. // 清空输入框
  265. prompt.value = "";
  266. // 同步历史记录缓存
  267. cacheDataHistoryList.value.push({
  268. type: "user",
  269. content: content,
  270. createTime: new Date(),
  271. });
  272. localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
  273. // 执行发送
  274. await doSendMessageStream({
  275. conversationId: activeConversationId.value,
  276. content: content,
  277. });
  278. };
  279. import { useAudioPlayer } from '@/api/tts/useAudioPlayer';
  280. import {Message} from "@/utils/message/Message.js";
  281. // 解构 stopPlayback 方法
  282. const { playAudioChunk,stopPlayback } = useAudioPlayer();
  283. /** 真正执行【发送】消息操作 */
  284. const doSendMessageStream = async (userMessage) => {
  285. // 创建 AbortController 实例,以便中止请求
  286. conversationInAbortController.value = new AbortController();
  287. // 标记对话进行中
  288. conversationInProgress.value = true;
  289. // 设置为空
  290. receiveMessageFullText.value = "";
  291. try {
  292. // 1.1 先添加两个假数据,等 stream 返回再替换
  293. activeMessageList.value.push({
  294. id: -1,
  295. conversationId: activeConversationId.value,
  296. type: "user",
  297. content: userMessage.content,
  298. createTime: new Date(),
  299. });
  300. activeMessageList.value.push({
  301. id: -2,
  302. conversationId: activeConversationId.value,
  303. type: "assistant",
  304. content: "思考中...",
  305. createTime: new Date(),
  306. });
  307. // 1.2 开始滚动
  308. textRoll();
  309. // 2. 发送 event stream
  310. let isFirstChunk = true; // 是否是第一个 chunk 消息段
  311. // 销毁语音读取
  312. stopPlayback();
  313. await sendChatMessageStream(
  314. userMessage.conversationId,
  315. userMessage.content, null,
  316. conversationInAbortController.value,
  317. enableContext.value,
  318. async (res) => {
  319. const { code, data, msg } = JSON.parse(res.data);
  320. if (code !== 0) {
  321. console.log(`对话异常! ${msg}`);
  322. return;
  323. }
  324. // 根据事件类型处理
  325. if (data.eventType === "TEXT") {
  326. // 如果内容为空,就不处理。
  327. if (data.receive?.content === "") {
  328. return;
  329. }
  330. // 处理文本消息
  331. receiveMessageFullText.value += data.receive.content;
  332. // 首次返回需要添加一个 message 到页面,后面的都是更新
  333. if (isFirstChunk) {
  334. isFirstChunk = false;
  335. // 弹出两个假数据
  336. activeMessageList.value.pop();
  337. activeMessageList.value.pop();
  338. // 更新返回的数据
  339. activeMessageList.value.push(data.send);
  340. activeMessageList.value.push(data.receive);
  341. }
  342. } else if (data.eventType === "AUDIO") {
  343. // 处理音频消息
  344. await playAudioChunk(data.audioData);
  345. }
  346. },
  347. (error) => {
  348. console.log(`对话异常! ${error}`);
  349. stopStream();
  350. // 需要抛出异常,禁止重试
  351. throw error;
  352. },
  353. () => {
  354. console.log(`结束对话! `)
  355. stopStream();
  356. }
  357. );
  358. } catch (error) {
  359. console.error('发送消息失败:', error)
  360. stopStream()
  361. }
  362. };
  363. /** 停止 stream 流式调用 */
  364. const stopStream = async () => {
  365. // tip:如果 stream 进行中的 message,就需要调用 controller 结束
  366. if (conversationInAbortController.value) {
  367. conversationInAbortController.value.abort();
  368. }
  369. // 设置为 false
  370. conversationInProgress.value = false;
  371. };
  372. /** 清空消息列表 */
  373. const clearMessageList = () => {
  374. activeMessageList.value = [];
  375. prompt.value = "";
  376. receiveMessageFullText.value = "";
  377. receiveMessageDisplayedText.value = "";
  378. };
  379. /** 处理终止按钮点击 */
  380. const handleStopButtonClick = async () => {
  381. // 手动点击时停止语音播放
  382. stopPlayback();
  383. // 调用stopStream停止流式传输
  384. await stopStream();
  385. };
  386. /**
  387. * 消息列表
  388. *
  389. * 和 {@link #getMessageList()} 的差异是,把 systemMessage 考虑进去
  390. */
  391. const messageList = computed(() => {
  392. if (activeMessageList.value.length > 0) {
  393. return activeMessageList.value;
  394. }
  395. // 没有消息时,如果有 systemMessage 则展示它
  396. if (activeConversation.value?.systemMessage) {
  397. let systemMessage = {
  398. id: 0,
  399. type: "system",
  400. content: activeConversation.value.systemMessage,
  401. };
  402. activeMessageList.value.push(systemMessage);
  403. return [systemMessage];
  404. }
  405. return [];
  406. });
  407. // ============== 【消息滚动】相关 =============
  408. //处理滚动事件,判断用户是否手动滚动
  409. const handleScroll = () => {
  410. if (messageListRef.value) {
  411. const { scrollTop, scrollHeight, clientHeight } = messageListRef.value
  412. // 当用户滚动距离底部超过50px时,认为是手动滚动
  413. userScrolled.value = scrollTop + clientHeight < scrollHeight - 50
  414. }
  415. }
  416. /** 滚动到 message 底部 */
  417. const scrollToBottom = async (isIgnore = false) => {
  418. // 如果用户手动滚动过,不自动滚动
  419. if (userScrolled.value) return
  420. await nextTick();
  421. if (messageListRef.value) {
  422. requestAnimationFrame(() => {
  423. messageListRef.value.scrollTop = messageListRef.value.scrollHeight;
  424. });
  425. }
  426. };
  427. /** 自提滚动效果 */
  428. const textRoll = async () => {
  429. let index = 0;
  430. try {
  431. // 只能执行一次
  432. if (textRoleRunning.value) {
  433. return;
  434. }
  435. // 设置状态
  436. textRoleRunning.value = true;
  437. receiveMessageDisplayedText.value = "";
  438. const task = async () => {
  439. // 调整速度
  440. const diff =
  441. (receiveMessageFullText.value.length -
  442. receiveMessageDisplayedText.value.length) /
  443. 10;
  444. if (diff > 5) {
  445. textSpeed.value = 10;
  446. } else if (diff > 2) {
  447. textSpeed.value = 30;
  448. } else if (diff > 1.5) {
  449. textSpeed.value = 50;
  450. } else {
  451. textSpeed.value = 100;
  452. }
  453. // 对话结束,就按 30 的速度
  454. if (!conversationInProgress.value) {
  455. textSpeed.value = 10;
  456. }
  457. if (index < receiveMessageFullText.value.length) {
  458. receiveMessageDisplayedText.value +=
  459. receiveMessageFullText.value[index];
  460. index++;
  461. // 更新 message
  462. const lastMessage =
  463. activeMessageList.value[activeMessageList.value.length - 1];
  464. lastMessage.content = receiveMessageDisplayedText.value;
  465. // 滚动到住下面
  466. await scrollToBottom();
  467. // 重新设置任务
  468. timer = setTimeout(task, textSpeed.value);
  469. } else {
  470. // 文本完全显示且对话已结束,添加补充信息
  471. if (!conversationInProgress.value && props.replySupplement) {
  472. activeMessageList.value.push({
  473. type: "system",
  474. content: props.replySupplement,
  475. });
  476. }
  477. // 同步缓存历史记录
  478. if (!conversationInProgress.value) {
  479. cacheDataHistoryList.value.push({
  480. type: "system",
  481. content: receiveMessageFullText.value,
  482. createTime: new Date(),
  483. });
  484. localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
  485. }
  486. // 不是对话中可以结束
  487. if (!conversationInProgress.value) {
  488. textRoleRunning.value = false;
  489. clearTimeout(timer);
  490. } else {
  491. // 重新设置任务
  492. timer = setTimeout(task, textSpeed.value);
  493. }
  494. }
  495. };
  496. let timer = setTimeout(task, textSpeed.value);
  497. } catch {}
  498. };
  499. // 监听消息列表变化,自动滚动到底部
  500. watch(
  501. () => messageList.value,
  502. () => {
  503. scrollToBottom();
  504. },
  505. { deep: true }
  506. );
  507. /** 初始化 **/
  508. onMounted(async () => {
  509. // 从全局状态初始化年级ID
  510. gradeId.value = globalState.initGradeId();
  511. //默认加载没传递数字人表示的话---默认加载小智
  512. if (!personId.value) {
  513. let grade = route.query.grade || localStorage.getItem('selectedGrade')
  514. grade = "小学低年级"
  515. // 获取小学低年级AI数据
  516. const juniorAIRes = await teacherList({category: grade + 'AI'})
  517. const aiPerson = juniorAIRes.data.list.find(
  518. person => person.name === '小智'
  519. )
  520. if (aiPerson) {
  521. personId.value = aiPerson.id;
  522. personName.value = aiPerson.name;
  523. personImage.value = aiPerson.model2dPath;
  524. personIntroduce.value = aiPerson.systemMessage;
  525. }
  526. }
  527. // 确保selectedImage被设置
  528. selectedImage.value = personImage.value;
  529. //创建对话
  530. let cacheDataKeyData = localStorage.getItem(props.cacheDataKey);
  531. if (cacheDataKeyData) {
  532. activeConversationId.value = cacheDataKeyData;
  533. }else{
  534. // 智能问答
  535. CreateDialogue({ roleId: personId.value })
  536. .then((res) => {
  537. console.log("创建会话:", res);
  538. activeConversationId.value = res.data;
  539. //将会话存入会话缓存
  540. localStorage.setItem(props.cacheDataKey, activeConversationId.value)
  541. })
  542. .catch((error) => {
  543. console.error("请求出错:", error);
  544. });
  545. }
  546. await getConversation(personId.value);
  547. // 获取列表数据
  548. // activeMessageListLoading.value = true
  549. });
  550. // 路由参数变化监听
  551. watch(
  552. () => route.query,
  553. (newQuery, oldQuery) => {
  554. // 只有当id变化时才更新数据,避免不必要的刷新
  555. if (newQuery.id && newQuery.id !== oldQuery?.id) {
  556. // 停止语音播放
  557. stopPlayback();
  558. // 更新相关数据
  559. personId.value = newQuery.id;
  560. personName.value = newQuery.name;
  561. personIntroduce.value = newQuery.message;
  562. personImage.value = newQuery.image;
  563. selectedImage.value = newQuery.image;
  564. // 重新初始化对话
  565. CreateDialogue({ roleId: newQuery.id })
  566. .then((res) => {
  567. activeConversationId.value = res.data;
  568. })
  569. .catch((error) => {
  570. console.error("请求出错:", error);
  571. });
  572. getConversation(newQuery.id);
  573. // 重置消息列表和默认消息显示状态
  574. activeMessageList.value = [];
  575. showDefaultMessages.value = true;
  576. }
  577. },
  578. { immediate: true, deep: true }
  579. );
  580. // 监听personImage变化,更新selectedImage
  581. watch(
  582. () => personImage.value,
  583. (newImage) => {
  584. if (newImage) {
  585. selectedImage.value = newImage;
  586. }
  587. },
  588. { immediate: true }
  589. );
  590. // 组件卸载时清理语音资源
  591. onUnmounted(() => {
  592. stopPlayback();
  593. });
  594. // 暴露方法给父组件
  595. defineExpose({
  596. stopStream,
  597. clearMessageList,
  598. stopPlayback
  599. });
  600. </script>
  601. <style scoped lang="scss">
  602. @use "sass:math";
  603. // 定义rpx转换函数
  604. @function rpx($px) {
  605. @return math.div($px, 750) * 100vw;
  606. }
  607. .content-wrapper {
  608. display: flex;
  609. flex: 1;
  610. }
  611. .left-group {
  612. width: rpx(135);
  613. height: 100%;
  614. background: linear-gradient(to bottom, #001169, #8a78d0);
  615. }
  616. .mb-2 {
  617. color: black;
  618. margin-top: rpx(1);
  619. }
  620. .tac ::v-deep(.el-menu) {
  621. background-color: transparent;
  622. border: none;
  623. width: 100%;
  624. margin-top: rpx(55);
  625. margin-left: rpx(10);
  626. }
  627. .el-menu-item {
  628. width: rpx(115);
  629. height: rpx(25);
  630. margin-bottom: rpx(5);
  631. border-radius: rpx(6);
  632. color: white;
  633. font-size: rpx(8);
  634. }
  635. .el-menu-item .el-icon svg {
  636. font-size: rpx(15);
  637. color: white;
  638. }
  639. .el-menu ::v-deep(.el-menu-item:hover),
  640. .el-menu ::v-deep(.el-menu-item:focus),
  641. .el-menu ::v-deep(.el-menu-item:active) {
  642. background: linear-gradient(
  643. to bottom,
  644. #ffefb0,
  645. #ffcc00
  646. ); /* 设置悬停、聚焦、点击状态下的背景色 */
  647. box-shadow: 0 8px 8px rgb(0, 0, 0, 0.3);
  648. color: black;
  649. font-size: rpx(8);
  650. }
  651. .el-menu .el-menu-item.is-active {
  652. background: linear-gradient(to bottom, #fee78a, #ffce1b);
  653. color: black;
  654. font-size: rpx(8);
  655. box-shadow: 0 4px 8px rgba(3, 3, 3, 0.3);
  656. }
  657. // 侧边栏
  658. .people-image {
  659. width: rpx(130);
  660. height: 100%;
  661. display: flex;
  662. background-color: #ece9fd;
  663. overflow: hidden;
  664. }
  665. .people-image img {
  666. width: rpx(120);
  667. height: auto;
  668. object-fit: contain;
  669. }
  670. .selected-image {
  671. flex: 1;
  672. margin: auto;
  673. display: flex;
  674. justify-content: center;
  675. align-items: center;
  676. }
  677. .title-box {
  678. height: rpx(50);
  679. }
  680. .box-icon {
  681. width: 100%;
  682. height: 100%;
  683. flex: 1;
  684. display: flex; // 添加 flex 布局
  685. align-items: center; // 垂直居中
  686. color: black; // 设置图标颜色为白色
  687. padding-left: rpx(15);
  688. font-size: rpx(10); // 设置图标大小,可按需调整
  689. cursor: pointer; // 添加鼠标指针样式
  690. }
  691. .box-icon .left-icon {
  692. margin-left: rpx(10);
  693. margin-right: rpx(5); // 设置图标和文字之间的间距 ;
  694. }
  695. .number-people {
  696. flex: 1;
  697. height: 100%;
  698. display: flex;
  699. background-color: #ece9fd;
  700. }
  701. .content-box {
  702. flex: 1;
  703. // margin-top: rpx(10);
  704. // margin-bottom: rpx(10);
  705. margin: rpx(7);
  706. border-radius: rpx(15);
  707. background: rgba($color: #ffffff, $alpha: 0.5);
  708. }
  709. // 对话框
  710. .chat-dialog {
  711. display: flex;
  712. flex-direction: column;
  713. height: 100%;
  714. }
  715. .message-list {
  716. flex: 1;
  717. overflow-y: auto;
  718. padding: rpx(15);
  719. }
  720. /* 自定义滚动条样式 */
  721. .message-list::-webkit-scrollbar {
  722. width: rpx(2); /* 滚动条宽度 */
  723. }
  724. .message-list::-webkit-scrollbar-track {
  725. background: #f1effd; /* 滚动条轨道背景色 */
  726. border-radius: rpx(4);
  727. }
  728. .message-list::-webkit-scrollbar-thumb {
  729. background: #e2ddfc; /* 滚动条滑块颜色 */
  730. border-radius: rpx(4);
  731. }
  732. .message-list::-webkit-scrollbar-thumb:hover {
  733. background: #e2ddfc; /* 滚动条滑块 hover 状态颜色 */
  734. }
  735. .message-list .user-message {
  736. background-color: #ffffff;
  737. margin-left: auto; // 消息靠右显示
  738. margin-right: 0; // 重置右边距
  739. margin-bottom: rpx(10);
  740. max-width: rpx(400);
  741. font-size: rpx(8);
  742. width: fit-content; // 宽度随文字内容变化
  743. border-radius: rpx(5);
  744. padding: rpx(5);
  745. text-align: left; // 文字左对齐
  746. }
  747. .message-list .ai-message {
  748. background-color: #ffdd55;
  749. margin-left: 0; // 消息靠左显示
  750. margin-right: auto; // 重置右边距
  751. margin-bottom: rpx(10);
  752. width: fit-content;
  753. max-width: rpx(400);
  754. padding: rpx(5);
  755. font-size: rpx(8);
  756. border-radius: rpx(5);
  757. text-align: left; // 文字左对齐
  758. }
  759. .input-section {
  760. display: flex;
  761. padding: rpx(10);
  762. gap: rpx(5);
  763. .speech-btn {
  764. padding: rpx(5) rpx(10);
  765. background: #fff;
  766. border: 1px solid #ffce1b;
  767. border-radius: rpx(5);
  768. cursor: pointer;
  769. display: flex;
  770. align-items: center;
  771. &.recording {
  772. background: #ffeeba;
  773. border-color: #ffc107;
  774. .el-icon {
  775. color: #dc3545;
  776. }
  777. }
  778. .el-icon {
  779. font-size: rpx(8);
  780. color: #666;
  781. }
  782. }
  783. // 终止按钮样式
  784. .stop-btn {
  785. cursor: pointer;
  786. display: flex;
  787. align-items: center;
  788. img {
  789. width: rpx(20);
  790. height: rpx(20);
  791. }
  792. }
  793. }
  794. .input-section input {
  795. flex: 1;
  796. padding: rpx(5);
  797. font-size: rpx(7);
  798. border: 1px solid #ccc;
  799. border-radius: rpx(5);
  800. }
  801. .input-section button {
  802. padding: rpx(5) rpx(15);
  803. background: linear-gradient(
  804. to bottom,
  805. #fee78a,
  806. #ffce1b
  807. ); /* 设置悬停、聚焦、点击状态下的背景色 */
  808. color: black;
  809. border: none;
  810. font-size: rpx(7);
  811. border-radius: rpx(5);
  812. cursor: pointer;
  813. box-shadow: 0 0px 2px rgba(0, 0, 0, 0.3);
  814. }
  815. </style>