init
0 parents
Showing
278 changed files
with
4831 additions
and
0 deletions
.eslintignore
0 → 100644
.eslintrc
0 → 100644
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 | } |
.gitignore
0 → 100644
README.md
0 → 100644
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 |  | ||
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 |  | ||
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视频通话,基础流程跑通 |
_doc/image/commit-message.png
0 → 100644

206 KB
_doc/image/demo-init-1.png
0 → 100644

127 KB
babel.config.js
0 → 100644
package.json
0 → 100644
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 | } |
public/BenzAMRRecorder.js
0 → 100644
This diff could not be displayed because it is too large.
public/debug/GenerateTestUserSig.js
0 → 100644
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.
public/favicon.ico
0 → 100644
No preview for this file type
public/index.html
0 → 100644
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> |
public/logo.png
0 → 100644

120 KB
sdk/tim-js.js
0 → 100644
This diff could not be displayed because it is too large.
sdk/trtc-calling-js.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/assets/css/base.styl
0 → 100644
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 |
src/assets/icon/iconfont.css
0 → 100644
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 |
src/assets/icon/iconfont.eot
0 → 100644
No preview for this file type
src/assets/icon/iconfont.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/assets/icon/iconfont.svg
0 → 100644
This diff is collapsed.
Click to expand it.
src/assets/icon/iconfont.ttf
0 → 100644
No preview for this file type
src/assets/icon/iconfont.woff
0 → 100644
No preview for this file type
src/assets/icon/iconfont.woff2
0 → 100644
No preview for this file type
src/assets/icon/tim.css
0 → 100644
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 | } |
src/assets/icon/tim.eot
0 → 100644
No preview for this file type
src/assets/icon/tim.svg
0 → 100644
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=" " horiz-adv-x="512" d="" /> | ||
10 | <glyph unicode="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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 |
src/assets/icon/tim.ttf
0 → 100644
No preview for this file type
src/assets/icon/tim.woff
0 → 100644
No preview for this file type
src/assets/image/bg.jpg
0 → 100644

162 KB
src/assets/image/big-camera-off.png
0 → 100644

2.7 KB
src/assets/image/big-camera-on.png
0 → 100644

2.64 KB
src/assets/image/big-mic-off.png
0 → 100644

2.51 KB
src/assets/image/big-mic-on.png
0 → 100644

2.63 KB
src/assets/image/call.png
0 → 100644

1.48 KB
src/assets/image/camera-max.png
0 → 100644

2.63 KB
src/assets/image/camera-open.png
0 → 100644

933 Bytes
src/assets/image/close-mic.png
0 → 100644

6.83 KB
src/assets/image/close.png
0 → 100644

773 Bytes
src/assets/image/live-icon-gray.png
0 → 100644

4.83 KB
src/assets/image/live-icon-hover.png
0 → 100644

4.59 KB
src/assets/image/live-icon.png
0 → 100644

4.55 KB
src/assets/image/living-icon.gif
0 → 100644

3.9 KB
src/assets/image/logo.png
0 → 100644

19.8 KB
src/assets/image/logo1.png
0 → 100644

120 KB
src/assets/image/no-video.png
0 → 100644

13.2 KB
src/assets/image/open-mic.png
0 → 100644

5.74 KB
src/assets/image/pause-icon.png
0 → 100644

6.04 KB
src/assets/image/player-start.png
0 → 100644

1.67 KB
src/assets/image/player-stop.png
0 → 100644

1.36 KB
src/assets/image/poster.png
0 → 100644

326 KB
src/assets/image/share-icon.png
0 → 100644

6.17 KB
src/assets/image/smile.png
0 → 100644

1.85 KB
src/assets/image/system.png
0 → 100644

2.25 KB
src/assets/image/txc-logo.png
0 → 100644

2.41 KB
src/assets/image/video-bg.png
0 → 100644

326 KB
src/assets/image/web-pusher-start.png
0 → 100644

897 Bytes
src/assets/image/web-pusher-stop.png
0 → 100644

