Browse Source

Revert "Revert "更新添加IM""

This reverts commit 775e33330f7a647818601281e8663681bf1b19d0.
lex 1 year ago
parent
commit
e3b0ee3fac

+ 1 - 0
components.d.ts

@@ -11,6 +11,7 @@ declare module '@vue/runtime-core' {
   export interface GlobalComponents {
     Application: typeof import('./src/components/Application/Application.vue')['default']
     HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
+    NInput: typeof import('naive-ui')['NInput']
     NTabPane: typeof import('naive-ui')['NTabPane']
     NTabs: typeof import('naive-ui')['NTabs']
     RouterLink: typeof import('vue-router')['RouterLink']

+ 11 - 0
package-lock.json

@@ -19,6 +19,7 @@
         "cropperjs": "^1.5.13",
         "dayjs": "^1.11.7",
         "echarts": "^5.4.2",
+        "eventemitter3": "^5.0.1",
         "html2canvas": "^1.4.1",
         "lib-flexible": "^0.3.2",
         "lodash": "^4.17.21",
@@ -5530,6 +5531,11 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/eventemitter3": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.1.tgz",
+      "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
+    },
     "node_modules/evtd": {
       "version": "0.2.4",
       "resolved": "https://registry.npmmirror.com/evtd/-/evtd-0.2.4.tgz",
@@ -15786,6 +15792,11 @@
       "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz",
       "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
     },
+    "eventemitter3": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.1.tgz",
+      "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
+    },
     "evtd": {
       "version": "0.2.4",
       "resolved": "https://registry.npmmirror.com/evtd/-/evtd-0.2.4.tgz",

+ 1 - 0
package.json

@@ -33,6 +33,7 @@
     "cropperjs": "^1.5.13",
     "dayjs": "^1.11.7",
     "echarts": "^5.4.2",
+    "eventemitter3": "^5.0.1",
     "html2canvas": "^1.4.1",
     "lib-flexible": "^0.3.2",
     "lodash": "^4.17.21",

+ 58 - 0
src/TUIKit/TUIComponents/components/TheSearch/index.module.less

@@ -0,0 +1,58 @@
+.TheSearch {
+  border-radius: 20Px !important;
+
+  &.noBorder {
+    --n-border: none !important;
+  }
+
+  :global {
+    .n-input-wrapper {
+      padding-left: 12Px;
+      padding-right: 4Px;
+      height: 32Px !important;
+    }
+
+    .n-button {
+      height: 26Px;
+      font-size: 12Px;
+      font-weight: 500;
+      width: auto;
+      opacity: 0.7;
+    }
+
+  }
+
+  .active {
+    display: none;
+  }
+
+  .active,
+  .default {
+    width: 14Px;
+    height: 14Px;
+  }
+
+  &:global(.n-input--focus) {
+    .active {
+      display: block;
+    }
+
+    .default {
+      display: none;
+    }
+
+    :global {
+      .n-button {
+        opacity: 1;
+      }
+    }
+  }
+
+  &:hover {
+    :global {
+      .n-button {
+        opacity: 1;
+      }
+    }
+  }
+}

+ 81 - 0
src/TUIKit/TUIComponents/components/TheSearch/index.tsx

@@ -0,0 +1,81 @@
+import { PropType, defineComponent, reactive } from 'vue';
+import styles from './index.module.less';
+import { InputProps, NButton, NInput } from 'naive-ui';
+import icon_search from '/src/common/images/icon_search.svg';
+import icon_searchActive from '/src/common/images/icon_searchActive.svg';
+import { useThrottleFn } from '@vueuse/core';
+
+export default defineComponent({
+  name: 'TheSearch',
+  props: {
+    /** 圆角 */
+    round: {
+      type: Boolean as PropType<InputProps['round']>,
+      default: false
+    },
+    border: {
+      type: Boolean,
+      default: true
+    },
+    placeholder: {
+      type: String,
+      default: '请输入搜索关键词'
+    },
+    showSearchButton: {
+      type: Boolean,
+      default: true
+    },
+    loading: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ['search'],
+  setup(props, { emit }) {
+    const searchData = reactive({
+      value: ''
+    });
+    const debouncedRequest = useThrottleFn(() => {
+      if (props.loading) return;
+      emit('search', searchData.value ? searchData.value.trim() : '');
+    }, 500);
+    return () => (
+      <NInput
+        class={[styles.TheSearch, props.border ? '' : styles.noBorder]}
+        style={'--n-font-size: 12px;--n-height:32px'}
+        round={props.round}
+        placeholder={props.placeholder}
+        clearable
+        v-model:value={searchData.value}
+        onClear={() => {
+          searchData.value = '';
+          debouncedRequest();
+        }}
+        onKeyup={(e: KeyboardEvent) => {
+          e.stopPropagation();
+          if (e.code === 'Enter') {
+            debouncedRequest();
+          }
+        }}>
+        {{
+          prefix: () => (
+            <>
+              <img class={styles.default} src={icon_search} />
+              <img class={styles.active} src={icon_searchActive} />
+            </>
+          ),
+          suffix: () =>
+            props.showSearchButton && (
+              <NButton
+                size="small"
+                round
+                type="primary"
+                onClick={() => debouncedRequest()}>
+                搜索
+              </NButton>
+            )
+        }}
+      </NInput>
+    );
+  }
+});

+ 4 - 0
src/TUIKit/TUIComponents/container/TUIChat/manage-components/style/web.scss

@@ -326,4 +326,8 @@
 .delDialog-title {
   text-align: center;
   padding: 20Px 0;
+}
+
+.icon-chat-setting {
+  margin-right: 35Px;
 }

+ 59 - 17
src/TUIKit/TUIComponents/container/TUIConversation/index.vue

@@ -1,25 +1,44 @@
 <template>
-  <div class="TUI-conversation">
-    <div class="network" v-if="isNetwork">
-      <i class="icon icon-error">!</i>
-      <p>️{{ $t('TUIConversation.网络异常,请您检查网络设置') }}</p>
+  <div style="height: 100%">
+    <div class="sectionSearch">
+      <the-search
+        placeholder="请输入名称"
+        :show-search-button="false"
+        :loading="loading"
+        @search="(val: any) => {
+          noSearch(val)
+        }"
+      />
     </div>
-    <main class="TUI-conversation-list">
-      <TUIConversationList
-        :currentID="currentConversationID"
-        :data="conversationData"
-        @handleItem="handleCurrentConversation"
-        :isH5="env.isH5"
-        :displayOnlineStatus="displayOnlineStatus"
-        :userStatusList="userStatusList"
+    <div class="TUI-conversation">
+      <div class="network" v-if="isNetwork">
+        <i class="icon icon-error">!</i>
+        <p>️{{ $t('TUIConversation.网络异常,请您检查网络设置') }}</p>
+      </div>
+      <main class="TUI-conversation-list">
+        <TUIConversationList
+          :currentID="currentConversationID"
+          :data="conversationData"
+          @handleItem="handleCurrentConversation"
+          :isH5="env.isH5"
+          :displayOnlineStatus="displayOnlineStatus"
+          :userStatusList="userStatusList"
+        />
+      </main>
+
+      <the-empty
+        v-if="!isNetwork && !loading && conversationData.list.length <= 0"
+        style="height: 90%"
       />
-    </main>
+    </div>
   </div>
 </template>
 <script lang="ts">
+import TheEmpty from '@/components/TheEmpty';
 import { defineComponent, reactive, toRefs, computed, watch } from 'vue';
 import TUIConversationList from './components/list';
 import { caculateTimeago, isArrayEqual } from '../utils';
+import TheSearch from '../../components/TheSearch';
 import {
   handleAvatar,
   handleName,
@@ -31,7 +50,9 @@ const TUIConversation = defineComponent({
   name: 'TUIConversation',
 
   components: {
-    TUIConversationList
+    TUIConversationList,
+    TheSearch,
+    TheEmpty
   },
   props: {
     displayOnlineStatus: {
@@ -43,8 +64,10 @@ const TUIConversation = defineComponent({
     const TUIServer: any = TUIConversation?.TUIServer;
     const data = reactive({
       currentConversationID: '',
+      keyword: '',
+      loading: false,
       conversationData: {
-        list: [],
+        list: [] as any,
         handleItemAvator: (item: any) => handleAvatar(item),
         handleItemName: (item: any) => handleName(item),
         handleShowAt: (item: any) => handleAt(item),
@@ -63,7 +86,14 @@ const TUIConversation = defineComponent({
       userStatusList: new Map()
     });
 
-    TUIServer.bind(data);
+    try {
+      data.loading = true;
+      TUIServer.bind(data, () => {
+        data.loading = false;
+      });
+    } catch {
+      data.loading = false;
+    }
 
     TUIConversationList.TUIServer = TUIServer;
 
@@ -116,10 +146,22 @@ const TUIConversation = defineComponent({
       TUIServer.handleCurrentConversation(value);
     };
 
+    //
+    const noSearch = async (val: string) => {
+      data.loading = true;
+      try {
+        data.conversationData.list = [];
+        await TUIServer.getConversationListForName(val);
+      } catch {
+        //
+      }
+      data.loading = false;
+    };
     return {
       ...toRefs(data),
       handleCurrentConversation,
-      isNetwork
+      isNetwork,
+      noSearch
     };
   }
 });

+ 124 - 17
src/TUIKit/TUIComponents/container/TUIConversation/server.ts

@@ -1,5 +1,5 @@
 import IComponentServer from '../IComponentServer';
-
+import { eventGlobal } from '/src/utils';
 /**
  * class TUIConversationServer
  *
@@ -14,7 +14,11 @@ export default class TUIConversationServer extends IComponentServer {
     super();
     this.TUICore = TUICore;
     this.bindTIMEvent();
-    this.store = TUICore.setComponentStore('TUIConversation', {}, this.updateStore.bind(this));
+    this.store = TUICore.setComponentStore(
+      'TUIConversation',
+      {},
+      this.updateStore.bind(this)
+    );
   }
 
   /**
@@ -53,7 +57,7 @@ export default class TUIConversationServer extends IComponentServer {
         TUIName: 'TUIConversation',
         callback: () => {
           callback && callback(resolve, reject);
-        },
+        }
       };
       this.TUICore.setAwaitFunc(config.TUIName, config.callback);
     });
@@ -68,16 +72,38 @@ export default class TUIConversationServer extends IComponentServer {
    */
 
   private bindTIMEvent() {
-    this.TUICore.tim.on(this.TUICore.TIM.EVENT.CONVERSATION_LIST_UPDATED, this.handleConversationListUpdate, this);
-    this.TUICore.tim.on(this.TUICore.TIM.EVENT.NET_STATE_CHANGE, this.handleNetStateChange, this);
+    this.TUICore.tim.on(
+      this.TUICore.TIM.EVENT.CONVERSATION_LIST_UPDATED,
+      this.handleConversationListUpdate,
+      this
+    );
+    this.TUICore.tim.on(
+      this.TUICore.TIM.EVENT.NET_STATE_CHANGE,
+      this.handleNetStateChange,
+      this
+    );
   }
 
   private unbindTIMEvent() {
-    this.TUICore.tim.off(this.TUICore.TIM.EVENT.CONVERSATION_LIST_UPDATED, this.handleConversationListUpdate);
-    this.TUICore.tim.off(this.TUICore.TIM.EVENT.NET_STATE_CHANGE, this.handleNetStateChange);
+    this.TUICore.tim.off(
+      this.TUICore.TIM.EVENT.CONVERSATION_LIST_UPDATED,
+      this.handleConversationListUpdate
+    );
+    this.TUICore.tim.off(
+      this.TUICore.TIM.EVENT.NET_STATE_CHANGE,
+      this.handleNetStateChange
+    );
   }
 
   private handleConversationListUpdate(res: any) {
+    // 判断SDK是否初始化
+    if (this.TUICore.tim.isReady()) {
+      eventGlobal.emit(
+        'onNoReadMessageCount',
+        this.TUICore.tim.getTotalUnreadMessageCount()
+      );
+    }
+
     this.handleFilterSystem(res.data);
   }
 
@@ -94,13 +120,65 @@ export default class TUIConversationServer extends IComponentServer {
   private handleFilterSystem(list: any) {
     const options = {
       allConversationList: list,
-      conversationList: [],
+      conversationList: []
     };
-    const currentList = list.filter((item: any) => item?.conversationID === this?.currentStore?.currentConversationID);
+    const currentList = list.filter(
+      (item: any) =>
+        item?.conversationID === this?.currentStore?.currentConversationID
+    );
     if (currentList.length === 0) {
       this.handleCurrentConversation({});
     }
-    options.conversationList = list.filter((item: any) => item.type !== this.TUICore.TIM.TYPES.CONV_SYSTEM);
+    options.conversationList = list.filter(
+      (item: any) => item.type !== this.TUICore.TIM.TYPES.CONV_SYSTEM
+    );
+    this.store.allConversationList = options.allConversationList;
+    this.store.conversationList = options.conversationList;
+    return options;
+  }
+
+  /**
+   * 处理conversationList
+   *
+   * @param {Array} list conversationList
+   * @returns {Object}
+   */
+  private handleFilterName(list: any, keyword: string) {
+    const options = {
+      allConversationList: list,
+      conversationList: []
+    };
+    console.log(list, 'list');
+    const currentList: any = [];
+    list.forEach((item: any) => {
+      console.log(item, 'item', keyword);
+      if (item.type === 'GROUP') {
+        console.log(
+          item.type,
+          item.groupProfile?.name,
+          item.groupProfile?.name.indexOf(keyword)
+        );
+        if (item.groupProfile?.name.indexOf(keyword) >= 0) {
+          currentList.push(item);
+        }
+      }
+      if (item.type === 'C2C') {
+        console.log(
+          item.type,
+          item.userProfile?.nick,
+          item.userProfile?.nick.indexOf(keyword)
+        );
+        if (item.userProfile?.nick.indexOf(keyword) >= 0) {
+          currentList.push(item);
+        }
+      }
+    });
+    // if (currentList.length === 0) {
+    //   this.handleCurrentConversation({});
+    // }
+    options.conversationList = currentList.filter(
+      (item: any) => item.type !== this.TUICore.TIM.TYPES.CONV_SYSTEM
+    );
     this.store.allConversationList = options.allConversationList;
     this.store.conversationList = options.conversationList;
     return options;
@@ -124,7 +202,9 @@ export default class TUIConversationServer extends IComponentServer {
   public async setMessageRead(conversationID: string) {
     return this.handlePromiseCallback(async (resolve: any, reject: any) => {
       try {
-        const imResponse: any = await this.TUICore.tim.setMessageRead({ conversationID });
+        const imResponse: any = await this.TUICore.tim.setMessageRead({
+          conversationID
+        });
         resolve(imResponse);
       } catch (error) {
         reject(error);
@@ -141,7 +221,9 @@ export default class TUIConversationServer extends IComponentServer {
   public async deleteConversation(conversationID: string) {
     return this.handlePromiseCallback(async (resolve: any, reject: any) => {
       try {
-        const imResponse: any = await this.TUICore.tim.deleteConversation(conversationID);
+        const imResponse: any = await this.TUICore.tim.deleteConversation(
+          conversationID
+        );
         resolve(imResponse);
       } catch (error) {
         reject(error);
@@ -175,7 +257,9 @@ export default class TUIConversationServer extends IComponentServer {
   public async muteConversation(options: any) {
     return this.handlePromiseCallback(async (resolve: any, reject: any) => {
       try {
-        const imResponse: any = await this.TUICore.tim.setMessageRemindType(options);
+        const imResponse: any = await this.TUICore.tim.setMessageRemindType(
+          options
+        );
         resolve(imResponse);
       } catch (error) {
         reject(error);
@@ -191,7 +275,9 @@ export default class TUIConversationServer extends IComponentServer {
   public async getConversationProfile(conversationID: string) {
     return this.handlePromiseCallback(async (resolve: any, reject: any) => {
       try {
-        const imResponse = await this.TUICore.tim.getConversationProfile(conversationID);
+        const imResponse = await this.TUICore.tim.getConversationProfile(
+          conversationID
+        );
         resolve(imResponse);
       } catch (error) {
         reject(error);
@@ -208,6 +294,7 @@ export default class TUIConversationServer extends IComponentServer {
     return this.handlePromiseCallback(async (resolve: any, reject: any) => {
       try {
         const imResponse = await this.TUICore.tim.getConversationList();
+        console.log(imResponse, 'getConversationList');
         this.handleFilterSystem(imResponse.data.conversationList);
         resolve(imResponse);
       } catch (error) {
@@ -216,6 +303,24 @@ export default class TUIConversationServer extends IComponentServer {
     });
   }
 
+  /*
+   * 获取 conversationList
+   *
+   * @returns {Promise}
+   */
+  public async getConversationListForName(keyword: string) {
+    return this.handlePromiseCallback(async (resolve: any, reject: any) => {
+      try {
+        const imResponse = await this.TUICore.tim.getConversationList();
+        console.log(imResponse, 'getConversationListForName');
+        this.handleFilterName(imResponse.data.conversationList, keyword);
+        resolve(imResponse);
+      } catch (error) {
+        reject(error);
+      }
+    });
+  }
+
   /**
    * 获取其他用户资料
    *
@@ -225,7 +330,9 @@ export default class TUIConversationServer extends IComponentServer {
   public async getUserProfile(userIDList: Array<string>) {
     return this.handlePromiseCallback(async (resolve: any, reject: any) => {
       try {
-        const imResponse = await this.TUICore.tim.getUserProfile({ userIDList });
+        const imResponse = await this.TUICore.tim.getUserProfile({
+          userIDList
+        });
         resolve(imResponse);
       } catch (error) {
         reject(error);
@@ -247,9 +354,10 @@ export default class TUIConversationServer extends IComponentServer {
    * @param {Object} params 使用的数据
    * @returns {Object} 数据
    */
-  public async bind(params: any) {
+  public async bind(params: any, callBack?: any) {
     this.currentStore = params;
     await this.getConversationList();
+    callBack && callBack();
     return this.currentStore;
   }
 
@@ -257,7 +365,6 @@ export default class TUIConversationServer extends IComponentServer {
   public handleCurrentConversation(value: any) {
     // 通知 TUIChat 切换会话或关闭会话
     this.TUICore.getStore().TUIChat.conversation = value || {};
-
     if (!value?.conversationID) {
       this.currentStore.currentConversationID = '';
       return;

+ 5 - 1
src/TUIKit/TUIComponents/container/TUIConversation/style/web.scss

@@ -1,7 +1,7 @@
 .TUI-conversation {
   position: relative;
   width: 100%;
-  height: 100%;
+  height: calc(100% - 49px);
   overflow-y: auto;
   box-sizing: border-box;
 
@@ -52,4 +52,8 @@ input {
   height: 24Px;
   border-radius: 5Px;
   padding: 0 10Px;
+}
+
+.sectionSearch {
+  padding: 0 20px 12px;
 }

+ 88 - 52
src/TUIKit/TUIComponents/container/TUIGroup/index.vue

@@ -1,59 +1,77 @@
 <template>
-  <div class="TUI-group">
-    <div class="network" v-if="isNetwork">
-      <i class="icon icon-error">!</i>
-      <p>️{{ $t('TUIConversation.网络异常,请您检查网络设置') }}</p>
+  <div style="height: 100%">
+    <div class="sectionSearch">
+      <the-search
+        placeholder="请输入群聊名称"
+        :show-search-button="false"
+        @search="(val: any) => {
+          noSearch(val)
+        }"
+      />
     </div>
-    <main class="TUI-conversation-list">
-      <aside class="TUI-contact-left">
-        <ul class="TUI-contact-list">
-          <li
-            class="TUI-contact-list-item"
-            v-for="(item, index) in groupList"
-            :key="index"
-            @click="handleListItem(item)"
-          >
-            <aside class="left">
-              <img
-                class="avatar"
-                :src="
-                  item.img ||
-                  'https://news-info.ks3-cn-beijing.ksyuncs.com/07/1690775328089.png'
-                "
-                onerror="this.src='https://news-info.ks3-cn-beijing.ksyuncs.com/07/1690775328089.png'"
-              />
-            </aside>
-            <div class="content-header">
-              <label>
-                <p class="name">{{ item.name }}</p>
-              </label>
-              <div class="middle-box">
-                <p>共{{ item.memberNum || 0 }}人</p>
+    <div class="TUI-group">
+      <div class="network" v-if="isNetwork">
+        <i class="icon icon-error">!</i>
+        <p>️{{ $t('TUIConversation.网络异常,请您检查网络设置') }}</p>
+      </div>
+      <main class="TUI-conversation-list">
+        <aside class="TUI-contact-left">
+          <ul class="TUI-contact-list">
+            <li
+              class="TUI-contact-list-item"
+              v-for="(item, index) in groupList"
+              :key="index"
+              @click="handleListItem(item)"
+            >
+              <aside class="left">
+                <img
+                  class="avatar"
+                  :src="
+                    item.img ||
+                    'https://news-info.ks3-cn-beijing.ksyuncs.com/07/1690775328089.png'
+                  "
+                  onerror="this.src='https://news-info.ks3-cn-beijing.ksyuncs.com/07/1690775328089.png'"
+                />
+              </aside>
+              <div class="content-header">
+                <label>
+                  <p class="name">{{ item.name }}</p>
+                </label>
+                <div class="middle-box">
+                  <p>共{{ item.memberNum || 0 }}人</p>
+                </div>
               </div>
-            </div>
-            <div class="content-footer">
-              <span class="time"></span>
-              <img
-                v-if="item.selfInfo?.messageRemindType === 'AcceptNotNotify'"
-                class="mute-icon"
-                src="../../assets/icon/mute.svg"
-              />
-              <i></i>
-            </div>
-          </li>
-        </ul>
-      </aside>
-    </main>
-    <!--  -->
+              <div class="content-footer">
+                <span class="time"></span>
+                <img
+                  v-if="item.selfInfo?.messageRemindType === 'AcceptNotNotify'"
+                  class="mute-icon"
+                  src="../../assets/icon/mute.svg"
+                />
+                <i></i>
+              </div>
+            </li>
+          </ul>
+        </aside>
+      </main>
+      <!--  -->
+      <the-empty
+        v-if="!isNetwork && !loading && groupList.length <= 0"
+        style="height: 90%"
+      />
+    </div>
   </div>
 </template>
 <script lang="ts">
+import TheEmpty from '@/components/TheEmpty';
 import { computed, defineComponent, onMounted, reactive, toRefs } from 'vue';
 import { caculateTimeago } from '../utils';
 import { handleAvatar, handleName, handleAt } from '../TUIChat/utils/utils';
 import { imGroupPage } from '/src/TUIKit/api';
+import TheSearch from '../../components/TheSearch';
 const TUIGroup = defineComponent({
   name: 'TUIGroup',
+  components: { TheEmpty, TheSearch },
   props: {
     displayOnlineStatus: {
       type: Boolean,
@@ -63,9 +81,11 @@ const TUIGroup = defineComponent({
   setup(props) {
     const TUIServer: any = TUIGroup.TUIServer;
     const data = reactive({
+      loading: true,
       page: 1,
-      rows: 20,
-      finish: false, // 是否加载完
+      rows: 100,
+      keyword: '',
+      finshed: false, // 是否加载完
       groupList: [] as any,
       searchGroup: [] as any,
       searchID: '',
@@ -95,31 +115,46 @@ const TUIGroup = defineComponent({
       });
     };
     const isNetwork = computed(() => {
-      const disconnected =
-        data.netWork === TUIServer.TUICore.TIM.TYPES.NET_STATE_DISCONNECTED;
-      const connecting =
-        data.netWork === TUIServer.TUICore.TIM.TYPES.NET_STATE_CONNECTING;
-      return disconnected || connecting;
+      // const disconnected =
+      //   data.netWork === TUIServer.TUICore.TIM.TYPES.NET_STATE_DISCONNECTED;
+      // const connecting =
+      //   data.netWork === TUIServer.TUICore.TIM.TYPES.NET_STATE_CONNECTING;
+      // return disconnected || connecting;
+      return false;
     });
 
     // 获取列表
     const getList = async () => {
+      data.loading = true;
       try {
         const res = await imGroupPage({
+          keyword: data.keyword,
           page: data.page,
           rows: data.rows
         });
 
         data.groupList.push(...(res.data.rows || []));
+
+        // 是否加载完成
+        data.finshed = res.data.pages <= res.data.current ? true : false;
       } catch {
         //
       }
+      data.loading = false;
     };
 
     onMounted(() => {
       getList();
     });
 
+    const noSearch = (val: string) => {
+      console.log(val, 'val');
+      data.page = 1;
+      data.keyword = val;
+      data.groupList = [];
+      getList();
+    };
+
     return {
       ...toRefs(data),
       handleListItem,
@@ -127,7 +162,8 @@ const TUIGroup = defineComponent({
       handleAvatar,
       handleName,
       handleAt,
-      handleItemTime
+      handleItemTime,
+      noSearch
     };
   }
 });

+ 293 - 265
src/TUIKit/TUIComponents/container/TUIGroup/style/web.scss

@@ -1,265 +1,293 @@
-.TUI-contact-list {
-  flex: 1;
-  width: 100%;
-  list-style: none;
-
-  &-item {
-    padding: 16Px 12Px;
-    position: relative;
-    display: flex;
-    align-items: center;
-    cursor: pointer;
-
-    label {
-      font-size: 14Px;
-    }
-
-    &:hover {
-      .icon-close {
-        display: inline-block;
-      }
-    }
-
-    .left {
-      position: relative;
-      width: 48Px;
-      height: 48Px;
-      margin-right: 10px;
-
-      .num {
-        position: absolute;
-        display: inline-block;
-        right: -8Px;
-        top: -8Px;
-        width: 20Px;
-        height: 20Px;
-        font-size: 10Px;
-        text-align: center;
-        line-height: 20Px;
-        border-radius: 50%;
-      }
-
-      .avatar {
-        width: 100%;
-        border-radius: 5Px;
-      }
-
-      .online-status {
-        box-sizing: border-box;
-        position: absolute;
-        width: 11Px;
-        height: 11Px;
-        // left: 24Px;
-        // top: 22Px;
-        right: 0px;
-        bottom: 3px;
-        border: 2Px solid #ffffff;
-        box-shadow: 0Px 0Px 4Px rgba(0, 0, 0, 0.102606);
-        border-radius: 50%;
-
-        &-online {
-          background: #29cc85;
-        }
-
-        &-offline {
-          background: #a4a4a4;
-        }
-      }
-    }
-
-    .content {
-      flex: 1;
-      padding-left: 8Px;
-      display: flex;
-      justify-content: space-between;
-      align-items: center;
-
-      ul {
-        flex: 1;
-        display: flex;
-        flex-direction: column;
-
-        li {
-          flex: 1;
-          display: flex;
-          align-items: center;
-          font-size: 14Px;
-          line-height: 16Px;
-
-          span {
-            flex: 1;
-            width: 0;
-            overflow: hidden;
-            text-overflow: ellipsis;
-            white-space: nowrap;
-          }
-        }
-
-        .name {
-          padding-bottom: 2Px;
-          font-size: 14Px;
-          color: #333;
-          font-weight: 500;
-        }
-      }
-
-      .type {
-        padding: 0 4Px;
-        line-height: 14Px;
-        font-size: 12Px;
-        border-radius: 1Px;
-      }
-    }
-  }
-
-  .not-aside {
-    padding-left: 40Px;
-    display: flex;
-    justify-content: space-between;
-  }
-}
-
-.reduce {
-  position: relative;
-  display: inline-block;
-  width: 36Px;
-  height: 36Px;
-
-  &::before {
-    position: absolute;
-    content: '';
-    width: 50%;
-    height: 1Px;
-    top: 0;
-    bottom: 0;
-    left: 0;
-    right: 0;
-    margin: auto;
-  }
-}
-
-.center {
-  display: flex;
-  justify-content: center;
-}
-
-.btn {
-  padding: 8Px 20Px;
-  border-radius: 4Px;
-  border: none;
-  font-size: 14Px;
-  text-align: center;
-  line-height: 20Px;
-}
-
-input {
-  box-sizing: border-box;
-  border-radius: 5Px;
-  padding: 10Px;
-}
-
-.search {
-  flex: 1;
-  display: flex;
-
-  &-box {
-    flex: 1;
-    display: flex;
-    align-items: center;
-
-    h1 {
-      padding: 2Px 8Px;
-      font-size: 14Px;
-    }
-
-    .input-box {
-      display: flex;
-      position: relative;
-      flex: 1;
-
-      input {
-        flex: 1;
-        margin-right: 0;
-      }
-
-      .icon {
-        position: absolute;
-        right: 10Px;
-        top: 0;
-        bottom: 0;
-        margin: auto 0;
-      }
-    }
-
-    .search-cancel {
-      padding-left: 10Px;
-      font-size: 12Px;
-      line-height: 18Px;
-    }
-  }
-}
-
-.content-header {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-
-  label {
-    flex: 1;
-    font-size: 14Px;
-  }
-
-  .name {
-    width: 110Px;
-    letter-spacing: 0;
-    font-size: 14Px;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    white-space: nowrap;
-  }
-}
-
-.middle-box {
-  flex: 1;
-  display: flex;
-  align-items: center;
-  font-weight: 400;
-  color: #999999;
-  letter-spacing: 0;
-
-  span {
-    font-size: 12Px;
-  }
-
-  p {
-    flex: 1;
-    width: 0;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    white-space: nowrap;
-    font-size: 12Px;
-    line-height: 16Px;
-  }
-}
-
-.content-footer {
-  line-height: 16Px;
-  display: flex;
-  flex-direction: column;
-
-  .time {
-    font-size: 12Px;
-    line-height: 16Px;
-    display: inline-block;
-    max-width: 75Px;
-    white-space: nowrap;
-    color: #bbbbbb
-  }
-
-  img {
-    width: 16Px;
-    height: 16Px;
-    margin-top: 5Px;
-    align-self: flex-end;
-  }
-}
+ .TUI-group {
+   position: relative;
+   width: 100%;
+   height: calc(100% - 49px);
+   overflow-y: auto;
+   box-sizing: border-box;
+ }
+
+ .TUI-conversation-list {
+   position: absolute;
+   width: 100%;
+ }
+
+ .TUI-contact-list {
+   flex: 1;
+   width: 100%;
+   list-style: none;
+   position: relative;
+   width: 100%;
+   height: 100%;
+   overflow-y: auto;
+   box-sizing: border-box;
+
+   &-item {
+     margin: 0 6px;
+     padding: 12Px 9Px;
+     position: relative;
+     display: flex;
+     align-items: center;
+     border-radius: 8px;
+     cursor: pointer;
+
+     label {
+       font-size: 14Px;
+     }
+
+     &:hover {
+       background: #F5F6FA;
+
+       .icon-close {
+         display: inline-block;
+       }
+     }
+
+     .left {
+       position: relative;
+       width: 48Px;
+       height: 48Px;
+       margin-right: 10px;
+
+       .num {
+         position: absolute;
+         display: inline-block;
+         right: -8Px;
+         top: -8Px;
+         width: 20Px;
+         height: 20Px;
+         font-size: 10Px;
+         text-align: center;
+         line-height: 20Px;
+         border-radius: 50%;
+       }
+
+       .avatar {
+         width: 100%;
+         border-radius: 5Px;
+         height: 100%;
+         object-fit: cover;
+       }
+
+       .online-status {
+         box-sizing: border-box;
+         position: absolute;
+         width: 11Px;
+         height: 11Px;
+         // left: 24Px;
+         // top: 22Px;
+         right: 0px;
+         bottom: 3px;
+         border: 2Px solid #ffffff;
+         box-shadow: 0Px 0Px 4Px rgba(0, 0, 0, 0.102606);
+         border-radius: 50%;
+
+         &-online {
+           background: #29cc85;
+         }
+
+         &-offline {
+           background: #a4a4a4;
+         }
+       }
+     }
+
+     .content {
+       flex: 1;
+       padding-left: 8Px;
+       display: flex;
+       justify-content: space-between;
+       align-items: center;
+
+       ul {
+         flex: 1;
+         display: flex;
+         flex-direction: column;
+
+         li {
+           flex: 1;
+           display: flex;
+           align-items: center;
+           font-size: 14Px;
+           line-height: 16Px;
+
+           span {
+             flex: 1;
+             width: 0;
+             overflow: hidden;
+             text-overflow: ellipsis;
+             white-space: nowrap;
+           }
+         }
+
+         .name {
+           padding-bottom: 2Px;
+           font-size: 14Px;
+           color: #333;
+           font-weight: 500;
+         }
+       }
+
+       .type {
+         padding: 0 4Px;
+         line-height: 14Px;
+         font-size: 12Px;
+         border-radius: 1Px;
+       }
+     }
+   }
+
+   .not-aside {
+     padding-left: 40Px;
+     display: flex;
+     justify-content: space-between;
+   }
+ }
+
+ .reduce {
+   position: relative;
+   display: inline-block;
+   width: 36Px;
+   height: 36Px;
+
+   &::before {
+     position: absolute;
+     content: '';
+     width: 50%;
+     height: 1Px;
+     top: 0;
+     bottom: 0;
+     left: 0;
+     right: 0;
+     margin: auto;
+   }
+ }
+
+ .center {
+   display: flex;
+   justify-content: center;
+ }
+
+ .btn {
+   padding: 8Px 20Px;
+   border-radius: 4Px;
+   border: none;
+   font-size: 14Px;
+   text-align: center;
+   line-height: 20Px;
+ }
+
+ input {
+   box-sizing: border-box;
+   border-radius: 5Px;
+   padding: 10Px;
+ }
+
+ .search {
+   flex: 1;
+   display: flex;
+
+   &-box {
+     flex: 1;
+     display: flex;
+     align-items: center;
+
+     h1 {
+       padding: 2Px 8Px;
+       font-size: 14Px;
+     }
+
+     .input-box {
+       display: flex;
+       position: relative;
+       flex: 1;
+
+       input {
+         flex: 1;
+         margin-right: 0;
+       }
+
+       .icon {
+         position: absolute;
+         right: 10Px;
+         top: 0;
+         bottom: 0;
+         margin: auto 0;
+       }
+     }
+
+     .search-cancel {
+       padding-left: 10Px;
+       font-size: 12Px;
+       line-height: 18Px;
+     }
+   }
+ }
+
+ .content-header {
+   flex: 1;
+   display: flex;
+   flex-direction: column;
+
+   label {
+     flex: 1;
+     font-size: 14Px;
+   }
+
+   .name {
+     width: 110Px;
+     letter-spacing: 0;
+     font-size: 14Px;
+     overflow: hidden;
+     text-overflow: ellipsis;
+     white-space: nowrap;
+   }
+ }
+
+ .middle-box {
+   flex: 1;
+   display: flex;
+   align-items: center;
+   font-weight: 400;
+   color: #999999;
+   letter-spacing: 0;
+
+   span {
+     font-size: 12Px;
+   }
+
+   p {
+     flex: 1;
+     width: 0;
+     overflow: hidden;
+     text-overflow: ellipsis;
+     white-space: nowrap;
+     font-size: 12Px;
+     line-height: 16Px;
+   }
+ }
+
+ .content-footer {
+   line-height: 16Px;
+   display: flex;
+   flex-direction: column;
+
+   .time {
+     font-size: 12Px;
+     line-height: 16Px;
+     display: inline-block;
+     max-width: 75Px;
+     white-space: nowrap;
+     color: #bbbbbb
+   }
+
+   img {
+     width: 16Px;
+     height: 16Px;
+     margin-top: 5Px;
+     align-self: flex-end;
+   }
+ }
+
+ .sectionSearch {
+   padding: 0 20px 12px;
+ }

+ 101 - 10
src/TUIKit/TUIComponents/container/TUIPerson/index.vue

@@ -1,35 +1,126 @@
 <template>
-  <div class="TUI-person" style="min-height: 100%">
-    <the-empty style="hieght: 90%" />
+  <div style="height: 100%">
+    <div class="sectionSearch">
+      <the-search
+        placeholder="请输入联系人名称"
+        :show-search-button="false"
+        @search="(val: any) => {
+          noSearch(val)
+        }"
+      />
+    </div>
+    <div class="TUI-person">
+      <main class="TUI-conversation-list">
+        <aside class="TUI-contact-left">
+          <ul class="TUI-contact-list">
+            <li
+              class="TUI-contact-list-item"
+              v-for="(item, index) in friendList"
+              :key="index"
+              @click="handleListItem(item)"
+            >
+              <aside class="left">
+                <img
+                  class="avatar"
+                  :src="
+                    item.friendAvatar ||
+                    'https://news-info.ks3-cn-beijing.ksyuncs.com/07/1690787574969.png'
+                  "
+                  onerror="this.src='https://news-info.ks3-cn-beijing.ksyuncs.com/07/1690787574969.png'"
+                />
+              </aside>
+              <div class="content-header">
+                <label>
+                  <p class="name">{{ item.friendNickname }}</p>
+                </label>
+                <div class="middle-box">
+                  <p>{{ item.subjectName }}</p>
+                </div>
+              </div>
+            </li>
+          </ul>
+        </aside>
+      </main>
+      <the-empty
+        v-if="!isNetwork && !loading && friendList.length <= 0"
+        style="height: 90%"
+      />
+    </div>
   </div>
 </template>
 <script lang="ts">
+import { defineComponent, reactive, toRefs, onMounted } from 'vue';
+import { imUserFriendPage } from '/src/TUIKit/api';
 import TheEmpty from '@/components/TheEmpty';
-import { defineComponent, reactive, toRefs } from 'vue';
-
+import TheSearch from '../../components/TheSearch';
 const TUIPerson = defineComponent({
   name: 'TUIPerson',
   components: {
-    TheEmpty
+    TheEmpty,
+    TheSearch
   },
-
   setup(props) {
     const TUIServer: any = TUIPerson.TUIServer;
     const data = reactive({
       loading: true,
-      friendList: [],
+      page: 1,
+      rows: 100,
+      keyword: '',
+      finshed: false, // 是否加载完
+      friendList: [] as any,
       searchGroup: [],
       searchID: '',
       currentGroup: null
     });
+    // 获取列表
+    const getList = async () => {
+      data.loading = true;
+      try {
+        const res = await imUserFriendPage({
+          keyword: data.keyword,
+          page: data.page,
+          rows: data.rows
+        });
+
+        data.friendList.push(...(res.data.rows || []));
 
-    TUIServer.bind(data, (list: any) => {
+        // 是否加载完成
+        data.finshed = res.data.pages <= res.data.current ? true : false;
+      } catch {
+        //
+      }
       data.loading = false;
-      console.log(data.friendList);
+    };
+
+    const handleListItem = async (item: any) => {
+      const name = `C2C${item.imUserId}`;
+      TUIServer.TUICore.TUIServer.TUIConversation.getConversationProfile(
+        name
+      ).then((imResponse: any) => {
+        // 通知 TUIConversation 添加当前会话
+        TUIServer.TUICore.TUIServer.TUIConversation.handleCurrentConversation(
+          imResponse.data.conversation
+        );
+
+        (data.currentGroup as any) = {};
+      });
+    };
+
+    const noSearch = (val: string) => {
+      data.page = 1;
+      data.keyword = val;
+      data.friendList = [];
+      getList();
+    };
+
+    onMounted(() => {
+      getList();
     });
 
     return {
-      ...toRefs(data)
+      ...toRefs(data),
+      handleListItem,
+      noSearch
     };
   }
 });

+ 289 - 265
src/TUIKit/TUIComponents/container/TUIPerson/style/web.scss

@@ -1,265 +1,289 @@
-.TUI-contact-list {
-  flex: 1;
-  width: 100%;
-  list-style: none;
-
-  &-item {
-    padding: 16Px 12Px;
-    position: relative;
-    display: flex;
-    align-items: center;
-    cursor: pointer;
-
-    label {
-      font-size: 14Px;
-    }
-
-    &:hover {
-      .icon-close {
-        display: inline-block;
-      }
-    }
-
-    .left {
-      position: relative;
-      width: 48Px;
-      height: 48Px;
-      margin-right: 10px;
-
-      .num {
-        position: absolute;
-        display: inline-block;
-        right: -8Px;
-        top: -8Px;
-        width: 20Px;
-        height: 20Px;
-        font-size: 10Px;
-        text-align: center;
-        line-height: 20Px;
-        border-radius: 50%;
-      }
-
-      .avatar {
-        width: 100%;
-        border-radius: 5Px;
-      }
-
-      .online-status {
-        box-sizing: border-box;
-        position: absolute;
-        width: 11Px;
-        height: 11Px;
-        // left: 24Px;
-        // top: 22Px;
-        right: 0px;
-        bottom: 3px;
-        border: 2Px solid #ffffff;
-        box-shadow: 0Px 0Px 4Px rgba(0, 0, 0, 0.102606);
-        border-radius: 50%;
-
-        &-online {
-          background: #29cc85;
-        }
-
-        &-offline {
-          background: #a4a4a4;
-        }
-      }
-    }
-
-    .content {
-      flex: 1;
-      padding-left: 8Px;
-      display: flex;
-      justify-content: space-between;
-      align-items: center;
-
-      ul {
-        flex: 1;
-        display: flex;
-        flex-direction: column;
-
-        li {
-          flex: 1;
-          display: flex;
-          align-items: center;
-          font-size: 14Px;
-          line-height: 16Px;
-
-          span {
-            flex: 1;
-            width: 0;
-            overflow: hidden;
-            text-overflow: ellipsis;
-            white-space: nowrap;
-          }
-        }
-
-        .name {
-          padding-bottom: 2Px;
-          font-size: 14Px;
-          color: #333;
-          font-weight: 500;
-        }
-      }
-
-      .type {
-        padding: 0 4Px;
-        line-height: 14Px;
-        font-size: 12Px;
-        border-radius: 1Px;
-      }
-    }
-  }
-
-  .not-aside {
-    padding-left: 40Px;
-    display: flex;
-    justify-content: space-between;
-  }
-}
-
-.reduce {
-  position: relative;
-  display: inline-block;
-  width: 36Px;
-  height: 36Px;
-
-  &::before {
-    position: absolute;
-    content: '';
-    width: 50%;
-    height: 1Px;
-    top: 0;
-    bottom: 0;
-    left: 0;
-    right: 0;
-    margin: auto;
-  }
-}
-
-.center {
-  display: flex;
-  justify-content: center;
-}
-
-.btn {
-  padding: 8Px 20Px;
-  border-radius: 4Px;
-  border: none;
-  font-size: 14Px;
-  text-align: center;
-  line-height: 20Px;
-}
-
-input {
-  box-sizing: border-box;
-  border-radius: 5Px;
-  padding: 10Px;
-}
-
-.search {
-  flex: 1;
-  display: flex;
-
-  &-box {
-    flex: 1;
-    display: flex;
-    align-items: center;
-
-    h1 {
-      padding: 2Px 8Px;
-      font-size: 14Px;
-    }
-
-    .input-box {
-      display: flex;
-      position: relative;
-      flex: 1;
-
-      input {
-        flex: 1;
-        margin-right: 0;
-      }
-
-      .icon {
-        position: absolute;
-        right: 10Px;
-        top: 0;
-        bottom: 0;
-        margin: auto 0;
-      }
-    }
-
-    .search-cancel {
-      padding-left: 10Px;
-      font-size: 12Px;
-      line-height: 18Px;
-    }
-  }
-}
-
-.content-header {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-
-  label {
-    flex: 1;
-    font-size: 14Px;
-  }
-
-  .name {
-    width: 110Px;
-    letter-spacing: 0;
-    font-size: 14Px;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    white-space: nowrap;
-  }
-}
-
-.middle-box {
-  flex: 1;
-  display: flex;
-  align-items: center;
-  font-weight: 400;
-  color: #999999;
-  letter-spacing: 0;
-
-  span {
-    font-size: 12Px;
-  }
-
-  p {
-    flex: 1;
-    width: 0;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    white-space: nowrap;
-    font-size: 12Px;
-    line-height: 16Px;
-  }
-}
-
-.content-footer {
-  line-height: 16Px;
-  display: flex;
-  flex-direction: column;
-
-  .time {
-    font-size: 12Px;
-    line-height: 16Px;
-    display: inline-block;
-    max-width: 75Px;
-    white-space: nowrap;
-    color: #bbbbbb
-  }
-
-  img {
-    width: 16Px;
-    height: 16Px;
-    margin-top: 5Px;
-    align-self: flex-end;
-  }
-}
+ .TUI-person {
+   position: relative;
+   width: 100%;
+   height: calc(100% - 49px);
+   overflow-y: auto;
+   box-sizing: border-box;
+ }
+
+ .TUI-conversation-list {
+   position: absolute;
+   width: 100%;
+ }
+
+
+ .TUI-contact-list {
+   flex: 1;
+   width: 100%;
+   list-style: none;
+
+   &-item {
+     margin: 0 6px;
+     padding: 12Px 9Px;
+     position: relative;
+     display: flex;
+     align-items: center;
+     cursor: pointer;
+
+     label {
+       font-size: 14Px;
+     }
+
+     &:hover {
+       background: #F5F6FA;
+
+       .icon-close {
+         display: inline-block;
+       }
+     }
+
+     .left {
+       position: relative;
+       width: 48Px;
+       height: 48Px;
+       margin-right: 10px;
+
+       .num {
+         position: absolute;
+         display: inline-block;
+         right: -8Px;
+         top: -8Px;
+         width: 20Px;
+         height: 20Px;
+         font-size: 10Px;
+         text-align: center;
+         line-height: 20Px;
+         border-radius: 50%;
+       }
+
+       .avatar {
+         width: 100%;
+         border-radius: 5Px;
+         height: 100%;
+         object-fit: cover;
+       }
+
+       .online-status {
+         box-sizing: border-box;
+         position: absolute;
+         width: 11Px;
+         height: 11Px;
+         // left: 24Px;
+         // top: 22Px;
+         right: 0px;
+         bottom: 3px;
+         border: 2Px solid #ffffff;
+         box-shadow: 0Px 0Px 4Px rgba(0, 0, 0, 0.102606);
+         border-radius: 50%;
+
+         &-online {
+           background: #29cc85;
+         }
+
+         &-offline {
+           background: #a4a4a4;
+         }
+       }
+     }
+
+     .content {
+       flex: 1;
+       padding-left: 8Px;
+       display: flex;
+       justify-content: space-between;
+       align-items: center;
+
+       ul {
+         flex: 1;
+         display: flex;
+         flex-direction: column;
+
+         li {
+           flex: 1;
+           display: flex;
+           align-items: center;
+           font-size: 14Px;
+           line-height: 16Px;
+
+           span {
+             flex: 1;
+             width: 0;
+             overflow: hidden;
+             text-overflow: ellipsis;
+             white-space: nowrap;
+           }
+         }
+
+         .name {
+           padding-bottom: 2Px;
+           font-size: 14Px;
+           color: #333;
+           font-weight: 500;
+         }
+       }
+
+       .type {
+         padding: 0 4Px;
+         line-height: 14Px;
+         font-size: 12Px;
+         border-radius: 1Px;
+       }
+     }
+   }
+
+   .not-aside {
+     padding-left: 40Px;
+     display: flex;
+     justify-content: space-between;
+   }
+ }
+
+ .reduce {
+   position: relative;
+   display: inline-block;
+   width: 36Px;
+   height: 36Px;
+
+   &::before {
+     position: absolute;
+     content: '';
+     width: 50%;
+     height: 1Px;
+     top: 0;
+     bottom: 0;
+     left: 0;
+     right: 0;
+     margin: auto;
+   }
+ }
+
+ .center {
+   display: flex;
+   justify-content: center;
+ }
+
+ .btn {
+   padding: 8Px 20Px;
+   border-radius: 4Px;
+   border: none;
+   font-size: 14Px;
+   text-align: center;
+   line-height: 20Px;
+ }
+
+ input {
+   box-sizing: border-box;
+   border-radius: 5Px;
+   padding: 10Px;
+ }
+
+ .search {
+   flex: 1;
+   display: flex;
+
+   &-box {
+     flex: 1;
+     display: flex;
+     align-items: center;
+
+     h1 {
+       padding: 2Px 8Px;
+       font-size: 14Px;
+     }
+
+     .input-box {
+       display: flex;
+       position: relative;
+       flex: 1;
+
+       input {
+         flex: 1;
+         margin-right: 0;
+       }
+
+       .icon {
+         position: absolute;
+         right: 10Px;
+         top: 0;
+         bottom: 0;
+         margin: auto 0;
+       }
+     }
+
+     .search-cancel {
+       padding-left: 10Px;
+       font-size: 12Px;
+       line-height: 18Px;
+     }
+   }
+ }
+
+ .content-header {
+   flex: 1;
+   display: flex;
+   flex-direction: column;
+   cursor: pointer;
+
+   label {
+     flex: 1;
+     font-size: 14Px;
+   }
+
+   .name {
+     width: 110Px;
+     letter-spacing: 0;
+     font-size: 14Px;
+     overflow: hidden;
+     text-overflow: ellipsis;
+     white-space: nowrap;
+   }
+ }
+
+ .middle-box {
+   flex: 1;
+   display: flex;
+   align-items: center;
+   font-weight: 400;
+   color: #999999;
+   letter-spacing: 0;
+
+   span {
+     font-size: 12Px;
+   }
+
+   p {
+     flex: 1;
+     width: 0;
+     overflow: hidden;
+     text-overflow: ellipsis;
+     white-space: nowrap;
+     font-size: 12Px;
+     line-height: 16Px;
+   }
+ }
+
+ .content-footer {
+   line-height: 16Px;
+   display: flex;
+   flex-direction: column;
+
+   .time {
+     font-size: 12Px;
+     line-height: 16Px;
+     display: inline-block;
+     max-width: 75Px;
+     white-space: nowrap;
+     color: #bbbbbb
+   }
+
+   img {
+     width: 16Px;
+     height: 16Px;
+     margin-top: 5Px;
+     align-self: flex-end;
+   }
+ }
+
+ .sectionSearch {
+   padding: 0 20px 12px;
+ }

+ 9 - 0
src/TUIKit/api.ts

@@ -8,3 +8,12 @@ export const imGroupPage = (params?: object) => {
     data: params
   });
 };
+
+/**
+ * 即时通讯 - 好友列表
+ */
+export const imUserFriendPage = (params?: object) => {
+  return request.post('/edu-app/imUserFriend/page', {
+    data: params
+  });
+};

+ 14 - 9
src/components/layout/index.module.less

@@ -133,21 +133,26 @@
 
         :global {
           .n-badge-sup {
-            left: 20px;
-            -webkit-animation: TadaNum 1s 2s both infinite !important;
-            -moz-animation: TadaNum 1s 2s both infinite !important;
-            -ms-animation: TadaNum 1s 2s both infinite !important;
-            animation: TadaNum 1s 2s both infinite !important;
+            // left: 20px;
+            // -webkit-animation: TadaNum 1s 2s both infinite !important;
+            // -moz-animation: TadaNum 1s 2s both infinite !important;
+            // -ms-animation: TadaNum 1s 2s both infinite !important;
+            // animation: TadaNum 1s 2s both infinite !important;
           }
         }
 
         .messageIcon {
-          -webkit-animation: Tada 1s 2s both infinite;
-          -moz-animation: Tada 1s 2s both infinite;
-          -ms-animation: Tada 1s 2s both infinite;
-          animation: Tada 1s 2s both infinite;
+
           width: 32px;
           height: 32px;
+          cursor: pointer;
+
+          &.animation {
+            -webkit-animation: Tada 1s 2s both infinite;
+            -moz-animation: Tada 1s 2s both infinite;
+            -ms-animation: Tada 1s 2s both infinite;
+            animation: Tada 1s 2s both infinite;
+          }
         }
       }
 

+ 51 - 12
src/components/layout/layoutTop.tsx

@@ -1,4 +1,10 @@
-import { defineComponent, getCurrentInstance, ref } from 'vue';
+import {
+  defineComponent,
+  getCurrentInstance,
+  onBeforeMount,
+  ref,
+  onMounted
+} from 'vue';
 import styles from './index.module.less';
 import {
   NImage,
@@ -25,10 +31,12 @@ import { SDKAppID, secretKey } from '/src/main';
 import 'animate.css';
 import ForgotPassword from '/src/views/setting/modal/forgotPassword';
 import ImChat from '/src/views/im-chat/sChat.vue';
+import { eventGlobal } from '/src/utils';
 export default defineComponent({
   name: 'layoutTop',
   setup() {
     const router = useRouter();
+    const noReadCount = ref(0); // 未读数
     const showHeadFlag = ref(false);
     const showImChat = ref(false);
     const users = useUserStore();
@@ -51,8 +59,11 @@ export default defineComponent({
     const message = useMessage();
 
     const TUIKit: any = instance?.appContext.config.globalProperties.$TUIKit;
-
-    const onLoginIm = () => {
+    const onUnReadMessageCount = (res: any) => {
+      console.log(res, 'TOTAL_UNREAD_MESSAGE_COUNT_UPDATED');
+      noReadCount.value = res.data || 0;
+    };
+    const onLoginIm = (status = true) => {
       const options = genTestUserSig({
         SDKAppID,
         secretKey,
@@ -62,20 +73,31 @@ export default defineComponent({
         userID: info.value.imUserId,
         userSig: options.userSig
       };
-      console.log(TUIKit, 'TUIKit');
+      console.log(TUIKit, 'TUIKit', TUIKit.tim.isReady());
+
+      // chat;
       // 进页面开始登录 - 判断是否已经登录
-      if (TUIKit.isSDKReady) {
-        showImChat.value = true;
+      // 监听未读消息事件
+      TUIKit.tim.on(
+        TUIKit.TIM.EVENT.TOTAL_UNREAD_MESSAGE_COUNT_UPDATED,
+        onUnReadMessageCount
+      );
+      // 判断sdk是否准备好,可以防止被挤
+      if (TUIKit.isSDKReady && TUIKit.tim.isReady()) {
+        if (status) {
+          showImChat.value = true;
+        }
       } else {
         TUIKit.login(userInfo)
-          .then((res: any) => {
+          .then(() => {
             const options = {
               ...userInfo,
               expire: new Date().getTime() + EXPIRETIME * 1000
             };
-            console.log(options, res);
             users.setImUserInfo(options);
-            showImChat.value = true;
+            if (status) {
+              showImChat.value = true;
+            }
           })
           .catch((error: any) => {
             message.error(error);
@@ -83,6 +105,20 @@ export default defineComponent({
       }
     };
 
+    onLoginIm(false);
+
+    onMounted(() => {
+      eventGlobal.on('onNoReadMessageCount', (count: number) => {
+        noReadCount.value = count || 0;
+      });
+    });
+
+    onBeforeMount(() => {
+      TUIKit.tim.off(
+        TUIKit.TIM.EVENT.TOTAL_UNREAD_MESSAGE_COUNT_UPDATED,
+        onUnReadMessageCount
+      );
+    });
     return () => (
       <>
         <div class={styles.layoutTop}>
@@ -102,13 +138,16 @@ export default defineComponent({
                 onLoginIm();
               }}>
               <NBadge
-                value={8}
+                value={noReadCount.value}
                 max={99}
                 class={styles.messageBadge}
                 {...{ id: 'home-2' }}
                 color={'#FF1036'}>
                 <NImage
-                  class={[styles.messageIcon]}
+                  class={[
+                    styles.messageIcon,
+                    noReadCount.value > 0 ? styles.animation : ''
+                  ]}
                   preview-disabled
                   src={messageIcon}></NImage>
               </NBadge>
@@ -229,7 +268,7 @@ export default defineComponent({
           </NModal>
 
           <NModal class={styles.imChatModal} v-model:show={showImChat.value}>
-            <ImChat />
+            <ImChat onClose={() => (showImChat.value = false)} />
           </NModal>
         </div>
       </>

+ 8 - 0
src/store/modules/users.ts

@@ -11,6 +11,7 @@ export interface IUserState {
   avatar: string;
   info: any;
   imUserInfo: any;
+  noReadCount: number;
 }
 
 export const useUserStore = defineStore('user-store', {
@@ -19,10 +20,14 @@ export const useUserStore = defineStore('user-store', {
     imToken: storage.get(IM_TOKEN, ''),
     username: '',
     avatar: '',
+    noReadCount: 0, // 未读数量
     info: storage.get(CURRENT_USER, {}),
     imUserInfo: {} // IM
   }),
   getters: {
+    getNoReadCount(): number {
+      return this.noReadCount;
+    },
     getToken(): string {
       return this.token;
     },
@@ -43,6 +48,9 @@ export const useUserStore = defineStore('user-store', {
     }
   },
   actions: {
+    setNoReadCount(count: number) {
+      this.noReadCount = count;
+    },
     setToken(token: string) {
       this.token = token;
     },

+ 5 - 0
src/utils/index.ts

@@ -5,6 +5,11 @@ import { PageEnum } from '@/enums/pageEnum';
 import { isObject } from './is/index';
 import { cloneDeep } from 'lodash';
 import dayjs from 'dayjs';
+
+import EventEmitter from 'eventemitter3';
+
+export const eventGlobal = new EventEmitter();
+
 /**
  * render 图标
  * */

BIN
src/views/im-chat/assets/icon-close.png


+ 14 - 1
src/views/im-chat/index.module.less

@@ -749,7 +749,7 @@
           height: 100%;
           box-sizing: border-box;
           padding-left: 40Px;
-          padding-top: 100Px;
+          padding-top: 40Px;
           display: flex;
           flex-direction: column;
           background: url('./assets/image/login-background.png') no-repeat;
@@ -1183,4 +1183,17 @@
 
 /deep/ .n-tabs.n-tabs--top .n-tab-pane {
   padding: 10Px 0 0;
+}
+
+.closeModal {
+  position: absolute;
+  right: 21Px;
+  top: 14Px;
+  content: ' ';
+  width: 20Px;
+  height: 20Px;
+  background: url('./assets/icon-close.png') no-repeat center;
+  background-size: contain;
+  z-index: 99;
+  cursor: pointer;
 }

+ 15 - 171
src/views/im-chat/sChat.vue

@@ -10,115 +10,6 @@
     >
       <div class="home-main-box">
         <div class="home-TUIKit">
-          <!-- <div class="setting">
-            <main class="setting-main">
-              <aside class="userInfo">
-                <img
-                  class="avatar"
-                  :src="
-                    userInfo.avatar
-                      ? userInfo.avatar
-                      : 'https://news-info.ks3-cn-beijing.ksyuncs.com/07/1690787574969.png'
-                  "
-                  onerror="this.src='https://news-info.ks3-cn-beijing.ksyuncs.com/07/1690787574969.png'"
-                />
-                <div
-                  class="userInfo-main"
-                  :class="[showProfile ? 'TUIProfile' : '']"
-                  @click.self="handleChangeStatus"
-                >
-                  <main>
-                    <TUIProfile
-                      :view="showProfile ? 'edit' : 'default'"
-                      @changeStatus="handleChangeStatus"
-                    />
-                  </main>
-                </div>
-              </aside>
-              <ul class="setting-main-list">
-                <li
-                  class="setting-main-list-item"
-                  :class="[currentModel === 'message' && 'selected']"
-                  @click="selectModel('message')"
-                >
-                  <i
-                    v-show="currentModel === 'message'"
-                    class="icon icon-message-selected"
-                  ></i>
-                  <i
-                    v-show="currentModel !== 'message'"
-                    class="icon icon-message"
-                  ></i>
-                </li>
-                <li
-                  class="setting-main-list-item"
-                  :class="[currentModel === 'group' && 'selected']"
-                  @click="selectModel('group')"
-                >
-                  <i
-                    v-show="currentModel === 'group'"
-                    class="icon icon-relation-selected"
-                  ></i>
-                  <i
-                    v-show="currentModel !== 'group'"
-                    class="icon icon-relation"
-                  ></i>
-                </li>
-              </ul>
-            </main>
-            <div class="setting-footer">
-              <i class="icon icon-setting" @click="openShowMore"></i>
-              <div class="setting-more" v-if="showMore">
-                <div class="showmore">
-                  <ul class="setting-more-ul">
-                    <li
-                      v-for="item in moreList"
-                      :key="item.key"
-                      class="setting-more-li"
-                    >
-                      <div
-                        class="setting-more-item"
-                        @click="handleSelectClick(item)"
-                        @mouseover="showSelectMore = item.key"
-                      >
-                        <span>{{ $t(`Home.${item?.name}`) }}</span>
-                        <i
-                          v-show="item?.moreSelect"
-                          class="icon icon-right-transparent"
-                        ></i>
-                      </div>
-                      <ul
-                        v-if="item?.moreSelect && showSelectMore === item?.key"
-                        class="setting-more-item-next"
-                      >
-                        <li
-                          class="setting-more-item"
-                          @click="handleSelectClick(item, true)"
-                        >
-                          <span>{{ $t(`Home.开启`) }}</span>
-                          <i
-                            v-show="item?.status"
-                            class="icon icon-selected"
-                          ></i>
-                        </li>
-                        <li
-                          class="setting-more-item"
-                          @click="handleSelectClick(item, false)"
-                        >
-                          <span>{{ $t(`Home.关闭`) }}</span>
-                          <i
-                            v-show="!item?.status"
-                            class="icon icon-selected"
-                          ></i>
-                        </li>
-                      </ul>
-                    </li>
-                  </ul>
-                </div>
-                <div class="moreMask" @click.self="openShowMore"></div>
-              </div>
-            </div>
-          </div> -->
           <div class="home-TUIKit-main">
             <div class="conversation">
               <n-tabs
@@ -136,7 +27,6 @@
                 :value="currentModel"
                 @update:value="
                   (val: any) => {
-                    console.log(val, 'val')
                     currentModel = val;
                   }
                 "
@@ -157,6 +47,7 @@
                   tab="联系人"
                 ></n-tab-pane>
               </n-tabs>
+
               <TUIConversation
                 v-show="currentModel === 'message'"
                 @current="handleCurrentConversation"
@@ -180,67 +71,12 @@
                 :isNeedEmojiReact="true"
               >
                 <div class="chat-default">
-                  <h1>欢迎使用-即时通信</h1>
+                  <!-- <h1>欢迎使用</h1> -->
                   <!-- <p>随时随地</p> -->
                 </div>
               </TUIChat>
             </div>
           </div>
-          <!-- <div class="home-TUIKit-main" v-show="currentModel === 'message'">
-            <div class="conversation">
-              <n-tabs default-value="chat">
-                <n-tab-pane
-                  name="chat"
-                  display-directive="show:lazy"
-                  tab="聊天"
-                ></n-tab-pane>
-                <n-tab-pane
-                  name="group"
-                  display-directive="show:lazy"
-                  tab="群聊"
-                ></n-tab-pane>
-                <n-tab-pane
-                  name="contact"
-                  display-directive="show:lazy"
-                  tab="联系人"
-                ></n-tab-pane>
-              </n-tabs>
-              <TUIConversation
-                @current="handleCurrentConversation"
-                :displayOnlineStatus="displayOnlineStatus"
-              />
-            </div>
-            <div class="chat">
-              <TUIChat
-                :isMsgNeedReadReceipt="isMsgNeedReadReceipt"
-                :isNeedTyping="true"
-                :isNeedEmojiReact="true"
-              >
-                <div class="chat-default">
-                  <h1>
-                    欢迎使用
-                    即时通信
-                  </h1>
-                  <p>随时随地</p>
-                </div>
-              </TUIChat>
-            </div>
-          </div>
-          <div class="home-TUIKit-main" v-show="currentModel === 'group'">
-            <TUIContact
-              v-show="currentModel === 'group'"
-              :displayOnlineStatus="displayOnlineStatus"
-            >
-              <div class="chat-default">
-                <h1>
-                  欢迎使用
-                  <img class="logo" src="./assets/image/logo.svg" alt="" />
-                  即时通信
-                </h1>
-                <p>随时随地</p>
-              </div>
-            </TUIContact>
-          </div> -->
         </div>
       </div>
     </main>
@@ -374,7 +210,7 @@
         :isNeedEmojiReact="true"
       />
     </main>
-    <Drag
+    <!-- <Drag
       :show="showCall || showCallMini"
       :class="[
         showCallMini && 'callkit-drag-container-mini',
@@ -396,7 +232,9 @@
         :onMinimized="onMinimized"
         :onMessageSentByMe="onMessageSentByMe"
       />
-    </Drag>
+    </Drag> -->
+
+    <i class="closeModal" @click="onClose"></i>
   </div>
 </template>
 
@@ -417,14 +255,15 @@ import { useStore } from 'vuex';
 import router from '@/router';
 // import { cancellation } from '../api';
 import { handleErrorPrompts } from '@/TUIKit/TUIComponents/container/utils';
-import Drag from '@/TUIKit/TUIComponents/components/drag';
+// import Drag from '@/TUIKit/TUIComponents/components/drag';
 import { TUINotification } from '@/TUIKit/TUIPlugin';
 export default defineComponent({
   components: {
     // HeaderTUI,
     // MenuTUI,
-    Drag
+    // Drag
   },
+  emits: ['close'],
   setup(props, context) {
     const instance = getCurrentInstance();
     const locale = useI18nLocale();
@@ -688,6 +527,10 @@ export default defineComponent({
       return;
     };
 
+    const onClose = () => {
+      context.emit('close');
+    };
+
     return {
       ...toRefs(data),
       dragRef,
@@ -718,7 +561,8 @@ export default defineComponent({
       onMessageSentByMe,
       handleSelectClick,
       allowNotification,
-      setNotification
+      setNotification,
+      onClose
     };
   }
 });