Commit 7e3e473c 7e3e473c8132600de559552c0241dc114a057e74 by yangjun@hikoon.cn

init

0 parents
Showing 278 changed files with 4831 additions and 0 deletions
1 src/assets/*
2 node_modules/*
3 sdk/*
...\ No newline at end of file ...\ No newline at end of file
1 {
2 "root": true,
3 "env": {
4 "node": true
5 },
6 "extends": [
7 "plugin:vue/essential",
8 "eslint:recommended"
9 ],
10 "rules": {
11 "no-console": 'off',
12 "quotes": ["error", "single"],
13 "semi": ["error","never"],
14 "space-before-blocks": "error",
15 "space-unary-ops": "error"
16 },
17 "parserOptions": {
18 "parser": "babel-eslint"
19 }
20 }
1 .DS_Store
2 node_modules/
3 dist/
4 npm-debug.log*
5 yarn-debug.log*
6 yarn-error.log*
7 package-lock.json
8 tests/**/coverage/
9 dist/*
10
11 # Editor directories and files
12 .idea
13 .vscode
14 *.suo
15 *.ntvs*
16 *.njsproj
17 *.sln
18 .env.development
1 # TIMSDK Web Demo
2
3 ## 一分钟跑通Demo
4
5 1. 下载源码到本地
6
7 3. 配置 `SDKAppID``SECRETKEY`,参考:[密钥获取方法](https://cloud.tencent.com/document/product/269/36838#.E6.AD.A5.E9.AA.A42.EF.BC.9A.E8.8E.B7.E5.8F.96.E5.AF.86.E9.92.A5.E4.BF.A1.E6.81.AF)
8
9 2.1 打开 `/dist/debug/GenerateTestUserSig.js` 文件
10
11 2.2 按图示填写相应配置后,保存文件
12
13 ![配置SDKAppID和SECRETKEY](_doc/image/demo-init-1.png)
14
15 4. 建议使用 `Chrome` 浏览器打开 `/dist/index.html` 文件,即可预览。
16
17 ## 开发运行
18
19 Web Demo 使用 `Vue` + `Vuex` + `Element-UI` 开发,你可以参考该 Demo 进行业务开发,也可以直接基于本Demo 进行二次开发。
20
21 > 参考文档:
22 >
23 > - [TIMSDK 官方文档](https://imsdk-1252463788.file.myqcloud.com/IM_DOC/Web/index.html)
24
25 ### 目录结构
26
27 ```
28 ├───sdk/
29 │ ├───tim-js.js - tim sdk 文件,demo 中未使用,仅供自行集成使用
30 ├───dist/ - 打包编译后的目录
31 ├───public/ - 公共入口
32 │ ├───debug/ - 用于配置SDKAppID 和 SECRETKEY
33 │ └───index.html
34 ├───src/ - 源码目录
35 │ ├───assets/ - 静态资源目录
36 │ ├───components/ - 组件目录
37 │ ├───store/ - Vuex Store 目录
38 │ ├───utils/ - 工具函数目录
39 │ ├───index.vue - 入口文件
40 │ ├───main.js - Vue 全局配置
41 │ └───tim.js - TIM SDK相关
42 ├───_doc/ - 文档相关
43 ├───.eslintignore - eslint 忽略配置
44 ├───babel.config.js - babel 配置
45 ├───package.json
46 ├───README.md
47 └───vue.config.js - vue-cli@3 配置文件
48 ```
49
50 ### 准备工作
51
52 1. 准备好您的 `SDKAPPID``SECRETKEY`,获取方式参考:[密钥获取方法](https://cloud.tencent.com/document/product/269/36838#.E6.AD.A5.E9.AA.A41.EF.BC.9A.E5.88.9B.E5.BB.BA.E5.BA.94.E7.94.A8)
53
54 2. 搭建 [nodejs 环境](https://nodejs.org/zh-cn/) (建议安装 8.0 版本以上的 nodejs),选择官网推荐的安装包,安装即可
55
56 安装完成后,打开命令行,输入以下命令:
57
58 ```shell
59 node -v
60 ```
61
62 如果上述命令输出相应的版本号,说明环境搭建完成。
63
64 ### 启动流程
65
66 1. 克隆本仓库到本地
67
68 ```shell
69 # 命令行执行
70 git clone https://github.com/tencentyun/TIMSDK.git
71
72 # 进入 Web Demo 项目
73 cd TIMSDK/H5
74 ```
75
76 2. 配置 `SDKAppID``SECRETKEY`,参考:[密钥获取方法](https://cloud.tencent.com/document/product/269/36838#.E6.AD.A5.E9.AA.A42.EF.BC.9A.E8.8E.B7.E5.8F.96.E5.AF.86.E9.92.A5.E4.BF.A1.E6.81.AF)
77
78 2.1 打开 `/public/debug/GenerateTestUserSig.js` 文件
79
80 2.2 按图示填写相应配置后,保存文件
81
82 ![配置SDKAppID和SECRETKEY](_doc/image/demo-init-1.png)
83
84 3. 启动项目
85
86 ```shell
87 # 同步依赖
88 npm install
89 # 启动项目
90 npm start
91 ```
92
93 > 若同步依赖过程中出现问题,尝试切换 npm 源后重试。
94 >
95 > ```shell
96 > # 切换 cnpm 源
97 > npm config set registry http://r.cnpmjs.org/
98 > ```
99
100 4. 浏览器中打开链接:http://localhost:8080/
101
102 ### 注意事项
103
104 1. 避免在前端进行签名计算
105
106 本 Demo 为了用户体验的便利,将 `userSig` 签发放到前端执行。若直接部署上线,会面临 `SECRETKEY` 泄露的风险。
107
108 正确的 `userSig` 签发方式是将 `userSig` 的计算代码集成到您的服务端,并提供相应接口。在需要 `userSig` 时,发起请求获取动态 `userSig`。更多详情请参见 [服务端生成 UserSig](https://cloud.tencent.com/document/product/269/32688#GeneratingdynamicUserSig)
109
110 ### WebIM Demo Change Log
111
112 #### 2020/12/04
113
114 **Features**
115
116 - Web Demo增加群直播功能
117
118 - Web Demo新增 1v1 和群语音视频通话,和native互通
119
120 #### 2020/10/22
121
122 **Features**
123
124 - SDK 版本更新,支持查询直播群在线人数,发送图片消息接入图片压缩
125
126 **BUG Fixes**
127
128 - 修复C2C 消息发送失败显示未读标示问题
129
130 #### 2020/7/3
131
132 **Features**
133
134 - SDK 版本更新至 2.7.6
135
136 #### 2020/7/3
137
138 **Features**
139
140 - SDK 版本更新至 2.7.5
141
142 **Changes**
143
144 - Web demo 更新群名称:好友工作群(Work)、陌生人社交群(Public)、临时会议群(Meeting)和直播群(AVChatRoom)
145
146 #### 2020/6/10
147
148 **Features**
149
150 - SDK 版本更新至 2.7.0,支持C2C已读回执功能
151 - Web demo C2C支持已读回执功能上报显示
152
153 **Changes**
154
155 - Web demo 登录页面增加直播电商解决方案入口
156
157 #### 2020/4/28
158
159 **Features**
160
161 - SDK 版本更新至 2.6.3,支持群组全体禁言和取消禁言
162 - Web demo 群组支持全体禁言和取消全体禁言的功能入口
163
164 **Changes**
165
166 - 监听 TIM.EVENT.NET_STATE_CHANGE ,添加网络状态变更提醒
167 - 废弃 TIM.EVENT.GROUP_SYSTEM_NOTICE_RECEIVED事件
168 - 用户入群、退群、发消息,优先展示其 nick 没有 nick 才用 userID
169
170 #### 2020/3/30
171
172 **Features**
173
174 - SDK 版本更新至 2.6.0, 支持收发视频消息
175 - Web demo 支持收发视频消息,可拖拽发送框发送或选取文件发送
176
177 **Change**
178
179 - 修改视频通话界面UI
180
181 **BUG Fixes**
182
183 - 修复撤回 C2C 消息通知在 web 多实例登录时的同步问题
184 - 修复大文件或空文件发送失败后无法第二次发送的问题
185
186 #### 2020/1/14
187
188 **Features**
189
190 - 支持 C2C 视频通话
191
192 **Change**
193
194 - 消息发送两分钟后,不展示撤回菜单
195
196 #### 2020/1/6
197
198 **Features**
199
200 - SDK 版本更新,支持消息撤回
201 - Web Demo增加消息撤回与重新编辑功能
202 - 账号被踢出时,给出原因提醒
203
204 #### 2019/12/13
205
206 **Features**
207
208 - 支持粘贴发送截图
209
210 **Change**
211
212 - 完善收到新消息时的通知处理
213 - 处理完【加群申请】后,将相应的通知删除
214
215 #### 2019/11/22
216
217 **Features**
218
219 - 支持地理位置消息的渲染
220 - 支持点击群消息头像查看详细资料
221 - 支持我的名片的展示和修改
222
223 **Change**
224
225 - 优化几处体验问题
226
227 #### 2019/11/01
228
229 **Change**
230
231 - 优化几处体验问题
232
233 **BUG Fixes**
234
235 - 修复删除群组会话后,会话又出现的问题
236 - 修复退出群组时,Demo 出现空白区域的问题
237
238 #### 2019/10/17
239
240 **Features**
241
242 - Web Demo 样式调整
243 - SDK 版本更新,支持接收视频消息
244 - 移除掷骰子功能,替换为使用评分
245
246 #### 2019/10/12
247
248 **Bug Fixes**
249
250 - 修复 React 框架下发图片消息失败的问题
251
252 #### 2019/09/21
253
254 **Bug Fixes**
255
256 - 修复收到新群系统通知事件名不正确的问题
257
258 #### 2019/09/06
259
260 **Bug Fixes**
261
262 - 修复 IE 下超长文本消息的显示超出会话框的问题
263 - 修复重发消息失败时无错误提示的问题
264
265
266 #### 2019/09/05
267
268 **Bug Fixes**
269
270 - 修复预览图片时,图片显示不正确的问题
271 - 修复点击群组列表时,群成员列表不更新的问题
272 - 解决修改个人资料时,报错的问题
273
274 #### 2019/12/19
275
276 **Feat**
277
278 - 添加trtc视频通话,基础流程跑通
1 module.exports = {
2 presets: ['@vue/app'],
3 ignore: ['sdk/**'],
4 plugins: [
5 [
6 'component',
7 {
8 libraryName: 'element-ui',
9 styleLibraryName: 'theme-chalk'
10 }
11 ]
12 ]
13 }
1 {
2 "name": "timsdk-demo",
3 "version": "2.1.3",
4 "private": true,
5 "scripts": {
6 "start": "node node_modules/@vue/cli-service/bin/vue-cli-service.js serve",
7 "serve": "node node_modules/@vue/cli-service/bin/vue-cli-service.js serve",
8 "build": "node node_modules/@vue/cli-service/bin/vue-cli-service.js build",
9 "lint": "node node_modules/@vue/cli-service/bin/vue-cli-service.js lint src --ext .vue,.js --fix"
10 },
11 "dependencies": {
12 "axios": "^0.21.0",
13 "core-js": "^2.6.11",
14 "cos-js-sdk-v5": "^0.5.22",
15 "element-ui": "^2.13.0",
16 "md5": "^2.3.0",
17 "mta-h5-analysis": "^2.0.15",
18 "tim-js-sdk": "^2.8.5",
19 "trtc-calling-js": "^0.6.0",
20 "trtc-js-sdk": "^4.7.1",
21 "tsignaling": "^0.3.0",
22 "tweblive": "^1.1.1",
23 "vue": "^2.6.11",
24 "vue-clipboard2": "^0.3.1",
25 "vuex": "^3.1.2"
26 },
27 "devDependencies": {
28 "@vue/cli-plugin-babel": "^3.12.1",
29 "@vue/cli-plugin-eslint": "^3.12.1",
30 "@vue/cli-service": "^3.12.1",
31 "babel-eslint": "^10.0.3",
32 "babel-plugin-component": "^1.1.1",
33 "eslint": "^5.16.0",
34 "eslint-plugin-vue": "^5.0.0",
35 "stylus": "^0.54.7",
36 "stylus-loader": "^3.0.2",
37 "vue-template-compiler": "^2.6.11"
38 },
39 "postcss": {
40 "plugins": {
41 "autoprefixer": {}
42 }
43 },
44 "browserslist": [
45 "> 1%",
46 "last 2 versions",
47 "not ie <= 8"
48 ]
49 }
This diff could not be displayed because it is too large.
1 /*eslint-disable*/
2 /*
3 * Module: GenerateTestUserSig
4 *
5 * Function: 用于生成测试用的 UserSig,UserSig 是腾讯云为其云服务设计的一种安全保护签名。
6 * 其计算方法是对 SDKAppID、UserID 和 EXPIRETIME 进行加密,加密算法为 HMAC-SHA256。
7 *
8 * Attention: 请不要将如下代码发布到您的线上正式版本的 App 中,原因如下:
9 *
10 * 本文件中的代码虽然能够正确计算出 UserSig,但仅适合快速调通 SDK 的基本功能,不适合线上产品,
11 * 这是因为客户端代码中的 SECRETKEY 很容易被反编译逆向破解,尤其是 Web 端的代码被破解的难度几乎为零。
12 * 一旦您的密钥泄露,攻击者就可以计算出正确的 UserSig 来盗用您的腾讯云流量。
13 *
14 * 正确的做法是将 UserSig 的计算代码和加密密钥放在您的业务服务器上,然后由 App 按需向您的服务器获取实时算出的 UserSig。
15 * 由于破解服务器的成本要高于破解客户端 App,所以服务器计算的方案能够更好地保护您的加密密钥。
16 *
17 * Reference:https://cloud.tencent.com/document/product/647/17275#Server
18 */
19 function genTestUserSig(userID) {
20 /**
21 * 腾讯云 SDKAppId,需要替换为您自己账号下的 SDKAppId。
22 *
23 * 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/rav ) 创建应用,即可看到 SDKAppId,
24 * 它是腾讯云用于区分客户的唯一标识。
25 */
26 var SDKAPPID = 1400514950;
27
28 /**
29 * 签名过期时间,建议不要设置的过短
30 * <p>
31 * 时间单位:秒
32 * 默认时间:7 x 24 x 60 x 60 = 604800 = 7 天
33 */
34 var EXPIRETIME = 604800;
35
36 /**
37 * 计算签名用的加密密钥,获取步骤如下:
38 *
39 * step1. 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/rav ),如果还没有应用就创建一个,
40 * step2. 单击“应用配置”进入基础配置页面,并进一步找到“帐号体系集成”部分。
41 * step3. 点击“查看密钥”按钮,就可以看到计算 UserSig 使用的加密的密钥了,请将其拷贝并复制到如下的变量中
42 *
43 * 注意:该方案仅适用于调试Demo,正式上线前请将 UserSig 计算代码和密钥迁移到您的后台服务器上,以避免加密密钥泄露导致的流量盗用。
44 * 文档:https://cloud.tencent.com/document/product/647/17275#Server
45 */
46 var SECRETKEY = 'eb219c4b42bdbbf5dca38f21f5be26ab38f1c157d0ba4277d5acce6507f2d727';
47
48 var generator = new window.LibGenerateTestUserSig(SDKAPPID, SECRETKEY, EXPIRETIME);
49 var userSig = generator.genTestUserSig(userID);
50 return {
51 SDKAppID: SDKAPPID,
52 userSig: userSig
53 };
54 }
...\ No newline at end of file ...\ No newline at end of file
This diff could not be displayed because it is too large.
No preview for this file type
1 <!DOCTYPE html>
2 <html>
3
4 <head>
5 <meta charset="utf-8">
6 <!-- 优先使用Chrome内核 -->
7 <meta name="renderer" content="webkit"/>
8 <meta name="force-rendering" content="webkit"/>
9 <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1"/>
10 <title>星斗推</title>
11 <link rel="icon" href="favicon.ico" type="image/x-icon"/>
12 </head>
13
14 <body>
15 <div id="app"></div>
16 <script src="./debug/GenerateTestUserSig.js"></script>
17 <script src="./debug/lib-generate-test-usersig.min.js"></script>
18 </body>
19 </html>
This diff could not be displayed because it is too large.
1 $bg = #4a5a6c
2
3 $black = #000000
4 $white = #ffffff
5
6 // font-related
7 $base = #1c2438
8 $regular = #495060
9 // $first = #99a8b4
10 $first = #a5b5c1
11 $secondary = #a5b5c1
12 $font-light = #f7f7f8
13 $font-dark = #76828c
14
15 // border-related
16 $border-base = #e7e7e7
17 $border-light = #e9eaec
18 $border-highlight = #55d48b
19
20 // theme-related
21 $dark-primary = #2b85e4
22 $primary = #2d8cf0
23 $light-primary = #5cadff
24 // $light-primary = #55d48b
25 $success = #0ac160
26 $warning = #fa9d3b
27 $danger = #f35f5f
28
29 // background-related
30 $light-background = #f2f7f7
31 $background = #404953
32 $dark-background = #363e47
33 $deep-background = #303841
34
35 $background = #404953
36 $background-light = #f5f5f5
37 $background-dark = #363e47
38 $background-deep-dark = #303841
39
40
41 $height = 80vh
42 $width = 80vw
...\ No newline at end of file ...\ No newline at end of file
1 @font-face {font-family: "iconfont";
2 src: url('iconfont.eot?t=1571037879389'); /* IE9 */
3 src: url('iconfont.eot?t=1571037879389#iefix') format('embedded-opentype'), /* IE6-IE8 */
4 url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAtUAAsAAAAAE2gAAAsGAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCEcAqXNJM0ATYCJAM4Cx4ABCAFhG0HgSUbfxAjkg9WkOyvD7g5YB2zHKxobKzh+qoa2/WqPbHCSnoiPo59IsJbFdKfyG2qSMv5HA/A/fL9DL4COjrgO8sFZgpMRyU84NGIu80RgNAHwN/Tpr3dD72wi4QikUZ9LhAqSoxSB8LuRqhp0tQkvetSCwetYzWJQFqP1jWC1NKemKcHzMz/7W/a31pGRKguefYvPz2lgaaM8CC9xQAOJvc5QDA3736timhdwDwkHrVsp9sX3g3fHWaeqJAImXaimH38Ny5eBEKy0IjUSiz4MLx+r06Igxt8aPvCwwQoTUwF88/lvsQYF2DMA12fKlEmLokO5CWp4dJY8AKJPHzCHwPP3vcH/zALQgglEfaorYsLC7sCto89YAs89sewcUaEKeKN+GnUfYAPTrGRcmrsjYE+86IvDFdsysaaaa4FnAJfTLjizcWMWQmrDwx40RHPTixTbs6KVBKhVKNWrP94KcbXWnxP9DR8YZLKTwIXVAwhFlQGQQmqhDAGnDbCWFARhJmgKghzwelLWAAqhOAElUIIIIuG7wlZ1Pwo9AqMb/UlI8yYT3QWgJsCEKu8ThwQcKWIoFhsEqLdyI3g8/uHhUYn8HgJ4v5CIS8mhp+WJhTwwvsT/OjsCFFIhECDYQKMR+ULVHy+nMer8fmMrD/bSOsoo56hSmjmCRUMMoFAqSC0fwu/n/L5anyfGv2fUcEvmMDnq00SzcES6uCN9Ps+o4Dq9DH0scDYpluYnqFp9mXRPUGygWdCmzujOAe7VOK2nljuoe7VYocD8JuBDG+rFGtulxpHejOuFtlPSI3I5VoTanXKMSbMxR3KPOnZjrpkR0MY45LvjrRFXPHnA2tf2tZj7rZ2sl2MbS/rtnrNTWuErd0x3MM9g0g40Qgc1pMsamjCuGZvCka2dEVzDnTOsVvltNPcZJNRjMvmYaXl/EauU8nDPbauaLLOwzYuvMMac4C+Sxcz6EdBzBwYyjxJfISuv2q+ZrvCXp770A5Eq5nbGmkBZDXkjwkGY8GdtG9kHgYSdGkdm0G1DHURjCV5Ov71NcLbvjggrI2shyS9X3zUXyy6F0zkHgtMdl9OEtZfjcRIW5PZSxDOpP3XUkV1VyJm9TiJPB4cfccfT/af3vbslvnmU0J/1M/cMrHe+gdr+NZ6Ob3vKcGfCy90i7UCR52M2vuEFNg8AKGYq8n+RrxyJMbZxunLPtN68hRJGq8GiIdRPiuZIxlgOy5h2BprRrmxMcloJ6zy+vMDmP2ZDO09rliTLd3a57aRLUP0p9gsY5nnSJqxsdz6k3R2qZRykOTMNsrLjdRsOyhxVdmlRpmDZKI0YbRTLh82QkYtk9PT6p5DJFEM7SQII7AyaoATtMgaRclkk8xyg5Nyyp0d4X3a6T5Xg8xzNC2LYUibxWnQvG4IWI0+NhhYyq93sK+Erm46gAXngwVt1J2U+n1F2MjuwlQQ7N46W8N+87566wn3dIL1POed1sbpps5lVi+GXkvUAxYKKrUifnqQuRAc5qrzuKU+40lfPOtnQbagE8wcyAnQzn1hYVPq7PDn+v3DQNE4PfA5/R5C76R636drrVkdCJjoULnzcFOLzJFyFeZq8xyU2n3l9d6R0GJofKmpx6OX4I7d+EJFAvMTTW/YkLfB3NGRJxTB0A0V5V9MgsVl1dX4Qs0WX9J/beqwiXxA1Tw9psOIMnI0H+Ik6wLcmGhukAyIiQlwghyClIeCMdGCjZ529OtvtetV20MuXcpNPft8fCVo2CVL2O2qf/auXftR0Vj8l5FpfL2+iy1Ijd/r3mlq1LWj36pfCTrVdhiyMU81LaFfc7+Ead6rVV3PVx3pJb7T6pZfoQ7/CP8yde8SZ83jwuhfx4vt6//n9Sp//kxF9BKmq/Uzc/XnV39v7UqcKmZdrx9eUQ47i4l+lK72VpaRvTs6WEpPG2g9lVYj+DGCe4FEGAhLrh+x8G4QliOK2epHMpP8kdqEKLlJJnm24oiFuMGzLBn6Xdp322rfpL5O/W/+nLPeBPTf2HfUgBzZ48xHagXPQlt4CvWjzMdQM1xTrC823YJl1dSlsUUaTalh1lvYAJaqL2IqtdrMYdNHDs4tP6UZbVOzMA70Y2r/39hZO6J2uDwF/tmrrQINNvX996biHGqJ2FQoHl11aUu8pxvbgVIR7nrwwI0AQXFX3DG209P1xwrFbcBPnkRKRkp/fI2tuTqOGDaFxOhly2gsLjWuGKuowJyoogpT3ji8k5yIzZmL4QmkbCIW4Qnuj5o3F/KQWq3TarnqOKE4SiyMO7LuVnO1Wl0pYQPL8f4nlEfUR5REVXXZtKnr9VhlJaZvqlido8b0lZX6/MRdmUWZuxIzyNitujJAOZBLhCEzjmlIYkhy4ZP8uMHXH/M3CS53pnguBF8LHqveNQzeGZkvNzdeI1SKRlQGhQlKw4MUUaJwVuUskRKc5Fp0/DhSuGadtBavwRHwWkgSSsokuQrJnN7eP+b0UW706BHu5hCIYhz/LnIXZCyr7u2dI0mUSHrh3c7V0crf33l8M77WlrabCFnS2lkRv2vRime3X36VxE06dkwxRPn2u0+ff7dIdl34wY+lP3715qdlHld57LjBbOKLwy2rjdNvHahP4CDxsWNFmp+w857h6mGy4lpL9lRNeMIeXNZve/p0xabqaqXUlHMiX1m9KLKv7WVbsvjx4R/UWvgwdfZ5z5tvExBqqEechMqlGDHXUpt7TvKtfsLSpQlljvt+SHtjeQ37pp+dt27dkNIF2sYV8dfEy/c0EY/qYqxhnKl0bEmMeUnRrmMCujCEcXyVEsNVfvEuvTaqMFKdR0teS+bYYrSxYw1QkVqSottWUx4/JeEpoAtDGG9JK4G2PLnthbPBIn+RpqoeuTxb0DwuvKI5orkifFxzJJkjdadyr73OIqU5ggumcNOFiGY9oRlEm0ONoS2SxMGtotmRfdPPaTQFBepkY/qMkC7F5KNjDT/c96VNuD9xYHbJ2jAT6EbsWLHEMaxOOFqxaMQuXW6YaUFOSYH8tsk2OHD7+duxhnOKySFdGTOSjLVmOrEiCfrWPedGR3MVfk4RtNEEwR7XOfTVVwj4HEVYQE/dMJe7ZMqQQ8V3Zp9Ck9QFHA7pQHRq9p3iQ0NM3T8PV1sySBVAwNb/C2xQ3y82o2gPbPI/Wgce+BWpwHZ30PvgprFoJHjpS5Sku44SNcCH4Wg4OK0bGUCtf9B6cEgfDTb7GW3p3eg/9N7/kcMgj2dSThUN/y2E/Nuy58XDwjZC+zb2h+orf6MJcFL915jHQQB9/4dvNaxcyDmq5EYUrQbKXgxC2PhviYexCMG3Afy3qO8Pb4goq636NyXKomQc3oFogkz+SYQD/6xkLpCQR/rBf1QyAZkw+moSJLAUEGAcHgBkwGYkYxAKZ5JxEMI1guD/hHDg35vMBTl8h/shHEPJBNCYRCIJKe0Bnd4yKsEB+ieQ5mhEpZz8sd9oZ68y0ayO+8dciCO0VXPt+8KIuY8zys12IgZM5gCfzinoPUPKPKGWakRvuta12Z1ZaQ6Lk7eMSnT6AfTPwqQ5mt3llPn6b7SzV7lkyVPhf8yF9g9alaaG9xcfay15K5PLzXaEMANyduYAPglDz7sZJPNBE2qpjC0601VNtjJ1+Wp8MbzUeS++ZZnH2iBKsqJqumFatuN6YXeFQXk89l5p56nITuZEKu4HdTbrUW1vTBq3NvOc9vM2WZSW/R3jVM5tWjk5vGigaJ90KSG3JaZKtqgHl6THeVsCeVwsAAA=') format('woff2'),
5 url('iconfont.woff?t=1571037879389') format('woff'),
6 url('iconfont.ttf?t=1571037879389') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
7 url('iconfont.svg?t=1571037879389#iconfont') format('svg'); /* iOS 4.1- */
8 }
9
10 .iconfont {
11 font-family: "iconfont" !important;
12 font-size: 16px;
13 font-style: normal;
14 -webkit-font-smoothing: antialiased;
15 -moz-osx-font-smoothing: grayscale;
16 }
17
18 .icon-female:before {
19 content: "\e649";
20 }
21
22 .icon-blacklist:before {
23 content: "\e66a";
24 }
25
26 .icon-tupian:before {
27 content: "\e64a";
28 }
29
30 .icon-diaocha:before {
31 content: "\e605";
32 }
33
34 .icon-voice:before {
35 content: "\e667";
36 }
37
38 .icon-group:before {
39 content: "\e696";
40 }
41
42 .icon-contact:before {
43 content: "\e61f";
44 }
45
46 .icon-wenjian:before {
47 content: "\e601";
48 }
49
50 .icon-male:before {
51 content: "\e832";
52 }
53
54 .icon-zidingyi:before {
55 content: "\e633";
56 }
57
58 .icon-conversation:before {
59 content: "\e663";
60 }
61
62 .icon-tuichu:before {
63 content: "\e74d";
64 }
65
66 .icon-smile:before {
67 content: "\e6d6";
68 }
69
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
1 @font-face {
2 font-family: 'tim';
3 src: url('tim.eot?gollaf');
4 src: url('tim.eot?gollaf#iefix') format('embedded-opentype'),
5 url('tim.ttf?gollaf') format('truetype'),
6 url('tim.woff?gollaf') format('woff'),
7 url('tim.svg?gollaf#tim') format('svg');
8 font-weight: normal;
9 font-style: normal;
10 font-display: block;
11 }
12
13 [class^="tim-icon-"], [class*=" tim-icon-"] {
14 /* use !important to prevent issues with browser extensions that change fonts */
15 font-family: 'tim' !important;
16 speak: none;
17 font-style: normal;
18 font-weight: normal;
19 font-variant: normal;
20 text-transform: none;
21 line-height: 1;
22
23 /* Better Font Rendering =========== */
24 -webkit-font-smoothing: antialiased;
25 -moz-osx-font-smoothing: grayscale;
26 }
27
28 .tim-icon-friend-add:before {
29 content: "\e907";
30 }
31 .tim-icon-close:before {
32 content: "\e901";
33 }
34 .tim-icon-right:before {
35 content: "\e903";
36 }
37 .tim-icon-add:before {
38 content: "\e904";
39 }
40 .tim-icon-refresh:before {
41 content: "\e905";
42 }
43 .tim-icon-send:before {
44 content: "\e902";
45 }
46 .tim-icon-angle:before {
47 content: "\e900";
48 }
49 .tim-icon-angle-middle:before {
50 content: "\e906";
51 }
No preview for this file type
1 <?xml version="1.0" standalone="no"?>
2 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
3 <svg xmlns="http://www.w3.org/2000/svg">
4 <metadata>Generated by IcoMoon</metadata>
5 <defs>
6 <font id="tim" horiz-adv-x="1024">
7 <font-face units-per-em="1024" ascent="960" descent="-64" />
8 <missing-glyph horiz-adv-x="1024" />
9 <glyph unicode="&#x20;" horiz-adv-x="512" d="" />
10 <glyph unicode="&#xe900;" glyph-name="angle" horiz-adv-x="410" d="M53.893-51.211c5.48 176.067 42.594 341.987 105.815 494.339l-3.415-9.287c67.368 170.846 113.179 269.474 220.968 377.263 14.339 14.217 24.492 32.634 28.461 53.252l0.103 0.643c1.321 5.986 2.078 12.862 2.078 19.914 0 29.579-13.312 56.048-34.27 73.745l-0.144 0.118c-13.613 8.803-30.249 14.035-48.108 14.035-0.708 0-1.415-0.008-2.119-0.025l0.105 0.002h-323.368s0-916.21 0-1024z" />
11 <glyph unicode="&#xe901;" glyph-name="close" d="M557.312 459.552l265.28 263.904c12.544 12.48 12.608 32.704 0.128 45.248-12.512 12.576-32.704 12.608-45.248 0.128l-265.344-263.936-263.040 263.84c-12.448 12.48-32.704 12.544-45.248 0.064-12.512-12.48-12.544-32.736-0.064-45.28l262.976-263.776-265.152-263.744c-12.544-12.48-12.608-32.704-0.128-45.248 6.24-6.272 14.464-9.44 22.688-9.44 8.16 0 16.32 3.104 22.56 9.312l265.216 263.808 265.44-266.24c6.24-6.272 14.432-9.408 22.656-9.408 8.192 0 16.352 3.136 22.592 9.344 12.512 12.48 12.544 32.704 0.064 45.248l-265.376 266.176z" />
12 <glyph unicode="&#xe902;" glyph-name="send" d="M350.912 367.424l-299.968 182.72 908.48 355.648-159.072-818.272-387.040 247.872 345.952 373.472zM416 268.8v-209.92l128.256 130.208z" />
13 <glyph unicode="&#xe903;" glyph-name="right" d="M822.464 707.456c-5.192 5.738-12.666 9.328-20.979 9.328-8.918 0-16.871-4.131-22.050-10.585l-0.043-0.055-352.96-417.664-181.92 212.992c-5.228 6.441-13.144 10.523-22.014 10.523-8.368 0-15.887-3.633-21.066-9.409l-0.023-0.027c-5.835-6.529-9.401-15.192-9.401-24.689 0-8.977 3.187-17.211 8.491-23.63l-0.050 0.063 204.096-238.944c5.76-6.752 13.696-10.56 22.016-10.56h0.096c8.877 0.144 16.768 4.243 22.008 10.606l0.040 0.050 374.976 443.744c11.52 13.728 11.008 35.328-1.216 48.256z" />
14 <glyph unicode="&#xe904;" glyph-name="add" d="M512 140.8c17.673 0 32 14.327 32 32v0 256h256c17.673 0 32 14.327 32 32s-14.327 32-32 32v0h-256v256c0 17.673-14.327 32-32 32s-32-14.327-32-32v0-256h-256c-17.673 0-32-14.327-32-32s14.327-32 32-32v0h256v-256c0-17.673 14.327-32 32-32v0z" />
15 <glyph unicode="&#xe905;" glyph-name="refresh" d="M832 476.8c-17.673 0-32-14.327-32-32v0c0-158.784-129.216-288-288-288s-288 129.216-288 288 129.216 288 288 288c66.208 0 129.536-22.752 180.608-64h-84.608c-17.673 0-32-14.327-32-32s14.327-32 32-32v0h160c17.673 0 32 14.327 32 32v0 160c0 17.673-14.327 32-32 32s-32-14.327-32-32v0-80.96c-60.256 50.365-138.555 80.951-223.998 80.96h-0.002c-194.080 0-352-157.92-352-352s157.92-352 352-352 352 157.92 352 352c0 17.673-14.327 32-32 32v0z" />
16 <glyph unicode="&#xe906;" glyph-name="angle-middle" d="M361.945-51.199c5.48 176.063 42.593 341.979 105.813 494.327l-3.415-9.287c67.366 170.842 113.176 269.467 220.963 377.255 14.339 14.217 24.491 32.633 28.46 53.251l0.103 0.643c1.321 5.986 2.078 12.862 2.078 19.914 0 29.578-13.312 56.047-34.269 73.743l-0.144 0.118c-13.613 8.803-30.248 14.035-48.107 14.035-0.708 0-1.415-0.008-2.119-0.025l0.105 0.002h-323.361s0-916.191 0-1023.977z" />
17 <glyph unicode="&#xe907;" glyph-name="friend-add" d="M781.713 596.267c0 150.519-120.415 270.933-270.933 270.933s-270.933-120.415-270.933-270.933c0-99.342 54.187-189.653 138.477-234.809-114.394-45.156-201.695-144.498-225.778-270.933-3.010-15.052 6.021-33.114 24.083-36.124h6.021c15.052 0 27.093 9.031 30.104 24.083 27.093 141.487 150.519 243.84 295.016 246.85h6.021c147.508 0 267.923 123.425 267.923 270.933zM300.054 596.267c0 117.404 93.321 210.726 210.726 210.726s210.726-93.321 210.726-210.726c0-114.394-93.321-207.716-207.716-210.726h-9.031c-114.394 6.021-204.705 99.342-204.705 210.726zM841.921 207.929h-60.207v60.207c0 18.062-12.041 30.104-30.104 30.104s-30.104-12.041-30.104-30.104v-60.207h-60.207c-18.062 0-30.104-12.041-30.104-30.104s12.041-30.104 30.104-30.104h60.207v-60.207c0-18.062 12.041-30.104 30.104-30.104s30.104 12.041 30.104 30.104v60.207h60.207c18.062 0 30.104 12.041 30.104 30.104s-12.041 30.104-30.104 30.104z" />
18 </font></defs></svg>
...\ No newline at end of file ...\ No newline at end of file
No preview for this file type
No preview for this file type
1 <template>
2 <div class="avatar" :class="shape === 'circle' ? 'shape-circle' : ''">
3 <img :src="avatarSrc">
4 </div>
5 </template>
6
7 <script>
8 import systemAvatar from '@/assets/image/system.png'
9 export default {
10 props: {
11 src: String,
12 type: {
13 type: String,
14 default: 'C2C'
15 },
16 shape: {
17 type: String,
18 default: 'circle'
19 }
20 },
21 computed: {
22 avatarSrc: function () {
23 let src = this.src
24 if (/^(https:|http:|\/\/)/.test(src)) {
25 return src
26 } else {
27 return this.defaultSrc
28 }
29 },
30 defaultSrc: function () {
31 switch(this.type) {
32 case 'C2C':
33 // 个人头像
34 return 'https://imgcache.qq.com/open/qcloud/video/act/webim-avatar/avatar-2.png'
35 case 'GROUP':
36 // 群默认头像
37 return 'https://imgcache.qq.com/open/qcloud/video/act/webim-avatar/avatar-3.png'
38 case this.TIM.TYPES.CONV_SYSTEM:
39 return systemAvatar
40 default:
41 // 默认头像
42 return 'https://imgcache.qq.com/open/qcloud/video/act/webim-avatar/avatar-1.png'
43 }
44 }
45 }
46 }
47 </script>
48
49 <style lang="stylus" scoped>
50 .avatar
51 background-color $first
52 text-align center
53 width 100%
54 height 100%
55 overflow hidden
56 img
57 width 100%
58 height 100%
59 .shape-circle
60 border-radius 50%
61 </style>
1 <template>
2 <div class="avatar">
3 <img :src="avatarSrc">
4 </div>
5 </template>
6
7 <script>
8 import systemAvatar from '@/assets/image/system.png'
9 export default {
10 props: {
11 src: String,
12 type: {
13 type: String,
14 default: 'C2C'
15 },
16 shape: {
17 type: String,
18 default: 'circle'
19 }
20 },
21 computed: {
22 avatarSrc: function () {
23 let src = this.src
24 if(/^(https:|http:|\/\/)/.test(src)) {
25 return src
26 } else {
27 return this.defaultSrc
28 }
29 },
30 defaultSrc: function () {
31 switch(this.type) {
32 case 'C2C':
33 // 个人头像
34 return 'https://imgcache.qq.com/open/qcloud/video/act/webim-avatar/avatar-2.png'
35 case 'GROUP':
36 // 群默认头像
37 return 'https://imgcache.qq.com/open/qcloud/video/act/webim-avatar/avatar-3.png'
38 case this.TIM.TYPES.CONV_SYSTEM:
39 return systemAvatar
40 default:
41 // 默认头像
42 return 'https://imgcache.qq.com/open/qcloud/video/act/webim-avatar/avatar-1.png'
43 }
44 }
45 }
46 }
47 </script>
48
49 <style lang="stylus" scoped>
50 .avatar
51 background-color $first
52 text-align center
53 width 100%
54 height 100%
55 overflow hidden
56 img
57 width 100%
58 height 100%
59 </style>
1 <template>
2 <div class="blacklist-item-wrapper">
3 <img
4 class="avatar"
5 :src="profile.avatar ? profile.avatar : 'http://imgcache.qq.com/open/qcloud/video/act/webim-avatar/avatar-2.png'"
6 />
7 <div class="item">{{profile.nick||profile.userID}}</div>
8 <el-button type="text" @click="removeFromBlacklist">取消拉黑</el-button>
9 </div>
10 </template>
11
12 <script>
13 export default {
14 name: 'BlacklistItem',
15 props: {
16 profile: {
17 type: Object,
18 required: true
19 }
20 },
21 methods: {
22 removeFromBlacklist() {
23 this.tim
24 .removeFromBlacklist({ userIDList: [this.profile.userID] })
25 .then(() => {
26 this.$store.commit('removeFromBlacklist', this.profile.userID)
27 })
28 .catch(error => {
29 this.$store.commit('showMessage', {
30 type: 'error',
31 message: error.message
32 })
33 })
34 }
35 }
36 }
37 </script>
38
39 <style lang="stylus" scoped>
40 .item {
41 padding-left: 20px;
42 width: 100%;
43 color: $white;
44 box-sizing: border-box;
45 word-wrap: break-word;
46 overflow: hidden;
47 text-overflow: ellipsis;
48 }
49
50 .blacklist-item-wrapper {
51 padding-bottom: 15px;
52 display: flex;
53 align-items: center;
54 justify-content: flex-start;
55 }
56
57 .avatar {
58 width: 48px;
59 height: 48px;
60 border-radius: 50%;
61 }
62 </style>
1 <template>
2 <div class="blacklist-wrapper" :class="{'default': !hasBlacklist}">
3 <div v-if="hasBlacklist">
4 <blacklist-item
5 v-for="item in blacklist"
6 :key="item.userID"
7 :profile="item"
8 />
9 </div>
10 <span style="color:gray" v-else>黑名单还是空的</span>
11 </div>
12 </template>
13
14 <script>
15 import BlacklistItem from './blacklist-item'
16 import { mapState } from 'vuex'
17 export default {
18 name: 'Blacklist',
19 components: {
20 BlacklistItem
21 },
22 computed: {
23 ...mapState({
24 blacklist: state => state.blacklist.blacklist
25 }),
26 hasBlacklist() {
27 return this.blacklist.length > 0
28 }
29 }
30 }
31 </script>
32
33 <style lang="stylus" scoped>
34 .blacklist-wrapper {
35 padding: 12px;
36 overflow-y: scroll;
37 }
38 .default {
39 display: flex;
40 justify-content: center;
41 align-items: center;
42 height: 100%;
43 }
44 </style>
1 <template>
2 <popover
3 placement="top"
4 style="display: flex;flex-direction: column;"
5 v-model="visible"
6 >
7 <el-button class="context-menu-button" type="text" @click="option.handler" v-for="option in options" :key="option.text">{{options.text}}</el-button>
8 <el-button class="context-menu-button" type="text" @click="visible = false ">取消</el-button>
9 <slot slot="reference" @contextmenu.prevent=""></slot>
10 </popover>
11 </template>
12
13 <script>
14 import { Popover } from 'element-ui'
15 export default {
16 props: ['visible', 'options'],
17 components: {
18 Popover
19 }
20 }
21 </script>
22
23 <style lang="stylus" scoped>
24 </style>
1 <template>
2 <div
3 class="conversation-item-container"
4 :class="{ 'choose': conversation.conversationID === currentConversation.conversationID }"
5 @click="selectConversation"
6 >
7 <div class="close-btn">
8 <span class="tim-icon-close" title="删除会话" @click="deleteConversation"></span>
9 </div>
10 <div class="warp">
11 <avatar :src="avatar" :type="conversation.type" />
12 <div class="content">
13 <div class="row-1">
14 <div class="name">
15 <div class="text-ellipsis">
16 <span :title="conversation.userProfile.nick || conversation.userProfile.userID"
17 v-if="conversation.type === TIM.TYPES.CONV_C2C"
18 >{{conversation.userProfile.nick || conversation.userProfile.userID}}
19 </span>
20 <span :title="conversation.groupProfile.name || conversation.groupProfile.groupID"
21 v-else-if="conversation.type === TIM.TYPES.CONV_GROUP"
22 >{{conversation.groupProfile.name || conversation.groupProfile.groupID}}
23 </span>
24 <span
25 v-else-if="conversation.type === TIM.TYPES.CONV_SYSTEM"
26 >系统通知
27 </span>
28 </div>
29 </div>
30 <div class="unread-count">
31 <span class="badge" v-if="showUnreadCount">
32 {{conversation.unreadCount > 99 ? '99+' : conversation.unreadCount}}
33 </span>
34 </div>
35 </div>
36 <div class="row-2">
37 <div class="summary">
38 <div v-if="conversation.lastMessage" class="text-ellipsis">
39 <span class="remind" style="color:red;" v-if="hasMessageAtMe">[有人提到我]</span>
40 <span class="text" :title="conversation.lastMessage.messageForShow">
41 {{messageForShow}}
42 </span>
43 </div>
44 </div>
45 <div class="date">
46 {{date}}
47 </div>
48 </div>
49 </div>
50 </div>
51 </div>
52
53 </template>
54
55 <script>
56 import { mapGetters, mapState } from 'vuex'
57 import { isToday, getDate, getTime } from '../../utils/date'
58 export default {
59 name: 'conversation-item',
60 props: ['conversation'],
61 data() {
62 return {
63 popoverVisible: false,
64 hasMessageAtMe: false
65 }
66 },
67 computed: {
68 showUnreadCount() {
69 if (this.$store.getters.hidden) {
70 return this.conversation.unreadCount > 0
71 }
72 // 是否显示未读计数。当前会话和未读计数为0的会话,不显示。
73 return (
74 this.currentConversation.conversationID !==
75 this.conversation.conversationID && this.conversation.unreadCount > 0
76 )
77 },
78 date() {
79 if (
80 !this.conversation.lastMessage ||
81 !this.conversation.lastMessage.lastTime
82 ) {
83 return ''
84 }
85 const date = new Date(this.conversation.lastMessage.lastTime * 1000)
86 if (isToday(date)) {
87 return getTime(date)
88 }
89 return getDate(date)
90 },
91 avatar: function() {
92 switch (this.conversation.type) {
93 case 'GROUP':
94 return this.conversation.groupProfile.avatar
95 case 'C2C':
96 return this.conversation.userProfile.avatar
97 default:
98 return ''
99 }
100 },
101 conversationName: function() {
102 if (this.conversation.type === this.TIM.TYPES.CONV_C2C) {
103 return this.conversation.userProfile.nick || this.conversation.userProfile.userID
104 }
105 if (this.conversation.type === this.TIM.TYPES.CONV_GROUP) {
106 return this.conversation.groupProfile.name || this.conversation.groupProfile.groupID
107 }
108 if (this.conversation.type === this.TIM.TYPES.CONV_SYSTEM) {
109 return '系统通知'
110 }
111 return ''
112 },
113 showGrayBadge() {
114 if (this.conversation.type !== this.TIM.TYPES.CONV_GROUP) {
115 return false
116 }
117 return (
118 this.conversation.groupProfile.selfInfo.messageRemindType ===
119 'AcceptNotNotify'
120 )
121 },
122 messageForShow() {
123 if (this.conversation.lastMessage.isRevoked) {
124 if (this.conversation.lastMessage.fromAccount === this.currentUserProfile.userID) {
125 return '你撤回了一条消息'
126 }
127 if (this.conversation.type === this.TIM.TYPES.CONV_C2C) {
128 return '对方撤回了一条消息'
129 }
130 return `${this.conversation.lastMessage.fromAccount}撤回了一条消息`
131 }
132 return this.conversation.lastMessage.messageForShow
133 },
134 ...mapState({
135 currentConversation: state => state.conversation.currentConversation,
136 currentUserProfile: state => state.user.currentUserProfile
137 }),
138 ...mapGetters(['toAccount'])
139 },
140 mounted() {
141 this.$bus.$on('new-messsage-at-me', event => {
142 if (
143 event.data.conversationID === this.conversation.conversationID &&
144 this.conversation.conversationID !==
145 this.currentConversation.conversationID
146 ) {
147 this.hasMessageAtMe = true
148 }
149 })
150 },
151 methods: {
152 selectConversation() {
153 if (this.conversation.conversationID !== this.currentConversation.conversationID) {
154 this.$store.dispatch(
155 'checkoutConversation',
156 this.conversation.conversationID
157 )
158 }
159 },
160 deleteConversation(event) {
161 // 停止冒泡,避免和点击会话的事件冲突
162 event.stopPropagation()
163 this.tim
164 .deleteConversation(this.conversation.conversationID)
165 .then(() => {
166 this.$store.commit('showMessage', {
167 message: `会话【${this.conversationName}】删除成功!`,
168 type: 'success'
169 })
170 this.popoverVisible = false
171 this.$store.commit('resetCurrentConversation')
172 })
173 .catch(error => {
174 this.$store.commit('showMessage', {
175 message: `会话【${this.conversationName}】删除失败!, error=${error.message}`,
176 type: 'error'
177 })
178 this.popoverVisible = false
179 })
180 },
181 showContextMenu() {
182 this.popoverVisible = true
183 },
184 },
185 watch: {
186 currentConversation(next) {
187 if (next.conversationID === this.conversation.conversationID) {
188 this.hasMessageAtMe = false
189 }
190 }
191 }
192 }
193 </script>
194
195 <style lang="stylus" scoped>
196
197
198 .conversation-item-container
199 padding 15px 20px
200 cursor pointer
201 position relative
202 overflow hidden
203 transition .2s
204 // &:first-child
205 // padding-top 30px
206 &:hover
207 background-color $background
208 .close-btn
209 right 3px
210 .close-btn
211 position absolute
212 right -20px
213 top 3px
214 color $font-dark
215 transition: all .2s ease;
216 &:hover
217 color $danger
218 .warp
219 display flex
220 .avatar
221 width 40px
222 height 40px
223 margin-right 10px
224 border-radius 50%
225 flex-shrink 0
226 .content
227 flex 1
228 height 40px
229 overflow hidden
230 .row-1
231 display flex
232 line-height 21px
233 .name
234 color $font-light
235 flex 1
236 min-width 0px
237 .unread-count
238 padding-left 10px
239 flex-shrink 0
240 color $font-dark
241 font-size 12px
242 .badge
243 vertical-align bottom
244 background-color $danger
245 border-radius 10px
246 color #FFF
247 display inline-block
248 font-size 12px
249 height 18px
250 max-width 40px
251 line-height 18px
252 padding 0 6px
253 text-align center
254 white-space nowrap
255 .row-2
256 display flex
257 font-size 12px
258 padding-top 3px
259 .summary
260 flex 1
261 overflow hidden
262 min-width 0px
263 color: $secondary
264 .remind
265 color $danger
266 .date
267 padding-left 10px
268 flex-shrink 0
269 text-align right
270 color $font-dark
271 .choose {
272 background-color: $background;
273 }
274 .context-menu-button {
275 padding: 10px
276 border: 2px solid $primary;
277 border-radius: 8px;
278 }
279 </style>
1 <template>
2 <div class="list-container">
3 <div class="header-bar">
4 <button title="刷新列表" @click="handleRefresh">
5 <i class="tim-icon-refresh"></i>
6 </button>
7 <button title="创建会话" @click="handleAddButtonClick">
8 <i class="tim-icon-add"></i>
9 </button>
10 </div>
11 <div class="scroll-container">
12 <conversation-item
13 :conversation="item"
14 v-for="item in conversationList"
15 :key="item.conversationID"
16 />
17 </div>
18 <el-dialog title="快速发起会话" :visible.sync="showDialog" width="30%">
19 <el-input placeholder="请输入用户ID" v-model="userID" @keydown.enter.native="handleConfirm"/>
20 <span slot="footer" class="dialog-footer">
21 <el-button @click="showDialog = false">取 消</el-button>
22 <el-button type="primary" @click="handleConfirm">确 定</el-button>
23 </span>
24 </el-dialog>
25 </div>
26 </template>
27
28 <script>
29 import ConversationItem from './conversation-item'
30 import {mapState} from 'vuex'
31
32 export default {
33 name: 'ConversationList',
34 components: { ConversationItem },
35 data() {
36 return {
37 showDialog: false,
38 userID: '',
39 isCheckouting: false, // 是否正在切换会话
40 timeout: null
41 }
42 },
43 computed: {
44 ...mapState({
45 conversationList: state => state.conversation.conversationList,
46 currentConversation: state => state.conversation.currentConversation
47 })
48 },
49 mounted() {
50 window.addEventListener('keydown', this.handleKeydown)
51 },
52 destroyed() {
53 window.removeEventListener('keydown', this.handleKeydown)
54 },
55 methods: {
56 handleRefresh() {
57 this.refreshConversation()()
58 },
59 refreshConversation() {
60 let that = this
61 return function () {
62 if (!that.timeout) {
63 that.timeout = setTimeout(() =>{
64 that.timeout = null
65 that.tim.getConversationList().then(() => {
66 that.$store.commit('showMessage', {
67 message: '刷新成功',
68 type: 'success'
69 })
70 })
71 }, 1000)
72 }
73 }
74 },
75 handleAddButtonClick() {
76 this.showDialog = true
77 },
78 handleConfirm() {
79 if (this.userID !== '@TIM#SYSTEM') {
80 this.$store
81 .dispatch('checkoutConversation', `C2C${this.userID}`)
82 .then(() => {
83 this.showDialog = false
84 }).catch(() => {
85 this.$store.commit('showMessage', {
86 message: '没有找到该用户',
87 type: 'warning'
88 })
89 })
90 } else {
91 this.$store.commit('showMessage', {
92 message: '没有找到该用户',
93 type: 'warning'
94 })
95 }
96 this.userID = ''
97 },
98 handleKeydown(event) {
99 if (event.keyCode !== 38 && event.keyCode !== 40 || this.isCheckouting) {
100 return
101 }
102 const currentIndex = this.conversationList.findIndex(
103 item => item.conversationID === this.currentConversation.conversationID
104 )
105 if (event.keyCode === 38 && currentIndex - 1 >= 0) {
106 this.checkoutPrev(currentIndex)
107 }
108 if (
109 event.keyCode === 40 &&
110 currentIndex + 1 < this.conversationList.length
111 ) {
112 this.checkoutNext(currentIndex)
113 }
114 },
115 checkoutPrev(currentIndex) {
116 this.isCheckouting = true
117 this.$store
118 .dispatch(
119 'checkoutConversation',
120 this.conversationList[currentIndex - 1].conversationID
121 )
122 .then(() => {
123 this.isCheckouting = false
124 })
125 .catch(() => {
126 this.isCheckouting = false
127 })
128 },
129 checkoutNext(currentIndex) {
130 this.isCheckouting = true
131 this.$store
132 .dispatch(
133 'checkoutConversation',
134 this.conversationList[currentIndex + 1].conversationID
135 )
136 .then(() => {
137 this.isCheckouting = false
138 })
139 .catch(() => {
140 this.isCheckouting = false
141 })
142 }
143 }
144 }
145 </script>
146
147 <style lang="stylus" scoped>
148 .list-container
149 height 100%
150 width 100%
151 display flex
152 flex-direction column // -reverse
153 .header-bar
154 flex-shrink 0
155 height 50px
156 border-bottom 1px solid $background-deep-dark
157 padding 10px 10px 10px 20px
158 button
159 float right
160 display: inline-block;
161 cursor: pointer;
162 background $background-deep-dark
163 border: none
164 color: $font-dark;
165 box-sizing: border-box;
166 transition: .3s;
167 -moz-user-select: none;
168 -webkit-user-select: none;
169 -ms-user-select: none;
170 margin: 0 10px 0 0
171 padding 0
172 width 30px
173 height 30px
174 line-height 34px
175 font-size: 24px;
176 text-align: center;
177 white-space: nowrap;
178 border-radius: 50%
179 outline 0
180 &:hover
181 // background $light-primary
182 // color $white
183 transform: rotate(360deg);
184 color $light-primary
185 .scroll-container
186 overflow-y scroll
187 flex 1
188 .bottom-circle-btn {
189 position: absolute;
190 bottom: 20px;
191 right: 20px;
192 }
193
194 .refresh {
195 bottom: 70px;
196 }
197 </style>
1 <template>
2 <div class="conversation-profile-wrapper">
3 <user-profile
4 v-if="currentConversation.type === TIM.TYPES.CONV_C2C"
5 :userProfile="currentConversation.userProfile"
6 />
7 <group-profile
8 v-else-if="currentConversation.type === TIM.TYPES.CONV_GROUP"
9 :groupProfile="currentConversation.groupProfile"
10 />
11 </div>
12 </template>
13
14 <script>
15 import { mapState } from 'vuex'
16 import GroupProfile from './conversationProfile/group-profile.vue'
17 import UserProfile from './conversationProfile/user-profile.vue'
18 export default {
19 name: 'ConversationProfile',
20 components: {
21 GroupProfile,
22 UserProfile
23 },
24 data() {
25 return {}
26 },
27 computed: {
28 ...mapState({
29 currentConversation: state => state.conversation.currentConversation
30 })
31 }
32 }
33 </script>
34
35 <style lang="stylus" scoped>
36 .conversation-profile-wrapper
37 background-color $white
38 height 100%
39 overflow-y scroll
40
41 /* 设置滚动条的样式 */
42 ::-webkit-scrollbar {
43 width: 0px;
44 height: 0px;
45 }
46 </style>
1 <template>
2 <div>
3 <el-input v-model="userID" placeholder="输入userID后 按回车键" @keydown.enter.native="addGroupMember"></el-input>
4 </div>
5 </template>
6
7 <script>
8 import { Input } from 'element-ui'
9 import { mapState } from 'vuex'
10 export default {
11 components: {
12 ElInput: Input
13 },
14 data() {
15 return {
16 userID: ''
17 }
18 },
19 computed: {
20 ...mapState({
21 currentConversation: state => state.conversation.currentConversation
22 })
23 },
24 methods: {
25 addGroupMember() {
26 const groupID = this.currentConversation.conversationID.replace('GROUP', '')
27 this.tim
28 .addGroupMember({
29 groupID,
30 userIDList: [this.userID]
31 })
32 .then((imResponse) => {
33 const {
34 successUserIDList,
35 failureUserIDList,
36 existedUserIDList
37 } = imResponse.data
38 if (successUserIDList.length > 0) {
39 this.$store.commit('showMessage', {
40 message: `群成员:${successUserIDList.join(',')},加群成功`,
41 type: 'success'
42 })
43 this.tim.getGroupMemberProfile({groupID, userIDList: successUserIDList})
44 .then(({ data: { memberList }}) => {
45 this.$store.commit('updateCurrentMemberList', memberList)
46 })
47 }
48 if (failureUserIDList.length > 0) {
49 this.$store.commit('showMessage', {
50 message: `群成员:${failureUserIDList.join(',')},添加失败!`,
51 type: 'error'
52 })
53 }
54 if (existedUserIDList.length > 0) {
55 this.$store.commit('showMessage', {
56 message: `群成员:${existedUserIDList.join(',')},已在群中`
57 })
58 }
59 })
60 .catch(error => {
61 this.$store.commit('showMessage', {
62 type: 'error',
63 message: error.message
64 })
65 })
66 }
67 }
68 }
69 </script>
70
71 <style lang="stylus" scoped></style>
1 <template>
2 <div>
3 <div>
4 <span class="label">userID:</span>
5 {{ member.userID }}
6 <el-button v-if="showCancelBan" type="text" @click="cancelMute">取消禁言</el-button>
7 <el-popover title="禁言" v-model="popoverVisible" v-show="showBan">
8 <el-input
9 v-model="muteTime"
10 placeholder="请输入禁言时间"
11 @keydown.enter.native="setGroupMemberMuteTime"
12 />
13 <el-button slot="reference" type="text" style="color:red;">禁言</el-button>
14 </el-popover>
15 </div>
16 <div>
17 <span class="label">nick:</span>
18 {{ member.nick || '暂无' }}
19 </div>
20 <div>
21 <span class="label">nameCard:</span>
22 {{ member.nameCard || '暂无' }}
23 <el-popover title="修改群名片" v-model="nameCardPopoverVisible" v-show="showEditNameCard">
24 <el-input
25 v-model="nameCard"
26 placeholder="请输入群名片"
27 @keydown.enter.native="setGroupMemberNameCard"
28 />
29 <i
30 class="el-icon-edit"
31 title="修改群名片"
32 slot="reference"
33 style="cursor:pointer; font-size:1.6rem;"
34 ></i>
35 </el-popover>
36 </div>
37 <div>
38 <span class="label">role:</span>
39 <span class="content role" :title="changeRoleTitle">{{ member.role }}</span>
40 </div>
41 <div v-if="showMuteUntil">
42 <span class="label">禁言至:</span>
43 <span class="content">{{ muteUntil }}</span>
44 </div>
45 <el-button type="text" v-if="canChangeRole" @click="changeMemberRole">
46 {{
47 member.role === 'Admin' ? '取消管理员' : '设为管理员'
48 }}
49 </el-button>
50 <el-button type="text" v-if="showKickout" style="color:red;" @click="kickoutGroupMember">踢出群组</el-button>
51 </div>
52 </template>
53
54 <script>
55 import { mapState } from 'vuex'
56 import { Popover } from 'element-ui'
57 import { getFullDate } from '../../../utils/date'
58 export default {
59 components: {
60 ElPopover: Popover
61 },
62 props: ['member'],
63 data() {
64 return {
65 muteTime: '',
66 popoverVisible: false,
67 nameCardPopoverVisible: false,
68 nameCard: this.member.nameCard
69 }
70 },
71 computed: {
72 ...mapState({
73 currentConversation: state => state.conversation.currentConversation,
74 currentUserProfile: state => state.user.currentUserProfile,
75 current: state => state.current
76 }),
77 // 是否显示踢出群成员按钮
78 showKickout() {
79 return (this.isOwner || this.isAdmin) && !this.isMine
80 },
81 isOwner() {
82 return this.currentConversation.groupProfile.selfInfo.role === 'Owner'
83 },
84 isAdmin() {
85 return this.currentConversation.groupProfile.selfInfo.role === 'Admin'
86 },
87 isMine() {
88 return this.currentUserProfile.userID === this.member.userID
89 },
90 canChangeRole() {
91 return (
92 this.isOwner &&
93 ['ChatRoom', 'Public'].includes(this.currentConversation.subType)
94 )
95 },
96 changeRoleTitle() {
97 if (!this.canChangeRole) {
98 return ''
99 }
100 return this.isOwner && this.member.role === 'Admin'
101 ? '设为:Member'
102 : '设为:Admin'
103 },
104 // 是否显示禁言时间
105 showMuteUntil() {
106 // 禁言时间小于当前时间
107 return this.member.muteUntil * 1000 > this.current
108 },
109 // 是否显示取消禁言按钮
110 showCancelBan() {
111 if (
112 this.showMuteUntil &&
113 this.currentConversation.type === this.TIM.TYPES.CONV_GROUP &&
114 !this.isMine
115 ) {
116 return this.isOwner || this.isAdmin
117 }
118 return false
119 },
120 // 是否显示禁言按钮
121 showBan() {
122 if (this.currentConversation.type === this.TIM.TYPES.CONV_GROUP) {
123 return this.isOwner || this.isAdmin
124 }
125 return false
126 },
127 // 是否显示编辑群名片按钮
128 showEditNameCard() {
129 return this.isOwner || this.isAdmin
130 },
131 // 日期格式化后的禁言时间
132 muteUntil() {
133 return getFullDate(new Date(this.member.muteUntil * 1000))
134 }
135 },
136 methods: {
137 kickoutGroupMember() {
138 this.tim
139 .deleteGroupMember({
140 groupID: this.currentConversation.groupProfile.groupID,
141 reason: '我要踢你出群',
142 userIDList: [this.member.userID]
143 })
144 .then(() => {
145 this.$store.commit('deleteGroupMemeber', this.member.userID)
146 })
147 .catch(error => {
148 this.$store.commit('showMessage', {
149 type: 'error',
150 message: error.message
151 })
152 })
153 },
154 changeMemberRole() {
155 if (!this.canChangeRole) {
156 return
157 }
158 let currentRole = this.member.role
159 this.tim
160 .setGroupMemberRole({
161 groupID: this.currentConversation.groupProfile.groupID,
162 userID: this.member.userID,
163 role: currentRole === 'Admin' ? 'Member' : 'Admin'
164 })
165 .catch(error => {
166 this.$store.commit('showMessage', {
167 type: 'error',
168 message: error.message
169 })
170 })
171 },
172 setGroupMemberMuteTime() {
173 this.tim
174 .setGroupMemberMuteTime({
175 groupID: this.currentConversation.groupProfile.groupID,
176 userID: this.member.userID,
177 muteTime: Number(this.muteTime)
178 })
179 .then(() => {
180 this.muteTime = ''
181 this.popoverVisible = false
182 })
183 .catch(error => {
184 this.$store.commit('showMessage', {
185 type: 'error',
186 message: error.message
187 })
188 })
189 },
190 // 取消禁言
191 cancelMute() {
192 this.tim
193 .setGroupMemberMuteTime({
194 groupID: this.currentConversation.groupProfile.groupID,
195 userID: this.member.userID,
196 muteTime: 0
197 })
198 .then(() => {
199 this.muteTime = ''
200 })
201 .catch(error => {
202 this.$store.commit('showMessage', {
203 type: 'error',
204 message: error.message
205 })
206 })
207 },
208 setGroupMemberNameCard() {
209 if (this.nameCard.trim().length === 0) {
210 this.$store.commit('showMessage', {
211 message: '不能设置空的群名片',
212 type: 'warning'
213 })
214 return
215 }
216 this.tim
217 .setGroupMemberNameCard({
218 groupID: this.currentConversation.groupProfile.groupID,
219 userID: this.member.userID,
220 nameCard: this.nameCard
221 })
222 .then(() => {
223 this.nameCardPopoverVisible = false
224 this.$store.commit('showMessage', {
225 message: '修改成功'
226 })
227 })
228 .catch(error => {
229 this.$store.commit('showMessage', {
230 type: 'error',
231 message: error.message
232 })
233 })
234 }
235 }
236 }
237 </script>
238
239 <style lang="stylus" scoped>
240 .label {
241 color: rgb(204, 200, 200);
242 }
243
244 .cursor-pointer {
245 cursor: pointer;
246 }
247 </style>
1 <template>
2 <div class="group-member-list-wrapper">
3 <div class="header">
4 <span class="member-count text-ellipsis">群成员:{{currentConversation.groupProfile.memberCount}}</span>
5 <popover v-model="addGroupMemberVisible">
6 <add-group-member></add-group-member>
7 <div slot="reference" class="btn-add-member" title="添加群成员">
8 <span class="tim-icon-friend-add"></span>
9 </div>
10 </popover>
11 </div>
12 <div class="scroll-content">
13 <div class="group-member-list">
14 <div v-for="member in members" :key="member.userID">
15 <popover placement="right" :key="member.userID">
16 <group-member-info :member="member" />
17 <div slot="reference" class="group-member" @click="currentMemberID = member.userID">
18 <avatar :title=getGroupMemberAvatarText(member.role) :src="member.avatar" />
19 <div class="member-name text-ellipsis">
20 <span v-if="member.nameCard" :title=member.nameCard>{{ member.nameCard }}</span>
21 <span v-else-if="member.nick" :title=member.nick>{{ member.nick }}</span>
22 <span v-else :title=member.userID>{{ member.userID }}</span>
23 </div>
24 </div>
25 </popover>
26 </div>
27 </div>
28 </div>
29 <div class="more">
30 <el-button v-if="showLoadMore" type="text" @click="loadMore">查看更多</el-button>
31 </div>
32 </div>
33 </template>
34
35 <script>
36 import { Popover } from 'element-ui'
37 import { mapState } from 'vuex'
38 import AddGroupMember from './add-group-member.vue'
39 import GroupMemberInfo from './group-member-info.vue'
40 export default {
41 data() {
42 return {
43 addGroupMemberVisible: false,
44 currentMemberID: '',
45 count: 30 // 显示的群成员数量
46 }
47 },
48 props: ['groupProfile'],
49 components: {
50 Popover,
51 AddGroupMember,
52 GroupMemberInfo
53 },
54 computed: {
55 ...mapState({
56 currentConversation: state => state.conversation.currentConversation,
57 currentMemberList: state => state.group.currentMemberList
58 }),
59 showLoadMore() {
60 return this.members.length < this.groupProfile.memberCount
61 },
62 members() {
63 return this.currentMemberList.slice(0, this.count)
64 }
65 },
66 methods: {
67 getGroupMemberAvatarText(role) {
68 switch (role) {
69 case 'Owner':
70 return '群主'
71 case 'Admin':
72 return '管理员'
73 default:
74 return '群成员'
75 }
76 },
77 loadMore() {
78 this.$store
79 .dispatch('getGroupMemberList', this.groupProfile.groupID)
80 .then(() => {
81 this.count += 30
82 })
83 }
84 }
85 }
86 </script>
87
88 <style lang="stylus" scoped>
89 .group-member-list-wrapper
90 .header
91 height 50px
92 padding 10px 16px 10px 20px
93 border-bottom 1px solid $border-base
94 .member-count
95 display inline-block
96 max-width 130px
97 line-height 30px
98 font-size 14px
99 vertical-align bottom
100 .btn-add-member
101 width 30px
102 height 30px
103 font-size 28px
104 text-align center
105 line-height 32px
106 cursor pointer
107 float right
108 &:hover
109 color $light-primary
110 .scroll-content
111 max-height: 250px;
112 overflow-y: scroll;
113 padding 10px 15px 10px 15px
114 width 100%
115 .group-member-list
116 display flex
117 justify-content flex-start
118 flex-wrap wrap
119 width 100%
120 .group-member
121 width 40px
122 height 70px
123 display: flex;
124 justify-content center
125 align-content center
126 flex-direction: column;
127 text-align: center;
128 color: $black;
129 cursor: pointer;
130 margin: 0 20px 10px 0;
131 padding: 10px 0 0 0;
132 .avatar
133 width 40px
134 height 40px
135 border-radius 50%
136 .member-name
137 font-size 12px
138 width: 50px;
139 text-align center
140 .more
141 padding 0 20px
142 border-bottom 1px solid $border-base
143
144 // .add-group-member {
145 // cursor: pointer;
146 // }
147 // .add-button {
148 // border: 1px solid gray;
149 // text-align: center;
150 // line-height: 30px;
151 // }
152
153
154
155 </style>
1 <template>
2 <div class="profile-user">
3 <avatar :title=userProfile.userID :src="userProfile.avatar" />
4 <div class="nick-name text-ellipsis">
5 <span v-if="userProfile.nick" :title=userProfile.nick>
6 {{ userProfile.nick }}
7 </span>
8 <span v-else class="anonymous" title="该用户未设置昵称">
9 [Anonymous]
10 </span>
11 </div>
12 <div class="gender" v-if="genderClass">
13 <span :title="gender" class="iconfont" :class="genderClass"></span>
14 </div>
15 <el-button
16 title="将该用户加入黑名单"
17 type="text"
18 @click="addToBlackList"
19 v-if="!isInBlacklist && userProfile.userID !== myUserID"
20 class="btn-add-blacklist"
21 >加入黑名单</el-button
22 >
23 <el-button title="将该用户移出黑名单" type="text" @click="removeFromBlacklist" v-else-if="isInBlacklist">移出黑名单</el-button>
24 <!-- 拉黑 和 反拉黑 -->
25 </div>
26 </template>
27
28 <script>
29 import { mapState } from 'vuex'
30 export default {
31 props: {
32 userProfile: {
33 type: Object,
34 required: true
35 }
36 },
37 computed: {
38 ...mapState({
39 blacklist: state => state.blacklist.blacklist,
40 myUserID: state => state.user.currentUserProfile.userID
41 }),
42 isInBlacklist() {
43 return this.blacklist.findIndex(item => item.userID === this.userProfile.userID) >= 0
44 },
45 gender() {
46 switch (this.userProfile.gender) {
47 case this.TIM.TYPES.GENDER_MALE:
48 return '男'
49 case this.TIM.TYPES.GENDER_FEMALE:
50 return '女'
51 default:
52 return '未设置'
53 }
54 },
55 genderClass() {
56 switch (this.userProfile.gender) {
57 case this.TIM.TYPES.GENDER_MALE:
58 return 'icon-male'
59 case this.TIM.TYPES.GENDER_FEMALE:
60 return 'icon-female'
61 default:
62 return ''
63 }
64 }
65 },
66 methods: {
67 addToBlackList() {
68 this.tim
69 .addToBlacklist({ userIDList: [this.userProfile.userID] })
70 .then(() => {
71 this.$store.dispatch('getBlacklist')
72 })
73 .catch(imError => {
74 this.$store.commit('showMessage', {
75 message: imError.message,
76 type: 'error'
77 })
78 })
79 },
80 removeFromBlacklist() {
81 this.tim.removeFromBlacklist({ userIDList: [this.userProfile.userID] }).then(() => {
82 this.$store.commit('removeFromBlacklist', this.userProfile.userID)
83 })
84 .catch(error => {
85 this.$store.commit('showMessage', {
86 type: 'error',
87 message: error.message
88 })
89 })
90 }
91 }
92 }
93 </script>
94
95 <style lang="stylus" scoped>
96 .profile-user
97 width 100%
98 text-align center
99 padding 0 20px
100 .avatar
101 width 160px
102 height 160px
103 border-radius 50%
104 margin 30px auto
105 .nick-name
106 width 100%
107 color $base
108 font-size 20px
109 font-weight bold
110 text-shadow $font-dark 0 0 0.1em
111 .anonymous
112 color $first
113 text-shadow none
114 .gender
115 padding 5px 0 10px 0
116 border-bottom 1px solid $border-base
117 .btn-add-blacklist
118 color $danger
119 </style>
1 <template>
2 <div class="current-conversation-wrapper">
3 <div class="current-conversation" @scroll="onScroll" v-if="showCurrentConversation">
4 <div class="header">
5 <div class="name">{{ name }}</div>
6 <div class="btn-more-info"
7 :class="showConversationProfile ? '' : 'left-arrow'"
8 @click="showMore"
9 v-show="!currentConversation.conversationID.includes('SYSTEM')"
10 title="查看详细信息">
11 </div>
12 </div>
13 <div class="content">
14 <div class="message-list" ref="message-list" @scroll="this.onScroll">
15 <div class="more" v-if="!isCompleted">
16 <el-button
17 type="text"
18 @click="$store.dispatch('getMessageList', currentConversation.conversationID)"
19 >查看更多</el-button>
20 </div>
21 <div class="no-more" v-else>没有更多了</div>
22 <message-item v-for="message in currentMessageList" :key="message.ID" :message="message"/>
23 </div>
24 <div v-show="isShowScrollButtomTips" class="newMessageTips" @click="scrollMessageListToButtom">回到最新位置</div>
25 </div>
26 <div class="footer" v-if="showMessageSendBox" >
27 <message-send-box/>
28 </div>
29 </div>
30 <div class="profile" v-if="showConversationProfile" >
31 <conversation-profile/>
32 </div>
33 <!-- 群成员资料组件 -->
34 <member-profile-card />
35 </div>
36 </template>
37
38 <script>
39 import { mapGetters, mapState } from 'vuex'
40 import MessageSendBox from '../message/message-send-box'
41 import MessageItem from '../message/message-item'
42 import ConversationProfile from './conversation-profile.vue'
43 import MemberProfileCard from '../group/member-profile-card'
44 export default {
45 name: 'CurrentConversation',
46 components: {
47 MessageSendBox,
48 MessageItem,
49 ConversationProfile,
50 MemberProfileCard
51 },
52 data() {
53 return {
54 isShowScrollButtomTips: false,
55 preScrollHeight: 0,
56 showConversationProfile: false,
57 timeout: ''
58 }
59 },
60 computed: {
61 ...mapState({
62 currentConversation: state => state.conversation.currentConversation,
63 currentUnreadCount: state => state.conversation.currentConversation.unreadCount,
64 currentMessageList: state => state.conversation.currentMessageList,
65 isCompleted: state => state.conversation.isCompleted
66 }),
67 ...mapGetters(['toAccount', 'hidden']),
68 // 是否显示当前会话组件
69 showCurrentConversation() {
70 return !!this.currentConversation.conversationID
71 },
72 name() {
73 if (this.currentConversation.type === 'C2C') {
74 return this.currentConversation.userProfile.nick || this.toAccount
75 } else if (this.currentConversation.type === 'GROUP') {
76 return this.currentConversation.groupProfile.name || this.toAccount
77 } else if (this.currentConversation.conversationID === '@TIM#SYSTEM') {
78 return '系统通知'
79 }
80 return this.toAccount
81 },
82 showMessageSendBox() {
83 return this.currentConversation.type !== this.TIM.TYPES.CONV_SYSTEM
84 }
85 },
86 mounted() {
87 this.$bus.$on('image-loaded', this.onImageLoaded)
88 this.$bus.$on('scroll-bottom', this.scrollMessageListToButtom)
89 if (this.currentConversation.conversationID === '@TIM#SYSTEM') {
90 return false
91 }
92 },
93 updated() {
94 this.keepMessageListOnButtom()
95 // 1. 系统会话隐藏右侧资料组件
96 // 2. 没有当前会话时,隐藏右侧资料组件。
97 // 背景:退出群组/删除会话时,会出现一处空白区域
98 if (this.currentConversation.conversationID === '@TIM#SYSTEM' ||
99 typeof this.currentConversation.conversationID === 'undefined') {
100 this.showConversationProfile = false
101 }
102 },
103 watch: {
104 currentUnreadCount(next) {
105 if (!this.hidden && next > 0) {
106 this.tim.setMessageRead({ conversationID: this.currentConversation.conversationID })
107 }
108 },
109 hidden(next) {
110 if (!next && this.currentUnreadCount > 0) {
111 this.tim.setMessageRead({ conversationID: this.currentConversation.conversationID })
112 }
113 }
114 },
115 methods: {
116 onScroll({ target: { scrollTop } }) {
117 let messageListNode = this.$refs['message-list']
118 if (!messageListNode) {
119 return
120 }
121 if (this.preScrollHeight - messageListNode.clientHeight - scrollTop < 20) {
122 this.isShowScrollButtomTips = false
123 }
124 },
125 // 如果滚到底部就保持在底部,否则提示是否要滚到底部
126 keepMessageListOnButtom() {
127 let messageListNode = this.$refs['message-list']
128 if (!messageListNode) {
129 return
130 }
131 // 距离底部20px内强制滚到底部,否则提示有新消息
132 if (this.preScrollHeight - messageListNode.clientHeight - messageListNode.scrollTop < 20) {
133 this.$nextTick(() => {
134 messageListNode.scrollTop = messageListNode.scrollHeight
135 })
136 this.isShowScrollButtomTips = false
137 } else {
138 this.isShowScrollButtomTips = true
139 }
140 this.preScrollHeight = messageListNode.scrollHeight
141 },
142 // 直接滚到底部
143 scrollMessageListToButtom() {
144 this.$nextTick(() => {
145 let messageListNode = this.$refs['message-list']
146 if (!messageListNode) {
147 return
148 }
149 messageListNode.scrollTop = messageListNode.scrollHeight
150 this.preScrollHeight = messageListNode.scrollHeight
151 this.isShowScrollButtomTips = false
152 })
153 },
154 showMore() {
155 this.showConversationProfile = !this.showConversationProfile
156 },
157 onImageLoaded() {
158 this.keepMessageListOnButtom()
159 }
160 }
161 }
162 </script>
163
164 <style lang="stylus" scoped>
165 /* 当前会话的骨架屏 */
166 .current-conversation-wrapper
167 height $height
168 background-color $background-light
169 color $base
170 display flex
171 .current-conversation
172 display: flex;
173 flex-direction: column;
174 width: 100%;
175 height: $height;
176 .profile
177 height: $height;
178 overflow-y: scroll;
179 width 220px
180 border-left 1px solid $border-base
181 flex-shrink 0
182 .more
183 display: flex;
184 justify-content: center;
185 font-size: 12px;
186 .no-more
187 display: flex;
188 justify-content: center;
189 color: $secondary;
190 font-size: 12px;
191 padding: 10px 10px;
192
193 .header
194 border-bottom 1px solid $border-base
195 height 50px
196 position relative
197 .name
198 padding 0 20px
199 color $base
200 font-size 18px
201 font-weight bold
202 line-height 50px
203 text-shadow $font-dark 0 0 0.1em
204 .btn-more-info
205 position absolute
206 top 10px
207 right -15px
208 border-radius 50%
209 width 30px
210 height 30px
211 cursor pointer
212 &::before
213 position absolute
214 right 0
215 z-index 0
216 content ""
217 width: 15px
218 height: 30px
219 border: 1px solid $border-base
220 border-radius: 0 100% 100% 0/50%
221 border-left: none
222 background-color $background-light
223 &::after
224 content ""
225 width: 8px;
226 height: 8px;
227 transition: transform 0.8s;
228 border-top: 2px solid $secondary;
229 border-right: 2px solid $secondary;
230 float:right;
231 position:relative;
232 top: 11px;
233 right: 8px;
234 transform:rotate(45deg)
235 &.left-arrow
236 transform rotate(180deg)
237 &::before
238 background-color $white
239 &:hover
240 &::after
241 border-color $light-primary
242 .content
243 display: flex;
244 flex 1
245 flex-direction: column;
246 height: 100%;
247 overflow: hidden;
248 position: relative;
249 .message-list
250 width: 100%;
251 box-sizing: border-box;
252 overflow-y: scroll;
253 padding: 0 20px;
254 .newMessageTips
255 position: absolute
256 cursor: pointer;
257 padding: 5px;
258 width: 120px;
259 margin: auto;
260 left: 0;
261 right: 0;
262 bottom: 5px;
263 font-size: 12px;
264 text-align: center;
265 border-radius: 10px;
266 border: $border-light 1px solid;
267 background-color: $white;
268 color: $primary;
269 .footer
270 border-top: 1px solid $border-base;
271 .show-more {
272 text-align: right;
273 padding: 10px 20px 0 0;
274 }
275 </style>
1 <template>
2 <div>
3 <el-row class="friend-item-container">
4 <el-col :span="6">
5 <avatar :src="friend.profile.avatar"/>
6 </el-col>
7 <el-col :span="18">
8 <div class="friend-name">{{ friend.profile.nick || friend.userID }}</div>
9 </el-col>
10 </el-row>
11 </div>
12 </template>
13
14 <script>
15 export default {
16 props: {
17 friend: {
18 type: Object,
19 required: true
20 }
21 },
22 methods: {
23 handleFriendClick() {
24 this.tim.getConversationProfile(`C2C${this.friend.userID}`)
25 .then(({data}) => this.$store.commit('updateCurrentConversation', data))
26 .catch(error => {
27 this.$store.commit('showMessage', {
28 type: 'error',
29 message: error.message
30 })
31 })
32 }
33 }
34 }
35 </script>
36
37 <style lang="stylus" scoped>
38 </style>
1 <template>
2 <!-- <div class="friend-list-container" :class="{'default': !hasFriend}">-->
3 <div class="list-container">
4
5 <el-collapse v-model="activeNames" class="friend-list-container">
6 <el-collapse-item title="用户组" name="user">
7 <friend-item v-for="friend in friendList" :key="friend.userID" :friend="friend"/>
8 </el-collapse-item>
9 <el-collapse-item title="策划组" name="scheme">
10
11 </el-collapse-item>
12 <el-collapse-item title="管理组" name="admin">
13
14 </el-collapse-item>
15 </el-collapse>
16 </div>
17 <!-- <div v-if="hasFriend">-->
18 <!-- <friend-item v-for="friend in friendList" :key="friend.userID" :friend="friend"/>-->
19 <!-- </div>-->
20 <!-- <div style="color:gray;" v-else>暂无数据</div>-->
21 <!-- </div>-->
22 </template>
23
24 <script>
25 import {mapState} from 'vuex'
26 import FriendItem from './friend-item.vue'
27
28
29 export default {
30 components: {
31 FriendItem,
32 },
33 computed: {
34 ...mapState({
35 friendList: state => state.friend.friendList
36 }),
37 hasFriend() {
38 return this.friendList.length > 0
39 }
40 },
41 data() {
42 return {
43 activeNames: [],
44 }
45 }
46 }
47 </script>
48
49 <style lang="stylus" scpoed>
50 .list-container
51 height 100%
52 width 100%
53 display flex
54 flex-direction column
55
56 .friend-list-container
57 margin 1px
58 border-top:unset
59 border-bottom unset
60
61
62 .default {
63 display: flex;
64 justify-content: center;
65 align-items: center;
66 height: 100%;
67 overflow-y: scroll;
68 }
69 </style>
1 <template>
2 <div class="chat-footer-container">
3 <!-- <p class="gift-title">礼物列表</p> -->
4 <carousel :autoplay="false" :loop="false" :initial-index="1" indicator-position="none" arrow="always">
5 <carousel-item v-for="(item, index) in giftList" :key="`item_${index}`">
6 <template v-for="(_item, _index) in item">
7 <div class="gift-item" :key="`gift_item_${index}_${_index}`" @click="handleGiftPic(_item.index)">
8 <img class="gift-icon" :src="_item.icon" alt=""/>
9 <p class="gift-name">{{ _item.name }}</p>
10 </div>
11 </template>
12 </carousel-item>
13 </carousel>
14 </div>
15 </template>
16
17 <script>
18 import { Carousel , CarouselItem } from 'element-ui'
19 export default {
20 name: 'liveGift',
21 props: {},
22 data() {
23 return {
24 giftList: [
25 [
26 {
27 index: 1,
28 icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590482989_25.png',
29 name: '火箭'
30 },
31 {
32 index: 2,
33 icon: 'https://8.url.cn/huayang/resource/now/new_gift/1507876726_3',
34 name: '鸡蛋'
35 },
36 {
37 index: 3,
38 icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590482294_7.png',
39 name: '吻'
40 },
41 ],
42 [
43 {
44 index: 4,
45 icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590482461_11.png',
46 name: '跑车'
47 },
48 {
49 index: 5,
50 icon: 'https://8.url.cn/huayang/resource/now/new_gift/1594714453_7.png',
51 name: '嘉年华'
52 },
53 {
54 index: 6,
55 icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590482754_17.png',
56 name: '玫瑰'
57 }
58 ],
59 [
60 {
61 index: 7,
62 icon: 'https://8.url.cn/huayang/resource/now/new_gift/1594281297_11.png',
63 name: '直升机'
64 },
65 {
66 index: 8,
67 icon: 'https://8.url.cn/huayang/resource/now/new_gift/1507876472_1',
68 name: '点赞'
69 },
70 {
71 index: 9,
72 icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483038_27.png',
73 name: '比心'
74 }
75 ],
76 [
77 {
78 index: 10,
79 icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483168_31.png',
80 name: '冰淇淋'
81 },
82 {
83 index: 11,
84 icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483225_33.png',
85 name: '玩偶'
86 },
87 {
88 index: 12,
89 icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483278_35.png',
90 name: '蛋糕'
91 }
92 ],
93 [
94 {
95 index: 13,
96 icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483348_37.png',
97 name: '豪华轿车'
98 },
99 {
100 index: 14,
101 icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483429_39.png',
102 name: '游艇'
103 },
104 {
105 index: 15,
106 icon: 'https://8.url.cn/huayang/resource/now/new_gift/1590483505_41.png',
107 name: '翅膀'
108 }
109 ]
110 ]
111 }
112 },
113 components: {
114 Carousel,
115 CarouselItem
116 },
117 methods: {
118 handleGiftPic(index) {
119 this.$bus.$emit('group-live-send-gift', index)
120 }
121 }
122 }
123 </script>
124
125 <style lang="stylus" scoped>
126 .chat-footer-container {
127 position relative
128 width 100%
129 height 50px
130 box-sizing border-box
131 border-top 1px solid #e6e6e6
132 .gift-title {
133 margin 10px 0 0 0
134 padding 0 10px
135 font-size 16px
136 font-weight 400
137 color #888585
138 border-bottom 1px solid #e6e6e6
139 }
140
141 }
142 </style>
143 <style>
144 .el-carousel {
145 height: 60px;
146 }
147 .el-carousel .el-carousel__container {
148 height: 100%;
149 }
150 .el-carousel__arrow {
151 top: 40%!important
152 }
153 .el-carousel__item {
154 padding: 0px 30px 0 45px;
155 box-sizing: 'border-box'
156 }
157 .el-carousel__item div {
158 float: left;
159 margin: 0 15px;
160 width: 65px;
161 height: 40px;
162 cursor: pointer;
163 }
164 .el-carousel__item div img {
165 width: 30px;
166 height: 30px;
167 margin: 0 0 0 15px;
168 }
169 .el-carousel__item div p {
170 position: relative;
171 top: -8px;
172 margin: 0;
173 text-align: center;
174 color: #888585;
175 font-size: 12px;
176 }
177 </style>
...\ No newline at end of file ...\ No newline at end of file
1 <template>
2 <div class="header-container">
3 <div v-show="showLiveInfo">
4 <div class="anchor-info">
5 <img class="anchor-avatar" :src="avatar">
6 <div class="anchor-other">
7 <p class="anchor-nick">{{nick}}</p>
8 <p class="online-num">在线:{{onlineMemberCount}}</p>
9 </div>
10 </div>
11 <div class="online-info">
12 <p class="room-name">直播中</p>
13 <img class="living-icon" src="../../../assets/image/living-icon.gif" />
14 <span>{{` ${pusherTime}`}}</span>
15 </div>
16 </div>
17 <div class="close-box" @click="closeLiveMask">
18 <i class="el-icon-circle-close"></i>
19 </div>
20 </div>
21 </template>
22
23 <script>
24 import { mapState } from 'vuex'
25 export default {
26 name: 'liveHeader',
27 props: {
28 fr: {
29 type: String,
30 requred: true
31 },
32 isPushingStream: {
33 type: Boolean,
34 default: false
35 },
36 stopPushStream: {
37 type: Function
38 },
39 pusherTime: {
40 type: String,
41 default: ''
42 }
43 },
44 data() {
45 return {
46 nick: '',
47 avatar: 'https://imgcache.qq.com/open/qcloud/video/act/webim-avatar/avatar-2.png',
48 onlineMemberCount: 0,
49 timer: null,
50 onlineList: []
51 }
52 },
53 computed: {
54 ...mapState({
55 groupLiveInfo: state => state.groupLive.groupLiveInfo
56 }),
57 showLiveInfo() {
58 return (this.fr === 'pusher' && this.isPushingStream) || this.fr === 'player'
59 },
60 roomName() {
61 return this.groupLiveInfo.roomName || `${this.groupLiveInfo.anchorID}的直播`
62 }
63 },
64 mounted() {
65 this.getAnchorProfile()
66 if (this.fr === 'player') {
67 this.timer = setInterval(() => {
68 this.getGroupOnlineMemberCount()
69 }, 5000)
70 }
71 },
72 beforeDestroy() {
73 this.timer && clearInterval(this.timer)
74 },
75 methods: {
76 closeLiveMask() {
77 if (this.fr === 'pusher') {
78 this.stopPushStream()
79 return
80 }
81 this.$store.commit('updateGroupLiveInfo', { isNeededQuitRoom: 1 })
82 this.$bus.$emit('close-group-live')
83 },
84 async getAnchorProfile() {
85 const res = await this.tim.getUserProfile({userIDList: [this.groupLiveInfo.anchorID]})
86 if (res.code === 0) {
87 this.nick = res.data[0].nick || res.data[0].userID
88 this.avatar = res.data[0].avatar || 'https://imgcache.qq.com/open/qcloud/video/act/webim-avatar/avatar-2.png'
89 }
90 },
91 async getGroupOnlineMemberCount() {
92 const res = await this.tim.getGroupOnlineMemberCount(this.groupLiveInfo.roomID)
93 if (res.code === 0 && res.data) {
94 this.onlineMemberCount = res.data.memberCount
95 }
96 }
97 },
98 watch: {
99 isPushingStream: function(val) {
100 if (val && this.fr === 'pusher') {
101 this.timer = setInterval(() => {
102 this.getGroupOnlineMemberCount()
103 }, 5000)
104 }
105 }
106 }
107 }
108 </script>
109
110 <style lang="stylus" scoped>
111 .header-container {
112 position absolute
113 left 0
114 top 0
115 width 100%
116 height 100%
117 box-sizing border-box
118 z-index 99
119 padding 10px 10px 10px 20px
120 .anchor-info {
121 position absolute
122 top 50%
123 transform translateY(-50%)
124 width 200px
125 height 50px
126 background rgba(255, 255 ,255 ,0.1)
127 border-radius 30px
128 display flex
129 align-items center
130 .anchor-avatar {
131 width 50px
132 height 50px
133 border-radius 50%
134 margin 0 5px
135 }
136 .anchor-other {
137 height 100%
138 flex 1
139 p {
140 margin 0
141 }
142 .anchor-nick{
143 max-width 140px
144 margin 6px 0 0 0
145 color: #ffffff
146 font-weight 500
147 word-break keep-all
148 overflow hidden
149 text-overflow ellipsis
150 white-space nowrap
151 }
152 .online-num{
153 font-size 14px
154 font-weight 400
155 color #d2cbcbad
156 }
157 }
158 }
159 .online-info {
160 position absolute
161 left 50%
162 top 50%
163 transform translate(-50%, -50%)
164 height 50px
165 color #fff
166 display flex
167 align-items center
168 .room-name{
169 display inline-block
170 max-width 160px
171 overflow hidden
172 white-space nowrap
173 text-overflow ellipsis
174 margin 0
175 padding 0 0 0 10px
176 }
177 .living-icon{
178 position relative
179 top -3px
180 margin 0 5px
181 width 25px
182 }
183 span {
184 margin 2px 0 0 0
185 }
186 }
187 .close-box {
188 position absolute
189 right 0
190 top 0px
191 width 70px
192 height 70px
193 color: #959798
194 font-size 36px
195 cursor pointer
196 display flex
197 align-items center
198 justify-content center
199 }
200 }
201 </style>
1 <template>
2 <div>
3 <div class="share-content" ref="shareCon" v-show="showShareContent">
4 <p class="qrcode-tips">手机扫码观看或复制链接分享给好友</p>
5 <qrcode ref="childQrcode"/>
6 <button class="copy-link" @click="copyLink" v-clipboard="playUrl" v-clipboard:success="onCopySuccess" v-clipboard:error="onCopyError">复制链接</button>
7 </div>
8 <div class="share-btn" ref="shareBtn">
9 <img class="share-icon" src="../../../assets/image/share-icon.png" alt=""/>
10 分享直播
11 </div>
12 </div>
13 </template>
14
15 <script>
16 import qrcode from './qrcode'
17 export default {
18 name: 'liveShare',
19 data() {
20 return {
21 showShareContent: false,
22 playUrl: '',
23 }
24 },
25 computed: {},
26 components: {
27 qrcode
28 },
29 mounted() {
30 const shareCon = this.$refs.shareCon
31 const shareBtn = this.$refs.shareBtn
32 shareBtn.addEventListener('mouseover', () => {
33 this.showShareContent = true
34 })
35 shareBtn.addEventListener('mouseout', () => {
36 this.showShareContent = false
37 })
38 shareCon.addEventListener('mouseover', () => {
39 this.showShareContent = true
40 })
41 shareCon.addEventListener('mouseout', () => {
42 this.showShareContent = false
43 })
44 },
45 methods: {
46 copyLink() {
47 this.playUrl= this.$refs.childQrcode.playUrl
48 },
49 onCopySuccess() {
50 this.$store.commit('showMessage', {
51 type: 'success',
52 message: '复制成功'
53 })
54 },
55 onCopyError() {
56 this.$store.commit('showMessage', {
57 type: 'error',
58 message: '复制失败'
59 })
60 }
61 }
62 }
63 </script>
64 <style lang="stylus" scoped>
65 .share-content {
66 position absolute
67 top -250px
68 left 20px
69 width 200px
70 height 250px
71 background #ffffff
72 border-radius 5px 5px 0 0
73 z-index 1
74 padding 10px
75 box-sizing border-box
76 text-align center
77 .qrcode-tips {
78 margin 0 0 0 0
79 color #a5b5c1
80 text-align center
81 }
82 .copy-link{
83 width 160px
84 height 40px
85 border hidden
86 outline-style none
87 background #5cadff
88 color #fff
89 font-size 16px
90 border-radius 25px
91 margin 20px 0
92 cursor pointer
93 }
94 }
95 .share-btn {
96 position absolute
97 bottom 0
98 line-height 55px
99 font-size 14px
100 color #8a9099
101 letter-spacing 0
102 margin 0 0 0 20px
103 box-sizing border-box
104 display flex
105 align-items center
106 cursor pointer
107 .share-icon {
108 width 20px
109 height 20px
110 margin 0 5px 2px 0
111 }
112 }
113 </style>
1 <template>
2 <img class="qrcode-img" v-if="qrcodeUrl" :src="qrcodeUrl"/>
3 </template>
4
5 <script>
6 import { mapState } from 'vuex'
7 import QRCode from 'qrcode'
8 export default {
9 props: {
10 url: String
11 },
12 data() {
13 return {
14 qrcodeUrl: '',
15 playUrl: '',
16 }
17 },
18 computed: {
19 ...mapState({
20 user: state => state.user,
21 roomID: state => state.groupLive.groupLiveInfo.roomID,
22 anchorID: state => state.groupLive.groupLiveInfo.anchorID,
23 }),
24 },
25 async mounted() {
26 this.qrcodeUrl = await this.generateQRcode()
27 },
28 methods: {
29 generateQRcode() {
30 const streamID = `${this.user.sdkAppID}_${this.roomID}_${this.anchorID}_main`
31 const flv = `https://tuikit.qcloud.com/live/${streamID}.flv`
32 const hls = `https://tuikit.qcloud.com/live/${streamID}.m3u8`
33 this.playUrl = `https://webim-1252463788.cos.ap-shanghai.myqcloud.com/tweblivedemo/0.3.2-cdn-player/index.html?flv=${encodeURIComponent(flv)}&hls=${encodeURIComponent(hls)}&roomid=${this.roomID}`
34 return QRCode.toDataURL(this.playUrl)
35 }
36 }
37 }
38 </script>
39 <style lang="stylus" scoped>
40 .qrcode-img {
41 display block
42 width 120px
43 height 120px
44 margin 0 auto
45 }
46 </style>
1 <template>
2 <div class="group-live-mask" v-if="groupLiveVisible">
3 <div class="live-container">
4 <div class="video-wrap">
5 <template v-if="channel === 3 && userID !== anchorID">
6 <live-player />
7 </template>
8 <template v-else>
9 <live-pusher />
10 </template>
11 </div>
12 <div class="chat-wrap">
13 <live-chat v-if="groupLiveVisible" />
14 </div>
15 </div>
16 </div>
17 </template>
18
19 <script>
20 import { mapState } from 'vuex'
21 import livePusher from './components/live-pusher'
22 import livePlayer from './components/live-player'
23 import liveChat from './components/live-chat'
24
25 export default {
26 name: 'groupLive',
27 data() {
28 return {
29 groupLiveVisible: false,
30 channel: 1 // 进入直播间渠道:1 群组内直播 2 群组外直播 3 点击消息卡片
31 }
32 },
33 computed: {
34 ...mapState({
35 userID: state => state.user.userID,
36 groupID: state => state.groupLive.groupLiveInfo.groupID,
37 roomID: state => state.groupLive.groupLiveInfo.roomID,
38 anchorID: state => state.groupLive.groupLiveInfo.anchorID,
39 }),
40 },
41 mounted() {
42 this.$bus.$on('open-group-live', (options) => {
43 this.channel = options.channel
44 this.groupLiveVisible = true
45 })
46 this.$bus.$on('close-group-live', () => {
47 this.groupLiveVisible = false
48 this.$store.commit('clearAvChatRoomMessageList')
49 })
50 },
51 beforeDestroy() {
52 this.$bus.$off('open-group-live')
53 this.$bus.$off('close-group-live')
54 },
55 components: {
56 livePusher,
57 livePlayer,
58 liveChat,
59 },
60 methods: {}
61 }
62 </script>
63 <style lang="stylus" scoped>
64 .group-live-mask{
65 position absolute
66 top 8vh
67 width 80vw
68 height 80vh
69 max-width: 1280px
70 background: #fff
71 z-index 999
72 }
73 .live-container {
74 width 100%
75 height 100%
76 display flex
77 .video-wrap {
78 position relative
79 flex 1
80 min-width 500px
81 height 100%
82 background url('../../assets/image/video-bg.png') center no-repeat
83 }
84 .chat-wrap {
85 width 375px
86 height 100%
87 background #f5f5f5
88 }
89 }
90 </style>
...\ No newline at end of file ...\ No newline at end of file
1 <template>
2 <div>
3 <el-form :model="form" :rules="rules" ref="createGroupForm" label-width="100px">
4 <el-form-item label="群ID">
5 <el-input v-model="form.groupID"></el-input>
6 </el-form-item>
7 <el-form-item label="群名称" prop="name">
8 <el-input v-model="form.name"></el-input>
9 </el-form-item>
10 <el-form-item label="群类型">
11 <el-select v-model="form.type">
12 <el-option label="Work" :value="TIM.TYPES.GRP_WORK"></el-option>
13 <el-option label="Public" :value="TIM.TYPES.GRP_PUBLIC"></el-option>
14 <el-option label="Meeting" :value="TIM.TYPES.GRP_MEETING"></el-option>
15 <el-option label="AVChatRoom" :value="TIM.TYPES.GRP_AVCHATROOM"></el-option>
16 </el-select>
17 </el-form-item>
18 <el-form-item label="群头像地址">
19 <el-input v-model="form.avatar"></el-input>
20 </el-form-item>
21 <el-form-item label="群简介">
22 <el-input type="textarea" v-model="form.introduction" :maxlength="240"></el-input>
23 </el-form-item>
24 <el-form-item label="群公告">
25 <el-input type="textarea" v-model="form.notification" :maxlength="300"></el-input>
26 </el-form-item>
27 <el-form-item label="加群方式">
28 <el-radio-group v-model="form.joinOption" :disabled="joinOptionDisabled">
29 <el-radio label="FreeAccess">自由加群</el-radio>
30 <el-radio label="NeedPermission">需要验证</el-radio>
31 <el-radio label="DisableApply">禁止加群</el-radio>
32 </el-radio-group>
33 </el-form-item>
34 <el-form-item label="群成员列表">
35 <el-select
36 v-model="form.memberList"
37 default-first-option
38 multiple
39 filterable
40 remote
41 :disabled="form.type === TIM.TYPES.GRP_AVCHATROOM"
42 :remote-method="handleSearchUser"
43 :loading="loading"
44 placeholder="请输入群成员 userID"
45 >
46 <el-option v-for="item in options" :key="item" :label="item" :value="item"></el-option>
47 </el-select>
48 </el-form-item>
49 </el-form>
50 <div slot="footer">
51 <el-button type="primary" @click="onSubmit('createGroupForm')">立即创建</el-button>
52 <el-button @click="closeCreateGroupModel">取消</el-button>
53 </div>
54 </div>
55 </template>
56
57 <script>
58 import {
59 Form,
60 FormItem,
61 Input,
62 Select,
63 Option,
64 Radio,
65 RadioGroup
66 } from 'element-ui'
67 export default {
68 components: {
69 ElForm: Form,
70 ElFormItem: FormItem,
71 ElInput: Input,
72 ElSelect: Select,
73 ElOption: Option,
74 ElRadioGroup: RadioGroup,
75 ElRadio: Radio
76 },
77 data() {
78 return {
79 form: {
80 groupID: '',
81 name: '',
82 type: this.TIM.TYPES.GRP_WORK,
83 avatar: '',
84 introduction: '',
85 notification: '',
86 joinOption: 'FreeAccess',
87 memberList: []
88 },
89 options: [],
90 loading: false,
91 rules: {
92 name: [{ required: true, message: '请输入群名称', trigger: 'blur' }]
93 }
94 }
95 },
96 computed: {
97 joinOptionDisabled() {
98 return [
99 this.TIM.TYPES.GRP_WORK,
100 this.TIM.TYPES.GRP_MEETING,
101 this.TIM.TYPES.GRP_AVCHATROOM
102 ].includes(this.form.type)
103 }
104 },
105 methods: {
106 onSubmit(ref) {
107 this.$refs[ref].validate(valid => {
108 if (!valid) {
109 return false
110 }
111 this.createGroup()
112 })
113 },
114 closeCreateGroupModel() {
115 this.$store.commit('updateCreateGroupModelVisible', false)
116 },
117 createGroup() {
118 this.tim.createGroup(this.getOptions()).then((imResponse) => {
119 this.$store.commit('showMessage', {
120 message: `群组:【${imResponse.data.group.name}】创建成功`,
121 type: 'success'
122 })
123 this.closeCreateGroupModel()
124 })
125 .catch(error => {
126 this.$store.commit('showMessage', {
127 type: 'error',
128 message: error.message
129 })
130 })
131 },
132 getOptions() {
133 let options = {
134 ...this.form,
135 memberList: this.form.memberList.map(userID => ({ userID }))
136 }
137 if ([this.TIM.TYPES.GRP_WORK, this.TIM.TYPES.GRP_AVCHATROOM].includes(this.form.type)) {
138 delete options.joinOption
139 }
140 return options
141 },
142 handleSearchUser(userID) {
143 if (userID !== '') {
144 this.loading = true
145 this.tim.getUserProfile({ userIDList: [userID] }).then(({ data }) => {
146 this.options = data.map(item => item.userID)
147 this.loading = false
148 })
149 .catch(error => {
150 this.$store.commit('showMessage', {
151 type: 'error',
152 message: error.message
153 })
154 })
155 }
156 }
157 }
158 }
159 </script>
160
161 <style lang="stylus" scoped>
162 </style>
1 <template>
2 <div @click="handleGroupClick" class="scroll-container">
3 <div class="group-item">
4 <avatar :src="group.avatar" />
5 <div class="group-name text-ellipsis">{{ group.name }}</div>
6 </div>
7 </div>
8 </template>
9
10 <script>
11 export default {
12 props: ['group'],
13 data() {
14 return {
15 visible: false,
16 options: [
17 {
18 text: '退出群组',
19 handler: this.quitGroup
20 }
21 ]
22 }
23 },
24 methods: {
25 handleGroupClick() {
26 const conversationID = `GROUP${this.group.groupID}`
27 this.$store.dispatch('checkoutConversation', conversationID)
28 },
29 quitGroup() {
30 this.tim.quitGroup(this.group.groupID)
31 .catch(error => {
32 this.$store.commit('showMessage', {
33 type: 'error',
34 message: error.message
35 })
36 })
37 }
38 }
39 }
40 </script>
41
42 <style lang="stylus" scoped>
43 .scroll-container
44 overflow-y scroll
45 flex 1
46 .group-item
47 display flex
48 padding 10px 20px
49 cursor pointer
50 position relative
51 overflow hidden
52 transition .2s
53 &:hover
54 background-color $background
55 .avatar
56 width 30px
57 height 30px
58 border-radius 50%
59 margin-right 10px
60 flex-shrink 0
61 .group-name
62 flex 1
63 color $font-light
64 line-height 30px
65 </style>
1 <template>
2 <div class="list-container">
3 <div class="header-bar">
4 <el-autocomplete
5 :value-key="'groupID'"
6 :debounce="500"
7 size="mini"
8 v-model="groupID"
9 placeholder="输入群ID搜索"
10 :fetch-suggestions="searchGroupByID"
11 class="group-seach-bar"
12 prefix-icon="el-icon-search"
13 :hide-loading="hideSearchLoading"
14 @input="hideSearchLoading = false"
15 @select="applyJoinGroup"
16 ></el-autocomplete>
17 <!-- <button title="创建群组" @click="showCreateGroupModel">-->
18 <!-- <i class="tim-icon-add"></i>-->
19 <!-- </button>-->
20 </div>
21 <div class="group-container">
22 <group-item v-for="group in groupList" :key="group.groupID" :group="group" />
23 <el-dialog title="创建群组" :visible="createGroupModelVisible" @close="closeCreateGroupModel" width="30%">
24 <create-group></create-group>
25 </el-dialog>
26 </div>
27 </div>
28 </template>
29
30 <script>
31 import { mapState } from 'vuex'
32 import { Dialog, Autocomplete } from 'element-ui'
33 import CreateGroup from './create-group.vue'
34 import GroupItem from './group-item.vue'
35 export default {
36 data() {
37 return {
38 groupID: '',
39 hideSearchLoading: true
40 }
41 },
42 components: {
43 GroupItem,
44 ElDialog: Dialog,
45 CreateGroup,
46 ElAutocomplete: Autocomplete
47 },
48 computed: {
49 groupList: function() {
50 return this.$store.state.group.groupList
51 },
52 ...mapState({
53 createGroupModelVisible: state => {
54 return state.group.createGroupModelVisible
55 }
56 })
57 },
58 methods: {
59 onGroupUpdated(groupList) {
60 this.$store.dispatch('updateGroupList', groupList)
61 },
62 createGroup() {},
63 closeCreateGroupModel() {
64 this.$store.commit('updateCreateGroupModelVisible', false)
65 },
66 searchGroupByID(queryString, showInSearchResult) {
67 if (queryString.trim().length > 0) {
68 this.hideSearchLoading = false
69 this.tim
70 .searchGroupByID(queryString)
71 .then(({ data: { group } }) => {
72 showInSearchResult([group])
73 })
74 .catch(() => {
75 this.$store.commit('showMessage', {
76 message: '没有找到该群',
77 type: 'error'
78 })
79 })
80 } else {
81 this.hideSearchLoading = true
82 }
83 },
84 showCreateGroupModel() {
85 this.$store.commit('updateCreateGroupModelVisible', true)
86 },
87 applyJoinGroup(group) {
88 this.tim
89 .joinGroup({ groupID: group.groupID })
90 .then(async res => {
91 switch(res.data.status) {
92 case this.TIM.TYPES.JOIN_STATUS_WAIT_APPROVAL:
93 this.$store.commit('showMessage', {
94 message: '申请成功,等待群管理员确认。',
95 type: 'info'
96 })
97 break
98 case this.TIM.TYPES.JOIN_STATUS_SUCCESS:
99 await this.$store.dispatch(
100 'checkoutConversation',
101 `GROUP${res.data.group.groupID}`
102 )
103 this.$store.commit('showMessage', {
104 message: '加群成功',
105 type: 'success'
106 })
107 break
108 case this.TIM.TYPES.JOIN_STATUS_ALREADY_IN_GROUP:
109 this.$store.commit('showMessage', {
110 message: '您已经是群成员了,请勿重复加群哦!',
111 type: 'info'
112 })
113 break
114 default: break
115 }
116 })
117 .catch(error => {
118 this.$store.commit('showMessage', {
119 message: error.message,
120 type: 'error'
121 })
122 })
123 }
124 }
125 }
126 </script>
127
128 <style lang="stylus" scoped>
129 .list-container
130 height 100%
131 width 100%
132 display flex
133 flex-direction column
134 .group-container
135 overflow-y scroll
136 .header-bar
137 display: flex;
138 flex-shrink 0
139 height 50px
140 border-bottom 1px solid $background-deep-dark
141 padding 10px 10px 10px 20px
142 .group-seach-bar
143 width 100%
144 margin-right 10px
145 >>> .el-input
146 input
147 color $first
148 border none
149 border-radius 30px
150 background-color $deep-background !important
151 &::placeholder
152 color $font-dark
153 .el-icon-search
154 color $font-dark
155 button
156 float right
157 display: inline-block;
158 cursor: pointer;
159 background $background-deep-dark
160 border: none
161 color: $font-dark;
162 box-sizing: border-box;
163 transition: .3s;
164 -moz-user-select: none;
165 -webkit-user-select: none;
166 -ms-user-select: none;
167 margin: 0
168 padding 0
169 width 30px
170 height 30px
171 line-height 34px
172 font-size: 24px;
173 text-align: center;
174 white-space: nowrap;
175 border-radius: 50%
176 outline 0
177 flex-shrink 0
178 &:hover
179 transform: rotate(360deg);
180 color $light-primary
181 .scroll-container
182 overflow-y scroll
183 flex 1
184
185 </style>
1 <template>
2 <transition name="el-fade-in">
3 <div
4 class="member-profile-card-wrapper"
5 ref="member-profile-card"
6 v-show="visible"
7 :style="{top: y + 'px', left: x + 'px'}"
8 >
9 <div class="profile">
10 <avatar :src="member.avatar" class="avatar" />
11 <div class="basic">
12 <span>ID:{{member.userID}}</span>
13 <span>昵称:{{member.nick||"暂无"}}</span>
14 </div>
15 </div>
16 <el-divider class="divider" />
17 <div class="member-profile">
18 <div class="item">
19 <span class="label">群名片</span>
20 {{member.nameCard||"暂无"}}
21 </div>
22 <div class="item">
23 <span class="label">入群时间</span>
24 {{joinTime}}
25 </div>
26 <div v-if="member.muteUntil" class="item">
27 <span class="label">禁言至</span>
28 {{muteUntil}}
29 </div>
30 </div>
31 <el-button
32 class="send-message-btn"
33 type="primary"
34 size="mini"
35 title="发消息"
36 @click="handleSendMessage"
37 icon="el-icon-message"
38 circle
39 ></el-button>
40 </div>
41 </transition>
42 </template>
43
44 <script>
45 import { Divider } from 'element-ui'
46 import { getFullDate } from '../../utils/date'
47
48 // 群成员资料卡片组件,全局共用同一个组件。
49 export default {
50 name: 'MemberProfileCard',
51 components: {
52 ElDivider: Divider
53 },
54 data() {
55 return {
56 member: {},
57 x: 0, // 显示的位置 x
58 y: 0, // 显示的位置 y
59 visible: false
60 }
61 },
62 mounted() {
63 // 通过事件总线,监听 showMemebrProfile 事件
64 this.$bus.$on('showMemberProfile', this.handleShowMemberProfile, this)
65 },
66 computed: {
67 joinTime() {
68 if (this.member.joinTime) {
69 return getFullDate(new Date(this.member.joinTime * 1000))
70 }
71 return ''
72 },
73 muteUntil() {
74 if (this.member.muteUntil) {
75 return getFullDate(new Date(this.member.muteUntil * 1000))
76 }
77 return ''
78 }
79 },
80 methods: {
81 handleSendMessage() {
82 this.$store.dispatch('checkoutConversation', `C2C${this.member.userID}`)
83 this.hide()
84 },
85 handleShowMemberProfile({ event, member }) {
86 // 可以拿到 meber 和 点击事件的 event 信息
87 this.member = member || {}
88 this.x = event.x
89 this.y = event.y
90 this.show()
91 },
92 show() {
93 if (this.visible) {
94 return
95 }
96 // 显示时,监听全局点击事件,若点击区域不是当前组件,则隐藏
97 window.addEventListener('click', this.handleClick, this)
98 this.visible = true
99 },
100 hide() {
101 if (!this.visible) {
102 return
103 }
104 // 隐藏时,注销监听
105 window.removeEventListener('click', this.handleClick, this)
106 this.visible = false
107 },
108 handleClick(event) {
109 // 判断点击区域是否是当前组件,若不是,则隐藏组件
110 if (event.target !== this.$refs['member-profile-card']) {
111 this.hide()
112 }
113 }
114 }
115 }
116 </script>
117
118 <style lang="stylus" scoped>
119 .member-profile-card-wrapper {
120 max-width: 300px;
121 padding: 24px;
122 background: #fff;
123 border-radius: 5px;
124 position: fixed;
125 box-shadow: 0 0 10px gray;
126
127 .profile {
128 display: flex;
129
130 .avatar {
131 width: 60px;
132 height: 60px;
133 margin-right: 12px;
134 }
135
136 .basic {
137 display: flex;
138 align-items: flex-start;
139 flex-direction: column;
140 }
141 }
142
143 .divider {
144 margin: 12px 0;
145 }
146
147 .member-profile {
148 margin-bottom: 12px;
149
150 .item {
151 font-size: 15px;
152
153 .label {
154 display: inline-block;
155 width: 4em;
156 text-align: justify;
157 text-align-last: justify;
158 color: gray;
159 }
160 }
161 }
162 .send-message-btn {
163 float right
164 }
165 }
166 </style>
...\ No newline at end of file ...\ No newline at end of file
1 <template>
2 <div class="side-bar-wrapper">
3 <div class="bar-left">
4 <my-profile/>
5 <div class="tab-items" @click="handleClick">
6 <div id="conversation-list"
7 class="iconfont icon-conversation"
8 :class="{ active: showConversationList }"
9 title="会话列表">
10 <sup class="unread" v-if="totalUnreadCount !== 0">
11 <template v-if="totalUnreadCount > 99">99+</template>
12 <template v-else>{{ totalUnreadCount }}</template>
13 </sup>
14 </div>
15 <div
16 id="group-list"
17 class="iconfont icon-group"
18 :class="{ active: showGroupList }"
19 title="群组列表"
20 ></div>
21 <div
22 id="friend-list"
23 class="iconfont icon-contact"
24 :class="{ active: showFriendList }"
25 title="通讯录"
26 ></div>
27 <!-- <div-->
28 <!-- id="black-list"-->
29 <!-- class="iconfont icon-blacklist"-->
30 <!-- :class="{ active: showBlackList }"-->
31 <!-- title="黑名单列表"-->
32 <!-- ></div>-->
33 <!-- <div-->
34 <!-- id="group-live"-->
35 <!-- class="group-live"-->
36 <!-- title="群直播"-->
37 <!-- ></div>-->
38 </div>
39 <div class="bottom">
40 <div class="iconfont icon-tuichu" @click="$store.dispatch('logout')" title="退出"></div>
41 </div>
42 </div>
43 <div class="bar-right">
44 <conversation-list v-show="showConversationList"/>
45 <group-list v-show="showGroupList"/>
46 <friend-list v-show="showFriendList"/>
47 <black-list v-show="showBlackList"/>
48 </div>
49 </div>
50 </template>
51
52 <script>
53 import {mapGetters, mapState} from 'vuex'
54 import MyProfile from '../my-profile'
55 import ConversationList from '../conversation/conversation-list'
56 import GroupList from '../group/group-list'
57 import FriendList from '../friend/friend-list'
58 import BlackList from '../blacklist/blacklist'
59
60 const activeName = {
61 CONVERSATION_LIST: 'conversation-list',
62 GROUP_LIST: 'group-list',
63 FRIEND_LIST: 'friend-list',
64 BLACK_LIST: 'black-list',
65 GROUP_LIVE: 'group-live',
66 }
67 export default {
68 name: 'SideBar',
69 components: {
70 MyProfile,
71 ConversationList,
72 GroupList,
73 FriendList,
74 BlackList
75 },
76 data() {
77 return {
78 active: activeName.CONVERSATION_LIST,
79 activeName: activeName
80 }
81 },
82 computed: {
83 ...mapGetters(['totalUnreadCount']),
84 ...mapState({
85 userID: state => state.user.userID,
86 }),
87 showConversationList() {
88 return this.active === activeName.CONVERSATION_LIST
89 },
90 showGroupList() {
91 return this.active === activeName.GROUP_LIST
92 },
93 showFriendList() {
94 return this.active === activeName.FRIEND_LIST
95 },
96 showBlackList() {
97 return this.active === activeName.BLACK_LIST
98 },
99 showAddButton() {
100 return [activeName.CONVERSATION_LIST, activeName.GROUP_LIST].includes(
101 this.active
102 )
103 }
104 },
105 methods: {
106 checkoutActive(name) {
107 this.active = name
108 },
109 handleClick(event) {
110 switch (event.target.id) {
111 case activeName.CONVERSATION_LIST:
112 this.checkoutActive(activeName.CONVERSATION_LIST)
113 break
114 case activeName.GROUP_LIST:
115 this.checkoutActive(activeName.GROUP_LIST)
116 break
117 case activeName.FRIEND_LIST:
118 this.checkoutActive(activeName.FRIEND_LIST)
119 break
120 case activeName.BLACK_LIST:
121 this.checkoutActive(activeName.BLACK_LIST)
122 break
123 case activeName.GROUP_LIVE:
124 this.groupLive()
125 break
126 }
127 },
128 handleRefresh() {
129 switch (this.active) {
130 case activeName.CONVERSATION_LIST:
131 this.tim.getConversationList().catch(error => {
132 this.$store.commit('showMessage', {
133 type: 'error',
134 message: error.message
135 })
136 })
137 break
138 case activeName.GROUP_LIST:
139 this.getGroupList()
140 break
141 case activeName.FRIEND_LIST:
142 this.getFriendList()
143 break
144 case activeName.BLACK_LIST:
145 this.$store.dispatch('getBlacklist')
146 break
147 }
148 },
149 getGroupList() {
150 this.tim
151 .getGroupList()
152 .then(({data: groupList}) => {
153 this.$store.dispatch('updateGroupList', groupList)
154 })
155 .catch(error => {
156 this.$store.commit('showMessage', {
157 type: 'error',
158 message: error.message
159 })
160 })
161 },
162 getFriendList() {
163 this.tim
164 .getFriendList()
165 .then(({data: friendList}) => {
166 this.$store.commit('upadteFriendList', friendList)
167 })
168 .catch(error => {
169 this.$store.commit('showMessage', {
170 type: 'error',
171 message: error.message
172 })
173 })
174 .catch(error => {
175 this.$store.commit('showMessage', {
176 type: 'error',
177 message: error.message
178 })
179 })
180 },
181 groupLive() {
182 this.$store.commit('updateGroupLiveInfo', {
183 groupID: 0,
184 anchorID: this.userID,
185 })
186 this.$bus.$emit('open-group-live', {channel: 2})
187 },
188 }
189 }
190 </script>
191
192 <style lang="stylus" scoped>
193 .side-bar-wrapper {
194 height: 100%;
195 color: $black;
196 display: flex;
197 width: 100%;
198 overflow: hidden;
199
200 .bar-left {
201 display: flex;
202 flex-shrink: 0;
203 flex-direction: column;
204 width: 80px;
205 height: $height;
206 background-color: $background-deep-dark;
207
208 .tab-items {
209 display: flex;
210 flex-direction: column;
211 flex-grow: 1;
212
213 .iconfont {
214 position: relative;
215 margin: 0;
216 height: 70px;
217 line-height: 70px;
218 text-align: center;
219 font-size: 30px;
220 cursor: pointer;
221 color: $first;
222 user-select: none;
223 -moz-user-select: none;
224 }
225
226 .active {
227 color: $white;
228 background-color: $background-dark;
229
230 &::after {
231 content: ' ';
232 display: block;
233 position: absolute;
234 top: 0;
235 z-index: 0;
236 height: 70px;
237 // border-left 4px solid $border-highlight
238 border-left: 4px solid $light-primary;
239 }
240 }
241
242 .unread {
243 position: absolute;
244 top: 10px;
245 right: 10px;
246 z-index: 999;
247 display: inline-block;
248 height: 18px;
249 padding: 0 6px;
250 font-size: 12px;
251 color: #FFF;
252 line-height: 18px;
253 text-align: center;
254 white-space: nowrap;
255 border-radius: 10px;
256 background-color: $danger;
257 }
258 }
259
260 .bottom {
261 height: 70px;
262
263 & > span {
264 display: block;
265 }
266
267 .btn-more {
268 width: 100%;
269 height: 70px;
270 line-height: 70px;
271 font-size: 30px;
272 color: $first;
273 text-align: center;
274 cursor: pointer;
275 }
276
277 .iconfont {
278 height: 70px;
279 line-height: 70px;
280 text-align: center;
281 font-size: 30px;
282 cursor: pointer;
283 color: $first;
284 user-select: none;
285 -moz-user-select: none;
286 }
287
288 .iconfont:hover {
289 color: white;
290 }
291 }
292
293 .btn-more:hover {
294 color: $white;
295 }
296 }
297
298 .bar-right {
299 // flex 1
300 flex: 1 1 auto;
301 width: 100%;
302 min-width: 0;
303 height: $height;
304 position: relative;
305 background-color: $background-dark;
306 }
307
308 .group-live {
309 position relative
310 top 10px
311 left 25px
312 width 30px
313 height 30px
314 background url('../../assets/image/live-icon-gray.png') center no-repeat
315 background-size cover
316 cursor pointer
317 }
318 }
319 </style>
1 <template>
2 <div class="login-wrapper">
3 <img :src="logo" width="50" height="50" style="border-radius: 5px;margin-bottom:12px;"/>
4 <el-select v-model="userID">
5 <el-option v-for="index in 30" :key="index" :label="`user${index-1}`" :value="`user${index-1}`"></el-option>
6 </el-select>
7 <br>
8 <el-button type="primary" @click="login" style="width:100%;">登录</el-button>
9 </div>
10 </template>
11
12 <script>
13 import { Select, Option } from 'element-ui'
14 import logo from '../../assets/image/logo.png'
15 export default {
16 name: 'Login',
17 components: {
18 ElSelect: Select,
19 ElOption: Option
20 },
21 data() {
22 return {
23 userID: 'user0',
24 logo: logo
25 }
26 },
27 methods: {
28 login() {
29 this.$store.dispatch('login', this.userID)
30 }
31 }
32 }
33 </script>
34
35 <style lang="stylus" scoped>
36 .login-wrapper {
37 display: flex;
38 align-items: center;
39 flex-direction: column;
40 padding: 24px;
41 background: $white;
42 color: $black;
43 border-radius: 5px;
44 box-shadow: 0 11px 20px 0 rgba(0, 0, 0, .3);
45 }
46 </style>
1 <template>
2 <div class="image-previewer-wrapper" v-show="showPreviewer" @mousewheel="handleMouseWheel">
3 <div class="image-wrapper">
4 <img
5 class="image-preview"
6 :style="{transform: `scale(${zoom}) rotate(${rotate}deg)`}"
7 :src="previewUrl"
8 @click="close"
9 />
10 </div>
11 <i class="el-icon-close close-button" @click="close" />
12 <i class="el-icon-back prev-button" @click="goPrev"></i>
13 <i class="el-icon-right next-button" @click="goNext"></i>
14 <div class="actions-bar">
15 <i class="el-icon-zoom-out" @click="zoomOut"></i>
16 <i class="el-icon-zoom-in" @click="zoomIn"></i>
17 <i class="el-icon-refresh-left" @click="rotateLeft"></i>
18 <i class="el-icon-refresh-right" @click="rotateRight"></i>
19 <span class="image-counter">{{index+1}} / {{imgUrlList.length}}</span>
20 </div>
21 </div>
22 </template>
23
24 <script>
25 import { mapGetters } from 'vuex'
26 export default {
27 name: 'ImagePreviewer',
28 data() {
29 return {
30 url: '',
31 index: 0,
32 visible: false,
33 zoom: 1,
34 rotate: 0,
35 minZoom: 0.1
36 }
37 },
38 computed: {
39 ...mapGetters(['imgUrlList']),
40 showPreviewer() {
41 return this.url.length > 0 && this.visible
42 },
43 imageStyle() {
44 return {
45 transform: `scale(${this.zoom});`
46 }
47 },
48 previewUrl() {
49 return this.formatUrl(this.imgUrlList[this.index])
50 }
51 },
52 mounted() {
53 this.$bus.$on('image-preview', this.handlePreview)
54 },
55 methods: {
56 handlePreview({ url }) {
57 this.url = url
58 this.index = this.imgUrlList.findIndex(item => item === url)
59 this.visible = true
60 },
61 handleMouseWheel(event) {
62 if (event.wheelDelta > 0) {
63 this.zoomIn()
64 } else {
65 this.zoomOut()
66 }
67 },
68 zoomIn() {
69 this.zoom += 0.1
70 },
71 zoomOut() {
72 this.zoom =
73 this.zoom - 0.1 > this.minZoom ? this.zoom - 0.1 : this.minZoom
74 },
75 close() {
76 Object.assign(this, { zoom: 1 })
77 this.visible = false
78 },
79 rotateLeft() {
80 this.rotate -= 90
81 },
82 rotateRight() {
83 this.rotate += 90
84 },
85 goNext() {
86 this.index = (this.index + 1) % this.imgUrlList.length
87 },
88 goPrev() {
89 this.index =
90 this.index - 1 >= 0 ? this.index - 1 : this.imgUrlList.length - 1
91 },
92 formatUrl(url) {
93 if (!url) {
94 return ''
95 }
96 return url.slice(0, 2) === '//' ? `https:${url}` : url
97 }
98 }
99 }
100 </script>
101
102 <style scoped>
103 .image-previewer-wrapper {
104 position: fixed;
105 width: 100%;
106 left: 0;
107 top: 0;
108 height: 100%;
109 display: flex;
110 justify-content: center;
111 align-items: flex-start;
112 background: rgba(14, 12, 12, 0.7);
113 z-index: 2000;
114 cursor: zoom-out;
115 }
116
117 .close-button {
118 cursor: pointer;
119 font-size: 28px;
120 color: #000;
121 position: fixed;
122 top: 50px;
123 right: 50px;
124 background: rgba(255, 255, 255, 0.8);
125 border-radius: 50%;
126 padding: 6px;
127 }
128 .image-wrapper {
129 position: relative;
130 width: 100%;
131 height: 100%;
132 display: flex;
133 justify-content: center;
134 align-items: center;
135 }
136 .image-preview {
137 transition: transform 0.1s ease 0s;
138 }
139 .actions-bar {
140 display: flex;
141 justify-content: space-around;
142 align-items: center;
143 position: fixed;
144 bottom: 50px;
145 left: 50%;
146 margin-left: -100px;
147 padding: 12px;
148 border-radius: 6px;
149 background: rgba(255, 255, 255, 0.8);
150 }
151 .actions-bar i {
152 font-size: 24px;
153 cursor: pointer;
154 margin: 0 6px;
155 }
156
157 .prev-button,
158 .next-button {
159 position: fixed;
160 cursor: pointer;
161 background: rgba(255, 255, 255, 0.8);
162 border-radius: 50%;
163 font-size: 24px;
164 padding: 12px;
165 }
166 .prev-button {
167 left: 0;
168 top: 50%;
169 }
170 .next-button {
171 right: 0;
172 top: 50%;
173 }
174 .image-counter {
175 background: rgba(20, 18, 20, 0.53);
176 padding: 3px;
177 border-radius: 3px;
178 color: #fff;
179 }
180 </style>
1 <template>
2 <div class="chat-bubble" @mousedown.stop @contextmenu.prevent>
3 <el-dropdown trigger="" ref="dropdown" v-if="!message.isRevoked" @command="handleCommand">
4 <div style="display: flex">
5 <div v-if="isMine && messageReadByPeer" class="message-status">
6 <span>{{messageReadByPeer}}</span>
7 </div>
8 <div class="message-content" :class="bubbleStyle">
9 <slot></slot>
10 </div>
11 </div>
12 <el-dropdown-menu slot="dropdown">
13 <el-dropdown-item command="revoke" v-if="isMine">撤回</el-dropdown-item>
14 <!-- <el-dropdown-item command="delete">删除</el-dropdown-item> -->
15 </el-dropdown-menu>
16 </el-dropdown>
17 <div class="group-tip-element-wrapper" v-if="message.isRevoked">
18 {{text}}
19 <el-button type="text" size="mini" class="edit-button" v-show="isEdit" @click="reEdit">&nbsp;重新编辑</el-button>
20 </div>
21 </div>
22 </template>
23
24 <script>
25 export default {
26 name: 'MessageBubble',
27 data() {
28 return {
29 isTimeout: false
30 }
31 },
32 props: {
33 isMine: {
34 type: Boolean
35 },
36 isNew: {
37 type: Boolean
38 },
39 message: {
40 type: Object,
41 required: true
42 }
43 },
44 created() {
45 this.isTimeoutHandler()
46 },
47 mounted() {
48 if (this.$refs.dropdown && this.$refs.dropdown.$el) {
49 this.$refs.dropdown.$el.addEventListener('mousedown', this.handleDropDownMousedown)
50 }
51 },
52 beforeDestroy() {
53 if (this.$refs.dropdown && this.$refs.dropdown.$el) {
54 this.$refs.dropdown.$el.removeEventListener('mousedown', this.handleDropDownMousedown)
55 }
56 },
57 computed: {
58 bubbleStyle() {
59 let classString = ''
60 if (this.isMine) {
61 classString += 'message-send'
62 } else {
63 classString += 'message-received'
64 }
65 if (this.isNew) {
66 classString += 'new'
67 }
68 return classString
69 },
70 text() {
71 if (this.message.conversationType === this.TIM.TYPES.CONV_C2C && !this.isMine) {
72 return '对方撤回了一条消息'
73 }
74 if (this.message.conversationType === this.TIM.TYPES.CONV_GROUP && !this.isMine) {
75 return `${this.message.from}撤回了一条消息`
76 }
77 return '你撤回了一条消息'
78 },
79 messageReadByPeer() {
80 if (this.message.status !== 'success') {
81 return false
82 }
83 if (this.message.conversationType === this.TIM.TYPES.CONV_C2C && this.message.isPeerRead) {
84 return '已读'
85 }
86 if (this.message.conversationType === this.TIM.TYPES.CONV_C2C && !this.message.isPeerRead) {
87 return '未读'
88 }
89 return ''
90 },
91 isEdit() {
92 if (!this.isMine) {
93 return false
94 }
95 if (this.message.type !== this.TIM.TYPES.MSG_TEXT) {
96 return false
97 }
98 if (this.isTimeout) {
99 return false
100 }
101 return true
102 },
103 },
104 methods: {
105 handleDropDownMousedown(e) {
106 if (!this.isMine || this.isTimeout) {
107 return
108 }
109 if (e.buttons === 2) {
110 if (this.$refs.dropdown.visible) {
111 this.$refs.dropdown.hide()
112 } else {
113 this.$refs.dropdown.show()
114 }
115 }
116 },
117 handleCommand(command) {
118 switch (command) {
119 case 'revoke':
120 this.tim.revokeMessage(this.message).then(() => {
121 this.isTimeoutHandler()
122 }).catch((err) => {
123 this.$store.commit('showMessage', {
124 message: err,
125 type: 'warning'
126 })
127 })
128 break
129 case 'delete':
130 break
131 default:
132 break
133 }
134 },
135 isTimeoutHandler() { // 从发送消息时间开始算起,两分钟内可以编辑
136 let now = new Date()
137 if (parseInt(now.getTime() / 1000) - this.message.time > 2 * 60) {
138 this.isTimeout = true
139 return
140 }
141 setTimeout(this.isTimeoutHandler, 1000)
142 },
143 reEdit() {
144 this.$bus.$emit('reEditMessage', this.message.payload.text)
145 }
146 }
147 }
148 </script>
149
150 <style lang="stylus" scoped>
151 .chat-bubble
152 position relative
153 .message-status
154 display: flex;
155 min-width: 25px;
156 margin-right: 10px;
157 justify-content: center;
158 align-items: center;
159 font-size: 12px;
160 color: #6e7981;
161 .message-content
162 outline: none
163 font-size 14px
164 position relative
165 max-width 350px
166 word-wrap break-word
167 word-break break-all
168 padding 10px
169 box-shadow: 0 5px 10px 0 rgba(0,0,0,.1);
170 span
171 white-space pre-wrap
172 margin 0
173 text-shadow $regular 0 0 0.05em
174 img
175 vertical-align bottom
176 &::before
177 position: absolute
178 top: 0
179 width: 12px
180 height: 40px
181 content "\e900"
182 // content "\e906"
183 font-family 'tim' !important
184 font-size 24px // 32px mac上会模糊 24px正常 , window 24px模糊 28px 32px正常 36px windows mac 基本一致,但是太大
185 .message-received
186 background-color $white
187 margin-left 15px
188 border-radius 0 4px 4px 4px
189 &::before
190 left -10px
191 transform scaleX(-1)
192 color $white
193 &.new
194 transform: scale(0);
195 transform-origin: top left;
196 animation: bounce 500ms linear both;
197 .message-send
198 background-color $light-primary
199 margin-right 15px
200 border-radius 4px 0 4px 4px
201 color $white
202 &::before
203 right: -10px
204 color $light-primary
205 &.new
206 transform: scale(0);
207 transform-origin: top right;
208 animation: bounce 500ms linear both;
209 .el-dropdown {
210 vertical-align: top;
211 display flex
212 }
213 .el-dropdown + .el-dropdown {
214 margin-left: 15px;
215 }
216 .el-icon-arrow-down {
217 font-size: 12px;
218 }
219 .group-tip-element-wrapper
220 background $white
221 padding 4px 15px
222 border-radius 3px
223 color $secondary
224 font-size 12px
225 // text-shadow $secondary 0 0 0.05em
226 .edit-button
227 padding-top 4px
228 height 20px
229 font-size 10px
230 @keyframes bounce {
231 0% { transform: matrix3d(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
232 4.7% { transform: matrix3d(0.45, 0, 0, 0, 0, 0.45, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
233 9.41% { transform: matrix3d(0.883, 0, 0, 0, 0, 0.883, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
234 14.11% { transform: matrix3d(1.141, 0, 0, 0, 0, 1.141, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
235 18.72% { transform: matrix3d(1.212, 0, 0, 0, 0, 1.212, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
236 24.32% { transform: matrix3d(1.151, 0, 0, 0, 0, 1.151, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
237 29.93% { transform: matrix3d(1.048, 0, 0, 0, 0, 1.048, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
238 35.54% { transform: matrix3d(0.979, 0, 0, 0, 0, 0.979, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
239 41.04% { transform: matrix3d(0.961, 0, 0, 0, 0, 0.961, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
240 52.15% { transform: matrix3d(0.991, 0, 0, 0, 0, 0.991, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
241 63.26% { transform: matrix3d(1.007, 0, 0, 0, 0, 1.007, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
242 85.49% { transform: matrix3d(0.999, 0, 0, 0, 0, 0.999, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
243 100% { transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); }
244 }
245 </style>
1 <template>
2 <message-bubble :isMine=isMine :message=message>
3 <div class="custom-element-wrapper">
4 <div class="survey" v-if="this.payload.data === 'survey'">
5 <div class="title">对IM DEMO的评分和建议</div>
6 <el-rate
7 v-model="rate"
8 disabled
9 show-score
10 text-color="#ff9900"
11 score-template="{value}">
12 </el-rate>
13 <div class="suggestion">{{this.payload.extension}}</div>
14 </div>
15 <span class="text" title="您可以自行解析自定义消息" v-else>
16 <template v-if="text.isFromGroupLive && text.isFromGroupLive === 1">
17 <message-group-live-status :liveInfo='text' />
18 </template>
19 <template v-else>{{text}}</template>
20 </span>
21 </div>
22 </message-bubble>
23 </template>
24
25 <script>
26 import { mapState } from 'vuex'
27 import MessageBubble from '../message-bubble'
28 import { Rate } from 'element-ui'
29 import MessageGroupLiveStatus from '../message-group-live-status'
30
31 export default {
32 name: 'CustomElement',
33 props: {
34 payload: {
35 type: Object,
36 required: true
37 },
38 message: {
39 type: Object,
40 required: true
41 },
42 isMine: {
43 type: Boolean
44 }
45 },
46 components: {
47 MessageBubble,
48 ElRate: Rate,
49 MessageGroupLiveStatus
50 },
51 computed: {
52 ...mapState({
53 currentUserProfile: state => state.user.currentUserProfile
54 }),
55 text() {
56 return this.translateCustomMessage(this.payload)
57 },
58 rate() {
59 return parseInt(this.payload.description)
60 }
61 },
62 methods: {
63 translateCustomMessage(payload) {
64 let videoPayload = {}
65 try{
66 videoPayload = JSON.parse(payload.data)
67 } catch(e) {
68 videoPayload = {}
69 }
70 if (payload.data === 'group_create') {
71 return `${payload.extension}`
72 }
73 if (videoPayload.roomId) {
74 videoPayload.roomId = videoPayload.roomId.toString()
75 videoPayload.isFromGroupLive = 1
76 return videoPayload
77 }
78 if(payload.text) {
79 return payload.text
80 }else{
81 return '[自定义消息]'
82 }
83 }
84 }
85 }
86 </script>
87
88 <style lang="stylus" scoped>
89 .text
90 font-weight bold
91 .title
92 font-size 16px
93 font-weight 600
94 padding-bottom 10px
95 .survey
96 background-color white
97 color black
98 padding 20px
99 display flex
100 flex-direction column
101 .suggestion
102 padding-top 10px
103 font-size 14px
104 </style>
1 <template>
2 <message-bubble :isMine=isMine :message=message>
3 <div class="face-element-wrapper">
4 <img :src="url"/>
5 </div>
6 </message-bubble>
7 </template>
8
9 <script>
10 import MessageBubble from '../message-bubble'
11 export default {
12 name: 'FaceElement',
13 props: {
14 payload: {
15 type: Object,
16 required: true
17 },
18 message: {
19 type: Object,
20 required: true
21 },
22 isMine: {
23 type: Boolean
24 }
25 },
26 components: {
27 MessageBubble,
28 },
29 computed:{
30 url() {
31 let name = ''
32 if (this.payload.data.indexOf('@2x') > 0) {
33 name = this.payload.data
34 } else {
35 name = this.payload.data + '@2x'
36 }
37 return `https://webim-1252463788.file.myqcloud.com/assets/face-elem/${name}.png`
38 }
39 }
40 }
41 </script>
42
43 <style lang="stylus" scoped>
44 .face-element-wrapper
45 img
46 max-width 90px
47 </style>
1 <template>
2 <message-bubble :isMine=isMine :message=message>
3 <div class="file-element-wrapper" title="单击下载" @click="downloadFile">
4 <div class="header">
5 <i class="el-icon-document file-icon"></i>
6 <div class="file-element">
7 <span class="file-name">{{ fileName }}</span>
8 <span class="file-size">{{ size }}</span>
9 </div>
10 </div>
11 <el-progress
12 v-if="showProgressBar"
13 :percentage="percentage"
14 :color="percentage => (percentage === 100 ? '#67c23a' : '#409eff')"
15 />
16 </div>
17 </message-bubble>
18 </template>
19
20 <script>
21 import MessageBubble from '../message-bubble'
22 import { Progress } from 'element-ui'
23 export default {
24 name: 'FileElement',
25 props: {
26 payload: {
27 type: Object,
28 required: true
29 },
30 message: {
31 type: Object,
32 required: true
33 },
34 isMine: {
35 type: Boolean
36 }
37 },
38 components: {
39 MessageBubble,
40 ElProgress: Progress
41 },
42 computed: {
43 fileName() {
44 return this.payload.fileName
45 },
46 fileUrl() {
47 return this.payload.fileUrl
48 },
49 size() {
50 const size = this.payload.fileSize
51 if (size > 1024) {
52 if (size / 1024 > 1024) {
53 return `${this.toFixed(size / 1024 / 1024)} Mb`
54 }
55 return `${this.toFixed(size / 1024)} Kb`
56 }
57 return `${this.toFixed(size)}B`
58 },
59 showProgressBar() {
60 return this.$parent.message.status === 'unSend'
61 },
62 percentage() {
63 return Math.floor((this.$parent.message.progress || 0) * 100)
64 }
65 },
66 methods: {
67 toFixed(number, precision = 2) {
68 return number.toFixed(precision)
69 },
70 downloadFile() {
71 // 浏览器支持fetch则用blob下载,避免浏览器点击a标签,跳转到新页面预览的行为
72 if (window.fetch) {
73 fetch(this.fileUrl)
74 .then(res => res.blob())
75 .then(blob => {
76 let a = document.createElement('a')
77 let url = window.URL.createObjectURL(blob)
78 a.href = url
79 a.download = this.fileName
80 a.click()
81 })
82 } else {
83 let a = document.createElement('a')
84 a.href = this.fileUrl
85 a.target = '_blank'
86 a.download = this.filename
87 a.click()
88 }
89 }
90 }
91 }
92 </script>
93
94 <style lang="stylus" scoped>
95 .file-element-wrapper {
96 cursor pointer
97 }
98 .header {
99 display: flex;
100 }
101 .file-icon {
102 font-size: 40px !important;
103 }
104 .file-element {
105 display: flex;
106 flex-direction: column;
107 margin-left: 12px;
108 }
109 .file-size {
110 font-size: 12px;
111 padding-top 5px
112 }
113 </style>
1 <template>
2 <message-bubble :isMine="isMine" :message=message>
3 <a class="geo-element" :href="href" target="_blank" title="点击查看详情">
4 <span class="el-icon-location-outline">{{payload.description}}</span>
5 <img :src="url" />
6 </a>
7 </message-bubble>
8 </template>
9
10 <script>
11 import MessageBubble from '../message-bubble'
12
13 export default {
14 name: 'GeoElement',
15 components: {
16 MessageBubble
17 },
18 props: {
19 payload: {
20 type: Object,
21 required: true
22 },
23 message: {
24 type: Object,
25 required: true
26 },
27 isMine: {
28 type: Boolean
29 }
30 },
31 data() {
32 return {
33 url: ''
34 }
35 },
36 computed: {
37 lon() {
38 return this.payload.longitude.toFixed(6)
39 },
40 lat() {
41 return this.payload.latitude.toFixed(6)
42 },
43 href() {
44 return `https://map.qq.com/?type=marker&isopeninfowin=1&markertype=1&pointx=${
45 this.lon
46 }&pointy=${this.lat}&name=${this.payload.description}`
47 }
48 },
49 mounted() {
50 this.url = `https://apis.map.qq.com/ws/staticmap/v2/?center=${this.lat},${
51 this.lon
52 }&zoom=10&size=300*150&maptype=roadmap&markers=size:large|color:0xFFCCFF|label:k|${
53 this.lat
54 },${this.lon}&key=UBNBZ-PTP3P-TE7DB-LHRTI-Y4YLE-VWBBD`
55 }
56 }
57 </script>
58
59 <style lang="stylus" scoped>
60 .geo-element {
61 text-decoration: none;
62 color: #000;
63 display: flex;
64 flex-direction: column;
65 padding: 6px;
66 font-size: 18px;
67
68 img {
69 margin-top: 12px;
70 }
71 }
72 </style>
1 <template>
2 <message-bubble :isMine="false" :message=message>
3 <div class="group-system-element-wrapper">
4 {{ text }}
5 <el-button v-if="isJoinGroupRequest" type="text" @click="showDialog = true">处理</el-button>
6 <el-dialog title="处理加群申请" :visible.sync="showDialog" width="30%">
7 <el-form ref="form" v-model="form" label-width="100px">
8 <el-form-item label="处理结果:">
9 <el-radio-group v-model="form.handleAction">
10 <el-radio label="Agree">同意</el-radio>
11 <el-radio label="Reject">拒绝</el-radio>
12 </el-radio-group>
13 </el-form-item>
14 <el-form-item label="附言:">
15 <el-input
16 type="textarea"
17 resize="none"
18 :rows="3"
19 placeholder="请输入附言"
20 v-model="form.handleMessage"
21 />
22 </el-form-item>
23 </el-form>
24 <span slot="footer" class="dialog-footer">
25 <el-button @click="showDialog = false">取 消</el-button>
26 <el-button type="primary" @click="handleGroupApplication">确 定</el-button>
27 </span>
28 </el-dialog>
29 </div>
30 </message-bubble>
31 </template>
32
33 <script>
34 import { Dialog, Form, FormItem, RadioGroup, Radio } from 'element-ui'
35 import MessageBubble from '../message-bubble'
36 import { translateGroupSystemNotice } from '../../../utils/common'
37
38 export default {
39 name: 'GroupSystemNoticeElement',
40 props: {
41 payload: {
42 type: Object,
43 required: true
44 },
45 message: {
46 type: Object,
47 required: false
48 }
49 },
50 components: {
51 ElDialog: Dialog,
52 ElForm: Form,
53 ElFormItem: FormItem,
54 ElRadioGroup: RadioGroup,
55 ElRadio: Radio,
56 MessageBubble
57 },
58 data() {
59 return {
60 showDialog: false,
61 form: {
62 handleAction: 'Agree',
63 handleMessage: ''
64 }
65 }
66 },
67 computed: {
68 text() {
69 return translateGroupSystemNotice(this.message)
70 },
71 title() {
72 if (this.message.type === this.TIM.TYPES.MSG_GRP_SYS_NOTICE) {
73 return '群系统通知'
74 }
75 return '系统通知'
76 },
77 isJoinGroupRequest() {
78 return this.payload.operationType === 1
79 }
80 },
81 methods: {
82 handleGroupApplication() {
83 this.tim
84 .handleGroupApplication({
85 handleAction: this.form.handleAction,
86 handleMessage: this.form.handleMessage,
87 message: this.message
88 })
89 .then(() => {
90 this.showDialog = false
91 this.$store.commit('removeMessage', this.message)
92 })
93 .catch(error => {
94 this.$store.commit('showMessage', {
95 type: 'error',
96 message: error.message
97 })
98 this.showDialog = false
99 })
100 }
101 }
102 }
103 </script>
104
105 <style lang="stylus" scoped>
106 .card {
107 background: #fff;
108 padding: 12px;
109 border-radius: 5px;
110 width: 300px;
111
112 .card-header {
113 font-size: 18px;
114 }
115
116 .card-content {
117 font-size: 14px;
118 }
119 }
120 </style>
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
No preview for this file type
No preview for this file type
No preview for this file type
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
No preview for this file type
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.