1022 Bytes
src/components/avatar.vue
0 → 100644
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> |
src/components/avatars.vue
0 → 100644
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> |
src/components/blacklist/blacklist-item.vue
0 → 100644
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> |
src/components/blacklist/blacklist.vue
0 → 100644
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> |
src/components/context-menu.vue
0 → 100644
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> |
This diff is collapsed.
Click to expand it.
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> |
src/components/friend/friend-item.vue
0 → 100644
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> |
src/components/friend/friend-list.vue
0 → 100644
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> |
This diff is collapsed.
Click to expand it.
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> |
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
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> |
src/components/group-live/index.vue
0 → 100644
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 |
src/components/group/create-group.vue
0 → 100644
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> |
src/components/group/group-item.vue
0 → 100644
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> |
src/components/group/group-list.vue
0 → 100644
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> |
src/components/group/member-profile-card.vue
0 → 100644
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 |
src/components/layout/side-bar.vue
0 → 100644
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> |
src/components/login/login.vue
0 → 100644
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> |
src/components/message/call-layer.vue
0 → 100644
This diff is collapsed.
Click to expand it.
src/components/message/image-previewer.vue
0 → 100644
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> |
src/components/message/message-bubble.vue
0 → 100644
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"> 重新编辑</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 is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
src/components/message/message-footer.vue
0 → 100644
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
src/components/message/message-header.vue
0 → 100644
This diff is collapsed.
Click to expand it.
src/components/message/message-item.vue
0 → 100644
This diff is collapsed.
Click to expand it.
src/components/message/message-send-box.vue
0 → 100644
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
src/components/my-profile.vue
0 → 100644
This diff is collapsed.
Click to expand it.
src/components/profile-card.vue
0 → 100644
This diff is collapsed.
Click to expand it.
src/components/user/login.vue
0 → 100644
This diff is collapsed.
Click to expand it.
src/index.vue
0 → 100644
This diff is collapsed.
Click to expand it.
src/main.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/store/actions.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/store/index.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/store/modules/blacklist.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/store/modules/conversation.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/store/modules/friend.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/store/modules/group.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/store/modules/groupLive.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/store/modules/user.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/store/modules/video.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/store/mutations.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/tim.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/trtc-calling.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/common.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/date.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/decodeText.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/emojiMap.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/formatDuration.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/helper.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/index.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/mta.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/request.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/rtc-client.js
0 → 100644
This diff is collapsed.
Click to expand it.
src/utils/trtcCustomMessageMap.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/AVChatRoom/css/mobile.css
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/AVChatRoom/img/2016.gif
0 → 100644

38.7 KB
v1.7.x/AVChatRoom/img/2017.jpg
0 → 100644

1.29 KB
v1.7.x/AVChatRoom/img/back-img.png
0 → 100644

505 KB
v1.7.x/AVChatRoom/img/back-img2.png
0 → 100644

224 KB
v1.7.x/AVChatRoom/img/back-img2.png.back.png
0 → 100644

463 KB
v1.7.x/AVChatRoom/img/back.png
0 → 100644

936 Bytes
v1.7.x/AVChatRoom/img/comment.png
0 → 100644

2.94 KB
v1.7.x/AVChatRoom/img/comment_hover.png
0 → 100644

2.92 KB
v1.7.x/AVChatRoom/img/drop_out.png
0 → 100644

322 Bytes
v1.7.x/AVChatRoom/img/drop_out_hover.png
0 → 100644

438 Bytes
v1.7.x/AVChatRoom/img/face.png
0 → 100644

2.96 KB
v1.7.x/AVChatRoom/img/face_hover.png
0 → 100644

2.79 KB
v1.7.x/AVChatRoom/img/fav.png
0 → 100644

704 Bytes
v1.7.x/AVChatRoom/img/form-back.png
0 → 100644

936 Bytes
v1.7.x/AVChatRoom/img/like.png
0 → 100644

2.82 KB
v1.7.x/AVChatRoom/img/like_hover.png
0 → 100644

2.72 KB
v1.7.x/AVChatRoom/img/like_icon_1.png
0 → 100644

1.16 KB
v1.7.x/AVChatRoom/img/like_icon_2.png
0 → 100644

1.13 KB
v1.7.x/AVChatRoom/img/like_icon_3.png
0 → 100644

1.29 KB
v1.7.x/AVChatRoom/img/like_icon_4.png
0 → 100644

1.18 KB
v1.7.x/AVChatRoom/img/like_white.png
0 → 100644

299 Bytes
v1.7.x/AVChatRoom/img/user-img.png
0 → 100644

6.54 KB
v1.7.x/AVChatRoom/img/visitor_white.png
0 → 100644

305 Bytes
v1.7.x/AVChatRoom/index.html
0 → 100644
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.
v1.7.x/AVChatRoom/js/demo_base.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/AVChatRoom/js/demo_group_notice.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/AVChatRoom/js/jquery-2.1.4.min.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/AVChatRoom/sdk/json2.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/AVChatRoom/sdk/webim.js
0 → 100644
This diff could not be displayed because it is too large.
v1.7.x/README.md
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/css/bootstrap-table.css
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/css/bootstrap.css
0 → 100644
This diff could not be displayed because it is too large.
v1.7.x/css/jquery-ui.css
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/css/webim_demo.css
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/docs/一分钟集成SDK(Web).md
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/docs/一分钟集成SDK(小程序).md
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/docs/未读计数(Web SDK).md
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/docs/概述(Web SDK).md
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/docs/概述(Web 直播聊天室).md
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/docs/消息收发(Web SDK).md
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/docs/消息收发(Web 直播聊天室).md
0 → 100644
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
v1.7.x/docs/登出(Web SDK).md
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/docs/登出(Web 直播聊天室).md
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/docs/登录(Web SDK).md
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/docs/登录(Web 直播聊天室).md
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/docs/群组管理(Web SDK)/群成员管理(Web SDK).md
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/docs/群组管理(Web SDK)/群提示消息(Web SDK).md
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/docs/群组管理(Web SDK)/群系统消息(Web SDK).md
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/docs/群组管理(Web SDK)/群组管理(Web SDK).md
0 → 100644
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
No preview for this file type
This diff is collapsed.
Click to expand it.
No preview for this file type
No preview for this file type
v1.7.x/img/2016.gif
0 → 100644

