chat-list.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. <script setup lang="ts">
  2. import AgoraRTC from "agora-rtc-sdk-ng"
  3. import type { IAgoraRTCClient, IMicrophoneAudioTrack, ICameraVideoTrack } from "agora-rtc-sdk-ng"
  4. import type { type_dyaw_xlfw_zxhd, type_dyaw_xlfw_zxhd_log, type_archives_item } from '~/types';
  5. import { Search } from '@element-plus/icons-vue'
  6. // import { ElMessage, ElMessageBox } from 'element-plus'
  7. import user from '~/store/user';
  8. import { createSocket, socketSend } from '~/utils/ws';
  9. import type { TSocketRes } from '~/utils/ws';
  10. import { formatTimestamp2Date } from '~/utils/time';
  11. import { CHAT_STATUS, CHAT_OPERATION } from '~/types';
  12. import { showConfirmDialog, showSuccessToast, showFailToast } from 'vant';
  13. let dyaw_xlfw_zxhd = $ref<type_dyaw_xlfw_zxhd>(JSON.parse(sessionStorage.getItem('dyaw_xlfw_zxhd')!))
  14. let dyaw_xlfw_zxhd_list = $ref<type_dyaw_xlfw_zxhd[] | undefined>()
  15. const emits = defineEmits<{
  16. (event: 'openRtcDialog', dyaw_xlfw_zxhd: type_dyaw_xlfw_zxhd, type: 'audio' | 'video'): void;
  17. }>()
  18. const props = defineProps<{ updateFnList: Function[] }>()
  19. function formatter(e: string) {
  20. if (!e) return e
  21. // 转义字符串中的危险字符
  22. return e.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
  23. }
  24. let infoList = $ref<Array<type_dyaw_xlfw_zxhd_log>>([])
  25. let inputValue = $ref('')
  26. let isSending = $ref(false)
  27. let TinyRef = $ref<typeof import('~/components/tinymce-area/index.vue')['default']>()
  28. async function handleClickSend(val?: string) {
  29. if (!dyaw_xlfw_zxhd) return;
  30. if (isSending) return;
  31. isSending = true
  32. const reqDate = {
  33. dxz_id: dyaw_xlfw_zxhd.dxz_id,
  34. dxzl_stu_user_id: dyaw_xlfw_zxhd.dxz_stu_user_id,
  35. dxzl_stu_user_realname: dyaw_xlfw_zxhd.dxz_stu_user_realname,
  36. dxzl_tea_user_id: dyaw_xlfw_zxhd.dxz_tea_user_id,
  37. dxzl_tea_user_realname: dyaw_xlfw_zxhd.dxz_tea_user_realname,
  38. dxzl_last_msg_content: encodeURIComponent(val || formatter(inputValue)),
  39. dxzl_type: (val || formatter(inputValue)).includes('<img') ? '2' : '1'
  40. }
  41. // const reqDate = {
  42. // dxz_id: dyaw_xlfw_zxhd.dxz_id,
  43. // dxzl_stu_user_id: dyaw_xlfw_zxhd.dxz_stu_user_id,
  44. // dxzl_stu_user_realname: dyaw_xlfw_zxhd.dxz_stu_user_realname,
  45. // dxzl_tea_user_id: dyaw_xlfw_zxhd.dxz_tea_user_id,
  46. // dxzl_tea_user_realname: dyaw_xlfw_zxhd.dxz_tea_user_realname,
  47. // dxzl_last_msg_content: encodeURIComponent(inputValue),
  48. // dxzl_type: inputValue.includes('><img') ? '2' : '1'
  49. // }
  50. // infoList.push({
  51. // create_user_id: user.user_id,
  52. // create_dateline: Date.now().toString().slice(0, 10),
  53. // ...reqDate
  54. // })
  55. TinyRef?.clear()
  56. // console.log('inputValue :>> ', inputValue);
  57. request({
  58. url: '/dyaw/xlfw_zxhd_log/add',
  59. data: {
  60. dyaw_xlfw_zxhd_log: reqDate
  61. }
  62. }).then(res => {
  63. if (res.code === '1') {
  64. const fullSendData = {
  65. create_user_id: user.user_id,
  66. create_dateline: Date.now().toString().slice(0, 10),
  67. ...reqDate,
  68. dxzl_id: `${res.data.insert_id}`
  69. }
  70. if (!ws||ws.readyState !== 1) {
  71. ws = createSocket(
  72. { teacher: user.user_id, student: dyaw_xlfw_zxhd.dxz_stu_user_id },
  73. {
  74. message(socketRes: TSocketRes<type_dyaw_xlfw_zxhd_log & { $?: boolean, dxz_stu_user_id?: string, dyaw_xlfw_zxhd: type_dyaw_xlfw_zxhd }>) {
  75. if (socketRes.from_client_name.endsWith('student')) {
  76. if (socketRes.content.$) {
  77. if (socketRes.content?.dxz_stu_user_id === dyaw_xlfw_zxhd?.dxz_stu_user_id) {
  78. dyaw_xlfw_zxhd = socketRes.content.dyaw_xlfw_zxhd
  79. }
  80. return;
  81. }
  82. infoList.push(socketRes.content)
  83. request({
  84. url: '/dyaw/xlfw_zxhd_log/index',
  85. data: {
  86. dxzl_stu_user_id: dyaw_xlfw_zxhd.dxz_stu_user_id,
  87. dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
  88. limit: 0
  89. }
  90. })
  91. }
  92. }
  93. }
  94. )
  95. }
  96. infoList.push(fullSendData)
  97. socketSend(ws!, fullSendData)
  98. isSending = false
  99. }
  100. })
  101. }
  102. let ws = $ref<WebSocket>()
  103. let cardLoading = $ref(true)
  104. let unreadNum = $ref(0)
  105. // function getAllUnreadMsg() {
  106. // request({
  107. // url: '/dyaw/xlfw_zxhd_log/index',
  108. // data: {
  109. // dxzl_stu_user_id: dyaw_xlfw_zxhd?.dxz_stu_user_id,
  110. // dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
  111. // limit: 1
  112. // }
  113. // }).then(res => {
  114. // if (res.code === '1') {
  115. // ifScroll = false
  116. // infoList = (res.data.page_data.reverse())
  117. // unreadNum = 0
  118. // }
  119. // nextTick(() => {
  120. // ifScroll = true
  121. // // scrollbarRef!.scrollTo(0, 0);
  122. // })
  123. // })
  124. // }
  125. // function handleClickStuCard(stu: type_dyaw_xlfw_zxhd) {
  126. // cardLoading = true
  127. // dyaw_xlfw_zxhd = stu
  128. // unreadNum = parseInt(stu.dxz_unread_msg_num)
  129. // stu.dxz_unread_msg_num = "0"
  130. // archivesList = []
  131. // ws?.close()
  132. // request({
  133. // url: '/dyaw/xlfw_zxhd_log/index',
  134. // data: {
  135. // dxzl_stu_user_id: stu.dxz_stu_user_id,
  136. // dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
  137. // }
  138. // }).then(res => {
  139. // if (res.code === '1') {
  140. // infoList = res.data.page_data.reverse()
  141. // cardLoading = false
  142. // ws = createSocket(
  143. // { teacher: user.user_id, student: stu.dxz_stu_user_id },
  144. // {
  145. // message(socketRes: TSocketRes<type_dyaw_xlfw_zxhd_log & { $?: boolean, dxz_stu_user_id?: string, dyaw_xlfw_zxhd: type_dyaw_xlfw_zxhd }>) {
  146. // if (socketRes.from_client_name.endsWith('student')) {
  147. // if (socketRes.content.$) {
  148. // if (socketRes.content?.dxz_stu_user_id === dyaw_xlfw_zxhd?.dxz_stu_user_id) {
  149. // dyaw_xlfw_zxhd = socketRes.content.dyaw_xlfw_zxhd
  150. // }
  151. // return;
  152. // }
  153. // infoList.push(socketRes.content)
  154. // request({
  155. // url: '/dyaw/xlfw_zxhd_log/index',
  156. // data: {
  157. // dxzl_stu_user_id: stu.dxz_stu_user_id,
  158. // dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
  159. // limit: 0
  160. // }
  161. // })
  162. // }
  163. // }
  164. // }
  165. // )
  166. // handleQueryArchives()
  167. // }
  168. // })
  169. // }
  170. unreadNum = parseInt(dyaw_xlfw_zxhd.dxz_unread_msg_num)
  171. dyaw_xlfw_zxhd.dxz_unread_msg_num = "0"
  172. request({
  173. url: '/dyaw/xlfw_zxhd_log/index',
  174. data: {
  175. dxzl_stu_user_id: dyaw_xlfw_zxhd.dxz_stu_user_id,
  176. dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
  177. }
  178. }).then(res => {
  179. if (res.code === '1') {
  180. infoList = res.data.page_data.reverse()
  181. cardLoading = false
  182. ws = createSocket(
  183. { teacher: user.user_id, student: dyaw_xlfw_zxhd.dxz_stu_user_id },
  184. {
  185. message(socketRes: TSocketRes<type_dyaw_xlfw_zxhd_log & { $?: boolean, dxz_stu_user_id?: string, dyaw_xlfw_zxhd: type_dyaw_xlfw_zxhd }>) {
  186. if (socketRes.from_client_name.endsWith('student')) {
  187. if (socketRes.content.$) {
  188. if (socketRes.content?.dxz_stu_user_id === dyaw_xlfw_zxhd?.dxz_stu_user_id) {
  189. dyaw_xlfw_zxhd = socketRes.content.dyaw_xlfw_zxhd
  190. }
  191. return;
  192. }
  193. infoList.push(socketRes.content)
  194. request({
  195. url: '/dyaw/xlfw_zxhd_log/index',
  196. data: {
  197. dxzl_stu_user_id: dyaw_xlfw_zxhd.dxz_stu_user_id,
  198. dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
  199. limit: 0
  200. }
  201. })
  202. }
  203. }
  204. }
  205. )
  206. handleQueryArchives()
  207. }
  208. })
  209. let ifScroll = $ref(true)
  210. function handleLoadMoreInfo() {
  211. request({
  212. url: '/dyaw/xlfw_zxhd_log/index',
  213. data: {
  214. dxzl_stu_user_id: dyaw_xlfw_zxhd?.dxz_stu_user_id,
  215. dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
  216. zxhd_log_id: infoList[0].dxzl_id
  217. }
  218. }).then(res => {
  219. if (res.code === '1') {
  220. ifScroll = false
  221. if (res.data.page_data.length === 0) return showFailToast('暂无更多消息');
  222. infoList.unshift(...res.data.page_data.reverse())
  223. }
  224. })
  225. }
  226. const scrollbarRef = $ref<HTMLElement>()
  227. const scrollContainRef = $ref<HTMLElement>()
  228. function scrollToBottom() {
  229. if (!scrollbarRef) return;
  230. const scrollHeight = scrollContainRef!.scrollHeight;
  231. // console.log('scrollHeight : ', scrollHeight)
  232. scrollbarRef!.scrollTo(0, scrollHeight + 200);
  233. }
  234. watch(
  235. () => (infoList),
  236. () => {
  237. nextTick(() => {
  238. ifScroll && scrollToBottom();
  239. ifScroll = true
  240. })
  241. },
  242. {
  243. deep: true,
  244. }
  245. )
  246. const ArchivesCardRef = $ref<typeof import('~/components/archives-card/index.vue')['default']>()
  247. function handleSubmitArchives() {
  248. showConfirmDialog({ message: '一次咨询只能提交一次档案,请确认完毕后点击提交', title: '提示' })
  249. .then(() => {
  250. request({
  251. url: '/dyaw/xlfw_xsda_dajl/add',
  252. data: {
  253. dyaw_xlfw_xsda_dajl: ArchivesCardRef!.form
  254. }
  255. }).then(res => {
  256. if (res.code === '1') {
  257. showSuccessToast('提交成功');
  258. handleQueryArchives()
  259. } else {
  260. showFailToast('提交失败');
  261. }
  262. })
  263. })
  264. .catch(() => {
  265. showFailToast('取消提交');
  266. })
  267. }
  268. let archivesList = $ref<type_archives_item[]>([])
  269. function handleQueryArchives() {
  270. request({
  271. url: '/dyaw/xlfw_xsda_dajl/index',
  272. data: {
  273. user_id: dyaw_xlfw_zxhd!.dxz_stu_user_id
  274. }
  275. }).then(res => {
  276. if (res.code === '1') {
  277. archivesList = res.data.page_data
  278. }
  279. })
  280. }
  281. // ==========
  282. // chat audio/video
  283. // ==========
  284. // let RtcDialogRef = $ref<typeof import("~/components/rtc-dialog/index.vue")['default']>()
  285. // const ws2 = createSocket(
  286. // { teacher: user.user_id, student: '*' },
  287. // {
  288. // message(socketRes: TSocketRes<type_dyaw_xlfw_zxhd_log & { operate: CHAT_OPERATION }>) {
  289. // if (socketRes.from_client_name.endsWith('student')) {
  290. // // infoList.push(socketRes.content)
  291. // if (socketRes.content.dxzl_tea_user_id === user.user_id) {
  292. // console.log('RtcDialogRef : ', RtcDialogRef)
  293. // RtcDialogRef!.publisher(socketRes.content)
  294. // }
  295. // }
  296. // }
  297. // }
  298. // )
  299. // onMounted(() => {
  300. // RtcDialogRef!.init(ws2)
  301. // })
  302. async function handleAudioChatStart() {
  303. // RtcDialogRef!.open(dyaw_xlfw_zxhd, 'audio')
  304. emits('openRtcDialog', dyaw_xlfw_zxhd, 'audio')
  305. }
  306. async function handleVideoChatStart() {
  307. // RtcDialogRef!.open(dyaw_xlfw_zxhd, 'video')
  308. emits('openRtcDialog', dyaw_xlfw_zxhd, 'video')
  309. }
  310. function emitUpdateInfo(info: type_dyaw_xlfw_zxhd_log, isUpdate?: boolean) {
  311. if (!isUpdate) {
  312. if (info.dxz_id === dyaw_xlfw_zxhd?.dxz_id)
  313. infoList.push(info)
  314. }
  315. else {
  316. const target = infoList.find(item => (item.dxzl_id)?.toString() === info.dxzl_id?.toString())
  317. if (target)
  318. target.dxzl_last_msg_content = info.dxzl_last_msg_content
  319. }
  320. }
  321. props.updateFnList.push(emitUpdateInfo)
  322. onBeforeUnmount(() => {
  323. props.updateFnList.splice(0)
  324. ws?.close()
  325. })
  326. const router = useRouter()
  327. function onClickLeft() {
  328. router.back()
  329. }
  330. let showRightArchives = $ref(false)
  331. </script>
  332. <template>
  333. <div class="h-full flex justify-center divide-x">
  334. <div class="w-full h-full divide-y flex flex-col relative">
  335. <template v-if="dyaw_xlfw_zxhd">
  336. <van-nav-bar
  337. :title="`${dyaw_xlfw_zxhd.dxz_stu_user_realname} ${dyaw_xlfw_zxhd.dxz_stu_school_name} ${dyaw_xlfw_zxhd.dxz_class_name}`"
  338. left-text="" left-arrow @click-left="onClickLeft" right-text="学生档案" @click-right="showRightArchives = true"
  339. 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;" />
  340. <div ref="scrollbarRef"
  341. 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">
  342. <div ref="scrollContainRef" class="scrollContainRef space-y-2">
  343. <div v-show="!cardLoading" @click="handleLoadMoreInfo"
  344. class="w-full text-center text-sm text-blue-400 hover:underline underline-blue-400 cursor-pointer">查看更多
  345. </div>
  346. <info-item v-for="item in infoList" :key="item.dxzl_id" :left="item.create_user_id !== user.user_id" :d="item"
  347. :w="300"></info-item>
  348. </div>
  349. </div>
  350. <div class="bg-hex-e4e6eb p-5px flex justify-between space-x-2 items-end">
  351. <tinymce-area-m v-model="inputValue" ref="TinyRef" @click:audio="handleAudioChatStart" class="flex-auto"
  352. @click:video="handleVideoChatStart" @click:submit="handleClickSend"></tinymce-area-m>
  353. <van-button type="primary" @click="handleClickSend()">发送</van-button>
  354. </div>
  355. </template>
  356. <template v-else>
  357. <div class="bg-pink-300 flex justify-between h-48px items-center px-18px"></div>
  358. <div class="bg-hex-fff8fb flex-auto w-full flex_center">
  359. <el-empty description="请先选择聊天的学生"></el-empty>
  360. </div>
  361. </template>
  362. </div>
  363. <van-popup v-model:show="showRightArchives" position="right" :style="{ width: '100vw', height: '100vh' }">
  364. <div
  365. class="w-full h-full bg-white overflow-y-auto flex flex-col items-center px-2 py-2 divide-y scrollbar scrollbar-thin scrollbar-thumb-rounded-md scrollbar-thumb-gray-200 scrollbar-track-transparent">
  366. <div v-if="dyaw_xlfw_zxhd" :key="dyaw_xlfw_zxhd.dxz_stu_user_id">
  367. <van-sticky :offset-top="8">
  368. <div class="mb-4 flex justify-end">
  369. <van-button @click="showRightArchives = false" type="primary">返回</van-button>
  370. </div>
  371. </van-sticky>
  372. <div class="w-full"
  373. v-if="archivesList && (archivesList.length === 0 || dyaw_xlfw_zxhd.dxz_id !== archivesList[0].dxz_id)">
  374. <archives-card storage ref="ArchivesCardRef"
  375. :d="{ dxz_id: dyaw_xlfw_zxhd.dxz_id, user_id: dyaw_xlfw_zxhd.dxz_stu_user_id, dxxd_lfzxm: dyaw_xlfw_zxhd.dxz_stu_user_realname, dxxd_jfls: dyaw_xlfw_zxhd.dxz_tea_user_realname, dxxd_school_name: dyaw_xlfw_zxhd.dxz_stu_school_name, dxxd_class_name: dyaw_xlfw_zxhd.dxz_class_name, dxxd_date: formatTimestamp2Date(dyaw_xlfw_zxhd.create_dateline) }"></archives-card>
  376. <div class="flex_center py-2">
  377. <van-button @click="handleSubmitArchives" type="success" size="large">提交</van-button>
  378. </div>
  379. </div>
  380. <div class=" py-2 space-y-1">
  381. <div class="flex_center text-lg">历史档案</div>
  382. <el-empty v-show="archivesList.length === 0" :image-size="60" description="暂无历史档案"></el-empty>
  383. <archives-card disabled v-for="item in archivesList" :d="item" :key="item.dxxd_id"></archives-card>
  384. </div>
  385. </div>
  386. </div>
  387. </van-popup>
  388. </div>
  389. <!-- <rtc-dialog ref="RtcDialogRef" @update-info="emitUpdateInfo"></rtc-dialog> -->
  390. </template>
  391. <style scoped lang="scss">
  392. </style>