Browse Source

Enhance IM component with conversation detail view and state management

- Introduced a detail view for conversations in Im.vue, allowing users to click on a conversation to view more details.
- Added reactive state management for the active conversation and view mode, improving user experience.
- Updated computed properties to retrieve active conversation details, including name, avatar, and business user info.
- Implemented navigation functions to switch between list and detail views, enhancing the overall functionality of the IM popup.
0es 3 months ago
parent
commit
3c4d59db8d
2 changed files with 212 additions and 2 deletions
  1. 152 0
      app/components/im/ConversationDetail.vue
  2. 60 2
      app/components/popup/Im.vue

+ 152 - 0
app/components/im/ConversationDetail.vue

@@ -0,0 +1,152 @@
+<script setup lang="ts">
+import type { Conversation } from '@tencentcloud/chat'
+import type { UserInfoItemVO } from '~/types/api'
+import TitleIcon from '~/assets/icons/tab-active.svg'
+import CloseListIcon from '~/assets/icons/im/close-list.svg'
+import ArrowSvg from '~/assets/icons/mine/arrow-temp.svg'
+
+type Props = {
+  conversation: Conversation | null
+  name: string
+  avatar: string
+  bizUserInfo?: UserInfoItemVO
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  bizUserInfo: undefined,
+})
+
+const emit = defineEmits<{
+  (e: 'back' | 'close'): void
+}>()
+</script>
+
+<template>
+  <div class="im-detail-page">
+    <div class="im-detail-page__bg" />
+
+    <div class="im-detail-page__header">
+      <div class="im-detail-page__header-left">
+        <button
+          class="im-detail-page__back"
+          type="button"
+          @click="emit('back')"
+        >
+          <ArrowSvg class="im-detail-page__back-icon" />
+          <span class="im-detail-page__back-text">Back</span>
+        </button>
+
+        <div class="im-detail-page__title">
+          <TitleIcon class="im-detail-page__title-icon" />
+          <span class="im-detail-page__title-text">
+            {{ props.name || 'Conversation' }}
+          </span>
+        </div>
+      </div>
+
+      <button
+        class="im-detail-page__close"
+        type="button"
+        @click="emit('close')"
+      >
+        <CloseListIcon />
+      </button>
+    </div>
+
+    <div class="im-detail-page__content" />
+  </div>
+</template>
+
+<style scoped lang="scss">
+.im-detail-page {
+  height: 100vh;
+  padding: 16px 16px 20px;
+  position: relative;
+  display: flex;
+  flex-direction: column;
+
+  &__bg {
+    @include size(100%, 173px);
+    @include bg('~/assets/images/home/header-bg.png', 100% 173px);
+    position: absolute;
+    inset: 0;
+    z-index: 0;
+  }
+
+  &__header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    position: relative;
+    z-index: 1;
+  }
+
+  &__header-left {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    min-width: 0;
+  }
+
+  &__back {
+    border: none;
+    background: transparent;
+    display: inline-flex;
+    align-items: center;
+    gap: 4px;
+    padding: 6px 0;
+    cursor: pointer;
+    -webkit-tap-highlight-color: transparent;
+    flex: 0 0 auto;
+  }
+
+  &__back-icon {
+    transform: rotate(180deg);
+  }
+
+  &__back-text {
+    font-size: 12px;
+    color: rgba(0, 0, 0, 0.70);
+  }
+
+  &__title {
+    font-size: 24px;
+    font-family: var(--font-title);
+    font-weight: 600;
+    color: #000;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    min-width: 0;
+
+    &-icon {
+      position: absolute;
+      z-index: 0;
+    }
+    &-text {
+      position: relative;
+      z-index: 1;
+      max-width: 60vw;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+  }
+
+  &__close {
+    border: none;
+    background: transparent;
+  }
+
+  &__content {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    position: relative;
+    z-index: 1;
+    flex: 1 1 auto;
+    min-height: 0;
+    overflow: auto;
+  }
+}
+</style>

