consult.vue 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. <script setup lang="ts">
  2. import type { type_dyaw_xlfw_zxhd, type_dyaw_xlfw_zxhd_log } from '~/types';
  3. import user from '~/store/user';
  4. import { createSocket, socketSend } from '~/utils/ws';
  5. import type { TSocketRes } from '~/utils/ws';
  6. import { formatTimestamp } from '~/utils/time'
  7. import { CHAT_STATUS, CHAT_OPERATION } from '~/types';
  8. const router = useRouter()
  9. let teacher
  10. const SessionConsultTeacher = sessionStorage.getItem('consult_teacher')
  11. if (SessionConsultTeacher !== null) {
  12. teacher = JSON.parse(SessionConsultTeacher)
  13. } else {
  14. router.back()
  15. }
  16. function formatter(e: string) {
  17. if (!e) return e
  18. // 转义字符串中的危险字符
  19. return e.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
  20. }
  21. let teacherInfo = $ref<{
  22. dxp_user_realname: string;
  23. dxp_user_phone: string;
  24. dxp_wx_qrcode: string;
  25. dxp_jj: string;
  26. dxp_user_avatar: string;
  27. }>()
  28. request({
  29. url: '/dyaw/xlfw_pbgl/detail',
  30. data: {
  31. user_id: teacher.user_id
  32. }
  33. }).then(res => {
  34. if (res.code === '1') {
  35. teacherInfo = res.data.one_info
  36. }
  37. })
  38. const dyaw_xlfw_zxhd: type_dyaw_xlfw_zxhd = (await request({
  39. url: '/dyaw/xlfw_zxhd/add',
  40. data: {
  41. dyaw_xlfw_zxhd: {
  42. dxz_stu_user_id: user.user_id,
  43. dxz_tea_user_id: teacher.user_id
  44. }
  45. }
  46. })).data.one_info
  47. let infoDialogVisible = $ref(false)
  48. let rateDialogVisible = $ref(false)
  49. let endTime = $ref<string>()
  50. let rateNum = $ref(5)
  51. function handleClickEnd() {
  52. // ChatAudioRef && ChatAudioRef.open()
  53. endTime = getDatabaseTime(new Date())
  54. rateDialogVisible = true
  55. }
  56. function handleConfirmRate() {
  57. request({
  58. url: '/dyaw/xlfw_zxhd/edit',
  59. data: {
  60. dxz_id: dyaw_xlfw_zxhd.dxz_id,
  61. dyaw_xlfw_zxhd: {
  62. dxz_star: rateNum,
  63. dxz_star_datetime: endTime,
  64. // 下行代码【可能】导致使用问题
  65. dxz_status: '1'
  66. }
  67. }
  68. }).then(res => {
  69. if (res.code === '1') {
  70. rateDialogVisible = false
  71. router.back()
  72. }
  73. })
  74. }
  75. let infoList = $ref<Array<type_dyaw_xlfw_zxhd_log>>([])
  76. let inputValue = $ref('')
  77. let isSending = $ref(false)
  78. let TinyRef = $ref<typeof import('~/components/tinymce-area/index.vue')['default']>()
  79. async function handleClickSend(val?: string) {
  80. if (isSending) return;
  81. isSending = true
  82. const reqDate = {
  83. dxz_id: dyaw_xlfw_zxhd.dxz_id,
  84. dxzl_stu_user_id: dyaw_xlfw_zxhd.dxz_stu_user_id,
  85. dxzl_stu_user_realname: dyaw_xlfw_zxhd.dxz_stu_user_realname,
  86. dxzl_tea_user_id: dyaw_xlfw_zxhd.dxz_tea_user_id,
  87. dxzl_tea_user_realname: dyaw_xlfw_zxhd.dxz_tea_user_realname,
  88. dxzl_last_msg_content: encodeURIComponent(val || formatter(inputValue)),
  89. dxzl_type: (val || formatter(inputValue)).includes('<img') ? '2' : '1'
  90. }
  91. // infoList.push({
  92. // create_user_id: user.user_id,
  93. // create_dateline: Date.now().toString().slice(0, 10),
  94. // ...reqDate
  95. // })
  96. TinyRef?.clear()
  97. // console.log('inputValue :>> ', inputValue);
  98. request({
  99. url: '/dyaw/xlfw_zxhd_log/add',
  100. data: {
  101. dyaw_xlfw_zxhd_log: reqDate
  102. }
  103. }).then(res => {
  104. if (res.code === '1') {
  105. const fullSendData = {
  106. create_user_id: user.user_id,
  107. create_dateline: Date.now().toString().slice(0, 10),
  108. ...reqDate,
  109. dxzl_id: `${res.data.insert_id}`
  110. }
  111. infoList.push(fullSendData)
  112. socketSend(ws, fullSendData)
  113. isSending = false
  114. }
  115. })
  116. }
  117. request({
  118. url: '/dyaw/xlfw_zxhd_log/index',
  119. data: {
  120. dxz_id: dyaw_xlfw_zxhd.dxz_id,
  121. limit: 100
  122. }
  123. }).then(res => {
  124. if (res.code === '1') {
  125. infoList = res.data.page_data.reverse()
  126. }
  127. })
  128. const ws = createSocket(
  129. { teacher: teacher.user_id, student: user.user_id },
  130. {
  131. message(socketRes: TSocketRes<type_dyaw_xlfw_zxhd_log>) {
  132. if (socketRes.from_client_name.endsWith('teacher')) {
  133. infoList.push(socketRes.content)
  134. }
  135. }
  136. }
  137. )
  138. // console.log('ws :>> ', ws);
  139. if (!dyaw_xlfw_zxhd.old_data) {
  140. socketSend(ws, {
  141. $: true,
  142. dxz_stu_user_id: dyaw_xlfw_zxhd.dxz_stu_user_id,
  143. dyaw_xlfw_zxhd
  144. })
  145. }
  146. const scrollbarRef = $ref<HTMLElement>()
  147. function scrollToBottom() {
  148. if (!scrollbarRef) return;
  149. const scrollHeight = scrollbarRef!.scrollHeight;
  150. scrollbarRef!.scrollTo(0, scrollHeight);
  151. }
  152. watch(
  153. () => (infoList),
  154. () => {
  155. nextTick(() => {
  156. scrollToBottom()
  157. })
  158. },
  159. {
  160. deep: true,
  161. }
  162. )
  163. // ==========
  164. // chat audio/video
  165. // ==========
  166. let RtcDialogRef = $ref<typeof import("~/components/rtc-dialog/index.vue")['default']>()
  167. const ws2 = createSocket(
  168. { teacher: teacher.user_id, student: '*' },
  169. {
  170. message(socketRes: TSocketRes<type_dyaw_xlfw_zxhd_log & { operate: CHAT_OPERATION }>) {
  171. console.log('enter', socketRes);
  172. if (socketRes.from_client_name.endsWith('teacher')) {
  173. if (socketRes.content.dxzl_stu_user_id === user.user_id) {
  174. RtcDialogRef!.publisher(socketRes.content)
  175. }
  176. }
  177. }
  178. }
  179. )
  180. onMounted(() => {
  181. RtcDialogRef!.init(ws2)
  182. })
  183. async function handleAudioChatStart() {
  184. RtcDialogRef!.open(dyaw_xlfw_zxhd, 'audio')
  185. }
  186. async function handleVideoChatStart() {
  187. RtcDialogRef!.open(dyaw_xlfw_zxhd, 'video')
  188. }
  189. function emitUpdateInfo(info: type_dyaw_xlfw_zxhd_log, isUpdate?: boolean) {
  190. if (!isUpdate) {
  191. if (info.dxz_id === dyaw_xlfw_zxhd?.dxz_id)
  192. infoList.push(info)
  193. }
  194. else {
  195. const target = infoList.find(item => item.dxzl_id === info.dxzl_id)
  196. if (target)
  197. target.dxzl_last_msg_content = info.dxzl_last_msg_content
  198. }
  199. }
  200. // 从⼼灵氧吧/梦⾥寻芳/微⻛细⾬/花⾹⻦语获取随机昵称
  201. function getRandomName() {
  202. const names = ['⼼灵氧吧', '梦⾥寻芳', '微⻛细⾬', '花⾹⻦语']
  203. return names[Math.floor(Math.random() * names.length)]
  204. }
  205. const virtualName = getRandomName()
  206. </script>
  207. <template>
  208. <div class="h-full flex justify-center divide-x">
  209. <div class="w-full h-full divide-y flex flex-col relative">
  210. <van-nav-bar :title="`${dyaw_xlfw_zxhd.dxz_tea_user_realname}`" @click-left="infoDialogVisible = true"
  211. right-text="结束咨询" @click-right="handleClickEnd"
  212. style="--van-nav-bar-background:#397FF6;--van-nav-bar-icon-color:#fff;--van-nav-bar-title-text-color:#fff;--van-nav-bar-title-font-size:18px;--van-nav-bar-text-color:#fff;">
  213. <template #left>
  214. <van-icon name="wap-nav" size="18" />
  215. </template>
  216. </van-nav-bar>
  217. <div ref="scrollbarRef"
  218. class="bg-hex-ededed space-y-2 flex-auto py-2 px-6 scrollbar scrollbar-thin scrollbar-thumb-rounded-md scrollbar-thumb-gray-200 scrollbar-track-transparent relative">
  219. <div ref="scrollContainRef" class="scrollContainRef space-y-2">
  220. <info-item v-for="item in infoList" :key="item.dxzl_id" :left="item.create_user_id !== user.user_id" :d="item"
  221. :w="300" :virtualName="virtualName"></info-item>
  222. </div>
  223. </div>
  224. <div class="bg-hex-e4e6eb p-5px flex justify-between space-x-2 items-end">
  225. <tinymce-area-m v-model="inputValue" ref="TinyRef" @click:audio="handleAudioChatStart" class="flex-auto"
  226. @click:video="handleVideoChatStart" @click:submit="handleClickSend"></tinymce-area-m>
  227. <van-button type="primary" @click="handleClickSend()">发送</van-button>
  228. </div>
  229. </div>
  230. </div>
  231. <rtc-dialog ref="RtcDialogRef" @update-info="emitUpdateInfo"></rtc-dialog>
  232. <van-dialog v-model:show="infoDialogVisible" title="" @confirm="infoDialogVisible = false">
  233. <div class="space-y-4 bg-white flex flex-col items-stretch px-30px py-4 divide-y">
  234. <template v-if="teacherInfo">
  235. <div class="w-full min-h-48">
  236. <img :src="teacherInfo.dxp_wx_qrcode" alt="">
  237. </div>
  238. <div class="py-1 space-y-2">
  239. <div class="text-2xl font-bold max-w-24 truncate">{{ teacherInfo.dxp_user_realname }}</div>
  240. <div class="text-hex-5B9FF0">联系电话:{{ teacherInfo.dxp_user_phone }}</div>
  241. <div class=" text-hex-00000041 h-10 leading-5">
  242. {{ teacherInfo.dxp_jj }}
  243. </div>
  244. </div>
  245. </template>
  246. </div>
  247. </van-dialog>
  248. <van-dialog v-model:show="rateDialogVisible" title="请评价" @confirm="handleConfirmRate" show-cancel-button>
  249. <div class="flex flex-col py-6 px-10 space-y-4">
  250. <p>于 {{ endTime }} 结束沟通</p>
  251. <div>
  252. <div>评价:</div>
  253. <van-rate :size="25" v-model="rateNum"></van-rate>
  254. </div>
  255. </div>
  256. </van-dialog>
  257. <rtc-dialog ref="RtcDialogRef" @update-info="emitUpdateInfo"></rtc-dialog>
  258. <!-- <chat-dialog ref="ChatVideoRef">
  259. <i:ant-design:audio-outlined />
  260. <i:ant-design:audio-muted-outlined />
  261. <i:material-symbols:video-camera-back-rounded />
  262. <i:material-symbols:video-camera-front-off-rounded />
  263. </chat-dialog> -->
  264. </template>
  265. <style scoped lang="scss">
  266. </style>