38.7 KB
v1.7.x/img/2017.jpg
0 → 100644

1.29 KB
v1.7.x/img/friend.jpg
0 → 100644

4.17 KB
v1.7.x/img/group.jpg
0 → 100644

3.52 KB
v1.7.x/img/icon.png
0 → 100644

3.03 KB
v1.7.x/img/icon_1.png
0 → 100644

3.03 KB
v1.7.x/img/layer_arrow.png
0 → 100644

315 Bytes
v1.7.x/img/me.jpg
0 → 100644

7.48 KB

418 Bytes

312 Bytes
v1.7.x/img/ui-bg_flat_10_000000_40x100.png
0 → 100644

205 Bytes
v1.7.x/img/ui-bg_glass_100_f6f6f6_1x400.png
0 → 100644

262 Bytes
v1.7.x/img/ui-bg_glass_100_fdf5ce_1x400.png
0 → 100644

348 Bytes
v1.7.x/img/ui-bg_glass_65_ffffff_1x400.png
0 → 100644

207 Bytes
v1.7.x/img/ui-bg_glass_65_ffffff_1x400_1.png
0 → 100644

207 Bytes

5.68 KB

278 Bytes

328 Bytes
v1.7.x/img/ui-icons_222222_256x240.png
0 → 100644

6.76 KB
v1.7.x/img/ui-icons_228ef1_256x240.png
0 → 100644

4.44 KB
v1.7.x/img/ui-icons_454545_256x240.png
0 → 100644

6.91 KB
v1.7.x/img/ui-icons_ef8c08_256x240.png
0 → 100644

4.44 KB
v1.7.x/img/ui-icons_ffd27a_256x240.png
0 → 100644

4.44 KB
v1.7.x/img/ui-icons_ffffff_256x240.png
0 → 100644

6.15 KB
v1.7.x/img/wlf_bg.png
0 → 100644

110 Bytes
v1.7.x/img/wlf_title_bg.jpg
0 → 100644

319 Bytes
v1.7.x/img/wlf_title_btn.jpg
0 → 100644

477 Bytes
v1.7.x/index.html
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/base.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/common/show_one_msg.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/debug/GenerateTestUserSig.js
0 → 100644
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 is collapsed.
Click to expand it.
v1.7.x/js/friend/friend_manager.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/friend/friend_pendency_manager.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/group/group_manager.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/group/group_member_manager.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/group/group_pendency_manager.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/lib/BenzAMRRecorder.js
0 → 100644
This diff could not be displayed because it is too large.
v1.7.x/js/lib/audiojs/audio.min.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/lib/audiojs/audio.min_my.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/lib/audiojs/audiojs.swf
0 → 100644
No preview for this file type
v1.7.x/js/lib/audiojs/player-graphics.gif
0 → 100644

4.39 KB
v1.7.x/js/lib/base64/base64.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/lib/base64/base64.min.js
0 → 100644
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
v1.7.x/js/lib/bootstrap/bootstrap-table.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/lib/bootstrap/bootstrap.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/lib/jquery/jquery-ui.js
0 → 100644
This diff could not be displayed because it is too large.
v1.7.x/js/lib/jquery/jquery.js
0 → 100644
This diff could not be displayed because it is too large.
v1.7.x/js/lib/lodash.min.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/lib/md5/spark-md5.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/lib/xss.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/login/login.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/logout/logout.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/msg/get_history_msg.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/msg/receive_friend_system_msg.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/msg/receive_group_system_msg.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/msg/receive_new_msg.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/msg/receive_profile_system_msg.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/msg/send_common_msg.js
0 → 100644
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
v1.7.x/js/msg/send_custom_msg.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/msg/switch_play_sound_msg.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/msg/upload_and_send_file_msg.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/msg/upload_and_send_pic_msg.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/js/profile/profile_manager.js
0 → 100644
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
v1.7.x/js/switch_chat_obj.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/sdk/json2.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/sdk/json2.min.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/sdk/webim.js
0 → 100644
This diff could not be displayed because it is too large.
v1.7.x/sdk/webim.min.js
0 → 100644
This diff is collapsed.
Click to expand it.
v1.7.x/sdk/webim_wx.js
0 → 100644
This diff could not be displayed because it is too large.
v1.7.x/sdk/webim_wx.min.js
0 → 100644
This diff is collapsed.
Click to expand it.
vue.config.js
0 → 100644
This diff is collapsed.
Click to expand it.
yarn.lock
0 → 100644
This diff could not be displayed because it is too large.
-
Please register or sign in to post a comment