+ 60 - 2
app/components/popup/Im.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import dayjs from 'dayjs'
-import { computed, watch } from 'vue'
+import { computed, ref, watch } from 'vue'
 import type { Conversation } from '@tencentcloud/chat'
 import { useImPopup } from '~/composables/useImPopup'
 import TitleIcon from '~/assets/icons/tab-active.svg'
@@ -9,12 +9,22 @@ import CloseListIcon from '~/assets/icons/im/close-list.svg'
 const { state, close } = useImPopup()
 const { ready, conversationList, conversationLoading, conversationError, getConversationList, bizUserInfoMap } = useChat()
 
+type ViewMode = 'list' | 'detail'
+const view = ref<ViewMode>('list')
+const activeConversationId = ref<string | null>(null)
+
 const list = computed(() => conversationList.value
   ? conversationList.value.filter((conv) => {
       return conv.userProfile?.userID !== '10000'
     })
   : [])
 
+const activeConversation = computed(() => {
+  const id = activeConversationId.value
+  if (!id) return null
+  return list.value.find(c => c.conversationID === id) ?? null
+})
+
 const getConversationName = (conv: Conversation) => {
   const userNo = conv?.userProfile?.userID
   const biz = userNo ? bizUserInfoMap.value?.[userNo] : undefined
@@ -55,6 +65,24 @@ const fetchConversations = async () => {
   await getConversationList()
 }
 
+const activeName = computed(() => activeConversation.value ? getConversationName(activeConversation.value) : '')
+const activeAvatar = computed(() => activeConversation.value ? getConversationAvatar(activeConversation.value) : '/avatar.png')
+const activeBizUserInfo = computed(() => {
+  const userNo = activeConversation.value?.userProfile?.userID
+  if (!userNo) return undefined
+  return bizUserInfoMap.value?.[userNo]
+})
+
+const openConversationDetail = (conv: Conversation) => {
+  activeConversationId.value = conv.conversationID
+  view.value = 'detail'
+}
+
+const backToList = () => {
+  view.value = 'list'
+  activeConversationId.value = null
+}
+
 watch(
   () => [state.visible, ready.value] as const,
   ([visible, isReady]) => {
@@ -66,6 +94,16 @@ watch(
 const handleUpdateShow = (value: boolean) => {
   if (!value) close()
 }
+
+watch(
+  () => state.visible,
+  (visible) => {
+    if (!visible) {
+      view.value = 'list'
+      activeConversationId.value = null
+    }
+  },
+)
 </script>
 
 <template>
@@ -75,7 +113,20 @@ const handleUpdateShow = (value: boolean) => {
     teleport="body"
     @update:show="handleUpdateShow"
   >
-    <div class="im-popup">
+    <ImConversationDetail
+      v-if="view === 'detail'"
+      :conversation="activeConversation"
+      :name="activeName"
+      :avatar="activeAvatar"
+      :biz-user-info="activeBizUserInfo"
+      @back="backToList"
+      @close="close"
+    />
+
+    <div
+      v-else
+      class="im-popup"
+    >
       <div class="im-popup__bg" />
 
       <div class="im-popup__header">
@@ -139,6 +190,7 @@ const handleUpdateShow = (value: boolean) => {
             :time="getConversationTimeLabel(conv)"
             :message="getConversationLastText(conv)"
             :unread="conv.unreadCount"
+            @click="openConversationDetail(conv)"
           />
         </div>
       </div>
@@ -151,6 +203,8 @@ const handleUpdateShow = (value: boolean) => {
   height: 100vh;
   padding: 16px 16px 20px;
   position: relative;
+  display: flex;
+  flex-direction: column;
 
   &__bg {
     @include size(100%, 173px);
@@ -176,6 +230,7 @@ const handleUpdateShow = (value: boolean) => {
     display: flex;
     align-items: center;
     justify-content: center;
+    min-width: 0;
 
     &-icon {
       position: absolute;
@@ -198,6 +253,9 @@ const handleUpdateShow = (value: boolean) => {
     gap: 10px;
     position: relative;
     z-index: 1;
+    flex: 1 1 auto;
+    min-height: 0;
+    overflow: auto;
   }
 
   &__list {