Home.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. <template>
  2. <div class="home-page">
  3. <div class="app-container" :class="{ fullscreen: isFullscreen }">
  4. <div class="navi-bar">
  5. <div class="navi-bar-box">
  6. <div class="top">
  7. <div class="user-head-image">
  8. <head-image :name="$store.state.userStore.userInfo.nickName" :size="38"
  9. :url="$store.state.userStore.userInfo.headImageThumb"
  10. @click.native="showSettingDialog = true">
  11. </head-image>
  12. </div>
  13. <div class="menu">
  14. <router-link class="link" v-bind:to="'/home/chat'">
  15. <div class="menu-item">
  16. <span class="icon iconfont icon-chat"></span>
  17. <div v-show="unreadCount > 0" class="unread-text">{{ unreadCount }}</div>
  18. </div>
  19. </router-link>
  20. <router-link class="link" v-bind:to="'/home/friend'">
  21. <div class="menu-item">
  22. <span class="icon iconfont icon-friend"></span>
  23. </div>
  24. </router-link>
  25. <router-link class="link" v-bind:to="'/home/group'">
  26. <div class="menu-item">
  27. <span class="icon iconfont icon-group" style="font-size: 28px"></span>
  28. </div>
  29. </router-link>
  30. </div>
  31. </div>
  32. <div class="botoom">
  33. <div class="botoom-item" @click="isFullscreen = !isFullscreen">
  34. <i class="el-icon-full-screen"></i>
  35. </div>
  36. <div class="botoom-item" @click="showSetting">
  37. <span class="icon iconfont icon-setting" style="font-size: 20px"></span>
  38. </div>
  39. <div class="botoom-item" @click="onExit()" title="退出">
  40. <span class="icon iconfont icon-exit"></span>
  41. </div>
  42. </div>
  43. </div>
  44. </div>
  45. <div class="content-box">
  46. <router-view></router-view>
  47. </div>
  48. <setting :visible="showSettingDialog" @close="closeSetting()"></setting>
  49. <user-info v-show="uiStore.userInfo.show" :pos="uiStore.userInfo.pos" :user="uiStore.userInfo.user"
  50. @close="$store.commit('closeUserInfoBox')"></user-info>
  51. <full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url"
  52. @close="$store.commit('closeFullImageBox')"></full-image>
  53. <rtc-private-video ref="rtcPrivateVideo"></rtc-private-video>
  54. <rtc-group-video ref="rtcGroupVideo"></rtc-group-video>
  55. </div>
  56. </div>
  57. </template>
  58. <script>
  59. import HeadImage from '../components/common/HeadImage.vue';
  60. import Setting from '../components/setting/Setting.vue';
  61. import UserInfo from '../components/common/UserInfo.vue';
  62. import FullImage from '../components/common/FullImage.vue';
  63. import RtcPrivateVideo from '../components/rtc/RtcPrivateVideo.vue';
  64. import RtcPrivateAcceptor from '../components/rtc/RtcPrivateAcceptor.vue';
  65. import RtcGroupVideo from '../components/rtc/RtcGroupVideo.vue';
  66. export default {
  67. components: {
  68. HeadImage,
  69. Setting,
  70. UserInfo,
  71. FullImage,
  72. RtcPrivateVideo,
  73. RtcPrivateAcceptor,
  74. RtcGroupVideo
  75. },
  76. data() {
  77. return {
  78. showSettingDialog: false,
  79. lastPlayAudioTime: new Date().getTime() - 1000,
  80. isFullscreen: true,
  81. reconnecting: false
  82. }
  83. },
  84. methods: {
  85. init() {
  86. this.$eventBus.$on('openPrivateVideo', (rctInfo) => {
  87. // 进入单人视频通话
  88. this.$refs.rtcPrivateVideo.open(rctInfo);
  89. });
  90. this.$eventBus.$on('openGroupVideo', (rctInfo) => {
  91. // 进入多人视频通话
  92. this.$refs.rtcGroupVideo.open(rctInfo);
  93. });
  94. this.$store.dispatch("load").then(() => {
  95. // ws初始化
  96. this.$wsApi.connect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken"));
  97. this.$wsApi.onConnect(() => {
  98. if (this.reconnecting) {
  99. this.onReconnectWs();
  100. } else {
  101. // 加载离线消息
  102. this.pullPrivateOfflineMessage(this.$store.state.chatStore.privateMsgMaxId);
  103. this.pullGroupOfflineMessage(this.$store.state.chatStore.groupMsgMaxId);
  104. }
  105. });
  106. this.$wsApi.onMessage((cmd, msgInfo) => {
  107. if (cmd == 2) {
  108. // 关闭ws
  109. this.$wsApi.close(3000)
  110. // 异地登录,强制下线
  111. this.$alert("您已在其他地方登录,将被强制下线", "强制下线通知", {
  112. confirmButtonText: '确定',
  113. callback: action => {
  114. location.href = "/";
  115. }
  116. });
  117. } else if (cmd == 3) {
  118. // 插入私聊消息
  119. this.handlePrivateMessage(msgInfo);
  120. } else if (cmd == 4) {
  121. // 插入群聊消息
  122. this.handleGroupMessage(msgInfo);
  123. } else if (cmd == 5) {
  124. // 处理系统消息
  125. this.handleSystemMessage(msgInfo);
  126. }
  127. });
  128. this.$wsApi.onClose((e) => {
  129. console.log(e);
  130. if (e.code != 3000) {
  131. // 断线重连
  132. this.reconnectWs();
  133. }
  134. });
  135. }).catch((e) => {
  136. console.log("初始化失败", e);
  137. })
  138. },
  139. reconnectWs() {
  140. // 记录标志
  141. this.reconnecting = true;
  142. // 重新加载一次个人信息,目的是为了保证网络已经正常且token有效
  143. this.$store.dispatch("loadUser").then(() => {
  144. // 断线重连
  145. this.$message.error("连接断开,正在尝试重新连接...");
  146. this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem(
  147. "accessToken"));
  148. }).catch(() => {
  149. // 10s后重试
  150. setTimeout(() => this.reconnectWs(), 10000)
  151. })
  152. },
  153. onReconnectWs() {
  154. // 重连成功
  155. this.reconnecting = false;
  156. // 重新加载群和好友
  157. const promises = [];
  158. promises.push(this.$store.dispatch("loadFriend"));
  159. promises.push(this.$store.dispatch("loadGroup"));
  160. Promise.all(promises).then(() => {
  161. // 加载离线消息
  162. this.pullPrivateOfflineMessage(this.$store.state.chatStore.privateMsgMaxId);
  163. this.pullGroupOfflineMessage(this.$store.state.chatStore.groupMsgMaxId);
  164. this.$message.success("重新连接成功");
  165. }).catch(() => {
  166. this.$message.error("初始化失败");
  167. this.onExit();
  168. })
  169. },
  170. pullPrivateOfflineMessage(minId) {
  171. this.$store.commit("loadingPrivateMsg", true)
  172. this.$http({
  173. url: "/message/private/pullOfflineMessage?minId=" + minId,
  174. method: 'GET'
  175. }).catch(() => {
  176. this.$store.commit("loadingPrivateMsg", false)
  177. })
  178. },
  179. pullGroupOfflineMessage(minId) {
  180. this.$store.commit("loadingGroupMsg", true)
  181. this.$http({
  182. url: "/message/group/pullOfflineMessage?minId=" + minId,
  183. method: 'GET'
  184. }).catch(() => {
  185. this.$store.commit("loadingGroupMsg", false)
  186. })
  187. },
  188. handlePrivateMessage(msg) {
  189. // 标记这条消息是不是自己发的
  190. msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
  191. // 好友id
  192. let friendId = msg.selfSend ? msg.recvId : msg.sendId;
  193. // 会话信息
  194. let chatInfo = {
  195. type: 'PRIVATE',
  196. targetId: friendId
  197. }
  198. // 消息加载标志
  199. if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) {
  200. this.$store.commit("loadingPrivateMsg", JSON.parse(msg.content))
  201. return;
  202. }
  203. // 消息已读处理,清空已读数量
  204. if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
  205. this.$store.commit("resetUnreadCount", chatInfo)
  206. return;
  207. }
  208. // 消息回执处理,改消息状态为已读
  209. if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) {
  210. this.$store.commit("readedMessage", {
  211. friendId: msg.sendId
  212. })
  213. return;
  214. }
  215. // 消息撤回
  216. if (msg.type == this.$enums.MESSAGE_TYPE.RECALL) {
  217. this.$store.commit("recallMessage", [msg, chatInfo])
  218. return;
  219. }
  220. // 新增好友
  221. if (msg.type == this.$enums.MESSAGE_TYPE.FRIEND_NEW) {
  222. this.$store.commit("addFriend", JSON.parse(msg.content));
  223. return;
  224. }
  225. // 删除好友
  226. if (msg.type == this.$enums.MESSAGE_TYPE.FRIEND_DEL) {
  227. this.$store.commit("removeFriend", friendId);
  228. return;
  229. }
  230. // 单人webrtc 信令
  231. if (this.$msgType.isRtcPrivate(msg.type)) {
  232. this.$refs.rtcPrivateVideo.onRTCMessage(msg)
  233. return;
  234. }
  235. // 插入消息
  236. if (this.$msgType.isNormal(msg.type) || this.$msgType.isTip(msg.type) || this.$msgType.isAction(msg.type)) {
  237. let friend = this.loadFriendInfo(friendId);
  238. this.insertPrivateMessage(friend, msg);
  239. }
  240. },
  241. insertPrivateMessage(friend, msg) {
  242. let chatInfo = {
  243. type: 'PRIVATE',
  244. targetId: friend.id,
  245. showName: friend.nickName,
  246. headImage: friend.headImage
  247. };
  248. // 打开会话
  249. this.$store.commit("openChat", chatInfo);
  250. // 插入消息
  251. this.$store.commit("insertMessage", [msg, chatInfo]);
  252. // 播放提示音
  253. if (!msg.selfSend && this.$msgType.isNormal(msg.type) &&
  254. msg.status != this.$enums.MESSAGE_STATUS.READED) {
  255. this.playAudioTip();
  256. }
  257. },
  258. handleGroupMessage(msg) {
  259. // 标记这条消息是不是自己发的
  260. msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
  261. let chatInfo = {
  262. type: 'GROUP',
  263. targetId: msg.groupId
  264. }
  265. // 消息加载标志
  266. if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) {
  267. this.$store.commit("loadingGroupMsg", JSON.parse(msg.content))
  268. return;
  269. }
  270. // 消息已读处理
  271. if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
  272. // 我已读对方的消息,清空已读数量
  273. this.$store.commit("resetUnreadCount", chatInfo)
  274. return;
  275. }
  276. // 消息回执处理
  277. if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) {
  278. // 更新消息已读人数
  279. let msgInfo = {
  280. id: msg.id,
  281. groupId: msg.groupId,
  282. readedCount: msg.readedCount,
  283. receiptOk: msg.receiptOk
  284. };
  285. this.$store.commit("updateMessage", [msgInfo, chatInfo])
  286. return;
  287. }
  288. // 消息撤回
  289. if (msg.type == this.$enums.MESSAGE_TYPE.RECALL) {
  290. this.$store.commit("recallMessage", [msg, chatInfo])
  291. return;
  292. }
  293. // 新增群
  294. if (msg.type == this.$enums.MESSAGE_TYPE.GROUP_NEW) {
  295. this.$store.commit("addGroup", JSON.parse(msg.content));
  296. return;
  297. }
  298. // 删除群
  299. if (msg.type == this.$enums.MESSAGE_TYPE.GROUP_DEL) {
  300. console.log("this.$enums.MESSAGE_TYPE.GROUP_DE")
  301. this.$store.commit("removeGroup", msg.groupId);
  302. return;
  303. }
  304. // 群视频信令
  305. if (this.$msgType.isRtcGroup(msg.type)) {
  306. this.$nextTick(() => {
  307. this.$refs.rtcGroupVideo.onRTCMessage(msg);
  308. })
  309. return;
  310. }
  311. // 插入群聊消息
  312. if (this.$msgType.isNormal(msg.type) || this.$msgType.isTip(msg.type) || this.$msgType.isAction(msg.type)) {
  313. let group = this.loadGroupInfo(msg.groupId);
  314. this.insertGroupMessage(group, msg);
  315. }
  316. },
  317. insertGroupMessage(group, msg) {
  318. let chatInfo = {
  319. type: 'GROUP',
  320. targetId: group.id,
  321. showName: group.showGroupName,
  322. headImage: group.headImageThumb
  323. };
  324. // 打开会话
  325. this.$store.commit("openChat", chatInfo);
  326. // 插入消息
  327. this.$store.commit("insertMessage", [msg, chatInfo]);
  328. // 播放提示音
  329. if (!msg.selfSend && msg.type <= this.$enums.MESSAGE_TYPE.VIDEO &&
  330. msg.status != this.$enums.MESSAGE_STATUS.READED) {
  331. this.playAudioTip();
  332. }
  333. },
  334. handleSystemMessage(msg) {
  335. // 用户被封禁
  336. if (msg.type == this.$enums.MESSAGE_TYPE.USER_BANNED) {
  337. this.$wsApi.close(3000);
  338. this.$alert("您的账号已被管理员封禁,原因:" + msg.content, "账号被封禁", {
  339. confirmButtonText: '确定',
  340. callback: action => {
  341. this.onExit();
  342. }
  343. });
  344. return;
  345. }
  346. },
  347. onExit() {
  348. this.$wsApi.close(3000);
  349. sessionStorage.removeItem("accessToken");
  350. location.href = "/";
  351. },
  352. playAudioTip() {
  353. // 离线消息不播放铃声
  354. if (this.$store.getters.isLoading()) {
  355. return;
  356. }
  357. // 防止过于密集播放
  358. if (new Date().getTime() - this.lastPlayAudioTime > 1000) {
  359. this.lastPlayAudioTime = new Date().getTime();
  360. let audio = new Audio();
  361. let url = require(`@/assets/audio/tip.wav`);
  362. audio.src = url;
  363. audio.play();
  364. }
  365. },
  366. showSetting() {
  367. this.showSettingDialog = true;
  368. },
  369. closeSetting() {
  370. this.showSettingDialog = false;
  371. },
  372. loadFriendInfo(id) {
  373. let friend = this.$store.getters.findFriend(id);
  374. if (!friend) {
  375. friend = {
  376. id: id,
  377. showNickName: "未知用户",
  378. headImage: ""
  379. }
  380. }
  381. return friend;
  382. },
  383. loadGroupInfo(id) {
  384. let group = this.$store.getters.findGroup(id);
  385. if (!group) {
  386. group = {
  387. id: id,
  388. showGroupName: "未知群聊",
  389. headImageThumb: ""
  390. }
  391. }
  392. return group;
  393. }
  394. },
  395. computed: {
  396. uiStore() {
  397. return this.$store.state.uiStore;
  398. },
  399. unreadCount() {
  400. let unreadCount = 0;
  401. let chats = this.$store.state.chatStore.chats;
  402. chats.forEach((chat) => {
  403. if (!chat.delete) {
  404. unreadCount += chat.unreadCount
  405. }
  406. });
  407. return unreadCount;
  408. }
  409. },
  410. watch: {
  411. unreadCount: {
  412. handler(newCount, oldCount) {
  413. let tip = newCount > 0 ? `${newCount}条未读` : "";
  414. this.$elm.setTitleTip(tip);
  415. },
  416. immediate: true
  417. }
  418. },
  419. mounted() {
  420. this.init();
  421. },
  422. unmounted() {
  423. this.$wsApi.close();
  424. }
  425. }
  426. </script>
  427. <style scoped lang="scss">
  428. .home-page {
  429. height: 100vh;
  430. width: 100vw;
  431. display: flex;
  432. justify-content: center;
  433. align-items: center;
  434. border-radius: 4px;
  435. overflow: hidden;
  436. background: var(--im-color-primary-light-9);
  437. //background-image: url('../assets/image/background.jpg');
  438. .app-container {
  439. width: 62vw;
  440. height: 80vh;
  441. display: flex;
  442. min-height: 600px;
  443. min-width: 970px;
  444. position: absolute;
  445. border-radius: 4px;
  446. overflow: hidden;
  447. box-shadow: var(--im-box-shadow-dark);
  448. transition: 0.2s;
  449. &.fullscreen {
  450. transition: 0.2s;
  451. width: 100vw;
  452. height: 100vh;
  453. }
  454. }
  455. .navi-bar {
  456. --icon-font-size: 22px;
  457. --width: 60px;
  458. width: var(--width);
  459. background: var(--im-color-primary-light-1);
  460. padding-top: 20px;
  461. .navi-bar-box {
  462. height: 100%;
  463. display: flex;
  464. flex-direction: column;
  465. justify-content: space-between;
  466. .botoom {
  467. margin-bottom: 30px;
  468. }
  469. }
  470. .user-head-image {
  471. display: flex;
  472. justify-content: center;
  473. }
  474. .menu {
  475. height: 200px;
  476. //margin-top: 10px;
  477. display: flex;
  478. flex-direction: column;
  479. justify-content: center;
  480. align-content: center;
  481. .link {
  482. text-decoration: none;
  483. }
  484. .router-link-active .menu-item {
  485. color: #fff;
  486. background: var(--im-color-primary-light-2);
  487. }
  488. .link:not(.router-link-active) .menu-item:hover {
  489. color: var(--im-color-primary-light-7);
  490. }
  491. .menu-item {
  492. position: relative;
  493. color: var(--im-color-primary-light-4);
  494. width: var(--width);
  495. height: 46px;
  496. display: flex;
  497. justify-content: center;
  498. align-items: center;
  499. margin-bottom: 12px;
  500. .icon {
  501. font-size: var(--icon-font-size)
  502. }
  503. .unread-text {
  504. position: absolute;
  505. background-color: var(--im-color-danger);
  506. left: 28px;
  507. top: 8px;
  508. color: white;
  509. border-radius: 30px;
  510. padding: 0 5px;
  511. font-size: var(--im-font-size-smaller);
  512. text-align: center;
  513. white-space: nowrap;
  514. border: 1px solid #f1e5e5;
  515. }
  516. }
  517. }
  518. .botoom-item {
  519. display: flex;
  520. justify-content: center;
  521. align-items: center;
  522. height: 50px;
  523. width: 100%;
  524. cursor: pointer;
  525. color: var(--im-color-primary-light-4);
  526. font-size: var(--icon-font-size);
  527. .icon {
  528. font-size: var(--icon-font-size)
  529. }
  530. &:hover {
  531. font-weight: 600;
  532. color: var(--im-color-primary-light-7);
  533. }
  534. }
  535. }
  536. .content-box {
  537. flex: 1;
  538. padding: 0;
  539. background-color: #fff;
  540. text-align: center;
  541. }
  542. }
  543. </style>