TextToImage.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. <template>
  2. <!-- 右侧AI问答 -->
  3. <div class="number-people">
  4. <div class="content-box">
  5. <!-- AI对话框 -->
  6. <div class="chat-dialog">
  7. <!-- 对话消息列表 -->
  8. <div class="message-list">
  9. <div v-if="imageAllList.length > 0">
  10. <div v-for="(item, index) in imageAllList" :key="index">
  11. <!-- 用户消息 -->
  12. <div class="user-message" v-if="item.type === 'user'">
  13. {{ item.content }}
  14. </div>
  15. <!-- AI生成图片对话框 -->
  16. <div class="ai-message" v-if="item.type !== 'user'">
  17. {{ item.content }}
  18. <span v-if="item.loading" class="loading-dots">
  19. <span class="dot"></span>
  20. <span class="dot"></span>
  21. <span class="dot"></span>
  22. </span>
  23. <div class="image-list" v-if="item.imageList">
  24. <el-image
  25. v-for="(image, index) in item.imageList"
  26. :key="index"
  27. style=" width: fit-content; height: 220px; margin: 10px;"
  28. :src="image"
  29. :preview-src-list="item.imageList"
  30. fit="cover"
  31. show-progress
  32. >
  33. <template
  34. #toolbar="{ actions, prev, next, reset, activeIndex, setActiveItem }"
  35. >
  36. <el-icon @click="prev"><Back /></el-icon>
  37. <el-icon @click="next"><Right /></el-icon>
  38. <el-icon @click="setActiveItem(item.imageList.length - 1)">
  39. <DArrowRight />
  40. </el-icon>
  41. <el-icon @click="actions('zoomOut')"><ZoomOut /></el-icon>
  42. <el-icon
  43. @click="actions('zoomIn', { enableTransition: false, zoomRate: 2 })">
  44. <ZoomIn />
  45. </el-icon>
  46. <el-icon
  47. @click="actions('clockwise', { rotateDeg: 180, enableTransition: false })">
  48. <RefreshRight />
  49. </el-icon>
  50. <el-icon @click="actions('anticlockwise')"><RefreshLeft /></el-icon>
  51. <el-icon @click="reset"><Refresh /></el-icon>
  52. <el-icon @click="download(activeIndex)"><Download /></el-icon>
  53. </template>
  54. </el-image>
  55. </div>
  56. </div>
  57. </div>
  58. </div>
  59. <div v-else class="content-demo">
  60. <h3>请参考示例:</h3>
  61. <!-- 用户消息 -->
  62. <div class="user-message">
  63. 生成粉色的会飞的猪
  64. </div>
  65. <!-- AI生成图片对话框 -->
  66. <div class="ai-message" >
  67. 为您生成图片:
  68. <div class="image-list" v-if="demoImageList">
  69. <el-image
  70. v-for="(image, index) in demoImageList"
  71. :key="index"
  72. style=" width: fit-content; height: 180px; margin: 10px;"
  73. :src="image"
  74. :preview-src-list="demoImageList"
  75. fit="cover"
  76. show-progress
  77. >
  78. <template
  79. #toolbar="{ actions, prev, next, reset, activeIndex, setActiveItem }"
  80. >
  81. <el-icon @click="prev"><Back /></el-icon>
  82. <el-icon @click="next"><Right /></el-icon>
  83. <el-icon @click="setActiveItem(demoImageList.length - 1)">
  84. <DArrowRight />
  85. </el-icon>
  86. <el-icon @click="actions('zoomOut')"><ZoomOut /></el-icon>
  87. <el-icon
  88. @click="actions('zoomIn', { enableTransition: false, zoomRate: 2 })">
  89. <ZoomIn />
  90. </el-icon>
  91. <el-icon
  92. @click="actions('clockwise', { rotateDeg: 180, enableTransition: false })">
  93. <RefreshRight />
  94. </el-icon>
  95. <el-icon @click="actions('anticlockwise')"><RefreshLeft /></el-icon>
  96. <el-icon @click="reset"><Refresh /></el-icon>
  97. <el-icon @click="download(activeIndex)"><Download /></el-icon>
  98. </template>
  99. </el-image>
  100. </div>
  101. </div>
  102. </div>
  103. </div>
  104. <!-- 输入框和发送按钮 -->
  105. <div class="input-section">
  106. <input
  107. type="text"
  108. v-model="inputMessage"
  109. placeholder="描述任何画面..."
  110. @keyup.enter="sendMessage"
  111. />
  112. <!-- 语音输入按钮 -->
  113. <VoiceInput
  114. @voiceRecognized="handleVoiceRecognized"
  115. lang="zh-CN"
  116. maxDuration="10"
  117. />
  118. <!-- 终止按钮 -->
  119. <div
  120. v-if="conversationInProgress"
  121. @click="stopStream"
  122. class="stop-btn"
  123. title="终止问答"
  124. >
  125. <img :src="stopicon" alt="停止" />
  126. </div>
  127. <button v-if="!conversationInProgress"
  128. @click="sendMessage">发送</button>
  129. </div>
  130. </div>
  131. </div>
  132. </div>
  133. </template>
  134. <script setup>
  135. import { ref, onMounted,onUnmounted} from 'vue'
  136. import {AiImageStatusEnum, CreatePainting, PaintingGetMys} from '@/api/questions.js'
  137. import { useRouter, useRoute } from 'vue-router'
  138. import demo1 from '@/assets/images/ai-demo/ai-image-demo1.png'
  139. import demo2 from '@/assets/images/ai-demo/ai-image-demo2.png'
  140. import demo3 from '@/assets/images/ai-demo/ai-image-demo3.png'
  141. import demo4 from '@/assets/images/ai-demo/ai-image-demo4.png'
  142. import {
  143. Document,
  144. Menu as IconMenu,
  145. Location,
  146. Setting,
  147. ArrowLeftBold,
  148. Fold,
  149. Expand,
  150. ChatLineRound,
  151. Picture,
  152. MagicStick,
  153. Tickets,
  154. User
  155. } from '@element-plus/icons-vue'
  156. import { saveRecord } from '@/api/personalized/index.js'
  157. // 导入全局状态
  158. import { globalState } from '@/utils/globalState.js'
  159. // 语音图标
  160. import { Microphone, Mute } from "@element-plus/icons-vue";
  161. import VoiceInput from '../voice/VoiceInput.vue'
  162. // 终止按钮
  163. import stopicon from "@/assets/icon/stopicon.png";
  164. // 消息组件
  165. import {Message} from "@/utils/message/Message.js";
  166. // 导入getModelIdByType接口
  167. import { getModelIdByType } from '@/api/teachers.js'
  168. import { ModelTypeEnum } from '@/api/teachers.js'
  169. // 对话状态变量
  170. const conversationInProgress = ref(false); // 对话是否正在进行中
  171. const conversationInAbortController = ref(); // 对话进行中 abort 控制器
  172. // 返回上一页
  173. const goBack = () => {
  174. router.push('/ai-laboratory')
  175. }
  176. const router = useRouter()
  177. const route = useRoute()
  178. // 导入图片
  179. import question from '@/assets/icon/question.png'
  180. import painting from '@/assets/icon/painting.png'
  181. import human from '@/assets/icon/human.png'
  182. import LeftPanel from '@/components/LeftPanel.vue'
  183. const leftPanelRef = ref(null)
  184. // tts 语音
  185. import { useAudioPlayer } from '@/api/tts/useAudioPlayer';
  186. const { playAudioChunk } = useAudioPlayer();
  187. // 添加抽屉显示状态
  188. const drawerVisible = ref(true)
  189. // 添加切换抽屉显示状态的函数
  190. const toggleDrawer = () => {
  191. drawerVisible.value = !drawerVisible.value
  192. }
  193. const demoImageList = [demo1, demo2, demo3, demo4]
  194. // 年级ID相关
  195. const gradeId = ref('')
  196. // 添加消息计数器变量
  197. const messageCount = ref(0)
  198. // modelId响应式变量
  199. const modelId = ref(0)
  200. // 保存记录
  201. onMounted(async () => {
  202. // 从全局状态初始化年级ID
  203. gradeId.value = globalState.initGradeId()
  204. try{
  205. const res = await saveRecord({
  206. brpNjId: gradeId.value,
  207. brpType: "aiCount",
  208. brpProgress: 1
  209. });
  210. // 获取modelId
  211. const modelRes = await getModelIdByType({ type: ModelTypeEnum.TEXT_TO_IMAGE, platform: "DouBao" })
  212. modelId.value = modelRes.data
  213. }catch(error){
  214. console.error('保存记录失败:', error);
  215. }
  216. });
  217. // 消息列表和输入内容的响应式变量
  218. const messages = ref([])
  219. const inputMessage = ref('')
  220. // 处理语音识别结果
  221. const handleVoiceRecognized = (transcript) => {
  222. inputMessage.value += transcript;
  223. }
  224. // 停止操作函数
  225. const stopStream = async () => {
  226. // tip:如果 stream 进行中的 message,就需要调用 controller 结束
  227. if (conversationInAbortController.value) {
  228. conversationInAbortController.value.abort();
  229. }
  230. // 设置为 false
  231. conversationInProgress.value = false;
  232. };
  233. // 发送消息函数
  234. const sendMessage = async() => {
  235. if (inputMessage.value.trim()) {
  236. // 创建 AbortController 实例,以便中止请求
  237. conversationInAbortController.value = new AbortController();
  238. // 标记对话进行中
  239. conversationInProgress.value = true;
  240. // messages.value.push(inputMessage.value.trim())
  241. // 先保存内容 再置空输入框
  242. let content = inputMessage.value;
  243. inputMessage.value = ''
  244. imageAllList.value.push({
  245. type: 'user',
  246. content: content,
  247. })
  248. imageAllList.value.push({
  249. type: 'ai',
  250. content: "正在为您生成图片,请稍等",
  251. loading: true
  252. })
  253. // 递增消息计数器
  254. messageCount.value++
  255. // 发送saveRecord请求 保存消息次数
  256. try{
  257. await saveRecord({
  258. brpNjId: gradeId.value,
  259. brpType: "aiCount",
  260. brpProgress: messageCount.value
  261. });
  262. console.log('保存记录成功,消息次数:', messageCount.value);
  263. }catch(error){
  264. console.error('保存记录失败:', error);
  265. conversationInProgress.value = false;
  266. }
  267. try {
  268. CreatePainting({
  269. "modelId": modelId.value,
  270. "prompt":content,
  271. "width":1024,
  272. "height":1024
  273. }).then(res=>{
  274. console.log("生成图片",res)
  275. //目前写死调用已生成的图片,全部通了后再改
  276. inProgressImageMap.value[res.data] = {id:res.data,status:AiImageStatusEnum.IN_PROGRESS}
  277. // inProgressImageMap.value[260] = {id:260,status:AiImageStatusEnum.IN_PROGRESS}
  278. }).finally(() => {
  279. // 图片生成请求完成后更新状态
  280. conversationInProgress.value = false;
  281. });
  282. } catch (error) {
  283. console.error('生成图片失败:', error);
  284. conversationInProgress.value = false;
  285. }
  286. }
  287. };
  288. // 生成图片
  289. import { ElIcon } from 'element-plus'
  290. import {
  291. Back,
  292. DArrowRight,
  293. Download,
  294. Refresh,
  295. RefreshLeft,
  296. RefreshRight,
  297. Right,
  298. ZoomIn,
  299. ZoomOut,
  300. } from '@element-plus/icons-vue'
  301. const imageAllList = ref([]) // 对话的消息列表
  302. const imageList = ref([]) // image 列表
  303. // 图片轮询相关的参数(正在生成中的)
  304. const inProgressImageMap = ref({}) // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
  305. const inProgressTimer = ref() // 生成中的 image 定时器,轮询生成进展
  306. /** 轮询生成中的 image 列表 */
  307. const refreshWatchImages = async () => {
  308. const imageIds = Object.keys(inProgressImageMap.value).map(Number)
  309. if (imageIds.length === 0) {
  310. return
  311. }
  312. const list = await PaintingGetMys(imageIds)
  313. const newWatchImages = {}
  314. list.data.forEach((image) => {
  315. if (image.status === AiImageStatusEnum.IN_PROGRESS) {
  316. newWatchImages[image.id] = image
  317. } else {
  318. imageAllList.value.pop();
  319. imageAllList.value.push({
  320. type: 'ai',
  321. content: "已为您生成图片:",
  322. imageList: [image.picUrl],
  323. })
  324. }
  325. })
  326. inProgressImageMap.value = newWatchImages
  327. if (newWatchImages.size === 0) {
  328. inProgressTimerFun()
  329. }
  330. }
  331. /** 组件挂在的时候 */
  332. onMounted(async () => {
  333. refreshWatchImagesFun()
  334. })
  335. /** 组件取消挂在的时候 */
  336. onUnmounted(async () => {
  337. inProgressTimerFun()
  338. })
  339. // 自动刷新 image 列表
  340. const refreshWatchImagesFun = () => {
  341. inProgressTimer.value = setInterval(async () => {
  342. await refreshWatchImages()
  343. }, 1000 * 3)
  344. }
  345. // 停止刷新image列表
  346. const inProgressTimerFun = () => {
  347. if (inProgressTimer.value) {
  348. clearInterval(inProgressTimer.value)
  349. }
  350. }
  351. </script>
  352. <style scoped lang="scss">
  353. @use 'sass:math';
  354. // 定义rpx转换函数
  355. @function rpx($px) {
  356. @return math.div($px, 750) * 100vw;
  357. }
  358. //===========================================全局除了删除侧边栏内容唯一改动的地方
  359. .number-people {
  360. flex: 1;
  361. height: 100%;
  362. display: flex;
  363. background-color: #ece9fd;
  364. }
  365. .content-box {
  366. flex: 1;
  367. // margin-top: rpx(10);
  368. // margin-bottom: rpx(10);
  369. margin: rpx(7);
  370. border-radius: rpx(15);
  371. background: rgba($color: #ffffff, $alpha: 0.5);
  372. overflow-y: auto;
  373. }
  374. //左侧展览区图标
  375. .img-box {
  376. margin-top: rpx(50);
  377. color: #a39dce;
  378. }
  379. // 对话框
  380. .chat-dialog {
  381. display: flex;
  382. flex-direction: column;
  383. height: 100%;
  384. }
  385. .message-list {
  386. flex: 1;
  387. overflow-y: auto;
  388. padding: rpx(15);
  389. }
  390. /* 自定义滚动条样式 */
  391. .message-list::-webkit-scrollbar {
  392. width: rpx(2); /* 滚动条宽度 */
  393. }
  394. .message-list::-webkit-scrollbar-track {
  395. background: #f1effd; /* 滚动条轨道背景色 */
  396. border-radius: rpx(4);
  397. }
  398. .message-list::-webkit-scrollbar-thumb {
  399. background: #e2ddfc; /* 滚动条滑块颜色 */
  400. border-radius: rpx(4);
  401. }
  402. .message-list::-webkit-scrollbar-thumb:hover {
  403. background: #e2ddfc; /* 滚动条滑块 hover 状态颜色 */
  404. }
  405. .message-list .user-message {
  406. background-color: #ffffff;
  407. margin-left: auto; // 消息靠右显示
  408. margin-right: 0; // 重置右边距
  409. max-width: rpx(400);
  410. font-size: rpx(8);
  411. width: fit-content; // 宽度随文字内容变化
  412. border-radius: rpx(5);
  413. padding: rpx(5);
  414. text-align: left; // 文字左对齐
  415. }
  416. .message-list .ai-message {
  417. background-color: #ffdd55;
  418. margin-left: 0; // 消息靠左显示
  419. margin-right: auto; // 重置右边距
  420. margin-bottom: rpx(10);
  421. width: fit-content;
  422. max-width: rpx(400);
  423. padding: rpx(5);
  424. font-size: rpx(8);
  425. border-radius: rpx(5);
  426. text-align: left; // 文字左对齐
  427. }
  428. // 加载动画效果
  429. .loading-dots {
  430. display: inline-block;
  431. margin-left: rpx(5);
  432. }
  433. .loading-dots .dot {
  434. display: inline-block;
  435. width: rpx(3);
  436. height: rpx(3);
  437. border-radius: 50%;
  438. background-color: #333;
  439. margin: 0 rpx(1);
  440. animation: loading-dot 1.4s infinite ease-in-out both;
  441. }
  442. .loading-dots .dot:nth-child(1) {
  443. animation-delay: -0.32s;
  444. }
  445. .loading-dots .dot:nth-child(2) {
  446. animation-delay: -0.16s;
  447. }
  448. @keyframes loading-dot {
  449. 0%, 80%, 100% {
  450. transform: scale(0);
  451. }
  452. 40% {
  453. transform: scale(1);
  454. }
  455. }
  456. .image-list {
  457. display: flex;
  458. flex-wrap: wrap;
  459. }
  460. .content-demo {
  461. background-color: #f4f2fa;
  462. border-radius: 15px;
  463. padding: 30px 10px;
  464. }
  465. .input-section {
  466. display: flex;
  467. padding: rpx(10);
  468. gap: rpx(5);
  469. .speech-btn {
  470. padding: rpx(5) rpx(10);
  471. background: #fff;
  472. border: 1px solid #ffce1b;
  473. border-radius: rpx(5);
  474. cursor: pointer;
  475. display: flex;
  476. align-items: center;
  477. &.recording {
  478. background: #ffeeba;
  479. border-color: #ffc107;
  480. .el-icon {
  481. color: #dc3545;
  482. }
  483. }
  484. .el-icon {
  485. font-size: rpx(8);
  486. color: #666;
  487. }
  488. }
  489. // 终止按钮样式
  490. .stop-btn {
  491. cursor: pointer;
  492. display: flex;
  493. align-items: center;
  494. img {
  495. width: rpx(20);
  496. height: rpx(20);
  497. }
  498. }
  499. }
  500. .input-section input {
  501. flex: 1;
  502. padding: rpx(5);
  503. font-size: rpx(7);
  504. border: 1px solid #ccc;
  505. border-radius: rpx(5);
  506. }
  507. .input-section button {
  508. padding: rpx(5) rpx(15);
  509. background: linear-gradient(
  510. to bottom,
  511. #fee78a,
  512. #ffce1b
  513. ); /* 设置悬停、聚焦、点击状态下的背景色 */
  514. color: black;
  515. border: none;
  516. font-size: rpx(7);
  517. border-radius: rpx(5);
  518. cursor: pointer;
  519. box-shadow: 0 0px 2px rgba(0, 0, 0, 0.3);
  520. }
  521. </style>