online.vue 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. <template>
  2. <div
  3. tabindex="0"
  4. class="chat-online relative flex size-full flex-col items-center justify-center overflow-hidden rounded-lg bg-[url('@/assets/images/chat/bg-2.png')] bg-[length:100%_100%] bg-no-repeat"
  5. @focus="state.isFocus = true"
  6. @blur="state.isFocus = false"
  7. @mousedown.left="onSpeakStart"
  8. @mouseup.left="onSpeakStop"
  9. @keydown.space="onSpaceSpeakStart"
  10. @keyup.space="onSpaceSpeakStop"
  11. @keyup.enter="onCall"
  12. @keyup.esc="$emit('hangUp')"
  13. >
  14. <div
  15. class="absolute top-1/10 flex flex-col items-center gap-3 text-2xl font-bold text-[#303133]"
  16. >
  17. <img src="@/assets/images/app/app-default-logo.png" class="size-42" />
  18. 应用名称
  19. </div>
  20. <template v-if="!state.isWaiting">
  21. <div class="flex flex-col items-center">
  22. <div>
  23. <soundWave :volume="volumeCpt" :num="30" />
  24. </div>
  25. <div class="mt-4 h-7 text-lg font-bold text-[var(--czr-main-color)]">
  26. <template v-if="state.isAsk">正在聆听 {{ durationCpt }}</template>
  27. <template v-else-if="state.isThink">正在思考……</template>
  28. <template v-else-if="state.isAnswer">正在回答</template>
  29. <template v-else>等待提问中……</template>
  30. </div>
  31. <div class="mt-3 flex items-center text-sm text-[#909399]">
  32. <SvgIcon
  33. name="czr_tip"
  34. color="var(--czr-warning-color)"
  35. size="20"
  36. class="mr-1"
  37. />
  38. <template v-if="state.isAsk"> 松开鼠标左键/Space以提问 </template>
  39. <template v-else-if="state.isThink"> 正在思考中,请稍后…… </template>
  40. <template v-else-if="state.isAnswer"> 鼠标左键/Space以打断 </template>
  41. <template v-else> 按住鼠标左键/Space开始提问 </template>
  42. </div>
  43. </div>
  44. </template>
  45. <div class="absolute bottom-1/15 flex w-full justify-center gap-[20%]">
  46. <template v-if="state.isWaiting">
  47. <div
  48. class="__hover flex flex-col items-center gap-2 text-[#0064F9]"
  49. @click.stop="onCall"
  50. >
  51. <div class="relative flex size-38 items-center justify-center">
  52. <img src="@/assets/images/chat/chat-call.png" class="size-full" />
  53. <div
  54. class="absolute z-0 size-full scale-70 animate-ping rounded-full bg-[#0064F9]/20"
  55. style="animation-duration: 1.5s"
  56. ></div>
  57. </div>
  58. <div class="text-sm font-bold">接听</div>
  59. <div class="text-xs">Enter</div>
  60. </div>
  61. </template>
  62. <div
  63. class="__hover flex flex-col items-center gap-2 text-[#FF5454]"
  64. @click.stop="$emit('hangUp')"
  65. >
  66. <div class="relative flex size-38 items-center justify-center">
  67. <img src="@/assets/images/chat/chat-hang.png" class="size-full" />
  68. </div>
  69. <div class="text-sm font-bold">挂断</div>
  70. <div class="text-xs">Esc</div>
  71. </div>
  72. </div>
  73. </div>
  74. </template>
  75. <script setup lang="ts">
  76. import { computed, reactive } from 'vue'
  77. import soundWave from './audio/sound-wave.vue'
  78. import useSpeechToAudio from '@/views/chat/audio/useSpeechToAudio'
  79. import { ElMessage } from 'element-plus'
  80. import useAudio from '@/views/chat/audio/useAudio'
  81. const emit = defineEmits(['hangUp'])
  82. const state: any = reactive({
  83. isWaiting: true, // 等待接听
  84. isAsk: false, // 提问录音中
  85. isAnswer: false, // 聆听回答中
  86. isThink: false, // 思考回答中
  87. isFocus: false, // 语音聊天是否聚焦
  88. duration: 0, // 提问录音时长
  89. timer: null, // 开始提问防抖
  90. clickSpace: false, // 按下空格会一直触发,添加个判断
  91. answerVolume: 0, //回答音频的音量
  92. answerAudioControl: null,
  93. })
  94. const durationCpt = computed(() => {
  95. const minutes = Math.floor(state.duration / 60)
  96. const seconds = Math.floor(state.duration % 60)
  97. return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
  98. })
  99. const onSpeak = ({ duration }) => {
  100. state.isAsk = true
  101. state.duration = duration
  102. }
  103. const onEnd = (audio) => {
  104. state.isAsk = false
  105. if (state.duration < 2) {
  106. ElMessage.warning('提问时长过短,请持续提问!')
  107. return
  108. }
  109. console.log(audio)
  110. state.isThink = true
  111. setTimeout(() => {
  112. state.isThink = false
  113. useAudio(
  114. // 'https://lf-bot-studio-plugin-resource.coze.cn/obj/bot-studio-platform-plugin-tos/sami_podcast/tts/134d9d71475043199ef7372567fa9689.mp3',
  115. 'https://lf-bot-studio-plugin-resource.coze.cn/obj/bot-studio-platform-plugin-tos/artist/image/32afe00820aa466192ce0f7b6495e946.mp3',
  116. () => {
  117. state.isAnswer = false
  118. },
  119. ).then(({ volume, play, stop, pause }: any) => {
  120. state.isAnswer = true
  121. state.answerAudioControl = {
  122. play,
  123. stop,
  124. pause,
  125. }
  126. state.answerVolume = volume
  127. play()
  128. })
  129. }, 1000)
  130. }
  131. const { speak, stop, volume } = useSpeechToAudio({
  132. onEnd,
  133. onSpeak,
  134. timeout: 0,
  135. })
  136. const volumeCpt = computed(() => {
  137. if (state.isAsk) {
  138. return volume.value
  139. }
  140. if (state.isAnswer) {
  141. return state.answerVolume
  142. }
  143. return 0
  144. })
  145. const onCall = () => {
  146. if (state.isWaiting) {
  147. state.isWaiting = false
  148. state.isAsk = false
  149. state.isThink = false
  150. state.isAnswer = false
  151. }
  152. }
  153. const onSpeakStart = () => {
  154. if (!state.isWaiting && !state.isAsk && !state.clickSpace) {
  155. state.timer = setTimeout(() => {
  156. if (state.isAnswer) {
  157. onStopAnswer()
  158. }
  159. speak()
  160. }, 300)
  161. }
  162. }
  163. const onSpeakStop = () => {
  164. clearTimeout(state.timer)
  165. if (!state.isWaiting && state.isAsk) {
  166. stop()
  167. }
  168. }
  169. const onSpaceSpeakStart = () => {
  170. if (!state.isThink) {
  171. onSpeakStart()
  172. state.clickSpace = true
  173. }
  174. }
  175. const onSpaceSpeakStop = () => {
  176. onSpeakStop()
  177. state.clickSpace = false
  178. }
  179. const onStopAnswer = () => {
  180. state.isAnswer = false
  181. state.answerAudioControl?.stop()
  182. }
  183. </script>
  184. <style lang="scss" scoped>
  185. .chat-online {
  186. * {
  187. outline: none;
  188. }
  189. }
  190. </style>