| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564 | <template>  <div class="default-width message" :style="`height: calc(100vh - ${isEnterprise ? '130px' : '50px'});`">    <div class="message-left">      <div class="message-left-search d-flex align-center px-3 justify-space-between" >        <div>          <v-icon class="mr-3">mdi-history</v-icon>          最近联系人        </div>        <div>          <v-btn            density="compact"            :color="showDelete ? '' : 'red'"            :icon="showDelete ? 'mdi-close' : 'mdi-trash-can-outline'"            variant="text"            @click="showDelete = !showDelete"          >          </v-btn>        </div>        <!-- {{ connected ? '连接成功': '连接失败' }} -->      </div>      <div class="message-chat-box mt-5">        <v-overlay          :model-value="!IM.connected"          contained          class="align-center justify-center"        >          <v-progress-circular            color="primary"            size="64"            indeterminate          ></v-progress-circular>        </v-overlay>        <div v-if="conversationList.length">          <v-list density="compact" mandatory @update:selected="handleChange">            <v-list-item              v-for="(val, i) in conversationList"              :key="i"              :value="val"              color="primary"              class="mb-2"              :active="val.channel.channelID === info?.channel?.channelID"              :title="val.userInfoVo ? (val.userInfoVo.userInfoResp.name ? val.userInfoVo.userInfoResp.name : '游客') : '系统消息'"              :subtitle="timesTampChange(+val.timestamp.padEnd(13, '0'))"            >              <template v-slot:subtitle="{ subtitle }">                <div class="mt-2">{{ subtitle }}</div>              </template>              <template v-slot:prepend>                <v-avatar :image="getUserAvatar(val?.userInfoVo?.userInfoResp?.avatar, val?.userInfoVo?.userInfoResp?.sex)"></v-avatar>              </template>              <template v-slot:append>                <v-badge                  v-if="val.unread > 0"                  color="error"                  :content="val.unread"                  inline                ></v-badge>                <v-btn v-show="showDelete" density="compact" icon="mdi-trash-can-outline" variant="text" color="red" @click.stop="handleDelete(val)"></v-btn>              </template>            </v-list-item>          </v-list>          <div class="message-no-more-text mt-3">没有更多了</div>        </div>        <div v-else class="left-noData">          <Empty :elevation="false" message="暂无30天内联系人" width="300" height="150"></Empty>        </div>      </div>    </div>    <div class="message-right">      <div v-if="showRightNoData" class="right-noData">        <Empty :elevation="false" message="与您进行过沟通的 Boss 都会在左侧列表中显示"></Empty>      </div>      <Chatting        ref="chatRef"        :items="messageItems"        :info="info"        :interview="interview"        :has-more="hasMore"        @handleSend="handleUpdate"        @handleMore="handleGetMore"        @handleAgree="handleAgree"        @handleRefuse="handleRefuse"        @handlePreview="handlePreview"        @handleSendResume="handleSendResume"      >        <template #tools>          <v-btn            v-for="tool in tools"            :key="tool.name"            size="small"            class="mr-3"            :color="tool.color"            @click="tool.handle(tool)"          >            <v-progress-circular              v-if="tool.loading"              :width="2"              :size="16"              color="white"              class="mr-2"              indeterminate            ></v-progress-circular>            <v-icon v-else class="mr-2">{{ tool.icon }}</v-icon>            {{ tool.name }}          </v-btn>        </template>      </Chatting>    </div>  </div>  <File ref="uploadFile" @success="handleUploadResume"></File>  <CtDialog :visible="showInvite" :widthType="2" titleClass="text-h6" title="邀请面试" @close="showInvite = false" @submit="handleSubmit">    <InvitePage v-if="showInvite" ref="inviteRef" :item-data="itemData" :position="positionList"></InvitePage>  </CtDialog>  <CtDialog :visible="showResume" :widthType="2" titleClass="text-h6" title="发送简历" @close="showResume = false; selectResume = null " @submit="handleSubmitResume">        <div style="position: relative; min-height: 200px">      <v-radio-group v-model="selectResume">        <v-radio v-for="val in resumeList" :key="val.id" :value="val.id" :label="val.title" color="primary"></v-radio>      </v-radio-group>    </div>      </CtDialog></template><script setup>defineOptions({ name: 'personal-message-index'})import InvitePage from '@/views/recruit/enterprise/interview/components/invite'import { timesTampChange } from '@/utils/date'import { ref, inject, watch,onMounted, nextTick } from 'vue'import Chatting from './components/chatting.vue'import { initConnect, send, initChart, getMoreMessages, checkConversation } from '@/hooks/web/useIM'import { useRoute } from 'vue-router'import { getPositionDetails } from '@/api/position'import { getInterviewInviteListByInviteUserId } from '@/api/common'import { getUserInfo } from '@/api/personal/user'import { useIMStore } from '@/store/im'import { useUserStore } from '@/store/user'import Snackbar from '@/plugins/snackbar'import { getUserAvatar } from '@/utils/avatar'import { getJobAdvertised } from '@/api/enterprise'import { dealDictArrayData } from '@/utils/position'import { saveInterviewInvite } from '@/api/recruit/enterprise/interview'import { savePersonResumeCv } from '@/api/recruit/personal/resume'import { useI18n } from '@/hooks/web/useI18n'import { userInterviewInviteReject, userInterviewInviteConsent } from '@/api/recruit/personal/personalCenter'import { getPersonResumeCv } from '@/api/recruit/personal/resume'import { previewFile } from '@/utils'import Confirm from '@/plugins/confirm'const { t } = useI18n()const chatRef = ref()const IM = useIMStore()// 自己的信息const { baseInfo } = useUserStore()const isEnterprise = inject('isEnterprise')// 实例const route = useRoute()const channelItem = ref(null)const messageItems = ref([])const pageSize = ref(1)const hasMore = ref(false)const showInvite = ref(false)const positionList = ref([])const showDelete = ref(false)const itemData = ref({})const inviteRef = ref()// 发送简历const uploadFile = ref()const showResume = ref(false)const resumeList = ref([])const selectResume = ref(null)// 求职者面试列表const interview = ref([])const showRightNoData = ref(false)const info = ref({})const enterpriseTools = ref([  { name: '求简历', icon: 'mdi-email', color:"primary", loading: false, handle: handleRequest },  { name: '面试邀约', icon: 'mdi-email', color:"primary", loading: false, handle: handleInvite }])const userTools = ref([  { name: '发送简历', icon: 'mdi-email', color:"primary", loading: false, handle: handleSendResume }])const tools = isEnterprise ? enterpriseTools.value : userTools.valueif (!IM) {  console.log('IM is disconnected')}if (route.query.id) {  const api = route.query.enterprise ? getPositionDetails : getUserInfo  const res = await api({ id: route.query.id })  const query = route.query.enterprise ? [res.contact?.userId, res.contact?.enterpriseId] : [res?.id]  onMounted(() => {    nextTick(async () => {      const { channel } = await checkConversation(...query)      const items = [        {          channel,          userInfoVo: {            userInfoResp: route.query.enterprise ? res.contact : { ...res, userId: res.id}          }        }      ]      handleChange(items)    })  })}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()  messageItems.value = list.value  chatRef.value.scrollBottom()})const getInterviewInviteList = async () => {  if (!info.value.userId) return  const data = await getInterviewInviteListByInviteUserId(info.value.userId)  interview.value = data.slice(0, 1)}watch(  () => conversationList.value,  async () => {    // console.log('发生变化', val)    // 数据发生变化    if (channelItem.value && IM.fromChannel === channelItem.value.channelID) {      // 更新      const { list } = await getMoreMessages(1, channelItem.value)      messageItems.value = list.value      if (!isEnterprise) getInterviewInviteList()      chatRef.value.scrollBottom()    }  },  {    deep: true,    immediate: true  })async function handleChange (items) {  // console.log([...items])  try {    chatRef.value.changeOverlay(true)    const { userInfoVo, channel: myChannel } = items.pop()    info.value = userInfoVo?.userInfoResp ?? { name: '系统消息' }    Object.assign(info.value, {      channel: myChannel    })    const userId = userInfoVo.userInfoResp.userId    const enterpriseId = userInfoVo.userInfoResp.enterpriseId || undefined    const { channel, list, more } = await initChart(userId, enterpriseId)    // console.log('--------',list)    channelItem.value = channel.value    messageItems.value = list.value    hasMore.value = more    chatRef.value.scrollBottom()    // 点开窗口消除未读数量    await resetUnread(channel.value, baseInfo?.enterpriseId)    await updateConversation()    updateUnreadCount()    if (!isEnterprise) getInterviewInviteList()  } catch (error) {    messageItems.value = []  } finally {    chatRef.value.changeOverlay(false)  }}// 普通消息const handleUpdate = (val) => {  send(val.value, channelItem.value)}// 获取简历async function handleSendResume (item) {  try {    item.loading = true    // showResume.value = true    // 获取简历列表    const result = await getPersonResumeCv()    if (result.length === 0) {      Snackbar.error(t('resume.resumeYetSubmit'))      uploadFile.value.trigger()      return    }    resumeList.value = result    showResume.value = true  } finally {    item.loading = false  }}/** * 发送简历 * text param * { *  remark: 备注 *  query: {} 自定义参数 access -1 未确定 0 拒绝 1 同意 *  type: 1 => 发送简历 *        2 => 索要简历 *        3 => 信息描述 * } */// 没有上传过简历的弹窗上传并发送给对方const handleUploadResume = async (url, title) => {  if (!url || !title) return  await savePersonResumeCv({ title, url })  const text = {    remark: '发送简历',    query: {      src: url,      title    },    type: 1  }  send (JSON.stringify(text), channelItem.value, 105)}function handleSubmitResume () {  if (!selectResume.value) {    Snackbar.error(t('resume.selectResumeToSubmit'))    return  }  const _info = resumeList.value.find((item) => item.id === selectResume.value)  const text = {    remark: '发送简历',    query: {      src: _info.url,      title: _info.title,      id: _info.id,    },    type: 1  }  send (JSON.stringify(text), channelItem.value, 105)  showResume.value = false}// 求简历function handleRequest () {  const text = {    remark: '求简历',    query: {      src: '',      title: '',      id: '',    },    type: 2  }  send (JSON.stringify(text), channelItem.value, 105)}// 简历预览const handlePreview = (val) => {  previewFile(val.content.query.src)}const handleGetMore = async () => {  try {    chatRef.value.changeLoading(true)    pageSize.value++    const { list, more } = await getMoreMessages(pageSize.value, channelItem.value)    messageItems.value.unshift(...list.value)    hasMore.value = more    // chatRef.value.scrollBottom()  } finally {    chatRef.value.changeLoading(false)  }}const handleDelete = async ({ channel }) => {  await deleteConversations(channel, baseInfo?.enterpriseId)  await updateConversation()  updateUnreadCount()}// 没有企业ID则enterpriseId为undefined// 发送消息体 { text, type: 2 }// 面试邀约async function handleInvite (item) {  item.loading = true  positionList.value = []  try {    const data = await getJobAdvertised({ hire: false })    if (!data.length) return    const list = dealDictArrayData([], data)    positionList.value = list.map(e => {      return {        label: `${e.name}${e.areaName ? '_' + e.areaName : ''} ${e.payFrom}-${e.payTo}/${e.payName}`,        value: e.id,        data: e      }    })    // itemData.value = {    //   userId: '',    //   jobId: ''    // }    showInvite.value = true    // send(JSON.stringify(msg), channelItem.value, 101)    // console.log(query)  } catch (error) {    console.log(error)  } finally {    item.loading = false  }}const handleSubmit = async () => {  const { valid } = await inviteRef.value.CtFormRef.formRef.validate()  if (!valid) {    return  }  const query = inviteRef.value.getQuery()  if (!query.time) {    Snackbar.error('时间不能为空')    return  }  query.userId = info.value.userId  query.positionInfo = positionList.value.find(e => e.value === query.jobId)  // 需要id  const data = await saveInterviewInvite(query)  // 保留邀请id  query.id = data.id  Snackbar.success(t('common.operationSuccessful'))  send(JSON.stringify(query), channelItem.value, 101)  showInvite.value = false}const handleAgree = (val) => {  if (!val.id) return  const query = {    id: val.id  }  const baseInfo = localStorage.getItem('baseInfo')  if (baseInfo) {    const { phone } = JSON.parse(baseInfo)    query.phone = phone  }  Confirm(t('common.confirmTitle'), '是否确定接收此面试邀请?').then(async () => {    await userInterviewInviteConsent(query)    Snackbar.success(t('common.operationSuccessful'))    getInterviewInviteList()    send(JSON.stringify({ id: val.id }), channelItem.value, 104)  })}// 拒绝面试邀请const handleRefuse = (val) => {  if (!val.id) return  Confirm(t('common.confirmTitle'), '您是否确定要拒绝此面试邀请?').then(async () => {    await userInterviewInviteReject(val.id)    Snackbar.success(t('common.operationSuccessful'))    getInterviewInviteList()    send(JSON.stringify({ id: val.id }), channelItem.value, 103)  })}</script><style scoped lang="scss">.message {  display: flex;  &-left {    position: relative;    flex-shrink: 0;    height: 100%;;    width: 360px;    background-color: #fff;    border-radius: 8px;    margin-right: 12px;    .message-left-search {      width: 100%;      height: 60px;      background: linear-gradient(90deg, #f5fcfc, #fcfbfa);      border-radius: 8px 8px 0 0;    }    .message-chat-box {      .chat-item {        position: relative;        width: 100%;        height: 78px;        padding: 14px 12px;        cursor: pointer;        &:hover {          background-color: #f8f8f8;        }        .chat-item-time {          position: absolute;          right: 12px;          top: 50%;          transform: translateY(-50%);        }        .title-box {          max-width: 114px;          overflow: hidden;          white-space: nowrap;          text-overflow: ellipsis;          display: inline-block;        }      }    }    .message-no-more-text {      color: var(--color-999);      font-size: 14px;      text-align: center    }    .left-noData {      position: absolute;      top: 50%;      left: 50%;      transform: translate(-50%, -50%);    }  }  &-right {    height: 100%;    flex: 1;    width: 0;    position: relative;    background-color: #fff;    border-radius: 8px;    .right-noData {      position: absolute;      top: 50%;      left: 50%;      transform: translate(-50%, -50%);    }  }}</style>
 |