├── index.md ├── static ├── .gitkeep ├── emoji │ ├── 100.gif │ ├── 101.gif │ ├── 102.gif │ ├── 103.gif │ ├── 104.gif │ ├── 105.gif │ ├── 106.gif │ ├── 107.gif │ ├── 108.gif │ ├── 109.gif │ ├── 110.gif │ ├── 111.gif │ ├── 112.gif │ ├── 113.gif │ ├── 114.gif │ ├── 115.gif │ ├── 116.gif │ ├── 117.gif │ ├── 118.gif │ ├── 119.gif │ ├── 120.gif │ ├── 121.gif │ ├── 122.gif │ ├── 123.gif │ ├── 124.gif │ ├── 125.gif │ ├── 126.gif │ ├── 127.gif │ ├── 128.gif │ ├── 129.gif │ ├── 130.gif │ ├── 131.gif │ ├── 132.gif │ ├── 133.gif │ ├── 134.gif │ ├── 135.gif │ ├── 136.gif │ ├── 137.gif │ ├── 138.gif │ ├── 139.gif │ ├── 140.gif │ ├── 141.gif │ ├── 142.gif │ ├── 143.gif │ ├── 144.gif │ ├── 145.gif │ ├── 146.gif │ ├── 147.gif │ ├── 148.gif │ ├── 149.gif │ ├── 150.gif │ ├── 151.gif │ ├── 152.gif │ ├── 153.gif │ ├── 154.gif │ ├── 155.gif │ ├── 156.gif │ ├── 157.gif │ ├── 158.gif │ ├── 159.gif │ ├── 160.gif │ ├── 161.gif │ ├── 162.gif │ ├── 163.gif │ ├── 164.gif │ ├── 165.gif │ ├── 166.gif │ ├── 167.gif │ ├── 168.gif │ ├── 169.gif │ ├── 170.gif │ ├── 171.gif │ ├── 172.gif │ ├── 173.gif │ ├── 174.gif │ ├── 175.gif │ ├── 176.gif │ ├── 177.gif │ ├── 178.gif │ ├── 179.gif │ ├── 180.gif │ ├── 181.gif │ ├── 182.gif │ ├── 183.gif │ ├── 184.gif │ ├── 185.gif │ ├── 186.gif │ ├── 187.gif │ ├── 188.gif │ ├── 189.gif │ ├── 190.gif │ ├── 191.gif │ ├── 192.gif │ ├── 193.gif │ ├── 194.gif │ ├── 195.gif │ ├── 196.gif │ ├── 197.gif │ ├── 198.gif │ ├── 199.gif │ ├── meinv.png │ ├── shangxin.png │ └── weixiao.png ├── images │ ├── 悟空.jpg │ ├── Guai.jpg │ ├── vue.jpg │ ├── 加菲猫.jpg │ ├── 大飞哥.jpg │ ├── 小姨妈.jpg │ ├── 新之助.jpg │ ├── 萌萌俊.jpg │ ├── father.jpg │ ├── microzz.jpg │ ├── mother.jpg │ ├── orange.jpg │ ├── newfriend.jpg │ └── UserAvatar.jpg └── css │ └── reset.css ├── favicon.ico ├── attachment ├── vue-chat.png ├── vue-chat-pic.png ├── vue-chat-unread.png ├── vue-chat-video.png ├── vue-chat-rtc-audio.png ├── vue-chat-rtc-video.png ├── qq_qrcode_universe_push.jpg └── wechat-full-screen-mode.png ├── src ├── assets │ ├── img │ │ ├── logo.png │ │ └── qrcode.png │ ├── fonts │ │ ├── iconfont.eot │ │ ├── iconfont.ttf │ │ ├── iconfont.woff │ │ ├── iconfont.woff2 │ │ ├── iconfont.json │ │ └── iconfont.css │ └── dark-mode.css ├── page │ ├── group │ │ └── delete.png │ ├── friend │ │ ├── friend.vue │ │ └── searchfriend.vue │ ├── chat │ │ └── chat.vue │ └── main.vue ├── components │ ├── info │ │ ├── man.png │ │ └── woman.png │ ├── search │ │ ├── delete.png │ │ └── search.vue │ ├── menu │ │ ├── addtip.vue │ │ ├── about.vue │ │ ├── rightMenu.vue │ │ └── relayMessage.vue │ ├── friendlist │ │ └── friendlist.vue │ ├── mycard │ │ └── mycard.vue │ └── chatlist │ │ └── chatlist.vue ├── websocket │ ├── model │ │ ├── groupType.js │ │ ├── groupMemberType.js │ │ ├── stateSelectChatMessage.js │ │ ├── stateConversationInfo.js │ │ ├── conversationType.js │ │ ├── unReadCount.js │ │ ├── conversationInfo.js │ │ ├── protoConversationInfo.js │ │ ├── groupMember.js │ │ ├── groupInfo.js │ │ ├── conversation.js │ │ └── userInfo.js │ ├── listener │ │ └── onReceiverMessageListener.js │ ├── handler │ │ ├── messageHandler.js │ │ ├── quitGroupHandler.js │ │ ├── dismissGroupHandler.js │ │ ├── addGroupMemberHandler.js │ │ ├── kickGroupmemberHandler.js │ │ ├── createGroupHandler.js │ │ ├── setFriendAliasRequestHandler.js │ │ ├── recallMessageHandler.js │ │ ├── getMinioUploadUrlHandler.js │ │ ├── modifyMyInfoHandler.js │ │ ├── friendAddRequestHandler.js │ │ ├── searchUserResultHandler.js │ │ ├── handleFriendRequestHandler.js │ │ ├── notifyFriendHandler.js │ │ ├── notifyRecallMessageHandler.js │ │ ├── getGroupMemberHandler.js │ │ ├── getUploadtokenHandler.js │ │ ├── getGroupInfoHandler.js │ │ ├── getfriendresultHandler.js │ │ ├── notifyMessageHandler.js │ │ ├── notifyFriendRequestHandler.js │ │ ├── friendRequestHandler.js │ │ ├── abstractmessagehandler.js │ │ ├── connectackhandler.js │ │ ├── receiveMessageHandler.js │ │ ├── sendMessageHandler.js │ │ └── getuserinfoHandler.js │ ├── utils │ │ ├── logger.js │ │ ├── StringUtil.js │ │ ├── aes.js │ │ └── timeUtils.js │ ├── message │ │ ├── persistFlag.js │ │ ├── modifyGroupInfoType.js │ │ ├── notification │ │ │ ├── groupNotification.js │ │ │ ├── notificationMessageContent.js │ │ │ ├── quitGroupNotification.js │ │ │ ├── recallMessageNotification.js │ │ │ ├── dismissGroupNotification.js │ │ │ ├── changeGroupNameNotification.js │ │ │ ├── createGroupNotification.js │ │ │ ├── kickoffGroupMemberNotification.js │ │ │ └── addGroupMemberNotification.js │ │ ├── sendMessage.js │ │ ├── unsupportMessageContent.js │ │ ├── messageContentMediaType.js │ │ ├── messageStatus.js │ │ ├── myInfoType.js │ │ ├── unknownMessageContent.js │ │ ├── textMessageContent.js │ │ ├── messagePayload.js │ │ ├── videoMessageContent.js │ │ ├── imageMessageContent.js │ │ ├── websocketprotomessage.js │ │ ├── messageContent.js │ │ ├── mediaMessageContent.js │ │ ├── messageContentType.js │ │ ├── protomessageContent.js │ │ ├── protomessage.js │ │ └── message.js │ ├── future │ │ ├── futureResult.js │ │ └── promiseResolve.js │ ├── chatManager.js │ ├── websocketcli.js │ └── store │ │ └── localstore.js ├── webrtc │ ├── engineCallback.js │ ├── callState.js │ ├── sessionCallback.js │ ├── callEndReason.js │ ├── message │ │ ├── callByeMessageContent.js │ │ ├── callSignalMessageContent.js │ │ ├── callModifyMessageContent.js │ │ ├── callAnswerMessageContent.js │ │ ├── callAnswerTMessageContent.js │ │ └── callStartMessageContent.js │ └── callSession.js ├── permission.js ├── main.js ├── router │ └── index.js ├── App.vue └── constant │ └── index.js ├── .gitignore ├── .editorconfig ├── .postcssrc.js ├── .babelrc ├── index.html ├── config ├── prod.env.js ├── dev.env.js └── index.js ├── .github └── workflows │ └── deploy.yml ├── ABOUT_FEATURE.md ├── package.json ├── DARK_MODE_FEATURE.md └── dev.log /index.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/favicon.ico -------------------------------------------------------------------------------- /static/emoji/100.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/100.gif -------------------------------------------------------------------------------- /static/emoji/101.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/101.gif -------------------------------------------------------------------------------- /static/emoji/102.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/102.gif -------------------------------------------------------------------------------- /static/emoji/103.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/103.gif -------------------------------------------------------------------------------- /static/emoji/104.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/104.gif -------------------------------------------------------------------------------- /static/emoji/105.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/105.gif -------------------------------------------------------------------------------- /static/emoji/106.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/106.gif -------------------------------------------------------------------------------- /static/emoji/107.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/107.gif -------------------------------------------------------------------------------- /static/emoji/108.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/108.gif -------------------------------------------------------------------------------- /static/emoji/109.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/109.gif -------------------------------------------------------------------------------- /static/emoji/110.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/110.gif -------------------------------------------------------------------------------- /static/emoji/111.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/111.gif -------------------------------------------------------------------------------- /static/emoji/112.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/112.gif -------------------------------------------------------------------------------- /static/emoji/113.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/113.gif -------------------------------------------------------------------------------- /static/emoji/114.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/114.gif -------------------------------------------------------------------------------- /static/emoji/115.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/115.gif -------------------------------------------------------------------------------- /static/emoji/116.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/116.gif -------------------------------------------------------------------------------- /static/emoji/117.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/117.gif -------------------------------------------------------------------------------- /static/emoji/118.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/118.gif -------------------------------------------------------------------------------- /static/emoji/119.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/119.gif -------------------------------------------------------------------------------- /static/emoji/120.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/120.gif -------------------------------------------------------------------------------- /static/emoji/121.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/121.gif -------------------------------------------------------------------------------- /static/emoji/122.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/122.gif -------------------------------------------------------------------------------- /static/emoji/123.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/123.gif -------------------------------------------------------------------------------- /static/emoji/124.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/124.gif -------------------------------------------------------------------------------- /static/emoji/125.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/125.gif -------------------------------------------------------------------------------- /static/emoji/126.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/126.gif -------------------------------------------------------------------------------- /static/emoji/127.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/127.gif -------------------------------------------------------------------------------- /static/emoji/128.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/128.gif -------------------------------------------------------------------------------- /static/emoji/129.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/129.gif -------------------------------------------------------------------------------- /static/emoji/130.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/130.gif -------------------------------------------------------------------------------- /static/emoji/131.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/131.gif -------------------------------------------------------------------------------- /static/emoji/132.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/132.gif -------------------------------------------------------------------------------- /static/emoji/133.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/133.gif -------------------------------------------------------------------------------- /static/emoji/134.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/134.gif -------------------------------------------------------------------------------- /static/emoji/135.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/135.gif -------------------------------------------------------------------------------- /static/emoji/136.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/136.gif -------------------------------------------------------------------------------- /static/emoji/137.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/137.gif -------------------------------------------------------------------------------- /static/emoji/138.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/138.gif -------------------------------------------------------------------------------- /static/emoji/139.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/139.gif -------------------------------------------------------------------------------- /static/emoji/140.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/140.gif -------------------------------------------------------------------------------- /static/emoji/141.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/141.gif -------------------------------------------------------------------------------- /static/emoji/142.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/142.gif -------------------------------------------------------------------------------- /static/emoji/143.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/143.gif -------------------------------------------------------------------------------- /static/emoji/144.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/144.gif -------------------------------------------------------------------------------- /static/emoji/145.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/145.gif -------------------------------------------------------------------------------- /static/emoji/146.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/146.gif -------------------------------------------------------------------------------- /static/emoji/147.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/147.gif -------------------------------------------------------------------------------- /static/emoji/148.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/148.gif -------------------------------------------------------------------------------- /static/emoji/149.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/149.gif -------------------------------------------------------------------------------- /static/emoji/150.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/150.gif -------------------------------------------------------------------------------- /static/emoji/151.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/151.gif -------------------------------------------------------------------------------- /static/emoji/152.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/152.gif -------------------------------------------------------------------------------- /static/emoji/153.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/153.gif -------------------------------------------------------------------------------- /static/emoji/154.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/154.gif -------------------------------------------------------------------------------- /static/emoji/155.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/155.gif -------------------------------------------------------------------------------- /static/emoji/156.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/156.gif -------------------------------------------------------------------------------- /static/emoji/157.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/157.gif -------------------------------------------------------------------------------- /static/emoji/158.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/158.gif -------------------------------------------------------------------------------- /static/emoji/159.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/159.gif -------------------------------------------------------------------------------- /static/emoji/160.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/160.gif -------------------------------------------------------------------------------- /static/emoji/161.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/161.gif -------------------------------------------------------------------------------- /static/emoji/162.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/162.gif -------------------------------------------------------------------------------- /static/emoji/163.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/163.gif -------------------------------------------------------------------------------- /static/emoji/164.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/164.gif -------------------------------------------------------------------------------- /static/emoji/165.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/165.gif -------------------------------------------------------------------------------- /static/emoji/166.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/166.gif -------------------------------------------------------------------------------- /static/emoji/167.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/167.gif -------------------------------------------------------------------------------- /static/emoji/168.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/168.gif -------------------------------------------------------------------------------- /static/emoji/169.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/169.gif -------------------------------------------------------------------------------- /static/emoji/170.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/170.gif -------------------------------------------------------------------------------- /static/emoji/171.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/171.gif -------------------------------------------------------------------------------- /static/emoji/172.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/172.gif -------------------------------------------------------------------------------- /static/emoji/173.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/173.gif -------------------------------------------------------------------------------- /static/emoji/174.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/174.gif -------------------------------------------------------------------------------- /static/emoji/175.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/175.gif -------------------------------------------------------------------------------- /static/emoji/176.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/176.gif -------------------------------------------------------------------------------- /static/emoji/177.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/177.gif -------------------------------------------------------------------------------- /static/emoji/178.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/178.gif -------------------------------------------------------------------------------- /static/emoji/179.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/179.gif -------------------------------------------------------------------------------- /static/emoji/180.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/180.gif -------------------------------------------------------------------------------- /static/emoji/181.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/181.gif -------------------------------------------------------------------------------- /static/emoji/182.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/182.gif -------------------------------------------------------------------------------- /static/emoji/183.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/183.gif -------------------------------------------------------------------------------- /static/emoji/184.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/184.gif -------------------------------------------------------------------------------- /static/emoji/185.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/185.gif -------------------------------------------------------------------------------- /static/emoji/186.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/186.gif -------------------------------------------------------------------------------- /static/emoji/187.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/187.gif -------------------------------------------------------------------------------- /static/emoji/188.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/188.gif -------------------------------------------------------------------------------- /static/emoji/189.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/189.gif -------------------------------------------------------------------------------- /static/emoji/190.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/190.gif -------------------------------------------------------------------------------- /static/emoji/191.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/191.gif -------------------------------------------------------------------------------- /static/emoji/192.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/192.gif -------------------------------------------------------------------------------- /static/emoji/193.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/193.gif -------------------------------------------------------------------------------- /static/emoji/194.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/194.gif -------------------------------------------------------------------------------- /static/emoji/195.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/195.gif -------------------------------------------------------------------------------- /static/emoji/196.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/196.gif -------------------------------------------------------------------------------- /static/emoji/197.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/197.gif -------------------------------------------------------------------------------- /static/emoji/198.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/198.gif -------------------------------------------------------------------------------- /static/emoji/199.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/199.gif -------------------------------------------------------------------------------- /static/images/悟空.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/images/悟空.jpg -------------------------------------------------------------------------------- /static/emoji/meinv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/meinv.png -------------------------------------------------------------------------------- /static/images/Guai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/images/Guai.jpg -------------------------------------------------------------------------------- /static/images/vue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/images/vue.jpg -------------------------------------------------------------------------------- /static/images/加菲猫.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/images/加菲猫.jpg -------------------------------------------------------------------------------- /static/images/大飞哥.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/images/大飞哥.jpg -------------------------------------------------------------------------------- /static/images/小姨妈.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/images/小姨妈.jpg -------------------------------------------------------------------------------- /static/images/新之助.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/images/新之助.jpg -------------------------------------------------------------------------------- /static/images/萌萌俊.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/images/萌萌俊.jpg -------------------------------------------------------------------------------- /attachment/vue-chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/attachment/vue-chat.png -------------------------------------------------------------------------------- /src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/src/assets/img/logo.png -------------------------------------------------------------------------------- /src/assets/img/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/src/assets/img/qrcode.png -------------------------------------------------------------------------------- /src/page/group/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/src/page/group/delete.png -------------------------------------------------------------------------------- /static/emoji/shangxin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/shangxin.png -------------------------------------------------------------------------------- /static/emoji/weixiao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/emoji/weixiao.png -------------------------------------------------------------------------------- /static/images/father.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/images/father.jpg -------------------------------------------------------------------------------- /static/images/microzz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/images/microzz.jpg -------------------------------------------------------------------------------- /static/images/mother.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/images/mother.jpg -------------------------------------------------------------------------------- /static/images/orange.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/images/orange.jpg -------------------------------------------------------------------------------- /attachment/vue-chat-pic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/attachment/vue-chat-pic.png -------------------------------------------------------------------------------- /src/components/info/man.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/src/components/info/man.png -------------------------------------------------------------------------------- /static/images/newfriend.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/images/newfriend.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | -------------------------------------------------------------------------------- /attachment/vue-chat-unread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/attachment/vue-chat-unread.png -------------------------------------------------------------------------------- /attachment/vue-chat-video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/attachment/vue-chat-video.png -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/src/assets/fonts/iconfont.eot -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/src/assets/fonts/iconfont.ttf -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/src/assets/fonts/iconfont.woff -------------------------------------------------------------------------------- /src/components/info/woman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/src/components/info/woman.png -------------------------------------------------------------------------------- /static/images/UserAvatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/static/images/UserAvatar.jpg -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/src/assets/fonts/iconfont.woff2 -------------------------------------------------------------------------------- /src/components/search/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/src/components/search/delete.png -------------------------------------------------------------------------------- /attachment/vue-chat-rtc-audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/attachment/vue-chat-rtc-audio.png -------------------------------------------------------------------------------- /attachment/vue-chat-rtc-video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/attachment/vue-chat-rtc-video.png -------------------------------------------------------------------------------- /attachment/qq_qrcode_universe_push.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/attachment/qq_qrcode_universe_push.jpg -------------------------------------------------------------------------------- /attachment/wechat-full-screen-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comsince/vue-chat/HEAD/attachment/wechat-full-screen-mode.png -------------------------------------------------------------------------------- /src/websocket/model/groupType.js: -------------------------------------------------------------------------------- 1 | export default class GroupType { 2 | static Normal = 0; 3 | static Free = 1; 4 | static Restricted = 2; 5 | } -------------------------------------------------------------------------------- /src/websocket/listener/onReceiverMessageListener.js: -------------------------------------------------------------------------------- 1 | export default class OnReceiverMessageListener { 2 | onReceiveMessage(protoMessage){ 3 | 4 | } 5 | } -------------------------------------------------------------------------------- /src/websocket/model/groupMemberType.js: -------------------------------------------------------------------------------- 1 | export default class GroupMemberType { 2 | static Normal = 0; 3 | static Manager = 1; 4 | static Owner = 2; 5 | } -------------------------------------------------------------------------------- /src/websocket/model/stateSelectChatMessage.js: -------------------------------------------------------------------------------- 1 | export default class StateSelectChateMessage{ 2 | name = ''; 3 | target; 4 | protoMessages = []; 5 | } -------------------------------------------------------------------------------- /src/webrtc/engineCallback.js: -------------------------------------------------------------------------------- 1 | export default class EngineCallback{ 2 | onReceiveCall(callSession){} 3 | 4 | shouldStartRing(startRing){} 5 | 6 | shouldSopRing(){} 7 | } -------------------------------------------------------------------------------- /src/websocket/model/stateConversationInfo.js: -------------------------------------------------------------------------------- 1 | export default class StateConversationInfo{ 2 | name; 3 | img; 4 | //ProtoConversationInfo 5 | conversationInfo = {}; 6 | } -------------------------------------------------------------------------------- /src/websocket/handler/messageHandler.js: -------------------------------------------------------------------------------- 1 | export default class MessageHandler{ 2 | match(signal){ 3 | return false; 4 | } 5 | 6 | processMessage(data){ 7 | 8 | } 9 | } -------------------------------------------------------------------------------- /src/websocket/model/conversationType.js: -------------------------------------------------------------------------------- 1 | export default class ConversationType { 2 | static Single = 0; 3 | static Group = 1; 4 | static ChatRoom = 2; 5 | static Channel = 3; 6 | } -------------------------------------------------------------------------------- /src/websocket/model/unReadCount.js: -------------------------------------------------------------------------------- 1 | export default class UnreadCount { 2 | // 单聊未读数 3 | unread = 0; 4 | // 群聊@数 5 | unreadMention = 0; 6 | // 群聊@All数 7 | unreadMentionAll = 0; 8 | } -------------------------------------------------------------------------------- /src/websocket/utils/logger.js: -------------------------------------------------------------------------------- 1 | export default class Logger { 2 | static log(text){ 3 | var time = new Date(); 4 | console.log("[" + time.toLocaleTimeString() + "] " + text); 5 | } 6 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/websocket/message/persistFlag.js: -------------------------------------------------------------------------------- 1 | export default class PersistFlag { 2 | static No_Persist = 0; 3 | static Persist = 1; 4 | static Persist_And_Count = 3; 5 | static Transparent = 4; 6 | } -------------------------------------------------------------------------------- /src/websocket/message/modifyGroupInfoType.js: -------------------------------------------------------------------------------- 1 | export default class ModifyGroupInfoType { 2 | static Modify_Group_Name = 0; 3 | static Modify_Group_Portrait = 1; 4 | static Modify_Group_Extra = 2; 5 | } -------------------------------------------------------------------------------- /src/websocket/future/futureResult.js: -------------------------------------------------------------------------------- 1 | export default class FutureResult { 2 | code; 3 | result; 4 | 5 | constructor(code, result){ 6 | this.code = code 7 | this.result = result 8 | } 9 | } -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserlist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/webrtc/callState.js: -------------------------------------------------------------------------------- 1 | export default class CallState { 2 | static STATUS_IDLE = 0; 3 | static STATUS_OUTGOING = 1; 4 | static STATUS_INCOMING = 2; 5 | static STATUS_CONNECTING = 3; 6 | static STATUS_CONNECTED = 4; 7 | } -------------------------------------------------------------------------------- /src/websocket/message/notification/groupNotification.js: -------------------------------------------------------------------------------- 1 | import NotificationMessageContent from "./notificationMessageContent"; 2 | 3 | export default class GroupNotificationContent extends NotificationMessageContent { 4 | groupId = ''; 5 | } -------------------------------------------------------------------------------- /src/websocket/message/sendMessage.js: -------------------------------------------------------------------------------- 1 | export default class SendMessage{ 2 | target; 3 | messageContent; 4 | 5 | constructor(target,messageContent){ 6 | this.target = target; 7 | this.messageContent = messageContent; 8 | } 9 | } -------------------------------------------------------------------------------- /src/websocket/message/unsupportMessageContent.js: -------------------------------------------------------------------------------- 1 | import MessageContent from "./messageContent"; 2 | 3 | export default class UnsupportMessageContent extends MessageContent { 4 | 5 | digest() { 6 | return '尚不支持该类型消息, 请手机查看 : ' + this.type; 7 | } 8 | } -------------------------------------------------------------------------------- /src/websocket/model/conversationInfo.js: -------------------------------------------------------------------------------- 1 | export default class ConversationInfo{ 2 | conversation = {}; 3 | lastMessage = {}; 4 | timestamp = 0; 5 | draft = ''; 6 | unreadCount = {}; 7 | isTop = false; 8 | isSilent = false; 9 | 10 | } -------------------------------------------------------------------------------- /src/websocket/future/promiseResolve.js: -------------------------------------------------------------------------------- 1 | export default class PromiseResolve { 2 | resolve; 3 | timeoutId; 4 | protoMessageId; 5 | 6 | constructor(resolve,timeoutId){ 7 | this.resolve = resolve; 8 | this.timeoutId = timeoutId; 9 | } 10 | } -------------------------------------------------------------------------------- /src/websocket/message/messageContentMediaType.js: -------------------------------------------------------------------------------- 1 | export default class MessageContentMediaType { 2 | static General = 0; 3 | static Image = 1; 4 | static Voice = 2; 5 | static Video = 3; 6 | static File = 4; 7 | static Portrait = 5; 8 | static Fvaorite = 6; 9 | } -------------------------------------------------------------------------------- /src/websocket/utils/StringUtil.js: -------------------------------------------------------------------------------- 1 | export default class StringUtils { 2 | static b64_to_utf8(str) { 3 | return decodeURIComponent(escape(atob(str))); 4 | } 5 | 6 | 7 | static utf8_to_b64(str) { 8 | return btoa(unescape(encodeURIComponent(str))); 9 | } 10 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }], 4 | "stage-2" 5 | ], 6 | "plugins": ["transform-runtime"], 7 | "comments": false, 8 | "env": { 9 | "test": { 10 | "presets": ["env", "stage-2"], 11 | "plugins": [ "istanbul" ] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 飞享IM 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/websocket/message/messageStatus.js: -------------------------------------------------------------------------------- 1 | export default class MessageStatus { 2 | static Sending = 0; 3 | static Sent = 1; 4 | static SendFailure = 2; 5 | static Mentioned = 3; 6 | static AllMentioned = 4; 7 | static Unread = 5; 8 | static Readed = 6; 9 | static Played = 7; 10 | } -------------------------------------------------------------------------------- /src/websocket/handler/quitGroupHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { PUB_ACK, GQ } from "../../constant"; 3 | 4 | export default class QuitGroupHandler extends AbstractMessageHandler { 5 | match(proto){ 6 | return proto.signal == PUB_ACK && proto.subSignal == GQ; 7 | } 8 | } -------------------------------------------------------------------------------- /src/websocket/handler/dismissGroupHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { PUB_ACK, GD } from "../../constant"; 3 | 4 | export default class DismissGroupHandler extends AbstractMessageHandler{ 5 | match(proto){ 6 | return proto.signal == PUB_ACK && proto.subSignal == GD; 7 | } 8 | } -------------------------------------------------------------------------------- /src/websocket/handler/addGroupMemberHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { PUB_ACK, GAM } from "../../constant"; 3 | 4 | export default class AddGroupMemberHandler extends AbstractMessageHandler { 5 | match(proto){ 6 | return proto.signal == PUB_ACK && proto.subSignal == GAM; 7 | } 8 | } -------------------------------------------------------------------------------- /src/websocket/handler/kickGroupmemberHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { PUB_ACK, GKM } from "../../constant"; 3 | 4 | export default class KickGroupMemberHandler extends AbstractMessageHandler{ 5 | match(proto){ 6 | return proto.signal == PUB_ACK && proto.subSignal == GKM; 7 | } 8 | } -------------------------------------------------------------------------------- /src/websocket/handler/createGroupHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { PUB_ACK, GC, ERROR_CODE, SUCCESS_CODE } from "../../constant"; 3 | 4 | export default class CreateGroupHandler extends AbstractMessageHandler { 5 | match(proto){ 6 | return proto.signal == PUB_ACK && proto.subSignal == GC; 7 | } 8 | } -------------------------------------------------------------------------------- /src/websocket/handler/setFriendAliasRequestHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { PUB_ACK, FALS } from "../../constant"; 3 | 4 | export default class SetFriendAliasRequestHandler extends AbstractMessageHandler{ 5 | match(proto){ 6 | return proto.signal == PUB_ACK && proto.subSignal == FALS; 7 | } 8 | 9 | } -------------------------------------------------------------------------------- /src/websocket/model/protoConversationInfo.js: -------------------------------------------------------------------------------- 1 | import UnreadCount from "./unReadCount"; 2 | 3 | export default class ProtoConversationInfo{ 4 | conversationType; 5 | target; 6 | line; 7 | lastMessage = {}; 8 | timestamp = 0; 9 | draft = ''; 10 | unreadCount = new UnreadCount(); 11 | isTop = false; 12 | isSilent = false; 13 | } -------------------------------------------------------------------------------- /src/websocket/message/myInfoType.js: -------------------------------------------------------------------------------- 1 | export default class MyInfotype { 2 | static Modify_DisplayName = 0; 3 | static Modify_Portrait = 1; 4 | static Modify_Gender = 2; 5 | static Modify_Mobile = 3; 6 | static Modify_Email = 4; 7 | static Modify_Address = 5; 8 | static Modify_Company = 6; 9 | static Modify_Social = 7; 10 | static Modify_Extra = 8; 11 | } -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | var pkg = require('../package.json') 2 | 3 | module.exports = { 4 | NODE_ENV: '"production"', 5 | APP_VERSION: '"' + pkg.version + '"', 6 | BUILD_TIME: '"' + new Date().toLocaleString('zh-CN', { 7 | year: 'numeric', 8 | month: '2-digit', 9 | day: '2-digit', 10 | hour: '2-digit', 11 | minute: '2-digit', 12 | second: '2-digit' 13 | }) + '"' 14 | } 15 | -------------------------------------------------------------------------------- /src/webrtc/sessionCallback.js: -------------------------------------------------------------------------------- 1 | export default class SessionCallback{ 2 | didCallEndWithReason(callEndReason){} 3 | 4 | didChangeState(callState){} 5 | 6 | didChangeMode(mode){} 7 | 8 | didCreateLocalVideoTrack(stream){} 9 | 10 | didReceiveRemoteVideoTrack(stream){} 11 | 12 | didReceiveRemoteAudioTrack(stream){} 13 | 14 | didError(error){} 15 | 16 | didGetStats(stats){} 17 | } -------------------------------------------------------------------------------- /src/websocket/handler/recallMessageHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { PUB_ACK, MR } from "../../constant"; 3 | 4 | export default class RecallMessageHandler extends AbstractMessageHandler { 5 | match(proto){ 6 | return proto.signal == PUB_ACK && proto.subSignal == MR; 7 | } 8 | 9 | notifyContent(content){ 10 | var content = JSON.parse(content); 11 | return content.code; 12 | } 13 | } -------------------------------------------------------------------------------- /src/websocket/handler/getMinioUploadUrlHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { PUB_ACK, GMURL } from "../../constant"; 3 | 4 | export default class GetMinioUploadUrlHandler extends AbstractMessageHandler { 5 | match(proto){ 6 | return proto.signal == PUB_ACK && proto.subSignal == GMURL; 7 | } 8 | notifyContent(content){ 9 | var result = JSON.parse(content); 10 | return result; 11 | } 12 | } -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | var pkg = require('../package.json') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"', 7 | APP_VERSION: '"' + pkg.version + '"', 8 | BUILD_TIME: '"' + new Date().toLocaleString('zh-CN', { 9 | year: 'numeric', 10 | month: '2-digit', 11 | day: '2-digit', 12 | hour: '2-digit', 13 | minute: '2-digit', 14 | second: '2-digit' 15 | }) + '"' 16 | }) 17 | -------------------------------------------------------------------------------- /src/websocket/handler/modifyMyInfoHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { MMI, PUB_ACK } from "../../constant"; 3 | import Logger from "../utils/logger"; 4 | 5 | export default class ModifyInfoHandler extends AbstractMessageHandler{ 6 | match(proto){ 7 | return proto.signal == PUB_ACK && proto.subSignal == MMI; 8 | } 9 | 10 | processMessage(proto){ 11 | if(proto && proto.content != ''){ 12 | Logger.log("modify myInfo "+proto.content); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/webrtc/callEndReason.js: -------------------------------------------------------------------------------- 1 | export default class CallEndReason { 2 | static REASON_Unknown = 'unknown'; 3 | static REASON_Busy = 'busy'; 4 | static REASON_SignalError = 'signalError'; 5 | static REASON_Hangup = 'hangup'; 6 | static REASON_MediaError = 'mediaError'; 7 | static REASON_RemoteHangup = 'remoteHangup'; 8 | static REASON_OpenCameraFailure = 'openCameraError'; 9 | static REASON_Timeout = 'timeout'; 10 | static REASON_AcceptByOtherClient = 'acceptByOtherClient'; 11 | static REASON_AllLeft = 'allLeft'; 12 | } -------------------------------------------------------------------------------- /src/websocket/handler/friendAddRequestHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { FAR,PUB_ACK } from "../../constant"; 3 | import Logger from "../utils/logger"; 4 | 5 | export default class FriendAddRequestHandler extends AbstractMessageHandler{ 6 | match(proto){ 7 | return proto.signal == PUB_ACK && proto.subSignal == FAR; 8 | } 9 | 10 | processMessage(proto){ 11 | var result = JSON.parse(proto.content); 12 | Logger.log("friend add request result "+result); 13 | } 14 | } -------------------------------------------------------------------------------- /src/websocket/model/groupMember.js: -------------------------------------------------------------------------------- 1 | export default class GroupMember { 2 | groupId = ''; 3 | memberId = ''; 4 | alias = ''; 5 | type = 0; 6 | updateDt = 0; 7 | diplayName = ''; 8 | avatarUrl = ''; 9 | 10 | static convert2GroupMember(jsonObj){ 11 | var groupMember = new GroupMember(); 12 | groupMember.memberId = jsonObj.memberId; 13 | groupMember.alias = jsonObj.alias; 14 | groupMember.type = jsonObj.type; 15 | groupMember.updateDt = jsonObj.updateDt; 16 | return groupMember; 17 | } 18 | } -------------------------------------------------------------------------------- /src/websocket/chatManager.js: -------------------------------------------------------------------------------- 1 | export default class ChatManager { 2 | static onReceiveMessageListeners = []; 3 | 4 | static addReceiveMessageListener(listener){ 5 | this.onReceiveMessageListeners.push(listener); 6 | } 7 | 8 | static onReceiveMessage(protoMessage){ 9 | for(var messageListener of this.onReceiveMessageListeners){ 10 | messageListener.onReceiveMessage(protoMessage); 11 | } 12 | } 13 | 14 | static removeOnReceiveMessageListener(){ 15 | ChatManager.onReceiveMessageListeners = []; 16 | } 17 | } -------------------------------------------------------------------------------- /src/websocket/handler/searchUserResultHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { US,PUB_ACK } from "../../constant"; 3 | import Logger from "../utils/logger"; 4 | import UserInfo from "../model/userInfo"; 5 | 6 | export default class SearchUserResultHandler extends AbstractMessageHandler{ 7 | match(proto){ 8 | return proto.signal == PUB_ACK && proto.subSignal == US; 9 | } 10 | 11 | processMessage(proto){ 12 | var searchUserList = JSON.parse(proto.content); 13 | this.vueWebsocket.sendAction("updateSearchUser",searchUserList); 14 | } 15 | } -------------------------------------------------------------------------------- /src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import store from './store' 3 | const whiteList = ['/login'] // 不重定向白名单 4 | router.beforeEach((to, from, next) => { 5 | var token = localStorage.getItem('vue-token'); 6 | console.log('match route token '+token+" to "+to.path +" from "+from.path +" next "+next.path); 7 | if (token) { 8 | if (to.path === '/login') { 9 | next({ path: '/conversation' }) 10 | } else { 11 | next(); 12 | } 13 | } else { 14 | if (whiteList.indexOf(to.path) !== -1) { 15 | next() 16 | } else { 17 | next('/login') 18 | } 19 | } 20 | }) -------------------------------------------------------------------------------- /src/websocket/handler/handleFriendRequestHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { PUB_ACK, FHR } from "../../constant"; 3 | import Logger from "../utils/logger"; 4 | 5 | export default class HandleFriendRequestHandler extends AbstractMessageHandler { 6 | match(proto){ 7 | return proto.signal == PUB_ACK && proto.subSignal == FHR; 8 | } 9 | 10 | processMessage(proto){ 11 | if(proto.content && proto.content != ''){ 12 | var message = JSON.parse(proto.content); 13 | Logger.log("handle friend request "+message); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/websocket/message/unknownMessageContent.js: -------------------------------------------------------------------------------- 1 | import MessageContent from "./messageContent"; 2 | import MessageContentType from "./messageContentType"; 3 | 4 | export default class UnknownMessageContent extends MessageContent { 5 | originalPayload; 6 | 7 | constructor(originalPayload) { 8 | super(MessageContentType.Unknown); 9 | this.originalPayload = originalPayload; 10 | } 11 | 12 | encode() { 13 | return this.originalPayload; 14 | } 15 | 16 | decode(paylaod) { 17 | this.originalPayload = paylaod; 18 | } 19 | 20 | digest() { 21 | return '未知类型消息'; 22 | } 23 | } -------------------------------------------------------------------------------- /src/websocket/handler/notifyFriendHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { FN, PUBLISH } from "../../constant"; 3 | import Logger from "../utils/logger"; 4 | 5 | export default class NotifyFriendHandler extends AbstractMessageHandler { 6 | match(proto){ 7 | return proto.signal == PUBLISH && proto.subSignal == FN; 8 | } 9 | 10 | processMessage(proto){ 11 | if(proto.content && proto.content != ''){ 12 | var friendNotify = JSON.parse(proto.content); 13 | Logger.log("friend head "+friendNotify.head); 14 | this.vueWebsocket.getFriend(); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/websocket/message/notification/notificationMessageContent.js: -------------------------------------------------------------------------------- 1 | import MessageContent from "../messageContent"; 2 | export default class NotificationMessageContent extends MessageContent { 3 | // message#protoMessageToMessage时设置 4 | fromSelf = false; 5 | constructor(type) { 6 | super(type); 7 | } 8 | 9 | digest(message) { 10 | var desc = ''; 11 | try { 12 | desc = this.formatNotification(message); 13 | } catch (error) { 14 | console.log('disgest', error); 15 | } 16 | return desc; 17 | }; 18 | 19 | formatNotification(message) { 20 | return '..nofication..'; 21 | } 22 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import router from './router' 6 | import store from './store' 7 | import './permission' // permission control 8 | Vue.config.productionTip = false 9 | 10 | import ElementUI from 'element-ui'; 11 | import 'element-ui/lib/theme-chalk/index.css'; 12 | import './assets/dark-mode.css'; 13 | Vue.use(ElementUI); 14 | /* eslint-disable no-new */ 15 | const vm = new Vue({ 16 | el: '#app', 17 | router, 18 | store, 19 | template: '', 20 | components: { App } 21 | }) 22 | -------------------------------------------------------------------------------- /src/websocket/handler/notifyRecallMessageHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { PUBLISH, RMN } from "../../constant"; 3 | 4 | export default class NotifyRecallMessageHandler extends AbstractMessageHandler { 5 | match(proto){ 6 | return proto.signal == PUBLISH && proto.subSignal == RMN; 7 | } 8 | 9 | processMessage(proto){ 10 | if(proto.content){ 11 | var notifyRecalMessage = JSON.parse(proto.content); 12 | console.log("from user "+notifyRecalMessage.fromUser + " messageUid "+notifyRecalMessage.messageUid); 13 | this.vueWebsocket.sendAction('updateMessageContent',notifyRecalMessage); 14 | } 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /src/websocket/handler/getGroupMemberHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { PUB_ACK, GPGM } from "../../constant"; 3 | import GroupMember from "../model/groupMember"; 4 | 5 | export default class GetGroupMemberHandler extends AbstractMessageHandler { 6 | match(proto){ 7 | return proto.signal == PUB_ACK && proto.subSignal == GPGM; 8 | } 9 | 10 | notifyContent(content){ 11 | var groupMemberList = JSON.parse(content); 12 | var groupMembers = []; 13 | for(var groupMember of groupMemberList){ 14 | groupMembers.push(GroupMember.convert2GroupMember(groupMember)); 15 | } 16 | return groupMembers; 17 | } 18 | } -------------------------------------------------------------------------------- /src/websocket/handler/getUploadtokenHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { PUB_ACK, GQNUT } from "../../constant"; 3 | import LocalStore from "../store/localstore"; 4 | 5 | export default class UploadTokenHandler extends AbstractMessageHandler{ 6 | match(proto){ 7 | return proto.signal == PUB_ACK && proto.subSignal == GQNUT; 8 | } 9 | 10 | processMessage(proto){ 11 | if(proto.content){ 12 | var uploadTokenResponse = JSON.parse(proto.content); 13 | var domain = uploadTokenResponse.domain; 14 | var token = uploadTokenResponse.token; 15 | console.log("domain "+domain+" token "+token); 16 | LocalStore.setUploadToken(domain,token); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/webrtc/message/callByeMessageContent.js: -------------------------------------------------------------------------------- 1 | 2 | import MessageContent from '../../websocket/message/messageContent'; 3 | import MessageContentType from '../../websocket/message/messageContentType'; 4 | export default class CallByeMessageContent extends MessageContent { 5 | callId; 6 | 7 | constructor(mentionedType = 0, mentionedTargets = []) { 8 | super(MessageContentType.VOIP_CONTENT_TYPE_END, mentionedType, mentionedTargets); 9 | } 10 | 11 | digest() { 12 | return ''; 13 | } 14 | 15 | encode() { 16 | let payload = super.encode(); 17 | payload.content = this.callId; 18 | return payload; 19 | }; 20 | 21 | decode(payload) { 22 | super.decode(payload); 23 | this.callId = payload.content; 24 | } 25 | } -------------------------------------------------------------------------------- /src/websocket/model/groupInfo.js: -------------------------------------------------------------------------------- 1 | import GroupType from './groupType' 2 | export default class GroupInfo { 3 | target = ''; 4 | name = ''; 5 | portrait = ''; 6 | owner = ''; 7 | type = GroupType.Normal; 8 | memberCount = 0; 9 | extra = ''; 10 | updateDt = 0; 11 | 12 | static convert2GroupInfo(jsonObj){ 13 | var groupInfo = new GroupInfo(); 14 | groupInfo.target = jsonObj.target; 15 | groupInfo.name = jsonObj.name; 16 | groupInfo.portrait = jsonObj.portrait; 17 | groupInfo.owner = jsonObj.owner; 18 | groupInfo.type = jsonObj.type; 19 | groupInfo.memberCount = jsonObj.memberCount; 20 | groupInfo.extra = jsonObj.extra; 21 | groupInfo.updateDt = jsonObj.updateDt; 22 | return groupInfo; 23 | } 24 | } -------------------------------------------------------------------------------- /src/websocket/handler/getGroupInfoHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { GPGI, PUB_ACK } from "../../constant"; 3 | import GroupInfo from "../model/groupInfo"; 4 | 5 | export default class GetGroupInfoHandler extends AbstractMessageHandler{ 6 | 7 | match(proto){ 8 | return proto.signal == PUB_ACK && proto.subSignal == GPGI; 9 | } 10 | 11 | processMessage(proto){ 12 | if(proto.content != null){ 13 | var groupInfoList = JSON.parse(proto.content); 14 | var groups = []; 15 | for(var groupInfo of groupInfoList){ 16 | groups.push(GroupInfo.convert2GroupInfo(groupInfo)); 17 | } 18 | this.vueWebsocket.sendAction("updateGroupInfos",groups); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/websocket/handler/getfriendresultHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { PUB_ACK, FP } from "../../constant"; 3 | import LocalStore from "../store/localstore"; 4 | 5 | export default class GetFriendResultHandler extends AbstractMessageHandler{ 6 | match(proto){ 7 | return proto.signal == PUB_ACK && proto.subSignal == FP; 8 | } 9 | 10 | processMessage(proto){ 11 | var friendList = JSON.parse(proto.content); 12 | var userIds = []; 13 | for(var i in friendList){ 14 | userIds[i] = friendList[i].friendUid; 15 | } 16 | this.vueWebsocket.sendAction("updateFriendIds",friendList); 17 | //获取当前登录用户信息 18 | userIds.push(LocalStore.getUserId()); 19 | this.vueWebsocket.getUserInfos(userIds); 20 | } 21 | } -------------------------------------------------------------------------------- /src/websocket/model/conversation.js: -------------------------------------------------------------------------------- 1 | import ConversationType from "./conversationType"; 2 | 3 | /** 4 | * 5 | "conversation":{ 6 | "conversationType": 0, 7 | "target": "UZUWUWuu", 8 | "line": 0, 9 | } 10 | */ 11 | export default class Conversation { 12 | type = ConversationType.Single; 13 | conversationType = this.type; // 这行是为了做一个兼容处理 14 | target = ''; 15 | line = 0; 16 | 17 | constructor(type, target, line) { 18 | this.type = type; 19 | this.conversationType = type; 20 | this.target = target; 21 | this.line = line; 22 | } 23 | 24 | equal(conversation) { 25 | return this.type === conversation.type 26 | && this.target === conversation.target 27 | && this.line === conversation.line; 28 | } 29 | } -------------------------------------------------------------------------------- /src/websocket/handler/notifyMessageHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { PUBLISH, MN } from "../../constant"; 3 | import LocalStore from "../store/localstore"; 4 | 5 | export default class NotifyMessageHandler extends AbstractMessageHandler{ 6 | match(proto){ 7 | return proto.signal == PUBLISH && proto.subSignal == MN; 8 | } 9 | 10 | processMessage(proto){ 11 | console.log("notifymessage "+proto.content); 12 | let notify = JSON.parse(proto.content); 13 | console.log("notify messageHead "+notify.messageHead+" notify type "+notify.type); 14 | //更新messageHead 15 | LocalStore.resetSendMessageCount(); 16 | LocalStore.setLastMessageSeq(notify.messageHead); 17 | this.vueWebsocket.pullMessage(notify.messageHead,notify.type,1); 18 | } 19 | } -------------------------------------------------------------------------------- /src/websocket/message/textMessageContent.js: -------------------------------------------------------------------------------- 1 | import MessageContent from './messageContent' 2 | import MessagePayload from './messagePayload'; 3 | import MessageContentType from './messageContentType'; 4 | 5 | export default class TextMessageContent extends MessageContent { 6 | content; 7 | 8 | constructor(content, mentionedType = 0, mentionedTargets = []) { 9 | super(MessageContentType.Text, mentionedType, mentionedTargets); 10 | this.content = content; 11 | } 12 | 13 | digest() { 14 | return this.content; 15 | } 16 | 17 | encode() { 18 | let payload = super.encode(); 19 | payload.searchableContent = this.content; 20 | return payload; 21 | }; 22 | 23 | decode(payload) { 24 | super.decode(payload); 25 | this.content = payload.searchableContent; 26 | } 27 | 28 | 29 | } -------------------------------------------------------------------------------- /src/websocket/message/messagePayload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | "content": { 4 | "type": 1, 5 | "searchableContent": "1234", 6 | "pushContent": "", 7 | "content": "", 8 | "binaryContent": "", 9 | "localContent": "", 10 | "mediaType": 0, 11 | "remoteMediaUrl": "", 12 | "localMediaPath": "", 13 | "mentionedType": 0, 14 | "mentionedTargets": [ ] 15 | }, 16 | */ 17 | export default class MessagePayload { 18 | type; 19 | searchableContent; 20 | pushContent; 21 | content; 22 | binaryContent; // base64 string, 图片时,不包含头部信息:data:image/png;base64, 23 | localContent; 24 | mediaType; 25 | remoteMediaUrl; 26 | localMediaPath; 27 | mentionedType = 0; 28 | mentionedTargets = []; 29 | extra; 30 | } -------------------------------------------------------------------------------- /src/websocket/handler/notifyFriendRequestHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { PUBLISH, FRN } from "../../constant"; 3 | import Logger from "../utils/logger"; 4 | import LocalStore from "../store/localstore"; 5 | 6 | export default class NotifyFriendRequestHandler extends AbstractMessageHandler { 7 | match(proto){ 8 | return proto.signal == PUBLISH && proto.subSignal == FRN; 9 | } 10 | 11 | 12 | processMessage(proto){ 13 | if(proto.content != null){ 14 | var friendRequestNotification = JSON.parse(proto.content); 15 | Logger.log("friend request version "+friendRequestNotification.version); 16 | LocalStore.setFriendRequestVersion(friendRequestNotification.version); 17 | this.vueWebsocket.getFriendRequest(friendRequestNotification.version); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | const router = new Router({ 7 | // 共三个页面: 聊天页面,好友页面,个人简历分别对应一下路由 8 | routes: [ 9 | { 10 | path: '/login', 11 | component: require('@/page/login/login.vue') 12 | }, 13 | { 14 | path: '/', 15 | component: require('@/page/main.vue'), 16 | redirect: 'conversation', 17 | children: [{ 18 | path: 'conversation', 19 | name: 'conversation', 20 | component: require('@/page/chat/chat.vue'), 21 | hidden:true, 22 | }, 23 | { 24 | path: 'friend', 25 | name: 'friend', 26 | component: require('@/page/friend/friend.vue'), 27 | hidden:true, 28 | }], 29 | }, 30 | ], 31 | linkActiveClass: 'active' 32 | }) 33 | // router.push({ path: '/chat' }); 34 | export default router -------------------------------------------------------------------------------- /src/webrtc/callSession.js: -------------------------------------------------------------------------------- 1 | import CallState from "./callState"; 2 | export default class CallSession{ 3 | callId; 4 | clientId; 5 | audioOnly; 6 | startTime; 7 | sessionCallback; 8 | callState; 9 | voipClient; 10 | 11 | constructor(voipClient){ 12 | this.startTime = new Date().getTime(); 13 | this.voipClient = voipClient; 14 | } 15 | 16 | setState(state){ 17 | this.callState = state; 18 | console.log("set current call state "+this.callState); 19 | if(this.sessionCallback){ 20 | this.sessionCallback.didChangeState(this.callState); 21 | } 22 | } 23 | 24 | endCall(endCallReason){ 25 | this.setState(CallState.STATUS_IDLE); 26 | this.voipClient.closeCall(); 27 | this.voipClient.currentSession = null; 28 | if(this.sessionCallback){ 29 | this.sessionCallback.didCallEndWithReason(endCallReason); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/page/friend/friend.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 25 | 26 | -------------------------------------------------------------------------------- /src/websocket/message/videoMessageContent.js: -------------------------------------------------------------------------------- 1 | import MediaMessageContent from './mediaMessageContent' 2 | import MessageContentMediaType from './messageContentMediaType'; 3 | import MessageContentType from './messageContentType'; 4 | 5 | export default class VideoMessageContent extends MediaMessageContent { 6 | // base64 encoded 7 | thumbnail; 8 | constructor(fileOrLocalPath, remotePath, thumbnail) { 9 | super(MessageContentType.Video, MessageContentMediaType.Video, fileOrLocalPath, remotePath); 10 | this.thumbnail = thumbnail; 11 | } 12 | 13 | digest() { 14 | return '[视频]'; 15 | } 16 | 17 | encode() { 18 | let payload = super.encode(); 19 | payload.binaryContent = this.thumbnail; 20 | payload.mediaType = MessageContentMediaType.Video; 21 | return payload; 22 | }; 23 | 24 | decode(payload) { 25 | super.decode(payload); 26 | this.thumbnail = payload.binaryContent; 27 | } 28 | } -------------------------------------------------------------------------------- /src/websocket/message/imageMessageContent.js: -------------------------------------------------------------------------------- 1 | import MediaMessageContent from './mediaMessageContent'; 2 | import MessageContentMediaType from './messageContentMediaType'; 3 | import MessageContentType from './messageContentType'; 4 | 5 | export default class ImageMessageContent extends MediaMessageContent { 6 | // base64 encoded, 不包含头部:data:image/png;base64, 7 | thumbnail; 8 | 9 | constructor(fileOrLocalPath, remotePath, thumbnail) { 10 | super(MessageContentType.Image, MessageContentMediaType.Image, fileOrLocalPath, remotePath); 11 | this.thumbnail = thumbnail; 12 | } 13 | 14 | digest() { 15 | return '[图片]'; 16 | } 17 | 18 | encode() { 19 | let payload = super.encode(); 20 | payload.mediaType = MessageContentMediaType.Image; 21 | payload.binaryContent = this.thumbnail; 22 | return payload; 23 | }; 24 | 25 | decode(payload) { 26 | super.decode(payload); 27 | this.thumbnail = payload.binaryContent; 28 | } 29 | } -------------------------------------------------------------------------------- /src/webrtc/message/callSignalMessageContent.js: -------------------------------------------------------------------------------- 1 | import MessageContent from '../../websocket/message/messageContent'; 2 | import MessageContentType from '../../websocket/message/messageContentType'; 3 | import StringUtils from "../../websocket/utils/StringUtil" 4 | export default class CallSignalMessageContent extends MessageContent { 5 | callId; 6 | payload; 7 | 8 | constructor(mentionedType = 0, mentionedTargets = []) { 9 | super(MessageContentType.VOIP_CONTENT_TYPE_SIGNAL, mentionedType, mentionedTargets); 10 | } 11 | 12 | digest() { 13 | return ''; 14 | } 15 | 16 | encode() { 17 | let payload = super.encode(); 18 | payload.content = this.callId; 19 | payload.binaryContent = StringUtils.utf8_to_b64(this.payload); 20 | return payload; 21 | }; 22 | 23 | decode(payload) { 24 | super.decode(payload); 25 | this.callId = payload.content; 26 | this.payload = StringUtils.b64_to_utf8(payload.binaryContent); 27 | } 28 | } -------------------------------------------------------------------------------- /src/websocket/model/userInfo.js: -------------------------------------------------------------------------------- 1 | export default class UserInfo { 2 | uid = ''; 3 | name = ''; 4 | displayName = ''; 5 | gender = 0; 6 | portrait = ''; 7 | mobile = ''; 8 | email = ''; 9 | address = ''; 10 | social = ''; 11 | extra = ''; 12 | type = 0; //0 normal; 1 robot; 2 thing; 13 | updateDt = 1550652404513; 14 | 15 | static convert2UserInfo(jsonObj){ 16 | let userInfo = new UserInfo(); 17 | userInfo.uid = jsonObj.uid; 18 | userInfo.name = jsonObj.name; 19 | userInfo.displayName = jsonObj.displayName; 20 | userInfo.gender = jsonObj.gender; 21 | userInfo.portrait = jsonObj.portrait; 22 | userInfo.mobile = jsonObj.mobile; 23 | userInfo.email = jsonObj.email; 24 | userInfo.address = jsonObj.address; 25 | userInfo.social = jsonObj.social; 26 | userInfo.extra = jsonObj.extra; 27 | userInfo.type = jsonObj.type; 28 | userInfo.updateDt = jsonObj.updateDt; 29 | return userInfo; 30 | } 31 | } -------------------------------------------------------------------------------- /src/websocket/handler/friendRequestHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { PUB_ACK, FRP } from "../../constant"; 3 | import LocalStore from "../store/localstore"; 4 | 5 | export default class FriendRequestHandler extends AbstractMessageHandler { 6 | match(proto){ 7 | return proto.signal == PUB_ACK && proto.subSignal == FRP; 8 | } 9 | 10 | 11 | processMessage(proto){ 12 | if(proto.content != null && proto.content != ''){ 13 | var friendRequests = JSON.parse(proto.content); 14 | var validRequest = []; 15 | var targeIds = []; 16 | for(var friendRequest of friendRequests){ 17 | if(friendRequest.from != LocalStore.getUserId()){ 18 | validRequest.push(friendRequest); 19 | targeIds.push(friendRequest.from); 20 | } 21 | } 22 | this.vueWebsocket.getUserInfos(targeIds); 23 | this.vueWebsocket.sendAction("updateFriendRequest",validRequest); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/websocket/message/websocketprotomessage.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * websocket json 主协议 4 | * 5 | * { 6 | "signal": "connect", 7 | "sub_signal": "conect_ack", 8 | "message_id": 0, 9 | "content": "" 10 | } 11 | */ 12 | export class WebSocketProtoMessage { 13 | signal; 14 | subSignal; 15 | 16 | constructor(){ 17 | console.log('constuctor WebSocketProtoMessage'); 18 | } 19 | 20 | setMessageId(messageId){ 21 | this.messageId = messageId; 22 | } 23 | 24 | setSignal(signal){ 25 | this.signal = signal; 26 | } 27 | 28 | setSubSignal(subSignal){ 29 | this.subSignal = subSignal; 30 | } 31 | 32 | setContent(content){ 33 | this.content = content; 34 | } 35 | 36 | toJson(){ 37 | let message = { 38 | signal : this.signal, 39 | subSignal : this.subSignal == null ? 'NONE' : this.subSignal, 40 | messageId : this.messageId == null ? 0 : this.messageId, 41 | content : this.content 42 | } 43 | 44 | return JSON.stringify(message); 45 | } 46 | } -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Upload File to Linux Server 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: windows-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v2 15 | 16 | - name: Install Git 17 | run: | 18 | choco install openssh -y 19 | 20 | - name: Upload file via SCP 21 | env: 22 | SERVER_HOST: ${{ secrets.SERVER_HOST }} 23 | run: | 24 | echo "GITHUB_REF: ${{vars.GITHUB_REF}}" 25 | # ssh 26 | # cat package.json 27 | # echo "SERVER_HOST is: $env:SERVER_HOST " 28 | # echo "V_SERVER_HOST: ${{ vars.V_SERVER_HOST }}" 29 | echo "${{ secrets.SSH_PRIVATE_KEY }}" > private_key.ppk 30 | cat private_key.ppk 31 | # ssh -i private_key.ppk root@fsharechat.cn 32 | # echo "login in fsharechat" 33 | # echo "SSHPASS=${{ secrets.SSH_PASSWORD }}" >> $GITHUB_ENV 34 | # scp -o StrictHostKeyChecking=no -P 22 build root@fsharechat.cn:/data/temp-release/windows 35 | -------------------------------------------------------------------------------- /src/webrtc/message/callModifyMessageContent.js: -------------------------------------------------------------------------------- 1 | import MessageContent from '../../websocket/message/messageContent'; 2 | import MessageContentType from '../../websocket/message/messageContentType'; 3 | import StringUtils from "../../websocket/utils/StringUtil" 4 | export default class CallModifyMessageContent extends MessageContent { 5 | callId; 6 | audioOnly; 7 | 8 | constructor(mentionedType = 0, mentionedTargets = []) { 9 | super(MessageContentType.VOIP_CONTENT_TYPE_MODIFY, mentionedType, mentionedTargets); 10 | } 11 | 12 | digest() { 13 | return ''; 14 | } 15 | 16 | encode() { 17 | let payload = super.encode(); 18 | payload.content = this.callId; 19 | 20 | var obj; 21 | if (this.audioOnly) { 22 | obj = '1'; 23 | } else { 24 | obj = '0'; 25 | } 26 | payload.binaryContent = StringUtils.utf8_to_b64(obj); 27 | return payload; 28 | }; 29 | 30 | decode(payload) { 31 | super.decode(payload); 32 | this.callId = payload.content; 33 | let str = StringUtils.b64_to_utf8(payload.binaryContent); 34 | 35 | this.audioOnly = (str === '1'); 36 | } 37 | } -------------------------------------------------------------------------------- /src/webrtc/message/callAnswerMessageContent.js: -------------------------------------------------------------------------------- 1 | import MessageContent from '../../websocket/message/messageContent'; 2 | import MessageContentType from '../../websocket/message/messageContentType'; 3 | import StringUtils from "../../websocket/utils/StringUtil" 4 | 5 | export default class CallAnswerMessageContent extends MessageContent { 6 | callId; 7 | audioOnly; 8 | 9 | constructor(mentionedType = 0, mentionedTargets = []) { 10 | super(MessageContentType.VOIP_CONTENT_TYPE_ACCEPT, mentionedType, mentionedTargets); 11 | } 12 | 13 | digest() { 14 | return ''; 15 | } 16 | 17 | encode() { 18 | let payload = super.encode(); 19 | payload.content = this.callId; 20 | 21 | var obj; 22 | if (this.audioOnly) { 23 | obj = '1'; 24 | } else { 25 | obj = '0'; 26 | } 27 | payload.binaryContent = StringUtils.utf8_to_b64(obj); 28 | return payload; 29 | }; 30 | 31 | decode(payload) { 32 | super.decode(payload); 33 | this.callId = payload.content; 34 | let str = StringUtils.b64_to_utf8(payload.binaryContent); 35 | 36 | this.audioOnly = (str === '1'); 37 | } 38 | } -------------------------------------------------------------------------------- /src/webrtc/message/callAnswerTMessageContent.js: -------------------------------------------------------------------------------- 1 | 2 | import MessageContent from '../../websocket/message/messageContent'; 3 | import MessageContentType from '../../websocket/message/messageContentType'; 4 | import StringUtils from "../../websocket/utils/StringUtil" 5 | 6 | export default class CallAnswerTMessageContent extends MessageContent { 7 | callId; 8 | audioOnly; 9 | 10 | constructor(mentionedType = 0, mentionedTargets = []) { 11 | super(MessageContentType.VOIP_CONTENT_TYPE_ACCEPT_T, mentionedType, mentionedTargets); 12 | } 13 | 14 | digest() { 15 | return ''; 16 | } 17 | 18 | encode() { 19 | let payload = super.encode(); 20 | payload.content = this.callId; 21 | 22 | var obj; 23 | if (this.audioOnly) { 24 | obj = '1'; 25 | } else { 26 | obj = '0'; 27 | } 28 | payload.binaryContent = StringUtils.utf8_to_b64(obj); 29 | return payload; 30 | }; 31 | 32 | decode(payload) { 33 | super.decode(payload); 34 | this.callId = payload.content; 35 | let str = StringUtils.b64_to_utf8(payload.binaryContent); 36 | 37 | this.audioOnly = (str === '1'); 38 | } 39 | } -------------------------------------------------------------------------------- /src/websocket/message/messageContent.js: -------------------------------------------------------------------------------- 1 | import MessagePayload from "./messagePayload"; 2 | 3 | /** 4 | * 消息类型基类,一般包括文本消息,文件类消息,媒体类消息 5 | */ 6 | export default class MessageContent { 7 | type; 8 | //0 普通消息, 1 部分提醒, 2 提醒全部 9 | mentionedType = 0; 10 | //提醒对象,mentionedType 1时有效 11 | mentionedTargets = []; 12 | extra; 13 | 14 | constructor(type, mentionedType = 0, mentionedTargets = []) { 15 | this.type = type; 16 | this.mentionedType = mentionedType; 17 | this.mentionedTargets = mentionedTargets; 18 | } 19 | 20 | digest() { 21 | return '...digest...'; 22 | } 23 | 24 | /** 25 | * return MessagePayload in json format 26 | */ 27 | encode() { 28 | let payload = new MessagePayload(); 29 | payload.type = this.type; 30 | payload.mentionedType = this.mentionedType; 31 | payload.mentionedTargets = this.mentionedTargets; 32 | return payload; 33 | } 34 | 35 | /** 36 | * 37 | * @param {object} payload object json.parse from message#content 38 | */ 39 | decode(payload) { 40 | this.type = payload.type; 41 | this.mentionedType = payload.mentionedType; 42 | this.mentionedTargets = payload.mentionedTargets; 43 | } 44 | } -------------------------------------------------------------------------------- /src/websocket/message/notification/quitGroupNotification.js: -------------------------------------------------------------------------------- 1 | import MessageContentType from '../messageContentType'; 2 | 3 | import GroupNotificationContent from './groupNotification'; 4 | import StringUtils from '../../utils/StringUtil' 5 | import webSocketCli from '../../websocketcli' 6 | 7 | export default class QuitGroupNotification extends GroupNotificationContent { 8 | operator = ''; 9 | 10 | constructor(operator) { 11 | super(MessageContentType.QuitGroup_Notification); 12 | this.operator = operator; 13 | } 14 | 15 | formatNotification() { 16 | if (this.fromSelf) { 17 | return '您退出了群组'; 18 | } else { 19 | return webSocketCli.getDisplayName(this.operator) + '退出了群组'; 20 | } 21 | } 22 | 23 | encode() { 24 | let payload = super.encode(); 25 | let obj = { 26 | g: this.groupId, 27 | o: this.operator, 28 | }; 29 | payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj)); 30 | return payload; 31 | } 32 | 33 | decode(payload) { 34 | super.decode(payload); 35 | let json = StringUtils.b64_to_utf8(payload.binaryContent) 36 | let obj = JSON.parse(json); 37 | this.groupId = obj.g; 38 | this.operator = obj.o; 39 | } 40 | } -------------------------------------------------------------------------------- /src/websocket/message/notification/recallMessageNotification.js: -------------------------------------------------------------------------------- 1 | import NotificationMessageContent from './notificationMessageContent' 2 | import MessageContentType from '../messageContentType'; 3 | import StringUtils from '../../utils/StringUtil' 4 | import webSocketCli from '../../websocketcli' 5 | 6 | export default class RecallMessageNotification extends NotificationMessageContent { 7 | operatorId = ''; 8 | messageUid = ''; 9 | 10 | constructor(operatorId, messageUid) { 11 | super(MessageContentType.RecallMessage_Notification); 12 | this.operatorId = operatorId; 13 | this.messageUid = messageUid; 14 | } 15 | 16 | formatNotification() { 17 | if(this.fromSelf){ 18 | return "您撤回了一条消息"; 19 | }else { 20 | return webSocketCli.getDisplayName(this.operatorId) + "撤回了一条消息"; 21 | } 22 | } 23 | 24 | encode() { 25 | let payload = super.encode(); 26 | payload.content = this.operatorId; 27 | payload.binaryContent = StringUtils.utf8_to_b64(this.messageUid.toString()); 28 | return payload; 29 | }; 30 | 31 | decode(payload) { 32 | super.decode(payload); 33 | this.operatorId = payload.content; 34 | this.messageUid = StringUtils.b64_to_utf8(payload.binaryContent); 35 | } 36 | } -------------------------------------------------------------------------------- /src/websocket/message/notification/dismissGroupNotification.js: -------------------------------------------------------------------------------- 1 | import MessageContentType from '../messageContentType'; 2 | 3 | import GroupNotificationContent from './groupNotification'; 4 | import StringUtils from '../../utils/StringUtil' 5 | import webSocketCli from '../../websocketcli' 6 | 7 | export default class DismissGroupNotification extends GroupNotificationContent { 8 | operator = ''; 9 | 10 | constructor(operator) { 11 | super(MessageContentType.DismissGroup_Notification); 12 | this.operator = operator; 13 | } 14 | 15 | formatNotification() { 16 | if (this.fromSelf) { 17 | return '您解散了群组'; 18 | } else { 19 | return webSocketCli.getDisplayName(this.operator) + '解散了群组'; 20 | } 21 | } 22 | 23 | encode() { 24 | let payload = super.encode(); 25 | let obj = { 26 | g: this.groupId, 27 | o: this.operator, 28 | }; 29 | payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj)); 30 | return payload; 31 | } 32 | 33 | decode(payload) { 34 | super.decode(payload); 35 | let json = StringUtils.b64_to_utf8(payload.binaryContent) 36 | let obj = JSON.parse(json); 37 | this.groupId = obj.g; 38 | this.operator = obj.o; 39 | } 40 | } -------------------------------------------------------------------------------- /src/page/chat/chat.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 34 | 35 | 49 | -------------------------------------------------------------------------------- /src/websocket/handler/abstractmessagehandler.js: -------------------------------------------------------------------------------- 1 | import MessageHandler from "./messageHandler"; 2 | import Logger from "../utils/logger"; 3 | import FutureResult from '../future/futureResult'; 4 | import { SUCCESS_CODE, ERROR_CODE } from "../../constant"; 5 | /** 6 | * 关于promise返回说明 7 | * 1.对于操作型消息,一般只需要返回成功与失败即可 8 | * 2.对于结果型消息,一般判断是否为空 9 | */ 10 | export default class AbstractMessageHandler extends MessageHandler{ 11 | 12 | constructor(vueWebsocket){ 13 | super(); 14 | this.vueWebsocket = vueWebsocket; 15 | } 16 | 17 | get vueWebsocketClient(){ 18 | return this.vueWebsocket; 19 | } 20 | 21 | processMessage(proto){ 22 | Logger.log("AbstractMessageHandler messageId "+proto.messageId +" proto content "+proto.content); 23 | var promiseReslove = this.vueWebsocket.resolvePromiseMap.get(proto.messageId); 24 | if(promiseReslove){ 25 | clearTimeout(promiseReslove.timeoutId); 26 | if(proto.content == ''){ 27 | promiseReslove.resolve(new FutureResult(ERROR_CODE,proto.content)); 28 | } else { 29 | promiseReslove.resolve(new FutureResult(SUCCESS_CODE,this.notifyContent(proto.content))); 30 | } 31 | this.vueWebsocket.resolvePromiseMap.delete(proto.messageId); 32 | } 33 | } 34 | 35 | notifyContent(content){ 36 | return content 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /src/websocket/handler/connectackhandler.js: -------------------------------------------------------------------------------- 1 | import { CONNECT_ACK } from "../../constant"; 2 | import AbstractMessageHandler from "./abstractmessagehandler"; 3 | import LocalStore from "../store/localstore"; 4 | 5 | export default class ConnectAckHandler extends AbstractMessageHandler{ 6 | constructor(vueWebsocket){ 7 | super(vueWebsocket); 8 | } 9 | match(protoObj){ 10 | return protoObj.signal == CONNECT_ACK; 11 | } 12 | 13 | processMessage(data){ 14 | console.log("ConnectAckHandler process message"); 15 | var connectAcceptedMessage = JSON.parse(data.content); 16 | console.log("connectAcceptedMessage friendHead "+connectAcceptedMessage.friendHead+" messageHead "+connectAcceptedMessage.messageHead); 17 | //拉取朋友列表 18 | this.vueWebsocket.getFriend(); 19 | let lastMessageSeq = LocalStore.getLastMessageSeq(); 20 | if(!lastMessageSeq){ 21 | lastMessageSeq = '0'; 22 | this.vueWebsocket.sendAction('changetFirstLogin',true); 23 | } else { 24 | this.vueWebsocket.sendAction('changetFirstLogin',false); 25 | } 26 | //好友请求信息 27 | this.vueWebsocket.getFriendRequest(LocalStore.getFriendRequestVersion()); 28 | //初始拉取消息列表 29 | this.vueWebsocket.pullMessage(lastMessageSeq,0,0,LocalStore.getSendMessageCount()); 30 | LocalStore.setLastMessageSeq(connectAcceptedMessage.messageHead); 31 | } 32 | } -------------------------------------------------------------------------------- /src/websocket/message/notification/changeGroupNameNotification.js: -------------------------------------------------------------------------------- 1 | import MessageContentType from "../messageContentType"; 2 | 3 | import GroupNotificationContent from "./groupNotification"; 4 | import StringUtils from '../../utils/StringUtil' 5 | import webSocketCli from '../../websocketcli' 6 | 7 | export default class ChangeGroupNameNotification extends GroupNotificationContent { 8 | operator = ''; 9 | name = ''; 10 | 11 | constructor(operator, name) { 12 | super(MessageContentType.ChangeGroupName_Notification); 13 | this.operator = operator; 14 | this.name = name; 15 | } 16 | 17 | formatNotification() { 18 | if (this.fromSelf) { 19 | return '您修改群名称为:' + this.name; 20 | } else { 21 | return webSocketCli.getDisplayName(this.operator)+' 修改群名称为:' + this.name; 22 | } 23 | } 24 | 25 | encode() { 26 | let payload = super.encode(); 27 | let obj = { 28 | g: this.groupId, 29 | n: this.name, 30 | o: this.operator, 31 | }; 32 | payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj)); 33 | return payload; 34 | } 35 | 36 | decode(payload) { 37 | super.decode(payload); 38 | let json = StringUtils.b64_to_utf8(payload.binaryContent) 39 | let obj = JSON.parse(json); 40 | this.groupId = obj.g; 41 | this.operator = obj.o; 42 | this.name = obj.n; 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/websocket/message/notification/createGroupNotification.js: -------------------------------------------------------------------------------- 1 | import MessageContentType from '../messageContentType'; 2 | 3 | import GroupNotificationContent from './groupNotification'; 4 | import StringUtils from '../../utils/StringUtil' 5 | import webSocketCli from '../../websocketcli' 6 | 7 | export default class CreateGroupNotification extends GroupNotificationContent { 8 | creator = ''; 9 | groupName = ''; 10 | 11 | constructor(creator, groupName) { 12 | super(MessageContentType.CreateGroup_Notification); 13 | this.creator = creator; 14 | this.groupName = groupName; 15 | } 16 | 17 | formatNotification() { 18 | if (this.fromSelf) { 19 | return '您创建了群组 ' + this.groupName; 20 | } else { 21 | return webSocketCli.getDisplayName(this.creator)+' 创建了群组 ' + this.groupName; 22 | } 23 | } 24 | 25 | encode() { 26 | let payload = super.encode(); 27 | let obj = { 28 | g: this.groupId, 29 | n: this.groupName, 30 | o: this.creator, 31 | }; 32 | payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj)); 33 | return payload; 34 | } 35 | 36 | decode(payload) { 37 | super.decode(payload); 38 | let json = StringUtils.b64_to_utf8(payload.binaryContent) 39 | let obj = JSON.parse(json); 40 | this.groupId = obj.g; 41 | this.creator = obj.o; 42 | this.groupName = obj.n; 43 | } 44 | } -------------------------------------------------------------------------------- /src/websocket/message/mediaMessageContent.js: -------------------------------------------------------------------------------- 1 | import MessageContent from './messageContent' 2 | export default class MediaMessageContent extends MessageContent { 3 | file; 4 | remotePath = ''; 5 | localPath = ''; 6 | mediaType = 0; 7 | 8 | constructor(messageType, mediaType = 0, fileOrLocalPath, remotePath) { 9 | super(messageType); 10 | this.mediaType = mediaType; 11 | if(typeof fileOrLocalPath === "string"){ 12 | this.localPath = fileOrLocalPath; 13 | this.remotePath = remotePath; 14 | }else { 15 | this.file = fileOrLocalPath; 16 | if (fileOrLocalPath && fileOrLocalPath.path !== undefined) { 17 | this.localPath = fileOrLocalPath.path; 18 | // attention: 粘贴的时候,path是空字符串,故采用了这个trick 19 | if (this.localPath.indexOf(fileOrLocalPath.name) < 0) { 20 | this.localPath += fileOrLocalPath.name; 21 | } 22 | } 23 | } 24 | } 25 | 26 | encode() { 27 | let payload = super.encode(); 28 | payload.localMediaPath = this.localPath; 29 | payload.remoteMediaUrl = this.remotePath; 30 | payload.mediaType = this.mediaType; 31 | payload.searchableContent = this.digest(); 32 | return payload; 33 | }; 34 | 35 | decode(payload) { 36 | super.decode(payload); 37 | this.localPath = payload.localMediaPath; 38 | this.remotePath = payload.remoteMediaUrl; 39 | this.mediaType = payload.mediaType; 40 | } 41 | } -------------------------------------------------------------------------------- /src/websocket/message/messageContentType.js: -------------------------------------------------------------------------------- 1 | export default class MessageContentType { 2 | // 基本消息类型 3 | static Unknown = 0; 4 | static Text = 1; 5 | static Voice = 2; 6 | static Image = 3; 7 | static Location = 4; 8 | static File = 5; 9 | static Video = 6; 10 | static Sticker = 7; 11 | static ImageText = 8; 12 | static P_Text = 9; 13 | 14 | // 提醒消息 15 | static RecallMessage_Notification = 80; 16 | static Tip_Notification = 90; 17 | static Typing = 91; 18 | 19 | // 群相关消息 20 | static CreateGroup_Notification = 104; 21 | static AddGroupMember_Notification = 105; 22 | static KickOffGroupMember_Notification = 106; 23 | static QuitGroup_Notification = 107; 24 | static DismissGroup_Notification = 108; 25 | static TransferGroupOwner_Notification = 109; 26 | static ChangeGroupName_Notification = 110; 27 | static ModifyGroupAlias_Notification = 111; 28 | static ChangeGroupPortrait_Notification = 112; 29 | 30 | static MuteGroupMember_Notification = 113; 31 | static ChangeJoinType_Notification = 114; 32 | static ChangePrivateChat_Notification = 115; 33 | static ChangeSearchable_Notificaiton = 116; 34 | static SetGroupManager_Notification = 117; 35 | static VOIP_CONTENT_TYPE_START = 400; 36 | static VOIP_CONTENT_TYPE_END = 402; 37 | static VOIP_CONTENT_TYPE_ACCEPT = 401; 38 | static VOIP_CONTENT_TYPE_SIGNAL = 403; 39 | static VOIP_CONTENT_TYPE_MODIFY = 404; 40 | static VOIP_CONTENT_TYPE_ACCEPT_T = 405; 41 | } -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 29 | 30 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../dist'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: './', 11 | productionSourceMap: true, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'], 18 | // Run the build command with an extra argument to 19 | // View the bundle analyzer report after build finishes: 20 | // `npm run build --report` 21 | // Set to `true` or `false` to always turn it on or off 22 | bundleAnalyzerReport: process.env.npm_config_report 23 | }, 24 | dev: { 25 | env: require('./dev.env'), 26 | port: 9080, 27 | autoOpenBrowser: true, 28 | assetsSubDirectory: 'static', 29 | assetsPublicPath: '/', 30 | proxyTable: {}, 31 | // CSS Sourcemaps off by default because relative paths are "buggy" 32 | // with this option, according to the CSS-Loader README 33 | // (https://github.com/webpack/css-loader#sourcemaps) 34 | // In our experience, they generally work as expected, 35 | // just be aware of this issue when enabling this option. 36 | cssSourceMap: false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/webrtc/message/callStartMessageContent.js: -------------------------------------------------------------------------------- 1 | import MessageContent from '../../websocket/message/messageContent'; 2 | import MessageContentType from '../../websocket/message/messageContentType'; 3 | import StringUtils from "../../websocket/utils/StringUtil" 4 | export default class CallStartMessageContent extends MessageContent { 5 | callId; 6 | targetId; 7 | connectTime; 8 | endTime; 9 | status; 10 | audioOnly; 11 | 12 | constructor(callId, targetId, audioOnly){ 13 | super(MessageContentType.VOIP_CONTENT_TYPE_START); 14 | this.callId = callId; 15 | this.targetId = targetId; 16 | this.audioOnly = audioOnly; 17 | } 18 | 19 | digest() { 20 | if (this.audioOnly) { 21 | return '[语音通话]'; 22 | } else { 23 | return '[视频通话]'; 24 | } 25 | } 26 | 27 | encode() { 28 | let payload = super.encode(); 29 | payload.content = this.callId; 30 | 31 | let obj = { 32 | c: this.connectTime, 33 | e: this.endTime, 34 | s: this.status, 35 | a: this.audioOnly ? 1 : 0, 36 | t:this.targetId 37 | }; 38 | payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj)); 39 | return payload; 40 | }; 41 | 42 | decode(payload) { 43 | super.decode(payload); 44 | this.callId = payload.content; 45 | let json = StringUtils.b64_to_utf8(payload.binaryContent); 46 | let obj = JSON.parse(json); 47 | 48 | this.connectTime = obj.c; 49 | this.endTime = obj.e; 50 | this.status = obj.s; 51 | this.audioOnly = (obj.a === 1); 52 | this.targetId = obj.t; 53 | } 54 | } -------------------------------------------------------------------------------- /src/components/menu/addtip.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 28 | 29 | -------------------------------------------------------------------------------- /ABOUT_FEATURE.md: -------------------------------------------------------------------------------- 1 | # 关于界面功能说明 2 | 3 | ## 功能概述 4 | 5 | 本次更新为即时通讯应用增加了关于界面功能,用户可以通过侧边栏底部的关于按钮查看软件版本、作者和编译时间等信息。 6 | 7 | ## 实现内容 8 | 9 | ### 1. 新增组件 10 | 11 | **文件位置**: `src/components/menu/about.vue` 12 | 13 | - 创建了独立的关于对话框组件 14 | - 显示应用名称、版本号、作者、编译时间和描述信息 15 | - 采用模态对话框设计,具有良好的用户体验 16 | - 支持点击背景区域或关闭按钮关闭对话框 17 | 18 | ### 2. 侧边栏增强 19 | 20 | **文件位置**: `src/components/mycard/mycard.vue` 21 | 22 | - 在侧边栏底部添加了关于按钮 23 | - 使用现有图标系统(icon-dkw_xiaoxi) 24 | - 按钮位于退出按钮左侧,布局合理 25 | 26 | ### 3. 构建配置优化 27 | 28 | **修改文件**: 29 | - `config/prod.env.js` - 生产环境配置 30 | - `config/dev.env.js` - 开发环境配置 31 | 32 | 增加了以下环境变量: 33 | - `APP_VERSION`: 从package.json自动读取版本号 34 | - `BUILD_TIME`: 自动生成编译时间(中文格式) 35 | 36 | ### 4. 显示信息 37 | 38 | 关于界面显示以下信息: 39 | - **应用名称**: WeChat Web 40 | - **版本号**: 1.0.14(从package.json读取) 41 | - **作者**: comsince 42 | - **编译时间**: 实时生成的编译时间 43 | - **描述**: 基于unverse-push的开源即时通讯web客户端 44 | 45 | ## 使用方法 46 | 47 | 1. 启动应用后,在左侧边栏底部可以看到关于按钮(消息图标) 48 | 2. 点击关于按钮,弹出关于对话框 49 | 3. 查看完毕后,可以点击"确定"按钮或对话框外部区域关闭 50 | 51 | ## 技术特点 52 | 53 | 1. **响应式设计**: 关于对话框支持移动端适配 54 | 2. **环境变量注入**: 版本和编译时间通过webpack在构建时注入 55 | 3. **组件化开发**: 关于功能作为独立组件,便于维护 56 | 4. **用户体验**: 采用现代化的UI设计,符合应用整体风格 57 | 58 | ## 文件结构 59 | 60 | ``` 61 | src/ 62 | ├── components/ 63 | │ └── menu/ 64 | │ └── about.vue # 新增关于组件 65 | ├── components/ 66 | │ └── mycard/ 67 | │ └── mycard.vue # 修改侧边栏组件 68 | config/ 69 | ├── prod.env.js # 修改生产环境配置 70 | └── dev.env.js # 修改开发环境配置 71 | ``` 72 | 73 | ## 构建和部署 74 | 75 | 构建命令: 76 | ```bash 77 | npm run build 78 | ``` 79 | 80 | 开发模式: 81 | ```bash 82 | npm run dev 83 | ``` 84 | 85 | 构建完成后,关于界面将显示正确的版本信息和编译时间。 86 | 87 | ## 注意事项 88 | 89 | 1. 编译时间在每次构建时自动更新 90 | 2. 版本号与package.json保持同步 91 | 3. 关于按钮使用现有图标库,无需额外资源 92 | 4. 对话框采用固定定位,确保在所有页面状态下正常显示 -------------------------------------------------------------------------------- /src/websocket/message/notification/kickoffGroupMemberNotification.js: -------------------------------------------------------------------------------- 1 | import MessageContentType from "../messageContentType"; 2 | 3 | import GroupNotificationContent from "./groupNotification"; 4 | import StringUtils from '../../utils/StringUtil' 5 | import webSocketCli from '../../websocketcli' 6 | 7 | export default class KickoffGroupMemberNotification extends GroupNotificationContent { 8 | operator = ''; 9 | kickedMembers = []; 10 | 11 | constructor(operator, kickedMembers) { 12 | super(MessageContentType.KickOffGroupMember_Notification); 13 | this.operator = operator; 14 | this.kickedMembers = kickedMembers; 15 | } 16 | 17 | formatNotification() { 18 | let notifyStr; 19 | if (this.fromSelf) { 20 | notifyStr = '您把 '; 21 | } else { 22 | notifyStr = webSocketCli.getDisplayName(this.operator) + '把 '; 23 | } 24 | 25 | let kickedMembersStr = ''; 26 | this.kickedMembers.forEach(m => { 27 | kickedMembersStr += ' ' + webSocketCli.getDisplayName(m); 28 | }); 29 | 30 | return notifyStr + kickedMembersStr + ' 移除了群组'; 31 | } 32 | 33 | encode() { 34 | let payload = super.encode(); 35 | let obj = { 36 | g: this.groupId, 37 | ms: this.kickedMembers, 38 | o: this.operateUser, 39 | }; 40 | payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj)); 41 | return payload; 42 | } 43 | 44 | decode(payload) { 45 | super.decode(payload); 46 | let json = StringUtils.b64_to_utf8(payload.binaryContent) 47 | let obj = JSON.parse(json); 48 | this.groupId = obj.g; 49 | this.operator = obj.o; 50 | this.kickedMembers = obj.ms; 51 | } 52 | } -------------------------------------------------------------------------------- /src/websocket/handler/receiveMessageHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { MP, PUB_ACK } from "../../constant"; 3 | import Message from "../message/message"; 4 | import ProtoMessage from "../message/protomessage"; 5 | import ProtoConversationInfo from '../model/protoConversationInfo' 6 | import LocalStore from "../store/localstore"; 7 | import UnreadCount from "../model/unReadCount"; 8 | import MessageConfig from "../message/messageConfig"; 9 | import PersistFlag from "../message/persistFlag"; 10 | import ChatManager from "../chatManager"; 11 | 12 | export default class ReceiveMessageHandler extends AbstractMessageHandler { 13 | conversations = []; 14 | 15 | match(proto){ 16 | return proto.signal == PUB_ACK && proto.subSignal == MP; 17 | } 18 | 19 | processMessage(proto){ 20 | LocalStore.resetSendMessageCount(); 21 | var content = JSON.parse(proto.content); 22 | console.log("current "+content.current+" head "+content.head+" messageCount "+content.messageCount); 23 | if(content.messageCount == 0){ 24 | this.vueWebsocket.sendAction('changeEmptyMessageState',true); 25 | } else { 26 | this.vueWebsocket.sendAction('changeEmptyMessageState',false); 27 | } 28 | for(var protoMessage of content.messageResponseList){ 29 | var protoMessage = ProtoMessage.toProtoMessage(protoMessage); 30 | if(MessageConfig.isDisplayableMessage(protoMessage)){ 31 | this.addProtoMessage(protoMessage); 32 | } 33 | ChatManager.onReceiveMessage(protoMessage); 34 | } 35 | this.vueWebsocket.sendAction('changetFirstLogin',false); 36 | } 37 | 38 | addProtoMessage(protoMessage){ 39 | this.vueWebsocket.sendAction('addProtoMessage',protoMessage); 40 | } 41 | } -------------------------------------------------------------------------------- /src/websocket/message/notification/addGroupMemberNotification.js: -------------------------------------------------------------------------------- 1 | import MessageContentType from '../messageContentType'; 2 | 3 | import GroupNotificationContent from './groupNotification'; 4 | import StringUtils from '../../utils/StringUtil' 5 | import webSocketCli from '../../websocketcli' 6 | 7 | export default class AddGroupMemberNotification extends GroupNotificationContent { 8 | invitor = ''; 9 | invitees = []; 10 | 11 | constructor(invitor, invitees) { 12 | super(MessageContentType.AddGroupMember_Notification); 13 | this.invitor = invitor; 14 | this.invitees = invitees; 15 | } 16 | 17 | formatNotification() { 18 | let notifyStr; 19 | if (this.invitees.length === 1 && this.invitees[0] === this.invitor) { 20 | if (this.fromSelf) { 21 | return '您加入了群组'; 22 | } else { 23 | return webSocketCli.getDisplayName(this.invitor) + ' 加入了群组'; 24 | } 25 | } 26 | 27 | if (this.fromSelf) { 28 | notifyStr = '您邀请:'; 29 | } else { 30 | notifyStr = webSocketCli.getDisplayName(this.invitor) + '邀请:'; 31 | } 32 | 33 | let membersStr = ''; 34 | this.invitees.forEach(m => { 35 | membersStr += ' ' + webSocketCli.getDisplayName(m); 36 | }); 37 | 38 | return notifyStr + membersStr + '加入了群组'; 39 | } 40 | 41 | encode() { 42 | let payload = super.encode(); 43 | let obj = { 44 | g: this.groupId, 45 | o: this.invitor, 46 | ms: this.invitees, 47 | }; 48 | payload.binaryContent = StringUtils.utf8_to_b64(JSON.stringify(obj)); 49 | return payload; 50 | }; 51 | 52 | decode(payload) { 53 | super.decode(payload); 54 | let json = StringUtils.b64_to_utf8(payload.binaryContent); 55 | let obj = JSON.parse(json); 56 | this.groupId = obj.g; 57 | this.invitor = obj.o; 58 | this.invitees = obj.ms; 59 | } 60 | } -------------------------------------------------------------------------------- /src/websocket/handler/sendMessageHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { MS, PUB_ACK } from "../../constant"; 3 | import LocalStore from "../store/localstore"; 4 | import Logger from "../utils/logger"; 5 | import MessageStatus from "../message/messageStatus"; 6 | 7 | export default class SendMessageHandler extends AbstractMessageHandler{ 8 | 9 | match(proto){ 10 | return proto.signal == PUB_ACK && proto.subSignal == MS; 11 | } 12 | 13 | processMessage(proto){ 14 | Logger.log("sendMessage Handler messageId "+proto.messageId) 15 | if(this.vueWebsocket.resolvePromiseMap.has(proto.messageId)){ 16 | var promiseReslove = this.vueWebsocket.resolvePromiseMap.get(proto.messageId); 17 | if(proto.content != ''){ 18 | var content = JSON.parse(proto.content); 19 | console.log("SendMessageHandler messageId "+content.messageId+" timestamp protoMessageId "+promiseReslove.protoMessageId); 20 | this.vueWebsocket.sendAction("updateProtoMessageUid",{ 21 | messageId: promiseReslove.protoMessageId, 22 | messageUid: content.messageId 23 | }); 24 | //update messageStatus 25 | this.vueWebsocket.sendAction("updateMessageStatus",{ 26 | messageId: promiseReslove.protoMessageId, 27 | status: MessageStatus.Sent 28 | }); 29 | LocalStore.updateSendMessageCount(); 30 | promiseReslove.resolve('success'); 31 | } else { 32 | this.vueWebsocket.sendAction("updateMessageStatus",{ 33 | messageId: promiseReslove.protoMessageId, 34 | status: MessageStatus.SendFailure 35 | }); 36 | promiseReslove.resolve('fail'); 37 | } 38 | clearTimeout(promiseReslove.timeoutId); 39 | this.vueWebsocket.resolvePromiseMap.delete(proto.messageId); 40 | } 41 | 42 | } 43 | } -------------------------------------------------------------------------------- /src/page/main.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 52 | 53 | 76 | -------------------------------------------------------------------------------- /src/websocket/message/protomessageContent.js: -------------------------------------------------------------------------------- 1 | import MessageContentType from "./messageContentType"; 2 | 3 | export default class ProtoMessageContent{ 4 | type; 5 | searchableContent; 6 | pushContent; 7 | content; 8 | binaryContent; // base64 string, 图片时,不包含头部信息:data:image/png;base64, 9 | localContent; 10 | mediaType; 11 | remoteMediaUrl; 12 | localMediaPath; 13 | mentionedType = 0; 14 | mentionedTargets = []; 15 | 16 | static toProtoMessageContent(content){ 17 | var protoMessageContent = new ProtoMessageContent(); 18 | protoMessageContent.type = content.type; 19 | protoMessageContent.content = content.content; 20 | protoMessageContent.searchableContent = content.searchableContent; 21 | protoMessageContent.pushContent = content.pushContent; 22 | protoMessageContent.binaryContent = content.binaryContent; 23 | protoMessageContent.localContent = content.localContent; 24 | protoMessageContent.mediaType = content.mediaType; 25 | protoMessageContent.remoteMediaUrl = content.remoteMediaUrl; 26 | protoMessageContent.localMediaPath = content.localMediaPath; 27 | protoMessageContent.mentionedType = content.mentionedType; 28 | protoMessageContent.mentionedTargets = content.mentionedTargets; 29 | return protoMessageContent; 30 | } 31 | 32 | static typeToContent(messageContent){ 33 | var showText = "您有新的消息"; 34 | switch(messageContent.type){ 35 | case MessageContentType.Image: 36 | showText = "[图片]"; 37 | break; 38 | case MessageContentType.File: 39 | showText = "[文件]"; 40 | break; 41 | case MessageContentType.Video: 42 | showText = "[视频]"; 43 | break; 44 | case MessageContentType.VOIP_CONTENT_TYPE_START: 45 | showText = "[网络电话]"; 46 | break; 47 | case MessageContentType.Tip_Notification: 48 | showText = '[通知消息]'; 49 | } 50 | return showText; 51 | } 52 | } -------------------------------------------------------------------------------- /src/constant/index.js: -------------------------------------------------------------------------------- 1 | export const WS_PROTOCOL = 'wss'; 2 | export const WS_IP = 'backend-websocket.fsharechat.cn/ws'; 3 | export const HTTP_IP = 'backend-http.fsharechat.cn'; 4 | // export const WS_IP = 'localhost'; 5 | export const WS_PORT = 9326; 6 | export const HEART_BEAT_INTERVAL = 25 * 1000; 7 | export const RECONNECT_INTERVAL = 30 * 1000; 8 | export const BINTRAY_TYPE = 'blob'; 9 | 10 | //signal 11 | export const CONNECT = 'CONNECT'; 12 | export const DISCONNECT = 'DISCONNECT'; 13 | export const CONNECT_ACK = 'CONNECT_ACK'; 14 | export const PUBLISH = 'PUBLISH'; 15 | export const PUB_ACK = 'PUB_ACK'; 16 | //subsignal 17 | export const FRP = 'FRP'; 18 | export const FP = 'FP'; 19 | export const FALS = 'FALS'; 20 | export const UPUI = 'UPUI'; 21 | export const GPGI = 'GPGI'; 22 | export const GPGM = 'GPGM'; 23 | export const GAM = 'GAM'; 24 | export const GC = 'GC'; 25 | export const GMI = 'GMI'; 26 | export const GKM = 'GKM'; 27 | export const GQ = 'GQ'; 28 | export const GD = 'GD'; 29 | export const MP = 'MP'; 30 | export const MS = "MS"; 31 | export const MN = "MN"; 32 | export const MR = "MR"; 33 | export const RMN = "RMN"; 34 | export const GQNUT = "GQNUT"; 35 | export const GMURL = "GMURL"; 36 | export const US = "US"; 37 | export const FAR = "FAR"; 38 | export const FRN = "FRN"; 39 | export const FHR = "FHR"; 40 | export const FN = "FN"; 41 | export const MMI = "MMI"; 42 | 43 | export const HTTP_HOST = "https://"+HTTP_IP + "/" 44 | export const LOGIN_API = HTTP_HOST + "login"; 45 | export const SNED_VERIFY_CODE_API = HTTP_HOST + "send_code";; 46 | 47 | export const KEY_VUE_DEVICE_ID = 'vue-device-id'; 48 | export const KEY_VUE_USER_ID = 'vue-user-id'; 49 | export const KEY_VUE_TOKEN = 'vue-token'; 50 | 51 | //userId 这里为了演示静态登录,由于还没有登录界面所以暂时使用静态userid 52 | export const USER_ID = 'TYTzTz33'; 53 | export const CLINET_ID = 'bccdb58cfdb34d861576810441000'; 54 | //token 55 | export const TOKEN = '6Yz2rQDrtRPRc3j9PesLy0De17uX2RlVcvkxU/UmGEaMamd/kaagwWNThIWSGMd6SPVHxLeynho03sJWdbm7wFMRO8VTKf5Wogv7l7gKLsq81mswRha3j6FMdDVHVJ+MLJrVjrThkqXrK1rHwsZvGxpqSGcekHIggI1UEEJSXyQ='; 56 | 57 | //是否使用七牛上传文件 58 | export const UPLOAD_BY_QINIU = false; 59 | 60 | export const ERROR_CODE = 400; 61 | export const SUCCESS_CODE = 200; 62 | -------------------------------------------------------------------------------- /static/css/reset.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video, input { 14 | margin: 0; 15 | padding: 0; 16 | border: 0; 17 | font-size: 100%; 18 | font-weight: normal; 19 | vertical-align: baseline; 20 | } 21 | 22 | article, aside, details, figcaption, figure, 23 | footer, header, menu, nav, section { 24 | display: block; 25 | } 26 | 27 | body { 28 | line-height: 1; 29 | overflow: hidden; 30 | } 31 | 32 | blockquote, q { 33 | quotes: none; 34 | } 35 | 36 | blockquote:before, blockquote:after, 37 | q:before, q:after { 38 | content: none; 39 | } 40 | 41 | table { 42 | border-collapse: collapse; 43 | border-spacing: 0; 44 | } 45 | 46 | a { 47 | color: #7e8c8d; 48 | text-decoration: none; 49 | -webkit-backface-visibility: hidden; 50 | } 51 | 52 | li { 53 | list-style: none; 54 | } 55 | 56 | 57 | html, body { 58 | width: 100%; 59 | height: 100%; 60 | } 61 | body { 62 | -webkit-text-size-adjust: none; 63 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 64 | background: #466368; 65 | background: -webkit-linear-gradient(#648880, #293f50); 66 | background: -moz-linear-gradient(#648880, #293f50); 67 | background: linear-gradient(#648880, #293f50); 68 | background-size: cover; 69 | } 70 | 71 | /*在mac上,增加此设置,会导致滚动条不能自动显示与隐藏,滚动条会一直显示 */ 72 | ::-webkit-scrollbar { 73 | width: 8px; 74 | } 75 | 76 | /* 滚动条滑块 */ 77 | ::-webkit-scrollbar-thumb { 78 | border-radius: 6px; 79 | background: rgba(0,0,0,0.1); 80 | } 81 | 82 | /* video { 83 | background: #222; 84 | margin: 0 0 20px 0; 85 | --width: 100%; 86 | width: var(--width); 87 | height: var(--width); 88 | } */ 89 | 90 | 91 | *,*:before,*:after { 92 | -moz-box-sizing: border-box; 93 | -webkit-box-sizing: border-box; 94 | box-sizing: border-box 95 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wechat", 3 | "version": "1.0.14", 4 | "description": "基于unverse-push的开源即时通讯web客户端", 5 | "author": "comsince", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "start": "node build/dev-server.js", 10 | "build": "node build/build.js" 11 | }, 12 | "dependencies": { 13 | "@wcjiang/notify": "^2.0.12", 14 | "axios": "^0.19.0", 15 | "crypto-js": "^3.1.9-1", 16 | "element-ui": "^2.13.0", 17 | "pinyin": "^2.9.0", 18 | "qiniu-js": "^2.5.5", 19 | "stylus": "^0.54.5", 20 | "stylus-loader": "^3.0.1", 21 | "uuid-js": "^0.7.5", 22 | "v-viewer": "^1.5.1", 23 | "vue": "^2.5.2", 24 | "vue-axios": "^2.1.4", 25 | "vue-router": "^3.0.1", 26 | "vuex": "^3.0.1", 27 | "webrtc-adapter": "^7.5.1", 28 | "xgplayer": "^2.4.7", 29 | "xgplayer-vue": "^1.1.5" 30 | }, 31 | "devDependencies": { 32 | "autoprefixer": "^6.7.2", 33 | "babel-core": "^6.22.1", 34 | "babel-loader": "^6.2.10", 35 | "babel-plugin-transform-runtime": "^6.22.0", 36 | "babel-preset-env": "^1.3.2", 37 | "babel-preset-stage-2": "^6.22.0", 38 | "babel-register": "^6.22.0", 39 | "chalk": "^1.1.3", 40 | "connect-history-api-fallback": "^1.3.0", 41 | "copy-webpack-plugin": "^4.0.1", 42 | "css-loader": "^0.28.0", 43 | "eventsource-polyfill": "^0.9.6", 44 | "express": "^4.14.1", 45 | "extract-text-webpack-plugin": "^2.0.0", 46 | "file-loader": "^0.11.1", 47 | "friendly-errors-webpack-plugin": "^1.1.3", 48 | "html-webpack-plugin": "^2.28.0", 49 | "http-proxy-middleware": "^0.17.3", 50 | "webpack-bundle-analyzer": "^2.2.1", 51 | "semver": "^5.3.0", 52 | "shelljs": "^0.7.6", 53 | "opn": "^4.0.2", 54 | "optimize-css-assets-webpack-plugin": "^1.3.0", 55 | "ora": "^1.2.0", 56 | "rimraf": "^2.6.0", 57 | "url-loader": "^0.5.8", 58 | "vue-loader": "^11.3.4", 59 | "vue-style-loader": "^2.0.5", 60 | "vue-template-compiler": "^2.2.6", 61 | "webpack": "^2.3.3", 62 | "webpack-dev-middleware": "^1.10.0", 63 | "webpack-hot-middleware": "^2.18.0", 64 | "webpack-merge": "^4.1.0" 65 | }, 66 | "engines": { 67 | "node": ">= 4.0.0", 68 | "npm": ">= 3.0.0" 69 | }, 70 | "browserslist": [ 71 | "> 1%", 72 | "last 2 versions", 73 | "not ie <= 8" 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /src/websocket/websocketcli.js: -------------------------------------------------------------------------------- 1 | import Logger from "./utils/logger"; 2 | import vuexStore from '../store' 3 | 4 | 5 | export class WebSocketClient { 6 | 7 | getDisplayName(userId){ 8 | var userInfolist = vuexStore.state.userInfoList; 9 | var userInfo = userInfolist.find(user => user.uid == userId); 10 | var displayName = userId; 11 | var friendData = vuexStore.state.friendDatas.find(friend => friend.friendUid == userId) 12 | if(friendData && friendData.alias && friendData.alias != ""){ 13 | displayName = friendData.alias 14 | } else if(userInfo){ 15 | displayName = userInfo.displayName; 16 | if(displayName == ''){ 17 | displayName = userInfo.mobile; 18 | } 19 | } else { 20 | vuexStore.state.vueSocket.getUserInfo(userId); 21 | } 22 | // console.log("userId "+userId +" displayName "+displayName) 23 | return displayName; 24 | } 25 | 26 | getPortrait(userId){ 27 | var userInfolist = vuexStore.state.userInfoList; 28 | var userInfo = userInfolist.find(user => user.uid == userId); 29 | var portrait = 'static/images/vue.jpg' 30 | if(userInfo){ 31 | portrait = userInfo.portrait; 32 | } 33 | return portrait; 34 | } 35 | 36 | 37 | createGroup(groupName,memberIds){ 38 | return vuexStore.state.vueSocket.createGroup(groupName,memberIds); 39 | } 40 | 41 | modifyGroupInfo(info){ 42 | return vuexStore.state.vueSocket.modifyGroupInfo(info); 43 | } 44 | 45 | quitGroup(groupId){ 46 | return vuexStore.state.vueSocket.quitGroup(groupId); 47 | } 48 | 49 | dismissGroup(groupId){ 50 | return vuexStore.state.vueSocket.dismissGroup(groupId); 51 | } 52 | 53 | getGroupMember(groupId){ 54 | return vuexStore.state.vueSocket.getGroupMember(groupId); 55 | } 56 | 57 | addMembers(groupId, memberIds){ 58 | return vuexStore.state.vueSocket.addMembers(groupId,memberIds); 59 | } 60 | 61 | kickeMembers(groupId,memberIds){ 62 | return vuexStore.state.vueSocket.kickeMembers(groupId,memberIds); 63 | } 64 | 65 | recallMessage(messageUid){ 66 | return vuexStore.state.vueSocket.recallMessage(messageUid); 67 | } 68 | 69 | getMinioUploadUrl(mediaType,key){ 70 | return vuexStore.state.vueSocket.getMinioUploadUrl(mediaType,key); 71 | } 72 | 73 | modifyFriendAlias(targetUid,alias){ 74 | return vuexStore.state.vueSocket.modifyFriendAlias(targetUid,alias) 75 | } 76 | 77 | } 78 | 79 | const self = new WebSocketClient(); 80 | export default self; -------------------------------------------------------------------------------- /src/components/search/search.vue: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 44 | 45 | 101 | -------------------------------------------------------------------------------- /src/websocket/message/protomessage.js: -------------------------------------------------------------------------------- 1 | import MessageStatus from "./messageStatus"; 2 | import ConversationType from "../model/conversationType"; 3 | import ProtoMessageContent from "./protomessageContent"; 4 | import LocalStore from "../store/localstore"; 5 | 6 | /** 7 | * 聊天信息content,在发送的时候转换为json消息体发送 8 | */ 9 | export default class ProtoMessage { 10 | conversationType; 11 | target; 12 | line; 13 | from = ''; 14 | content = {}; 15 | messageId = '0'; 16 | direction = 0; 17 | status = 0; 18 | messageUid = '0'; 19 | timestamp = 0; 20 | to = ''; 21 | 22 | 23 | static toProtoMessage(obj){ 24 | let currentUserId = LocalStore.getUserId(); 25 | let protoMessage = new ProtoMessage(); 26 | if(obj.from == currentUserId){ 27 | protoMessage.direction = 0; 28 | protoMessage.status = MessageStatus.Sent; 29 | } else { 30 | protoMessage.direction = 1; 31 | protoMessage.status = MessageStatus.Unread; 32 | } 33 | protoMessage.messageId = obj.messageId; 34 | protoMessage.messageUid = obj.messageId; 35 | protoMessage.timestamp = obj.timestamp; 36 | protoMessage.conversationType = obj.conversationType; 37 | protoMessage.target = obj.target; 38 | protoMessage.from = obj.from; 39 | if(protoMessage.conversationType == ConversationType.Single){ 40 | if(obj.from != currentUserId){ 41 | protoMessage.target = obj.from; 42 | protoMessage.from = obj.from; 43 | } else { 44 | protoMessage.target = obj.target; 45 | protoMessage.from = obj.from; 46 | } 47 | } 48 | protoMessage.line = obj.line; 49 | protoMessage.content = ProtoMessageContent.toProtoMessageContent(obj.content); 50 | return protoMessage; 51 | } 52 | 53 | /*** 54 | * 转为即将发送的protomessage 55 | */ 56 | static convertToProtoMessage(message){ 57 | var protoMessage = new ProtoMessage(); 58 | protoMessage.conversationType = message.conversation.type; 59 | protoMessage.target = message.conversation.target; 60 | protoMessage.line = message.conversation.line; 61 | protoMessage.from = message.from; 62 | protoMessage.direction = message.direction; 63 | protoMessage.status = message.status; 64 | protoMessage.messageId = message.messageId; 65 | protoMessage.messageUid = message.messageUid; 66 | protoMessage.timestamp = message.timestamp; 67 | console.log("protomessage content "+message.content) 68 | var payload = message.content.encode(); 69 | protoMessage.content = ProtoMessageContent.toProtoMessageContent(payload); 70 | return protoMessage; 71 | } 72 | } -------------------------------------------------------------------------------- /src/websocket/handler/getuserinfoHandler.js: -------------------------------------------------------------------------------- 1 | import AbstractMessageHandler from "./abstractmessagehandler"; 2 | import { PUB_ACK, UPUI, ERROR_CODE, SUCCESS_CODE } from "../../constant"; 3 | import UserInfo from "../model/userInfo"; 4 | import py from "pinyin" 5 | import { isBuffer } from "util"; 6 | import FutureResult from "../future/futureResult"; 7 | 8 | export default class GetUserInfoHandler extends AbstractMessageHandler{ 9 | match(proto){ 10 | return proto.signal == PUB_ACK && proto.subSignal == UPUI; 11 | } 12 | 13 | processMessage(proto){ 14 | if(proto.content != null && proto.content != ''){ 15 | var userInfoList = JSON.parse(proto.content); 16 | var stateFriendList = []; 17 | var userInfos = []; 18 | for(var i in userInfoList){ 19 | var displayName = userInfoList[i].displayName === '' ? userInfoList[i].mobile : userInfoList[i].displayName; 20 | var pinyinInitial = py(displayName,{ 21 | style: py.STYLE_FIRST_LETTER 22 | }); 23 | var initial = pinyinInitial[0][0]; 24 | 25 | if(initial.length > 1){ 26 | var initial = initial.substr(0,1); 27 | var reg= /^[A-Za-z]/; 28 | if(reg.test(initial)){ 29 | initial = initial.toUpperCase(); 30 | } else { 31 | initial = "#"; 32 | } 33 | } else { 34 | initial = initial.toUpperCase(); 35 | } 36 | stateFriendList.push({ 37 | id: parseInt(i) + 1, 38 | wxid: userInfoList[i].uid, //微信号 39 | initial: initial, //姓名首字母 40 | img: userInfoList[i].portrait, //头像 41 | signature: "", //个性签名 42 | nickname: displayName, //昵称 43 | sex: 0, //性别 1为男,0为女 44 | remark: displayName, //备注 45 | area: userInfoList[i].address, //地区 46 | }); 47 | userInfos.push(UserInfo.convert2UserInfo(userInfoList[i])); 48 | } 49 | if(this.vueWebsocket.resolvePromiseMap.has(proto.messageId)){ 50 | console.log("messageId "+proto.messageId); 51 | var promiseReslove = this.vueWebsocket.resolvePromiseMap.get(proto.messageId); 52 | clearTimeout(promiseReslove.timeoutId); 53 | var displayName = userInfoList[0].displayName === '' ? userInfoList[0].mobile : userInfoList[0].displayName; 54 | promiseReslove.resolve(new FutureResult(SUCCESS_CODE,displayName)); 55 | this.vueWebsocket.resolvePromiseMap.delete(proto.messageId); 56 | } 57 | 58 | this.vueWebsocket.sendAction("updateUserInfos",userInfos); 59 | this.vueWebsocket.sendAction("updateFriendList",stateFriendList); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/websocket/store/localstore.js: -------------------------------------------------------------------------------- 1 | import { KEY_VUE_USER_ID } from "../../constant"; 2 | 3 | /** 4 | * 本地缓存,包括如下基本信息 5 | * 消息缓存 6 | * 消息当前序列号,用于消息拉取 7 | * 朋友列表信息 8 | * 群组信息 9 | */ 10 | export default class LocalStore { 11 | 12 | static saveConverSations(value){ 13 | localStorage.setItem("coversations",JSON.stringify(value)); 14 | } 15 | 16 | static getConversations(){ 17 | let vaule = localStorage.getItem("coversations"); 18 | return JSON.parse(vaule); 19 | } 20 | 21 | static getLastMessageSeq(){ 22 | return localStorage.getItem("last_message_seq"); 23 | } 24 | 25 | //设置上传token,根据不同media类型存储 26 | static setUploadToken(key,token){ 27 | localStorage.setItem(key,token); 28 | } 29 | 30 | static getImageUploadToken(){ 31 | return localStorage.getItem("http://image.comsince.cn/"); 32 | } 33 | 34 | /** 35 | * messageSeq为string类型 36 | */ 37 | static setLastMessageSeq(messageSeq){ 38 | localStorage.setItem("last_message_seq",messageSeq); 39 | } 40 | 41 | static saveMessages(value){ 42 | localStorage.setItem("messages",JSON.stringify(value)); 43 | } 44 | 45 | static getMessages(){ 46 | let value = localStorage.getItem("messages"); 47 | return JSON.parse(value); 48 | } 49 | 50 | static saveUserInfoList(value){ 51 | localStorage.setItem("user_infos_list",JSON.stringify(value)); 52 | } 53 | 54 | static getUserInfoList(){ 55 | let value = localStorage.getItem("user_infos_list"); 56 | return JSON.parse(value); 57 | } 58 | 59 | /** 60 | * 记录消息发送条数主要时为了 61 | */ 62 | static updateSendMessageCount(){ 63 | let value = localStorage.getItem("send_message_count"); 64 | if(value){ 65 | value = parseInt(value) + 1; 66 | } else { 67 | value = 1; 68 | } 69 | localStorage.setItem("send_message_count",value); 70 | } 71 | 72 | static getSendMessageCount(){ 73 | let value = localStorage.getItem("send_message_count"); 74 | if(!value){ 75 | value = 0; 76 | } 77 | return value; 78 | } 79 | 80 | static resetSendMessageCount(){ 81 | localStorage.setItem("send_message_count",0); 82 | } 83 | 84 | static getUserId(){ 85 | return localStorage.getItem(KEY_VUE_USER_ID); 86 | } 87 | 88 | static setSelectTarget(value){ 89 | localStorage.setItem("select_target",value); 90 | } 91 | 92 | static getSelectTarget(){ 93 | return localStorage.getItem("select_target"); 94 | } 95 | 96 | static setFriendRequestVersion(version){ 97 | localStorage.setItem("friend_request_version",version); 98 | } 99 | 100 | static getFriendRequestVersion(){ 101 | return localStorage.getItem("friend_request_version"); 102 | } 103 | 104 | static saveMessageId(messageId){ 105 | localStorage.setItem("message_id",messageId); 106 | } 107 | 108 | static getMessageId(){ 109 | var messageId = localStorage.getItem("message_id"); 110 | if(!messageId){ 111 | messageId = 0; 112 | } 113 | return messageId; 114 | } 115 | 116 | static clearLocalStore(){ 117 | localStorage.setItem("coversations",""); 118 | localStorage.setItem("last_message_seq",""); 119 | localStorage.setItem("messages",""); 120 | localStorage.setItem("send_message_count",0); 121 | localStorage.setItem("select_target",""); 122 | localStorage.setItem("friend_request_version",0); 123 | localStorage.setItem(KEY_VUE_USER_ID,''); 124 | localStorage.setItem("message_id",0); 125 | } 126 | } -------------------------------------------------------------------------------- /src/websocket/utils/aes.js: -------------------------------------------------------------------------------- 1 | 2 | import CryptoJS from 'crypto-js' 3 | const iv = CryptoJS.enc.Base64.parse('ABEiM0RVZnd4eXp7fH1+fw=='); 4 | 5 | export function decrypt (text) { 6 | let decrypted = CryptoJS.AES.decrypt(text, iv, { 7 | iv: iv, 8 | mode: CryptoJS.mode.CBC, 9 | padding: CryptoJS.pad.Pkcs7 10 | }) 11 | //由于服务端加密的时候前四个字节时间参数,现在暂时忽略前4字节 12 | var resultWordArr = CryptoJS.enc.Hex.parse(decrypted.toString().slice(8)); 13 | let result = resultWordArr.toString(CryptoJS.enc.Utf8); 14 | // let rmtimeResult = result.slice(4); 15 | return result; 16 | } 17 | 18 | 19 | export function encrypt(encryptCode,key) { 20 | console.log('code '+encryptCode+' key '+key); 21 | let keyArry = new Array(16); 22 | for(let i = 0; i<16; i++){ 23 | //这里是至打印字符编码 24 | keyArry[i] = key.charCodeAt(i) & 0xFF; 25 | } 26 | let keyCode = String.fromCharCode.apply(String, keyArry); 27 | let secretCode = CryptoJS.enc.Utf8.parse(keyCode); 28 | console.log('keycode '+keyCode); 29 | 30 | let timeEncryptCode = convertTimeEncryptCode(encryptCode); 31 | console.log('timeEncryptCode '+timeEncryptCode); 32 | 33 | //pwd 必须base64解码 34 | 35 | let encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Base64.parse(timeEncryptCode), secretCode, { 36 | iv: secretCode, 37 | mode: CryptoJS.mode.CBC, 38 | padding: CryptoJS.pad.Pkcs7 39 | }); 40 | return encrypted.ciphertext.toString(CryptoJS.enc.Base64); 41 | } 42 | 43 | function convertTimeEncryptCode(encryptCode){ 44 | 45 | // let encryptArr = string2Bin(encryptCode); 46 | let encryptArr = CryptoJS.enc.Base64.parse(encryptCode); 47 | 48 | encryptArr = wordToByteArray(encryptArr.words); 49 | // encryptArr = string2Bin(encryptArr); 50 | let result = []; 51 | let curhour = (new Date().getMilliseconds()/1000 - 1514736000)/3600; 52 | for(var i=0; i < encryptArr.length + 4; i++){ 53 | if(i < 4){ 54 | if(i == 0){ 55 | result.push(curhour & 0xFF); 56 | } else if(i == 1){ 57 | result.push((curhour & 0xFF00) >> 8); 58 | } else if(i == 2){ 59 | result.push((curhour & 0xFF0000) >> 16); 60 | } else if(i == 3){ 61 | result.push((curhour & 0xFF) >> 24); 62 | } 63 | } else{ 64 | result.push(encryptArr[i - 4]); 65 | } 66 | } 67 | console.log('convertTimeEncryptCode result '+result); 68 | 69 | // https://stackoverflow.com/questions/9267899/arraybuffer-to-base64-encoded-string 70 | var base64wordarr = btoa(String.fromCharCode(...new Uint8Array(result))); 71 | console.log("hash word "+base64wordarr); 72 | 73 | return base64wordarr; 74 | } 75 | 76 | function bin2String(array) { 77 | var result = ""; 78 | for (var i = 0; i < array.length; i++) { 79 | result += String.fromCharCode(parseInt(array[i], 2)); 80 | } 81 | return result; 82 | } 83 | 84 | function string2Bin(str) { 85 | var result = []; 86 | for (var i = 0; i < str.length; i++) { 87 | result.push(str.charCodeAt(i).toString(2)); 88 | } 89 | return result; 90 | } 91 | 92 | function wordToByteArray(wordArray) { 93 | var byteArray = [], word, i, j; 94 | for (i = 0; i < wordArray.length; ++i) { 95 | word = wordArray[i]; 96 | for (j = 3; j >= 0; --j) { 97 | byteArray.push((word >> 8 * j) & 0xFF); 98 | } 99 | } 100 | return byteArray; 101 | } 102 | 103 | function byteArrayToString(byteArray) { 104 | var str = "", i; 105 | for (i = 0; i < byteArray.length; ++i) { 106 | str += escape(String.fromCharCode(byteArray[i])); 107 | } 108 | return str; 109 | } 110 | -------------------------------------------------------------------------------- /src/components/friendlist/friendlist.vue: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 42 | 43 | 104 | -------------------------------------------------------------------------------- /src/components/menu/about.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 64 | 65 | -------------------------------------------------------------------------------- /DARK_MODE_FEATURE.md: -------------------------------------------------------------------------------- 1 | # 夜间模式功能实现 2 | 3 | 本项目已成功添加一键切换夜间模式功能,为用户提供更好的视觉体验。 4 | 5 | ## 🌙 功能特性 6 | 7 | - ✅ **一键切换**: 点击侧边栏的月亮/太阳图标即可切换模式 8 | - ✅ **平滑过渡**: 使用CSS过渡动画,切换效果流畅自然 9 | - ✅ **持久化存储**: 用户的模式偏好会自动保存到localStorage 10 | - ✅ **全局适配**: 所有UI组件都已适配夜间模式 11 | - ✅ **Element UI支持**: 包含Element UI组件的夜间模式样式 12 | - ✅ **响应式设计**: 在不同设备上都能正常工作 13 | 14 | ## 📁 文件修改说明 15 | 16 | ### 1. 状态管理 (Vuex Store) 17 | **文件**: `src/store.js` 18 | - 添加 `isDarkMode` 状态 19 | - 添加 `toggleDarkMode` mutation 20 | - 添加 `toggleDarkMode` action 21 | - 在 `initData` 中加载保存的主题偏好 22 | 23 | ### 2. 主应用组件 24 | **文件**: `src/App.vue` 25 | - 添加动态CSS类绑定 `:class="{ 'dark-mode': isDarkMode }"` 26 | - 引入CSS变量系统 27 | - 添加平滑过渡动画 28 | 29 | ### 3. 主页面布局 30 | **文件**: `src/page/main.vue` 31 | - 更新样式使用CSS变量 32 | - 添加过渡动画效果 33 | 34 | ### 4. 侧边栏组件 35 | **文件**: `src/components/mycard/mycard.vue` 36 | - 添加夜间模式切换按钮 37 | - 使用emoji表情作为图标(🌙/☀️) 38 | - 添加切换逻辑和样式 39 | 40 | ### 5. 聊天页面 41 | **文件**: `src/page/chat/chat.vue` 42 | - 更新背景色使用CSS变量 43 | 44 | ### 6. 会话列表组件 45 | **文件**: `src/components/chatlist/chatlist.vue` 46 | - 更新所有颜色使用CSS变量 47 | - 添加hover和active状态的夜间模式适配 48 | - 修复文本颜色和时间显示 49 | 50 | ### 7. 消息组件 51 | **文件**: `src/components/message/message.vue` 52 | - 更新消息框背景和边框颜色 53 | - 适配消息气泡的夜间模式样式 54 | - 修复头部和时间标签颜色 55 | 56 | ### 8. 搜索组件 57 | **文件**: `src/components/search/search.vue` 58 | - 更新搜索框背景和边框 59 | - 适配输入框的夜间模式样式 60 | - 修复占位符文本颜色 61 | 62 | ### 9. 文本输入组件 63 | **文件**: `src/components/text/text.vue` 64 | - 更新输入区域背景色 65 | - 适配发送按钮和工具栏图标 66 | - 修复emoji面板的夜间模式样式 67 | 68 | ### 10. 登录界面 69 | **文件**: `src/page/login/login.vue` 70 | - 添加夜间模式状态检测 71 | - 更新登录框背景和阴影 72 | - 适配输入框和标签颜色 73 | - 独立的CSS变量定义 74 | 75 | ### 11. 朋友页面 76 | **文件**: `src/page/friend/friend.vue` 77 | - 更新朋友列表和信息区域背景色 78 | 79 | ### 12. 朋友列表组件 80 | **文件**: `src/components/friendlist/friendlist.vue` 81 | - 更新列表背景和边框颜色 82 | - 适配hover和active状态 83 | - 修复分组标题和好友名称颜色 84 | 85 | ### 13. 好友信息组件 86 | **文件**: `src/components/info/info.vue` 87 | - 更新信息区域背景色 88 | - 适配朋友请求表格样式 89 | - 修复个人信息显示颜色 90 | 91 | ### 14. 全局样式 92 | **文件**: `src/assets/dark-mode.css` (新建) 93 | - 定义CSS变量系统 94 | - Element UI 夜间模式样式覆盖 95 | - 全局组件样式适配 96 | - 添加表格和头像组件的夜间模式支持 97 | 98 | **文件**: `src/main.js` 99 | - 导入全局夜间模式样式文件 100 | 101 | ## 🎨 主题配色方案 102 | 103 | ### 日间模式 (默认) 104 | ```css 105 | --bg-color: #fff; 106 | --text-color: #333; 107 | --sidebar-bg: #2b2c2f; 108 | --main-bg: #ffffff; 109 | --border-color: #e1e1e1; 110 | --hover-bg: #f5f5f5; 111 | ``` 112 | 113 | ### 夜间模式 114 | ```css 115 | --bg-color: #1a1a1a; 116 | --text-color: #e1e1e1; 117 | --sidebar-bg: #1a1a1a; 118 | --main-bg: #2a2a2a; 119 | --border-color: #404040; 120 | --hover-bg: #333333; 121 | ``` 122 | 123 | ## 🔧 技术实现细节 124 | 125 | ### CSS变量系统 126 | 使用CSS自定义属性(CSS Variables)实现主题切换,确保: 127 | - 一致的颜色管理 128 | - 运行时动态切换 129 | - 良好的浏览器兼容性 130 | 131 | ### 状态管理 132 | 通过Vuex管理夜间模式状态: 133 | ```javascript 134 | // State 135 | isDarkMode: false 136 | 137 | // Mutation 138 | toggleDarkMode(state) { 139 | state.isDarkMode = !state.isDarkMode; 140 | localStorage.setItem('vue-dark-mode', state.isDarkMode); 141 | } 142 | 143 | // Action 144 | toggleDarkMode: ({ commit }) => commit('toggleDarkMode') 145 | ``` 146 | 147 | ### 持久化存储 148 | 用户的主题偏好通过localStorage保存: 149 | ```javascript 150 | // 保存设置 151 | localStorage.setItem('vue-dark-mode', state.isDarkMode); 152 | 153 | // 加载设置 154 | const darkMode = localStorage.getItem('vue-dark-mode'); 155 | if(darkMode !== null){ 156 | state.isDarkMode = darkMode === 'true'; 157 | } 158 | ``` 159 | 160 | ## 🖱️ 使用方法 161 | 162 | 1. **手动切换**: 点击左侧侧边栏底部的月亮🌙/太阳☀️图标 163 | 2. **自动恢复**: 下次打开应用时会自动恢复上次选择的模式 164 | 3. **实时切换**: 切换时所有界面元素会平滑过渡到新主题 165 | 166 | ## 📱 演示页面 167 | 168 | 打开项目根目录的 `test-dark-mode.html` 文件可以预览夜间模式功能效果,包括会话列表和聊天界面的完整夜间模式支持。 169 | 170 | 171 | ## 🔮 未来扩展 172 | 173 | 可以考虑添加以下增强功能: 174 | - 系统主题自动跟随 175 | - 自定义主题颜色 176 | - 定时自动切换 177 | - 更多主题预设 178 | 179 | ## 💡 注意事项 180 | 181 | 1. 确保所有新增的UI组件都使用CSS变量 182 | 2. 测试在不同浏览器中的兼容性 183 | 3. 保持良好的对比度以确保可访问性 184 | 4. 定期检查第三方组件的夜间模式适配 185 | 186 | --- 187 | 188 | **实现状态**: ✅ 已完成 189 | **测试状态**: ✅ 功能正常 190 | **文档状态**: ✅ 已更新 -------------------------------------------------------------------------------- /src/websocket/message/message.js: -------------------------------------------------------------------------------- 1 | import Conversation from '../model/conversation' 2 | import MessageStatus from './messageStatus' 3 | import store from '../../store' 4 | import LocalStore from '../store/localstore'; 5 | /*** 6 | * message in json format 7 | { 8 | "conversation":{ 9 | "conversationType": 0, 10 | "target": "UZUWUWuu", 11 | "line": 0, 12 | } 13 | "from": "UZUWUWuu", 14 | "content": { 15 | "type": 1, 16 | "searchableContent": "1234", 17 | "pushContent": "", 18 | "content": "", 19 | "binaryContent": "", 20 | "localContent": "", 21 | "mediaType": 0, 22 | "remoteMediaUrl": "", 23 | "localMediaPath": "", 24 | "mentionedType": 0, 25 | "mentionedTargets": [ ] 26 | }, 27 | "messageId": 52, 28 | "direction": 1, 29 | "status": 5, 30 | "messageUid": 75735276990792720, 31 | "timestamp": 1550849394256, 32 | "to": "" 33 | } 34 | */ 35 | export default class Message { 36 | conversation = {}; 37 | from = ''; 38 | content = {}; 39 | messageId = 0; 40 | direction = 0; 41 | status = 0; 42 | messageUid = 0; 43 | timestamp = 0; 44 | to = ''; 45 | 46 | // static toMessage(obj){ 47 | // let msg = new Message(); 48 | // msg.conversation = new Conversation(obj.conversationType,obj.target,obj.line); 49 | // msg.from = obj.from; 50 | // msg.content = obj.content; 51 | // msg.messageId = obj.messageId; 52 | // if(obj.from == USER_ID){ 53 | // msg.direction = 0; 54 | // } else { 55 | // msg.direction = 1; 56 | // } 57 | // msg.status = obj.status; 58 | // msg.messageUid = obj.messageId; 59 | // msg.timestamp = obj.timestamp; 60 | // msg.to = obj.target; 61 | // return msg; 62 | // } 63 | 64 | static toMessage(state,sendMessage){ 65 | var message = new Message(); 66 | var target = sendMessage.target; 67 | if(!target){ 68 | target = state.selectTarget; 69 | } 70 | console.log("to message target "+target); 71 | let stateConversationInfo = state.conversations.find(conversation => conversation.conversationInfo.target === target); 72 | console.log("conversationtype "+stateConversationInfo.conversationInfo.conversationType +" target "+stateConversationInfo.conversationInfo.target); 73 | message.conversation = new Conversation(stateConversationInfo.conversationInfo.conversationType, 74 | stateConversationInfo.conversationInfo.target, 75 | stateConversationInfo.conversationInfo.line); 76 | console.log("send message content "+sendMessage.messageContent) 77 | message.content = sendMessage.messageContent; 78 | message.from = state.userId; 79 | message.status = MessageStatus.Sending; 80 | message.timestamp = new Date().getTime(); 81 | message.direction = 0; 82 | message.messageId = new Date().getTime(); 83 | return message; 84 | } 85 | 86 | //方法不支持重载 87 | static conert2Message(sendMessage){ 88 | var message = new Message(); 89 | var target = sendMessage.target; 90 | if(!target){ 91 | target = store.state.selectTarget; 92 | } 93 | console.log("to message target "+target); 94 | let stateConversationInfo = store.state.conversations.find(conversation => conversation.conversationInfo.target === target); 95 | console.log("conversationtype "+stateConversationInfo.conversationInfo.conversationType +" target "+stateConversationInfo.conversationInfo.target); 96 | message.conversation = new Conversation(stateConversationInfo.conversationInfo.conversationType, 97 | stateConversationInfo.conversationInfo.target, 98 | stateConversationInfo.conversationInfo.line); 99 | message.content = sendMessage.messageContent; 100 | message.from = LocalStore.getUserId(); 101 | message.status = MessageStatus.Sending; 102 | message.timestamp = new Date().getTime(); 103 | message.direction = 0; 104 | message.messageId = new Date().getTime(); 105 | return message; 106 | } 107 | } -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1698562", 3 | "name": "vue-chat", 4 | "font_family": "iconfont", 5 | "css_prefix_text": "icon-", 6 | "description": "", 7 | "glyphs": [ 8 | { 9 | "icon_id": "5100253", 10 | "name": "视频", 11 | "font_class": "shipin", 12 | "unicode": "e669", 13 | "unicode_decimal": 58985 14 | }, 15 | { 16 | "icon_id": "4425816", 17 | "name": "删除-方框", 18 | "font_class": "shanchu-fangkuang", 19 | "unicode": "e791", 20 | "unicode_decimal": 59281 21 | }, 22 | { 23 | "icon_id": "4770755", 24 | "name": "增加数字", 25 | "font_class": "zengjiashuzi", 26 | "unicode": "e641", 27 | "unicode_decimal": 58945 28 | }, 29 | { 30 | "icon_id": "6129045", 31 | "name": "9减、删除-线性方框", 32 | "font_class": "jianshanchu-xianxingfangkuang", 33 | "unicode": "e75a", 34 | "unicode_decimal": 59226 35 | }, 36 | { 37 | "icon_id": "8358855", 38 | "name": "增加4", 39 | "font_class": "zengjia2", 40 | "unicode": "e689", 41 | "unicode_decimal": 59017 42 | }, 43 | { 44 | "icon_id": "5253606", 45 | "name": "移除", 46 | "font_class": "yichu", 47 | "unicode": "e656", 48 | "unicode_decimal": 58966 49 | }, 50 | { 51 | "icon_id": "1301395", 52 | "name": "增加", 53 | "font_class": "zengjia", 54 | "unicode": "e668", 55 | "unicode_decimal": 58984 56 | }, 57 | { 58 | "icon_id": "1272671", 59 | "name": "移除", 60 | "font_class": "delete-br", 61 | "unicode": "e63d", 62 | "unicode_decimal": 58941 63 | }, 64 | { 65 | "icon_id": "4581221", 66 | "name": "发送中", 67 | "font_class": "loading-solid", 68 | "unicode": "e647", 69 | "unicode_decimal": 58951 70 | }, 71 | { 72 | "icon_id": "3398713", 73 | "name": "发送失败", 74 | "font_class": "fasongshibai", 75 | "unicode": "e62c", 76 | "unicode_decimal": 58924 77 | }, 78 | { 79 | "icon_id": "6659620", 80 | "name": "朋友", 81 | "font_class": "pengyou1", 82 | "unicode": "e631", 83 | "unicode_decimal": 58929 84 | }, 85 | { 86 | "icon_id": "3315144", 87 | "name": "邀请好友", 88 | "font_class": "yaoqinghaoyou", 89 | "unicode": "e61c", 90 | "unicode_decimal": 58908 91 | }, 92 | { 93 | "icon_id": "6683016", 94 | "name": "加好友", 95 | "font_class": "jiahaoyou", 96 | "unicode": "e603", 97 | "unicode_decimal": 58883 98 | }, 99 | { 100 | "icon_id": "795513", 101 | "name": "视频", 102 | "font_class": "ai-video", 103 | "unicode": "e66b", 104 | "unicode_decimal": 58987 105 | }, 106 | { 107 | "icon_id": "842937", 108 | "name": "表情", 109 | "font_class": "biaoqing", 110 | "unicode": "e666", 111 | "unicode_decimal": 58982 112 | }, 113 | { 114 | "icon_id": "2078817", 115 | "name": "dkw_消息 ", 116 | "font_class": "dkw_xiaoxi", 117 | "unicode": "e606", 118 | "unicode_decimal": 58886 119 | }, 120 | { 121 | "icon_id": "2967254", 122 | "name": "符号-电话", 123 | "font_class": "dianhua", 124 | "unicode": "e729", 125 | "unicode_decimal": 59177 126 | }, 127 | { 128 | "icon_id": "4166140", 129 | "name": "朋友", 130 | "font_class": "pengyou", 131 | "unicode": "e61a", 132 | "unicode_decimal": 58906 133 | }, 134 | { 135 | "icon_id": "5582330", 136 | "name": "搜索", 137 | "font_class": "sousuo", 138 | "unicode": "e659", 139 | "unicode_decimal": 58969 140 | }, 141 | { 142 | "icon_id": "5893499", 143 | "name": "退出", 144 | "font_class": "tuichu", 145 | "unicode": "e61f", 146 | "unicode_decimal": 58911 147 | }, 148 | { 149 | "icon_id": "7588121", 150 | "name": "图片", 151 | "font_class": "tupian", 152 | "unicode": "e623", 153 | "unicode_decimal": 58915 154 | }, 155 | { 156 | "icon_id": "10099699", 157 | "name": "文件", 158 | "font_class": "wenjian", 159 | "unicode": "e61b", 160 | "unicode_decimal": 58907 161 | }, 162 | { 163 | "icon_id": "10515201", 164 | "name": "挂断", 165 | "font_class": "guaduan", 166 | "unicode": "e640", 167 | "unicode_decimal": 58944 168 | } 169 | ] 170 | } 171 | -------------------------------------------------------------------------------- /dev.log: -------------------------------------------------------------------------------- 1 | 2 | > wechat@1.0.14 dev 3 | > node build/dev-server.js 4 | 5 | > Starting dev server... 6 | (node:2248) Warning: Accessing non-existent property 'cat' of module exports inside circular dependency 7 | (Use `node --trace-warnings ...` to show where the warning was created) 8 | (node:2248) Warning: Accessing non-existent property 'cd' of module exports inside circular dependency 9 | (node:2248) Warning: Accessing non-existent property 'chmod' of module exports inside circular dependency 10 | (node:2248) Warning: Accessing non-existent property 'cp' of module exports inside circular dependency 11 | (node:2248) Warning: Accessing non-existent property 'dirs' of module exports inside circular dependency 12 | (node:2248) Warning: Accessing non-existent property 'pushd' of module exports inside circular dependency 13 | (node:2248) Warning: Accessing non-existent property 'popd' of module exports inside circular dependency 14 | (node:2248) Warning: Accessing non-existent property 'echo' of module exports inside circular dependency 15 | (node:2248) Warning: Accessing non-existent property 'tempdir' of module exports inside circular dependency 16 | (node:2248) Warning: Accessing non-existent property 'pwd' of module exports inside circular dependency 17 | (node:2248) Warning: Accessing non-existent property 'exec' of module exports inside circular dependency 18 | (node:2248) Warning: Accessing non-existent property 'ls' of module exports inside circular dependency 19 | (node:2248) Warning: Accessing non-existent property 'find' of module exports inside circular dependency 20 | (node:2248) Warning: Accessing non-existent property 'grep' of module exports inside circular dependency 21 | (node:2248) Warning: Accessing non-existent property 'head' of module exports inside circular dependency 22 | (node:2248) Warning: Accessing non-existent property 'ln' of module exports inside circular dependency 23 | (node:2248) Warning: Accessing non-existent property 'mkdir' of module exports inside circular dependency 24 | (node:2248) Warning: Accessing non-existent property 'rm' of module exports inside circular dependency 25 | (node:2248) Warning: Accessing non-existent property 'mv' of module exports inside circular dependency 26 | (node:2248) Warning: Accessing non-existent property 'sed' of module exports inside circular dependency 27 | (node:2248) Warning: Accessing non-existent property 'set' of module exports inside circular dependency 28 | (node:2248) Warning: Accessing non-existent property 'sort' of module exports inside circular dependency 29 | (node:2248) Warning: Accessing non-existent property 'tail' of module exports inside circular dependency 30 | (node:2248) Warning: Accessing non-existent property 'test' of module exports inside circular dependency 31 | (node:2248) Warning: Accessing non-existent property 'to' of module exports inside circular dependency 32 | (node:2248) Warning: Accessing non-existent property 'toEnd' of module exports inside circular dependency 33 | (node:2248) Warning: Accessing non-existent property 'touch' of module exports inside circular dependency 34 | (node:2248) Warning: Accessing non-existent property 'uniq' of module exports inside circular dependency 35 | (node:2248) Warning: Accessing non-existent property 'which' of module exports inside circular dependency 36 | (node:2248) Warning: Accessing non-existent property 'lineno' of module exports inside circular dependency 37 | (node:2248) Warning: Accessing non-existent property 'column' of module exports inside circular dependency 38 | (node:2248) Warning: Accessing non-existent property 'filename' of module exports inside circular dependency 39 | (node:2248) Warning: Accessing non-existent property 'lineno' of module exports inside circular dependency 40 | (node:2248) Warning: Accessing non-existent property 'column' of module exports inside circular dependency 41 | (node:2248) Warning: Accessing non-existent property 'filename' of module exports inside circular dependency 42 | DONE Compiled successfully in 2993ms2:54:20 AM 43 | 44 | > Listening at http://localhost:9080 45 | 46 | /workspace/node_modules/opn/index.js:83 47 | reject(new Error('Exited with code ' + code)); 48 | ^ 49 | 50 | Error: Exited with code 3 51 | at ChildProcess. (/workspace/node_modules/opn/index.js:83:13) 52 | at Object.onceWrapper (node:events:633:26) 53 | at ChildProcess.emit (node:events:518:28) 54 | at maybeClose (node:internal/child_process:1101:16) 55 | at Socket. (node:internal/child_process:456:11) 56 | at Socket.emit (node:events:518:28) 57 | at Pipe. (node:net:351:12) 58 | 59 | Node.js v22.16.0 60 | -------------------------------------------------------------------------------- /src/components/menu/rightMenu.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/assets/dark-mode.css: -------------------------------------------------------------------------------- 1 | /* 全局夜间模式样式 */ 2 | :root { 3 | --bg-color: #fff; 4 | --text-color: #333; 5 | --sidebar-bg: #2b2c2f; 6 | --main-bg: #ffffff; 7 | --border-color: #e1e1e1; 8 | --hover-bg: #f5f5f5; 9 | --input-bg: #fff; 10 | --input-border: #dcdfe6; 11 | --card-bg: #fff; 12 | --shadow: rgba(0, 0, 0, 0.1); 13 | } 14 | 15 | .dark-mode { 16 | --bg-color: #1a1a1a; 17 | --text-color: #e1e1e1; 18 | --sidebar-bg: #1a1a1a; 19 | --main-bg: #2d2d2d; 20 | --border-color: #404040; 21 | --hover-bg: #3a3a3a; 22 | --input-bg: #3a3a3a; 23 | --input-border: #505050; 24 | --card-bg: #3a3a3a; 25 | --shadow: rgba(0, 0, 0, 0.5); 26 | } 27 | 28 | /* Element UI 夜间模式样式覆盖 */ 29 | .dark-mode .el-input__inner { 30 | background-color: var(--input-bg) !important; 31 | border-color: var(--input-border) !important; 32 | color: var(--text-color) !important; 33 | } 34 | 35 | .dark-mode .el-input__inner:focus { 36 | border-color: #409eff !important; 37 | } 38 | 39 | .dark-mode .el-button { 40 | background-color: var(--card-bg) !important; 41 | border-color: var(--border-color) !important; 42 | color: var(--text-color) !important; 43 | } 44 | 45 | .dark-mode .el-button:hover { 46 | background-color: var(--hover-bg) !important; 47 | } 48 | 49 | .dark-mode .el-card { 50 | background-color: var(--card-bg) !important; 51 | border-color: var(--border-color) !important; 52 | color: var(--text-color) !important; 53 | } 54 | 55 | .dark-mode .el-dialog { 56 | background-color: var(--card-bg) !important; 57 | color: var(--text-color) !important; 58 | } 59 | 60 | .dark-mode .el-dialog__header { 61 | border-bottom-color: var(--border-color) !important; 62 | } 63 | 64 | .dark-mode .el-dialog__title { 65 | color: var(--text-color) !important; 66 | } 67 | 68 | .dark-mode .el-message-box { 69 | background-color: var(--card-bg) !important; 70 | color: var(--text-color) !important; 71 | } 72 | 73 | .dark-mode .el-form-item__label { 74 | color: var(--text-color) !important; 75 | } 76 | 77 | .dark-mode .el-textarea__inner { 78 | background-color: var(--input-bg) !important; 79 | border-color: var(--input-border) !important; 80 | color: var(--text-color) !important; 81 | } 82 | 83 | .dark-mode .el-select .el-input .el-select__caret { 84 | color: var(--text-color) !important; 85 | } 86 | 87 | .dark-mode .el-option { 88 | background-color: var(--card-bg) !important; 89 | color: var(--text-color) !important; 90 | } 91 | 92 | .dark-mode .el-option:hover { 93 | background-color: var(--hover-bg) !important; 94 | } 95 | 96 | .dark-mode .el-dropdown-menu { 97 | background-color: var(--card-bg) !important; 98 | border-color: var(--border-color) !important; 99 | } 100 | 101 | .dark-mode .el-dropdown-menu__item { 102 | color: var(--text-color) !important; 103 | } 104 | 105 | .dark-mode .el-dropdown-menu__item:hover { 106 | background-color: var(--hover-bg) !important; 107 | } 108 | 109 | /* Element UI 表格夜间模式样式 */ 110 | .dark-mode .el-table { 111 | background-color: var(--main-bg) !important; 112 | color: var(--text-color) !important; 113 | } 114 | 115 | .dark-mode .el-table th { 116 | background-color: var(--card-bg) !important; 117 | color: var(--text-color) !important; 118 | border-bottom-color: var(--border-color) !important; 119 | } 120 | 121 | .dark-mode .el-table td { 122 | background-color: var(--main-bg) !important; 123 | color: var(--text-color) !important; 124 | border-bottom-color: var(--border-color) !important; 125 | } 126 | 127 | .dark-mode .el-table tr:hover td { 128 | background-color: var(--hover-bg) !important; 129 | } 130 | 131 | .dark-mode .el-table--enable-row-hover .el-table__body tr:hover > td { 132 | background-color: var(--hover-bg) !important; 133 | } 134 | 135 | /* Element UI 头像组件 */ 136 | .dark-mode .el-avatar { 137 | background-color: var(--card-bg) !important; 138 | color: var(--text-color) !important; 139 | } 140 | 141 | /* 自定义组件夜间模式样式 */ 142 | .dark-mode * { 143 | color: var(--text-color); 144 | transition: color 0.3s ease, background-color 0.3s ease; 145 | } 146 | 147 | .dark-mode div, .dark-mode span, .dark-mode p { 148 | background-color: transparent; 149 | } 150 | 151 | .dark-mode .router-link-active { 152 | color: #409eff !important; 153 | } 154 | 155 | /* 输入框占位符颜色 */ 156 | .dark-mode input::placeholder { 157 | color: var(--text-color); 158 | opacity: 0.5; 159 | } 160 | 161 | /* 滚动条样式 */ 162 | .dark-mode ::-webkit-scrollbar { 163 | width: 6px; 164 | } 165 | 166 | .dark-mode ::-webkit-scrollbar-track { 167 | background: var(--main-bg); 168 | } 169 | 170 | .dark-mode ::-webkit-scrollbar-thumb { 171 | background: var(--border-color); 172 | border-radius: 3px; 173 | } 174 | 175 | .dark-mode ::-webkit-scrollbar-thumb:hover { 176 | background: var(--hover-bg); 177 | 178 | } -------------------------------------------------------------------------------- /src/websocket/utils/timeUtils.js: -------------------------------------------------------------------------------- 1 | export default class TimeUtils { 2 | //参考链接: https://juejin.im/entry/5c7103f5f265da2dab17d1a6 3 | static _formatDate(date, fmt){ 4 | var o = { 5 | "M+": date.getMonth() + 1, //月份 6 | "d+": date.getDate(), //日 7 | "h+": date.getHours(), //小时 8 | "m+": date.getMinutes(), //分 9 | "s+": date.getSeconds(), //秒 10 | "q+": Math.floor((date.getMonth() + 3) / 3), //季度 11 | "S": date.getMilliseconds() //毫秒 12 | }; 13 | if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length)); 14 | for (var k in o) 15 | if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); 16 | return fmt; 17 | } 18 | 19 | static getTimeStringAutoShort2(timestamp,mustIncludeTime){ 20 | 21 | // 当前时间 22 | var currentDate = new Date(); 23 | // 目标判断时间 24 | var srcDate = new Date(parseInt(timestamp)); 25 | 26 | var currentYear = currentDate.getFullYear(); 27 | var currentMonth = (currentDate.getMonth()+1); 28 | var currentDateD = currentDate.getDate(); 29 | 30 | var srcYear = srcDate.getFullYear(); 31 | var srcMonth = (srcDate.getMonth()+1); 32 | var srcDateD = srcDate.getDate(); 33 | 34 | var ret = ""; 35 | 36 | // 要额外显示的时间分钟 37 | var timeExtraStr = (mustIncludeTime?" "+this._formatDate(srcDate, "hh:mm"):""); 38 | 39 | // 当年 40 | if(currentYear == srcYear) { 41 | var currentTimestamp = currentDate.getTime(); 42 | var srcTimestamp = timestamp; 43 | // 相差时间(单位:毫秒) 44 | var deltaTime = (currentTimestamp-srcTimestamp); 45 | 46 | // 当天(月份和日期一致才是) 47 | if(currentMonth == srcMonth && currentDateD == srcDateD) { 48 | // 时间相差60秒以内 49 | if(deltaTime < 60 * 1000) 50 | ret = "刚刚"; 51 | // 否则当天其它时间段的,直接显示“时:分”的形式 52 | else 53 | ret = this._formatDate(srcDate, "hh:mm"); 54 | } 55 | // 当年 && 当天之外的时间(即昨天及以前的时间) 56 | else { 57 | // 昨天(以“现在”的时候为基准-1天) 58 | var yesterdayDate = new Date(); 59 | yesterdayDate.setDate(yesterdayDate.getDate()-1); 60 | 61 | // 前天(以“现在”的时候为基准-2天) 62 | var beforeYesterdayDate = new Date(); 63 | beforeYesterdayDate.setDate(beforeYesterdayDate.getDate()-2); 64 | 65 | // 用目标日期的“月”和“天”跟上方计算出来的“昨天”进行比较,是最为准确的(如果用时间戳差值 66 | // 的形式,是不准确的,比如:现在时刻是2019年02月22日1:00、而srcDate是2019年02月21日23:00, 67 | // 这两者间只相差2小时,直接用“deltaTime/(3600 * 1000)” > 24小时来判断是否昨天,就完全是扯蛋的逻辑了) 68 | if(srcMonth == (yesterdayDate.getMonth()+1) && srcDateD == yesterdayDate.getDate()) 69 | ret = "昨天"+timeExtraStr;// -1d 70 | // “前天”判断逻辑同上 71 | else if(srcMonth == (beforeYesterdayDate.getMonth()+1) && srcDateD == beforeYesterdayDate.getDate()) 72 | ret = "前天"+timeExtraStr;// -2d 73 | else{ 74 | // 跟当前时间相差的小时数 75 | var deltaHour = (deltaTime/(3600 * 1000)); 76 | 77 | // 如果小于或等 7*24小时就显示星期几 78 | if (deltaHour <= 7*24){ 79 | var weekday=new Array(7); 80 | weekday[0]="星期日"; 81 | weekday[1]="星期一"; 82 | weekday[2]="星期二"; 83 | weekday[3]="星期三"; 84 | weekday[4]="星期四"; 85 | weekday[5]="星期五"; 86 | weekday[6]="星期六"; 87 | 88 | // 取出当前是星期几 89 | var weedayDesc = weekday[srcDate.getDay()]; 90 | ret = weedayDesc+timeExtraStr; 91 | } 92 | // 否则直接显示完整日期时间 93 | else 94 | ret = this._formatDate(srcDate, "yy/M/d")+timeExtraStr; 95 | } 96 | } 97 | } 98 | // 往年 99 | else{ 100 | ret = this._formatDate(srcDate, "yy/M/d")+timeExtraStr; 101 | } 102 | 103 | return ret; 104 | } 105 | } -------------------------------------------------------------------------------- /src/page/friend/searchfriend.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 123 | 124 | -------------------------------------------------------------------------------- /src/components/mycard/mycard.vue: -------------------------------------------------------------------------------- 1 | 2 | 35 | 36 | 115 | 116 | 207 | -------------------------------------------------------------------------------- /src/assets/fonts/iconfont.css: -------------------------------------------------------------------------------- 1 | @font-face {font-family: "iconfont"; 2 | src: url('iconfont.eot?t=1592274269382'); /* IE9 */ 3 | src: url('iconfont.eot?t=1592274269382#iefix') format('embedded-opentype'), /* IE6-IE8 */ 4 | url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAy4AAsAAAAAF1wAAAxrAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCGNAqcCJY0ATYCJANgCzIABCAFhG0HgkgbjhMjEaac1Znsrwu4Q2pxlyQy2IObrVRoQS6vgYCib9QxOJH4IiO5qF+ow/eNfD6asz87syHZ3YSNIpaKUxrUQ1PFrWIWapq6UlGHe0epaDhfeu6XbnWqb5qigXvxVnttfwLZhMWW2FkAIGzCGPqKGBjm9r1elL5r9UfHVLmp/Mh2kRVXwMsKwuZGZq3NEiDU5pzCUfHDAgj4/+de7UtZdgxvGFdhembVe0k+PEhPbzk/I0qJX4egajeFx89jXjnD/AGAA9SrmnUzzg8CRpseqIuCj4xV3x0IAAGBCAax9+yXDzUYFCd0GzlsSA3UsUqwkgkE6jJVznUZiAs81NwSrhHAKnf8ydcII2qAA0+h7il7cMZApCi0dTV1/u9EOXWirj8PwPl6AAUQDIABnJy7iQigPgt2hZCnMrQA6AyreJNqhSoqxV8JUtopwUqokqikKg6lUClRBivDlBHKktYuraWtq///v1Zp38KUVQw9dOUTdNalgwtDCm3/hQdoIcMACySYYIYAEWp4gUIDFYzwhAcIdOChBwMHEBXoKgEAaS8bvQCFmiZoAUVlggwoviYYAMXPBAug+JsgAUoQwHQn2wEwI4MBCMhQACIyEYAamYI9vkwFQJEOABpkIRxzkiUAjMjBADyRQwF4IIcBIMgRAHTIJZjAA61dTNADraUmMKB1dQYclPC2r2MD0AtgYQDfDNx1rqDa5SG4BI+BM503teBSvTkQWcdkWTDAn6lWkr8syUxzFuvVVtKw9hLxN8tDzKgykzR+GslDI795b7OfJHkbzWZvKdggyRrewztohu5skz+3/Umk54WnQXRHc3H1uqWWDYTW1Ztr1m8a6wndjHNPAti2pgjd+eZAfufT6Gn8hg0W3bp1JrZ+vdlz48ZSad/RWqvfGalvbOgnPwbFPsRDvyopGdWosbBWSwmqleKswhpUpUCw5vvVmqbMD2pVVQuFNSA2JcaNTY75lOwV/UzZcC8SVDdF+gmw9k2l4yR1AHfjp8PSpmfEodQILz53R2rF2rhNu9PKvdQ60Sq/emdaZe36+C01f1Pj9ZU2DGBOhannK87sci8urTt1PO9lEu5qg6GBDIMsI0SmQUVmud1uo6J75VN6NCY2V0caKBEXcwp729cOq8KuLcKV3/OK+XM2AhF/p24tQgeS7ms/eqX6Am67jM/P2lBtDOMJA93QVVjhdrhFzLYrtpVmwFZvmRMpwhnsk72aSIJQDSF1wxxT0OhOb7S5Ba1ALweRBwT8Dqn8Lvul9xqBN8gUtUFIZfBHZuxM2Q5ckZNrVe6uHZViZEGwPXNyof4Gyc7EZS+fMDqpDxk4JDurzJwhxBxc9SKi6ECLSvpCJIgWyEzewOwortYPzon1dkMr8LJwMOj3+9DAqweg6uJXZxtaweAqzA2qFZHJAzQAa6hvUFVWdnV5Ze0T2ucIhYHhY3DlBbWF8bk0JEoBzNxnPo7Wxr5BtMrdIJ7zarVi/2VEM+1eIDTHpKl2fDnMP1TrAwg/W968IW8TiBoESNvZebCe/xCbZ+/OMffnHiBzBtuCgQjxcdKYbMnnJ3cEvtGF0NU8Sk5updHND/Lk4/8oiVJLcqM8H5ZnZ2X20BZjB1ETDkCMirKstZ3BvW6rw31ZDGbAIYFlhdB8/nJhQEax+0mfwLbYvfJaZFy39hyBqKrmDOzS9/UpzO9/fWUzuF6tCwOiYBdzEYUWKKRxjeGs9zPtdUCn5wotMp7v6exp8Z2IfJpCn7Yv+x0NOO8CR+u8r+6PsNx42pbSV5fIehWjtA1W7Iac14SGenlTpyawOVx5Vj2jUcQuj7Vt8jghd/xC5KPnAXXbw+74Fj9xa3/Qup+44fUz5L0DdLkv5T6eJ15e3ctDXdvFDZENiE7PGRvu8d4jfOx7hrPRjPeaRtd7V6Pm0H7Drve20FCbNSzC53WYfHKzOFncfJLzcm/1U/STzQvd4nf4sFbvq9c661FH/lXFKSkzirdar+VcUWYm3tS28TPo7HH11+N1SatcYC2QW8UwP02IEDbviti1WTjtGG/tFqFxRbg0Ilsk5jkPG6cY6w1TrPXyFFJPCulnB/IUPM+Pjinu634zX7/YIK5LBAtPnM50+nP8P3+9veLQzTZ+1puHDkkGP046dP3wIAPCal/VcmtjF8oLFy1c2mUprNSZ3RDcaDnGFIUd06Aolh3G0LadoD89JXrKzRPdXP3Za6v1NWl2qmCvhdVmr7t9chXt3nZeLd1bQsx49FGU74KRmDbo3/uSSFI8bVwFKSd2HSmOjMf5F9tO3IUawdQk2nRkxCbXJE633byaGyuadHdh/KBxe+I+DCousgNi8yA/mtj54rwgJLniJk0SrlquCs5pDZYGIXZbb89ed7QdlJ9U621vl+1EDjdp+/ZeiU2s9cjNrct5+D3BzX+LR8VGgWreidJ17tebrUxXj8bJJGgyVzN3Xi3RRPbiarjauXNrOQjRODdWlerwvXHAUGqoCOufGXlq1PcPj8yMfybD51y6O3xz/d1OvnccUro2ddjEdClVe/uQz8JR/97crU+IFRMO6trq+Lr6j35XVfYy3rk/bGTzDwU4cdCFDVrDT/dfauNGfGCG1GJRVG62P5/WbBDZP7dbONa1520/P7McQT+n32D//MOs9PUTxZKE9A22N33gSppGV9+ovLyMzBknFZlUEnx+EIk6vaowg6STqjxNKi/94AOJqMvrpgysaHM1rXpcBD2Td12vHPzF14Ls5R7jMpk0bLCSHFwzxN3dLX1XVV22+a6H5uMkS8LontP3s/jv+lsa+C27QGoKD/eP7Tp7ht+MRdK4blVh+RVeVdwrZv2Hv8YuBH6rvZNQwZV6z51YbosLWZR4qU943nZSRvepXjyiktgfle5jR9SfY3enopJ2mQtXFQT27bv6iHXs0fbhfarHFvNl+GLy7YdzWzUjjYOnpzc9vW0LPYr4UUVk4kRSBKG8iINALStsNTyNFBUT0qmSpZ2mFxeTVMLvP6ScpeanVQske8SIbOJym93CqcxMP7FHxqp142JHz5y0yy3kZCxNsU1uezJsQZfY9KguOZWCO9I2lCO7bghiWXi/eSnxBfMj1qTNz+hc6NRk+TSMGia0aDIDHf72YcMyAnoGZIpui1tcQa9e5VZoWhBts6bae5/BYP6V1fpKpnmq+FdhzBocS7cnzJiR7pkXYfe0f6e6z9RqJtL3BSUlQ3AhoMFmt1MHhdv87I124Tvzd4JdO/XjhqbFgY7AxU0NH0/VFog5oWKB0W7Xfi98ZxpSp3o5h5i+N01Zu8bRD1m7dvL8NX/vcptbxL+bjvUtbIYv/nOQykrScyZxUOUgMeRAPRuurMpGX/3Js9ZPuVIwderZzb/jhQt43G/0N/Xxv70pp4ysFf+VIqUfxLXi/yf9T/4vBsIPJw9I3yBuXOHBpS8m9ekUkOc3y2Ptx/CrGsjSAgYW/5JinHfecDhv8f0M/6Zkxl1GOGZt/B7A/8oW6usuOE6baW6bT3bSDlAb6Bzu/3gwJ0GdYsHt6zSp4cPvaFjZhHzuY3Yzj2fIGQzc/eAGvUXtBIcYaCENH++j7ev616i270nLaeCEw5zS8OcRSsSaE1zMX9qhwJpeWqo8E35Vi799T3mry6xwk/OphvoH9moC/F11ah7F7pKqypvd/JumPdoib4yCOuAACLof8Fd1nQEtc3LNtZ0/7q45xBw8EIAp1GhvsPxgzENEBFZBjWQsoBscNxZhxHhQEKYB0BULDphAxgnMQYcbmELGBwbLd2MeFnyLVZAJxQIKiTGmiI4rxMGrAe5AsGjDMKZSGjVu4HX/AuVzbqJJd/4fGItb2XV4lWz+hBJMHxF2qW6ck0waKtjH4d4gz4lVhlKIXZg4V70sFjIvMoypqA3waoA7e3nBRJuSGFMpT+4GqT//Bcrn3JTs97bkHxiLPnP9b8J+axifoqy137modqluOOFJZu5qqGA+hAe5biCmyh8shdiFkhZB9WIhjifrinB8t3hNnAMgQH1E7w0hFVXTDdOyHdfzz+/cvXf/wcNHj3UmJFhhOXeW5WWc+EB6KvNW8WgLaorcJn6L515ZWczWD8SsLVW1/N6Gm/YGC91uNKovIAcHQWTGOXFRWx9YylGMJLdUqkGDiGOvqqhuyF+PN5x0uZdwirJ+W8cfsscxWKIA6kVY2mYgstX/+oGY0BXz2JJ43u1hgI4lbz11nL+nDqvK6rsrKFN3vvJc+IpaDQ==') format('woff2'), 5 | url('iconfont.woff?t=1592274269382') format('woff'), 6 | url('iconfont.ttf?t=1592274269382') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ 7 | url('iconfont.svg?t=1592274269382#iconfont') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | .iconfont { 11 | font-family: "iconfont" !important; 12 | font-size: 16px; 13 | font-style: normal; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | .icon-shipin:before { 19 | content: "\e669"; 20 | } 21 | 22 | .icon-shanchu-fangkuang:before { 23 | content: "\e791"; 24 | } 25 | 26 | .icon-zengjiashuzi:before { 27 | content: "\e641"; 28 | } 29 | 30 | .icon-jianshanchu-xianxingfangkuang:before { 31 | content: "\e75a"; 32 | } 33 | 34 | .icon-zengjia2:before { 35 | content: "\e689"; 36 | } 37 | 38 | .icon-yichu:before { 39 | content: "\e656"; 40 | } 41 | 42 | .icon-zengjia:before { 43 | content: "\e668"; 44 | } 45 | 46 | .icon-delete-br:before { 47 | content: "\e63d"; 48 | } 49 | 50 | .icon-loading-solid:before { 51 | content: "\e647"; 52 | } 53 | 54 | .icon-fasongshibai:before { 55 | content: "\e62c"; 56 | } 57 | 58 | .icon-pengyou1:before { 59 | content: "\e631"; 60 | } 61 | 62 | .icon-yaoqinghaoyou:before { 63 | content: "\e61c"; 64 | } 65 | 66 | .icon-jiahaoyou:before { 67 | content: "\e603"; 68 | } 69 | 70 | .icon-ai-video:before { 71 | content: "\e66b"; 72 | } 73 | 74 | .icon-biaoqing:before { 75 | content: "\e666"; 76 | } 77 | 78 | .icon-dkw_xiaoxi:before { 79 | content: "\e606"; 80 | } 81 | 82 | .icon-dianhua:before { 83 | content: "\e729"; 84 | } 85 | 86 | .icon-pengyou:before { 87 | content: "\e61a"; 88 | } 89 | 90 | .icon-sousuo:before { 91 | content: "\e659"; 92 | } 93 | 94 | .icon-tuichu:before { 95 | content: "\e61f"; 96 | } 97 | 98 | .icon-tupian:before { 99 | content: "\e623"; 100 | } 101 | 102 | .icon-wenjian:before { 103 | content: "\e61b"; 104 | } 105 | 106 | .icon-guaduan:before { 107 | content: "\e640"; 108 | } 109 | 110 | -------------------------------------------------------------------------------- /src/components/chatlist/chatlist.vue: -------------------------------------------------------------------------------- 1 | 2 | 27 | 28 | 114 | 115 | 197 | -------------------------------------------------------------------------------- /src/components/menu/relayMessage.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 196 | 197 | --------------------------------------------------------------------------------