index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. <template>
  2. <div class="container">
  3. <div class="head-bg"/>
  4. <div class="message-header">
  5. <div class="header-lf">
  6. <div
  7. class="msg"
  8. :class="{ 'active-color': activeTab === 0 }"
  9. @click="activeTab = 0"
  10. >
  11. 消息
  12. <span v-if="showDot&&activeTab == 0" class="red-dot"></span>
  13. </div>
  14. <div
  15. class="discover"
  16. :class="{ 'active-color': activeTab === 1 }"
  17. @click="activeTab = 1"
  18. >
  19. 发现
  20. </div>
  21. </div>
  22. <div class="header-ri">
  23. <div class="contact">
  24. <span v-if="wsStore.unread" class="red-dot"></span>
  25. <svg-icon style="width: 25px; height: 25px;margin-right: 8px;" name="person" @click="goToPage('contact')"/>
  26. </div>
  27. <svg-icon style="width: 25px; height: 25px;" name="add" @click="showSheet = true;"/>
  28. </div>
  29. </div>
  30. <template v-if="activeTab === 0">
  31. <!-- <div class="search" @click="goSearch">
  32. <svg-icon class="search-icon" name="search" />
  33. <span>请输入关键词</span>
  34. </div> -->
  35. <van-pull-refresh v-model="loading" @refresh="onRefresh" style="height:100%">
  36. <div class="message-list">
  37. <template v-if="list.length > 0">
  38. <div class="list-item" :class="(item.uuid === wsStore.toUserInfo?.uuid ? wsStore.toUserInfo?.stick : item.stick) ? 'bgstick-color' : ''" v-for="(item,i) in list" :key="i" @click="goToChat(item,i)">
  39. <!-- 个人头像 -->
  40. <van-image class="item-img" round :src="formatAvatarUrl(item.avatar)" v-if="item.type == 'user'"/>
  41. <!-- 群聊头像 -->
  42. <template v-if="item.type == 'group'">
  43. <groupAvatar class="item-img" :users='item.avatars'></groupAvatar>
  44. </template>
  45. <div class="item-content">
  46. <div class="item-top">
  47. <div>{{item.sessionName || '群聊'}}</div>
  48. <div class="col">{{item.updatedAt}}</div>
  49. </div>
  50. <div class="item-bottom">
  51. <div class="col m-ellipsis">{{item.message || '我们已经相互关注,开始聊天吧'}}</div>
  52. <!-- 免打扰 -->
  53. <svg-icon
  54. v-if="item.uuid === wsStore.toUserInfo?.uuid ? wsStore.toUserInfo?.slience : item.slience"
  55. style="width: 15px; height: 15px;color: #8D8D8D;"
  56. name="disturb"
  57. />
  58. <div class="notice" v-if="item.unReadNum > 0 && !(item.uuid === wsStore.toUserInfo?.uuid ? wsStore.toUserInfo?.slience : item.slience)">{{item.unReadNum}}</div>
  59. </div>
  60. </div>
  61. </div>
  62. </template>
  63. <template v-else>
  64. <div class="no-more">
  65. <svg-icon class="no-more-img" name="no-more" />
  66. <div>暂无好友</div>
  67. </div>
  68. </template>
  69. </div>
  70. </van-pull-refresh>
  71. </template>
  72. <template v-else>
  73. <Discover />
  74. </template>
  75. <van-action-sheet
  76. v-model:show="showSheet"
  77. cancel-text="取消"
  78. close-on-click-action
  79. @cancel="showSheet = false"
  80. >
  81. <div class="sheet-content">
  82. <div class="sheet-li" @click="goToPage('search')">
  83. <svg-icon class="sheet-icon" name="add-friend" />
  84. <div class="sheet-text">添加朋友</div>
  85. </div>
  86. <div class="sheet-li" @click="goToPage('createGroupChat')">
  87. <svg-icon class="sheet-icon" name="group-chat" />
  88. <div class="sheet-text">创建群聊</div>
  89. </div>
  90. <div class="sheet-li no-border" @click="changeSM">
  91. <svg-icon class="sheet-icon" name="sm1"/>
  92. <div class="sheet-text">扫一扫</div>
  93. </div>
  94. </div>
  95. </van-action-sheet>
  96. </div>
  97. </template>
  98. <script setup>
  99. import { useWalletStore } from "@/stores/modules/walletStore";
  100. import { userList,friendRequest } from "@/api/path/im.api";
  101. import { ref } from 'vue'
  102. import { useRouter } from 'vue-router'
  103. import Discover from './components/Discover/Discover.vue'
  104. import groupAvatar from './components/groupAvatar/index.vue'
  105. import { useWebSocketStore } from "@/stores/modules/webSocketStore.js";
  106. import { startScan } from "@/composables/barcodeScanner.js"
  107. import { useSystemStore } from "@/stores/modules/systemStore";
  108. import { showToast } from "vant";
  109. const IM_PATH = import.meta.env.VITE_IM_PATH_FIlE;
  110. const walletStore = useWalletStore();
  111. const systemStore = useSystemStore();
  112. const router = useRouter();
  113. const route = useRoute();
  114. const wsStore = useWebSocketStore();
  115. const loading = ref(false);
  116. const activeTab = ref(Number(route.query.tab ?? 0));
  117. const showDot = ref(false)
  118. const showSheet = ref(false);
  119. const list = ref([]);
  120. const formatAvatarUrl = (url) => url && /^https?:\/\//.test(url) ? url: (IM_PATH + (url || ''))
  121. const onRefresh = () => {
  122. getuserList(true);
  123. };
  124. watch(
  125. () => wsStore.chatRefresh,
  126. (newVal,oldVal) => {
  127. if (newVal !== oldVal) {
  128. setTimeout(() => {
  129. getuserList(true)
  130. }, 500);
  131. }
  132. }
  133. );
  134. watch(
  135. () => wsStore.needRefreshIm,
  136. (newVal) => {
  137. if (newVal) {
  138. getuserList();
  139. systemStore.needRefreshIm = false;
  140. }
  141. }
  142. );
  143. // 当切换 tab 时,把 tab 写进 URL(用 replace,不污染历史栈)
  144. watch(activeTab, (val) => {
  145. if (String(route.query.tab) !== String(val)) {
  146. router.replace({ query: { ...route.query, tab: val } });
  147. }
  148. });
  149. // 当 URL 变化(比如返回)时,更新 activeTab
  150. watch(
  151. () => route.query.tab,
  152. (val) => {
  153. const n = Number(val ?? 0);
  154. if (n !== activeTab.value) activeTab.value = n;
  155. }
  156. );
  157. const getuserList = async (refresh=false) => {
  158. let res
  159. if(refresh || !systemStore.ImsessionList || systemStore.ImsessionList.length == 0){
  160. loading.value = true;
  161. res = await userList({uuid:walletStore.account});
  162. loading.value = false;
  163. list.value = res.data || []
  164. // const groups = res.data.groups || [];
  165. // const ulist = res.data.userList || [];
  166. // // 转换群组
  167. // const groupItems = groups.map(g => ({
  168. // id: g.groupId,
  169. // uuid: g.uuid,
  170. // nickname:g.name || "群聊",
  171. // avatar: g.users,
  172. // createDate:g.createDate,
  173. // notice:g.notice,
  174. // newMsg:g.newMsg,
  175. // unReadNum:g.unReadNum,
  176. // type: "group"
  177. // }));
  178. // // 转换用户
  179. // const userItems = ulist.map(u => ({
  180. // id: u.id,
  181. // uuid: u.uuid,
  182. // nickname: u.nickname,
  183. // avatar: u.avatar ? IM_PATH + u.avatar : u.avatar,
  184. // createdAt:u.createdAt,
  185. // newMsg:u.newMsg,
  186. // unReadNum:u.unReadNum,
  187. // type: "user"
  188. // }));
  189. systemStore.ImsessionList = [...list.value];
  190. // 记录用户群组
  191. // wsStore.updateGroupAuth(res.data || []);
  192. }else{
  193. list.value = systemStore.ImsessionList
  194. }
  195. // console.log('会话列表数据',list.value)
  196. for(var i in list.value){
  197. if(list.value[i].unReadNum>0){
  198. showDot.value=1;
  199. break
  200. }
  201. }
  202. }
  203. const goToChat = (item,index) => {
  204. if(wsStore.chatDelAuth[item.uuid]){
  205. showToast(`${item.type == 'user' ? '对方已删除' : '您已不在群聊里面'}`);
  206. return;
  207. }
  208. systemStore.ImsessionList[index].unReadNum = 0
  209. wsStore.toUserInfo = item;
  210. router.push({
  211. path: 'chat',
  212. query:{ uuid:item.uuid }
  213. })
  214. }
  215. // 扫码
  216. const changeSM = async () => {
  217. const result = await startScan();
  218. let walletAddress = result.ScanResult;
  219. router.push({
  220. path: 'search',
  221. query:{ walletAddress }
  222. })
  223. }
  224. const goToPage = (url) => {
  225. router.push(url)
  226. }
  227. // 跳转聊天搜索
  228. const goSearch = () => {
  229. router.push('chatSearch')
  230. }
  231. onMounted(()=>{
  232. getuserList(systemStore.needRefreshIm);
  233. systemStore.needRefreshIm = false;
  234. wsStore.funInitfriend(walletStore.account);
  235. })
  236. </script>
  237. <style lang="less" scoped>
  238. .container{
  239. height: calc(100vh - 60px);
  240. display: flex;
  241. flex-direction: column;
  242. .head-bg {
  243. .fn-head-bg()
  244. }
  245. .message-header{
  246. margin: 52px 16px 18px;
  247. display: flex;
  248. justify-content: space-between;
  249. align-items: center;
  250. .header-lf{
  251. display: flex;
  252. align-items: center;
  253. font-family: PingFang SC, PingFang SC;
  254. font-weight: 400;
  255. font-size: 15px;
  256. color: #4F4F4F;
  257. .msg, .discover {
  258. margin-right: 12px;
  259. position: relative;
  260. cursor: pointer;
  261. color: #4F4F4F;
  262. }
  263. .active-color {
  264. font-size: 19px;
  265. font-weight: 500;
  266. color: #000;
  267. }
  268. }
  269. .header-ri{
  270. display: flex;
  271. align-items: center;
  272. }
  273. }
  274. .search{
  275. margin-bottom: 16px;
  276. height: 33px;
  277. background: #FFFFFF;
  278. border-radius: 23px;
  279. opacity: 0.5;
  280. padding: 6px;
  281. box-sizing: border-box;
  282. margin: 0 16px;
  283. font-family: PingFang SC, PingFang SC;
  284. font-weight: 500;
  285. font-size: 15px;
  286. color: #95A9ED;
  287. display: flex;
  288. align-items: center;
  289. .search-icon{
  290. height: 25px;
  291. width: 25px;
  292. margin-right: 6px;
  293. }
  294. }
  295. .message-list{
  296. background: #F7F8FA;
  297. border-radius: 30px 30px 0 0;
  298. flex: 1;
  299. overflow: auto;
  300. // margin-top: 16px;
  301. // padding: 26px 16px 0;
  302. height: 100%;
  303. box-sizing: border-box;
  304. .list-item{
  305. margin-bottom:10px;
  306. display: flex;
  307. align-items: center;
  308. padding: 16px 16px 8px;
  309. .item-img{
  310. width:42px;
  311. height:42px;
  312. flex-shrink: 0;
  313. margin-right: 12px;
  314. }
  315. .item-content{
  316. flex: 1;
  317. overflow: hidden;
  318. .item-top,.item-bottom{
  319. display: flex;
  320. align-items: center;
  321. justify-content: space-between;
  322. font-family: PingFang SC, PingFang SC;
  323. font-weight: 400;
  324. font-size: 15px;
  325. color: #000000;
  326. .col{
  327. font-size: 12px;
  328. color: #8D8D8D;
  329. }
  330. }
  331. .item-top{
  332. margin-bottom: 2px;
  333. }
  334. .item-bottom{
  335. .m-ellipsis{
  336. overflow: hidden;
  337. white-space: nowrap;
  338. text-overflow: ellipsis;
  339. }
  340. }
  341. }
  342. }
  343. }
  344. .contact{
  345. position: relative;
  346. width: 25px;
  347. height: 25px;
  348. margin-right: 8px;
  349. }
  350. .red-dot {
  351. position: absolute;
  352. top: 0px;
  353. right: -4px;
  354. width: 6px;
  355. height: 6px;
  356. background: #FF6C6C;
  357. border-radius: 50%;
  358. display: inline-block;
  359. }
  360. .notice{
  361. min-width: 15px;
  362. height: 15px;
  363. line-height: 15px;
  364. background-color: #FF6C6C;
  365. font-size: 10px;
  366. color: #FFFFFF;
  367. text-align: center;
  368. border-radius: 50px;
  369. margin-left: 5px;
  370. }
  371. .addnotice{
  372. position: absolute;
  373. top: -5px;
  374. left: -15px;
  375. }
  376. .message-list::-webkit-scrollbar{
  377. width: 0;
  378. }
  379. .discover-list {
  380. flex: 1;
  381. display: flex;
  382. justify-content: center;
  383. align-items: center;
  384. background: #F7F8FA;
  385. border-radius: 30px 30px 0 0;
  386. margin-top: 16px;
  387. padding: 26px 16px 0;
  388. }
  389. .sheet-content{
  390. font-family: PingFang SC, PingFang SC;
  391. font-weight: 500;
  392. font-size: 15px;
  393. color: #000000;
  394. padding: 12px 16px 0;
  395. .sheet-li{
  396. display: flex;
  397. align-items: center;
  398. justify-content: center;
  399. border-bottom: 1px solid #F2F2F2;
  400. padding: 10px 0;
  401. .sheet-icon{
  402. width:20px;
  403. height:20px;
  404. }
  405. .sheet-text{
  406. text-align: center;
  407. width: 70px;
  408. }
  409. }
  410. .no-border{
  411. border-bottom:none;
  412. }
  413. }
  414. :deep(.van-action-sheet__cancel){
  415. font-family: PingFang SC, PingFang SC;
  416. font-weight: 500 !important;
  417. font-size: 15px !important;
  418. color: #FF0000 !important;
  419. }
  420. }
  421. .no-more{
  422. margin-top: 178px;
  423. text-align: center;
  424. font-family: PingFang SC, PingFang SC;
  425. font-weight: 400;
  426. font-size: 15px;
  427. color: #8D8D8D;
  428. .no-more-img{
  429. width: 302px;
  430. height: 222px;
  431. }
  432. }
  433. .bgstick-color{
  434. background: #f2f2f2;
  435. }
  436. </style>