ChatTts.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683
  1. <script setup>
  2. import { ref, computed, onMounted, onBeforeUnmount } from "vue";
  3. import BG from "./components/BG.vue";
  4. import BottomArea from "./components/BottomArea.vue";
  5. import ChatList from "./components/ChatList.vue";
  6. import NavBarPage from "../components/NavBarPage.vue";
  7. import PQueue from "p-queue";
  8. import axios from "axios";
  9. import { fetchEventSource } from "@microsoft/fetch-event-source";
  10. import { useRoute, useRouter } from "vue-router";
  11. import delay from "delay";
  12. import { fetchTaskStatus } from "./utils/api";
  13. const route = useRoute();
  14. const router = useRouter();
  15. const conversationId = computed(() => route.query.taskId); // 会话ID
  16. const round = computed(() => route.query.round); // 轮次
  17. const sessionId = ref("");
  18. const taskStatus = ref(false)
  19. const loading = ref(true); //初始化进入的加载状态
  20. const currentRate = ref(0);
  21. const currentRound = computed(() => {
  22. const filteredMessages = chatList.value.filter(
  23. (message) =>
  24. message.text !== "我没有听清,麻烦在重复一下。" && message.text !== ""
  25. );
  26. return Math.floor(filteredMessages.length / 2);
  27. });
  28. // 当前进度百分比整数部分
  29. const rate = computed(() => {
  30. return Math.floor((currentRound.value / round.value) * 100);
  31. });
  32. const text = computed(() => `${currentRound.value} / ${round.value}`);
  33. // 对话记录
  34. const chatList = ref([]);
  35. let i = 0;
  36. // 我 我是 我是中国人 我是中国人的英雄 我是中国人的英雄。[
  37. const updateChatList = async (message = "", loading = true) => {
  38. const timer = setInterval(() => {
  39. i = i + 1;
  40. chatList.value = chatList.value.map((item, index) => {
  41. if (index == chatList.value.length - 1) {
  42. return {
  43. ...item,
  44. text: message.substring(0, i),
  45. };
  46. } else {
  47. return item;
  48. }
  49. });
  50. localStorage.setItem("chatHistory", JSON.stringify(chatList.value));
  51. if (i >= message.length) {
  52. clearInterval(timer);
  53. }
  54. }, 50);
  55. };
  56. // 创建一个队列实例,设置并发数为 1
  57. const queue = new PQueue({ concurrency: 3 });
  58. const queue1 = new PQueue({ concurrency: 1 });
  59. const queue2 = new PQueue({ concurrency: 1 });
  60. // 需要tts的文本队列
  61. const messageQueue = [];
  62. // lastStrIndex 用于记录上一个字符串的结束位置
  63. let lastStrIndex = 0;
  64. // 将字符串根据标点符号断句分割,并添加到messageQueue中
  65. const splitMessage = async (str) => {
  66. const punctuation = ["。","," ,"!", "?", ";", ":"];
  67. for (let i = lastStrIndex; i < str.length; i++) {
  68. if (punctuation.includes(str[i])) {
  69. const message = str.slice(lastStrIndex, i + 1);
  70. console.log(message, "==========");
  71. playTTS(message);
  72. lastStrIndex = i + 1; // 更新上一个字符串的结束位置
  73. }
  74. }
  75. };
  76. const handleStartRecord = async () => {
  77. queue.clear();
  78. queue1.clear();
  79. };
  80. const generateSessionId = () => {
  81. // 获取当前时间戳
  82. const timestamp = Date.now().toString();
  83. // 生成一个随机数
  84. const randomNum = Math.floor(Math.random() * 10000);
  85. // 将时间戳和随机数拼接成会话ID
  86. const sessionId = `${timestamp}${randomNum}`;
  87. return sessionId;
  88. };
  89. // 发送消息
  90. const handleStopRecord = (message) => {
  91. lastStrIndex = 0; // 重置
  92. chatList.value = [
  93. ...chatList.value,
  94. {
  95. id: Date.now(),
  96. type: "user",
  97. text: message,
  98. loading: false,
  99. },
  100. {
  101. id: Date.now(),
  102. type: "gpt",
  103. text: "",
  104. loading: true,
  105. },
  106. ];
  107. sessionId.value = generateSessionId();
  108. chatGpt(message);
  109. };
  110. // let audioQueue = []; // 音频队列,保存按请求顺序的音频数据
  111. // let playingQueue = []; // 用来记录当前正在播放的音频序号
  112. // let isPlaying = false; // 当前是否正在播放
  113. // let orderId = 0; // 请求顺序号
  114. // 用来控制正在播放音频的锁定机制
  115. let playingLock = false; // 标识是否正在播放
  116. let audioQueue = []; // 音频队列,保存按请求顺序的音频数据
  117. let playingQueue = []; // 记录已播放的音频序号
  118. let isPlaying = false; // 当前是否正在播放
  119. let orderId = 0; // 请求顺序号
  120. // 1. 发起 TTS 请求(修改后)
  121. const playTTS = async (ttsMessage) => {
  122. queue.add(async () => {
  123. const currentOrderId = orderId++; // 先获取顺序号再发送请求
  124. try {
  125. const res = await axios.get(
  126. `/openapi-prd/ai/ttt/synthesize?text=${ttsMessage}&sessionId=${sessionId.value}`,
  127. { responseType: "arraybuffer" }
  128. );
  129. // 即使请求失败也要保持队列连续性(新增部分)
  130. const audioItem = {
  131. orderId: currentOrderId,
  132. audioData: res.data || null,
  133. status: res.data ? 'valid' : 'invalid'
  134. };
  135. audioQueue.push(audioItem);
  136. audioQueue.sort((a, b) => a.orderId - b.orderId); // 保持队列有序
  137. if (!isPlaying) await playNextAudio();
  138. } catch (error) {
  139. console.error("调用 TTS API 时出错:", error);
  140. // 请求失败时插入占位符保持顺序(关键修复)
  141. audioQueue.push({
  142. orderId: currentOrderId,
  143. audioData: null,
  144. status: 'invalid'
  145. });
  146. audioQueue.sort((a, b) => a.orderId - b.orderId);
  147. if (!isPlaying) await playNextAudio();
  148. }
  149. });
  150. };
  151. // 2. 播放下一个音频(修改后)
  152. const playNextAudio = async () => {
  153. // if (audioQueue.length === 0) {
  154. // isPlaying = false;
  155. // console.log("所有音频播放完毕.");
  156. // return;
  157. // }
  158. if (audioQueue.length === 0 && playingQueue.length > 0) {
  159. const maxPlayed = Math.max(...playingQueue);
  160. if (orderId > maxPlayed + 1) {
  161. console.log("仍有未完成请求,暂不重置");
  162. } else {
  163. isPlaying = false;
  164. // 新增:当队列完全播放完毕时重置所有状态
  165. audioQueue = [];
  166. playingQueue = [];
  167. orderId = 0; // 可选:如果需要重置顺序号
  168. console.log("所有音频播放完毕,已重置播放队列.");
  169. return;
  170. }
  171. }
  172. const nextAudio = getNextAudioToPlay();
  173. if (!nextAudio) {
  174. console.log("等待后续音频数据...");
  175. return;
  176. }
  177. try {
  178. isPlaying = true;
  179. console.log(`正在播放音频,序号:${nextAudio.orderId}`);
  180. if (nextAudio.status === 'valid') {
  181. await playAudio(nextAudio.audioData);
  182. } else {
  183. console.warn(`跳过无效音频,序号:${nextAudio.orderId}`);
  184. }
  185. // 更新播放状态(优化处理)
  186. playingQueue = [...new Set([...playingQueue, nextAudio.orderId])].sort((a, b) => a - b);
  187. audioQueue = audioQueue.filter(audio => audio.orderId !== nextAudio.orderId);
  188. // 立即尝试播放下一个(优化播放流程)
  189. isPlaying = false;
  190. await playNextAudio();
  191. } catch (error) {
  192. console.error(`播放失败,序号:${nextAudio.orderId}`, error);
  193. audioQueue = audioQueue.filter(audio => audio.orderId !== nextAudio.orderId);
  194. isPlaying = false;
  195. await playNextAudio();
  196. }
  197. };
  198. // 获取下一个应该播放的音频(修改后)
  199. const getNextAudioToPlay = () => {
  200. // 获取当前应该播放的最小序号
  201. const expectedOrderId = playingQueue.length > 0
  202. ? Math.max(...playingQueue) + 1
  203. : 0;
  204. // 查找第一个匹配的有效音频(新增有效性检查)
  205. const nextAudio = audioQueue.find(audio =>
  206. audio.orderId === expectedOrderId &&
  207. audio.status === 'valid'
  208. );
  209. // 如果找不到有效音频但有序号匹配的无效项,跳过该序号
  210. if (!nextAudio) {
  211. const invalidAudio = audioQueue.find(audio =>
  212. audio.orderId === expectedOrderId
  213. );
  214. if (invalidAudio) {
  215. console.warn(`检测到无效音频,自动跳过序号:${expectedOrderId}`);
  216. playingQueue.push(expectedOrderId);
  217. return getNextAudioToPlay(); // 递归处理
  218. }
  219. }
  220. return nextAudio || null;
  221. };
  222. // 1. 发起 TTS 请求
  223. // const playTTS = async (ttsMessage) => {
  224. // queue.add(async () => {
  225. // const currentOrderId = orderId++;
  226. // try {
  227. // const res = await axios.get(
  228. // `/openapi-prd/ai/ttt/synthesize?text=${ttsMessage}&sessionId=${sessionId.value}`,
  229. // { responseType: "arraybuffer" }
  230. // );
  231. // if (!res.data || res.data.byteLength === 0) {
  232. // console.error("音频数据无效,无法播放.");
  233. // return;
  234. // }
  235. // // 按顺序存入音频队列
  236. // audioQueue.push({ orderId: currentOrderId, audioData: res.data });
  237. // // 仅在没有播放时启动播放
  238. // if (!isPlaying) {
  239. // await playNextAudio();
  240. // }
  241. // } catch (error) {
  242. // console.error("调用 TTS API 时出错:", error);
  243. // }
  244. // });
  245. // };
  246. // 2. 播放下一个音频
  247. // const playNextAudio = async () => {
  248. // // 如果播放队列为空,停止播放
  249. // if (audioQueue.length === 0) {
  250. // isPlaying = false; // 播放完所有音频
  251. // // audioQueue = []; // 确保列表为空
  252. // console.log("所有音频播放完毕.");
  253. // return;
  254. // }
  255. // // 找到队列中最小序号的音频,确保顺序播放
  256. // const nextAudio = getNextAudioToPlay();
  257. // console.log(nextAudio,"没有找到下一个应该播放的音频,队列尚未按顺序准备好.");
  258. // // 如果找不到下一个应该播放的音频,表示队列还没有按顺序准备好
  259. // if (!nextAudio) {
  260. // console.log("没有找到下一个应该播放的音频,队列尚未按顺序准备好.");
  261. // console.log("当前音频队列:", audioQueue); // 打印当前音频队列
  262. // console.log("当前已播放的序列:", playingQueue); // 打印已经播放的音频序列
  263. // return;
  264. // }
  265. // // 打印正在播放的音频序号
  266. // console.log(`正在播放音频,序号:${nextAudio.orderId}`);
  267. // try {
  268. // isPlaying = true; // 标记当前正在播放
  269. // playingLock = true; // 锁定播放,避免新的音频插入
  270. // // 播放当前音频
  271. // await playAudio(nextAudio.audioData);
  272. // // 播放完成后,移除已播放的音频
  273. // audioQueue = audioQueue.filter(audio => audio.orderId !== nextAudio.orderId);
  274. // playingQueue.push(nextAudio.orderId); // 将已播放的序号添加到播放记录
  275. // // 输出音频播放完成的日志
  276. // console.log(`音频序号 ${nextAudio.orderId} 播放完成,等待播放下一个包...`);
  277. // // 解除锁定,等待下一个音频播放
  278. // playingLock = false;
  279. // // 使用定时器周期性检查是否可以播放下一个音频
  280. // const checkNextAudioTimer = setInterval(async () => {
  281. // if (!playingLock) {
  282. // await playNextAudio();
  283. // clearInterval(checkNextAudioTimer); // 清除定时器
  284. // }
  285. // }, 300); // 每秒检查一次
  286. // } catch (error) {
  287. // console.error(`播放音频序号 ${nextAudio.orderId} 时出错:`, error);
  288. // // 如果某个音频播放失败,跳过这个音频,继续播放下一个
  289. // audioQueue = audioQueue.filter(audio => audio.orderId !== nextAudio.orderId); // 移除报错的音频
  290. // playingQueue.push(nextAudio.orderId); // 将已播放的序号添加到播放记录
  291. // playingLock = false; // 解除锁定
  292. // // 继续播放下一个音频
  293. // await playNextAudio();
  294. // }
  295. // };
  296. // 获取下一个应该播放的音频(保证顺序)
  297. // const getNextAudioToPlay = () => {
  298. // // 检查音频队列中是否有下一个应该播放的音频
  299. // console.log("检查下一个播放的音频...");
  300. // for (let i = 0; i < audioQueue.length; i++) {
  301. // const audio = audioQueue[i];
  302. // // 找到最小的序号,即请求的顺序号
  303. // if (audio.orderId === playingQueue.length) {
  304. // console.log(`找到了下一个应该播放的音频,序号:${audio.orderId}`);
  305. // return audio;
  306. // }
  307. // }
  308. // console.log("没有找到下一个应该播放的音频,返回 null");
  309. // return null; // 如果没有找到匹配的音频,返回 null
  310. // };
  311. // 播放音频的方法
  312. const playAudio = (audioData) => {
  313. return new Promise((resolve, reject) => {
  314. const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  315. const blob = new Blob([audioData], { type: "audio/wav" });
  316. const url = URL.createObjectURL(blob);
  317. const audioBufferSourceNode = audioContext.createBufferSource();
  318. fetch(url)
  319. .then((response) => {
  320. if (!response.ok) {
  321. console.error("音频文件请求失败: ", response.statusText);
  322. reject(new Error("音频文件请求失败"));
  323. return;
  324. }
  325. return response.arrayBuffer();
  326. })
  327. .then((arrayBuffer) => audioContext.decodeAudioData(arrayBuffer))
  328. .then((audioBuffer) => {
  329. audioBufferSourceNode.buffer = audioBuffer;
  330. audioBufferSourceNode.connect(audioContext.destination);
  331. audioBufferSourceNode.start(0);
  332. // 当音频播放完毕时,调用 resolve
  333. audioBufferSourceNode.onended = () => {
  334. URL.revokeObjectURL(url);
  335. resolve(); // 播放完成后,继续播放下一个
  336. };
  337. audioBufferSourceNode.onerror = (error) => {
  338. console.error("音频播放错误:", error);
  339. URL.revokeObjectURL(url);
  340. reject(error); // 播放失败时,返回错误
  341. };
  342. })
  343. .catch((error) => {
  344. console.error("音频加载或解码时出错:", error);
  345. reject(error); // 如果解码或播放出错,reject
  346. });
  347. });
  348. };
  349. // const playTTS1 = async (ttsMessage) => {
  350. // try {
  351. // const res = await axios.post(
  352. // `/openapi-stg/ai/voice/tts/v2`,
  353. // { sessionId: "N7FB_G0WlrOLjc", text: ttsMessage },
  354. // {
  355. // responseType: "arraybuffer",
  356. // headers: {
  357. // "X-Ai-TTS-Appid": "2b1317fb5b284b308dc90a6fdeae6c4e",
  358. // },
  359. // }
  360. // );
  361. // console.log(res.data);
  362. // queue1.add(async () => {
  363. // // 播放获取到的音频
  364. // await playAudio(res.data);
  365. // });
  366. // } catch (error) {
  367. // console.error("Error calling TTS API:", error);
  368. // }
  369. // };
  370. // const playAudio1 = (audioData, options = {}) => {
  371. // return new Promise((resolve, reject) => {
  372. // const blob = new Blob([audioData], { type: "audio/wav" });
  373. // const url = URL.createObjectURL(blob);
  374. // const audio = new Audio(url);
  375. // audio.setAttribute("id", "audio");
  376. // audio.setAttribute("autoplay", "autoplay");
  377. // if (options.volume) {
  378. // audio.volume = options.volume; // 设置音量
  379. // }
  380. // audio.onended = () => {
  381. // URL.revokeObjectURL(url);
  382. // resolve();
  383. // };
  384. // audio.onerror = (error) => {
  385. // URL.revokeObjectURL(url);
  386. // reject(error);
  387. // };
  388. // audio
  389. // .play()
  390. // .then(() => console.log("Audio playing"))
  391. // .catch(reject);
  392. // });
  393. // };
  394. const chatGpt = async (userMessage) => {
  395. const downloadUrl = "/openapi-prd/ai/intelligent-tutoring/task/dialogue";
  396. i = 0;
  397. queue2.add(() => delay(3000));
  398. try {
  399. const response = await axios.post(
  400. downloadUrl,
  401. {
  402. conversationId: conversationId.value,
  403. content: userMessage,
  404. },
  405. {
  406. headers: {
  407. "Content-Type": "application/json",
  408. },
  409. onDownloadProgress: ({ event }) => {
  410. const xhr = event.target;
  411. let { responseText } = xhr;
  412. console.log("responseText", responseText);
  413. if (responseText.includes("code") && responseText.includes("400")) {
  414. // console.log("responseTextcode", responseText);
  415. // 更新聊天列表
  416. const text = "我没有听清,麻烦在重复一下。";
  417. updateChatList(text, false);
  418. queue.add(async () => {
  419. await splitMessage(text);
  420. });
  421. } else {
  422. // 用于语音播报
  423. queue.add(async () => {
  424. await splitMessage(responseText);
  425. });
  426. queue2.add(async () => await updateChatList(responseText));
  427. //updateChatList(responseText);
  428. }
  429. },
  430. }
  431. );
  432. setTimeout(() => {
  433. handleTaskStatus()
  434. },3000)
  435. //queue2.add(async () => await updateChatList("", false));
  436. } catch (error) {
  437. //updateChatList("", false);
  438. console.error("出错:", error);
  439. }
  440. };
  441. const chatGpt2 = (userMessage) => {
  442. const params = {
  443. conversationId: "2c64eb8b69be432ca0bb9ae55bc78def",
  444. content: userMessage,
  445. };
  446. const ctrlAbout = new AbortController();
  447. fetchEventSource(
  448. "https://fls-ai.pingan.com.cn/openapi/ai/intelligent-tutoring/task/dialogue",
  449. {
  450. method: "POST",
  451. headers: {
  452. "Content-Type": "application/json", // 文本返回格式
  453. },
  454. body: JSON.stringify(params),
  455. signal: ctrlAbout.signal,
  456. openWhenHidden: true, // 在浏览器标签页隐藏时保持与服务器的EventSource连接
  457. onmessage(res) {
  458. // 操作流式数据
  459. console.log(JSON.parse(res.data), "==========");
  460. },
  461. onclose(res) {
  462. // 关闭流
  463. },
  464. onerror(error) {
  465. // 返回流报错
  466. },
  467. }
  468. );
  469. };
  470. const chatGpt1 = async (userMessage) => {
  471. const downloadUrl =
  472. "https://fls-ai.pingan.com.cn/openapi/ai/llm/forward/api/ai/nlp/dialogue";
  473. try {
  474. const response = await axios.post(
  475. downloadUrl,
  476. {
  477. conversationId: "1976cfe3a5174f9ba768677f789cad7e",
  478. content: `${userMessage},请输出纯文本的回答,不要使用markdown输出`,
  479. messageId: Date.now(),
  480. applicationId: "",
  481. },
  482. {
  483. headers: {
  484. "Team-Id": "123456",
  485. Authorization: "9b2f86c99b5847739045e6b85f355301",
  486. "Content-Type": "application/json",
  487. },
  488. onDownloadProgress: ({ event }) => {
  489. const xhr = event.target;
  490. const { responseText } = xhr;
  491. console.log(responseText);
  492. // 更新聊天列表
  493. updateChatList(responseText);
  494. // 用于语音播报
  495. queue.add(async () => {
  496. await splitMessage(responseText);
  497. });
  498. },
  499. }
  500. );
  501. updateChatList("", false);
  502. } catch (error) {
  503. updateChatList("", false);
  504. console.error("下载文件时出错:", error);
  505. }
  506. };
  507. const next = () => {
  508. router.push({
  509. name: "result",
  510. query: { conversationId: conversationId.value },
  511. });
  512. };
  513. const loadChatHistory = () => {
  514. const chatHistory = localStorage.getItem("chatHistory");
  515. const chatStatus = localStorage.getItem("status");
  516. if (chatHistory) {
  517. const history = JSON.parse(chatHistory);
  518. history.forEach((item) => {
  519. chatList.value.push(item);
  520. });
  521. }
  522. if(chatStatus){
  523. taskStatus.value = JSON.parse(chatStatus)
  524. }
  525. };
  526. // 查询任务结束状态
  527. const handleTaskStatus = async () => {
  528. const res = await fetchTaskStatus(conversationId.value)
  529. if(res.code == 200){
  530. taskStatus.value = res.body
  531. localStorage.setItem("status", res.body);
  532. }
  533. }
  534. // 在组件挂载时调用
  535. onMounted(() => {
  536. loadChatHistory();
  537. setTimeout(() => {
  538. loading.value = false;
  539. }, 3000);
  540. });
  541. </script>
  542. <template>
  543. <div class="main">
  544. <nav-bar-page title="考试" />
  545. <div class="rate-circle">
  546. <van-circle
  547. :stroke-width="80"
  548. size="70px"
  549. v-model:current-rate="currentRate"
  550. :rate="rate"
  551. :text="text"
  552. />
  553. </div>
  554. <BG />
  555. <div class="loading" v-show="loading">
  556. <van-loading color="#0094ff" size="26px" vertical>加载中...</van-loading>
  557. </div>
  558. <ChatList :chatList="chatList" />
  559. <BottomArea
  560. @startRecord="handleStartRecord"
  561. @stopRecord="handleStopRecord"
  562. v-show="rate < 100 || taskStatus"
  563. />
  564. <div v-show="rate >= 100 || taskStatus" class="next-btn">
  565. <van-button style="width: 100%" @click="next" type="primary"
  566. >已完成对话,下一步</van-button
  567. >
  568. </div>
  569. </div>
  570. </template>
  571. <style>
  572. * {
  573. -webkit-touch-callout: none; /*系统默认菜单被禁用*/
  574. -webkit-user-select: none; /*webkit浏览器*/
  575. -khtml-user-select: none; /*早期浏览器*/
  576. -moz-user-select: none; /*火狐*/
  577. -ms-user-select: none; /*IE10*/
  578. user-select: none;
  579. }
  580. </style>
  581. <style scoped>
  582. .main {
  583. width: 100vw;
  584. height: 100vh;
  585. overflow: hidden;
  586. }
  587. .rate-circle {
  588. position: fixed;
  589. right: 20px;
  590. top: 70px;
  591. z-index: 9999;
  592. }
  593. .next-btn {
  594. position: fixed;
  595. bottom: calc(20px + env(safe-area-inset-bottom));
  596. width: 100%;
  597. box-sizing: border-box;
  598. padding: 0 20px;
  599. }
  600. .loading {
  601. position: fixed;
  602. top: 0;
  603. left: 0;
  604. width: 100%;
  605. height: 100%;
  606. background-color: rgba(0, 0, 0, 0.5);
  607. display: flex;
  608. justify-content: center;
  609. align-items: center;
  610. z-index: 9999;
  611. }
  612. </style>