bzkf3 лет назад: 2
Родитель
Сommit
c46828c8eb

+ 4 - 2
src/App.vue

@@ -2,9 +2,11 @@
 import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
 import { ElMessage } from 'element-plus';
 import { UserRole } from '~/store/user';
+import { showConfirmDialog, showSuccessToast, showFailToast } from 'vant';
 
 if (UserRole === 'other') {
-  ElMessage.error('非法用户')
+  // ElMessage.error('非法用户')
+  showFailToast('非法用户')
 }
 
 </script>
@@ -12,7 +14,7 @@ if (UserRole === 'other') {
 <template>
   <el-config-provider :message="{ max: 3 }" :z-index="100" :locale="zhCn" namespace="ep">
     <suspense>
-      <div class="bg-hex-f2f2f295  w-full h-100vh flex flex-col justify-start items-center">
+      <div class="bg-hex-f2f2f295 w-full h-100vh flex flex-col justify-start items-stretch">
         <router-view></router-view>
       </div>
 

+ 107 - 0
src/components/RemoteList/index.vue

@@ -0,0 +1,107 @@
+<script setup>
+const props = defineProps({
+  url: {
+    type: String,
+    required: true,
+  },
+  d: {
+    type: Object,
+    required: false,
+  },
+  loop: {
+    type: Boolean,
+    required: false,
+    default: false,
+  },
+})
+
+let page = 1
+
+let loading = $ref(false)
+let error = $ref(false)
+let finished = $ref(false)
+let refreshing = $ref(false)
+let refreshloop = $ref(false)
+
+let list = $ref([])
+
+// request({
+//   url: props.url,
+//   data: {
+//     page,
+//     ...props.d
+//   },
+// })
+
+
+function onLoad() {
+  loading = true
+  request({
+    url: props.url,
+    data: {
+      page,
+      ...props.d
+    },
+  }).then(res => {
+    if (res.code === '1') {
+      if (refreshing || refreshloop) {
+        list = [];
+        refreshing = false;
+        refreshloop = false;
+
+      }
+      list = list.concat(res.data.page_data)
+      loading = false
+      page++
+      if (res.data.page_now === res.data.total_page) {
+        finished = true
+      }
+    } else {
+      error = true
+    }
+
+  }).catch(err => {
+    console.log(err)
+  })
+
+}
+
+function onRefresh() {
+  finished = false
+  page = 1
+  onLoad()
+}
+
+let timer;
+
+if (props.loop) {
+  timer = setTimeout(async () => {
+    refreshloop = true;
+    await onRefresh();
+  }, 60 * 1000);
+}
+
+onBeforeUnmount(() => {
+  timer && clearInterval(timer)
+})
+
+watch(
+  () => props.d.dxz_stu_user_realname,
+  async (val, oldVal) => {
+    if (oldVal === '搜索') return;
+    refreshloop = true;
+    onRefresh()
+  },
+)
+</script>
+
+<template>
+  <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
+    <van-list v-model:loading="loading" v-model:error="error" error-text="请求失败,点击重新加载" :finished="finished"
+      finished-text="没有更多了" @load="onLoad" @click.stop>
+      <template v-for="item in list">
+        <slot :row="item"></slot>
+      </template>
+    </van-list>
+  </van-pull-refresh>
+</template>

+ 3 - 1
src/components/archives-card/index.vue

@@ -60,7 +60,9 @@ watch(
 </script>
 
 <template>
-  <div class="border border-dark-100 divide-y divide-dark-100 w-full text-xs">
+  <div class="border border-dark-100 divide-y divide-dark-100 w-full text-xs relative">
+
+    <div class="absolute inset-0 z-1" v-if="disabled"></div>
 
     <div class="flex justify-between items-end">
       <div class="p-1 text-base font-bold">咨询记录</div>

+ 8 - 2
src/components/info-item/index.vue

@@ -25,7 +25,7 @@ const showMsg = $computed(() => decodeURIComponent(props.d.dxzl_last_msg_content
       <div class="text-sm text-hex-AFB2B6">{{ formatTimeToShow(parseInt(props.d.create_dateline) * 1000) }}</div>
     </div>
     <div class="flex mt-2 r_flex-row-reverse">
-      <div class=" py-10px px-25px rounded-3xl min-h-44px bg-white text_wrapper" :style="`max-width:${props.w}px`"
+      <div class=" py-10px px-14px rounded-3xl min-h-44px bg-white text_wrapper min-w-50px" :style="`max-width:${props.w}px`"
         v-html="showMsg">
       </div>
     </div>
@@ -38,6 +38,12 @@ const showMsg = $computed(() => decodeURIComponent(props.d.dxzl_last_msg_content
   margin: 0;
   padding: 0;
   word-wrap: break-word;
+
+  img{
+    display: inline-block;
+    vertical-align: bottom;
+    margin: 0 2px;
+  }
 }
 
 
@@ -53,7 +59,7 @@ const showMsg = $computed(() => decodeURIComponent(props.d.dxzl_last_msg_content
   }
 
   .text_wrapper {
-    background-color: #F9A7BE;
+    background-color: #397FF6;
     color: white;
     border-radius: 24px 2px 24px 24px;
   }

+ 62 - 0
src/components/m-uploader/index.vue

@@ -0,0 +1,62 @@
+<script setup>
+import { REQUEST } from '~/utils/request'
+import { resolveFileString } from '~/utils/helper'
+
+const attrs = useAttrs()
+
+const props = defineProps({
+  // part: String,
+  // full: String,
+  modelValue: String,
+})
+
+// const emits = defineEmits(['update:part', 'update:full'])
+const emits = defineEmits(['update:modelValue'])
+const fileList = $ref(resolveFileString(props.modelValue).map(_ => ({ ..._, res: _ })))
+
+// if (props.part) {
+//   fileList = resolveFileString(props.part)
+// }
+// if (props.full) {
+//   fileList = resolveFileString(props.full)
+// }
+
+const handleAfterRead = (fileProxy) => {
+  fileProxy.status = 'uploading'
+  fileProxy.message = '上传中...'
+  const { file } = fileProxy
+  REQUEST.upload({
+    url: '/upload/main/file',
+    data: { filedata: file },
+  }).then((res) => {
+    console.log('res :>> ', res)
+    console.log('fileList :>> ', fileList)
+    if (res.code === '1') {
+      // fileList.value.push(res.data)
+      fileProxy.url = `${window.GLOBAL_CONFIG.oss}/${res.data.url}`
+      fileProxy.res = {
+        name: res.data.file_name,
+        url: fileProxy.url,
+        origin: res.data.url,
+      }
+      fileProxy.status = 'done'
+      fileProxy.message = ''
+
+      // emits('update:part', fileList.map((item) => item.res.name + ',' + item.res.url).join(';'))
+      // emits('update:part', fileList.map((item) => item.res.name + ',' + item.res.url).join(';'))
+      // emits('update:modelValue', fileList.map(item => `${item.res.name},${item.res.origin}`).join(';'))
+      emits('update:modelValue', fileList.map(item => `${item.res.origin}`).join(';'))
+    }
+    else {
+      fileProxy.status = 'failed'
+      fileProxy.message = '上传失败'
+    }
+  }).catch((err) => {
+    console.error(err)
+  })
+}
+</script>
+
+<template>
+  <van-uploader v-model="fileList" :after-read="handleAfterRead" :=attrs ><slot></slot></van-uploader>
+</template>

+ 12 - 10
src/components/rtc-dialog/index.vue

@@ -8,7 +8,8 @@ import type { type_dyaw_xlfw_zxhd, type_dyaw_xlfw_zxhd_log } from '~/types';
 import user, { UserRole } from '~/store/user';
 import { socketSend } from '~/utils/ws';
 import { formatOffsetSec } from '~/utils/time';
-import { ElMessage } from "element-plus";
+// import { ElMessage } from "element-plus";
+import { showConfirmDialog, showSuccessToast, showFailToast } from 'vant';
 let offsetTimer: NodeJS.Timeout
 let offsetTime = $ref<number>(0)
 
@@ -268,7 +269,8 @@ async function handleAudioChatStart() {
     if (isBusy) {
       // busy operation
       currentChatStatus = CHAT_STATUS.WAITING_BUSY
-      ElMessage.warning('对方忙线中')
+      // ElMessage.warning('对方忙线中')
+      showFailToast('对方忙线中')
       setTimeout(() => {
         handleClose()
       }, 2000)
@@ -425,14 +427,14 @@ function handleChangeMins() {
 </script>
 
 <template>
-  <UseDraggable v-if="isOpen" storage-key="chat-audio" storage-type="session" :initial-value="{ x: 584, y: 207 }"
-    class="fixed bg-transparent cursor-move z-4000">
-    <div v-show="mins" class="w-375px h-0 flex justify-end">
-      <el-avatar :size="90" :src="otherInfo?.avatar" @mousedown="isDown = true" @mousemove="isMove = isDown"
+  <div v-if="isOpen" class="fixed bg-transparent z-4000">
+    <UseDraggable v-show="mins" class="fixed" storage-key="chat-audio" storage-type="session"
+      :initial-value="{ x: 20, y: 500 }">
+      <el-avatar :size="70" :src="otherInfo?.avatar" @mousedown="isDown = true" @mousemove="isMove = isDown"
         @click="handleChangeMins"></el-avatar>
-    </div>
-    <div class="w-375px h-670px bg-hex-191919" v-show="!mins">
-      <div class="w-full flex justify-end items-center p-2 text-light-50 h-36px">
+    </UseDraggable>
+    <div class="w-screen h-screen bg-hex-191919" v-show="!mins">
+      <div class="w-full flex justify-end items-center p-2 text-light-50 h-36px space-x-2">
         <i:clarity:window-min-line class="cursor-pointer" @click="handleChangeMins" />
         <i:ic:outline-close class="cursor-pointer" @click="handleCloseForce" />
       </div>
@@ -519,6 +521,6 @@ function handleChangeMins() {
       </div>
     </div>
 
-  </UseDraggable>
+  </div>
 </template>
 

+ 2 - 2
src/components/teacher-card/index.vue

@@ -10,7 +10,7 @@ function handleClickConsult() {
 </script>
 
 <template>
-  <div class="bg-white w-370px h-130px rounded-md box_shadow flex flex-col justify-between p-3">
+  <div class="bg-white w-370px h-120px rounded-md box_shadow flex flex-col justify-between p-3 ">
     <div class="flex items-center justify-between">
       <div class="flex space-x-4 items-center">
         <el-avatar :size="48" :src="d.dxp_user_avatar"></el-avatar>
@@ -22,7 +22,7 @@ function handleClickConsult() {
 
     </div>
     <div class="text-hex-5B9FF0">联系电话:{{ d.dxp_user_phone }}</div>
-    <div class="line-clamp-2 overflow-ellipsis text-hex-00000041 h-10 leading-5">
+    <div class="line-clamp-1 overflow-ellipsis text-hex-00000041 h-5 leading-5">
       {{ d.dxp_jj }}
     </div>
   </div>

+ 72 - 12
src/components/tinymce-area/index.vue

@@ -32,14 +32,18 @@ function init() {
     selector: `#${props.id}`,
     base_url: '/tiny',
     language: 'zh-Hans',
-    height: 130,
+    // height: 40,
+    // min_height: 40,
+    max_height: 200,
     menubar: false,
     statusbar: false,
+    // inline: true,
+    resize: true,
     body_class: 'tinymce-area',
-    toolbar1: 'image emoticons',
-    plugins: 'image emoticons',
+    toolbar1: 'emoticons',
+    plugins: 'image emoticons autoresize',
     autosave_restore_when_empty: false,
-    content_style: 'body { margin: 0.4rem; line-height: 1; } p { margin: 0; } ',
+    content_style: 'body { margin: 0.4rem; line-height: 1;min-height: 1rem } p { margin: 0; } ',
     setup(_editor) {
       _editor.on('KeyDown', (e) => {
         if (e.ctrlKey && e.keyCode === 13) {
@@ -125,20 +129,51 @@ function handleClickAudioCall() {
 function handleClickVideoCall() {
   emits('click:video')
 }
+
+
+const handleAfterRead = (fileProxy) => {
+  // const file = blobInfo.blob()
+  const { file } = fileProxy
+  console.log('file : ', file)
+  return REQUEST.upload({
+    url: '/upload/main/file',
+    data: {
+      filedata: file,
+    },
+    // onUploadProgress(progressEvent) {
+    //   progress(~~(progressEvent.loaded / progressEvent.total * 100 | 0))
+    // },
+  }).then((res) => {
+    if (res.code === '1') {   // return window.GLOBAL_CONFIG.oss + (res.data.url)
+      editor.selection.setContent(`<img src="${window.GLOBAL_CONFIG.oss + (res.data.url)}" width="150" />`)
+      emits('update:modelValue', editor.getContent())
+    }
+    else {
+      fileProxy.status = 'failed'
+      fileProxy.message = '上传失败'
+    }
+  })
+}
+
 </script>
 
 <template>
   <div class="relative">
-    <textarea :id="id" class="tinyarea w-full h-full" v-model="content"></textarea>
+    <textarea :id="id" class="tinyarea w-full min-h-42px" v-model="content"></textarea>
+
+    <div class="absolute right-0 top-0 px-13px flex z-1000 text-hex-222f3e space-x-1px">
+
+      <div class="cursor-pointer flex_center box-content tbtn">
+        <van-uploader :after-read="handleAfterRead" class=" w-6 h-6">
+          <i:uil:image-upload class=" w-6 h-6" />
+        </van-uploader>
+      </div>
 
-    <div class="absolute right-0 top-0 h-28px px-13px flex z-1000 text-hex-666">
-      <div @click="handleClickAudioCall"
-        class="cursor-pointer flex_center box-content w-28px px-3px h-full  rounded-sm hover:bg-hex-cce2fa">
-        <i:mingcute:phone-call-fill class=" w flex_center  h-6" />
+      <div @click="handleClickAudioCall" class="cursor-pointer flex_center box-content tbtn ">
+        <i:material-symbols:call class="w-6 h-6" />
       </div>
-      <div @click="handleClickVideoCall"
-        class="cursor-pointer flex_center box-content w-28px px-3px h-full  rounded-sm hover:bg-hex-cce2fa">
-        <i:wpf:video-call class=" w-6 h-6" />
+      <div @click="handleClickVideoCall" class="cursor-pointer flex_center box-content tbtn ">
+        <i:material-symbols:video-call-outline class=" w-6 h-6" />
       </div>
     </div>
   </div>
@@ -147,16 +182,41 @@ function handleClickVideoCall() {
 <style lang="scss">
 .tinyarea+.tox-tinymce {
   border: none;
+  border-radius: 0;
+  // height: 42px !important;
+
+  // .tox-editor-header{
+  //   display: none;
+  // }
 
   &.tox:not(.tox-tinymce-inline) .tox-editor-header {
     box-shadow: none;
     padding: 0 0;
+
+    .tox-toolbar-overlord .tox-toolbar {
+      background: #e4e6eb;
+    }
   }
 
   .tox-tbtn {
     margin-top: 0;
     margin-bottom: 0;
     cursor: pointer;
+    width: 40px;
+    height: 40px;
+    // background: #f3f4f6;
+    box-shadow: inset 0 0 0 1px #e4e6eb;
+    border-radius: 2px;
   }
+
+
+}
+
+.tbtn {
+  width: 40px;
+  height: 40px;
+  // background: #f3f4f6;
+  box-shadow: inset 0 0 0 1px #e4e6eb;
+  border-radius: 2px;
 }
 </style>

+ 14 - 2
src/main.ts

@@ -4,7 +4,19 @@ import 'virtual:windi.css'
 import App from './App.vue'
 import router from './router/index'
 
-import 'element-plus/es/components/message/style/css'
-import 'element-plus/es/components/message-box/style/css'
+// import 'element-plus/es/components/message/style/css'
+// import 'element-plus/es/components/message-box/style/css'
+
+// Toast
+import 'vant/es/toast/style';
+
+// Dialog
+import 'vant/es/dialog/style';
+
+// Notify
+import 'vant/es/notify/style';
+
+// ImagePreview
+import 'vant/es/image-preview/style';
 
 createApp(App).use(router).mount('#appx')

+ 2 - 1
src/pages/admin/home.vue

@@ -8,7 +8,8 @@ const imgSrc = (await request({
 const teacherList = (await request({
   url: '/dyaw/xlfw_pbgl_pb/index',
   data: {
-    dxpp_date: (new Date('2023-4-2')).getTime()
+    // dxpp_date: (new Date('2023-4-2')).getTime()
+    dxpp_date: Date.now()
   }
 })).data?.page_data
 

+ 3 - 2
src/pages/student/home.vue

@@ -19,13 +19,14 @@ function onClickLeft() {
 
 <template>
   <div class=" flex flex-col justify-start ">
-    <van-nav-bar title="心理健康" left-text="" left-arrow @click-left="onClickLeft" 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 title="心理健康" left-text="" left-arrow @click-left="onClickLeft"
+      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;" />
     <div class="w-full">
       <img v-show="imgSrc" :src="imgSrc" class="w-full object-contain">
     </div>
     <div
       class="w-full h-full overflow-y-auto flex flex-col items-center space-y-4 py-4 scrollbar scrollbar-thin scrollbar-thumb-rounded-md scrollbar-thumb-gray-200 scrollbar-track-transparent">
-      <el-empty v-if="!teacherList?.length" description="今天暂无排班老师"></el-empty>
+      <el-empty v-if="!teacherList?.length" description="当前暂无排班老师"></el-empty>
       <teacher-card v-for="item in teacherList" class="flex-none" :d="item">
       </teacher-card>
     </div>

+ 383 - 0
src/pages/teacher/chat-list copy.vue

@@ -0,0 +1,383 @@
+<script setup lang="ts">
+import AgoraRTC from "agora-rtc-sdk-ng"
+import type { IAgoraRTCClient, IMicrophoneAudioTrack, ICameraVideoTrack } from "agora-rtc-sdk-ng"
+import type { type_dyaw_xlfw_zxhd, type_dyaw_xlfw_zxhd_log, type_archives_item } from '~/types';
+import { Search } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import user from '~/store/user';
+import { createSocket, socketSend } from '~/utils/ws';
+import type { TSocketRes } from '~/utils/ws';
+import { formatTimestamp2Date } from '~/utils/time';
+import { CHAT_STATUS, CHAT_OPERATION } from '~/types';
+let dyaw_xlfw_zxhd = $ref<type_dyaw_xlfw_zxhd | undefined>()
+let dyaw_xlfw_zxhd_list = $ref<type_dyaw_xlfw_zxhd[] | undefined>()
+
+
+
+const searchValue = $ref('搜索')
+watch(
+  () => searchValue,
+  async (val, oldVal) => {
+    if (oldVal === '搜索') return;
+    dyaw_xlfw_zxhd_list = (await request({
+      url: '/dyaw/xlfw_zxhd/index',
+      data: {
+        dxz_tea_user_id: user.user_id,
+        dxz_stu_user_realname: searchValue === '搜索' ? undefined : searchValue,
+        limit: 20,
+        last_msg: 1
+      }
+    })).data.page_data
+  }
+)
+
+
+let timer: NodeJS.Timeout;
+(async function loop() {
+  dyaw_xlfw_zxhd_list = (await request({
+    url: '/dyaw/xlfw_zxhd/index',
+    data: {
+      dxz_tea_user_id: user.user_id,
+      dxz_stu_user_realname: searchValue === '搜索' ? undefined : searchValue,
+      limit: 20,
+      last_msg: 1
+    }
+  })).data.page_data
+  if (dyaw_xlfw_zxhd !== undefined) {
+    const temp = dyaw_xlfw_zxhd_list!.find(item => item.dxz_stu_user_id === dyaw_xlfw_zxhd!.dxz_stu_user_id)
+    if (temp?.dxz_id !== dyaw_xlfw_zxhd?.dxz_id) {
+      dyaw_xlfw_zxhd = temp
+    }
+  }
+
+  timer = setTimeout(async () => {
+    await loop();
+  }, 60 * 1000);
+})();
+
+let infoList = $ref<Array<type_dyaw_xlfw_zxhd_log>>([])
+let inputValue = $ref('')
+let isSending = $ref(false)
+let TinyRef = $ref<typeof import('~/components/tinymce-area/index.vue')['default']>()
+async function handleClickSend() {
+  if (!dyaw_xlfw_zxhd) return;
+  if (isSending) return;
+  isSending = true
+  const reqDate = {
+    dxz_id: dyaw_xlfw_zxhd.dxz_id,
+    dxzl_stu_user_id: dyaw_xlfw_zxhd.dxz_stu_user_id,
+    dxzl_stu_user_realname: dyaw_xlfw_zxhd.dxz_stu_user_realname,
+    dxzl_tea_user_id: dyaw_xlfw_zxhd.dxz_tea_user_id,
+    dxzl_tea_user_realname: dyaw_xlfw_zxhd.dxz_tea_user_realname,
+    dxzl_last_msg_content: encodeURIComponent(inputValue),
+    dxzl_type: inputValue.includes('><img') ? '2' : '1'
+  }
+
+  // infoList.push({
+  //   create_user_id: user.user_id,
+  //   create_dateline: Date.now().toString().slice(0, 10),
+
+  //   ...reqDate
+  // })
+  TinyRef?.clear()
+  // console.log('inputValue :>> ', inputValue);
+  request({
+    url: '/dyaw/xlfw_zxhd_log/add',
+    data: {
+      dyaw_xlfw_zxhd_log: reqDate
+    }
+  }).then(res => {
+    if (res.code === '1') {
+      const fullSendData = {
+        create_user_id: user.user_id,
+        create_dateline: Date.now().toString().slice(0, 10),
+        ...reqDate,
+        dxzl_id: `${res.data.insert_id}`
+      }
+      infoList.push(fullSendData)
+      socketSend(ws!, fullSendData)
+      isSending = false
+    }
+  })
+}
+
+let ws = $ref<WebSocket>()
+
+let cardLoading = $ref(false)
+let unreadNum = $ref(0)
+
+// function getAllUnreadMsg() {
+//   request({
+//     url: '/dyaw/xlfw_zxhd_log/index',
+//     data: {
+//       dxzl_stu_user_id: dyaw_xlfw_zxhd?.dxz_stu_user_id,
+//       dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
+//       limit: 1
+//     }
+//   }).then(res => {
+//     if (res.code === '1') {
+//       ifScroll = false
+//       infoList = (res.data.page_data.reverse())
+//       unreadNum = 0
+//     }
+//     nextTick(() => {
+//       ifScroll = true
+//       // scrollbarRef!.scrollTo(0, 0);
+//     })
+//   })
+// }
+
+function handleClickStuCard(stu: type_dyaw_xlfw_zxhd) {
+  cardLoading = true
+  dyaw_xlfw_zxhd = stu
+  unreadNum = parseInt(stu.dxz_unread_msg_num)
+  stu.dxz_unread_msg_num = "0"
+  archivesList = []
+
+  ws?.close()
+  request({
+    url: '/dyaw/xlfw_zxhd_log/index',
+    data: {
+      dxzl_stu_user_id: stu.dxz_stu_user_id,
+      dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
+    }
+  }).then(res => {
+    if (res.code === '1') {
+      infoList = res.data.page_data.reverse()
+      cardLoading = false
+      ws = createSocket(
+        { teacher: user.user_id, student: stu.dxz_stu_user_id },
+        {
+          message(socketRes: TSocketRes<type_dyaw_xlfw_zxhd_log & { $?: boolean, dxz_stu_user_id?: string, dyaw_xlfw_zxhd: type_dyaw_xlfw_zxhd }>) {
+            if (socketRes.from_client_name.endsWith('student')) {
+              if (socketRes.content.$) {
+                if (socketRes.content?.dxz_stu_user_id === dyaw_xlfw_zxhd?.dxz_stu_user_id) {
+                  dyaw_xlfw_zxhd = socketRes.content.dyaw_xlfw_zxhd
+                }
+                return;
+              }
+              infoList.push(socketRes.content)
+              request({
+                url: '/dyaw/xlfw_zxhd_log/index',
+                data: {
+                  dxzl_stu_user_id: stu.dxz_stu_user_id,
+                  dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
+                  limit: 0
+                }
+              })
+
+            }
+          }
+        }
+      )
+
+      handleQueryArchives()
+    }
+  })
+}
+
+let ifScroll = $ref(true)
+function handleLoadMoreInfo() {
+  request({
+    url: '/dyaw/xlfw_zxhd_log/index',
+    data: {
+      dxzl_stu_user_id: dyaw_xlfw_zxhd?.dxz_stu_user_id,
+      dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
+      zxhd_log_id: infoList[0].dxzl_id
+    }
+  }).then(res => {
+    if (res.code === '1') {
+      ifScroll = false
+      if (res.data.page_data.length === 0) return ElMessage.info('暂无更多消息');
+      infoList.unshift(...res.data.page_data.reverse())
+    }
+  })
+}
+
+const scrollbarRef = $ref<HTMLElement>()
+function scrollToBottom() {
+  if (!scrollbarRef) return;
+  const scrollHeight = scrollbarRef!.scrollHeight;
+  scrollbarRef!.scrollTo(0, scrollHeight);
+}
+watch(
+  () => (infoList),
+  () => {
+    nextTick(() => {
+      ifScroll && scrollToBottom();
+      ifScroll = true
+    })
+  },
+  {
+    deep: true,
+  }
+)
+
+
+onBeforeUnmount(() => {
+  clearInterval(timer)
+})
+
+const ArchivesCardRef = $ref<typeof import('~/components/archives-card/index.vue')['default']>()
+function handleSubmitArchives() {
+  ElMessageBox.confirm('一次咨询只能提交一次档案,请确认完毕后点击提交', '提示')
+    .then(() => {
+      request({
+        url: '/dyaw/xlfw_xsda_dajl/add',
+        data: {
+          dyaw_xlfw_xsda_dajl: ArchivesCardRef!.form
+        }
+      }).then(res => {
+        if (res.code === '1') {
+
+          handleQueryArchives()
+        }
+      })
+    })
+    .catch(() => {
+      ElMessage({
+        type: 'info',
+        message: '取消提交',
+      })
+    })
+}
+
+let archivesList = $ref<type_archives_item[]>([])
+function handleQueryArchives() {
+  request({
+    url: '/dyaw/xlfw_xsda_dajl/index',
+    data: {
+      user_id: dyaw_xlfw_zxhd!.dxz_stu_user_id
+    }
+  }).then(res => {
+    if (res.code === '1') {
+      archivesList = res.data.page_data
+    }
+  })
+}
+
+// ==========
+// chat audio/video
+// ==========
+
+let RtcDialogRef = $ref<typeof import("~/components/rtc-dialog/index.vue")['default']>()
+
+const ws2 = createSocket(
+  { teacher: user.user_id, student: '*' },
+  {
+    message(socketRes: TSocketRes<type_dyaw_xlfw_zxhd_log & { operate: CHAT_OPERATION }>) {
+      if (socketRes.from_client_name.endsWith('student')) {
+        // infoList.push(socketRes.content)
+        if (socketRes.content.dxzl_tea_user_id === user.user_id) {
+          RtcDialogRef!.publisher(socketRes.content)
+        }
+      }
+    }
+  }
+)
+onMounted(() => {
+  RtcDialogRef!.init(ws2)
+})
+
+
+
+async function handleAudioChatStart() {
+  RtcDialogRef!.open(dyaw_xlfw_zxhd, 'audio')
+}
+
+async function handleVideoChatStart() {
+  RtcDialogRef!.open(dyaw_xlfw_zxhd, 'video')
+}
+
+function emitUpdateInfo(info: type_dyaw_xlfw_zxhd_log, isUpdate?: boolean) {
+  if (!isUpdate) {
+    if (info.dxz_id === dyaw_xlfw_zxhd?.dxz_id)
+      infoList.push(info)
+  }
+  else {
+    const target = infoList.find(item => (item.dxzl_id)?.toString() === info.dxzl_id?.toString())
+    if (target)
+      target.dxzl_last_msg_content = info.dxzl_last_msg_content
+  }
+}
+</script>
+
+<template>
+  <div class="h-590px bg-hex-f2f2f295 flex justify-center divide-x">
+    <div class="w-320px p-10px h-full flex flex-col justify-start bg-white space-y-4">
+      <el-input autocomplete="off" name="searchValue" v-model="searchValue" size="large" :prefix-icon="Search"
+        @focus="searchValue = ''" clearable placeholder="搜索"
+        :style="`${searchValue === '搜索' && '--ep-input-text-color:var(--ep-text-color-placeholder);'}`"></el-input>
+      <div
+        class="max-h-520px flex flex-col flex-none divide-y scrollbar scrollbar-thin scrollbar-thumb-rounded-md scrollbar-thumb-gray-200 scrollbar-track-transparent">
+        <chat-stu-card v-for="item in dyaw_xlfw_zxhd_list" :d="item" @click="handleClickStuCard(item)"
+          class="h-68px"></chat-stu-card>
+      </div>
+    </div>
+    <div class="w-680px h-full divide-y flex flex-col relative">
+      <template v-if="dyaw_xlfw_zxhd">
+        <div class="bg-pink-300 flex justify-between h-48px items-center px-18px">
+          <span class="text-lg px-1 tracking-wider">{{ dyaw_xlfw_zxhd.dxz_stu_user_realname }}
+            {{ dyaw_xlfw_zxhd.dxz_stu_school_name }} {{ dyaw_xlfw_zxhd.dxz_class_name }}</span>
+        </div>
+        <div ref="scrollbarRef"
+          class="bg-hex-fff8fb space-y-2 h-410px py-2 px-6 scrollbar scrollbar-thin scrollbar-thumb-rounded-md scrollbar-thumb-gray-200 scrollbar-track-transparent relative">
+          <div v-show="!cardLoading" @click="handleLoadMoreInfo"
+            class="w-full text-center text-sm text-blue-400 hover:underline underline-blue-400 cursor-pointer">查看更多
+          </div>
+          <info-item v-for="item in infoList" :key="item.dxzl_id" :left="item.create_user_id !== user.user_id" :d="item"
+            :w="500"></info-item>
+
+        </div>
+        <!--  -->
+        <!-- <div v-show="unreadNum > 10"
+                                    class="absolute bg-white right-2 bottom-200px text-blue-400 rounded-l-full cursor-pointer z-100 flex items-center space-x-1 p-2 text-sm"
+                                    @click="getAllUnreadMsg">
+                                    <i:material-symbols:keyboard-double-arrow-up />
+                                    <span>{{ unreadNum }}条消息</span>
+                                  </div> -->
+        <div class="bg-white h-180px p-5px flex flex-col justify-between">
+          <tinymce-area v-model="inputValue" ref="TinyRef" @click:audio="handleAudioChatStart"
+            @click:video="handleVideoChatStart" @click:submit="handleClickSend"></tinymce-area>
+          <div class="flex justify-end">
+            <div class="bg-pink-300 text-sm text-white w-80px h-32px flex_center rounded-2xl cursor-pointer"
+              @click="handleClickSend">发送</div>
+          </div>
+        </div>
+      </template>
+      <template v-else>
+        <div class="bg-pink-300 flex justify-between h-48px items-center px-18px"></div>
+        <div class="bg-hex-fff8fb flex-auto w-full flex_center">
+          <el-empty description="请先选择聊天的学生"></el-empty>
+        </div>
+      </template>
+
+    </div>
+    <div
+      class="w-400px 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">
+      <div v-if="dyaw_xlfw_zxhd" :key="dyaw_xlfw_zxhd.dxz_stu_user_id">
+        <div class="w-full"
+          v-if="archivesList && (archivesList.length === 0 || dyaw_xlfw_zxhd.dxz_id !== archivesList[0].dxz_id)">
+          <archives-card storage ref="ArchivesCardRef"
+            :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>
+          <div class="flex_center py-2">
+            <el-button @click="handleSubmitArchives" type="primary" size="small">提交</el-button>
+          </div>
+        </div>
+        <div class="bg-hex-FFF7FA py-2 space-y-1">
+          <div class="flex_center text-lg">历史档案</div>
+          <el-empty v-show="archivesList.length === 0" :image-size="60" description="暂无历史档案"></el-empty>
+          <archives-card disabled v-for="item in archivesList" :d="item" :key="item.dxxd_id"></archives-card>
+        </div>
+      </div>
+
+    </div>
+  </div>
+
+
+
+  <rtc-dialog ref="RtcDialogRef" @update-info="emitUpdateInfo"></rtc-dialog>
+</template>
+
+<style scoped lang="scss">
+</style>

+ 403 - 0
src/pages/teacher/chat-list.vue

@@ -0,0 +1,403 @@
+<script setup lang="ts">
+import AgoraRTC from "agora-rtc-sdk-ng"
+import type { IAgoraRTCClient, IMicrophoneAudioTrack, ICameraVideoTrack } from "agora-rtc-sdk-ng"
+import type { type_dyaw_xlfw_zxhd, type_dyaw_xlfw_zxhd_log, type_archives_item } from '~/types';
+import { Search } from '@element-plus/icons-vue'
+// import { ElMessage, ElMessageBox } from 'element-plus'
+import user from '~/store/user';
+import { createSocket, socketSend } from '~/utils/ws';
+import type { TSocketRes } from '~/utils/ws';
+import { formatTimestamp2Date } from '~/utils/time';
+import { CHAT_STATUS, CHAT_OPERATION } from '~/types';
+import { showConfirmDialog, showSuccessToast, showFailToast } from 'vant';
+let dyaw_xlfw_zxhd = $ref<type_dyaw_xlfw_zxhd>(JSON.parse(sessionStorage.getItem('dyaw_xlfw_zxhd')!))
+let dyaw_xlfw_zxhd_list = $ref<type_dyaw_xlfw_zxhd[] | undefined>()
+
+
+
+
+
+let infoList = $ref<Array<type_dyaw_xlfw_zxhd_log>>([])
+let inputValue = $ref('')
+let isSending = $ref(false)
+let TinyRef = $ref<typeof import('~/components/tinymce-area/index.vue')['default']>()
+async function handleClickSend() {
+  if (!dyaw_xlfw_zxhd) return;
+  if (isSending) return;
+  isSending = true
+  const reqDate = {
+    dxz_id: dyaw_xlfw_zxhd.dxz_id,
+    dxzl_stu_user_id: dyaw_xlfw_zxhd.dxz_stu_user_id,
+    dxzl_stu_user_realname: dyaw_xlfw_zxhd.dxz_stu_user_realname,
+    dxzl_tea_user_id: dyaw_xlfw_zxhd.dxz_tea_user_id,
+    dxzl_tea_user_realname: dyaw_xlfw_zxhd.dxz_tea_user_realname,
+    dxzl_last_msg_content: encodeURIComponent(inputValue),
+    dxzl_type: inputValue.includes('><img') ? '2' : '1'
+  }
+
+  // infoList.push({
+  //   create_user_id: user.user_id,
+  //   create_dateline: Date.now().toString().slice(0, 10),
+
+  //   ...reqDate
+  // })
+  TinyRef?.clear()
+  // console.log('inputValue :>> ', inputValue);
+  request({
+    url: '/dyaw/xlfw_zxhd_log/add',
+    data: {
+      dyaw_xlfw_zxhd_log: reqDate
+    }
+  }).then(res => {
+    if (res.code === '1') {
+      const fullSendData = {
+        create_user_id: user.user_id,
+        create_dateline: Date.now().toString().slice(0, 10),
+        ...reqDate,
+        dxzl_id: `${res.data.insert_id}`
+      }
+      infoList.push(fullSendData)
+      socketSend(ws!, fullSendData)
+      isSending = false
+    }
+  })
+}
+
+let ws = $ref<WebSocket>()
+
+let cardLoading = $ref(true)
+let unreadNum = $ref(0)
+
+// function getAllUnreadMsg() {
+//   request({
+//     url: '/dyaw/xlfw_zxhd_log/index',
+//     data: {
+//       dxzl_stu_user_id: dyaw_xlfw_zxhd?.dxz_stu_user_id,
+//       dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
+//       limit: 1
+//     }
+//   }).then(res => {
+//     if (res.code === '1') {
+//       ifScroll = false
+//       infoList = (res.data.page_data.reverse())
+//       unreadNum = 0
+//     }
+//     nextTick(() => {
+//       ifScroll = true
+//       // scrollbarRef!.scrollTo(0, 0);
+//     })
+//   })
+// }
+
+// function handleClickStuCard(stu: type_dyaw_xlfw_zxhd) {
+//   cardLoading = true
+//   dyaw_xlfw_zxhd = stu
+//   unreadNum = parseInt(stu.dxz_unread_msg_num)
+//   stu.dxz_unread_msg_num = "0"
+//   archivesList = []
+
+//   ws?.close()
+//   request({
+//     url: '/dyaw/xlfw_zxhd_log/index',
+//     data: {
+//       dxzl_stu_user_id: stu.dxz_stu_user_id,
+//       dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
+//     }
+//   }).then(res => {
+//     if (res.code === '1') {
+//       infoList = res.data.page_data.reverse()
+//       cardLoading = false
+//       ws = createSocket(
+//         { teacher: user.user_id, student: stu.dxz_stu_user_id },
+//         {
+//           message(socketRes: TSocketRes<type_dyaw_xlfw_zxhd_log & { $?: boolean, dxz_stu_user_id?: string, dyaw_xlfw_zxhd: type_dyaw_xlfw_zxhd }>) {
+//             if (socketRes.from_client_name.endsWith('student')) {
+//               if (socketRes.content.$) {
+//                 if (socketRes.content?.dxz_stu_user_id === dyaw_xlfw_zxhd?.dxz_stu_user_id) {
+//                   dyaw_xlfw_zxhd = socketRes.content.dyaw_xlfw_zxhd
+//                 }
+//                 return;
+//               }
+//               infoList.push(socketRes.content)
+//               request({
+//                 url: '/dyaw/xlfw_zxhd_log/index',
+//                 data: {
+//                   dxzl_stu_user_id: stu.dxz_stu_user_id,
+//                   dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
+//                   limit: 0
+//                 }
+//               })
+
+//             }
+//           }
+//         }
+//       )
+
+//       handleQueryArchives()
+//     }
+//   })
+// }
+
+
+unreadNum = parseInt(dyaw_xlfw_zxhd.dxz_unread_msg_num)
+dyaw_xlfw_zxhd.dxz_unread_msg_num = "0"
+
+request({
+  url: '/dyaw/xlfw_zxhd_log/index',
+  data: {
+    dxzl_stu_user_id: dyaw_xlfw_zxhd.dxz_stu_user_id,
+    dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
+  }
+}).then(res => {
+  if (res.code === '1') {
+    infoList = res.data.page_data.reverse()
+    cardLoading = false
+    ws = createSocket(
+      { teacher: user.user_id, student: dyaw_xlfw_zxhd.dxz_stu_user_id },
+      {
+        message(socketRes: TSocketRes<type_dyaw_xlfw_zxhd_log & { $?: boolean, dxz_stu_user_id?: string, dyaw_xlfw_zxhd: type_dyaw_xlfw_zxhd }>) {
+          if (socketRes.from_client_name.endsWith('student')) {
+            if (socketRes.content.$) {
+              if (socketRes.content?.dxz_stu_user_id === dyaw_xlfw_zxhd?.dxz_stu_user_id) {
+                dyaw_xlfw_zxhd = socketRes.content.dyaw_xlfw_zxhd
+              }
+              return;
+            }
+            infoList.push(socketRes.content)
+            request({
+              url: '/dyaw/xlfw_zxhd_log/index',
+              data: {
+                dxzl_stu_user_id: dyaw_xlfw_zxhd.dxz_stu_user_id,
+                dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
+                limit: 0
+              }
+            })
+
+          }
+        }
+      }
+    )
+
+    handleQueryArchives()
+  }
+})
+
+
+let ifScroll = $ref(true)
+function handleLoadMoreInfo() {
+  request({
+    url: '/dyaw/xlfw_zxhd_log/index',
+    data: {
+      dxzl_stu_user_id: dyaw_xlfw_zxhd?.dxz_stu_user_id,
+      dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
+      zxhd_log_id: infoList[0].dxzl_id
+    }
+  }).then(res => {
+    if (res.code === '1') {
+      ifScroll = false
+      if (res.data.page_data.length === 0) return showFailToast('暂无更多消息');
+      infoList.unshift(...res.data.page_data.reverse())
+    }
+  })
+}
+
+const scrollbarRef = $ref<HTMLElement>()
+const scrollContainRef = $ref<HTMLElement>()
+
+function scrollToBottom() {
+  if (!scrollbarRef) return;
+  const scrollHeight = scrollContainRef!.scrollHeight;
+  console.log('scrollHeight : ', scrollHeight)
+  scrollbarRef!.scrollTo(0, scrollHeight + 200);
+}
+watch(
+  () => (infoList),
+  () => {
+    nextTick(() => {
+      ifScroll && scrollToBottom();
+      ifScroll = true
+    })
+  },
+  {
+    deep: true,
+  }
+)
+
+
+const ArchivesCardRef = $ref<typeof import('~/components/archives-card/index.vue')['default']>()
+function handleSubmitArchives() {
+  showConfirmDialog({ message: '一次咨询只能提交一次档案,请确认完毕后点击提交', title: '提示' })
+    .then(() => {
+      request({
+        url: '/dyaw/xlfw_xsda_dajl/add',
+        data: {
+          dyaw_xlfw_xsda_dajl: ArchivesCardRef!.form
+        }
+      }).then(res => {
+        if (res.code === '1') {
+          showSuccessToast('提交成功');
+          handleQueryArchives()
+        } else {
+          showFailToast('提交失败');
+
+        }
+      })
+    })
+    .catch(() => {
+      showFailToast('取消提交');
+    })
+}
+
+let archivesList = $ref<type_archives_item[]>([])
+function handleQueryArchives() {
+  request({
+    url: '/dyaw/xlfw_xsda_dajl/index',
+    data: {
+      user_id: dyaw_xlfw_zxhd!.dxz_stu_user_id
+    }
+  }).then(res => {
+    if (res.code === '1') {
+      archivesList = res.data.page_data
+    }
+  })
+}
+
+// ==========
+// chat audio/video
+// ==========
+
+let RtcDialogRef = $ref<typeof import("~/components/rtc-dialog/index.vue")['default']>()
+
+const ws2 = createSocket(
+  { teacher: user.user_id, student: '*' },
+  {
+    message(socketRes: TSocketRes<type_dyaw_xlfw_zxhd_log & { operate: CHAT_OPERATION }>) {
+      if (socketRes.from_client_name.endsWith('student')) {
+        // infoList.push(socketRes.content)
+        if (socketRes.content.dxzl_tea_user_id === user.user_id) {
+          RtcDialogRef!.publisher(socketRes.content)
+        }
+      }
+    }
+  }
+)
+onMounted(() => {
+  RtcDialogRef!.init(ws2)
+})
+
+
+
+async function handleAudioChatStart() {
+  RtcDialogRef!.open(dyaw_xlfw_zxhd, 'audio')
+}
+
+async function handleVideoChatStart() {
+  RtcDialogRef!.open(dyaw_xlfw_zxhd, 'video')
+}
+
+function emitUpdateInfo(info: type_dyaw_xlfw_zxhd_log, isUpdate?: boolean) {
+  if (!isUpdate) {
+    if (info.dxz_id === dyaw_xlfw_zxhd?.dxz_id)
+      infoList.push(info)
+  }
+  else {
+    const target = infoList.find(item => (item.dxzl_id)?.toString() === info.dxzl_id?.toString())
+    if (target)
+      target.dxzl_last_msg_content = info.dxzl_last_msg_content
+  }
+}
+
+const router = useRouter()
+function onClickLeft() {
+  router.back()
+}
+
+
+let showRightArchives = $ref(false)
+</script>
+
+<template>
+  <div class="h-full flex justify-center divide-x">
+    <div class="w-full h-full divide-y flex flex-col relative">
+      <template v-if="dyaw_xlfw_zxhd">
+        <!-- <div class="bg-pink-300 flex justify-between h-48px items-center px-18px">
+                                        <span class="text-lg px-1 tracking-wider">{{ dyaw_xlfw_zxhd.dxz_stu_user_realname }}
+                                          {{ dyaw_xlfw_zxhd.dxz_stu_school_name }} {{ dyaw_xlfw_zxhd.dxz_class_name }}</span>
+                                      </div> -->
+        <van-nav-bar
+          :title="`${dyaw_xlfw_zxhd.dxz_stu_user_realname}  ${dyaw_xlfw_zxhd.dxz_stu_school_name}  ${dyaw_xlfw_zxhd.dxz_class_name}`"
+          left-text="" left-arrow @click-left="onClickLeft" right-text="学生档案" @click-right="showRightArchives = true"
+          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;" />
+
+        <div ref="scrollbarRef"
+          class="bg-hex-fff8fb space-y-2 flex-auto py-2 px-6 scrollbar scrollbar-thin scrollbar-thumb-rounded-md scrollbar-thumb-gray-200 scrollbar-track-transparent relative">
+          <div ref="scrollContainRef" class="scrollContainRef space-y-2">
+            <div v-show="!cardLoading" @click="handleLoadMoreInfo"
+              class="w-full text-center text-sm text-blue-400 hover:underline underline-blue-400 cursor-pointer">查看更多
+            </div>
+            <info-item v-for="item in infoList" :key="item.dxzl_id" :left="item.create_user_id !== user.user_id" :d="item"
+              :w="300"></info-item>
+          </div>
+
+
+        </div>
+        <!--  -->
+        <!-- <div v-show="unreadNum > 10"
+                                class="absolute bg-white right-2 bottom-200px text-blue-400 rounded-l-full cursor-pointer z-100 flex items-center space-x-1 p-2 text-sm"
+                                @click="getAllUnreadMsg">
+                                <i:material-symbols:keyboard-double-arrow-up />
+                                <span>{{ unreadNum }}条消息</span>
+                              </div> -->
+        <div class="bg-hex-e4e6eb p-5px flex justify-between space-x-2 items-end">
+          <tinymce-area v-model="inputValue" ref="TinyRef" @click:audio="handleAudioChatStart" class="flex-auto"
+            @click:video="handleVideoChatStart" @click:submit="handleClickSend"></tinymce-area>
+
+          <van-button type="primary" @click="handleClickSend">发送</van-button>
+
+        </div>
+      </template>
+      <template v-else>
+        <div class="bg-pink-300 flex justify-between h-48px items-center px-18px"></div>
+        <div class="bg-hex-fff8fb flex-auto w-full flex_center">
+          <el-empty description="请先选择聊天的学生"></el-empty>
+        </div>
+      </template>
+
+    </div>
+
+    <van-popup v-model:show="showRightArchives" position="right" :style="{ width: '100vw', height: '100vh' }">
+      <div
+        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">
+        <div v-if="dyaw_xlfw_zxhd" :key="dyaw_xlfw_zxhd.dxz_stu_user_id">
+          <van-sticky :offset-top="8">
+            <div class="mb-4 flex justify-end">
+              <van-button @click="showRightArchives = false" type="primary">返回</van-button>
+            </div>
+          </van-sticky>
+          <div class="w-full"
+            v-if="archivesList && (archivesList.length === 0 || dyaw_xlfw_zxhd.dxz_id !== archivesList[0].dxz_id)">
+            <archives-card storage ref="ArchivesCardRef"
+              :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>
+            <div class="flex_center py-2">
+              <van-button @click="handleSubmitArchives" type="success" size="large">提交</van-button>
+            </div>
+          </div>
+          <div class="bg-hex-FFF7FA py-2 space-y-1">
+            <div class="flex_center text-lg">历史档案</div>
+            <el-empty v-show="archivesList.length === 0" :image-size="60" description="暂无历史档案"></el-empty>
+            <archives-card disabled v-for="item in archivesList" :d="item" :key="item.dxxd_id"></archives-card>
+          </div>
+        </div>
+      </div>
+    </van-popup>
+
+  </div>
+
+
+
+
+  <rtc-dialog ref="RtcDialogRef" @update-info="emitUpdateInfo"></rtc-dialog>
+</template>
+
+<style scoped lang="scss">
+</style>

+ 45 - 334
src/pages/teacher/consult.vue

@@ -1,14 +1,8 @@
 <script setup lang="ts">
-import AgoraRTC from "agora-rtc-sdk-ng"
-import type { IAgoraRTCClient, IMicrophoneAudioTrack, ICameraVideoTrack } from "agora-rtc-sdk-ng"
 import type { type_dyaw_xlfw_zxhd, type_dyaw_xlfw_zxhd_log, type_archives_item } from '~/types';
 import { Search } from '@element-plus/icons-vue'
-import { ElMessage, ElMessageBox } from 'element-plus'
 import user from '~/store/user';
-import { createSocket, socketSend } from '~/utils/ws';
-import type { TSocketRes } from '~/utils/ws';
-import { formatTimestamp2Date } from '~/utils/time';
-import { CHAT_STATUS, CHAT_OPERATION } from '~/types';
+import router from '@/router';
 let dyaw_xlfw_zxhd = $ref<type_dyaw_xlfw_zxhd | undefined>()
 let dyaw_xlfw_zxhd_list = $ref<type_dyaw_xlfw_zxhd[] | undefined>()
 
@@ -32,351 +26,68 @@ watch(
 )
 
 
-let timer: NodeJS.Timeout;
-(async function loop() {
-  dyaw_xlfw_zxhd_list = (await request({
-    url: '/dyaw/xlfw_zxhd/index',
-    data: {
-      dxz_tea_user_id: user.user_id,
-      dxz_stu_user_realname: searchValue === '搜索' ? undefined : searchValue,
-      limit: 20,
-      last_msg: 1
-    }
-  })).data.page_data
-  if (dyaw_xlfw_zxhd !== undefined) {
-    const temp = dyaw_xlfw_zxhd_list!.find(item => item.dxz_stu_user_id === dyaw_xlfw_zxhd!.dxz_stu_user_id)
-    if (temp?.dxz_id !== dyaw_xlfw_zxhd?.dxz_id) {
-      dyaw_xlfw_zxhd = temp
-    }
-  }
-
-  timer = setTimeout(async () => {
-    await loop();
-  }, 60 * 1000);
-})();
-
-let infoList = $ref<Array<type_dyaw_xlfw_zxhd_log>>([])
-let inputValue = $ref('')
-let isSending = $ref(false)
-let TinyRef = $ref<typeof import('~/components/tinymce-area/index.vue')['default']>()
-async function handleClickSend() {
-  if (!dyaw_xlfw_zxhd) return;
-  if (isSending) return;
-  isSending = true
-  const reqDate = {
-    dxz_id: dyaw_xlfw_zxhd.dxz_id,
-    dxzl_stu_user_id: dyaw_xlfw_zxhd.dxz_stu_user_id,
-    dxzl_stu_user_realname: dyaw_xlfw_zxhd.dxz_stu_user_realname,
-    dxzl_tea_user_id: dyaw_xlfw_zxhd.dxz_tea_user_id,
-    dxzl_tea_user_realname: dyaw_xlfw_zxhd.dxz_tea_user_realname,
-    dxzl_last_msg_content: encodeURIComponent(inputValue),
-    dxzl_type: inputValue.includes('><img') ? '2' : '1'
-  }
-
-  // infoList.push({
-  //   create_user_id: user.user_id,
-  //   create_dateline: Date.now().toString().slice(0, 10),
-
-  //   ...reqDate
-  // })
-  TinyRef?.clear()
-  // console.log('inputValue :>> ', inputValue);
-  request({
-    url: '/dyaw/xlfw_zxhd_log/add',
-    data: {
-      dyaw_xlfw_zxhd_log: reqDate
-    }
-  }).then(res => {
-    if (res.code === '1') {
-      const fullSendData = {
-        create_user_id: user.user_id,
-        create_dateline: Date.now().toString().slice(0, 10),
-        ...reqDate,
-        dxzl_id: `${res.data.insert_id}`
-      }
-      infoList.push(fullSendData)
-      socketSend(ws!, fullSendData)
-      isSending = false
-    }
-  })
-}
-
-let ws = $ref<WebSocket>()
-
-let cardLoading = $ref(false)
-let unreadNum = $ref(0)
-
-// function getAllUnreadMsg() {
-//   request({
-//     url: '/dyaw/xlfw_zxhd_log/index',
+// let timer: NodeJS.Timeout;
+// (async function loop() {
+//   dyaw_xlfw_zxhd_list = (await request({
+//     url: '/dyaw/xlfw_zxhd/index',
 //     data: {
-//       dxzl_stu_user_id: dyaw_xlfw_zxhd?.dxz_stu_user_id,
-//       dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
-//       limit: 1
+//       dxz_tea_user_id: user.user_id,
+//       dxz_stu_user_realname: searchValue === '搜索' ? undefined : searchValue,
+//       limit: 20,
+//       last_msg: 1
 //     }
-//   }).then(res => {
-//     if (res.code === '1') {
-//       ifScroll = false
-//       infoList = (res.data.page_data.reverse())
-//       unreadNum = 0
+//   })).data.page_data
+//   if (dyaw_xlfw_zxhd !== undefined) {
+//     const temp = dyaw_xlfw_zxhd_list!.find(item => item.dxz_stu_user_id === dyaw_xlfw_zxhd!.dxz_stu_user_id)
+//     if (temp?.dxz_id !== dyaw_xlfw_zxhd?.dxz_id) {
+//       dyaw_xlfw_zxhd = temp
 //     }
-//     nextTick(() => {
-//       ifScroll = true
-//       // scrollbarRef!.scrollTo(0, 0);
-//     })
-//   })
-// }
-
-function handleClickStuCard(stu: type_dyaw_xlfw_zxhd) {
-  cardLoading = true
-  dyaw_xlfw_zxhd = stu
-  unreadNum = parseInt(stu.dxz_unread_msg_num)
-  stu.dxz_unread_msg_num = "0"
-  archivesList = []
-
-  ws?.close()
-  request({
-    url: '/dyaw/xlfw_zxhd_log/index',
-    data: {
-      dxzl_stu_user_id: stu.dxz_stu_user_id,
-      dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
-    }
-  }).then(res => {
-    if (res.code === '1') {
-      infoList = res.data.page_data.reverse()
-      cardLoading = false
-      ws = createSocket(
-        { teacher: user.user_id, student: stu.dxz_stu_user_id },
-        {
-          message(socketRes: TSocketRes<type_dyaw_xlfw_zxhd_log & { $?: boolean, dxz_stu_user_id?: string, dyaw_xlfw_zxhd: type_dyaw_xlfw_zxhd }>) {
-            if (socketRes.from_client_name.endsWith('student')) {
-              if (socketRes.content.$) {
-                if (socketRes.content?.dxz_stu_user_id === dyaw_xlfw_zxhd?.dxz_stu_user_id) {
-                  dyaw_xlfw_zxhd = socketRes.content.dyaw_xlfw_zxhd
-                }
-                return;
-              }
-              infoList.push(socketRes.content)
-              request({
-                url: '/dyaw/xlfw_zxhd_log/index',
-                data: {
-                  dxzl_stu_user_id: stu.dxz_stu_user_id,
-                  dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
-                  limit: 0
-                }
-              })
-
-            }
-          }
-        }
-      )
-
-      handleQueryArchives()
-    }
-  })
-}
+//   }
 
-let ifScroll = $ref(true)
-function handleLoadMoreInfo() {
-  request({
-    url: '/dyaw/xlfw_zxhd_log/index',
-    data: {
-      dxzl_stu_user_id: dyaw_xlfw_zxhd?.dxz_stu_user_id,
-      dxzl_tea_user_id: dyaw_xlfw_zxhd!.dxz_tea_user_id,
-      zxhd_log_id: infoList[0].dxzl_id
-    }
-  }).then(res => {
-    if (res.code === '1') {
-      ifScroll = false
-      if (res.data.page_data.length === 0) return ElMessage.info('暂无更多消息');
-      infoList.unshift(...res.data.page_data.reverse())
-    }
-  })
-}
+//   timer = setTimeout(async () => {
+//     await loop();
+//   }, 60 * 1000);
+// })();
 
-const scrollbarRef = $ref<HTMLElement>()
-function scrollToBottom() {
-  if (!scrollbarRef) return;
-  const scrollHeight = scrollbarRef!.scrollHeight;
-  scrollbarRef!.scrollTo(0, scrollHeight);
-}
-watch(
-  () => (infoList),
-  () => {
-    nextTick(() => {
-      ifScroll && scrollToBottom();
-      ifScroll = true
-    })
-  },
-  {
-    deep: true,
-  }
-)
-
-
-onBeforeUnmount(() => {
-  clearInterval(timer)
-})
-
-const ArchivesCardRef = $ref<typeof import('~/components/archives-card/index.vue')['default']>()
-function handleSubmitArchives() {
-  ElMessageBox.confirm('一次咨询只能提交一次档案,请确认完毕后点击提交', '提示')
-    .then(() => {
-      request({
-        url: '/dyaw/xlfw_xsda_dajl/add',
-        data: {
-          dyaw_xlfw_xsda_dajl: ArchivesCardRef!.form
-        }
-      }).then(res => {
-        if (res.code === '1') {
-
-          handleQueryArchives()
-        }
-      })
-    })
-    .catch(() => {
-      ElMessage({
-        type: 'info',
-        message: '取消提交',
-      })
-    })
-}
-
-let archivesList = $ref<type_archives_item[]>([])
-function handleQueryArchives() {
-  request({
-    url: '/dyaw/xlfw_xsda_dajl/index',
-    data: {
-      user_id: dyaw_xlfw_zxhd!.dxz_stu_user_id
-    }
-  }).then(res => {
-    if (res.code === '1') {
-      archivesList = res.data.page_data
-    }
+function handleClickStuCard(stu: type_dyaw_xlfw_zxhd) {
+  sessionStorage.setItem('dyaw_xlfw_zxhd', JSON.stringify(stu))
+  router.push({
+    name: 'teacher_consult_chat',
   })
 }
 
-// ==========
-// chat audio/video
-// ==========
 
-let RtcDialogRef = $ref<typeof import("~/components/rtc-dialog/index.vue")['default']>()
 
-const ws2 = createSocket(
-  { teacher: user.user_id, student: '*' },
-  {
-    message(socketRes: TSocketRes<type_dyaw_xlfw_zxhd_log & { operate: CHAT_OPERATION }>) {
-      if (socketRes.from_client_name.endsWith('student')) {
-        // infoList.push(socketRes.content)
-        if (socketRes.content.dxzl_tea_user_id === user.user_id) {
-          RtcDialogRef!.publisher(socketRes.content)
-        }
-      }
-    }
-  }
-)
-onMounted(() => {
-  RtcDialogRef!.init(ws2)
-})
-
-
-
-async function handleAudioChatStart() {
-  RtcDialogRef!.open(dyaw_xlfw_zxhd, 'audio')
-}
+// onBeforeUnmount(() => {
+//   clearInterval(timer)
+// })
 
-async function handleVideoChatStart() {
-  RtcDialogRef!.open(dyaw_xlfw_zxhd, 'video')
+function onClickLeft() {
+  router.back()
 }
 
-function emitUpdateInfo(info: type_dyaw_xlfw_zxhd_log, isUpdate?: boolean) {
-  if (!isUpdate) {
-    if (info.dxz_id === dyaw_xlfw_zxhd?.dxz_id)
-      infoList.push(info)
-  }
-  else {
-    const target = infoList.find(item => (item.dxzl_id)?.toString() === info.dxzl_id?.toString())
-    if (target)
-      target.dxzl_last_msg_content = info.dxzl_last_msg_content
-  }
-}
 </script>
 
 <template>
-  <div class="h-590px bg-hex-f2f2f295 flex justify-center divide-x">
-    <div class="w-320px p-10px h-full flex flex-col justify-start bg-white space-y-4">
-      <el-input autocomplete="off" name="searchValue" v-model="searchValue" size="large" :prefix-icon="Search"
-        @focus="searchValue = ''" clearable placeholder="搜索"
-        :style="`${searchValue === '搜索' && '--ep-input-text-color:var(--ep-text-color-placeholder);'}`"></el-input>
-      <div
-        class="max-h-520px flex flex-col flex-none divide-y scrollbar scrollbar-thin scrollbar-thumb-rounded-md scrollbar-thumb-gray-200 scrollbar-track-transparent">
-        <chat-stu-card v-for="item in dyaw_xlfw_zxhd_list" :d="item" @click="handleClickStuCard(item)"
-          class="h-68px"></chat-stu-card>
-      </div>
-    </div>
-    <div class="w-680px h-full divide-y flex flex-col relative">
-      <template v-if="dyaw_xlfw_zxhd">
-        <div class="bg-pink-300 flex justify-between h-48px items-center px-18px">
-          <span class="text-lg px-1 tracking-wider">{{ dyaw_xlfw_zxhd.dxz_stu_user_realname }}
-            {{ dyaw_xlfw_zxhd.dxz_stu_school_name }} {{ dyaw_xlfw_zxhd.dxz_class_name }}</span>
-        </div>
-        <div ref="scrollbarRef"
-          class="bg-hex-fff8fb space-y-2 h-410px py-2 px-6 scrollbar scrollbar-thin scrollbar-thumb-rounded-md scrollbar-thumb-gray-200 scrollbar-track-transparent relative">
-          <div v-show="!cardLoading" @click="handleLoadMoreInfo"
-            class="w-full text-center text-sm text-blue-400 hover:underline underline-blue-400 cursor-pointer">查看更多
-          </div>
-          <info-item v-for="item in infoList" :key="item.dxzl_id" :left="item.create_user_id !== user.user_id" :d="item"
-            :w="500"></info-item>
-
-        </div>
-        <!--  -->
-        <!-- <div v-show="unreadNum > 10"
-                                    class="absolute bg-white right-2 bottom-200px text-blue-400 rounded-l-full cursor-pointer z-100 flex items-center space-x-1 p-2 text-sm"
-                                    @click="getAllUnreadMsg">
-                                    <i:material-symbols:keyboard-double-arrow-up />
-                                    <span>{{ unreadNum }}条消息</span>
-                                  </div> -->
-        <div class="bg-white h-180px p-5px flex flex-col justify-between">
-          <tinymce-area v-model="inputValue" ref="TinyRef" @click:audio="handleAudioChatStart"
-            @click:video="handleVideoChatStart" @click:submit="handleClickSend"></tinymce-area>
-          <div class="flex justify-end">
-            <div class="bg-pink-300 text-sm text-white w-80px h-32px flex_center rounded-2xl cursor-pointer"
-              @click="handleClickSend">发送</div>
-          </div>
-        </div>
-      </template>
-      <template v-else>
-        <div class="bg-pink-300 flex justify-between h-48px items-center px-18px"></div>
-        <div class="bg-hex-fff8fb flex-auto w-full flex_center">
-          <el-empty description="请先选择聊天的学生"></el-empty>
-        </div>
+  <van-nav-bar title="咨询页面" left-text="" left-arrow @click-left="onClickLeft"
+    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;" />
+  <div class="p-10px h-full flex flex-col justify-start bg-white space-y-4">
+    <el-input autocomplete="off" name="searchValue" v-model="searchValue" size="large" :prefix-icon="Search"
+      @focus="searchValue = ''" clearable placeholder="搜索"
+      :style="`${searchValue === '搜索' && '--ep-input-text-color:var(--ep-text-color-placeholder);'}`"></el-input>
+
+    <remote-list url="/dyaw/xlfw_zxhd/index" loop :d="{
+      dxz_tea_user_id: user.user_id,
+      dxz_stu_user_realname: searchValue === '搜索' ? undefined : searchValue,
+      limit: 20,
+      last_msg: 1
+    }" class="mt-2 flex-auto">
+      <template #default="{ row: item }">
+        <chat-stu-card :d="item" @click="handleClickStuCard(item)" class="h-68px"
+          style="border-bottom: 1px solid #f3f4f6;"></chat-stu-card>
       </template>
-
-    </div>
-    <div
-      class="w-400px 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">
-      <div v-if="dyaw_xlfw_zxhd" :key="dyaw_xlfw_zxhd.dxz_stu_user_id">
-        <div class="w-full"
-          v-if="archivesList && (archivesList.length === 0 || dyaw_xlfw_zxhd.dxz_id !== archivesList[0].dxz_id)">
-          <archives-card storage ref="ArchivesCardRef"
-            :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>
-          <div class="flex_center py-2">
-            <el-button @click="handleSubmitArchives" type="primary" size="small">提交</el-button>
-          </div>
-        </div>
-        <div class="bg-hex-FFF7FA py-2 space-y-1">
-          <div class="flex_center text-lg">历史档案</div>
-          <el-empty v-show="archivesList.length === 0" :image-size="60" description="暂无历史档案"></el-empty>
-          <archives-card disabled v-for="item in archivesList" :d="item" :key="item.dxxd_id"></archives-card>
-        </div>
-      </div>
-
-    </div>
+    </remote-list>
   </div>
-
-
-
-  <rtc-dialog ref="RtcDialogRef" @update-info="emitUpdateInfo"></rtc-dialog>
 </template>
 
 <style scoped lang="scss">

+ 47 - 3
src/pages/teacher/home.vue

@@ -1,18 +1,62 @@
 <script setup lang="ts">
+import user from '~/store/user';
+const router = useRouter()
+const route = useRoute()
+
+let isPbTeacher = -1
+await request({
+  url: '/dyaw/xlfw_pbgl/index',
+  data: {
+    user_id: user.user_id
+  }
+}).then(res => {
+  if (res.code === '1') {
+    isPbTeacher = res.data?.page_data?.length
+  }
+})
+
+const list = [
+  { title: '个人中心', route: 'teacher_personal', icon: "user-o", bg: 'linear-gradient(142deg,#69b3fd 15%, #89c4ff 64%);' },
+  { title: '咨询页面', route: 'teacher_consult', icon: "service-o", bg: 'linear-gradient(142deg,#fea200 20%, #ffbd00 71%);' },
+  { title: '学生档案', route: 'teacher_archives', icon: "records", bg: 'linear-gradient(142deg,#14c080 15%, #66ea9e 64%);' }
+]
+
+
+function routerPush(name: string) {
+  router.push({ name })
+}
+
 const imgSrc = (await request({
   url: '/dyaw/xlfw_hbgl/index',
   data: {
     limit: 1
   }
 })).data?.page_data?.[0]?.dxh_path
+
+function onClickLeft() {
+
+}
 </script>
 
 <template>
-  <div class="w-full max-h-590px flex_center overflow-hidden">
-    <img v-show="imgSrc" :src="imgSrc" class="w-full h-full max-h-590px object-contain">
+  <div class=" flex flex-col justify-start ">
+    <van-nav-bar title="心理健康" left-text="" left-arrow @click-left="onClickLeft"
+      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;" />
+    <div class="w-full max-h-590px flex_center overflow-hidden">
+      <img v-show="imgSrc" :src="imgSrc" class="w-full h-full max-h-590px object-contain">
+    </div>
+    <div class="p-2 w-full mt-2">
+      <div class="bg-white w-full flex justify-around items-center shadow">
+        <div v-for="({ title, route, icon, bg }) in list" :key="route" class="py-4 space-y-2" @click="routerPush(route)">
+          <div class="w-75px h-75px rounded-full flex_center text-white" :style="`background:${bg}`">
+            <van-icon :name="icon" size="52"></van-icon>
+          </div>
+          <div class="text-center">{{ title }}</div>
+        </div>
+      </div>
+    </div>
   </div>
 </template>
 
 <style scoped>
-
 </style>

+ 1 - 59
src/pages/teacher/index.vue

@@ -1,67 +1,9 @@
 <script setup lang="ts">
-import user from '~/store/user';
-const router = useRouter()
-const route = useRoute()
-let list = $ref([{ title: '首页', route: 'teacher_home' }]);
-// #ifdef PROD
-await request({
-  url: '/dyaw/xlfw_pbgl/index',
-  data: {
-    user_id: user.user_id
-  }
-}).then(res => {
-  if (res.code === '1') {
-    if (res.data.page_data?.length === 1) {
-      list.push(
-        { title: '咨询页面', route: 'teacher_consult' },
-        { title: '学生档案', route: 'teacher_archives' })
-    }
-  }
-})
-// #endif
 
-
-// #ifdef DEV
-list = [
-  { title: '首页', route: 'teacher_home' },
-  { title: '咨询页面', route: 'teacher_consult' },
-  { title: '学生档案', route: 'teacher_archives' }
-]
-// #endif
-
-function routerPush(name: string) {
-  router.push({ name })
-}
 </script>
 
 <template>
-  <div class="w-1400px h-640px flex flex-col">
-    <div class="flex flex-none space-x-12 text-lg h-50px">
-      <div v-for="item in list" class="cursor-pointer relative mb-22px" @click="routerPush(item.route)"
-        :class="route.name === item.route && 'selected'">{{
-  item.title
-        }}</div>
-    </div>
-    <div class="flex-auto">
-      <router-view></router-view>
-    </div>
-  </div>
+  <router-view></router-view>
 </template>
 
-<style scoped lang="scss">
-.selected {
-  color: #007DFF;
 
-  &::after {
-    content: '-';
-    color: transparent;
-    position: absolute;
-    left: -5px;
-    right: -5px;
-    bottom: -4px;
-    height: 15px;
-    background: rgba(0, 125, 255, 0.30);
-    border-radius: 8px;
-  }
-}
-</style>

+ 101 - 0
src/pages/teacher/personal.vue

@@ -0,0 +1,101 @@
+<script setup lang='ts'>
+import user from '~/store/user';
+import { showSuccessToast, showFailToast } from 'vant';
+// import MUploader from '~/components/m-uploader/index.vue';
+
+const router = useRouter()
+function onClickLeft() {
+  router.back()
+}
+
+// 参数1为一个对象,返回一个新对象,新对象的属性值为原对象中属性值以参数2开头的属性
+const filterObj = (obj: { [x: string]: any; }, prefix: string) => {
+  const newObj: { [x: string]: any; } = {}
+  for (const key in obj) {
+    if (key.startsWith(prefix)) {
+      if (key === 'dxp_pjxj') {
+        newObj[key] = obj[key] * 1
+        continue
+      }
+      newObj[key] = obj[key]
+    }
+  }
+  return newObj
+}
+
+const form = reactive(filterObj((await request({
+  url: '/dyaw/xlfw_pbgl/detail',
+  data: {
+    user_id: user.user_id
+  }
+}))?.data?.one_info, 'dxp_'))
+console.log('form : ', form)
+
+let readonly = $ref(true)
+const FormRef = $ref()
+
+async function onClickRight() {
+  if (!readonly) {
+    try {
+      await FormRef?.validate()
+    } catch (error) {
+      showFailToast('校验未通过')
+      return
+    }
+
+    await request({
+      url: '/dyaw/xlfw_pbgl/edit',
+      data: {
+        dxp_id: form.dxp_id,
+        dyaw_xlfw_pbgl: form
+      }
+    }).then(res => {
+      if (res.code === '1') {
+        showSuccessToast('保存成功')
+      } else {
+        showFailToast(res.msg)
+      }
+    }).catch(err => {
+      showFailToast(`error: ${err}`)
+    })
+  }
+  readonly = !readonly
+
+}
+</script>
+
+<template>
+  <div>
+    <van-nav-bar title="个人中心" left-text="" left-arrow @click-left="onClickLeft"
+      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;"
+      :right-text="readonly ? '编辑' : '保存'" @click-right="onClickRight" />
+
+    <van-form :readonly="readonly" ref="FormRef">
+      <van-cell-group>
+        <van-field name="头像" label="头像">
+          <template #input>
+            <m-uploader v-model="form.dxp_user_avatar" :max-count="1" />
+          </template>
+        </van-field>
+        <van-field v-model="form.dxp_user_name" name="账号" label="账号" :rules="[{ required: true, message: '请填写账号' }]" />
+        <van-field v-model="form.dxp_user_realname" name="姓名" label="姓名"
+          :rules="[{ required: true, message: '请填写姓名' }]" />
+        <van-field v-model="form.dxp_user_phone" name="联系电话" label="联系电话"
+          :rules="[{ required: true, message: '请填写正确的联系电话', pattern: /^1[3456789]\d{9}$/ }]" />
+        <van-field name="微信二维码" label="微信二维码">
+          <template #input>
+            <m-uploader v-model="form.dxp_wx_qrcode" :max-count="1" />
+          </template>
+        </van-field>
+        <van-field v-model="form.dxp_bzxcs" name="被咨询次数" label="被咨询次数" readonly />
+        <van-field v-model="form.dxp_jdsc" name="接待时长" label="接待时长" readonly />
+        <van-field name="平均星级" label="平均星级" readonly>
+          <template #input>
+            <van-rate v-model="form.dxp_pjxj" readonly />
+          </template>
+        </van-field>
+        <van-field v-model="form.dxp_jj" name="简介" label="简介" rows="2" autosize type="textarea" />
+      </van-cell-group>
+    </van-form>
+  </div>
+</template>

+ 10 - 0
src/router/routes/teacher.ts

@@ -8,11 +8,21 @@ export default {
       component: () => import('@/pages/teacher/home.vue')
     },
     {
+      path: 'personal',
+      name: 'teacher_personal',
+      component: () => import('@/pages/teacher/personal.vue')
+    },
+    {
       path: 'consult',
       name: 'teacher_consult',
       component: () => import('@/pages/teacher/consult.vue')
     },
     {
+      path: 'clist',
+      name: 'teacher_consult_chat',
+      component: () => import('@/pages/teacher/chat-list.vue')
+    },
+    {
       path: 'archives',
       name: 'teacher_archives',
       component: () => import('@/pages/teacher/archives.vue'),

+ 5 - 0
src/store/user.ts

@@ -11,6 +11,7 @@ localStorage.setItem(
     dictionary[location.port]
   )
 )
+
 // #endif
 
 let user = reactive<IUser>(JSON.parse(localStorage.getItem('userInfo') as string))
@@ -19,3 +20,7 @@ export default user
 
 const ROLE_MAP: { [key: string]: 'teacher' | 'student' } = { '72': 'teacher', '75': 'teacher', '76': 'student' }
 export const UserRole: 'teacher' | 'student' | 'admin' | 'other' = user.sm_info.sm_id === '739' ? 'admin' : (ROLE_MAP[user.user_role_id] ?? 'other')
+
+// #ifdef DEV
+document.title = `${UserRole.toLocaleUpperCase()}-${document.title}`
+// #endif

+ 12 - 0
src/utils/helper.ts

@@ -0,0 +1,12 @@
+export function resolveFileString(str) {
+  return str ? str.split(';').map((s) => resolveSingleFileString(s)) : []
+}
+
+export function resolveSingleFileString(url) {
+  // const [name, url] = str.split(',')
+  return {
+    // name,
+    url: url.startsWith('http') ? url : window.GLOBAL_CONFIG.oss + '/' + url,
+    // origin: url
+  }
+}

+ 4 - 2
src/utils/request.ts

@@ -1,6 +1,7 @@
 import axios from 'axios'
 import type { AxiosRequestConfig } from 'axios'
-import { ElMessage } from 'element-plus'
+// import { ElMessage } from 'element-plus'
+import { showConfirmDialog, showSuccessToast, showFailToast } from 'vant';
 import user from '~/store/user'
 const token = user.token
 
@@ -51,7 +52,8 @@ instance.interceptors.response.use(
     response.data.msg = response.data.msg.replaceAll(/<.*?>/g, ' ')
     const { code, msg } = response.data
     if (code !== '1')
-      ElMessage.error(msg)
+      // ElMessage.error(msg)
+      showFailToast(msg)
 
     return response.data
   },