瀏覽代碼

聊天调整

zhengnaiwen_citu 7 月之前
父節點
當前提交
57e8b3e4d1
共有 6 個文件被更改,包括 516 次插入63 次删除
  1. 12 0
      api/common.js
  2. 0 1
      hooks/useIM.js
  3. 3 3
      layout/components/authModal/login/index.vue
  4. 16 8
      pages/index/communicate.vue
  5. 477 47
      pagesA/chart/index.vue
  6. 8 4
      utils/avatar.js

+ 12 - 0
api/common.js

@@ -245,4 +245,16 @@ export const deleteConversation = async (data) => {
       auth: true
     }
   })
+}
+
+// 求职端-根据邀请人id获取面试邀约列表
+export const getInterviewInviteListByInviteUserId = async (inviteUserId) => {
+  return request({
+    url: `/app-api/menduner/system/interview-invite/get/list/by/${inviteUserId}`,
+    method: 'GET',
+    custom: {
+      showLoading: false,
+      auth: true
+    }
+  })
 }

+ 0 - 1
hooks/useIM.js

@@ -96,7 +96,6 @@ export function useDataSource () {
 
     const resultConversations = []
     const resp = await getConversationSync(query)
-    // console.log(resp)
     const { data:conversationList } = resp
     if (conversationList) {
       conversationList.forEach(conversation => {

+ 3 - 3
layout/components/authModal/login/index.vue

@@ -51,7 +51,7 @@
 
       <button class="send-button" @tap="handleLogin"> 登录/注册 </button>
       <view class="agreement-box ss-flex ss-row-center">
-        <uni-icons size="20" :type="protocol ? 'checkbox-filled' : 'circle'" :color="protocol ? '#00897B' : '#ccc'" @tap.stop="protocol = !protocol"></uni-icons>
+        <uni-icons size="20" :type="protocol ? 'checkbox-filled' : 'circle'" :color="protocol ? '#00897B' : '#ccc'" @tap="protocol = !protocol"></uni-icons>
         <view class="color-999 ss-flex ss-col-center ss-m-l-8 font-size-13">
           我已阅读并遵守
           <view class="color-primary" @tap.stop="handleToDetail('user')">
@@ -87,8 +87,8 @@ const state = ref({
     code: ''
   },
   account: {
-    phone: '13229740091',
-    password: 'Citu123'
+    phone: '13229740092',
+    password: 'Citu123456'
   },
   rules: {
     phone: mobile,

+ 16 - 8
pages/index/communicate.vue

@@ -69,14 +69,22 @@ onShow(() => {
 
 
 
-const handleTo = ({ userInfoVo, thatName, postNameCn, enterpriseAnotherName  }) => {
-	const id = `id=${userInfoVo.userInfoResp.userId}`
-	const name = `name=${thatName}`
-	const postName = `postName=${postNameCn}`
-	const enterpriseName = `enterpriseName=${enterpriseAnotherName}`
-	const enterpriseId = `enterpriseId=${userInfoVo?.userInfoResp?.enterpriseId}`
+const handleTo = ({ userInfoVo, thatName, postNameCn, enterpriseAnotherName, channel_id }) => {
+	const query = {
+		id: userInfoVo?.userInfoResp?.userId,
+		name: thatName,
+		postName: postNameCn,
+		enterpriseName: enterpriseAnotherName,
+		enterpriseId: userInfoVo?.userInfoResp?.enterpriseId,
+		channelId: channel_id,
+		avatar: userInfoVo?.userInfoResp?.avatar,
+		sex: userInfoVo?.userInfoResp?.sex,
+	}
+	const queryStr = Object.keys(query).reduce((r, v) => {
+		return r += `${v}=${encodeURIComponent(query[v])}&`
+	}, '?')
 	uni.navigateTo({
-    url: `/pagesA/chart/index?${id}&${name}&${postName}&${enterpriseName}&${enterpriseId}`
+    url: `/pagesA/chart/index${queryStr.slice(0, -1)}`
   })
 }
 
@@ -97,7 +105,7 @@ async function init () {
 		}
 		items.value = data.map(item => {
 			return {
-				thatName: item.userInfoVo ? (item.userInfoVo.userInfoResp.name ? item.userInfoVo.userInfoResp.name : '游客') : '系统消息',
+				thatName: item.userInfoVo ? (item.userInfoVo.userInfoResp?.name ? item.userInfoVo.userInfoResp.name : '游客') : '系统消息',
 				enterpriseAnotherName: item.userInfoVo?.userInfoResp?.enterpriseAnotherName ?? '',
 				postNameCn: item.userInfoVo?.userInfoResp?.postNameCn ?? '',
 				...item

+ 477 - 47
pagesA/chart/index.vue

@@ -1,113 +1,441 @@
 <template>
   <view class="box">
     <view class="box-top">
-      {{ info.name }}
-      <text class="subText">
-        {{ info.postName }}
-        <text v-if="info.postName && info.enterpriseName" class="gun">|</text>
-        {{ info.enterpriseName }}
-      </text>
+      <view class="box-top-title">
+        {{ info.name }}
+        <text class="subText">
+          {{ info.postName }}
+          <text v-if="info.postName && info.enterpriseName" class="gun">|</text>
+          {{ info.enterpriseName }}
+        </text>
+      </view>
+      <view class="box-top-content" v-if="interview.length">
+        <view v-for="val in interview" :key="val.id" class="color-666">
+          <view class="box-top-content-t">
+            <view class="font-weight-bold color-primary">
+              <text>{{ val.job.name }}</text>
+              <text v-if="!val.job.payFrom && !val.job.payTo" class="ml-3">面议</text>
+              <text v-else class="ml-3">{{ val.job.payFrom ? val.job.payFrom + '-' : '' }}{{ val.job.payTo }}</text>
+            </view>
+            <view :style="{'color': ['5', '98', '99'].includes(val.status) ? '#FE574A' : '#0E8E80'}">
+              {{ showStatus(val.status) }}
+            </view>
+          </view>
+          <view class="mt-1 font-size-14 ellipsis" style="max-width: 100%;">
+            <view class="py-1">面试时间:{{ timesTampChange(val.time, 'Y-M-D h:m') }}</view>
+            <view class="py-1">面试地点:{{ val.address }}</view>
+            <view class="py-1">联系电话:{{ val.invitePhone }}</view>
+          </view>
+          <view class="bottom">
+            <view class="tipsText" @click="handleToCenter">在“个人中心-面试”中管理我的面试</view>
+            <view v-if="val.status === '0'">
+              <v-btn class="mr-3" variant="outlined" color="error" size="small" @click="handleRefuse(val)">拒绝邀请</v-btn>
+              <v-btn variant="outlined" color="primary" size="small" @click="handleAgree(val)">接受邀请</v-btn>
+            </view>
+          </view>
+        </view>
+      </view>
     </view>
     <view class="box-main"  ref="chatRef">
+      <view class="box-main-more" v-if="hasMore">
+        <text @click="handleMore">查看更多</text>
+      </view>
       <view v-for="val in items" :key="val.id">
         <view class="box-main-time">{{ timesTampChange(+(val.timestamp.padEnd(13, '0'))) }}</view>
+        <template v-if="val.payload?.type === 102">
+          <view class="jobCard">
+            <view class="jobCard-title"> {{ val.payload?.content?.positionInfo?.name }}</view>
+            <view
+              v-if="!val.payload?.content?.positionInfo?.payFrom && !val.payload?.content?.positionInfo?.payTo"
+              class="jobCard-subtitle">
+              薪酬待遇: 面议
+            </view>
+            <view
+              v-else
+              class="jobCard-subtitle"
+            >
+              薪酬待遇: 
+              {{ val.payload?.content?.positionInfo?.payFrom ? val.payload?.content?.positionInfo?.payFrom + ' - ' : '' }}
+              {{ val.payload?.content?.positionInfo?.payTo }}
+            </view>
+            <view class="jobCard-tag">
+              <view
+                v-for="(v, i) in (val.payload?.content?.positionInfo?.enterprise?.welfareList || [])"
+                :key="val.message_id + v + i"
+                style="margin: 10rpx"
+              >
+                <uni-tag
+                  :text="v"
+                  type="success"
+                />
+              </view>
+            </view>
+            <view class="jobCard-divider"></view>
+            <view class="jobCard-subtitle text-right">
+              <v-avatar size="24">
+                <v-img :src="val.payload?.content?.positionInfo?.contact?.avatar"></v-img>
+              </v-avatar>
+              {{ val.payload?.content?.positionInfo?.contact?.name }}
+              {{ val.payload?.content?.positionInfo?.contact?.postNameCn }}
+              {{ val.payload?.content?.positionInfo?.enterprise?.name }}
+            </view>
+            <div class="jobCard-subtitle text-right">
+              地址:{{ val.payload?.content?.positionInfo?.address }}
+            </div>
+          </view>
+        </template>
         <view :class="['message-view_item', val.from_uid === IM.uid ? 'is-self' : 'is-other']">
           <view class="image">
-            
             <image
+              :data-target="getUserAvatar(info.avatar, info.sex)"
               class="header"
-              :src="(val.from_uid === IM.uid ? mAvatar : getUserAvatar(info.avatar, info.sex)) || 'https://minio.citupro.com/dev/menduner/7.png'"
+              :src="(
+                val.from_uid === IM.uid ?
+                getUserAvatar(useUserStore.baseInfo?.avatar, useUserStore.baseInfo?.sex) :
+                getUserAvatar(info.avatar, info.sex)
+              )"
             ></image>
           </view>
-          <view class="message-text" :class="{ active: val.from_uid === IM.uid}">
+          <!-- 显示沟通职位 -->
+          <template v-if="val.payload?.type === 102">              
+            <view class="message-text" :class="val.from_uid === IM.uid ? 'active' : ''">
+              {{ val.payload?.content.text }}
+            </view>
+          </template>
+          <!-- 发起面试邀请 -->
+          <view class="message-text none" v-else-if="val.payload?.type === 101">
+            <uni-tag text="发起了面试邀请" type="primary" />
+          </view>
+          <view class="message-text none" v-else-if="val.payload?.type === 103">
+            <uni-tag text="拒绝了面试邀请" type="error" />
+          </view>
+          <view class="message-text none" v-else-if="val.payload?.type === 104">
+            <uni-tag text="接受了面试邀请" type="success" />
+          </view>
+          <view v-else-if="val.payload.type === 105" class="text-end">
+            <uni-tag
+              v-if="val.from_uid === IM.uid"
+              :text="val.payload.content?.type === 1 ? '附件简历已发送' : '简历请求已发送'"
+              type="success"
+            />
+            <view
+              v-if="val.payload.content?.type !== 2 || val.from_uid !== IM.uid"
+              class="message-text card"
+            >
+              <view class="text-left">
+                <text v-if="val.payload.content?.type === 1">{{
+                  val.payload.content?.query?.title || '附件简历' }}
+                </text>
+                <text v-if="val.payload.content?.type === 2">
+                  我想要一份您的简历,您是否同意
+                </text>
+              </view>
+              <view class="btn-actions">
+                <text class="btn" v-if="val.payload.content?.type === 1" @tap="handlePreview(val.payload)">点击预览附件简历</text>
+                <text class="btn" v-if="val.payload.content?.type === 2" @tap="handleFindResume">点击发送附件简历</text>
+              </view>
+            </view>
+          </view>
+          <view v-else class="message-text" :class="{ active: val.from_uid === IM.uid}">
             {{ val.payload?.content }}
           </view>
         </view>
       </view>
     </view>
     <view class="box-bottom">
+      <view class="box-bottom-tool">
+        <uni-tag text="发送简历" type="success" @tap="handleFindResume"/>
+      </view>
+      
       <textarea
         auto-height
         confirm-type="send"
         @confirm="handleSend"
       />
     </view>
+    <uni-popup ref="popup" background-color="#fff">
+      <view class="popup-title">
+        <text>请选择简历</text>
+        <uni-icons type="closeempty" size="20" @tap="handleClose"></uni-icons>
+      </view>
+      <view v-for="resume in resumeList" :key="resume.id" class="popup-content" @tap="resumeCheck = resume">
+        <view class="iconBox">
+          <uni-icons
+            v-show="resumeCheck.id === resume.id"
+            type="checkmarkempty"
+            size="20"
+            :color="resumeCheck.id === resume.id ? '#43AC57' : '#999'"></uni-icons>
+        </view>
+        <text class="text" :class="resumeCheck.id === resume.id ? 'active' : ''">{{ resume.title }}</text>
+      </view>
+      <view class="popup-actions">
+        <button class="default" type="default" @click="handleSendResume">发送简历</button>
+      </view>
+    </uni-popup>
   </view>
 </template>
 
 <script setup>
-import { ref, nextTick } from 'vue'
+import { ref, nextTick, watch, computed } from 'vue'
 import { useIMStore } from '@/store/im'
 import { userStore } from '@/store/user'
 import { timesTampChange } from '@/utils/date'
 import { getUserAvatar } from '@/utils/avatar'
 import { initConnect, send, initChart, getMoreMessages, checkConversation } from '@/hooks/useIM'
 import { onLoad } from '@dcloudio/uni-app'
+import { getPersonResumeCv } from '@/api/user'
+import { getInterviewInviteListByInviteUserId } from '@/api/common'
+import { getDict } from '@/hooks/useDictionaries'
 
 const useUserStore = userStore()
 const IM = useIMStore()
-const mAvatar = getUserAvatar(useUserStore.baseInfo?.avatar, useUserStore.baseInfo?.sex)
 const info = ref({})
 const chatRef = ref()
 const items = ref([])
+const channelItem = ref(null)
+const hasMore = ref(false)
+
+const popup = ref()
+const resumeCheck = ref({})
+const resumeList = ref([]) // 简历列表
+
+const pageSize = ref(1)
 
+// 求职者面试列表
+const interview = ref([])
+// 求职端-获取求职者与当前邀请人的面试记录
+const statusList = ref([])
+
+const {
+  conversationList,
+  updateConversation,
+  updateUnreadCount,
+  deleteConversations,
+  resetUnread
+} = initConnect(async (successful) => {
+  if (!successful) {
+    Snackbar.error('发送失败')
+    return
+  }
+  // chatRef.value.reset()
+  // // 发送成功
+  const { list } = await getMoreMessages(1, channelItem.value)
+  // updateConversation()
+  items.value = list.value
+  // chatRef.value.scrollBottom()
+})
+
+watch(
+  () => conversationList.value,
+  async (val) => {
+    if (!channelItem.value) {
+      return
+    }
+    const { list } = await getMoreMessages(1, channelItem.value)
+    items.value = list.value
+    // if (Object.keys(info.value).length) updateUnreadMessageCount(val)
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
 
 async function init(userId, enterpriseId) {
   const { channel, list, more } = await initChart(userId, enterpriseId)
-  console.log(channel.value, list.value, more)
+  hasMore.value = more
+  channelItem.value = channel.value
   items.value = list.value
-  setTimeout(() => {
-    scrollBottom()
-    console.log('滚动')
-  }, 1500)
+}
+
+function showStatus (status) {
+  return statusList.value.find(e => e.value === status)?.label
+}
+
+async function getInterviewInviteList () {
+  if (!info.value.id) return
+  const { data } = await getInterviewInviteListByInviteUserId(info.value.id)
+  interview.value = data.slice(0, 1)
+}
+
+const getStatusList = () => {
+  getDict('menduner_interview_invite_status').then(({data}) => {
+    if (data.length) statusList.value = data
+  })
 }
 
 function handleSend () {
   console.log('发送')
 }
 
-function scrollBottom () {
-  const chat = chatRef.value
-  if (chat) {
-    nextTick(function () {
-      console.log(chat.scrollHeight)
-      chat.scrollTop = chat.scrollHeight
-      uni.pageScrollTo({
-        scrollTop: 0,
-        duration: 300
-      })
+const getFileType = (url) => {  
+  const extension = url.split('.').pop().toLowerCase();  
+  switch (extension) {  
+    case 'pdf':  
+      return 'pdf';  
+    case 'doc':  
+    case 'docx':  
+      return 'doc';  
+    case 'xls':  
+    case 'xlsx':  
+      return 'xls';  
+    case 'ppt':  
+    case 'pptx':  
+      return 'ppt';  
+    case 'txt':  
+      return 'txt';  
+    default:  
+      return ''; // 不支持的文件类型  
+  }  
+}
+// 预览简历
+function handlePreview (payload) {
+  // 使用 wx.downloadFile 下载文档  
+  wx.downloadFile({  
+    url: payload.content.query.src,  
+    success: function (res) {  
+      // 下载成功后,打开文档  
+      if (res.statusCode === 200) {  
+        const fileType = getFileType(payload.content.query.src); // 动态获取文件类型  
+        if (fileType) {  
+          wx.openDocument({  
+            filePath: res.tempFilePath,  
+            fileType: fileType,  
+            success: function () {  
+              console.log('打开文档成功');  
+            },  
+            fail: function (error) {  
+              console.error('打开文档失败', error);  
+            }  
+          });  
+        } else {  
+          console.error('不支持的文件类型');  
+        }  
+      }  
+    },  
+    fail: function (error) {  
+      console.error('下载文件失败', error);  
+    }  
+  })
+}
+// 获取简历
+async function handleFindResume () {
+  uni.showLoading({
+    title: '真正查找简历'
+  })
+  try {
+    // 获取简历列表
+    const { data } = await getPersonResumeCv()
+    if (data.length === 0) {
+      Snackbar.error('您还未上传过简历,请先上传简历后再投递')
+      return
+    }
+    resumeList.value = data
+    resumeCheck.value = data[0]
+    popup.value.open('center')
+  } finally {
+    uni.hideLoading()
+  }
+}
+
+function handleClose () {
+  popup.value.close()
+}
+
+function handleSendResume () {}
+
+async function handleMore () {
+  try {
+    uni.showLoading({
+      title: '加载中...'
     })
+    pageSize.value++
+    const { list, more } = await getMoreMessages(pageSize.value, channelItem.value)
+    items.value.unshift(...list.value)
+    hasMore.value = more
+  } finally {
+    uni.hideLoading()
   }
 }
 
-onLoad((options) => {
-  info.value = options
+onLoad(async (options) => {
+  info.value = Object.keys(options).reduce((r, k) => {
+    r[k] = decodeURIComponent(options[k])
+    return r
+  }, {})
+  getStatusList()
+  getInterviewInviteList()
   init(options.id, options.enterpriseId)
 })
 </script>
 
 <style lang="scss" scoped>
+.white {
+  color: #FFF !important;
+}
+.text-left {
+  text-align: left !important;
+}
+.text-right {
+  text-align: right !important;
+}
 .box {
   width: 100%;
-  height: calc(100vh - 88rpx);
+  height: 100vh;
   display: flex;
   flex-direction: column;
   &-top {
-    padding: 0 60rpx;
-    box-sizing: border-box;
-    width: 100%;
-    height: 80rpx;
-    line-height: 80rpx;
-    // text-align: center;
-    border-bottom: 2rpx solid #EEE;
-    overflow: hidden;
-    white-space: nowrap;
-    text-overflow: ellipsis;
-    .subText {
-      font-size: .85em;
-      color: #999;
-      .gun {
-        padding: 0 10rpx;
+    &-title {
+      padding: 0 60rpx;
+      box-sizing: border-box;
+      width: 100%;
+      height: 80rpx;
+      line-height: 80rpx;
+      // text-align: center;
+      border-bottom: 2rpx solid #EEE;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      .subText {
+        font-size: .85em;
+        color: #999;
+        .gun {
+          padding: 0 10rpx;
+        }
+      }
+    }
+    &-content {
+      padding: 20rpx 50rpx;
+      padding-bottom: 20rpx;
+      border-bottom: 2rpx solid #eee;
+      .color-666 {
+        color: #666;
+      }
+      .font-weight-bold {
+        font-weight: bold;
+      }
+      .color-primary {
+        color: #009688;
+      }
+      .ml-3 {
+        margin-left: 40rpx;
+      }
+      .mt-1 {
+        margin-top: 12rpx;
+      }
+      .font-size-14 {
+        font-size: 24rpx;
+      }
+      .py-1 {
+        padding: 4rpx 0;
+      }
+      .tipsText {
+        font-size: .75em;
+        color: #999;
+      }
+      &-t {
+        display: flex;
+        justify-content: space-between;
       }
     }
   }
@@ -117,6 +445,14 @@ onLoad((options) => {
     height: 0;
     padding: 40rpx;
     overflow-y: auto;
+    &-more {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      color: #24bc3e;
+      font-size: .9em;
+      padding: 20rpx 0;
+    }
     &-time {
       user-select: none;
       position: relative;
@@ -129,6 +465,31 @@ onLoad((options) => {
       font-size: .85em;
       color: #999;
     }
+    .jobCard {
+      padding: 30rpx;
+      background: #E2F2F0;
+      color: #009688 ;
+      margin-top: 20rpx;
+      max-width: unset;
+      margin-right: 0;
+      &-title {
+        font-size: 1.2em;
+      }
+      &-subtitle {
+        padding: 10rpx 0;
+        // font-size: .5em;
+      }
+      &-divider {
+        width: 100%;
+        height: 2rpx;
+        margin: 20rpx 0;
+        background: #ddd;
+      }
+      &-tag {
+        display: flex;
+        flex-wrap: wrap;
+      }
+    }
     .message-view_item {
       display: flex;
       flex-direction: row;
@@ -147,15 +508,45 @@ onLoad((options) => {
           height: 60rpx;
         }
       }
+      .text-end {
+        text-align: right !important;
+        width: 400rpx;
+        margin-right: 20rpx;
+      }
       .message-text {
         overflow-wrap: break-word;
         background-color: #f0f2f5;
         border-radius: 12rpx;
-        max-width: 80%;
+        max-width: 75%;
         padding: 20rpx;
         &.active {
           background: #d5e6e8;
         }
+        &.card {
+          background: #E2F2F0;
+          color: #009688 ;
+          margin-top: 20rpx;
+          max-width: unset;
+          margin-right: 0;
+          .btn-actions {
+            margin: 40rpx auto 20rpx auto ;
+            text-align: center;
+            .btn {
+              padding: 10rpx 30rpx;
+              background: #C8E7D8;
+              color: #43AC57;
+              font-size: .75em;
+              border-radius: 10rpx;
+            }
+          }
+        }
+        &.none {
+          padding: 10rpx 0;
+          background-color: unset;
+        }
+        &.active {
+          background: #d5e6e8;
+        }
       }
     }
     .is-self {
@@ -174,11 +565,50 @@ onLoad((options) => {
   &-bottom {
     max-height: 300rpx;
     border-top: 2rpx solid #EEE;
+    background: rgba(230, 230, 230, 0.5);
+    padding: 20rpx 40rpx;
+    box-sizing: border-box;
+    &-tool {
+      margin-bottom: 40rpx;
+    }
     textarea {
+      border-radius: 10rpx;
       width: 100%;
-      min-height: 40rpx;
+      min-height: 80rpx;
+      max-height: 180rpx;
       padding: 20rpx;
-      background: rgba(211, 211, 211, 0.05);
+      box-sizing: border-box;
+      background: #FFF;
+    }
+  }
+  .popup-title {
+    padding: 30rpx 20rpx;
+    display: flex;
+    justify-content: space-between;
+    border-bottom: 2rpx solid #DDD;
+  }
+  .popup-content {
+    padding: 20rpx 40rpx;
+    color: #999;
+    display: flex;
+    align-content: center;
+    justify-items: center;
+    .iconBox {
+      width: 40rpx;
+    }
+    .text {
+      margin-left: 20rpx;
+      &.active {
+        color: #43AC57;
+      }
+    }
+  }
+  .popup-actions {
+    padding: 60rpx;
+    .default {
+      background: #43AC57;
+      color: #DDD;
+      font-size: .9em;
     }
   }
 }

+ 8 - 4
utils/avatar.js

@@ -3,8 +3,12 @@ const female = 'https://minio.citupro.com/dev/menduner/7.png'
 
 // 根据性别返回默认头像
 export const getUserAvatar = (avatar, sex) => {
-  if (avatar) return avatar
-  if (!sex) return female
-  if (sex === '1') return male
-  else return female
+  if (avatar) {
+    return avatar
+  }
+  // if (!sex) return female
+  if (sex === '1') {
+    return male
+  }
+  return female
 }