AIQuestions.vue 24 KB

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