Commit 7e3e473c 7e3e473c8132600de559552c0241dc114a057e74 by yangjun@hikoon.cn

init

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