diff --git a/im/.env b/im/.env
new file mode 100644
index 00000000..59d0738d
--- /dev/null
+++ b/im/.env
@@ -0,0 +1,6 @@
+NODE_ENV=production
+VUE_APP_PREVIEW=false
+VUE_APP_API_BASE_URL=https://im-api.pickmall.cn
+VUE_APP_WEB_SOCKET_URL=wss://im-api.pickmall.cn/lili/webSocket
+VUE_APP_COMMON=https://common-api.pickmall.cn
+VUE_APP_WEBSITE_NAME="LiLi IM"
diff --git a/im/.env.development b/im/.env.development
new file mode 100644
index 00000000..d6973c53
--- /dev/null
+++ b/im/.env.development
@@ -0,0 +1,7 @@
+NODE_ENV=development
+VUE_APP_PREVIEW=true
+VUE_APP_API_BASE_URL=http://192.168.0.113:8885
+VUE_APP_WEB_SOCKET_URL=ws://192.168.0.113:8885/lili/webSocket
+VUE_APP_COMMON=http://192.168.0.113:8890
+VUE_APP_PC_URL="http://192.168.0.113:10001"
+VUE_APP_WEBSITE_NAME="LiLi IM"
diff --git a/im/.gitignore b/im/.gitignore
new file mode 100644
index 00000000..403adbc1
--- /dev/null
+++ b/im/.gitignore
@@ -0,0 +1,23 @@
+.DS_Store
+node_modules
+/dist
+
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/im/LICENSE b/im/LICENSE
new file mode 100644
index 00000000..dbcbe685
--- /dev/null
+++ b/im/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 YuanDong
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/im/README.md b/im/README.md
new file mode 100644
index 00000000..6ac928fc
--- /dev/null
+++ b/im/README.md
@@ -0,0 +1,71 @@
+# Springboot-websocket
+
+过渡阶段IM
+
+前端仓库:https://gitee.com/beijing_hongye_huicheng/im
+
+后端仓库:https://gitee.com/beijing_hongye_huicheng/springboot-websocket
+
+部署方式:
+1、导入数据库,配置resource目录下application.yml的数据库以及redis配置文件。
+
+2、maven打包
+
+3、启动jar包即可。
+
+4、前端程序运行,测试环境需配置根目录 .env.development文件,正式环境需配置 .env 文件。
+
+# 参考项目
+项目前端参考:https://gitee.com/gzydong/LumenIM.git 功能更丰富,大家可以去学习一波
+
+
+# IM体验
+浏览器1打开地址:http://127.0.0.1:8000/message?token=eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyQ29udGV4dCI6IntcInVzZXJuYW1lXCI6XCIxMzAxMTExMTExMVwiLFwibmlja05hbWVcIjpcIuW8oOS4ieWTiOWTiOWTiFwiLFwiZmFjZVwiOlwiaHR0cHM6Ly9saWxpc2hvcC1vc3Mub3NzLWNuLWJlaWppbmcuYWxpeXVuY3MuY29tLzQ1ZDUyOGYwMjRjZTQyMzI4NzYxNjFhZmQxN2Y0ZWExLmpwZ1wiLFwiaWRcIjpcIjEzNzY0MTc2ODQxNDAzMjY5MTJcIixcImxvbmdUZXJtXCI6ZmFsc2UsXCJyb2xlXCI6XCJTVE9SRVwiLFwic3RvcmVJZFwiOlwiMTM3NjQzMzU2NTI0NzQ3MTYxNlwiLFwic3RvcmVOYW1lXCI6XCLlrrblrrbkuZBcIixcImlzU3VwZXJcIjpmYWxzZX0iLCJzdWIiOiIxMzAxMTExMTExMSIsImV4cCI6MTY0NTQ2OTk1M30.fXpCZ2YiFYqACpmxVvKjIpXovfPRDJavHjftpQzdEps
+
+浏览器2打开地址:http://127.0.0.1:8000/message?token=eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyQ29udGV4dCI6IntcInVzZXJuYW1lXCI6XCIxMzAxMTExMTExMVwiLFwibmlja05hbWVcIjpcIuW8oOS4ieWTiOWTiOWTiFwiLFwiZmFjZVwiOlwiaHR0cHM6Ly9saWxpc2hvcC1vc3Mub3NzLWNuLWJlaWppbmcuYWxpeXVuY3MuY29tLzk2N2UzMzU1Yzg0NTRiNGFhMTk1N2M1NTQ5ZTZiNzIwLnBuZ1wiLFwiaWRcIjpcIjEzNzY0MTc2ODQxNDAzMjY5MTJcIixcImxvbmdUZXJtXCI6ZmFsc2UsXCJyb2xlXCI6XCJNRU1CRVJcIixcImlzU3VwZXJcIjpmYWxzZX0iLCJzdWIiOiIxMzAxMTExMTExMSIsImV4cCI6MTY0NTQ2OTAyNn0.GEkGpKRKF3rqzHRhaPCFilPpWe37cIXTT4KnwWR4Bt0&id=1376433565247471616
+
+即可进行聊天。
+
+# NGINX配置事例
+线上NGINX配置在测试阶段出现问题,这里吧相关的配置贴出来供大家参考。
+````
+ server {
+ listen 443 ssl;
+ ssl_certificate "/etc/nginx/ssl/pickmall.cn.pem";
+ ssl_certificate_key "/etc/nginx/ssl/pickmall.cn.key";
+ ssl_session_cache shared:SSL:1m;
+ ssl_session_timeout 10m;
+ ssl_ciphers HIGH:!aNULL:!MD5;
+ ssl_prefer_server_ciphers on;
+ include /etc/nginx/default.d/*.conf;
+
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+
+ server_name im-api.pickmall.cn;
+ location / {
+ proxy_pass http://127.0.0.1:8088;
+ }
+ }
+
+ server {
+ listen 443 ssl;
+ ssl_certificate "/etc/nginx/ssl/pickmall.cn.pem";
+ ssl_certificate_key "/etc/nginx/ssl/pickmall.cn.key";
+ ssl_session_cache shared:SSL:1m;
+ ssl_session_timeout 10m;
+ ssl_ciphers HIGH:!aNULL:!MD5;
+ ssl_prefer_server_ciphers on;
+ include /etc/nginx/default.d/*.conf;
+
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header REMOTE-HOST $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+ server_name im.pickmall.cn;
+ try_files $uri $uri/ /index.html;
+ root /home/im/im/dist;
+ }
+````
\ No newline at end of file
diff --git a/im/babel.config.js b/im/babel.config.js
new file mode 100644
index 00000000..74f3c443
--- /dev/null
+++ b/im/babel.config.js
@@ -0,0 +1,51 @@
+module.exports = {
+ presets: [
+ '@vue/cli-plugin-babel/preset'
+ ],
+ plugins: [
+ [
+ "import",
+ {
+ "libraryName": "element-ui",
+ "styleLibraryName": "theme-chalk"
+ }
+ ],
+ [
+ "prismjs",
+ {
+ "languages": [
+ "html",
+ "css",
+ "less",
+ "javascript",
+ "typescript",
+ "json",
+ "xml",
+ "bash",
+ "nginx",
+ "sql",
+ "docker",
+ "php",
+ "java",
+ "go",
+ "python",
+ "ruby",
+ "rust",
+ "objectivec",
+ "c",
+ "csharp",
+ "cpp",
+ "lua",
+ "shell",
+ "vim",
+ "yaml",
+ "yml",
+ "md",
+ "erlang",
+ "ini"
+ ],
+ "theme": "okaidia"
+ }
+ ]
+ ]
+}
\ No newline at end of file
diff --git a/im/jsconfig.json b/im/jsconfig.json
new file mode 100644
index 00000000..09ea7562
--- /dev/null
+++ b/im/jsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "target": "es6",
+ "baseUrl": ".",
+ "paths": {
+ "@/*": [
+ "src/*"
+ ]
+ }
+ },
+ "exclude": [
+ "node_modules",
+ "dist"
+ ],
+ "include": [
+ "src/**/*"
+ ]
+}
\ No newline at end of file
diff --git a/im/package.json b/im/package.json
new file mode 100644
index 00000000..1393f7e6
--- /dev/null
+++ b/im/package.json
@@ -0,0 +1,66 @@
+{
+ "name": "LiLi-IM",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "vue-cli-service serve",
+ "serve": "vue-cli-service serve",
+ "build": "vue-cli-service build",
+ "lint": "vue-cli-service lint"
+ },
+ "dependencies": {
+ "axios": "^0.21.4",
+ "babel-plugin-prismjs": "^2.0.1",
+ "core-js": "^3.6.5",
+ "element-ui": "^2.14.1",
+ "js-audio-recorder": "^1.0.6",
+ "js-base64": "^2.5.1",
+ "mavon-editor": "^2.10.4",
+ "nprogress": "^0.2.0",
+ "prismjs": "^1.29.0",
+ "svg-sprite-loader": "^5.0.0",
+ "vue": "^2.6.11",
+ "vue-contextmenujs": "^1.3.13",
+ "vue-cropper": "^0.5.5",
+ "vue-prism-editor": "^0.5.1",
+ "vue-router": "^3.4.9",
+ "vuex": "^3.5.1"
+ },
+ "devDependencies": {
+ "@vue/cli-plugin-babel": "^5.0.8",
+ "@vue/cli-plugin-eslint": "^5.0.8",
+ "@vue/cli-service": "^5.0.8",
+ "babel-eslint": "^10.1.0",
+ "babel-plugin-import": "^1.13.1",
+ "compression-webpack-plugin": "^5.0.0",
+ "eslint": "^8.28.0",
+ "eslint-plugin-vue": "^6.2.2",
+ "less": "^3.0.4",
+ "less-loader": "^5.0.0",
+ "style-resources-loader": "^1.4.1",
+ "vue-cli-plugin-style-resources-loader": "^0.1.4",
+ "vue-svg-component-runtime": "^1.0.1",
+ "vue-svg-icon-loader": "^2.1.1",
+ "vue-template-compiler": "^2.6.11",
+ "webpack": "^5.75.0"
+ },
+ "eslintConfig": {
+ "root": true,
+ "env": {
+ "node": true
+ },
+ "extends": [
+ "plugin:vue/essential",
+ "eslint:recommended"
+ ],
+ "parserOptions": {
+ "parser": "babel-eslint"
+ },
+ "rules": {}
+ },
+ "browserslist": [
+ "> 1%",
+ "last 2 versions",
+ "not dead"
+ ]
+}
diff --git a/im/public/favicon.ico b/im/public/favicon.ico
new file mode 100644
index 00000000..5d8e506a
Binary files /dev/null and b/im/public/favicon.ico differ
diff --git a/im/public/index.html b/im/public/index.html
new file mode 100644
index 00000000..1dd7f3d8
--- /dev/null
+++ b/im/public/index.html
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+ LiLi IM
+
+
+ <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
+
+ <% } %>
+
+
+
+
+
+
+
+ <% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
+
+ <% } %>
+
+
+
diff --git a/im/src/App.vue b/im/src/App.vue
new file mode 100644
index 00000000..89b704ca
--- /dev/null
+++ b/im/src/App.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
diff --git a/im/src/api/chat.js b/im/src/api/chat.js
new file mode 100644
index 00000000..25d077c0
--- /dev/null
+++ b/im/src/api/chat.js
@@ -0,0 +1,109 @@
+import { post, get, upload, del } from "@/utils/request";
+
+// 获取聊天列表服务接口
+export const ServeGetTalkList = (data) => {
+ return get("/im/talk/list", data);
+};
+
+// 获取聊天列表服务接口
+export const ServeGetStoreTalkList = (data) => {
+ return get("/im/talk/store/list", data);
+};
+
+// 聊天列表创建服务接口
+export const ServeCreateTalkList = (id) => {
+ return get(`/im/talk/user/${id}`);
+};
+
+// 删除聊天列表服务接口
+export const ServeDeleteTalkList = (data) => {
+ return del("/im/talk", data);
+};
+
+// 对话列表置顶服务接口
+export const ServeTopTalkList = (data) => {
+ return post("/im/talk/top", data);
+};
+
+// 清除聊天消息未读数服务接口
+export const ServeClearTalkUnreadNum = (data) => {
+ return post("/im/talk/update-unread-num", data);
+};
+
+// 获取聊天记录服务接口
+export const ServeTalkRecords = (data) => {
+ return get("/im/message", data);
+};
+
+// 获取转发会话记录详情列表服务接口
+export const ServeGetForwardRecords = (data) => {
+ return get("/im/talk/get-forward-records", data);
+};
+
+// 对话列表置顶服务接口
+export const ServeSetNotDisturb = (data) => {
+ return post("/im/talk/disturb", data);
+};
+
+// 查找用户聊天记录服务接口
+export const ServeFindTalkRecords = (data) => {
+ return get("/im/talk/find-chat-records", data);
+};
+
+// 搜索用户聊天记录服务接口
+export const ServeSearchTalkRecords = (data) => {
+ return get("/im/talk/search-chat-records", data);
+};
+
+export const ServeGetRecordsContext = (data) => {
+ return get("/im/talk/get-records-context", data);
+};
+
+// 发送代码块消息服务接口
+export const ServeSendTalkCodeBlock = (data) => {
+ return post("/im/talk/message/code", data);
+};
+
+// 发送聊天文件服务接口
+export const ServeSendTalkFile = (data) => {
+ return post("/im/talk/message/file", data);
+};
+
+// 发送聊天图片服务接口
+export const ServeSendTalkImage = (data) => {
+ return upload("/common/common/upload/file", data);
+};
+
+// 发送表情包服务接口
+export const ServeSendEmoticon = (data) => {
+ return post("/im/talk/message/emoticon", data);
+};
+
+// 转发消息服务接口
+export const ServeForwardRecords = (data) => {
+ return post("/im/talk/message/forward", data);
+};
+
+// 撤回消息服务接口
+export const ServeRevokeRecords = (data) => {
+ return post("/im/talk/message/revoke", data);
+};
+
+// 删除消息服务接口
+export const ServeRemoveRecords = (data) => {
+ return post("/im/talk/message/delete", data);
+};
+
+// 收藏表情包服务接口
+export const ServeCollectEmoticon = (data) => {
+ return post("/im/talk/message/collect", data);
+};
+
+//投票
+export const ServeSendVote = (data) => {
+ return post("/im/talk/message/vote", data);
+};
+
+export const ServeConfirmVoteHandle = (data) => {
+ return post("/im/talk/message/vote/handle", data);
+};
diff --git a/im/src/api/contacts.js b/im/src/api/contacts.js
new file mode 100644
index 00000000..653242d3
--- /dev/null
+++ b/im/src/api/contacts.js
@@ -0,0 +1,45 @@
+import { post, get } from '@/utils/request'
+
+// 获取好友列表服务接口
+export const ServeGetContacts = data => {
+ return get('/contacts/list', data)
+}
+
+// 解除好友关系服务接口
+export const ServeDeleteContact = data => {
+ return post('/contacts/delete', data)
+}
+
+// 修改好友备注服务接口
+export const ServeEditContactRemark = data => {
+ return post('/contacts/edit-remark', data)
+}
+
+// 搜索联系人
+export const ServeSearchContact = data => {
+ return get('/contacts/search', data)
+}
+
+// 好友申请服务接口
+export const ServeCreateContact = data => {
+ return post('/contacts/apply/create', data)
+}
+
+// 查询好友申请服务接口
+export const ServeGetContactApplyRecords = data => {
+ return get('/contacts/apply/records', data)
+}
+
+// 处理好友申请服务接口
+export const ServeApplyAccept = data => {
+ return post('/contacts/apply/accept', data)
+}
+
+export const ServeApplyDecline = data => {
+ return post('/contacts/apply/decline', data)
+}
+
+// 查询好友申请未读数量服务接口
+export const ServeFindFriendApplyNum = () => {
+ return get('/contacts/apply-unread-num')
+}
diff --git a/im/src/api/emoticon.js b/im/src/api/emoticon.js
new file mode 100644
index 00000000..d287fe30
--- /dev/null
+++ b/im/src/api/emoticon.js
@@ -0,0 +1,26 @@
+import { post, get, upload } from '@/utils/request'
+
+// 查询用户表情包服务接口
+export const ServeFindUserEmoticon = () => {
+ return get('/emoticon/list')
+}
+
+// 查询系统表情包服务接口
+export const ServeFindSysEmoticon = () => {
+ return get('/emoticon/system')
+}
+
+// 设置用户表情包服务接口
+export const ServeSetUserEmoticon = data => {
+ return post('/emoticon/set-user-emoticon', data)
+}
+
+// 移除收藏表情包服务接口
+export const ServeDelCollectEmoticon = data => {
+ return post('/emoticon/del-collect-emoticon', data)
+}
+
+// 上传表情包服务接口
+export const ServeUploadEmoticon = data => {
+ return upload('/emoticon/upload-emoticon', data)
+}
diff --git a/im/src/api/goods.js b/im/src/api/goods.js
new file mode 100644
index 00000000..0ddde395
--- /dev/null
+++ b/im/src/api/goods.js
@@ -0,0 +1,5 @@
+import { post, get, upload, del } from "@/utils/request";
+
+export const ServeGetGoodsDetail = (data) => {
+ return get(`/im/goods/goods/sku/${data.goodsId}/${data.skuId}`);
+ };
diff --git a/im/src/api/group.js b/im/src/api/group.js
new file mode 100644
index 00000000..3e271672
--- /dev/null
+++ b/im/src/api/group.js
@@ -0,0 +1,66 @@
+import { post, get } from '@/utils/request'
+
+// 查询用户群聊服务接口
+export const ServeGetGroups = () => {
+ return get('/group/list')
+}
+
+// 获取群信息服务接口
+export const ServeGroupDetail = data => {
+ return get('/group/detail', data)
+}
+
+// 创建群聊服务接口
+export const ServeCreateGroup = data => {
+ return post('/group/create', data)
+}
+
+// 修改群信息
+export const ServeEditGroup = data => {
+ return post('/group/edit', data)
+}
+
+// 邀请好友加入群聊服务接口
+export const ServeInviteGroup = data => {
+ return post('/group/invite', data)
+}
+
+// 移除群聊成员服务接口
+export const ServeRemoveMembersGroup = data => {
+ return post('/group/remove-members', data)
+}
+
+// 管理员解散群聊服务接口
+export const ServeDismissGroup = data => {
+ return post('/group/dismiss', data)
+}
+
+// 用户退出群聊服务接口
+export const ServeSecedeGroup = data => {
+ return post('/group/secede', data)
+}
+
+// 修改群聊名片服务接口
+export const ServeUpdateGroupCard = data => {
+ return post('/group/set-group-card', data)
+}
+
+// 获取用户可邀请加入群组的好友列表
+export const ServeGetInviteFriends = data => {
+ return get('/group/invite-friends', data)
+}
+
+// 获取群组成员列表
+export const ServeGetGroupMembers = data => {
+ return get('/group/members', data)
+}
+
+// 获取群组公告列表
+export const ServeGetGroupNotices = data => {
+ return get('/group/notices', data)
+}
+
+// 编辑群公告
+export const ServeEditGroupNotice = data => {
+ return post('/group/edit-notice', data)
+}
diff --git a/im/src/api/upload.js b/im/src/api/upload.js
new file mode 100644
index 00000000..afbbb580
--- /dev/null
+++ b/im/src/api/upload.js
@@ -0,0 +1,16 @@
+import { post, get, upload } from '@/utils/request'
+
+// 上传头像裁剪图片服务接口
+export const ServeUploadFileStream = data => {
+ return post('/upload/file-stream', data)
+}
+
+// 查询大文件拆分信息服务接口
+export const ServeFindFileSplitInfo = (data = {}) => {
+ return get('/upload/get-file-split-info', data)
+}
+
+// 文件拆分上传服务接口
+export const ServeFileSubareaUpload = (data = {}, options = {}) => {
+ return upload('/upload/file-subarea-upload', data, options)
+}
diff --git a/im/src/api/user.js b/im/src/api/user.js
new file mode 100644
index 00000000..061b9513
--- /dev/null
+++ b/im/src/api/user.js
@@ -0,0 +1,26 @@
+import { get } from "@/utils/request";
+
+// 获取用户相关设置信息
+export const ServeGetUserSetting = () => {
+ return get("/im/user");
+};
+
+// 获取店铺相关设置信息
+export const ServeGetStoreSetting = () => {
+ return get("/im/user/store");
+};
+
+// 获取用户相关设置信息
+export const ServeGetUserDetail = (memberId) => {
+ return get(`/im/user/${memberId}`);
+};
+
+// 获取店铺相关设置信息
+export const ServeGetStoreDetail = (storeId) => {
+ return get(`/im/user/store/${storeId}`);
+};
+
+// 获取店铺相关设置信息
+export const ServeGetFootPrint = (params) => {
+ return get(`/im/user/history`,params);
+};
diff --git a/im/src/assets/css/global.less b/im/src/assets/css/global.less
new file mode 100644
index 00000000..c1353b95
--- /dev/null
+++ b/im/src/assets/css/global.less
@@ -0,0 +1,238 @@
+@import './reset.css';
+
+.no-select {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.no-padding {
+ padding: 0;
+}
+
+.avatar-box {
+ height: 35px;
+ width: 35px;
+ flex-shrink: 0;
+ background-color: #508afe;
+ border-radius: 50%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 14px;
+ color: white;
+ user-select: none;
+ transition: ease 1s;
+ position: relative;
+ overflow: hidden;
+
+ img {
+ width: 100%;
+ height: 100%;
+ background-color: white;
+ border-radius: 3px;
+ }
+
+ .top-mask {
+ width: 100%;
+ height: 100%;
+ background-color: rgba(22, 25, 29, 0.6);
+ position: absolute;
+ top: 0;
+ left: 0;
+ color: white;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+ }
+
+ &:hover .top-mask {
+ display: flex;
+ }
+}
+
+.no-border {
+ border: 0;
+}
+
+.pointer {
+ cursor: pointer;
+}
+
+.border-radius0 {
+ border-radius: 0;
+}
+
+.full-height {
+ height: 100%;
+}
+
+.talk-height {
+ height: 80%;
+ width: 80%;
+}
+
+.ov-hidden {
+ overflow: hidden;
+}
+
+// 滚动条样式
+.lum-scrollbar {
+ &::-webkit-scrollbar {
+ width: 3px;
+ background-color: #e4e4e5;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ border-radius: 3px;
+ background-color: #c0bebc;
+ }
+}
+
+.larkc-tag {
+ font-size: 12px;
+ font-weight: 400;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 6px;
+ height: 20px;
+ border-radius: 2px;
+ cursor: default;
+ user-select: none;
+ background-color: #dee0e3;
+ transform: scale(0.83);
+ transform-origin: left;
+ flex-shrink: 0;
+}
+
+.flex-center {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+// 自定义 dialog 样式
+// lum-dialog -- start
+.lum-dialog-mask {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 999;
+ width: 100%;
+ height: 100%;
+ background-color: @maskBagColor;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .lum-dialog-box {
+ min-width: 200px;
+ min-height: 200px;
+ background-color: white;
+ border-radius: 3px;
+ overflow: hidden;
+ box-shadow: 0 2px 8px 0 rgba(31, 35, 41, 0.2);
+ margin: 0 10px;
+
+ .container {
+ height: 100%;
+ }
+
+ .header {
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-bottom: 1px solid #f5eeee;
+
+ >p:first-child {
+ text-indent: 20px;
+ }
+
+ .tools {
+ height: 100%;
+ width: 100px;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ padding-right: 20px;
+
+ i {
+ font-size: 20px;
+ cursor: pointer;
+ margin-left: 8px;
+ }
+ }
+ }
+
+ .main {
+ /deep/.el-input__inner {
+ border-radius: 1px !important;
+ }
+
+ .submit-btn {
+ border-radius: 2px;
+ font-weight: 400;
+ }
+ }
+ }
+}
+
+// lum-dialog -- end
+
+.talk-notify {
+ .el-notification__title {
+ font-weight: 300;
+ font-size: 16px;
+ color: #f44336;
+ }
+
+ p {
+ max-height: 65px;
+ overflow: hidden;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ text-indent: -7px;
+ word-break: break-all;
+ }
+}
+
+.im-notify {
+ padding: 14px 26px 8px 0px;
+
+ .el-notification__closeBtn {
+ position: absolute;
+ top: 12px;
+ }
+
+ .el-notification__group {
+ margin-left: 5px;
+ }
+}
+
+.flex {
+ display: flex;
+}
+
+.flex-2 {
+ flex: 2;
+}
+
+.flex-8 {
+ flex: 8;
+}
+
+.flex-4 {
+ flex: 4;
+}
+
+.flex-10 {
+ flex: 10;
+}
\ No newline at end of file
diff --git a/im/src/assets/css/markdown.css b/im/src/assets/css/markdown.css
new file mode 100644
index 00000000..02615f34
--- /dev/null
+++ b/im/src/assets/css/markdown.css
@@ -0,0 +1,991 @@
+.markdown-body .octicon {
+ display: inline-block;
+ fill: currentColor;
+ vertical-align: text-bottom;
+}
+
+.markdown-body .anchor {
+ float: left;
+ line-height: 1;
+ margin-left: -20px;
+ padding-right: 4px;
+}
+
+.markdown-body .anchor:focus {
+ outline: none;
+}
+
+.markdown-body h1 .octicon-link,
+.markdown-body h2 .octicon-link,
+.markdown-body h3 .octicon-link,
+.markdown-body h4 .octicon-link,
+.markdown-body h5 .octicon-link,
+.markdown-body h6 .octicon-link {
+ color: #1b1f23;
+ vertical-align: middle;
+ visibility: hidden;
+}
+
+.markdown-body h1:hover .anchor,
+.markdown-body h2:hover .anchor,
+.markdown-body h3:hover .anchor,
+.markdown-body h4:hover .anchor,
+.markdown-body h5:hover .anchor,
+.markdown-body h6:hover .anchor {
+ text-decoration: none;
+}
+
+.markdown-body h1:hover .anchor .octicon-link,
+.markdown-body h2:hover .anchor .octicon-link,
+.markdown-body h3:hover .anchor .octicon-link,
+.markdown-body h4:hover .anchor .octicon-link,
+.markdown-body h5:hover .anchor .octicon-link,
+.markdown-body h6:hover .anchor .octicon-link {
+ visibility: visible;
+}
+
+.markdown-body h1:hover .anchor .octicon-link:before,
+.markdown-body h2:hover .anchor .octicon-link:before,
+.markdown-body h3:hover .anchor .octicon-link:before,
+.markdown-body h4:hover .anchor .octicon-link:before,
+.markdown-body h5:hover .anchor .octicon-link:before,
+.markdown-body h6:hover .anchor .octicon-link:before {
+ width: 16px;
+ height: 16px;
+ content: ' ';
+ display: inline-block;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath fill-rule='evenodd' d='M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z'%3E%3C/path%3E%3C/svg%3E");
+}
+
+.markdown-body {
+ -ms-text-size-adjust: 100%;
+ -webkit-text-size-adjust: 100%;
+ font-size: 16px;
+ word-wrap: break-word;
+ font-family: Content-font, Roboto, sans-serif;
+ font-weight: 400;
+ color: #3B454E;
+ line-height: 1.625;
+}
+
+.markdown-body details {
+ display: block;
+}
+
+.markdown-body summary {
+ display: list-item;
+}
+
+.markdown-body a {
+ background-color: initial;
+}
+
+.markdown-body a:active,
+.markdown-body a:hover {
+ outline-width: 0;
+}
+
+.markdown-body strong {
+ font-weight: inherit;
+ font-weight: bolder;
+}
+
+.markdown-body h1 {
+ font-size: 2em;
+ margin: .67em 0;
+}
+
+.markdown-body img {
+ border-style: none;
+}
+
+.markdown-body code,
+.markdown-body kbd,
+.markdown-body pre {
+ font-family: monospace, monospace;
+ font-size: 1em;
+}
+
+.markdown-body hr {
+ box-sizing: initial;
+ height: 0;
+ overflow: visible;
+}
+
+.markdown-body input {
+ font: inherit;
+ margin: 0;
+}
+
+.markdown-body input {
+ overflow: visible;
+}
+
+.markdown-body [type=checkbox] {
+ box-sizing: border-box;
+ padding: 0;
+}
+
+.markdown-body * {
+ box-sizing: border-box;
+}
+
+.markdown-body input {
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+}
+
+.markdown-body a {
+ color: #0366d6;
+ text-decoration: none;
+}
+
+.markdown-body a:hover {
+ text-decoration: underline;
+}
+
+.markdown-body strong {
+ font-weight: 600;
+}
+
+.markdown-body hr {
+ height: 0;
+ margin: 15px 0;
+ overflow: hidden;
+ background: transparent;
+ border: 0;
+ border-bottom: 1px solid #dfe2e5;
+}
+
+.markdown-body hr:after,
+.markdown-body hr:before {
+ display: table;
+ content: "";
+}
+
+.markdown-body hr:after {
+ clear: both;
+}
+
+.markdown-body table {
+ border-spacing: 0;
+ border-collapse: collapse;
+}
+
+.markdown-body td,
+.markdown-body th {
+ padding: 0;
+}
+
+.markdown-body details summary {
+ cursor: pointer;
+}
+
+.markdown-body kbd {
+ display: inline-block;
+ padding: 3px 5px;
+ font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
+ line-height: 10px;
+ color: #444d56;
+ vertical-align: middle;
+ background-color: #fafbfc;
+ border: 1px solid #d1d5da;
+ border-radius: 3px;
+ box-shadow: inset 0 -1px 0 #d1d5da;
+}
+
+.markdown-body h1,
+.markdown-body h2,
+.markdown-body h3,
+.markdown-body h4,
+.markdown-body h5,
+.markdown-body h6 {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.markdown-body h1 {
+ font-size: 32px;
+}
+
+.markdown-body h1,
+.markdown-body h2 {
+ font-weight: 600;
+}
+
+.markdown-body h2 {
+ font-size: 24px;
+}
+
+.markdown-body h3 {
+ font-size: 20px;
+}
+
+.markdown-body h3,
+.markdown-body h4 {
+ font-weight: 600;
+}
+
+.markdown-body h4 {
+ font-size: 16px;
+}
+
+.markdown-body h5 {
+ font-size: 14px;
+}
+
+.markdown-body h5,
+.markdown-body h6 {
+ font-weight: 600;
+}
+
+.markdown-body h6 {
+ font-size: 12px;
+}
+
+.markdown-body p {
+ margin-top: 0;
+ margin-bottom: 10px;
+}
+
+.markdown-body blockquote {
+ margin: 20px 0 !important;
+ background-color: #f5f8fc;
+ padding: 1rem;
+ color: #8796a8;
+ border-left: none;
+}
+
+.markdown-body ol,
+.markdown-body ul {
+ padding-left: 0;
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.markdown-body ol ol,
+.markdown-body ul ol {
+ list-style-type: lower-roman;
+}
+
+.markdown-body ol ol ol,
+.markdown-body ol ul ol,
+.markdown-body ul ol ol,
+.markdown-body ul ul ol {
+ list-style-type: lower-alpha;
+}
+
+.markdown-body dd {
+ margin-left: 0;
+}
+
+.markdown-body code,
+.markdown-body pre {
+ font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
+ font-size: 12px;
+}
+
+.markdown-body pre {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.markdown-body input::-webkit-inner-spin-button,
+.markdown-body input::-webkit-outer-spin-button {
+ margin: 0;
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+.markdown-body :checked+.radio-label {
+ position: relative;
+ z-index: 1;
+ border-color: #0366d6;
+}
+
+.markdown-body .border {
+ border: 1px solid #e1e4e8 !important;
+}
+
+.markdown-body .border-0 {
+ border: 0 !important;
+}
+
+.markdown-body .border-bottom {
+ border-bottom: 1px solid #e1e4e8 !important;
+}
+
+.markdown-body .rounded-1 {
+ border-radius: 3px !important;
+}
+
+.markdown-body .bg-white {
+ background-color: #fff !important;
+}
+
+.markdown-body .bg-gray-light {
+ background-color: #fafbfc !important;
+}
+
+.markdown-body .text-gray-light {
+ color: #6a737d !important;
+}
+
+.markdown-body .mb-0 {
+ margin-bottom: 0 !important;
+}
+
+.markdown-body .my-2 {
+ margin-top: 8px !important;
+ margin-bottom: 8px !important;
+}
+
+.markdown-body .pl-0 {
+ padding-left: 0 !important;
+}
+
+.markdown-body .py-0 {
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+}
+
+.markdown-body .pl-1 {
+ padding-left: 4px !important;
+}
+
+.markdown-body .pl-2 {
+ padding-left: 8px !important;
+}
+
+.markdown-body .py-2 {
+ padding-top: 8px !important;
+ padding-bottom: 8px !important;
+}
+
+.markdown-body .pl-3,
+.markdown-body .px-3 {
+ padding-left: 16px !important;
+}
+
+.markdown-body .px-3 {
+ padding-right: 16px !important;
+}
+
+.markdown-body .pl-4 {
+ padding-left: 24px !important;
+}
+
+.markdown-body .pl-5 {
+ padding-left: 32px !important;
+}
+
+.markdown-body .pl-6 {
+ padding-left: 40px !important;
+}
+
+.markdown-body .f6 {
+ font-size: 12px !important;
+}
+
+.markdown-body .lh-condensed {
+ line-height: 1.25 !important;
+}
+
+.markdown-body .text-bold {
+ font-weight: 600 !important;
+}
+
+.markdown-body .pl-c {
+ color: #6a737d;
+}
+
+.markdown-body .pl-c1,
+.markdown-body .pl-s .pl-v {
+ color: #005cc5;
+}
+
+.markdown-body .pl-e,
+.markdown-body .pl-en {
+ color: #6f42c1;
+}
+
+.markdown-body .pl-s .pl-s1,
+.markdown-body .pl-smi {
+ color: #24292e;
+}
+
+.markdown-body .pl-ent {
+ color: #22863a;
+}
+
+.markdown-body .pl-k {
+ color: #d73a49;
+}
+
+.markdown-body .pl-pds,
+.markdown-body .pl-s,
+.markdown-body .pl-s .pl-pse .pl-s1,
+.markdown-body .pl-sr,
+.markdown-body .pl-sr .pl-cce,
+.markdown-body .pl-sr .pl-sra,
+.markdown-body .pl-sr .pl-sre {
+ color: #032f62;
+}
+
+.markdown-body .pl-smw,
+.markdown-body .pl-v {
+ color: #e36209;
+}
+
+.markdown-body .pl-bu {
+ color: #b31d28;
+}
+
+.markdown-body .pl-ii {
+ color: #fafbfc;
+ background-color: #b31d28;
+}
+
+.markdown-body .pl-c2 {
+ color: #fafbfc;
+ background-color: #d73a49;
+}
+
+.markdown-body .pl-c2:before {
+ content: "^M";
+}
+
+.markdown-body .pl-sr .pl-cce {
+ font-weight: 700;
+ color: #22863a;
+}
+
+.markdown-body .pl-ml {
+ color: #735c0f;
+}
+
+.markdown-body .pl-mh,
+.markdown-body .pl-mh .pl-en,
+.markdown-body .pl-ms {
+ font-weight: 700;
+ color: #005cc5;
+}
+
+.markdown-body .pl-mi {
+ font-style: italic;
+ color: #24292e;
+}
+
+.markdown-body .pl-mb {
+ font-weight: 700;
+ color: #24292e;
+}
+
+.markdown-body .pl-md {
+ color: #b31d28;
+ background-color: #ffeef0;
+}
+
+.markdown-body .pl-mi1 {
+ color: #22863a;
+ background-color: #f0fff4;
+}
+
+.markdown-body .pl-mc {
+ color: #e36209;
+ background-color: #ffebda;
+}
+
+.markdown-body .pl-mi2 {
+ color: #f6f8fa;
+ background-color: #005cc5;
+}
+
+.markdown-body .pl-mdr {
+ font-weight: 700;
+ color: #6f42c1;
+}
+
+.markdown-body .pl-ba {
+ color: #586069;
+}
+
+.markdown-body .pl-sg {
+ color: #959da5;
+}
+
+.markdown-body .pl-corl {
+ text-decoration: underline;
+ color: #032f62;
+}
+
+.markdown-body .mb-0 {
+ margin-bottom: 0 !important;
+}
+
+.markdown-body .my-2 {
+ margin-bottom: 8px !important;
+}
+
+.markdown-body .my-2 {
+ margin-top: 8px !important;
+}
+
+.markdown-body .pl-0 {
+ padding-left: 0 !important;
+}
+
+.markdown-body .py-0 {
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+}
+
+.markdown-body .pl-1 {
+ padding-left: 4px !important;
+}
+
+.markdown-body .pl-2 {
+ padding-left: 8px !important;
+}
+
+.markdown-body .py-2 {
+ padding-top: 8px !important;
+ padding-bottom: 8px !important;
+}
+
+.markdown-body .pl-3 {
+ padding-left: 16px !important;
+}
+
+.markdown-body .pl-4 {
+ padding-left: 24px !important;
+}
+
+.markdown-body .pl-5 {
+ padding-left: 32px !important;
+}
+
+.markdown-body .pl-6 {
+ padding-left: 40px !important;
+}
+
+.markdown-body .pl-7 {
+ padding-left: 48px !important;
+}
+
+.markdown-body .pl-8 {
+ padding-left: 64px !important;
+}
+
+.markdown-body .pl-9 {
+ padding-left: 80px !important;
+}
+
+.markdown-body .pl-10 {
+ padding-left: 96px !important;
+}
+
+.markdown-body .pl-11 {
+ padding-left: 112px !important;
+}
+
+.markdown-body .pl-12 {
+ padding-left: 128px !important;
+}
+
+.markdown-body hr {
+ border-bottom-color: #eee;
+}
+
+.markdown-body kbd {
+ display: inline-block;
+ padding: 3px 5px;
+ font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
+ line-height: 10px;
+ color: #444d56;
+ vertical-align: middle;
+ background-color: #fafbfc;
+ border: 1px solid #d1d5da;
+ border-radius: 3px;
+ box-shadow: inset 0 -1px 0 #d1d5da;
+}
+
+.markdown-body:after,
+.markdown-body:before {
+ display: table;
+ content: "";
+}
+
+.markdown-body:after {
+ clear: both;
+}
+
+.markdown-body>:first-child {
+ margin-top: 0 !important;
+}
+
+.markdown-body>:last-child {
+ margin-bottom: 0 !important;
+}
+
+.markdown-body a:not([href]) {
+ color: inherit;
+ text-decoration: none;
+}
+
+.markdown-body blockquote,
+.markdown-body details,
+.markdown-body dl,
+.markdown-body ol,
+.markdown-body p,
+.markdown-body pre,
+.markdown-body table,
+.markdown-body ul {
+ margin-top: 0;
+ margin-bottom: 16px;
+}
+
+.markdown-body hr {
+ height: .25em;
+ padding: 0;
+ margin: 24px 0;
+ background-color: #e1e4e8;
+ border: 0;
+}
+
+.markdown-body blockquote {
+ margin: 20px 0 !important;
+ background-color: #f5f8fc;
+ padding: 1rem;
+ color: #8796a8;
+ border-left: 3px solid #03A9F4;
+}
+
+.markdown-body blockquote>:first-child {
+ margin-top: 0;
+}
+
+.markdown-body blockquote>:last-child {
+ margin-bottom: 0;
+}
+
+.markdown-body h1,
+.markdown-body h2,
+.markdown-body h3,
+.markdown-body h4,
+.markdown-body h5,
+.markdown-body h6 {
+ margin-top: 24px;
+ margin-bottom: 16px;
+ font-weight: 600;
+ line-height: 1.25;
+}
+
+.markdown-body h1 {
+ font-size: 2em;
+}
+
+.markdown-body h1,
+.markdown-body h2 {
+ padding-bottom: .3em;
+ /* border-bottom: 1px solid #eaecef; */
+}
+
+.markdown-body h2 {
+ font-size: 1.5em;
+}
+
+.markdown-body h3 {
+ font-size: 1.25em;
+}
+
+.markdown-body h4 {
+ font-size: 1em;
+}
+
+.markdown-body h5 {
+ font-size: .875em;
+}
+
+.markdown-body h6 {
+ font-size: .85em;
+ color: #6a737d;
+}
+
+.markdown-body ol,
+.markdown-body ul {
+ padding-left: 2em;
+}
+
+.markdown-body ol ol,
+.markdown-body ol ul,
+.markdown-body ul ol,
+.markdown-body ul ul {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.markdown-body li {
+ word-wrap: break-all;
+}
+
+.markdown-body li>p {
+ margin-top: 16px;
+}
+
+.markdown-body li+li {
+ margin-top: .25em;
+}
+
+.markdown-body dl {
+ padding: 0;
+}
+
+.markdown-body dl dt {
+ padding: 0;
+ margin-top: 16px;
+ font-size: 1em;
+ font-style: italic;
+ font-weight: 600;
+}
+
+.markdown-body dl dd {
+ padding: 0 16px;
+ margin-bottom: 16px;
+}
+
+.markdown-body table {
+ display: block;
+ width: 100%;
+ overflow: auto;
+}
+
+.markdown-body table th {
+ font-weight: 600;
+}
+
+.markdown-body table td,
+.markdown-body table th {
+ padding: 6px 13px;
+ border: 1px solid #dfe2e5;
+}
+
+.markdown-body table tr {
+ background-color: #fff;
+ border-top: 1px solid #c6cbd1;
+}
+
+.markdown-body table tr:nth-child(2n) {
+ background-color: #f6f8fa;
+}
+
+.markdown-body img {
+ max-width: 100%;
+ box-sizing: initial;
+ background-color: #fff;
+}
+
+.markdown-body img[align=right] {
+ padding-left: 20px;
+}
+
+.markdown-body img[align=left] {
+ padding-right: 20px;
+}
+
+.markdown-body code {
+ padding: .2em .4em;
+ margin: 0;
+ font-size: 85%;
+ background-color: rgba(27, 31, 35, .05);
+ border-radius: 3px;
+}
+
+.markdown-body pre {
+ word-wrap: normal;
+}
+
+.markdown-body pre>code {
+ padding: 0;
+ margin: 0;
+ font-size: 100%;
+ word-break: normal;
+ white-space: pre;
+ background: transparent;
+ border: 0;
+}
+
+.markdown-body .highlight {
+ margin-bottom: 16px;
+}
+
+.markdown-body .highlight pre {
+ margin-bottom: 0;
+ word-break: normal;
+}
+
+.markdown-body .highlight pre,
+.markdown-body pre {
+ padding: 16px;
+ overflow: auto;
+ font-size: 85%;
+ line-height: 1.45;
+}
+
+.markdown-body pre code {
+ display: inline;
+ max-width: auto;
+ padding: 0;
+ margin: 0;
+ overflow: visible;
+ line-height: inherit;
+ word-wrap: normal;
+ background-color: initial;
+ border: 0;
+}
+
+.markdown-body .commit-tease-sha {
+ display: inline-block;
+ font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
+ font-size: 90%;
+ color: #444d56;
+}
+
+.markdown-body .full-commit .btn-outline:not(:disabled):hover {
+ color: #005cc5;
+ border-color: #005cc5;
+}
+
+.markdown-body .blob-wrapper {
+ overflow-x: auto;
+ overflow-y: hidden;
+}
+
+.markdown-body .blob-wrapper-embedded {
+ max-height: 240px;
+ overflow-y: auto;
+}
+
+.markdown-body .blob-num {
+ width: 1%;
+ min-width: 50px;
+ padding-right: 10px;
+ padding-left: 10px;
+ font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
+ font-size: 12px;
+ line-height: 20px;
+ color: rgba(27, 31, 35, .3);
+ text-align: right;
+ white-space: nowrap;
+ vertical-align: top;
+ cursor: pointer;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.markdown-body .blob-num:hover {
+ color: rgba(27, 31, 35, .6);
+}
+
+.markdown-body .blob-num:before {
+ content: attr(data-line-number);
+}
+
+.markdown-body .blob-code {
+ position: relative;
+ padding-right: 10px;
+ padding-left: 10px;
+ line-height: 20px;
+ vertical-align: top;
+}
+
+.markdown-body .blob-code-inner {
+ overflow: visible;
+ font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
+ font-size: 12px;
+ color: #24292e;
+ word-wrap: normal;
+ white-space: pre;
+}
+
+.markdown-body .pl-token.active,
+.markdown-body .pl-token:hover {
+ cursor: pointer;
+ background: #ffea7f;
+}
+
+.markdown-body .tab-size[data-tab-size="1"] {
+ -moz-tab-size: 1;
+ tab-size: 1;
+}
+
+.markdown-body .tab-size[data-tab-size="2"] {
+ -moz-tab-size: 2;
+ tab-size: 2;
+}
+
+.markdown-body .tab-size[data-tab-size="3"] {
+ -moz-tab-size: 3;
+ tab-size: 3;
+}
+
+.markdown-body .tab-size[data-tab-size="4"] {
+ -moz-tab-size: 4;
+ tab-size: 4;
+}
+
+.markdown-body .tab-size[data-tab-size="5"] {
+ -moz-tab-size: 5;
+ tab-size: 5;
+}
+
+.markdown-body .tab-size[data-tab-size="6"] {
+ -moz-tab-size: 6;
+ tab-size: 6;
+}
+
+.markdown-body .tab-size[data-tab-size="7"] {
+ -moz-tab-size: 7;
+ tab-size: 7;
+}
+
+.markdown-body .tab-size[data-tab-size="8"] {
+ -moz-tab-size: 8;
+ tab-size: 8;
+}
+
+.markdown-body .tab-size[data-tab-size="9"] {
+ -moz-tab-size: 9;
+ tab-size: 9;
+}
+
+.markdown-body .tab-size[data-tab-size="10"] {
+ -moz-tab-size: 10;
+ tab-size: 10;
+}
+
+.markdown-body .tab-size[data-tab-size="11"] {
+ -moz-tab-size: 11;
+ tab-size: 11;
+}
+
+.markdown-body .tab-size[data-tab-size="12"] {
+ -moz-tab-size: 12;
+ tab-size: 12;
+}
+
+.markdown-body .task-list-item {
+ list-style-type: none;
+}
+
+.markdown-body .task-list-item+.task-list-item {
+ margin-top: 3px;
+}
+
+.markdown-body .task-list-item input {
+ margin: 0 .2em .25em -1.6em;
+ vertical-align: middle;
+}
\ No newline at end of file
diff --git a/im/src/assets/css/page/contacts.less b/im/src/assets/css/page/contacts.less
new file mode 100644
index 00000000..8b158ca1
--- /dev/null
+++ b/im/src/assets/css/page/contacts.less
@@ -0,0 +1,281 @@
+.aside-box {
+ position: relative;
+ background-color: white;
+ border-right: 1px solid rgb(245, 245, 245);
+ overflow: hidden;
+ padding: 0;
+
+ .header {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ padding: 0 15px;
+
+ .from {
+ flex: 1 1;
+ flex-shrink: 0;
+ height: 40px;
+
+ /deep/.el-input .el-input__inner {
+ border-radius: 20px;
+ width: 170px;
+ }
+ }
+
+ .tools {
+ flex-basis: 32px;
+ flex-shrink: 0;
+ height: 32px;
+ margin-bottom: 8px;
+ cursor: pointer;
+ line-height: 32px;
+ text-align: center;
+ position: relative;
+ user-select: none;
+
+ .tools-menu {
+ position: absolute;
+ right: 0;
+ top: 38px;
+ width: 100px;
+ min-height: 80px;
+ box-sizing: border-box;
+ background-color: rgba(31, 35, 41, 0.9);
+ border-radius: 5px;
+ z-index: 1;
+ padding: 3px 0;
+
+ .menu1-item {
+ height: 40px;
+ line-height: 40px;
+ color: white;
+ font-size: 14px;
+
+ &:hover {
+ background-color: rgba(70, 72, 73, 0.9);
+ }
+ }
+ }
+ }
+ }
+}
+
+// 右侧面板
+.panel {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+
+ .header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ &.border{
+ border-bottom: 1px solid #f5f5f5;
+ }
+ }
+
+ .subheader {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ border-top: 1px solid rgb(92, 156, 230);
+ border-bottom: 1px solid rgb(92, 156, 230);
+
+ p {
+ padding: 0 10px;
+ cursor: pointer;
+ font-size: 13px;
+
+ &:first-child {
+ padding-left: 0;
+ }
+
+ &.active {
+ color: #508afe;
+ }
+ }
+ }
+
+ .panel-body {
+ overflow: auto;
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+
+ .preloading {
+ height: 100%;
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ user-select: none;
+
+ p {
+ margin-top: 20px;
+ color: #afacac;
+ font-size: 14px;
+ font-weight: 300;
+ }
+ }
+
+ .data-item {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ height: 60px;
+ cursor: pointer;
+ padding: 5px 15px;
+ position: relative;
+ overflow: hidden;
+ border-bottom: 1px solid #f1ebeb;
+ margin-bottom: 2px;
+
+ .avatar {
+ height: 35px;
+ width: 35px;
+ flex-basis: 35px;
+ flex-shrink: 0;
+ background-color: #508afe;
+ border-radius: 50%;
+ overflow: hidden;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 14px;
+ color: white;
+ user-select: none;
+ transition: ease 1s;
+ position: relative;
+ }
+
+ .card {
+ height: 40px;
+ display: flex;
+ align-content: center;
+ flex-direction: column;
+ flex: 1 1;
+ margin-left: 10px;
+ overflow: hidden;
+
+ .title {
+ width: 100%;
+ height: 20px;
+ display: flex;
+ align-items: center;
+
+ .name {
+ margin-right: 15px;
+ color: #1f2329;
+ }
+
+ .larkc-tag {
+ font-size: 12px;
+ font-weight: 400;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 6px;
+ height: 20px;
+ border-radius: 2px;
+ cursor: default;
+ user-select: none;
+ background-color: #dee0e3;
+ transform: scale(0.8);
+ transform-origin: left;
+ flex-shrink: 0;
+ }
+
+ .wait {
+ background: #ffb445;
+ color: white;
+ }
+
+ .agree {
+ background: #53bd53;
+ color: white;
+ }
+ }
+
+ .content {
+ font-size: 10px;
+ line-height: 18px;
+ color: #8f959e;
+ overflow: hidden;
+ margin-top: 3px;
+ font-weight: 300;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ }
+
+ .apply-from {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ position: relative;
+ right: -110px;
+ top: 0px;
+ height: 60px;
+ width: 100px;
+ transition: ease 0.5s 0.3s;
+ background-color: white;
+ opacity: 0;
+ button {
+ margin: 2px;
+ }
+ }
+
+ &:hover {
+ box-shadow: 0 0 8px 4px #f1f1f1;
+
+ .avatar {
+ border-radius: 2px;
+ }
+
+ .apply-from {
+ opacity: 1;
+ right: 0px;
+ }
+ }
+ }
+ }
+}
+
+.broadside-box {
+ position: absolute;
+ width: 350px;
+ height: 100%;
+ top: 0;
+ right: 0;
+ z-index: 2;
+ animation: showBox 0.5s ease-in-out;
+ -webkit-animation: showBox 0.5s ease-in-out;
+ -moz-animation: showBox 0.5s ease-in-out;
+ -webkit-box-direction: normal;
+ background: white;
+ box-shadow: 0 0 14px #cccccc70;
+}
+
+@keyframes showBox {
+ 0% {
+ transform: translateX(350px);
+ }
+
+ to {
+ transform: translateX(0);
+ }
+}
+
+@-webkit-keyframes showBox {
+ 0% {
+ -webkit-transform: translateX(350px);
+ }
+
+ to {
+ -webkit-transform: translateX(0);
+ }
+}
diff --git a/im/src/assets/css/page/login-auth.less b/im/src/assets/css/page/login-auth.less
new file mode 100644
index 00000000..075dcf2c
--- /dev/null
+++ b/im/src/assets/css/page/login-auth.less
@@ -0,0 +1,198 @@
+/deep/.el-input__inner {
+ border-radius: 1px !important;
+}
+
+#auth-container {
+ position: fixed;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ background-color: #f6f8fb;
+
+ #logo-name {
+ width: 200px;
+ height: 38px;
+ font-size: 34px;
+ font-family: Times New Roman, Georgia, Serif;
+ color: #2196f3;
+ margin-left: 20px;
+ margin-top: 20px;
+ }
+
+ #login-box {
+ position: absolute;
+ width: 350px;
+ min-height: 480px;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background-color: white;
+ border-radius: 5px;
+ box-shadow: 0 0 0 #ccc;
+ box-shadow: 0 4px 14px 0 rgba(206, 207, 209, 0.5);
+ padding: 10px 20px;
+
+ .header {
+ width: 100%;
+ height: 38px;
+ font-size: 22px;
+ margin: 25px 0 20px 0;
+ }
+
+ .main {
+ width: 100%;
+
+ .links {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ a {
+ font-weight: normal !important;
+ }
+ }
+
+ .send-code-btn {
+ width: 140px;
+ height: 40px;
+ line-height: 40px;
+ display: inline-block;
+ background: #f3ecec;
+ text-align: center;
+ color: #777373;
+ cursor: pointer;
+ user-select: none;
+ margin-left: 5px;
+
+ &:active {
+ background: #e4dbdb;
+ }
+ }
+
+ .send-sms-disable {
+ cursor: not-allowed !important;
+ background: #f7f7f7 !important;
+ color: silver !important;
+ }
+
+ .submit-btn {
+ width: 100%;
+ border-radius: 2px;
+ }
+ }
+ }
+}
+
+.preview-account {
+ text-align: center;
+
+ p {
+ height: 25px;
+ line-height: 25px;
+ color: rgb(45, 44, 44);
+ font-weight: 100;
+ font-size: 12px;
+ }
+}
+
+.copyright {
+ position: absolute;
+ bottom: 30px;
+ left: 0;
+ right: 0;
+ width: 70%;
+ text-align: center;
+ margin: 0 auto;
+ font-size: 12px;
+ color: #b1a0a0;
+
+ a {
+ color: #777272;
+ font-weight: 400;
+ }
+}
+
+@media screen and (max-height: 500px) {
+ .copyright {
+ display: none;
+ }
+}
+
+.fly-box {
+ .fly {
+ pointer-events: none;
+ position: fixed;
+ z-index: 100;
+ }
+
+ .bg-fly-circle1 {
+ left: 40px;
+ top: 100px;
+ width: 100px;
+ height: 100px;
+ border-radius: 50%;
+ background: linear-gradient(
+ to right,
+ rgba(100, 84, 239, 0.07) 0%,
+ rgba(48, 33, 236, 0.04) 100%
+ );
+ animation: move 2.5s linear infinite;
+ }
+
+ .bg-fly-circle2 {
+ left: 3%;
+ top: 60%;
+ width: 150px;
+ height: 150px;
+ border-radius: 50%;
+ background: linear-gradient(
+ to right,
+ rgba(100, 84, 239, 0.08) 0%,
+ rgba(48, 33, 236, 0.04) 100%
+ );
+ animation: move 3s linear infinite;
+ }
+
+ .bg-fly-circle3 {
+ right: 2%;
+ top: 140px;
+ width: 145px;
+ height: 145px;
+ border-radius: 50%;
+ background: linear-gradient(
+ to right,
+ rgba(100, 84, 239, 0.1) 0%,
+ rgba(48, 33, 236, 0.04) 100%
+ );
+ animation: move 2.5s linear infinite;
+ }
+
+ .bg-fly-circle4 {
+ right: 5%;
+ top: 60%;
+ width: 160px;
+ height: 160px;
+ border-radius: 50%;
+ background: linear-gradient(
+ to right,
+ rgba(100, 84, 239, 0.02) 0%,
+ rgba(48, 33, 236, 0.04) 100%
+ );
+ animation: move 3.5s linear infinite;
+ }
+}
+
+@keyframes move {
+ 0% {
+ transform: translateY(0px);
+ }
+
+ 50% {
+ transform: translateY(25px);
+ }
+
+ 100% {
+ transform: translateY(0px);
+ }
+}
diff --git a/im/src/assets/css/reset.css b/im/src/assets/css/reset.css
new file mode 100644
index 00000000..929e8500
--- /dev/null
+++ b/im/src/assets/css/reset.css
@@ -0,0 +1,62 @@
+* {
+ margin: 0;
+ padding: 0;
+}
+
+body,
+html {
+ height: 100%;
+ min-width: 500px;
+ font-family: "Microsoft YaHei";
+ font-size: 16px;
+ color: #333;
+}
+
+button,
+input,
+select,
+textarea {
+ font-size: 100%;
+ margin: 0;
+ padding: 0;
+ border: none;
+ outline: none;
+}
+
+img {
+ border: 0;
+}
+
+a,
+img {
+ -webkit-touch-callout: none
+}
+
+a {
+ text-decoration: none;
+}
+
+textarea {
+ resize: none;
+ outline: 0;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ border: none;
+ background: #fff;
+ font-family: "Microsoft YaHei";
+}
+
+:focus {
+ outline: none;
+}
+
+.clearfix {
+ clear: both;
+ content: "";
+ display: block;
+ overflow: hidden
+}
+
+.clear {
+ clear: both;
+}
\ No newline at end of file
diff --git a/im/src/assets/css/talk/talk-records.less b/im/src/assets/css/talk/talk-records.less
new file mode 100644
index 00000000..c852598a
--- /dev/null
+++ b/im/src/assets/css/talk/talk-records.less
@@ -0,0 +1,50 @@
+.message-group {
+ min-height: 30px;
+ display: flex;
+ margin-bottom: 5px;
+ flex-direction: row;
+ padding: 3px 12px 3px 0;
+
+ &:first-child {
+ margin-top: 10px;
+ }
+
+ .left-box {
+ width: 50px;
+ flex-shrink: 0;
+ display: flex;
+ justify-content: center;
+ user-select: none;
+ padding-top: 8px;
+
+ img {
+ height: 30px;
+ width: 30px;
+ border-radius: 3px;
+ cursor: pointer;
+ }
+ }
+
+ .right-box {
+ flex: auto;
+ overflow-x: auto;
+ padding: 0px 5px 15px 5px;
+
+ .msg-header {
+ height: 30px;
+ line-height: 30px;
+ font-size: 12px;
+ color: #a09a9a;
+ position: relative;
+ user-select: none;
+
+ .name {
+ color: #333;
+ }
+ }
+
+ /deep/.text-message {
+ border-radius: 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/im/src/assets/css/variable.less b/im/src/assets/css/variable.less
new file mode 100644
index 00000000..631f08e7
--- /dev/null
+++ b/im/src/assets/css/variable.less
@@ -0,0 +1,10 @@
+//主题皮肤 - 预留功能
+:root {
+ --themeBagColor: red;
+}
+
+// ------- 定义 Less 变量 -------
+@themeBagColor: var(--themeBagColor);
+
+// 遮罩层背景颜色
+@maskBagColor: rgba(31, 35, 41, .3);
\ No newline at end of file
diff --git a/im/src/assets/image/1701.mp3 b/im/src/assets/image/1701.mp3
new file mode 100644
index 00000000..2f50c05a
Binary files /dev/null and b/im/src/assets/image/1701.mp3 differ
diff --git a/im/src/assets/image/59y888piCn92.mp3 b/im/src/assets/image/59y888piCn92.mp3
new file mode 100644
index 00000000..86150d8d
Binary files /dev/null and b/im/src/assets/image/59y888piCn92.mp3 differ
diff --git a/im/src/assets/image/RaJik9TWDi.png b/im/src/assets/image/RaJik9TWDi.png
new file mode 100644
index 00000000..63fc88fd
Binary files /dev/null and b/im/src/assets/image/RaJik9TWDi.png differ
diff --git a/im/src/assets/image/aliyun-abs.jpg b/im/src/assets/image/aliyun-abs.jpg
new file mode 100644
index 00000000..6c178b7e
Binary files /dev/null and b/im/src/assets/image/aliyun-abs.jpg differ
diff --git a/im/src/assets/image/background/001.jpg b/im/src/assets/image/background/001.jpg
new file mode 100644
index 00000000..1d1a75b7
Binary files /dev/null and b/im/src/assets/image/background/001.jpg differ
diff --git a/im/src/assets/image/background/002.jpg b/im/src/assets/image/background/002.jpg
new file mode 100644
index 00000000..2375e686
Binary files /dev/null and b/im/src/assets/image/background/002.jpg differ
diff --git a/im/src/assets/image/background/003.jpg b/im/src/assets/image/background/003.jpg
new file mode 100644
index 00000000..2257eee2
Binary files /dev/null and b/im/src/assets/image/background/003.jpg differ
diff --git a/im/src/assets/image/background/004.jpg b/im/src/assets/image/background/004.jpg
new file mode 100644
index 00000000..1d928773
Binary files /dev/null and b/im/src/assets/image/background/004.jpg differ
diff --git a/im/src/assets/image/background/005.png b/im/src/assets/image/background/005.png
new file mode 100644
index 00000000..675c74b3
Binary files /dev/null and b/im/src/assets/image/background/005.png differ
diff --git a/im/src/assets/image/chat-search-no-message.png b/im/src/assets/image/chat-search-no-message.png
new file mode 100644
index 00000000..d0fcea45
Binary files /dev/null and b/im/src/assets/image/chat-search-no-message.png differ
diff --git a/im/src/assets/image/chat.png b/im/src/assets/image/chat.png
new file mode 100644
index 00000000..1ca3cbb6
Binary files /dev/null and b/im/src/assets/image/chat.png differ
diff --git a/im/src/assets/image/default-user-banner.png b/im/src/assets/image/default-user-banner.png
new file mode 100644
index 00000000..55ba3569
Binary files /dev/null and b/im/src/assets/image/default-user-banner.png differ
diff --git a/im/src/assets/image/detault-avatar.jpg b/im/src/assets/image/detault-avatar.jpg
new file mode 100644
index 00000000..0de65dc2
Binary files /dev/null and b/im/src/assets/image/detault-avatar.jpg differ
diff --git a/im/src/assets/image/gitee-avatar.jpg b/im/src/assets/image/gitee-avatar.jpg
new file mode 100644
index 00000000..5dcfbd96
Binary files /dev/null and b/im/src/assets/image/gitee-avatar.jpg differ
diff --git a/im/src/assets/image/github-avatar.jpg b/im/src/assets/image/github-avatar.jpg
new file mode 100644
index 00000000..7d8316ef
Binary files /dev/null and b/im/src/assets/image/github-avatar.jpg differ
diff --git a/im/src/assets/image/icon_face.png b/im/src/assets/image/icon_face.png
new file mode 100644
index 00000000..38b6ece0
Binary files /dev/null and b/im/src/assets/image/icon_face.png differ
diff --git a/im/src/assets/image/icon_heart.png b/im/src/assets/image/icon_heart.png
new file mode 100644
index 00000000..ee141915
Binary files /dev/null and b/im/src/assets/image/icon_heart.png differ
diff --git a/im/src/assets/image/no-oncall.6b776fcf.png b/im/src/assets/image/no-oncall.6b776fcf.png
new file mode 100644
index 00000000..fb531dbd
Binary files /dev/null and b/im/src/assets/image/no-oncall.6b776fcf.png differ
diff --git a/im/src/assets/image/obj_w5zD.mp3 b/im/src/assets/image/obj_w5zD.mp3
new file mode 100644
index 00000000..887331fa
Binary files /dev/null and b/im/src/assets/image/obj_w5zD.mp3 differ
diff --git a/im/src/components/chat/TalkCodeBlock.vue b/im/src/components/chat/TalkCodeBlock.vue
new file mode 100644
index 00000000..44340101
--- /dev/null
+++ b/im/src/components/chat/TalkCodeBlock.vue
@@ -0,0 +1,313 @@
+
+
+
+
+
diff --git a/im/src/components/chat/TalkForwardRecord.vue b/im/src/components/chat/TalkForwardRecord.vue
new file mode 100644
index 00000000..276ea90f
--- /dev/null
+++ b/im/src/components/chat/TalkForwardRecord.vue
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
未知消息类型
+
+
+
+
+
+
+
+
+
diff --git a/im/src/components/chat/TalkSearchRecord.vue b/im/src/components/chat/TalkSearchRecord.vue
new file mode 100644
index 00000000..98cc487b
--- /dev/null
+++ b/im/src/components/chat/TalkSearchRecord.vue
@@ -0,0 +1,602 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ tab.name }}
+
+
+
+
+
+
+
暂无聊天记录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
未知消息类型
+
+
+
+
+
+
+ 加载数据中...
+
+
+
+ 加载更多...
+
+
+
+
+
+
+
+
+
+
+
diff --git a/im/src/components/chat/messaege/AudioMessage.vue b/im/src/components/chat/messaege/AudioMessage.vue
new file mode 100644
index 00000000..04153ad9
--- /dev/null
+++ b/im/src/components/chat/messaege/AudioMessage.vue
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+ {{ getCurrDuration }} / {{ getTotalDuration }}
+
+
+
+
+
+
+
+
+
+
diff --git a/im/src/components/chat/messaege/CodeMessage.vue b/im/src/components/chat/messaege/CodeMessage.vue
new file mode 100644
index 00000000..7eae1cba
--- /dev/null
+++ b/im/src/components/chat/messaege/CodeMessage.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
diff --git a/im/src/components/chat/messaege/FileMessage.vue b/im/src/components/chat/messaege/FileMessage.vue
new file mode 100644
index 00000000..968e2885
--- /dev/null
+++ b/im/src/components/chat/messaege/FileMessage.vue
@@ -0,0 +1,145 @@
+
+
+
+
{{ ext }}
+
+
+ {{ fileName }}
+ ({{ fileSize }})
+
+
文件已成功发送, 文件助手永久保存
+
+
+
+
+
+
+
diff --git a/im/src/components/chat/messaege/ForwardMessage.vue b/im/src/components/chat/messaege/ForwardMessage.vue
new file mode 100644
index 00000000..3243cd54
--- /dev/null
+++ b/im/src/components/chat/messaege/ForwardMessage.vue
@@ -0,0 +1,116 @@
+
+
+
+
{{ title }}
+
+
+ {{ record.nickname }}:
+ {{ record.text }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/im/src/components/chat/messaege/FriendApplyMessage.vue b/im/src/components/chat/messaege/FriendApplyMessage.vue
new file mode 100644
index 00000000..5df0dc4f
--- /dev/null
+++ b/im/src/components/chat/messaege/FriendApplyMessage.vue
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/im/src/components/chat/messaege/ImageMessage.vue b/im/src/components/chat/messaege/ImageMessage.vue
new file mode 100644
index 00000000..309e07d1
--- /dev/null
+++ b/im/src/components/chat/messaege/ImageMessage.vue
@@ -0,0 +1,51 @@
+
+
+
+ 图片加载失败...
+ 图片加载中...
+
+
+
+
+
diff --git a/im/src/components/chat/messaege/InviteMessage.vue b/im/src/components/chat/messaege/InviteMessage.vue
new file mode 100644
index 00000000..476b3eb1
--- /dev/null
+++ b/im/src/components/chat/messaege/InviteMessage.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
diff --git a/im/src/components/chat/messaege/LoginMessage.vue b/im/src/components/chat/messaege/LoginMessage.vue
new file mode 100644
index 00000000..3226b643
--- /dev/null
+++ b/im/src/components/chat/messaege/LoginMessage.vue
@@ -0,0 +1,101 @@
+
+
+
+
登录操作通知
+
登录时间:{{ datetime }} (CST)
+
IP地址:{{ ip }}
+
登录地点:{{ address }}
+
登录设备:{{ platform }}
+
异常原因:{{ reason }}
+
+
+
+
+
diff --git a/im/src/components/chat/messaege/ReplyMessage.vue b/im/src/components/chat/messaege/ReplyMessage.vue
new file mode 100644
index 00000000..d8fb65fc
--- /dev/null
+++ b/im/src/components/chat/messaege/ReplyMessage.vue
@@ -0,0 +1,26 @@
+
+ 这是回复的消息[预留]
+
+
+
+
diff --git a/im/src/components/chat/messaege/RevokeMessage.vue b/im/src/components/chat/messaege/RevokeMessage.vue
new file mode 100644
index 00000000..281bb6ca
--- /dev/null
+++ b/im/src/components/chat/messaege/RevokeMessage.vue
@@ -0,0 +1,66 @@
+
+
+
+
+ 你撤回了一条消息 | {{ sendTime(item.created_at) }}
+
+
+ 对方撤回了一条消息 | {{ sendTime(item.created_at) }}
+
+
+ "{{ item.nickname }}" 撤回了一条消息 | {{ sendTime(item.created_at) }}
+
+
+
+
+
+
diff --git a/im/src/components/chat/messaege/SystemTextMessage.vue b/im/src/components/chat/messaege/SystemTextMessage.vue
new file mode 100644
index 00000000..dc31d4fc
--- /dev/null
+++ b/im/src/components/chat/messaege/SystemTextMessage.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
diff --git a/im/src/components/chat/messaege/TextMessage.vue b/im/src/components/chat/messaege/TextMessage.vue
new file mode 100644
index 00000000..80008d26
--- /dev/null
+++ b/im/src/components/chat/messaege/TextMessage.vue
@@ -0,0 +1,109 @@
+
+
+
+
+
diff --git a/im/src/components/chat/messaege/UserCardMessage.vue b/im/src/components/chat/messaege/UserCardMessage.vue
new file mode 100644
index 00000000..740f7e08
--- /dev/null
+++ b/im/src/components/chat/messaege/UserCardMessage.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/im/src/components/chat/messaege/VideoMessage.vue b/im/src/components/chat/messaege/VideoMessage.vue
new file mode 100644
index 00000000..c8d78dd0
--- /dev/null
+++ b/im/src/components/chat/messaege/VideoMessage.vue
@@ -0,0 +1,18 @@
+
+
+ 视频消息
+
+
+
+
+
diff --git a/im/src/components/chat/messaege/VisitCardMessage.vue b/im/src/components/chat/messaege/VisitCardMessage.vue
new file mode 100644
index 00000000..81a62f52
--- /dev/null
+++ b/im/src/components/chat/messaege/VisitCardMessage.vue
@@ -0,0 +1,134 @@
+
+
+
+
个性签名 : {{ sign }}
+
+
你是谁?
+ 分享了用户名片,可点击添加好友 ...
+
+
+
+
+
diff --git a/im/src/components/chat/messaege/VoiceMessage.vue b/im/src/components/chat/messaege/VoiceMessage.vue
new file mode 100644
index 00000000..fb3feb6f
--- /dev/null
+++ b/im/src/components/chat/messaege/VoiceMessage.vue
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/im/src/components/chat/messaege/VoteMessage.vue b/im/src/components/chat/messaege/VoteMessage.vue
new file mode 100644
index 00000000..eb713468
--- /dev/null
+++ b/im/src/components/chat/messaege/VoteMessage.vue
@@ -0,0 +1,299 @@
+
+
+
+
+
+
+
+
+
+
{{ option.value }}. {{ option.text }}
+
+ {{ option.num }} 票 {{ option.progress }}%
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ option.value }} 、{{ option.text }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/im/src/components/chat/messaege/index.js b/im/src/components/chat/messaege/index.js
new file mode 100644
index 00000000..64a30484
--- /dev/null
+++ b/im/src/components/chat/messaege/index.js
@@ -0,0 +1,33 @@
+import AudioMessage from './AudioMessage.vue';
+import CodeMessage from './CodeMessage.vue';
+import ForwardMessage from './ForwardMessage.vue';
+import ImageMessage from './ImageMessage.vue';
+import TextMessage from './TextMessage.vue';
+import VideoMessage from './VideoMessage.vue';
+import VoiceMessage from './VoiceMessage.vue';
+import SystemTextMessage from './SystemTextMessage.vue';
+import FileMessage from './FileMessage.vue';
+import InviteMessage from './InviteMessage.vue';
+import RevokeMessage from './RevokeMessage.vue';
+import VisitCardMessage from './VisitCardMessage.vue';
+import ReplyMessage from './ReplyMessage.vue';
+import VoteMessage from './VoteMessage.vue';
+import LoginMessage from './LoginMessage.vue';
+
+export {
+ AudioMessage,
+ CodeMessage,
+ ForwardMessage,
+ ImageMessage,
+ TextMessage,
+ VideoMessage,
+ VoiceMessage,
+ SystemTextMessage,
+ FileMessage,
+ InviteMessage,
+ RevokeMessage,
+ VisitCardMessage,
+ ReplyMessage,
+ VoteMessage,
+ LoginMessage
+}
\ No newline at end of file
diff --git a/im/src/components/chat/panel/OtherLink.vue b/im/src/components/chat/panel/OtherLink.vue
new file mode 100644
index 00000000..15ed81ab
--- /dev/null
+++ b/im/src/components/chat/panel/OtherLink.vue
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/im/src/components/chat/panel/PanelHeader.vue b/im/src/components/chat/panel/PanelHeader.vue
new file mode 100644
index 00000000..dedceddc
--- /dev/null
+++ b/im/src/components/chat/panel/PanelHeader.vue
@@ -0,0 +1,278 @@
+
+
+
+
+
diff --git a/im/src/components/chat/panel/PanelToolbar.vue b/im/src/components/chat/panel/PanelToolbar.vue
new file mode 100644
index 00000000..e55d6b20
--- /dev/null
+++ b/im/src/components/chat/panel/PanelToolbar.vue
@@ -0,0 +1,101 @@
+
+
+
+ 已选中:{{ value }} 条消息
+
+
+
+
+
+
+
diff --git a/im/src/components/chat/panel/TalkPanel.vue b/im/src/components/chat/panel/TalkPanel.vue
new file mode 100644
index 00000000..72193ed5
--- /dev/null
+++ b/im/src/components/chat/panel/TalkPanel.vue
@@ -0,0 +1,1103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 回到底部
+
+
+
+
+
+
+ 新消息({{ unreadMessage.num }}条)
+
+ #{{ unreadMessage.nickname }}#
+ {{ unreadMessage.content }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/im/src/components/chat/panel/template/footPrint.vue b/im/src/components/chat/panel/template/footPrint.vue
new file mode 100644
index 00000000..80bf3e25
--- /dev/null
+++ b/im/src/components/chat/panel/template/footPrint.vue
@@ -0,0 +1,71 @@
+
+
+ 最近浏览
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/im/src/components/chat/panel/template/goodsLink.vue b/im/src/components/chat/panel/template/goodsLink.vue
new file mode 100644
index 00000000..2c267176
--- /dev/null
+++ b/im/src/components/chat/panel/template/goodsLink.vue
@@ -0,0 +1,168 @@
+
+
+ 当前浏览
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/im/src/components/chat/panel/template/storeDetail.vue b/im/src/components/chat/panel/template/storeDetail.vue
new file mode 100644
index 00000000..470f09dc
--- /dev/null
+++ b/im/src/components/chat/panel/template/storeDetail.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+ {{ storeInfo.storeName }}
+ 自营
+
+
+ 联系方式: {{ storeInfo.memberName }}
+
+
+ 进入店铺
+
+
+
+
+
店铺评分: {{ storeInfo.serviceScore }}
+
服务评分: {{ storeInfo.descriptionScore }}
+
物流评分: {{ storeInfo.deliveryScore }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/im/src/components/editor/MeEditor.vue b/im/src/components/editor/MeEditor.vue
new file mode 100644
index 00000000..cc0605c0
--- /dev/null
+++ b/im/src/components/editor/MeEditor.vue
@@ -0,0 +1,506 @@
+
+
+
+
+
+ -
+
+
表情符号
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{
+ this.vote.isShow = false;
+ }
+ "
+ />
+
+
+
+
+
diff --git a/im/src/components/editor/MeEditorEmoticon.vue b/im/src/components/editor/MeEditorEmoticon.vue
new file mode 100644
index 00000000..d5614241
--- /dev/null
+++ b/im/src/components/editor/MeEditorEmoticon.vue
@@ -0,0 +1,345 @@
+
+
+
+
+
+
+
+
QQ表情
+
+
+
符号表情
+
+ {{ item }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/im/src/components/editor/MeEditorFileManage.vue b/im/src/components/editor/MeEditorFileManage.vue
new file mode 100644
index 00000000..65b89846
--- /dev/null
+++ b/im/src/components/editor/MeEditorFileManage.vue
@@ -0,0 +1,440 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 上传进度
+
+
+
+ 文件类型:{{ file.filetype }}
+
+
+ 文件大小:{{ file.filesize }}
+
+
+ 上传时间:{{ file.datetime }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/im/src/components/editor/MeEditorImageView.vue b/im/src/components/editor/MeEditorImageView.vue
new file mode 100644
index 00000000..cad37b14
--- /dev/null
+++ b/im/src/components/editor/MeEditorImageView.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+ {{ fileName }}
+
+ {{ fileSize }} KB
+
+
+
+
+
+
+
+
+
diff --git a/im/src/components/editor/MeEditorRecorder.vue b/im/src/components/editor/MeEditorRecorder.vue
new file mode 100644
index 00000000..3e2c79ec
--- /dev/null
+++ b/im/src/components/editor/MeEditorRecorder.vue
@@ -0,0 +1,517 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 语音消息,让聊天更简单方便 ...
+
+
+
+ {{ datetime }}
+
+ 正在录音
+ 已暂停录音
+ 录音时长
+
+
+
+ {{ formatPlayTime }}
+
+ 正在播放
+ 已暂停播放
+ 播放已结束
+
+
+
+
+
+
+
+
+
+
+
diff --git a/im/src/components/editor/MeEditorSystemEmoticon.vue b/im/src/components/editor/MeEditorSystemEmoticon.vue
new file mode 100644
index 00000000..c7028bd7
--- /dev/null
+++ b/im/src/components/editor/MeEditorSystemEmoticon.vue
@@ -0,0 +1,169 @@
+
+
+
+
+
+
diff --git a/im/src/components/editor/MeEditorVote.vue b/im/src/components/editor/MeEditorVote.vue
new file mode 100644
index 00000000..7819fa7d
--- /dev/null
+++ b/im/src/components/editor/MeEditorVote.vue
@@ -0,0 +1,226 @@
+
+
+
+
+
+ 投票方式
+
+
+ 单选
+ 多选
+
+
+ 投票主题
+
+
+
+
+ 投票选项
+
+
+
+
+
+
+
+
diff --git a/im/src/components/face-null.vue b/im/src/components/face-null.vue
new file mode 100644
index 00000000..f041cb81
--- /dev/null
+++ b/im/src/components/face-null.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/im/src/components/face.vue b/im/src/components/face.vue
new file mode 100644
index 00000000..9f4d6709
--- /dev/null
+++ b/im/src/components/face.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/im/src/components/global/Empty.vue b/im/src/components/global/Empty.vue
new file mode 100644
index 00000000..a4cb33a6
--- /dev/null
+++ b/im/src/components/global/Empty.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/im/src/components/global/Loading.vue b/im/src/components/global/Loading.vue
new file mode 100644
index 00000000..2cb1def5
--- /dev/null
+++ b/im/src/components/global/Loading.vue
@@ -0,0 +1,313 @@
+
+
+
+
+
+
+
+
+
+
+
{{ text }}
+
+
+
+
+
diff --git a/im/src/components/layout/AbsModule.vue b/im/src/components/layout/AbsModule.vue
new file mode 100644
index 00000000..7768db15
--- /dev/null
+++ b/im/src/components/layout/AbsModule.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
diff --git a/im/src/components/layout/AvatarCropper.vue b/im/src/components/layout/AvatarCropper.vue
new file mode 100644
index 00000000..0f9883f4
--- /dev/null
+++ b/im/src/components/layout/AvatarCropper.vue
@@ -0,0 +1,246 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 上传图片
+
+ 刷新
+
+ 左转
+
+ 右转
+
+
+
+
+
+
+
+
+
+
+
+ 保存图片
+
+
+
+
+
+
+
+
+
+
+
diff --git a/im/src/components/layout/RewardModule.vue b/im/src/components/layout/RewardModule.vue
new file mode 100644
index 00000000..6fdff64d
--- /dev/null
+++ b/im/src/components/layout/RewardModule.vue
@@ -0,0 +1,129 @@
+
+
+
+
+ Donate
+
+
+
+
+
+
支付宝
+
+
+
+
微信
+
+
+
+
+
+
+
+
diff --git a/im/src/components/layout/SkinModule.vue b/im/src/components/layout/SkinModule.vue
new file mode 100644
index 00000000..f25b6ff0
--- /dev/null
+++ b/im/src/components/layout/SkinModule.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+
diff --git a/im/src/components/layout/WelcomeModule.vue b/im/src/components/layout/WelcomeModule.vue
new file mode 100644
index 00000000..7d2fa6c0
--- /dev/null
+++ b/im/src/components/layout/WelcomeModule.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/im/src/components/notify/FriendApplyNotify.vue b/im/src/components/notify/FriendApplyNotify.vue
new file mode 100644
index 00000000..db61840e
--- /dev/null
+++ b/im/src/components/notify/FriendApplyNotify.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
申请备注:
+
{{ content }}
+
+
+
+
+
+
+
diff --git a/im/src/components/notify/NewMessageNotify.vue b/im/src/components/notify/NewMessageNotify.vue
new file mode 100644
index 00000000..6bb63a6a
--- /dev/null
+++ b/im/src/components/notify/NewMessageNotify.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
@{{ nickname }}
+
{{ content }}
+
+
+
+
+
+
+
diff --git a/im/src/components/svg-icon/index.vue b/im/src/components/svg-icon/index.vue
new file mode 100644
index 00000000..7ba6cfdc
--- /dev/null
+++ b/im/src/components/svg-icon/index.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
diff --git a/im/src/components/user/UserSearch.vue b/im/src/components/user/UserSearch.vue
new file mode 100644
index 00000000..97bfcab2
--- /dev/null
+++ b/im/src/components/user/UserSearch.vue
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+ 无法找到该用户,请检查搜索内容并重试
+
+ 立即查找
+
+
+
+
+
+
+
+
diff --git a/im/src/components/user/user-card/UserCardDetail.vue b/im/src/components/user/user-card/UserCardDetail.vue
new file mode 100644
index 00000000..93ed30c7
--- /dev/null
+++ b/im/src/components/user/user-card/UserCardDetail.vue
@@ -0,0 +1,483 @@
+
+
+
+
+
+
+
+
+
+
+ {{ detail.mobile | mobile }}
+
+
+
+ {{ detail.nickname || "未设置昵称" }}
+
+
+
+ {{ detail.gender | gender }}
+
+
+
+ {{
+ detail.nickname_remark ? detail.nickname_remark : "暂无备注"
+ }}
+
+
+
+
+
+
+
+ 未设置
+
+
+
+
+
+
+
+
+ 请填写好友申请备注:
+ 取消
+
+
+
+
+ 立即提交
+
+
+
+
+
+
+
+
diff --git a/im/src/components/user/user-card/index.js b/im/src/components/user/user-card/index.js
new file mode 100644
index 00000000..578b394c
--- /dev/null
+++ b/im/src/components/user/user-card/index.js
@@ -0,0 +1,33 @@
+import UserCardDetail from './UserCardDetail'
+
+export default {
+ install(Vue) {
+ function user(user_id, options) {
+ let _vm = this
+ const el = new Vue({
+ router: _vm.$router,
+ store: _vm.$store,
+ render(h) {
+ return h(UserCardDetail, {
+ on: {
+ close: () => {
+ el.$destroy()
+ document.body.removeChild(el.$el)
+ },
+ changeRemark: data => {
+ options.editRemarkCallbak && options.editRemarkCallbak(data)
+ },
+ },
+ props: {
+ user_id,
+ },
+ })
+ },
+ }).$mount()
+
+ document.body.appendChild(el.$el)
+ }
+
+ Vue.prototype.$user = user
+ },
+}
diff --git a/im/src/config/config.js b/im/src/config/config.js
new file mode 100644
index 00000000..f923c038
--- /dev/null
+++ b/im/src/config/config.js
@@ -0,0 +1,7 @@
+export default {
+ WEBSITE_NAME: process.env.VUE_APP_WEBSITE_NAME || "LiLi IM",
+ BASE_API_URL: process.env.VUE_APP_API_BASE_URL || "",
+ BASE_WS_URL: process.env.VUE_APP_WEB_SOCKET_URL || "",
+ BASE_COMMON: process.env.VUE_APP_COMMON || "",
+ PC_URL: process.env.VUE_APP_PC_URL || "",
+};
diff --git a/im/src/constants/talk.js b/im/src/constants/talk.js
new file mode 100644
index 00000000..adc2a17e
--- /dev/null
+++ b/im/src/constants/talk.js
@@ -0,0 +1,9 @@
+/**
+ * 私聊
+ */
+const PRIVATE_CHAT = 1
+
+/**
+ * 群聊
+ */
+const GROUP_CHAT = 2
diff --git a/im/src/core/directives.js b/im/src/core/directives.js
new file mode 100644
index 00000000..a1162a0e
--- /dev/null
+++ b/im/src/core/directives.js
@@ -0,0 +1,49 @@
+import Vue from 'vue'
+import Clickoutside from 'element-ui/src/utils/clickoutside'
+
+// 自定义聚焦指令
+Vue.directive('focus', {
+ inserted(el) {
+ el.focus()
+ },
+})
+
+// 自定义粘贴指令
+Vue.directive('paste', {
+ bind(el, binding, vnode) {
+ el.addEventListener('paste', function(event) {
+ //这里直接监听元素的粘贴事件
+ binding.value(event)
+ })
+ },
+})
+
+// 自定义拖拽指令
+Vue.directive('drag', {
+ bind(el, binding, vnode) {
+ // 因为拖拽还包括拖动时的经过事件,离开事件,和进入事件,放下事件,
+ // 浏览器对于拖拽的默认事件的处理是打开拖进来的资源,
+ // 所以要先对这三个事件进行默认事件的禁止
+ el.addEventListener('dragenter', function(event) {
+ event.stopPropagation()
+ event.preventDefault()
+ })
+ el.addEventListener('dragover', function(event) {
+ event.stopPropagation()
+ event.preventDefault()
+ })
+ el.addEventListener('dragleave', function(event) {
+ event.stopPropagation()
+ event.preventDefault()
+ })
+ el.addEventListener('drop', function(event) {
+ // 这里阻止默认事件,并绑定事件的对象,用来在组件上返回事件对象
+ event.stopPropagation()
+ event.preventDefault()
+ binding.value(event)
+ })
+ },
+})
+
+// 点击其他地方隐藏指令
+Vue.directive('outside', Clickoutside)
diff --git a/im/src/core/filter.js b/im/src/core/filter.js
new file mode 100644
index 00000000..e69de29b
diff --git a/im/src/core/global-component.js b/im/src/core/global-component.js
new file mode 100644
index 00000000..d4166261
--- /dev/null
+++ b/im/src/core/global-component.js
@@ -0,0 +1,37 @@
+import Vue from "vue";
+import {
+ AudioMessage,
+ CodeMessage,
+ ForwardMessage,
+ ImageMessage,
+ TextMessage,
+ VideoMessage,
+ VoiceMessage,
+ SystemTextMessage,
+ FileMessage,
+ InviteMessage,
+ RevokeMessage,
+ VisitCardMessage,
+ ReplyMessage,
+ VoteMessage,
+ LoginMessage,
+} from "@/components/chat/messaege";
+
+Vue.component(AudioMessage.name, AudioMessage);
+Vue.component(CodeMessage.name, CodeMessage);
+Vue.component(ForwardMessage.name, ForwardMessage);
+Vue.component(ImageMessage.name, ImageMessage);
+Vue.component(TextMessage.name, TextMessage);
+Vue.component(VideoMessage.name, VideoMessage);
+Vue.component(VoiceMessage.name, VoiceMessage);
+Vue.component(SystemTextMessage.name, SystemTextMessage);
+Vue.component(FileMessage.name, FileMessage);
+Vue.component(InviteMessage.name, InviteMessage);
+Vue.component(RevokeMessage.name, RevokeMessage);
+Vue.component(VisitCardMessage.name, VisitCardMessage);
+Vue.component(ReplyMessage.name, ReplyMessage);
+Vue.component(VoteMessage.name, VoteMessage);
+Vue.component(LoginMessage.name, LoginMessage);
+
+import UserCard from "@/components/user/user-card/index";
+Vue.use(UserCard);
diff --git a/im/src/core/icons.js b/im/src/core/icons.js
new file mode 100644
index 00000000..dfe7eceb
--- /dev/null
+++ b/im/src/core/icons.js
@@ -0,0 +1,22 @@
+/**
+ * Custom icon list
+ * All icons are loaded here for easy management
+ *
+ * 自定义图标加载表
+ * 所有图标均从这里加载,方便管理
+ */
+import SvgMentionDown from '@/icons/svg/mention-down.svg?inline' // path to your '*.svg?inline' file.
+import SvgNotFount from '@/icons/svg/not-fount.svg?inline' // path to your '*.svg?inline' file.
+import SvgNote from '@/icons/svg/note.svg?inline' // path to your '*.svg?inline' file.
+import SvgNoteBook from '@/icons/svg/note-book.svg?inline' // path to your '*.svg?inline' file.
+import SvgNotData from '@/icons/svg/not-data.svg?inline' // path to your '*.svg?inline' file.
+import SvgZhuangFa from '@/icons/svg/zhuangfa.svg?inline' // path to your '*.svg?inline' file.
+
+export {
+ SvgMentionDown,
+ SvgNotFount,
+ SvgNote,
+ SvgNoteBook,
+ SvgNotData,
+ SvgZhuangFa,
+}
diff --git a/im/src/core/lazy-use.js b/im/src/core/lazy-use.js
new file mode 100644
index 00000000..43a07afd
--- /dev/null
+++ b/im/src/core/lazy-use.js
@@ -0,0 +1,89 @@
+import Vue from 'vue'
+import 'element-ui/lib/theme-chalk/index.css'
+
+import {
+ Notification,
+ Popover,
+ Switch,
+ Dropdown,
+ DropdownMenu,
+ DropdownItem,
+ Message,
+ Container,
+ Header,
+ Aside,
+ Main,
+ Footer,
+ Menu,
+ Submenu,
+ MenuItem,
+ MenuItemGroup,
+ Button,
+ Image,
+ Loading,
+ Row,
+ Col,
+ MessageBox,
+ Form,
+ FormItem,
+ Input,
+ Divider,
+ Link,
+ Tooltip,
+ Autocomplete,
+ Scrollbar,
+ Avatar,
+ Radio,
+ RadioGroup,
+ Progress,
+ Dialog,
+ Checkbox,
+ Tag
+} from 'element-ui'
+
+Vue.use(Popover)
+Vue.use(Switch)
+Vue.use(Dropdown)
+Vue.use(DropdownMenu)
+Vue.use(DropdownItem)
+Vue.use(Container)
+Vue.use(Header)
+Vue.use(Aside)
+Vue.use(Main)
+Vue.use(Footer)
+Vue.use(Menu)
+Vue.use(Submenu)
+Vue.use(MenuItem)
+Vue.use(MenuItemGroup)
+Vue.use(Button)
+Vue.use(Image)
+Vue.use(Row)
+Vue.use(Col)
+Vue.use(Input)
+Vue.use(Form)
+Vue.use(FormItem)
+Vue.use(Divider)
+Vue.use(Link)
+Vue.use(Tooltip)
+Vue.use(Autocomplete)
+Vue.use(Scrollbar)
+Vue.use(Avatar)
+Vue.use(Radio)
+Vue.use(Checkbox)
+Vue.use(RadioGroup)
+Vue.use(Progress)
+Vue.use(Dialog)
+Vue.use(Tag)
+Vue.use(Loading.directive)
+
+Vue.prototype.$notify = Notification
+Vue.prototype.$message = Message
+Vue.prototype.$confirm = MessageBox.confirm
+Vue.prototype.$prompt = MessageBox.prompt
+Vue.prototype.$alert = MessageBox.alert
+
+import Contextmenu from 'vue-contextmenujs'
+Vue.use(Contextmenu)
+
+process.env.NODE_ENV !== 'production' &&
+ console.warn('[Lumen-IM] NOTICE: element-ui use lazy-load.')
diff --git a/im/src/directive/PreCode.js b/im/src/directive/PreCode.js
new file mode 100644
index 00000000..7c881422
--- /dev/null
+++ b/im/src/directive/PreCode.js
@@ -0,0 +1,47 @@
+import { copyTextToClipboard as Clipboard } from '@/utils/functions'
+
+const copyFunc = (pre, text) => {
+ let el = document.createElement('p')
+ el.className = 'fz-btn'
+ el.innerText = '复制'
+ el.onclick = () => {
+ Clipboard(text.replace(/(^\s*)|(\s*$)/g, ''), function() {
+ el.innerText = '复制成功!'
+ setTimeout(() => {
+ el.innerText = '复制'
+ }, 1000)
+ })
+ }
+
+ pre.appendChild(el)
+}
+
+const preNmae = (pre, lang) => {
+ let el = document.createElement('p')
+ el.className = 'lang-name'
+ el.innerText = lang
+ pre.appendChild(el)
+}
+
+function updateNodes(el, binding, vnode) {
+ let preNodes = el.querySelectorAll('pre')
+ preNodes.forEach(elPre => {
+ let elCode = elPre.querySelector('code')
+ let className = elCode.className
+ let language = className.split('-')[1]
+
+ copyFunc(elPre, elCode.innerText)
+
+ if (language != undefined) {
+ preNmae(elPre, language)
+ }
+ })
+}
+
+/**
+ * 代码格式化
+ */
+export default {
+ bind: updateNodes,
+ update: updateNodes,
+}
diff --git a/im/src/icons/index.js b/im/src/icons/index.js
new file mode 100644
index 00000000..75da57f0
--- /dev/null
+++ b/im/src/icons/index.js
@@ -0,0 +1,9 @@
+import Vue from 'vue';
+import SvgIcon from '@/components/svg-icon'; // svg component
+
+// register globally
+Vue.component('svg-icon', SvgIcon);
+
+const req = require.context('./svg', false, /\.svg$/);
+const requireAll = requireContext => requireContext.keys().map(requireContext);
+requireAll(req);
diff --git a/im/src/icons/svg/mention-down.svg b/im/src/icons/svg/mention-down.svg
new file mode 100644
index 00000000..354a96a9
--- /dev/null
+++ b/im/src/icons/svg/mention-down.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/im/src/icons/svg/not-data.svg b/im/src/icons/svg/not-data.svg
new file mode 100644
index 00000000..fab577c3
--- /dev/null
+++ b/im/src/icons/svg/not-data.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/im/src/icons/svg/not-fount.svg b/im/src/icons/svg/not-fount.svg
new file mode 100644
index 00000000..88ad63f8
--- /dev/null
+++ b/im/src/icons/svg/not-fount.svg
@@ -0,0 +1,309 @@
+
\ No newline at end of file
diff --git a/im/src/icons/svg/note-book.svg b/im/src/icons/svg/note-book.svg
new file mode 100644
index 00000000..ab37847a
--- /dev/null
+++ b/im/src/icons/svg/note-book.svg
@@ -0,0 +1,3 @@
+
diff --git a/im/src/icons/svg/note.svg b/im/src/icons/svg/note.svg
new file mode 100644
index 00000000..f95a7a0c
--- /dev/null
+++ b/im/src/icons/svg/note.svg
@@ -0,0 +1,43 @@
+
diff --git a/im/src/icons/svg/zhuangfa.svg b/im/src/icons/svg/zhuangfa.svg
new file mode 100644
index 00000000..37935f77
--- /dev/null
+++ b/im/src/icons/svg/zhuangfa.svg
@@ -0,0 +1,4 @@
+
diff --git a/im/src/im-server/event/base.js b/im/src/im-server/event/base.js
new file mode 100644
index 00000000..8097527b
--- /dev/null
+++ b/im/src/im-server/event/base.js
@@ -0,0 +1,62 @@
+import store from "@/store";
+import router from "@/router";
+import { Notification } from "element-ui";
+
+class Base {
+ /**
+ * 初始化
+ */
+ constructor() {
+ this.$notify = Notification;
+ }
+
+ getStoreInstance() {
+ return store;
+ }
+
+ /**
+ * 获取当前登录用户的ID
+ */
+ getAccountId() {
+ console.log("store.state", store.state.user);
+ return store.state.user.id;
+ }
+
+ getTalkParams() {
+ let { talk_type, receiver_id, index_name } = store.state.dialogue;
+
+ return { talk_type, receiver_id, index_name };
+ }
+
+ /**
+ * 判断消息是否来自当前对话
+ *
+ * @param {Number} talk_type 聊天消息类型[1:私信;2:群聊;]
+ * @param {Number} sender_id 发送者ID
+ * @param {Number} receiver_id 接收者ID
+ */
+ isTalk(talk_type, sender_id, receiver_id) {
+ let params = this.getTalkParams();
+
+ if (talk_type != params.talk_type) {
+ return false;
+ } else if (
+ params.receiver_id == receiver_id ||
+ params.receiver_id == sender_id
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 判断用户是否打开对话页
+ */
+ isTalkPage() {
+ let path = router.currentRoute.path;
+ return !(path != "/message" && path != "/");
+ }
+}
+
+export default Base;
diff --git a/im/src/im-server/event/friend-apply.js b/im/src/im-server/event/friend-apply.js
new file mode 100644
index 00000000..50028e61
--- /dev/null
+++ b/im/src/im-server/event/friend-apply.js
@@ -0,0 +1,47 @@
+import Base from './base'
+import store from '@/store'
+import router from '@/router'
+
+/**
+ * 好友邀请消息处理
+ */
+class FriendApply extends Base {
+ /**
+ * @var resource 资源
+ */
+ resource
+
+ /**
+ * 初始化构造方法
+ *
+ * @param {Object} resource Socket消息
+ */
+ constructor(resource) {
+ super()
+
+ this.resource = resource
+ }
+
+ handle() {
+ store.commit('INCR_APPLY_NUM')
+
+ this.$notify({
+ title: '好友申请',
+ dangerouslyUseHTMLString: true,
+ message: `您有一条好友申请消息,请注意查收...
`,
+ duration: 0,
+ type: 'info',
+ customClass: 'pointer',
+ onClick: function() {
+ store.commit('SET_APPLY_NUM', 0)
+ router.push({
+ path: '/contacts/apply',
+ query: { t: new Date().getTime() },
+ })
+ this.close()
+ },
+ })
+ }
+}
+
+export default FriendApply
diff --git a/im/src/im-server/event/group-join.js b/im/src/im-server/event/group-join.js
new file mode 100644
index 00000000..702a19e2
--- /dev/null
+++ b/im/src/im-server/event/group-join.js
@@ -0,0 +1,31 @@
+import Base from './base'
+
+/**
+ * 好友邀请消息处理
+ */
+class GroupJoin extends Base {
+ /**
+ * @var resource 资源
+ */
+ resource
+
+ /**
+ * 初始化构造方法
+ *
+ * @param {Object} resource Socket消息
+ */
+ constructor(resource) {
+ super()
+
+ this.resource = resource
+ }
+
+ handle() {
+ this.$notify({
+ message: '您有一条入群消息通知,请注意查收...',
+ duration: 5000,
+ })
+ }
+}
+
+export default GroupJoin
diff --git a/im/src/im-server/event/keyboard.js b/im/src/im-server/event/keyboard.js
new file mode 100644
index 00000000..257c2606
--- /dev/null
+++ b/im/src/im-server/event/keyboard.js
@@ -0,0 +1,47 @@
+import Base from './base'
+
+/**
+ * 键盘输入事件
+ */
+class Keyboard extends Base {
+ /**
+ * @var resource 资源
+ */
+ resource
+
+ /**
+ * 初始化构造方法
+ *
+ * @param {Object} resource Socket消息
+ */
+ constructor(resource) {
+ super()
+
+ this.resource = resource
+ }
+
+ handle() {
+ let params = this.getTalkParams()
+
+ // 判断当前是否正在对话
+ if (params.index_name === null) {
+ return false
+ }
+
+ // 判断是否是私信
+ if (params.talk_type != 1) {
+ return false
+ }
+
+ // 判断消息是否来当前对话
+ if (params.receiver_id != this.resource.sender_id) {
+ return false
+ }
+
+ let store = this.getStoreInstance()
+
+ store.commit('UPDATE_KEYBOARD_EVENT')
+ }
+}
+
+export default Keyboard
diff --git a/im/src/im-server/event/login.js b/im/src/im-server/event/login.js
new file mode 100644
index 00000000..59191839
--- /dev/null
+++ b/im/src/im-server/event/login.js
@@ -0,0 +1,31 @@
+import Base from './base'
+
+/**
+ * 好友状态事件
+ */
+class Login extends Base {
+ /**
+ * @var resource 资源
+ */
+ resource
+
+ /**
+ * 初始化构造方法
+ *
+ * @param {Object} resource Socket消息
+ */
+ constructor(resource) {
+ super()
+
+ this.resource = resource
+ }
+
+ handle() {
+ this.getStoreInstance().dispatch('ACT_UPDATE_FRIEND_STATUS', {
+ status: this.resource.status,
+ friend_id: parseInt(this.resource.user_id),
+ })
+ }
+}
+
+export default Login
diff --git a/im/src/im-server/event/revoke.js b/im/src/im-server/event/revoke.js
new file mode 100644
index 00000000..037dcfdd
--- /dev/null
+++ b/im/src/im-server/event/revoke.js
@@ -0,0 +1,42 @@
+import Base from './base'
+import store from '@/store'
+
+/**
+ * 好友邀请消息处理
+ */
+class Revoke extends Base {
+ /**
+ * @var resource 资源
+ */
+ resource
+
+ /**
+ * 初始化构造方法
+ *
+ * @param {Object} resource Socket消息
+ */
+ constructor(resource) {
+ super()
+
+ this.resource = resource
+ }
+
+ handle() {
+ if (
+ !this.isTalk(
+ this.resource.talk_type,
+ this.resource.receiver_id,
+ this.resource.sender_id
+ )
+ ) {
+ return false
+ }
+
+ store.commit('UPDATE_DIALOGUE', {
+ id: this.resource.record_id,
+ is_revoke: 1,
+ })
+ }
+}
+
+export default Revoke
diff --git a/im/src/im-server/event/talk.js b/im/src/im-server/event/talk.js
new file mode 100644
index 00000000..110b3484
--- /dev/null
+++ b/im/src/im-server/event/talk.js
@@ -0,0 +1,256 @@
+import Base from "./base";
+import Vue from "vue";
+import router from "@/router";
+import vm from "@/main";
+import NewMessageNotify from "@/components/notify/NewMessageNotify";
+import { ServeClearTalkUnreadNum, ServeCreateTalkList } from "@/api/chat";
+import { formatTalkItem, findTalkIndex, toTalk } from "@/utils/talk";
+import { parseTime } from "@/utils/functions";
+
+import mixin from '@/mixins/main-mixin'
+/**
+ * 好友状态事件
+ */
+class Talk extends Base {
+ /**
+ * @var resource 资源
+ */
+ resource;
+
+ /**
+ * 发送者ID
+ */
+ sender_id = 0;
+
+ /**
+ * 接收者ID
+ */
+ receiver_id = 0;
+
+ /**
+ * 聊天类型[1:私聊;2:群聊;]
+ */
+ talk_type = 0;
+
+ /**
+ * 初始化构造方法
+ *
+ * @param {Object} resource Socket消息
+ */
+ constructor(resource) {
+ super();
+ console.log("接口构造 resource", resource);
+ this.sender_id = resource.fromUser; //发送
+ this.receiver_id = resource.toUser; //接收
+ this.talk_type = resource.messageType; //类型
+
+ this.resource = resource;
+
+ // 判断发送者消息是否在当前用户列表中
+ if(this.sender_id && !vm.$store.state.talks.items.find(item=>{
+ return item.userId == this.sender_id
+ })){
+ // 没有当前用户,未在当前列表 进行重新加载
+ vm.loadUserSetting('update')
+ }
+ }
+
+ /**
+ * 判断消息发送者是否来自于我
+ * @returns
+ */
+ isCurrSender() {
+ console.log("sender_id", this.sender_id);
+ return this.sender_id == this.getAccountId();
+ }
+
+ /**
+ * 获取对话索引
+ *
+ * @return String
+ */
+ getIndexName() {
+ if (this.talk_type == 2) {
+ return `${this.talk_type}_${this.receiver_id}`;
+ }
+
+ let receiver_id = this.isCurrSender() ? this.receiver_id : this.sender_id;
+
+ return `${this.talk_type}_${receiver_id}`;
+ }
+
+ /**
+ * 消息浮动方式
+ *
+ * @returns
+ */
+ getFloatType() {
+ let userId = this.resource.userId;
+
+ if (userId == 0) return "center";
+
+ return userId == this.getAccountId() ? "right" : "left";
+ }
+
+ /**
+ * 获取聊天列表左侧的对话信息
+ */
+ getTalkText() {
+ let text = this.resource.content || this.resource.text;
+ switch (this.resource.msg_type) {
+ case 'GOODS':
+ text = "[商品链接]";
+ break;
+ }
+
+ return text;
+ }
+
+ handle() {
+ let store = this.getStoreInstance();
+ console.log("触发handle");
+ // 判断当前是否在聊天页面
+ if (!this.isTalkPage()) {
+ store.commit("INCR_UNREAD_NUM");
+
+ // 判断消息是否来自于我自己,否则会提示消息通知
+ return !this.isCurrSender() && this.showMessageNocice();
+ }
+ console.log("this.receiver_id", this.receiver_id);
+ console.log("this.sender_id", this.sender_id);
+ let isTrue = this.isTalk(1, this.receiver_id, this.sender_id);
+ console.log("判断当前是否正在和好友对话", isTrue);
+ // 判断当前是否正在和好友对话
+ if (isTrue) {
+ this.insertTalkRecord();
+ } else {
+ this.updateTalkItem();
+ }
+ }
+
+ /**
+ * 显示消息提示
+ * @returns
+ */
+ showMessageNocice() {
+ let avatar = this.resource.avatar;
+ let nickname = this.resource.nickname;
+ let talk_type = this.resource.talk_type;
+ let receiver_id = this.receiver_id;
+
+ if (talk_type == 2) {
+ avatar = this.resource.group_avatar;
+ nickname += `【 ${this.resource.group_name} 】`;
+ } else {
+ receiver_id = this.sender_id;
+ }
+
+ this.$notify({
+ message: vm.$createElement(NewMessageNotify, {
+ props: {
+ avatar,
+ talk_type,
+ nickname,
+ content: this.getTalkText(),
+ datetime: this.resource.created_at,
+ },
+ }),
+ customClass: "im-notify",
+ duration: 3000,
+ position: "top-right",
+ onClick: function () {
+ this.close();
+ toTalk(talk_type, receiver_id);
+ },
+ });
+ }
+
+ /**
+ * 加载对接节点
+ */
+ addTalkItem() {
+ let receiver_id = this.sender_id;
+ let talk_type = this.talk_type;
+
+ if (talk_type == 1 && this.receiver_id != this.getAccountId()) {
+ receiver_id = this.receiver_id;
+ } else if (talk_type == 2) {
+ receiver_id = this.receiver_id;
+ }
+ console.log("加载对接节点", this.resource);
+
+ ServeCreateTalkList(receiver_id).then(({ code, data }) => {
+ if (code == 200) {
+ this.getStoreInstance().commit("PUSH_TALK_ITEM", formatTalkItem(data));
+ }
+ });
+ }
+
+ /**
+ * 插入对话记录
+ */
+ insertTalkRecord() {
+ let store = this.getStoreInstance();
+ let record = this.resource;
+ console.log("插入谈话记录", record);
+
+ record.float = this.getFloatType();
+
+ store.commit("PUSH_DIALOGUE", record);
+
+ // 获取聊天面板元素节点
+ let el = document.getElementById("lumenChatPanel");
+
+ // 判断的滚动条是否在底部
+ let isBottom = Math.ceil(el.scrollTop) + el.clientHeight >= el.scrollHeight;
+
+ if (
+ isBottom ||
+ record.userId == this.getAccountId() ||
+ record.fromUser == this.getAccountId()
+ ) {
+ Vue.nextTick(() => {
+ el.scrollTop = el.scrollHeight;
+ });
+ } else {
+ console.log("%c SET_TLAK_UNREAD_MESSAGE %c", "color:red");
+ store.commit("SET_TLAK_UNREAD_MESSAGE", {
+ content: this.getTalkText(),
+ nickname: record.name,
+ });
+ }
+ console.log("%c 准备更新...UPDATE_TALK_ITEM ", "color:red");
+
+ store.commit("UPDATE_TALK_ITEM", {
+ index_name: this.getIndexName(),
+ msg_text: this.getTalkText() || record.text,
+ updated_at: parseTime(new Date()),
+ });
+
+ if (this.talk_type == 1 && this.getAccountId() !== this.sender_id) {
+ console.log("%c 清除 未读数...ServeClearTalkUnreadNum ", "color:blue");
+ ServeClearTalkUnreadNum({
+ talk_type: 1,
+ receiver_id: this.sender_id,
+ });
+ }
+ }
+
+ /**
+ * 更新对话列表记录
+ */
+ updateTalkItem() {
+ console.log("%c 更新对话列表记录", "color:#32ccbc");
+ let store = this.getStoreInstance();
+
+ store.commit("INCR_UNREAD_NUM");
+
+ store.commit("UPDATE_TALK_MESSAGE", {
+ index_name: this.getIndexName(),
+ msg_text: this.getTalkText(),
+ updated_at: parseTime(new Date()),
+ });
+ }
+}
+
+export default Talk;
diff --git a/im/src/im-server/socket-instance.js b/im/src/im-server/socket-instance.js
new file mode 100644
index 00000000..d30b94d5
--- /dev/null
+++ b/im/src/im-server/socket-instance.js
@@ -0,0 +1,117 @@
+import store from "@/store";
+import config from "@/config/config";
+import WsSocket from "@/plugins/ws-socket";
+import { getToken } from "@/utils/auth";
+import { Notification } from "element-ui";
+
+// 引入消息处理类
+import KeyboardEvent from "@/im-server/event/keyboard";
+import LoginEvent from "@/im-server/event/login";
+import TalkEvent from "@/im-server/event/talk";
+import RevokeEvent from "@/im-server/event/revoke";
+import GroupJoinEvent from "@/im-server/event/group-join";
+import FriendApplyEvent from "@/im-server/event/friend-apply";
+
+/**
+ * SocketInstance 连接实例
+ *
+ * 注释: 所有 WebSocket 消息接收处理在此实例中处理
+ */
+class SocketInstance {
+ /**
+ * WsSocket 实例
+ */
+ socket;
+
+ /**
+ * SocketInstance 初始化实例
+ */
+ constructor() {
+ this.socket = new WsSocket(
+ () => {
+ return `${config.BASE_WS_URL}/` + getToken();
+ },
+ {
+ onError: (evt) => {
+ console.log("Websocket 连接失败回调方法");
+ },
+ // Websocket 连接成功回调方法
+ onOpen: (evt) => {
+ this.updateSocketStatus(true);
+ },
+ // Websocket 断开连接回调方法
+ onClose: (evt) => {
+ this.updateSocketStatus(false);
+ },
+ }
+ );
+
+ this.registerEvents();
+ }
+
+ // 连接 WebSocket 服务
+ connect() {
+ console.log("🔗连接 WebSocket");
+ this.socket.connection();
+ }
+
+ /**
+ * 注册回调消息处理事件
+ */
+ registerEvents() {
+ this.socket.on("event_talk", (data) => {
+ console.log("接收到消息,event_talk", data);
+ new TalkEvent(data).handle();
+ });
+
+ this.socket.on("event_online_status", (data) => {
+ new LoginEvent(data).handle();
+ });
+
+ this.socket.on("event_keyboard", (data) => {
+ console.log("推送", data);
+ new KeyboardEvent(data).handle();
+ });
+
+ this.socket.on("event_revoke_talk", (data) => {
+ new RevokeEvent(data).handle();
+ });
+
+ this.socket.on("event_friend_apply", (data) => {
+ new FriendApplyEvent(data).handle();
+ });
+
+ this.socket.on("join_group", (data) => {
+ new GroupJoinEvent(data).handle();
+ });
+
+ this.socket.on("event_error", (data) => {
+ Notification({
+ title: "友情提示",
+ message: data.message,
+ type: "warning",
+ });
+ });
+ }
+
+ /**
+ * 更新 WebSocket 连接状态
+ *
+ * @param {Boolean} status 连接状态
+ */
+ updateSocketStatus(status) {
+ store.commit("UPDATE_SOCKET_STATUS", status);
+ }
+
+ /**
+ * 推送消息
+ *
+ * @param {String} event 事件名
+ * @param {Object} data 数据
+ */
+ emit(event, data) {
+ this.socket.emit(event, data);
+ }
+}
+
+export default new SocketInstance();
diff --git a/im/src/im-server/util.js b/im/src/im-server/util.js
new file mode 100644
index 00000000..7ec6a9a6
--- /dev/null
+++ b/im/src/im-server/util.js
@@ -0,0 +1 @@
+// IM 助手
\ No newline at end of file
diff --git a/im/src/main.js b/im/src/main.js
new file mode 100644
index 00000000..71b59ede
--- /dev/null
+++ b/im/src/main.js
@@ -0,0 +1,41 @@
+import 'core-js/stable'
+import 'regenerator-runtime/runtime'
+
+import Vue from 'vue'
+import App from '@/App'
+import store from '@/store'
+import router from '@/router'
+import MainMixin from './mixins/main-mixin'
+import face from '@/components/face'
+import faceNull from '@/components/face-null'
+import config from "@/config/config";
+import './core/lazy-use'
+import './core/global-component'
+import './core/filter'
+import './core/directives'
+import '@/permission'
+import '@/icons'
+
+// 引入自定义全局css
+import '@/assets/css/global.less'
+
+Vue.config.productionTip = false
+Vue.mixin(MainMixin) // 引入mixins
+Vue.component('face',face)
+Vue.component('face-null',faceNull)
+
+Vue.prototype.linkToGoods = function (goodsId, skuId) { // 跳转买家端商品
+ console.log(`${config.PC_URL}/goodsDetail?skuId=${skuId}&goodsId=${goodsId}`)
+ window.open(`${config.PC_URL}/goodsDetail?skuId=${skuId}&goodsId=${goodsId}`, '_blank')
+};
+Vue.prototype.linkToStore = function (storeId) { // 跳转买家端商品
+ console.log(`${config.PC_URL}/Merchant?id=${storeId}`)
+ window.open(`${config.PC_URL}/Merchant?id=${storeId}`, '_blank')
+ };
+const Instance = new Vue({
+ router,
+ store,
+ mixins: [MainMixin],
+ render: h => h(App),
+}).$mount('#app')
+export default Instance
diff --git a/im/src/mixins/main-mixin.js b/im/src/mixins/main-mixin.js
new file mode 100644
index 00000000..5e2f3aba
--- /dev/null
+++ b/im/src/mixins/main-mixin.js
@@ -0,0 +1,125 @@
+import SocketInstance from "@/im-server/socket-instance";
+import { ServeGetUserSetting,ServeGetStoreSetting } from "@/api/user";
+import store from "@/store";
+import { ServeGetTalkList,ServeGetStoreTalkList } from "@/api/chat";
+import { formatTalkItem } from "@/utils/talk";
+export default {
+
+ created() {
+ // 判断用户是否登录
+
+ },
+ methods: {
+ // 页面初始化设置
+ initialize() {
+ SocketInstance.connect();
+ },
+
+ // 加载用户相关设置信息,更新本地缓存
+ loadUserSetting() {
+ //标识没有值,获取用户信息
+
+ if(this.$route.query.id){
+ ServeGetUserSetting().then(async ({ code, result }) => {
+ // 如果result有值说明用户创建成功
+ if (result) {
+ store.commit("UPDATE_USER_INFO", {
+ id: result.id,
+ face: result.face,
+ name: result.nickName,
+ });
+ console.log(result.nickName)
+ /**
+ * 用户像商家进行聊天,商家进行刷新好友列表
+ */
+ // 判断如果是有id说明是 用户像商家进行聊天。
+ if (this.$route.query.id) {
+ await this.createTalk(this.$route.query.id);
+ }
+ if(this.$route.query.goodsId && this.$route.query.skuId){
+ this.goodsParams.goodsId = this.$route.query.goodsId
+ this.goodsParams.skuId = this.$route.query.skuId
+ }
+ } else if (code === 200 && !result) {
+ setTimeout(() => {
+ this.loadUserSetting();
+ }, 2000);
+ }
+ });
+ }else{
+ //标识有值代表是店铺
+ ServeGetStoreSetting().then(async ({ code, result }) => {
+ if (result) {
+ store.commit("UPDATE_USER_INFO", {
+ id: result.id,
+ face: result.storeLogo,
+ name: result.storeName,
+ });
+ //获取店铺聊天列表
+ await this.loadStoreChatList()
+ }else if (code === 200 && !result) {
+ setTimeout(() => {
+ this.loadUserSetting();
+ }, 2000);
+ }
+ })
+ }
+ },
+
+
+ // 获取用户对话列表
+ loadChatListInJs() {
+
+ ServeGetTalkList()
+ .then(({ code, result }) => {
+ if (code !== 200) return false;
+ store.commit("SET_UNREAD_NUM", 0);
+ store.commit("SET_TALK_ITEMS", {
+ items: result.map((item) => formatTalkItem(item)),
+ });
+ let index_name = sessionStorage.getItem("send_message_index_name");
+ if (index_name) {
+ sessionStorage.removeItem("send_message_index_name");
+ }
+ })
+ .finally(() => {
+
+ });
+ },
+
+
+ //获取商家聊天记录
+ loadStoreChatList() {
+ this.loadStatus = this.talkNum == 0 ? 0 : 1;
+ ServeGetStoreTalkList().then(({ code, result }) => {
+ if (code !== 200) return false;
+ this.$store.commit("SET_UNREAD_NUM", 0);
+ this.$store.commit("SET_TALK_ITEMS", {
+ items: result.map((item) => formatTalkItem(item)),
+ });
+
+ // 判断
+ if (this.$route.query.id) {
+ let takeData, takeIndex;
+ console.log(result)
+ result.forEach((take, index) => {
+ if (take.id == this.$route.query.id) {
+ takeData = take;
+ takeIndex = index;
+ }
+ });
+ this.$nextTick(() =>
+ this.clickTab(this.$route.query.id, takeData, takeIndex)
+ );
+ }
+ }).finally(() => {
+ this.loadStatus = 1;
+ });
+ },
+
+
+ reload() {
+ this.$root.$children[0].refreshView();
+ },
+ },
+};
diff --git a/im/src/permission.js b/im/src/permission.js
new file mode 100644
index 00000000..d34eda34
--- /dev/null
+++ b/im/src/permission.js
@@ -0,0 +1,26 @@
+import router from '@/router'
+import NProgress from 'nprogress'
+import 'nprogress/nprogress.css'
+import config from '@/config/config'
+
+
+NProgress.configure({
+ showSpinner: false,
+})
+
+const WEBSITE_NAME = config.WEBSITE_NAME
+
+
+router.beforeEach((to, from, next) => {
+ document.title = to.meta.title
+ ? `${WEBSITE_NAME} | ${to.meta.title}`
+ : WEBSITE_NAME
+
+
+ NProgress.start()
+ next()
+})
+
+router.afterEach(() => {
+ NProgress.done()
+})
diff --git a/im/src/plugins/recorder/record-sdk.js b/im/src/plugins/recorder/record-sdk.js
new file mode 100644
index 00000000..45cb23dd
--- /dev/null
+++ b/im/src/plugins/recorder/record-sdk.js
@@ -0,0 +1,45 @@
+import Recorder from './recorder'
+
+export default class Record {
+ startRecord(param) {
+ let self = this
+ try {
+ Recorder.get(rec => {
+ if (rec.error) return param.error(rec.error)
+ self.recorder = rec
+ self.recorder.start()
+ param.success('开始录音')
+ })
+ } catch (e) {
+ param.error('开始录音失败' + e)
+ }
+ }
+
+ stopRecord(param) {
+ let self = this
+ try {
+ let blobData = self.recorder.getBlob()
+ param.success(blobData)
+ } catch (e) {
+ param.error('结束录音失败' + e)
+ }
+ }
+
+ play(audio) {
+ let self = this
+ try {
+ self.recorder.play(audio)
+ } catch (e) {
+ console.error('录音播放失败' + e)
+ }
+ }
+
+ clear(audio) {
+ let self = this
+ try {
+ self.recorder.clear(audio)
+ } catch (e) {
+ console.error('清空录音失败' + e)
+ }
+ }
+}
diff --git a/im/src/plugins/recorder/recorder.js b/im/src/plugins/recorder/recorder.js
new file mode 100644
index 00000000..0b1b1418
--- /dev/null
+++ b/im/src/plugins/recorder/recorder.js
@@ -0,0 +1,239 @@
+export default class Recorder {
+ constructor(stream, config) {
+ //兼容
+ window.URL = window.URL || window.webkitURL
+ navigator.getUserMedia =
+ navigator.getUserMedia ||
+ navigator.webkitGetUserMedia ||
+ navigator.mozGetUserMedia ||
+ navigator.msGetUserMedia
+
+ config = config || {}
+ config.sampleBits = config.sampleBits || 16 //采样数位 8, 16
+ config.sampleRate = config.sampleRate || 8000 //采样率(1/6 44100)
+
+ this.context = new (window.webkitAudioContext || window.AudioContext)()
+ this.audioInput = this.context.createMediaStreamSource(stream)
+ this.createScript =
+ this.context.createScriptProcessor || this.context.createJavaScriptNode
+ this.recorder = this.createScript.apply(this.context, [4096, 1, 1])
+
+ this.audioData = {
+ size: 0, //录音文件长度
+ buffer: [], //录音缓存
+ inputSampleRate: this.context.sampleRate, //输入采样率
+ inputSampleBits: 16, //输入采样数位 8, 16
+ outputSampleRate: config.sampleRate, //输出采样率
+ oututSampleBits: config.sampleBits, //输出采样数位 8, 16
+ input: function(data) {
+ this.buffer.push(new Float32Array(data))
+ this.size += data.length
+ },
+ compress: function() {
+ //合并压缩
+ //合并
+ let data = new Float32Array(this.size)
+ let offset = 0
+ for (let i = 0; i < this.buffer.length; i++) {
+ data.set(this.buffer[i], offset)
+ offset += this.buffer[i].length
+ }
+ //压缩
+ let compression = parseInt(this.inputSampleRate / this.outputSampleRate)
+ let length = data.length / compression
+ let result = new Float32Array(length)
+ let index = 0,
+ j = 0
+ while (index < length) {
+ result[index] = data[j]
+ j += compression
+ index++
+ }
+ return result
+ },
+ encodeWAV: function() {
+ let sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate)
+ let sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits)
+ let bytes = this.compress()
+ let dataLength = bytes.length * (sampleBits / 8)
+ let buffer = new ArrayBuffer(44 + dataLength)
+ let data = new DataView(buffer)
+
+ let channelCount = 1 //单声道
+ let offset = 0
+
+ let writeString = function(str) {
+ for (let i = 0; i < str.length; i++) {
+ data.setUint8(offset + i, str.charCodeAt(i))
+ }
+ }
+
+ // 资源交换文件标识符
+ writeString('RIFF')
+ offset += 4
+ // 下个地址开始到文件尾总字节数,即文件大小-8
+ data.setUint32(offset, 36 + dataLength, true)
+ offset += 4
+ // WAV文件标志
+ writeString('WAVE')
+ offset += 4
+ // 波形格式标志
+ writeString('fmt ')
+ offset += 4
+ // 过滤字节,一般为 0x10 = 16
+ data.setUint32(offset, 16, true)
+ offset += 4
+ // 格式类别 (PCM形式采样数据)
+ data.setUint16(offset, 1, true)
+ offset += 2
+ // 通道数
+ data.setUint16(offset, channelCount, true)
+ offset += 2
+ // 采样率,每秒样本数,表示每个通道的播放速度
+ data.setUint32(offset, sampleRate, true)
+ offset += 4
+ // 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8
+ data.setUint32(
+ offset,
+ channelCount * sampleRate * (sampleBits / 8),
+ true
+ )
+ offset += 4
+ // 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8
+ data.setUint16(offset, channelCount * (sampleBits / 8), true)
+ offset += 2
+ // 每样本数据位数
+ data.setUint16(offset, sampleBits, true)
+ offset += 2
+ // 数据标识符
+ writeString('data')
+ offset += 4
+ // 采样数据总数,即数据总大小-44
+ data.setUint32(offset, dataLength, true)
+ offset += 4
+ // 写入采样数据
+ if (sampleBits === 8) {
+ for (let i = 0; i < bytes.length; i++, offset++) {
+ let s = Math.max(-1, Math.min(1, bytes[i]))
+ let val = s < 0 ? s * 0x8000 : s * 0x7fff
+ val = parseInt(255 / (65535 / (val + 32768)))
+ data.setInt8(offset, val, true)
+ }
+ } else {
+ for (let i = 0; i < bytes.length; i++, offset += 2) {
+ let s = Math.max(-1, Math.min(1, bytes[i]))
+ data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true)
+ }
+ }
+ return new Blob([data], {
+ type: 'audio/wav',
+ })
+ },
+ }
+ }
+
+ //开始录音
+ start() {
+ this.audioInput.connect(this.recorder)
+ this.recorder.connect(this.context.destination)
+
+ //音频采集
+ let self = this
+ this.recorder.onaudioprocess = function(e) {
+ self.audioData.input(e.inputBuffer.getChannelData(0))
+ }
+ }
+
+ //停止
+ stop() {
+ this.recorder.disconnect()
+ }
+
+ //获取音频文件
+ getBlob() {
+ this.stop()
+ return this.audioData.encodeWAV()
+ }
+
+ //回放
+ play(audio) {
+ audio.src = window.URL.createObjectURL(this.getBlob())
+ }
+
+ //清理缓存的录音数据
+ clear(audio) {
+ this.audioData.buffer = []
+ this.audioData.size = 0
+ audio.src = ''
+ }
+
+ static checkError(e) {
+ const { name } = e
+ let errorMsg = ''
+ switch (name) {
+ case 'AbortError':
+ errorMsg = '录音设备无法被使用'
+ break
+ case 'NotAllowedError':
+ errorMsg = '用户已禁止网页调用录音设备'
+ break
+ case 'PermissionDeniedError':
+ errorMsg = '用户已禁止网页调用录音设备'
+ break // 用户拒绝
+ case 'NotFoundError':
+ errorMsg = '录音设备未找到'
+ break
+ case 'DevicesNotFoundError':
+ errorMsg = '录音设备未找到'
+ break
+ case 'NotReadableError':
+ errorMsg = '录音设备无法使用'
+ break
+ case 'NotSupportedError':
+ errorMsg = '不支持录音功能'
+ break
+ case 'MandatoryUnsatisfiedError':
+ errorMsg = '无法发现指定的硬件设备'
+ break
+ default:
+ errorMsg = '录音调用错误'
+ break
+ }
+ return {
+ error: errorMsg,
+ }
+ }
+
+ static get(callback, config) {
+ if (callback) {
+ if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
+ navigator.mediaDevices
+ .getUserMedia({
+ audio: true,
+ video: false,
+ })
+ .then(stream => {
+ let rec = new Recorder(stream, config)
+ callback(rec)
+ })
+ .catch(e => {
+ callback(Recorder.checkError(e))
+ })
+ } else {
+ navigator
+ .getUserMedia({
+ audio: true,
+ video: false,
+ })
+ .then(stream => {
+ let rec = new Recorder(stream, config)
+ callback(rec)
+ })
+ .catch(e => {
+ // Recorder.checkError(e)
+ callback(Recorder.checkError(e))
+ })
+ }
+ }
+ }
+}
diff --git a/im/src/plugins/sms-lock.js b/im/src/plugins/sms-lock.js
new file mode 100644
index 00000000..13febcf0
--- /dev/null
+++ b/im/src/plugins/sms-lock.js
@@ -0,0 +1,76 @@
+/**
+ * 短信倒计时锁
+ */
+class SmsLock {
+ // 发送倒计时默认60秒
+ time = null
+
+ // 计时器
+ timer = null
+
+ // 倒计时默认60秒
+ lockTime = 60
+
+ // 锁标记名称
+ lockName = ''
+
+ /**
+ * 实例化构造方法
+ *
+ * @param {String} purpose 唯一标识
+ * @param {Number} time
+ */
+ constructor(purpose, lockTime = 60) {
+ this.lockTime = lockTime
+ this.lockName = `SMSLOCK_${purpose}`
+
+ this.init()
+ }
+
+ // 开始计时
+ start(time = null) {
+ this.time = time == null || time >= this.lockTime ? this.lockTime : time
+
+ this.clearInterval()
+
+ this.timer = setInterval(() => {
+ if (this.time == 0) {
+ this.clearInterval()
+ this.time = null
+ localStorage.removeItem(this.lockName)
+ return
+ }
+
+ this.time--
+
+ // 设置本地缓存
+ localStorage.setItem(this.lockName, this.getTime() + this.time)
+ }, 1000)
+ }
+
+ // 页面刷新初始化
+ init() {
+ let result = localStorage.getItem(this.lockName)
+
+ if (result == null) return
+
+ let time = result - this.getTime()
+ if (time > 0) {
+ this.start(time)
+ } else {
+ localStorage.removeItem(this.lockName)
+ }
+ }
+
+ // 获取当前时间
+ getTime() {
+ return Math.floor(new Date().getTime() / 1000)
+ }
+
+ // 清除计时器
+ clearInterval() {
+ clearInterval(this.timer)
+ }
+}
+
+export default SmsLock
diff --git a/im/src/plugins/ws-socket.js b/im/src/plugins/ws-socket.js
new file mode 100644
index 00000000..f6124410
--- /dev/null
+++ b/im/src/plugins/ws-socket.js
@@ -0,0 +1,275 @@
+class WsSocket {
+ /**
+ * Websocket 连接
+ *
+ * @var Websocket
+ */
+ connect;
+
+ /**
+ * 服务器连接地址
+ */
+ url;
+
+ /**
+ * 配置信息
+ *
+ * @var Object
+ */
+ config = {
+ heartbeat: {
+ enabled: false, // 是否发送心跳包
+ time: 10000, // 心跳包发送间隔时长
+ setInterval: null, // 心跳包计时器
+ },
+ reconnect: {
+ lockReconnect: false,
+ setTimeout: null, // 计时器对象
+ time: 5000, // 重连间隔时间
+ number: 1000, // 重连次数
+ },
+ };
+
+ /**
+ * 自定义绑定消息事件
+ *
+ * @var Array
+ */
+ onCallBacks = [];
+
+ /**
+ * 创建 WsSocket 的实例
+ *
+ * @param {Function} urlCallBack url闭包函数
+ * @param {Object} events 原生 WebSocket 绑定事件
+ */
+ constructor(urlCallBack, events) {
+ this.urlCallBack = urlCallBack;
+
+ // 定义 WebSocket 原生方法
+ this.events = Object.assign(
+ {
+ onError: (evt) => {},
+ onOpen: (evt) => {},
+ onClose: (evt) => {},
+ },
+ events
+ );
+ }
+
+ /**
+ * 事件绑定
+ *
+ * @param {String} event 事件名
+ * @param {Function} callBack 回调方法
+ */
+ on(event, callBack) {
+ // 对应 socket-instance.js
+ console.log("事件绑定", event, callBack);
+ this.onCallBacks[event] = callBack;
+ return this;
+ }
+
+ /**
+ * 加载 WebSocket
+ */
+ loadSocket() {
+ // 判断当前是否已经连接
+ if (this.connect != null) {
+ this.connect.close();
+ this.connect = null;
+ }
+
+ this.url = this.urlCallBack();
+ const connect = new WebSocket(this.url);
+ connect.onerror = this.onError.bind(this);
+ connect.onopen = this.onOpen.bind(this);
+ connect.onmessage = this.onMessage.bind(this);
+ connect.onclose = this.onClose.bind(this);
+
+ this.connect = connect;
+ }
+
+ /**
+ * 连接 Websocket
+ */
+ connection() {
+ this.loadSocket();
+ }
+
+ /**
+ * 掉线重连 Websocket
+ */
+ reconnect() {
+ console.log("掉线重连接");
+ let reconnect = this.config.reconnect;
+ if (reconnect.lockReconnect || reconnect.number == 0) {
+ return;
+ }
+
+ this.config.reconnect.lockReconnect = true;
+
+ // 没连接上会一直重连,设置延迟避免请求过多
+ reconnect.setTimeout && clearTimeout(reconnect.setTimeout);
+
+ this.config.reconnect.setTimeout = setTimeout(() => {
+ this.connection();
+
+ this.config.reconnect.lockReconnect = false;
+ this.config.reconnect.number--;
+
+ console.log(
+ `网络连接已断开,正在尝试重新连接(${this.config.reconnect.number})...`
+ );
+ }, reconnect.time);
+ }
+
+ /**
+ * 解析接受的消息
+ *
+ * @param {Object} evt Websocket 消息
+ */
+ onParse(evt) {
+
+ const res = JSON.parse(evt.data).result;
+
+ //如果创建时间是时间戳类型则转换为 日期类型,否则新压入栈的消息的创建时间和从数据库读取出来的创建时间格式对不上,处理的时候会出异常。
+ if (typeof res.createTime == "number") {
+ res.createTime = this.unixToDate(res.createTime, "yyyy-MM-dd hh:mm");
+ }
+ return res;
+ }
+
+ /**
+ * 将unix时间戳转换为指定格式
+ * @param unix 时间戳【秒】
+ * @param format 转换格式
+ * @returns {*|string}
+ */
+ unixToDate(unix, format) {
+ if (!unix) return unix;
+ let _format = format || "yyyy-MM-dd hh:mm:ss";
+ const d = new Date(unix);
+ const o = {
+ "M+": d.getMonth() + 1,
+ "d+": d.getDate(),
+ "h+": d.getHours(),
+ "m+": d.getMinutes(),
+ "s+": d.getSeconds(),
+ "q+": Math.floor((d.getMonth() + 3) / 3),
+ S: d.getMilliseconds(),
+ };
+ if (/(y+)/.test(_format))
+ _format = _format.replace(
+ RegExp.$1,
+ (d.getFullYear() + "").substr(4 - RegExp.$1.length)
+ );
+ for (const k in o)
+ if (new RegExp("(" + k + ")").test(_format))
+ _format = _format.replace(
+ RegExp.$1,
+ RegExp.$1.length === 1
+ ? o[k]
+ : ("00" + o[k]).substr(("" + o[k]).length)
+ );
+ return _format;
+ }
+
+ /**
+ * 打开连接
+ *
+ * @param {Object} evt Websocket 消息
+ */
+ onOpen(evt) {
+ this.events.onOpen(evt);
+
+ if (this.config.heartbeat.enabled) {
+ this.heartbeat();
+ }
+ }
+
+ /**
+ * 关闭连接
+ *
+ * @param {Object} evt Websocket 消息
+ */
+ onClose(evt) {
+ console.log("关闭连接", evt);
+ if (this.config.heartbeat.enabled) {
+ clearInterval(this.config.heartbeat.setInterval);
+ }
+ console.log("evt", evt);
+
+ if (evt.code == 1006) {
+ this.reconnect();
+ }
+
+ // this.events.onClose(evt);
+ }
+
+ /**
+ * 连接错误
+ *
+ * @param {Object} evt Websocket 消息
+ */
+ onError(evt) {
+ this.events.onError(evt);
+ }
+
+ /**
+ * 接收消息
+ *
+ * @param {Object} evt Websocket 消息
+ */
+ onMessage(evt) {
+ let result = this.onParse(evt);
+ console.log("接收消息", result, "color:red");
+ // 判断消息事件是否被绑定
+ // event_talk;
+
+ // 指定推送消息
+ this.onCallBacks["event_talk"](result);
+ }
+
+ /**
+ * WebSocket心跳检测
+ */
+ heartbeat() {
+ console.log("WebSocket心跳检测");
+ this.config.heartbeat.setInterval = setInterval(() => {
+ this.connect.send("PING");
+ }, this.config.heartbeat.time);
+ }
+
+ /**
+ * 聊天发送数据
+ *
+ * @param {Object} message
+ */
+ send(message) {
+ this.connect.send(JSON.stringify(message));
+ }
+
+ /**
+ * 关闭连接
+ */
+ close() {
+ this.connect.close();
+ }
+
+ /**
+ * 推送消息
+ *
+ * @param {String} event 事件名
+ * @param {Object} data 数据
+ */
+ emit(event, data) {
+ if (this.connect && this.connect.readyState === 1) {
+ this.connect.send(JSON.stringify(data));
+ } else {
+ console.error("WebSocket 连接已关闭...", this.connect);
+ }
+ }
+}
+
+export default WsSocket;
diff --git a/im/src/router/contacts.js b/im/src/router/contacts.js
new file mode 100644
index 00000000..38d2f1c5
--- /dev/null
+++ b/im/src/router/contacts.js
@@ -0,0 +1,24 @@
+export default {
+ path: '/contacts',
+ name: 'contacts',
+ redirect: '/contacts/apply',
+ component: () => import('@/views/contacts/layout'),
+ children: [
+ {
+ path: '/contacts/apply',
+ meta: {
+ title: '我的联系人',
+ needLogin: true,
+ },
+ component: () => import('@/views/contacts/apply'),
+ },
+ {
+ path: '/contacts/friends',
+ meta: {
+ title: '我的好友',
+ needLogin: true,
+ },
+ component: () => import('@/views/contacts/friends'),
+ },
+ ],
+}
diff --git a/im/src/router/index.js b/im/src/router/index.js
new file mode 100644
index 00000000..930be40b
--- /dev/null
+++ b/im/src/router/index.js
@@ -0,0 +1,44 @@
+import Vue from 'vue'
+import Router from 'vue-router'
+
+Vue.use(Router)
+
+const RouteView = {
+ name: 'RouteView',
+ render: h => h('router-view'),
+}
+
+const routes = [
+ {
+ path: '/',
+ name: 'home',
+ component: () => import('@/views/message/index'),
+ meta: {
+ title: '',
+ needLogin: true,
+ },
+ },
+ {
+ path: '/message',
+ name: 'message',
+ component: () => import('@/views/message/index'),
+ meta: {
+ title: '消息通知',
+ needLogin: true,
+ },
+ },
+ {
+ path: '*',
+ name: '404 NotFound',
+ component: () => import('@/views/other/404'),
+ meta: {
+ title: '404 NotFound',
+ needLogin: false,
+ },
+ },
+]
+
+export default new Router({
+ routes,
+ mode: 'history',
+})
diff --git a/im/src/router/settings.js b/im/src/router/settings.js
new file mode 100644
index 00000000..4c0525ad
--- /dev/null
+++ b/im/src/router/settings.js
@@ -0,0 +1,52 @@
+export default {
+ path: "/settings",
+ name: "settings",
+ meta: {
+ title: "个人设置",
+ needLogin: true,
+ },
+ redirect: "/settings/detail",
+ component: () => import("@/views/settings/layout"),
+ children: [
+ {
+ path: "/settings/detail",
+ meta: {
+ title: "个人信息",
+ needLogin: true,
+ },
+ component: () => import("@/views/settings/detail"),
+ },
+ {
+ path: "/settings/security",
+ meta: {
+ title: "安全设置",
+ needLogin: true,
+ },
+ component: () => import("@/views/settings/security"),
+ },
+ {
+ path: "/settings/binding",
+ meta: {
+ title: "账户绑定",
+ needLogin: true,
+ },
+ component: () => import("@/views/settings/binding"),
+ },
+ {
+ path: "/settings/personalize",
+ meta: {
+ title: "个性化设置",
+ needLogin: true,
+ },
+ component: () => import("@/views/settings/personalize"),
+ },
+ {
+ path: "/settings/notification",
+ meta: {
+ title: "消息设置",
+ needLogin: true,
+ },
+ component: () => import("@/views/settings/notification"),
+ },
+ ],
+};
diff --git a/im/src/store/getters.js b/im/src/store/getters.js
new file mode 100644
index 00000000..c912f3bd
--- /dev/null
+++ b/im/src/store/getters.js
@@ -0,0 +1,9 @@
+const getters = {
+ // 用户登录状态
+ loginStatus: state => state.user.loginStatus,
+
+ // socket 连接状态
+ socketStatus: state => state.socketStatus,
+}
+
+export default getters
diff --git a/im/src/store/index.js b/im/src/store/index.js
new file mode 100644
index 00000000..b4995740
--- /dev/null
+++ b/im/src/store/index.js
@@ -0,0 +1,33 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+
+import user from './modules/user'
+import talks from './modules/talk'
+import notify from './modules/notify'
+import settings from './modules/settings'
+import emoticon from './modules/emoticon'
+import dialogue from './modules/dialogue'
+import note from './modules/note'
+
+import state from './state'
+import getters from './getters'
+import mutations from './mutations'
+
+Vue.use(Vuex)
+
+const store = new Vuex.Store({
+ modules: {
+ user,
+ notify,
+ talks,
+ settings,
+ emoticon,
+ dialogue,
+ note,
+ },
+ state,
+ getters,
+ mutations,
+})
+
+export default store
diff --git a/im/src/store/modules/dialogue.js b/im/src/store/modules/dialogue.js
new file mode 100644
index 00000000..3c8cb372
--- /dev/null
+++ b/im/src/store/modules/dialogue.js
@@ -0,0 +1,89 @@
+export default {
+ state: {
+ // 对话来源[1:私聊;2:群聊]
+ talk_type: 0,
+
+ // 接收者ID
+ receiver_id: 0,
+
+ is_robot: 0,
+
+ // 聊天记录
+ records: [
+ {
+ id: "",
+ createTime: "",
+ toUser: "",
+ isRead: false,
+ messageType: "",
+ talkId: "",
+ text: "",
+ float: "",
+ },
+ ],
+
+ // 对话索引(聊天对话的唯一索引)
+ index_name: null,
+ },
+ mutations: {
+ // 更新对话
+ UPDATE_DIALOGUE_MESSAGE(state, resource) {
+ state.records = [];
+ state.talk_type = parseInt(resource.talk_type);
+ state.receiver_id = parseInt(resource.receiver_id);
+ state.is_robot = parseInt(resource.is_robot);
+
+ /**
+ * receiver_id 就是 好友id
+ * 比如 a 和 b 聊天 receiver_id = b的id
+ */
+ state.index_name = (resource.talk_type || 1) + "_" + resource.receiver_id;
+ },
+
+ // 数组头部压入对话记录1494593861786271744 1494593778193793024
+ UNSHIFT_DIALOGUE(state, records) {
+ // console.log("%c 数组头部压入对话记录", "color:green");
+ // console.log("state", state);
+ // console.log("records", records);
+ if(state.records.length>0){
+ state.records.unshift(...records);
+ }else{
+ state.records.push(...records);
+ }
+
+ console.log("最后的数据",state.records)
+ },
+
+ // 推送对话记录
+ PUSH_DIALOGUE(state, record) {
+ state.records.push(record);
+ },
+
+ // 更新对话记录
+ UPDATE_DIALOGUE(state, resource) {
+ for (let i in state.records) {
+ if (state.records[i].id === resource.id) {
+ Object.assign(state.records[i], resource);
+ break;
+ }
+ }
+ },
+
+ // 删除对话记录
+ DELETE_DIALOGUE(state, index) {
+ state.records.splice(index, 1);
+ },
+
+ BATCH_DELETE_DIALOGUE(state, ids) {
+ ids.forEach((record_id) => {
+ let index = state.records.findIndex((item) => item.id == record_id);
+ if (index >= 0) state.records.splice(index, 1);
+ });
+ },
+
+ // 数组头部压入对话记录
+ SET_DIALOGUE(state, records) {
+ state.records = records;
+ },
+ },
+};
diff --git a/im/src/store/modules/emoticon.js b/im/src/store/modules/emoticon.js
new file mode 100644
index 00000000..f4a841fc
--- /dev/null
+++ b/im/src/store/modules/emoticon.js
@@ -0,0 +1,91 @@
+import { ServeFindUserEmoticon, ServeUploadEmoticon } from "@/api/emoticon";
+
+import { ServeCollectEmoticon } from "@/api/chat";
+
+import { Notification } from "element-ui";
+
+export default {
+ state: {
+ items: [
+ ],
+ },
+ mutations: {
+ // 加载用户表情包
+ LOAD_USER_EMOTICON(state) {
+ // TODO 隐藏
+ return;
+ ServeFindUserEmoticon().then((res) => {
+ if (res.code == 200) {
+ const { collect_emoticon, sys_emoticon } = res.data;
+
+ state.items = state.items.slice(0, 2);
+
+ // 用户收藏的系统表情包
+ state.items[1].list = collect_emoticon;
+
+ // 用户添加的系统表情包
+ state.items.push(...sys_emoticon);
+ }
+ });
+ },
+
+ // 收藏用户表情包
+ SAVE_USER_EMOTICON(state, resoure) {
+ ServeCollectEmoticon({
+ record_id: resoure.record_id,
+ })
+ .then((res) => {
+ if (res.code == 200) {
+ Notification({
+ title: "收藏提示",
+ message: "表情包收藏成功...",
+ type: "success",
+ });
+
+ this.commit("LOAD_USER_EMOTICON");
+ }
+ })
+ .catch(() => {
+ Notification({
+ title: "收藏提示",
+ message: "表情包收藏失败...",
+ type: "warning",
+ });
+ });
+ },
+
+ // 自定义上传用户表情包
+ UPLOAD_USER_EMOTICON(state, resoure) {
+ let fileData = new FormData();
+ fileData.append("emoticon", resoure.file);
+ ServeUploadEmoticon(fileData)
+ .then((res) => {
+ if (res.code == 200) {
+ state.items[1].list.push(res.data);
+ }
+ })
+ .catch(() => {
+ Notification({
+ message: "网络异常请稍后再试...",
+ type: "error",
+ duration: 3000,
+ });
+ });
+ },
+
+ // 添加系统表情包
+ APPEND_SYS_EMOTICON(state, resoure) {
+ state.items.push(resoure);
+ },
+
+ // 移除系统表情包
+ REMOVE_SYS_EMOTICON(state, resoure) {
+ for (let i in state.items) {
+ if (state.items[i].emoticon_id === resoure.emoticon_id) {
+ state.items.splice(i, 1);
+ break;
+ }
+ }
+ },
+ },
+};
diff --git a/im/src/store/modules/note.js b/im/src/store/modules/note.js
new file mode 100644
index 00000000..cd9ee769
--- /dev/null
+++ b/im/src/store/modules/note.js
@@ -0,0 +1,18 @@
+const Note = {
+ state: {
+ tags: [],
+ class: [],
+ },
+ getters: {},
+ mutations: {
+ SET_NOTE_TAGS(state, tags) {
+ state.tags = tags
+ },
+
+ PUSH_NOTE_TAG(state, tag) {
+ state.tags.unshift(tag)
+ },
+ },
+}
+
+export default Note
diff --git a/im/src/store/modules/notify.js b/im/src/store/modules/notify.js
new file mode 100644
index 00000000..6fb769b0
--- /dev/null
+++ b/im/src/store/modules/notify.js
@@ -0,0 +1,62 @@
+export default {
+ state: {
+ // 聊天消息未读数
+ unreadNum: 0,
+
+ // 好友申请未读数
+ applyNum: 0,
+
+ // 好友键盘事件监听
+ inputEvent: 0,
+
+ // 好友登录状态监听
+ friendStatus: {
+ // 登录状态[0:下线;1:在线;]
+ status: 0,
+ // 好友ID
+ friend_id: 0,
+ },
+ },
+ mutations: {
+ // 消息未读数自增
+ INCR_UNREAD_NUM(state) {
+ console.log("触发消息未读")
+ state.unreadNum++
+ },
+
+ // 好友申请事件监听
+ INCR_APPLY_NUM(state) {
+ state.applyNum++
+ },
+
+ // 设置消息未读数
+ SET_UNREAD_NUM(state, value) {
+ state.unreadNum = value
+ },
+
+ // 好友申请事件监听
+ SET_APPLY_NUM(state, value) {
+ state.applyNum = value
+ },
+
+ // 自增好友键盘输入事件
+ UPDATE_KEYBOARD_EVENT(state) {
+ state.inputEvent++
+ },
+
+ // 更新好友登录状态
+ UPDATE_FRIEND_STATUS(state, value) {
+ state.friendStatus = value
+ },
+ },
+ actions: {
+ ACT_UPDATE_FRIEND_STATUS({ commit }, value) {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ commit('UPDATE_FRIEND_STATUS', value)
+ resolve()
+ }, 0)
+ })
+ },
+ },
+}
diff --git a/im/src/store/modules/settings.js b/im/src/store/modules/settings.js
new file mode 100644
index 00000000..49c120fd
--- /dev/null
+++ b/im/src/store/modules/settings.js
@@ -0,0 +1,66 @@
+import {
+ getToken,
+ getUserSettingCache,
+ setUserSettingCache,
+} from '@/utils/auth'
+
+let state = {
+ // 主题模式 true:全屏模式 false:居中模式
+ themeMode: true,
+
+ // 主题模式为居中模式下,body 的背景图片
+ themeBagImg: 'bag003',
+
+ // 主题颜色
+ themeColor: '',
+
+ // 消息提示音
+ notifyCueTone: true,
+
+ // 键盘输入事件消息推送开关
+ keyboardEventNotify: true,
+}
+
+if (getToken()) {
+ Object.assign(state, getUserSettingCache())
+}
+
+/**
+ * 用户相关设置
+ */
+const Settings = {
+ state,
+ mutations: {
+ // 设置主题模式
+ SET_THEME_MODE(state, mode) {
+ state.themeMode = mode
+ setUserSettingCache(state)
+ },
+
+ // 设置主题的背景图片
+ SET_THEME_BAGIMG(state, bagName) {
+ state.themeBagImg = bagName
+ setUserSettingCache(state)
+ },
+
+ // 主题颜色
+ SET_THEME_COLOR(state, color) {
+ state.themeColor = color
+ setUserSettingCache(state)
+ },
+
+ // 设置消息提示音状态
+ SET_NOTIFY_CUE_TONE(state, isTrue) {
+ state.notifyCueTone = isTrue
+ setUserSettingCache(state)
+ },
+
+ // 设置键盘输入事件消息推送状态
+ SET_KEYBOARD_EVENT_NOTIFY(state, isTrue) {
+ state.keyboardEventNotify = isTrue
+ setUserSettingCache(state)
+ },
+ },
+}
+
+export default Settings
diff --git a/im/src/store/modules/talk.js b/im/src/store/modules/talk.js
new file mode 100644
index 00000000..8a7ae4ef
--- /dev/null
+++ b/im/src/store/modules/talk.js
@@ -0,0 +1,132 @@
+import { getSort, getMutipSort } from "@/utils/functions";
+import { formatTalkItem } from "@/utils/talk";
+import { ServeGetTalkList } from "@/api/chat";
+import store from '@/store/index.js'
+import Vue from 'vue'
+const Talk = {
+ state: {
+ // 用户对话列表
+ items: [],
+
+ // 最后一条消息
+ unreadMessage: {
+ num: 0,
+ nickname: "未知",
+ content: "...",
+ },
+
+ // 对话列表重载状态
+ heavyLoad: false,
+ },
+ getters: {
+ // 过滤所有置顶对话列表
+ topItems: (state) => {
+ return state.items.filter((item) => item.is_top == 1);
+ },
+ talkItems: (state) => {
+ return state.items.sort(
+ getMutipSort([getSort((a, b) => a.lastTalkTime > b.lastTalkTime)])
+ );
+ },
+ // 消息未读数总计
+ unreadNum: (state) => {
+ return state.items.reduce((total, item) => {
+ return total + parseInt(item.unread);
+ }, 0);
+ },
+ talkNum: (state) => state.items.length,
+ },
+ mutations: {
+ // 设置对话列表
+ SET_TALK_ITEMS(state, resource) {
+ console.log("设置对话列表", resource.items);
+ Vue.set(state,'items',resource.items)
+ },
+
+ // 更新对话节点
+ UPDATE_TALK_ITEM(state, resource) {
+ console.log("%c 更新对话节点", "color:#32c");
+ console.log("state", state);
+ console.log("resource", resource);
+
+ console.log("%c 更新对话节点结束", "color:#32c",state.items);
+ let index = state.items.findIndex(
+ (item) => item.userId === resource.index_name.split("_")[1]
+ );
+ if (index >= 0) {
+ Object.assign(state.items[index], resource);
+ }
+ },
+
+ // 新增对话节点
+ PUSH_TALK_ITEM(state, resource) {
+ console.log(state)
+ state.items.push(resource);
+ },
+
+ // 移除对话节点
+ REMOVE_TALK_ITEM(state, index_name) {
+ for (let i in state.items) {
+ if (state.items[i].index_name === index_name) {
+ state.items.splice(i, 1);
+ break;
+ }
+ }
+ },
+
+ // async getTalkList() {
+ // let { code, result } = ServeGetTalkList();
+ // if (code !== 200) return false;
+
+ // store.commit("SET_TALK_ITEMS", {
+ // items: result.map((item) => formatTalkItem(item)),
+ // });
+ // },
+
+ // 更新对话消息
+ UPDATE_TALK_MESSAGE(state, resource) {
+ console.log("%c 更新对话消息", "color:green");
+
+
+ console.log("state", state);
+ console.log("resource", resource);
+ console.log("%c 更新对话结束", "color:green",state.items);
+
+ let enableGetTalkList = true
+ state.items.forEach(item=>{
+ if(item.userId == resource.index_name.split("_")[1]){
+ item.unread++;
+ item.msg_text = resource.msg_text;
+ item.lastTalkTime = resource.updated_at;
+ item.lastTalkMessage = resource.msg_text;
+ item.updated_at = resource.updated_at;
+ enableGetTalkList = false
+ }
+ })
+ // 循环如果当前用户不在对话记录列表中 就重新请求对话列表接口
+ enableGetTalkList ? this.commit('getTalkList'):''
+ },
+
+ // 触发对话列表重新加载
+ TRIGGER_TALK_ITEMS_LOAD(state, status = false) {
+ state.heavyLoad = status;
+ },
+
+ SET_TLAK_UNREAD_MESSAGE(state, resource) {
+ state.unreadMessage.num++;
+ state.unreadMessage.nickname = resource.nickname;
+ state.unreadMessage.content = resource.content;
+ },
+
+ // 清除最后一条未读消息
+ CLEAR_TLAK_UNREAD_MESSAGE(state) {
+ state.unreadMessage = {
+ num: 0,
+ nickname: "未知",
+ content: "...",
+ };
+ },
+ },
+};
+
+export default Talk;
diff --git a/im/src/store/modules/user.js b/im/src/store/modules/user.js
new file mode 100644
index 00000000..995c4566
--- /dev/null
+++ b/im/src/store/modules/user.js
@@ -0,0 +1,73 @@
+import { setUserInfo, getUserInfo, getToken } from "@/utils/auth";
+
+// import { ServeLogout } from "@/api/user";
+
+let state = {
+ // 用户ID
+ id: 0,
+ // 用户昵称
+ name: "",
+ // 个性头像
+ face: require("@/assets/image/detault-avatar.jpg"),
+ // 名片背景
+ visitCardBag: require("@/assets/image/default-user-banner.png"),
+ // 当前登录状态
+ loginStatus: false,
+ toUser:""
+};
+
+// 判断用户是否登录
+if (getToken()) {
+ let userInfo = getUserInfo();
+ console.error(userInfo)
+ state.name = userInfo.name;
+ state.id = userInfo.id;
+ state.face = userInfo.face ? userInfo.face : state.avatar;
+ state.loginStatus = true;
+}
+
+const User = {
+ state,
+ mutations: {
+ // 用户退出登录
+ USER_LOGOUT(state) {
+ state.id = 0;
+ state.face = "";
+ state.name = "";
+ state.loginStatus = false;
+ },
+
+ // 设置用户登录状态
+ UPDATE_LOGIN_STATUS(state) {
+ state.loginStatus = true;
+ },
+
+ // 更新用户信息
+ UPDATE_USER_INFO(state, data) {
+ for (const key in data) {
+ if (state.hasOwnProperty(key)) {
+ state[key] = data[key];
+ }
+ }
+
+ // 保存用户信息到缓存
+ setUserInfo({
+ id: state.id,
+ face: state.face,
+ name: state.name,
+ });
+ },
+ },
+ actions: {
+ // 退出登录处理操作
+ ACT_USER_LOGOUT({ commit }) {
+ commit("USER_LOGOUT");
+ // ServeLogout().finally(() => {
+ // removeAll();
+ // location.reload();
+ // });
+ },
+ },
+};
+
+export default User;
diff --git a/im/src/store/mutations.js b/im/src/store/mutations.js
new file mode 100644
index 00000000..fac40256
--- /dev/null
+++ b/im/src/store/mutations.js
@@ -0,0 +1,9 @@
+// 根级别的 mutation
+const mutations = {
+ // 更新socket 连接状态
+ UPDATE_SOCKET_STATUS(state, status) {
+ state.socketStatus = status
+ },
+}
+
+export default mutations
diff --git a/im/src/store/state.js b/im/src/store/state.js
new file mode 100644
index 00000000..e571ecaf
--- /dev/null
+++ b/im/src/store/state.js
@@ -0,0 +1,13 @@
+const defaultAvatar = require('@/assets/image/detault-avatar.jpg')
+
+// 根级别的 state
+const state = {
+ socketStatus: false,
+ website_name: process.env.VUE_APP_WEBSITE_NAME,
+ copyright: `©2020 - 2021 ${process.env.VUE_APP_WEBSITE_NAME} 在线聊天 Github源码`,
+
+ // 头像加载失败后的默认头像
+ defaultAvatar: "this.src='" + defaultAvatar + "'",
+}
+
+export default state
diff --git a/im/src/utils/auth.js b/im/src/utils/auth.js
new file mode 100644
index 00000000..487f40f0
--- /dev/null
+++ b/im/src/utils/auth.js
@@ -0,0 +1,67 @@
+import JsBase64 from 'js-base64'
+
+const USER_TOKEN = 'LILI-TOKEN'
+const USER_INFO = 'LILI-USERINFO'
+const USER_SETTING = 'LILI-SETTING'
+
+/**
+ * 设置用户授权token
+ *
+ * @param {String} token
+ */
+export function setToken(token) {
+ return localStorage.setItem(
+ USER_TOKEN,
+ token
+ )
+}
+
+/**
+ * 获取授权token
+ */
+export function getToken() {
+ return localStorage.getItem(USER_TOKEN)
+}
+
+/**
+ * 设置用户信息
+ *
+ * @param {Object} data
+ */
+export function setUserInfo(data) {
+ localStorage.setItem(USER_INFO, JsBase64.Base64.encode(JSON.stringify(data)))
+}
+
+/**
+ * 获取用户信息
+ */
+export function getUserInfo() {
+ const data = JsBase64.Base64.decode(localStorage.getItem(USER_INFO) || '')
+ return data ? JSON.parse(data) : {}
+}
+
+/**
+ * 获取用户本地缓存的设置信息
+ */
+export function getUserSettingCache() {
+ const data = localStorage.getItem(USER_SETTING)
+ return data ? JSON.parse(data) : {}
+}
+
+/**
+ * 用户设置保存到浏览器缓存中
+ *
+ * @param {Object} state 用户设置相关信息
+ */
+export function setUserSettingCache(state) {
+ localStorage.setItem(USER_SETTING, JSON.stringify(state))
+}
+
+/**
+ * 删除用户相关缓存信息
+ */
+export function removeAll() {
+ localStorage.removeItem(USER_TOKEN)
+ localStorage.removeItem(USER_INFO)
+ localStorage.removeItem(USER_SETTING)
+}
diff --git a/im/src/utils/date.js b/im/src/utils/date.js
new file mode 100644
index 00000000..c85b7893
--- /dev/null
+++ b/im/src/utils/date.js
@@ -0,0 +1,144 @@
+/**
+ * 对Date的扩展,将 Date 转化为指定格式的String。
+ *
+ * 月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 可以用 1-2 个占位符,
+ * 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字)。
+ *
+ * 【示例】:
+ * formatDate(new Date(), 'yyyy-MM-dd hh:mm:ss.S') ==> 2006-07-02 08:09:04.423
+ * formatDate(new Date(), 'yyyy-M-d h:m:s.S') ==> 2006-7-2 8:9:4.18
+ * formatDate(new Date(), 'hh:mm:ss.S') ==> 08:09:04.423
+ */
+export function formatDate(date, fmt) {
+ const o = {
+ 'M+': date.getMonth() + 1, //月份
+ 'd+': date.getDate(), //日
+ 'h+': date.getHours(), //小时
+ 'm+': date.getMinutes(), //分
+ 's+': date.getSeconds(), //秒
+ 'q+': Math.floor((date.getMonth() + 3) / 3), //季度
+ S: date.getMilliseconds(), //毫秒
+ }
+
+ if (/(y+)/.test(fmt)) {
+ fmt = fmt.replace(
+ RegExp.$1,
+ (date.getFullYear() + '').substr(4 - RegExp.$1.length)
+ )
+ }
+
+ for (let k in o) {
+ if (new RegExp('(' + k + ')').test(fmt)) {
+ let value =
+ RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)
+ fmt = fmt.replace(RegExp.$1, value)
+ }
+ }
+
+ return fmt
+}
+
+/**
+ * 仿照微信中的消息时间显示逻辑,将时间戳(单位:毫秒)转换为友好的显示格式.
+ *
+ * 1)7天之内的日期显示逻辑是:今天、昨天(-1d)、前天(-2d)、星期?(只显示总计7天之内的星期数,即<=-4d);
+ * 2)7天之外(即>7天)的逻辑:直接显示完整日期时间。
+ *
+ * @param {[long]} timestamp 时间戳(单位:毫秒),形如:1550789954260
+ * @param {boolean} mustIncludeTime true表示输出的格式里一定会包含“时间:分钟”
+ * ,否则不包含(参考微信,不包含时分的情况,用于首页“消息”中显示时)
+ *
+ * @return {string} 输出格式形如:“刚刚”、“10:30”、“昨天 12:04”、“前天 20:51”、“星期二”、“2019/2/21 12:09”等形式
+ */
+export function formatDateShort(timestamp, mustIncludeTime) {
+ // 当前时间
+ let currentDate = new Date()
+ // 目标判断时间
+ let srcDate = new Date(parseInt(timestamp))
+
+ let currentYear = currentDate.getFullYear()
+ let currentMonth = currentDate.getMonth() + 1
+ let currentDateD = currentDate.getDate()
+
+ let srcYear = srcDate.getFullYear()
+ let srcMonth = srcDate.getMonth() + 1
+ let srcDateD = srcDate.getDate()
+
+ let ret = ''
+
+ // 要额外显示的时间分钟
+ let timeExtraStr = mustIncludeTime ? ' ' + formatDate(srcDate, 'hh:mm') : ''
+
+ // 当年
+ if (currentYear == srcYear) {
+ let currentTimestamp = currentDate.getTime()
+ let srcTimestamp = timestamp
+ // 相差时间(单位:毫秒)
+ let deltaTime = currentTimestamp - srcTimestamp
+
+ // 当天(月份和日期一致才是)
+ if (currentMonth == srcMonth && currentDateD == srcDateD) {
+ // 时间相差60秒以内
+ if (deltaTime < 60 * 1000) ret = '刚刚'
+ // 否则当天其它时间段的,直接显示“时:分”的形式
+ else ret = formatDate(srcDate, 'hh:mm')
+ }
+ // 当年 && 当天之外的时间(即昨天及以前的时间)
+ else {
+ // 昨天(以“现在”的时候为基准-1天)
+ let yesterdayDate = new Date()
+ yesterdayDate.setDate(yesterdayDate.getDate() - 1)
+
+ // 前天(以“现在”的时候为基准-2天)
+ let beforeYesterdayDate = new Date()
+ beforeYesterdayDate.setDate(beforeYesterdayDate.getDate() - 2)
+
+ // 用目标日期的“月”和“天”跟上方计算出来的“昨天”进行比较,是最为准确的(如果用时间戳差值
+ // 的形式,是不准确的,比如:现在时刻是2019年02月22日1:00、而srcDate是2019年02月21日23:00,
+ // 这两者间只相差2小时,直接用“deltaTime/(3600 * 1000)” > 24小时来判断是否昨天,就完全是扯蛋的逻辑了)
+ if (
+ srcMonth == yesterdayDate.getMonth() + 1 &&
+ srcDateD == yesterdayDate.getDate()
+ )
+ ret = '昨天' + timeExtraStr
+ // -1d
+ // “前天”判断逻辑同上
+ else if (
+ srcMonth == beforeYesterdayDate.getMonth() + 1 &&
+ srcDateD == beforeYesterdayDate.getDate()
+ )
+ ret = '前天' + timeExtraStr
+ // -2d
+ else {
+ // 跟当前时间相差的小时数
+ let deltaHour = deltaTime / (3600 * 1000)
+
+ // 如果小于或等 7*24小时就显示星期几
+ if (deltaHour <= 7 * 24) {
+ let weekday = new Array(7)
+ weekday[0] = '星期日'
+ weekday[1] = '星期一'
+ weekday[2] = '星期二'
+ weekday[3] = '星期三'
+ weekday[4] = '星期四'
+ weekday[5] = '星期五'
+ weekday[6] = '星期六'
+
+ // 取出当前是星期几
+ let weedayDesc = weekday[srcDate.getDay()]
+ ret = weedayDesc + timeExtraStr
+ }
+ // 否则直接显示完整日期时间
+ else {
+ ret = formatDate(srcDate, 'yyyy/M/d') + timeExtraStr
+ }
+ }
+ }
+ }
+ // 往年
+ else {
+ ret = formatDate(srcDate, 'yyyy/M/d') + timeExtraStr
+ }
+
+ return ret
+}
diff --git a/im/src/utils/editor.js b/im/src/utils/editor.js
new file mode 100644
index 00000000..e07d7203
--- /dev/null
+++ b/im/src/utils/editor.js
@@ -0,0 +1,180 @@
+/**
+ * 遍历对象
+ *
+ * @param {Object} obj
+ * @param {Object} fn
+ */
+export function objForEach(obj, fn) {
+ let key = void 0,
+ result = void 0
+ for (key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ result = fn.call(obj, key, obj[key])
+ if (result === false) {
+ break
+ }
+ }
+ }
+}
+
+/**
+ * 遍历类数组
+ *
+ * @param {Object} fakeArr
+ * @param {Object} fn
+ */
+export function arrForEach(fakeArr, fn) {
+ let i = void 0,
+ item = void 0,
+ result = void 0
+ let length = fakeArr.length || 0
+ for (i = 0; i < length; i++) {
+ item = fakeArr[i]
+ result = fn.call(fakeArr, item, i)
+ if (result === false) {
+ break
+ }
+ }
+}
+
+/**
+ * 替换 html 特殊字符
+ *
+ * @param {Object} html
+ */
+export function replaceHtmlSymbol(html) {
+ if (html == null) {
+ return ''
+ }
+ return html
+ .replace(//gm, '>')
+ .replace(/"/gm, '"')
+ .replace(/(\r\n|\r|\n)/g, '
')
+}
+
+/**
+ * 获取粘贴的纯文本
+ *
+ * @param {Object} e
+ */
+export function getPasteText(e) {
+ let clipboardData =
+ e.clipboardData || (e.originalEvent && e.originalEvent.clipboardData)
+ let pasteText = void 0
+ if (clipboardData == null) {
+ pasteText = window.clipboardData && window.clipboardData.getData('text')
+ } else {
+ pasteText = clipboardData.getData('text/plain')
+ }
+
+ return replaceHtmlSymbol(pasteText)
+}
+
+/**
+ * 获取粘贴的html
+ *
+ * @param {Object} e
+ * @param {Object} filterStyle
+ * @param {Object} ignoreImg
+ */
+export function getPasteHtml(e, filterStyle, ignoreImg) {
+ let clipboardData =
+ e.clipboardData || (e.originalEvent && e.originalEvent.clipboardData)
+ let pasteText = void 0,
+ pasteHtml = void 0
+ if (clipboardData == null) {
+ pasteText = window.clipboardData && window.clipboardData.getData('text')
+ } else {
+ pasteText = clipboardData.getData('text/plain')
+ pasteHtml = clipboardData.getData('text/html')
+ }
+ if (!pasteHtml && pasteText) {
+ pasteHtml = '' + replaceHtmlSymbol(pasteText) + '
'
+ }
+ if (!pasteHtml) {
+ return
+ }
+
+ // 过滤word中状态过来的无用字符
+ let docSplitHtml = pasteHtml.split('