ChatInput.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. <template>
  2. <div @click.stop="" class="fixed bottom-0 left-0 w-full bg-[#fff] pt-10 pb-30 z-52">
  3. <div :class="`flex ${inputValue.length > 13 ? 'items-end' : 'items-center'} px-15 pb-10`">
  4. <!-- @click="showVoice = !showVoice" -->
  5. <span
  6. v-if="!showVoice"
  7. :class="`iconfont icon-voice-one text-black-6 `"
  8. style="font-size: 32px"
  9. ></span>
  10. <van-icon @click="showVoice = !showVoice" v-else name="volume" size="22" />
  11. <div
  12. v-if="showVoice"
  13. @mousedown="startRecording"
  14. @mouseup="stopRecording"
  15. @touchstart="startRecording"
  16. @touchend="stopRecording"
  17. :class="`relative rounded-full bg-white justify-center flex-1 ml-12 mr-12 pl-5 pr-5 pt-4 pb-4 h-40 border flex items-center `"
  18. >
  19. {{ isRecording ? '松开结束' : '按住说话' }}
  20. </div>
  21. <div
  22. v-else
  23. style="overflow-y: scroll; -webkit-scrollbar-width: 0px; -webkit-scrollbar: none"
  24. class="box-border rounded-full bg-[#F3F3F3] justify-between px-16 py-8 flex-1 mx-12 min-h-40 border flex items-center"
  25. >
  26. <textarea
  27. v-model="inputValue"
  28. ref="textareaRef"
  29. placeholder="请输入"
  30. @focus="textareaFocus"
  31. @blur="handleBlur"
  32. class="ml-8 flex-1 box-border w-full h-full bg-[#F3F3F3]"
  33. maxlength="5000"
  34. style="height: 100%; resize: none; border: none; outline: none"
  35. ></textarea>
  36. <!-- @keydown.enter="addComment" -->
  37. </div>
  38. <span
  39. @click="openEmoji"
  40. class="iconfont icon-slightly-smiling-face text-black-6 mr-12"
  41. style="font-size: 32px"
  42. ></span>
  43. <span
  44. @click="openOther"
  45. class="iconfont icon-close-one text-black-6"
  46. style="font-size: 32px"
  47. ></span>
  48. </div>
  49. <div v-if="showEmoji" class="w-full h-300 bg-[#fff] overflow-auto">
  50. <div @click="closeEmojiBox" class="flex justify-end pr-15 text-black-9 text-sm">收起表情</div>
  51. <div class="flex items-center flex-wrap w-full px-8">
  52. <div
  53. v-for="(item, index) in emojiJson"
  54. :key="index"
  55. class="active:bg-[#ddd] text-4xl w-[10%] aspect-[1/1] flex items-center justify-center"
  56. >
  57. <div @click="selectEmoji(index)" v-html="item.emoji"></div>
  58. </div>
  59. </div>
  60. <div class="fixed bottom-45 right-20 flex">
  61. <div
  62. @click="delteMessage"
  63. class="text-sm py-5 px-16 mr-12 bg-[#FD9A00] text-[#fff] flex items-center justify-center rounded-full shrink-0"
  64. >
  65. 删除
  66. </div>
  67. <div
  68. @click="addComment"
  69. class="text-sm py-5 px-16 mr-12 bg-[#FD9A00] text-[#fff] flex items-center justify-center rounded-full shrink-0"
  70. >
  71. 发送
  72. </div>
  73. </div>
  74. </div>
  75. <div v-if="showOther" class="w-full h-200 bg-[#fff] overflow-auto">
  76. <div class="flex items-start flex-wrap w-full h-full py-15 px-8 bg-white">
  77. <template v-for="(item, index) in otherList" :key="index">
  78. <div
  79. @click="index == 0 ? () => {} : item.fn"
  80. v-if="item?.isShow"
  81. class="mx-10 text-4xl relative active:text-[#FF9300] w-[15%] aspect-[1/1] flex flex-wrap items-center justify-center"
  82. >
  83. <div
  84. class="w-54 h-54 active:bg-[#FF9300]/[0.1] bg-[#F3F3F3] shrink-0 rounded-full mb-5 overflow-auto flex justify-center items-center"
  85. >
  86. <span
  87. :class="`iconfont ${item.icon} text-black-6 active:text-[#FF9300] `"
  88. style="font-size: 32px"
  89. ></span>
  90. </div>
  91. <p class="text-sm w-full text-center">{{ item.title }}</p>
  92. <input
  93. type="file"
  94. @change="item.fn"
  95. class="absolute top-0 left-0 w-full h-full"
  96. style="position: absolute; top: 0; left: 0; z-index: 10; opacity: 0"
  97. />
  98. </div>
  99. </template>
  100. </div>
  101. </div>
  102. </div>
  103. </template>
  104. <script setup>
  105. import emojiJson from './emoji.js'
  106. const shareGroup = defineModel('shareGroup')
  107. const emit = defineEmits(['onSendMessage', 'onSelectImg'])
  108. const textareaRef = ref(null)
  109. // 显示输入框
  110. // const showInput = ref(true)
  111. // 是否展示表情
  112. const showEmoji = ref(true)
  113. // 是否展示其他功能
  114. const showOther = ref(false)
  115. // 是否展示语音
  116. const showVoice = ref(false)
  117. // 是否在录音
  118. const isRecording = ref(false)
  119. const transcript = ref('') // 存储语音识别结果
  120. // 输入的内容
  121. const inputValue = ref('')
  122. const addContent = ref(true)
  123. // 记录光标位置
  124. const cursorIndex = ref(0)
  125. // 创建语音识别实例
  126. let recognition = null
  127. // if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) {
  128. // recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)()
  129. // recognition.continuous = true // 连续识别
  130. // recognition.interimResults = true // 显示中间结果
  131. // recognition.lang = 'zh-CN' // 设置识别语言(中文)
  132. // // 监听识别结果
  133. // recognition.onresult = (event) => {
  134. // const lastResult = event.results[event.resultIndex]
  135. // if (lastResult.isFinal) {
  136. // transcript.value = lastResult[0].transcript // 获取最终结果
  137. // } else {
  138. // transcript.value = lastResult[0].transcript // 获取中间结果
  139. // }
  140. // }
  141. // // 错误处理
  142. // recognition.onerror = (event) => {
  143. // console.error('语音识别出错:', event.error)
  144. // isRecording.value = false
  145. // }
  146. // // 结束时重置状态
  147. // recognition.onend = () => {
  148. // isRecording.value = false
  149. // }
  150. // }
  151. // 启动录音
  152. const startRecording = () => {
  153. if (recognition) {
  154. // recognition?.start()
  155. isRecording.value = true
  156. }
  157. }
  158. // 停止录音
  159. const stopRecording = () => {
  160. if (recognition) {
  161. // recognition?.stop()
  162. isRecording.value = false
  163. }
  164. }
  165. const { open, onChange } = useFileDialog({
  166. accept: '.png,.png,.jpeg,.JPG,Png '
  167. })
  168. // 上传相册
  169. const uploadPictures = () => {
  170. open()
  171. }
  172. onChange(async (files) => {
  173. if (!files.length) return
  174. console.log(files[0])
  175. const formData = new FormData()
  176. formData.append('uploadFile', files[0])
  177. formData.append('asImage', true)
  178. formData.append('fieldName', 'messageContent')
  179. const maxSize = 10 * 1024 * 1024 // 10 MB
  180. if (files[0].size > maxSize) {
  181. showToast('上传图片过大,请重新上传')
  182. return
  183. } else {
  184. try {
  185. showLoadingToast({
  186. message: '图片上传中...',
  187. duration: 1000000
  188. })
  189. const { data } = await request('/website/tourMessage/upload', {
  190. method: 'post',
  191. body: formData
  192. })
  193. // form.image.push(data.fileUrl)
  194. closeToast()
  195. showToast('图片上传成功')
  196. emit('onSelectImg', data.fileUrl)
  197. } catch (error) {
  198. // form.image.push({
  199. // url: files[0].name,
  200. // status: 'failed',
  201. // isImage: true,
  202. // message: '上传失败',
  203. // imageFit: 'contain'
  204. // })
  205. closeToast()
  206. showToast('图片上传失败')
  207. console.log('图片上传失败')
  208. }
  209. }
  210. })
  211. // 分享群聊
  212. const shareGroupChat = () => {
  213. console.log('分享群聊')
  214. }
  215. // 按住说话 松开结束
  216. const changeShowVoiceing = () => {}
  217. const otherList = reactive([
  218. {
  219. title: '相册',
  220. icon: 'icon-pic',
  221. isShow: true,
  222. // fn: emit('onSelectImg')
  223. fn: uploadPictures
  224. },
  225. {
  226. title: '分享群聊',
  227. icon: 'icon-peoples-two',
  228. isShow: shareGroup.value,
  229. fn: shareGroupChat
  230. }
  231. ])
  232. // 转换评论中的一些非字符emoji
  233. // function coveredContent(val) {
  234. // if (!val) return ''
  235. // return val.replace(/\[.*?]/g, function (str) {
  236. // const baseApi = import.meta.env.VITE_APP_EMOJI_API
  237. // const emojiName = emojiJson[str]
  238. // console.log(emojiName, 'emojiName')
  239. // if (!emojiName) return str
  240. // return `<img src=${baseApi}${emojiJson[str]} style="width: 20px; height: 20px;display: inline-block; vertical-align: middle"/>`
  241. // })
  242. // }
  243. function addComment() {
  244. if (!inputValue.value.trim()) {
  245. showToast('发送内容不能为空!')
  246. return
  247. }
  248. emit('onSendMessage', inputValue.value)
  249. inputValue.value = ''
  250. }
  251. // 监听输入框的enter事件
  252. function addEventListenerTextarea() {
  253. nextTick(() => {
  254. if (textareaRef.value) {
  255. textareaRef.value.removeEventListener('keydown', () => {})
  256. textareaRef.value.addEventListener('keydown', function (event) {
  257. if (event.key === 'Enter') {
  258. event.preventDefault()
  259. emit('onSendMessage', inputValue.value)
  260. inputValue.value = ''
  261. }
  262. })
  263. }
  264. })
  265. }
  266. // 获取焦点
  267. function textareaFocus() {
  268. showEmoji.value = false
  269. // showInput.value = true
  270. textareaRef.value?.focus()
  271. }
  272. // 文本域失焦
  273. function handleBlur() {
  274. showEmoji.value = false
  275. showOther.value = false
  276. textareaRef.value?.blur()
  277. }
  278. // 打开表情
  279. function openEmoji() {
  280. showOther.value = false
  281. nextTick(() => {
  282. textareaRef.value.selectionStart && (cursorIndex.value = textareaRef.value.selectionStart)
  283. showEmoji.value = true
  284. })
  285. }
  286. // 打开上传图片或者其他的
  287. function openOther() {
  288. showEmoji.value = false
  289. showOther.value = true
  290. }
  291. // 收起表情
  292. function closeEmojiBox() {
  293. showEmoji.value = false
  294. nextTick(() => {
  295. textareaRef.value.focus()
  296. })
  297. }
  298. // 选择表情
  299. function selectEmoji(emojiStr = '') {
  300. const length = emojiStr.length
  301. inputValue.value =
  302. inputValue.value.slice(0, cursorIndex.value) +
  303. emojiJson[emojiStr].emoji +
  304. inputValue.value.slice(cursorIndex.value)
  305. nextTick(() => {
  306. cursorIndex.value += length
  307. textareaRef.value.setSelectionRange(cursorIndex.value, cursorIndex.value)
  308. })
  309. }
  310. // 删除内容
  311. const delteMessage = () => {
  312. inputValue.value = inputValue.value.slice(1, inputValue.value.length)
  313. }
  314. watch(() => {
  315. addEventListenerTextarea()
  316. })
  317. </script>
  318. <style lang="scss" scoped>
  319. .no-scrollbar::-webkit-scrollbar {
  320. width: 0;
  321. }
  322. </style>